jsonpath 0.5.8 → 1.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.
@@ -1,95 +1,139 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class JsonPath
2
4
  class Enumerable
3
5
  include ::Enumerable
4
- attr_reader :allow_eval
5
- alias_method :allow_eval?, :allow_eval
6
+ include Dig
6
7
 
7
- def initialize(path, object, mode, options = nil)
8
- @path, @object, @mode, @options = path.path, object, mode, options
9
- @allow_eval = @options && @options.key?(:allow_eval) ? @options[:allow_eval] : true
8
+ def initialize(path, object, mode, options = {})
9
+ @path = path.path
10
+ @object = object
11
+ @mode = mode
12
+ @options = options
10
13
  end
11
14
 
12
15
  def each(context = @object, key = nil, pos = 0, &blk)
13
- node = key ? context[key] : context
16
+ node = key ? dig_one(context, key) : context
14
17
  @_current_node = node
15
18
  return yield_value(blk, context, key) if pos == @path.size
19
+
16
20
  case expr = @path[pos]
17
- when '*', '..'
21
+ when '*', '..', '@'
18
22
  each(context, key, pos + 1, &blk)
19
23
  when '$'
20
24
  each(context, key, pos + 1, &blk) if node == @object
21
- when '@'
22
- each(context, key, pos + 1, &blk)
23
25
  when /^\[(.*)\]$/
24
- expr[1,expr.size - 2].split(',').each do |sub_path|
25
- case sub_path[0]
26
- when ?', ?"
27
- if node.is_a?(Hash)
28
- k = sub_path[1,sub_path.size - 2]
29
- each(node, k, pos + 1, &blk) if node.key?(k)
30
- end
31
- when ??
32
- raise "Cannot use ?(...) unless eval is enabled" unless allow_eval?
33
- case node
34
- when Hash, Array
35
- (node.is_a?(Hash) ? node.keys : (0..node.size)).each do |e|
36
- @_current_node = node[e]
37
- if process_function_or_literal(sub_path[1, sub_path.size - 1])
38
- each(@_current_node, nil, pos + 1, &blk)
39
- end
40
- end
41
- else
42
- yield node if process_function_or_literal(sub_path[1, sub_path.size - 1])
43
- end
26
+ handle_wildecard(node, expr, context, key, pos, &blk)
27
+ when /\(.*\)/
28
+ keys = expr.gsub(/[()]/, '').split(',').map(&:strip)
29
+ new_context = filter_context(context, keys)
30
+ yield_value(blk, new_context, key)
31
+ end
32
+
33
+ if pos > 0 && @path[pos - 1] == '..' || (@path[pos - 1] == '*' && @path[pos] != '..')
34
+ case node
35
+ when Hash then node.each { |k, _| each(node, k, pos, &blk) }
36
+ when Array then node.each_with_index { |_, i| each(node, i, pos, &blk) }
37
+ end
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def filter_context(context, keys)
44
+ case context
45
+ when Hash
46
+ dig_as_hash(context, keys)
47
+ when Array
48
+ context.each_with_object([]) do |c, memo|
49
+ memo << dig_as_hash(c, keys)
50
+ end
51
+ end
52
+ end
53
+
54
+ def handle_wildecard(node, expr, _context, _key, pos, &blk)
55
+ expr[1, expr.size - 2].split(',').each do |sub_path|
56
+ case sub_path[0]
57
+ when '\'', '"'
58
+ k = sub_path[1, sub_path.size - 2]
59
+ yield_if_diggable(node, k) do
60
+ each(node, k, pos + 1, &blk)
61
+ end
62
+ when '?'
63
+ handle_question_mark(sub_path, node, pos, &blk)
64
+ else
65
+ next if node.is_a?(Array) && node.empty?
66
+ next if node.nil? # when default_path_leaf_to_null is true
67
+
68
+ array_args = sub_path.split(':')
69
+ if array_args[0] == '*'
70
+ start_idx = 0
71
+ end_idx = node.size - 1
72
+ elsif sub_path.count(':') == 0
73
+ start_idx = end_idx = process_function_or_literal(array_args[0], 0)
74
+ next unless start_idx
75
+ next if start_idx >= node.size
44
76
  else
45
- if node.is_a?(Array)
46
- next if node.empty?
47
- array_args = sub_path.split(':')
48
- if array_args[0] == ?*
49
- start_idx = 0
50
- end_idx = node.size - 1
51
- else
52
- start_idx = process_function_or_literal(array_args[0], 0)
53
- next unless start_idx
54
- end_idx = (array_args[1] && process_function_or_literal(array_args[1], -1) || (sub_path.count(':') == 0 ? start_idx : -1))
55
- next unless end_idx
56
- if start_idx == end_idx
57
- next unless start_idx < node.size
58
- end
59
- end
60
- start_idx %= node.size
61
- end_idx %= node.size
62
- step = process_function_or_literal(array_args[2], 1)
63
- next unless step
64
- (start_idx..end_idx).step(step) {|i| each(node, i, pos + 1, &blk)}
65
- end
77
+ start_idx = process_function_or_literal(array_args[0], 0)
78
+ next unless start_idx
79
+
80
+ end_idx = array_args[1] && ensure_exclusive_end_index(process_function_or_literal(array_args[1], -1)) || -1
81
+ next unless end_idx
82
+ next if start_idx == end_idx && start_idx >= node.size
66
83
  end
67
- end
68
- else
69
- if pos == (@path.size - 1) && node && allow_eval?
70
- if eval("node #{@path[pos]}")
71
- yield_value(blk, context, key)
84
+ start_idx %= node.size
85
+ end_idx %= node.size
86
+ step = process_function_or_literal(array_args[2], 1)
87
+ next unless step
88
+
89
+ if @mode == :delete
90
+ (start_idx..end_idx).step(step) { |i| node[i] = nil }
91
+ node.compact!
92
+ else
93
+ (start_idx..end_idx).step(step) { |i| each(node, i, pos + 1, &blk) }
72
94
  end
73
95
  end
74
96
  end
97
+ end
75
98
 
76
- if pos > 0 && @path[pos-1] == '..'
77
- case node
78
- when Hash then node.each {|k, v| each(node, k, pos, &blk) }
79
- when Array then node.each_with_index {|n, i| each(node, i, pos, &blk) }
99
+ def ensure_exclusive_end_index(value)
100
+ return value unless value.is_a?(Integer) && value > 0
101
+
102
+ value - 1
103
+ end
104
+
105
+ def handle_question_mark(sub_path, node, pos, &blk)
106
+ case node
107
+ when Array
108
+ node.size.times do |index|
109
+ @_current_node = node[index]
110
+ if process_function_or_literal(sub_path[1, sub_path.size - 1])
111
+ each(@_current_node, nil, pos + 1, &blk)
112
+ end
80
113
  end
114
+ when Hash
115
+ if process_function_or_literal(sub_path[1, sub_path.size - 1])
116
+ each(@_current_node, nil, pos + 1, &blk)
117
+ end
118
+ else
119
+ yield node if process_function_or_literal(sub_path[1, sub_path.size - 1])
81
120
  end
82
121
  end
83
122
 
84
- private
85
123
  def yield_value(blk, context, key)
86
124
  case @mode
87
125
  when nil
88
- blk.call(key ? context[key] : context)
126
+ blk.call(key ? dig_one(context, key) : context)
89
127
  when :compact
90
- context.delete(key) if key && context[key].nil?
128
+ if key && context[key].nil?
129
+ key.is_a?(Integer) ? context.delete_at(key) : context.delete(key)
130
+ end
91
131
  when :delete
92
- context.delete(key) if key
132
+ if key
133
+ key.is_a?(Integer) ? context.delete_at(key) : context.delete(key)
134
+ else
135
+ context.replace({})
136
+ end
93
137
  when :substitute
94
138
  if key
95
139
  context[key] = blk.call(context[key])
@@ -100,36 +144,23 @@ class JsonPath
100
144
  end
101
145
 
102
146
  def process_function_or_literal(exp, default = nil)
103
- if exp.nil?
104
- default
105
- elsif exp[0] == ?(
106
- return nil unless allow_eval? && @_current_node
107
- identifiers = /@?(\.(\w+))+/.match(exp)
108
-
109
- if !identifiers.nil? && !@_current_node.methods.include?(identifiers[2].to_sym)
110
- exp_to_eval = exp.dup
111
- exp_to_eval[identifiers[0]] = identifiers[0].split('.').map{|el| el == '@' ? '@_current_node' : "['#{el}']"}.join
112
- begin
113
- return eval(exp_to_eval)
114
- rescue StandardError # if eval failed because of bad arguments or missing methods
115
- return default
116
- end
117
- end
147
+ return default if exp.nil? || exp.empty?
148
+ return Integer(exp) if exp[0] != '('
149
+ return nil unless @_current_node
118
150
 
119
- # otherwise eval as is
120
- # TODO: this eval is wrong, because hash accessor could be nil and nil cannot be compared with anything,
121
- # for instance, @_current_node['price'] - we can't be sure that 'price' are in every node, but it's only in several nodes
122
- # I wrapped this eval into rescue returning false when error, but this eval should be refactored.
151
+ identifiers = /@?((?<!\d)\.(?!\d)(\w+))+/.match(exp)
152
+ if !identifiers.nil? && !@_current_node.methods.include?(identifiers[2].to_sym)
153
+ exp_to_eval = exp.dup
154
+ exp_to_eval[identifiers[0]] = identifiers[0].split('.').map do |el|
155
+ el == '@' ? '@' : "['#{el}']"
156
+ end.join
123
157
  begin
124
- eval(exp.gsub(/@/, '@_current_node'))
125
- rescue
126
- false
158
+ return JsonPath::Parser.new(@_current_node, @options).parse(exp_to_eval)
159
+ rescue StandardError
160
+ return default
127
161
  end
128
- elsif exp.empty?
129
- default
130
- else
131
- Integer(exp)
132
162
  end
163
+ JsonPath::Parser.new(@_current_node, @options).parse(exp)
133
164
  end
134
165
  end
135
166
  end
@@ -0,0 +1,217 @@
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@&*\/$%^?_]+'\]|\.[a-zA-Z0-9_]+[?!]?/))
72
+ elements << t.gsub(/[\[\]'.]|\s+/, '')
73
+ elsif (t = scanner.scan(/(\s+)?[<>=!\-+][=~]?(\s+)?/))
74
+ operator = t
75
+ elsif (t = scanner.scan(/(\s+)?'?.*'?(\s+)?/))
76
+ # If we encounter a node which does not contain `'` it means
77
+ #  that we are dealing with a boolean type.
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(/.*/))
90
+ raise "Could not process symbol: #{t}"
91
+ end
92
+ end
93
+
94
+ el = if elements.empty?
95
+ @_current_node
96
+ elsif @_current_node.is_a?(Hash)
97
+ dig(@_current_node, *elements)
98
+ else
99
+ elements.inject(@_current_node, &:__send__)
100
+ end
101
+
102
+ return (el ? true : false) if el.nil? || operator.nil?
103
+
104
+ el = Float(el) rescue el
105
+ operand = Float(operand) rescue operand
106
+
107
+ el.__send__(operator.strip, operand)
108
+ end
109
+
110
+ private
111
+
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
+ res &&= top[index + 1]
163
+ elsif item == '||'
164
+ res ||= top[index + 1]
165
+ end
166
+ end
167
+
168
+ #  if we are at the last item, the opening index will be 0
169
+ # and the closing index will be the last index. To avoid
170
+ # off-by-one errors we simply return the result at that point.
171
+ if closing_index + 1 >= str.length && opening_index == 0
172
+ res.to_s
173
+ else
174
+ "#{str[0..opening_index - 1]}#{res}#{str[closing_index + 1..str.length]}"
175
+ end
176
+ end
177
+
178
+ #  This is convoluted and I should probably refactor it somehow.
179
+ #  The map that is created will contain strings since essentially I'm
180
+ # constructing a string like `true || true && false`.
181
+ # With eval the need for this would disappear but never the less, here
182
+ #  it is. The fact is that the results can be either boolean, or a number
183
+ # in case there is only indexing happening like give me the 3rd item... or
184
+ # it also can be nil in case of regexes or things that aren't found.
185
+ # Hence, I have to be clever here to see what kind of variable I need to
186
+ # provide back.
187
+ def bool_or_exp(b)
188
+ if b.to_s == 'true'
189
+ return true
190
+ elsif b.to_s == 'false'
191
+ return false
192
+ elsif b.to_s == ''
193
+ return nil
194
+ end
195
+
196
+ b = Float(b) rescue b
197
+ b
198
+ end
199
+
200
+ # this simply makes sure that we aren't getting into the whole
201
+ #  parenthesis parsing business without knowing that every parenthesis
202
+ # has its pair.
203
+ def check_parenthesis_count(exp)
204
+ return true unless exp.include?('(')
205
+
206
+ depth = 0
207
+ exp.chars.each do |c|
208
+ if c == '('
209
+ depth += 1
210
+ elsif c == ')'
211
+ depth -= 1
212
+ end
213
+ end
214
+ depth == 0
215
+ end
216
+ end
217
+ end
@@ -1,18 +1,20 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class JsonPath
2
4
  class Proxy
3
5
  attr_reader :obj
4
- alias_method :to_hash, :obj
6
+ alias to_hash obj
5
7
 
6
8
  def initialize(obj)
7
9
  @obj = obj
8
10
  end
9
11
 
10
12
  def gsub(path, replacement = nil, &replacement_block)
11
- _gsub(_deep_copy, path, replacement ? proc{replacement} : replacement_block)
13
+ _gsub(_deep_copy, path, replacement ? proc(&method(:replacement)) : replacement_block)
12
14
  end
13
15
 
14
16
  def gsub!(path, replacement = nil, &replacement_block)
15
- _gsub(@obj, path, replacement ? proc{replacement} : replacement_block)
17
+ _gsub(@obj, path, replacement ? proc(&method(:replacement)) : replacement_block)
16
18
  end
17
19
 
18
20
  def delete(path = JsonPath::PATH_ALL)
@@ -32,8 +34,9 @@ class JsonPath
32
34
  end
33
35
 
34
36
  private
37
+
35
38
  def _deep_copy
36
- Marshal::load(Marshal::dump(@obj))
39
+ Marshal.load(Marshal.dump(@obj))
37
40
  end
38
41
 
39
42
  def _gsub(obj, path, replacement)
@@ -43,12 +46,22 @@ class JsonPath
43
46
 
44
47
  def _delete(obj, path)
45
48
  JsonPath.new(path)[obj, :delete].each
49
+ obj = _remove(obj)
46
50
  Proxy.new(obj)
47
51
  end
48
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
+
49
62
  def _compact(obj, path)
50
63
  JsonPath.new(path)[obj, :compact].each
51
64
  Proxy.new(obj)
52
65
  end
53
66
  end
54
- end
67
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class JsonPath
2
- VERSION = '0.5.8'
3
- end
4
+ VERSION = '1.1.0'
5
+ end
data/lib/jsonpath.rb CHANGED
@@ -1,58 +1,74 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'strscan'
2
4
  require 'multi_json'
3
5
  require 'jsonpath/proxy'
6
+ require 'jsonpath/dig'
4
7
  require 'jsonpath/enumerable'
5
8
  require 'jsonpath/version'
9
+ require 'jsonpath/parser'
6
10
 
11
+ # JsonPath: initializes the class with a given JsonPath and parses that path
12
+ # into a token array.
7
13
  class JsonPath
8
-
9
14
  PATH_ALL = '$..*'
10
15
 
16
+ DEFAULT_OPTIONS = {
17
+ :default_path_leaf_to_null => false,
18
+ :symbolize_keys => false,
19
+ :use_symbols => false,
20
+ :allow_send => true
21
+ }
22
+
11
23
  attr_accessor :path
12
24
 
13
- def initialize(path, opts = nil)
14
- @opts = opts
15
- scanner = StringScanner.new(path)
25
+ def initialize(path, opts = {})
26
+ @opts = DEFAULT_OPTIONS.merge(opts)
27
+ scanner = StringScanner.new(path.strip)
16
28
  @path = []
17
- while not scanner.eos?
18
- if token = scanner.scan(/\$/)
19
- @path << token
20
- elsif token = scanner.scan(/@/)
29
+ until scanner.eos?
30
+ if (token = scanner.scan(/\$\B|@\B|\*|\.\./))
21
31
  @path << token
22
- elsif token = scanner.scan(/[a-zA-Z0-9_-]+/)
32
+ elsif (token = scanner.scan(/[$@a-zA-Z0-9:{}_-]+/))
23
33
  @path << "['#{token}']"
24
- elsif token = scanner.scan(/'(.*?)'/)
34
+ elsif (token = scanner.scan(/'(.*?)'/))
25
35
  @path << "[#{token}]"
26
- elsif token = scanner.scan(/\[/)
27
- count = 1
28
- while !count.zero?
29
- if t = scanner.scan(/\[/)
30
- token << t
31
- count += 1
32
- elsif t = scanner.scan(/\]/)
33
- token << t
34
- count -= 1
35
- elsif t = scanner.scan(/[^\[\]]+/)
36
- token << t
37
- elsif scanner.eos?
38
- raise ArgumentError, 'unclosed bracket'
39
- end
40
- end
41
- @path << token
42
- elsif token = scanner.scan(/\]/)
36
+ elsif (token = scanner.scan(/\[/))
37
+ @path << find_matching_brackets(token, scanner)
38
+ elsif (token = scanner.scan(/\]/))
43
39
  raise ArgumentError, 'unmatched closing bracket'
44
- elsif token = scanner.scan(/\.\./)
40
+ elsif (token = scanner.scan(/\(.*\)/))
45
41
  @path << token
46
42
  elsif scanner.scan(/\./)
47
43
  nil
48
- elsif token = scanner.scan(/\*/)
49
- @path << token
50
- elsif token = scanner.scan(/[><=] \d+/)
51
- @path.last << token
52
- elsif token = scanner.scan(/./)
44
+ elsif (token = scanner.scan(/[><=] \d+/))
53
45
  @path.last << token
46
+ elsif (token = scanner.scan(/./))
47
+ begin
48
+ @path.last << token
49
+ rescue RuntimeError
50
+ raise ArgumentError, "character '#{token}' not supported in query"
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ def find_matching_brackets(token, scanner)
57
+ count = 1
58
+ until count.zero?
59
+ if (t = scanner.scan(/\[/))
60
+ token << t
61
+ count += 1
62
+ elsif (t = scanner.scan(/\]/))
63
+ token << t
64
+ count -= 1
65
+ elsif (t = scanner.scan(/[^\[\]]+/))
66
+ token << t
67
+ elsif scanner.eos?
68
+ raise ArgumentError, 'unclosed bracket'
54
69
  end
55
70
  end
71
+ token
56
72
  end
57
73
 
58
74
  def join(join_path)
@@ -61,8 +77,14 @@ class JsonPath
61
77
  res
62
78
  end
63
79
 
64
- def on(obj_or_str)
65
- enum_on(obj_or_str).to_a
80
+ def on(obj_or_str, opts = {})
81
+ a = enum_on(obj_or_str).to_a
82
+ if opts[:symbolize_keys]
83
+ a.map! do |e|
84
+ e.each_with_object({}) { |(k, v), memo| memo[k.to_sym] = v; }
85
+ end
86
+ end
87
+ a
66
88
  end
67
89
 
68
90
  def first(obj_or_str, *args)
@@ -70,12 +92,13 @@ class JsonPath
70
92
  end
71
93
 
72
94
  def enum_on(obj_or_str, mode = nil)
73
- JsonPath::Enumerable.new(self, self.class.process_object(obj_or_str), mode, @opts)
95
+ JsonPath::Enumerable.new(self, self.class.process_object(obj_or_str), mode,
96
+ @opts)
74
97
  end
75
- alias_method :[], :enum_on
98
+ alias [] enum_on
76
99
 
77
- def self.on(obj_or_str, path, opts = nil)
78
- self.new(path, opts).on(process_object(obj_or_str))
100
+ def self.on(obj_or_str, path, opts = {})
101
+ new(path, opts).on(process_object(obj_or_str))
79
102
  end
80
103
 
81
104
  def self.for(obj_or_str)
@@ -83,6 +106,7 @@ class JsonPath
83
106
  end
84
107
 
85
108
  private
109
+
86
110
  def self.process_object(obj_or_str)
87
111
  obj_or_str.is_a?(String) ? MultiJson.decode(obj_or_str) : obj_or_str
88
112
  end