jsonatapy 2.2.5__tar.gz → 2.2.6__tar.gz
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.
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/CHANGELOG.md +35 -0
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/Cargo.lock +1 -1
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/Cargo.toml +1 -1
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/PKG-INFO +13 -6
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/README.md +12 -5
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/bindings/c/README.md +10 -1
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/bindings/c/examples/smoke.c +58 -0
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/bindings/c/jsonata.h +35 -0
- jsonatapy-2.2.6/examples/host_functions.rs +113 -0
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/pyproject.toml +1 -1
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/python/jsonatapy/__init__.py +68 -0
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/src/capi.rs +352 -4
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/src/evaluator.rs +148 -0
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/src/lib.rs +153 -2
- jsonatapy-2.2.6/tests/host_functions_test.rs +188 -0
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/.gitignore +0 -0
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/.gitmodules +0 -0
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/.serena/.gitignore +0 -0
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/.serena/memories/architecture.md +0 -0
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/.serena/memories/conventions.md +0 -0
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/.serena/memories/core.md +0 -0
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/.serena/memories/memory_maintenance.md +0 -0
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/.serena/memories/suggested_commands.md +0 -0
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/.serena/memories/task_completion.md +0 -0
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/.serena/memories/tech_stack.md +0 -0
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/.serena/project.yml +0 -0
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/LICENSE +0 -0
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/benches/evaluator_bench.rs +0 -0
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/examples/evaluator_demo.rs +0 -0
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/examples/parser_demo.rs +0 -0
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/examples/simd_json_bench.rs +0 -0
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/python/jsonatapy/__main__.py +0 -0
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/python/jsonatapy/_cli/__init__.py +0 -0
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/python/jsonatapy/_cli/bindings.py +0 -0
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/python/jsonatapy/_cli/error_format.py +0 -0
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/python/jsonatapy/_cli/mcp_server.py +0 -0
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/python/jsonatapy/_cli/resolve.py +0 -0
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/python/jsonatapy/_cli/run.py +0 -0
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/python/jsonatapy/py.typed +0 -0
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/src/ast.rs +0 -0
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/src/ast_transform.rs +0 -0
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/src/bin/jsonata/bindings.rs +0 -0
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/src/bin/jsonata/error_format.rs +0 -0
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/src/bin/jsonata/main.rs +0 -0
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/src/bin/jsonata/resolve.rs +0 -0
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/src/compiler.rs +0 -0
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/src/datetime.rs +0 -0
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/src/functions.rs +0 -0
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/src/lazy.rs +0 -0
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/src/parser/README.md +0 -0
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/src/parser.rs +0 -0
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/src/signature.rs +0 -0
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/src/value.rs +0 -0
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/src/vm.rs +0 -0
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/study/cli_fixtures.json +0 -0
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/study/cli_fixtures_testdata.json +0 -0
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/study/cli_spec.md +0 -0
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/tests/cli_fixtures_test.rs +0 -0
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/tests/cli_test.rs +0 -0
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/tests/datetime_picture_suite.rs +0 -0
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/tests/integration_test.rs +0 -0
- {jsonatapy-2.2.5 → jsonatapy-2.2.6}/tests/parent_and_focus_binding_suite.rs +0 -0
|
@@ -19,6 +19,41 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
19
19
|
|
|
20
20
|
### Security
|
|
21
21
|
|
|
22
|
+
## [2.2.6] - 2026-07-20
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
- Host-callable custom functions (Rust core): `Evaluator::register_fn` and
|
|
26
|
+
`Evaluator::register_fn_override` let a host register native functions callable
|
|
27
|
+
from an expression as `$name(...)` — the equivalent of jsonata-js's
|
|
28
|
+
`registerFunction`. Functions are plain closures
|
|
29
|
+
(`Fn(&[JValue]) -> Result<JValue, EvaluatorError>`) and resolve after the
|
|
30
|
+
expression's own bindings/lambdas and before built-ins. `register_fn` rejects
|
|
31
|
+
collisions with built-ins; `register_fn_override` allows deliberately replacing
|
|
32
|
+
the impure built-ins (`$now`, `$millis`, `$random`, `$eval`) for determinism
|
|
33
|
+
injection or sandboxing. Evaluation stays synchronous. See
|
|
34
|
+
`examples/host_functions.rs` and the Rust crate docs.
|
|
35
|
+
- Host-callable custom functions (Python binding): `JsonataExpression.register(name,
|
|
36
|
+
func)` and `.register_override(name, func)` expose the above to Python. The callable
|
|
37
|
+
receives already-evaluated positional arguments and must return a JSON-compatible
|
|
38
|
+
value synchronously; an `async def` (coroutine) is rejected at call time with
|
|
39
|
+
guidance to await I/O outside jsonata and pass results via `bindings`. Collision and
|
|
40
|
+
compilable-builtin-override rules are validated at `register()` time.
|
|
41
|
+
- Host-callable custom functions (C ABI): `jsonata_register_fn(expr, name, fn, user_data)`
|
|
42
|
+
and `jsonata_register_fn_override(...)` expose the feature to C and any language with C
|
|
43
|
+
interop. The callback receives its arguments as a JSON array string and returns a JSON
|
|
44
|
+
result string (jsonata copies it; the host retains ownership), or NULL to signal an error.
|
|
45
|
+
See `bindings/c/jsonata.h`, `bindings/c/README.md`, and `bindings/c/examples/smoke.c`.
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
|
|
49
|
+
### Deprecated
|
|
50
|
+
|
|
51
|
+
### Removed
|
|
52
|
+
|
|
53
|
+
### Fixed
|
|
54
|
+
|
|
55
|
+
### Security
|
|
56
|
+
|
|
22
57
|
## [2.2.5] - 2026-07-14
|
|
23
58
|
|
|
24
59
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: jsonatapy
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.6
|
|
4
4
|
Classifier: Development Status :: 4 - Beta
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -47,19 +47,26 @@ Project-URL: Documentation, https://github.com/txjmb/jsonata-core
|
|
|
47
47
|
Project-URL: Homepage, https://github.com/txjmb/jsonata-core
|
|
48
48
|
Project-URL: Repository, https://github.com/txjmb/jsonata-core
|
|
49
49
|
|
|
50
|
-
# jsonata-core + jsonatapy
|
|
50
|
+
# jsonata-core (rust) + jsonatapy + jsonata C-ABI library + jsonata cli
|
|
51
|
+
#### Pypi stats
|
|
52
|
+
[](https://piptrends.com/package/jsonatapy) [](https://piptrends.com/package/jsonatapy) [](https://piptrends.com/package/jsonatapy)
|
|
51
53
|
|
|
52
|
-
|
|
54
|
+
#### Crates.io stats
|
|
55
|
+

|
|
56
|
+
|
|
57
|
+
High-performance [JSONata](https://jsonata.org/) implementation in Rust, with Python binding and C ABI/library. If you use this library, please add a github star!
|
|
53
58
|
|
|
54
59
|
Much of this project was built using Claude Code with significant human oversight. There was no performant
|
|
55
60
|
JSONata implementation in Python, so the goal was to port JSONata to Rust (with a PyO3 wrapper
|
|
56
61
|
for Python) and see how fast it could go. The answer: faster than V8 for most expression
|
|
57
|
-
workloads, and faster than the next pure-Rust implementation. The rust versions are published on crates.io, and the python wheels on pypi.
|
|
62
|
+
workloads, and faster than the next pure-Rust implementation. The rust versions are published on crates.io, and the python wheels on pypi. There is also a command-line binary and Python command-line available (works great with uvx) for use in scripting. The Python library is also usable in a command-line fashion, and a C-compatible library is available for those who want to easily use jsonata in C/C++.
|
|
58
63
|
|
|
59
|
-
Many, many thanks to the incredible work of all the maintainers of the [JSONata](https://github.com/jsonata-js/jsonata) reference library. JSONata is a very powerful, well-designed, and useful language that has made an impact on many projects. This project leverages their outstanding work to extend that capability to Python and Rust and would not be possible without that project. The implementation in Rust was strongly influenced by their implementation. The 1600+ tests they created provided the scaffolding and validation for all of this project. This project will continue to follow and be a derivative of the reference project as the JSONata reference library evolves.
|
|
64
|
+
Many, many thanks to the incredible work of all the maintainers of the [JSONata](https://github.com/jsonata-js/jsonata) reference library. JSONata is a very powerful, well-designed, and useful language that has made an impact on many projects. This project leverages their outstanding work to extend that capability to Python and Rust and would not be possible without that project. The implementation in Rust was strongly influenced by their implementation. The 1600+ (1682/1682 passing for last build of this project) tests they created provided the scaffolding and validation for all of this project. This project will continue to follow and be a derivative of the reference project as the JSONata reference library evolves.
|
|
60
65
|
|
|
61
66
|
Release versions will follow the reference jsonata-js project major and minor release numbers, but not necessarily patches. This will make it easier for adopters of this library to understand each release's JSONata API compatibility. As an example, 2.1.7 should be compliant with 2.1.x jsonata-js tests, but may have fixes specific to this library. If a patch release for jsonata-js is relevant for this project, it will be included in a patch release that may or may not follow the patch numbers of the upstream project.
|
|
62
67
|
|
|
68
|
+
This project currently chooses not to implement async, because it has limited value for most of the most common use-cases. We
|
|
69
|
+
|
|
63
70
|
[](https://crates.io/crates/jsonata-core)
|
|
64
71
|
[](https://pypi.org/project/jsonatapy/)
|
|
65
72
|
[](https://pypi.org/project/jsonatapy/)
|
|
@@ -141,7 +148,7 @@ Supports Python 3.10, 3.11, 3.12, 3.13, 3.14 on Linux, macOS (Intel & ARM), and
|
|
|
141
148
|
|
|
142
149
|
## Command-line quick start
|
|
143
150
|
|
|
144
|
-
Both packages also ship a CLI, `jq`-shaped, with an identical contract:
|
|
151
|
+
Both packages also ship a binary CLI and a Python-based CLI, `jq`-shaped, with an identical contract:
|
|
145
152
|
|
|
146
153
|
```bash
|
|
147
154
|
pip install jsonatapy
|
|
@@ -1,16 +1,23 @@
|
|
|
1
|
-
# jsonata-core + jsonatapy
|
|
1
|
+
# jsonata-core (rust) + jsonatapy + jsonata C-ABI library + jsonata cli
|
|
2
|
+
#### Pypi stats
|
|
3
|
+
[](https://piptrends.com/package/jsonatapy) [](https://piptrends.com/package/jsonatapy) [](https://piptrends.com/package/jsonatapy)
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
#### Crates.io stats
|
|
6
|
+

|
|
7
|
+
|
|
8
|
+
High-performance [JSONata](https://jsonata.org/) implementation in Rust, with Python binding and C ABI/library. If you use this library, please add a github star!
|
|
4
9
|
|
|
5
10
|
Much of this project was built using Claude Code with significant human oversight. There was no performant
|
|
6
11
|
JSONata implementation in Python, so the goal was to port JSONata to Rust (with a PyO3 wrapper
|
|
7
12
|
for Python) and see how fast it could go. The answer: faster than V8 for most expression
|
|
8
|
-
workloads, and faster than the next pure-Rust implementation. The rust versions are published on crates.io, and the python wheels on pypi.
|
|
13
|
+
workloads, and faster than the next pure-Rust implementation. The rust versions are published on crates.io, and the python wheels on pypi. There is also a command-line binary and Python command-line available (works great with uvx) for use in scripting. The Python library is also usable in a command-line fashion, and a C-compatible library is available for those who want to easily use jsonata in C/C++.
|
|
9
14
|
|
|
10
|
-
Many, many thanks to the incredible work of all the maintainers of the [JSONata](https://github.com/jsonata-js/jsonata) reference library. JSONata is a very powerful, well-designed, and useful language that has made an impact on many projects. This project leverages their outstanding work to extend that capability to Python and Rust and would not be possible without that project. The implementation in Rust was strongly influenced by their implementation. The 1600+ tests they created provided the scaffolding and validation for all of this project. This project will continue to follow and be a derivative of the reference project as the JSONata reference library evolves.
|
|
15
|
+
Many, many thanks to the incredible work of all the maintainers of the [JSONata](https://github.com/jsonata-js/jsonata) reference library. JSONata is a very powerful, well-designed, and useful language that has made an impact on many projects. This project leverages their outstanding work to extend that capability to Python and Rust and would not be possible without that project. The implementation in Rust was strongly influenced by their implementation. The 1600+ (1682/1682 passing for last build of this project) tests they created provided the scaffolding and validation for all of this project. This project will continue to follow and be a derivative of the reference project as the JSONata reference library evolves.
|
|
11
16
|
|
|
12
17
|
Release versions will follow the reference jsonata-js project major and minor release numbers, but not necessarily patches. This will make it easier for adopters of this library to understand each release's JSONata API compatibility. As an example, 2.1.7 should be compliant with 2.1.x jsonata-js tests, but may have fixes specific to this library. If a patch release for jsonata-js is relevant for this project, it will be included in a patch release that may or may not follow the patch numbers of the upstream project.
|
|
13
18
|
|
|
19
|
+
This project currently chooses not to implement async, because it has limited value for most of the most common use-cases. We
|
|
20
|
+
|
|
14
21
|
[](https://crates.io/crates/jsonata-core)
|
|
15
22
|
[](https://pypi.org/project/jsonatapy/)
|
|
16
23
|
[](https://pypi.org/project/jsonatapy/)
|
|
@@ -92,7 +99,7 @@ Supports Python 3.10, 3.11, 3.12, 3.13, 3.14 on Linux, macOS (Intel & ARM), and
|
|
|
92
99
|
|
|
93
100
|
## Command-line quick start
|
|
94
101
|
|
|
95
|
-
Both packages also ship a CLI, `jq`-shaped, with an identical contract:
|
|
102
|
+
Both packages also ship a binary CLI and a Python-based CLI, `jq`-shaped, with an identical contract:
|
|
96
103
|
|
|
97
104
|
```bash
|
|
98
105
|
pip install jsonatapy
|
|
@@ -132,7 +132,16 @@ Rust library rebuilds with your project.)
|
|
|
132
132
|
6. **Variables:** `jsonata_bind_var(expr, "x", "[1,2,3]")` binds `$x` for
|
|
133
133
|
all subsequent evaluations on that handle (values are JSON text;
|
|
134
134
|
re-binding replaces).
|
|
135
|
-
7. **
|
|
135
|
+
7. **Host functions:** `jsonata_register_fn(expr, "name", fn, user_data)`
|
|
136
|
+
exposes a C callback to the expression as `$name(...)`. The callback
|
|
137
|
+
receives its arguments as a JSON array string and returns a JSON result
|
|
138
|
+
string that jsonata copies (you keep ownership — do not expect jsonata to
|
|
139
|
+
free it); returning NULL signals an error. A name colliding with a
|
|
140
|
+
built-in is rejected; `jsonata_register_fn_override` replaces a built-in
|
|
141
|
+
deliberately (e.g. a frozen `$now`, or a disabled `$eval`). `user_data`
|
|
142
|
+
must outlive the handle. Registering a host function routes evaluation
|
|
143
|
+
through the tree-walker, like variable bindings.
|
|
144
|
+
8. **Panics:** internal engine panics are caught at the boundary and
|
|
136
145
|
surface as errors prefixed `internal error:` — they will not abort
|
|
137
146
|
your process.
|
|
138
147
|
|
|
@@ -27,6 +27,31 @@ static int failures = 0;
|
|
|
27
27
|
|
|
28
28
|
static char *take_error(void) { return jsonata_last_error_message(); }
|
|
29
29
|
|
|
30
|
+
/*
|
|
31
|
+
* Host function: given the JSON array ["<name>"], returns "hello <name>".
|
|
32
|
+
* Uses a static buffer — valid until the next call, which is all jsonata needs
|
|
33
|
+
* (it copies the result before returning to the expression).
|
|
34
|
+
*/
|
|
35
|
+
static char host_buf[256];
|
|
36
|
+
static const char *greet_cb(void *user_data, const char *args_json) {
|
|
37
|
+
(void)user_data;
|
|
38
|
+
const char *open = strchr(args_json, '"');
|
|
39
|
+
if (!open) return NULL;
|
|
40
|
+
const char *start = open + 1;
|
|
41
|
+
const char *end = strchr(start, '"');
|
|
42
|
+
if (!end) return NULL;
|
|
43
|
+
int len = (int)(end - start);
|
|
44
|
+
snprintf(host_buf, sizeof(host_buf), "\"hello %.*s\"", len, start);
|
|
45
|
+
return host_buf;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* Host override for $now(): a frozen timestamp (static string, always valid). */
|
|
49
|
+
static const char *frozen_now_cb(void *user_data, const char *args_json) {
|
|
50
|
+
(void)user_data;
|
|
51
|
+
(void)args_json;
|
|
52
|
+
return "\"2020-01-01T00:00:00.000Z\"";
|
|
53
|
+
}
|
|
54
|
+
|
|
30
55
|
int main(void) {
|
|
31
56
|
/* version */
|
|
32
57
|
const char *v = jsonata_version();
|
|
@@ -119,6 +144,39 @@ int main(void) {
|
|
|
119
144
|
jsonata_free_expr(e);
|
|
120
145
|
}
|
|
121
146
|
|
|
147
|
+
/* host function registration + call */
|
|
148
|
+
{
|
|
149
|
+
JsonataExpr *e = jsonata_compile("$greet(name)");
|
|
150
|
+
int rc = jsonata_register_fn(e, "greet", greet_cb, NULL);
|
|
151
|
+
CHECK(rc == 0, "register_fn succeeds");
|
|
152
|
+
char *r = jsonata_evaluate(e, "{\"name\":\"Ada\"}");
|
|
153
|
+
CHECK(r != NULL && strcmp(r, "\"hello Ada\"") == 0, "host function call");
|
|
154
|
+
jsonata_free_string(r);
|
|
155
|
+
jsonata_free_expr(e);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/* host function override of an impure built-in ($now) */
|
|
159
|
+
{
|
|
160
|
+
JsonataExpr *e = jsonata_compile("$now()");
|
|
161
|
+
int rc = jsonata_register_fn_override(e, "now", frozen_now_cb, NULL);
|
|
162
|
+
CHECK(rc == 0, "register_fn_override succeeds");
|
|
163
|
+
char *r = jsonata_evaluate(e, "null");
|
|
164
|
+
CHECK(r != NULL && strcmp(r, "\"2020-01-01T00:00:00.000Z\"") == 0,
|
|
165
|
+
"override $now");
|
|
166
|
+
jsonata_free_string(r);
|
|
167
|
+
jsonata_free_expr(e);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/* collision with a built-in is rejected */
|
|
171
|
+
{
|
|
172
|
+
JsonataExpr *e = jsonata_compile("$sum(x)");
|
|
173
|
+
int rc = jsonata_register_fn(e, "sum", greet_cb, NULL);
|
|
174
|
+
char *err = take_error();
|
|
175
|
+
CHECK(rc == -1 && err != NULL, "register_fn collision rejected");
|
|
176
|
+
jsonata_free_string(err);
|
|
177
|
+
jsonata_free_expr(e);
|
|
178
|
+
}
|
|
179
|
+
|
|
122
180
|
/* NULL tolerance of free functions */
|
|
123
181
|
jsonata_free_expr(NULL);
|
|
124
182
|
jsonata_free_string(NULL);
|
|
@@ -75,6 +75,41 @@ char *jsonata_evaluate(JsonataExpr *expr, const char *json_utf8);
|
|
|
75
75
|
int jsonata_bind_var(JsonataExpr *expr, const char *name,
|
|
76
76
|
const char *json_value_utf8);
|
|
77
77
|
|
|
78
|
+
/*
|
|
79
|
+
* A host function callback, callable from the expression as $name(...).
|
|
80
|
+
* user_data: the pointer supplied at registration (opaque to jsonata).
|
|
81
|
+
* args_json: a NUL-terminated UTF-8 JSON ARRAY of the (already evaluated)
|
|
82
|
+
* arguments.
|
|
83
|
+
* Returns a NUL-terminated UTF-8 JSON string with the result, which must stay
|
|
84
|
+
* valid until the call returns — jsonata COPIES it and does NOT free it — or
|
|
85
|
+
* NULL to signal an error.
|
|
86
|
+
*/
|
|
87
|
+
typedef const char *(*jsonata_host_fn)(void *user_data, const char *args_json);
|
|
88
|
+
|
|
89
|
+
/*
|
|
90
|
+
* Register a host function callable from the expression as $name(...) — the
|
|
91
|
+
* C equivalent of jsonata-js's registerFunction. Applies to every subsequent
|
|
92
|
+
* jsonata_evaluate() on this handle. A leading '$' in name is accepted and
|
|
93
|
+
* stripped; re-registering a name replaces it. Host functions resolve after
|
|
94
|
+
* the expression's own bindings/lambdas and before built-ins.
|
|
95
|
+
*
|
|
96
|
+
* user_data is passed back to the callback unchanged and must remain valid for
|
|
97
|
+
* the lifetime of the handle. Returns 0 on success, -1 on error (slot set) —
|
|
98
|
+
* including when name collides with a built-in (use
|
|
99
|
+
* jsonata_register_fn_override to replace a built-in deliberately).
|
|
100
|
+
*/
|
|
101
|
+
int jsonata_register_fn(JsonataExpr *expr, const char *name,
|
|
102
|
+
jsonata_host_fn fn, void *user_data);
|
|
103
|
+
|
|
104
|
+
/*
|
|
105
|
+
* Like jsonata_register_fn, but deliberately replaces a built-in of the same
|
|
106
|
+
* name — for determinism injection (a frozen $now, seeded $random) or
|
|
107
|
+
* sandboxing (disabling $eval). Overriding a built-in that participates in the
|
|
108
|
+
* compiled fast path returns -1 with an error.
|
|
109
|
+
*/
|
|
110
|
+
int jsonata_register_fn_override(JsonataExpr *expr, const char *name,
|
|
111
|
+
jsonata_host_fn fn, void *user_data);
|
|
112
|
+
|
|
78
113
|
/* Free a handle returned by jsonata_compile(). NULL is a no-op. */
|
|
79
114
|
void jsonata_free_expr(JsonataExpr *expr);
|
|
80
115
|
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Host-callable custom functions
|
|
2
|
+
//
|
|
3
|
+
// Shows how a host application registers native Rust functions that a JSONata
|
|
4
|
+
// expression can call as `$name(...)`:
|
|
5
|
+
// - enrichment/lookup functions (the canonical use case)
|
|
6
|
+
// - determinism injection: overriding an impure built-in ($now) with a frozen
|
|
7
|
+
// implementation for reproducible output
|
|
8
|
+
// - sandboxing: overriding a powerful built-in ($eval) to disable it
|
|
9
|
+
//
|
|
10
|
+
// Run with: cargo run --example host_functions
|
|
11
|
+
|
|
12
|
+
use jsonata_core::value::JValue;
|
|
13
|
+
use jsonata_core::{evaluator::Evaluator, parser::parse};
|
|
14
|
+
use serde_json::json;
|
|
15
|
+
|
|
16
|
+
fn main() {
|
|
17
|
+
demo_enrichment_lookup();
|
|
18
|
+
demo_multi_arg();
|
|
19
|
+
demo_override_now();
|
|
20
|
+
demo_sandbox_eval();
|
|
21
|
+
demo_collision_is_rejected();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
fn eval(ev: &mut Evaluator, expr: &str, data: serde_json::Value) -> JValue {
|
|
25
|
+
let ast = parse(expr).expect("expression parses");
|
|
26
|
+
ev.evaluate(&ast, &JValue::from(data))
|
|
27
|
+
.expect("evaluation succeeds")
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// The canonical use case: a lookup backed by host-owned data. The expression
|
|
31
|
+
/// stays a clean artifact; the host owns the (here trivial) data source.
|
|
32
|
+
fn demo_enrichment_lookup() {
|
|
33
|
+
let mut ev = Evaluator::new();
|
|
34
|
+
ev.register_fn("productName", |args: &[JValue]| {
|
|
35
|
+
let sku = args.first().and_then(|v| v.as_str()).unwrap_or("");
|
|
36
|
+
let name = match sku {
|
|
37
|
+
"A-1" => "Widget",
|
|
38
|
+
"B-2" => "Gadget",
|
|
39
|
+
_ => "Unknown",
|
|
40
|
+
};
|
|
41
|
+
Ok(JValue::from(name))
|
|
42
|
+
})
|
|
43
|
+
.unwrap();
|
|
44
|
+
|
|
45
|
+
let out = eval(
|
|
46
|
+
&mut ev,
|
|
47
|
+
"items.{ 'sku': sku, 'name': $productName(sku) }",
|
|
48
|
+
json!({ "items": [ { "sku": "A-1" }, { "sku": "B-2" } ] }),
|
|
49
|
+
);
|
|
50
|
+
println!("enrichment lookup: {}", out.to_json_string().unwrap());
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// Host functions receive all arguments already evaluated.
|
|
54
|
+
fn demo_multi_arg() {
|
|
55
|
+
let mut ev = Evaluator::new();
|
|
56
|
+
ev.register_fn("convert", |args: &[JValue]| {
|
|
57
|
+
let amount = args.first().and_then(|v| v.as_f64()).unwrap_or(0.0);
|
|
58
|
+
let rate = match args.get(1).and_then(|v| v.as_str()) {
|
|
59
|
+
Some("EUR") => 1.1,
|
|
60
|
+
_ => 1.0,
|
|
61
|
+
};
|
|
62
|
+
Ok(JValue::from_f64(amount * rate))
|
|
63
|
+
})
|
|
64
|
+
.unwrap();
|
|
65
|
+
|
|
66
|
+
let out = eval(
|
|
67
|
+
&mut ev,
|
|
68
|
+
"$convert(amount, currency)",
|
|
69
|
+
json!({ "amount": 10, "currency": "EUR" }),
|
|
70
|
+
);
|
|
71
|
+
println!("multi-arg convert: {}", out.to_json_string().unwrap());
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// Determinism injection: freeze `$now()` so output is reproducible in tests.
|
|
75
|
+
/// `$now` is a non-compilable (impure) built-in, so overriding it is allowed.
|
|
76
|
+
fn demo_override_now() {
|
|
77
|
+
let mut ev = Evaluator::new();
|
|
78
|
+
ev.register_fn_override("now", |_args: &[JValue]| {
|
|
79
|
+
Ok(JValue::from("2020-01-01T00:00:00.000Z"))
|
|
80
|
+
})
|
|
81
|
+
.unwrap();
|
|
82
|
+
|
|
83
|
+
let out = eval(&mut ev, "{ 'generatedAt': $now() }", json!(null));
|
|
84
|
+
println!("frozen $now: {}", out.to_json_string().unwrap());
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/// Sandboxing: disable the dynamic-evaluation built-in `$eval` when running
|
|
88
|
+
/// semi-trusted expressions.
|
|
89
|
+
fn demo_sandbox_eval() {
|
|
90
|
+
let mut ev = Evaluator::new();
|
|
91
|
+
ev.register_fn_override("eval", |_args: &[JValue]| {
|
|
92
|
+
Err(jsonata_core::evaluator::EvaluatorError::EvaluationError(
|
|
93
|
+
"$eval is disabled in this environment".to_string(),
|
|
94
|
+
))
|
|
95
|
+
})
|
|
96
|
+
.unwrap();
|
|
97
|
+
|
|
98
|
+
let ast = parse("$eval('1 + 1')").unwrap();
|
|
99
|
+
match ev.evaluate(&ast, &JValue::Null) {
|
|
100
|
+
Ok(v) => println!("sandbox $eval: unexpectedly returned {v:?}"),
|
|
101
|
+
Err(e) => println!("sandbox $eval: blocked as expected -> {e}"),
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/// Registering a name that collides with a built-in is refused; use
|
|
106
|
+
/// `register_fn_override` to replace a built-in deliberately.
|
|
107
|
+
fn demo_collision_is_rejected() {
|
|
108
|
+
let mut ev = Evaluator::new();
|
|
109
|
+
match ev.register_fn("sum", |_args: &[JValue]| Ok(JValue::Null)) {
|
|
110
|
+
Ok(()) => println!("collision: unexpectedly accepted"),
|
|
111
|
+
Err(e) => println!("collision: rejected as expected -> {e}"),
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "jsonatapy"
|
|
7
|
-
version = "2.2.
|
|
7
|
+
version = "2.2.6"
|
|
8
8
|
description = "High-performance Python/Rust implementation of JSONata query and transformation language"
|
|
9
9
|
authors = [
|
|
10
10
|
{name = "txjmb", email = "txjmb@users.noreply.github.com"}
|
|
@@ -16,6 +16,7 @@ Example:
|
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
18
|
import json as _json
|
|
19
|
+
from collections.abc import Callable
|
|
19
20
|
from typing import Any
|
|
20
21
|
|
|
21
22
|
from ._jsonatapy import (
|
|
@@ -185,6 +186,73 @@ class JsonataExpression:
|
|
|
185
186
|
"""
|
|
186
187
|
return self._expr.evaluate(data, bindings, timeout, max_stack_depth, max_sequence_length)
|
|
187
188
|
|
|
189
|
+
def register(self, name: str, func: Callable[..., Any]) -> "JsonataExpression":
|
|
190
|
+
"""
|
|
191
|
+
Register a Python function callable from the expression as ``$name(...)``.
|
|
192
|
+
|
|
193
|
+
The function receives the (already-evaluated) arguments as positional
|
|
194
|
+
Python values and must return a JSON-compatible value **synchronously**.
|
|
195
|
+
This is the equivalent of jsonata-js's ``registerFunction`` — use it to
|
|
196
|
+
expose enrichment/lookup, formatting, or scoring logic to an expression.
|
|
197
|
+
|
|
198
|
+
Host functions resolve after the expression's own bindings/lambdas and
|
|
199
|
+
before built-ins. A name that collides with a built-in is rejected; use
|
|
200
|
+
:meth:`register_override` to replace a built-in deliberately.
|
|
201
|
+
|
|
202
|
+
Note:
|
|
203
|
+
The evaluator is synchronous, so an ``async def`` (which returns a
|
|
204
|
+
coroutine) is rejected at call time. For async I/O, await it outside
|
|
205
|
+
jsonata and pass the result in via ``bindings``.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
name: The function name, called as ``$name(...)`` in the expression.
|
|
209
|
+
func: A callable ``(*args) -> JSON-compatible value``.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
self, to allow chaining.
|
|
213
|
+
|
|
214
|
+
Raises:
|
|
215
|
+
TypeError: If ``func`` is not callable.
|
|
216
|
+
ValueError: If ``name`` collides with a built-in function.
|
|
217
|
+
|
|
218
|
+
Example:
|
|
219
|
+
>>> expr = compile("$greet(name)")
|
|
220
|
+
>>> expr.register("greet", lambda n: f"hello {n}")
|
|
221
|
+
>>> expr.evaluate({"name": "Ada"})
|
|
222
|
+
'hello Ada'
|
|
223
|
+
"""
|
|
224
|
+
self._expr.register(name, func)
|
|
225
|
+
return self
|
|
226
|
+
|
|
227
|
+
def register_override(self, name: str, func: Callable[..., Any]) -> "JsonataExpression":
|
|
228
|
+
"""
|
|
229
|
+
Register a Python function that deliberately replaces a built-in.
|
|
230
|
+
|
|
231
|
+
The two legitimate uses are determinism injection for the impure
|
|
232
|
+
built-ins (``$now``, ``$millis``, ``$random``) — e.g. a frozen clock for
|
|
233
|
+
reproducible output — and sandboxing (disabling ``$eval``). Overriding a
|
|
234
|
+
built-in that participates in the compiled fast path is rejected.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
name: The built-in name to replace (e.g. ``"now"``).
|
|
238
|
+
func: A callable ``(*args) -> JSON-compatible value``.
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
self, to allow chaining.
|
|
242
|
+
|
|
243
|
+
Raises:
|
|
244
|
+
TypeError: If ``func`` is not callable.
|
|
245
|
+
ValueError: If the built-in cannot be safely overridden.
|
|
246
|
+
|
|
247
|
+
Example:
|
|
248
|
+
>>> expr = compile("$now()")
|
|
249
|
+
>>> expr.register_override("now", lambda: "2020-01-01T00:00:00.000Z")
|
|
250
|
+
>>> expr.evaluate(None)
|
|
251
|
+
'2020-01-01T00:00:00.000Z'
|
|
252
|
+
"""
|
|
253
|
+
self._expr.register_override(name, func)
|
|
254
|
+
return self
|
|
255
|
+
|
|
188
256
|
def evaluate_json(
|
|
189
257
|
self,
|
|
190
258
|
json_str: str,
|