From 2d44300bf8ddff66af9b3ba3239fc4f0851acc54 Mon Sep 17 00:00:00 2001 From: yangdong Date: Thu, 5 Dec 2013 15:30:41 +0800 Subject: [PATCH 001/120] Update env path, since /usr/local/env not always availalbe --- bin/jsonpath | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/jsonpath b/bin/jsonpath index eef811c..4793a6d 100755 --- a/bin/jsonpath +++ b/bin/jsonpath @@ -1,4 +1,4 @@ -#!/usr/local/env ruby +#!/usr/bin/env ruby require 'jsonpath' require 'multi_json' From 8ad7c746933bc626c91fefc729afe6f4b589ff69 Mon Sep 17 00:00:00 2001 From: Benjamin Vetter Date: Sat, 14 Dec 2013 22:26:01 +0100 Subject: [PATCH 002/120] Path join feature added --- lib/jsonpath.rb | 12 +++++++++++- test/test_jsonpath.rb | 4 ++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/jsonpath.rb b/lib/jsonpath.rb index 2465800..a9d676e 100644 --- a/lib/jsonpath.rb +++ b/lib/jsonpath.rb @@ -8,7 +8,7 @@ class JsonPath PATH_ALL = '$..*' - attr_reader :path + attr_accessor :path def initialize(path, opts = nil) @opts = opts @@ -52,6 +52,12 @@ def initialize(path, opts = nil) end end + def join(join_path) + res = deep_clone + res.path += JsonPath.new(join_path).path + res + end + def on(obj_or_str) enum_on(obj_or_str).to_a end @@ -77,4 +83,8 @@ def self.for(obj_or_str) def self.process_object(obj_or_str) obj_or_str.is_a?(String) ? MultiJson.decode(obj_or_str) : obj_or_str end + + def deep_clone + Marshal.load Marshal.dump(self) + end end diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index 40667b7..d0eb839 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -94,6 +94,10 @@ def test_class_method assert_equal JsonPath.new('$..author').on(@object), JsonPath.on(@object, '$..author') end + def test_join + assert_equal JsonPath.new('$.store.book..author').on(@object), JsonPath.new('$.store').join('book..author').on(@object) + end + def test_gsub @object2['store']['bicycle']['price'] += 10 @object2['store']['book'][0]['price'] += 10 From 06e855ca895428389dac3044296983db2ccb23bd Mon Sep 17 00:00:00 2001 From: Akinori MUSHA Date: Thu, 2 Jul 2015 18:19:44 +0900 Subject: [PATCH 003/120] Detect unmatched brackets This fixes a bug where an unclosed bracket led to infinite loop. --- lib/jsonpath.rb | 7 +++++-- test/test_jsonpath.rb | 9 +++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/lib/jsonpath.rb b/lib/jsonpath.rb index 2465800..b72c09f 100644 --- a/lib/jsonpath.rb +++ b/lib/jsonpath.rb @@ -14,7 +14,6 @@ def initialize(path, opts = nil) @opts = opts scanner = StringScanner.new(path) @path = [] - bracket_count = 0 while not scanner.eos? if token = scanner.scan(/\$/) @path << token @@ -33,11 +32,15 @@ def initialize(path, opts = nil) elsif t = scanner.scan(/\]/) token << t count -= 1 - elsif t = scanner.scan(/[^\[\]]*/) + elsif t = scanner.scan(/[^\[\]]+/) token << t + elsif scanner.eos? + raise ArgumentError, 'unclosed bracket' end end @path << token + elsif token = scanner.scan(/\]/) + raise ArgumentError, 'unmatched closing bracket' elsif token = scanner.scan(/\.\./) @path << token elsif scanner.scan(/\./) diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index 40667b7..1e7a7c0 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -5,6 +5,15 @@ def setup @object2 = example_object end + def test_bracket_matching + assert_raises(ArgumentError) { + JsonPath.new('$.store.book[0') + } + assert_raises(ArgumentError) { + JsonPath.new('$.store.book[0]]') + } + end + def test_lookup_direct_path assert_equal 4, JsonPath.new('$.store.*').on(@object).first['book'].size end From 792ff9a928998f4252692cd3c1ba378ed931a5aa Mon Sep 17 00:00:00 2001 From: Joshua Hull Date: Thu, 9 Jul 2015 17:18:28 -0700 Subject: [PATCH 004/120] Bump version --- lib/jsonpath/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonpath/version.rb b/lib/jsonpath/version.rb index 97c8bda..b3341ac 100644 --- a/lib/jsonpath/version.rb +++ b/lib/jsonpath/version.rb @@ -1,3 +1,3 @@ class JsonPath - VERSION = '0.5.6' + VERSION = '0.5.7' end \ No newline at end of file From e5f91f7ffe0dfb68e30c08fec3995622f827d582 Mon Sep 17 00:00:00 2001 From: "Alexander.Iljushkin" Date: Tue, 29 Sep 2015 11:30:18 +0300 Subject: [PATCH 005/120] Added possibility to watch over the current node in sub query. For ex.: $.node[?(@.child.child == 'value')] --- lib/jsonpath/enumerable.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/jsonpath/enumerable.rb b/lib/jsonpath/enumerable.rb index 89b3702..4e640db 100644 --- a/lib/jsonpath/enumerable.rb +++ b/lib/jsonpath/enumerable.rb @@ -104,11 +104,11 @@ def process_function_or_literal(exp, default = nil) default elsif exp[0] == ?( return nil unless allow_eval? && @_current_node - match_result = /@\.(\p{Word}+)/.match(exp) || [] - identifier = match_result[1] - # if there's no such method - convert into hash subscript - if !identifier.nil? && !@_current_node.methods.include?(identifier.to_sym) - exp_to_eval = exp.gsub(/@/, '@_current_node').gsub(/@_current_node.#{identifier}/,"@_current_node['#{identifier}']") + identifiers = /@?(\.\w+)+/.match(exp) || [] + + if !identifiers.nil? + 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 From 216656c4c7d19151bf2fdea26a45ee02b8acca96 Mon Sep 17 00:00:00 2001 From: "Alexander.Iljushkin" Date: Tue, 29 Sep 2015 11:38:03 +0300 Subject: [PATCH 006/120] getting empty array if no matches --- lib/jsonpath/enumerable.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonpath/enumerable.rb b/lib/jsonpath/enumerable.rb index 4e640db..353c738 100644 --- a/lib/jsonpath/enumerable.rb +++ b/lib/jsonpath/enumerable.rb @@ -106,7 +106,7 @@ def process_function_or_literal(exp, default = nil) return nil unless allow_eval? && @_current_node identifiers = /@?(\.\w+)+/.match(exp) || [] - if !identifiers.nil? + if !identifiers.empty? exp_to_eval = exp.dup exp_to_eval[identifiers[0]] = identifiers[0].split('.').map{|el| el == '@' ? '@_current_node' : "['#{el}']"}.join begin From 6458af8bf14ed4df0a387334d7e03116b54e8279 Mon Sep 17 00:00:00 2001 From: "Alexander.Iljushkin" Date: Tue, 29 Sep 2015 11:49:54 +0300 Subject: [PATCH 007/120] Replacing match empty data with empty array is usless, removed replacement to [] and changed if statement --- lib/jsonpath/enumerable.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/jsonpath/enumerable.rb b/lib/jsonpath/enumerable.rb index 353c738..290bc6f 100644 --- a/lib/jsonpath/enumerable.rb +++ b/lib/jsonpath/enumerable.rb @@ -104,9 +104,9 @@ def process_function_or_literal(exp, default = nil) default elsif exp[0] == ?( return nil unless allow_eval? && @_current_node - identifiers = /@?(\.\w+)+/.match(exp) || [] + identifiers = /@?(\.\w+)+/.match(exp) - if !identifiers.empty? + if !identifiers.nil? exp_to_eval = exp.dup exp_to_eval[identifiers[0]] = identifiers[0].split('.').map{|el| el == '@' ? '@_current_node' : "['#{el}']"}.join begin From 3999b579ea16895cc548002b5adc144dcde071df Mon Sep 17 00:00:00 2001 From: "Alexander.Iljushkin" Date: Tue, 29 Sep 2015 13:35:07 +0300 Subject: [PATCH 008/120] Lost the symbol existance check for method evals, it's back again --- lib/jsonpath/enumerable.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/jsonpath/enumerable.rb b/lib/jsonpath/enumerable.rb index 290bc6f..ae39ee9 100644 --- a/lib/jsonpath/enumerable.rb +++ b/lib/jsonpath/enumerable.rb @@ -104,9 +104,9 @@ def process_function_or_literal(exp, default = nil) default elsif exp[0] == ?( return nil unless allow_eval? && @_current_node - identifiers = /@?(\.\w+)+/.match(exp) + identifiers = /@?(\.(\w+))+/.match(exp) - if !identifiers.nil? + 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 From 721528d2ed9c9bb2b3df872c023cca7225214ba7 Mon Sep 17 00:00:00 2001 From: "Alexander.Iljushkin" Date: Thu, 29 Oct 2015 18:45:25 +0300 Subject: [PATCH 009/120] written test for the deep search filter --- test/test_jsonpath.rb | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index 2463829..17345cc 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -165,6 +165,10 @@ def test_support_filter_by_childnode_value_and_select_child_key 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) + end + def example_object { "store"=> { "book" => [ @@ -189,6 +193,27 @@ def example_object "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"=> { From de76dfbfad8f4eab5965e22340262437118a9fb1 Mon Sep 17 00:00:00 2001 From: "Alexander.Iljushkin" Date: Fri, 30 Oct 2015 14:25:37 +0300 Subject: [PATCH 010/120] Fixed bug when filtering using hash accessor like $..[?(@['price'] < 10)]: added todo statement to think more about this kind of evals, because hash accessor can return nil and nil cannot be compared with binary operators <> also fixed tests --- lib/jsonpath/enumerable.rb | 10 +++++++++- test/test_jsonpath.rb | 25 ++++++++++++++----------- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/lib/jsonpath/enumerable.rb b/lib/jsonpath/enumerable.rb index ae39ee9..373ac66 100644 --- a/lib/jsonpath/enumerable.rb +++ b/lib/jsonpath/enumerable.rb @@ -115,8 +115,16 @@ def process_function_or_literal(exp, default = nil) return default end end + # otherwise eval as is - eval(exp.gsub(/@/, '@_current_node')) + # 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 + end elsif exp.empty? default else diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index 17345cc..76c267b 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -15,7 +15,7 @@ def test_bracket_matching end def test_lookup_direct_path - assert_equal 4, JsonPath.new('$.store.*').on(@object).first['book'].size + assert_equal 7, JsonPath.new('$.store.*').on(@object).first['book'].size end def test_lookup_missing_element @@ -27,7 +27,10 @@ def test_retrieve_all_authors @object['store']['book'][0]['author'], @object['store']['book'][1]['author'], @object['store']['book'][2]['author'], - @object['store']['book'][3]['author'] + @object['store']['book'][3]['author'], + @object['store']['book'][4]['author'], + @object['store']['book'][5]['author'], + @object['store']['book'][6]['author'] ], JsonPath.new('$..author').on(@object) end @@ -43,15 +46,15 @@ def test_retrieve_all_prices def test_recognize_array_splices assert_equal [@object['store']['book'][0], @object['store']['book'][1]], JsonPath.new('$..book[0:1:1]').on(@object) - assert_equal [@object['store']['book'][1], @object['store']['book'][3]], JsonPath.new('$..book[1::2]').on(@object) - assert_equal [@object['store']['book'][0], @object['store']['book'][2]], JsonPath.new('$..book[::2]').on(@object) - assert_equal [@object['store']['book'][0], @object['store']['book'][2]], JsonPath.new('$..book[:-2:2]').on(@object) - assert_equal [@object['store']['book'][2], @object['store']['book'][3]], JsonPath.new('$..book[2::]').on(@object) + assert_equal [@object['store']['book'][1], @object['store']['book'][3], @object['store']['book'][5]], JsonPath.new('$..book[1::2]').on(@object) + assert_equal [@object['store']['book'][0], @object['store']['book'][2], @object['store']['book'][4], @object['store']['book'][6]], JsonPath.new('$..book[::2]').on(@object) + assert_equal [@object['store']['book'][0], @object['store']['book'][2]], JsonPath.new('$..book[:-5:2]').on(@object) + assert_equal [@object['store']['book'][5], @object['store']['book'][6]], JsonPath.new('$..book[5::]').on(@object) end def test_recognize_array_comma assert_equal [@object['store']['book'][0], @object['store']['book'][1]], JsonPath.new('$..book[0,1]').on(@object) - assert_equal [@object['store']['book'][2], @object['store']['book'][3]], JsonPath.new('$..book[2,-1::]').on(@object) + assert_equal [@object['store']['book'][2], @object['store']['book'][6]], JsonPath.new('$..book[2,-1::]').on(@object) end def test_recognize_filters @@ -84,15 +87,15 @@ def test_paths_with_numbers end def test_recognize_array_with_evald_index - assert_equal [@object['store']['book'][2]], JsonPath.new('$..book[(@.length-2)]').on(@object) + assert_equal [@object['store']['book'][2]], JsonPath.new('$..book[(@.length-5)]').on(@object) end def test_use_first - assert_equal @object['store']['book'][2], JsonPath.new('$..book[(@.length-2)]').first(@object) + assert_equal @object['store']['book'][2], JsonPath.new('$..book[(@.length-5)]').first(@object) end def test_counting - assert_equal 31, JsonPath.new('$..*').on(@object).to_a.size + assert_equal 49, JsonPath.new('$..*').on(@object).to_a.size end def test_space_in_path @@ -144,7 +147,7 @@ def test_delete end def test_wildcard - assert_equal @object['store']['book'].collect{|e| e['price']}, JsonPath.on(@object, '$..book[*].price') + assert_equal @object['store']['book'].collect{|e| e['price']}.compact, JsonPath.on(@object, '$..book[*].price') end def test_wildcard_empty_array From 248826c5d71d2eaa3f135cd1f7479aefccb321a4 Mon Sep 17 00:00:00 2001 From: Joshua Hull Date: Mon, 9 Nov 2015 13:15:15 -0800 Subject: [PATCH 011/120] Bump version --- lib/jsonpath/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonpath/version.rb b/lib/jsonpath/version.rb index b3341ac..b7a592a 100644 --- a/lib/jsonpath/version.rb +++ b/lib/jsonpath/version.rb @@ -1,3 +1,3 @@ class JsonPath - VERSION = '0.5.7' + VERSION = '0.5.8' end \ No newline at end of file From e4474e010a61c8711f3d315d0420fd68bf4a4741 Mon Sep 17 00:00:00 2001 From: Joshua Hull Date: Wed, 9 Dec 2015 14:53:42 -0800 Subject: [PATCH 012/120] Example should be < 10, not < 20 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0e43a6b..cd10fba 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ JsonPath.new('$..book[::2]').on(json) ...and evals. ~~~~~ {ruby} -JsonPath.new('$..price[?(@ < 20)]').on(json) +JsonPath.new('$..price[?(@ < 10)]').on(json) # => [8.95, 8.99] ~~~~~ From a60526951b41934f5a6537b655db3fc2bdf3f686 Mon Sep 17 00:00:00 2001 From: Gergely Brautigam Date: Tue, 2 May 2017 08:14:25 +0200 Subject: [PATCH 013/120] 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 From 2299e9f20eaf89aa7ec2961f8b6fc40b2f6826b0 Mon Sep 17 00:00:00 2001 From: Karol Kozakowski Date: Tue, 2 May 2017 10:30:40 +0200 Subject: [PATCH 014/120] add simplecov test coverage --- Gemfile | 3 ++- Rakefile | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 34efd14..164278d 100644 --- a/Gemfile +++ b/Gemfile @@ -1,2 +1,3 @@ source "http://rubygems.org" -gemspec \ No newline at end of file +gemspec +gem 'simplecov', :require => false, :group => :test diff --git a/Rakefile b/Rakefile index b65d511..0afd6a4 100644 --- a/Rakefile +++ b/Rakefile @@ -1,3 +1,8 @@ +require 'simplecov' +SimpleCov.start do + add_filter '/test/' +end + require 'bundler' Bundler::GemHelper.install_tasks From b159dbfcf99b880fe0a9e3826f22e36cffff6240 Mon Sep 17 00:00:00 2001 From: Gergely Brautigam Date: Tue, 2 May 2017 13:46:49 +0200 Subject: [PATCH 015/120] Just a regular Readme Update. --- README.md | 52 ++++++++++++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 0508675..8202559 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ There is stand-alone usage through the binary `jsonpath` To use JsonPath as a library simply include and get goin'! -~~~~~ {ruby} +```ruby require 'jsonpath' json = <<-HERE_DOC @@ -38,74 +38,74 @@ json = <<-HERE_DOC } } HERE_DOC -~~~~~ +``` Now that we have a JSON object, let's get all the prices present in the object. We create an object for the path in the following way. -~~~~~ {ruby} +```ruby path = JsonPath.new('$..price') -~~~~~ +``` Now that we have a path, let's apply it to the object above. -~~~~~ {ruby} +```ruby path.on(json) # => [19.95, 8.95, 12.99, 8.99, 22.99] -~~~~~ +``` Or on some other object ... -~~~~~ {ruby} +```ruby path.on('{"books":[{"title":"A Tale of Two Somethings","price":18.88}]}') # => [18.88] -~~~~~ +``` You can also just combine this into one mega-call with the convenient `JsonPath.on` method. -~~~~~ {ruby} +```ruby JsonPath.on(json, '$..author') # => ["Nigel Rees", "Evelyn Waugh", "Herman Melville", "Tolkien"] -~~~~~ +``` Of course the full JsonPath syntax is supported, such as array slices -~~~~~ {ruby} +```ruby JsonPath.new('$..book[::2]').on(json) # => [ # {"price"=>8.95, "category"=>"reference", "author"=>"Nigel Rees", "title"=>"Sayings of the Century"}, # {"price"=>8.99, "category"=>"fiction", "author"=>"Herman Melville", "title"=>"Moby Dick", "isbn"=>"0-553-21311-3"} # ] -~~~~~ +``` ...and evals. -~~~~~ {ruby} +```ruby JsonPath.new('$..price[?(@ < 10)]').on(json) # => [8.95, 8.99] -~~~~~ +``` There is a convenience method, `#first` that gives you the first element for a JSON object and path. -~~~~~ {ruby} +```ruby JsonPath.new('$..color').first(object) # => "red" -~~~~~ +``` As well, we can directly create an `Enumerable` at any time using `#[]`. -~~~~~ {ruby} +```ruby enum = JsonPath.new('$..color')[object] # => # enum.first # => "red" 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 +### More examples For more usage examples and variations on paths, please visit the tests. There are some more complex ones as well. @@ -113,19 +113,19 @@ For more usage examples and variations on paths, please visit the tests. There a If you'd like to do substitution in a json object, you can use `#gsub` or `#gsub!` to modify the object in place. -~~~~~ {ruby} +```ruby JsonPath.for('{"candy":"lollipop"}').gsub('$..candy') {|v| "big turks" }.to_hash -~~~~~ +``` The result will be -~~~~~ {ruby} +```ruby {'candy' => 'big turks'} -~~~~~ +``` If you'd like to remove all nil keys, you can use `#compact` and `#compact!`. To remove all keys under a certain path, use `#delete` or `#delete!`. You can even chain these methods together as follows: -~~~~~ {ruby} +```ruby json = '{"candy":"lollipop","noncandy":null,"other":"things"}' o = JsonPath.for(json). gsub('$..candy') {|v| "big turks" }. @@ -133,8 +133,8 @@ o = JsonPath.for(json). delete('$..other'). 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 +Please feel free to submit an Issue or a Pull Request any time you feel like you would like to contribute. Thank you! From 100f3b4ab967869f4b088e0e67ed96fde4d6e17e Mon Sep 17 00:00:00 2001 From: Peter Williams Date: Wed, 20 Nov 2013 09:39:59 -0700 Subject: [PATCH 016/120] Support '@' and '$' as part of member names Allows using paths on documents that use sigils to namespace meta data or common fields such as JSON-LD --- lib/jsonpath.rb | 2 +- test/test_jsonpath.rb | 22 ++++++++++++++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/lib/jsonpath.rb b/lib/jsonpath.rb index 77a95ad..9166b63 100644 --- a/lib/jsonpath.rb +++ b/lib/jsonpath.rb @@ -16,7 +16,7 @@ def initialize(path, opts = nil) scanner = StringScanner.new(path) @path = [] until scanner.eos? - if token = scanner.scan(/\$|@\B|\*|\.\./) + if token = scanner.scan(/\$\B|@\B|\*|\.\./) @path << token elsif token = scanner.scan(/[\$@a-zA-Z0-9:_-]+/) @path << "['#{token}']" diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index ff530fc..5418fff 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -99,7 +99,7 @@ def test_use_first end def test_counting - assert_equal 50, JsonPath.new('$..*').on(@object).to_a.size + assert_equal 54, JsonPath.new('$..*').on(@object).to_a.size end def test_space_in_path @@ -231,6 +231,21 @@ def test_support_filter_by_object_childnode_value assert_equal [], JsonPath.new("$.data[?(@.type == 'admins')]").on(data) end + def test_support_at_sign_in_member_names + assert_equal [@object['store']['@id']], JsonPath.new("$.store.@id").on(@object) + end + + def test_support_dollar_sign_in_member_names + assert_equal [@object['store']['$meta-data']], + JsonPath.new("$.store.$meta-data").on(@object) + end + + def test_support_underscore_in_member_names + assert_equal [@object['store']['_links']], + JsonPath.new("$.store._links").on(@object) + end + + def example_object { 'store' => { 'book' => [ @@ -278,7 +293,10 @@ def example_object 'single-speed' => 'no', '2seater' => 'yes', 'make:model' => 'Zippy Sweetwheeler' - } + }, + "@id" => "http://example.org/store/42", + "$meta-data" => "whatevs", + "_links" => { "self" => {} } } } end end From 839d9438a5ee57d448a93e2c2db6fae93fb821eb Mon Sep 17 00:00:00 2001 From: Gergely Brautigam Date: Sun, 7 May 2017 08:25:58 +0200 Subject: [PATCH 017/120] Closes #49 --- README.md | 22 ++++++++++++++++++++++ lib/jsonpath/version.rb | 2 +- test/test_jsonpath.rb | 14 ++++++++++---- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8202559..c052ad2 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,28 @@ You can optionally prevent eval from being called on sub-expressions by passing For more usage examples and variations on paths, please visit the tests. There are some more complex ones as well. +### Conditional Operators Are Also Supported + +```ruby + def test_or_operator + assert_equal [@object['store']['book'][1], @object['store']['book'][3]], JsonPath.new("$..book[?(@['price'] == 13 || @['price'] == 23)]").on(@object) + end + + def test_and_operator + assert_equal [], JsonPath.new("$..book[?(@['price'] == 13 && @['price'] == 23)]").on(@object) + end + + def test_and_operator_with_more_results + assert_equal [@object['store']['book'][1]], JsonPath.new("$..book[?(@['price'] < 23 && @['price'] > 9)]").on(@object) + end +``` + +### Running an individual test + +```ruby +ruby -Ilib:../lib test/test_jsonpath.rb --name test_wildcard_on_intermediary_element_v6 +``` + ### Manipulation If you'd like to do substitution in a json object, you can use `#gsub` or `#gsub!` to modify the object in place. diff --git a/lib/jsonpath/version.rb b/lib/jsonpath/version.rb index 7ac8f11..1c18452 100644 --- a/lib/jsonpath/version.rb +++ b/lib/jsonpath/version.rb @@ -1,3 +1,3 @@ class JsonPath - VERSION = '0.7.0'.freeze + VERSION = '0.7.1'.freeze end diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index 5418fff..d7ff755 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -64,10 +64,16 @@ def test_recognize_filters assert_equal [@object['store']['book'][3]], JsonPath.new("$..book[?(@['price'] > 20)]").on(@object) end - 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) - end + def test_or_operator + assert_equal [@object['store']['book'][1], @object['store']['book'][3]], JsonPath.new("$..book[?(@['price'] == 13 || @['price'] == 23)]").on(@object) + end + + def test_and_operator + assert_equal [], JsonPath.new("$..book[?(@['price'] == 13 && @['price'] == 23)]").on(@object) + end + + def test_and_operator_with_more_results + assert_equal [@object['store']['book'][1]], JsonPath.new("$..book[?(@['price'] < 23 && @['price'] > 9)]").on(@object) end def test_no_eval From 44e1d8067a2b487f1a45d454ef3be926c24a78b1 Mon Sep 17 00:00:00 2001 From: Gergely Brautigam Date: Tue, 9 May 2017 16:19:11 +0200 Subject: [PATCH 018/120] Fix for #52 --- lib/jsonpath/enumerable.rb | 3 +-- lib/jsonpath/version.rb | 2 +- test/test_jsonpath.rb | 4 ++++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/jsonpath/enumerable.rb b/lib/jsonpath/enumerable.rb index 1a51529..7a1f490 100644 --- a/lib/jsonpath/enumerable.rb +++ b/lib/jsonpath/enumerable.rb @@ -115,7 +115,7 @@ def process_function_or_literal(exp, default = nil) return Integer(exp) if exp[0] != '(' return nil unless allow_eval? && @_current_node - identifiers = /@?(\.(\w+))+/.match(exp) + identifiers = /@?((? 9)]").on(@object) end + 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) end From a266c08c0d45f4b3a382abafb498052b906e8350 Mon Sep 17 00:00:00 2001 From: Gergely Brautigam Date: Fri, 26 May 2017 06:33:23 +0200 Subject: [PATCH 019/120] Eliminating the use of eval step by step (#55) * Working on replacing eval with send. * Removed the need for eval. * Added one more operation. Now on to removing the identifiers. * Working on it * Completely eliminated the use of Eval. * Removed begin rescue. * Added dig * Removed eval. * Added support for && and || operations. --- README.md | 2 -- lib/jsonpath.rb | 1 + lib/jsonpath/enumerable.rb | 40 ++++-------------------- lib/jsonpath/parser.rb | 62 ++++++++++++++++++++++++++++++++++++++ lib/jsonpath/version.rb | 2 +- test/test_jsonpath.rb | 13 +++++--- 6 files changed, 79 insertions(+), 41 deletions(-) create mode 100644 lib/jsonpath/parser.rb diff --git a/README.md b/README.md index c052ad2..b4729b0 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/lib/jsonpath.rb b/lib/jsonpath.rb index 9166b63..d25c2e8 100644 --- a/lib/jsonpath.rb +++ b/lib/jsonpath.rb @@ -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. diff --git a/lib/jsonpath/enumerable.rb b/lib/jsonpath/enumerable.rb index 7a1f490..7a1bd80 100644 --- a/lib/jsonpath/enumerable.rb +++ b/lib/jsonpath/enumerable.rb @@ -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) @@ -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 - 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] != '..') @@ -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 @@ -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 = /@?((?=][<>=]?\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 diff --git a/lib/jsonpath/version.rb b/lib/jsonpath/version.rb index cac7361..848d18e 100644 --- a/lib/jsonpath/version.rb +++ b/lib/jsonpath/version.rb @@ -1,3 +1,3 @@ class JsonPath - VERSION = '0.7.2'.freeze + VERSION = '0.8.2'.freeze end diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index 7f9ad0c..09e717a 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -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 @@ -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 @@ -255,6 +255,10 @@ 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' => { @@ -262,7 +266,8 @@ def example_object { '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', From 021c1bc50826cf5bfdbd2dcd07c8887155414861 Mon Sep 17 00:00:00 2001 From: Gergely Brautigam Date: Mon, 29 May 2017 17:48:36 +0200 Subject: [PATCH 020/120] Fix dig (#56) * Fixed dig not returning element in case it is not a hash * Bumped version --- lib/jsonpath/parser.rb | 1 + lib/jsonpath/version.rb | 2 +- test/test_jsonpath.rb | 7 +++---- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/jsonpath/parser.rb b/lib/jsonpath/parser.rb index f2c826b..96f4a68 100644 --- a/lib/jsonpath/parser.rb +++ b/lib/jsonpath/parser.rb @@ -53,6 +53,7 @@ def parse_exp(exp) # @TODO: Remove this once JsonPath no longer supports ruby versions below 2.3 def dig(keys, hash) + return hash unless hash.is_a? Hash return nil unless hash.key?(keys.first) return hash.fetch(keys.first) if keys.size == 1 prev = keys.shift diff --git a/lib/jsonpath/version.rb b/lib/jsonpath/version.rb index 848d18e..1e18927 100644 --- a/lib/jsonpath/version.rb +++ b/lib/jsonpath/version.rb @@ -1,3 +1,3 @@ class JsonPath - VERSION = '0.8.2'.freeze + VERSION = '0.8.3'.freeze end diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index 09e717a..9e56680 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -255,10 +255,9 @@ 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 test_dig_return_string + assert_equal ['asdf'], JsonPath.new("$.store.book..tags[?(@ == 'asdf')]").on(@object) + end def example_object { 'store' => { From 38c5c9c495b2934255f4909589855f79adcbbbbb Mon Sep 17 00:00:00 2001 From: Gergely Brautigam Date: Sat, 10 Jun 2017 14:36:01 +0200 Subject: [PATCH 021/120] Fixed #57. (#58) * Fixed #57. * Bumped version. --- lib/jsonpath/parser.rb | 7 ++++--- lib/jsonpath/version.rb | 2 +- test/test_jsonpath.rb | 10 ++++++++++ 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/lib/jsonpath/parser.rb b/lib/jsonpath/parser.rb index 96f4a68..c2cfa9c 100644 --- a/lib/jsonpath/parser.rb +++ b/lib/jsonpath/parser.rb @@ -22,7 +22,7 @@ def parse(exp) end def parse_exp(exp) - exp = exp.gsub(/@/, '').gsub(/[\(\)]/, '') + exp = exp.gsub(/@/, '').gsub(/[\(\)]/, '').gsub(/"/, '\'').strip scanner = StringScanner.new(exp) elements = [] until scanner.eos? @@ -34,9 +34,9 @@ def parse_exp(exp) end if t = scanner.scan(/\['\w+'\]+/) elements << t.gsub(/\[|\]|'|\s+/, '') - elsif t = scanner.scan(/\s+[<>=][<>=]?\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... + elsif t = scanner.scan(/(\s+)?'?.*'?(\s+)?/) operand = t.delete("'").strip elsif t = scanner.scan(/.*/) raise "Could not process symbol: #{t}" @@ -45,6 +45,7 @@ def parse_exp(exp) 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 diff --git a/lib/jsonpath/version.rb b/lib/jsonpath/version.rb index 1e18927..11de490 100644 --- a/lib/jsonpath/version.rb +++ b/lib/jsonpath/version.rb @@ -1,3 +1,3 @@ class JsonPath - VERSION = '0.8.3'.freeze + VERSION = '0.8.4'.freeze end diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index 9e56680..b8c147a 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -257,6 +257,16 @@ def test_support_underscore_in_member_names def test_dig_return_string assert_equal ['asdf'], JsonPath.new("$.store.book..tags[?(@ == 'asdf')]").on(@object) + assert_equal [], JsonPath.new("$.store.book..tags[?(@ == 'not_asdf')]").on(@object) + end + + def test_slash_in_value + data = { + 'data' => { + 'type' => 'mps/awesome' + } + } + assert_equal [{ 'type' => 'mps/awesome' }], JsonPath.new("$.data[?(@.type == \"mps/awesome\")]").on(data) end def example_object From 244678557a1bb415badb0372b5817db93d9df7b5 Mon Sep 17 00:00:00 2001 From: Gergely Brautigam Date: Wed, 14 Jun 2017 15:57:03 +0200 Subject: [PATCH 022/120] Bumped version. (#60) --- lib/jsonpath/parser.rb | 2 +- lib/jsonpath/version.rb | 2 +- test/test_jsonpath.rb | 11 ++++++++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/jsonpath/parser.rb b/lib/jsonpath/parser.rb index c2cfa9c..f178c6b 100644 --- a/lib/jsonpath/parser.rb +++ b/lib/jsonpath/parser.rb @@ -46,7 +46,7 @@ def parse_exp(exp) 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 + operand = Float(operand) rescue operand el.send(operator.strip, operand) end diff --git a/lib/jsonpath/version.rb b/lib/jsonpath/version.rb index 11de490..56e8c37 100644 --- a/lib/jsonpath/version.rb +++ b/lib/jsonpath/version.rb @@ -1,3 +1,3 @@ class JsonPath - VERSION = '0.8.4'.freeze + VERSION = '0.8.5'.freeze end diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index b8c147a..e18c233 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -76,7 +76,7 @@ def test_and_operator_with_more_results assert_equal [@object['store']['book'][1]], JsonPath.new("$..book[?(@['price'] < 23 && @['price'] > 9)]").on(@object) end - def test_eval_with_floating_point + def test_eval_with_floating_point_and_and assert_equal [@object['store']['book'][1]], JsonPath.new("$..book[?(@['price'] < 23.0 && @['price'] > 9.0)]").on(@object) end @@ -269,6 +269,15 @@ def test_slash_in_value assert_equal [{ 'type' => 'mps/awesome' }], JsonPath.new("$.data[?(@.type == \"mps/awesome\")]").on(data) end + def test_floating_point_with_precision_marker + data = { + 'data' => { + 'type' => 0.00001 + } + } + assert_equal [{"type"=>0.00001}], JsonPath.new("$.data[?(@.type == 0.00001)]").on(data) + end + def example_object { 'store' => { 'book' => [ From 744d8cf92646d1181a4e4c7414db8213a0ad9e34 Mon Sep 17 00:00:00 2001 From: Gergely Brautigam Date: Fri, 4 Aug 2017 09:15:39 +0200 Subject: [PATCH 023/120] Fixed delete with index as delete. Closes #61 (#62) --- lib/jsonpath/enumerable.rb | 9 +++++++-- test/test_jsonpath.rb | 7 +++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/lib/jsonpath/enumerable.rb b/lib/jsonpath/enumerable.rb index 7a1bd80..e04864c 100644 --- a/lib/jsonpath/enumerable.rb +++ b/lib/jsonpath/enumerable.rb @@ -84,13 +84,18 @@ def handle_question_mark(sub_path, node, pos, &blk) end def yield_value(blk, context, key) + key = Integer(key) rescue key if key case @mode when nil blk.call(key ? context[key] : context) when :compact - context.delete(key) if key && context[key].nil? + if key && context[key].nil? + key.is_a?(Integer) ? context.delete_at(key) : context.delete(key) + end when :delete - context.delete(key) if key + if key + key.is_a?(Integer) ? context.delete_at(key) : context.delete(key) + end when :substitute if key context[key] = blk.call(context[key]) diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index e18c233..e4f7836 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -170,6 +170,13 @@ def test_delete assert_equal({ 'you' => nil }, h) end + def test_delete_for_array + before = JsonPath.on(@object, '$..store.book[1]') + JsonPath.for(@object).delete!('$..store.book[0]') + after = JsonPath.on(@object, '$..store.book[0]') + assert_equal(after, before, 'Before is the second element. After should have been equal to the next element after delete.') + end + def test_at_sign_in_json_element data = { '@colors' => From 6691a47ecf8d83c1426e796a1eb45e340c28ebcd Mon Sep 17 00:00:00 2001 From: Gergely Brautigam Date: Fri, 4 Aug 2017 09:17:25 +0200 Subject: [PATCH 024/120] Bumped version. --- lib/jsonpath/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonpath/version.rb b/lib/jsonpath/version.rb index 56e8c37..d1698df 100644 --- a/lib/jsonpath/version.rb +++ b/lib/jsonpath/version.rb @@ -1,3 +1,3 @@ class JsonPath - VERSION = '0.8.5'.freeze + VERSION = '0.8.6'.freeze end From afcec9af8d688e0e61d48f978132464bf0425e8b Mon Sep 17 00:00:00 2001 From: Gergely Brautigam Date: Fri, 11 Aug 2017 20:40:38 +0200 Subject: [PATCH 025/120] Closes #63 (#64) --- lib/jsonpath/parser.rb | 1 + test/test_jsonpath.rb | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/lib/jsonpath/parser.rb b/lib/jsonpath/parser.rb index f178c6b..13a155d 100644 --- a/lib/jsonpath/parser.rb +++ b/lib/jsonpath/parser.rb @@ -46,6 +46,7 @@ def parse_exp(exp) return false unless el return true if operator.nil? && el + el = Float(el) rescue el operand = Float(operand) rescue operand el.send(operator.strip, operand) end diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index e4f7836..f12b505 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -285,6 +285,16 @@ def test_floating_point_with_precision_marker assert_equal [{"type"=>0.00001}], JsonPath.new("$.data[?(@.type == 0.00001)]").on(data) end + def test_digits_only_string + data = { + 'foo' => { + 'type' => 'users', + 'id' => '123' + } + } + assert_equal([{"type"=>"users", "id"=>"123"}], JsonPath.new("$.foo[?(@.id == '123')]").on(data)) + end + def example_object { 'store' => { 'book' => [ From f3a1dca0aedc1400b48e48b79202fad5d57c87d2 Mon Sep 17 00:00:00 2001 From: Gergely Brautigam Date: Fri, 11 Aug 2017 20:41:32 +0200 Subject: [PATCH 026/120] Bumped version. --- lib/jsonpath/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonpath/version.rb b/lib/jsonpath/version.rb index d1698df..a2bd377 100644 --- a/lib/jsonpath/version.rb +++ b/lib/jsonpath/version.rb @@ -1,3 +1,3 @@ class JsonPath - VERSION = '0.8.6'.freeze + VERSION = '0.8.7'.freeze end From 09795f464e6ee5bb74d5da03b0ac6d7c1d94630e Mon Sep 17 00:00:00 2001 From: Gergely Brautigam Date: Sun, 1 Oct 2017 11:24:13 +0200 Subject: [PATCH 027/120] Closes #67 (#68) --- .travis.yml | 3 ++- lib/jsonpath/parser.rb | 4 ++-- test/test_jsonpath.rb | 50 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index c80566a..307a027 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,5 @@ rvm: - 2.1.6 - 2.3.1 - - jruby + - ruby-head + - jruby-head diff --git a/lib/jsonpath/parser.rb b/lib/jsonpath/parser.rb index 13a155d..1097c89 100644 --- a/lib/jsonpath/parser.rb +++ b/lib/jsonpath/parser.rb @@ -22,7 +22,7 @@ def parse(exp) end def parse_exp(exp) - exp = exp.gsub(/@/, '').gsub(/[\(\)]/, '').gsub(/"/, '\'').strip + exp = exp.sub(/@/, '').gsub(/[\(\)]/, '').gsub(/"/, '\'').strip scanner = StringScanner.new(exp) elements = [] until scanner.eos? @@ -32,7 +32,7 @@ def parse_exp(exp) num = scanner.scan(/\d+/) return @_current_node.send(sym.to_sym).send(op.to_sym, num.to_i) end - if t = scanner.scan(/\['\w+'\]+/) + if t = scanner.scan(/\['[a-zA-Z@&\*\/\$%\^\?]+'\]+/) elements << t.gsub(/\[|\]|'|\s+/, '') elsif t = scanner.scan(/(\s+)?[<>=][<>=]?(\s+)?/) operator = t diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index f12b505..a3c4e7b 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -295,6 +295,56 @@ def test_digits_only_string assert_equal([{"type"=>"users", "id"=>"123"}], JsonPath.new("$.foo[?(@.id == '123')]").on(data)) end + def test_at_in_filter + jsonld = { + "mentions" => [ + { + "name" => "Delimara Powerplant", + "identifier" => "krzana://took/powerstation/Delimara Powerplant", + "@type" => "Place", + "geo" => { + "latitude" => 35.83020073454, + "longitude" => 14.55602645874 + } + } + ] + } + assert_equal(['Place'], JsonPath.new("$..mentions[?(@['@type'] == 'Place')].@type").on(jsonld)) + end + + def test_dollar_in_filter + jsonld = { + "mentions" => [ + { + "name" => "Delimara Powerplant", + "identifier" => "krzana://took/powerstation/Delimara Powerplant", + "$type" => "Place", + "geo" => { + "latitude" => 35.83020073454, + "longitude" => 14.55602645874 + } + } + ] + } + assert_equal(['Place'], JsonPath.new("$..mentions[?(@['$type'] == 'Place')].$type").on(jsonld)) + end + + def test_at_in_value + jsonld = { + "mentions" => + { + "name" => "Delimara Powerplant", + "identifier" => "krzana://took/powerstation/Delimara Powerplant", + "type" => "@Place", + "geo" => { + "latitude" => 35.83020073454, + "longitude" => 14.55602645874 + } + } + } + assert_equal(['@Place'], JsonPath.new("$..mentions.type[?(@ == '@Place')]").on(jsonld)) + end + def example_object { 'store' => { 'book' => [ From 8caab76ff047eaa3fc765834a4bb16b27163a3c9 Mon Sep 17 00:00:00 2001 From: Gergely Brautigam Date: Sun, 1 Oct 2017 11:26:52 +0200 Subject: [PATCH 028/120] Version bump for bugfix. --- lib/jsonpath/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonpath/version.rb b/lib/jsonpath/version.rb index a2bd377..13d30b8 100644 --- a/lib/jsonpath/version.rb +++ b/lib/jsonpath/version.rb @@ -1,3 +1,3 @@ class JsonPath - VERSION = '0.8.7'.freeze + VERSION = '0.8.8'.freeze end From aa641cdd034f7a4dfbca32c70e82f67e68455919 Mon Sep 17 00:00:00 2001 From: Carl Douglas <105003+carld@users.noreply.github.com> Date: Fri, 27 Oct 2017 21:16:28 +1300 Subject: [PATCH 029/120] Filter with underscore (#71) * Test filter containing underscore * Include underscore in filter token scanner --- lib/jsonpath/parser.rb | 2 +- test/test_jsonpath.rb | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/jsonpath/parser.rb b/lib/jsonpath/parser.rb index 1097c89..942f426 100644 --- a/lib/jsonpath/parser.rb +++ b/lib/jsonpath/parser.rb @@ -32,7 +32,7 @@ def parse_exp(exp) num = scanner.scan(/\d+/) return @_current_node.send(sym.to_sym).send(op.to_sym, num.to_i) end - if t = scanner.scan(/\['[a-zA-Z@&\*\/\$%\^\?]+'\]+/) + if t = scanner.scan(/\['[a-zA-Z@&\*\/\$%\^\?_]+'\]+/) elements << t.gsub(/\[|\]|'|\s+/, '') elsif t = scanner.scan(/(\s+)?[<>=][<>=]?(\s+)?/) operator = t diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index a3c4e7b..b88c8de 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -329,6 +329,20 @@ def test_dollar_in_filter assert_equal(['Place'], JsonPath.new("$..mentions[?(@['$type'] == 'Place')].$type").on(jsonld)) end + def test_underscore_in_filter + jsonld = { + "attributes" => [ + { + "store" => [ + { "with" => "urn" }, + { "with_underscore" => "urn:1" } + ] + } + ] + } + assert_equal(['urn:1'], JsonPath.new("$.attributes..store[?(@['with_underscore'] == 'urn:1')].with_underscore").on(jsonld)) + end + def test_at_in_value jsonld = { "mentions" => From c222c50273a102b32570f48c99740b37dbbb9a9b Mon Sep 17 00:00:00 2001 From: Gergely Brautigam Date: Fri, 27 Oct 2017 10:16:46 +0200 Subject: [PATCH 030/120] Removing trailing and leading parentheses rather than nuking everything. Closes #69. (#72) --- lib/jsonpath/parser.rb | 2 +- test/test_jsonpath.rb | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/jsonpath/parser.rb b/lib/jsonpath/parser.rb index 942f426..fcd9bec 100644 --- a/lib/jsonpath/parser.rb +++ b/lib/jsonpath/parser.rb @@ -22,7 +22,7 @@ def parse(exp) end def parse_exp(exp) - exp = exp.sub(/@/, '').gsub(/[\(\)]/, '').gsub(/"/, '\'').strip + exp = exp.sub(/@/, '').gsub(/^\(/, '').gsub(/\)$/, '').gsub(/"/, '\'').strip scanner = StringScanner.new(exp) elements = [] until scanner.eos? diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index b88c8de..e9ff7d3 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -357,7 +357,16 @@ def test_at_in_value } } assert_equal(['@Place'], JsonPath.new("$..mentions.type[?(@ == '@Place')]").on(jsonld)) - end + end + + def test_parens_in_value + data = { + 'data' => { + 'number' => '(492) 080-3961' + } + } + assert_equal [{'number'=>'(492) 080-3961'}], JsonPath.new("$.data[?(@.number == '(492) 080-3961')]").on(data) + end def example_object { 'store' => { From 0cc948f5496c4fbd9a271be72cc5269e55c74b10 Mon Sep 17 00:00:00 2001 From: Gergely Brautigam Date: Fri, 27 Oct 2017 10:21:26 +0200 Subject: [PATCH 031/120] Bumped version for release. --- lib/jsonpath/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonpath/version.rb b/lib/jsonpath/version.rb index 13d30b8..aa7a798 100644 --- a/lib/jsonpath/version.rb +++ b/lib/jsonpath/version.rb @@ -1,3 +1,3 @@ class JsonPath - VERSION = '0.8.8'.freeze + VERSION = '0.8.10'.freeze end From d8e2436f969cdf74293332b0d352ea80e6f2fe05 Mon Sep 17 00:00:00 2001 From: Hermann Date: Tue, 2 Jan 2018 08:54:44 +0100 Subject: [PATCH 032/120] Added regex parser (#75) * Added regex parser * Added full dependency --- jsonpath.gemspec | 1 + lib/jsonpath/parser.rb | 7 +++++-- test/test_jsonpath.rb | 11 +++++++++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/jsonpath.gemspec b/jsonpath.gemspec index 30b5a00..6345756 100644 --- a/jsonpath.gemspec +++ b/jsonpath.gemspec @@ -24,6 +24,7 @@ Gem::Specification.new do |s| # dependencies s.add_runtime_dependency 'multi_json' + s.add_runtime_dependency 'to_regexp', '~> 0.2.1' s.add_development_dependency 'code_stats' s.add_development_dependency 'rake' s.add_development_dependency 'minitest', '~> 2.2.0' diff --git a/lib/jsonpath/parser.rb b/lib/jsonpath/parser.rb index fcd9bec..d299975 100644 --- a/lib/jsonpath/parser.rb +++ b/lib/jsonpath/parser.rb @@ -1,4 +1,5 @@ require 'strscan' +require 'to_regexp' class JsonPath # Parser parses and evaluates an expression passed to @_current_node. @@ -34,10 +35,12 @@ def parse_exp(exp) end if t = scanner.scan(/\['[a-zA-Z@&\*\/\$%\^\?_]+'\]+/) elements << t.gsub(/\[|\]|'|\s+/, '') - elsif t = scanner.scan(/(\s+)?[<>=][<>=]?(\s+)?/) + elsif t = scanner.scan(/(\s+)?[<>=][=~]?(\s+)?/) operator = t elsif t = scanner.scan(/(\s+)?'?.*'?(\s+)?/) - operand = t.delete("'").strip + operand = operator.strip == "=~" ? t.to_regexp : t.delete("'").strip + elsif t = scanner.scan(/\/\w+\//) + elsif t = scanner.scan(/.*/) raise "Could not process symbol: #{t}" end diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index e9ff7d3..4dcaec6 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -368,6 +368,17 @@ def test_parens_in_value assert_equal [{'number'=>'(492) 080-3961'}], JsonPath.new("$.data[?(@.number == '(492) 080-3961')]").on(data) end + def test_regex + assert_equal [], JsonPath.new('$..book[?(@.author =~ /herman/)]').on(@object) + assert_equal [ + @object['store']['book'][2], + @object['store']['book'][4], + @object['store']['book'][5], + @object['store']['book'][6], + ], JsonPath.new('$..book[?(@.author =~ /herman|lukyanenko/i)]').on(@object) + assert_equal ["asdf", "asdf2"], JsonPath.new("$.store.book..tags[?(@ =~ /asdf/)]").on(@object) + end + def example_object { 'store' => { 'book' => [ From 385799710b5de342ea93abdf098a8ad12114d63c Mon Sep 17 00:00:00 2001 From: Gergely Brautigam Date: Tue, 2 Jan 2018 20:41:33 +0100 Subject: [PATCH 033/120] Bumped version. --- lib/jsonpath/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonpath/version.rb b/lib/jsonpath/version.rb index aa7a798..b0f5c89 100644 --- a/lib/jsonpath/version.rb +++ b/lib/jsonpath/version.rb @@ -1,3 +1,3 @@ class JsonPath - VERSION = '0.8.10'.freeze + VERSION = '0.8.11'.freeze end From 4d53500e4e9a8880ad1d67e4625328aa99b33472 Mon Sep 17 00:00:00 2001 From: Gergely Brautigam Date: Thu, 8 Feb 2018 15:35:22 +0100 Subject: [PATCH 034/120] Added boolean flag test. (#78) --- test/test_jsonpath.rb | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index 4dcaec6..4f7306d 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -368,6 +368,20 @@ def test_parens_in_value assert_equal [{'number'=>'(492) 080-3961'}], JsonPath.new("$.data[?(@.number == '(492) 080-3961')]").on(data) end + + def test_boolean_parameter_value + data = { + 'data' => [{ + 'isTrue' => true, + 'name' => 'testname1', + },{ + 'isTrue' => false, + 'name' => 'testname2', + }] + } + assert_equal [{"isTrue"=>true, "name"=>"testname1"}], JsonPath.new("$.data[?(@.isTrue)]").on(data) + end + def test_regex assert_equal [], JsonPath.new('$..book[?(@.author =~ /herman/)]').on(@object) assert_equal [ From bcc6c611d861940364ca2879968da4f25ae86e84 Mon Sep 17 00:00:00 2001 From: Gergely Brautigam Date: Sat, 12 May 2018 17:12:28 +0200 Subject: [PATCH 035/120] Fix regession reported in #79 (#80) * Fix regession reported in #79 * Failing test. * Fixed. --- lib/jsonpath/enumerable.rb | 3 +- lib/jsonpath/parser.rb | 5 ++- test/test_jsonpath.rb | 79 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 3 deletions(-) diff --git a/lib/jsonpath/enumerable.rb b/lib/jsonpath/enumerable.rb index e04864c..390bf64 100644 --- a/lib/jsonpath/enumerable.rb +++ b/lib/jsonpath/enumerable.rb @@ -111,8 +111,7 @@ def process_function_or_literal(exp, default = nil) return nil unless @_current_node identifiers = /@?((? { 'book' => [ From f6475b0f34182edd08c37da27e0d536e8b419b8a Mon Sep 17 00:00:00 2001 From: Gergely Brautigam Date: Sat, 12 May 2018 17:13:33 +0200 Subject: [PATCH 036/120] Bumped version. --- lib/jsonpath/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonpath/version.rb b/lib/jsonpath/version.rb index b0f5c89..6a36ff4 100644 --- a/lib/jsonpath/version.rb +++ b/lib/jsonpath/version.rb @@ -1,3 +1,3 @@ class JsonPath - VERSION = '0.8.11'.freeze + VERSION = '0.8.12'.freeze end From 8fc1874d291359b2e86e7144d7ba0039479d92e0 Mon Sep 17 00:00:00 2001 From: Gergely Brautigam Date: Sun, 13 May 2018 22:25:18 +0200 Subject: [PATCH 037/120] Slowly fixing the regression. (#81) * Slowly fixing the regression. * Fixed all the regression issues. --- lib/jsonpath/enumerable.rb | 23 +++++++----------- lib/jsonpath/parser.rb | 9 ++++--- test/test_jsonpath.rb | 49 ++++++++++++++++++++++++++++++++------ 3 files changed, 57 insertions(+), 24 deletions(-) diff --git a/lib/jsonpath/enumerable.rb b/lib/jsonpath/enumerable.rb index 390bf64..e95152b 100644 --- a/lib/jsonpath/enumerable.rb +++ b/lib/jsonpath/enumerable.rb @@ -36,13 +36,14 @@ 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) + 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 '?' handle_question_mark(sub_path, node, pos, &blk) else - next unless node.is_a?(Array) && !node.empty? + next if node.is_a?(Array) && node.empty? array_args = sub_path.split(':') if array_args[0] == '*' start_idx = 0 @@ -65,19 +66,13 @@ def handle_wildecard(node, expr, context, key, pos, &blk) def handle_question_mark(sub_path, node, pos, &blk) 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')}]") + 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 - 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 @@ -111,7 +106,7 @@ def process_function_or_literal(exp, default = nil) return nil unless @_current_node identifiers = /@?((? '123' } } - assert_equal [{ 'type' => 'users', 'id' => '123' }], JsonPath.new("$.data[?(@.type == 'users')]").on(data) - assert_equal [], JsonPath.new("$.data[?(@.type == 'admins')]").on(data) + assert_equal [{ 'type' => 'users', 'id' => '123' }], JsonPath.new("$.[?(@.type == 'users')]").on(data) + assert_equal [], JsonPath.new("$.[?(@.type == 'admins')]").on(data) end def test_support_at_sign_in_member_names @@ -270,9 +270,11 @@ def test_dig_return_string def test_slash_in_value data = { - 'data' => { - 'type' => 'mps/awesome' - } + 'data' => [{ + 'type' => 'mps/awesome', + },{ + 'type' => 'not', + }] } assert_equal [{ 'type' => 'mps/awesome' }], JsonPath.new("$.data[?(@.type == \"mps/awesome\")]").on(data) end @@ -283,7 +285,7 @@ def test_floating_point_with_precision_marker 'type' => 0.00001 } } - assert_equal [{"type"=>0.00001}], JsonPath.new("$.data[?(@.type == 0.00001)]").on(data) + assert_equal [{"type"=>0.00001}], JsonPath.new("$.[?(@.type == 0.00001)]").on(data) end def test_digits_only_string @@ -293,6 +295,19 @@ def test_digits_only_string 'id' => '123' } } + assert_equal([{"type"=>"users", "id"=>"123"}], JsonPath.new("$.[?(@.id == '123')]").on(data)) + end + + def test_digits_only_string_in_array + data = { + 'foo' => [{ + 'type' => 'users', + 'id' => '123' + },{ + 'type' => 'users', + 'id' => '321' + }] + } assert_equal([{"type"=>"users", "id"=>"123"}], JsonPath.new("$.foo[?(@.id == '123')]").on(data)) end @@ -366,7 +381,7 @@ def test_parens_in_value 'number' => '(492) 080-3961' } } - assert_equal [{'number'=>'(492) 080-3961'}], JsonPath.new("$.data[?(@.number == '(492) 080-3961')]").on(data) + assert_equal [{'number'=>'(492) 080-3961'}], JsonPath.new("$.[?(@.number == '(492) 080-3961')]").on(data) end @@ -452,6 +467,26 @@ def test_regression_3 assert_equal 'C09C598QL', JsonPath.on(json, "$..channels[?(@.is_archived)].id")[0] end + def test_regression_4 + json = { + ok: true, + channels: [ + { + id: 'C09C5GYHF', + name: 'general', + is_archived: false + }, + { + id: 'C09C598QL', + name: 'random', + is_archived: true + } + ] + }.to_json + + assert_equal ['C09C5GYHF'], JsonPath.on(json, "$..[?(@.name == 'general')].id") + end + def test_regression_5 json = { ok: true, From bc0ae7a837d2ec8e357a07c92fff1bba9ba4dadb Mon Sep 17 00:00:00 2001 From: Gergely Brautigam Date: Sun, 13 May 2018 23:17:23 +0200 Subject: [PATCH 038/120] Bumped version. --- lib/jsonpath/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonpath/version.rb b/lib/jsonpath/version.rb index 6a36ff4..285509e 100644 --- a/lib/jsonpath/version.rb +++ b/lib/jsonpath/version.rb @@ -1,3 +1,3 @@ class JsonPath - VERSION = '0.8.12'.freeze + VERSION = '0.9.0'.freeze end From 72227158443fc8ff5cd1a06b3dbedcec457c4192 Mon Sep 17 00:00:00 2001 From: Gergely Brautigam Date: Fri, 8 Jun 2018 07:41:37 +0200 Subject: [PATCH 039/120] Regression fixing for issue: #83 #82. (#85) * Regression fixing for issue: #83 #82. * Fixing new line error in json path. --- lib/jsonpath.rb | 2 +- lib/jsonpath/enumerable.rb | 16 +++++++---- test/test_jsonpath.rb | 56 ++++++++++++++++++++++++++++++++++---- 3 files changed, 63 insertions(+), 11 deletions(-) diff --git a/lib/jsonpath.rb b/lib/jsonpath.rb index d25c2e8..4a1a5a6 100644 --- a/lib/jsonpath.rb +++ b/lib/jsonpath.rb @@ -14,7 +14,7 @@ class JsonPath def initialize(path, opts = nil) @opts = opts - scanner = StringScanner.new(path) + scanner = StringScanner.new(path.strip) @path = [] until scanner.eos? if token = scanner.scan(/\$\B|@\B|\*|\.\./) diff --git a/lib/jsonpath/enumerable.rb b/lib/jsonpath/enumerable.rb index e95152b..05ab61b 100644 --- a/lib/jsonpath/enumerable.rb +++ b/lib/jsonpath/enumerable.rb @@ -66,14 +66,20 @@ def handle_wildecard(node, expr, context, key, pos, &blk) def handle_question_mark(sub_path, node, pos, &blk) case node - when Hash, Array - (node.is_a?(Hash) ? node.keys : (0..node.size)).each do |e| - @_current_node = node[e] + 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 + end + when Hash if process_function_or_literal(sub_path[1, sub_path.size - 1]) each(@_current_node, nil, pos + 1, &blk) end - end - else + else yield node if process_function_or_literal(sub_path[1, sub_path.size - 1]) end end diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index d8ad012..8a45cfb 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -245,7 +245,7 @@ def test_support_filter_by_object_childnode_value 'id' => '123' } } - assert_equal [{ 'type' => 'users', 'id' => '123' }], JsonPath.new("$.[?(@.type == 'users')]").on(data) + assert_equal [{ 'type' => 'users', 'id' => '123' }], JsonPath.new("$.data[?(@.type == 'users')]").on(data) assert_equal [], JsonPath.new("$.[?(@.type == 'admins')]").on(data) end @@ -285,7 +285,7 @@ def test_floating_point_with_precision_marker 'type' => 0.00001 } } - assert_equal [{"type"=>0.00001}], JsonPath.new("$.[?(@.type == 0.00001)]").on(data) + assert_equal [{"type"=>0.00001}], JsonPath.new("$.data[?(@.type == 0.00001)]").on(data) end def test_digits_only_string @@ -295,7 +295,7 @@ def test_digits_only_string 'id' => '123' } } - assert_equal([{"type"=>"users", "id"=>"123"}], JsonPath.new("$.[?(@.id == '123')]").on(data)) + assert_equal([{"type"=>"users", "id"=>"123"}], JsonPath.new("$.foo[?(@.id == '123')]").on(data)) end def test_digits_only_string_in_array @@ -381,7 +381,7 @@ def test_parens_in_value 'number' => '(492) 080-3961' } } - assert_equal [{'number'=>'(492) 080-3961'}], JsonPath.new("$.[?(@.number == '(492) 080-3961')]").on(data) + assert_equal [{'number'=>'(492) 080-3961'}], JsonPath.new("$.data[?(@.number == '(492) 080-3961')]").on(data) end @@ -484,7 +484,7 @@ def test_regression_4 ] }.to_json - assert_equal ['C09C5GYHF'], JsonPath.on(json, "$..[?(@.name == 'general')].id") + assert_equal ['C09C5GYHF'], JsonPath.on(json, "$..channels[?(@.name == 'general')].id") end def test_regression_5 @@ -507,6 +507,52 @@ def test_regression_5 assert_equal 'C09C5GYHF', JsonPath.on(json, "$..channels[?(@.is_archived == 'false')].id")[0] end + def test_changed + json = + { + "snapshot"=> { + "objects"=> { + "whatever"=> [ + { + "column"=> { + "name"=> "ASSOCIATE_FLAG", + "nullable"=> true + } + }, + { + "column"=> { + "name"=> "AUTHOR", + "nullable"=> false + } + } + ] + } + } + } + assert_equal true, JsonPath.on(json, "$..column[?(@.name == 'ASSOCIATE_FLAG')].nullable")[0] + end + + def test_another + json = { + initial: true, + not: true + }.to_json + assert_equal [true], JsonPath.on(json, "$.initial[?(@)]") + json = { + initial: false, + not: true + }.to_json + assert_equal [], JsonPath.on(json, "$.initial[?(@)]") + end + + def test_hanging + json = { initial: true }.to_json + success_path = "$.initial" + assert_equal [true], JsonPath.on(json, success_path) + broken_path = "$.initial\n" + assert_equal [true], JsonPath.on(json, broken_path) + end + def example_object { 'store' => { 'book' => [ From a1a4efebc202496b3216dede625393fd96e90632 Mon Sep 17 00:00:00 2001 From: Gergely Brautigam Date: Fri, 8 Jun 2018 07:43:14 +0200 Subject: [PATCH 040/120] Updated version for bug fix release. --- lib/jsonpath/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonpath/version.rb b/lib/jsonpath/version.rb index 285509e..d9afb17 100644 --- a/lib/jsonpath/version.rb +++ b/lib/jsonpath/version.rb @@ -1,3 +1,3 @@ class JsonPath - VERSION = '0.9.0'.freeze + VERSION = '0.9.1'.freeze end From dbacecb514f56f913cca09af949ff0f3bcabfb5b Mon Sep 17 00:00:00 2001 From: Jan Kreuzzieger Date: Wed, 13 Jun 2018 22:47:59 +0200 Subject: [PATCH 041/120] Support returning null for missing leaves in a path (#86) --- lib/jsonpath.rb | 4 ++-- lib/jsonpath/enumerable.rb | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/jsonpath.rb b/lib/jsonpath.rb index 4a1a5a6..eaeba18 100644 --- a/lib/jsonpath.rb +++ b/lib/jsonpath.rb @@ -12,7 +12,7 @@ class JsonPath attr_accessor :path - def initialize(path, opts = nil) + def initialize(path, opts = {}) @opts = opts scanner = StringScanner.new(path.strip) @path = [] @@ -75,7 +75,7 @@ def enum_on(obj_or_str, mode = nil) end alias_method :[], :enum_on - def self.on(obj_or_str, path, opts = nil) + def self.on(obj_or_str, path, opts = {}) new(path, opts).on(process_object(obj_or_str)) end diff --git a/lib/jsonpath/enumerable.rb b/lib/jsonpath/enumerable.rb index 05ab61b..feef2b9 100644 --- a/lib/jsonpath/enumerable.rb +++ b/lib/jsonpath/enumerable.rb @@ -2,7 +2,7 @@ class JsonPath class Enumerable include ::Enumerable - def initialize(path, object, mode, options = nil) + def initialize(path, object, mode, options = {}) @path = path.path @object = object @mode = mode @@ -38,6 +38,7 @@ def handle_wildecard(node, expr, context, key, pos, &blk) when '\'', '"' if node.is_a?(Hash) k = sub_path[1, sub_path.size - 2] + node[k] ||= nil if @options[:default_path_leaf_to_null] each(node, k, pos + 1, &blk) if node.key?(k) end when '?' From 65ee068e355c6d3ae23a2e8f84cfd5160e587606 Mon Sep 17 00:00:00 2001 From: Gergely Brautigam Date: Thu, 14 Jun 2018 06:30:15 +0200 Subject: [PATCH 042/120] Fixed using root element as matcher. (#87) * Fixed using root element as matcher. * Proper fix for boolean values this time. --- lib/jsonpath/parser.rb | 15 +++++++++++---- test/test_jsonpath.rb | 10 +++++++++- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/lib/jsonpath/parser.rb b/lib/jsonpath/parser.rb index 160fa27..63e8a54 100644 --- a/lib/jsonpath/parser.rb +++ b/lib/jsonpath/parser.rb @@ -38,9 +38,16 @@ def parse_exp(exp) elsif t = scanner.scan(/(\s+)?[<>=][=~]?(\s+)?/) operator = t elsif t = scanner.scan(/(\s+)?'?.*'?(\s+)?/) - operand = operator.strip == "=~" ? t.to_regexp : t.delete("'").strip + # If we encounter a node which does not contain `'` it means + # that we are dealing with a boolean type. + if t == "true" + operand = true + elsif t == "false" + operand = false + else + operand = operator.strip == "=~" ? t.to_regexp : t.delete("'").strip + end elsif t = scanner.scan(/\/\w+\//) - elsif t = scanner.scan(/.*/) raise "Could not process symbol: #{t}" end @@ -56,8 +63,8 @@ def parse_exp(exp) el = Float(el) rescue el operand = Float(operand) rescue operand - operand = false if operand == 'false' && el == false - el.send(operator.strip, operand) + + el.__send__(operator.strip, operand) end private diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index 8a45cfb..227534a 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -537,12 +537,20 @@ def test_another initial: true, not: true }.to_json - assert_equal [true], JsonPath.on(json, "$.initial[?(@)]") + assert_equal [{"initial"=>true, "not"=>true}], JsonPath.on(json, "$.[?(@.initial == true)]") json = { initial: false, not: true }.to_json assert_equal [], JsonPath.on(json, "$.initial[?(@)]") + assert_equal [], JsonPath.on(json, "$.[?(@.initial == true)]") + assert_equal [{"initial"=>false, "not"=>true}], JsonPath.on(json, "$.[?(@.initial == false)]") + json = { + initial: 'false', + not: true + }.to_json + assert_equal [{"initial"=>"false", "not"=>true}], JsonPath.on(json, "$.[?(@.initial == 'false')]") + assert_equal [], JsonPath.on(json, "$.[?(@.initial == false)]") end def test_hanging From 74a5a5bc5e514861c963907faac66dae058b9531 Mon Sep 17 00:00:00 2001 From: Gergely Brautigam Date: Thu, 14 Jun 2018 06:35:23 +0200 Subject: [PATCH 043/120] Bumped version. --- lib/jsonpath/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonpath/version.rb b/lib/jsonpath/version.rb index d9afb17..3c33b9f 100644 --- a/lib/jsonpath/version.rb +++ b/lib/jsonpath/version.rb @@ -1,3 +1,3 @@ class JsonPath - VERSION = '0.9.1'.freeze + VERSION = '0.9.2'.freeze end From e2d2e8a62244eb9e7c5ca315fdbf086a08fbba8f Mon Sep 17 00:00:00 2001 From: Gergely Brautigam Date: Sun, 8 Jul 2018 10:04:33 +0200 Subject: [PATCH 044/120] Fixed a problem where deleting slices was not working. (#90) * Fixed a problem where deleting slices was not working. * Ran a rubocop fix. * Added rubocop run. * Added rubocop todo * Updated rescue. --- .rubocop.yml | 1 + .rubocop_todo.yml | 112 +++++++++++++++++++ .travis.yml | 1 + Gemfile | 7 +- Rakefile | 11 +- bin/jsonpath | 1 + jsonpath.gemspec | 13 ++- lib/jsonpath.rb | 6 +- lib/jsonpath/enumerable.rb | 33 +++--- lib/jsonpath/parser.rb | 30 ++--- lib/jsonpath/proxy.rb | 2 + lib/jsonpath/version.rb | 4 +- test/test_jsonpath.rb | 222 ++++++++++++++++++++++--------------- test/test_jsonpath_bin.rb | 2 + 14 files changed, 318 insertions(+), 127 deletions(-) create mode 100644 .rubocop.yml create mode 100644 .rubocop_todo.yml diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..cc32da4 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1 @@ +inherit_from: .rubocop_todo.yml diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml new file mode 100644 index 0000000..cc1ba9b --- /dev/null +++ b/.rubocop_todo.yml @@ -0,0 +1,112 @@ +# This configuration was generated by +# `rubocop --auto-gen-config` +# on 2018-07-07 21:29:45 +0200 using RuboCop version 0.57.2. +# The point is for the user to remove these configuration records +# one by one as the offenses are removed from the code base. +# Note that changes in the inspected code, or installation of new +# versions of RuboCop, may require this file to be generated again. + +# Offense count: 15 +# Configuration parameters: AllowSafeAssignment. +Lint/AssignmentInCondition: + Exclude: + - 'lib/jsonpath.rb' + - 'lib/jsonpath/parser.rb' + +# Offense count: 1 +Lint/IneffectiveAccessModifier: + Exclude: + - 'lib/jsonpath.rb' + +# Offense count: 16 +Metrics/AbcSize: + Max: 54 + +# Offense count: 2 +# Configuration parameters: CountComments, ExcludedMethods. +Metrics/BlockLength: + Max: 33 + +# Offense count: 1 +# Configuration parameters: CountBlocks. +Metrics/BlockNesting: + Max: 4 + +# Offense count: 2 +# Configuration parameters: CountComments. +Metrics/ClassLength: + Max: 578 + +# Offense count: 6 +Metrics/CyclomaticComplexity: + Max: 18 + +# Offense count: 24 +# Configuration parameters: CountComments. +Metrics/MethodLength: + Max: 52 + +# Offense count: 1 +# Configuration parameters: CountKeywordArgs. +Metrics/ParameterLists: + Max: 6 + +# Offense count: 6 +Metrics/PerceivedComplexity: + Max: 20 + +# Offense count: 1 +# Configuration parameters: AllowedChars. +Style/AsciiComments: + Exclude: + - 'lib/jsonpath/parser.rb' + +# Offense count: 2 +Style/Documentation: + Exclude: + - 'spec/**/*' + - 'test/**/*' + - 'lib/jsonpath/enumerable.rb' + - 'lib/jsonpath/proxy.rb' + +# Offense count: 1 +# Configuration parameters: MinBodyLength. +Style/GuardClause: + Exclude: + - 'lib/jsonpath/enumerable.rb' + +# Offense count: 2 +# Cop supports --auto-correct. +Style/IfUnlessModifier: + Exclude: + - 'lib/jsonpath/enumerable.rb' + +# Offense count: 2 +# Cop supports --auto-correct. +# Configuration parameters: AutoCorrect, EnforcedStyle. +# SupportedStyles: predicate, comparison +Style/NumericPredicate: + Exclude: + - 'spec/**/*' + - 'lib/jsonpath/enumerable.rb' + +# Offense count: 2 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, AllowInnerSlashes. +# SupportedStyles: slashes, percent_r, mixed +Style/RegexpLiteral: + Exclude: + - 'lib/jsonpath/parser.rb' + +# Offense count: 3 +# Cop supports --auto-correct. +Style/RescueModifier: + Exclude: + - 'lib/jsonpath/enumerable.rb' + - 'lib/jsonpath/parser.rb' + +# Offense count: 71 +# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. +# URISchemes: http, https +Metrics/LineLength: + Max: 175 diff --git a/.travis.yml b/.travis.yml index 307a027..6a13dbf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,4 @@ +language: ruby rvm: - 2.1.6 - 2.3.1 diff --git a/Gemfile b/Gemfile index 164278d..18ec7c8 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,6 @@ -source "http://rubygems.org" +# frozen_string_literal: true + +source 'http://rubygems.org' gemspec -gem 'simplecov', :require => false, :group => :test +gem 'rubocop', require: true, group: :test +gem 'simplecov', require: false, group: :test diff --git a/Rakefile b/Rakefile index 0afd6a4..672e204 100644 --- a/Rakefile +++ b/Rakefile @@ -1,3 +1,12 @@ +# frozen_string_literal: true + +desc 'run rubocop' +task(:rubocop) do + require 'rubocop' + cli = RuboCop::CLI.new + cli.run +end + require 'simplecov' SimpleCov.start do add_filter '/test/' @@ -11,4 +20,4 @@ task :test do Dir['./test/**/test_*.rb'].each { |test| require test } end -task default: :test +task default: %i[test rubocop] diff --git a/bin/jsonpath b/bin/jsonpath index b7093fa..72aab9a 100755 --- a/bin/jsonpath +++ b/bin/jsonpath @@ -1,4 +1,5 @@ #!/usr/bin/env ruby +# frozen_string_literal: true require 'jsonpath' require 'multi_json' diff --git a/jsonpath.gemspec b/jsonpath.gemspec index 6345756..7a9d4ac 100644 --- a/jsonpath.gemspec +++ b/jsonpath.gemspec @@ -1,12 +1,14 @@ -# -*- encoding: utf-8 -*- +# frozen_string_literal: true 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= + if s.respond_to? :required_rubygems_version= + s.required_rubygems_version = + Gem::Requirement.new('>= 0') + end 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/.' @@ -25,10 +27,9 @@ Gem::Specification.new do |s| # dependencies s.add_runtime_dependency 'multi_json' s.add_runtime_dependency 'to_regexp', '~> 0.2.1' + s.add_development_dependency 'bundler' s.add_development_dependency 'code_stats' - s.add_development_dependency 'rake' s.add_development_dependency 'minitest', '~> 2.2.0' s.add_development_dependency 'phocus' - s.add_development_dependency 'bundler' + s.add_development_dependency 'rake' end - diff --git a/lib/jsonpath.rb b/lib/jsonpath.rb index eaeba18..e7bde58 100644 --- a/lib/jsonpath.rb +++ b/lib/jsonpath.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'strscan' require 'multi_json' require 'jsonpath/proxy' @@ -8,7 +10,7 @@ # JsonPath: initializes the class with a given JsonPath and parses that path # into a token array. class JsonPath - PATH_ALL = '$..*'.freeze + PATH_ALL = '$..*' attr_accessor :path @@ -73,7 +75,7 @@ def enum_on(obj_or_str, mode = nil) JsonPath::Enumerable.new(self, self.class.process_object(obj_or_str), mode, @opts) end - alias_method :[], :enum_on + alias [] enum_on def self.on(obj_or_str, path, opts = {}) new(path, opts).on(process_object(obj_or_str)) diff --git a/lib/jsonpath/enumerable.rb b/lib/jsonpath/enumerable.rb index feef2b9..8c37128 100644 --- a/lib/jsonpath/enumerable.rb +++ b/lib/jsonpath/enumerable.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class JsonPath class Enumerable include ::Enumerable @@ -32,7 +34,7 @@ def each(context = @object, key = nil, pos = 0, &blk) private - def handle_wildecard(node, expr, context, key, pos, &blk) + def handle_wildecard(node, expr, _context, _key, pos, &blk) expr[1, expr.size - 2].split(',').each do |sub_path| case sub_path[0] when '\'', '"' @@ -60,27 +62,32 @@ def handle_wildecard(node, expr, context, key, pos, &blk) 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) } + if @mode == :delete + (start_idx..end_idx).step(step) { |i| node[i] = nil } + node.compact! + else + (start_idx..end_idx).step(step) { |i| each(node, i, pos + 1, &blk) } + end end end end def handle_question_mark(sub_path, node, pos, &blk) 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 - end - when Hash + 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 - else + 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 diff --git a/lib/jsonpath/parser.rb b/lib/jsonpath/parser.rb index 63e8a54..496508a 100644 --- a/lib/jsonpath/parser.rb +++ b/lib/jsonpath/parser.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'strscan' require 'to_regexp' @@ -23,7 +25,7 @@ def parse(exp) end def parse_exp(exp) - exp = exp.sub(/@/, '').gsub(/^\(/, '').gsub(/\)$/, '').gsub(/"/, '\'').strip + exp = exp.sub(/@/, '').gsub(/^\(/, '').gsub(/\)$/, '').tr('"', '\'').strip scanner = StringScanner.new(exp) elements = [] until scanner.eos? @@ -39,25 +41,25 @@ def parse_exp(exp) operator = t elsif t = scanner.scan(/(\s+)?'?.*'?(\s+)?/) # If we encounter a node which does not contain `'` it means - # that we are dealing with a boolean type. - if t == "true" - operand = true - elsif t == "false" - operand = false - else - operand = operator.strip == "=~" ? t.to_regexp : t.delete("'").strip - end + #  that we are dealing with a boolean type. + operand = if t == 'true' + true + elsif t == 'false' + false + else + operator.strip == '=~' ? t.to_regexp : t.delete("'").strip + end elsif t = scanner.scan(/\/\w+\//) elsif t = scanner.scan(/.*/) raise "Could not process symbol: #{t}" end end - if elements.empty? - el = @_current_node - else - el = dig(elements, @_current_node) - end + el = if elements.empty? + @_current_node + else + dig(elements, @_current_node) + end return false if el.nil? return true if operator.nil? && el diff --git a/lib/jsonpath/proxy.rb b/lib/jsonpath/proxy.rb index 9d54703..8f25822 100644 --- a/lib/jsonpath/proxy.rb +++ b/lib/jsonpath/proxy.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class JsonPath class Proxy attr_reader :obj diff --git a/lib/jsonpath/version.rb b/lib/jsonpath/version.rb index 3c33b9f..ec747d0 100644 --- a/lib/jsonpath/version.rb +++ b/lib/jsonpath/version.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class JsonPath - VERSION = '0.9.2'.freeze + VERSION = '0.9.2' end diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index 227534a..505c7c2 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'minitest/autorun' require 'phocus' require 'jsonpath' @@ -250,17 +252,17 @@ def test_support_filter_by_object_childnode_value end def test_support_at_sign_in_member_names - assert_equal [@object['store']['@id']], JsonPath.new("$.store.@id").on(@object) + assert_equal [@object['store']['@id']], JsonPath.new('$.store.@id').on(@object) end def test_support_dollar_sign_in_member_names assert_equal [@object['store']['$meta-data']], - JsonPath.new("$.store.$meta-data").on(@object) + JsonPath.new('$.store.$meta-data').on(@object) end def test_support_underscore_in_member_names assert_equal [@object['store']['_links']], - JsonPath.new("$.store._links").on(@object) + JsonPath.new('$.store._links').on(@object) end def test_dig_return_string @@ -271,12 +273,12 @@ def test_dig_return_string def test_slash_in_value data = { 'data' => [{ - 'type' => 'mps/awesome', - },{ - 'type' => 'not', + 'type' => 'mps/awesome' + }, { + 'type' => 'not' }] } - assert_equal [{ 'type' => 'mps/awesome' }], JsonPath.new("$.data[?(@.type == \"mps/awesome\")]").on(data) + assert_equal [{ 'type' => 'mps/awesome' }], JsonPath.new('$.data[?(@.type == "mps/awesome")]').on(data) end def test_floating_point_with_precision_marker @@ -285,7 +287,7 @@ def test_floating_point_with_precision_marker 'type' => 0.00001 } } - assert_equal [{"type"=>0.00001}], JsonPath.new("$.data[?(@.type == 0.00001)]").on(data) + assert_equal [{ 'type' => 0.00001 }], JsonPath.new('$.data[?(@.type == 0.00001)]').on(data) end def test_digits_only_string @@ -295,7 +297,7 @@ def test_digits_only_string 'id' => '123' } } - assert_equal([{"type"=>"users", "id"=>"123"}], JsonPath.new("$.foo[?(@.id == '123')]").on(data)) + assert_equal([{ 'type' => 'users', 'id' => '123' }], JsonPath.new("$.foo[?(@.id == '123')]").on(data)) end def test_digits_only_string_in_array @@ -303,55 +305,55 @@ def test_digits_only_string_in_array 'foo' => [{ 'type' => 'users', 'id' => '123' - },{ + }, { 'type' => 'users', 'id' => '321' }] } - assert_equal([{"type"=>"users", "id"=>"123"}], JsonPath.new("$.foo[?(@.id == '123')]").on(data)) + assert_equal([{ 'type' => 'users', 'id' => '123' }], JsonPath.new("$.foo[?(@.id == '123')]").on(data)) end def test_at_in_filter jsonld = { - "mentions" => [ - { - "name" => "Delimara Powerplant", - "identifier" => "krzana://took/powerstation/Delimara Powerplant", - "@type" => "Place", - "geo" => { - "latitude" => 35.83020073454, - "longitude" => 14.55602645874 - } - } + 'mentions' => [ + { + 'name' => 'Delimara Powerplant', + 'identifier' => 'krzana://took/powerstation/Delimara Powerplant', + '@type' => 'Place', + 'geo' => { + 'latitude' => 35.83020073454, + 'longitude' => 14.55602645874 + } + } ] - } - assert_equal(['Place'], JsonPath.new("$..mentions[?(@['@type'] == 'Place')].@type").on(jsonld)) + } + assert_equal(['Place'], JsonPath.new("$..mentions[?(@['@type'] == 'Place')].@type").on(jsonld)) end def test_dollar_in_filter jsonld = { - "mentions" => [ - { - "name" => "Delimara Powerplant", - "identifier" => "krzana://took/powerstation/Delimara Powerplant", - "$type" => "Place", - "geo" => { - "latitude" => 35.83020073454, - "longitude" => 14.55602645874 - } - } + 'mentions' => [ + { + 'name' => 'Delimara Powerplant', + 'identifier' => 'krzana://took/powerstation/Delimara Powerplant', + '$type' => 'Place', + 'geo' => { + 'latitude' => 35.83020073454, + 'longitude' => 14.55602645874 + } + } ] - } - assert_equal(['Place'], JsonPath.new("$..mentions[?(@['$type'] == 'Place')].$type").on(jsonld)) + } + assert_equal(['Place'], JsonPath.new("$..mentions[?(@['$type'] == 'Place')].$type").on(jsonld)) end def test_underscore_in_filter jsonld = { - "attributes" => [ + 'attributes' => [ { - "store" => [ - { "with" => "urn" }, - { "with_underscore" => "urn:1" } + 'store' => [ + { 'with' => 'urn' }, + { 'with_underscore' => 'urn:1' } ] } ] @@ -361,18 +363,18 @@ def test_underscore_in_filter def test_at_in_value jsonld = { - "mentions" => + 'mentions' => { - "name" => "Delimara Powerplant", - "identifier" => "krzana://took/powerstation/Delimara Powerplant", - "type" => "@Place", - "geo" => { - "latitude" => 35.83020073454, - "longitude" => 14.55602645874 - } + 'name' => 'Delimara Powerplant', + 'identifier' => 'krzana://took/powerstation/Delimara Powerplant', + 'type' => '@Place', + 'geo' => { + 'latitude' => 35.83020073454, + 'longitude' => 14.55602645874 + } } - } - assert_equal(['@Place'], JsonPath.new("$..mentions.type[?(@ == '@Place')]").on(jsonld)) + } + assert_equal(['@Place'], JsonPath.new("$..mentions.type[?(@ == '@Place')]").on(jsonld)) end def test_parens_in_value @@ -381,21 +383,20 @@ def test_parens_in_value 'number' => '(492) 080-3961' } } - assert_equal [{'number'=>'(492) 080-3961'}], JsonPath.new("$.data[?(@.number == '(492) 080-3961')]").on(data) + assert_equal [{ 'number' => '(492) 080-3961' }], JsonPath.new("$.data[?(@.number == '(492) 080-3961')]").on(data) end - def test_boolean_parameter_value data = { 'data' => [{ 'isTrue' => true, - 'name' => 'testname1', - },{ + 'name' => 'testname1' + }, { 'isTrue' => false, - 'name' => 'testname2', - }] + 'name' => 'testname2' + }] } - assert_equal [{"isTrue"=>true, "name"=>"testname1"}], JsonPath.new("$.data[?(@.isTrue)]").on(data) + assert_equal [{ 'isTrue' => true, 'name' => 'testname1' }], JsonPath.new('$.data[?(@.isTrue)]').on(data) end def test_regex @@ -404,9 +405,9 @@ def test_regex @object['store']['book'][2], @object['store']['book'][4], @object['store']['book'][5], - @object['store']['book'][6], + @object['store']['book'][6] ], JsonPath.new('$..book[?(@.author =~ /herman|lukyanenko/i)]').on(@object) - assert_equal ["asdf", "asdf2"], JsonPath.new("$.store.book..tags[?(@ =~ /asdf/)]").on(@object) + assert_equal %w[asdf asdf2], JsonPath.new('$.store.book..tags[?(@ =~ /asdf/)]').on(@object) end def test_regression_1 @@ -444,7 +445,7 @@ def test_regression_2 ] }.to_json - assert_equal 'C09C5GYHF', JsonPath.on(json, "$..channels[?(@.is_archived == false)].id")[0] + assert_equal 'C09C5GYHF', JsonPath.on(json, '$..channels[?(@.is_archived == false)].id')[0] end def test_regression_3 @@ -464,7 +465,7 @@ def test_regression_3 ] }.to_json - assert_equal 'C09C598QL', JsonPath.on(json, "$..channels[?(@.is_archived)].id")[0] + assert_equal 'C09C598QL', JsonPath.on(json, '$..channels[?(@.is_archived)].id')[0] end def test_regression_4 @@ -509,26 +510,26 @@ def test_regression_5 def test_changed json = - { - "snapshot"=> { - "objects"=> { - "whatever"=> [ - { - "column"=> { - "name"=> "ASSOCIATE_FLAG", - "nullable"=> true - } - }, - { - "column"=> { - "name"=> "AUTHOR", - "nullable"=> false + { + 'snapshot' => { + 'objects' => { + 'whatever' => [ + { + 'column' => { + 'name' => 'ASSOCIATE_FLAG', + 'nullable' => true + } + }, + { + 'column' => { + 'name' => 'AUTHOR', + 'nullable' => false + } } - } - ] + ] + } } } - } assert_equal true, JsonPath.on(json, "$..column[?(@.name == 'ASSOCIATE_FLAG')].nullable")[0] end @@ -537,30 +538,75 @@ def test_another initial: true, not: true }.to_json - assert_equal [{"initial"=>true, "not"=>true}], JsonPath.on(json, "$.[?(@.initial == true)]") + assert_equal [{ 'initial' => true, 'not' => true }], JsonPath.on(json, '$.[?(@.initial == true)]') json = { initial: false, not: true }.to_json - assert_equal [], JsonPath.on(json, "$.initial[?(@)]") - assert_equal [], JsonPath.on(json, "$.[?(@.initial == true)]") - assert_equal [{"initial"=>false, "not"=>true}], JsonPath.on(json, "$.[?(@.initial == false)]") + assert_equal [], JsonPath.on(json, '$.initial[?(@)]') + assert_equal [], JsonPath.on(json, '$.[?(@.initial == true)]') + assert_equal [{ 'initial' => false, 'not' => true }], JsonPath.on(json, '$.[?(@.initial == false)]') json = { initial: 'false', not: true }.to_json - assert_equal [{"initial"=>"false", "not"=>true}], JsonPath.on(json, "$.[?(@.initial == 'false')]") - assert_equal [], JsonPath.on(json, "$.[?(@.initial == false)]") + assert_equal [{ 'initial' => 'false', 'not' => true }], JsonPath.on(json, "$.[?(@.initial == 'false')]") + assert_equal [], JsonPath.on(json, '$.[?(@.initial == false)]') end def test_hanging json = { initial: true }.to_json - success_path = "$.initial" + success_path = '$.initial' assert_equal [true], JsonPath.on(json, success_path) broken_path = "$.initial\n" assert_equal [true], JsonPath.on(json, broken_path) end + def test_delete_more_items + a = { 'itemList' => + [{ 'alfa' => 'beta1' }, + { 'alfa' => 'beta2' }, + { 'alfa' => 'beta3' }, + { 'alfa' => 'beta4' }, + { 'alfa' => 'beta5' }, + { 'alfa' => 'beta6' }, + { 'alfa' => 'beta7' }, + { 'alfa' => 'beta8' }, + { 'alfa' => 'beta9' }, + { 'alfa' => 'beta10' }, + { 'alfa' => 'beta11' }, + { 'alfa' => 'beta12' }] } + expected = { 'itemList' => [{ 'alfa' => 'beta1' }] } + assert_equal expected, JsonPath.for(a.to_json).delete('$.itemList[1:11:1]').to_hash + end + + def test_delete_more_items_with_stepping + a = { 'itemList' => + [{ 'alfa' => 'beta1' }, + { 'alfa' => 'beta2' }, + { 'alfa' => 'beta3' }, + { 'alfa' => 'beta4' }, + { 'alfa' => 'beta5' }, + { 'alfa' => 'beta6' }, + { 'alfa' => 'beta7' }, + { 'alfa' => 'beta8' }, + { 'alfa' => 'beta9' }, + { 'alfa' => 'beta10' }, + { 'alfa' => 'beta11' }, + { 'alfa' => 'beta12' }] } + expected = { 'itemList' => + [{ 'alfa' => 'beta1' }, + { 'alfa' => 'beta3' }, + { 'alfa' => 'beta5' }, + { 'alfa' => 'beta7' }, + { 'alfa' => 'beta8' }, + { 'alfa' => 'beta9' }, + { 'alfa' => 'beta10' }, + { 'alfa' => 'beta11' }, + { 'alfa' => 'beta12' }] } + assert_equal expected, JsonPath.for(a.to_json).delete('$.itemList[1:6:2]').to_hash + end + def example_object { 'store' => { 'book' => [ @@ -568,7 +614,7 @@ def example_object 'author' => 'Nigel Rees', 'title' => 'Sayings of the Century', 'price' => 9, - 'tags' => ['asdf', 'asdf2']}, + 'tags' => %w[asdf asdf2] }, { 'category' => 'fiction', 'author' => 'Evelyn Waugh', 'title' => 'Sword of Honour', @@ -610,9 +656,9 @@ def example_object '2seater' => 'yes', 'make:model' => 'Zippy Sweetwheeler' }, - "@id" => "http://example.org/store/42", - "$meta-data" => "whatevs", - "_links" => { "self" => {} } + '@id' => 'http://example.org/store/42', + '$meta-data' => 'whatevs', + '_links' => { 'self' => {} } } } end end diff --git a/test/test_jsonpath_bin.rb b/test/test_jsonpath_bin.rb index 68285c2..b812661 100644 --- a/test/test_jsonpath_bin.rb +++ b/test/test_jsonpath_bin.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'minitest/autorun' require 'phocus' require 'jsonpath' From 9ef512efd79b71da7fa8bcae87dad0d199bee803 Mon Sep 17 00:00:00 2001 From: Gergely Brautigam Date: Sun, 8 Jul 2018 10:05:47 +0200 Subject: [PATCH 045/120] Updated version. --- lib/jsonpath/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonpath/version.rb b/lib/jsonpath/version.rb index ec747d0..d04a2d4 100644 --- a/lib/jsonpath/version.rb +++ b/lib/jsonpath/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true class JsonPath - VERSION = '0.9.2' + VERSION = '0.9.3' end From e94b4524524e0c8d3a55b1e447d79c0d0c7152b6 Mon Sep 17 00:00:00 2001 From: Gergely Brautigam Date: Mon, 27 Aug 2018 21:31:05 +0200 Subject: [PATCH 046/120] Delete elements with path (#95) * Regex work * Cleanup. * Addressed rubocop stuff. --- lib/jsonpath.rb | 2 +- lib/jsonpath/enumerable.rb | 4 +- lib/jsonpath/proxy.rb | 10 +++++ lib/jsonpath/version.rb | 2 +- test/test_jsonpath.rb | 83 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 97 insertions(+), 4 deletions(-) diff --git a/lib/jsonpath.rb b/lib/jsonpath.rb index e7bde58..82385e7 100644 --- a/lib/jsonpath.rb +++ b/lib/jsonpath.rb @@ -10,7 +10,7 @@ # 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 diff --git a/lib/jsonpath/enumerable.rb b/lib/jsonpath/enumerable.rb index 8c37128..7c2fd4d 100644 --- a/lib/jsonpath/enumerable.rb +++ b/lib/jsonpath/enumerable.rb @@ -77,8 +77,6 @@ def handle_question_mark(sub_path, node, pos, &blk) 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 @@ -104,6 +102,8 @@ def yield_value(blk, context, key) when :delete if key key.is_a?(Integer) ? context.delete_at(key) : context.delete(key) + else + context.replace({}) end when :substitute if key diff --git a/lib/jsonpath/proxy.rb b/lib/jsonpath/proxy.rb index 8f25822..f7a4804 100644 --- a/lib/jsonpath/proxy.rb +++ b/lib/jsonpath/proxy.rb @@ -46,9 +46,19 @@ def _gsub(obj, path, replacement) def _delete(obj, path) JsonPath.new(path)[obj, :delete].each + obj = _remove(obj) Proxy.new(obj) end + def _remove(obj) + obj.each do |o| + if o.is_a?(Hash) || o.is_a?(Array) + _remove(o) + o.delete({}) + end + end + end + def _compact(obj, path) JsonPath.new(path)[obj, :compact].each Proxy.new(obj) diff --git a/lib/jsonpath/version.rb b/lib/jsonpath/version.rb index d04a2d4..8a1012f 100644 --- a/lib/jsonpath/version.rb +++ b/lib/jsonpath/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true class JsonPath - VERSION = '0.9.3' + VERSION = '0.9.3'.freeze end diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index 505c7c2..a30d5dd 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -173,6 +173,89 @@ def test_delete assert_equal({ 'you' => nil }, h) end + def test_delete_2 + json = { 'store' => { + 'book' => [ + { 'category' => 'reference', + 'author' => 'Nigel Rees', + 'title' => 'Sayings of the Century', + 'price' => 9, + 'tags' => %w[asdf asdf2] }, + { 'category' => 'fiction', + 'author' => 'Evelyn Waugh', + 'title' => 'Sword of Honour', + 'price' => 13 }, + { 'category' => 'fiction', + 'author' => 'Aasdf', + 'title' => 'Aaasdf2', + 'price' => 1 } + ] + } } + json_deleted = { 'store' => { + 'book' => [ + { 'category' => 'fiction', + 'author' => 'Evelyn Waugh', + 'title' => 'Sword of Honour', + 'price' => 13 }, + { 'category' => 'fiction', + 'author' => 'Aasdf', + 'title' => 'Aaasdf2', + 'price' => 1 } + ] + } } + assert_equal(json_deleted, JsonPath.for(json).delete("$..store.book[?(@.category == 'reference')]").obj) + end + + def test_delete_3 + json = { 'store' => { + 'book' => [ + { 'category' => 'reference', + 'author' => 'Nigel Rees', + 'title' => 'Sayings of the Century', + 'price' => 9, + 'tags' => %w[asdf asdf2], + 'this' => { + 'delete_me' => [ + 'no' => 'do not' + ] + } + }, + { 'category' => 'fiction', + 'author' => 'Evelyn Waugh', + 'title' => 'Sword of Honour', + 'price' => 13 + }, + { 'category' => 'fiction', + 'author' => 'Aasdf', + 'title' => 'Aaasdf2', + 'price' => 1 + } + ] + } } + json_deleted = { 'store' => { + 'book' => [ + { 'category' => 'reference', + 'author' => 'Nigel Rees', + 'title' => 'Sayings of the Century', + 'price' => 9, + 'tags' => %w[asdf asdf2], + 'this' => {} + }, + { 'category' => 'fiction', + 'author' => 'Evelyn Waugh', + 'title' => 'Sword of Honour', + 'price' => 13 + }, + { 'category' => 'fiction', + 'author' => 'Aasdf', + 'title' => 'Aaasdf2', + 'price' => 1 + } + ] + } } + assert_equal(json_deleted, JsonPath.for(json).delete('$..store.book..delete_me').obj) + end + def test_delete_for_array before = JsonPath.on(@object, '$..store.book[1]') JsonPath.for(@object).delete!('$..store.book[0]') From 613b2fd7c90c0ca0c73c93d6bf87476a0a9ca3c3 Mon Sep 17 00:00:00 2001 From: Gergely Brautigam Date: Mon, 27 Aug 2018 21:33:18 +0200 Subject: [PATCH 047/120] Bumped version. --- lib/jsonpath/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonpath/version.rb b/lib/jsonpath/version.rb index 8a1012f..70b1abb 100644 --- a/lib/jsonpath/version.rb +++ b/lib/jsonpath/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true class JsonPath - VERSION = '0.9.3'.freeze + VERSION = '0.9.4'.freeze end From dbd0f13fa94bf641094e43aa53cfaa1ab6406508 Mon Sep 17 00:00:00 2001 From: Gergely Brautigam Date: Mon, 15 Oct 2018 11:32:46 +0200 Subject: [PATCH 048/120] Fix quote in value. Closes #96 (#97) --- lib/jsonpath/parser.rb | 2 +- test/test_jsonpath.rb | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/jsonpath/parser.rb b/lib/jsonpath/parser.rb index 496508a..13e2453 100644 --- a/lib/jsonpath/parser.rb +++ b/lib/jsonpath/parser.rb @@ -47,7 +47,7 @@ def parse_exp(exp) elsif t == 'false' false else - operator.strip == '=~' ? t.to_regexp : t.delete("'").strip + operator.strip == '=~' ? t.to_regexp : t.gsub(%r{^'|'$}, '').strip end elsif t = scanner.scan(/\/\w+\//) elsif t = scanner.scan(/.*/) diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index a30d5dd..e16417c 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -591,6 +591,18 @@ def test_regression_5 assert_equal 'C09C5GYHF', JsonPath.on(json, "$..channels[?(@.is_archived == 'false')].id")[0] end + def test_quote + json = { + channels: [ + { + name: "King's Speech", + } + ] + }.to_json + + assert_equal [{"name" => "King\'s Speech"}], JsonPath.on(json, "$..channels[?(@.name == 'King\'s Speech')]") + end + def test_changed json = { From 2064e09c606e51e581d5138677e5d4438434b456 Mon Sep 17 00:00:00 2001 From: Gergely Brautigam Date: Mon, 15 Oct 2018 11:34:33 +0200 Subject: [PATCH 049/120] Bumped version. --- lib/jsonpath/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonpath/version.rb b/lib/jsonpath/version.rb index 70b1abb..b91893c 100644 --- a/lib/jsonpath/version.rb +++ b/lib/jsonpath/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true class JsonPath - VERSION = '0.9.4'.freeze + VERSION = '0.9.5'.freeze end From d9522faa6597216d56e17c549a4c59e0d7d73db1 Mon Sep 17 00:00:00 2001 From: Gergely Brautigam Date: Tue, 30 Oct 2018 10:12:19 +0100 Subject: [PATCH 050/120] Fixing curly brackets not allowed in root element name. (#99) --- lib/jsonpath.rb | 4 +++- test/test_jsonpath.rb | 7 +++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/jsonpath.rb b/lib/jsonpath.rb index 82385e7..52cff64 100644 --- a/lib/jsonpath.rb +++ b/lib/jsonpath.rb @@ -21,7 +21,7 @@ def initialize(path, opts = {}) until scanner.eos? if token = scanner.scan(/\$\B|@\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}]" @@ -33,6 +33,8 @@ def initialize(path, opts = {}) nil elsif token = scanner.scan(/[><=] \d+/) @path.last << token + # TODO: If there are characters that it can't match in the previous legs, this will throw + # a RuntimeError: can't modify frozen String error. elsif token = scanner.scan(/./) @path.last << token end diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index e16417c..7b92e34 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -603,6 +603,13 @@ def test_quote assert_equal [{"name" => "King\'s Speech"}], JsonPath.on(json, "$..channels[?(@.name == 'King\'s Speech')]") end + def test_curly_brackets + data = { + '{data}' => 'data' + } + assert_equal ['data'], JsonPath.new("$.{data}").on(data) + end + def test_changed json = { From 19cdda1c930acbdb11a2246f92c9a3e707a44446 Mon Sep 17 00:00:00 2001 From: Gergely Brautigam Date: Tue, 30 Oct 2018 10:13:31 +0100 Subject: [PATCH 051/120] Bumped version to release fix. --- lib/jsonpath/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonpath/version.rb b/lib/jsonpath/version.rb index b91893c..f3490ce 100644 --- a/lib/jsonpath/version.rb +++ b/lib/jsonpath/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true class JsonPath - VERSION = '0.9.5'.freeze + VERSION = '0.9.6'.freeze end From 7172d65348177e208de7b5dc8680a5825cfaa022 Mon Sep 17 00:00:00 2001 From: Gergely Brautigam Date: Mon, 3 Dec 2018 13:33:05 +0100 Subject: [PATCH 052/120] Allow for symbolizing result array by passing on value for on. (#101) --- lib/jsonpath.rb | 10 ++++++++-- test/test_jsonpath.rb | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/lib/jsonpath.rb b/lib/jsonpath.rb index 52cff64..e9ee312 100644 --- a/lib/jsonpath.rb +++ b/lib/jsonpath.rb @@ -65,8 +65,14 @@ def join(join_path) res end - def on(obj_or_str) - enum_on(obj_or_str).to_a + def on(obj_or_str, opts = {}) + a = enum_on(obj_or_str).to_a + if opts[:symbolize_keys] + a.map! do |e| + e.inject({}) { |memo, (k, v)| memo[k.to_sym] = v; memo } + end + end + a end def first(obj_or_str, *args) diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index 7b92e34..827d017 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -610,6 +610,49 @@ def test_curly_brackets assert_equal ['data'], JsonPath.new("$.{data}").on(data) end + def test_symbolize + data = ' + { + "store": { + "bicycle": { + "price": 19.95, + "color": "red" + }, + "book": [ + { + "price": 8.95, + "category": "reference", + "title": "Sayings of the Century", + "author": "Nigel Rees" + }, + { + "price": 12.99, + "category": "fiction", + "title": "Sword of Honour", + "author": "Evelyn Waugh" + }, + { + "price": 8.99, + "category": "fiction", + "isbn": "0-553-21311-3", + "title": "Moby Dick", + "author": "Herman Melville", + "color": "blue" + }, + { + "price": 22.99, + "category": "fiction", + "isbn": "0-395-19395-8", + "title": "The Lord of the Rings", + "author": "Tolkien" + } + ] + } + } + ' + assert_equal [{:price=>8.95, :category=>"reference", :title=>"Sayings of the Century", :author=>"Nigel Rees"}, {:price=>8.99, :category=>"fiction", :isbn=>"0-553-21311-3", :title=>"Moby Dick", :author=>"Herman Melville", :color=>"blue"}], JsonPath.new('$..book[::2]').on(data, {symbolize_keys: true}) + end + def test_changed json = { From 8ab6e6f56de06ad3bbefb39a118f265f55880a82 Mon Sep 17 00:00:00 2001 From: Gergely Brautigam Date: Mon, 3 Dec 2018 21:52:23 +0100 Subject: [PATCH 053/120] Bumped version to release fix. --- lib/jsonpath/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonpath/version.rb b/lib/jsonpath/version.rb index f3490ce..c23a8f2 100644 --- a/lib/jsonpath/version.rb +++ b/lib/jsonpath/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true class JsonPath - VERSION = '0.9.6'.freeze + VERSION = '0.9.7'.freeze end From 2e6fdcc64fb39eba81e48b0ec3844cabc6eae0b9 Mon Sep 17 00:00:00 2001 From: Khairi Hafsham Date: Thu, 6 Dec 2018 13:23:09 +0800 Subject: [PATCH 054/120] End index in array slicing should be exclusive (#103) * End index in array slicing should be exclusive This follows the examples given in https://goessner.net/articles/JsonPath/ where ":2" is equal to "[0,1]". Issue: #102 * Re-add previously changed tests In response to code review Also, a good way to show syntax changes before and after the exclusive end index implementation. Issue: #102 --- lib/jsonpath/enumerable.rb | 12 +++++++++++- test/test_jsonpath.rb | 9 +++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/lib/jsonpath/enumerable.rb b/lib/jsonpath/enumerable.rb index 7c2fd4d..78c9a6a 100644 --- a/lib/jsonpath/enumerable.rb +++ b/lib/jsonpath/enumerable.rb @@ -51,10 +51,14 @@ def handle_wildecard(node, expr, _context, _key, pos, &blk) if array_args[0] == '*' start_idx = 0 end_idx = node.size - 1 + elsif sub_path.count(':') == 0 + start_idx = end_idx = process_function_or_literal(array_args[0], 0) + next unless start_idx + next if start_idx >= node.size 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)) + end_idx = array_args[1] && ensure_exclusive_end_index(process_function_or_literal(array_args[1], -1)) || -1 next unless end_idx next if start_idx == end_idx && start_idx >= node.size end @@ -72,6 +76,12 @@ def handle_wildecard(node, expr, _context, _key, pos, &blk) end end + def ensure_exclusive_end_index(value) + return value unless value.is_a?(Integer) && value > 0 + + value - 1 + end + def handle_question_mark(sub_path, node, pos, &blk) case node when Array diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index 827d017..84f1d37 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -48,13 +48,18 @@ def test_retrieve_all_prices end def test_recognize_array_splices - assert_equal [@object['store']['book'][0], @object['store']['book'][1]], JsonPath.new('$..book[0:1:1]').on(@object) + assert_equal [@object['store']['book'][0]], JsonPath.new('$..book[0:1:1]').on(@object) + assert_equal [@object['store']['book'][0], @object['store']['book'][1]], JsonPath.new('$..book[0:2:1]').on(@object) assert_equal [@object['store']['book'][1], @object['store']['book'][3], @object['store']['book'][5]], JsonPath.new('$..book[1::2]').on(@object) assert_equal [@object['store']['book'][0], @object['store']['book'][2], @object['store']['book'][4], @object['store']['book'][6]], JsonPath.new('$..book[::2]').on(@object) assert_equal [@object['store']['book'][0], @object['store']['book'][2]], JsonPath.new('$..book[:-5:2]').on(@object) assert_equal [@object['store']['book'][5], @object['store']['book'][6]], JsonPath.new('$..book[5::]').on(@object) end + def test_slice_array_with_exclusive_end_correctly + assert_equal [@object['store']['book'][0], @object['store']['book'][1]], JsonPath.new('$..book[:2]').on(@object) + end + def test_recognize_array_comma assert_equal [@object['store']['book'][0], @object['store']['book'][1]], JsonPath.new('$..book[0,1]').on(@object) assert_equal [@object['store']['book'][2], @object['store']['book'][6]], JsonPath.new('$..book[2,-1::]').on(@object) @@ -722,7 +727,7 @@ def test_delete_more_items { 'alfa' => 'beta11' }, { 'alfa' => 'beta12' }] } expected = { 'itemList' => [{ 'alfa' => 'beta1' }] } - assert_equal expected, JsonPath.for(a.to_json).delete('$.itemList[1:11:1]').to_hash + assert_equal expected, JsonPath.for(a.to_json).delete('$.itemList[1:12:1]').to_hash end def test_delete_more_items_with_stepping From 897dc590e3529a3fe5983343bb3cd4fea6beecd4 Mon Sep 17 00:00:00 2001 From: Gergely Brautigam Date: Thu, 6 Dec 2018 06:31:42 +0100 Subject: [PATCH 055/120] Bumped version to release. --- lib/jsonpath/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonpath/version.rb b/lib/jsonpath/version.rb index c23a8f2..b87119e 100644 --- a/lib/jsonpath/version.rb +++ b/lib/jsonpath/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true class JsonPath - VERSION = '0.9.7'.freeze + VERSION = '0.9.8'.freeze end From 2b77fba5f3f5a16271dde7180381b4b735a8fec6 Mon Sep 17 00:00:00 2001 From: John Allen Date: Mon, 7 Jan 2019 04:06:11 -0500 Subject: [PATCH 056/120] Fix bug related to nested groupings (#104) * Fix bug related to nested groupings Prior to this commit if a nested grouping was encountered (ie. `((this && that) || the_other)` the gem would raise the following error: ``` NoMethodError: undefined method `strip' for nil:NilClass ``` It appears that in this scenario it's possible that the `operator` variable hasn't yet been set. The proposed fix is simply coercing the variable into a `String` before attempting to call the `#strip` function. * Added travis fix for old bundler version --- .travis.yml | 3 +++ lib/jsonpath/parser.rb | 2 +- test/test_jsonpath.rb | 5 +++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 6a13dbf..c8a3390 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,3 +4,6 @@ rvm: - 2.3.1 - ruby-head - jruby-head + +before_install: + - gem install bundler -v '< 2' diff --git a/lib/jsonpath/parser.rb b/lib/jsonpath/parser.rb index 13e2453..9bf0a07 100644 --- a/lib/jsonpath/parser.rb +++ b/lib/jsonpath/parser.rb @@ -47,7 +47,7 @@ def parse_exp(exp) elsif t == 'false' false else - operator.strip == '=~' ? t.to_regexp : t.gsub(%r{^'|'$}, '').strip + operator.to_s.strip == '=~' ? t.to_regexp : t.gsub(%r{^'|'$}, '').strip end elsif t = scanner.scan(/\/\w+\//) elsif t = scanner.scan(/.*/) diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index 84f1d37..a7e928a 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -84,6 +84,11 @@ def test_and_operator_with_more_results assert_equal [@object['store']['book'][1]], JsonPath.new("$..book[?(@['price'] < 23 && @['price'] > 9)]").on(@object) end + def test_nested_grouping + path = "$..book[?((@['price'] == 19 && @['author'] == 'Herman Melville') || @['price'] == 23)]" + assert_equal [@object['store']['book'][3]], JsonPath.new(path).on(@object) + end + def test_eval_with_floating_point_and_and assert_equal [@object['store']['book'][1]], JsonPath.new("$..book[?(@['price'] < 23.0 && @['price'] > 9.0)]").on(@object) end From fd06d86f3dffb4b747af0b89d712fcd11c9d8581 Mon Sep 17 00:00:00 2001 From: Gergely Brautigam Date: Mon, 7 Jan 2019 10:07:30 +0100 Subject: [PATCH 057/120] Bumped version for release. --- lib/jsonpath/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonpath/version.rb b/lib/jsonpath/version.rb index b87119e..f9139e5 100644 --- a/lib/jsonpath/version.rb +++ b/lib/jsonpath/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true class JsonPath - VERSION = '0.9.8'.freeze + VERSION = '0.9.9'.freeze end From f91f82fcfd88dee64cbb9abd37ba596e328d669e Mon Sep 17 00:00:00 2001 From: Gergely Brautigam Date: Sun, 20 Jan 2019 23:04:06 +0100 Subject: [PATCH 058/120] Fix proper handling of nested groups (#106) * WIP * Closes #105 * Not working yet. * One last test to fix. * Works! * Added some documentation around the logic --- lib/jsonpath/parser.rb | 121 ++++++++++++++++++++++++++++++++++++++--- test/test_jsonpath.rb | 11 ++++ 2 files changed, 124 insertions(+), 8 deletions(-) diff --git a/lib/jsonpath/parser.rb b/lib/jsonpath/parser.rb index 9bf0a07..cc56a69 100644 --- a/lib/jsonpath/parser.rb +++ b/lib/jsonpath/parser.rb @@ -8,22 +8,46 @@ class JsonPath class Parser def initialize(node) @_current_node = node + @_expr_map = {} end + # parse will parse an expression in the following way. + # Split the expression up into an array of legs for && and || operators. + # Parse this array into a map for which the keys are the parsed legs + # of the split. This map is then used to replace the expression with their + # corresponding boolean or numeric value. This might look something like this: + # ((false || false) && (false || true)) + # Once this string is assembled... we proceed to evaluate from left to right. + # The above string is broken down like this: + # (false && (false || true)) + # (false && true) + # false def parse(exp) exps = exp.split(/(&&)|(\|\|)/) - ret = parse_exp(exps.shift) + construct_expression_map(exps) + @_expr_map.each {|k, v| exp.sub!(k, "#{v}")} + raise ArgumentError, "unmatched parenthesis in expression: #{exp}" unless check_parenthesis_count(exp) + while (exp.include?("(")) + exp = parse_parentheses(exp) + end + bool_or_exp(exp) + end + + # Construct a map for which the keys are the expressions + # and the values are the corresponding parsed results. + # Exp.: + # {"(@['author'] =~ /herman|lukyanenko/i)"=>0} + # {"@['isTrue']"=>true} + def construct_expression_map(exps) exps.each_with_index do |item, index| - case item - when '&&' - ret &&= parse_exp(exps[index + 1]) - when '||' - ret ||= parse_exp(exps[index + 1]) - end + next if item == '&&' || item == '||' + item = item.strip.gsub(/\)*$/, '').gsub(/^\(*/, '') + @_expr_map[item] = parse_exp(item) end - ret end + # using a scanner break down the individual expressions and determine if + # there is a match in the JSON for it or not. def parse_exp(exp) exp = exp.sub(/@/, '').gsub(/^\(/, '').gsub(/\)$/, '').tr('"', '\'').strip scanner = StringScanner.new(exp) @@ -79,5 +103,86 @@ def dig(keys, hash) prev = keys.shift dig(keys, hash.fetch(prev)) end + + # This will break down a parenthesis from the left to the right + # and replace the given expression with it's returned value. + # It does this in order to make it easy to eliminate groups + # one-by-one. + def parse_parentheses(str) + opening_index = 0 + closing_index = 0 + + (0..str.length-1).step(1) do |i| + if str[i] == '(' + opening_index = i + end + if str[i] == ')' + closing_index = i + break + end + end + + to_parse = str[opening_index+1..closing_index-1] + + # handle cases like (true && true || false && true) in + # one giant parenthesis. + top = to_parse.split(/(&&)|(\|\|)/) + top = top.map{|t| t.strip} + res = bool_or_exp(top.shift) + top.each_with_index do |item, index| + case item + when '&&' + res &&= top[index + 1] + when '||' + res ||= top[index + 1] + end + end + + # if we are at the last item, the opening index will be 0 + # and the closing index will be the last index. To avoid + # off-by-one errors we simply return the result at that point. + if closing_index+1 >= str.length && opening_index == 0 + return "#{res}" + else + return "#{str[0..opening_index-1]}#{res}#{str[closing_index+1..str.length]}" + end + end + + # This is convoluted and I should probably refactor it somehow. + # The map that is created will contain strings since essentially I'm + # constructing a string like `true || true && false`. + # With eval the need for this would disappear but never the less, here + # it is. The fact is that the results can be either boolean, or a number + # in case there is only indexing happening like give me the 3rd item... or + # it also can be nil in case of regexes or things that aren't found. + # Hence, I have to be clever here to see what kind of variable I need to + # provide back. + def bool_or_exp(b) + if "#{b}" == 'true' + return true + elsif "#{b}" == 'false' + return false + elsif "#{b}" == "" + return nil + end + b = Float(b) rescue b + b + end + + # this simply makes sure that we aren't getting into the whole + # parenthesis parsing business without knowing that every parenthesis + # has its pair. + def check_parenthesis_count(exp) + return true unless exp.include?("(") + depth = 0 + exp.chars.each do |c| + if c == '(' + depth += 1 + elsif c == ')' + depth -= 1 + end + end + depth == 0 + end end end diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index a7e928a..41d5bc3 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -717,6 +717,17 @@ def test_hanging assert_equal [true], JsonPath.on(json, broken_path) end + def test_complex_nested_grouping + path = "$..book[?((@['author'] == 'Evelyn Waugh' || @['author'] == 'Herman Melville') && (@['price'] == 33 || @['price'] == 9))]" + assert_equal [@object['store']['book'][2]], JsonPath.new(path).on(@object) + end + + def test_complex_nested_grouping_unmatched_parent + path = "$..book[?((@['author'] == 'Evelyn Waugh' || @['author'] == 'Herman Melville' && (@['price'] == 33 || @['price'] == 9))]" + err = assert_raises(ArgumentError, "should have raised an exception") { JsonPath.new(path).on(@object)} + assert_match(/unmatched parenthesis in expression: \(\(false \|\| false && \(false \|\| true\)\)/, err.message) + end + def test_delete_more_items a = { 'itemList' => [{ 'alfa' => 'beta1' }, From 8dbe3412779243e3ff2f3ead035dac8409ac7909 Mon Sep 17 00:00:00 2001 From: Gergely Brautigam Date: Sun, 20 Jan 2019 23:04:44 +0100 Subject: [PATCH 059/120] Bumped version to release. --- lib/jsonpath/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonpath/version.rb b/lib/jsonpath/version.rb index f9139e5..4c2b790 100644 --- a/lib/jsonpath/version.rb +++ b/lib/jsonpath/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true class JsonPath - VERSION = '0.9.9'.freeze + VERSION = '1.0.0'.freeze end From b2525b8e8c596ddf1c8b40982529300b5a98132b Mon Sep 17 00:00:00 2001 From: Gergely Brautigam Date: Fri, 25 Jan 2019 09:41:22 +0100 Subject: [PATCH 060/120] Added a user friendly error message (#107) * First failing test. Now to fix it. * Added meaningfull error message. * Catching runtime instead of FrozenError. * Skipping if ruby version is below 2.2.0 * Rubocop * increasing version\ --- .rubocop_todo.yml | 51 +++++++++++++++++++----------- lib/jsonpath.rb | 10 +++--- lib/jsonpath/enumerable.rb | 4 +++ lib/jsonpath/parser.rb | 65 +++++++++++++++++++------------------- lib/jsonpath/version.rb | 2 +- test/test_jsonpath.rb | 40 +++++++++++++---------- 6 files changed, 100 insertions(+), 72 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index cc1ba9b..ea5082d 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2018-07-07 21:29:45 +0200 using RuboCop version 0.57.2. +# on 2019-01-25 09:23:04 +0100 using RuboCop version 0.63.1. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new @@ -18,31 +18,32 @@ Lint/IneffectiveAccessModifier: Exclude: - 'lib/jsonpath.rb' -# Offense count: 16 +# Offense count: 17 Metrics/AbcSize: - Max: 54 + Max: 60 # Offense count: 2 # Configuration parameters: CountComments, ExcludedMethods. +# ExcludedMethods: refine Metrics/BlockLength: - Max: 33 + Max: 37 # Offense count: 1 # Configuration parameters: CountBlocks. Metrics/BlockNesting: Max: 4 -# Offense count: 2 +# Offense count: 3 # Configuration parameters: CountComments. Metrics/ClassLength: - Max: 578 + Max: 739 -# Offense count: 6 +# Offense count: 7 Metrics/CyclomaticComplexity: - Max: 18 + Max: 20 -# Offense count: 24 -# Configuration parameters: CountComments. +# Offense count: 26 +# Configuration parameters: CountComments, ExcludedMethods. Metrics/MethodLength: Max: 52 @@ -53,9 +54,16 @@ Metrics/ParameterLists: # Offense count: 6 Metrics/PerceivedComplexity: - Max: 20 + Max: 21 # Offense count: 1 +# Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames. +# AllowedNames: io, id, to, by, on, in, at, ip, db +Naming/UncommunicativeMethodParamName: + Exclude: + - 'lib/jsonpath/parser.rb' + +# Offense count: 15 # Configuration parameters: AllowedChars. Style/AsciiComments: Exclude: @@ -69,11 +77,12 @@ Style/Documentation: - 'lib/jsonpath/enumerable.rb' - 'lib/jsonpath/proxy.rb' -# Offense count: 1 +# Offense count: 3 # Configuration parameters: MinBodyLength. Style/GuardClause: Exclude: - 'lib/jsonpath/enumerable.rb' + - 'lib/jsonpath/parser.rb' # Offense count: 2 # Cop supports --auto-correct. @@ -81,16 +90,22 @@ Style/IfUnlessModifier: Exclude: - 'lib/jsonpath/enumerable.rb' -# Offense count: 2 +# Offense count: 1 +Style/MultipleComparison: + Exclude: + - 'lib/jsonpath/parser.rb' + +# Offense count: 3 # Cop supports --auto-correct. -# Configuration parameters: AutoCorrect, EnforcedStyle. +# Configuration parameters: AutoCorrect, EnforcedStyle, IgnoredMethods. # SupportedStyles: predicate, comparison Style/NumericPredicate: Exclude: - 'spec/**/*' - 'lib/jsonpath/enumerable.rb' + - 'lib/jsonpath/parser.rb' -# Offense count: 2 +# Offense count: 3 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, AllowInnerSlashes. # SupportedStyles: slashes, percent_r, mixed @@ -98,15 +113,15 @@ Style/RegexpLiteral: Exclude: - 'lib/jsonpath/parser.rb' -# Offense count: 3 +# Offense count: 4 # Cop supports --auto-correct. Style/RescueModifier: Exclude: - 'lib/jsonpath/enumerable.rb' - 'lib/jsonpath/parser.rb' -# Offense count: 71 +# Offense count: 89 # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. # URISchemes: http, https Metrics/LineLength: - Max: 175 + Max: 296 diff --git a/lib/jsonpath.rb b/lib/jsonpath.rb index e9ee312..5fc468f 100644 --- a/lib/jsonpath.rb +++ b/lib/jsonpath.rb @@ -33,10 +33,12 @@ def initialize(path, opts = {}) nil elsif token = scanner.scan(/[><=] \d+/) @path.last << token - # TODO: If there are characters that it can't match in the previous legs, this will throw - # a RuntimeError: can't modify frozen String error. elsif token = scanner.scan(/./) - @path.last << token + begin + @path.last << token + rescue RuntimeError + raise ArgumentError, "character '#{token}' not supported in query" + end end end end @@ -69,7 +71,7 @@ def on(obj_or_str, opts = {}) a = enum_on(obj_or_str).to_a if opts[:symbolize_keys] a.map! do |e| - e.inject({}) { |memo, (k, v)| memo[k.to_sym] = v; memo } + e.each_with_object({}) { |(k, v), memo| memo[k.to_sym] = v; } end end a diff --git a/lib/jsonpath/enumerable.rb b/lib/jsonpath/enumerable.rb index 78c9a6a..0aa486d 100644 --- a/lib/jsonpath/enumerable.rb +++ b/lib/jsonpath/enumerable.rb @@ -15,6 +15,7 @@ def each(context = @object, key = nil, pos = 0, &blk) node = key ? context[key] : context @_current_node = node return yield_value(blk, context, key) if pos == @path.size + case expr = @path[pos] when '*', '..', '@' each(context, key, pos + 1, &blk) @@ -47,6 +48,7 @@ def handle_wildecard(node, expr, _context, _key, pos, &blk) handle_question_mark(sub_path, node, pos, &blk) else next if node.is_a?(Array) && node.empty? + array_args = sub_path.split(':') if array_args[0] == '*' start_idx = 0 @@ -58,6 +60,7 @@ def handle_wildecard(node, expr, _context, _key, pos, &blk) else start_idx = process_function_or_literal(array_args[0], 0) next unless start_idx + end_idx = array_args[1] && ensure_exclusive_end_index(process_function_or_literal(array_args[1], -1)) || -1 next unless end_idx next if start_idx == end_idx && start_idx >= node.size @@ -66,6 +69,7 @@ def handle_wildecard(node, expr, _context, _key, pos, &blk) end_idx %= node.size step = process_function_or_literal(array_args[2], 1) next unless step + if @mode == :delete (start_idx..end_idx).step(step) { |i| node[i] = nil } node.compact! diff --git a/lib/jsonpath/parser.rb b/lib/jsonpath/parser.rb index cc56a69..ed065e9 100644 --- a/lib/jsonpath/parser.rb +++ b/lib/jsonpath/parser.rb @@ -14,39 +14,39 @@ def initialize(node) # parse will parse an expression in the following way. # Split the expression up into an array of legs for && and || operators. # Parse this array into a map for which the keys are the parsed legs - # of the split. This map is then used to replace the expression with their + #  of the split. This map is then used to replace the expression with their # corresponding boolean or numeric value. This might look something like this: # ((false || false) && (false || true)) - # Once this string is assembled... we proceed to evaluate from left to right. - # The above string is broken down like this: + #  Once this string is assembled... we proceed to evaluate from left to right. + #  The above string is broken down like this: # (false && (false || true)) # (false && true) - # false + #  false def parse(exp) exps = exp.split(/(&&)|(\|\|)/) construct_expression_map(exps) - @_expr_map.each {|k, v| exp.sub!(k, "#{v}")} + @_expr_map.each { |k, v| exp.sub!(k, v.to_s) } raise ArgumentError, "unmatched parenthesis in expression: #{exp}" unless check_parenthesis_count(exp) - while (exp.include?("(")) - exp = parse_parentheses(exp) - end + + exp = parse_parentheses(exp) while exp.include?('(') bool_or_exp(exp) end # Construct a map for which the keys are the expressions - # and the values are the corresponding parsed results. + #  and the values are the corresponding parsed results. # Exp.: # {"(@['author'] =~ /herman|lukyanenko/i)"=>0} # {"@['isTrue']"=>true} def construct_expression_map(exps) - exps.each_with_index do |item, index| + exps.each_with_index do |item, _index| next if item == '&&' || item == '||' + item = item.strip.gsub(/\)*$/, '').gsub(/^\(*/, '') @_expr_map[item] = parse_exp(item) end end - # using a scanner break down the individual expressions and determine if + #  using a scanner break down the individual expressions and determine if # there is a match in the JSON for it or not. def parse_exp(exp) exp = exp.sub(/@/, '').gsub(/^\(/, '').gsub(/\)$/, '').tr('"', '\'').strip @@ -100,34 +100,33 @@ def dig(keys, hash) return nil unless hash.is_a? 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 - # This will break down a parenthesis from the left to the right - # and replace the given expression with it's returned value. + #  This will break down a parenthesis from the left to the right + #  and replace the given expression with it's returned value. # It does this in order to make it easy to eliminate groups # one-by-one. def parse_parentheses(str) opening_index = 0 closing_index = 0 - (0..str.length-1).step(1) do |i| - if str[i] == '(' - opening_index = i - end + (0..str.length - 1).step(1) do |i| + opening_index = i if str[i] == '(' if str[i] == ')' closing_index = i break end end - to_parse = str[opening_index+1..closing_index-1] + to_parse = str[opening_index + 1..closing_index - 1] - # handle cases like (true && true || false && true) in + #  handle cases like (true && true || false && true) in # one giant parenthesis. top = to_parse.split(/(&&)|(\|\|)/) - top = top.map{|t| t.strip} + top = top.map(&:strip) res = bool_or_exp(top.shift) top.each_with_index do |item, index| case item @@ -138,42 +137,44 @@ def parse_parentheses(str) end end - # if we are at the last item, the opening index will be 0 + #  if we are at the last item, the opening index will be 0 # and the closing index will be the last index. To avoid # off-by-one errors we simply return the result at that point. - if closing_index+1 >= str.length && opening_index == 0 - return "#{res}" + if closing_index + 1 >= str.length && opening_index == 0 + return res.to_s else - return "#{str[0..opening_index-1]}#{res}#{str[closing_index+1..str.length]}" + return "#{str[0..opening_index - 1]}#{res}#{str[closing_index + 1..str.length]}" end end - # This is convoluted and I should probably refactor it somehow. - # The map that is created will contain strings since essentially I'm + #  This is convoluted and I should probably refactor it somehow. + #  The map that is created will contain strings since essentially I'm # constructing a string like `true || true && false`. # With eval the need for this would disappear but never the less, here - # it is. The fact is that the results can be either boolean, or a number + #  it is. The fact is that the results can be either boolean, or a number # in case there is only indexing happening like give me the 3rd item... or # it also can be nil in case of regexes or things that aren't found. # Hence, I have to be clever here to see what kind of variable I need to # provide back. def bool_or_exp(b) - if "#{b}" == 'true' + if b.to_s == 'true' return true - elsif "#{b}" == 'false' + elsif b.to_s == 'false' return false - elsif "#{b}" == "" + elsif b.to_s == '' return nil end + b = Float(b) rescue b b end # this simply makes sure that we aren't getting into the whole - # parenthesis parsing business without knowing that every parenthesis + #  parenthesis parsing business without knowing that every parenthesis # has its pair. def check_parenthesis_count(exp) - return true unless exp.include?("(") + return true unless exp.include?('(') + depth = 0 exp.chars.each do |c| if c == '(' diff --git a/lib/jsonpath/version.rb b/lib/jsonpath/version.rb index 4c2b790..a85073d 100644 --- a/lib/jsonpath/version.rb +++ b/lib/jsonpath/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true class JsonPath - VERSION = '1.0.0'.freeze + VERSION = '1.0.1'.freeze end diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index 41d5bc3..781c0a5 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -228,18 +228,15 @@ def test_delete_3 'delete_me' => [ 'no' => 'do not' ] - } - }, + } }, { 'category' => 'fiction', 'author' => 'Evelyn Waugh', 'title' => 'Sword of Honour', - 'price' => 13 - }, + 'price' => 13 }, { 'category' => 'fiction', 'author' => 'Aasdf', 'title' => 'Aaasdf2', - 'price' => 1 - } + 'price' => 1 } ] } } json_deleted = { 'store' => { @@ -249,18 +246,15 @@ def test_delete_3 'title' => 'Sayings of the Century', 'price' => 9, 'tags' => %w[asdf asdf2], - 'this' => {} - }, + 'this' => {} }, { 'category' => 'fiction', 'author' => 'Evelyn Waugh', 'title' => 'Sword of Honour', - 'price' => 13 - }, + 'price' => 13 }, { 'category' => 'fiction', 'author' => 'Aasdf', 'title' => 'Aaasdf2', - 'price' => 1 - } + 'price' => 1 } ] } } assert_equal(json_deleted, JsonPath.for(json).delete('$..store.book..delete_me').obj) @@ -605,19 +599,19 @@ def test_quote json = { channels: [ { - name: "King's Speech", + name: "King's Speech" } ] }.to_json - assert_equal [{"name" => "King\'s Speech"}], JsonPath.on(json, "$..channels[?(@.name == 'King\'s Speech')]") + assert_equal [{ 'name' => "King\'s Speech" }], JsonPath.on(json, "$..channels[?(@.name == 'King\'s Speech')]") end def test_curly_brackets data = { '{data}' => 'data' } - assert_equal ['data'], JsonPath.new("$.{data}").on(data) + assert_equal ['data'], JsonPath.new('$.{data}').on(data) end def test_symbolize @@ -660,7 +654,7 @@ def test_symbolize } } ' - assert_equal [{:price=>8.95, :category=>"reference", :title=>"Sayings of the Century", :author=>"Nigel Rees"}, {:price=>8.99, :category=>"fiction", :isbn=>"0-553-21311-3", :title=>"Moby Dick", :author=>"Herman Melville", :color=>"blue"}], JsonPath.new('$..book[::2]').on(data, {symbolize_keys: true}) + assert_equal [{ price: 8.95, category: 'reference', title: 'Sayings of the Century', author: 'Nigel Rees' }, { price: 8.99, category: 'fiction', isbn: '0-553-21311-3', title: 'Moby Dick', author: 'Herman Melville', color: 'blue' }], JsonPath.new('$..book[::2]').on(data, symbolize_keys: true) end def test_changed @@ -724,10 +718,22 @@ def test_complex_nested_grouping def test_complex_nested_grouping_unmatched_parent path = "$..book[?((@['author'] == 'Evelyn Waugh' || @['author'] == 'Herman Melville' && (@['price'] == 33 || @['price'] == 9))]" - err = assert_raises(ArgumentError, "should have raised an exception") { JsonPath.new(path).on(@object)} + err = assert_raises(ArgumentError, 'should have raised an exception') { JsonPath.new(path).on(@object) } assert_match(/unmatched parenthesis in expression: \(\(false \|\| false && \(false \|\| true\)\)/, err.message) end + def test_runtime_error_frozen_string + skip('in ruby version below 2.2.0 this error is not raised') if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.2.0') + json = ' + { + "test": "something" + } + '.to_json + assert_raises(ArgumentError, "RuntimeError: character '|' not supported in query") do + JsonPath.on(json, '$.description|title') + end + end + def test_delete_more_items a = { 'itemList' => [{ 'alfa' => 'beta1' }, From 1b6bf61aa722ec2ef8d8095f49792dd5ba11d9b5 Mon Sep 17 00:00:00 2001 From: Markus Doits Date: Tue, 14 May 2019 10:51:40 +0200 Subject: [PATCH 061/120] add support for `!=` filter (#109) * add support for `!=` filter * update travis ruby versions to current ones drops not supported ruby 2.1 --- .travis.yml | 6 ++++-- lib/jsonpath/parser.rb | 2 +- test/test_jsonpath.rb | 6 ++++++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index c8a3390..b9e1508 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,9 @@ language: ruby rvm: - - 2.1.6 - - 2.3.1 + - 2.3.8 + - 2.4.6 + - 2.5.5 + - 2.6.3 - ruby-head - jruby-head diff --git a/lib/jsonpath/parser.rb b/lib/jsonpath/parser.rb index ed065e9..d280eb1 100644 --- a/lib/jsonpath/parser.rb +++ b/lib/jsonpath/parser.rb @@ -61,7 +61,7 @@ def parse_exp(exp) end if t = scanner.scan(/\['[a-zA-Z@&\*\/\$%\^\?_]+'\]+/) elements << t.gsub(/\[|\]|'|\s+/, '') - elsif t = scanner.scan(/(\s+)?[<>=][=~]?(\s+)?/) + elsif t = scanner.scan(/(\s+)?[<>=!][=~]?(\s+)?/) operator = t elsif t = scanner.scan(/(\s+)?'?.*'?(\s+)?/) # If we encounter a node which does not contain `'` it means diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index 781c0a5..d0d2634 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -70,6 +70,12 @@ def test_recognize_filters assert_equal [@object['store']['book'][0], @object['store']['book'][2]], JsonPath.new("$..book[?(@['price'] < 10)]").on(@object) assert_equal [@object['store']['book'][0], @object['store']['book'][2]], JsonPath.new("$..book[?(@['price'] == 9)]").on(@object) assert_equal [@object['store']['book'][3]], JsonPath.new("$..book[?(@['price'] > 20)]").on(@object) + assert_equal [ + @object['store']['book'][0], + @object['store']['book'][4], + @object['store']['book'][5], + @object['store']['book'][6] + ], JsonPath.new("$..book[?(@['category'] != 'fiction')]").on(@object) end def test_or_operator From f8d8c8c503927b2fcc7e7a3fe48c05f4e85920ff Mon Sep 17 00:00:00 2001 From: Markus Doits Date: Tue, 14 May 2019 11:03:06 +0200 Subject: [PATCH 062/120] Support dot notation in filters (#111) * support dot notation in filters * remove custom dig method --- lib/jsonpath/parser.rb | 29 +++++++++-------------------- test/test_jsonpath.rb | 4 ++++ 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/lib/jsonpath/parser.rb b/lib/jsonpath/parser.rb index d280eb1..b789c28 100644 --- a/lib/jsonpath/parser.rb +++ b/lib/jsonpath/parser.rb @@ -53,15 +53,9 @@ def parse_exp(exp) 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(/\['[a-zA-Z@&\*\/\$%\^\?_]+'\]+/) - elements << t.gsub(/\[|\]|'|\s+/, '') - elsif t = scanner.scan(/(\s+)?[<>=!][=~]?(\s+)?/) + if t = scanner.scan(/\['[a-zA-Z@&\*\/\$%\^\?_]+'\]|\.[a-zA-Z0-9_]+[?!]?/) + elements << t.gsub(/[\[\]'\.]|\s+/, '') + elsif t = scanner.scan(/(\s+)?[<>=!\-+][=~]?(\s+)?/) operator = t elsif t = scanner.scan(/(\s+)?'?.*'?(\s+)?/) # If we encounter a node which does not contain `'` it means @@ -81,9 +75,14 @@ def parse_exp(exp) el = if elements.empty? @_current_node + elsif @_current_node.is_a?(Hash) + @_current_node.dig(*elements) else - dig(elements, @_current_node) + elements.inject(@_current_node) do |agg, key| + agg.__send__(key) + end end + return false if el.nil? return true if operator.nil? && el @@ -95,16 +94,6 @@ def parse_exp(exp) private - # @TODO: Remove this once JsonPath no longer supports ruby versions below 2.3 - def dig(keys, hash) - return nil unless hash.is_a? 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 - #  This will break down a parenthesis from the left to the right #  and replace the given expression with it's returned value. # It does this in order to make it easy to eliminate groups diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index d0d2634..f10fb90 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -119,6 +119,10 @@ def test_paths_with_numbers assert_equal [@object['store']['bicycle']['2seater']], JsonPath.new('$.store.bicycle.2seater').on(@object) end + def test_recognized_dot_notation_in_filters + assert_equal [@object['store']['book'][2], @object['store']['book'][3]], JsonPath.new('$..book[?(@.isbn)]').on(@object) + end + def test_recognize_array_with_evald_index assert_equal [@object['store']['book'][2]], JsonPath.new('$..book[(@.length-5)]').on(@object) end From 3e1861a69c5df612a72122c4efcad53dd28b2201 Mon Sep 17 00:00:00 2001 From: Markus Doits Date: Thu, 16 May 2019 06:09:47 +0200 Subject: [PATCH 063/120] Support traversing ruby objects (#112) * support traversing arbitrary ruby objects * return correctly if there's no oparator * add another test just to be sure filters work on objects, too --- lib/jsonpath/enumerable.rb | 11 +++++++++-- lib/jsonpath/parser.rb | 3 +-- test/test_jsonpath.rb | 15 +++++++++++++++ 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/lib/jsonpath/enumerable.rb b/lib/jsonpath/enumerable.rb index 0aa486d..0f00015 100644 --- a/lib/jsonpath/enumerable.rb +++ b/lib/jsonpath/enumerable.rb @@ -12,7 +12,12 @@ def initialize(path, object, mode, options = {}) end def each(context = @object, key = nil, pos = 0, &blk) - node = key ? context[key] : context + node = + if key + context.is_a?(Hash) || context.is_a?(Array) ? context[key] : context.__send__(key) + else + context + end @_current_node = node return yield_value(blk, context, key) if pos == @path.size @@ -39,10 +44,12 @@ def handle_wildecard(node, expr, _context, _key, pos, &blk) expr[1, expr.size - 2].split(',').each do |sub_path| case sub_path[0] when '\'', '"' + k = sub_path[1, sub_path.size - 2] if node.is_a?(Hash) - k = sub_path[1, sub_path.size - 2] node[k] ||= nil if @options[:default_path_leaf_to_null] each(node, k, pos + 1, &blk) if node.key?(k) + elsif node.respond_to?(k.to_s) + each(node, k, pos + 1, &blk) end when '?' handle_question_mark(sub_path, node, pos, &blk) diff --git a/lib/jsonpath/parser.rb b/lib/jsonpath/parser.rb index b789c28..76356d8 100644 --- a/lib/jsonpath/parser.rb +++ b/lib/jsonpath/parser.rb @@ -83,8 +83,7 @@ def parse_exp(exp) end end - return false if el.nil? - return true if operator.nil? && el + return (el ? true : false) if el.nil? || operator.nil? el = Float(el) rescue el operand = Float(operand) rescue operand diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index f10fb90..fa608c9 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -123,6 +123,21 @@ def test_recognized_dot_notation_in_filters assert_equal [@object['store']['book'][2], @object['store']['book'][3]], JsonPath.new('$..book[?(@.isbn)]').on(@object) end + def test_works_on_non_hash + klass = Struct.new(:a, :b) + object = klass.new('some', 'value') + + assert_equal ['value'], JsonPath.new('$.b').on(object) + end + + def test_works_on_non_hash_with_filters + klass = Struct.new(:a, :b) + first_object = klass.new('some', 'value') + second_object = klass.new('next', 'other value') + + assert_equal ['other value'], JsonPath.new('$[?(@.a == "next")].b').on([first_object, second_object]) + end + def test_recognize_array_with_evald_index assert_equal [@object['store']['book'][2]], JsonPath.new('$..book[(@.length-5)]').on(@object) end From 024e0ec10c8ebdb1ed9646b4ac60bca883aa5cbb Mon Sep 17 00:00:00 2001 From: Gergely Brautigam Date: Thu, 16 May 2019 09:45:27 +0200 Subject: [PATCH 064/120] Bumped version for release. --- lib/jsonpath/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonpath/version.rb b/lib/jsonpath/version.rb index a85073d..20f360c 100644 --- a/lib/jsonpath/version.rb +++ b/lib/jsonpath/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true class JsonPath - VERSION = '1.0.1'.freeze + VERSION = '1.0.2'.freeze end From 0b7a4b2794fcfd6ea8627e4c8d92209f2a996353 Mon Sep 17 00:00:00 2001 From: Gergely Brautigam Date: Sun, 26 May 2019 22:48:18 +0200 Subject: [PATCH 065/120] Handle nested json elements by indexing the filter. (#113) * Closes #108 * added some error handling around invalid arrays. --- .gitignore | 2 ++ lib/jsonpath/parser.rb | 36 ++++++++++++++++++++++-------------- lib/jsonpath/proxy.rb | 4 ++-- test/test_jsonpath.rb | 29 +++++++++++++++++++++++++++++ 4 files changed, 55 insertions(+), 16 deletions(-) diff --git a/.gitignore b/.gitignore index 79744e1..49c00fe 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ Gemfile.lock coverage/* doc/* .yardoc +.DS_Store +.idea diff --git a/lib/jsonpath/parser.rb b/lib/jsonpath/parser.rb index 76356d8..9d46d22 100644 --- a/lib/jsonpath/parser.rb +++ b/lib/jsonpath/parser.rb @@ -46,18 +46,29 @@ def construct_expression_map(exps) end end - #  using a scanner break down the individual expressions and determine if + # Using a scanner break down the individual expressions and determine if # there is a match in the JSON for it or not. def parse_exp(exp) exp = exp.sub(/@/, '').gsub(/^\(/, '').gsub(/\)$/, '').tr('"', '\'').strip + exp.scan(/^\[(\d+)\]/) do |i| + next if i.empty? + + index = Integer(i[0]) + raise ArgumentError, 'Node does not appear to be an array.' unless @_current_node.is_a?(Array) + raise ArgumentError, "Index out of bounds for nested array. Index: #{index}" if @_current_node.size < index + + @_current_node = @_current_node[index] + # Remove the extra '' and the index. + exp = exp.gsub(/^\[\d+\]|\[''\]/, '') + end scanner = StringScanner.new(exp) elements = [] until scanner.eos? - if t = scanner.scan(/\['[a-zA-Z@&\*\/\$%\^\?_]+'\]|\.[a-zA-Z0-9_]+[?!]?/) + if (t = scanner.scan(/\['[a-zA-Z@&*\/$%^?_]+'\]|\.[a-zA-Z0-9_]+[?!]?/)) elements << t.gsub(/[\[\]'\.]|\s+/, '') - elsif t = scanner.scan(/(\s+)?[<>=!\-+][=~]?(\s+)?/) + elsif (t = scanner.scan(/(\s+)?[<>=!\-+][=~]?(\s+)?/)) operator = t - elsif t = scanner.scan(/(\s+)?'?.*'?(\s+)?/) + elsif (t = scanner.scan(/(\s+)?'?.*'?(\s+)?/)) # If we encounter a node which does not contain `'` it means #  that we are dealing with a boolean type. operand = if t == 'true' @@ -67,8 +78,8 @@ def parse_exp(exp) else operator.to_s.strip == '=~' ? t.to_regexp : t.gsub(%r{^'|'$}, '').strip end - elsif t = scanner.scan(/\/\w+\//) - elsif t = scanner.scan(/.*/) + elsif (t = scanner.scan(/\/\w+\//)) + elsif (t = scanner.scan(/.*/)) raise "Could not process symbol: #{t}" end end @@ -78,9 +89,7 @@ def parse_exp(exp) elsif @_current_node.is_a?(Hash) @_current_node.dig(*elements) else - elements.inject(@_current_node) do |agg, key| - agg.__send__(key) - end + elements.inject(@_current_node, &:__send__) end return (el ? true : false) if el.nil? || operator.nil? @@ -117,10 +126,9 @@ def parse_parentheses(str) top = top.map(&:strip) res = bool_or_exp(top.shift) top.each_with_index do |item, index| - case item - when '&&' + if item == '&&' res &&= top[index + 1] - when '||' + elsif item == '||' res ||= top[index + 1] end end @@ -129,9 +137,9 @@ def parse_parentheses(str) # and the closing index will be the last index. To avoid # off-by-one errors we simply return the result at that point. if closing_index + 1 >= str.length && opening_index == 0 - return res.to_s + res.to_s else - return "#{str[0..opening_index - 1]}#{res}#{str[closing_index + 1..str.length]}" + "#{str[0..opening_index - 1]}#{res}#{str[closing_index + 1..str.length]}" end end diff --git a/lib/jsonpath/proxy.rb b/lib/jsonpath/proxy.rb index f7a4804..3d11a8b 100644 --- a/lib/jsonpath/proxy.rb +++ b/lib/jsonpath/proxy.rb @@ -10,11 +10,11 @@ def initialize(obj) end def gsub(path, replacement = nil, &replacement_block) - _gsub(_deep_copy, path, replacement ? proc { replacement } : replacement_block) + _gsub(_deep_copy, path, replacement ? proc(&method(:replacement)) : replacement_block) end def gsub!(path, replacement = nil, &replacement_block) - _gsub(@obj, path, replacement ? proc { replacement } : replacement_block) + _gsub(@obj, path, replacement ? proc(&method(:replacement)) : replacement_block) end def delete(path = JsonPath::PATH_ALL) diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index fa608c9..b33d0f4 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -804,6 +804,35 @@ def test_delete_more_items_with_stepping assert_equal expected, JsonPath.for(a.to_json).delete('$.itemList[1:6:2]').to_hash end + def test_nested_values + json = ' + { + "phoneNumbers": [ + [{ + "type" : "iPhone", + "number": "0123-4567-8888" + }], + [{ + "type" : "home", + "number": "0123-4567-8910" + }] + ] + } + '.to_json + assert_equal [[{ 'type' => 'home', 'number' => '0123-4567-8910' }]], JsonPath.on(json, "$.phoneNumbers[?(@[0].type == 'home')]") + assert_equal [], JsonPath.on(json, "$.phoneNumbers[?(@[2].type == 'home')]") + json = ' + { + "phoneNumbers": + { + "type" : "iPhone", + "number": "0123-4567-8888" + } + } + '.to_json + assert_equal [], JsonPath.on(json, "$.phoneNumbers[?(@[0].type == 'home')]") + end + def example_object { 'store' => { 'book' => [ From 54e031b980cc0e1587ac7411d2d3665356f876df Mon Sep 17 00:00:00 2001 From: Gergely Brautigam Date: Sun, 26 May 2019 22:48:50 +0200 Subject: [PATCH 066/120] Bumped for release. --- lib/jsonpath/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonpath/version.rb b/lib/jsonpath/version.rb index 20f360c..285b92c 100644 --- a/lib/jsonpath/version.rb +++ b/lib/jsonpath/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true class JsonPath - VERSION = '1.0.2'.freeze + VERSION = '1.0.3'.freeze end From 16cfe87827b060fb77c0ad938aeaab520fa3744b Mon Sep 17 00:00:00 2001 From: Gergely Brautigam Date: Mon, 3 Jun 2019 13:00:48 +0200 Subject: [PATCH 067/120] Selecting multiple keys/values (#114) * Closes #92 * A little refactor. Going to deal with versions later * Fixed. * Added readme and increased version. * A little refactoring. * strip whitespace from the selector syntax. * Fixed space being stripped at the definition level --- README.md | 45 ++++++++++++++++++++++ lib/jsonpath.rb | 24 ++++++------ lib/jsonpath/enumerable.rb | 16 ++++++++ lib/jsonpath/parser.rb | 2 +- lib/jsonpath/version.rb | 2 +- test/test_jsonpath.rb | 76 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 152 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index b4729b0..e8b0c13 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,51 @@ For more usage examples and variations on paths, please visit the tests. There a end ``` +### Selecting Values + +It's possible to select results once a query has been defined after the query. For example given this JSON data: + +```bash +{ + "store": { + "book": [ + { + "category": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95 + }, + { + "category": "fiction", + "author": "Evelyn Waugh", + "title": "Sword of Honour", + "price": 12.99 + } + ] +} +``` + +... and this query: + +```ruby +"$.store.book[*](category,author)" +``` + +... the result can be filtered as such: + +```bash +[ + { + "category" : "reference", + "author" : "Nigel Rees" + }, + { + "category" : "fiction", + "author" : "Evelyn Waugh" + } +] +``` + ### Running an individual test ```ruby diff --git a/lib/jsonpath.rb b/lib/jsonpath.rb index 5fc468f..7919cb1 100644 --- a/lib/jsonpath.rb +++ b/lib/jsonpath.rb @@ -10,7 +10,7 @@ # JsonPath: initializes the class with a given JsonPath and parses that path # into a token array. class JsonPath - PATH_ALL = '$..*'.freeze + PATH_ALL = '$..*' attr_accessor :path @@ -19,21 +19,23 @@ def initialize(path, opts = {}) scanner = StringScanner.new(path.strip) @path = [] until scanner.eos? - if token = scanner.scan(/\$\B|@\B|\*|\.\./) + if (token = scanner.scan(/\$\B|@\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(/'(.*?)'/) + elsif (token = scanner.scan(/'(.*?)'/)) @path << "[#{token}]" - elsif token = scanner.scan(/\[/) + elsif (token = scanner.scan(/\[/)) @path << find_matching_brackets(token, scanner) - elsif token = scanner.scan(/\]/) + elsif (token = scanner.scan(/\]/)) raise ArgumentError, 'unmatched closing bracket' + elsif (token = scanner.scan(/\(.*\)/)) + @path << token elsif scanner.scan(/\./) nil - elsif token = scanner.scan(/[><=] \d+/) + elsif (token = scanner.scan(/[><=] \d+/)) @path.last << token - elsif token = scanner.scan(/./) + elsif (token = scanner.scan(/./)) begin @path.last << token rescue RuntimeError @@ -46,13 +48,13 @@ def initialize(path, opts = {}) def find_matching_brackets(token, scanner) count = 1 until count.zero? - if t = scanner.scan(/\[/) + if (t = scanner.scan(/\[/)) token << t count += 1 - elsif t = scanner.scan(/\]/) + elsif (t = scanner.scan(/\]/)) token << t count -= 1 - elsif t = scanner.scan(/[^\[\]]+/) + elsif (t = scanner.scan(/[^\[\]]+/)) token << t elsif scanner.eos? raise ArgumentError, 'unclosed bracket' diff --git a/lib/jsonpath/enumerable.rb b/lib/jsonpath/enumerable.rb index 0f00015..25d0d10 100644 --- a/lib/jsonpath/enumerable.rb +++ b/lib/jsonpath/enumerable.rb @@ -28,6 +28,10 @@ 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) + when /\(.*\)/ + keys = expr.gsub(/[()]/, '').split(',').map(&:strip) + new_context = filter_context(context, keys) + yield_value(blk, new_context, key) end if pos > 0 && @path[pos - 1] == '..' || (@path[pos - 1] == '*' && @path[pos] != '..') @@ -40,6 +44,18 @@ def each(context = @object, key = nil, pos = 0, &blk) private + def filter_context(context, keys) + case context + when Hash + # TODO: Change this to `slice(*keys)` when ruby version support is > 2.4 + context.select { |k| keys.include?(k) } + when Array + context.each_with_object([]) do |c, memo| + memo << c.select { |k| keys.include?(k) } + end + end + end + def handle_wildecard(node, expr, _context, _key, pos, &blk) expr[1, expr.size - 2].split(',').each do |sub_path| case sub_path[0] diff --git a/lib/jsonpath/parser.rb b/lib/jsonpath/parser.rb index 9d46d22..3b765cd 100644 --- a/lib/jsonpath/parser.rb +++ b/lib/jsonpath/parser.rb @@ -65,7 +65,7 @@ def parse_exp(exp) elements = [] until scanner.eos? if (t = scanner.scan(/\['[a-zA-Z@&*\/$%^?_]+'\]|\.[a-zA-Z0-9_]+[?!]?/)) - elements << t.gsub(/[\[\]'\.]|\s+/, '') + elements << t.gsub(/[\[\]'.]|\s+/, '') elsif (t = scanner.scan(/(\s+)?[<>=!\-+][=~]?(\s+)?/)) operator = t elsif (t = scanner.scan(/(\s+)?'?.*'?(\s+)?/)) diff --git a/lib/jsonpath/version.rb b/lib/jsonpath/version.rb index 285b92c..dcc6abe 100644 --- a/lib/jsonpath/version.rb +++ b/lib/jsonpath/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true class JsonPath - VERSION = '1.0.3'.freeze + VERSION = '1.0.4' end diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index b33d0f4..de75647 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -833,6 +833,82 @@ def test_nested_values assert_equal [], JsonPath.on(json, "$.phoneNumbers[?(@[0].type == 'home')]") end + def test_selecting_multiple_keys + json = ' + { + "store": { + "book": [ + { + "category": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95 + }, + { + "category": "fiction", + "author": "Evelyn Waugh", + "title": "Sword of Honour", + "price": 12.99 + } + ] + } + } + '.to_json + + assert_equal [{ 'category' => 'reference', 'author' => 'Nigel Rees' }, { 'category' => 'fiction', 'author' => 'Evelyn Waugh' }], JsonPath.on(json, '$.store.book[*](category,author)') + end + + def test_selecting_multiple_keys_with_filter + json = ' + { + "store": { + "book": [ + { + "category": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95 + }, + { + "category": "fiction", + "author": "Evelyn Waugh", + "title": "Sword of Honour", + "price": 12.99 + } + ] + } + } + '.to_json + + assert_equal [{ 'category' => 'reference', 'author' => 'Nigel Rees' }], JsonPath.on(json, "$.store.book[?(@['price'] == 8.95)](category,author)") + assert_equal [{ 'category' => 'reference', 'author' => 'Nigel Rees' }], JsonPath.on(json, "$.store.book[?(@['price'] == 8.95)]( category, author )") + end + + def test_selecting_multiple_keys_with_filter_with_space_in_catergory + json = ' + { + "store": { + "book": [ + { + "cate gory": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95 + }, + { + "cate gory": "fiction", + "author": "Evelyn Waugh", + "title": "Sword of Honour", + "price": 12.99 + } + ] + } + } + '.to_json + + assert_equal [{ 'cate gory' => 'reference', 'author' => 'Nigel Rees' }], JsonPath.on(json, "$.store.book[?(@['price'] == 8.95)]( cate gory, author )") + end + def example_object { 'store' => { 'book' => [ From 17a03e9c167a47ffccc2518722bb8eba3933e0eb Mon Sep 17 00:00:00 2001 From: Gergely Brautigam Date: Mon, 14 Oct 2019 16:00:58 +0200 Subject: [PATCH 068/120] Only check respond_to in case we are not dealing with a built in type. (#119) * Only check respond_to in case we are not dealing with a built in type. * Do not allow respond tos which are also Object methods. --- lib/jsonpath/enumerable.rb | 2 +- test/test_jsonpath.rb | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/jsonpath/enumerable.rb b/lib/jsonpath/enumerable.rb index 25d0d10..331c2d2 100644 --- a/lib/jsonpath/enumerable.rb +++ b/lib/jsonpath/enumerable.rb @@ -64,7 +64,7 @@ def handle_wildecard(node, expr, _context, _key, pos, &blk) if node.is_a?(Hash) node[k] ||= nil if @options[:default_path_leaf_to_null] each(node, k, pos + 1, &blk) if node.key?(k) - elsif node.respond_to?(k.to_s) + elsif node.respond_to?(k.to_s) && !Object.respond_to?(k.to_s) each(node, k, pos + 1, &blk) end when '?' diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index de75647..e891511 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -909,6 +909,19 @@ def test_selecting_multiple_keys_with_filter_with_space_in_catergory assert_equal [{ 'cate gory' => 'reference', 'author' => 'Nigel Rees' }], JsonPath.on(json, "$.store.book[?(@['price'] == 8.95)]( cate gory, author )") end + def test_object_method_send + j = {height: 5, hash: "some_hash"}.to_json + hs = JsonPath.new "$..send" + assert_equal([], hs.on(j)) + hs = JsonPath.new "$..hash" + assert_equal(["some_hash"], hs.on(j)) + hs = JsonPath.new "$..send" + assert_equal([], hs.on(j)) + j = {height: 5, send: "should_still_work"}.to_json + hs = JsonPath.new "$..send" + assert_equal(['should_still_work'], hs.on(j)) + end + def example_object { 'store' => { 'book' => [ From 56c2fd0b356e463883bc06cf71289da3f27d9830 Mon Sep 17 00:00:00 2001 From: Gergely Brautigam Date: Tue, 15 Oct 2019 12:30:40 +0200 Subject: [PATCH 069/120] Increased version for new release. --- lib/jsonpath/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonpath/version.rb b/lib/jsonpath/version.rb index dcc6abe..5abae48 100644 --- a/lib/jsonpath/version.rb +++ b/lib/jsonpath/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true class JsonPath - VERSION = '1.0.4' + VERSION = '1.0.5' end From 39a84233bf515a4a0ef16e504da33b02a4caf7fa Mon Sep 17 00:00:00 2001 From: Gergely Brautigam Date: Wed, 18 Nov 2020 08:30:32 +0100 Subject: [PATCH 070/120] Apparently after 2.6 this error is no longer raised (#131) --- test/test_jsonpath.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index e891511..d259b47 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -748,7 +748,7 @@ def test_complex_nested_grouping_unmatched_parent end def test_runtime_error_frozen_string - skip('in ruby version below 2.2.0 this error is not raised') if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.2.0') + skip('in ruby version below 2.2.0 this error is not raised') if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.2.0') || Gem::Version.new(RUBY_VERSION) > Gem::Version::new('2.6') json = ' { "test": "something" From ccfb39be456c88f4961e96f981e154936a537782 Mon Sep 17 00:00:00 2001 From: Michael Grosser Date: Wed, 18 Nov 2020 00:03:04 -0800 Subject: [PATCH 071/120] drop support for EOL rubies (#130) --- .travis.yml | 10 +++------- jsonpath.gemspec | 6 ++---- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/.travis.yml b/.travis.yml index b9e1508..739e3cf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,7 @@ language: ruby rvm: - - 2.3.8 - - 2.4.6 - - 2.5.5 - - 2.6.3 + - 2.5 + - 2.6 + - 2.7 - ruby-head - jruby-head - -before_install: - - gem install bundler -v '< 2' diff --git a/jsonpath.gemspec b/jsonpath.gemspec index 7a9d4ac..975f00d 100644 --- a/jsonpath.gemspec +++ b/jsonpath.gemspec @@ -5,10 +5,8 @@ require File.join(File.dirname(__FILE__), 'lib', 'jsonpath', 'version') Gem::Specification.new do |s| s.name = 'jsonpath' s.version = JsonPath::VERSION - if s.respond_to? :required_rubygems_version= - s.required_rubygems_version = - Gem::Requirement.new('>= 0') - end + s.required_rubygems_version = Gem::Requirement.new('>= 0') + s.required_ruby_version = '>= 2.5' 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/.' From 7ec50e623c290dd4f1f651da19bb1de351ec92c4 Mon Sep 17 00:00:00 2001 From: Michael Grosser Date: Wed, 18 Nov 2020 00:35:40 -0800 Subject: [PATCH 072/120] remove to_regex dependency and the warnings it causes (#128) Co-authored-by: Gergely Brautigam --- jsonpath.gemspec | 6 ------ lib/jsonpath/parser.rb | 45 ++++++++++++++++++++++++++++++++++-------- 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/jsonpath.gemspec b/jsonpath.gemspec index 975f00d..0158718 100644 --- a/jsonpath.gemspec +++ b/jsonpath.gemspec @@ -5,7 +5,6 @@ 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') s.required_ruby_version = '>= 2.5' s.authors = ['Joshua Hull', 'Gergely Brautigam'] s.summary = 'Ruby implementation of http://goessner.net/articles/JsonPath/' @@ -14,17 +13,12 @@ Gem::Specification.new do |s| s.extra_rdoc_files = ['README.md'] s.files = `git ls-files`.split("\n") 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.licenses = ['MIT'] # dependencies s.add_runtime_dependency 'multi_json' - s.add_runtime_dependency 'to_regexp', '~> 0.2.1' s.add_development_dependency 'bundler' s.add_development_dependency 'code_stats' s.add_development_dependency 'minitest', '~> 2.2.0' diff --git a/lib/jsonpath/parser.rb b/lib/jsonpath/parser.rb index 3b765cd..624d46e 100644 --- a/lib/jsonpath/parser.rb +++ b/lib/jsonpath/parser.rb @@ -1,11 +1,12 @@ # frozen_string_literal: true require 'strscan' -require 'to_regexp' class JsonPath # Parser parses and evaluates an expression passed to @_current_node. class Parser + REGEX = /\A\/(.+)\/([imxnesu]*)\z|\A%r{(.+)}([imxnesu]*)\z/ + def initialize(node) @_current_node = node @_expr_map = {} @@ -71,13 +72,16 @@ def parse_exp(exp) elsif (t = scanner.scan(/(\s+)?'?.*'?(\s+)?/)) # If we encounter a node which does not contain `'` it means #  that we are dealing with a boolean type. - operand = if t == 'true' - true - elsif t == 'false' - false - else - operator.to_s.strip == '=~' ? t.to_regexp : t.gsub(%r{^'|'$}, '').strip - end + operand = + if t == 'true' + true + elsif t == 'false' + false + elsif operator.to_s.strip == '=~' + parse_regex(t) + else + t.gsub(%r{^'|'$}, '').strip + end elsif (t = scanner.scan(/\/\w+\//)) elsif (t = scanner.scan(/.*/)) raise "Could not process symbol: #{t}" @@ -102,6 +106,31 @@ def parse_exp(exp) private + # /foo/i -> Regex.new("foo", Regexp::IGNORECASE) without using eval + # also supports %r{foo}i + # following https://github.com/seamusabshere/to_regexp/blob/master/lib/to_regexp.rb + def parse_regex(t) + t =~ REGEX + content = $1 || $3 + options = $2 || $4 + + raise "unsupported regex #{t} use /foo/ style" if !content || !options + + content = content.gsub '\\/', '/' + + flags = 0 + flags |= Regexp::IGNORECASE if options.include?('i') + flags |= Regexp::MULTILINE if options.include?('m') + flags |= Regexp::EXTENDED if options.include?('x') + + # 'n' = none, 'e' = EUC, 's' = SJIS, 'u' = UTF-8 + lang = options.scan(/[nes]/).join.downcase # ignores u since that is default and causes a warning + + args = [content, flags] + args << lang unless lang.empty? # avoid warning + Regexp.new(*args) + end + #  This will break down a parenthesis from the left to the right #  and replace the given expression with it's returned value. # It does this in order to make it easy to eliminate groups From 8e2c4af8edddb9a82c42d8789e6895e617e31b0a Mon Sep 17 00:00:00 2001 From: Gergely Brautigam Date: Wed, 18 Nov 2020 09:36:57 +0100 Subject: [PATCH 073/120] Bumped version for release --- lib/jsonpath/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonpath/version.rb b/lib/jsonpath/version.rb index 5abae48..6357631 100644 --- a/lib/jsonpath/version.rb +++ b/lib/jsonpath/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true class JsonPath - VERSION = '1.0.5' + VERSION = '1.0.6' end From 64f919ae9117d5be5e3dc1a8755a45b48033e664 Mon Sep 17 00:00:00 2001 From: Michael Grosser Date: Thu, 19 Nov 2020 12:18:13 -0800 Subject: [PATCH 074/120] split regex tests and add %r (#132) --- lib/jsonpath/parser.rb | 2 +- test/test_jsonpath.rb | 22 +++++++++++++++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/lib/jsonpath/parser.rb b/lib/jsonpath/parser.rb index 624d46e..a83fa2f 100644 --- a/lib/jsonpath/parser.rb +++ b/lib/jsonpath/parser.rb @@ -114,7 +114,7 @@ def parse_regex(t) content = $1 || $3 options = $2 || $4 - raise "unsupported regex #{t} use /foo/ style" if !content || !options + raise ArgumentError, "unsupported regex #{t} use /foo/ style" if !content || !options content = content.gsub '\\/', '/' diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index d259b47..eb8466b 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -511,15 +511,31 @@ def test_boolean_parameter_value assert_equal [{ 'isTrue' => true, 'name' => 'testname1' }], JsonPath.new('$.data[?(@.isTrue)]').on(data) end - def test_regex - assert_equal [], JsonPath.new('$..book[?(@.author =~ /herman/)]').on(@object) + def test_regex_simple + assert_equal %w[asdf asdf2], JsonPath.new('$.store.book..tags[?(@ =~ /asdf/)]').on(@object) + end + + def test_regex_simple_miss + assert_equal [], JsonPath.new('$.store.book..tags[?(@ =~ /wut/)]').on(@object) + end + + def test_regex_r + assert_equal %w[asdf asdf2], JsonPath.new('$.store.book..tags[?(@ =~ %r{asdf})]').on(@object) + end + + def test_regex_flags assert_equal [ @object['store']['book'][2], @object['store']['book'][4], @object['store']['book'][5], @object['store']['book'][6] ], JsonPath.new('$..book[?(@.author =~ /herman|lukyanenko/i)]').on(@object) - assert_equal %w[asdf asdf2], JsonPath.new('$.store.book..tags[?(@ =~ /asdf/)]').on(@object) + end + + def test_regex_error + assert_raises ArgumentError do + JsonPath.new('$.store.book..tags[?(@ =~ asdf)]').on(@object) + end end def test_regression_1 From 85c8b15d82da6c22d906b548f9915256be34b107 Mon Sep 17 00:00:00 2001 From: Igor Victor Date: Fri, 27 Nov 2020 16:33:39 +0100 Subject: [PATCH 075/120] Update .travis.yml (#133) --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 739e3cf..6febcb2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,3 +5,4 @@ rvm: - 2.7 - ruby-head - jruby-head + - truffleruby-head From fb73c177128c3502591eba1c18171a03f4369f05 Mon Sep 17 00:00:00 2001 From: Gergely Brautigam Date: Tue, 15 Dec 2020 07:12:47 +0100 Subject: [PATCH 076/120] Closes #134 (#135) --- .gitignore | 1 + lib/jsonpath/enumerable.rb | 1 - test/test_jsonpath.rb | 7 +++++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 49c00fe..0b1b293 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ doc/* .yardoc .DS_Store .idea +vendor diff --git a/lib/jsonpath/enumerable.rb b/lib/jsonpath/enumerable.rb index 331c2d2..268ba18 100644 --- a/lib/jsonpath/enumerable.rb +++ b/lib/jsonpath/enumerable.rb @@ -128,7 +128,6 @@ def handle_question_mark(sub_path, node, pos, &blk) end def yield_value(blk, context, key) - key = Integer(key) rescue key if key case @mode when nil blk.call(key ? context[key] : context) diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index eb8466b..190e3a5 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -938,6 +938,13 @@ def test_object_method_send assert_equal(['should_still_work'], hs.on(j)) end + def test_index_access_by_number + data = { + '1': 'foo' + } + assert_equal ['foo'], JsonPath.new('$.1').on(data.to_json) + end + def example_object { 'store' => { 'book' => [ From a5983ba3943ceb5b5b76ea1913781a94f4eecac0 Mon Sep 17 00:00:00 2001 From: Brautigam Gergely Date: Tue, 15 Dec 2020 07:19:43 +0100 Subject: [PATCH 077/120] bumped version and added ignore --- .gitignore | 2 ++ lib/jsonpath/version.rb | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 0b1b293..12f524a 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ doc/* .DS_Store .idea vendor +.tags +*.gem diff --git a/lib/jsonpath/version.rb b/lib/jsonpath/version.rb index 6357631..c8528aa 100644 --- a/lib/jsonpath/version.rb +++ b/lib/jsonpath/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true class JsonPath - VERSION = '1.0.6' + VERSION = '1.0.7' end From f9ab9d4c8e0fe23a0249719854f87f05d7c108e4 Mon Sep 17 00:00:00 2001 From: Bernard Lambeau Date: Mon, 28 Dec 2020 11:18:19 +0100 Subject: [PATCH 078/120] Factorize `dig` logic ; add a few options ; more tests ; fix README (#137) * Add a few tests to improve coverage and highlight pending behaviors. * Factorize digging logic to a Dig helper module. * Add support for a :use_symbols option. * Add support for objects responding to dig. * Fix digging behavior in presence of explicit null/nil. * Fix & test README claims, document available options. Also includes a library fix when default_path_leaf_to_null is used. * Introduce :allow_send and document it (defaults to true). --- README.md | 134 ++++++++++++++++++++++--------- lib/jsonpath.rb | 10 ++- lib/jsonpath/dig.rb | 57 ++++++++++++++ lib/jsonpath/enumerable.rb | 25 +++--- lib/jsonpath/parser.rb | 7 +- test/test_jsonpath.rb | 157 ++++++++++++++++++++++++++++++++++++- test/test_readme.rb | 117 +++++++++++++++++++++++++++ 7 files changed, 449 insertions(+), 58 deletions(-) create mode 100644 lib/jsonpath/dig.rb create mode 100644 test/test_readme.rb diff --git a/README.md b/README.md index e8b0c13..32cb162 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@ This is an implementation of http://goessner.net/articles/JsonPath/. ## What is JsonPath? -JsonPath is a way of addressing elements within a JSON object. Similar to xpath of yore, JsonPath lets you -traverse a json object and manipulate or access it. +JsonPath is a way of addressing elements within a JSON object. Similar to xpath +of yore, JsonPath lets you traverse a json object and manipulate or access it. ## Usage @@ -15,8 +15,8 @@ There is stand-alone usage through the binary `jsonpath` jsonpath [expression] (file|string) - If you omit the second argument, it will read stdin, assuming one valid JSON object - per line. Expression must be a valid jsonpath expression. + If you omit the second argument, it will read stdin, assuming one valid JSON + object per line. Expression must be a valid jsonpath expression. ### Library @@ -40,8 +40,8 @@ json = <<-HERE_DOC HERE_DOC ``` -Now that we have a JSON object, let's get all the prices present in the object. We create an object for the path -in the following way. +Now that we have a JSON object, let's get all the prices present in the object. +We create an object for the path in the following way. ```ruby path = JsonPath.new('$..price') @@ -54,14 +54,15 @@ path.on(json) # => [19.95, 8.95, 12.99, 8.99, 22.99] ``` -Or on some other object ... +Or reuse it later on some other object (thread safe) ... ```ruby path.on('{"books":[{"title":"A Tale of Two Somethings","price":18.88}]}') # => [18.88] ``` -You can also just combine this into one mega-call with the convenient `JsonPath.on` method. +You can also just combine this into one mega-call with the convenient +`JsonPath.on` method. ```ruby JsonPath.on(json, '$..author') @@ -73,29 +74,36 @@ Of course the full JsonPath syntax is supported, such as array slices ```ruby JsonPath.new('$..book[::2]').on(json) # => [ -# {"price"=>8.95, "category"=>"reference", "author"=>"Nigel Rees", "title"=>"Sayings of the Century"}, -# {"price"=>8.99, "category"=>"fiction", "author"=>"Herman Melville", "title"=>"Moby Dick", "isbn"=>"0-553-21311-3"} +# {"price" => 8.95, "category" => "reference", "title" => "Sayings of the Century", "author" => "Nigel Rees"}, +# {"price" => 8.99, "category" => "fiction", "isbn" => "0-553-21311-3", "title" => "Moby Dick", "author" => "Herman Melville","color" => "blue"}, # ] ``` -...and evals. +...and evals, including those with conditional operators ```ruby -JsonPath.new('$..price[?(@ < 10)]').on(json) +JsonPath.new("$..price[?(@ < 10)]").on(json) # => [8.95, 8.99] + +JsonPath.new("$..book[?(@['price'] == 8.95 || @['price'] == 8.99)].title").on(json) +# => ["Sayings of the Century", "Moby Dick"] + +JsonPath.new("$..book[?(@['price'] == 8.95 && @['price'] == 8.99)].title").on(json) +# => [] ``` -There is a convenience method, `#first` that gives you the first element for a JSON object and path. +There is a convenience method, `#first` that gives you the first element for a +JSON object and path. ```ruby -JsonPath.new('$..color').first(object) +JsonPath.new('$..color').first(json) # => "red" ``` As well, we can directly create an `Enumerable` at any time using `#[]`. ```ruby -enum = JsonPath.new('$..color')[object] +enum = JsonPath.new('$..color')[json] # => # enum.first # => "red" @@ -103,29 +111,77 @@ enum.any?{ |c| c == 'red' } # => true ``` -### More examples +For more usage examples and variations on paths, please visit the tests. There +are some more complex ones as well. -For more usage examples and variations on paths, please visit the tests. There are some more complex ones as well. +### Querying ruby data structures -### Conditional Operators Are Also Supported +If you have ruby hashes with symbolized keys as input, you +can use `:use_symbols` to make JsonPath work fine on them too: ```ruby - def test_or_operator - assert_equal [@object['store']['book'][1], @object['store']['book'][3]], JsonPath.new("$..book[?(@['price'] == 13 || @['price'] == 23)]").on(@object) - end +book = { title: "Sayings of the Century" } - def test_and_operator - assert_equal [], JsonPath.new("$..book[?(@['price'] == 13 && @['price'] == 23)]").on(@object) - end +JsonPath.new('$.title').on(book) +# => [] - def test_and_operator_with_more_results - assert_equal [@object['store']['book'][1]], JsonPath.new("$..book[?(@['price'] < 23 && @['price'] > 9)]").on(@object) - end +JsonPath.new('$.title', use_symbols: true).on(book) +# => ["Sayings of the Century"] +``` + +JsonPath also recognizes objects responding to `dig` (introduced +in ruby 2.3), and therefore works out of the box with Struct, +OpenStruct, and other Hash-like structures: + +```ruby +book_class = Struct.new(:title) +book = book_class.new("Sayings of the Century") + +JsonPath.new('$.title').on(book) +# => ["Sayings of the Century"] +``` + +JsonPath is able to query pure ruby objects and uses `__send__` +on them. The option is enabled by default in JsonPath 1.x, but +we encourage to enable it explicitly: + +```ruby +book_class = Class.new{ attr_accessor :title } +book = book_class.new +book.title = "Sayings of the Century" + +JsonPath.new('$.title', allow_send: true).on(book) +# => ["Sayings of the Century"] +``` + +### Other available options + +By default, JsonPath does not return null values on unexisting paths. +This can be changed using the `:default_path_leaf_to_null` option + +```ruby +JsonPath.new('$..book[*].isbn').on(json) +# => ["0-553-21311-3", "0-395-19395-8"] + +JsonPath.new('$..book[*].isbn', default_path_leaf_to_null: true).on(json) +# => [nil, nil, "0-553-21311-3", "0-395-19395-8"] +``` + +When JsonPath returns a Hash, you can ask to symbolize its keys +using the `:symbolize_keys` option + +```ruby +JsonPath.new('$..book[0]').on(json) +# => [{"category" => "reference", ...}] + +JsonPath.new('$..book[0]', symbolize_keys: true).on(json) +# => [{category: "reference", ...}] ``` ### Selecting Values -It's possible to select results once a query has been defined after the query. For example given this JSON data: +It's possible to select results once a query has been defined after the query. For +example given this JSON data: ```bash { @@ -168,15 +224,10 @@ It's possible to select results once a query has been defined after the query. F ] ``` -### Running an individual test - -```ruby -ruby -Ilib:../lib test/test_jsonpath.rb --name test_wildcard_on_intermediary_element_v6 -``` - ### Manipulation -If you'd like to do substitution in a json object, you can use `#gsub` or `#gsub!` to modify the object in place. +If you'd like to do substitution in a json object, you can use `#gsub` +or `#gsub!` to modify the object in place. ```ruby JsonPath.for('{"candy":"lollipop"}').gsub('$..candy') {|v| "big turks" }.to_hash @@ -188,7 +239,9 @@ The result will be {'candy' => 'big turks'} ``` -If you'd like to remove all nil keys, you can use `#compact` and `#compact!`. To remove all keys under a certain path, use `#delete` or `#delete!`. You can even chain these methods together as follows: +If you'd like to remove all nil keys, you can use `#compact` and `#compact!`. +To remove all keys under a certain path, use `#delete` or `#delete!`. You can +even chain these methods together as follows: ```ruby json = '{"candy":"lollipop","noncandy":null,"other":"things"}' @@ -202,4 +255,11 @@ o = JsonPath.for(json). # Contributions -Please feel free to submit an Issue or a Pull Request any time you feel like you would like to contribute. Thank you! +Please feel free to submit an Issue or a Pull Request any time you feel like +you would like to contribute. Thank you! + +## Running an individual test + +```ruby +ruby -Ilib:../lib test/test_jsonpath.rb --name test_wildcard_on_intermediary_element_v6 +``` diff --git a/lib/jsonpath.rb b/lib/jsonpath.rb index 7919cb1..e976d4b 100644 --- a/lib/jsonpath.rb +++ b/lib/jsonpath.rb @@ -3,6 +3,7 @@ require 'strscan' require 'multi_json' require 'jsonpath/proxy' +require 'jsonpath/dig' require 'jsonpath/enumerable' require 'jsonpath/version' require 'jsonpath/parser' @@ -12,10 +13,17 @@ class JsonPath PATH_ALL = '$..*' + DEFAULT_OPTIONS = { + :default_path_leaf_to_null => false, + :symbolize_keys => false, + :use_symbols => false, + :allow_send => true + } + attr_accessor :path def initialize(path, opts = {}) - @opts = opts + @opts = DEFAULT_OPTIONS.merge(opts) scanner = StringScanner.new(path.strip) @path = [] until scanner.eos? diff --git a/lib/jsonpath/dig.rb b/lib/jsonpath/dig.rb new file mode 100644 index 0000000..7a13004 --- /dev/null +++ b/lib/jsonpath/dig.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +class JsonPath + module Dig + + # Similar to what Hash#dig or Array#dig + def dig(context, *keys) + keys.inject(context){|memo,k| + dig_one(memo, k) + } + end + + # Returns a hash mapping each key from keys + # to its dig value on context. + def dig_as_hash(context, keys) + keys.each_with_object({}) do |k, memo| + memo[k] = dig_one(context, k) + end + end + + # Dig the value of k on context. + def dig_one(context, k) + case context + when Hash + context[@options[:use_symbols] ? k.to_sym : k] + when Array + context[k.to_i] + else + if context.respond_to?(:dig) + context.dig(k) + elsif @options[:allow_send] + context.__send__(k) + end + end + end + + # Yields the block if context has a diggable + # value for k + def yield_if_diggable(context, k, &blk) + case context + when Array + nil + when Hash + k = @options[:use_symbols] ? k.to_sym : k + return yield if context.key?(k) || @options[:default_path_leaf_to_null] + else + if context.respond_to?(:dig) + digged = dig_one(context, k) + yield if !digged.nil? || @options[:default_path_leaf_to_null] + elsif @options[:allow_send] && context.respond_to?(k.to_s) && !Object.respond_to?(k.to_s) + yield + end + end + end + + end +end \ No newline at end of file diff --git a/lib/jsonpath/enumerable.rb b/lib/jsonpath/enumerable.rb index 268ba18..7a6f270 100644 --- a/lib/jsonpath/enumerable.rb +++ b/lib/jsonpath/enumerable.rb @@ -3,6 +3,7 @@ class JsonPath class Enumerable include ::Enumerable + include Dig def initialize(path, object, mode, options = {}) @path = path.path @@ -12,12 +13,7 @@ def initialize(path, object, mode, options = {}) end def each(context = @object, key = nil, pos = 0, &blk) - node = - if key - context.is_a?(Hash) || context.is_a?(Array) ? context[key] : context.__send__(key) - else - context - end + node = key ? dig_one(context, key) : context @_current_node = node return yield_value(blk, context, key) if pos == @path.size @@ -47,11 +43,10 @@ def each(context = @object, key = nil, pos = 0, &blk) def filter_context(context, keys) case context when Hash - # TODO: Change this to `slice(*keys)` when ruby version support is > 2.4 - context.select { |k| keys.include?(k) } + dig_as_hash(context, keys) when Array context.each_with_object([]) do |c, memo| - memo << c.select { |k| keys.include?(k) } + memo << dig_as_hash(c, keys) end end end @@ -61,16 +56,14 @@ def handle_wildecard(node, expr, _context, _key, pos, &blk) case sub_path[0] when '\'', '"' k = sub_path[1, sub_path.size - 2] - if node.is_a?(Hash) - node[k] ||= nil if @options[:default_path_leaf_to_null] - each(node, k, pos + 1, &blk) if node.key?(k) - elsif node.respond_to?(k.to_s) && !Object.respond_to?(k.to_s) + yield_if_diggable(node, k) do each(node, k, pos + 1, &blk) end when '?' handle_question_mark(sub_path, node, pos, &blk) else next if node.is_a?(Array) && node.empty? + next if node.nil? # when default_path_leaf_to_null is true array_args = sub_path.split(':') if array_args[0] == '*' @@ -130,7 +123,7 @@ def handle_question_mark(sub_path, node, pos, &blk) def yield_value(blk, context, key) case @mode when nil - blk.call(key ? context[key] : context) + blk.call(key ? dig_one(context, key) : context) when :compact if key && context[key].nil? key.is_a?(Integer) ? context.delete_at(key) : context.delete(key) @@ -162,12 +155,12 @@ def process_function_or_literal(exp, default = nil) el == '@' ? '@' : "['#{el}']" end.join begin - return JsonPath::Parser.new(@_current_node).parse(exp_to_eval) + return JsonPath::Parser.new(@_current_node, @options).parse(exp_to_eval) rescue StandardError return default end end - JsonPath::Parser.new(@_current_node).parse(exp) + JsonPath::Parser.new(@_current_node, @options).parse(exp) end end end diff --git a/lib/jsonpath/parser.rb b/lib/jsonpath/parser.rb index a83fa2f..79feb8c 100644 --- a/lib/jsonpath/parser.rb +++ b/lib/jsonpath/parser.rb @@ -5,11 +5,14 @@ class JsonPath # Parser parses and evaluates an expression passed to @_current_node. class Parser + include Dig + REGEX = /\A\/(.+)\/([imxnesu]*)\z|\A%r{(.+)}([imxnesu]*)\z/ - def initialize(node) + def initialize(node, options) @_current_node = node @_expr_map = {} + @options = options end # parse will parse an expression in the following way. @@ -91,7 +94,7 @@ def parse_exp(exp) el = if elements.empty? @_current_node elsif @_current_node.is_a?(Hash) - @_current_node.dig(*elements) + dig(@_current_node, *elements) else elements.inject(@_current_node, &:__send__) end diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index 190e3a5..849e976 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -130,6 +130,50 @@ def test_works_on_non_hash assert_equal ['value'], JsonPath.new('$.b').on(object) end + def test_works_on_object + klass = Class.new{ + attr_reader :b + def initialize(b) + @b = b + end + } + object = klass.new("value") + + assert_equal ["value"], JsonPath.new('$.b').on(object) + end + + def test_works_on_object_can_be_disabled + klass = Class.new{ + attr_reader :b + def initialize(b) + @b = b + end + } + object = klass.new("value") + + assert_equal [], JsonPath.new('$.b', allow_send: false).on(object) + end + + def test_works_on_diggable + klass = Class.new{ + attr_reader :h + def initialize(h) + @h = h + end + def dig(*keys) + @h.dig(*keys) + end + } + + object = klass.new('a' => 'some', 'b' => 'value') + assert_equal ['value'], JsonPath.new('$.b').on(object) + + object = { + "foo" => klass.new('a' => 'some', 'b' => 'value') + } + assert_equal ['value'], JsonPath.new('$.foo.b').on(object) + end + def test_works_on_non_hash_with_filters klass = Struct.new(:a, :b) first_object = klass.new('some', 'value') @@ -138,6 +182,24 @@ def test_works_on_non_hash_with_filters assert_equal ['other value'], JsonPath.new('$[?(@.a == "next")].b').on([first_object, second_object]) end + def test_works_on_hash_with_summary + object = { + "foo" => [{ + "a" => "some", + "b" => "value" + }] + } + assert_equal [{ "b" => "value" }], JsonPath.new("$.foo[*](b)").on(object) + end + + def test_works_on_non_hash_with_summary + klass = Struct.new(:a, :b) + object = { + "foo" => [klass.new("some", "value")] + } + assert_equal [{ "b" => "value" }], JsonPath.new("$.foo[*](b)").on(object) + end + def test_recognize_array_with_evald_index assert_equal [@object['store']['book'][2]], JsonPath.new('$..book[(@.length-5)]').on(@object) end @@ -849,7 +911,34 @@ def test_nested_values assert_equal [], JsonPath.on(json, "$.phoneNumbers[?(@[0].type == 'home')]") end - def test_selecting_multiple_keys + def test_selecting_multiple_keys_on_hash + json = ' + { + "category": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95 + } + '.to_json + assert_equal [{ 'category' => 'reference', 'author' => 'Nigel Rees' }], JsonPath.on(json, '$.(category,author)') + end + + def test_selecting_multiple_keys_on_sub_hash + skip("Failing as the semantics of .(x,y) is unclear") + json = ' + { + "book": { + "category": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95 + } + } + '.to_json + assert_equal [{ 'category' => 'reference', 'author' => 'Nigel Rees' }], JsonPath.on(json, '$.book.(category,author)') + end + + def test_selecting_multiple_keys_on_array json = ' { "store": { @@ -874,7 +963,7 @@ def test_selecting_multiple_keys assert_equal [{ 'category' => 'reference', 'author' => 'Nigel Rees' }, { 'category' => 'fiction', 'author' => 'Evelyn Waugh' }], JsonPath.on(json, '$.store.book[*](category,author)') end - def test_selecting_multiple_keys_with_filter + def test_selecting_multiple_keys_on_array_with_filter json = ' { "store": { @@ -925,6 +1014,32 @@ def test_selecting_multiple_keys_with_filter_with_space_in_catergory assert_equal [{ 'cate gory' => 'reference', 'author' => 'Nigel Rees' }], JsonPath.on(json, "$.store.book[?(@['price'] == 8.95)]( cate gory, author )") end + def test_use_symbol_opt + json = { + store: { + book: [ + { + category: "reference", + author: "Nigel Rees", + title: "Sayings of the Century", + price: 8.95 + }, + { + category: "fiction", + author: "Evelyn Waugh", + title: "Sword of Honour", + price: 12.99 + } + ] + } + } + on = ->(path){ JsonPath.on(json, path, use_symbols: true) } + assert_equal ['reference', 'fiction'], on.("$.store.book[*].category") + assert_equal ['reference', 'fiction'], on.("$..category") + assert_equal ['reference'], on.("$.store.book[?(@['price'] == 8.95)].category") + assert_equal [{'category' => 'reference'}], on.("$.store.book[?(@['price'] == 8.95)](category)") + end + def test_object_method_send j = {height: 5, hash: "some_hash"}.to_json hs = JsonPath.new "$..send" @@ -945,6 +1060,44 @@ def test_index_access_by_number assert_equal ['foo'], JsonPath.new('$.1').on(data.to_json) end + def test_behavior_on_null_and_missing + data = { + "foo" => nil, + "bar" => { + "baz" => nil + }, + "bars" => [ + { "foo" => 12 }, + { "foo" => nil }, + { } + ] + } + assert_equal [nil], JsonPath.new('$.foo').on(data) + assert_equal [nil], JsonPath.new('$.bar.baz').on(data) + assert_equal [], JsonPath.new('$.baz').on(data) + assert_equal [], JsonPath.new('$.bar.foo').on(data) + assert_equal [12, nil], JsonPath.new('$.bars[*].foo').on(data) + end + + def test_default_path_leaf_to_null_opt + data = { + "foo" => nil, + "bar" => { + "baz" => nil + }, + "bars" => [ + { "foo" => 12 }, + { "foo" => nil }, + { } + ] + } + assert_equal [nil], JsonPath.new('$.foo', default_path_leaf_to_null: true).on(data) + assert_equal [nil], JsonPath.new('$.bar.baz', default_path_leaf_to_null: true).on(data) + assert_equal [nil], JsonPath.new('$.baz', default_path_leaf_to_null: true).on(data) + assert_equal [nil], JsonPath.new('$.bar.foo', default_path_leaf_to_null: true).on(data) + assert_equal [12, nil, nil], JsonPath.new('$.bars[*].foo', default_path_leaf_to_null: true).on(data) + end + def example_object { 'store' => { 'book' => [ diff --git a/test/test_readme.rb b/test/test_readme.rb new file mode 100644 index 0000000..ab5deb4 --- /dev/null +++ b/test/test_readme.rb @@ -0,0 +1,117 @@ +# frozen_string_literal: true + +require 'minitest/autorun' +require 'phocus' +require 'jsonpath' +require 'json' + +class TestJsonpathReadme < MiniTest::Unit::TestCase + + def setup + @json = <<-HERE_DOC + {"store": + {"bicycle": + {"price":19.95, "color":"red"}, + "book":[ + {"price":8.95, "category":"reference", "title":"Sayings of the Century", "author":"Nigel Rees"}, + {"price":12.99, "category":"fiction", "title":"Sword of Honour", "author":"Evelyn Waugh"}, + {"price":8.99, "category":"fiction", "isbn":"0-553-21311-3", "title":"Moby Dick", "author":"Herman Melville","color":"blue"}, + {"price":22.99, "category":"fiction", "isbn":"0-395-19395-8", "title":"The Lord of the Rings", "author":"Tolkien"} + ] + } + } + HERE_DOC + end + attr_reader :json + + def test_library_section + path = JsonPath.new('$..price') + assert_equal [19.95, 8.95, 12.99, 8.99, 22.99], path.on(json) + assert_equal [18.88], path.on('{"books":[{"title":"A Tale of Two Somethings","price":18.88}]}') + assert_equal ["Nigel Rees", "Evelyn Waugh", "Herman Melville", "Tolkien"], JsonPath.on(json, '$..author') + assert_equal [ + {"price" => 8.95, "category" => "reference", "title" => "Sayings of the Century", "author" => "Nigel Rees"}, + {"price" => 8.99, "category" => "fiction", "isbn" => "0-553-21311-3", "title" => "Moby Dick", "author" => "Herman Melville","color" => "blue"}, + ], JsonPath.new('$..book[::2]').on(json) + assert_equal [8.95, 8.99], JsonPath.new("$..price[?(@ < 10)]").on(json) + assert_equal ["Sayings of the Century", "Moby Dick"], JsonPath.new("$..book[?(@['price'] == 8.95 || @['price'] == 8.99)].title").on(json) + assert_equal [], JsonPath.new("$..book[?(@['price'] == 8.95 && @['price'] == 8.99)].title").on(json) + assert_equal "red", JsonPath.new('$..color').first(json) + end + + def test_library_section_enumerable + enum = JsonPath.new('$..color')[json] + assert_equal "red", enum.first + assert enum.any?{ |c| c == 'red' } + end + + def test_ruby_structures_section + book = { title: "Sayings of the Century" } + assert_equal [], JsonPath.new('$.title').on(book) + assert_equal ["Sayings of the Century"], JsonPath.new('$.title', use_symbols: true).on(book) + + book_class = Struct.new(:title) + book = book_class.new("Sayings of the Century") + assert_equal ["Sayings of the Century"], JsonPath.new('$.title').on(book) + + book_class = Class.new{ attr_accessor :title } + book = book_class.new + book.title = "Sayings of the Century" + assert_equal ["Sayings of the Century"], JsonPath.new('$.title', allow_send: true).on(book) + end + + def test_options_section + assert_equal ["0-553-21311-3", "0-395-19395-8"], JsonPath.new('$..book[*].isbn').on(json) + assert_equal [nil, nil, "0-553-21311-3", "0-395-19395-8"], JsonPath.new('$..book[*].isbn', default_path_leaf_to_null: true).on(json) + + assert_equal ["price", "category", "title", "author"], JsonPath.new('$..book[0]').on(json).map(&:keys).flatten.uniq + assert_equal [:price, :category, :title, :author], JsonPath.new('$..book[0]').on(json, symbolize_keys: true).map(&:keys).flatten.uniq + end + + def selecting_value_section + json = <<-HERE_DOC + { + "store": { + "book": [ + { + "category": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95 + }, + { + "category": "fiction", + "author": "Evelyn Waugh", + "title": "Sword of Honour", + "price": 12.99 + } + ] + } + HERE_DOC + got = JsonPath.on(json, "$.store.book[*](category,author)") + expected = [ + { + "category" => "reference", + "author" => "Nigel Rees" + }, + { + "category" => "fiction", + "author" => "Evelyn Waugh" + } + ] + assert_equal expected, got + end + + def test_manipulation_section + assert_equal({"candy" => "big turks"}, JsonPath.for('{"candy":"lollipop"}').gsub('$..candy') {|v| "big turks" }.to_hash) + + json = '{"candy":"lollipop","noncandy":null,"other":"things"}' + o = JsonPath.for(json). + gsub('$..candy') {|v| "big turks" }. + compact. + delete('$..other'). + to_hash + assert_equal({"candy" => "big turks"}, o) + end + +end From 5fc71ccb4cb940e434ea869770f3aafcfc8495d8 Mon Sep 17 00:00:00 2001 From: Brautigam Gergely Date: Mon, 28 Dec 2020 11:19:31 +0100 Subject: [PATCH 079/120] bumped version for release --- lib/jsonpath/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonpath/version.rb b/lib/jsonpath/version.rb index c8528aa..dc9e402 100644 --- a/lib/jsonpath/version.rb +++ b/lib/jsonpath/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true class JsonPath - VERSION = '1.0.7' + VERSION = '1.1.0' end From 3ea11b04d094a8ebe3cc4718d859b858705e0b07 Mon Sep 17 00:00:00 2001 From: mohanapriya2308 <57559402+mohanapriya2308@users.noreply.github.com> Date: Sat, 15 May 2021 17:05:18 +0530 Subject: [PATCH 080/120] update json_path.rb --- lib/jsonpath.rb | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/lib/jsonpath.rb b/lib/jsonpath.rb index e976d4b..35eee29 100644 --- a/lib/jsonpath.rb +++ b/lib/jsonpath.rb @@ -86,6 +86,39 @@ def on(obj_or_str, opts = {}) end a end + + def fetch_all_path(obj) + all_paths = ['$'] + find_path(obj, '$', all_paths, obj.class == Array) + return all_paths + end + + def find_path(obj, root_key, all_paths, is_array = false) + obj.each do |key, value| + table_params = { key: key, root_key: root_key} + is_loop = value.class == Array || value.class == Hash + if is_loop + path_exp = construct_path(table_params) + all_paths << path_exp + find_path(value, path_exp, all_paths, value.class == Array) + elsif is_array + table_params[:index] = obj.find_index(key) + path_exp = construct_path(table_params) + find_path(key, path_exp, all_paths, key.class == Array) if key.class == Hash || key.class == Array + all_paths << path_exp + else + all_paths << construct_path(table_params) + end + end + end + + def construct_path(table_row) + if table_row[:index] + return table_row[:root_key] + '['+ table_row[:index].to_s + ']' + else + return table_row[:root_key] + '.'+ table_row[:key] + end + end def first(obj_or_str, *args) enum_on(obj_or_str).first(*args) From b1b2f712fa234ab307e1f18c555d1f7b12339646 Mon Sep 17 00:00:00 2001 From: mohanapriya2308 <57559402+mohanapriya2308@users.noreply.github.com> Date: Sat, 15 May 2021 17:06:33 +0530 Subject: [PATCH 081/120] test cases --- test/test_jsonpath.rb | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index 849e976..181fcb6 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -1152,4 +1152,19 @@ def example_object '_links' => { 'self' => {} } } } end + + def test_fetch_all_paths + data = { + "foo" => nil, + "bar" => { + "baz" => nil + }, + "bars" => [ + { "foo" => 12 }, + { "foo" => nil }, + { } + ] + } + assert_equal ["$", "$.foo", "$.bar", "$.bar.baz", "$.bars", "$.bars[0].foo", "$.bars[0]", "$.bars[1].foo", "$.bars[1]", "$.bars[2]"], JsonPath.fetch_all_path(data) + end end From 5855ee5e4bbc4a564d8f548633f56d554c91a9ad Mon Sep 17 00:00:00 2001 From: mohanapriya2308 <57559402+mohanapriya2308@users.noreply.github.com> Date: Sat, 15 May 2021 17:21:42 +0530 Subject: [PATCH 082/120] Update README.md --- README.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/README.md b/README.md index 32cb162..46dffb7 100644 --- a/README.md +++ b/README.md @@ -253,6 +253,42 @@ o = JsonPath.for(json). # => {"candy" => "big turks"} ``` +### Fetch all paths + +To fetch all possible paths in given json, you can use `fetch_all_paths` method. + +data: + +```bash +{ + "store": { + "book": [ + { + "category": "reference", + "author": "Nigel Rees" + }, + { + "category": "fiction", + "author": "Evelyn Waugh" + } + ] +} +``` + +... and this query: + +```ruby +JsonPath.fetch_all_path(data) +``` + +... the result will be: + +```bash +["$", "$.store", "$.store.book", "$.store.book[0].category", "$.store.book[0].author", "$.store.book[0]", "$.store.book[1].category", "$.store.book[1].author", "$.store.book[1]"] +``` + + + # Contributions Please feel free to submit an Issue or a Pull Request any time you feel like From f9535a15d7707cccf1efcee9636405f600df7fb0 Mon Sep 17 00:00:00 2001 From: mohanapriya2308 <57559402+mohanapriya2308@users.noreply.github.com> Date: Thu, 20 May 2021 20:30:36 +0530 Subject: [PATCH 083/120] test case update --- test/test_jsonpath.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index 181fcb6..b977bde 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -1153,7 +1153,7 @@ def example_object } } end - def test_fetch_all_paths + def test_fetch_all_path data = { "foo" => nil, "bar" => { From b0d0c40605db4f9ca61a25fbf9c8e10549685b03 Mon Sep 17 00:00:00 2001 From: mohanapriya2308 <57559402+mohanapriya2308@users.noreply.github.com> Date: Thu, 20 May 2021 21:21:52 +0530 Subject: [PATCH 084/120] referencing self in the methods --- lib/jsonpath.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/jsonpath.rb b/lib/jsonpath.rb index 35eee29..e4d7a59 100644 --- a/lib/jsonpath.rb +++ b/lib/jsonpath.rb @@ -87,13 +87,13 @@ def on(obj_or_str, opts = {}) a end - def fetch_all_path(obj) + def self.fetch_all_path(obj) all_paths = ['$'] find_path(obj, '$', all_paths, obj.class == Array) return all_paths end - def find_path(obj, root_key, all_paths, is_array = false) + def self.find_path(obj, root_key, all_paths, is_array = false) obj.each do |key, value| table_params = { key: key, root_key: root_key} is_loop = value.class == Array || value.class == Hash @@ -112,7 +112,7 @@ def find_path(obj, root_key, all_paths, is_array = false) end end - def construct_path(table_row) + def self.construct_path(table_row) if table_row[:index] return table_row[:root_key] + '['+ table_row[:index].to_s + ']' else From 3126545485267136c9cbfb849d39df4e5ad204e1 Mon Sep 17 00:00:00 2001 From: mohanapriya2308 <57559402+mohanapriya2308@users.noreply.github.com> Date: Thu, 20 May 2021 22:05:12 +0530 Subject: [PATCH 085/120] dummy commit --- test/test_jsonpath.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index b977bde..f42a306 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -1154,7 +1154,7 @@ def example_object end def test_fetch_all_path - data = { + dataa = { "foo" => nil, "bar" => { "baz" => nil @@ -1165,6 +1165,6 @@ def test_fetch_all_path { } ] } - assert_equal ["$", "$.foo", "$.bar", "$.bar.baz", "$.bars", "$.bars[0].foo", "$.bars[0]", "$.bars[1].foo", "$.bars[1]", "$.bars[2]"], JsonPath.fetch_all_path(data) + assert_equal ["$", "$.foo", "$.bar", "$.bar.baz", "$.bars", "$.bars[0].foo", "$.bars[0]", "$.bars[1].foo", "$.bars[1]", "$.bars[2]"], JsonPath.fetch_all_path(dataa) end end From c92b65dccdcd2f32feafb2e4c5158177b193f0c2 Mon Sep 17 00:00:00 2001 From: Mohana Date: Mon, 21 Jun 2021 20:33:36 +0530 Subject: [PATCH 086/120] update *.gemspec --- jsonpath.gemspec | 1 + 1 file changed, 1 insertion(+) diff --git a/jsonpath.gemspec b/jsonpath.gemspec index 0158718..eb21f8f 100644 --- a/jsonpath.gemspec +++ b/jsonpath.gemspec @@ -23,5 +23,6 @@ Gem::Specification.new do |s| s.add_development_dependency 'code_stats' s.add_development_dependency 'minitest', '~> 2.2.0' s.add_development_dependency 'phocus' + s.add_development_dependency 'racc' s.add_development_dependency 'rake' end From b4b765be5e74e64d472226775a6fc72cb138ec44 Mon Sep 17 00:00:00 2001 From: Mohana Date: Mon, 21 Jun 2021 22:46:01 +0530 Subject: [PATCH 087/120] Revert "update *.gemspec" This reverts commit c92b65dccdcd2f32feafb2e4c5158177b193f0c2. --- jsonpath.gemspec | 1 - 1 file changed, 1 deletion(-) diff --git a/jsonpath.gemspec b/jsonpath.gemspec index eb21f8f..0158718 100644 --- a/jsonpath.gemspec +++ b/jsonpath.gemspec @@ -23,6 +23,5 @@ Gem::Specification.new do |s| s.add_development_dependency 'code_stats' s.add_development_dependency 'minitest', '~> 2.2.0' s.add_development_dependency 'phocus' - s.add_development_dependency 'racc' s.add_development_dependency 'rake' end From 60f21521a12b749900f4f62d5c4ba0b6abae287e Mon Sep 17 00:00:00 2001 From: mohanapriya2308 <57559402+mohanapriya2308@users.noreply.github.com> Date: Mon, 21 Jun 2021 22:59:47 +0530 Subject: [PATCH 088/120] dummy commit --- test/test_jsonpath.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index f42a306..b977bde 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -1154,7 +1154,7 @@ def example_object end def test_fetch_all_path - dataa = { + data = { "foo" => nil, "bar" => { "baz" => nil @@ -1165,6 +1165,6 @@ def test_fetch_all_path { } ] } - assert_equal ["$", "$.foo", "$.bar", "$.bar.baz", "$.bars", "$.bars[0].foo", "$.bars[0]", "$.bars[1].foo", "$.bars[1]", "$.bars[2]"], JsonPath.fetch_all_path(dataa) + assert_equal ["$", "$.foo", "$.bar", "$.bar.baz", "$.bars", "$.bars[0].foo", "$.bars[0]", "$.bars[1].foo", "$.bars[1]", "$.bars[2]"], JsonPath.fetch_all_path(data) end end From 7c08124c5615664205d86545255a0591866a1841 Mon Sep 17 00:00:00 2001 From: Mohana Date: Mon, 21 Jun 2021 23:29:15 +0530 Subject: [PATCH 089/120] Update jsonpath.gemspec --- jsonpath.gemspec | 1 + 1 file changed, 1 insertion(+) diff --git a/jsonpath.gemspec b/jsonpath.gemspec index 0158718..eb21f8f 100644 --- a/jsonpath.gemspec +++ b/jsonpath.gemspec @@ -23,5 +23,6 @@ Gem::Specification.new do |s| s.add_development_dependency 'code_stats' s.add_development_dependency 'minitest', '~> 2.2.0' s.add_development_dependency 'phocus' + s.add_development_dependency 'racc' s.add_development_dependency 'rake' end From 8fd2cc579982789fe1d00e0a7d9141acd025d4a7 Mon Sep 17 00:00:00 2001 From: Ethan Date: Fri, 21 Jan 2022 04:24:05 -0800 Subject: [PATCH 090/120] replace travis with github workflow for test CI --- .github/workflows/test.yml | 31 +++++++++++++++++++++++++++++++ .travis.yml | 8 -------- 2 files changed, 31 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/test.yml delete mode 100644 .travis.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..09e92c2 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,31 @@ +name: test +on: +- push +- pull_request +jobs: + test: + strategy: + fail-fast: false + matrix: + ruby-version: + - '2.5' + - '2.6' + - '2.7' + - ruby-head + - jruby-head + - truffleruby-head + runs-on: + - ubuntu-latest + + runs-on: ${{ matrix.runs-on }} + + steps: + + - uses: actions/checkout@v2 + + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby-version }} + bundler-cache: true + + - run: bundle exec rake test diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 6febcb2..0000000 --- a/.travis.yml +++ /dev/null @@ -1,8 +0,0 @@ -language: ruby -rvm: - - 2.5 - - 2.6 - - 2.7 - - ruby-head - - jruby-head - - truffleruby-head From 0e4c7ba8ebfe4accd701027915331e67158ea245 Mon Sep 17 00:00:00 2001 From: BK Date: Tue, 8 Mar 2022 13:26:41 -0500 Subject: [PATCH 091/120] Add Option to configure max_nesting --- lib/jsonpath.rb | 7 ++++--- test/test_jsonpath.rb | 26 ++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/lib/jsonpath.rb b/lib/jsonpath.rb index e976d4b..13faa03 100644 --- a/lib/jsonpath.rb +++ b/lib/jsonpath.rb @@ -17,7 +17,8 @@ class JsonPath :default_path_leaf_to_null => false, :symbolize_keys => false, :use_symbols => false, - :allow_send => true + :allow_send => true, + :max_nesting => 100 } attr_accessor :path @@ -107,8 +108,8 @@ def self.for(obj_or_str) private - def self.process_object(obj_or_str) - obj_or_str.is_a?(String) ? MultiJson.decode(obj_or_str) : obj_or_str + def self.process_object(obj_or_str, opts = {}) + obj_or_str.is_a?(String) ? MultiJson.decode(obj_or_str, max_nesting: opts[:max_nesting]) : obj_or_str end def deep_clone diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index 849e976..6c7eb3e 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -1098,6 +1098,32 @@ def test_default_path_leaf_to_null_opt assert_equal [12, nil, nil], JsonPath.new('$.bars[*].foo', default_path_leaf_to_null: true).on(data) end + def test_raise_max_nesting_error + json = { + a: { + b: { + c: { + } + } + } + }.to_json + + assert_raises(MultiJson::ParseError) { JsonPath.new('$.a', max_nesting: 1).on(json) } + end + + def test_with_max_nesting_false + json = { + a: { + b: { + c: { + } + } + } + }.to_json + + assert_equal [{}], JsonPath.new('$.a.b.c', max_nesting: false).on(json) + end + def example_object { 'store' => { 'book' => [ From 10816489e2ae91fa1bd88578e6fe1f2c0635f0d9 Mon Sep 17 00:00:00 2001 From: BK Date: Fri, 11 Mar 2022 08:48:00 -0500 Subject: [PATCH 092/120] Fix Test --- lib/jsonpath.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonpath.rb b/lib/jsonpath.rb index 13faa03..fc904b7 100644 --- a/lib/jsonpath.rb +++ b/lib/jsonpath.rb @@ -93,7 +93,7 @@ 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, + JsonPath::Enumerable.new(self, self.class.process_object(obj_or_str, @opts), mode, @opts) end alias [] enum_on From 083d858678b6fff71e9411e6d35d4e1ccc8d5b6e Mon Sep 17 00:00:00 2001 From: Fraser Hanson Date: Fri, 22 Apr 2022 17:09:25 -0700 Subject: [PATCH 093/120] bug #144: dn not convert expressions (eg. "@.isTrue") to alternate representation ("@['isTrue']") before parsing. This caused incorrect results for boolean expressions. --- lib/jsonpath/enumerable.rb | 3 --- test/test_jsonpath.rb | 19 +++++++++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/lib/jsonpath/enumerable.rb b/lib/jsonpath/enumerable.rb index 7a6f270..62dd71f 100644 --- a/lib/jsonpath/enumerable.rb +++ b/lib/jsonpath/enumerable.rb @@ -151,9 +151,6 @@ def process_function_or_literal(exp, default = nil) identifiers = /@?((? true, 'name' => 'testname1' }], JsonPath.new('$.data[?(@.isTrue)]').on(data) end + def test_and_operator_with_boolean_parameter_value + data = { + 'data' => [{ + 'hasProperty1' => true, + 'hasProperty2' => false, + 'name' => 'testname1' + }, { + 'hasProperty1' => false, + 'hasProperty2' => true, + 'name' => 'testname2' + }, { + 'hasProperty1' => true, + 'hasProperty2' => true, + 'name' => 'testname3' + }] + } + assert_equal ['testname3'], JsonPath.new('$.data[?(@.hasProperty1 && @.hasProperty2)].name').on(data) + end + def test_regex_simple assert_equal %w[asdf asdf2], JsonPath.new('$.store.book..tags[?(@ =~ /asdf/)]').on(@object) end From add40dfa6355d25322ac6ea4ce8a3fa0dfcdf0d2 Mon Sep 17 00:00:00 2001 From: Fraser Hanson Date: Fri, 22 Apr 2022 17:38:36 -0700 Subject: [PATCH 094/120] bug #145: when evaluating compound expressions, convert sub-expression result strings "true"/"false" into booleans before evaluation --- lib/jsonpath/parser.rb | 6 ++++-- test/test_jsonpath.rb | 7 +++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/jsonpath/parser.rb b/lib/jsonpath/parser.rb index 79feb8c..7838a8e 100644 --- a/lib/jsonpath/parser.rb +++ b/lib/jsonpath/parser.rb @@ -159,9 +159,11 @@ def parse_parentheses(str) res = bool_or_exp(top.shift) top.each_with_index do |item, index| if item == '&&' - res &&= top[index + 1] + next_value = bool_or_exp(top[index + 1]) + res &&= next_value elsif item == '||' - res ||= top[index + 1] + next_value = bool_or_exp(top[index + 1]) + res ||= next_value end end diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index 5bd18b6..801fc15 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -80,10 +80,17 @@ def test_recognize_filters def test_or_operator assert_equal [@object['store']['book'][1], @object['store']['book'][3]], JsonPath.new("$..book[?(@['price'] == 13 || @['price'] == 23)]").on(@object) + result = ["Sayings of the Century", "Sword of Honour", "Moby Dick", "The Lord of the Rings"] + assert_equal result, JsonPath.new("$..book[?(@.price==13 || @.price==9 || @.price==23)].title").on(@object) + assert_equal result, JsonPath.new("$..book[?(@.price==9 || @.price==23 || @.price==13)].title").on(@object) + assert_equal result, JsonPath.new("$..book[?(@.price==23 || @.price==13 || @.price==9)].title").on(@object) end def test_and_operator assert_equal [], JsonPath.new("$..book[?(@['price'] == 13 && @['price'] == 23)]").on(@object) + assert_equal [], JsonPath.new("$..book[?(@.price==13 && @.category==fiction && @.title==no_match)]").on(@object) + assert_equal [], JsonPath.new("$..book[?(@.title==no_match && @.category==fiction && @.price==13)]").on(@object) + assert_equal [], JsonPath.new("$..book[?(@.price==13 && @.title==no_match && @.category==fiction)]").on(@object) end def test_and_operator_with_more_results From 0cc23fae4a9baaade2f97fcf4d62eb0e2c4c99aa Mon Sep 17 00:00:00 2001 From: Joshua Hull Date: Sat, 23 Apr 2022 09:15:04 +0200 Subject: [PATCH 095/120] Bump version --- lib/jsonpath/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonpath/version.rb b/lib/jsonpath/version.rb index dc9e402..ccad52b 100644 --- a/lib/jsonpath/version.rb +++ b/lib/jsonpath/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true class JsonPath - VERSION = '1.1.0' + VERSION = '1.1.1' end From 6f3f441e52845c64ee8f2b38c0c1045a8d4e9717 Mon Sep 17 00:00:00 2001 From: Fraser Hanson Date: Sun, 24 Apr 2022 13:18:31 -0700 Subject: [PATCH 096/120] bug #147: stop at character '!' when scanning key name, to avoid consuming first char of operator '!=' --- lib/jsonpath/parser.rb | 2 +- test/test_jsonpath.rb | 61 +++++++++++++++++++++++++++++++++++------- 2 files changed, 52 insertions(+), 11 deletions(-) diff --git a/lib/jsonpath/parser.rb b/lib/jsonpath/parser.rb index 7838a8e..84e0c47 100644 --- a/lib/jsonpath/parser.rb +++ b/lib/jsonpath/parser.rb @@ -68,7 +68,7 @@ def parse_exp(exp) scanner = StringScanner.new(exp) elements = [] until scanner.eos? - if (t = scanner.scan(/\['[a-zA-Z@&*\/$%^?_]+'\]|\.[a-zA-Z0-9_]+[?!]?/)) + if (t = scanner.scan(/\['[a-zA-Z@&*\/$%^?_]+'\]|\.[a-zA-Z0-9_]+[?]?/)) elements << t.gsub(/[\[\]'.]|\s+/, '') elsif (t = scanner.scan(/(\s+)?[<>=!\-+][=~]?(\s+)?/)) operator = t diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index 8d260d3..91e7026 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -70,12 +70,20 @@ def test_recognize_filters assert_equal [@object['store']['book'][0], @object['store']['book'][2]], JsonPath.new("$..book[?(@['price'] < 10)]").on(@object) assert_equal [@object['store']['book'][0], @object['store']['book'][2]], JsonPath.new("$..book[?(@['price'] == 9)]").on(@object) assert_equal [@object['store']['book'][3]], JsonPath.new("$..book[?(@['price'] > 20)]").on(@object) - assert_equal [ - @object['store']['book'][0], - @object['store']['book'][4], - @object['store']['book'][5], - @object['store']['book'][6] - ], JsonPath.new("$..book[?(@['category'] != 'fiction')]").on(@object) + end + + def test_not_equals_operator + expected = + [ + @object['store']['book'][0], + @object['store']['book'][4], + @object['store']['book'][5], + @object['store']['book'][6] + ] + assert_equal(expected, JsonPath.new("$..book[?(@['category'] != 'fiction')]").on(@object)) + assert_equal(expected, JsonPath.new("$..book[?(@['category']!=fiction)]").on(@object)) + assert_equal(expected, JsonPath.new("$..book[?(@.category!=fiction)]").on(@object)) + assert_equal(expected, JsonPath.new("$..book[?(@.category != 'fiction')]").on(@object)) end def test_or_operator @@ -86,15 +94,36 @@ def test_or_operator assert_equal result, JsonPath.new("$..book[?(@.price==23 || @.price==13 || @.price==9)].title").on(@object) end + def test_or_operator_with_not_equals + # Should be the same regardless of key style ( @.key vs @['key'] ) + result = ['Nigel Rees', 'Evelyn Waugh', 'Herman Melville', 'J. R. R. Tolkien', 'Lukyanenko'] + assert_equal result, JsonPath.new("$..book[?(@['title']=='Osennie Vizity' || @['author']!='Lukyanenko')].author").on(@object) + assert_equal result, JsonPath.new("$..book[?(@.title=='Osennie Vizity' || @.author != Lukyanenko )].author").on(@object) + assert_equal result, JsonPath.new("$..book[?(@.title=='Osennie Vizity' || @.author!=Lukyanenko )].author").on(@object) + end + def test_and_operator assert_equal [], JsonPath.new("$..book[?(@['price'] == 13 && @['price'] == 23)]").on(@object) - assert_equal [], JsonPath.new("$..book[?(@.price==13 && @.category==fiction && @.title==no_match)]").on(@object) + assert_equal [], JsonPath.new("$..book[?(@.price>=13 && @.category==fiction && @.title==no_match)]").on(@object) assert_equal [], JsonPath.new("$..book[?(@.title==no_match && @.category==fiction && @.price==13)]").on(@object) assert_equal [], JsonPath.new("$..book[?(@.price==13 && @.title==no_match && @.category==fiction)]").on(@object) + assert_equal [], JsonPath.new("$..book[?(@.price==13 && @.bad_key_name==true && @.category==fiction)]").on(@object) + + expected = [@object['store']['book'][1]] + assert_equal expected, JsonPath.new("$..book[?(@['price'] < 23 && @['price'] > 9)]").on(@object) + assert_equal expected, JsonPath.new("$..book[?(@.price < 23 && @.price > 9)]").on(@object) + + expected = ['Sword of Honour', 'The Lord of the Rings'] + assert_equal expected, JsonPath.new("$..book[?(@.price>=13 && @.category==fiction)].title").on(@object) + assert_equal ['The Lord of the Rings'], JsonPath.new("$..book[?(@.category==fiction && @.isbn && @.price>9)].title").on(@object) + assert_equal ['Sayings of the Century'], JsonPath.new("$..book[?(@['price'] == 9 && @.author=='Nigel Rees')].title").on(@object) + assert_equal ['Sayings of the Century'], JsonPath.new("$..book[?(@['price'] == 9 && @.tags..asdf)].title").on(@object) end - def test_and_operator_with_more_results - assert_equal [@object['store']['book'][1]], JsonPath.new("$..book[?(@['price'] < 23 && @['price'] > 9)]").on(@object) + def test_and_operator_with_not_equals + expected = ['Nigel Rees'] + assert_equal expected, JsonPath.new("$..book[?(@['price']==9 && @['category']!=fiction)].author").on(@object) + assert_equal expected, JsonPath.new("$..book[?(@.price==9 && @.category!=fiction)].author").on(@object) end def test_nested_grouping @@ -577,7 +606,18 @@ def test_boolean_parameter_value 'name' => 'testname2' }] } - assert_equal [{ 'isTrue' => true, 'name' => 'testname1' }], JsonPath.new('$.data[?(@.isTrue)]').on(data) + + # These queries should be equivalent + expected = [{ 'isTrue' => true, 'name' => 'testname1' }] + assert_equal expected, JsonPath.new('$.data[?(@.isTrue)]').on(data) + assert_equal expected, JsonPath.new('$.data[?(@.isTrue==true)]').on(data) + assert_equal expected, JsonPath.new('$.data[?(@.isTrue == true)]').on(data) + + # These queries should be equivalent + expected = [{ 'isTrue' => false, 'name' => 'testname2' }] + assert_equal expected, JsonPath.new('$.data[?(@.isTrue != true)]').on(data) + assert_equal expected, JsonPath.new('$.data[?(@.isTrue!=true)]').on(data) + assert_equal expected, JsonPath.new('$.data[?(@.isTrue==false)]').on(data) end def test_and_operator_with_boolean_parameter_value @@ -601,6 +641,7 @@ def test_and_operator_with_boolean_parameter_value def test_regex_simple assert_equal %w[asdf asdf2], JsonPath.new('$.store.book..tags[?(@ =~ /asdf/)]').on(@object) + assert_equal %w[asdf asdf2], JsonPath.new('$.store.book..tags[?(@=~/asdf/)]').on(@object) end def test_regex_simple_miss From 7fe0e5591a90d26bfe661afae5bbf76c1e405088 Mon Sep 17 00:00:00 2001 From: Joshua Hull Date: Sun, 24 Apr 2022 22:36:55 +0200 Subject: [PATCH 097/120] Remove rubocop for now --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 18ec7c8..f60c354 100644 --- a/Gemfile +++ b/Gemfile @@ -2,5 +2,5 @@ source 'http://rubygems.org' gemspec -gem 'rubocop', require: true, group: :test +# gem 'rubocop', require: true, group: :test gem 'simplecov', require: false, group: :test From 26f657089fba90261d7683982868f478c35f48be Mon Sep 17 00:00:00 2001 From: Joshua Hull Date: Sun, 24 Apr 2022 22:39:08 +0200 Subject: [PATCH 098/120] Bump version to 1.1.2 --- lib/jsonpath/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonpath/version.rb b/lib/jsonpath/version.rb index ccad52b..4b01dd4 100644 --- a/lib/jsonpath/version.rb +++ b/lib/jsonpath/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true class JsonPath - VERSION = '1.1.1' + VERSION = '1.1.2' end From c7d7f6b28bfcfad7618f19754795e7d9fb2b85f1 Mon Sep 17 00:00:00 2001 From: a5-stable Date: Sun, 26 Mar 2023 14:20:32 +0900 Subject: [PATCH 099/120] skip when node size is zero --- lib/jsonpath/enumerable.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/jsonpath/enumerable.rb b/lib/jsonpath/enumerable.rb index 62dd71f..94926cb 100644 --- a/lib/jsonpath/enumerable.rb +++ b/lib/jsonpath/enumerable.rb @@ -64,6 +64,7 @@ def handle_wildecard(node, expr, _context, _key, pos, &blk) else next if node.is_a?(Array) && node.empty? next if node.nil? # when default_path_leaf_to_null is true + next if node.size.zero? array_args = sub_path.split(':') if array_args[0] == '*' @@ -81,6 +82,7 @@ def handle_wildecard(node, expr, _context, _key, pos, &blk) 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) From b526cdc5279e6afdf32863d097b8f1e8d2d808ad Mon Sep 17 00:00:00 2001 From: Isaac Post Date: Fri, 31 Mar 2023 11:45:31 -0400 Subject: [PATCH 100/120] fix hanging on LF character --- lib/jsonpath.rb | 2 +- test/test_jsonpath.rb | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/jsonpath.rb b/lib/jsonpath.rb index 3b9ed49..fff66e1 100644 --- a/lib/jsonpath.rb +++ b/lib/jsonpath.rb @@ -44,7 +44,7 @@ def initialize(path, opts = {}) nil elsif (token = scanner.scan(/[><=] \d+/)) @path.last << token - elsif (token = scanner.scan(/./)) + elsif (token = scanner.scan(/./m)) begin @path.last << token rescue RuntimeError diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index 91e7026..ec47275 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -1178,6 +1178,10 @@ def test_raise_max_nesting_error assert_raises(MultiJson::ParseError) { JsonPath.new('$.a', max_nesting: 1).on(json) } end + def test_linefeed_in_path_error + assert_raises(ArgumentError) { JsonPath.new("$.store\n.book") } + end + def test_with_max_nesting_false json = { a: { From 268c67201998c106222de778f2f6c7f887e76df5 Mon Sep 17 00:00:00 2001 From: anu radha <47864372+anuravi98@users.noreply.github.com> Date: Mon, 1 May 2023 19:28:52 +0530 Subject: [PATCH 101/120] Jsonpath with keyname $ not working --- README.md | 2 +- lib/jsonpath/enumerable.rb | 6 +++++- test/test_jsonpath.rb | 7 +++++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 46dffb7..638a104 100644 --- a/README.md +++ b/README.md @@ -255,7 +255,7 @@ o = JsonPath.for(json). ### Fetch all paths -To fetch all possible paths in given json, you can use `fetch_all_paths` method. +To fetch all possible paths in given json, you can use `fetch_all_path`` method. data: diff --git a/lib/jsonpath/enumerable.rb b/lib/jsonpath/enumerable.rb index 62dd71f..b415b99 100644 --- a/lib/jsonpath/enumerable.rb +++ b/lib/jsonpath/enumerable.rb @@ -21,7 +21,11 @@ def each(context = @object, key = nil, pos = 0, &blk) when '*', '..', '@' each(context, key, pos + 1, &blk) when '$' - each(context, key, pos + 1, &blk) if node == @object + if node == @object + each(context, key, pos + 1, &blk) + else + handle_wildecard(node, "['#{expr}']", context, key, pos, &blk) + end when /^\[(.*)\]$/ handle_wildecard(node, expr, context, key, pos, &blk) when /\(.*\)/ diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index 91e7026..21f49f0 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -1260,4 +1260,11 @@ def test_fetch_all_path } assert_equal ["$", "$.foo", "$.bar", "$.bar.baz", "$.bars", "$.bars[0].foo", "$.bars[0]", "$.bars[1].foo", "$.bars[1]", "$.bars[2]"], JsonPath.fetch_all_path(data) end + + + def test_extractore_with_dollar_key + json = {"test" => {"$" =>"success", "a" => "123"}} + assert_equal ["success"], JsonPath.on(json, "$.test.$") + assert_equal ["123"], JsonPath.on(json, "$.test.a") + end end From 2c7cea84fb076eefd16314166adaafc74f8efd95 Mon Sep 17 00:00:00 2001 From: Joshua Hull Date: Tue, 9 May 2023 09:20:35 +0200 Subject: [PATCH 102/120] Fix method typo --- lib/jsonpath/enumerable.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/jsonpath/enumerable.rb b/lib/jsonpath/enumerable.rb index 33be4ac..75fb629 100644 --- a/lib/jsonpath/enumerable.rb +++ b/lib/jsonpath/enumerable.rb @@ -24,10 +24,10 @@ def each(context = @object, key = nil, pos = 0, &blk) if node == @object each(context, key, pos + 1, &blk) else - handle_wildecard(node, "['#{expr}']", context, key, pos, &blk) + handle_wildcard(node, "['#{expr}']", context, key, pos, &blk) end when /^\[(.*)\]$/ - handle_wildecard(node, expr, context, key, pos, &blk) + handle_wildcard(node, expr, context, key, pos, &blk) when /\(.*\)/ keys = expr.gsub(/[()]/, '').split(',').map(&:strip) new_context = filter_context(context, keys) @@ -55,7 +55,7 @@ def filter_context(context, keys) end end - def handle_wildecard(node, expr, _context, _key, pos, &blk) + def handle_wildcard(node, expr, _context, _key, pos, &blk) expr[1, expr.size - 2].split(',').each do |sub_path| case sub_path[0] when '\'', '"' From 804a5afa1b3e13cbc11ce4983fc61927b4e7da97 Mon Sep 17 00:00:00 2001 From: Joshua Hull Date: Tue, 9 May 2023 09:20:49 +0200 Subject: [PATCH 103/120] Handle any failure to match a token --- lib/jsonpath.rb | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/jsonpath.rb b/lib/jsonpath.rb index fff66e1..238a7bd 100644 --- a/lib/jsonpath.rb +++ b/lib/jsonpath.rb @@ -44,12 +44,10 @@ def initialize(path, opts = {}) nil elsif (token = scanner.scan(/[><=] \d+/)) @path.last << token - elsif (token = scanner.scan(/./m)) - begin - @path.last << token - rescue RuntimeError - raise ArgumentError, "character '#{token}' not supported in query" - end + elsif (token = scanner.scan(/./)) + @path.last << token + else + raise ArgumentError, "character '#{scanner.peek(1)}' not supported in query" end end end From 4e31ac9fc05eb309999903e674cd3a79763cb7a0 Mon Sep 17 00:00:00 2001 From: Joshua Hull Date: Tue, 9 May 2023 09:21:21 +0200 Subject: [PATCH 104/120] Bump version --- lib/jsonpath/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonpath/version.rb b/lib/jsonpath/version.rb index 4b01dd4..784eaf7 100644 --- a/lib/jsonpath/version.rb +++ b/lib/jsonpath/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true class JsonPath - VERSION = '1.1.2' + VERSION = '1.1.3' end From bae8ab08b528362d45f59584f9fd42f022cdddf0 Mon Sep 17 00:00:00 2001 From: mohanapriya2308 <57559402+mohanapriya2308@users.noreply.github.com> Date: Mon, 15 May 2023 15:58:48 +0530 Subject: [PATCH 105/120] Update enumerable.rb --- lib/jsonpath/enumerable.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonpath/enumerable.rb b/lib/jsonpath/enumerable.rb index 75fb629..7b553f7 100644 --- a/lib/jsonpath/enumerable.rb +++ b/lib/jsonpath/enumerable.rb @@ -155,7 +155,7 @@ def process_function_or_literal(exp, default = nil) return nil unless @_current_node identifiers = /@?((? Date: Mon, 15 May 2023 16:02:39 +0530 Subject: [PATCH 106/120] Update enumerable.rb --- lib/jsonpath/enumerable.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/jsonpath/enumerable.rb b/lib/jsonpath/enumerable.rb index 7b553f7..ebc9b11 100644 --- a/lib/jsonpath/enumerable.rb +++ b/lib/jsonpath/enumerable.rb @@ -163,7 +163,6 @@ def process_function_or_literal(exp, default = nil) return default end end - JsonPath::Parser.new(@_current_node, @options).parse(exp) end end end From 7aa2052704bee3b63013e87047226312d4d58603 Mon Sep 17 00:00:00 2001 From: mohanapriya2308 <57559402+mohanapriya2308@users.noreply.github.com> Date: Mon, 15 May 2023 18:16:18 +0530 Subject: [PATCH 107/120] filter elements with unknown key --- lib/jsonpath/enumerable.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/jsonpath/enumerable.rb b/lib/jsonpath/enumerable.rb index ebc9b11..29660ab 100644 --- a/lib/jsonpath/enumerable.rb +++ b/lib/jsonpath/enumerable.rb @@ -154,8 +154,9 @@ def process_function_or_literal(exp, default = nil) return Integer(exp) if exp[0] != '(' return nil unless @_current_node - identifiers = /@?((? Date: Mon, 15 May 2023 18:17:37 +0530 Subject: [PATCH 108/120] Update test_jsonpath.rb --- test/test_jsonpath.rb | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index db8a5e4..2c46162 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -885,12 +885,17 @@ def test_complex_nested_grouping path = "$..book[?((@['author'] == 'Evelyn Waugh' || @['author'] == 'Herman Melville') && (@['price'] == 33 || @['price'] == 9))]" assert_equal [@object['store']['book'][2]], JsonPath.new(path).on(@object) end + + def test_nested_with_unknown_key + path = "$..[?(@.price == 9 || @.price == 33)].title" + assert_equal ["Sayings of the Century", "Moby Dick", "Sayings of the Century", "Moby Dick"], JsonPath.new(path).on(@object) + end - def test_complex_nested_grouping_unmatched_parent - path = "$..book[?((@['author'] == 'Evelyn Waugh' || @['author'] == 'Herman Melville' && (@['price'] == 33 || @['price'] == 9))]" - err = assert_raises(ArgumentError, 'should have raised an exception') { JsonPath.new(path).on(@object) } - assert_match(/unmatched parenthesis in expression: \(\(false \|\| false && \(false \|\| true\)\)/, err.message) + def test_nested_with_unknown_key_filtered_array + path = "$..[?(@['price'] == 9 || @['price'] == 33)].title" + assert_equal ["Sayings of the Century", "Moby Dick", "Sayings of the Century", "Moby Dick"], JsonPath.new(path).on(@object) end + def test_runtime_error_frozen_string skip('in ruby version below 2.2.0 this error is not raised') if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.2.0') || Gem::Version.new(RUBY_VERSION) > Gem::Version::new('2.6') From 647829b6459d14858ab128370ef75fe26746eac5 Mon Sep 17 00:00:00 2001 From: mohanapriya2308 <57559402+mohanapriya2308@users.noreply.github.com> Date: Mon, 15 May 2023 18:19:24 +0530 Subject: [PATCH 109/120] dummy commit --- test/test_jsonpath.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index 2c46162..6940f20 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -896,7 +896,6 @@ def test_nested_with_unknown_key_filtered_array assert_equal ["Sayings of the Century", "Moby Dick", "Sayings of the Century", "Moby Dick"], JsonPath.new(path).on(@object) end - def test_runtime_error_frozen_string skip('in ruby version below 2.2.0 this error is not raised') if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.2.0') || Gem::Version.new(RUBY_VERSION) > Gem::Version::new('2.6') json = ' From 695630cafcdd48eb53f6013aec1ade36f349fd10 Mon Sep 17 00:00:00 2001 From: mohanapriya2308 <57559402+mohanapriya2308@users.noreply.github.com> Date: Mon, 15 May 2023 18:41:07 +0530 Subject: [PATCH 110/120] Update test.yml --- .github/workflows/test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 09e92c2..97a9c96 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,6 +11,9 @@ jobs: - '2.5' - '2.6' - '2.7' + - '3.2' + - '3.1' + - '3.0' - ruby-head - jruby-head - truffleruby-head From b524dee27d9cb02f08f9a8445b916149b5f9df3b Mon Sep 17 00:00:00 2001 From: Anupama Kumari Date: Wed, 5 Jul 2023 18:34:36 +0530 Subject: [PATCH 111/120] nesting of 101 too deep --- lib/jsonpath.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/jsonpath.rb b/lib/jsonpath.rb index 238a7bd..ee2cb3c 100644 --- a/lib/jsonpath.rb +++ b/lib/jsonpath.rb @@ -25,6 +25,7 @@ class JsonPath def initialize(path, opts = {}) @opts = DEFAULT_OPTIONS.merge(opts) + @opts[:max_nesting] = false if @opts[:max_nesting] > 100 scanner = StringScanner.new(path.strip) @path = [] until scanner.eos? From da4e1dc479b3c317eed459d7a6eec9eda21b6e4e Mon Sep 17 00:00:00 2001 From: Anupama Kumari Date: Wed, 5 Jul 2023 19:01:13 +0530 Subject: [PATCH 112/120] test cases --- lib/jsonpath.rb | 5 +++-- test/test_jsonpath.rb | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/lib/jsonpath.rb b/lib/jsonpath.rb index ee2cb3c..b4b6ce4 100644 --- a/lib/jsonpath.rb +++ b/lib/jsonpath.rb @@ -12,20 +12,21 @@ # into a token array. class JsonPath PATH_ALL = '$..*' + MAX_NESTING_ALLOWED = 100 DEFAULT_OPTIONS = { :default_path_leaf_to_null => false, :symbolize_keys => false, :use_symbols => false, :allow_send => true, - :max_nesting => 100 + :max_nesting => MAX_NESTING_ALLOWED } attr_accessor :path def initialize(path, opts = {}) @opts = DEFAULT_OPTIONS.merge(opts) - @opts[:max_nesting] = false if @opts[:max_nesting] > 100 + @opts[:max_nesting] = false if @opts[:max_nesting] > MAX_NESTING_ALLOWED scanner = StringScanner.new(path.strip) @path = [] until scanner.eos? diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index db8a5e4..bbc04a0 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -1195,6 +1195,21 @@ def test_with_max_nesting_false assert_equal [{}], JsonPath.new('$.a.b.c', max_nesting: false).on(json) end + def test_initialize_with_max_nesting_exceeding_limit + json = { + a: { + b: { + c: { + } + } + } + }.to_json + + json_obj = JsonPath.new('$.a.b.c', max_nesting: 105) + assert_equal [{}], json_obj.on(json) + assert_equal false, json_obj.instance_variable_get(:@opts)[:max_nesting] + end + def example_object { 'store' => { 'book' => [ From 50d0bbf997442c3e13122d6e76d6b4bb39999472 Mon Sep 17 00:00:00 2001 From: Anupama Kumari Date: Wed, 5 Jul 2023 19:27:04 +0530 Subject: [PATCH 113/120] other test cases --- lib/jsonpath.rb | 7 ++++++- test/test_jsonpath.rb | 10 ++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/lib/jsonpath.rb b/lib/jsonpath.rb index b4b6ce4..914b44f 100644 --- a/lib/jsonpath.rb +++ b/lib/jsonpath.rb @@ -26,7 +26,7 @@ class JsonPath def initialize(path, opts = {}) @opts = DEFAULT_OPTIONS.merge(opts) - @opts[:max_nesting] = false if @opts[:max_nesting] > MAX_NESTING_ALLOWED + set_max_nesting scanner = StringScanner.new(path.strip) @path = [] until scanner.eos? @@ -148,4 +148,9 @@ def self.process_object(obj_or_str, opts = {}) def deep_clone Marshal.load Marshal.dump(self) end + + def set_max_nesting + return unless @opts[:max_nesting].is_a?(Integer) && @opts[:max_nesting] > MAX_NESTING_ALLOWED + @opts[:max_nesting] = false + end end diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index bbc04a0..026cda6 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -1210,6 +1210,16 @@ def test_initialize_with_max_nesting_exceeding_limit assert_equal false, json_obj.instance_variable_get(:@opts)[:max_nesting] end + def test_initialize_without_max_nesting_exceeding_limit + json_obj = JsonPath.new('$.a.b.c', max_nesting: 90) + assert_equal 90, json_obj.instance_variable_get(:@opts)[:max_nesting] + end + + def test_initialize_with_max_nesting_false_limit + json_obj = JsonPath.new('$.a.b.c', max_nesting: false) + assert_equal false, json_obj.instance_variable_get(:@opts)[:max_nesting] + end + def example_object { 'store' => { 'book' => [ From 0225e64003f423218bdc60f78cbff63a797afe0f Mon Sep 17 00:00:00 2001 From: James Goodwin Date: Wed, 6 Sep 2023 19:50:07 -0400 Subject: [PATCH 114/120] Update member name regexp to support UTF8 alphabetic and numeric characters see: https://ruby-doc.org/3.0.6/Regexp.html#class-Regexp-label-Character+Properties --- lib/jsonpath.rb | 4 ++-- test/test_jsonpath.rb | 10 ++++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/jsonpath.rb b/lib/jsonpath.rb index 238a7bd..f0e5fe1 100644 --- a/lib/jsonpath.rb +++ b/lib/jsonpath.rb @@ -30,7 +30,7 @@ def initialize(path, opts = {}) until scanner.eos? if (token = scanner.scan(/\$\B|@\B|\*|\.\./)) @path << token - elsif (token = scanner.scan(/[$@a-zA-Z0-9:{}_-]+/)) + elsif (token = scanner.scan(/[$@\p{Alnum}:{}_-]+/)) @path << "['#{token}']" elsif (token = scanner.scan(/'(.*?)'/)) @path << "[#{token}]" @@ -85,7 +85,7 @@ def on(obj_or_str, opts = {}) end a end - + def self.fetch_all_path(obj) all_paths = ['$'] find_path(obj, '$', all_paths, obj.class == Array) diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index db8a5e4..5fcbdbb 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -245,7 +245,7 @@ def test_use_first end def test_counting - assert_equal 57, JsonPath.new('$..*').on(@object).to_a.size + assert_equal 58, JsonPath.new('$..*').on(@object).to_a.size end def test_space_in_path @@ -475,6 +475,11 @@ def test_support_underscore_in_member_names JsonPath.new('$.store._links').on(@object) end + def test_support_for_umlauts_in_member_names + assert_equal [@object['store']['Übermorgen']], + JsonPath.new('$.store.Übermorgen').on(@object) + end + def test_dig_return_string assert_equal ['asdf'], JsonPath.new("$.store.book..tags[?(@ == 'asdf')]").on(@object) assert_equal [], JsonPath.new("$.store.book..tags[?(@ == 'not_asdf')]").on(@object) @@ -1246,10 +1251,11 @@ def example_object }, '@id' => 'http://example.org/store/42', '$meta-data' => 'whatevs', + 'Übermorgen' => 'The day after tomorrow', '_links' => { 'self' => {} } } } end - + def test_fetch_all_path data = { "foo" => nil, From c0e0640e354ff1ea228a85fa32612a76e518d80f Mon Sep 17 00:00:00 2001 From: James Goodwin Date: Mon, 11 Sep 2023 21:21:36 -0400 Subject: [PATCH 115/120] Update member name regexp to allow spaces --- lib/jsonpath.rb | 2 +- test/test_jsonpath.rb | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/jsonpath.rb b/lib/jsonpath.rb index f0e5fe1..c832f15 100644 --- a/lib/jsonpath.rb +++ b/lib/jsonpath.rb @@ -30,7 +30,7 @@ def initialize(path, opts = {}) until scanner.eos? if (token = scanner.scan(/\$\B|@\B|\*|\.\./)) @path << token - elsif (token = scanner.scan(/[$@\p{Alnum}:{}_-]+/)) + elsif (token = scanner.scan(/[$@\p{Alnum}:{}_ -]+/)) @path << "['#{token}']" elsif (token = scanner.scan(/'(.*?)'/)) @path << "[#{token}]" diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index 5fcbdbb..1f9d688 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -245,7 +245,7 @@ def test_use_first end def test_counting - assert_equal 58, JsonPath.new('$..*').on(@object).to_a.size + assert_equal 59, JsonPath.new('$..*').on(@object).to_a.size end def test_space_in_path @@ -480,6 +480,11 @@ def test_support_for_umlauts_in_member_names JsonPath.new('$.store.Übermorgen').on(@object) end + def test_support_for_spaces_in_member_name + assert_equal [@object['store']['Title Case']], + JsonPath.new('$.store.Title Case').on(@object) + end + def test_dig_return_string assert_equal ['asdf'], JsonPath.new("$.store.book..tags[?(@ == 'asdf')]").on(@object) assert_equal [], JsonPath.new("$.store.book..tags[?(@ == 'not_asdf')]").on(@object) @@ -1252,6 +1257,7 @@ def example_object '@id' => 'http://example.org/store/42', '$meta-data' => 'whatevs', 'Übermorgen' => 'The day after tomorrow', + 'Title Case' => 'A title case string', '_links' => { 'self' => {} } } } end From 34ab4f38b2b20fdd8f97f394d6af51b4641695ae Mon Sep 17 00:00:00 2001 From: Joshua Hull Date: Wed, 27 Sep 2023 19:43:27 +0200 Subject: [PATCH 116/120] Drop 2.5 --- .github/workflows/test.yml | 1 - jsonpath.gemspec | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 97a9c96..6dbfe70 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,6 @@ jobs: fail-fast: false matrix: ruby-version: - - '2.5' - '2.6' - '2.7' - '3.2' diff --git a/jsonpath.gemspec b/jsonpath.gemspec index eb21f8f..9ecd57a 100644 --- a/jsonpath.gemspec +++ b/jsonpath.gemspec @@ -5,7 +5,7 @@ require File.join(File.dirname(__FILE__), 'lib', 'jsonpath', 'version') Gem::Specification.new do |s| s.name = 'jsonpath' s.version = JsonPath::VERSION - s.required_ruby_version = '>= 2.5' + s.required_ruby_version = '>= 2.6' 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/.' From 3102fb343f42cc2695eb3b0a5e5c28d57ba4bf57 Mon Sep 17 00:00:00 2001 From: Joshua Hull Date: Wed, 27 Sep 2023 19:45:08 +0200 Subject: [PATCH 117/120] Bump to 1.1.4 --- lib/jsonpath/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonpath/version.rb b/lib/jsonpath/version.rb index 784eaf7..13fbb40 100644 --- a/lib/jsonpath/version.rb +++ b/lib/jsonpath/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true class JsonPath - VERSION = '1.1.3' + VERSION = '1.1.4' end From 139787e7ab83d658ecb62b0ff9ad010a2dd45b49 Mon Sep 17 00:00:00 2001 From: Anupama Kumari Date: Tue, 10 Oct 2023 16:29:38 +0530 Subject: [PATCH 118/120] symbolize_keys option doesn't work as the README says --- lib/jsonpath.rb | 6 +++++- test/test_jsonpath.rb | 6 ++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/jsonpath.rb b/lib/jsonpath.rb index 0bd509d..0388f62 100644 --- a/lib/jsonpath.rb +++ b/lib/jsonpath.rb @@ -80,7 +80,7 @@ def join(join_path) def on(obj_or_str, opts = {}) a = enum_on(obj_or_str).to_a - if opts[:symbolize_keys] + if symbolize_keys?(opts) a.map! do |e| e.each_with_object({}) { |(k, v), memo| memo[k.to_sym] = v; } end @@ -153,4 +153,8 @@ def set_max_nesting return unless @opts[:max_nesting].is_a?(Integer) && @opts[:max_nesting] > MAX_NESTING_ALLOWED @opts[:max_nesting] = false end + + def symbolize_keys?(opts) + opts.fetch(:symbolize_keys, @opts&.dig(:symbolize_keys)) + end end diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index 5ab02d7..5ad999d 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -1312,4 +1312,10 @@ def test_extractore_with_dollar_key assert_equal ["success"], JsonPath.on(json, "$.test.$") assert_equal ["123"], JsonPath.on(json, "$.test.a") end + + def test_symbolize_key + data = { "store" => { "book" => [{"category" => "test"}]}} + assert_equal [{"category": "reference"}], JsonPath.new('$..book[0]', symbolize_keys: true).on(data) + assert_equal [{"category": "reference"}], JsonPath.new('$..book[0]').on(data, symbolize_keys: true) + end end From b8bd29f89d55ae7c6dcfd5c528228e4d0afd6776 Mon Sep 17 00:00:00 2001 From: Anupama Kumari Date: Tue, 10 Oct 2023 16:40:05 +0530 Subject: [PATCH 119/120] test sample correction --- test/test_jsonpath.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_jsonpath.rb b/test/test_jsonpath.rb index 5ad999d..80fa0bb 100644 --- a/test/test_jsonpath.rb +++ b/test/test_jsonpath.rb @@ -1314,7 +1314,7 @@ def test_extractore_with_dollar_key end def test_symbolize_key - data = { "store" => { "book" => [{"category" => "test"}]}} + data = { "store" => { "book" => [{"category" => "reference"}]}} assert_equal [{"category": "reference"}], JsonPath.new('$..book[0]', symbolize_keys: true).on(data) assert_equal [{"category": "reference"}], JsonPath.new('$..book[0]').on(data, symbolize_keys: true) end From 725f1873a164955a129e70cd92417414f89efe26 Mon Sep 17 00:00:00 2001 From: Joshua Hull Date: Thu, 12 Oct 2023 10:12:33 +0200 Subject: [PATCH 120/120] Bump version to 1.1.5 --- lib/jsonpath/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonpath/version.rb b/lib/jsonpath/version.rb index 13fbb40..db0675c 100644 --- a/lib/jsonpath/version.rb +++ b/lib/jsonpath/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true class JsonPath - VERSION = '1.1.4' + VERSION = '1.1.5' end