Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,6 @@ enum.any?{ |c| c == 'red' }
# => true
```

You can optionally prevent eval from being called on sub-expressions by passing in :allow_eval => false to the constructor.

### More examples

For more usage examples and variations on paths, please visit the tests. There are some more complex ones as well.
Expand Down
1 change: 1 addition & 0 deletions lib/jsonpath.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require 'jsonpath/proxy'
require 'jsonpath/enumerable'
require 'jsonpath/version'
require 'jsonpath/parser'

# JsonPath: initializes the class with a given JsonPath and parses that path
# into a token array.
Expand Down
40 changes: 6 additions & 34 deletions lib/jsonpath/enumerable.rb
Original file line number Diff line number Diff line change
@@ -1,19 +1,12 @@
class JsonPath
class Enumerable
include ::Enumerable
attr_reader :allow_eval
alias_method :allow_eval?, :allow_eval

def initialize(path, object, mode, options = nil)
@path = path.path
@object = object
@mode = mode
@options = options
@allow_eval = if @options && @options.key?(:allow_eval)
@options[:allow_eval]
else
true
end
end

def each(context = @object, key = nil, pos = 0, &blk)
Expand All @@ -27,10 +20,6 @@ def each(context = @object, key = nil, pos = 0, &blk)
each(context, key, pos + 1, &blk) if node == @object
when /^\[(.*)\]$/
handle_wildecard(node, expr, context, key, pos, &blk)
else
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I literary found no use for this code segment.

Let it be said here, if anybody has any problems with this missing segment, submit an issue, and I'll investigate use cases for that part.

if pos == (@path.size - 1) && node && allow_eval?
yield_value(blk, context, key) if instance_eval("node #{@path[pos]}")
end
end

if pos > 0 && @path[pos - 1] == '..' || (@path[pos - 1] == '*' && @path[pos] != '..')
Expand Down Expand Up @@ -75,11 +64,12 @@ def handle_wildecard(node, expr, context, key, pos, &blk)
end

def handle_question_mark(sub_path, node, pos, &blk)
raise 'Cannot use ?(...) unless eval is enabled' unless allow_eval?
case node
when Array
node.size.times do |index|
@_current_node = node[index]
# exps = sub_path[1, sub_path.size - 1]
# if @_current_node.send("[#{exps.gsub(/@/, '@_current_node')}]")
if process_function_or_literal(sub_path[1, sub_path.size - 1])
each(@_current_node, nil, pos + 1, &blk)
end
Expand Down Expand Up @@ -113,40 +103,22 @@ def yield_value(blk, context, key)
def process_function_or_literal(exp, default = nil)
return default if exp.nil? || exp.empty?
return Integer(exp) if exp[0] != '('
return nil unless allow_eval? && @_current_node
return nil unless @_current_node

identifiers = /@?((?<!\d)\.(?!\d)(\w+))+/.match(exp)
# puts JsonPath.on(@_current_node, "#{identifiers}") unless identifiers.nil? ||
# @_current_node
# .methods
# .include?(identifiers[2].to_sym)

unless identifiers.nil? ||
@_current_node.methods.include?(identifiers[2].to_sym)
exp_to_eval = exp.dup
exp_to_eval[identifiers[0]] = identifiers[0].split('.').map do |el|
el == '@' ? '@_current_node' : "['#{el}']"
el == '@' ? '@' : "['#{el}']"
end.join

begin
return instance_eval(exp_to_eval)
# if eval failed because of bad arguments or missing methods
return JsonPath::Parser.new(@_current_node).parse(exp_to_eval)
rescue StandardError
return default
end
end

# otherwise eval as is
# TODO: this eval is wrong, because hash accessor could be nil and nil
# cannot be compared with anything, for instance,
# @a_current_node['price'] - we can't be sure that 'price' are in every
# node, but it's only in several nodes I wrapped this eval into rescue
# returning false when error, but this eval should be refactored.
begin
instance_eval(exp.gsub(/@/, '@_current_node'))
rescue
false
end
JsonPath::Parser.new(@_current_node).parse(exp)
end
end
end
62 changes: 62 additions & 0 deletions lib/jsonpath/parser.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
require 'strscan'

class JsonPath
# Parser parses and evaluates an expression passed to @_current_node.
class Parser
def initialize(node)
@_current_node = node
end

def parse(exp)
exps = exp.split(/(&&)|(\|\|)/)
ret = parse_exp(exps.shift)
exps.each_with_index do |item, index|
case item
when '&&'
ret &&= parse_exp(exps[index + 1])
when '||'
ret ||= parse_exp(exps[index + 1])
end
end
ret
end

def parse_exp(exp)
exp = exp.gsub(/@/, '').gsub(/[\(\)]/, '')
scanner = StringScanner.new(exp)
elements = []
until scanner.eos?
if scanner.scan(/\./)
sym = scanner.scan(/\w+/)
op = scanner.scan(/./)
num = scanner.scan(/\d+/)
return @_current_node.send(sym.to_sym).send(op.to_sym, num.to_i)
end
if t = scanner.scan(/\['\w+'\]+/)
elements << t.gsub(/\[|\]|'|\s+/, '')
elsif t = scanner.scan(/\s+[<>=][<>=]?\s+?/)
operator = t
elsif t = scanner.scan(/(\s+)?'?(\w+)?[.,]?(\w+)?'?(\s+)?/) # @TODO: At this point I should trim somewhere...
operand = t.delete("'").strip
elsif t = scanner.scan(/.*/)
raise "Could not process symbol: #{t}"
end
end
el = dig(elements, @_current_node)
return false unless el
return true if operator.nil? && el
operand = operand.to_f if operand.to_i.to_s == operand || operand.to_f.to_s == operand
el.send(operator.strip, operand)
end

private

# @TODO: Remove this once JsonPath no longer supports ruby versions below 2.3
def dig(keys, hash)
return nil unless hash.key?(keys.first)
return hash.fetch(keys.first) if keys.size == 1
prev = keys.shift
dig(keys, hash.fetch(prev))
end
end
end
2 changes: 1 addition & 1 deletion lib/jsonpath/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
class JsonPath
VERSION = '0.7.2'.freeze
VERSION = '0.8.2'.freeze
end
13 changes: 9 additions & 4 deletions test/test_jsonpath.rb
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ def test_eval_with_floating_point
assert_equal [@object['store']['book'][1]], JsonPath.new("$..book[?(@['price'] < 23.0 && @['price'] > 9.0)]").on(@object)
end

def test_no_eval
assert_equal [], JsonPath.new('$..book[(@.length-2)]', allow_eval: false).on(@object)
def test_eval_with_floating_point
assert_equal [@object['store']['book'][1]], JsonPath.new("$..book[?(@['price'] == 13.0)]").on(@object)
end

def test_paths_with_underscores
Expand Down Expand Up @@ -109,7 +109,7 @@ def test_use_first
end

def test_counting
assert_equal 54, JsonPath.new('$..*').on(@object).to_a.size
assert_equal 57, JsonPath.new('$..*').on(@object).to_a.size
end

def test_space_in_path
Expand Down Expand Up @@ -255,14 +255,19 @@ def test_support_underscore_in_member_names
JsonPath.new("$.store._links").on(@object)
end

# def test_filter_support_include
# #assert_equal true, JsonPath.new("$.store.book[(@.tags == 'asdf3')]").on(@object)
# assert_equal true, JsonPath.new("$.store.book..tags[?(@ == 'asdf')]").on(@object)
# end

def example_object
{ 'store' => {
'book' => [
{ 'category' => 'reference',
'author' => 'Nigel Rees',
'title' => 'Sayings of the Century',
'price' => 9 },
'price' => 9,
'tags' => ['asdf', 'asdf2']},
{ 'category' => 'fiction',
'author' => 'Evelyn Waugh',
'title' => 'Sword of Honour',
Expand Down