iso-jsonpath 1.1.6
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/.gemtest +0 -0
- data/.github/workflows/test.yml +33 -0
- data/.gitignore +10 -0
- data/.rspec +3 -0
- data/.rubocop.yml +1 -0
- data/.rubocop_todo.yml +127 -0
- data/Gemfile +6 -0
- data/LICENSE.md +21 -0
- data/README.md +301 -0
- data/Rakefile +23 -0
- data/bin/jsonpath +23 -0
- data/jsonpath.gemspec +28 -0
- data/lib/jsonpath/dig.rb +57 -0
- data/lib/jsonpath/enumerable.rb +170 -0
- data/lib/jsonpath/parser.rb +221 -0
- data/lib/jsonpath/proxy.rb +67 -0
- data/lib/jsonpath/version.rb +5 -0
- data/lib/jsonpath.rb +160 -0
- data/test/test_jsonpath.rb +1334 -0
- data/test/test_jsonpath_bin.rb +23 -0
- data/test/test_readme.rb +117 -0
- metadata +166 -0
@@ -0,0 +1,170 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class JsonPath
|
4
|
+
class Enumerable
|
5
|
+
include ::Enumerable
|
6
|
+
include Dig
|
7
|
+
|
8
|
+
def initialize(path, object, mode, options = {})
|
9
|
+
@path = path.path
|
10
|
+
@object = object
|
11
|
+
@mode = mode
|
12
|
+
@options = options
|
13
|
+
end
|
14
|
+
|
15
|
+
def each(context = @object, key = nil, pos = 0, &blk)
|
16
|
+
node = key ? dig_one(context, key) : context
|
17
|
+
@_current_node = node
|
18
|
+
return yield_value(blk, context, key) if pos == @path.size
|
19
|
+
|
20
|
+
case expr = @path[pos]
|
21
|
+
when '*', '..', '@'
|
22
|
+
each(context, key, pos + 1, &blk)
|
23
|
+
when '$'
|
24
|
+
if node == @object
|
25
|
+
each(context, key, pos + 1, &blk)
|
26
|
+
else
|
27
|
+
handle_wildcard(node, "['#{expr}']", context, key, pos, &blk)
|
28
|
+
end
|
29
|
+
when /^\[(.*)\]$/
|
30
|
+
handle_wildcard(node, expr, context, key, pos, &blk)
|
31
|
+
when /\(.*\)/
|
32
|
+
keys = expr.gsub(/[()]/, '').split(',').map(&:strip)
|
33
|
+
new_context = filter_context(context, keys)
|
34
|
+
yield_value(blk, new_context, key)
|
35
|
+
end
|
36
|
+
|
37
|
+
if pos > 0 && @path[pos - 1] == '..' || (@path[pos - 1] == '*' && @path[pos] != '..')
|
38
|
+
case node
|
39
|
+
when Hash then node.each { |k, _| each(node, k, pos, &blk) }
|
40
|
+
when Array then node.each_with_index { |_, i| each(node, i, pos, &blk) }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def filter_context(context, keys)
|
48
|
+
case context
|
49
|
+
when Hash
|
50
|
+
dig_as_hash(context, keys)
|
51
|
+
when Array
|
52
|
+
context.each_with_object([]) do |c, memo|
|
53
|
+
memo << dig_as_hash(c, keys)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def handle_wildcard(node, expr, _context, _key, pos, &blk)
|
59
|
+
expr[1, expr.size - 2].split(',').each do |sub_path|
|
60
|
+
case sub_path[0]
|
61
|
+
when '\'', '"'
|
62
|
+
k = sub_path[1, sub_path.size - 2]
|
63
|
+
yield_if_diggable(node, k) do
|
64
|
+
each(node, k, pos + 1, &blk)
|
65
|
+
end
|
66
|
+
when '?'
|
67
|
+
handle_question_mark(sub_path, node, pos, &blk)
|
68
|
+
else
|
69
|
+
next if node.is_a?(Array) && node.empty?
|
70
|
+
next if node.nil? # when default_path_leaf_to_null is true
|
71
|
+
next if node.size.zero?
|
72
|
+
|
73
|
+
array_args = sub_path.split(':')
|
74
|
+
if array_args[0] == '*'
|
75
|
+
start_idx = 0
|
76
|
+
end_idx = node.size - 1
|
77
|
+
elsif sub_path.count(':') == 0
|
78
|
+
start_idx = end_idx = process_function_or_literal(array_args[0], 0)
|
79
|
+
next unless start_idx
|
80
|
+
next if start_idx >= node.size
|
81
|
+
else
|
82
|
+
start_idx = process_function_or_literal(array_args[0], 0)
|
83
|
+
next unless start_idx
|
84
|
+
|
85
|
+
end_idx = array_args[1] && ensure_exclusive_end_index(process_function_or_literal(array_args[1], -1)) || -1
|
86
|
+
next unless end_idx
|
87
|
+
next if start_idx == end_idx && start_idx >= node.size
|
88
|
+
end
|
89
|
+
|
90
|
+
start_idx %= node.size
|
91
|
+
end_idx %= node.size
|
92
|
+
step = process_function_or_literal(array_args[2], 1)
|
93
|
+
next unless step
|
94
|
+
|
95
|
+
if @mode == :delete
|
96
|
+
(start_idx..end_idx).step(step) { |i| node[i] = nil }
|
97
|
+
node.compact!
|
98
|
+
else
|
99
|
+
(start_idx..end_idx).step(step) { |i| each(node, i, pos + 1, &blk) }
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def ensure_exclusive_end_index(value)
|
106
|
+
return value unless value.is_a?(Integer) && value > 0
|
107
|
+
|
108
|
+
value - 1
|
109
|
+
end
|
110
|
+
|
111
|
+
def handle_question_mark(sub_path, node, pos, &blk)
|
112
|
+
case node
|
113
|
+
when Array
|
114
|
+
node.size.times do |index|
|
115
|
+
@_current_node = node[index]
|
116
|
+
if process_function_or_literal(sub_path[1, sub_path.size - 1])
|
117
|
+
each(@_current_node, nil, pos + 1, &blk)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
when Hash
|
121
|
+
if process_function_or_literal(sub_path[1, sub_path.size - 1])
|
122
|
+
each(@_current_node, nil, pos + 1, &blk)
|
123
|
+
end
|
124
|
+
else
|
125
|
+
yield node if process_function_or_literal(sub_path[1, sub_path.size - 1])
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def yield_value(blk, context, key)
|
130
|
+
case @mode
|
131
|
+
when nil
|
132
|
+
blk.call(key ? dig_one(context, key) : context)
|
133
|
+
when :compact
|
134
|
+
if key && context[key].nil?
|
135
|
+
key.is_a?(Integer) ? context.delete_at(key) : context.delete(key)
|
136
|
+
end
|
137
|
+
when :delete
|
138
|
+
if key
|
139
|
+
key.is_a?(Integer) ? context.delete_at(key) : context.delete(key)
|
140
|
+
else
|
141
|
+
context.replace({})
|
142
|
+
end
|
143
|
+
when :substitute
|
144
|
+
if key
|
145
|
+
context[key] = blk.call(context[key])
|
146
|
+
else
|
147
|
+
context.replace(blk.call(context[key]))
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def process_function_or_literal(exp, default = nil)
|
153
|
+
return default if exp.nil? || exp.empty?
|
154
|
+
return Integer(exp) if exp[0] != '('
|
155
|
+
return nil unless @_current_node
|
156
|
+
|
157
|
+
identifiers = /@?(((?<!\d)\.(?!\d)(\w+))|\['(.*?)'\])+/.match(exp)
|
158
|
+
# to filter arrays with known/unknown name.
|
159
|
+
if (!identifiers.nil? && !(@_current_node.methods.include?(identifiers[2]&.to_sym) || @_current_node.methods.include?(identifiers[4]&.to_sym)))
|
160
|
+
exp_to_eval = exp.dup
|
161
|
+
begin
|
162
|
+
return JsonPath::Parser.new(@_current_node, @options).parse(exp_to_eval)
|
163
|
+
rescue StandardError
|
164
|
+
return default
|
165
|
+
end
|
166
|
+
end
|
167
|
+
JsonPath::Parser.new(@_current_node, @options).parse(exp)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
@@ -0,0 +1,221 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'strscan'
|
4
|
+
|
5
|
+
class JsonPath
|
6
|
+
# Parser parses and evaluates an expression passed to @_current_node.
|
7
|
+
class Parser
|
8
|
+
include Dig
|
9
|
+
|
10
|
+
REGEX = /\A\/(.+)\/([imxnesu]*)\z|\A%r{(.+)}([imxnesu]*)\z/
|
11
|
+
|
12
|
+
def initialize(node, options)
|
13
|
+
@_current_node = node
|
14
|
+
@_expr_map = {}
|
15
|
+
@options = options
|
16
|
+
end
|
17
|
+
|
18
|
+
# parse will parse an expression in the following way.
|
19
|
+
# Split the expression up into an array of legs for && and || operators.
|
20
|
+
# Parse this array into a map for which the keys are the parsed legs
|
21
|
+
# of the split. This map is then used to replace the expression with their
|
22
|
+
# corresponding boolean or numeric value. This might look something like this:
|
23
|
+
# ((false || false) && (false || true))
|
24
|
+
# Once this string is assembled... we proceed to evaluate from left to right.
|
25
|
+
# The above string is broken down like this:
|
26
|
+
# (false && (false || true))
|
27
|
+
# (false && true)
|
28
|
+
# false
|
29
|
+
def parse(exp)
|
30
|
+
exps = exp.split(/(&&)|(\|\|)/)
|
31
|
+
construct_expression_map(exps)
|
32
|
+
@_expr_map.each { |k, v| exp.sub!(k, v.to_s) }
|
33
|
+
raise ArgumentError, "unmatched parenthesis in expression: #{exp}" unless check_parenthesis_count(exp)
|
34
|
+
|
35
|
+
exp = parse_parentheses(exp) while exp.include?('(')
|
36
|
+
bool_or_exp(exp)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Construct a map for which the keys are the expressions
|
40
|
+
# and the values are the corresponding parsed results.
|
41
|
+
# Exp.:
|
42
|
+
# {"(@['author'] =~ /herman|lukyanenko/i)"=>0}
|
43
|
+
# {"@['isTrue']"=>true}
|
44
|
+
def construct_expression_map(exps)
|
45
|
+
exps.each_with_index do |item, _index|
|
46
|
+
next if item == '&&' || item == '||'
|
47
|
+
|
48
|
+
item = item.strip.gsub(/\)*$/, '').gsub(/^\(*/, '')
|
49
|
+
@_expr_map[item] = parse_exp(item)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Using a scanner break down the individual expressions and determine if
|
54
|
+
# there is a match in the JSON for it or not.
|
55
|
+
def parse_exp(exp)
|
56
|
+
exp = exp.sub(/@/, '').gsub(/^\(/, '').gsub(/\)$/, '').tr('"', '\'').strip
|
57
|
+
exp.scan(/^\[(\d+)\]/) do |i|
|
58
|
+
next if i.empty?
|
59
|
+
|
60
|
+
index = Integer(i[0])
|
61
|
+
raise ArgumentError, 'Node does not appear to be an array.' unless @_current_node.is_a?(Array)
|
62
|
+
raise ArgumentError, "Index out of bounds for nested array. Index: #{index}" if @_current_node.size < index
|
63
|
+
|
64
|
+
@_current_node = @_current_node[index]
|
65
|
+
# Remove the extra '' and the index.
|
66
|
+
exp = exp.gsub(/^\[\d+\]|\[''\]/, '')
|
67
|
+
end
|
68
|
+
scanner = StringScanner.new(exp)
|
69
|
+
elements = []
|
70
|
+
until scanner.eos?
|
71
|
+
if (t = scanner.scan(/\['[a-zA-Z@&*\/$%^?_ ]+'\]/))
|
72
|
+
elements << t.gsub(/^\s*\[\s*'(.*)'\s*\]\s*$/, '\1')
|
73
|
+
elsif (t = scanner.scan(/\.[a-zA-Z0-9_]+[?]?/))
|
74
|
+
elements << t.gsub(/[\[\]'.]|\s+/, '')
|
75
|
+
elsif (t = scanner.scan(/(\s+)?[<>=!\-+][=~]?(\s+)?/))
|
76
|
+
operator = t
|
77
|
+
elsif (t = scanner.scan(/(\s+)?'?.*'?(\s+)?/))
|
78
|
+
# If we encounter a node which does not contain `'` it means
|
79
|
+
# that we are dealing with a boolean type.
|
80
|
+
operand =
|
81
|
+
if t == 'true'
|
82
|
+
true
|
83
|
+
elsif t == 'false'
|
84
|
+
false
|
85
|
+
elsif operator.to_s.strip == '=~'
|
86
|
+
parse_regex(t)
|
87
|
+
else
|
88
|
+
t.gsub(%r{^'|'$}, '').strip
|
89
|
+
end
|
90
|
+
elsif (t = scanner.scan(/\/\w+\//))
|
91
|
+
elsif (t = scanner.scan(/.*/))
|
92
|
+
raise "Could not process symbol: #{t}"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
el = if elements.empty?
|
97
|
+
@_current_node
|
98
|
+
elsif @_current_node.is_a?(Hash)
|
99
|
+
dig(@_current_node, *elements)
|
100
|
+
else
|
101
|
+
elements.inject(@_current_node, &:__send__)
|
102
|
+
end
|
103
|
+
|
104
|
+
return (el ? true : false) if el.nil? || operator.nil?
|
105
|
+
|
106
|
+
el = Float(el) rescue el
|
107
|
+
operand = Float(operand) rescue operand
|
108
|
+
|
109
|
+
el.__send__(operator.strip, operand)
|
110
|
+
end
|
111
|
+
|
112
|
+
private
|
113
|
+
|
114
|
+
# /foo/i -> Regex.new("foo", Regexp::IGNORECASE) without using eval
|
115
|
+
# also supports %r{foo}i
|
116
|
+
# following https://github.com/seamusabshere/to_regexp/blob/master/lib/to_regexp.rb
|
117
|
+
def parse_regex(t)
|
118
|
+
t =~ REGEX
|
119
|
+
content = $1 || $3
|
120
|
+
options = $2 || $4
|
121
|
+
|
122
|
+
raise ArgumentError, "unsupported regex #{t} use /foo/ style" if !content || !options
|
123
|
+
|
124
|
+
content = content.gsub '\\/', '/'
|
125
|
+
|
126
|
+
flags = 0
|
127
|
+
flags |= Regexp::IGNORECASE if options.include?('i')
|
128
|
+
flags |= Regexp::MULTILINE if options.include?('m')
|
129
|
+
flags |= Regexp::EXTENDED if options.include?('x')
|
130
|
+
|
131
|
+
# 'n' = none, 'e' = EUC, 's' = SJIS, 'u' = UTF-8
|
132
|
+
lang = options.scan(/[nes]/).join.downcase # ignores u since that is default and causes a warning
|
133
|
+
|
134
|
+
args = [content, flags]
|
135
|
+
args << lang unless lang.empty? # avoid warning
|
136
|
+
Regexp.new(*args)
|
137
|
+
end
|
138
|
+
|
139
|
+
# This will break down a parenthesis from the left to the right
|
140
|
+
# and replace the given expression with it's returned value.
|
141
|
+
# It does this in order to make it easy to eliminate groups
|
142
|
+
# one-by-one.
|
143
|
+
def parse_parentheses(str)
|
144
|
+
opening_index = 0
|
145
|
+
closing_index = 0
|
146
|
+
|
147
|
+
(0..str.length - 1).step(1) do |i|
|
148
|
+
opening_index = i if str[i] == '('
|
149
|
+
if str[i] == ')'
|
150
|
+
closing_index = i
|
151
|
+
break
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
to_parse = str[opening_index + 1..closing_index - 1]
|
156
|
+
|
157
|
+
# handle cases like (true && true || false && true) in
|
158
|
+
# one giant parenthesis.
|
159
|
+
top = to_parse.split(/(&&)|(\|\|)/)
|
160
|
+
top = top.map(&:strip)
|
161
|
+
res = bool_or_exp(top.shift)
|
162
|
+
top.each_with_index do |item, index|
|
163
|
+
if item == '&&'
|
164
|
+
next_value = bool_or_exp(top[index + 1])
|
165
|
+
res &&= next_value
|
166
|
+
elsif item == '||'
|
167
|
+
next_value = bool_or_exp(top[index + 1])
|
168
|
+
res ||= next_value
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# if we are at the last item, the opening index will be 0
|
173
|
+
# and the closing index will be the last index. To avoid
|
174
|
+
# off-by-one errors we simply return the result at that point.
|
175
|
+
if closing_index + 1 >= str.length && opening_index == 0
|
176
|
+
res.to_s
|
177
|
+
else
|
178
|
+
"#{str[0..opening_index - 1]}#{res}#{str[closing_index + 1..str.length]}"
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
# This is convoluted and I should probably refactor it somehow.
|
183
|
+
# The map that is created will contain strings since essentially I'm
|
184
|
+
# constructing a string like `true || true && false`.
|
185
|
+
# With eval the need for this would disappear but never the less, here
|
186
|
+
# it is. The fact is that the results can be either boolean, or a number
|
187
|
+
# in case there is only indexing happening like give me the 3rd item... or
|
188
|
+
# it also can be nil in case of regexes or things that aren't found.
|
189
|
+
# Hence, I have to be clever here to see what kind of variable I need to
|
190
|
+
# provide back.
|
191
|
+
def bool_or_exp(b)
|
192
|
+
if b.to_s == 'true'
|
193
|
+
return true
|
194
|
+
elsif b.to_s == 'false'
|
195
|
+
return false
|
196
|
+
elsif b.to_s == ''
|
197
|
+
return nil
|
198
|
+
end
|
199
|
+
|
200
|
+
b = Float(b) rescue b
|
201
|
+
b
|
202
|
+
end
|
203
|
+
|
204
|
+
# this simply makes sure that we aren't getting into the whole
|
205
|
+
# parenthesis parsing business without knowing that every parenthesis
|
206
|
+
# has its pair.
|
207
|
+
def check_parenthesis_count(exp)
|
208
|
+
return true unless exp.include?('(')
|
209
|
+
|
210
|
+
depth = 0
|
211
|
+
exp.chars.each do |c|
|
212
|
+
if c == '('
|
213
|
+
depth += 1
|
214
|
+
elsif c == ')'
|
215
|
+
depth -= 1
|
216
|
+
end
|
217
|
+
end
|
218
|
+
depth == 0
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class JsonPath
|
4
|
+
class Proxy
|
5
|
+
attr_reader :obj
|
6
|
+
alias to_hash obj
|
7
|
+
|
8
|
+
def initialize(obj)
|
9
|
+
@obj = obj
|
10
|
+
end
|
11
|
+
|
12
|
+
def gsub(path, replacement = nil, &replacement_block)
|
13
|
+
_gsub(_deep_copy, path, replacement ? proc(&method(:replacement)) : replacement_block)
|
14
|
+
end
|
15
|
+
|
16
|
+
def gsub!(path, replacement = nil, &replacement_block)
|
17
|
+
_gsub(@obj, path, replacement ? proc(&method(:replacement)) : replacement_block)
|
18
|
+
end
|
19
|
+
|
20
|
+
def delete(path = JsonPath::PATH_ALL)
|
21
|
+
_delete(_deep_copy, path)
|
22
|
+
end
|
23
|
+
|
24
|
+
def delete!(path = JsonPath::PATH_ALL)
|
25
|
+
_delete(@obj, path)
|
26
|
+
end
|
27
|
+
|
28
|
+
def compact(path = JsonPath::PATH_ALL)
|
29
|
+
_compact(_deep_copy, path)
|
30
|
+
end
|
31
|
+
|
32
|
+
def compact!(path = JsonPath::PATH_ALL)
|
33
|
+
_compact(@obj, path)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def _deep_copy
|
39
|
+
Marshal.load(Marshal.dump(@obj))
|
40
|
+
end
|
41
|
+
|
42
|
+
def _gsub(obj, path, replacement)
|
43
|
+
JsonPath.new(path)[obj, :substitute].each(&replacement)
|
44
|
+
Proxy.new(obj)
|
45
|
+
end
|
46
|
+
|
47
|
+
def _delete(obj, path)
|
48
|
+
JsonPath.new(path)[obj, :delete].each
|
49
|
+
obj = _remove(obj)
|
50
|
+
Proxy.new(obj)
|
51
|
+
end
|
52
|
+
|
53
|
+
def _remove(obj)
|
54
|
+
obj.each do |o|
|
55
|
+
if o.is_a?(Hash) || o.is_a?(Array)
|
56
|
+
_remove(o)
|
57
|
+
o.delete({})
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def _compact(obj, path)
|
63
|
+
JsonPath.new(path)[obj, :compact].each
|
64
|
+
Proxy.new(obj)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
data/lib/jsonpath.rb
ADDED
@@ -0,0 +1,160 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'strscan'
|
4
|
+
require 'multi_json'
|
5
|
+
require 'jsonpath/proxy'
|
6
|
+
require 'jsonpath/dig'
|
7
|
+
require 'jsonpath/enumerable'
|
8
|
+
require 'jsonpath/version'
|
9
|
+
require 'jsonpath/parser'
|
10
|
+
|
11
|
+
# JsonPath: initializes the class with a given JsonPath and parses that path
|
12
|
+
# into a token array.
|
13
|
+
class JsonPath
|
14
|
+
PATH_ALL = '$..*'
|
15
|
+
MAX_NESTING_ALLOWED = 100
|
16
|
+
|
17
|
+
DEFAULT_OPTIONS = {
|
18
|
+
:default_path_leaf_to_null => false,
|
19
|
+
:symbolize_keys => false,
|
20
|
+
:use_symbols => false,
|
21
|
+
:allow_send => true,
|
22
|
+
:max_nesting => MAX_NESTING_ALLOWED
|
23
|
+
}
|
24
|
+
|
25
|
+
attr_accessor :path
|
26
|
+
|
27
|
+
def initialize(path, opts = {})
|
28
|
+
@opts = DEFAULT_OPTIONS.merge(opts)
|
29
|
+
set_max_nesting
|
30
|
+
scanner = StringScanner.new(path.strip)
|
31
|
+
@path = []
|
32
|
+
until scanner.eos?
|
33
|
+
if (token = scanner.scan(/\$\B|@\B|\*|\.\./))
|
34
|
+
@path << token
|
35
|
+
elsif (token = scanner.scan(/[$@\p{Alnum}:{}_ -]+/))
|
36
|
+
@path << "['#{token}']"
|
37
|
+
elsif (token = scanner.scan(/'(.*?)'/))
|
38
|
+
@path << "[#{token}]"
|
39
|
+
elsif (token = scanner.scan(/\[/))
|
40
|
+
@path << find_matching_brackets(token, scanner)
|
41
|
+
elsif (token = scanner.scan(/\]/))
|
42
|
+
raise ArgumentError, 'unmatched closing bracket'
|
43
|
+
elsif (token = scanner.scan(/\(.*\)/))
|
44
|
+
@path << token
|
45
|
+
elsif scanner.scan(/\./)
|
46
|
+
nil
|
47
|
+
elsif (token = scanner.scan(/[><=] \d+/))
|
48
|
+
@path.last << token
|
49
|
+
elsif (token = scanner.scan(/./))
|
50
|
+
@path.last << token
|
51
|
+
else
|
52
|
+
raise ArgumentError, "character '#{scanner.peek(1)}' not supported in query"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def find_matching_brackets(token, scanner)
|
58
|
+
count = 1
|
59
|
+
until count.zero?
|
60
|
+
if (t = scanner.scan(/\[/))
|
61
|
+
token << t
|
62
|
+
count += 1
|
63
|
+
elsif (t = scanner.scan(/\]/))
|
64
|
+
token << t
|
65
|
+
count -= 1
|
66
|
+
elsif (t = scanner.scan(/[^\[\]]+/))
|
67
|
+
token << t
|
68
|
+
elsif scanner.eos?
|
69
|
+
raise ArgumentError, 'unclosed bracket'
|
70
|
+
end
|
71
|
+
end
|
72
|
+
token
|
73
|
+
end
|
74
|
+
|
75
|
+
def join(join_path)
|
76
|
+
res = deep_clone
|
77
|
+
res.path += JsonPath.new(join_path).path
|
78
|
+
res
|
79
|
+
end
|
80
|
+
|
81
|
+
def on(obj_or_str, opts = {})
|
82
|
+
a = enum_on(obj_or_str).to_a
|
83
|
+
if symbolize_keys?(opts)
|
84
|
+
a.map! do |e|
|
85
|
+
e.each_with_object({}) { |(k, v), memo| memo[k.to_sym] = v; }
|
86
|
+
end
|
87
|
+
end
|
88
|
+
a
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.fetch_all_path(obj)
|
92
|
+
all_paths = ['$']
|
93
|
+
find_path(obj, '$', all_paths, obj.class == Array)
|
94
|
+
return all_paths
|
95
|
+
end
|
96
|
+
|
97
|
+
def self.find_path(obj, root_key, all_paths, is_array = false)
|
98
|
+
obj.each do |key, value|
|
99
|
+
table_params = { key: key, root_key: root_key}
|
100
|
+
is_loop = value.class == Array || value.class == Hash
|
101
|
+
if is_loop
|
102
|
+
path_exp = construct_path(table_params)
|
103
|
+
all_paths << path_exp
|
104
|
+
find_path(value, path_exp, all_paths, value.class == Array)
|
105
|
+
elsif is_array
|
106
|
+
table_params[:index] = obj.find_index(key)
|
107
|
+
path_exp = construct_path(table_params)
|
108
|
+
find_path(key, path_exp, all_paths, key.class == Array) if key.class == Hash || key.class == Array
|
109
|
+
all_paths << path_exp
|
110
|
+
else
|
111
|
+
all_paths << construct_path(table_params)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def self.construct_path(table_row)
|
117
|
+
if table_row[:index]
|
118
|
+
return table_row[:root_key] + '['+ table_row[:index].to_s + ']'
|
119
|
+
else
|
120
|
+
return table_row[:root_key] + '.'+ table_row[:key]
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def first(obj_or_str, *args)
|
125
|
+
enum_on(obj_or_str).first(*args)
|
126
|
+
end
|
127
|
+
|
128
|
+
def enum_on(obj_or_str, mode = nil)
|
129
|
+
JsonPath::Enumerable.new(self, self.class.process_object(obj_or_str, @opts), mode,
|
130
|
+
@opts)
|
131
|
+
end
|
132
|
+
alias [] enum_on
|
133
|
+
|
134
|
+
def self.on(obj_or_str, path, opts = {})
|
135
|
+
new(path, opts).on(process_object(obj_or_str))
|
136
|
+
end
|
137
|
+
|
138
|
+
def self.for(obj_or_str)
|
139
|
+
Proxy.new(process_object(obj_or_str))
|
140
|
+
end
|
141
|
+
|
142
|
+
private
|
143
|
+
|
144
|
+
def self.process_object(obj_or_str, opts = {})
|
145
|
+
obj_or_str.is_a?(String) ? MultiJson.decode(obj_or_str, max_nesting: opts[:max_nesting]) : obj_or_str
|
146
|
+
end
|
147
|
+
|
148
|
+
def deep_clone
|
149
|
+
Marshal.load Marshal.dump(self)
|
150
|
+
end
|
151
|
+
|
152
|
+
def set_max_nesting
|
153
|
+
return unless @opts[:max_nesting].is_a?(Integer) && @opts[:max_nesting] > MAX_NESTING_ALLOWED
|
154
|
+
@opts[:max_nesting] = false
|
155
|
+
end
|
156
|
+
|
157
|
+
def symbolize_keys?(opts)
|
158
|
+
opts.fetch(:symbolize_keys, @opts&.dig(:symbolize_keys))
|
159
|
+
end
|
160
|
+
end
|