janeway-jsonpath 0.3.0 → 0.4.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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +201 -28
  3. data/bin/janeway +36 -9
  4. data/lib/janeway/ast/expression.rb +1 -2
  5. data/lib/janeway/enumerator.rb +43 -0
  6. data/lib/janeway/error.rb +6 -9
  7. data/lib/janeway/functions/length.rb +1 -1
  8. data/lib/janeway/interpreter.rb +124 -19
  9. data/lib/janeway/interpreters/array_slice_selector_deleter.rb +41 -0
  10. data/lib/janeway/interpreters/array_slice_selector_interpreter.rb +15 -10
  11. data/lib/janeway/interpreters/base.rb +26 -2
  12. data/lib/janeway/interpreters/binary_operator_interpreter.rb +10 -7
  13. data/lib/janeway/interpreters/child_segment_deleter.rb +19 -0
  14. data/lib/janeway/interpreters/child_segment_interpreter.rb +48 -8
  15. data/lib/janeway/interpreters/current_node_interpreter.rb +5 -3
  16. data/lib/janeway/interpreters/descendant_segment_interpreter.rb +13 -9
  17. data/lib/janeway/interpreters/filter_selector_deleter.rb +65 -0
  18. data/lib/janeway/interpreters/filter_selector_interpreter.rb +54 -14
  19. data/lib/janeway/interpreters/function_interpreter.rb +7 -5
  20. data/lib/janeway/interpreters/index_selector_deleter.rb +26 -0
  21. data/lib/janeway/interpreters/index_selector_interpreter.rb +3 -2
  22. data/lib/janeway/interpreters/name_selector_deleter.rb +27 -0
  23. data/lib/janeway/interpreters/name_selector_interpreter.rb +19 -4
  24. data/lib/janeway/interpreters/root_node_deleter.rb +34 -0
  25. data/lib/janeway/interpreters/root_node_interpreter.rb +4 -2
  26. data/lib/janeway/interpreters/tree_constructor.rb +20 -1
  27. data/lib/janeway/interpreters/unary_operator_interpreter.rb +2 -2
  28. data/lib/janeway/interpreters/wildcard_selector_deleter.rb +32 -0
  29. data/lib/janeway/interpreters/wildcard_selector_interpreter.rb +26 -11
  30. data/lib/janeway/interpreters/yielder.rb +50 -12
  31. data/lib/janeway/lexer.rb +1 -1
  32. data/lib/janeway/normalized_path.rb +66 -0
  33. data/lib/janeway/parser.rb +3 -3
  34. data/lib/janeway/query.rb +70 -0
  35. data/lib/janeway/version.rb +1 -1
  36. data/lib/janeway.rb +16 -28
  37. metadata +12 -3
  38. data/lib/janeway/ast/query.rb +0 -98
@@ -18,15 +18,17 @@ module Janeway
18
18
  value: [:nodes_type],
19
19
  }.freeze
20
20
 
21
- # @param [AST::Function]
21
+ # @param function [AST::Function]
22
22
  def initialize(function)
23
23
  super
24
24
  @params = function.parameters.map { |param| TreeConstructor.ast_node_to_interpreter(param) }
25
25
  end
26
26
 
27
27
  # @param input [Array, Hash] the results of processing so far
28
+ # @param _parent [Array, Hash] parent of the input object
28
29
  # @param root [Array, Hash] the entire input
29
- def interpret(input, root)
30
+ # @param _path [Array<String>] elements of normalized path to the current input
31
+ def interpret(input, _parent, root, _path)
30
32
  params = interpret_function_parameters(@params, input, root)
31
33
  function.body.call(*params)
32
34
  end
@@ -40,8 +42,8 @@ module Janeway
40
42
  # @see https://www.rfc-editor.org/rfc/rfc9535.html#name-well-typedness-of-function-
41
43
  #
42
44
  # @param parameters [Array] parameters before evaluation
43
- # @param func [String] function name (eg. "length", "count")
44
45
  # @param input [Object]
46
+ # @param root [Array, Hash] the entire input
45
47
  # @return [Array] parameters after evaluation
46
48
  def interpret_function_parameters(parameters, input, root)
47
49
  param_types = FUNCTION_PARAMETER_TYPES[function.name.to_sym]
@@ -50,13 +52,13 @@ module Janeway
50
52
  type = param_types[i]
51
53
  case parameter.node
52
54
  when AST::CurrentNode, AST::RootNode
53
- result = parameter.interpret(input, root)
55
+ result = parameter.interpret(input, nil, root, [])
54
56
  if type == :value_type && parameter.node.singular_query?
55
57
  deconstruct(result)
56
58
  else
57
59
  result
58
60
  end
59
- when AST::Function, AST::StringType then parameter.interpret(input, root)
61
+ when AST::Function, AST::StringType then parameter.interpret(input, nil, root, [])
60
62
  when String then parameter.value # from a TreeConstructor::Literal
61
63
  else
62
64
  # invalid parameter type. Function must accept it and return Nothing result
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'name_selector_interpreter'
4
+
5
+ module Janeway
6
+ module Interpreters
7
+ # Interprets an index selector, and deletes the matched value.
8
+ class IndexSelectorDeleter < IndexSelectorInterpreter
9
+ # Interpret selector on the given input.
10
+ # @param input [Array, Hash] the results of processing so far
11
+ # @param _parent [Array, Hash] parent of the input object
12
+ # @param _root [Array, Hash] the entire input
13
+ # @param _path [Array<String>] elements of normalized path to the current input
14
+ def interpret(input, _parent, _root, _path)
15
+ return [] unless input.is_a?(Array)
16
+
17
+ index = selector.value
18
+ result = input.fetch(index) # raises IndexError if no such index
19
+ input.delete_at(index) # returns nil if deleted value is nil, or if no value was deleted
20
+ [result]
21
+ rescue IndexError
22
+ []
23
+ end
24
+ end
25
+ end
26
+ end
@@ -24,13 +24,14 @@ module Janeway
24
24
  #
25
25
  # @param input [Array, Hash] the results of processing so far
26
26
  # @param root [Array, Hash] the entire input
27
- def interpret(input, root)
27
+ # @param path [Array<String>] elements of normalized path to the current input
28
+ def interpret(input, _parent, root, path)
28
29
  return [] unless input.is_a?(Array)
29
30
 
30
31
  result = input.fetch(selector.value) # raises IndexError if no such index
31
32
  return [result] unless @next
32
33
 
33
- @next.interpret(result, root)
34
+ @next.interpret(result, input, root, path + [selector.value])
34
35
  rescue IndexError
35
36
  []
36
37
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'name_selector_interpreter'
4
+
5
+ module Janeway
6
+ module Interpreters
7
+ # Interprets a name selector, and deletes the matched values.
8
+ class NameSelectorDeleter < NameSelectorInterpreter
9
+ # Interpret selector on the given input.
10
+ # @param input [Array, Hash] the results of processing so far
11
+ # @param _parent [Array, Hash] parent of the input object
12
+ # @param _root [Array, Hash] the entire input
13
+ # @param _path [Array<String>] elements of normalized path to the current input
14
+ def interpret(input, _parent, _root, _path)
15
+ return [] unless input.is_a?(Hash) && input.key?(name)
16
+
17
+ [input.delete(name)]
18
+ end
19
+
20
+ # Return hash representation of this interpreter
21
+ # @return [Hash]
22
+ def as_json
23
+ { type: type, value: name, next: @next&.as_json }
24
+ end
25
+ end
26
+ end
27
+ end
@@ -12,18 +12,33 @@ module Janeway
12
12
  # and a key that does not exist ([])
13
13
  class NameSelectorInterpreter < Base
14
14
  alias selector node
15
+ attr_reader :name
16
+
17
+ # @param selector [AST::NameSelector]
18
+ def initialize(selector)
19
+ super
20
+ @name = selector.name
21
+ end
15
22
 
16
23
  # Interpret selector on the given input.
17
24
  # @param input [Array, Hash] the results of processing so far
25
+ # @param _parent [Array, Hash] parent of the input object
18
26
  # @param root [Array, Hash] the entire input
19
- def interpret(input, root)
20
- return [] unless input.is_a?(Hash) && input.key?(selector.name)
27
+ # @param path [Array<String>] 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)
21
30
 
22
- result = input[selector.name]
31
+ result = input[name]
23
32
  return [result] unless @next
24
33
 
25
34
  # Forward result to next selector
26
- @next.interpret(result, root)
35
+ @next.interpret(result, input, root, path + [name])
36
+ end
37
+
38
+ # Return hash representation of this interpreter
39
+ # @return [Hash]
40
+ def as_json
41
+ { type: type, value: name, next: @next&.as_json }
27
42
  end
28
43
  end
29
44
  end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'wildcard_selector_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
@@ -12,11 +12,13 @@ module Janeway
12
12
  # Start an expression chain using the entire, unfiltered input.
13
13
  #
14
14
  # @param _input [Array, Hash] the results of processing so far
15
+ # @param _parent [Array, Hash] parent of the input object
15
16
  # @param root [Array, Hash] the entire input
16
- def interpret(_input, root)
17
+ # @param _path [Array<String>] elements of normalized path to the current input
18
+ def interpret(_input, _parent, root, _path = nil)
17
19
  return [root] unless @next
18
20
 
19
- @next.interpret(root, root)
21
+ @next.interpret(root, nil, root, ['$'])
20
22
  end
21
23
  end
22
24
  end
@@ -4,7 +4,10 @@ require_relative 'base'
4
4
 
5
5
  module Janeway
6
6
  module Interpreters
7
- # Constructs a tree of interpreter objects
7
+ # Constructs a tree of interpreter objects.
8
+ #
9
+ # It is used when an iterator method such as #each has been called to
10
+ # construct the chain of interpreter objects to handle the query.
8
11
  module TreeConstructor
9
12
  # Fake interpreter which just returns the given value
10
13
  Literal = Struct.new(:value) do
@@ -34,6 +37,22 @@ module Janeway
34
37
  raise "Unknown AST expression: #{expr.inspect}"
35
38
  end
36
39
  end
40
+
41
+ def self.ast_node_to_deleter(expr)
42
+ case expr
43
+ when AST::IndexSelector then IndexSelectorDeleter.new(expr)
44
+ when AST::ArraySliceSelector then ArraySliceSelectorDeleter.new(expr)
45
+ when AST::NameSelector then NameSelectorDeleter.new(expr)
46
+ when AST::FilterSelector then FilterSelectorDeleter.new(expr)
47
+ when AST::WildcardSelector then WildcardSelectorDeleter.new(expr)
48
+ when AST::ChildSegment then ChildSegmentDeleter.new(expr)
49
+ when AST::RootNode then RootNodeDeleter.new(expr)
50
+
51
+ when nil then nil # caller has no @next node
52
+ else
53
+ raise "Unknown AST expression: #{expr.inspect}"
54
+ end
55
+ end
37
56
  end
38
57
  end
39
58
  end
@@ -18,8 +18,8 @@ module Janeway
18
18
  end
19
19
 
20
20
  # @return [Boolean]
21
- def interpret(input, root)
22
- result = @operand.interpret(input, root)
21
+ def interpret(input, parent, root, _path)
22
+ result = @operand.interpret(input, parent, root, [])
23
23
  case result
24
24
  when Array then result.empty?
25
25
  when TrueClass, FalseClass then !result
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'wildcard_selector_interpreter'
4
+
5
+ module Janeway
6
+ module Interpreters
7
+ # Interprets a wildcard selector, and deletes the results.
8
+ class WildcardSelectorDeleter < WildcardSelectorInterpreter
9
+ # Delete all elements from the input
10
+ #
11
+ # @param input [Array, Hash] the results of processing so far
12
+ # @param _parent [Array, Hash] parent of the input object
13
+ # @param _root [Array, Hash] the entire input
14
+ # @param _path [Array<String>] elements of normalized path to the current input
15
+ # @return [Array] deleted elements
16
+ def interpret(input, _parent, _root, _path)
17
+ case input
18
+ when Array
19
+ results = input.dup
20
+ input.clear
21
+ results
22
+ when Hash
23
+ results = input.values
24
+ input.clear
25
+ results
26
+ else
27
+ []
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -14,22 +14,37 @@ module Janeway
14
14
  # For anything else, return empty list.
15
15
  #
16
16
  # @param input [Array, Hash] the results of processing so far
17
+ # @param _parent [Array, Hash] parent of the input object
17
18
  # @param root [Array, Hash] the entire input
18
- def interpret(input, root)
19
- values =
20
- case input
21
- when Array then input
22
- when Hash then input.values
23
- else []
24
- end
19
+ # @param path [Array<String>] elements of normalized path to the current input
20
+ def interpret(input, _parent, root, path)
21
+ case input
22
+ when Array then interpret_array(input, root, path)
23
+ when Hash then interpret_hash(input, root, path)
24
+ else []
25
+ end
26
+ end
27
+
28
+ def interpret_hash(input, root, path)
29
+ return [] if input.empty? # early exit, no need for further processing on empty list
30
+ return input.values unless @next
31
+
32
+ # Apply child selector to each node in the output node list
33
+ results = []
34
+ input.each do |key, value|
35
+ results.concat @next.interpret(value, input, root, path + [key])
36
+ end
37
+ results
38
+ end
25
39
 
26
- return values if values.empty? # early exit, no need for further processing on empty list
27
- return values unless @next
40
+ def interpret_array(input, root, path)
41
+ return input if input.empty? # early exit, no need for further processing on empty list
42
+ return input unless @next
28
43
 
29
44
  # Apply child selector to each node in the output node list
30
45
  results = []
31
- values.each do |value|
32
- results.concat @next.interpret(value, root)
46
+ input.each_with_index do |value, i|
47
+ results.concat @next.interpret(value, input, root, path + [i])
33
48
  end
34
49
  results
35
50
  end
@@ -1,34 +1,72 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'base'
4
+ require_relative '../normalized_path'
4
5
 
5
6
  module Janeway
6
7
  module Interpreters
7
8
  # Yields each input value.
8
- # It is inserted at the end of the "real" selectors in the AST, to receive and yield the output.
9
- # This is a supporting class for the supports the Janeway.each method.
10
9
  #
11
- # This will get pushed onto the end of the query AST.
12
- # Currently it must act like both an AST node and an interpreter.
13
- # This will be simpler if TODO some day may the interpreter subclass methods can be merged into the AST classes
14
- class Yielder < Base
15
- # The implicit constructor forwards a closure to the base class constructor.
16
- # Base class constructor stores it in @node.
10
+ # It is inserted at the end of the "real" selectors in the AST, to receive and yield the output.
11
+ # This is a supporting class for the Janeway.each method.
12
+ class Yielder
17
13
  def initialize(&block)
18
- super(Struct.new(:next).new)
19
14
  @block = block
15
+
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
20
29
  end
21
30
 
22
31
  # Yield each input value
23
32
  #
24
33
  # @param input [Array, Hash] the results of processing so far
34
+ # @param parent [Array, Hash] parent of the input object
25
35
  # @param _root [Array, Hash] the entire input
36
+ # @param path [Array<String, Integer>] components of normalized path to the current input
26
37
  # @yieldparam [Object] matched value
27
- # @return [Object] input
28
- def interpret(input, _root)
29
- @block.call(input)
38
+ # @return [Object] input as node list
39
+ def interpret(input, parent, _root, path)
40
+ @yield_to_block.call(input, parent, path)
30
41
  input.is_a?(Array) ? input : [input]
31
42
  end
43
+
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
+ # Dummy method from Interpreters::Base, allow child segment interpreter to disable the
62
+ # non-exist 'next' link.
63
+ # @return [void]
64
+ def next=(*); end
65
+
66
+ # @return [Hash]
67
+ def as_json
68
+ { type: self.class.to_s.split('::').last }
69
+ end
32
70
  end
33
71
  end
34
72
  end
data/lib/janeway/lexer.rb CHANGED
@@ -170,7 +170,7 @@ module Janeway
170
170
  def lex_delimited_string(delimiter)
171
171
  allowed_delimiters = %w[' "]
172
172
  # the "other" delimiter char, which is not currently being treated as a delimiter
173
- non_delimiter = allowed_delimiters.reject { _1 == delimiter }.first
173
+ non_delimiter = allowed_delimiters.reject { |char| char == delimiter }.first
174
174
 
175
175
  literal_chars = []
176
176
  while lookahead != delimiter && source_uncompleted?
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Janeway
4
+ # Converts index and name selector values to normalized path components.
5
+ #
6
+ # This implements the normalized path description in RFC 9535:
7
+ # @see https://www.rfc-editor.org/rfc/rfc9535.html#name-normalized-paths
8
+ #
9
+ # This does a lot of escaping, and much of it is the inverse of Lexer code that does un-escaping.
10
+ module NormalizedPath
11
+ # Characters that do not need escaping, defined by hexadecimal range
12
+ NORMAL_UNESCAPED_RANGES = [(0x20..0x26), (0x28..0x5B), (0x5D..0xD7FF), (0xE000..0x10FFFF)].freeze
13
+
14
+ def self.normalize(value)
15
+ case value
16
+ when String then normalize_name(value)
17
+ when Integer then normalize_index(value)
18
+ else
19
+ raise "Cannot normalize #{value.inspect}"
20
+ end
21
+ end
22
+
23
+ # @param index [Integer] index selector value
24
+ # @return [String] eg. "[1]"
25
+ def self.normalize_index(index)
26
+ "[#{index}]"
27
+ end
28
+
29
+ # @param name [Integer] name selector value
30
+ # @return [String] eg. "['']"
31
+ def self.normalize_name(name)
32
+ "['#{escape(name)}']"
33
+ end
34
+
35
+ def self.escape(str)
36
+ # Common case, all chars are normal.
37
+ return str if str.chars.all? { |char| NORMAL_UNESCAPED_RANGES.any? { |range| range.include?(char.ord) } }
38
+
39
+ # Some escaping must be done
40
+ str.chars.map { |char| escape_char(char) }.join
41
+ end
42
+
43
+ # Escape or hex-encode the given character
44
+ # @param char [String] single character, possibly multi-byte
45
+ # @return [String]
46
+ def self.escape_char(char)
47
+ # Character ranges defined by https://www.rfc-editor.org/rfc/rfc9535.html#section-2.7-8
48
+ case char.ord
49
+ when 0x20..0x26, 0x28..0x5B, 0x5D..0xD7FF, 0xE000..0x10FFFF # normal-unescaped range
50
+ char # unescaped
51
+ when 0x62, 0x66, 0x6E, 0x72, 0x74, 0x27, 0x5C # normal-escapable range
52
+ # backspace, form feed, line feed, carriage return, horizontal tab, apostrophe, backslash
53
+ "\\#{char}" # escaped
54
+ else # normal-hexchar range
55
+ hex_encode_char(char)
56
+ end
57
+ end
58
+
59
+ # Hex-encode the given character
60
+ # @param char [String] single character, possibly multi-byte
61
+ # @return [String]
62
+ def self.hex_encode_char(char)
63
+ format('\\u00%02x', char.ord)
64
+ end
65
+ end
66
+ end
@@ -29,7 +29,7 @@ module Janeway
29
29
  }.freeze
30
30
 
31
31
  # @param jsonpath [String] jsonpath query to be lexed and parsed
32
- # @return [AST::Query]
32
+ # @return [Query]
33
33
  def self.parse(jsonpath)
34
34
  raise ArgumentError, "expect jsonpath string, got #{jsonpath.inspect}" unless jsonpath.is_a?(String)
35
35
 
@@ -46,7 +46,7 @@ module Janeway
46
46
  end
47
47
 
48
48
  # Parse the token list and create an Abstract Syntax Tree
49
- # @return [AST::Query]
49
+ # @return [Query]
50
50
  def parse
51
51
  consume
52
52
  raise err('JsonPath queries must start with root identifier "$"') unless current.type == :root
@@ -58,7 +58,7 @@ module Janeway
58
58
  raise err("Unrecognized expressions after query: #{remaining}")
59
59
  end
60
60
 
61
- AST::Query.new(root_node, @jsonpath)
61
+ Query.new(root_node, @jsonpath)
62
62
  end
63
63
 
64
64
  private
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Janeway
4
+ # Query holds the abstract syntax tree created by parsing the query.
5
+ # This can be frozen and passed to multiple threads or ractors for simultaneous use.
6
+ # No instance members are modified during the interpretation stage.
7
+ class Query
8
+ # The original jsonpath query, for use in error messages
9
+ # @return [String]
10
+ attr_reader :jsonpath
11
+
12
+ # @return [AST::Root]
13
+ attr_reader :root
14
+
15
+ # @param root_node [AST::Root]
16
+ # @param jsonpath [String]
17
+ def initialize(root_node, jsonpath)
18
+ raise ArgumentError, "expect root identifier, got #{root_node.inspect}" unless root_node.is_a?(AST::RootNode)
19
+ raise ArgumentError, "expect query string, got #{jsonpath.inspect}" unless jsonpath.is_a?(String)
20
+
21
+ @root = root_node
22
+ @jsonpath = jsonpath
23
+ end
24
+
25
+ # Combine this query with input to make an Enumerator.
26
+ # This can be used to iterate over results with #each, #map, etc.
27
+ #
28
+ # @return [Janeway::Enumerator]
29
+ def on(input)
30
+ Janeway::Enumerator.new(self, input)
31
+ end
32
+
33
+ def to_s
34
+ @root.to_s
35
+ end
36
+
37
+ # Return a list of the nodes in the AST.
38
+ # The AST of a jsonpath query is a straight line, so this is expressible as an array.
39
+ # The only part of the AST with branches is inside a filter selector, but that doesn't show up here.
40
+ # @return [Array<Expression>]
41
+ def node_list
42
+ nodes = []
43
+ node = @root
44
+ loop do
45
+ nodes << node
46
+ break unless node.next
47
+
48
+ node = node.next
49
+ end
50
+ nodes
51
+ end
52
+
53
+ # Queries are considered equal if their ASTs evaluate to the same JSONPath string.
54
+ #
55
+ # The string output is generated from the AST and should be considered a "normalized"
56
+ # form of the query. It may have different whitespace and parentheses than the original
57
+ # input but will be semantically equivalent.
58
+ def ==(other)
59
+ to_s == other.to_s
60
+ end
61
+
62
+ # Print AST in tree format
63
+ # Every AST class prints a 1-line representation of self, with children on separate lines
64
+ def tree
65
+ result = @root.tree(0)
66
+
67
+ result.flatten.join("\n")
68
+ end
69
+ end
70
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Janeway
4
- VERSION = '0.3.0'
4
+ VERSION = '0.4.0'
5
5
  end
data/lib/janeway.rb CHANGED
@@ -11,45 +11,33 @@ module Janeway
11
11
  INTEGER_MAX = 9_007_199_254_740_991
12
12
  end
13
13
 
14
- # Apply a JSONPath query to the input, and return all matched values.
14
+ # Pair a jsonpath query with data to make an enumerator.
15
+ # This can be used to apply the query to the data using Enumerator module
16
+ # methods such as #each and #map.
15
17
  #
16
- # @param query [String] JSONPath query
17
- # @param input [Hash, Array] ruby object to be searched
18
- # @return [Array] all matched objects
19
- def self.find_all(query, input)
20
- ast = compile(query)
21
- Janeway::Interpreter.new(ast).interpret(input)
18
+ # @param jsonpath [String] jsonpath query
19
+ # @param data [Array, Hash] input data
20
+ # @return [Janeway::Enumerator]
21
+ def self.enum_for(jsonpath, data)
22
+ query = compile(jsonpath)
23
+ Janeway::Enumerator.new(query, data)
22
24
  end
23
25
 
24
26
  # Compile a JSONPath query into an Abstract Syntax Tree.
25
27
  #
26
- # This can be used and re-used later on multiple inputs.
28
+ # This can be combined with inputs (using #on) to create Enumerators.
29
+ # @example
30
+ # query = Janeway.compile('$.store.books[? length(@.title) > 20]')
31
+ # long_title_books = query.on(local_json).search
32
+ # query.on(remote_json).each do |book|
33
+ # long_title_books << book
34
+ # end
27
35
  #
28
36
  # @param query [String] jsonpath query
29
37
  # @return [Janeway::AST::Query]
30
38
  def self.compile(query)
31
39
  Janeway::Parser.parse(query)
32
40
  end
33
-
34
- # Iterate through each value matched by the JSONPath query.
35
- #
36
- # @param query [String] jsonpath query
37
- # @param input [Hash, Array] ruby object to be searched
38
- # @yieldparam [Object] matched value
39
- # @return [void]
40
- def self.each(query, input, &block)
41
- raise ArgumentError, "Invalid jsonpath query: #{query.inspect}" unless query.is_a?(String)
42
- unless [Hash, Array, String].include?(input.class)
43
- raise ArgumentError, "Invalid input, expecting array or hash: #{input.inspect}"
44
- end
45
- return enum_for(:each, query, input) unless block_given?
46
-
47
- ast = Janeway::Parser.parse(query)
48
- interpreter = Janeway::Interpreter.new(ast)
49
- yielder = Janeway::Interpreters::Yielder.new(&block)
50
- interpreter.push(yielder)
51
- interpreter.interpret(input)
52
- end
53
41
  end
54
42
 
55
43
  # Require ruby source files in the given dir. Do not recurse to subdirs.