-
Notifications
You must be signed in to change notification settings - Fork 103
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a package for getting and setting unstructured fields by path
https://github.com/kubernetes-sigs/kustomize/blob/d190e1/api/k8sdeps/kunstruct/helper.go https://github.com/kubernetes/apimachinery/blob/2373d0/pkg/apis/meta/v1/unstructured/helpers.go This package is similar to the above two, with some key differences: * Our fieldpath lexer is a little stricter; it won't allow dangling open braces, unexpected periods, or empty brackets. It also supplies the position of any syntax error if lexing fails. * We support setting and getting fields within a pkg/json unmarshalled object by fieldpath. Other packages support only getting fields, or only setting fields in paths that do not contain any array indexes. Signed-off-by: Nic Cope <negz@rk0n.org>
- Loading branch information
Showing
4 changed files
with
1,486 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,280 @@ | ||
/* | ||
Copyright 2019 The Crossplane Authors. | ||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
http://www.apache.org/licenses/LICENSE-2.0 | ||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
|
||
// Package fieldpath provides utilities for working with field paths. | ||
// | ||
// Field paths reference a field within a Kubernetes object via a simple string. | ||
// API conventions describe the syntax as "standard JavaScript syntax for | ||
// accessing that field, assuming the JSON object was transformed into a | ||
// JavaScript object, without the leading dot, such as metadata.name". | ||
// | ||
// Valid examples: | ||
// | ||
// * metadata.name | ||
// * spec.containers[0].name | ||
// * data[.config.yml] | ||
// * metadata.annotations['crossplane.io/external-name'] | ||
// * spec.items[0][8] | ||
// * apiVersion | ||
// * [42] | ||
// | ||
// Invalid examples: | ||
// | ||
// * .metadata.name - Leading period. | ||
// * metadata..name - Double period. | ||
// * metadata.name. - Trailing period. | ||
// * spec.containers[] - Empty brackets. | ||
// * spec.containers.[0].name - Period before open bracket. | ||
// | ||
// https://github.com/kubernetes/community/blob/61f3d0/contributors/devel/sig-architecture/api-conventions.md#selecting-fields | ||
package fieldpath | ||
|
||
import ( | ||
"fmt" | ||
"strconv" | ||
"strings" | ||
"unicode/utf8" | ||
|
||
"github.com/pkg/errors" | ||
) | ||
|
||
// A SegmentType within a field path; either a field within an object, or an | ||
// index within an array. | ||
type SegmentType int | ||
|
||
// Segment types. | ||
const ( | ||
_ SegmentType = iota | ||
SegmentField | ||
SegmentIndex | ||
) | ||
|
||
// A Segment of a field path. | ||
type Segment struct { | ||
Type SegmentType | ||
Field string | ||
Index uint | ||
} | ||
|
||
// Segments of a field path. | ||
type Segments []Segment | ||
|
||
func (sg Segments) String() string { | ||
var b strings.Builder | ||
|
||
for _, s := range sg { | ||
switch s.Type { | ||
case SegmentField: | ||
if strings.ContainsRune(s.Field, period) { | ||
b.WriteString(fmt.Sprintf("[%s]", s.Field)) | ||
continue | ||
} | ||
b.WriteString(fmt.Sprintf(".%s", s.Field)) | ||
case SegmentIndex: | ||
b.WriteString(fmt.Sprintf("[%d]", s.Index)) | ||
} | ||
} | ||
|
||
return strings.TrimPrefix(b.String(), ".") | ||
} | ||
|
||
// FieldOrIndex produces a new segment from the supplied string. The segment is | ||
// considered to be an array index if the string can be interpreted as an | ||
// unsigned 32 bit integer. Anything else is interpreted as an object field | ||
// name. | ||
func FieldOrIndex(s string) Segment { | ||
// Attempt to parse the segment as an unsigned integer. If the integer is | ||
// larger than 2^32 (the limit for most JSON arrays) we presume it's too big | ||
// to be an array index, and is thus a field name. | ||
if i, err := strconv.ParseUint(s, 10, 32); err == nil { | ||
return Segment{Type: SegmentIndex, Index: uint(i)} | ||
} | ||
|
||
// If the segment is not a valid unsigned integer we presume it's | ||
// a string field name. | ||
return Field(s) | ||
} | ||
|
||
// Field produces a new segment from the supplied string. The segment is always | ||
// considered to be an object field name. | ||
func Field(s string) Segment { | ||
return Segment{Type: SegmentField, Field: strings.Trim(s, "'\"")} | ||
} | ||
|
||
// Parse the supplied path into a slice of Segments. | ||
func Parse(path string) (Segments, error) { | ||
l := &lexer{input: path, items: make(chan item)} | ||
go l.run() | ||
|
||
segments := make(Segments, 0, 1) | ||
for i := range l.items { | ||
switch i.typ { | ||
case itemField: | ||
segments = append(segments, Field(i.val)) | ||
case itemFieldOrIndex: | ||
segments = append(segments, FieldOrIndex(i.val)) | ||
case itemError: | ||
return nil, errors.Errorf("%s at position %d", i.val, i.pos) | ||
} | ||
} | ||
return segments, nil | ||
} | ||
|
||
const ( | ||
period = '.' | ||
leftBracket = '[' | ||
rightBracket = ']' | ||
) | ||
|
||
type itemType int | ||
|
||
const ( | ||
itemError itemType = iota | ||
itemPeriod | ||
itemLeftBracket | ||
itemRightBracket | ||
itemField | ||
itemFieldOrIndex | ||
itemEOL | ||
) | ||
|
||
type item struct { | ||
typ itemType | ||
pos int | ||
val string | ||
} | ||
|
||
type stateFn func(*lexer) stateFn | ||
|
||
// A simplified version of the text/template lexer. | ||
// https://github.com/golang/go/blob/6396bc9d/src/text/template/parse/lex.go#L108 | ||
type lexer struct { | ||
input string | ||
pos int | ||
start int | ||
items chan item | ||
} | ||
|
||
func (l *lexer) run() { | ||
for state := lexField; state != nil; { | ||
state = state(l) | ||
} | ||
close(l.items) | ||
} | ||
|
||
func (l *lexer) emit(t itemType) { | ||
// Don't emit empty values. | ||
if l.pos <= l.start { | ||
return | ||
} | ||
l.items <- item{typ: t, pos: l.start, val: l.input[l.start:l.pos]} | ||
l.start = l.pos | ||
} | ||
|
||
func (l *lexer) errorf(pos int, format string, args ...interface{}) stateFn { | ||
l.items <- item{typ: itemError, pos: pos, val: fmt.Sprintf(format, args...)} | ||
return nil | ||
} | ||
|
||
func lexField(l *lexer) stateFn { | ||
for i, r := range l.input[l.pos:] { | ||
switch r { | ||
// A right bracket may not appear in an object field name. | ||
case rightBracket: | ||
return l.errorf(l.pos+i, "unexpected %q", rightBracket) | ||
|
||
// A left bracket indicates the end of the field name. | ||
case leftBracket: | ||
l.pos += i | ||
l.emit(itemField) | ||
return lexLeftBracket | ||
|
||
// A period indicates the end of the field name. | ||
case period: | ||
l.pos += i | ||
l.emit(itemField) | ||
return lexPeriod | ||
} | ||
} | ||
|
||
// The end of the input indicates the end of the field name. | ||
l.pos = len(l.input) | ||
l.emit(itemField) | ||
l.emit(itemEOL) | ||
return nil | ||
} | ||
|
||
func lexPeriod(l *lexer) stateFn { | ||
// A period may not appear at the beginning or the end of the input. | ||
if l.pos == 0 || l.pos == len(l.input)-1 { | ||
return l.errorf(l.pos, "unexpected %q", period) | ||
} | ||
|
||
l.pos += utf8.RuneLen(period) | ||
l.emit(itemPeriod) | ||
|
||
// A period may only be followed by a field name. We defer checking for | ||
// right brackets to lexField, where they are invalid. | ||
r, _ := utf8.DecodeRuneInString(l.input[l.pos:]) | ||
if r == period { | ||
return l.errorf(l.pos, "unexpected %q", period) | ||
} | ||
if r == leftBracket { | ||
return l.errorf(l.pos, "unexpected %q", leftBracket) | ||
} | ||
|
||
return lexField | ||
} | ||
|
||
func lexLeftBracket(l *lexer) stateFn { | ||
// A right bracket must appear before the input ends. | ||
if !strings.ContainsRune(l.input[l.pos:], rightBracket) { | ||
return l.errorf(l.pos, "unterminated %q", leftBracket) | ||
} | ||
|
||
l.pos += utf8.RuneLen(leftBracket) | ||
l.emit(itemLeftBracket) | ||
return lexFieldOrIndex | ||
} | ||
|
||
// Strings between brackets may be either a field name or an array index. | ||
// Periods have no special meaning in this context. | ||
func lexFieldOrIndex(l *lexer) stateFn { | ||
// We know a right bracket exists before EOL thanks to the preceding | ||
// lexLeftBracket. | ||
rbi := strings.IndexRune(l.input[l.pos:], rightBracket) | ||
|
||
// A right bracket may not immediately follow a left bracket. | ||
if rbi == 0 { | ||
return l.errorf(l.pos, "unexpected %q", rightBracket) | ||
} | ||
|
||
// A left bracket may not appear before the next right bracket. | ||
if lbi := strings.IndexRune(l.input[l.pos:l.pos+rbi], leftBracket); lbi > -1 { | ||
return l.errorf(l.pos+lbi, "unexpected %q", leftBracket) | ||
} | ||
|
||
// Periods are not considered field separators when we're inside brackets. | ||
l.pos += rbi | ||
l.emit(itemFieldOrIndex) | ||
return lexRightBracket | ||
} | ||
|
||
func lexRightBracket(l *lexer) stateFn { | ||
l.pos += utf8.RuneLen(rightBracket) | ||
l.emit(itemRightBracket) | ||
return lexField | ||
} |
Oops, something went wrong.