jsonpath 0.5.8 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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