Skip to content

Commit f671770

Browse files
committed
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 <[email protected]>
1 parent 331b749 commit f671770

File tree

4 files changed

+1486
-0
lines changed

4 files changed

+1486
-0
lines changed

pkg/fieldpath/fieldpath.go

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
/*
2+
Copyright 2019 The Crossplane Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
// Package fieldpath provides utilities for working with field paths.
18+
//
19+
// Field paths reference a field within a Kubernetes object via a simple string.
20+
// API conventions describe the syntax as "standard JavaScript syntax for
21+
// accessing that field, assuming the JSON object was transformed into a
22+
// JavaScript object, without the leading dot, such as metadata.name".
23+
//
24+
// Valid examples:
25+
//
26+
// * metadata.name
27+
// * spec.containers[0].name
28+
// * data[.config.yml]
29+
// * metadata.annotations['crossplane.io/external-name']
30+
// * spec.items[0][8]
31+
// * apiVersion
32+
// * [42]
33+
//
34+
// Invalid examples:
35+
//
36+
// * .metadata.name - Leading period.
37+
// * metadata..name - Double period.
38+
// * metadata.name. - Trailing period.
39+
// * spec.containers[] - Empty brackets.
40+
// * spec.containers.[0].name - Period before open bracket.
41+
//
42+
// https://github.com/kubernetes/community/blob/61f3d0/contributors/devel/sig-architecture/api-conventions.md#selecting-fields
43+
package fieldpath
44+
45+
import (
46+
"fmt"
47+
"strconv"
48+
"strings"
49+
"unicode/utf8"
50+
51+
"github.com/pkg/errors"
52+
)
53+
54+
// A SegmentType within a field path; either a field within an object, or an
55+
// index within an array.
56+
type SegmentType int
57+
58+
// Segment types.
59+
const (
60+
_ SegmentType = iota
61+
SegmentField
62+
SegmentIndex
63+
)
64+
65+
// A Segment of a field path.
66+
type Segment struct {
67+
Type SegmentType
68+
Field string
69+
Index uint
70+
}
71+
72+
// Segments of a field path.
73+
type Segments []Segment
74+
75+
func (sg Segments) String() string {
76+
var b strings.Builder
77+
78+
for _, s := range sg {
79+
switch s.Type {
80+
case SegmentField:
81+
if strings.ContainsRune(s.Field, period) {
82+
b.WriteString(fmt.Sprintf("[%s]", s.Field))
83+
continue
84+
}
85+
b.WriteString(fmt.Sprintf(".%s", s.Field))
86+
case SegmentIndex:
87+
b.WriteString(fmt.Sprintf("[%d]", s.Index))
88+
}
89+
}
90+
91+
return strings.TrimPrefix(b.String(), ".")
92+
}
93+
94+
// FieldOrIndex produces a new segment from the supplied string. The segment is
95+
// considered to be an array index if the string can be interpreted as an
96+
// unsigned 32 bit integer. Anything else is interpreted as an object field
97+
// name.
98+
func FieldOrIndex(s string) Segment {
99+
// Attempt to parse the segment as an unsigned integer. If the integer is
100+
// larger than 2^32 (the limit for most JSON arrays) we presume it's too big
101+
// to be an array index, and is thus a field name.
102+
if i, err := strconv.ParseUint(s, 10, 32); err == nil {
103+
return Segment{Type: SegmentIndex, Index: uint(i)}
104+
}
105+
106+
// If the segment is not a valid unsigned integer we presume it's
107+
// a string field name.
108+
return Field(s)
109+
}
110+
111+
// Field produces a new segment from the supplied string. The segment is always
112+
// considered to be an object field name.
113+
func Field(s string) Segment {
114+
return Segment{Type: SegmentField, Field: strings.Trim(s, "'\"")}
115+
}
116+
117+
// Parse the supplied path into a slice of Segments.
118+
func Parse(path string) (Segments, error) {
119+
l := &lexer{input: path, items: make(chan item)}
120+
go l.run()
121+
122+
segments := make(Segments, 0, 1)
123+
for i := range l.items {
124+
switch i.typ {
125+
case itemField:
126+
segments = append(segments, Field(i.val))
127+
case itemFieldOrIndex:
128+
segments = append(segments, FieldOrIndex(i.val))
129+
case itemError:
130+
return nil, errors.Errorf("%s at position %d", i.val, i.pos)
131+
}
132+
}
133+
return segments, nil
134+
}
135+
136+
const (
137+
period = '.'
138+
leftBracket = '['
139+
rightBracket = ']'
140+
)
141+
142+
type itemType int
143+
144+
const (
145+
itemError itemType = iota
146+
itemPeriod
147+
itemLeftBracket
148+
itemRightBracket
149+
itemField
150+
itemFieldOrIndex
151+
itemEOL
152+
)
153+
154+
type item struct {
155+
typ itemType
156+
pos int
157+
val string
158+
}
159+
160+
type stateFn func(*lexer) stateFn
161+
162+
// A simplified version of the text/template lexer.
163+
// https://github.com/golang/go/blob/6396bc9d/src/text/template/parse/lex.go#L108
164+
type lexer struct {
165+
input string
166+
pos int
167+
start int
168+
items chan item
169+
}
170+
171+
func (l *lexer) run() {
172+
for state := lexField; state != nil; {
173+
state = state(l)
174+
}
175+
close(l.items)
176+
}
177+
178+
func (l *lexer) emit(t itemType) {
179+
// Don't emit empty values.
180+
if l.pos <= l.start {
181+
return
182+
}
183+
l.items <- item{typ: t, pos: l.start, val: l.input[l.start:l.pos]}
184+
l.start = l.pos
185+
}
186+
187+
func (l *lexer) errorf(pos int, format string, args ...interface{}) stateFn {
188+
l.items <- item{typ: itemError, pos: pos, val: fmt.Sprintf(format, args...)}
189+
return nil
190+
}
191+
192+
func lexField(l *lexer) stateFn {
193+
for i, r := range l.input[l.pos:] {
194+
switch r {
195+
// A right bracket may not appear in an object field name.
196+
case rightBracket:
197+
return l.errorf(l.pos+i, "unexpected %q", rightBracket)
198+
199+
// A left bracket indicates the end of the field name.
200+
case leftBracket:
201+
l.pos += i
202+
l.emit(itemField)
203+
return lexLeftBracket
204+
205+
// A period indicates the end of the field name.
206+
case period:
207+
l.pos += i
208+
l.emit(itemField)
209+
return lexPeriod
210+
}
211+
}
212+
213+
// The end of the input indicates the end of the field name.
214+
l.pos = len(l.input)
215+
l.emit(itemField)
216+
l.emit(itemEOL)
217+
return nil
218+
}
219+
220+
func lexPeriod(l *lexer) stateFn {
221+
// A period may not appear at the beginning or the end of the input.
222+
if l.pos == 0 || l.pos == len(l.input)-1 {
223+
return l.errorf(l.pos, "unexpected %q", period)
224+
}
225+
226+
l.pos += utf8.RuneLen(period)
227+
l.emit(itemPeriod)
228+
229+
// A period may only be followed by a field name. We defer checking for
230+
// right brackets to lexField, where they are invalid.
231+
r, _ := utf8.DecodeRuneInString(l.input[l.pos:])
232+
if r == period {
233+
return l.errorf(l.pos, "unexpected %q", period)
234+
}
235+
if r == leftBracket {
236+
return l.errorf(l.pos, "unexpected %q", leftBracket)
237+
}
238+
239+
return lexField
240+
}
241+
242+
func lexLeftBracket(l *lexer) stateFn {
243+
// A right bracket must appear before the input ends.
244+
if !strings.ContainsRune(l.input[l.pos:], rightBracket) {
245+
return l.errorf(l.pos, "unterminated %q", leftBracket)
246+
}
247+
248+
l.pos += utf8.RuneLen(leftBracket)
249+
l.emit(itemLeftBracket)
250+
return lexFieldOrIndex
251+
}
252+
253+
// Strings between brackets may be either a field name or an array index.
254+
// Periods have no special meaning in this context.
255+
func lexFieldOrIndex(l *lexer) stateFn {
256+
// We know a right bracket exists before EOL thanks to the preceding
257+
// lexLeftBracket.
258+
rbi := strings.IndexRune(l.input[l.pos:], rightBracket)
259+
260+
// A right bracket may not immediately follow a left bracket.
261+
if rbi == 0 {
262+
return l.errorf(l.pos, "unexpected %q", rightBracket)
263+
}
264+
265+
// A left bracket may not appear before the next right bracket.
266+
if lbi := strings.IndexRune(l.input[l.pos:l.pos+rbi], leftBracket); lbi > -1 {
267+
return l.errorf(l.pos+lbi, "unexpected %q", leftBracket)
268+
}
269+
270+
// Periods are not considered field separators when we're inside brackets.
271+
l.pos += rbi
272+
l.emit(itemFieldOrIndex)
273+
return lexRightBracket
274+
}
275+
276+
func lexRightBracket(l *lexer) stateFn {
277+
l.pos += utf8.RuneLen(rightBracket)
278+
l.emit(itemRightBracket)
279+
return lexField
280+
}

0 commit comments

Comments
 (0)