From a60526951b41934f5a6537b655db3fc2bdf3f686 Mon Sep 17 00:00:00 2001 From: Gergely Brautigam Date: Tue, 2 May 2017 08:14:25 +0200 Subject: [PATCH] A lot of refactoring and addresses pull #39 and #41. (#46) * A lot of refactoring and addresses pull #39 and #41. * Closes #44 and Closes #30. * Bumped version. * Updated readme. --- .travis.yml | 5 +- LICENSE.md | 21 ++++ README.md | 8 ++ Rakefile | 7 +- bin/jsonpath | 2 +- jsonpath.gemspec | 23 ++-- lib/jsonpath.rb | 57 +++++----- lib/jsonpath/enumerable.rb | 178 +++++++++++++++++-------------- lib/jsonpath/proxy.rb | 11 +- lib/jsonpath/version.rb | 4 +- test/test_jsonpath.rb | 213 +++++++++++++++++++++++-------------- test/test_jsonpath_bin.rb | 14 +-- 12 files changed, 321 insertions(+), 222 deletions(-) create mode 100644 LICENSE.md diff --git a/.travis.yml b/.travis.yml index e10ddff..c80566a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,4 @@ rvm: - - 1.9.2 - - 1.9.3 - - 2.0.0 + - 2.1.6 + - 2.3.1 - jruby diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..b45beba --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017 Joshua Lin & Gergely Brautigam + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index cd10fba..0508675 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,10 @@ enum.any?{ |c| c == 'red' } 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. + ### Manipulation If you'd like to do substitution in a json object, you can use `#gsub` or `#gsub!` to modify the object in place. @@ -130,3 +134,7 @@ o = JsonPath.for(json). to_hash # => {"candy" => "big turks"} ~~~~~ + +# Contributions + +Please feel free to submit an Issue or a Pull Request any time you feel like you would like to contribute. Thank you! \ No newline at end of file diff --git a/Rakefile b/Rakefile index 39f6501..b65d511 100644 --- a/Rakefile +++ b/Rakefile @@ -2,11 +2,8 @@ require 'bundler' Bundler::GemHelper.install_tasks task :test do - $: << 'lib' - require 'minitest/autorun' - require 'phocus' - require 'jsonpath' + $LOAD_PATH << 'lib' Dir['./test/**/test_*.rb'].each { |test| require test } end -task :default => :test \ No newline at end of file +task default: :test diff --git a/bin/jsonpath b/bin/jsonpath index 4793a6d..b7093fa 100755 --- a/bin/jsonpath +++ b/bin/jsonpath @@ -15,7 +15,7 @@ usage unless ARGV[0] jsonpath = JsonPath.new(ARGV[0]) case ARGV[1] -when nil #stdin +when nil # stdin puts MultiJson.encode(jsonpath.on(MultiJson.decode(STDIN.read))) when String puts MultiJson.encode(jsonpath.on(MultiJson.decode(File.exist?(ARGV[1]) ? File.read(ARGV[1]) : ARGV[1]))) diff --git a/jsonpath.gemspec b/jsonpath.gemspec index 7ff16b0..30b5a00 100644 --- a/jsonpath.gemspec +++ b/jsonpath.gemspec @@ -5,20 +5,21 @@ require File.join(File.dirname(__FILE__), 'lib', 'jsonpath', 'version') Gem::Specification.new do |s| s.name = 'jsonpath' s.version = JsonPath::VERSION - s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= - s.authors = ["Joshua Hull"] - s.summary = "Ruby implementation of http://goessner.net/articles/JsonPath/" - s.description = "Ruby implementation of http://goessner.net/articles/JsonPath/." - s.email = %q{joshbuddy@gmail.com} + s.required_rubygems_version = + Gem::Requirement.new('>= 0') if s.respond_to? :required_rubygems_version= + s.authors = ['Joshua Hull', 'Gergely Brautigam'] + s.summary = 'Ruby implementation of http://goessner.net/articles/JsonPath/' + s.description = 'Ruby implementation of http://goessner.net/articles/JsonPath/.' + s.email = ['joshbuddy@gmail.com', 'skarlso777@gmail.com'] s.extra_rdoc_files = ['README.md'] s.files = `git ls-files`.split("\n") - s.homepage = %q{http://github.com/joshbuddy/jsonpath} - s.rdoc_options = ["--charset=UTF-8"] - s.require_paths = ["lib"] - s.rubygems_version = %q{1.3.7} - s.test_files = `git ls-files`.split("\n").select{|f| f =~ /^spec/} + s.homepage = 'https://github.com/joshbuddy/jsonpath' + s.rdoc_options = ['--charset=UTF-8'] + s.require_paths = ['lib'] + s.rubygems_version = '1.3.7' + s.test_files = `git ls-files`.split("\n").select { |f| f =~ /^spec/ } s.rubyforge_project = 'jsonpath' - s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } + s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) } s.licenses = ['MIT'] # dependencies diff --git a/lib/jsonpath.rb b/lib/jsonpath.rb index 82bd295..77a95ad 100644 --- a/lib/jsonpath.rb +++ b/lib/jsonpath.rb @@ -4,9 +4,10 @@ require 'jsonpath/enumerable' require 'jsonpath/version' +# JsonPath: initializes the class with a given JsonPath and parses that path +# into a token array. class JsonPath - - PATH_ALL = '$..*' + PATH_ALL = '$..*'.freeze attr_accessor :path @@ -14,39 +15,19 @@ def initialize(path, opts = nil) @opts = opts scanner = StringScanner.new(path) @path = [] - while not scanner.eos? - if token = scanner.scan(/\$/) - @path << token - elsif token = scanner.scan(/@/) + until scanner.eos? + if token = scanner.scan(/\$|@\B|\*|\.\./) @path << token - elsif token = scanner.scan(/[a-zA-Z0-9_-]+/) + elsif token = scanner.scan(/[\$@a-zA-Z0-9:_-]+/) @path << "['#{token}']" elsif token = scanner.scan(/'(.*?)'/) @path << "[#{token}]" elsif token = scanner.scan(/\[/) - count = 1 - while !count.zero? - if t = scanner.scan(/\[/) - token << t - count += 1 - elsif t = scanner.scan(/\]/) - token << t - count -= 1 - elsif t = scanner.scan(/[^\[\]]+/) - token << t - elsif scanner.eos? - raise ArgumentError, 'unclosed bracket' - end - end - @path << token + @path << find_matching_brackets(token, scanner) elsif token = scanner.scan(/\]/) raise ArgumentError, 'unmatched closing bracket' - elsif token = scanner.scan(/\.\./) - @path << token elsif scanner.scan(/\./) nil - elsif token = scanner.scan(/\*/) - @path << token elsif token = scanner.scan(/[><=] \d+/) @path.last << token elsif token = scanner.scan(/./) @@ -55,6 +36,24 @@ def initialize(path, opts = nil) end end + def find_matching_brackets(token, scanner) + count = 1 + until count.zero? + if t = scanner.scan(/\[/) + token << t + count += 1 + elsif t = scanner.scan(/\]/) + token << t + count -= 1 + elsif t = scanner.scan(/[^\[\]]+/) + token << t + elsif scanner.eos? + raise ArgumentError, 'unclosed bracket' + end + end + token + end + def join(join_path) res = deep_clone res.path += JsonPath.new(join_path).path @@ -70,12 +69,13 @@ def first(obj_or_str, *args) end def enum_on(obj_or_str, mode = nil) - JsonPath::Enumerable.new(self, self.class.process_object(obj_or_str), mode, @opts) + JsonPath::Enumerable.new(self, self.class.process_object(obj_or_str), mode, + @opts) end alias_method :[], :enum_on def self.on(obj_or_str, path, opts = nil) - self.new(path, opts).on(process_object(obj_or_str)) + new(path, opts).on(process_object(obj_or_str)) end def self.for(obj_or_str) @@ -83,6 +83,7 @@ def self.for(obj_or_str) end private + def self.process_object(obj_or_str) obj_or_str.is_a?(String) ? MultiJson.decode(obj_or_str) : obj_or_str end diff --git a/lib/jsonpath/enumerable.rb b/lib/jsonpath/enumerable.rb index 373ac66..1a51529 100644 --- a/lib/jsonpath/enumerable.rb +++ b/lib/jsonpath/enumerable.rb @@ -5,8 +5,15 @@ class Enumerable alias_method :allow_eval?, :allow_eval def initialize(path, object, mode, options = nil) - @path, @object, @mode, @options = path.path, object, mode, options - @allow_eval = @options && @options.key?(:allow_eval) ? @options[:allow_eval] : true + @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) @@ -14,74 +21,78 @@ def each(context = @object, key = nil, pos = 0, &blk) @_current_node = node return yield_value(blk, context, key) if pos == @path.size case expr = @path[pos] - when '*', '..' + when '*', '..', '@' each(context, key, pos + 1, &blk) when '$' each(context, key, pos + 1, &blk) if node == @object - when '@' - each(context, key, pos + 1, &blk) when /^\[(.*)\]$/ - expr[1,expr.size - 2].split(',').each do |sub_path| - case sub_path[0] - when ?', ?" - if node.is_a?(Hash) - k = sub_path[1,sub_path.size - 2] - each(node, k, pos + 1, &blk) if node.key?(k) - end - when ?? - raise "Cannot use ?(...) unless eval is enabled" unless allow_eval? - case node - when Hash, Array - (node.is_a?(Hash) ? node.keys : (0..node.size)).each do |e| - @_current_node = node[e] - if process_function_or_literal(sub_path[1, sub_path.size - 1]) - each(@_current_node, nil, pos + 1, &blk) - end - end - else - yield node if process_function_or_literal(sub_path[1, sub_path.size - 1]) - end - else - if node.is_a?(Array) - next if node.empty? - array_args = sub_path.split(':') - if array_args[0] == ?* - start_idx = 0 - end_idx = node.size - 1 - else - start_idx = process_function_or_literal(array_args[0], 0) - next unless start_idx - end_idx = (array_args[1] && process_function_or_literal(array_args[1], -1) || (sub_path.count(':') == 0 ? start_idx : -1)) - next unless end_idx - if start_idx == end_idx - next unless start_idx < node.size - end - end - start_idx %= node.size - end_idx %= node.size - step = process_function_or_literal(array_args[2], 1) - next unless step - (start_idx..end_idx).step(step) {|i| each(node, i, pos + 1, &blk)} - end - end - end + handle_wildecard(node, expr, context, key, pos, &blk) else if pos == (@path.size - 1) && node && allow_eval? - if eval("node #{@path[pos]}") - yield_value(blk, context, key) - end + yield_value(blk, context, key) if instance_eval("node #{@path[pos]}") end end - if pos > 0 && @path[pos-1] == '..' + if pos > 0 && @path[pos - 1] == '..' || (@path[pos - 1] == '*' && @path[pos] != '..') case node - when Hash then node.each {|k, v| each(node, k, pos, &blk) } - when Array then node.each_with_index {|n, i| each(node, i, pos, &blk) } + when Hash then node.each { |k, _| each(node, k, pos, &blk) } + when Array then node.each_with_index { |_, i| each(node, i, pos, &blk) } end end end private + + def handle_wildecard(node, expr, context, key, pos, &blk) + expr[1, expr.size - 2].split(',').each do |sub_path| + case sub_path[0] + when '\'', '"' + next unless node.is_a?(Hash) + k = sub_path[1, sub_path.size - 2] + each(node, k, pos + 1, &blk) if node.key?(k) + when '?' + handle_question_mark(sub_path, node, pos, &blk) + else + next unless node.is_a?(Array) && !node.empty? + array_args = sub_path.split(':') + if array_args[0] == '*' + start_idx = 0 + end_idx = node.size - 1 + else + start_idx = process_function_or_literal(array_args[0], 0) + next unless start_idx + end_idx = (array_args[1] && process_function_or_literal(array_args[1], -1) || (sub_path.count(':') == 0 ? start_idx : -1)) + next unless end_idx + next if start_idx == end_idx && start_idx >= node.size + end + start_idx %= node.size + end_idx %= node.size + step = process_function_or_literal(array_args[2], 1) + next unless step + (start_idx..end_idx).step(step) { |i| each(node, i, pos + 1, &blk) } + end + end + 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] + if process_function_or_literal(sub_path[1, sub_path.size - 1]) + each(@_current_node, nil, pos + 1, &blk) + end + end + when Hash + if process_function_or_literal(sub_path[1, sub_path.size - 1]) + each(@_current_node, nil, pos + 1, &blk) + end + else + yield node if process_function_or_literal(sub_path[1, sub_path.size - 1]) + end + end + def yield_value(blk, context, key) case @mode when nil @@ -100,35 +111,42 @@ def yield_value(blk, context, key) end def process_function_or_literal(exp, default = nil) - if exp.nil? - default - elsif exp[0] == ?( - return nil unless allow_eval? && @_current_node - identifiers = /@?(\.(\w+))+/.match(exp) + return default if exp.nil? || exp.empty? + return Integer(exp) if exp[0] != '(' + return nil unless allow_eval? && @_current_node - if !identifiers.nil? && !@_current_node.methods.include?(identifiers[2].to_sym) - exp_to_eval = exp.dup - exp_to_eval[identifiers[0]] = identifiers[0].split('.').map{|el| el == '@' ? '@_current_node' : "['#{el}']"}.join - begin - return eval(exp_to_eval) - rescue StandardError # if eval failed because of bad arguments or missing methods - return default - end - end + identifiers = /@?(\.(\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}']" + end.join - # otherwise eval as is - # TODO: this eval is wrong, because hash accessor could be nil and nil cannot be compared with anything, - # for instance, @_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 - eval(exp.gsub(/@/, '@_current_node')) - rescue - false + return instance_eval(exp_to_eval) + # if eval failed because of bad arguments or missing methods + rescue StandardError + return default end - elsif exp.empty? - default - else - Integer(exp) + 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 end end diff --git a/lib/jsonpath/proxy.rb b/lib/jsonpath/proxy.rb index 4e8f1b7..9d54703 100644 --- a/lib/jsonpath/proxy.rb +++ b/lib/jsonpath/proxy.rb @@ -1,18 +1,18 @@ class JsonPath class Proxy attr_reader :obj - alias_method :to_hash, :obj + alias to_hash obj def initialize(obj) @obj = obj end def gsub(path, replacement = nil, &replacement_block) - _gsub(_deep_copy, path, replacement ? proc{replacement} : replacement_block) + _gsub(_deep_copy, path, replacement ? proc { replacement } : replacement_block) end def gsub!(path, replacement = nil, &replacement_block) - _gsub(@obj, path, replacement ? proc{replacement} : replacement_block) + _gsub(@obj, path, replacement ? proc { replacement } : replacement_block) end def delete(path = JsonPath::PATH_ALL) @@ -32,8 +32,9 @@ def compact!(path = JsonPath::PATH_ALL) end private + def _deep_copy - Marshal::load(Marshal::dump(@obj)) + Marshal.load(Marshal.dump(@obj)) end def _gsub(obj, path, replacement) @@ -51,4 +52,4 @@ def _compact(obj, path) Proxy.new(obj) end end -end \ No newline at end of file +end diff --git a/lib/jsonpath/version.rb b/lib/jsonpath/version.rb index b7a592a..7ac8f11 100644 --- a/lib/jsonpath/version.rb +++ b/lib/jsonpath/version.rb @@ -1,3 +1,3 @@ class JsonPath - VERSION = '0.5.8' -end \ No newline at end of file + VERSION = '0.7.0'.freeze +end diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index 76c267b..ff530fc 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -1,17 +1,17 @@ -class TestJsonpath < MiniTest::Unit::TestCase +require 'minitest/autorun' +require 'phocus' +require 'jsonpath' +class TestJsonpath < MiniTest::Unit::TestCase def setup @object = example_object @object2 = example_object end def test_bracket_matching - assert_raises(ArgumentError) { - JsonPath.new('$.store.book[0') - } - assert_raises(ArgumentError) { - JsonPath.new('$.store.book[0]]') - } + assert_raises(ArgumentError) { JsonPath.new('$.store.book[0') } + assert_raises(ArgumentError) { JsonPath.new('$.store.book[0]]') } + assert_equal [9], JsonPath.new('$.store.book[0].price').on(@object) end def test_lookup_direct_path @@ -66,12 +66,12 @@ def test_recognize_filters if RUBY_VERSION[/^1\.9/] def test_recognize_filters_on_val - assert_equal [@object['store']['book'][1]['price'], @object['store']['book'][3]['price'], @object['store']['bicycle']['price']], JsonPath.new("$..price[?(@ > 10)]").on(@object) + assert_equal [@object['store']['book'][1]['price'], @object['store']['book'][3]['price'], @object['store']['bicycle']['price']], JsonPath.new('$..price[?(@ > 10)]').on(@object) end end def test_no_eval - assert_equal [], JsonPath.new('$..book[(@.length-2)]', :allow_eval => false).on(@object) + assert_equal [], JsonPath.new('$..book[(@.length-2)]', allow_eval: false).on(@object) end def test_paths_with_underscores @@ -79,7 +79,11 @@ def test_paths_with_underscores end def test_path_with_hyphens - assert_equal [@object['store']['bicycle']['single-speed']], JsonPath.new('$.store.bicycle.single-speed').on(@object) + assert_equal [@object['store']['bicycle']['single-speed']], JsonPath.new('$.store.bicycle.single-speed').on(@object) + end + + def test_path_with_colon + assert_equal [@object['store']['bicycle']['make:model']], JsonPath.new('$.store.bicycle.make:model').on(@object) end def test_paths_with_numbers @@ -95,11 +99,11 @@ def test_use_first end def test_counting - assert_equal 49, JsonPath.new('$..*').on(@object).to_a.size + assert_equal 50, JsonPath.new('$..*').on(@object).to_a.size end def test_space_in_path - assert_equal ['e'], JsonPath.new("$.'c d'").on({"a" => "a","b" => "b", "c d" => "e"}) + assert_equal ['e'], JsonPath.new("$.'c d'").on('a' => 'a', 'b' => 'b', 'c d' => 'e') end def test_class_method @@ -129,103 +133,152 @@ def test_gsub! end def test_weird_gsub! - h = {'hi' => 'there'} - JsonPath.for(@object).gsub!('$.*') { |n| h } + h = { 'hi' => 'there' } + JsonPath.for(@object).gsub!('$.*') { |_| h } assert_equal h, @object end + def test_gsub_to_false! + h = { 'hi' => 'there' } + h2 = { 'hi' => false } + assert_equal h2, JsonPath.for(h).gsub!('$.hi') { |_| false }.to_hash + end + + def test_where_selector + JsonPath.for(@object).gsub!('$..book.price[?(@ > 20)]') { |p| p + 10 } + end + def test_compact - h = {'hi' => 'there', 'you' => nil} + h = { 'hi' => 'there', 'you' => nil } JsonPath.for(h).compact! - assert_equal({'hi' => 'there'}, h) + assert_equal({ 'hi' => 'there' }, h) end def test_delete - h = {'hi' => 'there', 'you' => nil} + h = { 'hi' => 'there', 'you' => nil } JsonPath.for(h).delete!('*.hi') - assert_equal({'you' => nil}, h) + assert_equal({ 'you' => nil }, h) + end + + def test_at_sign_in_json_element + data = + { '@colors' => + [{ '@r' => 255, '@g' => 0, '@b' => 0 }, + { '@r' => 0, '@g' => 255, '@b' => 0 }, + { '@r' => 0, '@g' => 0, '@b' => 255 }] } + + assert_equal [255, 0, 0], JsonPath.on(data, '$..@r') end def test_wildcard - assert_equal @object['store']['book'].collect{|e| e['price']}.compact, JsonPath.on(@object, '$..book[*].price') + assert_equal @object['store']['book'].collect { |e| e['price'] }.compact, JsonPath.on(@object, '$..book[*].price') + end + + def test_wildcard_on_intermediary_element + assert_equal [1], JsonPath.on({ 'a' => { 'b' => { 'c' => 1 } } }, '$.a..c') + end + + def test_wildcard_on_intermediary_element_v2 + assert_equal [1], JsonPath.on({ 'a' => { 'b' => { 'd' => { 'c' => 1 } } } }, '$.a..c') + end + + def test_wildcard_on_intermediary_element_v3 + assert_equal [1], JsonPath.on({ 'a' => { 'b' => { 'd' => { 'c' => 1 } } } }, '$.a.*..c') + end + + def test_wildcard_on_intermediary_element_v4 + assert_equal [1], JsonPath.on({ 'a' => { 'b' => { 'd' => { 'c' => 1 } } } }, '$.a.*..c') + end + + def test_wildcard_on_intermediary_element_v5 + assert_equal [1], JsonPath.on({ 'a' => { 'b' => { 'c' => 1 } } }, '$.a.*.c') + end + + def test_wildcard_on_intermediary_element_v6 + assert_equal ['red'], JsonPath.new('$.store.*.color').on(@object) end def test_wildcard_empty_array - object = @object.merge("bicycle" => { "tire" => [] }) - assert_equal [], JsonPath.on(object, "$..bicycle.tire[*]") + object = @object.merge('bicycle' => { 'tire' => [] }) + assert_equal [], JsonPath.on(object, '$..bicycle.tire[*]') end - def test_support_filter_by_childnode_value - assert_equal [@object['store']['book'][3]], JsonPath.new("$..book[?(@.price > 20)]").on(@object) + def test_support_filter_by_array_childnode_value + assert_equal [@object['store']['book'][3]], JsonPath.new('$..book[?(@.price > 20)]').on(@object) end def test_support_filter_by_childnode_value_with_inconsistent_children - @object['store']['book'][0] = "string_instead_of_object" - assert_equal [@object['store']['book'][3]], JsonPath.new("$..book[?(@.price > 20)]").on(@object) + @object['store']['book'][0] = 'string_instead_of_object' + assert_equal [@object['store']['book'][3]], JsonPath.new('$..book[?(@.price > 20)]').on(@object) end def test_support_filter_by_childnode_value_and_select_child_key - assert_equal [23], JsonPath.new("$..book[?(@.price > 20)].price").on(@object) + assert_equal [23], JsonPath.new('$..book[?(@.price > 20)].price').on(@object) end def test_support_filter_by_childnode_value_over_childnode_and_select_child_key - assert_equal ["Osennie Vizity"], JsonPath.new("$..book[?(@.written.year == 1996)].title").on(@object) + assert_equal ['Osennie Vizity'], JsonPath.new('$..book[?(@.written.year == 1996)].title').on(@object) end - + + def test_support_filter_by_object_childnode_value + data = { + 'data' => { + 'type' => 'users', + 'id' => '123' + } + } + assert_equal [{ 'type' => 'users', 'id' => '123' }], JsonPath.new("$.data[?(@.type == 'users')]").on(data) + assert_equal [], JsonPath.new("$.data[?(@.type == 'admins')]").on(data) + end + def example_object - { "store"=> { - "book" => [ - { "category"=> "reference", - "author"=> "Nigel Rees", - "title"=> "Sayings of the Century", - "price"=> 9 - }, - { "category"=> "fiction", - "author"=> "Evelyn Waugh", - "title"=> "Sword of Honour", - "price"=> 13 - }, - { "category"=> "fiction", - "author"=> "Herman Melville", - "title"=> "Moby Dick", - "isbn"=> "0-553-21311-3", - "price"=> 9 - }, - { "category"=> "fiction", - "author"=> "J. R. R. Tolkien", - "title"=> "The Lord of the Rings", - "isbn"=> "0-395-19395-8", - "price"=> 23 - }, - { "category"=> "russian_fiction", - "author"=> "Lukyanenko", - "title"=> "Imperatory Illuziy", - "written" => { - "year" => 1995 - } - }, - { "category"=> "russian_fiction", - "author"=> "Lukyanenko", - "title"=> "Osennie Vizity", - "written" => { - "year" => 1996 - } - }, - { "category"=> "russian_fiction", - "author"=> "Lukyanenko", - "title"=> "Ne vremya dlya drakonov", - "written" => { - "year" => 1997 - } - } + { 'store' => { + 'book' => [ + { 'category' => 'reference', + 'author' => 'Nigel Rees', + 'title' => 'Sayings of the Century', + 'price' => 9 }, + { 'category' => 'fiction', + 'author' => 'Evelyn Waugh', + 'title' => 'Sword of Honour', + 'price' => 13 }, + { 'category' => 'fiction', + 'author' => 'Herman Melville', + 'title' => 'Moby Dick', + 'isbn' => '0-553-21311-3', + 'price' => 9 }, + { 'category' => 'fiction', + 'author' => 'J. R. R. Tolkien', + 'title' => 'The Lord of the Rings', + 'isbn' => '0-395-19395-8', + 'price' => 23 }, + { 'category' => 'russian_fiction', + 'author' => 'Lukyanenko', + 'title' => 'Imperatory Illuziy', + 'written' => { + 'year' => 1995 + } }, + { 'category' => 'russian_fiction', + 'author' => 'Lukyanenko', + 'title' => 'Osennie Vizity', + 'written' => { + 'year' => 1996 + } }, + { 'category' => 'russian_fiction', + 'author' => 'Lukyanenko', + 'title' => 'Ne vremya dlya drakonov', + 'written' => { + 'year' => 1997 + } } ], - "bicycle"=> { - "color"=> "red", - "price"=> 20, - "catalogue_number" => 12345, - "single-speed" => "no", - "2seater" => "yes"} + 'bicycle' => { + 'color' => 'red', + 'price' => 20, + 'catalogue_number' => 123_45, + 'single-speed' => 'no', + '2seater' => 'yes', + 'make:model' => 'Zippy Sweetwheeler' + } } } end - end diff --git a/test/test_jsonpath_bin.rb b/test/test_jsonpath_bin.rb index 626a119..68285c2 100644 --- a/test/test_jsonpath_bin.rb +++ b/test/test_jsonpath_bin.rb @@ -1,6 +1,10 @@ +require 'minitest/autorun' +require 'phocus' +require 'jsonpath' + class TestJsonpathBin < MiniTest::Unit::TestCase def setup - @runner = "ruby -Ilib bin/jsonpath" + @runner = 'ruby -Ilib bin/jsonpath' @original_dir = Dir.pwd Dir.chdir(File.join(File.dirname(__FILE__), '..')) end @@ -11,11 +15,7 @@ def teardown end def test_stdin - assert_equal '["time"]', `echo '{"test": "time"}' | #{@runner} '$.test'`.strip - end - - def test_stdin - File.open('/tmp/test.json', 'w'){|f| f << '{"test": "time"}'} + File.open('/tmp/test.json', 'w') { |f| f << '{"test": "time"}' } assert_equal '["time"]', `#{@runner} '$.test' /tmp/test.json`.strip end -end \ No newline at end of file +end