janeway-jsonpath 0.6.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/lib/janeway/ast/array_slice_selector.rb +6 -90
  4. data/lib/janeway/ast/binary_operator.rb +57 -51
  5. data/lib/janeway/ast/child_segment.rb +3 -5
  6. data/lib/janeway/ast/current_node.rb +2 -15
  7. data/lib/janeway/ast/descendant_segment.rb +0 -3
  8. data/lib/janeway/ast/expression.rb +35 -5
  9. data/lib/janeway/ast/function.rb +15 -10
  10. data/lib/janeway/ast/name_selector.rb +12 -7
  11. data/lib/janeway/ast/root_node.rb +2 -15
  12. data/lib/janeway/ast/selector.rb +5 -6
  13. data/lib/janeway/ast/unary_operator.rb +12 -2
  14. data/lib/janeway/ast/wildcard_selector.rb +2 -2
  15. data/lib/janeway/enumerator.rb +14 -7
  16. data/lib/janeway/error.rb +6 -1
  17. data/lib/janeway/functions/count.rb +1 -8
  18. data/lib/janeway/functions/length.rb +1 -12
  19. data/lib/janeway/functions/match.rb +3 -8
  20. data/lib/janeway/functions/search.rb +3 -8
  21. data/lib/janeway/functions/value.rb +1 -7
  22. data/lib/janeway/functions.rb +75 -2
  23. data/lib/janeway/interpreter.rb +50 -35
  24. data/lib/janeway/interpreters/array_slice_selector_delete_if.rb +10 -13
  25. data/lib/janeway/interpreters/array_slice_selector_interpreter.rb +56 -13
  26. data/lib/janeway/interpreters/binary_operator_interpreter.rb +18 -13
  27. data/lib/janeway/interpreters/descendant_segment_interpreter.rb +32 -13
  28. data/lib/janeway/interpreters/filter_selector_interpreter.rb +26 -34
  29. data/lib/janeway/interpreters/function_interpreter.rb +5 -1
  30. data/lib/janeway/interpreters/index_selector_delete_if.rb +1 -0
  31. data/lib/janeway/interpreters/index_selector_interpreter.rb +3 -1
  32. data/lib/janeway/interpreters/iteration_helper.rb +19 -5
  33. data/lib/janeway/interpreters/root_node_delete_if.rb +57 -16
  34. data/lib/janeway/interpreters/tree_constructor.rb +0 -20
  35. data/lib/janeway/interpreters/wildcard_selector_delete_if.rb +16 -0
  36. data/lib/janeway/interpreters/yielder.rb +8 -2
  37. data/lib/janeway/lexer.rb +66 -77
  38. data/lib/janeway/location.rb +3 -1
  39. data/lib/janeway/normalized_path.rb +23 -7
  40. data/lib/janeway/parser.rb +46 -34
  41. data/lib/janeway/query.rb +48 -5
  42. data/lib/janeway/token.rb +2 -5
  43. data/lib/janeway/version.rb +1 -1
  44. data/lib/janeway.rb +16 -3
  45. metadata +3 -10
  46. data/lib/janeway/interpreters/array_slice_selector_deleter.rb +0 -41
  47. data/lib/janeway/interpreters/child_segment_deleter.rb +0 -19
  48. data/lib/janeway/interpreters/filter_selector_deleter.rb +0 -65
  49. data/lib/janeway/interpreters/index_selector_deleter.rb +0 -26
  50. data/lib/janeway/interpreters/name_selector_deleter.rb +0 -27
  51. data/lib/janeway/interpreters/root_node_deleter.rb +0 -34
  52. data/lib/janeway/interpreters/wildcard_selector_deleter.rb +0 -32
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 33fc9254c0443121404ced6b9e60c15133111299b8f7052f012fe45d7ad4d281
4
- data.tar.gz: c5a9eea01a8a332343b47328c64991a52fc50292969247433650914c8af727bd
3
+ metadata.gz: ff463dd64e17811dddaa23fb3fb82c9bc5cb728dd5b288574f39010174c4f705
4
+ data.tar.gz: 27f5f8d6c39435fb4ec97eb8a96c7e204c2f06a1e3e7dc569b606d4a52b5cf58
5
5
  SHA512:
6
- metadata.gz: 0c101d0e02380fc56bd4abc83b5012965e3281aabc3bcce10e7f55c6cff61ba5b825f0d582f95c076234e01d59f07a8db19e6b8984c85e923c780d6f362adc2c
7
- data.tar.gz: e2a762c773653c01415dee8f988b95be53d45da8bb3d1ed6f20817eb333fce00b3c960f9268a2212c929a27582d4c0cdb8e480e3948b2e193974f452b32e526f
6
+ metadata.gz: 113d48240302143e72de6249d1975d0be52074374e94abd0d2a340e54867031653d6f912e0ccb3ab25e0b9734a3d33119bea1fb100c4283bce95560f9c02ad69
7
+ data.tar.gz: cfdca8a82c1c38f7b3b646afcca7c54d6d996564f2f191cb9cd407fa5abcac8c258c4d4534ce7cd437739174298fda0c66b3d41602e4058a47b7f5d94e4b1208
data/README.md CHANGED
@@ -29,7 +29,7 @@ Install the gem from the command-line:
29
29
  or add it to your Gemfile:
30
30
 
31
31
  ```
32
- gem 'janeway-jsonpath', '~> 0.4.0'
32
+ gem 'janeway-jsonpath', '~> 0.6'
33
33
  ```
34
34
 
35
35
  ### Usage
@@ -22,6 +22,11 @@ module Janeway
22
22
  # "explicit" arguments, since they're interpreted differently.
23
23
  #
24
24
  class ArraySliceSelector < Janeway::AST::Selector
25
+ # Raw start / end / step values from the query source. Any of them may
26
+ # be nil, meaning "use the default for the current step direction" —
27
+ # the interpreter resolves that.
28
+ attr_reader :start, :end, :step
29
+
25
30
  # @param start [Integer, nil]
26
31
  # @param end_ [Integer, nil]
27
32
  # @param step [Integer, nil]
@@ -40,102 +45,13 @@ module Janeway
40
45
  raise Error, "Array slice selector value too large: #{arg.inspect}" if arg > INTEGER_MAX
41
46
  end
42
47
 
43
- # Nil values are kept to indicate that the "default" values is to be used.
48
+ # Nil values are kept to indicate that the default value should be used.
44
49
  # The interpreter selects the actual values.
45
50
  @start = start
46
51
  @end = end_
47
52
  @step = step
48
53
  end
49
54
 
50
- # Return the step size to use for stepping through the array.
51
- # Defaults to 1 if it was not given to the constructor.
52
- #
53
- # @return [Integer]
54
- def step
55
- # The iteration behavior of jsonpath does not match that of ruby Integer#step.
56
- # Support the behavior of Integer#step, which needs this:
57
- # 1. for stepping forward between positive numbers, use a positive number
58
- # 2. for stepping backward between positive numbers, use a negative number
59
- # 3. for stepping backward from positive to negative, use a negative number
60
- # 4. for stepping backward from negative to negative, use a positive number
61
- # Case #4 has to be detected and the sign of step inverted
62
- @step || 1
63
- end
64
-
65
- # Return the start index to use for stepping through the array, based on a specified array size
66
- #
67
- # @param input_size [Integer]
68
- # @return [Integer]
69
- def upper_index(input_size)
70
- calculate_index_values(input_size).last
71
- end
72
-
73
- # Return the end index to use for stepping through the array, based on a specified array size
74
- # End index is calculated to omit the final index value, as per the RFC.
75
- #
76
- # @param input_size [Integer]
77
- # @return [Integer]
78
- def lower_index(input_size)
79
- calculate_index_values(input_size).first
80
- end
81
-
82
- # Assign lower and upper bounds to instance variables, based on the input array size.
83
- # @see https://www.rfc-editor.org/rfc/rfc9535.html#section-2.3.4.2.2-3
84
- #
85
- # @param input_size [Integer]
86
- def calculate_index_values(input_size)
87
- if step >= 0
88
- start = @start || 0
89
- end_ = @end || input_size
90
- else
91
- start = @start || (input_size - 1)
92
- end_ = @end || ((-1 * input_size) - 1)
93
- end
94
-
95
- bounds(start, end_, step, input_size)
96
- end
97
-
98
- # NOTE: Conversion of start:end:step to array indexes is defined as pseudocode
99
- # in the IETF spec.
100
- # The methods #normalize and #bounds are ruby implementations of that code.
101
- # Don't make changes here without referring to the original code in the spec.
102
- # @see https://www.rfc-editor.org/rfc/rfc9535.html#section-2.3.4.2.2-6
103
-
104
- # IETF: Slice expression parameters start and end are not directly usable
105
- # as slice bounds and must first be normalized.
106
- #
107
- # @param index [Integer]
108
- # @param len [Integer]
109
- def normalize(index, len)
110
- return index if index >= 0
111
-
112
- len + index
113
- end
114
-
115
- # IETF: Slice expression parameters start and end are used to derive
116
- # slice bounds lower and upper. The direction of the iteration, defined
117
- # by the sign of step, determines which of the parameters is the lower
118
- # bound and which is the upper bound:¶
119
- # @see https://www.rfc-editor.org/rfc/rfc9535.html#section-2.3.4.2.2-9
120
- # @param start [Integer] start index, normalized
121
- # @param end_ [Integer] end index, normalized
122
- # @param step [Integer] step value
123
- # @param len [Integer] length of input array
124
- def bounds(start, end_, step, len)
125
- n_start = normalize(start, len)
126
- n_end = normalize(end_, len)
127
-
128
- if step >= 0
129
- lower = n_start.clamp(0, len)
130
- upper = n_end.clamp(0, len)
131
- else
132
- upper = n_start.clamp(-1, len - 1)
133
- lower = n_end.clamp(-1, len - 1)
134
- end
135
-
136
- [lower, upper]
137
- end
138
-
139
55
  # @param brackets [Boolean] add surrounding brackets if true
140
56
  # @return [String]
141
57
  def to_s(brackets: true, **_ignored)
@@ -5,48 +5,31 @@ module Janeway
5
5
  # AST node for binary operators:
6
6
  # == != <= >= < > || &&
7
7
  class BinaryOperator < Janeway::AST::Expression
8
- attr_reader :name, :left, :right
8
+ attr_reader :name
9
+ attr_accessor :left, :right
9
10
 
10
11
  def initialize(operator, left = nil, right = nil)
11
12
  super(nil)
12
13
  raise ArgumentError, "expect symbol, got #{operator.inspect}" unless operator.is_a?(Symbol)
13
14
 
14
15
  @name = operator # eg. :equal
15
- self.left = left if left
16
- self.right = right if right
16
+ @left = left
17
+ @right = right
17
18
  end
18
19
 
19
- # Set the left-hand-side expression
20
- # @param expr [AST::Expression]
21
- def left=(expr)
22
- if comparison_operator? && !(expr.literal? || expr.singular_query?)
23
- raise Error, "Expression #{expr} does not produce a singular value for #{operator_to_s} comparison"
24
- elsif comparison_operator? && expr.is_a?(AST::Function) && !expr.literal?
25
- msg = "Function #{expr} returns a non-comparable value which is not usable for #{operator_to_s} comparison"
26
- raise Error, msg
27
- end
28
-
29
- # Compliance test suite requires error for this, but don't have go so far as to bar every literal
30
- if expr.is_a?(Boolean) && right.is_a?(Boolean)
31
- raise Error, "Literal \"#{expr}\" must be compared to an expression, not another literal (\"#{left}\")"
32
- end
33
-
34
- @left = expr
35
- end
36
-
37
- # Set the left-hand-side expression
38
- # @param expr [AST::Expression]
39
- def right=(expr)
40
- if comparison_operator? && !(expr.literal? || expr.singular_query?)
41
- raise Error, "Expression #{expr} does not produce a singular value for #{operator_to_s} comparison"
42
- end
43
-
44
- # Compliance test suite requires error for this, but don't have go so far as to bar every literal
45
- if expr.is_a?(Boolean) && left.is_a?(Boolean)
46
- raise Error, "Literal \"#{expr}\" must be compared to an expression, not another literal (\"#{left}\")"
47
- end
48
-
49
- @right = expr
20
+ # Validate the currently-set left and right operands as a well-formed
21
+ # binary expression. Called by the parser once both sides are assigned —
22
+ # the previous per-setter validation was order-dependent (left=
23
+ # couldn't see right and vice-versa) and missed the both-literal case
24
+ # when the parser assigned left first.
25
+ #
26
+ # @raise [Janeway::Error] on any semantic error
27
+ def validate!
28
+ raise Error, 'BinaryOperator requires both left and right' unless @left && @right
29
+
30
+ validate_side(@left)
31
+ validate_side(@right)
32
+ validate_pair
50
33
  end
51
34
 
52
35
  def to_s
@@ -76,31 +59,54 @@ module Janeway
76
59
  operator_type == :logical
77
60
  end
78
61
 
62
+ # Single source of truth for what binary operators exist and how they
63
+ # render / classify. Adding a new operator here fills both call sites.
64
+ OPERATOR_META = {
65
+ and: { str: '&&', type: :logical },
66
+ or: { str: '||', type: :logical },
67
+ equal: { str: '==', type: :comparison },
68
+ not_equal: { str: '!=', type: :comparison },
69
+ less_than: { str: '<', type: :comparison },
70
+ less_than_or_equal: { str: '<=', type: :comparison },
71
+ greater_than: { str: '>', type: :comparison },
72
+ greater_than_or_equal: { str: '>=', type: :comparison },
73
+ }.freeze
74
+
79
75
  private
80
76
 
77
+ def operator_meta
78
+ OPERATOR_META.fetch(name) { raise "unknown binary operator #{name}" }
79
+ end
80
+
81
81
  def operator_to_s
82
- case name
83
- when :and then '&&'
84
- when :equal then '=='
85
- when :greater_than then '>'
86
- when :greater_than_or_equal then '>='
87
- when :less_than then '<'
88
- when :less_than_or_equal then '<='
89
- when :not_equal then '!='
90
- when :or then '||'
91
- else
92
- raise "unknown binary operator #{name}"
93
- end
82
+ operator_meta[:str]
94
83
  end
95
84
 
96
85
  def operator_type
97
- case name
98
- when :and, :or then :logical
99
- when :equal, :not_equal, :greater_than, :greater_than_or_equal, :less_than, :less_than_or_equal then :comparison
100
- else
101
- raise "unknown binary operator #{name}"
86
+ operator_meta[:type]
87
+ end
88
+
89
+ # Per-side checks that apply to either operand of a comparison.
90
+ def validate_side(expr)
91
+ return unless comparison_operator?
92
+
93
+ unless expr.literal? || expr.singular_query?
94
+ raise Error, "Expression #{expr} does not produce a singular value for #{operator_to_s} comparison"
95
+ end
96
+ if expr.is_a?(AST::Function) && !expr.literal?
97
+ raise Error,
98
+ "Function #{expr} returns a non-comparable value which is not usable for #{operator_to_s} comparison"
102
99
  end
103
100
  end
101
+
102
+ # Checks that need both sides in scope.
103
+ def validate_pair
104
+ return unless @left.is_a?(Boolean) && @right.is_a?(Boolean)
105
+
106
+ # Compliance test suite requires error for this, but don't go so far as to bar every literal.
107
+ raise Error,
108
+ "Literal \"#{@right}\" must be compared to an expression, not another literal (\"#{@left}\")"
109
+ end
104
110
  end
105
111
  end
106
112
  end
@@ -15,10 +15,8 @@ module Janeway
15
15
  # $[name1, [1:10]]
16
16
  class ChildSegment < Janeway::AST::Expression
17
17
  extend Forwardable
18
- def_delegators :@value, :size, :first, :last, :each, :map, :empty?
19
18
 
20
- # Subsequent expression that modifies the result of this selector list.
21
- attr_accessor :next
19
+ def_delegators :@value, :size, :first, :last, :each, :map, :empty?
22
20
 
23
21
  def initialize
24
22
  super([]) # @value holds the expressions in the selector
@@ -31,8 +29,8 @@ module Janeway
31
29
  @value << selector
32
30
  end
33
31
 
34
- def to_s(with_child: true)
35
- str = @value.map { |selector| selector.to_s(brackets: false, dot_prefix: false) }.join(', ')
32
+ def to_s(with_child: true, **)
33
+ str = @value.map { |selector| selector.to_s(brackets: false, dot_prefix: false, bracketed: true) }.join(', ')
36
34
  with_child ? "[#{str}]#{@next}" : "[#{str}]"
37
35
  end
38
36
 
@@ -29,9 +29,6 @@ module Janeway
29
29
  #
30
30
  # Construct accepts an optional Selector which will be applied to the "current" node
31
31
  class CurrentNode < Janeway::AST::Expression
32
- # Subsequent expression that modifies the output of this expression
33
- attr_accessor :next
34
-
35
32
  def to_s
36
33
  "@#{@next}"
37
34
  end
@@ -41,17 +38,7 @@ module Janeway
41
38
  #
42
39
  # @return [Boolean]
43
40
  def singular_query?
44
- return true unless @next # there are no following selectors
45
-
46
- selector_types = []
47
- selector = @next
48
- loop do
49
- selector_types << selector.class
50
- selector = selector.next
51
- break unless selector
52
- end
53
- allowed = [AST::IndexSelector, AST::NameSelector]
54
- selector_types.uniq.all? { allowed.include?(_1) }
41
+ chain_of?(AST::IndexSelector, AST::NameSelector)
55
42
  end
56
43
 
57
44
  # True if this is a bare current node operator, without a following expression
@@ -63,7 +50,7 @@ module Janeway
63
50
  # @param level [Integer]
64
51
  # @return [Array]
65
52
  def tree(level)
66
- [indented(level, '@'), @next.tree(indent + 1)]
53
+ [indented(level, '@'), @next&.tree(level + 1)]
67
54
  end
68
55
  end
69
56
  end
@@ -16,9 +16,6 @@ module Janeway
16
16
  # $..[*, *] All values, twice non-deterministic order
17
17
  # $..[0, 1] Multiple segments
18
18
  class DescendantSegment < Janeway::AST::Selector
19
- # Subsequent expression that modifies the result of this selector list.
20
- attr_accessor :next
21
-
22
19
  def to_s
23
20
  "..#{@next&.to_s(dot_prefix: false)}"
24
21
  end
@@ -1,10 +1,14 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require_relative 'helpers'
3
4
  require_relative 'error'
4
5
 
5
6
  module Janeway
6
7
  module AST
7
8
  INDENT = ' '
9
+ # Precomputed indent prefixes so #indented does not do `INDENT * level`
10
+ # (a fresh String allocation) on every line printed.
11
+ INDENTS = Array.new(16) { |i| (INDENT * i).freeze }.freeze
8
12
 
9
13
  # Base class for jsonpath expressions.
10
14
  #
@@ -16,8 +20,10 @@ module Janeway
16
20
  # Value provided by subclass constructor.
17
21
  attr_accessor :value
18
22
 
19
- # Next expression in the AST, if any
20
- attr_reader :next
23
+ # Next expression in the AST, if any. Writable so the parser can link
24
+ # nodes as it constructs the chain, and so Query#dup / #pop can rewire
25
+ # the top-level chain.
26
+ attr_accessor :next
21
27
 
22
28
  def initialize(val = nil)
23
29
  # don't set the instance variable if unused, because it makes the
@@ -27,8 +33,16 @@ module Janeway
27
33
 
28
34
  # @return [String]
29
35
  def type
30
- name = self.class.to_s.split('::').last # eg. Janeway::AST::FunctionCall => "FunctionCall"
31
- Helpers.camelcase_to_underscore(name) # eg. "FunctionCall" => "function_call"
36
+ self.class.type_name
37
+ end
38
+
39
+ # Cached camelcase→underscore transform of the class name.
40
+ # Same value on every instance of the class — compute once per class.
41
+ #
42
+ # @return [String]
43
+ def self.type_name
44
+ @type_name ||=
45
+ Helpers.camelcase_to_underscore(name.split('::').last).freeze
32
46
  end
33
47
 
34
48
  # Return the given message, indented
@@ -37,7 +51,7 @@ module Janeway
37
51
  # @param msg [String]
38
52
  # @return [String]
39
53
  def indented(level, msg)
40
- format('%s%s', (INDENT * level), msg)
54
+ (INDENTS[level] || (INDENT * level)) + msg
41
55
  end
42
56
 
43
57
  # @param level [Integer]
@@ -59,6 +73,22 @@ module Janeway
59
73
  def singular_query?
60
74
  false
61
75
  end
76
+
77
+ # True if every selector in the @next chain is one of the allowed classes.
78
+ # An empty chain (no @next) returns true. Extracted from the identical
79
+ # implementations that used to live in CurrentNode and RootNode.
80
+ #
81
+ # @param allowed_classes [Array<Class>]
82
+ # @return [Boolean]
83
+ def chain_of?(*allowed_classes)
84
+ selector = @next
85
+ while selector
86
+ return false unless allowed_classes.include?(selector.class)
87
+
88
+ selector = selector.next
89
+ end
90
+ true
91
+ end
62
92
  end
63
93
  end
64
94
  end
@@ -5,18 +5,28 @@ require_relative 'expression'
5
5
  module Janeway
6
6
  module AST
7
7
  # Represents a JSONPath built-in function.
8
+ #
9
+ # This is pure structure: name + parameters. The body / literal_return
10
+ # metadata lives in Functions::REGISTRY (looked up by name), so AST nodes
11
+ # don't drag closures over the parser's binding.
8
12
  class Function < Janeway::AST::Expression
9
13
  alias name value
10
14
 
11
- attr_reader :parameters, :body
15
+ attr_reader :parameters
12
16
 
13
- def initialize(name, parameters, &body)
17
+ def initialize(name, parameters)
14
18
  raise ArgumentError, "expect string, got #{name.inspect}" unless name.is_a?(String)
15
19
 
20
+ spec = Functions::REGISTRY[name]
21
+ raise ArgumentError, "unknown function #{name.inspect}" unless spec
22
+ unless spec[:arity] == parameters.size
23
+ raise ArgumentError,
24
+ "function #{name.inspect}: declared arity #{spec[:arity]} does not " \
25
+ "match parameter count #{parameters.size}"
26
+ end
27
+
16
28
  super(name)
17
29
  @parameters = parameters
18
- @body = body
19
- raise "expect body to be a Proc, got #{body.class}" unless body.is_a?(Proc)
20
30
  end
21
31
 
22
32
  def to_s
@@ -39,12 +49,7 @@ module Janeway
39
49
 
40
50
  # True if the function's return value is a literal
41
51
  def literal?
42
- case name
43
- when 'length', 'count', 'value' then true
44
- when 'search', 'match' then false
45
- else
46
- raise "Unknown jsonpath function #{name}"
47
- end
52
+ Functions::REGISTRY.fetch(name)[:literal_return]
48
53
  end
49
54
  end
50
55
  end
@@ -17,16 +17,21 @@ module Janeway
17
17
  raise "Invalid name: #{value.inspect}:#{value.class}" unless value.is_a?(String)
18
18
  end
19
19
 
20
+ # Chars that force bracket notation (they are not permitted in the
21
+ # dotted shorthand).
22
+ SPECIAL_CHARS = /[ .]/
23
+
20
24
  # @param brackets [Boolean] request for bracket syntax
21
25
  # @param dot_prefix [Boolean] include . prefix, if shorthand notation is used
22
- def to_s(brackets: false, dot_prefix: true)
26
+ # @param bracketed [Boolean] caller already provided brackets (e.g. within a union);
27
+ # emit the quoted name only, without adding brackets
28
+ def to_s(brackets: false, dot_prefix: true, bracketed: false)
23
29
  # Add quotes and surrounding brackets if the name includes chars that require quoting.
24
- # These chars are not allowed in dotted notation, only bracket notation.
25
- special_chars = [' ', '.']
26
- brackets ||= special_chars.any? { |char| @value.include?(char) }
27
- if brackets
28
- name_str = quote(@value)
29
- "[#{name_str}]#{@next}"
30
+ brackets ||= @value.match?(SPECIAL_CHARS)
31
+ if bracketed
32
+ "#{quote(@value)}#{@next}"
33
+ elsif brackets
34
+ "[#{quote(@value)}]#{@next}"
30
35
  elsif dot_prefix
31
36
  ".#{@value}#{@next}"
32
37
  else # omit dot prefix after a descendant segment
@@ -14,9 +14,6 @@ module Janeway
14
14
  # $(? $.key1 == $.key2 )
15
15
  #
16
16
  class RootNode < Janeway::AST::Expression
17
- # Subsequent expression that modifies the output of this expression
18
- attr_accessor :next
19
-
20
17
  def to_s
21
18
  "$#{@next}"
22
19
  end
@@ -26,23 +23,13 @@ module Janeway
26
23
  #
27
24
  # @return [Boolean]
28
25
  def singular_query?
29
- return true unless @next # there are no following selectors
30
-
31
- selector_types = []
32
- selector = @next
33
- loop do
34
- selector_types << selector.class
35
- selector = selector&.next
36
- break unless selector
37
- end
38
- allowed = [AST::IndexSelector, AST::NameSelector]
39
- selector_types.uniq.all? { allowed.include?(_1) }
26
+ chain_of?(AST::IndexSelector, AST::NameSelector)
40
27
  end
41
28
 
42
29
  # @param level [Integer]
43
30
  # @return [Array]
44
31
  def tree(level = 0)
45
- [indented(level, '$'), @next.tree(level + 1)]
32
+ [indented(level, '$'), @next&.tree(level + 1)]
46
33
  end
47
34
  end
48
35
  end
@@ -23,12 +23,11 @@ module Janeway
23
23
  # Filter expressions ?<logical-expr> select certain children of an object or array, as in:
24
24
  # $.store.book[?@.price < 10].title
25
25
  class Selector < Janeway::AST::Expression
26
- # Subsequent expression that modifies the result of this selector list.
27
- attr_accessor :next
28
-
29
- def ==(other)
30
- value == other&.value
31
- end
26
+ # Intentionally NO #== override. The previous implementation compared
27
+ # only `value`, ignoring class and @next, which made two selector chains
28
+ # with identical heads but different tails compare equal (and matched
29
+ # across selector types by coincidence of value). Query#== is the
30
+ # correct equality entry point for query comparisons.
32
31
  end
33
32
  end
34
33
  end
@@ -13,13 +13,23 @@ module Janeway
13
13
  end
14
14
 
15
15
  def to_s
16
- "#{@operator} #{operand}"
16
+ "#{operator_to_s}#{operand}"
17
17
  end
18
18
 
19
19
  # @param level [Integer]
20
20
  # @return [Array]
21
21
  def tree(level)
22
- indented(level, "#{operator}#{operand}")
22
+ indented(level, "#{operator_to_s}#{operand}")
23
+ end
24
+
25
+ private
26
+
27
+ def operator_to_s
28
+ case @name
29
+ when :not then '!'
30
+ else
31
+ raise "unknown unary operator #{@name}"
32
+ end
23
33
  end
24
34
  end
25
35
  end
@@ -23,7 +23,7 @@ module Janeway
23
23
  end
24
24
 
25
25
  # @return [String]
26
- def to_s(brackets: false, dot_prefix: true)
26
+ def to_s(brackets: false, dot_prefix: true, **)
27
27
  if brackets
28
28
  "[*]#{@next&.to_s(brackets: true)}"
29
29
  elsif dot_prefix
@@ -36,7 +36,7 @@ module Janeway
36
36
  # @param level [Integer]
37
37
  # @return [Array]
38
38
  def tree(level)
39
- [indented(level, '*'), @next.tree(level + 1)]
39
+ [indented(level, '*'), @next&.tree(level + 1)]
40
40
  end
41
41
  end
42
42
  end
@@ -56,7 +56,7 @@ module Janeway
56
56
  # Assign the given value at every query match.
57
57
  # @param replacement [Object]
58
58
  # @return [void]
59
- def replace(replacement = :no_replacement_value_was_given, &block)
59
+ def replace(replacement = :no_replacement_value_was_given, &_)
60
60
  if replacement != :no_replacement_value_was_given && block_given?
61
61
  raise Janeway::Error.new('#replace needs either replacement value or block, not both', @query)
62
62
  end
@@ -118,6 +118,15 @@ module Janeway
118
118
  end
119
119
  end
120
120
 
121
+ # Return the normalized path of every query match.
122
+ #
123
+ # @see https://www.rfc-editor.org/rfc/rfc9535.html#name-normalized-paths
124
+ #
125
+ # @return [Array<String>] normalized paths, eg. [ "$['key'][0]"] ]
126
+ def find_paths
127
+ map { |_, _, _, path| path }
128
+ end
129
+
121
130
  private
122
131
 
123
132
  # Find the 'parent' of the object pointed to by the query. For singular query only.
@@ -125,9 +134,7 @@ module Janeway
125
134
  # @!visibility private
126
135
  # @return [Object, Object, String] parent object (Hash / Array), key/index (String / Integer), path to parent
127
136
  def find_parent
128
- # Make a Query that points to the target's parent
129
- parent_query = @query.dup
130
- selector = parent_query.pop # returns a name or index selector
137
+ parent_query, leaf = @query.parent_query
131
138
 
132
139
  # Find parent element, give up if parent does not exist
133
140
  results = Interpreter.new(parent_query).interpret(@input)
@@ -137,7 +144,7 @@ module Janeway
137
144
  else raise "query #{parent_query} matched multiple elements!" # not possible for singular query
138
145
  end
139
146
 
140
- [parent, selector.value, parent_query.to_s]
147
+ [parent, leaf.value, parent_query.to_s]
141
148
  end
142
149
 
143
150
  # Insert value into hash at the given key
@@ -149,7 +156,7 @@ module Janeway
149
156
  # @param path [String] jsonpath singular query to parent
150
157
  # @yieldparam [Hash] parent object
151
158
  # @yieldparam [String] hash key
152
- def insert_into_hash(hash, key, value, path, &block)
159
+ def insert_into_hash(hash, key, value, path, &_)
153
160
  unless key.is_a?(String) || key.is_a?(Symbol)
154
161
  raise Error.new("cannot use #{key.inspect} as hash key", @query.to_s)
155
162
  end
@@ -172,7 +179,7 @@ module Janeway
172
179
  # @param path [String] jsonpath singular query to parent
173
180
  # @yieldparam [Array] parent object
174
181
  # @yieldparam [Integer] array index
175
- def insert_into_array(array, index, value, path, &block)
182
+ def insert_into_array(array, index, value, path)
176
183
  raise Error.new("cannot use #{index.inspect} as array index", @query.to_s) unless index.is_a?(Integer)
177
184
 
178
185
  if index < array.size