jsonata 2.2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rspec +1 -0
- data/.rubocop.yml +4 -0
- data/.rubocop_local.yml +52 -0
- data/AGENTS.md +171 -0
- data/CHANGELOG.md +17 -0
- data/Gemfile +3 -0
- data/LICENSE +22 -0
- data/README.md +189 -0
- data/Rakefile +41 -0
- data/lib/jsonata/datetime.rb +1237 -0
- data/lib/jsonata/error.rb +113 -0
- data/lib/jsonata/evaluator.rb +1669 -0
- data/lib/jsonata/functions.rb +1505 -0
- data/lib/jsonata/parser.rb +1206 -0
- data/lib/jsonata/signature.rb +235 -0
- data/lib/jsonata/utils.rb +600 -0
- data/lib/jsonata/version.rb +5 -0
- data/lib/jsonata.rb +47 -0
- metadata +108 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 52797dede85591f15e5e3ad10670d556d3b4683471c5b5e00f0ff0602e147694
|
|
4
|
+
data.tar.gz: 0d235b9161ee1d2a45a1f492c813c5a2b04f438aa91af7707f9144c48e39724b
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 945f2e2fca929e067dd684ce171edc66f8d233047e3bea1b6e3b1513022e662a05342b1d393f585d43e2ed0892cb824a77896df04a19cb56922fe5fb6fc7216a
|
|
7
|
+
data.tar.gz: 1d960e3b290979f87f8f71509fcd1bbbd76a95081a08a701df7a1d33c57270857fe4f3d908530796cdc56c747857b95b53d3d9b13616a739d343dabe942ffd80
|
data/.rspec
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
--require spec_helper
|
data/.rubocop.yml
ADDED
data/.rubocop_local.yml
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# The minimum supported Ruby version (see the gemspec); manageiq-style targets a newer one.
|
|
2
|
+
AllCops:
|
|
3
|
+
TargetRubyVersion: 3.1
|
|
4
|
+
|
|
5
|
+
# This is not a Rails project, and the suggested corrections (e.g. blank?) need ActiveSupport.
|
|
6
|
+
Rails:
|
|
7
|
+
Enabled: false
|
|
8
|
+
|
|
9
|
+
# lib/ is ported from jsonata-js and keeps the structure, names and comments of the JS code, so it stays
|
|
10
|
+
# diffable with upstream (see AGENTS.md). These cops restructure the code or rename things away from the JS.
|
|
11
|
+
Lint/DuplicateBranch:
|
|
12
|
+
Enabled: false
|
|
13
|
+
Naming/MethodParameterName:
|
|
14
|
+
Enabled: false
|
|
15
|
+
Naming/PredicateMethod:
|
|
16
|
+
Enabled: false
|
|
17
|
+
Style/CaseLikeIf:
|
|
18
|
+
Enabled: false
|
|
19
|
+
Style/CommentAnnotation:
|
|
20
|
+
Enabled: false
|
|
21
|
+
Style/ConditionalAssignment:
|
|
22
|
+
Enabled: false
|
|
23
|
+
Style/EmptyElse:
|
|
24
|
+
AllowComments: true
|
|
25
|
+
Style/GuardClause:
|
|
26
|
+
Enabled: false
|
|
27
|
+
Style/IdenticalConditionalBranches:
|
|
28
|
+
Enabled: false
|
|
29
|
+
Style/IfUnlessModifier:
|
|
30
|
+
Enabled: false
|
|
31
|
+
Style/ItAssignment:
|
|
32
|
+
Enabled: false
|
|
33
|
+
Style/MultilineBlockChain:
|
|
34
|
+
Enabled: false
|
|
35
|
+
Style/MultipleComparison:
|
|
36
|
+
Enabled: false
|
|
37
|
+
Style/NegatedIfElseCondition:
|
|
38
|
+
Enabled: false
|
|
39
|
+
Style/RedundantAssignment:
|
|
40
|
+
Enabled: false
|
|
41
|
+
|
|
42
|
+
# Built-in function parameters are positional, as their arity (JS Function.length) is used when invoking them.
|
|
43
|
+
Style/OptionalBooleanParameter:
|
|
44
|
+
Enabled: false
|
|
45
|
+
|
|
46
|
+
# Array#sum uses Kahan-Babuska summation, which gives different results from the JS loop (see AGENTS.md).
|
|
47
|
+
Performance/Sum:
|
|
48
|
+
Enabled: false
|
|
49
|
+
|
|
50
|
+
# Jsonata::Error is raised with a code and fields, e.g. raise Jsonata::Error.new('T0410', :index => 1).
|
|
51
|
+
Style/RaiseArgs:
|
|
52
|
+
EnforcedStyle: compact
|
data/AGENTS.md
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
Guidance for agents working on jsonata-rb. Read the README first; this file covers what the README does not:
|
|
4
|
+
the decisions behind the port and how JavaScript semantics are emulated in Ruby.
|
|
5
|
+
|
|
6
|
+
## Ground rules
|
|
7
|
+
|
|
8
|
+
- **Port, don't invent.** Every change to `lib/` should correspond to code in `vendor/jsonata-js/src`. When a
|
|
9
|
+
test fails, find how jsonata-js does it and port that, rather than writing a new solution that happens to pass.
|
|
10
|
+
- **Keep the structure diffable.** Ruby files mirror the JS files: same function order, same names (snake_case),
|
|
11
|
+
same comments (including the JSDoc text and TODOs). Ruby-only code is marked with a comment such as
|
|
12
|
+
`(Ruby specific)` or `(jsonata-js: <original construct>)`, so it is clear what is a deviation.
|
|
13
|
+
- **Keep the upstream copyright headers.** Each ported file starts with the IBM copyright header of the JS file it
|
|
14
|
+
ports (in ASCII, `(c)` for the copyright sign), as required by the MIT license. Copy the header when porting a
|
|
15
|
+
new file, and update the years if upstream changes them.
|
|
16
|
+
- **Tests come from upstream only.** Language behavior is tested exclusively by the jsonata-js test suite via
|
|
17
|
+
`spec/test_suite_spec.rb`. Do not add hand-written specs for language features. `spec/jsonata_spec.rb` is only
|
|
18
|
+
for the Ruby API (conversion of values, `assign`, `register_function`, errors, ...).
|
|
19
|
+
- **Never edit `vendor/jsonata-js`.** It is a git submodule tracking jsonata-js master. Bump it with
|
|
20
|
+
`rake jsonata_js:update` and commit the submodule bump together with the ported `lib/` changes.
|
|
21
|
+
- **ASCII only.** Code, comments and commit messages must not contain non-ASCII characters, including (but not
|
|
22
|
+
limited to) em-dashes and en-dashes, curly quotes, the section sign, Greek letters (e.g. lambda) and
|
|
23
|
+
mathematical symbols (e.g. less-than-or-equal). Use `"\uXXXX"` escapes in string literals that
|
|
24
|
+
need non-ASCII characters, and ASCII equivalents in comments.
|
|
25
|
+
- **LF line endings.** Enforced by `.gitattributes`; never commit CRLF.
|
|
26
|
+
- **Optimize within Ruby, not by changing the algorithm.** Performance work may change how the Ruby is written
|
|
27
|
+
(e.g. caching in the Ruby-only helpers in `utils.rb`), but must not change the logic ported from jsonata-js.
|
|
28
|
+
Algorithmic improvements should be proposed upstream first, and then ported.
|
|
29
|
+
- **Ruby >= 3.1.** Do not use syntax or core methods newer than 3.1.
|
|
30
|
+
- **Rubocop must pass.** The style is ManageIQ's (`manageiq-style` gem), with project overrides in
|
|
31
|
+
`.rubocop_local.yml`. Cops that would restructure ported code away from the JS are disabled there; prefer adding
|
|
32
|
+
to that list (with the reason) over rewriting ported code to satisfy a cop. Review unsafe autocorrections by
|
|
33
|
+
hand - some are wrong here (e.g. `Style/HashExcept` on `#parameters`, or `Style/BitwisePredicate`).
|
|
34
|
+
|
|
35
|
+
## Porting workflow for jsonata-js changes
|
|
36
|
+
|
|
37
|
+
The `jsonata-js sync` workflow (`.github/workflows/jsonata-js-sync.yaml`) runs these steps daily with Claude and
|
|
38
|
+
opens a PR from the `jsonata-js-sync` branch; they also apply when porting by hand.
|
|
39
|
+
|
|
40
|
+
1. `bundle exec rake jsonata_js:update` - moves the submodule to jsonata-js master and lists new commits.
|
|
41
|
+
2. `bundle exec rake jsonata_js:diff` - shows the `src/` and `test/` changes since the commit recorded in this repo.
|
|
42
|
+
3. Port each `src/` hunk to the corresponding Ruby file (see the mapping table in the README), at the same place
|
|
43
|
+
in the file.
|
|
44
|
+
4. `bundle exec rspec` (or `GROUP=<group> bundle exec rspec` while iterating) and `bundle exec rubocop` until
|
|
45
|
+
everything passes.
|
|
46
|
+
5. If `test/run-test-suite.js` changed, port that change to `spec/test_suite_spec.rb`.
|
|
47
|
+
6. Changes to the JS-only mocha tests (`test/*.js` other than `run-test-suite.js`) are not run by this repo;
|
|
48
|
+
only port them if they reveal behavior worth covering in `spec/jsonata_spec.rb`.
|
|
49
|
+
|
|
50
|
+
## JavaScript vs Ruby: decisions made
|
|
51
|
+
|
|
52
|
+
### undefined and null
|
|
53
|
+
|
|
54
|
+
- `nil` is JavaScript `undefined`. `Jsonata::NULL` (a frozen sentinel) is JSON `null`. This was chosen because
|
|
55
|
+
JS code tests for `undefined` far more often than `null`, and Ruby methods return `nil` implicitly.
|
|
56
|
+
- Values are converted at the API boundary only: `Utils.from_ruby` (nested `nil` -> `NULL`, Symbol keys -> String
|
|
57
|
+
keys) on the way in, `Utils.to_ruby` (`NULL` -> `nil`, `JArray` -> `Array`) on the way out. A top-level `nil`
|
|
58
|
+
input or binding value means `undefined`.
|
|
59
|
+
- `Expression#evaluate_raw` returns the internal representation; the test harness uses it to tell `null` from
|
|
60
|
+
`undefined`.
|
|
61
|
+
|
|
62
|
+
### Truthiness and typeof
|
|
63
|
+
|
|
64
|
+
- JS truthiness (`if (x)`, `!x`, `x || y`) on values must use `Utils.truthy?` - in Ruby `0`, `""` and `NULL` are
|
|
65
|
+
truthy. Only use plain Ruby truthiness where the JS value is known to be a boolean/object/undefined.
|
|
66
|
+
- `typeof x` maps to `Utils.typeof(x)`. Remember `typeof null === 'object'` and arrays are objects in JS:
|
|
67
|
+
`Utils.keys` returns index strings for arrays, as `Object.keys` does (needed by the wildcard operator).
|
|
68
|
+
|
|
69
|
+
### Objects, arrays and sequences
|
|
70
|
+
|
|
71
|
+
- JSON objects are Hashes with String keys (insertion ordered, like JS objects).
|
|
72
|
+
- jsonata-js attaches properties to arrays (`sequence`, `keepSingleton`, `tupleStream`, `cons`, `outerWrapper`).
|
|
73
|
+
These live on `Jsonata::JArray < Array`. Read them with `x.is_a?(JArray) && x.flag` (or `Utils.sequence?`),
|
|
74
|
+
because many arrays are plain `Array`s. Before setting a flag on an array that may be plain, wrap it in a
|
|
75
|
+
`JArray`.
|
|
76
|
+
- In Ruby 3, `Array#+`, `#map`, `#select`, `#[]` with ranges etc. on a `JArray` return a plain `Array`. This
|
|
77
|
+
matches JS, where `concat`/`slice`/`map` do not copy the custom properties.
|
|
78
|
+
- `createSequence` (with the `sequence` length limit) is `Frame#create_sequence` / `Focus#create_sequence`.
|
|
79
|
+
`create_sequence(nil)` pushes `nil` (as JS does with an explicit `undefined` argument), while
|
|
80
|
+
`create_sequence()` creates an empty sequence - keep the distinction.
|
|
81
|
+
- Beware Ruby negative indexes: `arr[-1]` is the last element, whereas JS returns `undefined`. Guard indexes that
|
|
82
|
+
may be negative.
|
|
83
|
+
|
|
84
|
+
### Numbers
|
|
85
|
+
|
|
86
|
+
- JS numbers are doubles; here they are Integers when integral (and within +/-2^53) and Floats otherwise.
|
|
87
|
+
- Do arithmetic in Float and pass results through `Utils.normalize_number`, so integer overflow/precision behaves
|
|
88
|
+
like JS and integral results come back as Integers.
|
|
89
|
+
- Do not use `Array#sum` - it uses Kahan-Babuska summation and gives different results from JS's naive loop.
|
|
90
|
+
- `Math.floor` on large values: use `Utils.js_floor`, which keeps the result a Float beyond 2^53.
|
|
91
|
+
- Formatting: `Utils.number_to_s` (Number::toString), `Utils.to_fixed` (toFixed), `Utils.to_precision`
|
|
92
|
+
(toPrecision) are exact (via Rational), including JS's round-half-up tie breaking.
|
|
93
|
+
- `Math.round` is `(x.to_r + 1/2).floor` (ties toward +infinity), not Ruby's `round`.
|
|
94
|
+
- JS parsing helpers: `Utils.parse_int` (returns `nil` for NaN), `Utils.js_number` (`Number(str)`).
|
|
95
|
+
|
|
96
|
+
### Strings
|
|
97
|
+
|
|
98
|
+
- Ruby strings are indexed by code point, JS strings by UTF-16 code unit. The port uses code points throughout
|
|
99
|
+
(consistent within Ruby); this only matters for characters outside the BMP.
|
|
100
|
+
- Ruby cannot hold UTF-16 surrogates. The tokenizer combines `\uD8xx\uDCxx` escape pairs into one character; a
|
|
101
|
+
lone surrogate is stored as invalid UTF-8 bytes (`Utils.from_char_code`), which `encode_uri` rejects and
|
|
102
|
+
`json_stringify` re-escapes as `\udxxx`.
|
|
103
|
+
- JS string helpers with different Ruby semantics: `Utils.js_slice`, `Utils.js_split` (keeps empty strings),
|
|
104
|
+
`Utils.json_stringify` (JSON.stringify, including the replacer and indentation used by `$string`),
|
|
105
|
+
`Utils.encode_uri` / `Utils.decode_uri`.
|
|
106
|
+
|
|
107
|
+
### Regular expressions
|
|
108
|
+
|
|
109
|
+
- Regex literals are parsed into `Parser::RegexLiteral` (source + flags) and translated by `Utils.js_regexp`:
|
|
110
|
+
`^`/`$` become `\A`/`\z` unless the `m` flag is set, `[]` and `[^]` are rewritten, and `(?a)` is prepended so
|
|
111
|
+
`\w`, `\d`, `\b` are ASCII-only as in JS.
|
|
112
|
+
- `RegExp.lastIndex` is emulated with a `last_index` variable captured by the matcher closure in
|
|
113
|
+
`evaluate_regex`.
|
|
114
|
+
- The jsonata-js `RegexEngine` option is the `:regex_engine` option: a callable `(source, flags)` returning an
|
|
115
|
+
object that responds to `match(str, pos)`.
|
|
116
|
+
|
|
117
|
+
### Functions
|
|
118
|
+
|
|
119
|
+
- Function values: `Jsonata::Lambda` (`_jsonata_lambda`), `Jsonata::NativeFunction` (`_jsonata_function`), and
|
|
120
|
+
plain callables (`Proc`, `Method`, `Jsonata::Closure` - the JS function with an `arity` property).
|
|
121
|
+
`Utils.function?` covers all of them; `Utils.callable?` is `typeof x === 'function'`.
|
|
122
|
+
- Built-in functions are instance methods of `Jsonata::Functions` and `Jsonata::DateTime`, which are included in
|
|
123
|
+
`Jsonata::Focus`. A `Focus` is the JS `this` (`environment`, `input`, `options`, `create_sequence`), so
|
|
124
|
+
`fn.lookup.call(focus, ...)` becomes `focus.lookup(...)`. Built-ins are registered with
|
|
125
|
+
`Focus.instance_method(:name)`.
|
|
126
|
+
- `Utils.invoke` calls a Ruby callable with JS semantics: missing arguments become `nil` and extra arguments are
|
|
127
|
+
dropped. Within built-ins, call function arguments with `call_function`, which also handles `Lambda#apply`.
|
|
128
|
+
- Arity (JS `Function.length`) is the number of *required* Ruby parameters. Declare built-in parameters as
|
|
129
|
+
required (no defaults) unless the JS function has a default (e.g. `string(arg, prettify = false)`), otherwise
|
|
130
|
+
higher-order functions like `$map` pass the wrong number of arguments.
|
|
131
|
+
- Partial application of native functions reads parameter names from `#parameters` (JS parses
|
|
132
|
+
`func.toString()`).
|
|
133
|
+
- `not` is a valid Ruby method name and is used for `$not`.
|
|
134
|
+
- JS `async`/`await` is dropped; evaluation is synchronous. `Promise.all` loops run sequentially but still set
|
|
135
|
+
`environment.is_parallel_call`, which the evaluate entry/exit hooks rely on. `$sort`'s recursive merge is
|
|
136
|
+
iterative to avoid deep Ruby recursion.
|
|
137
|
+
- The `Symbol.for('jsonata.__evaluate_entry')` style hook names are Ruby Symbols, e.g.
|
|
138
|
+
`:'jsonata.__evaluate_entry'`; `Expression#assign` keeps such Symbols as-is.
|
|
139
|
+
|
|
140
|
+
### Errors
|
|
141
|
+
|
|
142
|
+
- JS `throw { code: ... }` is `raise Jsonata::Error.new('CODE', field: ...)`; the fields use Ruby names
|
|
143
|
+
(`function_name` for `functionName`) and `Error#to_h` maps them back. Messages are filled in by
|
|
144
|
+
`Evaluator.populate_message` from `ERROR_CODES`.
|
|
145
|
+
- JS `catch (err)` catches everything; the port rescues only `Jsonata::Error`, so Ruby bugs surface as Ruby
|
|
146
|
+
exceptions instead of being annotated as JSONata errors.
|
|
147
|
+
- `SystemStackError` is reported as `D1011` (Ruby specific). Non-tail recursion in an expression overflows the
|
|
148
|
+
Ruby stack at roughly 600-1000 levels, as each JSONata call uses about a dozen Ruby frames; tail calls are
|
|
149
|
+
handled by the trampoline in `Evaluator.apply`.
|
|
150
|
+
|
|
151
|
+
### Parser
|
|
152
|
+
|
|
153
|
+
- AST nodes are Hashes with Symbol keys (`:keep_array` for `keepArray`, etc.). Symbol keys keep AST nodes
|
|
154
|
+
distinguishable from user data, which has String keys (e.g. the `?` placeholder check in partial application).
|
|
155
|
+
- JS nodes inherit `nud`/`led`/`lbp`/`id` from the symbol table via the prototype chain; here a
|
|
156
|
+
`compare_by_identity` Hash maps each node to its `Sym`. `JSON.parse(JSON.stringify(node))` is `deep_clone`,
|
|
157
|
+
which drops that mapping just as the JS clone drops the prototype.
|
|
158
|
+
- `nud`/`led` are blocks: use `next`, not `return`, to return early from them.
|
|
159
|
+
|
|
160
|
+
### Dates
|
|
161
|
+
|
|
162
|
+
- JS `Date` is a UTC Ruby `Time`. `DateTime.date_utc` reproduces `Date.UTC` (month overflow, truncation, years
|
|
163
|
+
0-99 mapping to 1900+), and `DateTime.date_parse` reproduces `Date.parse` for the ISO 8601 subset accepted by
|
|
164
|
+
`$toMillis`. Timestamps without an offset are treated as UTC.
|
|
165
|
+
|
|
166
|
+
### Test harness
|
|
167
|
+
|
|
168
|
+
- `spec/test_suite_spec.rb` mirrors `run-test-suite.js`. Results are compared with `evaluate_raw` against the
|
|
169
|
+
expected JSON converted with `Utils.from_ruby`, so `null` and `undefined` are distinguished.
|
|
170
|
+
- Ruby's JSON parser rejects lone surrogates, so test files containing them are re-parsed with the surrogates
|
|
171
|
+
escaped; they reach the tokenizer as `\uXXXX` in `expr`, and are decoded with `Utils.from_char_code` elsewhere.
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and the version follows the jsonata-js version it is ported from (see [Versioning](README.md#versioning)).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Ruby port of jsonata-js: parser, evaluator, function library, date/time functions and signature validation
|
|
12
|
+
- The full JSONata language and function library
|
|
13
|
+
- `Jsonata::Error` with the jsonata-js error codes and messages
|
|
14
|
+
- `Expression#assign`, `#register_function`, `#ast`, `#errors` and `#evaluate_raw`
|
|
15
|
+
- `recover`, `timeout`, `stack`, `sequence` and `regex_engine` options
|
|
16
|
+
- The jsonata-js test suite, via a git submodule at `vendor/jsonata-js`
|
|
17
|
+
- `rake jsonata_js:update` and `rake jsonata_js:diff` tasks for tracking jsonata-js
|
data/Gemfile
ADDED
data/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2016-2018 IBM Corp.
|
|
4
|
+
Copyright (c) 2026 jsonata-rb contributors
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# JSONata-RB
|
|
2
|
+
|
|
3
|
+
A Ruby port of [jsonata-js](https://github.com/jsonata-js/jsonata), the reference implementation of
|
|
4
|
+
[JSONata](http://jsonata.org/) - a lightweight query and transformation language for JSON data.
|
|
5
|
+
|
|
6
|
+
This is a faithful, file-by-file port of the JavaScript implementation, and it is tested against the
|
|
7
|
+
jsonata-js test suite itself (included as a git submodule), so it supports the full JSONata language and
|
|
8
|
+
function library.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
Add this line to your application's Gemfile:
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
gem 'jsonata'
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
And then execute:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
bundle install
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
require 'jsonata'
|
|
28
|
+
|
|
29
|
+
data = {
|
|
30
|
+
example: [
|
|
31
|
+
{ value: 4 },
|
|
32
|
+
{ value: 7 },
|
|
33
|
+
{ value: 13 }
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# Compile an expression, then evaluate it
|
|
38
|
+
expression = Jsonata.expression('$sum(example.value)')
|
|
39
|
+
expression.evaluate(data) # => 24
|
|
40
|
+
|
|
41
|
+
# Or use the convenience method
|
|
42
|
+
Jsonata.evaluate('$sum(example.value)', data) # => 24
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
See the [JSONata documentation](http://docs.jsonata.org/) for the language and function library.
|
|
46
|
+
|
|
47
|
+
### Variable bindings
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
Jsonata.evaluate('value > $threshold', { value: 150 }, { threshold: 100 }) # => true
|
|
51
|
+
|
|
52
|
+
expr = Jsonata.expression('value > $threshold')
|
|
53
|
+
expr.assign('threshold', 100)
|
|
54
|
+
expr.evaluate({ value: 150 }) # => true
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Custom functions
|
|
58
|
+
|
|
59
|
+
Any Ruby callable can be bound as a function, optionally with a
|
|
60
|
+
[signature](http://docs.jsonata.org/embedding-extending#function-signature-syntax) to validate its arguments:
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
expr = Jsonata.expression('$double(value)')
|
|
64
|
+
expr.assign('double', ->(x) { x * 2 })
|
|
65
|
+
expr.evaluate({ value: 21 }) # => 42
|
|
66
|
+
|
|
67
|
+
expr = Jsonata.expression('$add(1, 2)')
|
|
68
|
+
expr.register_function('add', ->(a, b) { a + b }, '<nn:n>')
|
|
69
|
+
expr.evaluate # => 3
|
|
70
|
+
|
|
71
|
+
expr = Jsonata.expression('$greet("World")')
|
|
72
|
+
expr.register_function('greet', '<s:s>') { |name| "Hello, #{name}" }
|
|
73
|
+
expr.evaluate # => "Hello, World"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Errors
|
|
77
|
+
|
|
78
|
+
Syntax and evaluation errors raise `Jsonata::Error`, which carries the same fields as the error objects
|
|
79
|
+
thrown by jsonata-js (`code`, `position`, `token`, `value`, ...):
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
begin
|
|
83
|
+
Jsonata.evaluate('1 +')
|
|
84
|
+
rescue Jsonata::Error => e
|
|
85
|
+
e.code # => "S0207"
|
|
86
|
+
e.position # => 3
|
|
87
|
+
e.message # => "Unexpected end of expression"
|
|
88
|
+
end
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Options
|
|
92
|
+
|
|
93
|
+
`Jsonata.expression(expr, options)` accepts the same options as jsonata-js:
|
|
94
|
+
|
|
95
|
+
| Option | Description |
|
|
96
|
+
|-----------------|-------------------------------------------------------------------------------------|
|
|
97
|
+
| `:recover` | Attempt to recover on parse errors (see `Expression#errors`) |
|
|
98
|
+
| `:timeout` | Evaluation timeout in milliseconds (error `D1012`) |
|
|
99
|
+
| `:stack` | Maximum evaluation depth (error `D1011`) |
|
|
100
|
+
| `:sequence` | Maximum sequence length (error `D2015`) |
|
|
101
|
+
| `:regex_engine` | A callable `(source, flags)` returning an object that responds to `match(str, pos)` |
|
|
102
|
+
|
|
103
|
+
### Mapping between JSON and Ruby values
|
|
104
|
+
|
|
105
|
+
* JSON objects are Hashes. Symbol keys in the input are converted to Strings; results always have String keys.
|
|
106
|
+
* JSON `null` and JavaScript `undefined` are both returned as `nil`. In the input, a `nil` nested inside a Hash
|
|
107
|
+
or Array is JSON `null`, while a top-level `nil` input means there is no input (`undefined`).
|
|
108
|
+
`Expression#evaluate_raw` returns the internal representation, in which `null` is `Jsonata::NULL` and
|
|
109
|
+
`undefined` is `nil`.
|
|
110
|
+
* Numbers are Integers when they are integral (and within the range of a JavaScript safe integer), and Floats
|
|
111
|
+
otherwise. Arithmetic follows JavaScript (IEEE 754 double) semantics.
|
|
112
|
+
* Functions (lambdas defined in an expression, or built-in functions) are returned as `Jsonata::Lambda` and
|
|
113
|
+
`Jsonata::NativeFunction` objects.
|
|
114
|
+
|
|
115
|
+
### Differences from jsonata-js
|
|
116
|
+
|
|
117
|
+
* Evaluation is synchronous (jsonata-js `evaluate` returns a Promise).
|
|
118
|
+
* Regular expressions use Ruby's regex engine. JavaScript regex syntax is translated where the two differ
|
|
119
|
+
(e.g. `^`/`$` anchors), but some exotic constructs may behave differently.
|
|
120
|
+
* Strings are indexed by Unicode code point, so `$match` indexes differ from jsonata-js (which uses UTF-16 code
|
|
121
|
+
units) for strings containing characters outside the Basic Multilingual Plane.
|
|
122
|
+
|
|
123
|
+
## Development
|
|
124
|
+
|
|
125
|
+
The jsonata-js repository is included as a git submodule at `vendor/jsonata-js`, so clone with:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
git clone --recurse-submodules <repo-url>
|
|
129
|
+
# or, in an existing clone
|
|
130
|
+
git submodule update --init
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Then:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
bundle install
|
|
137
|
+
bundle exec rspec # run all tests
|
|
138
|
+
GROUP=hof-map,regex bundle exec rspec # run specific test-suite groups
|
|
139
|
+
bundle exec rake console # interactive console
|
|
140
|
+
bundle exec rubocop # lint
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Structure of the port
|
|
144
|
+
|
|
145
|
+
Each Ruby file is a port of the corresponding jsonata-js source file, keeping the same structure, function
|
|
146
|
+
names (in snake_case), and comments, so that upstream changes can be applied to the Ruby code directly.
|
|
147
|
+
|
|
148
|
+
| jsonata-js | jsonata-rb |
|
|
149
|
+
|-----------------------|-----------------------------------------------------------------|
|
|
150
|
+
| `src/jsonata.js` | `lib/jsonata/evaluator.rb` |
|
|
151
|
+
| `src/parser.js` | `lib/jsonata/parser.rb` |
|
|
152
|
+
| `src/functions.js` | `lib/jsonata/functions.rb` |
|
|
153
|
+
| `src/datetime.js` | `lib/jsonata/datetime.rb` |
|
|
154
|
+
| `src/signature.js` | `lib/jsonata/signature.rb` |
|
|
155
|
+
| `src/utils.js` | `lib/jsonata/utils.rb` (plus helpers emulating JavaScript) |
|
|
156
|
+
| `test/run-test-suite.js` | `spec/test_suite_spec.rb` |
|
|
157
|
+
|
|
158
|
+
### Tracking jsonata-js
|
|
159
|
+
|
|
160
|
+
The tests come from the jsonata-js test suite (`vendor/jsonata-js/test/test-suite`). To pick up jsonata-js
|
|
161
|
+
changes:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
bundle exec rake jsonata_js:update # move the submodule to the latest jsonata-js master, and list the new commits
|
|
165
|
+
bundle exec rake jsonata_js:diff # show the src/ and test/ changes to port
|
|
166
|
+
bundle exec rspec # port the src/ changes until the tests pass
|
|
167
|
+
git add vendor/jsonata-js lib # commit the submodule bump together with the ported changes
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
The `jsonata-js sync` workflow (`.github/workflows/jsonata-js-sync.yaml`) does this daily: when jsonata-js
|
|
171
|
+
master moves, it bumps the submodule, has Claude port any `src/` and `test/` changes, and opens (or updates) a
|
|
172
|
+
pull request from the `jsonata-js-sync` branch. The PR is a draft if the tests or rubocop fail. The steps are in
|
|
173
|
+
`bin/jsonata-js-sync`; `DRY_RUN=1 bin/jsonata-js-sync publish` shows the commit message and PR description
|
|
174
|
+
for the current submodule checkout without pushing.
|
|
175
|
+
|
|
176
|
+
## Versioning
|
|
177
|
+
|
|
178
|
+
For consistency, the gem's version follows the jsonata-js version it is ported from, so `jsonata` 2.2.2
|
|
179
|
+
implements the same language and passes the same test suite as jsonata-js 2.2.2. Releases that contain only
|
|
180
|
+
Ruby-specific changes, between upstream releases, add a fourth segment (e.g. 2.2.2.1).
|
|
181
|
+
|
|
182
|
+
## License
|
|
183
|
+
|
|
184
|
+
The gem is available as open source under the terms of the [MIT License](LICENSE).
|
|
185
|
+
|
|
186
|
+
## Acknowledgments
|
|
187
|
+
|
|
188
|
+
This is a Ruby port of the [JSONata JavaScript library](https://github.com/jsonata-js/jsonata) created by
|
|
189
|
+
Andrew Coleman and contributors.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rake'
|
|
4
|
+
require 'rspec/core/rake_task'
|
|
5
|
+
|
|
6
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
7
|
+
|
|
8
|
+
task :default => :spec
|
|
9
|
+
|
|
10
|
+
desc 'Run RSpec tests'
|
|
11
|
+
task :test => :spec
|
|
12
|
+
|
|
13
|
+
desc 'Open an interactive console'
|
|
14
|
+
task :console do
|
|
15
|
+
require 'irb'
|
|
16
|
+
require_relative 'lib/jsonata'
|
|
17
|
+
ARGV.clear
|
|
18
|
+
IRB.start
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
JSONATA_JS_DIR = 'vendor/jsonata-js'
|
|
22
|
+
|
|
23
|
+
namespace :jsonata_js do
|
|
24
|
+
# The jsonata-js commit recorded in the superproject (HEAD, falling back to the index)
|
|
25
|
+
def recorded_jsonata_js_commit
|
|
26
|
+
commit = `git ls-tree HEAD #{JSONATA_JS_DIR} 2>/dev/null`.split[2]
|
|
27
|
+
commit || `git ls-files -s #{JSONATA_JS_DIR}`.split[1]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
desc 'Update the jsonata-js submodule to the latest jsonata-js master'
|
|
31
|
+
task :update do
|
|
32
|
+
sh "git -C #{JSONATA_JS_DIR} fetch origin"
|
|
33
|
+
sh "git -C #{JSONATA_JS_DIR} checkout --quiet origin/master"
|
|
34
|
+
sh "git -C #{JSONATA_JS_DIR} log --oneline #{recorded_jsonata_js_commit}..HEAD -- src test"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
desc 'Show the jsonata-js source and test changes that have not yet been committed to this repo'
|
|
38
|
+
task :diff do
|
|
39
|
+
sh "git -C #{JSONATA_JS_DIR} diff #{recorded_jsonata_js_commit} HEAD -- src test"
|
|
40
|
+
end
|
|
41
|
+
end
|