janeway-jsonpath 0.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 (41) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +7 -0
  3. data/README.md +43 -0
  4. data/bin/janeway +130 -0
  5. data/lib/janeway/ast/array_slice_selector.rb +104 -0
  6. data/lib/janeway/ast/binary_operator.rb +101 -0
  7. data/lib/janeway/ast/boolean.rb +24 -0
  8. data/lib/janeway/ast/child_segment.rb +48 -0
  9. data/lib/janeway/ast/current_node.rb +71 -0
  10. data/lib/janeway/ast/descendant_segment.rb +44 -0
  11. data/lib/janeway/ast/error.rb +10 -0
  12. data/lib/janeway/ast/expression.rb +55 -0
  13. data/lib/janeway/ast/filter_selector.rb +61 -0
  14. data/lib/janeway/ast/function.rb +40 -0
  15. data/lib/janeway/ast/helpers.rb +27 -0
  16. data/lib/janeway/ast/identifier.rb +35 -0
  17. data/lib/janeway/ast/index_selector.rb +27 -0
  18. data/lib/janeway/ast/name_selector.rb +52 -0
  19. data/lib/janeway/ast/null.rb +23 -0
  20. data/lib/janeway/ast/number.rb +21 -0
  21. data/lib/janeway/ast/query.rb +41 -0
  22. data/lib/janeway/ast/root_node.rb +50 -0
  23. data/lib/janeway/ast/selector.rb +32 -0
  24. data/lib/janeway/ast/string_type.rb +21 -0
  25. data/lib/janeway/ast/unary_operator.rb +26 -0
  26. data/lib/janeway/ast/wildcard_selector.rb +46 -0
  27. data/lib/janeway/error.rb +23 -0
  28. data/lib/janeway/functions/count.rb +39 -0
  29. data/lib/janeway/functions/length.rb +33 -0
  30. data/lib/janeway/functions/match.rb +69 -0
  31. data/lib/janeway/functions/search.rb +63 -0
  32. data/lib/janeway/functions/value.rb +47 -0
  33. data/lib/janeway/functions.rb +62 -0
  34. data/lib/janeway/interpreter.rb +644 -0
  35. data/lib/janeway/lexer.rb +514 -0
  36. data/lib/janeway/location.rb +3 -0
  37. data/lib/janeway/parser.rb +608 -0
  38. data/lib/janeway/token.rb +39 -0
  39. data/lib/janeway/version.rb +5 -0
  40. data/lib/janeway.rb +51 -0
  41. metadata +92 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 07de626741809ed0f0c9d62f626a45a1eb91513cfda3b536160060428e23cda0
4
+ data.tar.gz: 70953f9cee1de853145ff4eb56a80ed535a93d6126211982b7adaada9438c9a0
5
+ SHA512:
6
+ metadata.gz: 003fbc4fe8e79e174f8e06147f4790554ca2577ed0b29503f1ef61619a33ef082a7e2f2ee3a39a2eb412e0b28778ed90b7ad3c9b6205d67888b2121f9f32e488
7
+ data.tar.gz: ee42a2d1601aa765d748a5a806f79653622a7cfc83c1fdbe87a04cd0c3f7effb67b5a8c3a88fa6733bfb0c50fb138fa0b152aed58c5a28b646032a6951309b84
data/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2025 Fraser Hanson
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,43 @@
1
+ # Janeway JSONPath parser
2
+
3
+ ### Purpose
4
+
5
+ This is a [JsonPath](https://goessner.net/articles/JsonPath/) parser.
6
+
7
+ It reads a JSON input file and a query.
8
+ It uses the query to find and return a set of matching values from the input.
9
+ This does for JSON the same job that XPath does for XML.
10
+
11
+ This project includes:
12
+ * command-line tool to run jsonpath queries on a JSON input
13
+ * ruby library to run jsonpath queries on a JSON input
14
+
15
+ ### Goals
16
+
17
+ * parse Goessner JSONPath, similar to https://github.com/joshbuddy/jsonpath
18
+ * implement all of [IETF RFC 9535](https://github.com/ietf-wg-jsonpath)
19
+ * don't use regular expressions for parsing, for performance
20
+ * don't use `eval`, which is known to be an attack vector
21
+ * be simple and fast with minimal dependencies
22
+ * use helpful query parse errors which help understand and improve queries, rather than describing issues in the code
23
+ * modern, linted ruby 3 code with frozen string literals
24
+
25
+ ### Non-goals
26
+
27
+ * Changing behavior to follow [other implementations] (https://cburgmer.github.io/json-path-comparison/)
28
+
29
+ The JSONPath RFC was in draft status for a long time and has seen many changes.
30
+ There are many implementations based on older drafts, or which add features that were never in the RFC at all.
31
+
32
+ The goal here is perfect adherence to the finalized [RFC 9535](https://github.com/ietf-wg-jsonpath) rather than adding features that are in other implementations.
33
+
34
+ The RFC was finalized in 2024, and it has a rigorous [suite of compliance tests.](https://github.com/jsonpath-standard/jsonpath-compliance-test-suite)
35
+
36
+ With these tools it is possible to have JSONPath implementations in many languages with identical behavior.
37
+
38
+ ### Implementation
39
+
40
+ Functionality is based on [IETF RFC 9535, "JSONPath: Query Expressions for JSON"](https://www.rfc-editor.org/rfc/rfc9535.html#filter-selector)
41
+ The examples in the RFC have been implemented as unit tests.
42
+
43
+ For details not covered in the RFC, it does the most reasonable thing based on [what other JSONPath parsers do.](https://cburgmer.github.io/json-path-comparison/). However, this is always secondary to following the RFC. Many of the recommended behaviors there contradict the RFC, so it is one or the other.
data/bin/janeway ADDED
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # frozen_string_literal: true
4
+
5
+ require 'json'
6
+ require 'optparse'
7
+
8
+ # FIXME: delete from final version?
9
+ $LOAD_PATH << "#{__dir__}/../lib/"
10
+ require 'janeway'
11
+
12
+ SCRIPT_NAME = File.basename($PROGRAM_NAME)
13
+ HELP = <<~HELP_TEXT.freeze
14
+ Usage:
15
+ #{SCRIPT_NAME} [QUERY] [FILENAME]
16
+
17
+ Purpose:
18
+ Print the result of applying a JsonPath query to a JSON input.
19
+
20
+ QUERY is a JsonPath query. Quote it with single quotes to avoid shell errors.
21
+
22
+ FILENAME is the path to a JSON file to use as input.
23
+ Alternately, input JSON can be provided on STDIN.
24
+
25
+ For an introduction to JsonPath, see https://goessner.net/articles/JsonPath/
26
+ For the complete reference, see https://www.rfc-editor.org/info/rfc9535
27
+
28
+ Examples:
29
+ #{SCRIPT_NAME} '$.store.book[*].author' input.json
30
+ cat input.json | #{SCRIPT_NAME} '$.store.book[*].author'
31
+
32
+ HELP_TEXT
33
+
34
+ # Command-line options
35
+ Options = Struct.new(:query, :query_file, :input, :compact_output, :verbose)
36
+
37
+ # Parse the command-line arguments.
38
+ # This includes both bare words and hyphenated options.
39
+ #
40
+ # @param argv [Array<String>]
41
+ def parse_args(argv)
42
+ # parse command-line options
43
+ argv << '--help' if argv.empty?
44
+ options = parse_options(argv)
45
+
46
+ # Next get jsonpath query and input jsonn
47
+ options.query = read_query(options.query_file, argv)
48
+ options.input = read_input(argv.first)
49
+ options
50
+ end
51
+
52
+ # Parse the command-line options.
53
+ #
54
+ # @param argv [Array<String>]
55
+ def parse_options(argv)
56
+ options = Options.new
57
+ op = OptionParser.new do |opts|
58
+ opts.banner = HELP
59
+ opts.separator('Options:')
60
+
61
+ opts.on('-q', '--query FILE', 'Read jsonpath query from file') { |o| options.query_file = o }
62
+ opts.on('-c', '--compact', 'Express result in compact json format') { options.compact_output = true }
63
+ opts.on('--version', 'Show version number') { abort(Janeway::VERSION) }
64
+ opts.on('-h', '--help', 'Show this help message') { abort(opts.to_s) }
65
+ end
66
+ op.parse!(argv)
67
+ options
68
+ end
69
+
70
+ # Read jsonpath query from file or command-line arguments
71
+ #
72
+ # @param path [String,nil] query path, or nil if not provided
73
+ # @param argv [Array<String>] command line arguments
74
+ def read_query(path, argv)
75
+ return File.read(path).strip if path
76
+
77
+ query = argv.find { |arg| arg.start_with?('$') }
78
+ abort('No JsonPath query received, provide one on the command line.') unless query
79
+
80
+ argv.delete(query)
81
+ query
82
+ end
83
+
84
+ # Read input json from file or STDIN
85
+ # @param path [String,nil] json file path, or nil if not provided
86
+ # @return [Hash, Array] un-serialized json, as ruby objects
87
+ def read_input(path)
88
+ json =
89
+ if path
90
+ File.read(path)
91
+ elsif !$stdin.tty?
92
+ $stdin.read
93
+ else
94
+ abort('No input JSON provided. Provide a filename or pipe it to STDIN.')
95
+ end
96
+ parse_json(json)
97
+ end
98
+
99
+ # Parse JSON, and abort if it is invalid
100
+ # @param json [String]
101
+ # @return [Hash, Array] un-serialized json, as ruby objects
102
+ def parse_json(json)
103
+ JSON.parse(json)
104
+ rescue JSON::JSONError => e
105
+ # JSON error messages may include the entire input, so limit how much is printed.
106
+ msg = e.message[0..256]
107
+ msg += "\n..." if e.message.length > 256
108
+ abort "Input is not valid JSON: #{msg}"
109
+ end
110
+
111
+ # @param options [Options]
112
+ def main(options)
113
+ results = Janeway.find_all(options.query, options.input)
114
+
115
+ if options.compact_output
116
+ puts JSON.generate(results)
117
+ else
118
+ puts JSON.pretty_generate(results)
119
+ end
120
+ end
121
+
122
+ begin
123
+ options = parse_args(ARGV.dup)
124
+ main(options)
125
+ rescue Janeway::Error => e
126
+ puts
127
+ abort("Error: #{e}")
128
+ rescue Interrupt, Errno::EPIPE
129
+ abort("\n")
130
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'selector'
4
+
5
+ module Janeway
6
+ module AST
7
+ # An array slice selects a series of elements from an array.
8
+ #
9
+ # It accepts a start and end positions, and a step value that define the range to select.
10
+ # All of these are optional.
11
+ #
12
+ # @example
13
+ # $[1:3]
14
+ # $[5:]
15
+ # $[1:5:2]
16
+ # $[5:1:-2]
17
+ # $[::-1]
18
+ # $[:]
19
+ #
20
+ # ArraySliceSelector needs to store "default" arguments differently from
21
+ # "explicit" arguments, since they're interpreted differently.
22
+ #
23
+ class ArraySliceSelector < Janeway::AST::Selector
24
+ # @param start [Integer, nil]
25
+ # @param end_ [Integer, nil]
26
+ # @param step [Integer, nil]
27
+ def initialize(start = nil, end_ = nil, step = nil)
28
+ [start, end_, step].each do |arg|
29
+ next if arg.nil? || arg.is_a?(Integer)
30
+
31
+ raise ArgumentError, "Expect Integer or nil, got #{arg.inspect}"
32
+ end
33
+ super(nil)
34
+
35
+ # Nil values are kept to indicate that the "default" values is to be used.
36
+ # The interpreter selects the actual values.
37
+ @start = start
38
+ @end = end_
39
+ @step = step
40
+ end
41
+
42
+ # Return the step size to use for stepping through the array.
43
+ # Defaults to 1 if it was not given to the constructor.
44
+ #
45
+ # @return [Integer]
46
+ def step
47
+ # The iteration behavior of jsonpath does not match that of ruby Integer#step.
48
+ # Support the behavior of Integer#step, which needs this:
49
+ # 1. for stepping forward between positive numbers, use a positive number
50
+ # 2. for stepping backward between positive numbers, use a negative number
51
+ # 3. for stepping backward from positive to negative, use a negative number
52
+ # 4. for stepping backward from negative to negative, use a positive number
53
+ # Case #4 has to be detected and the sign of step inverted
54
+ @step || 1
55
+ end
56
+
57
+ # Return the start index to use for stepping through the array, based on a specified array size
58
+ #
59
+ # @param input_size [Integer]
60
+ # @return [Integer]
61
+ def start_index(input_size)
62
+ if @start
63
+ @start.clamp(0, input_size)
64
+ elsif step.positive?
65
+ 0
66
+ else # negative step
67
+ input_size - 1 # last index of input
68
+ end
69
+ end
70
+
71
+ # Return the end index to use for stepping through the array, based on a specified array size
72
+ # End index is calculated to omit the final index value, as per the RFC.
73
+ #
74
+ # @param input_size [Integer]
75
+ # @return [Integer]
76
+ def end_index(input_size)
77
+ if @end
78
+ value = @end.clamp(0, input_size)
79
+ step.positive? ? value - 1 : value + 1 # +/- to exclude the final element
80
+ elsif step.positive?
81
+ input_size - 1 # last index of input
82
+ else
83
+ 0
84
+ end
85
+ end
86
+
87
+ # ignores the brackets: argument, this always needs surrounding brackets
88
+ # @return [String]
89
+ def to_s(*)
90
+ if @step
91
+ "[#{@start}:#{@end}:#{@step}]"
92
+ else
93
+ "[#{@start}:#{@end}]"
94
+ end
95
+ end
96
+
97
+ # @param level [Integer]
98
+ # @return [Array]
99
+ def tree(level)
100
+ [indented(level, to_s)]
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Janeway
4
+ module AST
5
+ class BinaryOperator < Janeway::AST::Expression
6
+ attr_reader :operator, :left, :right
7
+
8
+ def initialize(operator, left = nil, right = nil)
9
+ super(nil)
10
+ raise ArgumentError, "expect symbol, got #{operator.inspect}" unless operator.is_a?(Symbol)
11
+
12
+ @operator = operator
13
+ self.left = left if left
14
+ self.right = right if right
15
+ end
16
+
17
+ # Set the left-hand-side value
18
+ # @param expr [AST::Expression]
19
+ def left=(expr)
20
+ if comparison_operator? && !(expr.literal? || expr.singular_query?)
21
+ raise Error, "Expression #{expr} does not produce a singular value for #{operator_to_s} comparison"
22
+ end
23
+
24
+ # Compliance test suite requires error for this, but don't have go so far as to bar every literal
25
+ if expr.is_a?(Boolean) && right.is_a?(Boolean)
26
+ raise Error, "Literal \"#{expr}\" must be compared to an expression, not another literal (\"#{left}\")"
27
+ end
28
+
29
+ @left = expr
30
+ end
31
+
32
+ # Set the left-hand-side value
33
+ # @param expr [AST::Expression]
34
+ def right=(expr)
35
+ if comparison_operator? && !(expr.literal? || expr.singular_query?)
36
+ raise Error, "Expression #{expr} does not produce a singular value for #{operator_to_s} comparison"
37
+ end
38
+
39
+ # Compliance test suite requires error for this, but don't have go so far as to bar every literal
40
+ if expr.is_a?(Boolean) && left.is_a?(Boolean)
41
+ raise Error, "Literal \"#{expr}\" must be compared to an expression, not another literal (\"#{left}\")"
42
+ end
43
+
44
+ @right = expr
45
+ end
46
+
47
+ def to_s
48
+ # Make precedence explicit by adding parentheses
49
+ "(#{@left} #{operator_to_s} #{@right})"
50
+ end
51
+
52
+ private
53
+
54
+ def operator_to_s
55
+ case operator
56
+ when :and then '&&'
57
+ when :equal then '=='
58
+ when :greater_than then '>'
59
+ when :greater_than_or_equal then '>='
60
+ when :less_than then '<'
61
+ when :less_than_or_equal then '<='
62
+ when :not_equal then '!='
63
+ when :or then '||'
64
+ else
65
+ raise "unknown binary operator #{operator}"
66
+ end
67
+ end
68
+
69
+ # @param level [Integer]
70
+ # @return [Array]
71
+ def tree(level)
72
+ [
73
+ indented(level, to_s),
74
+ @left.tree(level + 1),
75
+ @right.tree(level + 1),
76
+ ]
77
+ end
78
+
79
+ # True if this operator is a comparison operator
80
+ # @return [Boolean]
81
+ def comparison_operator?
82
+ operator_type == :comparison
83
+ end
84
+
85
+ # True if this operator is a logical operator
86
+ # @return [Boolean]
87
+ def logical_operator?
88
+ operator_type == :logical
89
+ end
90
+
91
+ def operator_type
92
+ case operator
93
+ when :and, :or then :logical
94
+ when :equal, :not_equal, :greater_than, :greater_than_or_equal, :less_than, :less_than_or_equal then :comparison
95
+ else
96
+ raise "unknown binary operator #{operator}"
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Janeway
4
+ module AST
5
+ # Represent keywords true, false
6
+ class Boolean < Janeway::AST::Expression
7
+ def to_s
8
+ @value ? 'true' : 'false'
9
+ end
10
+
11
+ # @param level [Integer]
12
+ # @return [Array]
13
+ def tree(level)
14
+ [indented(level, to_s)]
15
+ end
16
+
17
+ # Return true if this is a literal expression
18
+ # @return [Boolean]
19
+ def literal?
20
+ true
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ # A set of selectors within brackets, as a comma-separated list.
6
+ # https://www.rfc-editor.org/rfc/rfc9535.html#child-segment
7
+ #
8
+ # @example
9
+ # $[*, *]
10
+ # $[1, 2, 3]
11
+ # $[name1, [1:10]]
12
+ module Janeway
13
+ module AST
14
+ # Represent a union of 2 or more selectors.
15
+ class ChildSegment < Janeway::AST::Expression
16
+ extend Forwardable
17
+ def_delegators :@value, :size, :first, :last, :each, :map, :empty?
18
+
19
+ # Subsequent expression that modifies the result of this selector list.
20
+ # This one is not in the selector list, it comes afterward.
21
+ attr_accessor :child
22
+
23
+ def initialize
24
+ super([]) # @value holds the expressions in the selector
25
+ @child = nil # @child is the next expression, which modifies the output of this one
26
+ end
27
+
28
+ # Add a selector to the list
29
+ def <<(selector)
30
+ raise ArgumentError, "expect Selector, got #{selector.inspect}" unless selector.is_a?(Selector)
31
+
32
+ @value << selector
33
+ end
34
+
35
+ def to_s(with_child: true)
36
+ str = @value.map { |selector| selector.to_s(brackets: false) }.join(', ')
37
+ with_child ? "[#{str}]#{@child}" : "[#{str}]"
38
+ end
39
+
40
+ # @param level [Integer]
41
+ # @return [Array]
42
+ def tree(level)
43
+ msg = format('[%s]', @value.map(&:to_s).join(', '))
44
+ [indented(level, msg), @child&.tree(level + 1)]
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Janeway
4
+ module AST
5
+ # CurrentNode addresses the current node of the filter-selector that is directly enclosing the identifier.
6
+ #
7
+ # Note: Within nested filter-selectors, there is no syntax to address the
8
+ # current node of any other than the directly enclosing filter-selector
9
+ # (i.e., of filter-selectors enclosing the filter-selector that is directly
10
+ # enclosing the identifier).
11
+ #
12
+ # It may be followed by another selector which is applied to it.
13
+ # Within the IETF examples, I see @ followed by these selectors:
14
+ # * name selector (dot notation)
15
+ # * filter selector (bracketed)
16
+ # * wildcard selector
17
+ # Probably best to assume that any selector type could appear here.
18
+ #
19
+ # It may also not be followed by any selector, and be used directly with a comparison operator.
20
+ #
21
+ # @example
22
+ # $.a[?@.b == 'kilo']
23
+ # $.a[?@.b]
24
+ # $[?@.*]
25
+ # $[?@[?@.b]]
26
+ # $.a[?@<2 || @.b == "k"]
27
+ # $.o[?@>1 && @<4]
28
+ # $.a[?@ == @]
29
+ #
30
+ # Construct accepts an optional Selector which will be applied to the "current" node
31
+ class CurrentNode < Janeway::AST::Expression
32
+ def to_s
33
+ if @value.is_a?(NameSelector) || @value.is_a?(WildcardSelector)
34
+ "@.#{@value.to_s(brackets: false)}"
35
+ else
36
+ "@#{@value}"
37
+ end
38
+ end
39
+
40
+ # True if this is the root of a singular-query.
41
+ # @see https://www.rfc-editor.org/rfc/rfc9535.html#name-well-typedness-of-function-
42
+ #
43
+ # @return [Boolean]
44
+ def singular_query?
45
+ return true unless @value # there are no following selectors
46
+
47
+ selector_types = []
48
+ selector = @value
49
+ loop do
50
+ selector_types << selector.class
51
+ selector = selector.child
52
+ break unless selector
53
+ end
54
+ allowed = [AST::IndexSelector, AST::NameSelector]
55
+ selector_types.uniq.all? { allowed.include?(_1) }
56
+ end
57
+
58
+ # True if this is a bare current node operator, without a following expression
59
+ # @return [Boolean]
60
+ def empty?
61
+ @value.nil?
62
+ end
63
+
64
+ # @param level [Integer]
65
+ # @return [Array]
66
+ def tree(level)
67
+ [indented(level, '@'), @value.tree(indent + 1)]
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'selector'
4
+
5
+ module Janeway
6
+ module AST
7
+ # An array slice start:end:step selects a series of elements from
8
+ # an array, giving a start position, an end position, and an optional step
9
+ # value that moves the position from the start to the end.
10
+ #
11
+ # @example
12
+ # $..j Values of keys equal to 'j'
13
+ # $..[0] First entry of any array
14
+ # $..[*] All values
15
+ # $..* All values
16
+ # $..[*, *] All values, twice non-deterministic order
17
+ # $..[0, 1] Multiple segments
18
+ class DescendantSegment < Janeway::AST::Selector
19
+ attr_accessor :child
20
+
21
+ def initialize(selector)
22
+ super
23
+
24
+ @child = nil
25
+ end
26
+
27
+ def to_s
28
+ "..#{@value}#{@child}"
29
+ end
30
+
31
+ # @return [AST::Selector]
32
+ #
33
+ def selector
34
+ value
35
+ end
36
+
37
+ # @param level [Integer]
38
+ # @return [Array]
39
+ def tree(level)
40
+ [indented(level, "..#{@value}"), child&.tree(level + 1)]
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../error'
4
+
5
+ module Janeway
6
+ module AST
7
+ # Error raised during parsing, for use by AST classes
8
+ class Error < Janeway::Error; end
9
+ end
10
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helpers'
4
+ require_relative 'error'
5
+
6
+ module Janeway
7
+ module AST
8
+ INDENT = ' '
9
+
10
+ class Expression
11
+ attr_accessor :value
12
+
13
+ def initialize(val = nil)
14
+ # don't set the instance variable if unused, because it makes the
15
+ # "#inspect" output cleaner in rspec test failures
16
+ @value = val unless val.nil? # false must be stored though!
17
+ end
18
+
19
+ # @return [String]
20
+ def type
21
+ name = self.class.to_s.split('::').last # eg. Janeway::AST::FunctionCall => "FunctionCall"
22
+ Helpers.camelcase_to_underscore(name) # eg. "FunctionCall" => "function_call"
23
+ end
24
+
25
+ # Return the given message, indented
26
+ #
27
+ # @param level [Integer]
28
+ # @param msg [String]
29
+ # @return [String]
30
+ def indented(level, msg)
31
+ format('%s%s', (INDENT * level), msg)
32
+ end
33
+
34
+ # @param level [Integer]
35
+ # @return [Array]
36
+ def tree(level)
37
+ [indented(level, to_s)]
38
+ end
39
+
40
+ # Return true if this is a literal expression
41
+ # @return [Boolean]
42
+ def literal?
43
+ false
44
+ end
45
+
46
+ # True if this is the root of a singular-query.
47
+ # @see https://www.rfc-editor.org/rfc/rfc9535.html#name-well-typedness-of-function-
48
+ #
49
+ # @return [Boolean]
50
+ def singular_query?
51
+ false
52
+ end
53
+ end
54
+ end
55
+ end