diff --git a/pkg/jsonpath/jsonpath_plus_test.go b/pkg/jsonpath/jsonpath_plus_test.go index f4bcb64..b2581a3 100644 --- a/pkg/jsonpath/jsonpath_plus_test.go +++ b/pkg/jsonpath/jsonpath_plus_test.go @@ -1347,3 +1347,159 @@ func contains(s, substr string) bool { } return false } + +func TestUnquotedBracketNotation(t *testing.T) { + tests := []struct { + name string + yaml string + path string + expected int + }{ + { + name: "select GET operations with unquoted bracket", + yaml: ` +paths: + /users: + get: + operationId: "getUsers" + post: + operationId: "createUser" + /items: + get: + operationId: "getItems" + delete: + operationId: "deleteItems" +`, + path: `$.paths[*][get]`, + expected: 2, + }, + { + name: "union of unquoted bracket selectors", + yaml: ` +paths: + /users: + get: + operationId: "getUsers" + post: + operationId: "createUser" + delete: + operationId: "deleteUsers" + /items: + get: + operationId: "getItems" + put: + operationId: "updateItems" +`, + path: `$.paths[*][get,post]`, + expected: 3, + }, + { + name: "media type selector", + yaml: ` +paths: + /users: + post: + requestBody: + content: + application/vnd.api+json: + schema: + type: object + application/json: + schema: + type: object +`, + path: `$.paths..content[application/vnd.api+json].schema`, + expected: 1, + }, + { + name: "mixed name and integer selectors on mapping", + yaml: ` +responses: + default: + description: "Default error" + "200": + description: "Success" + "400": + description: "Bad request" + "500": + description: "Server error" +`, + path: `$.responses[default,400,500]`, + expected: 3, + }, + { + name: "integer index on mapping node (200 status code)", + yaml: ` +responses: + "200": + description: "Success" + "404": + description: "Not Found" +`, + path: `$.responses[200]`, + expected: 1, + }, + { + name: "array index still works as array index", + yaml: ` +items: + - name: "first" + - name: "second" + - name: "third" +`, + path: `$.items[0]`, + expected: 1, + }, + { + name: "nested brackets in filter stay as integers", + yaml: ` +data: + - items: + - value: 5 + - value: 10 + - items: + - value: 15 +`, + path: `$.data[?(@.items[0].value > 4)]`, + expected: 2, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var node yaml.Node + err := yaml.Unmarshal([]byte(tt.yaml), &node) + assert.NoError(t, err) + + path, err := NewPath(tt.path) + assert.NoError(t, err, "failed to parse path: %s", tt.path) + + results := path.Query(&node) + assert.Len(t, results, tt.expected, "expected %d results, got %d for path %s", tt.expected, len(results), tt.path) + }) + } +} + +func TestUnquotedBracketStrictModeRejection(t *testing.T) { + // In strict RFC 9535 mode, unquoted brackets should not produce STRING_LITERAL + // Instead they produce STRING (from scanLiteral) which the parser treats differently + yamlData := ` +paths: + /users: + get: + operationId: "getUsers" +` + var node yaml.Node + err := yaml.Unmarshal([]byte(yamlData), &node) + assert.NoError(t, err) + + // In strict mode, $[get] should still parse but 'get' is a STRING not STRING_LITERAL + // The parser may or may not accept this, but the behavior differs from JSONPath Plus + path, parseErr := NewPath(`$.paths['/users'][get]`, config.WithStrictRFC9535()) + if parseErr == nil { + results := path.Query(&node) + // In strict mode without unquoted bracket support, this may not find results + _ = results + } + // We just verify it doesn't panic +} diff --git a/pkg/jsonpath/parser.go b/pkg/jsonpath/parser.go index 488c6f7..ec1109a 100644 --- a/pkg/jsonpath/parser.go +++ b/pkg/jsonpath/parser.go @@ -30,18 +30,19 @@ var contextVarTokenMap = map[token.Token]contextVarKind{ // JSONPath represents a JSONPath parser. type JSONPath struct { - tokenizer *token.Tokenizer - tokens []token.TokenInfo - ast jsonPathAST - current int - mode []mode - config config.Config + tokenizer *token.Tokenizer + tokens []token.TokenInfo + ast jsonPathAST + current int + mode []mode + config config.Config + filterDepth int // tracks nesting depth inside filter expressions } // newParserPrivate creates a new JSONPath with the given tokens. func newParserPrivate(tokenizer *token.Tokenizer, tokens []token.TokenInfo, opts ...config.Option) *JSONPath { cfg := config.New(opts...) - return &JSONPath{tokenizer, tokens, jsonPathAST{lazyContextTracking: cfg.LazyContextTrackingEnabled()}, 0, []mode{modeNormal}, cfg} + return &JSONPath{tokenizer, tokens, jsonPathAST{lazyContextTracking: cfg.LazyContextTrackingEnabled(), jsonPathPlus: cfg.JSONPathPlusEnabled()}, 0, []mode{modeNormal}, cfg, 0} } // parse parses the JSONPath tokens and returns the root node of the AST. @@ -155,6 +156,12 @@ func (p *JSONPath) parseInnerSegment() (retValue *innerSegment, err error) { dotName := p.tokens[p.current].Literal p.current += 1 return &innerSegment{segmentDotMemberName, dotName, nil}, nil + } else if firstToken.Token == token.INTEGER && p.config.JSONPathPlusEnabled() && p.current >= 3 { + // JSONPath Plus: treat .201 as a member name (common for HTTP status codes in OpenAPI). + // Only when we're past the root (p.current >= 3 means at least $, ., and something before this). + dotName := p.tokens[p.current].Literal + p.current += 1 + return &innerSegment{segmentDotMemberName, dotName, nil}, nil } else if firstToken.Token == token.BRACKET_LEFT { prior := p.current p.current += 1 @@ -244,7 +251,7 @@ func (p *JSONPath) parseSelector() (retSelector *selector, err error) { p.current++ - return &selector{kind: selectorSubKindArrayIndex, index: i}, nil + return &selector{kind: selectorSubKindArrayIndex, index: i, jsonPathPlus: p.config.JSONPathPlusEnabled() && p.filterDepth == 0}, nil } else if p.tokens[p.current].Token == token.ARRAY_SLICE { slice, err := p.parseSliceSelector() if err != nil { @@ -341,6 +348,8 @@ func (p *JSONPath) parseFilterSelector() (*selector, error) { return nil, p.parseFailure(&p.tokens[p.current], "expected '?'") } p.current++ + p.filterDepth++ + defer func() { p.filterDepth-- }() expr, err := p.parseLogicalOrExpr() if err != nil { @@ -783,8 +792,9 @@ func (p *JSONPath) parseLiteral() (*literal, error) { type jsonPathAST struct { // "$" - segments []*segment + segments []*segment lazyContextTracking bool + jsonPathPlus bool // JSONPath Plus extensions enabled (unquoted brackets, mapping index fallback) } func (q jsonPathAST) ToString() string { diff --git a/pkg/jsonpath/selector.go b/pkg/jsonpath/selector.go index f71cb47..9f5ea06 100644 --- a/pkg/jsonpath/selector.go +++ b/pkg/jsonpath/selector.go @@ -23,11 +23,12 @@ type slice struct { } type selector struct { - kind selectorSubKind - name string - index int64 - slice *slice - filter *filterSelector + kind selectorSubKind + name string + index int64 + slice *slice + filter *filterSelector + jsonPathPlus bool // when true, enables MappingNode fallback for array index selectors } func (s selector) ToString() string { diff --git a/pkg/jsonpath/token/token.go b/pkg/jsonpath/token/token.go index a9e39a4..7e92c12 100644 --- a/pkg/jsonpath/token/token.go +++ b/pkg/jsonpath/token/token.go @@ -408,14 +408,15 @@ type Tokens []TokenInfo // Tokenizer represents a JSONPath tokenizer. type Tokenizer struct { - input string - pos int - line int - column int - tokens []TokenInfo - stack []Token - illegalWhitespace bool - config config.Config + input string + pos int + line int + column int + tokens []TokenInfo + stack []Token + illegalWhitespace bool + config config.Config + bracketFilterState []bool // lazy-init: nil until first BRACKET_LEFT; tracks filter context per bracket depth } // NewTokenizer creates a new JSONPath tokenizer for the given input string. @@ -488,6 +489,10 @@ func (t *Tokenizer) Tokenize() Tokens { t.addToken(ARRAY_SLICE, 1, "") case ch == '?': t.addToken(FILTER, 1, "") + // Mark current bracket as filter context + if len(t.bracketFilterState) > 0 { + t.bracketFilterState[len(t.bracketFilterState)-1] = true + } case ch == '(': t.addToken(PAREN_LEFT, 1, "") t.stack = append(t.stack, PAREN_LEFT) @@ -501,10 +506,21 @@ func (t *Tokenizer) Tokenize() Tokens { case ch == '[': t.addToken(BRACKET_LEFT, 1, "") t.stack = append(t.stack, BRACKET_LEFT) + // Lazy-init bracketFilterState on first bracket + if t.bracketFilterState == nil { + t.bracketFilterState = make([]bool, 0, 4) + } + // Inherit parent filter state: if parent is in filter context, so is this bracket + inFilter := len(t.bracketFilterState) > 0 && t.bracketFilterState[len(t.bracketFilterState)-1] + t.bracketFilterState = append(t.bracketFilterState, inFilter) case ch == ']': if len(t.stack) > 0 && t.stack[len(t.stack)-1] == BRACKET_LEFT { t.addToken(BRACKET_RIGHT, 1, "") t.stack = t.stack[:len(t.stack)-1] + // Pop bracket filter state + if len(t.bracketFilterState) > 0 { + t.bracketFilterState = t.bracketFilterState[:len(t.bracketFilterState)-1] + } } else { t.addToken(ILLEGAL, 1, "unmatched closing bracket") } @@ -581,9 +597,19 @@ func (t *Tokenizer) Tokenize() Tokens { case isDigit(ch): t.scanNumber() case isLiteralChar(ch): - t.scanLiteral() + if t.config.JSONPathPlusEnabled() && t.isInsideBracket() && !t.isInFilterContext() { + t.scanUnquotedBracketString() + } else { + t.scanLiteral() + } default: - t.addToken(ILLEGAL, 1, string(ch)) + // JSONPath Plus: handle special characters inside brackets as unquoted strings + // e.g., application/vnd.api+json where / and + would otherwise be ILLEGAL + if t.config.JSONPathPlusEnabled() && t.isInsideBracket() && !t.isInFilterContext() && isUnquotedBracketStartChar(ch) { + t.scanUnquotedBracketString() + } else { + t.addToken(ILLEGAL, 1, string(ch)) + } } t.pos++ t.column++ @@ -611,8 +637,6 @@ func (t *Tokenizer) scanString(quote rune) { var literal strings.Builder illegal: for i := start; i < len(t.input); i++ { - b := literal.String() - _ = b if t.input[i] == byte(quote) { t.addToken(STRING_LITERAL, len(t.input[start:i])+2, literal.String()) t.pos = i @@ -684,6 +708,15 @@ func (t *Tokenizer) scanNumber() { t.column += i - start return } + // Peek ahead: if '.' is NOT followed by a digit, it's a CHILD separator, + // not a decimal point. Stop the number scan here. + if i+1 >= len(t.input) || !isDigit(t.input[i+1]) { + literal := t.input[start:i] + t.addToken(tokenType, len(literal), literal) + t.pos = i - 1 + t.column += i - start - 1 + return + } tokenType = FLOAT dotSeen = true continue @@ -716,7 +749,7 @@ func (t *Tokenizer) scanNumber() { // no leading zero tokenType = ILLEGAL } else if len(literal) > 2 && literal[0] == '-' && literal[1] == '0' && !dotSeen { - // no trailing dot + // no negative zero without fraction tokenType = ILLEGAL } else if len(literal) > 0 && literal[len(literal)-1] == '.' { // no trailing dot @@ -828,6 +861,48 @@ func (t *Tokenizer) peek() byte { return 0 } +// isInsideBracket returns true if the tokenizer is currently inside a bracket pair. +// Nil-safe: returns false when bracketFilterState has not been initialized. +func (t *Tokenizer) isInsideBracket() bool { + return len(t.bracketFilterState) > 0 +} + +// isInFilterContext returns true if the current bracket context is a filter expression. +func (t *Tokenizer) isInFilterContext() bool { + return len(t.bracketFilterState) > 0 && t.bracketFilterState[len(t.bracketFilterState)-1] +} + +// scanUnquotedBracketString scans an unquoted string inside brackets (JSONPath Plus extension). +// Handles values like: get, post, application/vnd.api+json, default, etc. +// Zero-allocation: uses direct substring of t.input, matching scanLiteral's pattern. +func (t *Tokenizer) scanUnquotedBracketString() { + start := t.pos + end := start + for end < len(t.input) { + ch := t.input[end] + if ch == ']' || ch == ',' || ch == '[' || ch == '\'' || ch == '"' || ch == '?' { + break + } + if isSpace(ch) { + break + } + end++ + } + // Trim trailing whitespace by adjusting end index + trimEnd := end + for trimEnd > start && isSpace(t.input[trimEnd-1]) { + trimEnd-- + } + if trimEnd <= start { + t.addToken(ILLEGAL, 1, string(t.input[t.pos])) + return + } + literal := t.input[start:trimEnd] + t.addToken(STRING_LITERAL, len(literal), literal) + t.pos = end - 1 + t.column += end - start - 1 +} + func isDigit(ch byte) bool { return '0' <= ch && ch <= '9' } @@ -841,6 +916,15 @@ func isSpace(ch byte) bool { return ch == ' ' || ch == '\t' || ch == '\r' } +// isUnquotedBracketStartChar returns true if ch can start an unquoted bracket string +// but is not already handled by other cases (digits, literal chars, quotes, etc.). +// Note: + is NOT included here because $[+1] must remain ILLEGAL per RFC 9535. +// Characters like + that appear mid-value (e.g., application/vnd.api+json) are handled +// by scanUnquotedBracketString which scans until a delimiter is found. +func isUnquotedBracketStartChar(ch byte) bool { + return ch == '/' || ch == '%' || ch == '#' +} + // contextVariableKeywords maps context variable names to their token types. // These are JSONPath Plus extensions for accessing filter context. var contextVariableKeywords = map[string]Token{ diff --git a/pkg/jsonpath/token/token_test.go b/pkg/jsonpath/token/token_test.go index 9087e38..69ee813 100644 --- a/pkg/jsonpath/token/token_test.go +++ b/pkg/jsonpath/token/token_test.go @@ -368,6 +368,67 @@ func TestTokenizer(t *testing.T) { // {Token: BRACKET_RIGHT, Line: 1, Column: 18, literal: "", Len: 1}, // }, //}, + { + name: "UnquotedBracketGet", + input: "$[get]", + expected: []TokenInfo{ + {Token: ROOT, Line: 1, Column: 0, Literal: "", Len: 1}, + {Token: BRACKET_LEFT, Line: 1, Column: 1, Literal: "", Len: 1}, + {Token: STRING_LITERAL, Line: 1, Column: 2, Literal: "get", Len: 3}, + {Token: BRACKET_RIGHT, Line: 1, Column: 5, Literal: "", Len: 1}, + }, + }, + { + name: "UnquotedBracketUnion", + input: "$.paths[*][get,post]", + expected: []TokenInfo{ + {Token: ROOT, Line: 1, Column: 0, Literal: "", Len: 1}, + {Token: CHILD, Line: 1, Column: 1, Literal: "", Len: 1}, + {Token: STRING, Line: 1, Column: 2, Literal: "paths", Len: 5}, + {Token: BRACKET_LEFT, Line: 1, Column: 7, Literal: "", Len: 1}, + {Token: WILDCARD, Line: 1, Column: 8, Literal: "", Len: 1}, + {Token: BRACKET_RIGHT, Line: 1, Column: 9, Literal: "", Len: 1}, + {Token: BRACKET_LEFT, Line: 1, Column: 10, Literal: "", Len: 1}, + {Token: STRING_LITERAL, Line: 1, Column: 11, Literal: "get", Len: 3}, + {Token: COMMA, Line: 1, Column: 14, Literal: "", Len: 1}, + {Token: STRING_LITERAL, Line: 1, Column: 15, Literal: "post", Len: 4}, + {Token: BRACKET_RIGHT, Line: 1, Column: 19, Literal: "", Len: 1}, + }, + }, + { + name: "UnquotedBracketMediaType", + input: "$[application/vnd.api+json]", + expected: []TokenInfo{ + {Token: ROOT, Line: 1, Column: 0, Literal: "", Len: 1}, + {Token: BRACKET_LEFT, Line: 1, Column: 1, Literal: "", Len: 1}, + {Token: STRING_LITERAL, Line: 1, Column: 2, Literal: "application/vnd.api+json", Len: 24}, + {Token: BRACKET_RIGHT, Line: 1, Column: 26, Literal: "", Len: 1}, + }, + }, + { + name: "UnquotedBracketMixed", + input: "$[default,400,500]", + expected: []TokenInfo{ + {Token: ROOT, Line: 1, Column: 0, Literal: "", Len: 1}, + {Token: BRACKET_LEFT, Line: 1, Column: 1, Literal: "", Len: 1}, + {Token: STRING_LITERAL, Line: 1, Column: 2, Literal: "default", Len: 7}, + {Token: COMMA, Line: 1, Column: 9, Literal: "", Len: 1}, + {Token: INTEGER, Line: 1, Column: 10, Literal: "400", Len: 3}, + {Token: COMMA, Line: 1, Column: 13, Literal: "", Len: 1}, + {Token: INTEGER, Line: 1, Column: 14, Literal: "500", Len: 3}, + {Token: BRACKET_RIGHT, Line: 1, Column: 17, Literal: "", Len: 1}, + }, + }, + { + name: "ArrayIndexPreserved", + input: "$[0]", + expected: []TokenInfo{ + {Token: ROOT, Line: 1, Column: 0, Literal: "", Len: 1}, + {Token: BRACKET_LEFT, Line: 1, Column: 1, Literal: "", Len: 1}, + {Token: INTEGER, Line: 1, Column: 2, Literal: "0", Len: 1}, + {Token: BRACKET_RIGHT, Line: 1, Column: 3, Literal: "", Len: 1}, + }, + }, } for _, test := range tests { @@ -800,3 +861,18 @@ func TestPropertyNameExtension(t *testing.T) { }) } } + +func TestUnquotedBracketStrictMode(t *testing.T) { + tok := NewTokenizer("$[get]", config.WithStrictRFC9535()) + tokens := tok.Tokenize() + // In strict mode, 'get' inside brackets is scanned as a literal STRING, not STRING_LITERAL + found := false + for _, ti := range tokens { + if ti.Token == STRING && ti.Literal == "get" { + found = true + } + } + if !found { + t.Error("expected STRING token for 'get' in strict mode, got:", tokens) + } +} diff --git a/pkg/jsonpath/yaml_query.go b/pkg/jsonpath/yaml_query.go index d597a60..855f903 100644 --- a/pkg/jsonpath/yaml_query.go +++ b/pkg/jsonpath/yaml_query.go @@ -383,15 +383,15 @@ func (s innerSegment) Query(idx index, value *yaml.Node, root *yaml.Node) []*yam func (s selector) Query(idx index, value *yaml.Node, root *yaml.Node) []*yaml.Node { trackParents := parentTrackingEnabled(idx) + fc, hasFc := idx.(FilterContext) switch s.kind { case selectorSubKindName: if value.Kind != yaml.MappingNode { return nil } - // Check for inherited pending segment from wildcard/slice var inheritedPending string - if fc, ok := idx.(FilterContext); ok { + if hasFc { inheritedPending = fc.GetAndClearPendingPathSegment(value) } @@ -407,13 +407,11 @@ func (s selector) Query(idx index, value *yaml.Node, root *yaml.Node) []*yaml.No if trackParents { idx.setParentNode(child, value) } - if fc, ok := idx.(FilterContext); ok { + if hasFc { thisSegment := normalizePathSegment(key) if inheritedPending != "" { - // Propagate combined pending to result for later consumption fc.SetPendingPathSegment(child, inheritedPending+thisSegment) } else { - // No wildcard ancestry - push directly to path fc.PushPathSegment(thisSegment) } fc.SetPropertyName(key) @@ -422,15 +420,31 @@ func (s selector) Query(idx index, value *yaml.Node, root *yaml.Node) []*yaml.No } } case selectorSubKindArrayIndex: + if s.jsonPathPlus && value.Kind == yaml.MappingNode && s.index >= 0 { + // JSONPath Plus fallback: treat integer index as a string key lookup on mapping nodes. + // This handles YAML mappings with numeric keys like $.responses[200]. + keyStr := strconv.FormatInt(s.index, 10) + for i := 0; i < len(value.Content); i += 2 { + if value.Content[i].Value == keyStr { + child := value.Content[i+1] + idx.setPropertyKey(value.Content[i], value) + idx.setPropertyKey(child, value.Content[i]) + if trackParents { + idx.setParentNode(child, value) + } + return []*yaml.Node{child} + } + } + return nil + } if value.Kind != yaml.SequenceNode { return nil } if s.index >= int64(len(value.Content)) || s.index < -int64(len(value.Content)) { return nil } - // Check for inherited pending segment from wildcard/slice var inheritedPending string - if fc, ok := idx.(FilterContext); ok { + if hasFc { inheritedPending = fc.GetAndClearPendingPathSegment(value) } @@ -446,21 +460,18 @@ func (s selector) Query(idx index, value *yaml.Node, root *yaml.Node) []*yaml.No if trackParents { idx.setParentNode(child, value) } - if fc, ok := idx.(FilterContext); ok { + if hasFc { thisSegment := normalizeIndexSegment(actualIndex) if inheritedPending != "" { - // Propagate combined pending to result for later consumption fc.SetPendingPathSegment(child, inheritedPending+thisSegment) } else { - // No wildcard ancestry - push directly to path fc.PushPathSegment(thisSegment) } } return []*yaml.Node{child} case selectorSubKindWildcard: - // Check for inherited pending segment from previous wildcard/slice var inheritedPending string - if fc, ok := idx.(FilterContext); ok { + if hasFc { inheritedPending = fc.GetAndClearPendingPathSegment(value) } @@ -469,11 +480,10 @@ func (s selector) Query(idx index, value *yaml.Node, root *yaml.Node) []*yaml.No if trackParents { idx.setParentNode(child, value) } - // Track pending path segment and property name for this node - if fc, ok := idx.(FilterContext); ok { + if hasFc { thisSegment := normalizeIndexSegment(i) fc.SetPendingPathSegment(child, inheritedPending+thisSegment) - fc.SetPendingPropertyName(child, strconv.Itoa(i)) // For @parentProperty + fc.SetPendingPropertyName(child, strconv.Itoa(i)) } } return value.Content @@ -487,11 +497,10 @@ func (s selector) Query(idx index, value *yaml.Node, root *yaml.Node) []*yaml.No if trackParents { idx.setParentNode(child, value) } - // Track pending path segment and property name for this node - if fc, ok := idx.(FilterContext); ok { + if hasFc { thisSegment := normalizePathSegment(keyNode.Value) fc.SetPendingPathSegment(child, inheritedPending+thisSegment) - fc.SetPendingPropertyName(child, keyNode.Value) // For @parentProperty + fc.SetPendingPropertyName(child, keyNode.Value) } result = append(result, child) } @@ -506,9 +515,8 @@ func (s selector) Query(idx index, value *yaml.Node, root *yaml.Node) []*yaml.No if len(value.Content) == 0 { return nil } - // Check for inherited pending segment from previous wildcard/slice var inheritedPending string - if fc, ok := idx.(FilterContext); ok { + if hasFc { inheritedPending = fc.GetAndClearPendingPathSegment(value) } @@ -530,11 +538,10 @@ func (s selector) Query(idx index, value *yaml.Node, root *yaml.Node) []*yaml.No if trackParents { idx.setParentNode(child, value) } - // Track pending path segment and property name for this node - if fc, ok := idx.(FilterContext); ok { + if hasFc { thisSegment := normalizeIndexSegment(int(i)) fc.SetPendingPathSegment(child, inheritedPending+thisSegment) - fc.SetPendingPropertyName(child, strconv.Itoa(int(i))) // For @parentProperty + fc.SetPendingPropertyName(child, strconv.Itoa(int(i))) } result = append(result, child) } @@ -544,11 +551,10 @@ func (s selector) Query(idx index, value *yaml.Node, root *yaml.Node) []*yaml.No if trackParents { idx.setParentNode(child, value) } - // Track pending path segment and property name for this node - if fc, ok := idx.(FilterContext); ok { + if hasFc { thisSegment := normalizeIndexSegment(int(i)) fc.SetPendingPathSegment(child, inheritedPending+thisSegment) - fc.SetPendingPropertyName(child, strconv.Itoa(int(i))) // For @parentProperty + fc.SetPendingPropertyName(child, strconv.Itoa(int(i))) } result = append(result, child) } @@ -557,18 +563,14 @@ func (s selector) Query(idx index, value *yaml.Node, root *yaml.Node) []*yaml.No return result case selectorSubKindFilter: var result []*yaml.Node - // Get parent property name - prefer pending property name from wildcard/slice, - // fall back to current PropertyName var parentPropName string var pushedPendingSegment bool - if fc, ok := idx.(FilterContext); ok { - // First check for pending property name from wildcard/slice + if hasFc { if pendingPropName := fc.GetAndClearPendingPropertyName(value); pendingPropName != "" { parentPropName = pendingPropName } else { parentPropName = fc.PropertyName() } - // Check if this node has a pending path segment from a wildcard/slice if pendingSeg := fc.GetAndClearPendingPathSegment(value); pendingSeg != "" { fc.PushPathSegment(pendingSeg) pushedPendingSegment = true @@ -585,7 +587,7 @@ func (s selector) Query(idx index, value *yaml.Node, root *yaml.Node) []*yaml.No idx.setParentNode(valueNode, value) } - if fc, ok := idx.(FilterContext); ok { + if hasFc { fc.SetParentPropertyName(parentPropName) fc.SetPropertyName(keyNode.Value) fc.SetParent(value) @@ -597,7 +599,7 @@ func (s selector) Query(idx index, value *yaml.Node, root *yaml.Node) []*yaml.No result = append(result, valueNode) } - if fc, ok := idx.(FilterContext); ok { + if hasFc { fc.PopPathSegment() } } @@ -607,7 +609,7 @@ func (s selector) Query(idx index, value *yaml.Node, root *yaml.Node) []*yaml.No idx.setParentNode(child, value) } - if fc, ok := idx.(FilterContext); ok { + if hasFc { fc.SetParentPropertyName(parentPropName) fc.SetPropertyName(strconv.Itoa(i)) fc.SetParent(value) @@ -619,14 +621,13 @@ func (s selector) Query(idx index, value *yaml.Node, root *yaml.Node) []*yaml.No result = append(result, child) } - if fc, ok := idx.(FilterContext); ok { + if hasFc { fc.PopPathSegment() } } } - // Pop the pending segment if we pushed one if pushedPendingSegment { - if fc, ok := idx.(FilterContext); ok { + if hasFc { fc.PopPathSegment() } }