Skip to content

jg-rp/ruby-json-p3

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

92 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

JSONPath, JSON Patch and JSON Pointer for Ruby

We follow RFC 9535 strictly and test against the JSONPath Compliance Test Suite.

License Tests
Gem Version Static Badge


Table of Contents

Install

Add 'json_p3' to your Gemfile:

gem 'json_p3', '~> 0.2.1'

Or

gem install json_p3

Checksum

JSON P3 is cryptographically signed. To be sure the gem you install hasn’t been tampered with, add my public key (if you haven’t already) as a trusted certificate:

gem cert --add <(curl -Ls https://raw.githubusercontent.com/jg-rp/ruby-json-p3/refs/heads/main/certs/jgrp.pem)

Followed by:

gem install json_p3 -P MediumSecurity

JSON P3 has no runtime dependencies, so -P HighSecurity is OK too. See https://guides.rubygems.org/security/ for more information.

Example

require "json_p3"
require "json"

data = JSON.parse <<~JSON
  {
    "users": [
      {
        "name": "Sue",
        "score": 100
      },
      {
        "name": "Sally",
        "score": 84,
        "admin": false
      },
      {
        "name": "John",
        "score": 86,
        "admin": true
      },
      {
        "name": "Jane",
        "score": 55
      }
    ],
    "moderator": "John"
  }
JSON

JSONP3.find("$.users[[email protected] > 85]", data).each do |node|
  puts node.value
end

# {"name"=>"Sue", "score"=>100}
# {"name"=>"John", "score"=>86, "admin"=>true}

Or, reading JSON data from a file:

require "json_p3"
require "json"

data = JSON.load_file("/path/to/some.json")

JSONP3.find("$.some.query", data).each do |node|
  puts node.value
end

You could read data from a YAML formatted file too, or any data format that can be loaded into hashes and arrays.

require "json_p3"
require "yaml"

data = YAML.load_file("/tmp/some.yaml")

JSONP3.find("$.users[[email protected] > 85]", data).each do |node|
  puts node.value
end

Links

Related projects

  • Python JSONPath RFC 9535 - A Python implementation of JSONPath that follows RFC 9535 strictly.
  • Python JSONPath - Another Python package implementing JSONPath, but with additional features and customization options.
  • JSON P3 - RFC 9535 implemented in TypeScript.

Quick start

find

find(query, value) -> Array<JSONPathNode>

Apply JSONPath expression query to JSON-like data value. An array of JSONPathNode instances is returned, one node for each value matched by query. The returned array will be empty if there were no matches.

Each JSONPathNode has:

  • a value attribute, which is the JSON-like value associated with the node.
  • a location attribute, which is a nested array of hash/object names and array indices that were required to reach the node's value in the target JSON document.
  • a path() method, which returns the normalized path to the node in the target JSON document.
require "json_p3"
require "json"

data = JSON.parse <<~JSON
  {
    "users": [
      {
        "name": "Sue",
        "score": 100
      },
      {
        "name": "Sally",
        "score": 84,
        "admin": false
      },
      {
        "name": "John",
        "score": 86,
        "admin": true
      },
      {
        "name": "Jane",
        "score": 55
      }
    ],
    "moderator": "John"
  }
JSON

JSONP3.find("$.users[[email protected] > 85]", data).each do |node|
  puts "#{node.value} at #{node.path}"
end

# {"name"=>"Sue", "score"=>100} at $['users'][0]
# {"name"=>"John", "score"=>86, "admin"=>true} at $['users'][2]

compile

compile(query) -> JSONPath

Prepare a JSONPath expression for repeated application to different JSON-like data. An instance of JSONPath has a find(data) method, which behaves similarly to the module-level find(query, data) method.

require "json_p3"
require "json"

data = JSON.parse <<~JSON
  {
    "users": [
      {
        "name": "Sue",
        "score": 100
      },
      {
        "name": "Sally",
        "score": 84,
        "admin": false
      },
      {
        "name": "John",
        "score": 86,
        "admin": true
      },
      {
        "name": "Jane",
        "score": 55
      }
    ],
    "moderator": "John"
  }
JSON

path = JSONP3.compile("$.users[[email protected] > 85]")

path.find(data).each do |node|
  puts "#{node.value} at #{node.path}"
end

# {"name"=>"Sue", "score"=>100} at $['users'][0]
# {"name"=>"John", "score"=>86, "admin"=>true} at $['users'][2]

JSONPathEnvironment

The find and compile methods described above are convenience methods equivalent to

JSONP3::DEFAULT_ENVIRONMENT.find(query, data)

and

JSONP3::DEFAULT_ENVIRONMENT.compile(query)

You could create your own environment like this:

require "json_p3"

jsonpath = JSONP3::JSONPathEnvironment.new
nodes = jsonpath.find("$.*", { "a" => "b", "c" => "d" })
pp nodes.map(&:value) # ["b", "d"]

To configure an environment with custom filter functions or non-standard selectors, inherit from JSONPathEnvironment and override some of its constants or the #setup_function_extensions method.

class MyJSONPathEnvironment < JSONP3::JSONPathEnvironment
  # The maximum integer allowed when selecting array items by index.
  MAX_INT_INDEX = (2**53) - 1

  # The minimum integer allowed when selecting array items by index.
  MIN_INT_INDEX = -(2**53) + 1

  # The maximum number of arrays and hashes the recursive descent segment will
  # traverse before raising a {JSONPathRecursionError}.
  MAX_RECURSION_DEPTH = 100

  # One of the available implementations of the _name selector_.
  #
  # - {NameSelector} (the default) will select values from hashes using string keys.
  # - {SymbolNameSelector} will select values from hashes using string or symbol keys.
  #
  # Implement your own name selector by inheriting from {NameSelector} and overriding
  # `#resolve`.
  NAME_SELECTOR = NameSelector

  # An implementation of the _index selector_. The default implementation will
  # select values from arrays only. Implement your own by inheriting from
  # {IndexSelector} and overriding `#resolve`.
  INDEX_SELECTOR = IndexSelector

  # Override this function to configure JSONPath function extensions.
  # By default, only the standard functions described in RFC 9535 are enabled.
  def setup_function_extensions
    @function_extensions["length"] = Length.new
    @function_extensions["count"] = Count.new
    @function_extensions["value"] = Value.new
    @function_extensions["match"] = Match.new
    @function_extensions["search"] = Search.new
  end

JSONPathError

JSONPathError is the base class for all JSONPath exceptions. The following classes inherit from JSONPathError and will only occur when parsing a JSONPath expression, not when applying a path to some data.

  • JSONPathSyntaxError
  • JSONPathTypeError
  • JSONPathNameError

JSONPathError implements #detailed_message. With recent versions of Ruby you should get useful error messages.

JSONP3::JSONPathSyntaxError: unexpected trailing whitespace
  -> '$.foo ' 1:5
  |
1 | $.foo
  |      ^ unexpected trailing whitespace

resolve

resolve(pointer, value) -> Object

Resolve a JSON Pointer (RFC 6901) against some data using JSONP3.resolve().

require "json_p3"
require "json"

data = JSON.parse <<~JSON
  {
    "users": [
      {
        "name": "Sue",
        "score": 100
      },
      {
        "name": "Sally",
        "score": 84,
        "admin": false
      },
      {
        "name": "John",
        "score": 86,
        "admin": true
      },
      {
        "name": "Jane",
        "score": 55
      }
    ],
    "moderator": "John"
  }
JSON

puts JSONP3.resolve("/users/1", data)
# {"name"=>"Sally", "score"=>84, "admin"=>false}

If a pointer can not be resolved, JSONP3::JSONPointer::UNDEFINED is returned instead. You can use your own default value using the default: keyword argument.

# continued from above

pp JSONP3.resolve("/no/such/thing", data, default: nil) # nil

apply

apply(ops, value) -> Object

Apply a JSON Patch (RFC 6902) with JSONP3.apply(). Data is modified in place.

require "json"
require "json_p3"

ops = <<~JSON
  [
    { "op": "add", "path": "/some/foo", "value": { "foo": {} } },
    { "op": "add", "path": "/some/foo", "value": { "bar": [] } },
    { "op": "copy", "from": "/some/other", "path": "/some/foo/else" },
    { "op": "add", "path": "/some/foo/bar/-", "value": 1 }
  ]
JSON

data = { "some" => { "other" => "thing" } }
JSONP3.apply(JSON.parse(ops), data)
pp data
# {"some"=>{"other"=>"thing", "foo"=>{"bar"=>[1], "else"=>"thing"}}}

JSONP3.apply(ops, value) is a convenience method equivalent to JSONP3::JSONPatch.new(ops).apply(value). Use the JSONPatch constructor when you need to apply the same patch to different data.

As well as passing an array of hashes following RFC 6902 as ops to JSONPatch, we offer a builder API to construct JSON Patch documents programmatically.

require "json_p3"

data = { "some" => { "other" => "thing" } }

patch = JSONP3::JSONPatch.new
                         .add("/some/foo", { "foo" => [] })
                         .add("/some/foo", { "bar" => [] })
                         .copy("/some/other", "/some/foo/else")
                         .copy("/some/foo/else", "/some/foo/bar/-")

patch.apply(data)
pp data
# {"some"=>{"other"=>"thing", "foo"=>{"bar"=>["thing"], "else"=>"thing"}}}

Contributing

Your contributions and questions are always welcome. Feel free to ask questions, report bugs or request features on the issue tracker or on Github Discussions. Pull requests are welcome too.

Development

The JSONPath Compliance Test Suite is included as a git submodule. Clone the JSON P3 git repository and initialize the CTS submodule.

$ git clone [email protected]:jg-rp/ruby-json-p3.git
$ cd ruby-json-p3
$ git submodule update --init

We use Bundler and Rake. Install development dependencies with:

bundle install

Run tests with:

bundle exec rake test

Lint with:

bundle exec rubocop

And type check with:

bundle exec steep

Run one of the benchmarks with:

bundle exec ruby performance/benchmark_ips.rb

Profiling

CPU profile

Dump profile data with bundle exec ruby performance/profile.rb, then generate an HTML flame graph with:

bundle exec stackprof --d3-flamegraph .stackprof-cpu-just-compile.dump > flamegraph-cpu-just-compile.html

Memory profile

Print memory usage to the terminal.

bundle exec ruby performance/memory_profile.rb

Notes to self

Build

bundle exec rake release and bundle exec rake build will look for gem-private_key.pem and gem-public_cert.pem in ~/.gem.

TruffleRuby

On macOS Sonoma using MacPorts and rbenv, LIBYAML_PREFIX=/opt/local/lib is needed to install TruffleRuby and when executing any bundle command.

About

JSONPath, JSON Patch and JSON Pointer for Ruby

Topics

Resources

License

Stars

Watchers

Forks