JSONata-RB
A Ruby port of jsonata-js, the reference implementation of JSONata - a lightweight query and transformation language for JSON data.
This is a faithful, file-by-file port of the JavaScript implementation, and it is tested against the jsonata-js test suite itself (included as a git submodule), so it supports the full JSONata language and function library.
Installation
Add this line to your application's Gemfile:
gem 'jsonata'And then execute:
bundle installUsage
require 'jsonata'
data = {
example: [
{ value: 4 },
{ value: 7 },
{ value: 13 }
]
}
# Compile an expression, then evaluate it
expression = Jsonata.expression('$sum(example.value)')
expression.evaluate(data) # => 24
# Or use the convenience method
Jsonata.evaluate('$sum(example.value)', data) # => 24See the JSONata documentation for the language and function library.
Variable bindings
Jsonata.evaluate('value > $threshold', { value: 150 }, { threshold: 100 }) # => true
expr = Jsonata.expression('value > $threshold')
expr.assign('threshold', 100)
expr.evaluate({ value: 150 }) # => trueCustom functions
Any Ruby callable can be bound as a function, optionally with a signature to validate its arguments:
expr = Jsonata.expression('$double(value)')
expr.assign('double', ->(x) { x * 2 })
expr.evaluate({ value: 21 }) # => 42
expr = Jsonata.expression('$add(1, 2)')
expr.register_function('add', ->(a, b) { a + b }, '<nn:n>')
expr.evaluate # => 3
expr = Jsonata.expression('$greet("World")')
expr.register_function('greet', '<s:s>') { |name| "Hello, #{name}" }
expr.evaluate # => "Hello, World"Errors
Syntax and evaluation errors raise Jsonata::Error, which carries the same fields as the error objects
thrown by jsonata-js (code, position, token, value, ...):
begin
Jsonata.evaluate('1 +')
rescue Jsonata::Error => e
e.code # => "S0207"
e.position # => 3
e.message # => "Unexpected end of expression"
endOptions
Jsonata.expression(expr, options) accepts the same options as jsonata-js:
| Option | Description |
|---|---|
:recover |
Attempt to recover on parse errors (see Expression#errors) |
:timeout |
Evaluation timeout in milliseconds (error D1012) |
:stack |
Maximum evaluation depth (error D1011) |
:sequence |
Maximum sequence length (error D2015) |
:regex_engine |
A callable (source, flags) returning an object that responds to match(str, pos)
|
Mapping between JSON and Ruby values
- JSON objects are Hashes. Symbol keys in the input are converted to Strings; results always have String keys.
- JSON
nulland JavaScriptundefinedare both returned asnil. In the input, anilnested inside a Hash or Array is JSONnull, while a top-levelnilinput means there is no input (undefined).Expression#evaluate_rawreturns the internal representation, in whichnullisJsonata::NULLandundefinedisnil. - Numbers are Integers when they are integral (and within the range of a JavaScript safe integer), and Floats otherwise. Arithmetic follows JavaScript (IEEE 754 double) semantics.
- Functions (lambdas defined in an expression, or built-in functions) are returned as
Jsonata::LambdaandJsonata::NativeFunctionobjects.
Differences from jsonata-js
- Evaluation is synchronous (jsonata-js
evaluatereturns a Promise). - Regular expressions use Ruby's regex engine. JavaScript regex syntax is translated where the two differ
(e.g.
^/$anchors), but some exotic constructs may behave differently. - Strings are indexed by Unicode code point, so
$matchindexes differ from jsonata-js (which uses UTF-16 code units) for strings containing characters outside the Basic Multilingual Plane.
Development
The jsonata-js repository is included as a git submodule at vendor/jsonata-js, so clone with:
git clone --recurse-submodules <repo-url>
# or, in an existing clone
git submodule update --initThen:
bundle install
bundle exec rspec # run all tests
GROUP=hof-map,regex bundle exec rspec # run specific test-suite groups
bundle exec rake console # interactive console
bundle exec rubocop # lintStructure of the port
Each Ruby file is a port of the corresponding jsonata-js source file, keeping the same structure, function names (in snake_case), and comments, so that upstream changes can be applied to the Ruby code directly.
| jsonata-js | jsonata-rb |
|---|---|
src/jsonata.js |
lib/jsonata/evaluator.rb |
src/parser.js |
lib/jsonata/parser.rb |
src/functions.js |
lib/jsonata/functions.rb |
src/datetime.js |
lib/jsonata/datetime.rb |
src/signature.js |
lib/jsonata/signature.rb |
src/utils.js |
lib/jsonata/utils.rb (plus helpers emulating JavaScript) |
test/run-test-suite.js |
spec/test_suite_spec.rb |
Tracking jsonata-js
The tests come from the jsonata-js test suite (vendor/jsonata-js/test/test-suite). To pick up jsonata-js
changes:
bundle exec rake jsonata_js:update # move the submodule to the latest jsonata-js master, and list the new commits
bundle exec rake jsonata_js:diff # show the src/ and test/ changes to port
bundle exec rspec # port the src/ changes until the tests pass
git add vendor/jsonata-js lib # commit the submodule bump together with the ported changesThe jsonata-js sync workflow (.github/workflows/jsonata-js-sync.yaml) does this daily, one jsonata-js commit at
a time: for each new jsonata-js master commit, it bumps the submodule to it, follows a version change in
package.json, has Claude port any src/ and test/ changes, and commits. It opens (or updates) a pull request
from the jsonata-js-sync branch, which it rebases onto master on every run, so fixes pushed to the branch are
kept. If a commit cannot be ported cleanly, it stops there and makes the PR a draft; nothing more is synced until
the PR is fixed and marked ready for review. Merge the PR with "Rebase and merge" (not squash).
The jsonata-js tags workflow (.github/workflows/jsonata-js-tags.yaml) then gives the commit that syncs a
jsonata-js release tag the same tag (if its version matches), including tags that jsonata-js adds to older
commits later. It does not release the gem.
The steps of both are in bin/jsonata-js-sync. With DRY_RUN=1, bin/jsonata-js-sync sync commits to a local
jsonata-js-sync branch and prints the PR description, and bin/jsonata-js-sync tag tags locally, without
pushing.
Versioning
For consistency, the gem's version follows the jsonata-js version it is ported from, so jsonata 2.2.2
implements the same language and passes the same test suite as jsonata-js 2.2.2. Releases that contain only
Ruby-specific changes, between upstream releases, add a fourth segment (e.g. 2.2.2.1).
License
The gem is available as open source under the terms of the MIT License.
Acknowledgments
This is a Ruby port of the JSONata JavaScript library created by Andrew Coleman and contributors.