From 55aa0f4a423187ff91f364370e6aabefd256bec5 Mon Sep 17 00:00:00 2001 From: "red-hat-konflux-kflux-prd-rh02[bot]" <190377777+red-hat-konflux-kflux-prd-rh02[bot]@users.noreply.github.com> Date: Sat, 8 Aug 2026 00:16:07 +0000 Subject: [PATCH] Update module github.com/pb33f/jsonpath to v0.8.3 Signed-off-by: red-hat-konflux-kflux-prd-rh02 <190377777+red-hat-konflux-kflux-prd-rh02[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 +- .../jsonpath/pkg/jsonpath/config/config.go | 36 + .../jsonpath/pkg/jsonpath/context_usage.go | 21 +- .../pb33f/jsonpath/pkg/jsonpath/filter.go | 662 +++---- .../pb33f/jsonpath/pkg/jsonpath/jsonpath.go | 91 +- .../pb33f/jsonpath/pkg/jsonpath/parser.go | 1417 +++++++-------- .../pb33f/jsonpath/pkg/jsonpath/segment.go | 218 ++- .../pb33f/jsonpath/pkg/jsonpath/selector.go | 4 +- .../jsonpath/pkg/jsonpath/spectral_eval.go | 529 ++++++ .../jsonpath/pkg/jsonpath/spectral_expr.go | 659 +++++++ .../jsonpath/pkg/jsonpath/token/token.go | 1529 +++++++++-------- .../pb33f/jsonpath/pkg/jsonpath/yaml_query.go | 1046 +++++------ vendor/modules.txt | 2 +- 14 files changed, 3897 insertions(+), 2323 deletions(-) create mode 100644 vendor/github.com/pb33f/jsonpath/pkg/jsonpath/spectral_eval.go create mode 100644 vendor/github.com/pb33f/jsonpath/pkg/jsonpath/spectral_expr.go diff --git a/go.mod b/go.mod index 1a6cd25fd..df63cd119 100644 --- a/go.mod +++ b/go.mod @@ -143,7 +143,7 @@ require ( github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.157.0 // indirect github.com/open-telemetry/opentelemetry-collector-contrib/processor/deltatocumulativeprocessor v0.157.0 // indirect github.com/openai/openai-go/v3 v3.45.0 // indirect - github.com/pb33f/jsonpath v0.8.2 // indirect + github.com/pb33f/jsonpath v0.8.3 // indirect github.com/pb33f/libopenapi v0.38.7 // indirect github.com/pb33f/libopenapi-validator v0.14.0 // indirect github.com/pb33f/ordered-map/v2 v2.3.1 // indirect diff --git a/go.sum b/go.sum index a7c8344a6..ac8389611 100644 --- a/go.sum +++ b/go.sum @@ -404,8 +404,8 @@ github.com/openshift/machine-config-operator v0.0.1-0.20230815171034-c2bb862bc08 github.com/openshift/machine-config-operator v0.0.1-0.20230815171034-c2bb862bc08a/go.mod h1:kP51fbL8QBSY/mAkFicoF73x0QSraPrX4BjWIdzFPio= github.com/ovh/go-ovh v1.9.0 h1:6K8VoL3BYjVV3In9tPJUdT7qMx9h0GExN9EXx1r2kKE= github.com/ovh/go-ovh v1.9.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c= -github.com/pb33f/jsonpath v0.8.2 h1:Ou4C7zjYClBm97dfZjDCjdZGusJoynv/vrtiEKNfj2Y= -github.com/pb33f/jsonpath v0.8.2/go.mod h1:zBV5LJW4OQOPatmQE2QdKpGQJvhDTlE5IEj6ASaRNTo= +github.com/pb33f/jsonpath v0.8.3 h1:gdOUYn31vic8iRSSR4I2SEv7UQf2WxLeDcUnDPpGZNE= +github.com/pb33f/jsonpath v0.8.3/go.mod h1:zBV5LJW4OQOPatmQE2QdKpGQJvhDTlE5IEj6ASaRNTo= github.com/pb33f/libopenapi v0.38.7 h1:Q2jfgRPdnU38WW8wQvrX2HEPGiqsxj01PX1BHmAEihc= github.com/pb33f/libopenapi v0.38.7/go.mod h1:naZ03Auhn7i+RJtMv8ck8l7Ag8E2/x2w66j9vsDFL38= github.com/pb33f/libopenapi-validator v0.14.0 h1:K9cdv1kL6cZdQmSbzitkVcWCJKyRmmSZryZqgUqIv+Y= diff --git a/vendor/github.com/pb33f/jsonpath/pkg/jsonpath/config/config.go b/vendor/github.com/pb33f/jsonpath/pkg/jsonpath/config/config.go index a0647363b..c0c225043 100644 --- a/vendor/github.com/pb33f/jsonpath/pkg/jsonpath/config/config.go +++ b/vendor/github.com/pb33f/jsonpath/pkg/jsonpath/config/config.go @@ -1,5 +1,8 @@ package config +import "errors" + +// Option configures JSONPath compilation and evaluation. type Option func(*config) // WithPropertyNameExtension enables the use of the "~" character to access a property key. @@ -28,6 +31,20 @@ func WithStrictRFC9535() Option { } } +// WithSpectralCompatibility enables the safe Spectral expression dialect. +// +// Spectral compatibility includes JSONPath Plus context variables, parent +// selection and the property-name extension. It cannot be combined with +// WithStrictRFC9535; callers should validate Config before use. NewPath does +// this automatically. +func WithSpectralCompatibility() Option { + return func(cfg *config) { + cfg.spectralCompatibility = true + cfg.propertyNameExtension = true + } +} + +// Config exposes the resolved JSONPath dialect and context-tracking settings. type Config interface { PropertyNameEnabled() bool JSONPathPlusEnabled() bool @@ -38,6 +55,7 @@ type config struct { propertyNameExtension bool strictRFC9535 bool lazyContextTracking bool + spectralCompatibility bool } func (c *config) PropertyNameEnabled() bool { @@ -57,6 +75,24 @@ func (c *config) LazyContextTrackingEnabled() bool { return c.lazyContextTracking } +// SpectralCompatibilityEnabled reports whether cfg enables the safe Spectral dialect. +// It is a function rather than a Config method so adding the dialect does not +// break external implementations of the existing Config interface. +func SpectralCompatibilityEnabled(cfg Config) bool { + resolved, ok := cfg.(*config) + return ok && resolved.spectralCompatibility +} + +// Validate rejects incompatible dialect options without making option order significant. +func Validate(cfg Config) error { + resolved, ok := cfg.(*config) + if ok && resolved.strictRFC9535 && resolved.spectralCompatibility { + return errors.New("config.WithStrictRFC9535 and config.WithSpectralCompatibility cannot be combined") + } + return nil +} + +// New resolves options into an immutable-by-interface configuration view. func New(opts ...Option) Config { cfg := &config{} for _, opt := range opts { diff --git a/vendor/github.com/pb33f/jsonpath/pkg/jsonpath/context_usage.go b/vendor/github.com/pb33f/jsonpath/pkg/jsonpath/context_usage.go index 4ee137fc0..6d5e7d97d 100644 --- a/vendor/github.com/pb33f/jsonpath/pkg/jsonpath/context_usage.go +++ b/vendor/github.com/pb33f/jsonpath/pkg/jsonpath/context_usage.go @@ -63,7 +63,7 @@ func (q *jsonPathAST) hasPropertyNameReferencesPtr() bool { // hasPropertyNameReferences reports whether the segment references the property name selector func (s *segment) hasPropertyNameReferences() bool { - if s.kind == segmentKindProperyName { + if s.kind == segmentKindProperyName || s.kind == segmentKindRecursivePropertyName { return true } if s.child != nil && s.child.hasPropertyNameReferences() { @@ -87,7 +87,7 @@ func (s *innerSegment) hasPropertyNameReferences() bool { // hasPropertyNameReferences reports whether the selector references the property name selector func (s *selector) hasPropertyNameReferences() bool { - if s.filter != nil && s.filter.hasPropertyNameReferences() { + if s.filter.present() && s.filter.hasPropertyNameReferences() { return true } return false @@ -95,6 +95,9 @@ func (s *selector) hasPropertyNameReferences() bool { // hasPropertyNameReferences reports whether the filter selector references the property name selector func (f *filterSelector) hasPropertyNameReferences() bool { + if f.spectralExpression != nil { + return f.spectralExpression.usesPropertyNameSelector + } if f.expression == nil { return false } @@ -256,13 +259,25 @@ func (s *innerSegment) collectContextVarUsage(usage *contextVarUsage) { // collectContextVarUsage records usage from a selector func (s *selector) collectContextVarUsage(usage *contextVarUsage) { - if s.filter != nil { + if s.filter.present() { s.filter.collectContextVarUsage(usage) } } // collectContextVarUsage records usage from a filter selector func (f *filterSelector) collectContextVarUsage(usage *contextVarUsage) { + if f.spectralExpression != nil { + usage.property = usage.property || f.spectralExpression.usage.property + // Parent-property typing needs the container of the parent so sequence + // indexes remain numeric under lazy context tracking. + usage.parent = usage.parent || f.spectralExpression.usage.parent || f.spectralExpression.usage.parentProperty + usage.parentProperty = usage.parentProperty || f.spectralExpression.usage.parentProperty + usage.path = usage.path || f.spectralExpression.usage.path + // Spectral @property is numeric for sequence elements, so lazy mode + // needs the index even when @index is not referenced explicitly. + usage.index = usage.index || f.spectralExpression.usage.index || f.spectralExpression.usage.property + return + } if f.expression != nil { f.expression.collectContextVarUsage(usage) } diff --git a/vendor/github.com/pb33f/jsonpath/pkg/jsonpath/filter.go b/vendor/github.com/pb33f/jsonpath/pkg/jsonpath/filter.go index 67afe7e8a..a481039a5 100644 --- a/vendor/github.com/pb33f/jsonpath/pkg/jsonpath/filter.go +++ b/vendor/github.com/pb33f/jsonpath/pkg/jsonpath/filter.go @@ -1,81 +1,89 @@ package jsonpath import ( - "go.yaml.in/yaml/v4" - "strconv" - "strings" + "go.yaml.in/yaml/v4" + "strconv" + "strings" ) // filter-selector = "?" S logical-expr type filterSelector struct { - // logical-expr = logical-or-expr - expression *logicalOrExpr + // logical-expr = logical-or-expr + expression *logicalOrExpr + spectralExpression *spectralBoolExpr +} + +func (s filterSelector) present() bool { + return s.expression != nil || s.spectralExpression != nil } func (s filterSelector) ToString() string { - return s.expression.ToString() + if s.spectralExpression != nil { + return s.spectralExpression.String() + } + return s.expression.ToString() } // logical-or-expr = logical-and-expr *(S "||" S logical-and-expr) type logicalOrExpr struct { - expressions []*logicalAndExpr + expressions []*logicalAndExpr } func (e logicalOrExpr) ToString() string { - builder := strings.Builder{} - for i, expr := range e.expressions { - if i > 0 { - builder.WriteString(" || ") - } - builder.WriteString(expr.ToString()) - } - return builder.String() + builder := strings.Builder{} + for i, expr := range e.expressions { + if i > 0 { + builder.WriteString(" || ") + } + builder.WriteString(expr.ToString()) + } + return builder.String() } // logical-and-expr = basic-expr *(S "&&" S basic-expr) type logicalAndExpr struct { - expressions []*basicExpr + expressions []*basicExpr } func (e logicalAndExpr) ToString() string { - builder := strings.Builder{} - for i, expr := range e.expressions { - if i > 0 { - builder.WriteString(" && ") - } - builder.WriteString(expr.ToString()) - } - return builder.String() + builder := strings.Builder{} + for i, expr := range e.expressions { + if i > 0 { + builder.WriteString(" && ") + } + builder.WriteString(expr.ToString()) + } + return builder.String() } // relQuery rel-query = current-node-identifier segments // current-node-identifier = "@" type relQuery struct { - segments []*segment + segments []*segment } func (q relQuery) ToString() string { - builder := strings.Builder{} - builder.WriteString("@") - for _, segment := range q.segments { - builder.WriteString(segment.ToString()) - } - return builder.String() + builder := strings.Builder{} + builder.WriteString("@") + for _, segment := range q.segments { + builder.WriteString(segment.ToString()) + } + return builder.String() } // filterQuery filter-query = rel-query / jsonpath-query type filterQuery struct { - relQuery *relQuery - jsonPathQuery *jsonPathAST + relQuery *relQuery + jsonPathQuery *jsonPathAST } func (q filterQuery) ToString() string { - if q.relQuery != nil { - return q.relQuery.ToString() - } else if q.jsonPathQuery != nil { - return q.jsonPathQuery.ToString() - } - return "" + if q.relQuery != nil { + return q.relQuery.ToString() + } else if q.jsonPathQuery != nil { + return q.jsonPathQuery.ToString() + } + return "" } // functionArgument function-argument = literal / @@ -84,69 +92,69 @@ func (q filterQuery) ToString() string { // logical-expr / // function-expr type functionArgument struct { - literal *literal - filterQuery *filterQuery - logicalExpr *logicalOrExpr - functionExpr *functionExpr - contextVar *contextVariable // JSONPath Plus context variables + literal *literal + filterQuery *filterQuery + logicalExpr *logicalOrExpr + functionExpr *functionExpr + contextVar *contextVariable // JSONPath Plus context variables } type functionArgType int const ( - functionArgTypeLiteral functionArgType = iota - functionArgTypeNodes + functionArgTypeLiteral functionArgType = iota + functionArgTypeNodes ) type resolvedArgument struct { - kind functionArgType - literal *literal - nodes []*literal + kind functionArgType + literal *literal + nodes []*literal } func (a functionArgument) Eval(idx index, node *yaml.Node, root *yaml.Node) resolvedArgument { - if a.literal != nil { - return resolvedArgument{kind: functionArgTypeLiteral, literal: a.literal} - } else if a.filterQuery != nil { - result := a.filterQuery.Query(idx, node, root) - lits := make([]*literal, len(result)) - for i, node := range result { - lit := nodeToLiteral(node) - lits[i] = &lit - } - if len(result) != 1 { - return resolvedArgument{kind: functionArgTypeNodes, nodes: lits} - } else { - return resolvedArgument{kind: functionArgTypeLiteral, literal: lits[0]} - } - } else if a.logicalExpr != nil { - res := a.logicalExpr.Matches(idx, node, root) - return resolvedArgument{kind: functionArgTypeLiteral, literal: &literal{bool: &res}} - } else if a.functionExpr != nil { - res := a.functionExpr.Evaluate(idx, node, root) - return resolvedArgument{kind: functionArgTypeLiteral, literal: &res} - } else if a.contextVar != nil { - // Evaluate context variable and return as literal - res := a.contextVar.Evaluate(idx, node, root) - return resolvedArgument{kind: functionArgTypeLiteral, literal: &res} - } - return resolvedArgument{} + if a.literal != nil { + return resolvedArgument{kind: functionArgTypeLiteral, literal: a.literal} + } else if a.filterQuery != nil { + result := a.filterQuery.Query(idx, node, root) + lits := make([]*literal, len(result)) + for i, node := range result { + lit := nodeToLiteral(node) + lits[i] = &lit + } + if len(result) != 1 { + return resolvedArgument{kind: functionArgTypeNodes, nodes: lits} + } else { + return resolvedArgument{kind: functionArgTypeLiteral, literal: lits[0]} + } + } else if a.logicalExpr != nil { + res := a.logicalExpr.Matches(idx, node, root) + return resolvedArgument{kind: functionArgTypeLiteral, literal: &literal{bool: &res}} + } else if a.functionExpr != nil { + res := a.functionExpr.Evaluate(idx, node, root) + return resolvedArgument{kind: functionArgTypeLiteral, literal: &res} + } else if a.contextVar != nil { + // Evaluate context variable and return as literal + res := a.contextVar.Evaluate(idx, node, root) + return resolvedArgument{kind: functionArgTypeLiteral, literal: &res} + } + return resolvedArgument{} } func (a functionArgument) ToString() string { - builder := strings.Builder{} - if a.literal != nil { - builder.WriteString(a.literal.ToString()) - } else if a.filterQuery != nil { - builder.WriteString(a.filterQuery.ToString()) - } else if a.logicalExpr != nil { - builder.WriteString(a.logicalExpr.ToString()) - } else if a.functionExpr != nil { - builder.WriteString(a.functionExpr.ToString()) - } else if a.contextVar != nil { - builder.WriteString(a.contextVar.ToString()) - } - return builder.String() + builder := strings.Builder{} + if a.literal != nil { + builder.WriteString(a.literal.ToString()) + } else if a.filterQuery != nil { + builder.WriteString(a.filterQuery.ToString()) + } else if a.logicalExpr != nil { + builder.WriteString(a.logicalExpr.ToString()) + } else if a.functionExpr != nil { + builder.WriteString(a.functionExpr.ToString()) + } else if a.contextVar != nil { + builder.WriteString(a.contextVar.ToString()) + } + return builder.String() } //function-name = function-name-first *function-name-char @@ -158,74 +166,74 @@ func (a functionArgument) ToString() string { type functionType int const ( - functionTypeLength functionType = iota - functionTypeCount - functionTypeMatch - functionTypeSearch - functionTypeValue - // JSONPath Plus type selector functions - functionTypeIsNull - functionTypeIsBoolean - functionTypeIsNumber - functionTypeIsString - functionTypeIsArray - functionTypeIsObject - functionTypeIsInteger + functionTypeLength functionType = iota + functionTypeCount + functionTypeMatch + functionTypeSearch + functionTypeValue + // JSONPath Plus type selector functions + functionTypeIsNull + functionTypeIsBoolean + functionTypeIsNumber + functionTypeIsString + functionTypeIsArray + functionTypeIsObject + functionTypeIsInteger ) var functionTypeMap = map[string]functionType{ - "length": functionTypeLength, - "count": functionTypeCount, - "match": functionTypeMatch, - "search": functionTypeSearch, - "value": functionTypeValue, + "length": functionTypeLength, + "count": functionTypeCount, + "match": functionTypeMatch, + "search": functionTypeSearch, + "value": functionTypeValue, } // typeSelectorFunctionMap maps JSONPath Plus type selector function names to their types. // These are extensions enabled when JSONPath Plus mode is active. var typeSelectorFunctionMap = map[string]functionType{ - "isNull": functionTypeIsNull, - "isBoolean": functionTypeIsBoolean, - "isNumber": functionTypeIsNumber, - "isString": functionTypeIsString, - "isArray": functionTypeIsArray, - "isObject": functionTypeIsObject, - "isInteger": functionTypeIsInteger, + "isNull": functionTypeIsNull, + "isBoolean": functionTypeIsBoolean, + "isNumber": functionTypeIsNumber, + "isString": functionTypeIsString, + "isArray": functionTypeIsArray, + "isObject": functionTypeIsObject, + "isInteger": functionTypeIsInteger, } func (f functionType) String() string { - for k, v := range functionTypeMap { - if v == f { - return k - } - } - for k, v := range typeSelectorFunctionMap { - if v == f { - return k - } - } - return "unknown" + for k, v := range functionTypeMap { + if v == f { + return k + } + } + for k, v := range typeSelectorFunctionMap { + if v == f { + return k + } + } + return "unknown" } // functionExpr function-expr = function-name "(" S [function-argument // *(S "," S function-argument)] S ")" type functionExpr struct { - funcType functionType - args []*functionArgument + funcType functionType + args []*functionArgument } func (e functionExpr) ToString() string { - builder := strings.Builder{} - builder.WriteString(e.funcType.String()) - builder.WriteString("(") - for i, arg := range e.args { - if i > 0 { - builder.WriteString(", ") - } - builder.WriteString(arg.ToString()) - } - builder.WriteString(")") - return builder.String() + builder := strings.Builder{} + builder.WriteString(e.funcType.String()) + builder.WriteString("(") + for i, arg := range e.args { + if i > 0 { + builder.WriteString(", ") + } + builder.WriteString(arg.ToString()) + } + builder.WriteString(")") + return builder.String() } // testExpr test-expr = [logical-not-op S] @@ -233,22 +241,22 @@ func (e functionExpr) ToString() string { // (filter-query / ; existence/non-existence // function-expr) ; LogicalType or NodesType type testExpr struct { - not bool - filterQuery *filterQuery - functionExpr *functionExpr + not bool + filterQuery *filterQuery + functionExpr *functionExpr } func (e testExpr) ToString() string { - builder := strings.Builder{} - if e.not { - builder.WriteString("!") - } - if e.filterQuery != nil { - builder.WriteString(e.filterQuery.ToString()) - } else if e.functionExpr != nil { - builder.WriteString(e.functionExpr.ToString()) - } - return builder.String() + builder := strings.Builder{} + if e.not { + builder.WriteString("!") + } + if e.filterQuery != nil { + builder.WriteString(e.filterQuery.ToString()) + } else if e.functionExpr != nil { + builder.WriteString(e.functionExpr.ToString()) + } + return builder.String() } // basicExpr basic-expr = @@ -257,166 +265,166 @@ func (e testExpr) ToString() string { // comparison-expr / // test-expr type basicExpr struct { - parenExpr *parenExpr - comparisonExpr *comparisonExpr - testExpr *testExpr + parenExpr *parenExpr + comparisonExpr *comparisonExpr + testExpr *testExpr } func (e basicExpr) ToString() string { - if e.parenExpr != nil { - return e.parenExpr.ToString() - } else if e.comparisonExpr != nil { - return e.comparisonExpr.ToString() - } else if e.testExpr != nil { - return e.testExpr.ToString() - } - return "" + if e.parenExpr != nil { + return e.parenExpr.ToString() + } else if e.comparisonExpr != nil { + return e.comparisonExpr.ToString() + } else if e.testExpr != nil { + return e.testExpr.ToString() + } + return "" } // literal literal = number / // . string-literal / // . true / false / null type literal struct { - // we generally decompose these into their component parts for easier evaluation - integer *int - float64 *float64 - string *string - bool *bool - null *bool - node *yaml.Node + // we generally decompose these into their component parts for easier evaluation + integer *int + float64 *float64 + string *string + bool *bool + null *bool + node *yaml.Node } func (l literal) ToString() string { - if l.integer != nil { - return strconv.Itoa(*l.integer) - } else if l.float64 != nil { - return strconv.FormatFloat(*l.float64, 'f', -1, 64) - } else if l.string != nil { - builder := strings.Builder{} - builder.WriteString("'") - builder.WriteString(escapeString(*l.string)) - builder.WriteString("'") - return builder.String() - } else if l.bool != nil { - if *l.bool { - return "true" - } else { - return "false" - } - } else if l.null != nil { - if *l.null { - return "null" - } else { - return "null" - } - } else if l.node != nil { - switch l.node.Kind { - case yaml.ScalarNode: - return l.node.Value - case yaml.SequenceNode: - builder := strings.Builder{} - builder.WriteString("[") - for i, child := range l.node.Content { - if i > 0 { - builder.WriteString(",") - } - builder.WriteString(literal{node: child}.ToString()) - } - builder.WriteString("]") - return builder.String() - case yaml.MappingNode: - builder := strings.Builder{} - builder.WriteString("{") - for i, child := range l.node.Content { - if i > 0 { - builder.WriteString(",") - } - builder.WriteString(literal{node: child}.ToString()) - } - builder.WriteString("}") - return builder.String() - } - } - return "" + if l.integer != nil { + return strconv.Itoa(*l.integer) + } else if l.float64 != nil { + return strconv.FormatFloat(*l.float64, 'f', -1, 64) + } else if l.string != nil { + builder := strings.Builder{} + builder.WriteString("'") + builder.WriteString(escapeString(*l.string)) + builder.WriteString("'") + return builder.String() + } else if l.bool != nil { + if *l.bool { + return "true" + } else { + return "false" + } + } else if l.null != nil { + if *l.null { + return "null" + } else { + return "null" + } + } else if l.node != nil { + switch l.node.Kind { + case yaml.ScalarNode: + return l.node.Value + case yaml.SequenceNode: + builder := strings.Builder{} + builder.WriteString("[") + for i, child := range l.node.Content { + if i > 0 { + builder.WriteString(",") + } + builder.WriteString(literal{node: child}.ToString()) + } + builder.WriteString("]") + return builder.String() + case yaml.MappingNode: + builder := strings.Builder{} + builder.WriteString("{") + for i, child := range l.node.Content { + if i > 0 { + builder.WriteString(",") + } + builder.WriteString(literal{node: child}.ToString()) + } + builder.WriteString("}") + return builder.String() + } + } + return "" } func escapeString(value string) string { - b := strings.Builder{} - for i := 0; i < len(value); i++ { - if value[i] == '\n' { - b.WriteString("\\\\n") - } else if value[i] == '\\' { - b.WriteString("\\\\") - } else if value[i] == '\'' { - b.WriteString("\\'") - } else { - b.WriteByte(value[i]) - } - } - return b.String() + b := strings.Builder{} + for i := 0; i < len(value); i++ { + if value[i] == '\n' { + b.WriteString("\\\\n") + } else if value[i] == '\\' { + b.WriteString("\\\\") + } else if value[i] == '\'' { + b.WriteString("\\'") + } else { + b.WriteByte(value[i]) + } + } + return b.String() } type absQuery jsonPathAST func (q absQuery) ToString() string { - builder := strings.Builder{} - builder.WriteString("$") - for _, segment := range q.segments { - builder.WriteString(segment.ToString()) - } - return builder.String() + builder := strings.Builder{} + builder.WriteString("$") + for _, segment := range q.segments { + builder.WriteString(segment.ToString()) + } + return builder.String() } // singularQuery singular-query = rel-singular-query / abs-singular-query type singularQuery struct { - relQuery *relQuery - absQuery *absQuery + relQuery *relQuery + absQuery *absQuery } func (q singularQuery) ToString() string { - if q.relQuery != nil { - return q.relQuery.ToString() - } else if q.absQuery != nil { - return q.absQuery.ToString() - } - return "" + if q.relQuery != nil { + return q.relQuery.ToString() + } else if q.absQuery != nil { + return q.absQuery.ToString() + } + return "" } // contextVarKind represents the type of context variable type contextVarKind int const ( - contextVarProperty contextVarKind = iota // @property - current property name - contextVarRoot // @root - root node access - contextVarParent // @parent - parent node - contextVarParentProperty // @parentProperty - parent's property name - contextVarPath // @path - absolute path to current node - contextVarIndex // @index - current array index + contextVarProperty contextVarKind = iota // @property - current property name + contextVarRoot // @root - root node access + contextVarParent // @parent - parent node + contextVarParentProperty // @parentProperty - parent's property name + contextVarPath // @path - absolute path to current node + contextVarIndex // @index - current array index ) // contextVariable represents a JSONPath Plus context variable in filter expressions. // These provide access to metadata about the current node being evaluated. type contextVariable struct { - kind contextVarKind + kind contextVarKind } func (cv contextVariable) ToString() string { - switch cv.kind { - case contextVarProperty: - return "@property" - case contextVarRoot: - return "@root" - case contextVarParent: - return "@parent" - case contextVarParentProperty: - return "@parentProperty" - case contextVarPath: - return "@path" - case contextVarIndex: - return "@index" - default: - return "@unknown" - } + switch cv.kind { + case contextVarProperty: + return "@property" + case contextVarRoot: + return "@root" + case contextVarParent: + return "@parent" + case contextVarParentProperty: + return "@parentProperty" + case contextVarPath: + return "@path" + case contextVarIndex: + return "@index" + default: + return "@unknown" + } } // comparable @@ -426,23 +434,23 @@ func (cv contextVariable) ToString() string { // function-expr ; ValueType // context-variable ; JSONPath Plus extension type comparable struct { - literal *literal - singularQuery *singularQuery - functionExpr *functionExpr - contextVar *contextVariable // JSONPath Plus extension + literal *literal + singularQuery *singularQuery + functionExpr *functionExpr + contextVar *contextVariable // JSONPath Plus extension } func (c comparable) ToString() string { - if c.literal != nil { - return c.literal.ToString() - } else if c.singularQuery != nil { - return c.singularQuery.ToString() - } else if c.functionExpr != nil { - return c.functionExpr.ToString() - } else if c.contextVar != nil { - return c.contextVar.ToString() - } - return "" + if c.literal != nil { + return c.literal.ToString() + } else if c.singularQuery != nil { + return c.singularQuery.ToString() + } else if c.functionExpr != nil { + return c.functionExpr.ToString() + } else if c.contextVar != nil { + return c.contextVar.ToString() + } + return "" } // comparisonExpr represents a comparison expression @@ -457,73 +465,73 @@ func (c comparable) ToString() string { // "<=" / ">=" / // "<" / ">" type comparisonExpr struct { - left *comparable - op comparisonOperator - right *comparable + left *comparable + op comparisonOperator + right *comparable } func (e comparisonExpr) ToString() string { - builder := strings.Builder{} - builder.WriteString(e.left.ToString()) - builder.WriteString(" ") - builder.WriteString(e.op.ToString()) - builder.WriteString(" ") - builder.WriteString(e.right.ToString()) - return builder.String() + builder := strings.Builder{} + builder.WriteString(e.left.ToString()) + builder.WriteString(" ") + builder.WriteString(e.op.ToString()) + builder.WriteString(" ") + builder.WriteString(e.right.ToString()) + return builder.String() } // existExpr represents an existence expression type existExpr struct { - query string + query string } // parenExpr represents a parenthesized expression // // paren-expr = [logical-not-op S] "(" S logical-expr S ")" type parenExpr struct { - // "!" - not bool - // "(" logicalOrExpr ")" - expr *logicalOrExpr + // "!" + not bool + // "(" logicalOrExpr ")" + expr *logicalOrExpr } func (e parenExpr) ToString() string { - builder := strings.Builder{} - if e.not { - builder.WriteString("!") - } - builder.WriteString("(") - builder.WriteString(e.expr.ToString()) - builder.WriteString(")") - return builder.String() + builder := strings.Builder{} + if e.not { + builder.WriteString("!") + } + builder.WriteString("(") + builder.WriteString(e.expr.ToString()) + builder.WriteString(")") + return builder.String() } // comparisonOperator represents a comparison operator type comparisonOperator int const ( - equalTo comparisonOperator = iota - notEqualTo - lessThan - lessThanEqualTo - greaterThan - greaterThanEqualTo + equalTo comparisonOperator = iota + notEqualTo + lessThan + lessThanEqualTo + greaterThan + greaterThanEqualTo ) func (o comparisonOperator) ToString() string { - switch o { - case equalTo: - return "==" - case notEqualTo: - return "!=" - case lessThan: - return "<" - case lessThanEqualTo: - return "<=" - case greaterThan: - return ">" - case greaterThanEqualTo: - return ">=" - } - return "" + switch o { + case equalTo: + return "==" + case notEqualTo: + return "!=" + case lessThan: + return "<" + case lessThanEqualTo: + return "<=" + case greaterThan: + return ">" + case greaterThanEqualTo: + return ">=" + } + return "" } diff --git a/vendor/github.com/pb33f/jsonpath/pkg/jsonpath/jsonpath.go b/vendor/github.com/pb33f/jsonpath/pkg/jsonpath/jsonpath.go index 229dbebb2..aaacda68f 100644 --- a/vendor/github.com/pb33f/jsonpath/pkg/jsonpath/jsonpath.go +++ b/vendor/github.com/pb33f/jsonpath/pkg/jsonpath/jsonpath.go @@ -1,35 +1,82 @@ package jsonpath import ( - "fmt" - "github.com/pb33f/jsonpath/pkg/jsonpath/config" - "github.com/pb33f/jsonpath/pkg/jsonpath/token" - "go.yaml.in/yaml/v4" + "fmt" + "github.com/pb33f/jsonpath/pkg/jsonpath/config" + "github.com/pb33f/jsonpath/pkg/jsonpath/token" + "go.yaml.in/yaml/v4" ) +// NewPath compiles input into a reusable JSONPath using the supplied options. func NewPath(input string, opts ...config.Option) (*JSONPath, error) { - tokenizer := token.NewTokenizer(input, opts...) - tokens := tokenizer.Tokenize() - for i := 0; i < len(tokens); i++ { - if tokens[i].Token == token.ILLEGAL { - return nil, fmt.Errorf("%s", tokenizer.ErrorString(&tokens[i], "unexpected token")) - } - } - parser := newParserPrivate(tokenizer, tokens, opts...) - err := parser.parse() - if err != nil { - return nil, err - } - return parser, nil + cfg := config.New(opts...) + if err := config.Validate(cfg); err != nil { + return nil, err + } + tokenizer := token.NewTokenizerWithConfig(input, cfg) + tokens := tokenizer.Tokenize() + for i := 0; i < len(tokens); i++ { + if tokens[i].Token == token.ILLEGAL { + message := "unexpected token" + if config.SpectralCompatibilityEnabled(cfg) && tokens[i].Literal != "" { + message = "Spectral compatibility mode: " + tokens[i].Literal + } else if !cfg.JSONPathPlusEnabled() && tokens[i].Literal != "" { + message = tokens[i].Literal + } + return nil, fmt.Errorf("%s", tokenizer.ErrorString(&tokens[i], message)) + } + } + parser := newParserPrivateWithConfig(tokenizer, tokens, cfg) + err := parser.parse() + if err != nil { + return nil, err + } + return parser, nil } +// Query evaluates the compiled path against root. func (p *JSONPath) Query(root *yaml.Node) []*yaml.Node { - return p.ast.Query(root, root) + return p.ast.Query(root, root) } +// String returns a stable normalized representation of the compiled path. func (p *JSONPath) String() string { - if p == nil { - return "" - } - return p.ast.ToString() + if p == nil { + return "" + } + return p.ast.ToString() +} + +// IsSingular reports whether the path can select at most one node. +func (p *JSONPath) IsSingular() bool { + if p == nil { + return false + } + return p.ast.isSingular() +} + +// SegmentInfo describes a member-name or array-index segment in a singular path. +type SegmentInfo struct { + Kind SegmentKind + Key string + Index int64 + HasIndex bool +} + +// SegmentKind identifies the public kind of a singular path segment. +type SegmentKind int + +const ( + // SegmentKindMemberName identifies a mapping member segment. + SegmentKindMemberName SegmentKind = iota + // SegmentKindArrayIndex identifies a sequence index segment. + SegmentKindArrayIndex +) + +// GetSegmentInfo returns public segment metadata for a singular path. +func (p *JSONPath) GetSegmentInfo() ([]SegmentInfo, error) { + if p == nil { + return nil, fmt.Errorf("nil path") + } + return p.ast.getSegmentInfo() } diff --git a/vendor/github.com/pb33f/jsonpath/pkg/jsonpath/parser.go b/vendor/github.com/pb33f/jsonpath/pkg/jsonpath/parser.go index ec1109a2d..f321abd1f 100644 --- a/vendor/github.com/pb33f/jsonpath/pkg/jsonpath/parser.go +++ b/vendor/github.com/pb33f/jsonpath/pkg/jsonpath/parser.go @@ -1,12 +1,12 @@ package jsonpath import ( - "errors" - "fmt" - "github.com/pb33f/jsonpath/pkg/jsonpath/config" - "github.com/pb33f/jsonpath/pkg/jsonpath/token" - "strconv" - "strings" + "errors" + "fmt" + "github.com/pb33f/jsonpath/pkg/jsonpath/config" + "github.com/pb33f/jsonpath/pkg/jsonpath/token" + "strconv" + "strings" ) const MaxSafeFloat int64 = 9007199254740991 @@ -14,794 +14,811 @@ const MaxSafeFloat int64 = 9007199254740991 type mode int const ( - modeNormal mode = iota - modeSingular + modeNormal mode = iota + modeSingular ) // contextVarTokenMap maps context variable tokens to their kinds // CONTEXT_ROOT is handled separately as it requires path parsing var contextVarTokenMap = map[token.Token]contextVarKind{ - token.CONTEXT_PROPERTY: contextVarProperty, - token.CONTEXT_PARENT: contextVarParent, - token.CONTEXT_PARENT_PROPERTY: contextVarParentProperty, - token.CONTEXT_PATH: contextVarPath, - token.CONTEXT_INDEX: contextVarIndex, + token.CONTEXT_PROPERTY: contextVarProperty, + token.CONTEXT_PARENT: contextVarParent, + token.CONTEXT_PARENT_PROPERTY: contextVarParentProperty, + token.CONTEXT_PATH: contextVarPath, + token.CONTEXT_INDEX: contextVarIndex, } // JSONPath represents a JSONPath parser. type JSONPath struct { - tokenizer *token.Tokenizer - tokens []token.TokenInfo - ast jsonPathAST - current int - mode []mode - config config.Config - filterDepth int // tracks nesting depth inside filter expressions + 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(), jsonPathPlus: cfg.JSONPathPlusEnabled()}, 0, []mode{modeNormal}, cfg, 0} + return newParserPrivateWithConfig(tokenizer, tokens, config.New(opts...)) +} + +func newParserPrivateWithConfig(tokenizer *token.Tokenizer, tokens []token.TokenInfo, cfg config.Config) *JSONPath { + 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. // // jsonpath-query = root-identifier segments func (p *JSONPath) parse() error { - if len(p.tokens) == 0 { - return fmt.Errorf("empty JSONPath expression") - } - - if p.tokens[p.current].Token != token.ROOT { - return p.parseFailure(&p.tokens[p.current], "expected '$'") - } - p.current++ - - for p.current < len(p.tokens) { - segment, err := p.parseSegment() - if err != nil { - return err - } - p.ast.segments = append(p.ast.segments, segment) - } - return nil + if len(p.tokens) == 0 { + return fmt.Errorf("empty JSONPath expression") + } + + if p.tokens[p.current].Token != token.ROOT { + return p.parseFailure(&p.tokens[p.current], "expected '$'") + } + p.current++ + + for p.current < len(p.tokens) { + segment, err := p.parseSegment() + if err != nil { + return err + } + p.ast.segments = append(p.ast.segments, segment) + } + return nil } func (p *JSONPath) parseFailure(target *token.TokenInfo, msg string) error { - return errors.New(p.tokenizer.ErrorString(target, msg)) + return errors.New(p.tokenizer.ErrorString(target, msg)) } // peek returns true if the upcoming token matches the given token type. func (p *JSONPath) peek(token token.Token) bool { - return p.current+1 < len(p.tokens) && p.tokens[p.current+1].Token == token + return p.current+1 < len(p.tokens) && p.tokens[p.current+1].Token == token } // peek returns true if the upcoming token matches the given token type. func (p *JSONPath) next(token token.Token) bool { - return p.current < len(p.tokens) && p.tokens[p.current].Token == token + return p.current < len(p.tokens) && p.tokens[p.current].Token == token } // expect consumes the current token if it matches the given token type. func (p *JSONPath) expect(token token.Token) bool { - if p.peek(token) { - p.current++ - return true - } - return false + if p.peek(token) { + p.current++ + return true + } + return false } // isComparisonOperator returns true if the given token is a comparison operator. func (p *JSONPath) isComparisonOperator(tok token.Token) bool { - return tok == token.EQ || tok == token.NE || tok == token.GT || tok == token.GE || tok == token.LT || tok == token.LE + return tok == token.EQ || tok == token.NE || tok == token.GT || tok == token.GE || tok == token.LT || tok == token.LE } func (p *JSONPath) parseSegment() (*segment, error) { - currentToken := p.tokens[p.current] - if currentToken.Token == token.RECURSIVE { - if p.mode[len(p.mode)-1] == modeSingular { - return nil, p.parseFailure(&p.tokens[p.current], "unexpected recursive descent in singular query") - } - p.current++ - child, err := p.parseInnerSegment() - if err != nil { - return nil, err - } - return &segment{kind: segmentKindDescendant, descendant: child}, nil - } else if currentToken.Token == token.CHILD || currentToken.Token == token.BRACKET_LEFT { - if currentToken.Token == token.CHILD { - p.current++ - } - child, err := p.parseInnerSegment() - if err != nil { - return nil, err - } - return &segment{kind: segmentKindChild, child: child}, nil - } else if p.config.PropertyNameEnabled() && currentToken.Token == token.PROPERTY_NAME { - p.current++ - return &segment{kind: segmentKindProperyName}, nil - } else if p.config.JSONPathPlusEnabled() && currentToken.Token == token.PARENT_SELECTOR { - // JSONPath Plus parent selector: ^ returns parent of current node - p.current++ - return &segment{kind: segmentKindParent}, nil - } - return nil, p.parseFailure(¤tToken, "unexpected token when parsing segment") + currentToken := p.tokens[p.current] + if currentToken.Token == token.RECURSIVE { + if p.mode[len(p.mode)-1] == modeSingular { + return nil, p.parseFailure(&p.tokens[p.current], "unexpected recursive descent in singular query") + } + p.current++ + child, err := p.parseInnerSegment() + if err != nil { + return nil, err + } + return &segment{kind: segmentKindDescendant, descendant: child}, nil + } else if currentToken.Token == token.CHILD || currentToken.Token == token.BRACKET_LEFT { + if currentToken.Token == token.CHILD { + if config.SpectralCompatibilityEnabled(p.config) && p.current+1 < len(p.tokens) && p.tokens[p.current+1].Token == token.PROPERTY_NAME { + p.current += 2 + return &segment{kind: segmentKindRecursivePropertyName}, nil + } + p.current++ + } + child, err := p.parseInnerSegment() + if err != nil { + return nil, err + } + return &segment{kind: segmentKindChild, child: child}, nil + } else if p.config.PropertyNameEnabled() && currentToken.Token == token.PROPERTY_NAME { + p.current++ + return &segment{kind: segmentKindProperyName}, nil + } else if p.config.JSONPathPlusEnabled() && currentToken.Token == token.PARENT_SELECTOR { + // JSONPath Plus parent selector: ^ returns parent of current node + p.current++ + return &segment{kind: segmentKindParent}, nil + } + return nil, p.parseFailure(¤tToken, "unexpected token when parsing segment") } func (p *JSONPath) parseInnerSegment() (retValue *innerSegment, err error) { - defer func() { - if p.mode[len(p.mode)-1] == modeSingular && retValue != nil { - if len(retValue.selectors) > 1 { - retValue = nil - err = p.parseFailure(&p.tokens[p.current], "unexpected multiple selectors in singular query") - return - } else if retValue.kind == segmentDotWildcard { - retValue = nil - err = p.parseFailure(&p.tokens[p.current], "unexpected wildcard in singular query") - return - } - } - }() - // .* - // .STRING - // [] - if p.current >= len(p.tokens) { - return nil, p.parseFailure(nil, "unexpected end of input") - } - firstToken := p.tokens[p.current] - if firstToken.Token == token.WILDCARD { - p.current += 1 - return &innerSegment{segmentDotWildcard, "", nil}, nil - } else if firstToken.Token == token.STRING { - 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 - selectors := []*selector{} - for p.current < len(p.tokens) { - innerSelector, err := p.parseSelector() - if err != nil { - p.current = prior - return nil, err - } - selectors = append(selectors, innerSelector) - if len(p.tokens) <= p.current { - return nil, p.parseFailure(&p.tokens[p.current-1], "unexpected end of input") - } - if p.tokens[p.current].Token == token.BRACKET_RIGHT { - break - } else if p.tokens[p.current].Token == token.COMMA { - p.current++ - } - } - if p.tokens[p.current].Token != token.BRACKET_RIGHT { - prior = p.current - return nil, p.parseFailure(&p.tokens[p.current], "expected ']'") - } - p.current += 1 - return &innerSegment{kind: segmentLongHand, dotName: "", selectors: selectors}, nil - } - return nil, p.parseFailure(&firstToken, "unexpected token when parsing inner segment") + defer func() { + if p.mode[len(p.mode)-1] == modeSingular && retValue != nil { + if len(retValue.selectors) > 1 { + retValue = nil + err = p.parseFailure(&p.tokens[p.current], "unexpected multiple selectors in singular query") + return + } else if retValue.kind == segmentDotWildcard { + retValue = nil + err = p.parseFailure(&p.tokens[p.current], "unexpected wildcard in singular query") + return + } + } + }() + // .* + // .STRING + // [] + if p.current >= len(p.tokens) { + return nil, p.parseFailure(nil, "unexpected end of input") + } + firstToken := p.tokens[p.current] + if firstToken.Token == token.WILDCARD { + p.current += 1 + return &innerSegment{segmentDotWildcard, "", nil}, nil + } else if firstToken.Token == token.STRING { + 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 + selectors := []*selector{} + for p.current < len(p.tokens) { + innerSelector, err := p.parseSelector() + if err != nil { + p.current = prior + return nil, err + } + selectors = append(selectors, innerSelector) + if len(p.tokens) <= p.current { + return nil, p.parseFailure(&p.tokens[p.current-1], "unexpected end of input") + } + if p.tokens[p.current].Token == token.BRACKET_RIGHT { + break + } else if p.tokens[p.current].Token == token.COMMA { + p.current++ + } + } + if p.tokens[p.current].Token != token.BRACKET_RIGHT { + prior = p.current + return nil, p.parseFailure(&p.tokens[p.current], "expected ']'") + } + p.current += 1 + return &innerSegment{kind: segmentLongHand, dotName: "", selectors: selectors}, nil + } + return nil, p.parseFailure(&firstToken, "unexpected token when parsing inner segment") } func (p *JSONPath) parseSelector() (retSelector *selector, err error) { - //selector = name-selector / - // wildcard-selector / - // slice-selector / - // index-selector / - // filter-selector - initial := p.current - defer func() { - if p.mode[len(p.mode)-1] == modeSingular && retSelector != nil { - if retSelector.kind == selectorSubKindWildcard { - err = p.parseFailure(&p.tokens[initial], "unexpected wildcard in singular query") - retSelector = nil - } else if retSelector.kind == selectorSubKindArraySlice { - err = p.parseFailure(&p.tokens[initial], "unexpected slice in singular query") - retSelector = nil - } - } - }() - - // name-selector = string-literal - if p.tokens[p.current].Token == token.STRING_LITERAL { - name := p.tokens[p.current].Literal - p.current++ - return &selector{kind: selectorSubKindName, name: name}, nil - // wildcard-selector = "*" - } else if p.tokens[p.current].Token == token.WILDCARD { - p.current++ - return &selector{kind: selectorSubKindWildcard}, nil - } else if p.tokens[p.current].Token == token.INTEGER { - // peek ahead to see if it's a slice - if p.peek(token.ARRAY_SLICE) { - slice, err := p.parseSliceSelector() - if err != nil { - return nil, err - } - return &selector{kind: selectorSubKindArraySlice, slice: slice}, nil - } - // peek ahead to see if we close the array index properly - if !p.peek(token.BRACKET_RIGHT) && !p.peek(token.COMMA) { - return nil, p.parseFailure(&p.tokens[p.current], "expected ']' or ','") - } - // else it's an index - lit := p.tokens[p.current].Literal - // make sure it's not -0 - if lit == "-0" { - return nil, p.parseFailure(&p.tokens[p.current], "-0 unexpected") - } - // make sure lit is an integer - i, err := strconv.ParseInt(lit, 10, 64) - if err != nil { - return nil, p.parseFailure(&p.tokens[p.current], "expected an integer") - } - err = p.checkSafeInteger(i, lit) - if err != nil { - return nil, err - } - - p.current++ - - 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 { - return nil, err - } - return &selector{kind: selectorSubKindArraySlice, slice: slice}, nil - } else if p.tokens[p.current].Token == token.FILTER { - return p.parseFilterSelector() - } - - return nil, p.parseFailure(&p.tokens[p.current], "unexpected token when parsing selector") + //selector = name-selector / + // wildcard-selector / + // slice-selector / + // index-selector / + // filter-selector + initial := p.current + defer func() { + if p.mode[len(p.mode)-1] == modeSingular && retSelector != nil { + if retSelector.kind == selectorSubKindWildcard { + err = p.parseFailure(&p.tokens[initial], "unexpected wildcard in singular query") + retSelector = nil + } else if retSelector.kind == selectorSubKindArraySlice { + err = p.parseFailure(&p.tokens[initial], "unexpected slice in singular query") + retSelector = nil + } + } + }() + + // name-selector = string-literal + if p.tokens[p.current].Token == token.STRING_LITERAL { + name := p.tokens[p.current].Literal + p.current++ + return &selector{kind: selectorSubKindName, name: name}, nil + // wildcard-selector = "*" + } else if p.tokens[p.current].Token == token.WILDCARD { + p.current++ + return &selector{kind: selectorSubKindWildcard}, nil + } else if p.tokens[p.current].Token == token.INTEGER { + // peek ahead to see if it's a slice + if p.peek(token.ARRAY_SLICE) { + slice, err := p.parseSliceSelector() + if err != nil { + return nil, err + } + return &selector{kind: selectorSubKindArraySlice, slice: slice}, nil + } + // peek ahead to see if we close the array index properly + if !p.peek(token.BRACKET_RIGHT) && !p.peek(token.COMMA) { + return nil, p.parseFailure(&p.tokens[p.current], "expected ']' or ','") + } + // else it's an index + lit := p.tokens[p.current].Literal + // make sure it's not -0 + if lit == "-0" { + return nil, p.parseFailure(&p.tokens[p.current], "-0 unexpected") + } + // make sure lit is an integer + i, err := strconv.ParseInt(lit, 10, 64) + if err != nil { + return nil, p.parseFailure(&p.tokens[p.current], "expected an integer") + } + err = p.checkSafeInteger(i, lit) + if err != nil { + return nil, err + } + + p.current++ + + return &selector{ + kind: selectorSubKindArrayIndex, + index: i, + jsonPathPlus: p.config.JSONPathPlusEnabled() && p.filterDepth == 0, + spectral: config.SpectralCompatibilityEnabled(p.config), + }, nil + } else if p.tokens[p.current].Token == token.ARRAY_SLICE { + slice, err := p.parseSliceSelector() + if err != nil { + return nil, err + } + return &selector{kind: selectorSubKindArraySlice, slice: slice}, nil + } else if p.tokens[p.current].Token == token.FILTER { + return p.parseFilterSelector() + } + + return nil, p.parseFailure(&p.tokens[p.current], "unexpected token when parsing selector") } func (p *JSONPath) parseSliceSelector() (*slice, error) { - // slice-selector = [start S] ":" S [end S] [":" [S step]] - var start, end, step *int64 - - // parse the start index - if p.tokens[p.current].Token == token.INTEGER { - literal := p.tokens[p.current].Literal - i, err := strconv.ParseInt(literal, 10, 64) - if err != nil { - return nil, p.parseFailure(&p.tokens[p.current], "expected an integer") - } - err = p.checkSafeInteger(i, literal) - if err != nil { - return nil, err - } - - start = &i - p.current += 1 - } - - // Expect a colon - if p.tokens[p.current].Token != token.ARRAY_SLICE { - return nil, p.parseFailure(&p.tokens[p.current], "expected ':'") - } - p.current++ - - // parse the end index - if p.tokens[p.current].Token == token.INTEGER { - literal := p.tokens[p.current].Literal - i, err := strconv.ParseInt(literal, 10, 64) - if err != nil { - return nil, p.parseFailure(&p.tokens[p.current], "expected an integer") - } - err = p.checkSafeInteger(i, literal) - if err != nil { - return nil, err - } - - end = &i - p.current++ - } - - // Check for an optional second colon and step value - if p.tokens[p.current].Token == token.ARRAY_SLICE { - p.current++ - if p.tokens[p.current].Token == token.INTEGER { - literal := p.tokens[p.current].Literal - i, err := strconv.ParseInt(literal, 10, 64) - if err != nil { - return nil, p.parseFailure(&p.tokens[p.current], "expected an integer") - } - err = p.checkSafeInteger(i, literal) - if err != nil { - return nil, err - } - - step = &i - p.current++ - } - } - if p.tokens[p.current].Token != token.BRACKET_RIGHT { - return nil, p.parseFailure(&p.tokens[p.current], "expected ']'") - } - - return &slice{start: start, end: end, step: step}, nil + // slice-selector = [start S] ":" S [end S] [":" [S step]] + var start, end, step *int64 + + // parse the start index + if p.tokens[p.current].Token == token.INTEGER { + literal := p.tokens[p.current].Literal + i, err := strconv.ParseInt(literal, 10, 64) + if err != nil { + return nil, p.parseFailure(&p.tokens[p.current], "expected an integer") + } + err = p.checkSafeInteger(i, literal) + if err != nil { + return nil, err + } + + start = &i + p.current += 1 + } + + // Expect a colon + if p.tokens[p.current].Token != token.ARRAY_SLICE { + return nil, p.parseFailure(&p.tokens[p.current], "expected ':'") + } + p.current++ + + // parse the end index + if p.tokens[p.current].Token == token.INTEGER { + literal := p.tokens[p.current].Literal + i, err := strconv.ParseInt(literal, 10, 64) + if err != nil { + return nil, p.parseFailure(&p.tokens[p.current], "expected an integer") + } + err = p.checkSafeInteger(i, literal) + if err != nil { + return nil, err + } + + end = &i + p.current++ + } + + // Check for an optional second colon and step value + if p.tokens[p.current].Token == token.ARRAY_SLICE { + p.current++ + if p.tokens[p.current].Token == token.INTEGER { + literal := p.tokens[p.current].Literal + i, err := strconv.ParseInt(literal, 10, 64) + if err != nil { + return nil, p.parseFailure(&p.tokens[p.current], "expected an integer") + } + err = p.checkSafeInteger(i, literal) + if err != nil { + return nil, err + } + + step = &i + p.current++ + } + } + if p.tokens[p.current].Token != token.BRACKET_RIGHT { + return nil, p.parseFailure(&p.tokens[p.current], "expected ']'") + } + + return &slice{start: start, end: end, step: step}, nil } func (p *JSONPath) checkSafeInteger(i int64, literal string) error { - if i > MaxSafeFloat || i < -MaxSafeFloat { - return p.parseFailure(&p.tokens[p.current], "outside bounds for safe integers") - } - if literal == "-0" { - return p.parseFailure(&p.tokens[p.current], "-0 unexpected") - } - return nil + if i > MaxSafeFloat || i < -MaxSafeFloat { + return p.parseFailure(&p.tokens[p.current], "outside bounds for safe integers") + } + if literal == "-0" { + return p.parseFailure(&p.tokens[p.current], "-0 unexpected") + } + return nil } func (p *JSONPath) parseFilterSelector() (*selector, error) { - if p.tokens[p.current].Token != token.FILTER { - return nil, p.parseFailure(&p.tokens[p.current], "expected '?'") - } - p.current++ - p.filterDepth++ - defer func() { p.filterDepth-- }() - - expr, err := p.parseLogicalOrExpr() - if err != nil { - return nil, err - } - - return &selector{kind: selectorSubKindFilter, filter: &filterSelector{expr}}, nil + if p.tokens[p.current].Token != token.FILTER { + return nil, p.parseFailure(&p.tokens[p.current], "expected '?'") + } + p.current++ + p.filterDepth++ + defer func() { p.filterDepth-- }() + if config.SpectralCompatibilityEnabled(p.config) { + expr, err := p.parseSpectralExpression() + if err != nil { + return nil, err + } + return &selector{kind: selectorSubKindFilter, filter: filterSelector{spectralExpression: expr}}, nil + } + + expr, err := p.parseLogicalOrExpr() + if err != nil { + return nil, err + } + + return &selector{kind: selectorSubKindFilter, filter: filterSelector{expression: expr}}, nil } func (p *JSONPath) parseLogicalOrExpr() (*logicalOrExpr, error) { - var expr logicalOrExpr - - for { - andExpr, err := p.parseLogicalAndExpr() - if err != nil { - return nil, err - } - expr.expressions = append(expr.expressions, andExpr) - - if !p.next(token.OR) { - break - } - p.current++ - } - - return &expr, nil + var expr logicalOrExpr + + for { + andExpr, err := p.parseLogicalAndExpr() + if err != nil { + return nil, err + } + expr.expressions = append(expr.expressions, andExpr) + + if !p.next(token.OR) { + break + } + p.current++ + } + + return &expr, nil } func (p *JSONPath) parseLogicalAndExpr() (*logicalAndExpr, error) { - var expr logicalAndExpr - - for { - basicExpr, err := p.parseBasicExpr() - if err != nil { - return nil, err - } - expr.expressions = append(expr.expressions, basicExpr) - - if !p.next(token.AND) { - break - } - p.current++ - } - - return &expr, nil + var expr logicalAndExpr + + for { + basicExpr, err := p.parseBasicExpr() + if err != nil { + return nil, err + } + expr.expressions = append(expr.expressions, basicExpr) + + if !p.next(token.AND) { + break + } + p.current++ + } + + return &expr, nil } func (p *JSONPath) parseBasicExpr() (*basicExpr, error) { - //basic-expr = paren-expr / - // comparison-expr / - // test-expr - - switch p.tokens[p.current].Token { - case token.NOT: - p.current++ - expr, err := p.parseLogicalOrExpr() - if err != nil { - return nil, err - } - // Inspect if the expr is topped by a parenExpr -- if so we can simplify - if len(expr.expressions) == 1 && len(expr.expressions[0].expressions) == 1 && expr.expressions[0].expressions[0].parenExpr != nil { - child := expr.expressions[0].expressions[0].parenExpr - child.not = !child.not - return &basicExpr{parenExpr: child}, nil - } - return &basicExpr{parenExpr: &parenExpr{not: true, expr: expr}}, nil - case token.PAREN_LEFT: - p.current++ - expr, err := p.parseLogicalOrExpr() - if err != nil { - return nil, err - } - if p.tokens[p.current].Token != token.PAREN_RIGHT { - return nil, p.parseFailure(&p.tokens[p.current], "expected ')'") - } - p.current++ - return &basicExpr{parenExpr: &parenExpr{not: false, expr: expr}}, nil - } - prevCurrent := p.current - comparisonExpr, comparisonErr := p.parseComparisonExpr() - if comparisonErr == nil { - return &basicExpr{comparisonExpr: comparisonExpr}, nil - } - p.current = prevCurrent - testExpr, testErr := p.parseTestExpr() - if testErr == nil { - return &basicExpr{testExpr: testExpr}, nil - } - p.current = prevCurrent - return nil, p.parseFailure(&p.tokens[p.current], fmt.Sprintf("could not parse query: expected either testExpr [err: %s] or comparisonExpr: [err: %s]", testErr.Error(), comparisonErr.Error())) + //basic-expr = paren-expr / + // comparison-expr / + // test-expr + + switch p.tokens[p.current].Token { + case token.NOT: + p.current++ + expr, err := p.parseLogicalOrExpr() + if err != nil { + return nil, err + } + // Inspect if the expr is topped by a parenExpr -- if so we can simplify + if len(expr.expressions) == 1 && len(expr.expressions[0].expressions) == 1 && expr.expressions[0].expressions[0].parenExpr != nil { + child := expr.expressions[0].expressions[0].parenExpr + child.not = !child.not + return &basicExpr{parenExpr: child}, nil + } + return &basicExpr{parenExpr: &parenExpr{not: true, expr: expr}}, nil + case token.PAREN_LEFT: + p.current++ + expr, err := p.parseLogicalOrExpr() + if err != nil { + return nil, err + } + if p.tokens[p.current].Token != token.PAREN_RIGHT { + return nil, p.parseFailure(&p.tokens[p.current], "expected ')'") + } + p.current++ + return &basicExpr{parenExpr: &parenExpr{not: false, expr: expr}}, nil + } + prevCurrent := p.current + comparisonExpr, comparisonErr := p.parseComparisonExpr() + if comparisonErr == nil { + return &basicExpr{comparisonExpr: comparisonExpr}, nil + } + p.current = prevCurrent + testExpr, testErr := p.parseTestExpr() + if testErr == nil { + return &basicExpr{testExpr: testExpr}, nil + } + p.current = prevCurrent + return nil, p.parseFailure(&p.tokens[p.current], fmt.Sprintf("could not parse query: expected either testExpr [err: %s] or comparisonExpr: [err: %s]", testErr.Error(), comparisonErr.Error())) } func (p *JSONPath) parseComparisonExpr() (*comparisonExpr, error) { - left, err := p.parseComparable() - if err != nil { - return nil, err - } - - if !p.isComparisonOperator(p.tokens[p.current].Token) { - return nil, p.parseFailure(&p.tokens[p.current], "expected comparison operator") - } - operator := p.tokens[p.current].Token - var op comparisonOperator - switch operator { - case token.EQ: - op = equalTo - case token.NE: - op = notEqualTo - case token.LT: - op = lessThan - case token.LE: - op = lessThanEqualTo - case token.GT: - op = greaterThan - case token.GE: - op = greaterThanEqualTo - default: - return nil, p.parseFailure(&p.tokens[p.current], "expected comparison operator") - } - p.current++ - - right, err := p.parseComparable() - if err != nil { - return nil, err - } - - return &comparisonExpr{left: left, op: op, right: right}, nil + left, err := p.parseComparable() + if err != nil { + return nil, err + } + + if !p.isComparisonOperator(p.tokens[p.current].Token) { + return nil, p.parseFailure(&p.tokens[p.current], "expected comparison operator") + } + operator := p.tokens[p.current].Token + var op comparisonOperator + switch operator { + case token.EQ: + op = equalTo + case token.NE: + op = notEqualTo + case token.LT: + op = lessThan + case token.LE: + op = lessThanEqualTo + case token.GT: + op = greaterThan + case token.GE: + op = greaterThanEqualTo + default: + return nil, p.parseFailure(&p.tokens[p.current], "expected comparison operator") + } + p.current++ + + right, err := p.parseComparable() + if err != nil { + return nil, err + } + + return &comparisonExpr{left: left, op: op, right: right}, nil } func (p *JSONPath) parseComparable() (*comparable, error) { - // comparable = literal / - // singular-query / ; singular query value - // function-expr ; ValueType - // context-variable ; JSONPath Plus extension - if literal, err := p.parseLiteral(); err == nil { - return &comparable{literal: literal}, nil - } - if funcExpr, err := p.parseFunctionExpr(); err == nil { - if funcExpr.funcType == functionTypeMatch { - return nil, p.parseFailure(&p.tokens[p.current], "match result cannot be compared") - } else if funcExpr.funcType == functionTypeSearch { - return nil, p.parseFailure(&p.tokens[p.current], "search result cannot be compared") - } - return &comparable{functionExpr: funcExpr}, nil - } - switch p.tokens[p.current].Token { - case token.ROOT: - p.current++ - query, err := p.parseSingleQuery() - if err != nil { - return nil, err - } - return &comparable{singularQuery: &singularQuery{absQuery: &absQuery{segments: query.segments}}}, nil - case token.CURRENT: - p.current++ - query, err := p.parseSingleQuery() - if err != nil { - return nil, err - } - return &comparable{singularQuery: &singularQuery{relQuery: &relQuery{segments: query.segments}}}, nil - - case token.CONTEXT_ROOT: - // @root followed by a path - parse as a query starting from root - p.current++ - query, err := p.parseSingleQuery() - if err != nil { - return nil, err - } - return &comparable{singularQuery: &singularQuery{absQuery: &absQuery{segments: query.segments}}}, nil - - default: - // Check for JSONPath Plus context variables - if varKind, ok := contextVarTokenMap[p.tokens[p.current].Token]; ok { - p.current++ - return &comparable{contextVar: &contextVariable{kind: varKind}}, nil - } - return nil, p.parseFailure(&p.tokens[p.current], "expected literal or query") - } + // comparable = literal / + // singular-query / ; singular query value + // function-expr ; ValueType + // context-variable ; JSONPath Plus extension + if literal, err := p.parseLiteral(); err == nil { + return &comparable{literal: literal}, nil + } + if funcExpr, err := p.parseFunctionExpr(); err == nil { + if funcExpr.funcType == functionTypeMatch { + return nil, p.parseFailure(&p.tokens[p.current], "match result cannot be compared") + } else if funcExpr.funcType == functionTypeSearch { + return nil, p.parseFailure(&p.tokens[p.current], "search result cannot be compared") + } + return &comparable{functionExpr: funcExpr}, nil + } + switch p.tokens[p.current].Token { + case token.ROOT: + p.current++ + query, err := p.parseSingleQuery() + if err != nil { + return nil, err + } + return &comparable{singularQuery: &singularQuery{absQuery: &absQuery{segments: query.segments}}}, nil + case token.CURRENT: + p.current++ + query, err := p.parseSingleQuery() + if err != nil { + return nil, err + } + return &comparable{singularQuery: &singularQuery{relQuery: &relQuery{segments: query.segments}}}, nil + + case token.CONTEXT_ROOT: + // @root followed by a path - parse as a query starting from root + p.current++ + query, err := p.parseSingleQuery() + if err != nil { + return nil, err + } + return &comparable{singularQuery: &singularQuery{absQuery: &absQuery{segments: query.segments}}}, nil + + default: + // Check for JSONPath Plus context variables + if varKind, ok := contextVarTokenMap[p.tokens[p.current].Token]; ok { + p.current++ + return &comparable{contextVar: &contextVariable{kind: varKind}}, nil + } + return nil, p.parseFailure(&p.tokens[p.current], "expected literal or query") + } } func (p *JSONPath) parseQuery() (*jsonPathAST, error) { - var query jsonPathAST - p.mode = append(p.mode, modeNormal) - - for p.current < len(p.tokens) { - prior := p.current - segment, err := p.parseSegment() - if err != nil { - p.current = prior - break - } - query.segments = append(query.segments, segment) - } - p.mode = p.mode[:len(p.mode)-1] - return &query, nil + var query jsonPathAST + p.mode = append(p.mode, modeNormal) + + for p.current < len(p.tokens) { + prior := p.current + segment, err := p.parseSegment() + if err != nil { + p.current = prior + break + } + query.segments = append(query.segments, segment) + } + p.mode = p.mode[:len(p.mode)-1] + return &query, nil } func (p *JSONPath) parseTestExpr() (*testExpr, error) { - //test-expr = [logical-not-op S] - // (filter-query / ; existence/non-existence - // function-expr) ; LogicalType or NodesType - //filter-query = rel-query / jsonpath-query - //rel-query = current-node-identifier segments - //current-node-identifier = "@" - not := false - if p.tokens[p.current].Token == token.NOT { - not = true - p.current++ - } - switch p.tokens[p.current].Token { - case token.CURRENT: - p.current++ - query, err := p.parseQuery() - if err != nil { - return nil, err - } - return &testExpr{filterQuery: &filterQuery{relQuery: &relQuery{segments: query.segments}}, not: not}, nil - case token.ROOT: - p.current++ - query, err := p.parseQuery() - if err != nil { - return nil, err - } - return &testExpr{filterQuery: &filterQuery{jsonPathQuery: &jsonPathAST{ - segments: query.segments, - lazyContextTracking: p.config.LazyContextTrackingEnabled(), - }}, not: not}, nil - default: - funcExpr, err := p.parseFunctionExpr() - if err != nil { - return nil, err - } - if funcExpr.funcType == functionTypeCount { - return nil, p.parseFailure(&p.tokens[p.current], "count function must be compared") - } - if funcExpr.funcType == functionTypeLength { - return nil, p.parseFailure(&p.tokens[p.current], "length function must be compared") - } - if funcExpr.funcType == functionTypeValue { - return nil, p.parseFailure(&p.tokens[p.current], "length function must be compared") - } - return &testExpr{functionExpr: funcExpr, not: not}, nil - } - - return nil, p.parseFailure(&p.tokens[p.current], "unexpected token when parsing test expression") + //test-expr = [logical-not-op S] + // (filter-query / ; existence/non-existence + // function-expr) ; LogicalType or NodesType + //filter-query = rel-query / jsonpath-query + //rel-query = current-node-identifier segments + //current-node-identifier = "@" + not := false + if p.tokens[p.current].Token == token.NOT { + not = true + p.current++ + } + switch p.tokens[p.current].Token { + case token.CURRENT: + p.current++ + query, err := p.parseQuery() + if err != nil { + return nil, err + } + return &testExpr{filterQuery: &filterQuery{relQuery: &relQuery{segments: query.segments}}, not: not}, nil + case token.ROOT: + p.current++ + query, err := p.parseQuery() + if err != nil { + return nil, err + } + return &testExpr{filterQuery: &filterQuery{jsonPathQuery: &jsonPathAST{ + segments: query.segments, + lazyContextTracking: p.config.LazyContextTrackingEnabled(), + }}, not: not}, nil + default: + funcExpr, err := p.parseFunctionExpr() + if err != nil { + return nil, err + } + if funcExpr.funcType == functionTypeCount { + return nil, p.parseFailure(&p.tokens[p.current], "count function must be compared") + } + if funcExpr.funcType == functionTypeLength { + return nil, p.parseFailure(&p.tokens[p.current], "length function must be compared") + } + if funcExpr.funcType == functionTypeValue { + return nil, p.parseFailure(&p.tokens[p.current], "length function must be compared") + } + return &testExpr{functionExpr: funcExpr, not: not}, nil + } } func (p *JSONPath) parseFunctionExpr() (*functionExpr, error) { - // RFC 9535: function name must be immediately followed by '(' (no whitespace) - // The tokenizer only emits FUNCTION token when function name is directly followed by '(' - if p.tokens[p.current].Token != token.FUNCTION { - return nil, p.parseFailure(&p.tokens[p.current], "expected function") - } - functionName := p.tokens[p.current].Literal - if p.current+1 >= len(p.tokens) || p.tokens[p.current+1].Token != token.PAREN_LEFT { - return nil, p.parseFailure(&p.tokens[p.current], "expected '(' after function") - } - p.current += 2 - args := []*functionArgument{} - - // Check type selector functions first (JSONPath Plus) - // These take a single argument and return boolean - if funcType, ok := typeSelectorFunctionMap[functionName]; ok { - arg, err := p.parseFunctionArgument(false) - if err != nil { - return nil, err - } - args = append(args, arg) - if p.tokens[p.current].Token != token.PAREN_RIGHT { - return nil, p.parseFailure(&p.tokens[p.current], "expected ')'") - } - p.current++ - return &functionExpr{funcType: funcType, args: args}, nil - } - - switch functionTypeMap[functionName] { - case functionTypeLength: - arg, err := p.parseFunctionArgument(true) - if err != nil { - return nil, err - } - args = append(args, arg) - case functionTypeCount: - arg, err := p.parseFunctionArgument(false) - if err != nil { - return nil, err - } - if arg.literal != nil && arg.literal.node == nil { - return nil, p.parseFailure(&p.tokens[p.current], "count function only supports containers") - } - args = append(args, arg) - case functionTypeValue: - arg, err := p.parseFunctionArgument(false) - if err != nil { - return nil, err - } - args = append(args, arg) - case functionTypeMatch: - fallthrough - case functionTypeSearch: - arg, err := p.parseFunctionArgument(false) - if err != nil { - return nil, err - } - args = append(args, arg) - if p.tokens[p.current].Token != token.COMMA { - return nil, p.parseFailure(&p.tokens[p.current], "expected ','") - } - p.current++ - arg, err = p.parseFunctionArgument(false) - if err != nil { - return nil, err - } - args = append(args, arg) - default: - return nil, p.parseFailure(&p.tokens[p.current], "unknown function: "+functionName) - } - if p.tokens[p.current].Token != token.PAREN_RIGHT { - return nil, p.parseFailure(&p.tokens[p.current], "expected ')'") - } - p.current++ - return &functionExpr{funcType: functionTypeMap[functionName], args: args}, nil + // RFC 9535: function name must be immediately followed by '(' (no whitespace) + // The tokenizer only emits FUNCTION token when function name is directly followed by '(' + if p.tokens[p.current].Token != token.FUNCTION { + return nil, p.parseFailure(&p.tokens[p.current], "expected function") + } + functionName := p.tokens[p.current].Literal + if p.current+1 >= len(p.tokens) || p.tokens[p.current+1].Token != token.PAREN_LEFT { + return nil, p.parseFailure(&p.tokens[p.current], "expected '(' after function") + } + p.current += 2 + args := []*functionArgument{} + + // Check type selector functions first (JSONPath Plus) + // These take a single argument and return boolean + if funcType, ok := typeSelectorFunctionMap[functionName]; ok { + arg, err := p.parseFunctionArgument(false) + if err != nil { + return nil, err + } + args = append(args, arg) + if p.tokens[p.current].Token != token.PAREN_RIGHT { + return nil, p.parseFailure(&p.tokens[p.current], "expected ')'") + } + p.current++ + return &functionExpr{funcType: funcType, args: args}, nil + } + + switch functionTypeMap[functionName] { + case functionTypeLength: + arg, err := p.parseFunctionArgument(true) + if err != nil { + return nil, err + } + args = append(args, arg) + case functionTypeCount: + arg, err := p.parseFunctionArgument(false) + if err != nil { + return nil, err + } + if arg.literal != nil && arg.literal.node == nil { + return nil, p.parseFailure(&p.tokens[p.current], "count function only supports containers") + } + args = append(args, arg) + case functionTypeValue: + arg, err := p.parseFunctionArgument(false) + if err != nil { + return nil, err + } + args = append(args, arg) + case functionTypeMatch: + fallthrough + case functionTypeSearch: + arg, err := p.parseFunctionArgument(false) + if err != nil { + return nil, err + } + args = append(args, arg) + if p.tokens[p.current].Token != token.COMMA { + return nil, p.parseFailure(&p.tokens[p.current], "expected ','") + } + p.current++ + arg, err = p.parseFunctionArgument(false) + if err != nil { + return nil, err + } + args = append(args, arg) + default: + return nil, p.parseFailure(&p.tokens[p.current], "unknown function: "+functionName) + } + if p.tokens[p.current].Token != token.PAREN_RIGHT { + return nil, p.parseFailure(&p.tokens[p.current], "expected ')'") + } + p.current++ + return &functionExpr{funcType: functionTypeMap[functionName], args: args}, nil } func (p *JSONPath) parseSingleQuery() (*jsonPathAST, error) { - var query jsonPathAST - for p.current < len(p.tokens) { - try := p.current - p.mode = append(p.mode, modeSingular) - segment, err := p.parseSegment() - if err != nil { - // rollback - p.mode = p.mode[:len(p.mode)-1] - p.current = try - break - } - p.mode = p.mode[:len(p.mode)-1] - query.segments = append(query.segments, segment) - } - //if len(query.segments) == 0 { - // return nil, p.parseFailure(p.tokens[p.current], "expected at least one segment") - //} - return &query, nil + var query jsonPathAST + for p.current < len(p.tokens) { + try := p.current + p.mode = append(p.mode, modeSingular) + segment, err := p.parseSegment() + if err != nil { + // rollback + p.mode = p.mode[:len(p.mode)-1] + p.current = try + break + } + p.mode = p.mode[:len(p.mode)-1] + query.segments = append(query.segments, segment) + } + //if len(query.segments) == 0 { + // return nil, p.parseFailure(p.tokens[p.current], "expected at least one segment") + //} + return &query, nil } func (p *JSONPath) parseFunctionArgument(single bool) (*functionArgument, error) { - //function-argument = literal / - // filter-query / ; (includes singular-query) - // logical-expr / - // function-expr - - if lit, err := p.parseLiteral(); err == nil { - return &functionArgument{literal: lit}, nil - } - switch p.tokens[p.current].Token { - case token.CURRENT: - p.current++ - var query *jsonPathAST - var err error - if single { - query, err = p.parseSingleQuery() - } else { - query, err = p.parseQuery() - } - if err != nil { - return nil, err - } - return &functionArgument{filterQuery: &filterQuery{relQuery: &relQuery{segments: query.segments}}}, nil - case token.ROOT: - p.current++ - var query *jsonPathAST - var err error - if single { - query, err = p.parseSingleQuery() - } else { - query, err = p.parseQuery() - } - if err != nil { - return nil, err - } - return &functionArgument{filterQuery: &filterQuery{jsonPathQuery: &jsonPathAST{ - segments: query.segments, - lazyContextTracking: p.config.LazyContextTrackingEnabled(), - }}}, nil - } - - // Check for JSONPath Plus context variables as function arguments - if varKind, ok := contextVarTokenMap[p.tokens[p.current].Token]; ok { - p.current++ - return &functionArgument{contextVar: &contextVariable{kind: varKind}}, nil - } - - if expr, err := p.parseLogicalOrExpr(); err == nil { - return &functionArgument{logicalExpr: expr}, nil - } - if funcExpr, err := p.parseFunctionExpr(); err == nil { - return &functionArgument{functionExpr: funcExpr}, nil - } - - return nil, p.parseFailure(&p.tokens[p.current], "unexpected token for function argument") + //function-argument = literal / + // filter-query / ; (includes singular-query) + // logical-expr / + // function-expr + + if lit, err := p.parseLiteral(); err == nil { + return &functionArgument{literal: lit}, nil + } + switch p.tokens[p.current].Token { + case token.CURRENT: + p.current++ + var query *jsonPathAST + var err error + if single { + query, err = p.parseSingleQuery() + } else { + query, err = p.parseQuery() + } + if err != nil { + return nil, err + } + return &functionArgument{filterQuery: &filterQuery{relQuery: &relQuery{segments: query.segments}}}, nil + case token.ROOT: + p.current++ + var query *jsonPathAST + var err error + if single { + query, err = p.parseSingleQuery() + } else { + query, err = p.parseQuery() + } + if err != nil { + return nil, err + } + return &functionArgument{filterQuery: &filterQuery{jsonPathQuery: &jsonPathAST{ + segments: query.segments, + lazyContextTracking: p.config.LazyContextTrackingEnabled(), + }}}, nil + } + + // Check for JSONPath Plus context variables as function arguments + if varKind, ok := contextVarTokenMap[p.tokens[p.current].Token]; ok { + p.current++ + return &functionArgument{contextVar: &contextVariable{kind: varKind}}, nil + } + + if expr, err := p.parseLogicalOrExpr(); err == nil { + return &functionArgument{logicalExpr: expr}, nil + } + if funcExpr, err := p.parseFunctionExpr(); err == nil { + return &functionArgument{functionExpr: funcExpr}, nil + } + + return nil, p.parseFailure(&p.tokens[p.current], "unexpected token for function argument") } func (p *JSONPath) parseLiteral() (*literal, error) { - switch p.tokens[p.current].Token { - case token.STRING_LITERAL: - lit := p.tokens[p.current].Literal - p.current++ - return &literal{string: &lit}, nil - case token.INTEGER: - lit := p.tokens[p.current].Literal - p.current++ - i, err := strconv.Atoi(lit) - if err != nil { - return nil, p.parseFailure(&p.tokens[p.current], "expected integer") - } - return &literal{integer: &i}, nil - case token.FLOAT: - lit := p.tokens[p.current].Literal - p.current++ - f, err := strconv.ParseFloat(lit, 64) - if err != nil { - return nil, p.parseFailure(&p.tokens[p.current], "expected float") - } - return &literal{float64: &f}, nil - case token.TRUE: - p.current++ - res := true - return &literal{bool: &res}, nil - case token.FALSE: - p.current++ - res := false - return &literal{bool: &res}, nil - case token.NULL: - p.current++ - res := true - return &literal{null: &res}, nil - } - return nil, p.parseFailure(&p.tokens[p.current], "expected literal") + switch p.tokens[p.current].Token { + case token.STRING_LITERAL: + lit := p.tokens[p.current].Literal + p.current++ + return &literal{string: &lit}, nil + case token.INTEGER: + lit := p.tokens[p.current].Literal + p.current++ + i, err := strconv.Atoi(lit) + if err != nil { + return nil, p.parseFailure(&p.tokens[p.current], "expected integer") + } + return &literal{integer: &i}, nil + case token.FLOAT: + lit := p.tokens[p.current].Literal + p.current++ + f, err := strconv.ParseFloat(lit, 64) + if err != nil { + return nil, p.parseFailure(&p.tokens[p.current], "expected float") + } + return &literal{float64: &f}, nil + case token.TRUE: + p.current++ + res := true + return &literal{bool: &res}, nil + case token.FALSE: + p.current++ + res := false + return &literal{bool: &res}, nil + case token.NULL: + p.current++ + res := true + return &literal{null: &res}, nil + } + return nil, p.parseFailure(&p.tokens[p.current], "expected literal") } type jsonPathAST struct { - // "$" - segments []*segment - lazyContextTracking bool - jsonPathPlus bool // JSONPath Plus extensions enabled (unquoted brackets, mapping index fallback) + // "$" + segments []*segment + lazyContextTracking bool + jsonPathPlus bool // JSONPath Plus extensions enabled (unquoted brackets, mapping index fallback) } func (q jsonPathAST) ToString() string { - b := strings.Builder{} - b.WriteString("$") - for _, seg := range q.segments { - b.WriteString(seg.ToString()) - } - return b.String() + b := strings.Builder{} + b.WriteString("$") + for _, seg := range q.segments { + b.WriteString(seg.ToString()) + } + return b.String() } diff --git a/vendor/github.com/pb33f/jsonpath/pkg/jsonpath/segment.go b/vendor/github.com/pb33f/jsonpath/pkg/jsonpath/segment.go index c698b21e6..e4e9ec319 100644 --- a/vendor/github.com/pb33f/jsonpath/pkg/jsonpath/segment.go +++ b/vendor/github.com/pb33f/jsonpath/pkg/jsonpath/segment.go @@ -1,88 +1,188 @@ package jsonpath import ( - "go.yaml.in/yaml/v4" - "strings" + "fmt" + "go.yaml.in/yaml/v4" + "strings" ) type segmentKind int const ( - segmentKindChild segmentKind = iota // . - segmentKindDescendant // .. - segmentKindProperyName // ~ (extension only) - segmentKindParent // ^ (JSONPath Plus parent selector) + segmentKindChild segmentKind = iota // . + segmentKindDescendant // .. + segmentKindProperyName // ~ (extension only) + segmentKindParent // ^ (JSONPath Plus parent selector) + segmentKindRecursivePropertyName // .~ (Spectral/JSONPath Plus recursive property names) ) type segment struct { - kind segmentKind - child *innerSegment - descendant *innerSegment + kind segmentKind + child *innerSegment + descendant *innerSegment } type segmentSubKind int const ( - segmentDotWildcard segmentSubKind = iota // .* - segmentDotMemberName // .property - segmentLongHand // [ selector[] ] + segmentDotWildcard segmentSubKind = iota // .* + segmentDotMemberName // .property + segmentLongHand // [ selector[] ] ) func (s segment) ToString() string { - switch s.kind { - case segmentKindChild: - if s.child.kind != segmentLongHand { - return "." + s.child.ToString() - } else { - return s.child.ToString() - } - case segmentKindDescendant: - return ".." + s.descendant.ToString() - case segmentKindProperyName: - return "~" - case segmentKindParent: - return "^" - } - panic("unknown segment kind") + switch s.kind { + case segmentKindChild: + if s.child.kind != segmentLongHand { + return "." + s.child.ToString() + } else { + return s.child.ToString() + } + case segmentKindDescendant: + return ".." + s.descendant.ToString() + case segmentKindProperyName: + return "~" + case segmentKindParent: + return "^" + case segmentKindRecursivePropertyName: + return ".~" + } + panic("unknown segment kind") } type innerSegment struct { - kind segmentSubKind - dotName string - selectors []*selector + kind segmentSubKind + dotName string + selectors []*selector } func (s innerSegment) ToString() string { - builder := strings.Builder{} - switch s.kind { - case segmentDotWildcard: - builder.WriteString("*") - break - case segmentDotMemberName: - builder.WriteString(s.dotName) - break - case segmentLongHand: - builder.WriteString("[") - for i, selector := range s.selectors { - builder.WriteString(selector.ToString()) - if i < len(s.selectors)-1 { - builder.WriteString(", ") - } - } - builder.WriteString("]") - break - default: - panic("unknown child segment kind") - } - return builder.String() + builder := strings.Builder{} + switch s.kind { + case segmentDotWildcard: + builder.WriteString("*") + break + case segmentDotMemberName: + builder.WriteString(s.dotName) + break + case segmentLongHand: + builder.WriteString("[") + for i, selector := range s.selectors { + builder.WriteString(selector.ToString()) + if i < len(s.selectors)-1 { + builder.WriteString(", ") + } + } + builder.WriteString("]") + break + default: + panic("unknown child segment kind") + } + return builder.String() } func descendApply(value *yaml.Node, apply func(*yaml.Node)) { - if value == nil { - return - } - apply(value) - for _, child := range value.Content { - descendApply(child, apply) - } + if value == nil { + return + } + apply(value) + for _, child := range value.Content { + descendApply(child, apply) + } +} + +func (s segment) IsSingular() bool { + switch s.kind { + case segmentKindDescendant, segmentKindRecursivePropertyName: + return false + case segmentKindParent: + return true + case segmentKindProperyName: + return false + case segmentKindChild: + if s.child == nil { + return false + } + return s.child.IsSingular() + default: + return false + } +} + +func (s innerSegment) IsSingular() bool { + switch s.kind { + case segmentDotWildcard: + return false + case segmentDotMemberName: + return true + case segmentLongHand: + if len(s.selectors) != 1 { + return false + } + return s.selectors[0].IsSingular() + default: + return false + } +} + +func (s selector) IsSingular() bool { + switch s.kind { + case selectorSubKindName: + return true + case selectorSubKindArrayIndex: + return true + case selectorSubKindWildcard: + return false + case selectorSubKindArraySlice: + return false + case selectorSubKindFilter: + return false + default: + return false + } +} + +func (s segment) getSegmentInfo() ([]SegmentInfo, error) { + switch s.kind { + case segmentKindChild: + if s.child == nil { + return nil, fmt.Errorf("nil child segment") + } + return s.child.getSegmentInfo() + case segmentKindDescendant: + return nil, fmt.Errorf("recursive descent not supported for upsert") + case segmentKindParent, segmentKindProperyName, segmentKindRecursivePropertyName: + return nil, fmt.Errorf("parent/property selectors not supported for upsert") + default: + return nil, fmt.Errorf("unknown segment kind") + } +} + +func (s innerSegment) getSegmentInfo() ([]SegmentInfo, error) { + switch s.kind { + case segmentDotMemberName: + return []SegmentInfo{{Kind: SegmentKindMemberName, Key: s.dotName}}, nil + case segmentLongHand: + if len(s.selectors) != 1 { + return nil, fmt.Errorf("multiple selectors not supported for upsert") + } + return s.selectors[0].getSegmentInfo() + case segmentDotWildcard: + return nil, fmt.Errorf("wildcard not supported for upsert") + default: + return nil, fmt.Errorf("unknown inner segment kind") + } +} + +func (s selector) getSegmentInfo() ([]SegmentInfo, error) { + switch s.kind { + case selectorSubKindName: + return []SegmentInfo{{Kind: SegmentKindMemberName, Key: s.name}}, nil + case selectorSubKindArrayIndex: + return []SegmentInfo{{Kind: SegmentKindArrayIndex, Index: s.index, HasIndex: true}}, nil + case selectorSubKindWildcard, selectorSubKindArraySlice, selectorSubKindFilter: + return nil, fmt.Errorf("%v selector not supported for upsert", s.kind) + default: + return nil, fmt.Errorf("unknown selector kind") + } } diff --git a/vendor/github.com/pb33f/jsonpath/pkg/jsonpath/selector.go b/vendor/github.com/pb33f/jsonpath/pkg/jsonpath/selector.go index 9f5ea06fb..b06cb2e17 100644 --- a/vendor/github.com/pb33f/jsonpath/pkg/jsonpath/selector.go +++ b/vendor/github.com/pb33f/jsonpath/pkg/jsonpath/selector.go @@ -27,8 +27,9 @@ type selector struct { name string index int64 slice *slice - filter *filterSelector + filter filterSelector jsonPathPlus bool // when true, enables MappingNode fallback for array index selectors + spectral bool // when true, enables Spectral-only context typing for array index selectors } func (s selector) ToString() string { @@ -60,5 +61,4 @@ func (s selector) ToString() string { default: panic(fmt.Sprintf("unimplemented selector kind: %v", s.kind)) } - return "" } diff --git a/vendor/github.com/pb33f/jsonpath/pkg/jsonpath/spectral_eval.go b/vendor/github.com/pb33f/jsonpath/pkg/jsonpath/spectral_eval.go new file mode 100644 index 000000000..0abaadd58 --- /dev/null +++ b/vendor/github.com/pb33f/jsonpath/pkg/jsonpath/spectral_eval.go @@ -0,0 +1,529 @@ +package jsonpath + +import ( + "math" + "strconv" + "unicode/utf16" + + "github.com/pb33f/jsonpath/pkg/jsonpath/token" + "go.yaml.in/yaml/v4" +) + +type spectralRuntimeKind uint8 + +const ( + spectralMissing spectralRuntimeKind = iota + spectralInvalid + spectralNull + spectralBoolean + spectralNumber + spectralString + spectralNode + spectralRegexValue +) + +type spectralRuntimeValue struct { + kind spectralRuntimeKind + boolean bool + number float64 + string string + node *yaml.Node + regex *spectralRegex + propertyMethodCoercion bool +} + +type spectralEvalContext struct { + index index + current *yaml.Node + root *yaml.Node +} + +func (e *spectralBoolExpr) Matches(idx index, node *yaml.Node, root *yaml.Node) bool { + result, valid := e.evaluate(spectralEvalContext{index: idx, current: node, root: unwrapDocument(root)}) + return valid && result +} + +func (e *spectralBoolExpr) evaluate(ctx spectralEvalContext) (bool, bool) { + if e == nil { + return false, false + } + switch e.kind { + case spectralBoolValue: + value := e.value.evaluate(ctx) + return value.truthy(), value.kind != spectralInvalid + case spectralBoolNot: + result, valid := e.left.evaluate(ctx) + return !result, valid + case spectralBoolAnd: + left, valid := e.left.evaluate(ctx) + if !valid || !left { + return left, valid + } + return e.right.evaluate(ctx) + case spectralBoolOr: + left, valid := e.left.evaluate(ctx) + if !valid || left { + return left, valid + } + return e.right.evaluate(ctx) + case spectralBoolCompare: + left := e.left.value.evaluate(ctx) + right := e.right.value.evaluate(ctx) + if left.kind == spectralInvalid || right.kind == spectralInvalid { + return false, false + } + return spectralCompare(left, right, e.op), true + case spectralBoolParen: + return e.left.evaluate(ctx) + default: + return false, false + } +} + +func (e *spectralValueExpr) evaluate(ctx spectralEvalContext) spectralRuntimeValue { + if e == nil { + return spectralRuntimeValue{} + } + var value spectralRuntimeValue + switch e.kind { + case spectralValueLiteral: + value = spectralRuntimeFromLiteral(e.literal) + case spectralValueCurrent: + value = spectralRuntimeValue{kind: spectralNode, node: ctx.current} + case spectralValueRoot: + value = spectralRuntimeValue{kind: spectralNode, node: ctx.root} + case spectralValueContext: + value = spectralContextValue(e.context, ctx) + case spectralValueRegex: + value = spectralRuntimeValue{kind: spectralRegexValue, regex: e.regex} + case spectralValueUndefined: + value = spectralRuntimeValue{} + case spectralValueFunction: + value = spectralRuntimeFromLiteral(e.function.Evaluate(ctx.index, ctx.current, ctx.root)) + } + + if len(e.segments) > 0 { + if value.kind != spectralNode || value.node == nil { + return spectralRuntimeValue{} + } + resolved := value.node + for _, segment := range e.segments { + resolved = spectralResolveSegment(resolved, segment) + if resolved == nil { + return spectralRuntimeValue{} + } + } + value = spectralRuntimeFromNode(resolved) + } else if value.kind == spectralNode { + value = spectralRuntimeFromNode(value.node) + } + + for _, postfix := range e.postfix { + value = spectralApplyPostfix(value, postfix) + if value.kind == spectralMissing { + break + } + } + return value +} + +func spectralRuntimeFromLiteral(value literal) spectralRuntimeValue { + switch { + case value.integer != nil: + return spectralRuntimeValue{kind: spectralNumber, number: float64(*value.integer)} + case value.float64 != nil: + return spectralRuntimeValue{kind: spectralNumber, number: *value.float64} + case value.string != nil: + return spectralRuntimeValue{kind: spectralString, string: *value.string} + case value.bool != nil: + return spectralRuntimeValue{kind: spectralBoolean, boolean: *value.bool} + case value.null != nil: + return spectralRuntimeValue{kind: spectralNull} + case value.node != nil: + return spectralRuntimeFromNode(value.node) + default: + return spectralRuntimeValue{} + } +} + +func spectralRuntimeFromNode(node *yaml.Node) spectralRuntimeValue { + node = unwrapDocument(node) + if node == nil { + return spectralRuntimeValue{} + } + if node.Kind == yaml.SequenceNode || node.Kind == yaml.MappingNode { + return spectralRuntimeValue{kind: spectralNode, node: node} + } + switch node.Tag { + case "!!null": + return spectralRuntimeValue{kind: spectralNull} + case "!!bool": + value, err := strconv.ParseBool(node.Value) + if err != nil { + return spectralRuntimeValue{} + } + return spectralRuntimeValue{kind: spectralBoolean, boolean: value} + case "!!int": + value, err := strconv.ParseFloat(node.Value, 64) + if err != nil { + return spectralRuntimeValue{} + } + return spectralRuntimeValue{kind: spectralNumber, number: value} + case "!!float": + value, err := strconv.ParseFloat(node.Value, 64) + if err != nil { + return spectralRuntimeValue{} + } + return spectralRuntimeValue{kind: spectralNumber, number: value} + case "!!str": + return spectralRuntimeValue{kind: spectralString, string: node.Value} + default: + if node.Kind == yaml.ScalarNode { + return spectralRuntimeValue{kind: spectralString, string: node.Value} + } + return spectralRuntimeValue{kind: spectralNode, node: node} + } +} + +func spectralContextValue(kind contextVarKind, ctx spectralEvalContext) spectralRuntimeValue { + filterCtx, ok := ctx.index.(FilterContext) + if !ok { + return spectralRuntimeValue{} + } + switch kind { + case contextVarProperty: + if filterCtx.Index() >= 0 { + return spectralRuntimeValue{kind: spectralNumber, number: float64(filterCtx.Index()), propertyMethodCoercion: true} + } + return spectralRuntimeValue{kind: spectralString, string: filterCtx.PropertyName()} + case contextVarRoot: + return spectralRuntimeFromNode(ctx.root) + case contextVarParent: + return spectralRuntimeFromNode(filterCtx.Parent()) + case contextVarParentProperty: + parent := filterCtx.Parent() + if parent != nil { + if parentContainer := ctx.index.getParentNode(parent); parentContainer != nil && parentContainer.Kind == yaml.SequenceNode { + if parentIndex, err := strconv.Atoi(filterCtx.ParentPropertyName()); err == nil { + return spectralRuntimeValue{kind: spectralNumber, number: float64(parentIndex), propertyMethodCoercion: true} + } + } + } + return spectralRuntimeValue{kind: spectralString, string: filterCtx.ParentPropertyName()} + case contextVarPath: + return spectralRuntimeValue{kind: spectralString, string: filterCtx.Path()} + case contextVarIndex: + return spectralRuntimeValue{kind: spectralNumber, number: float64(filterCtx.Index())} + default: + return spectralRuntimeValue{} + } +} + +func spectralResolveSegment(node *yaml.Node, segment spectralPathSegment) *yaml.Node { + node = unwrapDocument(node) + if node == nil { + return nil + } + if segment.index != nil { + if node.Kind != yaml.SequenceNode { + return nil + } + index := *segment.index + if index < 0 { + index += int64(len(node.Content)) + } + if index < 0 || index >= int64(len(node.Content)) { + return nil + } + return node.Content[index] + } + if node.Kind != yaml.MappingNode { + return nil + } + for i := 0; i+1 < len(node.Content); i += 2 { + if node.Content[i].Value == segment.name { + return node.Content[i+1] + } + } + return nil +} + +func spectralApplyPostfix(value spectralRuntimeValue, postfix spectralPostfix) spectralRuntimeValue { + if value.kind == spectralNumber && value.propertyMethodCoercion && + (postfix.kind == spectralPostfixMatch || postfix.kind == spectralPostfixIndexOf || postfix.kind == spectralPostfixIncludes) { + value = spectralRuntimeValue{kind: spectralString, string: strconv.FormatFloat(value.number, 'f', -1, 64)} + } + switch postfix.kind { + case spectralPostfixMatch: + if value.kind != spectralString { + return spectralRuntimeValue{kind: spectralInvalid} + } + return spectralRuntimeValue{kind: spectralBoolean, boolean: postfix.regex.compiled.MatchString(value.string)} + case spectralPostfixIndexOf: + if value.kind != spectralString { + return spectralRuntimeValue{kind: spectralInvalid} + } + from := 0 + if postfix.fromIndex != nil { + from = int(math.Trunc(*postfix.fromIndex)) + } + return spectralRuntimeValue{kind: spectralNumber, number: float64(spectralUTF16IndexOf(value.string, postfix.search, from))} + case spectralPostfixLength: + switch value.kind { + case spectralString: + return spectralRuntimeValue{kind: spectralNumber, number: float64(len(utf16.Encode([]rune(value.string))))} + case spectralNode: + if value.node != nil && value.node.Kind == yaml.SequenceNode { + return spectralRuntimeValue{kind: spectralNumber, number: float64(len(value.node.Content))} + } + } + return spectralRuntimeValue{kind: spectralInvalid} + case spectralPostfixConstructorName: + name := "" + switch value.kind { + case spectralString: + name = "String" + case spectralNumber: + name = "Number" + case spectralBoolean: + name = "Boolean" + case spectralNode: + if value.node != nil && value.node.Kind == yaml.SequenceNode { + name = "Array" + } else if value.node != nil && value.node.Kind == yaml.MappingNode { + name = "Object" + } + } + if name == "" { + return spectralRuntimeValue{kind: spectralInvalid} + } + return spectralRuntimeValue{kind: spectralString, string: name} + case spectralPostfixIncludes: + argument := spectralRuntimeFromLiteral(postfix.argument) + from := 0 + if postfix.fromIndex != nil { + from = int(math.Trunc(*postfix.fromIndex)) + } + switch value.kind { + case spectralString: + if argument.kind != spectralString { + return spectralRuntimeValue{kind: spectralInvalid} + } + if from < 0 { + from = 0 + } + return spectralRuntimeValue{kind: spectralBoolean, boolean: spectralUTF16IndexOf(value.string, argument.string, from) >= 0} + case spectralNode: + if value.node == nil || value.node.Kind != yaml.SequenceNode { + return spectralRuntimeValue{kind: spectralInvalid} + } + if from < 0 { + from += len(value.node.Content) + if from < 0 { + from = 0 + } + } + if from > len(value.node.Content) { + return spectralRuntimeValue{kind: spectralBoolean, boolean: false} + } + for _, item := range value.node.Content[from:] { + if spectralStrictEqual(spectralRuntimeFromNode(item), argument) { + return spectralRuntimeValue{kind: spectralBoolean, boolean: true} + } + } + return spectralRuntimeValue{kind: spectralBoolean, boolean: false} + default: + return spectralRuntimeValue{kind: spectralInvalid} + } + default: + return spectralRuntimeValue{kind: spectralInvalid} + } +} + +func spectralUTF16IndexOf(value, search string, from int) int { + valueUnits := utf16.Encode([]rune(value)) + searchUnits := utf16.Encode([]rune(search)) + if from < 0 { + from = 0 + } + if from > len(valueUnits) { + from = len(valueUnits) + } + if len(searchUnits) == 0 { + return from + } + for i := from; i+len(searchUnits) <= len(valueUnits); i++ { + matched := true + for j := range searchUnits { + if valueUnits[i+j] != searchUnits[j] { + matched = false + break + } + } + if matched { + return i + } + } + return -1 +} + +func (value spectralRuntimeValue) truthy() bool { + switch value.kind { + case spectralMissing, spectralInvalid, spectralNull: + return false + case spectralBoolean: + return value.boolean + case spectralNumber: + return value.number != 0 && !math.IsNaN(value.number) + case spectralString: + return value.string != "" + case spectralNode, spectralRegexValue: + return true + default: + return false + } +} + +func spectralCompare(left, right spectralRuntimeValue, operator token.Token) bool { + switch operator { + case token.STRICT_EQ: + return spectralStrictEqual(left, right) + case token.STRICT_NE: + return !spectralStrictEqual(left, right) + case token.EQ: + return spectralLooseEqual(left, right) + case token.NE: + return !spectralLooseEqual(left, right) + case token.LT, token.LE, token.GT, token.GE: + comparison, ok := spectralRelationalCompare(left, right) + if !ok { + return false + } + switch operator { + case token.LT: + return comparison < 0 + case token.LE: + return comparison <= 0 + case token.GT: + return comparison > 0 + case token.GE: + return comparison >= 0 + } + } + return false +} + +func spectralStrictEqual(left, right spectralRuntimeValue) bool { + if left.kind == spectralInvalid || right.kind == spectralInvalid { + return false + } + if left.kind != right.kind { + return false + } + switch left.kind { + case spectralMissing, spectralNull: + return true + case spectralBoolean: + return left.boolean == right.boolean + case spectralNumber: + return left.number == right.number + case spectralString: + return left.string == right.string + case spectralNode: + return left.node == right.node + case spectralRegexValue: + return left.regex == right.regex + default: + return false + } +} + +func spectralLooseEqual(left, right spectralRuntimeValue) bool { + if spectralStrictEqual(left, right) { + return true + } + if (left.kind == spectralNull && right.kind == spectralMissing) || (left.kind == spectralMissing && right.kind == spectralNull) { + return true + } + if left.kind == spectralBoolean { + left = spectralRuntimeValue{kind: spectralNumber, number: spectralBooleanNumber(left.boolean)} + } + if right.kind == spectralBoolean { + right = spectralRuntimeValue{kind: spectralNumber, number: spectralBooleanNumber(right.boolean)} + } + if spectralStrictEqual(left, right) { + return true + } + if left.kind == spectralString && right.kind == spectralNumber { + number, ok := spectralStringNumber(left.string) + return ok && number == right.number + } + if left.kind == spectralNumber && right.kind == spectralString { + number, ok := spectralStringNumber(right.string) + return ok && left.number == number + } + return false +} + +func spectralBooleanNumber(value bool) float64 { + if value { + return 1 + } + return 0 +} + +func spectralStringNumber(value string) (float64, bool) { + if value == "" { + return 0, true + } + number, err := strconv.ParseFloat(value, 64) + return number, err == nil +} + +func spectralRelationalCompare(left, right spectralRuntimeValue) (int, bool) { + if left.kind == spectralString && right.kind == spectralString { + switch { + case left.string < right.string: + return -1, true + case left.string > right.string: + return 1, true + default: + return 0, true + } + } + leftNumber, leftOK := spectralComparableNumber(left) + rightNumber, rightOK := spectralComparableNumber(right) + if !leftOK || !rightOK || math.IsNaN(leftNumber) || math.IsNaN(rightNumber) { + return 0, false + } + switch { + case leftNumber < rightNumber: + return -1, true + case leftNumber > rightNumber: + return 1, true + default: + return 0, true + } +} + +func spectralComparableNumber(value spectralRuntimeValue) (float64, bool) { + switch value.kind { + case spectralNumber: + return value.number, true + case spectralString: + return spectralStringNumber(value.string) + case spectralBoolean: + return spectralBooleanNumber(value.boolean), true + case spectralNull: + return 0, true + default: + return 0, false + } +} + +func unwrapDocument(node *yaml.Node) *yaml.Node { + if node != nil && node.Kind == yaml.DocumentNode && len(node.Content) == 1 { + return node.Content[0] + } + return node +} diff --git a/vendor/github.com/pb33f/jsonpath/pkg/jsonpath/spectral_expr.go b/vendor/github.com/pb33f/jsonpath/pkg/jsonpath/spectral_expr.go new file mode 100644 index 000000000..50a89b937 --- /dev/null +++ b/vendor/github.com/pb33f/jsonpath/pkg/jsonpath/spectral_expr.go @@ -0,0 +1,659 @@ +package jsonpath + +import ( + "fmt" + "regexp" + "strconv" + "strings" + + "github.com/pb33f/jsonpath/pkg/jsonpath/token" +) + +type spectralBoolKind uint8 + +const ( + spectralBoolValue spectralBoolKind = iota + spectralBoolNot + spectralBoolAnd + spectralBoolOr + spectralBoolCompare + spectralBoolParen +) + +type spectralBoolExpr struct { + kind spectralBoolKind + value *spectralValueExpr + left *spectralBoolExpr + right *spectralBoolExpr + op token.Token + usage contextVarUsage + usesPropertyNameSelector bool +} + +func (e *spectralBoolExpr) String() string { + if e == nil { + return "" + } + switch e.kind { + case spectralBoolValue: + return e.value.String() + case spectralBoolNot: + return "!" + e.left.String() + case spectralBoolAnd: + return e.left.String() + " && " + e.right.String() + case spectralBoolOr: + return e.left.String() + " || " + e.right.String() + case spectralBoolCompare: + return e.left.String() + " " + e.op.String() + " " + e.right.String() + case spectralBoolParen: + return "(" + e.left.String() + ")" + default: + return "" + } +} + +type spectralValueKind uint8 + +const ( + spectralValueLiteral spectralValueKind = iota + spectralValueCurrent + spectralValueRoot + spectralValueContext + spectralValueRegex + spectralValueUndefined + spectralValueFunction +) + +type spectralPathSegment struct { + name string + index *int64 +} + +type spectralPostfixKind uint8 + +const ( + spectralPostfixMatch spectralPostfixKind = iota + spectralPostfixIndexOf + spectralPostfixLength + spectralPostfixConstructorName + spectralPostfixIncludes +) + +type spectralPostfix struct { + kind spectralPostfixKind + regex *spectralRegex + search string + fromIndex *float64 + argument literal +} + +type spectralRegex struct { + source string + pattern string + flags string + compiled *regexp.Regexp +} + +type spectralValueExpr struct { + kind spectralValueKind + literal literal + context contextVarKind + regex *spectralRegex + function *functionExpr + segments []spectralPathSegment + postfix []spectralPostfix +} + +func (e *spectralValueExpr) String() string { + if e == nil { + return "" + } + var b strings.Builder + switch e.kind { + case spectralValueLiteral: + b.WriteString(e.literal.ToString()) + case spectralValueCurrent: + b.WriteByte('@') + case spectralValueRoot: + b.WriteByte('$') + case spectralValueContext: + b.WriteString(contextVariable{kind: e.context}.ToString()) + case spectralValueRegex: + b.WriteString(e.regex.source) + case spectralValueUndefined: + b.WriteString("void 0") + case spectralValueFunction: + b.WriteString(e.function.ToString()) + } + for _, segment := range e.segments { + if segment.index != nil { + b.WriteByte('[') + b.WriteString(strconv.FormatInt(*segment.index, 10)) + b.WriteByte(']') + } else if spectralDotMemberName(segment.name) { + b.WriteByte('.') + b.WriteString(segment.name) + } else { + b.WriteString("['") + b.WriteString(escapeString(segment.name)) + b.WriteString("']") + } + } + for _, postfix := range e.postfix { + switch postfix.kind { + case spectralPostfixMatch: + b.WriteString(".match(") + b.WriteString(postfix.regex.source) + b.WriteByte(')') + case spectralPostfixIndexOf: + b.WriteString(".indexOf('") + b.WriteString(escapeString(postfix.search)) + b.WriteByte('\'') + if postfix.fromIndex != nil { + b.WriteString(", ") + b.WriteString(strconv.FormatFloat(*postfix.fromIndex, 'f', -1, 64)) + } + b.WriteByte(')') + case spectralPostfixLength: + b.WriteString(".length") + case spectralPostfixConstructorName: + b.WriteString(".constructor.name") + case spectralPostfixIncludes: + b.WriteString(".includes(") + b.WriteString(postfix.argument.ToString()) + if postfix.fromIndex != nil { + b.WriteString(", ") + b.WriteString(strconv.FormatFloat(*postfix.fromIndex, 'f', -1, 64)) + } + b.WriteByte(')') + } + } + return b.String() +} + +func spectralDotMemberName(name string) bool { + if name == "$ref" { + return true + } + if name == "" { + return false + } + for i := 0; i < len(name); i++ { + ch := name[i] + if 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || ch == '_' || ch >= 0x80 { + continue + } + if i > 0 && '0' <= ch && ch <= '9' { + continue + } + return false + } + return true +} + +func (p *JSONPath) parseSpectralExpression() (*spectralBoolExpr, error) { + expr, err := p.parseSpectralOr() + if err != nil { + return nil, err + } + if p.current >= len(p.tokens) || p.tokens[p.current].Token != token.BRACKET_RIGHT { + return nil, p.spectralFailure(p.current, "expected the end of the Spectral filter expression") + } + return expr, nil +} + +func (p *JSONPath) parseSpectralOr() (*spectralBoolExpr, error) { + left, err := p.parseSpectralAnd() + if err != nil { + return nil, err + } + for p.spectralNext(token.OR) { + p.current++ + right, err := p.parseSpectralAnd() + if err != nil { + return nil, err + } + left = p.spectralBool(spectralBoolOr, left, right, 0) + } + return left, nil +} + +func (p *JSONPath) parseSpectralAnd() (*spectralBoolExpr, error) { + left, err := p.parseSpectralUnary() + if err != nil { + return nil, err + } + for p.spectralNext(token.AND) { + p.current++ + right, err := p.parseSpectralUnary() + if err != nil { + return nil, err + } + left = p.spectralBool(spectralBoolAnd, left, right, 0) + } + return left, nil +} + +func (p *JSONPath) parseSpectralUnary() (*spectralBoolExpr, error) { + if p.spectralNext(token.NOT) { + p.current++ + child, err := p.parseSpectralUnary() + if err != nil { + return nil, err + } + return p.spectralBool(spectralBoolNot, child, nil, 0), nil + } + if p.spectralNext(token.PAREN_LEFT) { + p.current++ + child, err := p.parseSpectralOr() + if err != nil { + return nil, err + } + if !p.spectralNext(token.PAREN_RIGHT) { + return nil, p.spectralFailure(p.current, "expected ')' in Spectral filter expression") + } + p.current++ + return p.spectralBool(spectralBoolParen, child, nil, 0), nil + } + return p.parseSpectralComparison() +} + +func (p *JSONPath) parseSpectralComparison() (*spectralBoolExpr, error) { + left, err := p.parseSpectralValue() + if err != nil { + return nil, err + } + leftBool := p.spectralValueBool(left) + if p.current >= len(p.tokens) || !isSpectralComparison(p.tokens[p.current].Token) { + if len(left.postfix) > 0 && left.postfix[len(left.postfix)-1].kind == spectralPostfixMatch { + return leftBool, nil + } + return leftBool, nil + } + op := p.tokens[p.current].Token + if len(left.postfix) > 0 && left.postfix[len(left.postfix)-1].kind == spectralPostfixMatch { + return nil, p.spectralFailure(p.current, "match(...) results are only supported as truthy tests and cannot be compared or chained") + } + p.current++ + right, err := p.parseSpectralValue() + if err != nil { + return nil, err + } + if len(right.postfix) > 0 && right.postfix[len(right.postfix)-1].kind == spectralPostfixMatch { + return nil, p.spectralFailure(p.current-1, "match(...) results are only supported as truthy tests and cannot be compared or chained") + } + return p.spectralBool(spectralBoolCompare, leftBool, p.spectralValueBool(right), op), nil +} + +func (p *JSONPath) parseSpectralValue() (*spectralValueExpr, error) { + if p.current >= len(p.tokens) { + return nil, p.spectralFailure(p.current, "expected a Spectral value expression") + } + tok := p.tokens[p.current] + value := &spectralValueExpr{} + switch tok.Token { + case token.STRING_LITERAL, token.INTEGER, token.FLOAT, token.TRUE, token.FALSE, token.NULL: + lit, err := p.parseLiteral() + if err != nil { + return nil, err + } + value.kind = spectralValueLiteral + value.literal = *lit + case token.CURRENT: + value.kind = spectralValueCurrent + p.current++ + case token.ROOT, token.CONTEXT_ROOT: + value.kind = spectralValueRoot + p.current++ + case token.CONTEXT_PROPERTY, token.CONTEXT_PARENT, token.CONTEXT_PARENT_PROPERTY, token.CONTEXT_PATH, token.CONTEXT_INDEX: + value.kind = spectralValueContext + value.context = contextVarTokenMap[tok.Token] + p.current++ + case token.REGEX: + rx, err := compileSpectralRegex(tok.Literal) + if err != nil { + return nil, p.spectralFailure(p.current, err.Error()) + } + value.kind = spectralValueRegex + value.regex = rx + p.current++ + case token.FUNCTION: + function, err := p.parseFunctionExpr() + if err != nil { + return nil, err + } + value.kind = spectralValueFunction + value.function = function + case token.STRING: + if tok.Literal != "void" || p.current+1 >= len(p.tokens) || p.tokens[p.current+1].Token != token.INTEGER || p.tokens[p.current+1].Literal != "0" { + return nil, p.spectralFailure(p.current, "unsupported identifier; only the safe undefined expression 'void 0' is supported") + } + value.kind = spectralValueUndefined + p.current += 2 + default: + return nil, p.spectralFailure(p.current, "expected a literal, query, context value or regex literal") + } + + for p.current < len(p.tokens) { + if len(value.postfix) > 0 && (p.tokens[p.current].Token == token.CHILD || p.tokens[p.current].Token == token.BRACKET_LEFT) { + return nil, p.spectralFailure(p.current, "postfix results cannot be chained in Spectral compatibility mode") + } + if p.tokens[p.current].Token == token.BRACKET_LEFT { + if value.kind != spectralValueCurrent && value.kind != spectralValueRoot { + return nil, p.spectralFailure(p.current, "computed member access is not supported in Spectral compatibility mode") + } + if err := p.parseSpectralBracketSegment(value); err != nil { + return nil, err + } + continue + } + if p.tokens[p.current].Token != token.CHILD { + break + } + childAt := p.current + p.current++ + if p.current >= len(p.tokens) { + return nil, p.spectralFailure(childAt, "expected a member or method name after '.'") + } + nameToken := p.tokens[p.current] + nameTokenWidth := 1 + if nameToken.Token == token.ROOT { + if p.current+1 >= len(p.tokens) || p.tokens[p.current+1].Token != token.STRING || p.tokens[p.current+1].Literal != "ref" { + return nil, p.spectralFailure(p.current, "only $ref is supported as a '$'-prefixed member name") + } + nameToken.Literal = "$ref" + nameTokenWidth = 2 + } else if nameToken.Token != token.STRING && nameToken.Token != token.FUNCTION { + return nil, p.spectralFailure(p.current, "expected a member or method name after '.'") + } + name := nameToken.Literal + p.current += nameTokenWidth + if p.spectralNext(token.PAREN_LEFT) { + if err := p.parseSpectralMethod(value, name, childAt); err != nil { + return nil, err + } + continue + } + switch name { + case "length": + value.postfix = append(value.postfix, spectralPostfix{kind: spectralPostfixLength}) + case "constructor": + if !p.spectralNext(token.CHILD) || p.current+1 >= len(p.tokens) || p.tokens[p.current+1].Literal != "name" { + return nil, p.spectralFailure(childAt, "only the complete read-only constructor.name shape is supported") + } + p.current += 2 + value.postfix = append(value.postfix, spectralPostfix{kind: spectralPostfixConstructorName}) + case "__proto__", "prototype", "__defineGetter__", "__defineSetter__": + return nil, p.spectralFailure(childAt, "prototype and executable-object access is not supported") + default: + if len(value.postfix) > 0 { + return nil, p.spectralFailure(childAt, "postfix results cannot be chained in Spectral compatibility mode") + } + if value.kind != spectralValueCurrent && value.kind != spectralValueRoot { + return nil, p.spectralFailure(childAt, "member access is only supported on singular queries") + } + value.segments = append(value.segments, spectralPathSegment{name: name}) + } + } + return value, nil +} + +func (p *JSONPath) parseSpectralBracketSegment(value *spectralValueExpr) error { + start := p.current + p.current++ + if p.current >= len(p.tokens) { + return p.spectralFailure(start, "unterminated computed member access") + } + tok := p.tokens[p.current] + segment := spectralPathSegment{} + switch tok.Token { + case token.STRING_LITERAL: + if tok.Literal == "constructor" || tok.Literal == "__proto__" || tok.Literal == "prototype" { + return p.spectralFailure(p.current, "computed constructor and prototype access is not supported") + } + segment.name = tok.Literal + case token.INTEGER: + index, err := strconv.ParseInt(tok.Literal, 10, 64) + if err != nil { + return p.spectralFailure(p.current, "invalid singular query index") + } + segment.index = &index + default: + return p.spectralFailure(p.current, "only literal member names and indexes are supported in singular queries") + } + p.current++ + if !p.spectralNext(token.BRACKET_RIGHT) { + return p.spectralFailure(p.current, "expected ']' after singular query member") + } + p.current++ + value.segments = append(value.segments, segment) + return nil +} + +func (p *JSONPath) parseSpectralMethod(value *spectralValueExpr, name string, at int) error { + if len(value.postfix) > 0 { + return p.spectralFailure(at, "method results cannot be chained in Spectral compatibility mode") + } + p.current++ + switch name { + case "match": + if !p.spectralNext(token.REGEX) { + return p.spectralFailure(p.current, "match requires one regex literal argument") + } + rx, err := compileSpectralRegex(p.tokens[p.current].Literal) + if err != nil { + return p.spectralFailure(p.current, err.Error()) + } + p.current++ + if !p.spectralNext(token.PAREN_RIGHT) { + return p.spectralFailure(p.current, "match requires exactly one regex literal argument") + } + p.current++ + value.postfix = append(value.postfix, spectralPostfix{kind: spectralPostfixMatch, regex: rx}) + case "indexOf": + if !p.spectralNext(token.STRING_LITERAL) { + return p.spectralFailure(p.current, "indexOf requires a string argument") + } + postfix := spectralPostfix{kind: spectralPostfixIndexOf, search: p.tokens[p.current].Literal} + p.current++ + if p.spectralNext(token.COMMA) { + p.current++ + if p.current >= len(p.tokens) || (p.tokens[p.current].Token != token.INTEGER && p.tokens[p.current].Token != token.FLOAT) { + return p.spectralFailure(p.current, "indexOf fromIndex must be numeric") + } + from, err := strconv.ParseFloat(p.tokens[p.current].Literal, 64) + if err != nil { + return p.spectralFailure(p.current, "invalid indexOf fromIndex") + } + postfix.fromIndex = &from + p.current++ + } + if !p.spectralNext(token.PAREN_RIGHT) { + return p.spectralFailure(p.current, "indexOf accepts one string and an optional numeric fromIndex") + } + p.current++ + value.postfix = append(value.postfix, postfix) + case "includes": + argument, err := p.parseLiteral() + if err != nil { + return p.spectralFailure(p.current, "includes requires a scalar literal argument") + } + postfix := spectralPostfix{kind: spectralPostfixIncludes, argument: *argument} + if p.spectralNext(token.COMMA) { + p.current++ + if p.current >= len(p.tokens) || (p.tokens[p.current].Token != token.INTEGER && p.tokens[p.current].Token != token.FLOAT) { + return p.spectralFailure(p.current, "includes fromIndex must be numeric") + } + from, err := strconv.ParseFloat(p.tokens[p.current].Literal, 64) + if err != nil { + return p.spectralFailure(p.current, "invalid includes fromIndex") + } + postfix.fromIndex = &from + p.current++ + } + if !p.spectralNext(token.PAREN_RIGHT) { + return p.spectralFailure(p.current, "includes accepts one scalar and an optional numeric fromIndex") + } + p.current++ + value.postfix = append(value.postfix, postfix) + default: + return p.spectralFailure(at, fmt.Sprintf("unsupported method %q; supported methods are match, indexOf and includes", name)) + } + return nil +} + +func (p *JSONPath) spectralBool(kind spectralBoolKind, left, right *spectralBoolExpr, op token.Token) *spectralBoolExpr { + expr := &spectralBoolExpr{kind: kind, left: left, right: right, op: op} + if left != nil { + expr.usage = left.usage + expr.usesPropertyNameSelector = left.usesPropertyNameSelector + } + if right != nil { + expr.usage.property = expr.usage.property || right.usage.property + expr.usage.parent = expr.usage.parent || right.usage.parent + expr.usage.parentProperty = expr.usage.parentProperty || right.usage.parentProperty + expr.usage.path = expr.usage.path || right.usage.path + expr.usage.index = expr.usage.index || right.usage.index + expr.usesPropertyNameSelector = expr.usesPropertyNameSelector || right.usesPropertyNameSelector + } + return expr +} + +func (p *JSONPath) spectralValueBool(value *spectralValueExpr) *spectralBoolExpr { + expr := &spectralBoolExpr{kind: spectralBoolValue, value: value} + switch value.kind { + case spectralValueContext: + expr.usage.mark(value.context) + case spectralValueFunction: + value.function.collectContextVarUsage(&expr.usage) + expr.usesPropertyNameSelector = value.function.hasPropertyNameReferences() + } + return expr +} + +func (p *JSONPath) spectralNext(tok token.Token) bool { + return p.current < len(p.tokens) && p.tokens[p.current].Token == tok +} + +func (p *JSONPath) spectralFailure(at int, message string) error { + if len(p.tokens) == 0 { + return fmt.Errorf("spectral compatibility mode: %s", message) + } + if at >= len(p.tokens) { + at = len(p.tokens) - 1 + } + return p.parseFailure(&p.tokens[at], "Spectral compatibility mode: "+message) +} + +func isSpectralComparison(tok token.Token) bool { + switch tok { + case token.EQ, token.NE, token.STRICT_EQ, token.STRICT_NE, token.GT, token.GE, token.LT, token.LE: + return true + default: + return false + } +} + +func compileSpectralRegex(source string) (*spectralRegex, error) { + if len(source) < 2 || source[0] != '/' { + return nil, fmt.Errorf("invalid regex literal") + } + closing := -1 + escaped := false + inClass := false + for i := 1; i < len(source); i++ { + ch := source[i] + if escaped { + escaped = false + continue + } + if ch == '\\' { + escaped = true + continue + } + if ch == '[' { + inClass = true + } else if ch == ']' { + inClass = false + } else if ch == '/' && !inClass { + closing = i + break + } + } + if closing < 0 { + return nil, fmt.Errorf("unterminated regex literal") + } + pattern := source[1:closing] + flags := source[closing+1:] + seen := [256]bool{} + for i := 0; i < len(flags); i++ { + flag := flags[i] + if seen[flag] { + return nil, fmt.Errorf("duplicate regex flag %q", flag) + } + seen[flag] = true + } + var modes strings.Builder + for i := 0; i < len(flags); i++ { + flag := flags[i] + switch flag { + case 'i', 'm', 's': + modes.WriteByte(flag) + case 'g': + case 'u': + if strings.Contains(pattern, `\u{`) { + return nil, fmt.Errorf("regex u flag code-point escapes are not supported by the safe RE2 evaluator") + } + case 'd', 'v', 'y': + return nil, fmt.Errorf("regex flag %q is not supported by the safe RE2 evaluator", flag) + default: + return nil, fmt.Errorf("unknown regex flag %q", flag) + } + } + if err := validateSpectralRegexPattern(pattern); err != nil { + return nil, err + } + pattern = strings.ReplaceAll(pattern, `\/`, `/`) + compiledPattern := pattern + if modes.Len() > 0 { + compiledPattern = "(?" + modes.String() + ")" + pattern + } + compiled, err := regexp.Compile(compiledPattern) + if err != nil { + return nil, fmt.Errorf("invalid or unsupported regex: %v", err) + } + return &spectralRegex{source: source, pattern: pattern, flags: flags, compiled: compiled}, nil +} + +func validateSpectralRegexPattern(pattern string) error { + inClass := false + for i := 0; i < len(pattern); i++ { + switch pattern[i] { + case '\\': + if i+1 >= len(pattern) { + return fmt.Errorf("malformed regex escape") + } + if pattern[i+1] >= '1' && pattern[i+1] <= '9' { + return fmt.Errorf("regex backreferences are not supported by the safe RE2 evaluator") + } + i++ + case '[': + if !inClass { + inClass = true + } + case ']': + if inClass { + inClass = false + } + case '(': + if inClass || i+2 >= len(pattern) || pattern[i+1] != '?' { + continue + } + if pattern[i+2] == '=' || pattern[i+2] == '!' || + (pattern[i+2] == '<' && i+3 < len(pattern) && (pattern[i+3] == '=' || pattern[i+3] == '!')) { + return fmt.Errorf("regex lookaround is not supported by the safe RE2 evaluator") + } + } + } + return nil +} diff --git a/vendor/github.com/pb33f/jsonpath/pkg/jsonpath/token/token.go b/vendor/github.com/pb33f/jsonpath/pkg/jsonpath/token/token.go index 7e92c120e..ef7a1fe70 100644 --- a/vendor/github.com/pb33f/jsonpath/pkg/jsonpath/token/token.go +++ b/vendor/github.com/pb33f/jsonpath/pkg/jsonpath/token/token.go @@ -1,10 +1,10 @@ package token import ( - "fmt" - "github.com/pb33f/jsonpath/pkg/jsonpath/config" - "strconv" - "strings" + "fmt" + "github.com/pb33f/jsonpath/pkg/jsonpath/config" + "strconv" + "strings" ) // ***************************************************************************** @@ -162,245 +162,254 @@ type Token int // The list of tokens. const ( - ILLEGAL Token = iota - STRING - INTEGER - FLOAT - STRING_LITERAL - TRUE - FALSE - NULL - ROOT - CURRENT - WILDCARD - PROPERTY_NAME - RECURSIVE - CHILD - ARRAY_SLICE - FILTER - PAREN_LEFT - PAREN_RIGHT - BRACKET_LEFT - BRACKET_RIGHT - COMMA - TILDE - AND - OR - NOT - EQ - NE - GT - GE - LT - LE - MATCHES - FUNCTION - - // JSONPath Plus context variable tokens - CONTEXT_PROPERTY // @property - current property name - CONTEXT_ROOT // @root - root node access in filter - CONTEXT_PARENT // @parent - parent node reference - CONTEXT_PARENT_PROPERTY // @parentProperty - parent's property name - CONTEXT_PATH // @path - absolute path to current node - CONTEXT_INDEX // @index - current array index - - // JSONPath Plus parent selector - PARENT_SELECTOR // ^ - select parent of current node + ILLEGAL Token = iota + STRING + INTEGER + FLOAT + STRING_LITERAL + TRUE + FALSE + NULL + ROOT + CURRENT + WILDCARD + PROPERTY_NAME + RECURSIVE + CHILD + ARRAY_SLICE + FILTER + PAREN_LEFT + PAREN_RIGHT + BRACKET_LEFT + BRACKET_RIGHT + COMMA + TILDE + AND + OR + NOT + EQ + NE + GT + GE + LT + LE + MATCHES + FUNCTION + + // JSONPath Plus context variable tokens + CONTEXT_PROPERTY // @property - current property name + CONTEXT_ROOT // @root - root node access in filter + CONTEXT_PARENT // @parent - parent node reference + CONTEXT_PARENT_PROPERTY // @parentProperty - parent's property name + CONTEXT_PATH // @path - absolute path to current node + CONTEXT_INDEX // @index - current array index + + // JSONPath Plus parent selector + PARENT_SELECTOR // ^ - select parent of current node + + // Spectral compatibility tokens are appended to preserve every existing + // exported token's numeric value. + REGEX + STRICT_EQ + STRICT_NE ) var SimpleTokens = [...]Token{ - STRING, - INTEGER, - STRING_LITERAL, - CHILD, - BRACKET_LEFT, - BRACKET_RIGHT, - ROOT, + STRING, + INTEGER, + STRING_LITERAL, + CHILD, + BRACKET_LEFT, + BRACKET_RIGHT, + ROOT, } var tokens = [...]string{ - ILLEGAL: "ILLEGAL", - STRING: "STRING", - INTEGER: "INTEGER", - FLOAT: "FLOAT", - STRING_LITERAL: "STRING_LITERAL", - TRUE: "TRUE", - FALSE: "FALSE", - NULL: "NULL", - // root node identifier (Section 2.2) - ROOT: "$", - // current node identifier (Section 2.3.5) - // (valid only within filter selectors) - CURRENT: "@", - WILDCARD: "*", - RECURSIVE: "..", - CHILD: ".", - // start:end:step array slice operator (Section 2.3.4) - ARRAY_SLICE: ":", - // filter selector (Section 2.3.5): selects - // particular children using a logical - // expression - FILTER: "?", - PAREN_LEFT: "(", - PAREN_RIGHT: ")", - BRACKET_LEFT: "[", - BRACKET_RIGHT: "]", - COMMA: ",", - TILDE: "~", - AND: "&&", - OR: "||", - NOT: "!", - EQ: "==", - NE: "!=", - GT: ">", - GE: ">=", - LT: "<", - LE: "<=", - MATCHES: "=~", - FUNCTION: "FUNCTION", - - // JSONPath Plus context variables - CONTEXT_PROPERTY: "@property", - CONTEXT_ROOT: "@root", - CONTEXT_PARENT: "@parent", - CONTEXT_PARENT_PROPERTY: "@parentProperty", - CONTEXT_PATH: "@path", - CONTEXT_INDEX: "@index", - - // JSONPath Plus parent selector - PARENT_SELECTOR: "^", + ILLEGAL: "ILLEGAL", + STRING: "STRING", + INTEGER: "INTEGER", + FLOAT: "FLOAT", + STRING_LITERAL: "STRING_LITERAL", + TRUE: "TRUE", + FALSE: "FALSE", + NULL: "NULL", + // root node identifier (Section 2.2) + ROOT: "$", + // current node identifier (Section 2.3.5) + // (valid only within filter selectors) + CURRENT: "@", + WILDCARD: "*", + RECURSIVE: "..", + CHILD: ".", + // start:end:step array slice operator (Section 2.3.4) + ARRAY_SLICE: ":", + // filter selector (Section 2.3.5): selects + // particular children using a logical + // expression + FILTER: "?", + PAREN_LEFT: "(", + PAREN_RIGHT: ")", + BRACKET_LEFT: "[", + BRACKET_RIGHT: "]", + COMMA: ",", + TILDE: "~", + AND: "&&", + OR: "||", + NOT: "!", + EQ: "==", + NE: "!=", + GT: ">", + GE: ">=", + LT: "<", + LE: "<=", + MATCHES: "=~", + FUNCTION: "FUNCTION", + REGEX: "REGEX", + STRICT_EQ: "===", + STRICT_NE: "!==", + + // JSONPath Plus context variables + CONTEXT_PROPERTY: "@property", + CONTEXT_ROOT: "@root", + CONTEXT_PARENT: "@parent", + CONTEXT_PARENT_PROPERTY: "@parentProperty", + CONTEXT_PATH: "@path", + CONTEXT_INDEX: "@index", + + // JSONPath Plus parent selector + PARENT_SELECTOR: "^", } // String returns the string representation of the token. func (tok Token) String() string { - if tok >= 0 && tok < Token(len(tokens)) { - return tokens[tok] - } - return "token(" + strconv.Itoa(int(tok)) + ")" + if tok >= 0 && tok < Token(len(tokens)) { + return tokens[tok] + } + return "token(" + strconv.Itoa(int(tok)) + ")" } func (tok Tokens) IsSimple() bool { - if len(tok) == 0 { - return false - } - if tok[0].Token != ROOT { - return false - } - for _, token := range tok { - isSimple := false - for _, simpleToken := range SimpleTokens { - if token.Token == simpleToken { - isSimple = true - } - } - if !isSimple { - return false - } - } - return true + if len(tok) == 0 { + return false + } + if tok[0].Token != ROOT { + return false + } + for _, token := range tok { + isSimple := false + for _, simpleToken := range SimpleTokens { + if token.Token == simpleToken { + isSimple = true + } + } + if !isSimple { + return false + } + } + return true } // When there's an error in the tokenizer, this helps represent it. func (t Tokenizer) ErrorString(target *TokenInfo, msg string) string { - var errorBuilder strings.Builder - - var token TokenInfo - if target == nil { - // grab last token (as value) - token = t.tokens[len(t.tokens)-1] - // set column to +1 - token.Column++ - target = &token - } - - // Write the error message with line and column information - errorBuilder.WriteString(fmt.Sprintf("Error at line %d, column %d: %s\n", target.Line, target.Column, msg)) - - // Find the start and end positions of the line containing the target token - lineStart := 0 - lineEnd := len(t.input) - for i := target.Line - 1; i > 0; i-- { - if pos := strings.LastIndexByte(t.input[:lineStart], '\n'); pos != -1 { - lineStart = pos + 1 - break - } - } - if pos := strings.IndexByte(t.input[lineStart:], '\n'); pos != -1 { - lineEnd = lineStart + pos - } - - // Extract the line containing the target token - line := t.input[lineStart:lineEnd] - errorBuilder.WriteString(line) - errorBuilder.WriteString("\n") - - // Calculate the number of spaces before the target token - spaces := strings.Repeat(" ", target.Column) - - // Write the caret symbol pointing to the target token - errorBuilder.WriteString(spaces) - dots := "" - if target.Len > 0 { - dots = strings.Repeat(".", target.Len-1) - } - errorBuilder.WriteString("^" + dots + "\n") - - return errorBuilder.String() + var errorBuilder strings.Builder + + var token TokenInfo + if target == nil { + // grab last token (as value) + token = t.tokens[len(t.tokens)-1] + // set column to +1 + token.Column++ + target = &token + } + + // Write the error message with line and column information + errorBuilder.WriteString(fmt.Sprintf("Error at line %d, column %d: %s\n", target.Line, target.Column, msg)) + + // Find the start and end positions of the line containing the target token + lineStart := 0 + lineEnd := len(t.input) + for i := target.Line - 1; i > 0; i-- { + if pos := strings.LastIndexByte(t.input[:lineStart], '\n'); pos != -1 { + lineStart = pos + 1 + break + } + } + if pos := strings.IndexByte(t.input[lineStart:], '\n'); pos != -1 { + lineEnd = lineStart + pos + } + + // Extract the line containing the target token + line := t.input[lineStart:lineEnd] + errorBuilder.WriteString(line) + errorBuilder.WriteString("\n") + + // Calculate the number of spaces before the target token + spaces := strings.Repeat(" ", target.Column) + + // Write the caret symbol pointing to the target token + errorBuilder.WriteString(spaces) + dots := "" + if target.Len > 0 { + dots = strings.Repeat(".", target.Len-1) + } + errorBuilder.WriteString("^" + dots + "\n") + + return errorBuilder.String() } // When there's an error func (t Tokenizer) ErrorTokenString(target *TokenInfo, msg string) string { - var errorBuilder strings.Builder - var token TokenInfo - if target == nil { - // grab last token (as value) - token = t.tokens[len(t.tokens)-1] - // set column to +1 - token.Column++ - target = &token - } - // Write the error message with line and column information - errorBuilder.WriteString(t.ErrorString(target, msg)) - - // Find the start and end positions of the line containing the target token - lineStart := 0 - lineEnd := len(t.input) - for i := target.Line - 1; i > 0; i-- { - if pos := strings.LastIndexByte(t.input[:lineStart], '\n'); pos != -1 { - lineStart = pos + 1 - break - } - } - if pos := strings.IndexByte(t.input[lineStart:], '\n'); pos != -1 { - lineEnd = lineStart + pos - } - - // Extract the line containing the target token - line := t.input[lineStart:lineEnd] - - // Calculate the number of spaces before the target token - for _, token := range t.tokens { - errorBuilder.WriteString(line) - errorBuilder.WriteString("\n") - spaces := strings.Repeat(" ", token.Column) - dots := "" - if token.Len > 0 { - dots = strings.Repeat(".", token.Len-1) - } - errorBuilder.WriteString(spaces) - errorBuilder.WriteString(fmt.Sprintf("^%s %s\n", dots, tokens[token.Token])) - } - - return errorBuilder.String() + var errorBuilder strings.Builder + var token TokenInfo + if target == nil { + // grab last token (as value) + token = t.tokens[len(t.tokens)-1] + // set column to +1 + token.Column++ + target = &token + } + // Write the error message with line and column information + errorBuilder.WriteString(t.ErrorString(target, msg)) + + // Find the start and end positions of the line containing the target token + lineStart := 0 + lineEnd := len(t.input) + for i := target.Line - 1; i > 0; i-- { + if pos := strings.LastIndexByte(t.input[:lineStart], '\n'); pos != -1 { + lineStart = pos + 1 + break + } + } + if pos := strings.IndexByte(t.input[lineStart:], '\n'); pos != -1 { + lineEnd = lineStart + pos + } + + // Extract the line containing the target token + line := t.input[lineStart:lineEnd] + + // Calculate the number of spaces before the target token + for _, token := range t.tokens { + errorBuilder.WriteString(line) + errorBuilder.WriteString("\n") + spaces := strings.Repeat(" ", token.Column) + dots := "" + if token.Len > 0 { + dots = strings.Repeat(".", token.Len-1) + } + errorBuilder.WriteString(spaces) + errorBuilder.WriteString(fmt.Sprintf("^%s %s\n", dots, tokens[token.Token])) + } + + return errorBuilder.String() } // TokenInfo represents a token and its associated information. type TokenInfo struct { - Token Token - Line int - Column int - Literal string - Len int + Token Token + Line int + Column int + Literal string + Len int } // Tokens represents the list of tokens @@ -408,512 +417,622 @@ 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 - bracketFilterState []bool // lazy-init: nil until first BRACKET_LEFT; tracks filter context per bracket depth + 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. func NewTokenizer(input string, opts ...config.Option) *Tokenizer { - cfg := config.New(opts...) - return &Tokenizer{ - input: input, - config: cfg, - line: 1, - stack: make([]Token, 0), - } + return NewTokenizerWithConfig(input, config.New(opts...)) +} + +// NewTokenizerWithConfig creates a tokenizer using an already resolved +// configuration. It avoids resolving the same options again when a caller +// owns both tokenization and parsing. +func NewTokenizerWithConfig(input string, cfg config.Config) *Tokenizer { + if cfg == nil { + cfg = config.New() + } + return &Tokenizer{ + input: input, + config: cfg, + line: 1, + stack: make([]Token, 0), + } } // Tokenize tokenizes the input string and returns a slice of TokenInfo. func (t *Tokenizer) Tokenize() Tokens { - for t.pos < len(t.input) { - if !t.illegalWhitespace { - t.skipWhitespace() - } - if t.pos >= len(t.input) { - break - } - - switch ch := t.input[t.pos]; { - case ch == '$': - t.addToken(ROOT, 1, "") - case ch == '@': - // Check for JSONPath Plus context variables when enabled - handled := false - if t.config.JSONPathPlusEnabled() { - if contextToken, length := t.tryContextVariable(); contextToken != ILLEGAL { - t.addToken(contextToken, length, "") - // Advance past the token (minus 1 because main loop does pos++) - t.pos += length - 1 - t.column += length - 1 - handled = true - } - } - if !handled { - t.addToken(CURRENT, 1, "") - } - case ch == '*': - t.addToken(WILDCARD, 1, "") - case ch == '~': - if t.config.PropertyNameEnabled() { - t.addToken(PROPERTY_NAME, 1, "") - } else { - t.addToken(ILLEGAL, 1, "invalid property name token without config.PropertyNameExtension set to true") - } - case ch == '^': - // JSONPath Plus parent selector - if t.config.JSONPathPlusEnabled() { - t.addToken(PARENT_SELECTOR, 1, "") - } else { - t.addToken(ILLEGAL, 1, "parent selector ^ requires JSONPath Plus mode (enabled by default, disabled with StrictRFC9535)") - } - case ch == '.': - if t.peek() == '.' { - t.addToken(RECURSIVE, 2, "") - t.pos++ - t.column++ - t.illegalWhitespace = true - } else { - t.addToken(CHILD, 1, "") - t.illegalWhitespace = true - } - case ch == ',': - t.addToken(COMMA, 1, "") - case ch == ':': - 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) - case ch == ')': - t.addToken(PAREN_RIGHT, 1, "") - if len(t.stack) > 0 && t.stack[len(t.stack)-1] == PAREN_LEFT { - t.stack = t.stack[:len(t.stack)-1] - } else { - t.addToken(ILLEGAL, 1, "unmatched closing parenthesis") - } - 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") - } - case ch == '&': - if t.peek() == '&' { - t.addToken(AND, 2, "") - t.pos++ - t.column++ - } else { - t.addToken(ILLEGAL, 1, "invalid token") - } - case ch == '|': - if t.peek() == '|' { - t.addToken(OR, 2, "") - t.pos++ - t.column++ - } else { - t.addToken(ILLEGAL, 1, "invalid token") - } - case ch == '!': - if t.peek() == '=' { - // Check for JavaScript !== (strict not-equals) - treat as RFC 9535 != - if t.pos+2 < len(t.input) && t.input[t.pos+2] == '=' { - t.addToken(NE, 3, "") // !== becomes != - t.pos += 2 - t.column += 2 - } else { - t.addToken(NE, 2, "") - t.pos++ - t.column++ - } - } else { - t.addToken(NOT, 1, "") - } - case ch == '=': - if t.peek() == '=' { - // Check for JavaScript === (strict equals) - treat as RFC 9535 == - if t.pos+2 < len(t.input) && t.input[t.pos+2] == '=' { - t.addToken(EQ, 3, "") // === becomes == - t.pos += 2 - t.column += 2 - } else { - t.addToken(EQ, 2, "") - t.pos++ - t.column++ - } - } else if t.peek() == '~' { - t.addToken(MATCHES, 2, "") - t.pos++ - t.column++ - } else { - t.addToken(ILLEGAL, 1, "invalid token") - } - case ch == '>': - if t.peek() == '=' { - t.addToken(GE, 2, "") - t.pos++ - t.column++ - } else { - t.addToken(GT, 1, "") - } - case ch == '<': - if t.peek() == '=' { - t.addToken(LE, 2, "") - t.pos++ - t.column++ - } else { - t.addToken(LT, 1, "") - } - case ch == '"' || ch == '\'': - t.scanString(rune(ch)) - case ch == '-' && isDigit(t.peek()): - fallthrough - case isDigit(ch): - t.scanNumber() - case isLiteralChar(ch): - if t.config.JSONPathPlusEnabled() && t.isInsideBracket() && !t.isInFilterContext() { - t.scanUnquotedBracketString() - } else { - t.scanLiteral() - } - default: - // 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++ - } - - if len(t.stack) > 0 { - t.addToken(ILLEGAL, 1, fmt.Sprintf("unmatched %s", t.stack[len(t.stack)-1].String())) - } - return t.tokens + for t.pos < len(t.input) { + if !t.illegalWhitespace { + t.skipWhitespace() + } + if t.pos >= len(t.input) { + break + } + + switch ch := t.input[t.pos]; { + case ch == '$': + if config.SpectralCompatibilityEnabled(t.config) && t.spectralDollarMemberCanStartHere() { + t.addToken(STRING, 4, "$ref") + t.pos += 3 + t.column += 3 + } else { + t.addToken(ROOT, 1, "") + } + case ch == '@': + // Check for JSONPath Plus context variables when enabled + handled := false + if t.config.JSONPathPlusEnabled() { + if contextToken, length := t.tryContextVariable(); contextToken != ILLEGAL { + t.addToken(contextToken, length, "") + // Advance past the token (minus 1 because main loop does pos++) + t.pos += length - 1 + t.column += length - 1 + handled = true + } + } + if !handled { + t.addToken(CURRENT, 1, "") + } + case ch == '*': + t.addToken(WILDCARD, 1, "") + case ch == '~': + if t.config.PropertyNameEnabled() { + t.addToken(PROPERTY_NAME, 1, "") + } else { + t.addToken(ILLEGAL, 1, "invalid property name token without config.PropertyNameExtension set to true") + } + case ch == '^': + // JSONPath Plus parent selector + if t.config.JSONPathPlusEnabled() { + t.addToken(PARENT_SELECTOR, 1, "") + } else { + t.addToken(ILLEGAL, 1, "parent selector ^ requires JSONPath Plus mode (enabled by default, disabled with StrictRFC9535)") + } + case ch == '.': + if t.peek() == '.' { + t.addToken(RECURSIVE, 2, "") + t.pos++ + t.column++ + t.illegalWhitespace = true + } else { + t.addToken(CHILD, 1, "") + t.illegalWhitespace = true + } + case ch == ',': + t.addToken(COMMA, 1, "") + case ch == ':': + 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) + case ch == ')': + t.addToken(PAREN_RIGHT, 1, "") + if len(t.stack) > 0 && t.stack[len(t.stack)-1] == PAREN_LEFT { + t.stack = t.stack[:len(t.stack)-1] + } else { + t.addToken(ILLEGAL, 1, "unmatched closing parenthesis") + } + 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") + } + case ch == '&': + if t.peek() == '&' { + t.addToken(AND, 2, "") + t.pos++ + t.column++ + } else { + t.addToken(ILLEGAL, 1, "invalid token") + } + case ch == '|': + if t.peek() == '|' { + t.addToken(OR, 2, "") + t.pos++ + t.column++ + } else { + t.addToken(ILLEGAL, 1, "invalid token") + } + case ch == '!': + if t.peek() == '=' { + if t.pos+2 < len(t.input) && t.input[t.pos+2] == '=' { + if config.SpectralCompatibilityEnabled(t.config) { + t.addToken(STRICT_NE, 3, "") + } else if !t.config.JSONPathPlusEnabled() { + t.addToken(ILLEGAL, 3, "strict JavaScript inequality is not part of RFC 9535") + } else { + t.addToken(NE, 3, "") + } + t.pos += 2 + t.column += 2 + } else { + t.addToken(NE, 2, "") + t.pos++ + t.column++ + } + } else { + t.addToken(NOT, 1, "") + } + case ch == '=': + if t.peek() == '=' { + if t.pos+2 < len(t.input) && t.input[t.pos+2] == '=' { + if config.SpectralCompatibilityEnabled(t.config) { + t.addToken(STRICT_EQ, 3, "") + } else if !t.config.JSONPathPlusEnabled() { + t.addToken(ILLEGAL, 3, "strict JavaScript equality is not part of RFC 9535") + } else { + t.addToken(EQ, 3, "") + } + t.pos += 2 + t.column += 2 + } else { + t.addToken(EQ, 2, "") + t.pos++ + t.column++ + } + } else if t.peek() == '~' { + t.addToken(MATCHES, 2, "") + t.pos++ + t.column++ + } else { + message := "invalid token" + if config.SpectralCompatibilityEnabled(t.config) && t.isInFilterContext() { + message = "assignments are not supported" + } + t.addToken(ILLEGAL, 1, message) + } + case ch == '>': + if t.peek() == '=' { + t.addToken(GE, 2, "") + t.pos++ + t.column++ + } else { + t.addToken(GT, 1, "") + } + case ch == '<': + if t.peek() == '=' { + t.addToken(LE, 2, "") + t.pos++ + t.column++ + } else { + t.addToken(LT, 1, "") + } + case ch == '"' || ch == '\'': + t.scanString(rune(ch)) + case ch == '/' && config.SpectralCompatibilityEnabled(t.config) && t.isInFilterContext() && t.regexCanStartHere(): + t.scanRegex() + case ch == '-' && isDigit(t.peek()): + fallthrough + case isDigit(ch): + t.scanNumber() + case isLiteralChar(ch): + if t.config.JSONPathPlusEnabled() && t.isInsideBracket() && !t.isInFilterContext() { + t.scanUnquotedBracketString() + } else { + t.scanLiteral() + } + default: + // 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 { + message := string(ch) + if config.SpectralCompatibilityEnabled(t.config) && t.isInFilterContext() && ch == ';' { + message = "statement separators are not supported" + } else if config.SpectralCompatibilityEnabled(t.config) && t.isInFilterContext() && (ch == '{' || ch == '}') { + message = "function declarations and statement blocks are not supported" + } + t.addToken(ILLEGAL, 1, message) + } + } + t.pos++ + t.column++ + } + + if len(t.stack) > 0 { + t.addToken(ILLEGAL, 1, fmt.Sprintf("unmatched %s", t.stack[len(t.stack)-1].String())) + } + return t.tokens +} + +func (t *Tokenizer) spectralDollarMemberCanStartHere() bool { + if len(t.tokens) == 0 || t.tokens[len(t.tokens)-1].Token != CHILD || !strings.HasPrefix(t.input[t.pos:], "$ref") { + return false + } + end := t.pos + len("$ref") + return end == len(t.input) || !isLiteralChar(t.input[end]) && !isDigit(t.input[end]) +} + +// regexCanStartHere distinguishes a Spectral regex primary from an unquoted +// bracket member. A regex may begin only where an expression or argument is +// expected. +func (t *Tokenizer) regexCanStartHere() bool { + if len(t.tokens) == 0 { + return false + } + switch t.tokens[len(t.tokens)-1].Token { + case PAREN_LEFT, COMMA, AND, OR, NOT, EQ, NE, STRICT_EQ, STRICT_NE, GT, GE, LT, LE: + return true + default: + return false + } +} + +// scanRegex scans a JavaScript-style regex literal without interpreting its +// pattern. Compatibility validation and compilation happen once in the parser. +func (t *Tokenizer) scanRegex() { + start := t.pos + inClass := false + escaped := false + for i := start + 1; i < len(t.input); i++ { + ch := t.input[i] + if ch == '\n' || ch == '\r' { + t.addToken(ILLEGAL, i-start, "unterminated regex literal") + t.pos = i - 1 + t.column += i - start - 1 + return + } + if escaped { + escaped = false + continue + } + if ch == '\\' { + escaped = true + continue + } + switch ch { + case '[': + inClass = true + case ']': + inClass = false + case '/': + if inClass { + continue + } + end := i + 1 + for end < len(t.input) && isLiteralChar(t.input[end]) { + end++ + } + t.addToken(REGEX, end-start, t.input[start:end]) + t.pos = end - 1 + t.column += end - start - 1 + return + } + } + msg := "unterminated regex literal" + if inClass { + msg = "unterminated regex character class" + } + t.addToken(ILLEGAL, len(t.input)-start, msg) + t.pos = len(t.input) - 1 + t.column += len(t.input) - start - 1 } func (t *Tokenizer) addToken(token Token, len int, literal string) { - t.tokens = append(t.tokens, TokenInfo{ - Token: token, - Line: t.line, - Column: t.column, - Len: len, - Literal: literal, - }) - t.illegalWhitespace = false + t.tokens = append(t.tokens, TokenInfo{ + Token: token, + Line: t.line, + Column: t.column, + Len: len, + Literal: literal, + }) + t.illegalWhitespace = false } func (t *Tokenizer) scanString(quote rune) { - start := t.pos + 1 - var literal strings.Builder + start := t.pos + 1 + var literal strings.Builder illegal: - for i := start; i < len(t.input); i++ { - if t.input[i] == byte(quote) { - t.addToken(STRING_LITERAL, len(t.input[start:i])+2, literal.String()) - t.pos = i - t.column += i - start + 1 - return - } - if t.input[i] == '\\' { - i++ - if i >= len(t.input) { - t.addToken(ILLEGAL, len(t.input[start:]), literal.String()) - t.pos = len(t.input) - 1 - t.column = len(t.input) - 1 - return - } - switch t.input[i] { - case 'b': - literal.WriteByte('\b') - case 'f': - literal.WriteByte('\f') - case 'n': - literal.WriteByte('\n') - case 'r': - literal.WriteByte('\n') - case 't': - literal.WriteByte('\t') - case '\'': - if quote != '\'' { - // don't escape it, when we're not in a single quoted string - break illegal - } else { - literal.WriteByte(t.input[i]) - } - case '"': - if quote != '"' { - // don't escape it, when we're not in a single quoted string - break illegal - } else { - literal.WriteByte(t.input[i]) - } - case '\\', '/': - literal.WriteByte(t.input[i]) - default: - break illegal - } - } else { - literal.WriteByte(t.input[i]) - } - } - t.addToken(ILLEGAL, len(t.input[start:]), literal.String()) - t.pos = len(t.input) - 1 - t.column = len(t.input) - 1 + for i := start; i < len(t.input); i++ { + if t.input[i] == byte(quote) { + t.addToken(STRING_LITERAL, len(t.input[start:i])+2, literal.String()) + t.pos = i + t.column += i - start + 1 + return + } + if t.input[i] == '\\' { + i++ + if i >= len(t.input) { + t.addToken(ILLEGAL, len(t.input[start:]), literal.String()) + t.pos = len(t.input) - 1 + t.column = len(t.input) - 1 + return + } + switch t.input[i] { + case 'b': + literal.WriteByte('\b') + case 'f': + literal.WriteByte('\f') + case 'n': + literal.WriteByte('\n') + case 'r': + literal.WriteByte('\n') + case 't': + literal.WriteByte('\t') + case '\'': + if quote != '\'' { + // don't escape it, when we're not in a single quoted string + break illegal + } else { + literal.WriteByte(t.input[i]) + } + case '"': + if quote != '"' { + // don't escape it, when we're not in a single quoted string + break illegal + } else { + literal.WriteByte(t.input[i]) + } + case '\\', '/': + literal.WriteByte(t.input[i]) + default: + break illegal + } + } else { + literal.WriteByte(t.input[i]) + } + } + t.addToken(ILLEGAL, len(t.input[start:]), literal.String()) + t.pos = len(t.input) - 1 + t.column = len(t.input) - 1 } func (t *Tokenizer) scanNumber() { - start := t.pos - tokenType := INTEGER - dotSeen := false - exponentSeen := false - - for i := start; i < len(t.input); i++ { - if i == start && t.input[i] == '-' { - continue - } - - if t.input[i] == '.' { - if dotSeen || exponentSeen { - t.addToken(ILLEGAL, len(t.input[start:i]), t.input[start:i]) - t.pos = i - 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 - } - - if t.input[i] == 'e' || t.input[i] == 'E' { - if exponentSeen || (len(t.input) > 0 && t.input[i-1] == '.') { - t.addToken(ILLEGAL, len(t.input[start:i]), t.input[start:i]) - t.pos = i - t.column += i - start - return - } - tokenType = FLOAT - exponentSeen = true - if i+1 < len(t.input) && (t.input[i+1] == '+' || t.input[i+1] == '-') { - i++ - } - continue - } - - if !isDigit(t.input[i]) { - literal := t.input[start:i] - // check for legal numbers - _, err := strconv.ParseFloat(literal, 64) - if err != nil { - tokenType = ILLEGAL - } - // conformance spec - if len(literal) > 1 && literal[0] == '0' && !dotSeen { - // no leading zero - tokenType = ILLEGAL - } else if len(literal) > 2 && literal[0] == '-' && literal[1] == '0' && !dotSeen { - // no negative zero without fraction - tokenType = ILLEGAL - } else if len(literal) > 0 && literal[len(literal)-1] == '.' { - // no trailing dot - tokenType = ILLEGAL - } else if literal[len(literal)-1] == 'e' || literal[len(literal)-1] == 'E' { - // no exponent - tokenType = ILLEGAL - } - - t.addToken(tokenType, len(literal), literal) - t.pos = i - 1 - t.column += i - start - 1 - return - } - } - - if exponentSeen && !isDigit(t.input[len(t.input)-1]) { - t.addToken(ILLEGAL, len(t.input[start:]), t.input[start:]) - t.pos = len(t.input) - 1 - t.column = len(t.input) - 1 - return - } - - literal := t.input[start:] - t.addToken(tokenType, len(literal), literal) - t.pos = len(t.input) - 1 - t.column = len(t.input) - 1 + start := t.pos + tokenType := INTEGER + dotSeen := false + exponentSeen := false + + for i := start; i < len(t.input); i++ { + if i == start && t.input[i] == '-' { + continue + } + + if t.input[i] == '.' { + if dotSeen || exponentSeen { + t.addToken(ILLEGAL, len(t.input[start:i]), t.input[start:i]) + t.pos = i + 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 + } + + if t.input[i] == 'e' || t.input[i] == 'E' { + if exponentSeen || (len(t.input) > 0 && t.input[i-1] == '.') { + t.addToken(ILLEGAL, len(t.input[start:i]), t.input[start:i]) + t.pos = i + t.column += i - start + return + } + tokenType = FLOAT + exponentSeen = true + if i+1 < len(t.input) && (t.input[i+1] == '+' || t.input[i+1] == '-') { + i++ + } + continue + } + + if !isDigit(t.input[i]) { + literal := t.input[start:i] + // check for legal numbers + _, err := strconv.ParseFloat(literal, 64) + if err != nil { + tokenType = ILLEGAL + } + // conformance spec + if len(literal) > 1 && literal[0] == '0' && !dotSeen { + // no leading zero + tokenType = ILLEGAL + } else if len(literal) > 2 && literal[0] == '-' && literal[1] == '0' && !dotSeen { + // no negative zero without fraction + tokenType = ILLEGAL + } else if len(literal) > 0 && literal[len(literal)-1] == '.' { + // no trailing dot + tokenType = ILLEGAL + } else if literal[len(literal)-1] == 'e' || literal[len(literal)-1] == 'E' { + // no exponent + tokenType = ILLEGAL + } + + t.addToken(tokenType, len(literal), literal) + t.pos = i - 1 + t.column += i - start - 1 + return + } + } + + if exponentSeen && !isDigit(t.input[len(t.input)-1]) { + t.addToken(ILLEGAL, len(t.input[start:]), t.input[start:]) + t.pos = len(t.input) - 1 + t.column = len(t.input) - 1 + return + } + + literal := t.input[start:] + t.addToken(tokenType, len(literal), literal) + t.pos = len(t.input) - 1 + t.column = len(t.input) - 1 } func (t *Tokenizer) scanLiteral() { - start := t.pos - for i := start; i < len(t.input); i++ { - if !isLiteralChar(t.input[i]) && !isDigit(t.input[i]) { - literal := t.input[start:i] - switch literal { - case "true": - t.addToken(TRUE, len(literal), literal) - case "false": - t.addToken(FALSE, len(literal), literal) - case "null": - t.addToken(NULL, len(literal), literal) - default: - // Only treat as FUNCTION if it's a function name AND followed by '(' - // Otherwise it's a property name (STRING) - if isFunctionName(literal) && i < len(t.input) && t.input[i] == '(' { - t.addToken(FUNCTION, len(literal), literal) - t.illegalWhitespace = true - } else { - t.addToken(STRING, len(literal), literal) - } - } - t.pos = i - 1 - t.column += i - start - 1 - return - } - } - literal := t.input[start:] - switch literal { - case "true": - t.addToken(TRUE, len(literal), literal) - case "false": - t.addToken(FALSE, len(literal), literal) - case "null": - t.addToken(NULL, len(literal), literal) - default: - t.addToken(STRING, len(literal), literal) - } - t.pos = len(t.input) - 1 - t.column = len(t.input) - 1 + start := t.pos + for i := start; i < len(t.input); i++ { + if !isLiteralChar(t.input[i]) && !isDigit(t.input[i]) { + literal := t.input[start:i] + switch literal { + case "true": + t.addToken(TRUE, len(literal), literal) + case "false": + t.addToken(FALSE, len(literal), literal) + case "null": + t.addToken(NULL, len(literal), literal) + default: + // Only treat as FUNCTION if it's a function name AND followed by '(' + // Otherwise it's a property name (STRING) + if isFunctionName(literal) && i < len(t.input) && t.input[i] == '(' { + t.addToken(FUNCTION, len(literal), literal) + t.illegalWhitespace = true + } else { + t.addToken(STRING, len(literal), literal) + } + } + t.pos = i - 1 + t.column += i - start - 1 + return + } + } + literal := t.input[start:] + switch literal { + case "true": + t.addToken(TRUE, len(literal), literal) + case "false": + t.addToken(FALSE, len(literal), literal) + case "null": + t.addToken(NULL, len(literal), literal) + default: + t.addToken(STRING, len(literal), literal) + } + t.pos = len(t.input) - 1 + t.column = len(t.input) - 1 } func isFunctionName(literal string) bool { - switch literal { - // RFC 9535 standard functions - case "length", "count", "match", "search", "value": - return true - // JSONPath Plus type selector functions - case "isNull", "isBoolean", "isNumber", "isString", "isArray", "isObject", "isInteger": - return true - } - return false + switch literal { + // RFC 9535 standard functions + case "length", "count", "match", "search", "value": + return true + // JSONPath Plus type selector functions + case "isNull", "isBoolean", "isNumber", "isString", "isArray", "isObject", "isInteger": + return true + } + return false } func (t *Tokenizer) skipWhitespace() { - // S = *B ; optional blank space - // B = %x20 / ; Space - // %x09 / ; Horizontal tab - // %x0A / ; Line feed or New line - // %x0D ; Carriage return - for len(t.tokens) > 0 && t.pos+1 < len(t.input) { - ch := t.input[t.pos] - if ch == '\n' { - t.line++ - t.pos++ - t.column = 0 - } else if !isSpace(ch) { - break - } else { - t.pos++ - t.column++ - } - } + // S = *B ; optional blank space + // B = %x20 / ; Space + // %x09 / ; Horizontal tab + // %x0A / ; Line feed or New line + // %x0D ; Carriage return + for len(t.tokens) > 0 && t.pos+1 < len(t.input) { + ch := t.input[t.pos] + if ch == '\n' { + t.line++ + t.pos++ + t.column = 0 + } else if !isSpace(ch) { + break + } else { + t.pos++ + t.column++ + } + } } func (t *Tokenizer) peek() byte { - if t.pos+1 < len(t.input) { - return t.input[t.pos+1] - } - return 0 + if t.pos+1 < len(t.input) { + return t.input[t.pos+1] + } + 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 + 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] + 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 + 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' + return '0' <= ch && ch <= '9' } func isLiteralChar(ch byte) bool { - // allow unicode characters - return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || ch == '_' || ch >= 0x80 + // allow unicode characters + return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || ch == '_' || ch >= 0x80 } func isSpace(ch byte) bool { - return ch == ' ' || ch == '\t' || ch == '\r' + return ch == ' ' || ch == '\t' || ch == '\r' } // isUnquotedBracketStartChar returns true if ch can start an unquoted bracket string @@ -922,50 +1041,50 @@ func isSpace(ch byte) bool { // 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 == '#' + 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{ - "property": CONTEXT_PROPERTY, - "root": CONTEXT_ROOT, - "parent": CONTEXT_PARENT, - "parentProperty": CONTEXT_PARENT_PROPERTY, - "path": CONTEXT_PATH, - "index": CONTEXT_INDEX, + "property": CONTEXT_PROPERTY, + "root": CONTEXT_ROOT, + "parent": CONTEXT_PARENT, + "parentProperty": CONTEXT_PARENT_PROPERTY, + "path": CONTEXT_PATH, + "index": CONTEXT_INDEX, } // tryContextVariable checks if the current position starts a context variable. // It returns the token type and total length (including @) if found, or ILLEGAL and 0 if not. // Context variables are @property, @root, @parent, @parentProperty, @path, @index. func (t *Tokenizer) tryContextVariable() (Token, int) { - // Must start with @ - if t.pos >= len(t.input) || t.input[t.pos] != '@' { - return ILLEGAL, 0 - } - - // Extract the word following @ - start := t.pos + 1 - if start >= len(t.input) { - return ILLEGAL, 0 - } - - // Find the end of the identifier - end := start - for end < len(t.input) && isLiteralChar(t.input[end]) { - end++ - } - - if end == start { - return ILLEGAL, 0 - } - - keyword := t.input[start:end] - if tok, ok := contextVariableKeywords[keyword]; ok { - // Return the token and total length including @ - return tok, end - t.pos - } - - return ILLEGAL, 0 + // Must start with @ + if t.pos >= len(t.input) || t.input[t.pos] != '@' { + return ILLEGAL, 0 + } + + // Extract the word following @ + start := t.pos + 1 + if start >= len(t.input) { + return ILLEGAL, 0 + } + + // Find the end of the identifier + end := start + for end < len(t.input) && isLiteralChar(t.input[end]) { + end++ + } + + if end == start { + return ILLEGAL, 0 + } + + keyword := t.input[start:end] + if tok, ok := contextVariableKeywords[keyword]; ok { + // Return the token and total length including @ + return tok, end - t.pos + } + + return ILLEGAL, 0 } diff --git a/vendor/github.com/pb33f/jsonpath/pkg/jsonpath/yaml_query.go b/vendor/github.com/pb33f/jsonpath/pkg/jsonpath/yaml_query.go index 855f9030b..5c8096343 100644 --- a/vendor/github.com/pb33f/jsonpath/pkg/jsonpath/yaml_query.go +++ b/vendor/github.com/pb33f/jsonpath/pkg/jsonpath/yaml_query.go @@ -10,8 +10,6 @@ type Evaluator interface { Query(current *yaml.Node, root *yaml.Node) []*yaml.Node } -// index is the basic interface for tracking property key relationships. -// This is kept for backward compatibility; FilterContext extends this. type index interface { setPropertyKey(key *yaml.Node, value *yaml.Node) getPropertyKey(key *yaml.Node) *yaml.Node @@ -21,7 +19,7 @@ type index interface { type _index struct { propertyKeys map[*yaml.Node]*yaml.Node - parentNodes map[*yaml.Node]*yaml.Node // Maps child nodes to their parent nodes + parentNodes map[*yaml.Node]*yaml.Node } func (i *_index) setPropertyKey(key *yaml.Node, value *yaml.Node) { @@ -50,7 +48,6 @@ func (i *_index) getParentNode(child *yaml.Node) *yaml.Node { return nil } -// jsonPathAST can be Evaluated var _ Evaluator = jsonPathAST{} func (q jsonPathAST) Query(current *yaml.Node, root *yaml.Node) []*yaml.Node { @@ -65,7 +62,6 @@ func (q jsonPathAST) Query(current *yaml.Node, root *yaml.Node) []*yaml.Node { ctx = NewFilterContext(root) } - // Only enable parent tracking if the query uses ^ or @parent if q.hasParentReferences() { ctx.EnableParentTracking() } @@ -102,7 +98,27 @@ func (q jsonPathAST) Query(current *yaml.Node, root *yaml.Node) []*yaml.Node { return result } -// hasParentReferences checks if the AST uses parent selectors (^) or @parent context variable +func (q jsonPathAST) isSingular() bool { + for _, seg := range q.segments { + if !seg.IsSingular() { + return false + } + } + return true +} + +func (q jsonPathAST) getSegmentInfo() ([]SegmentInfo, error) { + result := make([]SegmentInfo, 0, len(q.segments)) + for _, seg := range q.segments { + info, err := seg.getSegmentInfo() + if err != nil { + return nil, err + } + result = append(result, info...) + } + return result, nil +} + func (q jsonPathAST) hasParentReferences() bool { for _, seg := range q.segments { if seg.hasParentReferences() { @@ -135,13 +151,16 @@ func (s *innerSegment) hasParentReferences() bool { } func (s *selector) hasParentReferences() bool { - if s.filter != nil && s.filter.hasParentReferences() { + if s.filter.present() && s.filter.hasParentReferences() { return true } return false } func (f *filterSelector) hasParentReferences() bool { + if f.spectralExpression != nil { + return f.spectralExpression.usage.parent + } if f.expression != nil { return f.expression.hasParentReferences() } @@ -218,7 +237,6 @@ func (e *testExpr) hasParentReferences() bool { return false } -// parentTrackingEnabled checks if parent tracking is enabled in the index func parentTrackingEnabled(idx index) bool { if fc, ok := idx.(FilterContext); ok { return fc.ParentTrackingEnabled() @@ -245,540 +263,566 @@ func enableIndexTracking(ctx FilterContext) { } func (s segment) Query(idx index, value *yaml.Node, root *yaml.Node) []*yaml.Node { - switch s.kind { - case segmentKindChild: - return s.child.Query(idx, value, root) - case segmentKindDescendant: - // run the inner segment against this node - var result = []*yaml.Node{} - descendApply(value, func(child *yaml.Node) { - result = append(result, s.descendant.Query(idx, child, root)...) - }) - // make children unique by pointer value - result = unique(result) - return result - case segmentKindProperyName: - found := idx.getPropertyKey(value) - if found != nil { - return []*yaml.Node{found} - } - return []*yaml.Node{} - case segmentKindParent: - // JSONPath Plus parent selector: ^ returns the parent of the current node - parent := idx.getParentNode(value) - if parent != nil { - return []*yaml.Node{parent} - } - // No parent found (could be root node) - return []*yaml.Node{} - } - panic("no segment type") + switch s.kind { + case segmentKindChild: + return s.child.Query(idx, value, root) + case segmentKindDescendant: + var result = []*yaml.Node{} + descendApply(value, func(child *yaml.Node) { + result = append(result, s.descendant.Query(idx, child, root)...) + }) + result = unique(result) + return result + case segmentKindProperyName: + found := idx.getPropertyKey(value) + if found != nil { + return []*yaml.Node{found} + } + return []*yaml.Node{} + case segmentKindRecursivePropertyName: + return recursivePropertyNames(idx, value) + case segmentKindParent: + parent := idx.getParentNode(value) + if parent != nil { + return []*yaml.Node{parent} + } + return []*yaml.Node{} + } + panic("no segment type") +} + +func recursivePropertyNames(idx index, value *yaml.Node) []*yaml.Node { + var result []*yaml.Node + if current := idx.getPropertyKey(value); current != nil { + result = append(result, current) + } + var walk func(*yaml.Node) + walk = func(node *yaml.Node) { + if node == nil { + return + } + switch node.Kind { + case yaml.MappingNode: + for i := 0; i+1 < len(node.Content); i += 2 { + key, child := node.Content[i], node.Content[i+1] + idx.setPropertyKey(key, node) + idx.setPropertyKey(child, key) + result = append(result, key) + walk(child) + } + case yaml.SequenceNode: + for _, child := range node.Content { + walk(child) + } + } + } + walk(value) + return result } func unique(nodes []*yaml.Node) []*yaml.Node { - // stably returns a new slice containing only the unique elements from nodes - res := make([]*yaml.Node, 0) - seen := make(map[*yaml.Node]bool) - for _, node := range nodes { - if _, ok := seen[node]; !ok { - res = append(res, node) - seen[node] = true - } - } - return res + res := make([]*yaml.Node, 0) + seen := make(map[*yaml.Node]bool) + for _, node := range nodes { + if _, ok := seen[node]; !ok { + res = append(res, node) + seen[node] = true + } + } + return res } func (s innerSegment) Query(idx index, value *yaml.Node, root *yaml.Node) []*yaml.Node { - result := []*yaml.Node{} - trackParents := parentTrackingEnabled(idx) - - switch s.kind { - case segmentDotWildcard: - // Check for inherited pending segment from previous wildcard/slice - var inheritedPending string - if fc, ok := idx.(FilterContext); ok { - inheritedPending = fc.GetAndClearPendingPathSegment(value) - } - - switch value.Kind { - case yaml.MappingNode: - for i, child := range value.Content { - if i%2 == 1 { - keyNode := value.Content[i-1] - idx.setPropertyKey(keyNode, value) - idx.setPropertyKey(child, keyNode) - if trackParents { - idx.setParentNode(child, value) - } - // Track pending path segment and property name for this node - if fc, ok := idx.(FilterContext); ok { - thisSegment := normalizePathSegment(keyNode.Value) - fc.SetPendingPathSegment(child, inheritedPending+thisSegment) - fc.SetPendingPropertyName(child, keyNode.Value) // For @parentProperty - } - result = append(result, child) - } - } - case yaml.SequenceNode: - for i, child := range value.Content { - if trackParents { - idx.setParentNode(child, value) - } - // Track pending path segment and property name for this node - if fc, ok := idx.(FilterContext); ok { - thisSegment := normalizeIndexSegment(i) - fc.SetPendingPathSegment(child, inheritedPending+thisSegment) - fc.SetPendingPropertyName(child, strconv.Itoa(i)) // For @parentProperty (array index as string) - } - result = append(result, child) - } - } - return result - case segmentDotMemberName: - if value.Kind == yaml.MappingNode { - // Check for inherited pending segment from wildcard/slice - var inheritedPending string - if fc, ok := idx.(FilterContext); ok { - inheritedPending = fc.GetAndClearPendingPathSegment(value) - } - - for i := 0; i < len(value.Content); i += 2 { - key := value.Content[i] - val := value.Content[i+1] - - if key.Value == s.dotName { - idx.setPropertyKey(key, value) - idx.setPropertyKey(val, key) - if trackParents { - idx.setParentNode(val, value) - } - if fc, ok := idx.(FilterContext); ok { - thisSegment := normalizePathSegment(key.Value) - if inheritedPending != "" { - // Propagate combined pending to result for later consumption - fc.SetPendingPathSegment(val, inheritedPending+thisSegment) - } else { - // No wildcard ancestry - push directly to path - fc.PushPathSegment(thisSegment) - } - fc.SetPropertyName(key.Value) - } - result = append(result, val) - break - } - } - } - - case segmentLongHand: - for _, selector := range s.selectors { - result = append(result, selector.Query(idx, value, root)...) - } - default: - panic("unknown child segment kind") - } - - return result + result := []*yaml.Node{} + trackParents := parentTrackingEnabled(idx) + + switch s.kind { + case segmentDotWildcard: + var inheritedPending string + if fc, ok := idx.(FilterContext); ok { + inheritedPending = fc.GetAndClearPendingPathSegment(value) + } + + switch value.Kind { + case yaml.MappingNode: + for i, child := range value.Content { + if i%2 == 1 { + keyNode := value.Content[i-1] + idx.setPropertyKey(keyNode, value) + idx.setPropertyKey(child, keyNode) + if trackParents { + idx.setParentNode(child, value) + } + if fc, ok := idx.(FilterContext); ok { + thisSegment := normalizePathSegment(keyNode.Value) + fc.SetPendingPathSegment(child, inheritedPending+thisSegment) + fc.SetPendingPropertyName(child, keyNode.Value) + } + result = append(result, child) + } + } + case yaml.SequenceNode: + for i, child := range value.Content { + if trackParents { + idx.setParentNode(child, value) + } + if fc, ok := idx.(FilterContext); ok { + thisSegment := normalizeIndexSegment(i) + fc.SetPendingPathSegment(child, inheritedPending+thisSegment) + fc.SetPendingPropertyName(child, strconv.Itoa(i)) + } + result = append(result, child) + } + } + return result + case segmentDotMemberName: + if value.Kind == yaml.MappingNode { + var inheritedPending string + if fc, ok := idx.(FilterContext); ok { + inheritedPending = fc.GetAndClearPendingPathSegment(value) + } + + for i := 0; i < len(value.Content); i += 2 { + key := value.Content[i] + val := value.Content[i+1] + + if key.Value == s.dotName { + idx.setPropertyKey(key, value) + idx.setPropertyKey(val, key) + if trackParents { + idx.setParentNode(val, value) + } + if fc, ok := idx.(FilterContext); ok { + thisSegment := normalizePathSegment(key.Value) + if inheritedPending != "" { + fc.SetPendingPathSegment(val, inheritedPending+thisSegment) + } else { + fc.PushPathSegment(thisSegment) + } + fc.SetPropertyName(key.Value) + } + result = append(result, val) + break + } + } + } + case segmentLongHand: + for _, selector := range s.selectors { + result = append(result, selector.Query(idx, value, root)...) + } + default: + panic("unknown child segment kind") + } + + return result } 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 - } - var inheritedPending string - if hasFc { - inheritedPending = fc.GetAndClearPendingPathSegment(value) - } - - var key string - for i, child := range value.Content { - if i%2 == 0 { - key = child.Value - continue - } - if key == s.name && i%2 == 1 { - idx.setPropertyKey(value.Content[i], value.Content[i-1]) - idx.setPropertyKey(value.Content[i-1], value) - if trackParents { - idx.setParentNode(child, value) - } - if hasFc { - thisSegment := normalizePathSegment(key) - if inheritedPending != "" { - fc.SetPendingPathSegment(child, inheritedPending+thisSegment) - } else { - fc.PushPathSegment(thisSegment) - } - fc.SetPropertyName(key) - } - return []*yaml.Node{child} - } - } - 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 - } - var inheritedPending string - if hasFc { - inheritedPending = fc.GetAndClearPendingPathSegment(value) - } - - var child *yaml.Node - var actualIndex int - if s.index < 0 { - actualIndex = int(int64(len(value.Content)) + s.index) - child = value.Content[actualIndex] - } else { - actualIndex = int(s.index) - child = value.Content[s.index] - } - if trackParents { - idx.setParentNode(child, value) - } - if hasFc { - thisSegment := normalizeIndexSegment(actualIndex) - if inheritedPending != "" { - fc.SetPendingPathSegment(child, inheritedPending+thisSegment) - } else { - fc.PushPathSegment(thisSegment) - } - } - return []*yaml.Node{child} - case selectorSubKindWildcard: - var inheritedPending string - if hasFc { - inheritedPending = fc.GetAndClearPendingPathSegment(value) - } - - if value.Kind == yaml.SequenceNode { - for i, child := range value.Content { - if trackParents { - idx.setParentNode(child, value) - } - if hasFc { - thisSegment := normalizeIndexSegment(i) - fc.SetPendingPathSegment(child, inheritedPending+thisSegment) - fc.SetPendingPropertyName(child, strconv.Itoa(i)) - } - } - return value.Content - } else if value.Kind == yaml.MappingNode { - var result []*yaml.Node - for i, child := range value.Content { - if i%2 == 1 { - keyNode := value.Content[i-1] - idx.setPropertyKey(keyNode, value) - idx.setPropertyKey(child, keyNode) - if trackParents { - idx.setParentNode(child, value) - } - if hasFc { - thisSegment := normalizePathSegment(keyNode.Value) - fc.SetPendingPathSegment(child, inheritedPending+thisSegment) - fc.SetPendingPropertyName(child, keyNode.Value) - } - result = append(result, child) - } - } - return result - } - return nil - case selectorSubKindArraySlice: - if value.Kind != yaml.SequenceNode { - return nil - } - if len(value.Content) == 0 { - return nil - } - var inheritedPending string - if hasFc { - inheritedPending = fc.GetAndClearPendingPathSegment(value) - } - - step := int64(1) - if s.slice.step != nil { - step = *s.slice.step - } - if step == 0 { - return nil - } - - start, end := s.slice.start, s.slice.end - lower, upper := bounds(start, end, step, int64(len(value.Content))) - - var result []*yaml.Node - if step > 0 { - for i := lower; i < upper; i += step { - child := value.Content[i] - if trackParents { - idx.setParentNode(child, value) - } - if hasFc { - thisSegment := normalizeIndexSegment(int(i)) - fc.SetPendingPathSegment(child, inheritedPending+thisSegment) - fc.SetPendingPropertyName(child, strconv.Itoa(int(i))) - } - result = append(result, child) - } - } else { - for i := upper; i > lower; i += step { - child := value.Content[i] - if trackParents { - idx.setParentNode(child, value) - } - if hasFc { - thisSegment := normalizeIndexSegment(int(i)) - fc.SetPendingPathSegment(child, inheritedPending+thisSegment) - fc.SetPendingPropertyName(child, strconv.Itoa(int(i))) - } - result = append(result, child) - } - } - - return result - case selectorSubKindFilter: - var result []*yaml.Node - var parentPropName string - var pushedPendingSegment bool - if hasFc { - if pendingPropName := fc.GetAndClearPendingPropertyName(value); pendingPropName != "" { - parentPropName = pendingPropName - } else { - parentPropName = fc.PropertyName() - } - if pendingSeg := fc.GetAndClearPendingPathSegment(value); pendingSeg != "" { - fc.PushPathSegment(pendingSeg) - pushedPendingSegment = true - } - } - switch value.Kind { - case yaml.MappingNode: - for i := 1; i < len(value.Content); i += 2 { - keyNode := value.Content[i-1] - valueNode := value.Content[i] - idx.setPropertyKey(keyNode, value) - idx.setPropertyKey(valueNode, keyNode) - if trackParents { - idx.setParentNode(valueNode, value) - } - - if hasFc { - fc.SetParentPropertyName(parentPropName) - fc.SetPropertyName(keyNode.Value) - fc.SetParent(value) - fc.SetIndex(-1) - fc.PushPathSegment(normalizePathSegment(keyNode.Value)) - } - - if s.filter.Matches(idx, valueNode, root) { - result = append(result, valueNode) - } - - if hasFc { - fc.PopPathSegment() - } - } - case yaml.SequenceNode: - for i, child := range value.Content { - if trackParents { - idx.setParentNode(child, value) - } - - if hasFc { - fc.SetParentPropertyName(parentPropName) - fc.SetPropertyName(strconv.Itoa(i)) - fc.SetParent(value) - fc.SetIndex(i) - fc.PushPathSegment(normalizeIndexSegment(i)) - } - - if s.filter.Matches(idx, child, root) { - result = append(result, child) - } - - if hasFc { - fc.PopPathSegment() - } - } - } - if pushedPendingSegment { - if hasFc { - fc.PopPathSegment() - } - } - return result - } - return nil + trackParents := parentTrackingEnabled(idx) + fc, hasFc := idx.(FilterContext) + + switch s.kind { + case selectorSubKindName: + if value.Kind != yaml.MappingNode { + return nil + } + var inheritedPending string + if hasFc { + inheritedPending = fc.GetAndClearPendingPathSegment(value) + } + + var key string + for i, child := range value.Content { + if i%2 == 0 { + key = child.Value + continue + } + if key == s.name && i%2 == 1 { + idx.setPropertyKey(value.Content[i], value.Content[i-1]) + idx.setPropertyKey(value.Content[i-1], value) + if trackParents { + idx.setParentNode(child, value) + } + if hasFc { + thisSegment := normalizePathSegment(key) + if inheritedPending != "" { + fc.SetPendingPathSegment(child, inheritedPending+thisSegment) + } else { + fc.PushPathSegment(thisSegment) + } + fc.SetPropertyName(key) + } + return []*yaml.Node{child} + } + } + case selectorSubKindArrayIndex: + if s.jsonPathPlus && value.Kind == yaml.MappingNode && s.index >= 0 { + 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) + } + if hasFc && s.spectral { + fc.SetPropertyName(keyStr) + } + 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 + } + var inheritedPending string + if hasFc { + inheritedPending = fc.GetAndClearPendingPathSegment(value) + } + + var child *yaml.Node + var actualIndex int + if s.index < 0 { + actualIndex = int(int64(len(value.Content)) + s.index) + child = value.Content[actualIndex] + } else { + actualIndex = int(s.index) + child = value.Content[s.index] + } + if trackParents { + idx.setParentNode(child, value) + } + if hasFc { + thisSegment := normalizeIndexSegment(actualIndex) + if inheritedPending != "" { + fc.SetPendingPathSegment(child, inheritedPending+thisSegment) + } else { + fc.PushPathSegment(thisSegment) + } + if s.spectral { + fc.SetPropertyName(strconv.Itoa(actualIndex)) + } + } + return []*yaml.Node{child} + case selectorSubKindWildcard: + var inheritedPending string + if hasFc { + inheritedPending = fc.GetAndClearPendingPathSegment(value) + } + + if value.Kind == yaml.SequenceNode { + for i, child := range value.Content { + if trackParents { + idx.setParentNode(child, value) + } + if hasFc { + thisSegment := normalizeIndexSegment(i) + fc.SetPendingPathSegment(child, inheritedPending+thisSegment) + fc.SetPendingPropertyName(child, strconv.Itoa(i)) + } + } + return value.Content + } else if value.Kind == yaml.MappingNode { + var result []*yaml.Node + for i, child := range value.Content { + if i%2 == 1 { + keyNode := value.Content[i-1] + idx.setPropertyKey(keyNode, value) + idx.setPropertyKey(child, keyNode) + if trackParents { + idx.setParentNode(child, value) + } + if hasFc { + thisSegment := normalizePathSegment(keyNode.Value) + fc.SetPendingPathSegment(child, inheritedPending+thisSegment) + fc.SetPendingPropertyName(child, keyNode.Value) + } + result = append(result, child) + } + } + return result + } + return nil + case selectorSubKindArraySlice: + if value.Kind != yaml.SequenceNode { + return nil + } + if len(value.Content) == 0 { + return nil + } + var inheritedPending string + if hasFc { + inheritedPending = fc.GetAndClearPendingPathSegment(value) + } + + step := int64(1) + if s.slice.step != nil { + step = *s.slice.step + } + if step == 0 { + return nil + } + + start, end := s.slice.start, s.slice.end + lower, upper := bounds(start, end, step, int64(len(value.Content))) + + var result []*yaml.Node + if step > 0 { + for i := lower; i < upper; i += step { + child := value.Content[i] + if trackParents { + idx.setParentNode(child, value) + } + if hasFc { + thisSegment := normalizeIndexSegment(int(i)) + fc.SetPendingPathSegment(child, inheritedPending+thisSegment) + fc.SetPendingPropertyName(child, strconv.Itoa(int(i))) + } + result = append(result, child) + } + } else { + for i := upper; i > lower; i += step { + child := value.Content[i] + if trackParents { + idx.setParentNode(child, value) + } + if hasFc { + thisSegment := normalizeIndexSegment(int(i)) + fc.SetPendingPathSegment(child, inheritedPending+thisSegment) + fc.SetPendingPropertyName(child, strconv.Itoa(int(i))) + } + result = append(result, child) + } + } + + return result + case selectorSubKindFilter: + var result []*yaml.Node + var parentPropName string + var pushedPendingSegment bool + if hasFc { + if pendingPropName := fc.GetAndClearPendingPropertyName(value); pendingPropName != "" { + parentPropName = pendingPropName + } else { + parentPropName = fc.PropertyName() + } + if pendingSeg := fc.GetAndClearPendingPathSegment(value); pendingSeg != "" { + fc.PushPathSegment(pendingSeg) + pushedPendingSegment = true + } + } + switch value.Kind { + case yaml.MappingNode: + for i := 1; i < len(value.Content); i += 2 { + keyNode := value.Content[i-1] + valueNode := value.Content[i] + idx.setPropertyKey(keyNode, value) + idx.setPropertyKey(valueNode, keyNode) + if trackParents { + idx.setParentNode(valueNode, value) + } + + if hasFc { + fc.SetParentPropertyName(parentPropName) + fc.SetPropertyName(keyNode.Value) + fc.SetParent(value) + fc.SetIndex(-1) + fc.PushPathSegment(normalizePathSegment(keyNode.Value)) + } + + if s.filter.Matches(idx, valueNode, root) { + result = append(result, valueNode) + } + + if hasFc { + fc.PopPathSegment() + } + } + case yaml.SequenceNode: + for i, child := range value.Content { + if trackParents { + idx.setParentNode(child, value) + } + + if hasFc { + fc.SetParentPropertyName(parentPropName) + fc.SetPropertyName(strconv.Itoa(i)) + fc.SetParent(value) + fc.SetIndex(i) + fc.PushPathSegment(normalizeIndexSegment(i)) + } + + if s.filter.Matches(idx, child, root) { + result = append(result, child) + } + + if hasFc { + fc.PopPathSegment() + } + } + } + if pushedPendingSegment { + if hasFc { + fc.PopPathSegment() + } + } + return result + } + return nil } func normalize(i, length int64) int64 { - if i >= 0 { - return i - } - return length + i + if i >= 0 { + return i + } + return length + i } func bounds(start, end *int64, step, length int64) (int64, int64) { - var nStart, nEnd int64 - if start != nil { - nStart = normalize(*start, length) - } else if step > 0 { - nStart = 0 - } else { - nStart = length - 1 - } - if end != nil { - nEnd = normalize(*end, length) - } else if step > 0 { - nEnd = length - } else { - nEnd = -1 - } - - var lower, upper int64 - if step >= 0 { - lower = max(min(nStart, length), 0) - upper = min(max(nEnd, 0), length) - } else { - upper = min(max(nStart, -1), length-1) - lower = min(max(nEnd, -1), length-1) - } - - return lower, upper + var nStart, nEnd int64 + if start != nil { + nStart = normalize(*start, length) + } else if step > 0 { + nStart = 0 + } else { + nStart = length - 1 + } + if end != nil { + nEnd = normalize(*end, length) + } else if step > 0 { + nEnd = length + } else { + nEnd = -1 + } + + var lower, upper int64 + if step >= 0 { + lower = max(min(nStart, length), 0) + upper = min(max(nEnd, 0), length) + } else { + upper = min(max(nStart, -1), length-1) + lower = min(max(nEnd, -1), length-1) + } + + return lower, upper } func (s filterSelector) Matches(idx index, node *yaml.Node, root *yaml.Node) bool { - return s.expression.Matches(idx, node, root) + if s.spectralExpression != nil { + return s.spectralExpression.Matches(idx, node, root) + } + return s.expression.Matches(idx, node, root) } func (e logicalOrExpr) Matches(idx index, node *yaml.Node, root *yaml.Node) bool { - for _, expr := range e.expressions { - if expr.Matches(idx, node, root) { - return true - } - } - return false + for _, expr := range e.expressions { + if expr.Matches(idx, node, root) { + return true + } + } + return false } func (e logicalAndExpr) Matches(idx index, node *yaml.Node, root *yaml.Node) bool { - for _, expr := range e.expressions { - if !expr.Matches(idx, node, root) { - return false - } - } - return true + for _, expr := range e.expressions { + if !expr.Matches(idx, node, root) { + return false + } + } + return true } func (e basicExpr) Matches(idx index, node *yaml.Node, root *yaml.Node) bool { - if e.parenExpr != nil { - result := e.parenExpr.expr.Matches(idx, node, root) - if e.parenExpr.not { - return !result - } - return result - } else if e.comparisonExpr != nil { - return e.comparisonExpr.Matches(idx, node, root) - } else if e.testExpr != nil { - return e.testExpr.Matches(idx, node, root) - } - return false + if e.parenExpr != nil { + result := e.parenExpr.expr.Matches(idx, node, root) + if e.parenExpr.not { + return !result + } + return result + } else if e.comparisonExpr != nil { + return e.comparisonExpr.Matches(idx, node, root) + } else if e.testExpr != nil { + return e.testExpr.Matches(idx, node, root) + } + return false } func (e comparisonExpr) Matches(idx index, node *yaml.Node, root *yaml.Node) bool { - leftValue := e.left.Evaluate(idx, node, root) - rightValue := e.right.Evaluate(idx, node, root) - - switch e.op { - case equalTo: - return leftValue.Equals(rightValue) - case notEqualTo: - return !leftValue.Equals(rightValue) - case lessThan: - return leftValue.LessThan(rightValue) - case lessThanEqualTo: - return leftValue.LessThanOrEqual(rightValue) - case greaterThan: - return rightValue.LessThan(leftValue) - case greaterThanEqualTo: - return rightValue.LessThanOrEqual(leftValue) - default: - return false - } + leftValue := e.left.Evaluate(idx, node, root) + rightValue := e.right.Evaluate(idx, node, root) + + switch e.op { + case equalTo: + return leftValue.Equals(rightValue) + case notEqualTo: + return !leftValue.Equals(rightValue) + case lessThan: + return leftValue.LessThan(rightValue) + case lessThanEqualTo: + return leftValue.LessThanOrEqual(rightValue) + case greaterThan: + return rightValue.LessThan(leftValue) + case greaterThanEqualTo: + return rightValue.LessThanOrEqual(leftValue) + default: + return false + } } func (e testExpr) Matches(idx index, node *yaml.Node, root *yaml.Node) bool { - var result bool - if e.filterQuery != nil { - result = len(e.filterQuery.Query(idx, node, root)) > 0 - } else if e.functionExpr != nil { - funcResult := e.functionExpr.Evaluate(idx, node, root) - if funcResult.bool != nil { - result = *funcResult.bool - } else if funcResult.null == nil { - result = true - } - } - if e.not { - return !result - } - return result + var result bool + if e.filterQuery != nil { + result = len(e.filterQuery.Query(idx, node, root)) > 0 + } else if e.functionExpr != nil { + funcResult := e.functionExpr.Evaluate(idx, node, root) + if funcResult.bool != nil { + result = *funcResult.bool + } else if funcResult.null == nil { + result = true + } + } + if e.not { + return !result + } + return result } func (q filterQuery) Query(idx index, node *yaml.Node, root *yaml.Node) []*yaml.Node { - if q.relQuery != nil { - return q.relQuery.Query(idx, node, root) - } - if q.jsonPathQuery != nil { - return q.jsonPathQuery.Query(node, root) - } - return nil + if q.relQuery != nil { + return q.relQuery.Query(idx, node, root) + } + if q.jsonPathQuery != nil { + return q.jsonPathQuery.Query(node, root) + } + return nil } func (q relQuery) Query(idx index, node *yaml.Node, root *yaml.Node) []*yaml.Node { - result := []*yaml.Node{node} - for _, seg := range q.segments { - var newResult []*yaml.Node - for _, value := range result { - newResult = append(newResult, seg.Query(idx, value, root)...) - } - result = newResult - } - return result + result := []*yaml.Node{node} + for _, seg := range q.segments { + var newResult []*yaml.Node + for _, value := range result { + newResult = append(newResult, seg.Query(idx, value, root)...) + } + result = newResult + } + return result } func (q absQuery) Query(idx index, node *yaml.Node, root *yaml.Node) []*yaml.Node { - result := []*yaml.Node{root} - for _, seg := range q.segments { - var newResult []*yaml.Node - for _, value := range result { - newResult = append(newResult, seg.Query(idx, value, root)...) - } - result = newResult - } - return result + result := []*yaml.Node{root} + for _, seg := range q.segments { + var newResult []*yaml.Node + for _, value := range result { + newResult = append(newResult, seg.Query(idx, value, root)...) + } + result = newResult + } + return result } diff --git a/vendor/modules.txt b/vendor/modules.txt index 717f5f7c6..8651d92d1 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -650,7 +650,7 @@ github.com/openshift/library-go/pkg/crypto github.com/openshift/machine-config-operator/pkg/apis/machineconfiguration.openshift.io github.com/openshift/machine-config-operator/pkg/apis/machineconfiguration.openshift.io/v1 github.com/openshift/machine-config-operator/pkg/daemon/constants -# github.com/pb33f/jsonpath v0.8.2 +# github.com/pb33f/jsonpath v0.8.3 ## explicit; go 1.24 github.com/pb33f/jsonpath/pkg/jsonpath github.com/pb33f/jsonpath/pkg/jsonpath/config