janeway-jsonpath 0.5.0 → 0.6.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.
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'array_slice_selector_interpreter'
4
+ require_relative 'iteration_helper'
5
+
6
+ module Janeway
7
+ module Interpreters
8
+ # Delete values that match the array slice selector and yield true from the block
9
+ class ArraySliceSelectorDeleteIf < ArraySliceSelectorInterpreter
10
+ include IterationHelper
11
+
12
+ # @param node [AST::Expression]
13
+ def initialize(node, &block)
14
+ super(node)
15
+ @block = block
16
+
17
+ # Make a proc that yields the correct number of values to a block
18
+ @yield_proc = make_yield_proc(&block)
19
+ end
20
+
21
+ # Delete values at the indices matched by the array slice selector
22
+ #
23
+ # @param input [Array, Hash] the results of processing so far
24
+ # @param _parent [Array, Hash] parent of the input object
25
+ # @param _root [Array, Hash] the entire input
26
+ # @param path [Array<String>] elements of normalized path to the current input
27
+ # @return [Array]
28
+ def interpret(input, _parent, _root, path)
29
+ return [] unless input.is_a?(Array)
30
+ return [] if selector&.step&.zero? # RFC: When step is 0, no elements are selected.
31
+
32
+ # Calculate the upper and lower indices of the target range
33
+ lower = selector.lower_index(input.size)
34
+ upper = selector.upper_index(input.size)
35
+
36
+ # Convert bounds and step to index values.
37
+ # Omit the final index, since no value is collected for that.
38
+ # Delete indexes from largest to smallest, so that deleting an index does
39
+ # not change the remaining indexes
40
+ results = []
41
+ if selector.step.positive?
42
+ indexes = lower.step(to: upper - 1, by: selector.step).to_a
43
+ indexes.reverse_each do |i|
44
+ next unless @yield_proc.call(input[i], input, path + [i])
45
+
46
+ results << input.delete_at(i)
47
+ end
48
+ results.reverse
49
+ else
50
+ indexes = upper.step(to: lower + 1, by: selector.step).to_a
51
+ indexes.each { |i| results << input.delete_at(i) }
52
+ results
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -6,7 +6,7 @@ module Janeway
6
6
  module Interpreters
7
7
  # Interprets array slice selector and deletes matching values
8
8
  class ArraySliceSelectorDeleter < ArraySliceSelectorInterpreter
9
- # Delete values at the indices matched by the array slice selector
9
+ # Delete values matched by the array slice selector
10
10
  #
11
11
  # @param input [Array, Hash] the results of processing so far
12
12
  # @param _parent [Array, Hash] parent of the input object
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'child_segment_interpreter'
4
+
5
+ module Janeway
6
+ module Interpreters
7
+ # Child segment interpreter with selectors that delete matching elements if
8
+ # the given block yields a truthy value
9
+ class ChildSegmentDeleteIf < ChildSegmentInterpreter
10
+ # @param child_segment [AST::ChildSegment]
11
+ def initialize(child_segment, &block)
12
+ super(child_segment)
13
+ @selectors =
14
+ child_segment.map do |expr|
15
+ TreeConstructor.ast_node_to_delete_if(expr, &block)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -5,7 +5,7 @@ require_relative 'child_segment_interpreter'
5
5
  module Janeway
6
6
  module Interpreters
7
7
  # Child segment interpreter with selectors that delete matching elements
8
- class ChildSegmentDeleter < ChildSegmentInterpreter
8
+ class ChildSegmentDeleter < ChildSegmentInterpreter
9
9
  # @param child_segment [AST::ChildSegment]
10
10
  def initialize(child_segment)
11
11
  super
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'filter_selector_interpreter'
4
+ require_relative 'iteration_helper'
5
+
6
+ module Janeway
7
+ module Interpreters
8
+ # Interprets a filter selector by deleting matching values for which the block returns a truthy value
9
+ class FilterSelectorDeleteIf < FilterSelectorInterpreter
10
+ include IterationHelper
11
+
12
+ # @param selector [AST::FilterSelector]
13
+ def initialize(selector, &block)
14
+ super(selector)
15
+ @block = block
16
+
17
+ # Make a proc that yields the correct number of values to a block
18
+ @yield_proc = make_yield_proc(&block)
19
+ end
20
+
21
+ # Interpret selector on the input.
22
+ # @param input [Hash] the results of processing so far
23
+ # @param root [Array, Hash] the entire input
24
+ # @param path [Array<String>] elements of normalized path to the current input
25
+ def interpret_hash(input, root, path)
26
+ # Apply filter expressions to the input data
27
+ results = []
28
+ input.each do |key, value|
29
+ # Run filter and interpret result
30
+ result = @expr.interpret(value, nil, root, [])
31
+ case result
32
+ when FalseClass then next # comparison test - fail
33
+ when Array then next if result.empty?
34
+ end
35
+
36
+ # filter test passed, next yield value to block
37
+ next unless @yield_proc.call(value, input, path + [key])
38
+
39
+ results << input.delete(key)
40
+ end
41
+ results
42
+ end
43
+
44
+ # Interpret selector on the input.
45
+ # @param input [Array] the results of processing so far
46
+ # @param root [Array, Hash] the entire input
47
+ # @param path [Array<String>] elements of normalized path to the current input
48
+ def interpret_array(input, root, path)
49
+ # Apply filter expressions to the input data
50
+ results = []
51
+
52
+ # Iterate in reverse order so that deletion does not alter the remaining indexes
53
+ i = input.size
54
+ input.reverse_each do |value|
55
+ i -= 1 # calculate array index
56
+
57
+ # Run filter and interpret result
58
+ result = @expr.interpret(value, nil, root, [])
59
+ case result
60
+ when FalseClass then next # comparison test - fail
61
+ when Array then next if result.empty?
62
+ end
63
+
64
+ # filter test passed, next yield value to block
65
+ next unless @yield_proc.call(value, input, path + [i])
66
+
67
+ results << input.delete_at(i)
68
+ end
69
+ results.reverse
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'index_selector_interpreter'
4
+ require_relative 'iteration_helper'
5
+
6
+ module Janeway
7
+ module Interpreters
8
+ # Interprets an index selector, and deletes the matched value if the block returns true.
9
+ class IndexSelectorDeleteIf < IndexSelectorInterpreter
10
+ include IterationHelper
11
+
12
+ # @param selector [AST::IndexSelector]
13
+ def initialize(selector, &block)
14
+ super(selector)
15
+ @block = block
16
+
17
+ # Make a proc that yields the correct number of values to a block
18
+ @yield_proc = make_yield_proc(&block)
19
+ end
20
+
21
+ # Interpret selector on the given input.
22
+ # @param input [Array, Hash] the results of processing so far
23
+ # @param _parent [Array, Hash] parent of the input object
24
+ # @param _root [Array, Hash] the entire input
25
+ # @param path [Array<String>] elements of normalized path to the current input
26
+ def interpret(input, _parent, _root, path)
27
+ return [] unless input.is_a?(Array)
28
+
29
+ index = selector.value
30
+ result = input.fetch(index) # raises IndexError if no such index
31
+ return unless @yield_proc.call(input[index], input, path + [index])
32
+
33
+ input.delete_at(index) # returns nil if deleted value is nil, or if no value was deleted
34
+ [result]
35
+ rescue IndexError
36
+ []
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../normalized_path'
4
+
5
+ module Janeway
6
+ module Interpreters
7
+ # Mixin for interpreter classes that yield to a block
8
+ module IterationHelper
9
+ # Returns a Proc that yields the correct number of parameters to a block
10
+ #
11
+ # block.arity is -1 when no block is given, and an enumerator is being returned
12
+ # @return [Proc] which takes 3 parameters
13
+ def make_yield_proc(&block)
14
+ if block.arity.negative?
15
+ # Yield just the value to an enumerator, to enable instance method calls on
16
+ # matched values like this: enum.delete_if(&:even?)
17
+ proc { |value, _parent, _path| @block.call(value) }
18
+ elsif block.arity > 3
19
+ # Only do the work of constructing the normalized path when it is actually used
20
+ proc { |value, parent, path| @block.call(value, parent, path.last, normalized_path(path)) }
21
+ else
22
+ # block arity is 1, 2 or 3. Send all 3 parameters regardless.
23
+ proc { |value, parent, path| @block.call(value, parent, path.last) }
24
+ end
25
+ end
26
+
27
+ # Convert the list of path elements into a normalized query string.
28
+ #
29
+ # This form uses a subset of jsonpath that unambiguously points to a value
30
+ # using only name and index selectors.
31
+ # @see https://www.rfc-editor.org/rfc/rfc9535.html#name-normalized-paths
32
+ #
33
+ # Name selectors must use bracket notation, not shorthand.
34
+ #
35
+ # @param components [Array<String, Integer>]
36
+ # @return [String]
37
+ def normalized_path(components)
38
+ # First component is the root identifer, the remaining components are
39
+ # all index selectors or name selectors.
40
+ # Handle the root identifier separately, because .normalize does not handle those.
41
+ '$' + components[1..].map { NormalizedPath.normalize(_1) }.join
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'name_selector_interpreter'
4
+ require_relative 'iteration_helper'
5
+
6
+ module Janeway
7
+ module Interpreters
8
+ # Interprets a name selector, and deletes the matched values if the block returns a truthy value.
9
+ class NameSelectorDeleteIf < NameSelectorInterpreter
10
+ include IterationHelper
11
+
12
+ # @param selector [AST::NameSelector]
13
+ def initialize(selector, &block)
14
+ super(selector)
15
+ @block = block
16
+
17
+ # Make a proc that yields the correct number of values to a block
18
+ @yield_proc = make_yield_proc(&block)
19
+ end
20
+
21
+ # Interpret selector on the given input.
22
+ # Delete matching value for which the block returns truthy value.
23
+ #
24
+ # @param input [Array, Hash] the results of processing so far
25
+ # @param _parent [Array, Hash] parent of the input object
26
+ # @param _root [Array, Hash] the entire input
27
+ # @param path [Array] elements of normalized path to the current input
28
+ def interpret(input, _parent, _root, path)
29
+ return [] unless input.is_a?(Hash) && input.key?(name)
30
+ return [] unless @yield_proc.call(input[name], input, path + [name])
31
+
32
+ [input.delete(name)]
33
+ end
34
+
35
+ # Return hash representation of this interpreter
36
+ # @return [Hash]
37
+ def as_json
38
+ { type: type, value: name }
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'root_node_interpreter'
4
+
5
+ module Janeway
6
+ module Interpreters
7
+ # Interprets the root node for deletion
8
+ class RootNodeDeleter < RootNodeInterpreter
9
+ # Delete all values from the root node.
10
+ #
11
+ # TODO: unclear, for deletion is there a difference between queries '$' and '$.*'?
12
+ #
13
+ # @param _input [Array, Hash] the results of processing so far
14
+ # @param _parent [Array, Hash] parent of the input object
15
+ # @param root [Array, Hash] the entire input
16
+ # @param _path [Array<String>] elements of normalized path to the current input
17
+ # @return [Array] deleted elements
18
+ def interpret(_input, _parent, root, _path)
19
+ case root
20
+ when Array
21
+ results = root.dup
22
+ root.clear
23
+ results
24
+ when Hash
25
+ results = root.values
26
+ root.clear
27
+ results
28
+ else
29
+ []
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -17,6 +17,12 @@ module Janeway
17
17
  alias_method :node, :interpret
18
18
  end
19
19
 
20
+ # Return an interpreter for the given AST node.
21
+ # This interpreter forwards matcheed values to the next interpreter, or
22
+ # returns them if there is no next interpreter.
23
+ #
24
+ # @param expr [AST::Expression]
25
+ # @return [Interprteters::Base]
20
26
  def self.ast_node_to_interpreter(expr)
21
27
  case expr
22
28
  when AST::RootNode then Interpreters::RootNodeInterpreter.new(expr)
@@ -38,6 +44,11 @@ module Janeway
38
44
  end
39
45
  end
40
46
 
47
+ # Return a Deleter interpreter for the given AST node.
48
+ # This interpreter deletes matched values.
49
+ #
50
+ # @param expr [AST::Expression]
51
+ # @return [Interprteters::Base]
41
52
  def self.ast_node_to_deleter(expr)
42
53
  case expr
43
54
  when AST::IndexSelector then IndexSelectorDeleter.new(expr)
@@ -47,12 +58,31 @@ module Janeway
47
58
  when AST::WildcardSelector then WildcardSelectorDeleter.new(expr)
48
59
  when AST::ChildSegment then ChildSegmentDeleter.new(expr)
49
60
  when AST::RootNode then RootNodeDeleter.new(expr)
50
-
51
61
  when nil then nil # caller has no @next node
52
62
  else
53
63
  raise "Unknown AST expression: #{expr.inspect}"
54
64
  end
55
65
  end
66
+
67
+ # Return a DeleteIf interpreter for the given AST node.
68
+ # This interpreter deletes matched values, but only after
69
+ # yielding to a block that returns a truthy value.
70
+ #
71
+ # @param expr [AST::Expression]
72
+ # @return [Interprteters::Base]
73
+ def self.ast_node_to_delete_if(expr, &block)
74
+ case expr
75
+ when AST::IndexSelector then IndexSelectorDeleteIf.new(expr, &block)
76
+ when AST::ArraySliceSelector then ArraySliceSelectorDeleteIf.new(expr, &block)
77
+ when AST::NameSelector then NameSelectorDeleteIf.new(expr, &block)
78
+ when AST::FilterSelector then FilterSelectorDeleteIf.new(expr, &block)
79
+ when AST::WildcardSelector then WildcardSelectorDeleteIf.new(expr, &block)
80
+ when AST::ChildSegment then ChildSegmentDeleteIf.new(expr, &block)
81
+ when AST::RootNode then RootNodeDeleteIf.new(expr, &block)
82
+ else
83
+ raise "Unknown AST expression: #{expr.inspect}"
84
+ end
85
+ end
56
86
  end
57
87
  end
58
88
  end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'wildcard_selector_interpreter'
4
+ require_relative 'iteration_helper'
5
+
6
+ module Janeway
7
+ module Interpreters
8
+ # Interprets a wildcard selector by deleting array / hash values for which a block returns true.
9
+ class WildcardSelectorDeleteIf < WildcardSelectorInterpreter
10
+ include IterationHelper
11
+
12
+ # @param selector [AST::WildcardSelector]
13
+ def initialize(selector, &block)
14
+ super(selector)
15
+ @block = block
16
+
17
+ # Make a proc that yields the correct number of values to a block
18
+ @yield_proc = make_yield_proc(&block)
19
+ end
20
+
21
+ # Delete all elements from the input
22
+ #
23
+ # @param input [Array, Hash] the results of processing so far
24
+ # @param _parent [Array, Hash] parent of the input object
25
+ # @param _root [Array, Hash] the entire input
26
+ # @param path [Array<String>] elements of normalized path to the current input
27
+ # @return [Array] deleted elements
28
+ def interpret(input, _parent, _root, path)
29
+ case input
30
+ when Array then maybe_delete_array_values(input, path)
31
+ when Hash then maybe_delete_hash_values(input, path)
32
+ else []
33
+ end
34
+ end
35
+
36
+ # @param input [Array]
37
+ # @param path [Array]
38
+ def maybe_delete_array_values(input, path)
39
+ results = []
40
+ (input.size - 1).downto(0).each do |i|
41
+ next unless @yield_proc.yield(input[i], input, path + [i])
42
+
43
+ results << input.delete_at(i)
44
+ end
45
+ results.reverse
46
+ end
47
+
48
+ # @param input [Hash]
49
+ # @param path [Array]
50
+ def maybe_delete_hash_values(input, path)
51
+ results = []
52
+ input.each do |key, value|
53
+ next unless @yield_proc.yield(value, input, path + [key])
54
+
55
+ results << input.delete(key)
56
+ end
57
+ results
58
+ end
59
+ end
60
+ end
61
+ end
@@ -1,31 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'base'
4
- require_relative '../normalized_path'
4
+ require_relative 'iteration_helper'
5
5
 
6
6
  module Janeway
7
7
  module Interpreters
8
8
  # Yields each input value.
9
9
  #
10
- # It is inserted at the end of the "real" selectors in the AST, to receive and yield the output.
10
+ # This is inserted at the end of the "real" selectors in the AST, to receive and yield the output.
11
11
  # This is a supporting class for the Janeway.each method.
12
12
  class Yielder
13
+ include IterationHelper
14
+
13
15
  def initialize(&block)
14
16
  @block = block
15
17
 
16
- # Decide how many parameters to yield to this block.
17
- # block.arity is -1 when no block was given, and an enumerator is being returned from #each
18
- @yield_to_block =
19
- if block.arity.negative?
20
- # Yield values only to an enumerator.
21
- proc { |value, _parent, _path| @block.call(value) }
22
- elsif block.arity > 3
23
- # Only do the work of constructing the normalized path when it is actually used
24
- proc { |value, parent, path| @block.call(value, parent, path.last, normalized_path(path)) }
25
- else
26
- # block arity is 1, 2 or 3. Send all 3.
27
- proc { |value, parent, path| @block.call(value, parent, path.last) }
28
- end
18
+ # Make a proc that yields the correct number of values to a block
19
+ @yield_proc = make_yield_proc(&block)
29
20
  end
30
21
 
31
22
  # Yield each input value
@@ -37,27 +28,10 @@ module Janeway
37
28
  # @yieldparam [Object] matched value
38
29
  # @return [Object] input as node list
39
30
  def interpret(input, parent, _root, path)
40
- @yield_to_block.call(input, parent, path)
31
+ @yield_proc.call(input, parent, path)
41
32
  input.is_a?(Array) ? input : [input]
42
33
  end
43
34
 
44
- # Convert the list of path elements into a normalized query string.
45
- #
46
- # This form uses a subset of jsonpath that unambiguously points to a value
47
- # using only name and index selectors.
48
- # @see https://www.rfc-editor.org/rfc/rfc9535.html#name-normalized-paths
49
- #
50
- # Name selectors must use bracket notation, not shorthand.
51
- #
52
- # @param components [Array<String, Integer>]
53
- # @return [String]
54
- def normalized_path(components)
55
- # First component is the root identifer, the remaining components are
56
- # all index selectors or name selectors.
57
- # Handle the root identifier separately, because .normalize does not handle those.
58
- '$' + components[1..].map { NormalizedPath.normalize(_1) }.join
59
- end
60
-
61
35
  # Dummy method from Interpreters::Base, allow child segment interpreter to disable the
62
36
  # non-exist 'next' link.
63
37
  # @return [void]
data/lib/janeway/lexer.rb CHANGED
@@ -5,50 +5,50 @@ require_relative 'token'
5
5
  require_relative 'error'
6
6
 
7
7
  module Janeway
8
- OPERATORS = {
9
- and: '&&',
10
- array_slice_separator: ':',
11
- child_end: ']',
12
- child_start: '[',
13
- current_node: '@',
14
- descendants: '..',
15
- dot: '.',
16
- equal: '==',
17
- filter: '?',
18
- greater_than: '>',
19
- greater_than_or_equal: '>=',
20
- group_end: ')',
21
- group_start: '(',
22
- less_than: '<',
23
- less_than_or_equal: '<=',
24
- minus: '-',
25
- not: '!',
26
- not_equal: '!=',
27
- or: '||',
28
- root: '$',
29
- union: ',',
30
- wildcard: '*',
31
- }.freeze
32
- ONE_CHAR_LEX = OPERATORS.values.select { |lexeme| lexeme.size == 1 }.freeze
33
- TWO_CHAR_LEX = OPERATORS.values.select { |lexeme| lexeme.size == 2 }.freeze
34
- TWO_CHAR_LEX_FIRST = TWO_CHAR_LEX.map { |lexeme| lexeme[0] }.freeze
35
- ONE_OR_TWO_CHAR_LEX = ONE_CHAR_LEX & TWO_CHAR_LEX.map { |str| str[0] }.freeze
36
-
37
- WHITESPACE = " \t\n\r"
38
- KEYWORD = %w[true false null].freeze
39
- FUNCTIONS = %w[length count match search value].freeze
40
-
41
- # faster to check membership in a string than an array of char (benchmarked ruby 3.1.2)
42
- ALPHABET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
43
- DIGITS = '0123456789'
44
-
45
- # chars that may be used as the first letter of member-name-shorthand
46
- NAME_FIRST = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_'
47
-
48
8
  # Transforms source code into tokens
49
9
  class Lexer
50
10
  class Error < Janeway::Error; end
51
11
 
12
+ OPERATORS = {
13
+ and: '&&',
14
+ array_slice_separator: ':',
15
+ child_end: ']',
16
+ child_start: '[',
17
+ current_node: '@',
18
+ descendants: '..',
19
+ dot: '.',
20
+ equal: '==',
21
+ filter: '?',
22
+ greater_than: '>',
23
+ greater_than_or_equal: '>=',
24
+ group_end: ')',
25
+ group_start: '(',
26
+ less_than: '<',
27
+ less_than_or_equal: '<=',
28
+ minus: '-',
29
+ not: '!',
30
+ not_equal: '!=',
31
+ or: '||',
32
+ root: '$',
33
+ union: ',',
34
+ wildcard: '*',
35
+ }.freeze
36
+ ONE_CHAR_LEX = OPERATORS.values.select { |lexeme| lexeme.size == 1 }.freeze
37
+ TWO_CHAR_LEX = OPERATORS.values.select { |lexeme| lexeme.size == 2 }.freeze
38
+ TWO_CHAR_LEX_FIRST = TWO_CHAR_LEX.map { |lexeme| lexeme[0] }.freeze
39
+ ONE_OR_TWO_CHAR_LEX = ONE_CHAR_LEX & TWO_CHAR_LEX.map { |str| str[0] }.freeze
40
+
41
+ WHITESPACE = " \t\n\r"
42
+ KEYWORD = %w[true false null].freeze
43
+ FUNCTIONS = %w[length count match search value].freeze
44
+
45
+ # faster to check membership in a string than an array of char (benchmarked ruby 3.1.2)
46
+ ALPHABET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
47
+ DIGITS = '0123456789'
48
+
49
+ # chars that may be used as the first letter of member-name-shorthand
50
+ NAME_FIRST = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_'
51
+
52
52
  attr_reader :source, :tokens
53
53
  attr_accessor :next_p, :lexeme_start_p
54
54
 
@@ -1,8 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'ast'
3
4
  require_relative 'error'
4
5
  require_relative 'functions'
5
6
  require_relative 'lexer'
7
+ require_relative 'query'
6
8
 
7
9
  module Janeway
8
10
  # Transform a list of tokens into an Abstract Syntax Tree
data/lib/janeway/query.rb CHANGED
@@ -30,10 +30,19 @@ module Janeway
30
30
  Janeway::Enumerator.new(self, input)
31
31
  end
32
32
 
33
+ # @return [String]
33
34
  def to_s
34
35
  @root.to_s
35
36
  end
36
37
 
38
+ # Return true if this query can only possibly match 0 or 1 elements in any input.
39
+ # Such a query must be composed exclusively of the root identifier followed by
40
+ # name selectors and / or index selectors.
41
+ # @return [Boolean]
42
+ def singular_query?
43
+ @root.singular_query?
44
+ end
45
+
37
46
  # Return a list of the nodes in the AST.
38
47
  # The AST of a jsonpath query is a straight line, so this is expressible as an array.
39
48
  # The only part of the AST with branches is inside a filter selector, but that doesn't show up here.
@@ -66,5 +75,36 @@ module Janeway
66
75
 
67
76
  result.flatten.join("\n")
68
77
  end
78
+
79
+ # Deep copy the query
80
+ # @return [Query]
81
+ def dup
82
+ Parser.parse(to_s)
83
+ end
84
+
85
+ # Delete the last element from the chain of selectors.
86
+ # For a singular query, this makes the query point to the match's parent instead of the match itself.
87
+ #
88
+ # Don't do this for a non-singular query, those may contain child segments and
89
+ # descendant segments which would lead to different results.
90
+ #
91
+ # @return [AST::Selector]
92
+ def pop
93
+ unless singular_query?
94
+ raise Janeway::Error.new('not allowed to pop from a non-singular query', to_s)
95
+ end
96
+
97
+ # Sever the link to the last selector
98
+ nodes = node_list
99
+ if nodes.size == 1
100
+ # Special case: cannot pop from the query "$" even though it is a singular query
101
+ raise Janeway::Error.new('cannot pop from single-element query', to_s)
102
+ end
103
+
104
+ nodes[-2].next = nil
105
+
106
+ # Return the last selector
107
+ nodes.last
108
+ end
69
109
  end
70
110
  end