Skip to content

Commit

Permalink
Add a package for getting and setting unstructured fields by path
Browse files Browse the repository at this point in the history
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
negz committed Feb 25, 2020
1 parent 331b749 commit f671770
Show file tree
Hide file tree
Showing 4 changed files with 1,486 additions and 0 deletions.
280 changes: 280 additions & 0 deletions pkg/fieldpath/fieldpath.go
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
}
Loading

0 comments on commit f671770

Please sign in to comment.