janeway-jsonpath 1.1.0 → 1.2.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 +4 -4
- data/README.md +1 -1
- data/bin/janeway +4 -2
- data/lib/janeway/ast/string_type.rb +4 -0
- data/lib/janeway/error.rb +25 -1
- data/lib/janeway/functions/match.rb +6 -10
- data/lib/janeway/functions/search.rb +8 -1
- data/lib/janeway/functions.rb +78 -51
- data/lib/janeway/iregexp.rb +402 -0
- data/lib/janeway/lexer.rb +3 -1
- data/lib/janeway/parser.rb +1 -1
- data/lib/janeway/token.rb +39 -1
- data/lib/janeway/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 11e0e5480e8e6e35fec72b79fb6effff412dae3349ead88ecb8f257e072d68fb
|
|
4
|
+
data.tar.gz: 047f434419ccacdbc6b1107146c97dd8305a6aec000d83c7cd3c1eb813e0f78e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a767d7c2f72d25b61b5ae8ef87a08f3c67c9c7bacc387fe9f0bc9b343c85643eedcd7f7be4a445522e84f1eff23f570f83b71d50dca0a67d3b70264c1552489d
|
|
7
|
+
data.tar.gz: 35d6038891ab976044be4ca499896bd614f4feae8adf5d894a85309ace9e5d2d5245bea5f4be1594d27f473906b4c58c32b7fe3a774c64942d507d2b3f2ea2b6
|
data/README.md
CHANGED
|
@@ -216,7 +216,7 @@ This is a jsonpath query string that uniquely points to the matched value.
|
|
|
216
216
|
The `#delete` method deletes matched values from the input.
|
|
217
217
|
```ruby
|
|
218
218
|
# delete any bird whose name starts with "s" or is "eagle"
|
|
219
|
-
Janeway.enum_for('$.birds[?
|
|
219
|
+
Janeway.enum_for('$.birds[? match(@, "s.*") || @=="eagle"]', data).delete
|
|
220
220
|
# input bird list is now ['cormorant']
|
|
221
221
|
|
|
222
222
|
# delete all dog names
|
data/bin/janeway
CHANGED
|
@@ -197,8 +197,10 @@ begin
|
|
|
197
197
|
options = parse_args(ARGV.dup)
|
|
198
198
|
main(options)
|
|
199
199
|
rescue Janeway::Error => e
|
|
200
|
-
|
|
201
|
-
|
|
200
|
+
# The message shows the query, and carets under it when there is a location. The label is on its own
|
|
201
|
+
# line so that the carets stay under the header's copy of the query.
|
|
202
|
+
warn 'Error:'
|
|
203
|
+
warn e.message
|
|
202
204
|
exit(1)
|
|
203
205
|
rescue Interrupt, Errno::EPIPE
|
|
204
206
|
abort("\n")
|
|
@@ -5,6 +5,10 @@ module Janeway
|
|
|
5
5
|
# Represents a string literal within a filter expression.
|
|
6
6
|
# May be used within a comparison or a jsonpath function call.
|
|
7
7
|
class StringType < Janeway::AST::Expression
|
|
8
|
+
# The string token this literal was parsed from, nil otherwise.
|
|
9
|
+
# @return [Token, nil]
|
|
10
|
+
attr_accessor :token
|
|
11
|
+
|
|
8
12
|
def to_s
|
|
9
13
|
if @value.include?("'")
|
|
10
14
|
%("#{@value}"')
|
data/lib/janeway/error.rb
CHANGED
|
@@ -20,14 +20,38 @@ module Janeway
|
|
|
20
20
|
# @param query [String] entire query string
|
|
21
21
|
# @param location [Location] location of error
|
|
22
22
|
def initialize(msg, query = nil, location = nil)
|
|
23
|
+
prefix = 'Jsonpath query '
|
|
24
|
+
pointer = query.is_a?(String) && location
|
|
25
|
+
# The pointer is only right if the header's copy of the query is on one line.
|
|
26
|
+
# scrub swaps each invalid byte for one replacement character, so the columns stay valid
|
|
27
|
+
# and a query that is not valid UTF-8 cannot make building the error raise.
|
|
28
|
+
shown = query
|
|
29
|
+
if pointer
|
|
30
|
+
shown = shown.scrub unless shown.valid_encoding?
|
|
31
|
+
shown = shown.tr("\r\n", ' ')
|
|
32
|
+
end
|
|
23
33
|
full = +''
|
|
24
|
-
full << "
|
|
34
|
+
full << "#{prefix}#{shown}" if query
|
|
25
35
|
full << " (col #{location.col})" if location
|
|
26
36
|
full << ' - ' unless full.empty?
|
|
27
37
|
full << msg
|
|
38
|
+
full << pointer_line(shown, location, prefix.length) if pointer
|
|
28
39
|
super(full)
|
|
29
40
|
@query = query
|
|
30
41
|
@location = location
|
|
31
42
|
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
# Build the line of carets that marks the problem, aligned under the query in the message header.
|
|
47
|
+
# @param line [String] the query as shown in the header, on one line
|
|
48
|
+
# @param location [Location]
|
|
49
|
+
# @param offset [Integer] width of the text that comes before the query in the header
|
|
50
|
+
# @return [String] starts with a newline
|
|
51
|
+
def pointer_line(line, location, offset)
|
|
52
|
+
col = (location.col || 0).clamp(0, line.length)
|
|
53
|
+
length = (location.length || 1).clamp(1, [line.length - col, 1].max)
|
|
54
|
+
"\n#{' ' * (offset + col)}#{'^' * length}"
|
|
55
|
+
end
|
|
32
56
|
end
|
|
33
57
|
end
|
|
@@ -19,16 +19,10 @@ module Janeway
|
|
|
19
19
|
# otherwise.
|
|
20
20
|
#
|
|
21
21
|
#
|
|
22
|
-
# The
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
# @see https://www.rfc-editor.org/rfc/rfc9485.html#name-pcre-re2-and-ruby-regexps
|
|
27
|
-
#
|
|
28
|
-
# The instructions are:
|
|
29
|
-
# * For any unescaped dots (.) outside character classes (first
|
|
30
|
-
# alternative of charClass production), replace the dot with [^\n\r].
|
|
31
|
-
# * Enclose the regexp in \A(?: and )\z.
|
|
22
|
+
# The pattern is validated and translated by Janeway::IRegexp (RFC 9485). A pattern that is not an
|
|
23
|
+
# I-Regexp gives LogicalFalse (RFC 9535 sections 2.4.6 and 2.4.7). A literal pattern that is over
|
|
24
|
+
# one of the limits in IRegexp (MAX_LENGTH, MAX_REPEAT, MAX_GROUP_DEPTH) raises Janeway::Error when
|
|
25
|
+
# the query is parsed. In a pattern ^ and $ are ordinary characters, as RFC 9485 has no anchors.
|
|
32
26
|
#
|
|
33
27
|
# tl;dr: How is this different from the search function?
|
|
34
28
|
# "match" must match the entire string, "search" matches a substring.
|
|
@@ -52,6 +46,8 @@ module Janeway
|
|
|
52
46
|
parameters << parse_function_parameter
|
|
53
47
|
raise Error, 'Too many parameters for match() function call' unless current.type == :group_end
|
|
54
48
|
|
|
49
|
+
validate_literal_pattern(parameters.last) # after the syntax checks, so they are reported first
|
|
50
|
+
|
|
55
51
|
# Body (including literal-regex fast path) is supplied by
|
|
56
52
|
# Functions::REGISTRY at FunctionInterpreter construction time.
|
|
57
53
|
AST::Function.new('match', parameters)
|
|
@@ -25,6 +25,11 @@ module Janeway
|
|
|
25
25
|
# the result is LogicalTrue if at least one such substring exists and is
|
|
26
26
|
# LogicalFalse otherwise.
|
|
27
27
|
#
|
|
28
|
+
# The pattern is validated and translated by Janeway::IRegexp (RFC 9485). A pattern that is not an
|
|
29
|
+
# I-Regexp gives LogicalFalse (RFC 9535 sections 2.4.6 and 2.4.7). A literal pattern that is over
|
|
30
|
+
# one of the limits in IRegexp (MAX_LENGTH, MAX_REPEAT, MAX_GROUP_DEPTH) raises Janeway::Error when
|
|
31
|
+
# the query is parsed. In a pattern ^ and $ are ordinary characters, as RFC 9485 has no anchors.
|
|
32
|
+
#
|
|
28
33
|
# tl;dr: How is this different from the match function?
|
|
29
34
|
# "match" must match the entire string, "search" matches a substring.
|
|
30
35
|
def parse_function_search
|
|
@@ -45,7 +50,9 @@ module Janeway
|
|
|
45
50
|
# Otherwise it is an expression that takes the regexp from the input document,
|
|
46
51
|
# and the iregexp will not be available until interpretation.
|
|
47
52
|
parameters << parse_function_parameter
|
|
48
|
-
raise Error, 'Too many parameters for
|
|
53
|
+
raise Error, 'Too many parameters for search() function call' unless current.type == :group_end
|
|
54
|
+
|
|
55
|
+
validate_literal_pattern(parameters.last) # after the syntax checks, so they are reported first
|
|
49
56
|
|
|
50
57
|
# Body (including literal-regex fast path) is supplied by
|
|
51
58
|
# Functions::REGISTRY at FunctionInterpreter construction time.
|
data/lib/janeway/functions.rb
CHANGED
|
@@ -2,42 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'ast/function'
|
|
4
4
|
require_relative 'ast/string_type'
|
|
5
|
+
require_relative 'iregexp'
|
|
5
6
|
|
|
6
7
|
module Janeway
|
|
7
|
-
# Mixin to provide JSONPath function handlers for Parser, plus the
|
|
8
|
-
# I-Regexp translation helper and the built-in function registry.
|
|
8
|
+
# Mixin to provide JSONPath function handlers for Parser, plus the built-in function registry.
|
|
9
9
|
module Functions
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
chars.each_with_index do |char, i|
|
|
22
|
-
case char
|
|
23
|
-
when '[' then in_char_class = true
|
|
24
|
-
when ']'
|
|
25
|
-
in_char_class = false unless chars[i - 1] == '\\' # escaped ] does not close char class
|
|
26
|
-
when '.'
|
|
27
|
-
next if in_char_class || chars[i - 1] == '\\' # escaped dot
|
|
28
|
-
|
|
29
|
-
indexes << i # replace this dot
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
indexes.reverse_each do |i|
|
|
33
|
-
chars[i] = '[^\n\r]'
|
|
34
|
-
end
|
|
10
|
+
# Reject a literal pattern that exceeds an I-Regexp limit, at the point where the query is parsed.
|
|
11
|
+
#
|
|
12
|
+
# A literal that is not an I-Regexp is not an error: match() and search() return LogicalFalse
|
|
13
|
+
# for it, see build_regex_body. A literal over a limit is a valid I-Regexp that this library
|
|
14
|
+
# refuses to run, so the user is told, with the position in the query.
|
|
15
|
+
# @see https://www.rfc-editor.org/rfc/rfc9485.html#name-security-considerations
|
|
16
|
+
# RFC 9485 section 8: an implementation may limit range quantifiers in range and composability
|
|
17
|
+
# @param parameter [AST::Expression] the second parameter of match() or search(), just parsed
|
|
18
|
+
# @raise [Janeway::Error]
|
|
19
|
+
def validate_literal_pattern(parameter)
|
|
20
|
+
return unless parameter.is_a?(AST::StringType)
|
|
35
21
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
22
|
+
IRegexp.compile(parameter.value)
|
|
23
|
+
rescue IRegexp::Invalid
|
|
24
|
+
nil # RFC 9535 section 2.4.6: the function returns LogicalFalse, see build_regex_body
|
|
25
|
+
rescue IRegexp::LimitExceeded => e
|
|
26
|
+
raise Error.new("I-Regexp #{e.message}", @jsonpath, parameter.token&.location_of(e.offset, e.length))
|
|
39
27
|
end
|
|
40
|
-
module_function :translate_iregex_to_ruby_regex
|
|
41
28
|
|
|
42
29
|
# All jsonpath function parameters are one of these accepted types.
|
|
43
30
|
# Parse the function parameter and return the result.
|
|
@@ -58,32 +45,72 @@ module Janeway
|
|
|
58
45
|
result
|
|
59
46
|
end
|
|
60
47
|
|
|
61
|
-
#
|
|
62
|
-
|
|
63
|
-
|
|
48
|
+
# Largest number of distinct document-supplied patterns that one function call remembers.
|
|
49
|
+
DYNAMIC_PATTERN_CACHE_SIZE = 100
|
|
50
|
+
|
|
51
|
+
# Build a match/search body. When the pattern is a StringType literal, the parser has already
|
|
52
|
+
# validated it (Functions#validate_literal_pattern, which discards the result); the interpreter
|
|
53
|
+
# compiles it once more, with the anchoring this function needs, when it is built.
|
|
54
|
+
# Otherwise the pattern comes from the input document and is compiled when it is first used.
|
|
55
|
+
#
|
|
56
|
+
# A pattern that is not an I-Regexp, or that exceeds a limit, gives LogicalFalse.
|
|
57
|
+
# @see https://www.rfc-editor.org/rfc/rfc9535.html#name-match-function-extension
|
|
58
|
+
# RFC 9535 section 2.4.6: "If the first argument is not a string or the second argument is
|
|
59
|
+
# not a string conforming to [RFC9485], the result is LogicalFalse." Section 2.4.7 says the
|
|
60
|
+
# same for search().
|
|
61
|
+
# A literal that exceeds a limit has already been rejected by the parser, see
|
|
62
|
+
# Functions#validate_literal_pattern.
|
|
64
63
|
def self.build_regex_body(parameters, anchor:)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
64
|
+
literal = parameters[1].is_a?(AST::StringType) ? parameters[1].value : nil
|
|
65
|
+
# The cache holds compiled regexps keyed by pattern text, so reusing it for other subjects is
|
|
66
|
+
# correct. It lives as long as the interpreter tree, which Query#interpreter_tree_for caches
|
|
67
|
+
# on an unfrozen Query.
|
|
68
|
+
cache = {} # pattern string => Regexp, or nil when the pattern is unusable
|
|
69
|
+
cache[literal] = compile_or_nil(literal, anchor) if literal # a literal is compiled once, now
|
|
70
|
+
lambda do |str, pattern|
|
|
71
|
+
pattern = literal if literal
|
|
72
|
+
next false unless str.is_a?(String) && pattern.is_a?(String)
|
|
73
|
+
# A pattern in the wrong encoding is false and never reaches the cache: Hash lookup compares
|
|
74
|
+
# strings by content, so a binary 'a' would otherwise share the entry of a UTF-8 'a'.
|
|
75
|
+
next false unless IRegexp.valid_encoding?(pattern)
|
|
76
|
+
# RFC 9485 section 8 lets an implementation limit patterns; the limit is this library's own
|
|
77
|
+
# choice. Checked before parsing so that a huge pattern costs almost nothing.
|
|
78
|
+
next false if pattern.length > IRegexp::MAX_LENGTH
|
|
73
79
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
lambda do |str, pattern|
|
|
78
|
-
if str.is_a?(String) && pattern.is_a?(String)
|
|
79
|
-
translate_iregex_to_ruby_regex(pattern, anchor: anchor).match?(str)
|
|
80
|
-
else
|
|
81
|
-
false
|
|
82
|
-
end
|
|
80
|
+
unless cache.key?(pattern)
|
|
81
|
+
cache.shift if cache.size >= DYNAMIC_PATTERN_CACHE_SIZE
|
|
82
|
+
cache[pattern] = compile_or_nil(pattern, anchor)
|
|
83
83
|
end
|
|
84
|
+
|
|
85
|
+
!cache[pattern].nil? && match_safely(cache[pattern], str)
|
|
84
86
|
end
|
|
85
87
|
end
|
|
86
88
|
|
|
89
|
+
# RFC 9535 section 2.4.6: a pattern that does not conform to RFC 9485 gives LogicalFalse, so it is nil here.
|
|
90
|
+
# A pattern over a limit (RFC 9485 section 8) is treated the same way.
|
|
91
|
+
# @return [Regexp, nil] nil when the pattern is not usable, see build_regex_body
|
|
92
|
+
def self.compile_or_nil(pattern, anchor)
|
|
93
|
+
IRegexp.compile(pattern, anchor: anchor)
|
|
94
|
+
rescue IRegexp::Invalid, IRegexp::LimitExceeded
|
|
95
|
+
nil
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Match, and treat a match that Ruby cannot or will not complete as no match.
|
|
99
|
+
#
|
|
100
|
+
# Deliberate deviation from RFC 9535 sections 2.4.6 and 2.4.7, which say the result is LogicalTrue or
|
|
101
|
+
# LogicalFalse and say nothing about a match that cannot complete: a timeout, or a subject that is
|
|
102
|
+
# not valid UTF-8, gives false. RFC 9485 section 8 (security considerations) is why a timeout exists.
|
|
103
|
+
# @param regexp [Regexp]
|
|
104
|
+
# @param str [String]
|
|
105
|
+
# @return [Boolean]
|
|
106
|
+
def self.match_safely(regexp, str)
|
|
107
|
+
regexp.match?(str)
|
|
108
|
+
rescue IRegexp::TIMEOUT_ERROR, ArgumentError, Encoding::CompatibilityError
|
|
109
|
+
# TIMEOUT_ERROR: the match took longer than IRegexp::MATCH_TIMEOUT.
|
|
110
|
+
# ArgumentError, Encoding::CompatibilityError: str is not valid UTF-8.
|
|
111
|
+
false
|
|
112
|
+
end
|
|
113
|
+
|
|
87
114
|
# Metadata + body builder for every built-in JSONPath function.
|
|
88
115
|
#
|
|
89
116
|
# arity — number of parameters
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'error'
|
|
4
|
+
|
|
5
|
+
module Janeway
|
|
6
|
+
# Validates an I-Regexp and translates it to an equivalent Ruby Regexp.
|
|
7
|
+
#
|
|
8
|
+
# The pattern is parsed with the grammar from RFC 9485 section 3, and the Ruby source is emitted
|
|
9
|
+
# while parsing, so a pattern that is not an I-Regexp never reaches Regexp.new, and characters
|
|
10
|
+
# that Ruby treats as syntax but I-Regexp treats as ordinary characters are escaped.
|
|
11
|
+
#
|
|
12
|
+
# The limits below are this library's own choices. RFC 9485 section 8 allows an implementation to
|
|
13
|
+
# limit range quantifiers in composability and range, but gives no values.
|
|
14
|
+
# @see https://www.rfc-editor.org/rfc/rfc9485.html
|
|
15
|
+
module IRegexp
|
|
16
|
+
# Longest pattern accepted, in characters.
|
|
17
|
+
MAX_LENGTH = 10_000
|
|
18
|
+
|
|
19
|
+
# Largest number accepted in a range quantifier such as a{2,4}.
|
|
20
|
+
MAX_REPEAT = 1_000
|
|
21
|
+
|
|
22
|
+
# Deepest nesting of groups accepted. This also bounds the recursion depth of the parser.
|
|
23
|
+
MAX_GROUP_DEPTH = 100
|
|
24
|
+
|
|
25
|
+
# Seconds a single match may take, where the Ruby version supports a per-regexp timeout (3.2+).
|
|
26
|
+
MATCH_TIMEOUT = 1
|
|
27
|
+
|
|
28
|
+
# Regexp.new accepts the timeout: keyword from Ruby 3.2, which is also when Regexp.timeout appeared.
|
|
29
|
+
# Probe with respond_to? and not by trying the keyword: on an older Ruby a trailing hash would be
|
|
30
|
+
# read as the positional options argument, and a pattern would silently become case-insensitive.
|
|
31
|
+
TIMEOUT_SUPPORTED = Regexp.respond_to?(:timeout)
|
|
32
|
+
|
|
33
|
+
# Error raised when a match exceeds MATCH_TIMEOUT. Regexp::TimeoutError only exists on Ruby 3.2+,
|
|
34
|
+
# elsewhere this is a class that is never raised for that reason.
|
|
35
|
+
TIMEOUT_ERROR = defined?(Regexp::TimeoutError) ? Regexp::TimeoutError : Class.new(StandardError)
|
|
36
|
+
|
|
37
|
+
# Unicode general category names, from the IsCategory production of RFC 9485 section 3.
|
|
38
|
+
CATEGORY = /\A(?:L[lmotu]?|M[cen]?|N[dlo]?|P[c-fios]?|Z[lps]?|S[ckmo]?|C[cfno]?)\z/
|
|
39
|
+
|
|
40
|
+
# Base class for the errors IRegexp.compile raises. They describe a problem in a pattern, not in a
|
|
41
|
+
# JSONPath query, so they carry the index of the problem in the pattern and have no query or location.
|
|
42
|
+
class Error < Janeway::Error
|
|
43
|
+
# @return [Integer, nil] index of the problem in the pattern, nil when there is no single place
|
|
44
|
+
attr_reader :offset
|
|
45
|
+
|
|
46
|
+
# @param msg [String]
|
|
47
|
+
# @param offset [Integer, nil]
|
|
48
|
+
def initialize(msg, offset: nil)
|
|
49
|
+
super(msg)
|
|
50
|
+
@offset = offset
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Raised for a pattern that is not an I-Regexp.
|
|
55
|
+
class Invalid < Error; end
|
|
56
|
+
|
|
57
|
+
# Raised for an I-Regexp that exceeds one of the limits above.
|
|
58
|
+
class LimitExceeded < Error
|
|
59
|
+
# @return [Integer] number of pattern characters the problem covers
|
|
60
|
+
attr_reader :length
|
|
61
|
+
|
|
62
|
+
# @param msg [String]
|
|
63
|
+
# @param offset [Integer, nil]
|
|
64
|
+
# @param length [Integer]
|
|
65
|
+
def initialize(msg, offset: nil, length: 1)
|
|
66
|
+
super(msg, offset: offset)
|
|
67
|
+
@length = length
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Can this string be an I-Regexp at all, as far as its encoding goes?
|
|
72
|
+
#
|
|
73
|
+
# RFC 9485 section 1: a pattern is a sequence of Unicode scalar values. Accept UTF-8, and US-ASCII
|
|
74
|
+
# which is a subset of it. Any other encoding is refused and not transcoded, and that includes a
|
|
75
|
+
# binary (ASCII-8BIT) string even when its bytes are all ASCII.
|
|
76
|
+
# @param string [String]
|
|
77
|
+
# @return [Boolean]
|
|
78
|
+
def self.valid_encoding?(string)
|
|
79
|
+
[Encoding::UTF_8, Encoding::US_ASCII].include?(string.encoding) && string.valid_encoding?
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Validate an I-Regexp and translate it to a Ruby Regexp.
|
|
83
|
+
#
|
|
84
|
+
# @param iregex [String]
|
|
85
|
+
# @param anchor [Boolean] add anchors to match the string start and end
|
|
86
|
+
# @return [Regexp]
|
|
87
|
+
# @raise [Invalid] the pattern is not an I-Regexp
|
|
88
|
+
# @raise [LimitExceeded] the pattern is an I-Regexp, but is over one of the limits
|
|
89
|
+
def self.compile(iregex, anchor: true)
|
|
90
|
+
body = Parser.new(iregex).parse
|
|
91
|
+
# RFC 9485 section 5.4: enclose the regexp in \A(?: and )\z.
|
|
92
|
+
body = "\\A(?:#{body})\\z" if anchor
|
|
93
|
+
# RFC 9485 section 3: the pattern is valid I-Regexp, so a Ruby warning about it (such as a redundant
|
|
94
|
+
# nested repeat operator in (a*)*) is not the caller's business. A pattern from the input document
|
|
95
|
+
# must not be able to write to stderr. $VERBOSE is process-wide, so warnings from other threads are
|
|
96
|
+
# also suppressed during this one compile.
|
|
97
|
+
old_verbose = $VERBOSE
|
|
98
|
+
begin
|
|
99
|
+
$VERBOSE = nil
|
|
100
|
+
TIMEOUT_SUPPORTED ? Regexp.new(body, timeout: MATCH_TIMEOUT) : Regexp.new(body)
|
|
101
|
+
rescue RegexpError => e
|
|
102
|
+
# Deliberate deviation from RFC 9485 section 3: Ruby refuses some patterns that are valid
|
|
103
|
+
# I-Regexp, such as a{3,2}, and these are treated as not an I-Regexp.
|
|
104
|
+
raise Invalid, e.message
|
|
105
|
+
ensure
|
|
106
|
+
$VERBOSE = old_verbose
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Recursive-descent parser for the ABNF of RFC 9485 section 3, which builds the Ruby regexp source.
|
|
111
|
+
# Each method is named for the production it implements.
|
|
112
|
+
class Parser
|
|
113
|
+
# Characters allowed after a backslash. RFC 9485 section 3, SingleCharEsc.
|
|
114
|
+
SINGLE_CHAR_ESC = '()*+-.?[]^nrt{|}\\'
|
|
115
|
+
|
|
116
|
+
# catEsc and complEsc start with one of these after the backslash, RFC 9485 section 3
|
|
117
|
+
CATEGORY_ESCAPES = %w[p P].freeze
|
|
118
|
+
|
|
119
|
+
# @param iregex [String]
|
|
120
|
+
def initialize(iregex)
|
|
121
|
+
@original = iregex
|
|
122
|
+
@src = iregex.valid_encoding? ? iregex.chars : [] # array: indexing a multibyte String is O(n)
|
|
123
|
+
@i = 0
|
|
124
|
+
@out = +''
|
|
125
|
+
@depth = 0
|
|
126
|
+
@has_range = false # does the group being parsed contain a range quantifier?
|
|
127
|
+
@limit = nil # first limit violation, raised only if the pattern is otherwise valid
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# @return [String] Ruby regexp source, without anchors
|
|
131
|
+
# @raise [Invalid, LimitExceeded]
|
|
132
|
+
def parse
|
|
133
|
+
raise Invalid, 'pattern is not valid UTF-8' unless IRegexp.valid_encoding?(@original) # RFC 9485 section 1
|
|
134
|
+
|
|
135
|
+
i_regexp
|
|
136
|
+
raise Invalid.new("unexpected #{@src[@i].inspect}", offset: @i) if @i < @src.length
|
|
137
|
+
|
|
138
|
+
limit("pattern is longer than #{MAX_LENGTH} characters", nil) if @src.length > MAX_LENGTH
|
|
139
|
+
raise @limit if @limit
|
|
140
|
+
|
|
141
|
+
@out
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
private
|
|
145
|
+
|
|
146
|
+
def peek
|
|
147
|
+
@src[@i]
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Grammar errors take precedence over limit errors, so a limit violation is remembered and
|
|
151
|
+
# only raised once the whole pattern is known to be an I-Regexp. The one exception is the group
|
|
152
|
+
# nesting limit, which is raised as soon as it is hit (see #group): reading on past it is what
|
|
153
|
+
# would overflow the stack.
|
|
154
|
+
def limit(msg, offset, length = 1)
|
|
155
|
+
@limit ||= LimitExceeded.new(msg, offset: offset, length: length)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# i-regexp = branch *( "|" branch )
|
|
159
|
+
# RFC 9485 section 3, the top production.
|
|
160
|
+
def i_regexp
|
|
161
|
+
branch
|
|
162
|
+
while peek == '|'
|
|
163
|
+
@i += 1
|
|
164
|
+
@out << '|'
|
|
165
|
+
branch
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# branch = *piece
|
|
170
|
+
def branch
|
|
171
|
+
piece while !peek.nil? && peek != '|' && peek != ')'
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# piece = atom [ quantifier ]
|
|
175
|
+
def piece
|
|
176
|
+
contains_range = atom
|
|
177
|
+
quantifier(contains_range)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# atom = NormalChar / charClass / ( "(" i-regexp ")" )
|
|
181
|
+
# @return [Boolean] true if the atom is a group that contains a range quantifier
|
|
182
|
+
def atom
|
|
183
|
+
case peek
|
|
184
|
+
when '(' then return group
|
|
185
|
+
when '[' then char_class
|
|
186
|
+
when '.'
|
|
187
|
+
# RFC 9485 section 5.4: replace an unescaped dot outside a character class with [^\n\r]
|
|
188
|
+
@i += 1
|
|
189
|
+
@out << '[^\n\r]'
|
|
190
|
+
when '\\' then escape
|
|
191
|
+
else normal_char(in_class: false)
|
|
192
|
+
end
|
|
193
|
+
false
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# atom = ... / ( "(" i-regexp ")" ), RFC 9485 section 3.
|
|
197
|
+
# The nesting limit is this library's own choice, which RFC 9485 section 8 permits: it bounds recursion.
|
|
198
|
+
# Unlike the other limits it is raised at once and not remembered (see #limit), because parsing on
|
|
199
|
+
# past it is what would overflow the stack. So a too-deeply nested pattern that is also invalid is
|
|
200
|
+
# reported as LimitExceeded, not Invalid.
|
|
201
|
+
# @return [Boolean] true if the group contains a range quantifier
|
|
202
|
+
def group
|
|
203
|
+
open = @i
|
|
204
|
+
@i += 1
|
|
205
|
+
@depth += 1
|
|
206
|
+
if @depth > MAX_GROUP_DEPTH
|
|
207
|
+
raise LimitExceeded.new("groups are nested deeper than #{MAX_GROUP_DEPTH}", offset: open)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
@out << '(?:'
|
|
211
|
+
outer_has_range = @has_range
|
|
212
|
+
@has_range = false
|
|
213
|
+
i_regexp
|
|
214
|
+
raise Invalid.new('unterminated group', offset: @i) unless peek == ')'
|
|
215
|
+
|
|
216
|
+
@i += 1
|
|
217
|
+
@depth -= 1
|
|
218
|
+
@out << ')'
|
|
219
|
+
inner_has_range = @has_range
|
|
220
|
+
@has_range = outer_has_range || inner_has_range
|
|
221
|
+
inner_has_range
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# quantifier = ( "*" / "+" / "?" ) / range-quantifier
|
|
225
|
+
def quantifier(contains_range)
|
|
226
|
+
case peek
|
|
227
|
+
when '*', '+', '?'
|
|
228
|
+
@out << peek
|
|
229
|
+
@i += 1
|
|
230
|
+
when '{' then range_quantifier(contains_range)
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# range-quantifier = "{" QuantExact [ "," [ QuantExact ] ] "}"
|
|
235
|
+
# QuantExact = 1*%x30-39
|
|
236
|
+
#
|
|
237
|
+
# RFC 9485 section 8 allows limiting range quantifiers in range and in composability: here a
|
|
238
|
+
# number above MAX_REPEAT, and a range quantifier applied to a group that contains one.
|
|
239
|
+
def range_quantifier(contains_range)
|
|
240
|
+
start = @i
|
|
241
|
+
counts, finish = scan_range_quantifier(start)
|
|
242
|
+
raise Invalid.new('malformed range quantifier', offset: start) unless counts
|
|
243
|
+
|
|
244
|
+
counts.each do |digits, offset|
|
|
245
|
+
next if digits.empty? || digits.to_i <= MAX_REPEAT
|
|
246
|
+
|
|
247
|
+
limit("repeat count #{digits} exceeds #{MAX_REPEAT}", offset, digits.length)
|
|
248
|
+
end
|
|
249
|
+
limit('range quantifier applied to a group that contains a range quantifier', start) if contains_range
|
|
250
|
+
@has_range = true
|
|
251
|
+
@out << @src[start...finish].join
|
|
252
|
+
@i = finish
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Match \{(\d+)(?:,(\d*))?\} at start by scanning the character array, which a Regexp cannot
|
|
256
|
+
# do in O(1) per call on a multibyte string. The digit runs can be long, so no fixed-size slice.
|
|
257
|
+
# @return [Array, nil] [[[digits, offset], ...], index after the closing brace], nil if malformed
|
|
258
|
+
def scan_range_quantifier(start)
|
|
259
|
+
return nil unless @src[start] == '{'
|
|
260
|
+
|
|
261
|
+
first_start = start + 1
|
|
262
|
+
first_end = digits_end(first_start)
|
|
263
|
+
return nil if first_end == first_start # QuantExact needs at least one digit
|
|
264
|
+
|
|
265
|
+
counts = [[@src[first_start...first_end].join, first_start]]
|
|
266
|
+
pos = first_end
|
|
267
|
+
if @src[pos] == ','
|
|
268
|
+
second_start = pos + 1
|
|
269
|
+
pos = digits_end(second_start)
|
|
270
|
+
counts << [@src[second_start...pos].join, second_start]
|
|
271
|
+
end
|
|
272
|
+
@src[pos] == '}' ? [counts, pos + 1] : nil
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# @return [Integer] index of the first character at or after pos that is not an ASCII digit
|
|
276
|
+
def digits_end(pos)
|
|
277
|
+
pos += 1 while @src[pos]&.match?(/\A[0-9]\z/)
|
|
278
|
+
pos
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# charClassExpr = "[" [ "^" ] ( "-" / CCE1 ) *CCE1 [ "-" ] "]"
|
|
282
|
+
def char_class
|
|
283
|
+
open = @i
|
|
284
|
+
@i += 1
|
|
285
|
+
@out << '['
|
|
286
|
+
negated = peek == '^'
|
|
287
|
+
if negated
|
|
288
|
+
@i += 1
|
|
289
|
+
@out << '^'
|
|
290
|
+
end
|
|
291
|
+
# RFC 9485 section 3: charClassExpr is not allowed to match [^]
|
|
292
|
+
raise Invalid.new('empty negated character class [^]', offset: open) if negated && peek == ']'
|
|
293
|
+
# RFC 9485 section 3: charClassExpr has at least one item, so [] is not allowed either
|
|
294
|
+
raise Invalid.new('empty character class []', offset: open) if peek == ']'
|
|
295
|
+
|
|
296
|
+
first = true
|
|
297
|
+
until peek == ']'
|
|
298
|
+
raise Invalid.new('unterminated character class', offset: @i) if peek.nil?
|
|
299
|
+
|
|
300
|
+
class_item(first)
|
|
301
|
+
first = false
|
|
302
|
+
end
|
|
303
|
+
@i += 1
|
|
304
|
+
@out << ']'
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# CCE1 = ( CCchar [ "-" CCchar ] ) / charClassEsc, RFC 9485 section 3
|
|
308
|
+
def class_item(first)
|
|
309
|
+
if peek == '-'
|
|
310
|
+
# a "-" is only a plain character at the start or the end of the class
|
|
311
|
+
raise Invalid.new("misplaced '-' in character class", offset: @i) unless first || @src[@i + 1] == ']'
|
|
312
|
+
|
|
313
|
+
@i += 1
|
|
314
|
+
@out << '\-'
|
|
315
|
+
elsif peek == '\\' && CATEGORY_ESCAPES.include?(@src[@i + 1])
|
|
316
|
+
# charClassEsc is not a CCchar, so it takes no range: a following "-" is rejected by the next item
|
|
317
|
+
escape
|
|
318
|
+
else
|
|
319
|
+
cc_char
|
|
320
|
+
class_range_end if peek == '-' && @src[@i + 1] != ']'
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# "-" CCchar, the end of a range. RFC 9485 section 3, CCE1.
|
|
325
|
+
def class_range_end
|
|
326
|
+
@i += 1
|
|
327
|
+
@out << '-'
|
|
328
|
+
raise Invalid.new('unterminated range in character class', offset: @i) if peek.nil? || peek == ']'
|
|
329
|
+
|
|
330
|
+
cc_char
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# CCchar = ( %x00-2C / %x2E-5A / %x5E-D7FF / %xE000-10FFFF ) / SingleCharEsc
|
|
334
|
+
# RFC 9485 section 3: a SingleCharEsc is a CCchar, so it can start or end a range.
|
|
335
|
+
def cc_char
|
|
336
|
+
if peek == '\\' then escape(single_char_only: true)
|
|
337
|
+
else normal_char(in_class: true)
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# NormalChar (outside a class) and CCchar (inside a class). RFC 9485 section 3.
|
|
342
|
+
# In I-Regexp "^" and "$" are ordinary characters, not anchors: the RFC 9485 author confirmed
|
|
343
|
+
# this, see https://github.com/jsonpath-standard/jsonpath-compliance-test-suite (open issue).
|
|
344
|
+
def normal_char(in_class:)
|
|
345
|
+
char = peek
|
|
346
|
+
raise Invalid.new('unexpected end of pattern', offset: @i) if char.nil?
|
|
347
|
+
unless allowed_char?(char.ord, in_class)
|
|
348
|
+
raise Invalid.new("character #{char.inspect} must be escaped", offset: @i)
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
@i += 1
|
|
352
|
+
# Regexp.escape does not escape "&", but Ruby reads "&&" in a class as set intersection
|
|
353
|
+
@out << (char == '&' ? '\&' : Regexp.escape(char))
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# @param code [Integer] code point
|
|
357
|
+
def allowed_char?(code, in_class)
|
|
358
|
+
return false if (0xD800..0xDFFF).cover?(code) # surrogates
|
|
359
|
+
|
|
360
|
+
if in_class
|
|
361
|
+
# CCchar: %x00-2C / %x2E-5A / %x5E-D7FF / %xE000-10FFFF
|
|
362
|
+
code <= 0x2C || (0x2E..0x5A).cover?(code) || code >= 0x5E
|
|
363
|
+
else
|
|
364
|
+
# NormalChar: %x00-27 / "," / "-" / %x2F-3E / %x40-5A / %x5E-7A / %x7E-D7FF / %xE000-10FFFF
|
|
365
|
+
code <= 0x27 || [0x2C, 0x2D].include?(code) || (0x2F..0x3E).cover?(code) ||
|
|
366
|
+
(0x40..0x5A).cover?(code) || (0x5E..0x7A).cover?(code) || code >= 0x7E
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
# SingleCharEsc / catEsc / complEsc, RFC 9485 section 3
|
|
371
|
+
# @param single_char_only [Boolean] refuse catEsc and complEsc, where a CCchar is required
|
|
372
|
+
def escape(single_char_only: false)
|
|
373
|
+
start = @i
|
|
374
|
+
nxt = @src[@i + 1]
|
|
375
|
+
raise Invalid.new('trailing backslash', offset: start) if nxt.nil?
|
|
376
|
+
|
|
377
|
+
if CATEGORY_ESCAPES.include?(nxt)
|
|
378
|
+
if single_char_only
|
|
379
|
+
raise Invalid.new("\\#{nxt} is not allowed here, only a single-character escape", offset: start)
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
category_escape(start)
|
|
383
|
+
elsif SINGLE_CHAR_ESC.include?(nxt)
|
|
384
|
+
@i += 2
|
|
385
|
+
@out << { 'n' => '\n', 'r' => '\r', 't' => '\t' }.fetch(nxt) { "\\#{nxt}" }
|
|
386
|
+
else
|
|
387
|
+
raise Invalid.new("invalid escape \\#{nxt}", offset: start)
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
# catEsc = %s"\p{" charProp "}" ; complEsc = %s"\P{" charProp "}", RFC 9485 section 3
|
|
392
|
+
def category_escape(start)
|
|
393
|
+
# at most \p{Xx}, 6 characters
|
|
394
|
+
m = /\A\\[pP]\{([A-Za-z]{1,2})\}/.match(@src[start, 6].join)
|
|
395
|
+
raise Invalid.new('malformed category escape', offset: start) unless m && CATEGORY.match?(m[1])
|
|
396
|
+
|
|
397
|
+
@i = start + m[0].length
|
|
398
|
+
@out << (@src[start + 1] == 'P' ? "\\p{^#{m[1]}}" : m[0])
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
end
|
data/lib/janeway/lexer.rb
CHANGED
|
@@ -200,9 +200,11 @@ module Janeway
|
|
|
200
200
|
non_delimiter = OTHER_DELIMITER[delimiter]
|
|
201
201
|
|
|
202
202
|
literal_chars = []
|
|
203
|
+
offsets = [] # index within the lexeme of the start of each literal char, see Token#offsets
|
|
203
204
|
while lookahead != delimiter && source_uncompleted?
|
|
204
205
|
# Transform escaped representation to literal chars
|
|
205
206
|
next_char = lookahead
|
|
207
|
+
offsets << (next_p - lexeme_start_p)
|
|
206
208
|
literal_chars <<
|
|
207
209
|
if next_char == '\\'
|
|
208
210
|
if lookahead(2) == delimiter
|
|
@@ -232,7 +234,7 @@ module Janeway
|
|
|
232
234
|
# lexeme value includes delimiters and literal escape characters
|
|
233
235
|
lexeme = current_lexeme
|
|
234
236
|
|
|
235
|
-
Token.new(:string, lexeme, literal, current_location)
|
|
237
|
+
Token.new(:string, lexeme, literal, current_location, offsets)
|
|
236
238
|
end
|
|
237
239
|
|
|
238
240
|
# Read escape char literals, and transform them into the described character
|
data/lib/janeway/parser.rb
CHANGED
data/lib/janeway/token.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'forwardable'
|
|
4
|
+
require_relative 'location'
|
|
4
5
|
|
|
5
6
|
module Janeway
|
|
6
7
|
# Tokens are produced by the lexer, they represent jsonpath query elements in a low-level, non-hierarchical way.
|
|
@@ -9,13 +10,50 @@ module Janeway
|
|
|
9
10
|
|
|
10
11
|
attr_reader :type, :lexeme, :literal, :location
|
|
11
12
|
|
|
13
|
+
# For a string token, the index within the lexeme of each character of the decoded literal,
|
|
14
|
+
# so lexeme[offsets[i]] is where literal[i] came from. It differs from i when the string
|
|
15
|
+
# contains escape sequences or the lexeme starts with a quote. Nil for other token types.
|
|
16
|
+
# @return [Array<Integer>, nil]
|
|
17
|
+
attr_reader :offsets
|
|
18
|
+
|
|
12
19
|
def_delegators :@location, :line, :col, :length
|
|
13
20
|
|
|
14
|
-
def initialize(type, lexeme, literal, location)
|
|
21
|
+
def initialize(type, lexeme, literal, location, offsets = nil)
|
|
15
22
|
@type = type
|
|
16
23
|
@lexeme = lexeme
|
|
17
24
|
@literal = literal
|
|
18
25
|
@location = location
|
|
26
|
+
@offsets = offsets
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Find where a character of a string token's decoded value came from in the query.
|
|
30
|
+
#
|
|
31
|
+
# Relies on two invariants: offsets[i] is the index within the lexeme of the start of literal[i],
|
|
32
|
+
# and location.col is the column of the lexeme in the query.
|
|
33
|
+
# @param index [Integer] index of a character in the decoded value
|
|
34
|
+
# @return [Integer, nil] column in the query of that character (or of the backslash that starts its
|
|
35
|
+
# escape); nil when the token has no offsets, or no character at that index
|
|
36
|
+
def column_of(index)
|
|
37
|
+
return nil if @offsets.nil? || !index.between?(0, @offsets.length - 1)
|
|
38
|
+
|
|
39
|
+
offset = @offsets[index]
|
|
40
|
+
offset && (@location.col + offset)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Find where a run of characters of a string token's decoded value came from in the query.
|
|
44
|
+
# @param offset [Integer, nil] index in the decoded value of the first character
|
|
45
|
+
# @param length [Integer, nil] number of decoded characters, treated as 1 if nil or not positive
|
|
46
|
+
# @return [Location] from the first character to the start of the last one; the location of the whole
|
|
47
|
+
# token when offset is nil or the range cannot be found in the token
|
|
48
|
+
def location_of(offset, length = 1)
|
|
49
|
+
return @location unless offset
|
|
50
|
+
|
|
51
|
+
length = 1 unless length&.positive?
|
|
52
|
+
first = column_of(offset)
|
|
53
|
+
last = column_of(offset + length - 1)
|
|
54
|
+
return @location unless first && last
|
|
55
|
+
|
|
56
|
+
Location.new(first, last - first + 1)
|
|
19
57
|
end
|
|
20
58
|
|
|
21
59
|
def to_s
|
data/lib/janeway/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: janeway-jsonpath
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Fraser Hanson
|
|
@@ -84,6 +84,7 @@ files:
|
|
|
84
84
|
- lib/janeway/interpreters/wildcard_selector_delete_if.rb
|
|
85
85
|
- lib/janeway/interpreters/wildcard_selector_interpreter.rb
|
|
86
86
|
- lib/janeway/interpreters/yielder.rb
|
|
87
|
+
- lib/janeway/iregexp.rb
|
|
87
88
|
- lib/janeway/lexer.rb
|
|
88
89
|
- lib/janeway/location.rb
|
|
89
90
|
- lib/janeway/normalized_path.rb
|
|
@@ -109,7 +110,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
109
110
|
- !ruby/object:Gem::Version
|
|
110
111
|
version: '0'
|
|
111
112
|
requirements: []
|
|
112
|
-
rubygems_version:
|
|
113
|
+
rubygems_version: 3.6.9
|
|
113
114
|
specification_version: 4
|
|
114
115
|
summary: jsonpath parser which implements the finalized IETF standard of Goessner
|
|
115
116
|
JSONPath
|