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.
- checksums.yaml +7 -0
- data/LICENSE +7 -0
- data/README.md +43 -0
- data/bin/janeway +130 -0
- data/lib/janeway/ast/array_slice_selector.rb +104 -0
- data/lib/janeway/ast/binary_operator.rb +101 -0
- data/lib/janeway/ast/boolean.rb +24 -0
- data/lib/janeway/ast/child_segment.rb +48 -0
- data/lib/janeway/ast/current_node.rb +71 -0
- data/lib/janeway/ast/descendant_segment.rb +44 -0
- data/lib/janeway/ast/error.rb +10 -0
- data/lib/janeway/ast/expression.rb +55 -0
- data/lib/janeway/ast/filter_selector.rb +61 -0
- data/lib/janeway/ast/function.rb +40 -0
- data/lib/janeway/ast/helpers.rb +27 -0
- data/lib/janeway/ast/identifier.rb +35 -0
- data/lib/janeway/ast/index_selector.rb +27 -0
- data/lib/janeway/ast/name_selector.rb +52 -0
- data/lib/janeway/ast/null.rb +23 -0
- data/lib/janeway/ast/number.rb +21 -0
- data/lib/janeway/ast/query.rb +41 -0
- data/lib/janeway/ast/root_node.rb +50 -0
- data/lib/janeway/ast/selector.rb +32 -0
- data/lib/janeway/ast/string_type.rb +21 -0
- data/lib/janeway/ast/unary_operator.rb +26 -0
- data/lib/janeway/ast/wildcard_selector.rb +46 -0
- data/lib/janeway/error.rb +23 -0
- data/lib/janeway/functions/count.rb +39 -0
- data/lib/janeway/functions/length.rb +33 -0
- data/lib/janeway/functions/match.rb +69 -0
- data/lib/janeway/functions/search.rb +63 -0
- data/lib/janeway/functions/value.rb +47 -0
- data/lib/janeway/functions.rb +62 -0
- data/lib/janeway/interpreter.rb +644 -0
- data/lib/janeway/lexer.rb +514 -0
- data/lib/janeway/location.rb +3 -0
- data/lib/janeway/parser.rb +608 -0
- data/lib/janeway/token.rb +39 -0
- data/lib/janeway/version.rb +5 -0
- data/lib/janeway.rb +51 -0
- 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,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
|