jsonpath 0.9.3 → 1.1.5

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