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.
Files changed (62) hide show
  1. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/CHANGELOG.md +35 -0
  2. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/Cargo.lock +1 -1
  3. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/Cargo.toml +1 -1
  4. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/PKG-INFO +13 -6
  5. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/README.md +12 -5
  6. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/bindings/c/README.md +10 -1
  7. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/bindings/c/examples/smoke.c +58 -0
  8. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/bindings/c/jsonata.h +35 -0
  9. jsonatapy-2.2.6/examples/host_functions.rs +113 -0
  10. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/pyproject.toml +1 -1
  11. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/python/jsonatapy/__init__.py +68 -0
  12. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/src/capi.rs +352 -4
  13. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/src/evaluator.rs +148 -0
  14. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/src/lib.rs +153 -2
  15. jsonatapy-2.2.6/tests/host_functions_test.rs +188 -0
  16. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/.gitignore +0 -0
  17. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/.gitmodules +0 -0
  18. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/.serena/.gitignore +0 -0
  19. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/.serena/memories/architecture.md +0 -0
  20. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/.serena/memories/conventions.md +0 -0
  21. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/.serena/memories/core.md +0 -0
  22. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/.serena/memories/memory_maintenance.md +0 -0
  23. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/.serena/memories/suggested_commands.md +0 -0
  24. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/.serena/memories/task_completion.md +0 -0
  25. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/.serena/memories/tech_stack.md +0 -0
  26. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/.serena/project.yml +0 -0
  27. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/LICENSE +0 -0
  28. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/benches/evaluator_bench.rs +0 -0
  29. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/examples/evaluator_demo.rs +0 -0
  30. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/examples/parser_demo.rs +0 -0
  31. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/examples/simd_json_bench.rs +0 -0
  32. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/python/jsonatapy/__main__.py +0 -0
  33. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/python/jsonatapy/_cli/__init__.py +0 -0
  34. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/python/jsonatapy/_cli/bindings.py +0 -0
  35. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/python/jsonatapy/_cli/error_format.py +0 -0
  36. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/python/jsonatapy/_cli/mcp_server.py +0 -0
  37. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/python/jsonatapy/_cli/resolve.py +0 -0
  38. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/python/jsonatapy/_cli/run.py +0 -0
  39. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/python/jsonatapy/py.typed +0 -0
  40. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/src/ast.rs +0 -0
  41. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/src/ast_transform.rs +0 -0
  42. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/src/bin/jsonata/bindings.rs +0 -0
  43. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/src/bin/jsonata/error_format.rs +0 -0
  44. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/src/bin/jsonata/main.rs +0 -0
  45. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/src/bin/jsonata/resolve.rs +0 -0
  46. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/src/compiler.rs +0 -0
  47. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/src/datetime.rs +0 -0
  48. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/src/functions.rs +0 -0
  49. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/src/lazy.rs +0 -0
  50. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/src/parser/README.md +0 -0
  51. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/src/parser.rs +0 -0
  52. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/src/signature.rs +0 -0
  53. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/src/value.rs +0 -0
  54. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/src/vm.rs +0 -0
  55. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/study/cli_fixtures.json +0 -0
  56. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/study/cli_fixtures_testdata.json +0 -0
  57. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/study/cli_spec.md +0 -0
  58. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/tests/cli_fixtures_test.rs +0 -0
  59. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/tests/cli_test.rs +0 -0
  60. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/tests/datetime_picture_suite.rs +0 -0
  61. {jsonatapy-2.2.5 → jsonatapy-2.2.6}/tests/integration_test.rs +0 -0
  62. {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
@@ -586,7 +586,7 @@ dependencies = [
586
586
 
587
587
  [[package]]
588
588
  name = "jsonata-core"
589
- version = "2.2.5"
589
+ version = "2.2.6"
590
590
  dependencies = [
591
591
  "assert_cmd",
592
592
  "base64",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "jsonata-core"
3
- version = "2.2.5"
3
+ version = "2.2.6"
4
4
  edition = "2021"
5
5
  authors = ["txjmb <txjmb@users.noreply.github.com>"]
6
6
  description = "High-performance Rust implementation of JSONata query and transformation language"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jsonatapy
3
- Version: 2.2.5
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
+ [![jsonatapy Downloads Last Month](https://assets.piptrends.com/get-last-month-downloads-badge/jsonatapy.svg 'jsonatapy Downloads Last Month by pip Trends')](https://piptrends.com/package/jsonatapy) [![jsonatapy Downloads Last Week](https://assets.piptrends.com/get-last-week-downloads-badge/jsonatapy.svg 'jsonatapy Downloads Last Week by pip Trends')](https://piptrends.com/package/jsonatapy) [![jsonatapy Average Daily Downloads](https://assets.piptrends.com/get-average-downloads-badge/jsonatapy.svg 'jsonatapy Average Daily Downloads by pip Trends')](https://piptrends.com/package/jsonatapy)
51
53
 
52
- High-performance [JSONata](https://jsonata.org/) implementation in Rust, with Python bindings.
54
+ #### Crates.io stats
55
+ ![downloads](https://shieldcn.dev/crates/d/jsonata-core.svg)
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
  [![Crates.io](https://img.shields.io/crates/v/jsonata-core.svg)](https://crates.io/crates/jsonata-core)
64
71
  [![PyPI version](https://badge.fury.io/py/jsonatapy.svg)](https://pypi.org/project/jsonatapy/)
65
72
  [![Python versions](https://img.shields.io/pypi/pyversions/jsonatapy.svg)](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
+ [![jsonatapy Downloads Last Month](https://assets.piptrends.com/get-last-month-downloads-badge/jsonatapy.svg 'jsonatapy Downloads Last Month by pip Trends')](https://piptrends.com/package/jsonatapy) [![jsonatapy Downloads Last Week](https://assets.piptrends.com/get-last-week-downloads-badge/jsonatapy.svg 'jsonatapy Downloads Last Week by pip Trends')](https://piptrends.com/package/jsonatapy) [![jsonatapy Average Daily Downloads](https://assets.piptrends.com/get-average-downloads-badge/jsonatapy.svg 'jsonatapy Average Daily Downloads by pip Trends')](https://piptrends.com/package/jsonatapy)
2
4
 
3
- High-performance [JSONata](https://jsonata.org/) implementation in Rust, with Python bindings.
5
+ #### Crates.io stats
6
+ ![downloads](https://shieldcn.dev/crates/d/jsonata-core.svg)
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
  [![Crates.io](https://img.shields.io/crates/v/jsonata-core.svg)](https://crates.io/crates/jsonata-core)
15
22
  [![PyPI version](https://badge.fury.io/py/jsonatapy.svg)](https://pypi.org/project/jsonatapy/)
16
23
  [![Python versions](https://img.shields.io/pypi/pyversions/jsonatapy.svg)](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. **Panics:** internal engine panics are caught at the boundary and
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.5"
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,