jsonpath-1.1.0/0000755000004100000410000000000014107722717013377 5ustar www-datawww-datajsonpath-1.1.0/.travis.yml0000644000004100000410000000013614107722717015510 0ustar www-datawww-datalanguage: ruby rvm: - 2.5 - 2.6 - 2.7 - ruby-head - jruby-head - truffleruby-head jsonpath-1.1.0/test/0000755000004100000410000000000014107722717014356 5ustar www-datawww-datajsonpath-1.1.0/test/test_jsonpath.rb0000644000004100000410000010347414107722717017601 0ustar www-datawww-data# frozen_string_literal: true require 'minitest/autorun' require 'phocus' require 'jsonpath' require 'json' 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_equal [9], JsonPath.new('$.store.book[0].price').on(@object) end def test_lookup_direct_path assert_equal 7, JsonPath.new('$.store.*').on(@object).first['book'].size end def test_lookup_missing_element assert_equal [], JsonPath.new('$.store.book[99].price').on(@object) end def test_retrieve_all_authors assert_equal [ @object['store']['book'][0]['author'], @object['store']['book'][1]['author'], @object['store']['book'][2]['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 def test_retrieve_all_prices assert_equal [ @object['store']['bicycle']['price'], @object['store']['book'][0]['price'], @object['store']['book'][1]['price'], @object['store']['book'][2]['price'], @object['store']['book'][3]['price'] ].sort, JsonPath.new('$..price').on(@object).sort end def test_recognize_array_splices 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) end def test_recognize_filters assert_equal [@object['store']['book'][2], @object['store']['book'][3]], JsonPath.new("$..book[?(@['isbn'])]").on(@object) 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 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_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 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 assert_equal [@object['store']['bicycle']['catalogue_number']], JsonPath.new('$.store.bicycle.catalogue_number').on(@object) end def test_path_with_hyphens 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 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_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_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') second_object = klass.new('next', 'other value') 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 def test_use_first assert_equal @object['store']['book'][2], JsonPath.new('$..book[(@.length-5)]').first(@object) end def test_counting assert_equal 57, 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') end 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 @object2['store']['book'][1]['price'] += 10 @object2['store']['book'][2]['price'] += 10 @object2['store']['book'][3]['price'] += 10 assert_equal @object2, JsonPath.for(@object).gsub('$..price') { |p| p + 10 }.to_hash end def test_gsub! JsonPath.for(@object).gsub!('$..price') { |p| p + 10 } assert_equal 30, @object['store']['bicycle']['price'] assert_equal 19, @object['store']['book'][0]['price'] assert_equal 23, @object['store']['book'][1]['price'] assert_equal 19, @object['store']['book'][2]['price'] assert_equal 33, @object['store']['book'][3]['price'] end def test_weird_gsub! 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 } JsonPath.for(h).compact! assert_equal({ 'hi' => 'there' }, h) end def test_delete h = { 'hi' => 'there', 'you' => nil } JsonPath.for(h).delete!('*.hi') 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]') 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' => [{ '@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') 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[*]') end 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) end 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 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("$.[?(@.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 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' }, { 'type' => 'not' }] } 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 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 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 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_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' => { '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 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 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_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) end def test_regex_error assert_raises ArgumentError do JsonPath.new('$.store.book..tags[?(@ =~ asdf)]').on(@object) end end def test_regression_1 json = { ok: true, channels: [ { id: 'C09C5GYHF', name: 'general' }, { id: 'C09C598QL', name: 'random' } ] }.to_json assert_equal 'C09C5GYHF', JsonPath.on(json, "$..channels[?(@.name == 'general')].id")[0] end def test_regression_2 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, '$..channels[?(@.is_archived == false)].id')[0] end def test_regression_3 json = { ok: true, channels: [ { id: 'C09C5GYHF', name: 'general', is_archived: false }, { id: 'C09C598QL', name: 'random', is_archived: true } ] }.to_json 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, "$..channels[?(@.name == 'general')].id") end def test_regression_5 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, "$..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_curly_brackets data = { '{data}' => 'data' } 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 = { '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 [{ '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 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 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_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 = ' { "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' }, { '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:12: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 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 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": { "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_on_array_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 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" 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 test_index_access_by_number data = { '1': 'foo' } 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' => [ { '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' => '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' => 123_45, 'single-speed' => 'no', '2seater' => 'yes', 'make:model' => 'Zippy Sweetwheeler' }, '@id' => 'http://example.org/store/42', '$meta-data' => 'whatevs', '_links' => { 'self' => {} } } } end end jsonpath-1.1.0/test/test_readme.rb0000644000004100000410000001055214107722717017202 0ustar www-datawww-data# 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 jsonpath-1.1.0/test/test_jsonpath_bin.rb0000644000004100000410000000102114107722717020412 0ustar www-datawww-data# frozen_string_literal: true require 'minitest/autorun' require 'phocus' require 'jsonpath' class TestJsonpathBin < MiniTest::Unit::TestCase def setup @runner = 'ruby -Ilib bin/jsonpath' @original_dir = Dir.pwd Dir.chdir(File.join(File.dirname(__FILE__), '..')) end def teardown Dir.chdir(@original_dir) `rm /tmp/test.json` end def test_stdin File.open('/tmp/test.json', 'w') { |f| f << '{"test": "time"}' } assert_equal '["time"]', `#{@runner} '$.test' /tmp/test.json`.strip end end jsonpath-1.1.0/.rspec0000644000004100000410000000004114107722717014507 0ustar www-datawww-data--colour --format doc --backtracejsonpath-1.1.0/README.md0000644000004100000410000001500714107722717014661 0ustar www-datawww-data# JsonPath 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. ## Usage ### Command-line 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. ### Library To use JsonPath as a library simply include and get goin'! ```ruby require 'jsonpath' 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 ``` 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') ``` Now that we have a path, let's apply it to the object above. ```ruby path.on(json) # => [19.95, 8.95, 12.99, 8.99, 22.99] ``` 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. ```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 JsonPath.new('$..book[::2]').on(json) # => [ # {"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, including those with conditional operators ```ruby 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. ```ruby JsonPath.new('$..color').first(json) # => "red" ``` As well, we can directly create an `Enumerable` at any time using `#[]`. ```ruby enum = JsonPath.new('$..color')[json] # => # enum.first # => "red" enum.any?{ |c| c == 'red' } # => true ``` For more usage examples and variations on paths, please visit the tests. There are some more complex ones as well. ### Querying ruby data structures If you have ruby hashes with symbolized keys as input, you can use `:use_symbols` to make JsonPath work fine on them too: ```ruby book = { title: "Sayings of the Century" } JsonPath.new('$.title').on(book) # => [] 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: ```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" } ] ``` ### Manipulation 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 ``` The result will be ```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 json = '{"candy":"lollipop","noncandy":null,"other":"things"}' o = JsonPath.for(json). gsub('$..candy') {|v| "big turks" }. compact. 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! ## Running an individual test ```ruby ruby -Ilib:../lib test/test_jsonpath.rb --name test_wildcard_on_intermediary_element_v6 ``` jsonpath-1.1.0/bin/0000755000004100000410000000000014107722717014147 5ustar www-datawww-datajsonpath-1.1.0/bin/jsonpath0000755000004100000410000000110314107722717015716 0ustar www-datawww-data#!/usr/bin/env ruby # frozen_string_literal: true require 'jsonpath' require 'multi_json' def usage puts "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." exit! end usage unless ARGV[0] jsonpath = JsonPath.new(ARGV[0]) case ARGV[1] 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]))) end jsonpath-1.1.0/LICENSE.md0000644000004100000410000000211114107722717014776 0ustar www-datawww-dataThe 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. jsonpath-1.1.0/.rubocop.yml0000644000004100000410000000004014107722717015643 0ustar www-datawww-datainherit_from: .rubocop_todo.yml jsonpath-1.1.0/.gitignore0000644000004100000410000000011714107722717015366 0ustar www-datawww-datapkg/* Gemfile.lock coverage/* doc/* .yardoc .DS_Store .idea vendor .tags *.gem jsonpath-1.1.0/.rubocop_todo.yml0000644000004100000410000000600514107722717016677 0ustar www-datawww-data# This configuration was generated by # `rubocop --auto-gen-config` # 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 # 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: 17 Metrics/AbcSize: Max: 60 # Offense count: 2 # Configuration parameters: CountComments, ExcludedMethods. # ExcludedMethods: refine Metrics/BlockLength: Max: 37 # Offense count: 1 # Configuration parameters: CountBlocks. Metrics/BlockNesting: Max: 4 # Offense count: 3 # Configuration parameters: CountComments. Metrics/ClassLength: Max: 739 # Offense count: 7 Metrics/CyclomaticComplexity: Max: 20 # Offense count: 26 # Configuration parameters: CountComments, ExcludedMethods. Metrics/MethodLength: Max: 52 # Offense count: 1 # Configuration parameters: CountKeywordArgs. Metrics/ParameterLists: Max: 6 # Offense count: 6 Metrics/PerceivedComplexity: 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: - 'lib/jsonpath/parser.rb' # Offense count: 2 Style/Documentation: Exclude: - 'spec/**/*' - 'test/**/*' - 'lib/jsonpath/enumerable.rb' - 'lib/jsonpath/proxy.rb' # Offense count: 3 # Configuration parameters: MinBodyLength. Style/GuardClause: Exclude: - 'lib/jsonpath/enumerable.rb' - 'lib/jsonpath/parser.rb' # Offense count: 2 # Cop supports --auto-correct. Style/IfUnlessModifier: Exclude: - 'lib/jsonpath/enumerable.rb' # Offense count: 1 Style/MultipleComparison: Exclude: - 'lib/jsonpath/parser.rb' # Offense count: 3 # Cop supports --auto-correct. # Configuration parameters: AutoCorrect, EnforcedStyle, IgnoredMethods. # SupportedStyles: predicate, comparison Style/NumericPredicate: Exclude: - 'spec/**/*' - 'lib/jsonpath/enumerable.rb' - 'lib/jsonpath/parser.rb' # Offense count: 3 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, AllowInnerSlashes. # SupportedStyles: slashes, percent_r, mixed Style/RegexpLiteral: Exclude: - 'lib/jsonpath/parser.rb' # Offense count: 4 # Cop supports --auto-correct. Style/RescueModifier: Exclude: - 'lib/jsonpath/enumerable.rb' - 'lib/jsonpath/parser.rb' # Offense count: 89 # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. # URISchemes: http, https Metrics/LineLength: Max: 296 jsonpath-1.1.0/.gemtest0000644000004100000410000000000014107722717015036 0ustar www-datawww-datajsonpath-1.1.0/Rakefile0000644000004100000410000000057014107722717015046 0ustar www-datawww-data# 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/' end require 'bundler' Bundler::GemHelper.install_tasks task :test do $LOAD_PATH << 'lib' Dir['./test/**/test_*.rb'].each { |test| require test } end task default: %i[test rubocop] jsonpath-1.1.0/lib/0000755000004100000410000000000014107722717014145 5ustar www-datawww-datajsonpath-1.1.0/lib/jsonpath.rb0000644000004100000410000000544614107722717016331 0ustar www-datawww-data# frozen_string_literal: true require 'strscan' require 'multi_json' require 'jsonpath/proxy' require 'jsonpath/dig' 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. 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 = DEFAULT_OPTIONS.merge(opts) scanner = StringScanner.new(path.strip) @path = [] until scanner.eos? if (token = scanner.scan(/\$\B|@\B|\*|\.\./)) @path << token elsif (token = scanner.scan(/[$@a-zA-Z0-9:{}_-]+/)) @path << "['#{token}']" elsif (token = scanner.scan(/'(.*?)'/)) @path << "[#{token}]" elsif (token = scanner.scan(/\[/)) @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(/[><=] \d+/)) @path.last << token elsif (token = scanner.scan(/./)) begin @path.last << token rescue RuntimeError raise ArgumentError, "character '#{token}' not supported in query" end end 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 res end def on(obj_or_str, opts = {}) a = enum_on(obj_or_str).to_a if opts[:symbolize_keys] a.map! do |e| e.each_with_object({}) { |(k, v), memo| memo[k.to_sym] = v; } end end a end def first(obj_or_str, *args) enum_on(obj_or_str).first(*args) end def enum_on(obj_or_str, mode = nil) JsonPath::Enumerable.new(self, self.class.process_object(obj_or_str), mode, @opts) end alias [] enum_on def self.on(obj_or_str, path, opts = {}) new(path, opts).on(process_object(obj_or_str)) end def self.for(obj_or_str) Proxy.new(process_object(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 def deep_clone Marshal.load Marshal.dump(self) end end jsonpath-1.1.0/lib/jsonpath/0000755000004100000410000000000014107722717015773 5ustar www-datawww-datajsonpath-1.1.0/lib/jsonpath/proxy.rb0000644000004100000410000000257414107722717017511 0ustar www-datawww-data# frozen_string_literal: true class JsonPath class Proxy attr_reader :obj alias to_hash obj def initialize(obj) @obj = obj end def gsub(path, replacement = nil, &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(&method(:replacement)) : replacement_block) end def delete(path = JsonPath::PATH_ALL) _delete(_deep_copy, path) end def delete!(path = JsonPath::PATH_ALL) _delete(@obj, path) end def compact(path = JsonPath::PATH_ALL) _compact(_deep_copy, path) end def compact!(path = JsonPath::PATH_ALL) _compact(@obj, path) end private def _deep_copy Marshal.load(Marshal.dump(@obj)) end def _gsub(obj, path, replacement) JsonPath.new(path)[obj, :substitute].each(&replacement) Proxy.new(obj) end 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) end end end jsonpath-1.1.0/lib/jsonpath/version.rb0000644000004100000410000000010614107722717020002 0ustar www-datawww-data# frozen_string_literal: true class JsonPath VERSION = '1.1.0' end jsonpath-1.1.0/lib/jsonpath/enumerable.rb0000644000004100000410000001204514107722717020441 0ustar www-datawww-data# frozen_string_literal: true class JsonPath class Enumerable include ::Enumerable include Dig def initialize(path, object, mode, options = {}) @path = path.path @object = object @mode = mode @options = options end def each(context = @object, key = nil, pos = 0, &blk) node = key ? dig_one(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) when '$' 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] != '..') case node 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 filter_context(context, keys) case context when Hash dig_as_hash(context, keys) when Array context.each_with_object([]) do |c, memo| memo << dig_as_hash(c, keys) 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] when '\'', '"' k = sub_path[1, sub_path.size - 2] 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] == '*' 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] && 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 start_idx %= node.size 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! else (start_idx..end_idx).step(step) { |i| each(node, i, pos + 1, &blk) } end end 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 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 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) end when :delete if key key.is_a?(Integer) ? context.delete_at(key) : context.delete(key) else context.replace({}) end when :substitute if key context[key] = blk.call(context[key]) else context.replace(blk.call(context[key])) end end end def process_function_or_literal(exp, default = nil) return default if exp.nil? || exp.empty? return Integer(exp) if exp[0] != '(' return nil unless @_current_node identifiers = /@?((?0} # {"@['isTrue']"=>true} def construct_expression_map(exps) 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 # 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_]+[?!]?/)) 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 #  that we are dealing with a boolean type. 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}" end end el = if elements.empty? @_current_node elsif @_current_node.is_a?(Hash) dig(@_current_node, *elements) else elements.inject(@_current_node, &:__send__) end return (el ? true : false) if el.nil? || operator.nil? el = Float(el) rescue el operand = Float(operand) rescue operand el.__send__(operator.strip, operand) end 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 ArgumentError, "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 # one-by-one. def parse_parentheses(str) opening_index = 0 closing_index = 0 (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] #  handle cases like (true && true || false && true) in # one giant parenthesis. top = to_parse.split(/(&&)|(\|\|)/) top = top.map(&:strip) res = bool_or_exp(top.shift) top.each_with_index do |item, index| if item == '&&' res &&= top[index + 1] elsif item == '||' 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 res.to_s else "#{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.to_s == 'true' return true elsif b.to_s == 'false' return false 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 # 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 jsonpath-1.1.0/jsonpath.gemspec0000644000004100000410000000210014107722717016563 0ustar www-datawww-data# 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_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/.' s.email = ['joshbuddy@gmail.com', 'skarlso777@gmail.com'] s.extra_rdoc_files = ['README.md'] s.files = `git ls-files`.split("\n") s.homepage = 'https://github.com/joshbuddy/jsonpath' s.test_files = `git ls-files`.split("\n").select { |f| f =~ /^spec/ } 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_development_dependency 'bundler' s.add_development_dependency 'code_stats' s.add_development_dependency 'minitest', '~> 2.2.0' s.add_development_dependency 'phocus' s.add_development_dependency 'rake' end jsonpath-1.1.0/Gemfile0000644000004100000410000000023514107722717014672 0ustar www-datawww-data# frozen_string_literal: true source 'http://rubygems.org' gemspec gem 'rubocop', require: true, group: :test gem 'simplecov', require: false, group: :test