From f9ab9d4c8e0fe23a0249719854f87f05d7c108e4 Mon Sep 17 00:00:00 2001 From: Bernard Lambeau Date: Mon, 28 Dec 2020 11:18:19 +0100 Subject: [PATCH] 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 e8b0c133..32cb162d 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 7919cb11..e976d4b3 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 00000000..7a130042 --- /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 268ba18c..7a6f2709 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 a83fa2f0..79feb8c9 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 190e3a51..849e9768 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 00000000..ab5deb48 --- /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