j-perm 0.1.3__py3-none-any.whl → 0.2.0__py3-none-any.whl
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.
- j_perm/__init__.py +15 -7
- j_perm/casters/__init__.py +4 -0
- j_perm/casters/_bool.py +9 -0
- j_perm/casters/_float.py +8 -0
- j_perm/casters/_int.py +8 -0
- j_perm/casters/_str.py +8 -0
- j_perm/constructs/__init__.py +2 -0
- j_perm/constructs/eval.py +16 -0
- j_perm/constructs/ref.py +21 -0
- j_perm/engine.py +139 -63
- j_perm/funcs/__init__.py +1 -0
- j_perm/funcs/subtract.py +11 -0
- j_perm/op_handler.py +57 -0
- j_perm/ops/__init__.py +1 -0
- j_perm/ops/{assert.py → _assert.py} +4 -4
- j_perm/ops/_exec.py +8 -9
- j_perm/ops/_if.py +8 -9
- j_perm/ops/copy.py +5 -5
- j_perm/ops/copy_d.py +5 -5
- j_perm/ops/delete.py +4 -4
- j_perm/ops/distinct.py +5 -5
- j_perm/ops/foreach.py +6 -7
- j_perm/ops/replace_root.py +5 -7
- j_perm/ops/set.py +6 -7
- j_perm/ops/update.py +7 -8
- j_perm/schema/__init__.py +109 -109
- j_perm/special_resolver.py +115 -0
- j_perm/subst.py +300 -0
- j_perm-0.2.0.dist-info/METADATA +818 -0
- j_perm-0.2.0.dist-info/RECORD +34 -0
- {j_perm-0.1.3.dist-info → j_perm-0.2.0.dist-info}/WHEEL +1 -1
- j_perm/jmes_ext.py +0 -17
- j_perm/registry.py +0 -30
- j_perm/utils/special.py +0 -41
- j_perm/utils/subst.py +0 -133
- j_perm/utils/tuples.py +0 -17
- j_perm-0.1.3.dist-info/METADATA +0 -116
- j_perm-0.1.3.dist-info/RECORD +0 -26
- {j_perm-0.1.3.dist-info → j_perm-0.2.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,818 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: j-perm
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: json permutation library
|
|
5
|
+
Author-email: Roman <kuschanow@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/kuschanow/j-perm
|
|
8
|
+
Project-URL: Source, https://github.com/kuschanow/j-perm
|
|
9
|
+
Project-URL: Tracker, https://github.com/kuschanow/j-perm/issues
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: jmespath
|
|
13
|
+
|
|
14
|
+
# J-Perm
|
|
15
|
+
|
|
16
|
+
A small, composable JSON-transformation DSL implemented in Python.
|
|
17
|
+
|
|
18
|
+
The library lets you describe transformations as **data** (a list of steps) and then apply them to an input document. It supports JSON Pointer paths, custom JMESPath expressions, interpolation with `${...}` syntax, special reference/evaluation values, and a rich set of built-in operations.
|
|
19
|
+
|
|
20
|
+
J-Perm is built around a **pluggable architecture**: operations, special constructs, JMESPath functions, and casters are all registered independently and composed into an execution engine.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Features
|
|
25
|
+
|
|
26
|
+
* JSON Pointer read/write with support for:
|
|
27
|
+
|
|
28
|
+
* root pointers (`""`, `"/"`, `"."`)
|
|
29
|
+
* relative `..` segments
|
|
30
|
+
* list slices like `/items[1:3]`
|
|
31
|
+
* Interpolation templates:
|
|
32
|
+
|
|
33
|
+
* `${/path/to/node}` — JSON Pointer lookup
|
|
34
|
+
* `${int:/path}` / `${float:/path}` / `${bool:/path}` — type casters
|
|
35
|
+
* `${? some.jmespath(expression) }` — JMESPath with custom functions
|
|
36
|
+
* Special values:
|
|
37
|
+
|
|
38
|
+
* `$ref` — reference into the source document
|
|
39
|
+
* `$eval` — nested DSL evaluation with optional `$select`
|
|
40
|
+
* Built-in operations:
|
|
41
|
+
|
|
42
|
+
* `set`, `copy`, `copyD`, `delete`, `assert`
|
|
43
|
+
* `foreach`, `if`, `distinct`
|
|
44
|
+
* `replace_root`, `exec`, `update`
|
|
45
|
+
* Shorthand syntax for concise scripts (`~delete`, `~assert`, `field[]`, pointer assignments)
|
|
46
|
+
* Schema helper: approximate JSON Schema generation for a given DSL script
|
|
47
|
+
* Fully extensible via registries:
|
|
48
|
+
|
|
49
|
+
* operations
|
|
50
|
+
* special constructs
|
|
51
|
+
* JMESPath functions
|
|
52
|
+
* casters
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Architecture overview
|
|
57
|
+
|
|
58
|
+
J-Perm is composed from four independent registries:
|
|
59
|
+
|
|
60
|
+
| Registry | Purpose |
|
|
61
|
+
| ----------------- | --------------------------------------------- |
|
|
62
|
+
| `OpRegistry` | Registers DSL operations (`op`) |
|
|
63
|
+
| `SpecialRegistry` | Registers special values (`$ref`, `$eval`, …) |
|
|
64
|
+
| `JpFuncRegistry` | Registers custom JMESPath functions |
|
|
65
|
+
| `CasterRegistry` | Registers `${type:...}` casters |
|
|
66
|
+
|
|
67
|
+
All registries can be imported directly from `j_perm`.
|
|
68
|
+
|
|
69
|
+
At runtime, these parts are wired together into an execution engine that evaluates the DSL.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Core API
|
|
74
|
+
|
|
75
|
+
### `ActionEngine.apply_actions`
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from j_perm import ActionEngine, Handlers, SpecialResolver, TemplateSubstitutor
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Typical setup:
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
substitutor = TemplateSubstitutor()
|
|
85
|
+
special = SpecialResolver(substitutor=substitutor)
|
|
86
|
+
handlers = Handlers()
|
|
87
|
+
|
|
88
|
+
engine = ActionEngine(
|
|
89
|
+
handlers=handlers,
|
|
90
|
+
special=special,
|
|
91
|
+
substitutor=substitutor,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
result = engine.apply_actions(actions, dest={}, source=source)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Signature
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
apply_actions(
|
|
101
|
+
actions: Any,
|
|
102
|
+
*,
|
|
103
|
+
dest: MutableMapping[str, Any] | List[Any],
|
|
104
|
+
source: Mapping[str, Any] | List[Any],
|
|
105
|
+
) -> Mapping[str, Any]
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
* **`actions`** — DSL script (list or mapping)
|
|
109
|
+
* **`dest`** — initial destination document
|
|
110
|
+
* **`source`** — source context available to pointers, interpolation, `$ref`, `$eval`
|
|
111
|
+
* Returns a **deep copy** of the final `dest`
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Basic usage
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
source = {
|
|
119
|
+
"users": [
|
|
120
|
+
{"name": "Alice", "age": 17},
|
|
121
|
+
{"name": "Bob", "age": 22}
|
|
122
|
+
]
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
actions = [
|
|
126
|
+
# Start with empty list
|
|
127
|
+
{"op": "replace_root", "value": []},
|
|
128
|
+
|
|
129
|
+
# For each user - build a simplified object
|
|
130
|
+
{
|
|
131
|
+
"op": "foreach",
|
|
132
|
+
"in": "/users",
|
|
133
|
+
"as": "u",
|
|
134
|
+
"do": [
|
|
135
|
+
{
|
|
136
|
+
"op": "set",
|
|
137
|
+
"path": "/-",
|
|
138
|
+
"value": {
|
|
139
|
+
"name": "${/u/name}",
|
|
140
|
+
"is_adult": {
|
|
141
|
+
"$eval": [
|
|
142
|
+
{"op": "replace_root", "value": False},
|
|
143
|
+
{
|
|
144
|
+
"op": "if",
|
|
145
|
+
"cond": "${?`${/u/age}` >= `18`}",
|
|
146
|
+
"then": [{"op": "replace_root", "value": True}]
|
|
147
|
+
}
|
|
148
|
+
]
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
]
|
|
153
|
+
}
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
result = engine.apply_actions(actions, dest={}, source=source)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Interpolation & expression system (`${...}`)
|
|
162
|
+
|
|
163
|
+
Interpolation is handled by `TemplateSubstitutor` and is used throughout operations such as `set`, `copy`, `exec`, `update`, schema building, etc.
|
|
164
|
+
|
|
165
|
+
### JSON Pointer interpolation
|
|
166
|
+
|
|
167
|
+
```text
|
|
168
|
+
${/path/to/value}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Resolves a JSON Pointer against the current source context.
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
### Casters
|
|
176
|
+
|
|
177
|
+
```text
|
|
178
|
+
${int:/age}
|
|
179
|
+
${float:/height}
|
|
180
|
+
${bool:/flag}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Casters are registered via `CasterRegistry` and can be extended by users.
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
### JMESPath expressions
|
|
188
|
+
|
|
189
|
+
```text
|
|
190
|
+
${? items[?price > `10`].name }
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
JMESPath expressions are evaluated against the source context with access to custom JMESPath functions registered in `JpFuncRegistry`.
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
### Multiple templates
|
|
198
|
+
|
|
199
|
+
Any string may contain multiple `${...}` expressions, resolved left-to-right.
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Special values: `$ref` and `$eval`
|
|
204
|
+
|
|
205
|
+
Special values are resolved by `SpecialResolver`.
|
|
206
|
+
|
|
207
|
+
### `$ref`
|
|
208
|
+
|
|
209
|
+
```json
|
|
210
|
+
{ "$ref": "/path" }
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Resolves a pointer against the source context and injects the value.
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
### `$eval`
|
|
218
|
+
|
|
219
|
+
```json
|
|
220
|
+
{ "$eval": [ ... ] }
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Executes a nested DSL script using the same engine configuration and injects its result.
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## Shorthand syntax
|
|
228
|
+
|
|
229
|
+
Shorthand syntax is expanded during normalization:
|
|
230
|
+
|
|
231
|
+
* `~delete`
|
|
232
|
+
* `~assert`
|
|
233
|
+
* `field[]`
|
|
234
|
+
* pointer assignments
|
|
235
|
+
|
|
236
|
+
These are converted into explicit operation steps before execution.
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
# Built-ins
|
|
241
|
+
|
|
242
|
+
## Built-in casters (`${type:...}`)
|
|
243
|
+
|
|
244
|
+
Casters are registered in `CasterRegistry` and used in templates as `${name:<expr>}`.
|
|
245
|
+
The `<expr>` part is first expanded (templates inside are allowed), then cast is applied.
|
|
246
|
+
|
|
247
|
+
### `int`
|
|
248
|
+
|
|
249
|
+
**Form:** `${int:/path}`
|
|
250
|
+
**Behavior:** `int(value)`
|
|
251
|
+
|
|
252
|
+
### `float`
|
|
253
|
+
|
|
254
|
+
**Form:** `${float:/path}`
|
|
255
|
+
**Behavior:** `float(value)`
|
|
256
|
+
|
|
257
|
+
### `str`
|
|
258
|
+
|
|
259
|
+
**Form:** `${str:/path}`
|
|
260
|
+
**Behavior:** `str(value)`
|
|
261
|
+
|
|
262
|
+
### `bool`
|
|
263
|
+
|
|
264
|
+
**Form:** `${bool:/path}`
|
|
265
|
+
**Behavior:** compatible with the old implementation:
|
|
266
|
+
|
|
267
|
+
* if value is `int` or `str` → `bool(int(value))`
|
|
268
|
+
* otherwise → `bool(value)`
|
|
269
|
+
|
|
270
|
+
Examples:
|
|
271
|
+
|
|
272
|
+
```text
|
|
273
|
+
${bool:1} -> True
|
|
274
|
+
${bool:"0"} -> False
|
|
275
|
+
${bool:"2"} -> True
|
|
276
|
+
${bool:""} -> False (falls back to bool("") == False)
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## Built-in special constructs (`$ref`, `$eval`)
|
|
282
|
+
|
|
283
|
+
Special values are resolved by `SpecialResolver` while walking value trees.
|
|
284
|
+
If a mapping contains a known special key, that handler takes over and the whole mapping is replaced by the resolved value.
|
|
285
|
+
|
|
286
|
+
### `$ref`
|
|
287
|
+
|
|
288
|
+
**Shape:**
|
|
289
|
+
|
|
290
|
+
```jsonc
|
|
291
|
+
{ "$ref": "<pointer or template>", "$default": <optional> }
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
**Behavior:**
|
|
295
|
+
|
|
296
|
+
* resolve `"$ref"` through template substitution (so it may contain `${...}`)
|
|
297
|
+
* treat it as a pointer and read from **source context**
|
|
298
|
+
* if pointer fails:
|
|
299
|
+
|
|
300
|
+
* if `"$default"` exists → deep-copy and return default
|
|
301
|
+
* else → re-raise (error)
|
|
302
|
+
|
|
303
|
+
Example:
|
|
304
|
+
|
|
305
|
+
```json
|
|
306
|
+
{ "op": "set", "path": "/user", "value": { "$ref": "/rawUser" } }
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### `$eval`
|
|
310
|
+
|
|
311
|
+
**Shape:**
|
|
312
|
+
|
|
313
|
+
```jsonc
|
|
314
|
+
{ "$eval": <actions>, "$select": "<optional pointer>" }
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
**Behavior:**
|
|
318
|
+
|
|
319
|
+
* run nested DSL using the same engine configuration
|
|
320
|
+
* if `"$select"` is present → select a sub-value from the nested result
|
|
321
|
+
|
|
322
|
+
Example:
|
|
323
|
+
|
|
324
|
+
```json
|
|
325
|
+
{
|
|
326
|
+
"op": "set",
|
|
327
|
+
"path": "/flag",
|
|
328
|
+
"value": {
|
|
329
|
+
"$eval": [
|
|
330
|
+
{ "op": "replace_root", "value": false },
|
|
331
|
+
{ "op": "replace_root", "value": true }
|
|
332
|
+
],
|
|
333
|
+
"$select": ""
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
---
|
|
339
|
+
|
|
340
|
+
## Built-in JMESPath functions
|
|
341
|
+
|
|
342
|
+
Custom JMESPath functions are registered in `JpFuncRegistry` and available inside `${? ... }`.
|
|
343
|
+
|
|
344
|
+
### `subtract(a, b)`
|
|
345
|
+
|
|
346
|
+
**Signature:** `subtract(number, number) -> number`
|
|
347
|
+
**Behavior:** returns `a - b`
|
|
348
|
+
|
|
349
|
+
Example:
|
|
350
|
+
|
|
351
|
+
```text
|
|
352
|
+
${? subtract(price, tax) }
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
> Если у тебя есть другие встроенные JMES-функции в проекте (кроме `subtract`), скажи их имена — я добавлю их описания в README в том же формате.
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
## Built-in operations
|
|
360
|
+
|
|
361
|
+
All operations are registered in `OpRegistry` under their `op` name.
|
|
362
|
+
They are executed by `ActionEngine.apply_actions()` after normalization/shorthand expansion.
|
|
363
|
+
|
|
364
|
+
### Common notes
|
|
365
|
+
|
|
366
|
+
* Many operations accept values that may contain:
|
|
367
|
+
|
|
368
|
+
* special constructs (`$ref`, `$eval`, and user-added ones)
|
|
369
|
+
* templates (`${...}`)
|
|
370
|
+
* Unless stated otherwise: values are typically resolved as:
|
|
371
|
+
|
|
372
|
+
1. `SpecialResolver.resolve(...)`
|
|
373
|
+
2. `TemplateSubstitutor.substitute(...)`
|
|
374
|
+
3. deep-copied before writing into `dest`
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
### `set`
|
|
379
|
+
|
|
380
|
+
Set or append a value at a JSON Pointer path in `dest`.
|
|
381
|
+
|
|
382
|
+
**Shape:**
|
|
383
|
+
|
|
384
|
+
```jsonc
|
|
385
|
+
{
|
|
386
|
+
"op": "set",
|
|
387
|
+
"path": "/pointer",
|
|
388
|
+
"value": <any>,
|
|
389
|
+
"create": true, // default: true
|
|
390
|
+
"extend": true // default: true
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
**Semantics:**
|
|
395
|
+
|
|
396
|
+
* writes resolved `value` into `dest[path]`
|
|
397
|
+
* if `path` ends with `"/-"` → append to list
|
|
398
|
+
* if appending and `value` is a list:
|
|
399
|
+
|
|
400
|
+
* `extend=true` → extend the list
|
|
401
|
+
* `extend=false` → append list as one item
|
|
402
|
+
* `create=true` → create missing parent containers
|
|
403
|
+
|
|
404
|
+
---
|
|
405
|
+
|
|
406
|
+
### `copy`
|
|
407
|
+
|
|
408
|
+
Copy value from **source context** to `dest` (internally uses `set`).
|
|
409
|
+
|
|
410
|
+
**Shape:**
|
|
411
|
+
|
|
412
|
+
```jsonc
|
|
413
|
+
{
|
|
414
|
+
"op": "copy",
|
|
415
|
+
"from": "/source/pointer",
|
|
416
|
+
"path": "/target/pointer",
|
|
417
|
+
"create": true, // default: true
|
|
418
|
+
"extend": true, // default: true
|
|
419
|
+
"ignore_missing": false, // default: false
|
|
420
|
+
"default": <any> // optional
|
|
421
|
+
}
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
**Semantics:**
|
|
425
|
+
|
|
426
|
+
* `"from"` may be templated, then used as a pointer into source
|
|
427
|
+
* if missing:
|
|
428
|
+
|
|
429
|
+
* `ignore_missing=true` → no-op
|
|
430
|
+
* else if `"default"` provided → use default
|
|
431
|
+
* else → error
|
|
432
|
+
|
|
433
|
+
---
|
|
434
|
+
|
|
435
|
+
### `copyD`
|
|
436
|
+
|
|
437
|
+
Copy value from **dest** into another location in `dest` (self-copy).
|
|
438
|
+
|
|
439
|
+
**Shape:**
|
|
440
|
+
|
|
441
|
+
```jsonc
|
|
442
|
+
{
|
|
443
|
+
"op": "copyD",
|
|
444
|
+
"from": "/dest/pointer",
|
|
445
|
+
"path": "/target/pointer",
|
|
446
|
+
"create": true, // default: true
|
|
447
|
+
"ignore_missing": false, // default: false
|
|
448
|
+
"default": <any> // optional
|
|
449
|
+
}
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
**Semantics:**
|
|
453
|
+
|
|
454
|
+
* `"from"` pointer is resolved against current `dest`
|
|
455
|
+
* pointer string itself may be templated with source context
|
|
456
|
+
|
|
457
|
+
---
|
|
458
|
+
|
|
459
|
+
### `delete`
|
|
460
|
+
|
|
461
|
+
Delete a node at a pointer in `dest`.
|
|
462
|
+
|
|
463
|
+
**Shape:**
|
|
464
|
+
|
|
465
|
+
```jsonc
|
|
466
|
+
{
|
|
467
|
+
"op": "delete",
|
|
468
|
+
"path": "/pointer",
|
|
469
|
+
"ignore_missing": true // default: true
|
|
470
|
+
}
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
**Notes:**
|
|
474
|
+
|
|
475
|
+
* path must not end with `"-"`
|
|
476
|
+
* if missing and `ignore_missing=false` → error
|
|
477
|
+
|
|
478
|
+
---
|
|
479
|
+
|
|
480
|
+
### `assert`
|
|
481
|
+
|
|
482
|
+
Assert node existence and optional equality in `dest`.
|
|
483
|
+
|
|
484
|
+
**Shape:**
|
|
485
|
+
|
|
486
|
+
```jsonc
|
|
487
|
+
{
|
|
488
|
+
"op": "assert",
|
|
489
|
+
"path": "/pointer",
|
|
490
|
+
"equals": <any> // optional
|
|
491
|
+
}
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
**Semantics:**
|
|
495
|
+
|
|
496
|
+
* if path missing → `AssertionError`
|
|
497
|
+
* if `equals` provided and not equal → `AssertionError`
|
|
498
|
+
|
|
499
|
+
---
|
|
500
|
+
|
|
501
|
+
### `foreach`
|
|
502
|
+
|
|
503
|
+
Iterate over an array (or mapping) from source context and execute nested actions.
|
|
504
|
+
|
|
505
|
+
**Shape:**
|
|
506
|
+
|
|
507
|
+
```jsonc
|
|
508
|
+
{
|
|
509
|
+
"op": "foreach",
|
|
510
|
+
"in": "/array/path",
|
|
511
|
+
"do": [ ... ],
|
|
512
|
+
"as": "item", // default: "item"
|
|
513
|
+
"default": [], // default: []
|
|
514
|
+
"skip_empty": true // default: true
|
|
515
|
+
}
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
**Semantics:**
|
|
519
|
+
|
|
520
|
+
* resolve `"in"` pointer against source context
|
|
521
|
+
* if missing → use `"default"`
|
|
522
|
+
* if resolved value is a dict → iterate over items as pairs
|
|
523
|
+
* for each element:
|
|
524
|
+
|
|
525
|
+
* extend source context with variable name `"as"`
|
|
526
|
+
* execute `"do"` with same engine
|
|
527
|
+
* on exception inside body → restore `dest` from snapshot
|
|
528
|
+
|
|
529
|
+
---
|
|
530
|
+
|
|
531
|
+
### `if`
|
|
532
|
+
|
|
533
|
+
Conditionally execute nested actions.
|
|
534
|
+
|
|
535
|
+
**Path-based mode:**
|
|
536
|
+
|
|
537
|
+
```jsonc
|
|
538
|
+
{
|
|
539
|
+
"op": "if",
|
|
540
|
+
"path": "/pointer",
|
|
541
|
+
"equals": <any>, // optional
|
|
542
|
+
"exists": true, // optional
|
|
543
|
+
"then": [ ... ], // optional
|
|
544
|
+
"else": [ ... ], // optional
|
|
545
|
+
"do": [ ... ] // optional fallback success branch
|
|
546
|
+
}
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
**Expression-based mode:**
|
|
550
|
+
|
|
551
|
+
```jsonc
|
|
552
|
+
{
|
|
553
|
+
"op": "if",
|
|
554
|
+
"cond": "${?...}",
|
|
555
|
+
"then": [ ... ],
|
|
556
|
+
"else": [ ... ],
|
|
557
|
+
"do": [ ... ]
|
|
558
|
+
}
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
**Semantics:**
|
|
562
|
+
|
|
563
|
+
* one of `path` or `cond` must be present
|
|
564
|
+
* `then` runs on true, `else` runs on false
|
|
565
|
+
* `do` is used as “then” if `then` is missing
|
|
566
|
+
* snapshot + restore on exceptions inside chosen branch
|
|
567
|
+
|
|
568
|
+
---
|
|
569
|
+
|
|
570
|
+
### `distinct`
|
|
571
|
+
|
|
572
|
+
Remove duplicates from a list at `dest[path]`, preserving order.
|
|
573
|
+
|
|
574
|
+
**Shape:**
|
|
575
|
+
|
|
576
|
+
```jsonc
|
|
577
|
+
{
|
|
578
|
+
"op": "distinct",
|
|
579
|
+
"path": "/list/path",
|
|
580
|
+
"key": "/key/pointer" // optional
|
|
581
|
+
}
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
**Semantics:**
|
|
585
|
+
|
|
586
|
+
* target must be a list
|
|
587
|
+
* if `key` provided → key pointer is evaluated per item
|
|
588
|
+
|
|
589
|
+
---
|
|
590
|
+
|
|
591
|
+
### `replace_root`
|
|
592
|
+
|
|
593
|
+
Replace the whole destination root with a new value.
|
|
594
|
+
|
|
595
|
+
**Shape:**
|
|
596
|
+
|
|
597
|
+
```jsonc
|
|
598
|
+
{
|
|
599
|
+
"op": "replace_root",
|
|
600
|
+
"value": <any>
|
|
601
|
+
}
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
**Semantics:**
|
|
605
|
+
|
|
606
|
+
* resolve specials/templates inside `value`
|
|
607
|
+
* deep-copy, then replace entire `dest`
|
|
608
|
+
|
|
609
|
+
---
|
|
610
|
+
|
|
611
|
+
### `exec`
|
|
612
|
+
|
|
613
|
+
Execute a nested script held inline or referenced from source context.
|
|
614
|
+
|
|
615
|
+
**Pointer mode:**
|
|
616
|
+
|
|
617
|
+
```jsonc
|
|
618
|
+
{
|
|
619
|
+
"op": "exec",
|
|
620
|
+
"from": "/script/path",
|
|
621
|
+
"default": <any>, // optional
|
|
622
|
+
"merge": false // default: false
|
|
623
|
+
}
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
**Inline mode:**
|
|
627
|
+
|
|
628
|
+
```jsonc
|
|
629
|
+
{
|
|
630
|
+
"op": "exec",
|
|
631
|
+
"actions": [ ... ],
|
|
632
|
+
"merge": false // default: false
|
|
633
|
+
}
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
**Semantics:**
|
|
637
|
+
|
|
638
|
+
* exactly one of `from` / `actions`
|
|
639
|
+
* if `from` cannot be resolved:
|
|
640
|
+
|
|
641
|
+
* if `default` present → use it (after specials/templates)
|
|
642
|
+
* else → error
|
|
643
|
+
* `merge=false`:
|
|
644
|
+
|
|
645
|
+
* run nested script with `dest={}`
|
|
646
|
+
* replace current `dest` with result
|
|
647
|
+
* `merge=true`:
|
|
648
|
+
|
|
649
|
+
* run nested script on current dest (like a sub-call)
|
|
650
|
+
|
|
651
|
+
---
|
|
652
|
+
|
|
653
|
+
### `update`
|
|
654
|
+
|
|
655
|
+
Update a mapping at `path` using either source mapping (`from`) or inline mapping (`value`).
|
|
656
|
+
|
|
657
|
+
**Shape:**
|
|
658
|
+
|
|
659
|
+
```jsonc
|
|
660
|
+
{
|
|
661
|
+
"op": "update",
|
|
662
|
+
"path": "/target/path",
|
|
663
|
+
"from": "/source/path", // required in from-mode
|
|
664
|
+
"value": { ... }, // required in value-mode
|
|
665
|
+
"default": { ... }, // optional (from-mode only)
|
|
666
|
+
"create": true, // default: true
|
|
667
|
+
"deep": false // default: false
|
|
668
|
+
}
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
**Semantics:**
|
|
672
|
+
|
|
673
|
+
* exactly one of `from` / `value`
|
|
674
|
+
* update payload must be a mapping, else `TypeError`
|
|
675
|
+
* target at `path` must be mutable mapping, else `TypeError`
|
|
676
|
+
* `deep=false` → shallow `dict.update`
|
|
677
|
+
* `deep=true` → recursive merge for nested mappings
|
|
678
|
+
|
|
679
|
+
---
|
|
680
|
+
|
|
681
|
+
## Shorthand expansion (built-in)
|
|
682
|
+
|
|
683
|
+
Normalization expands shorthand syntax into explicit operation steps:
|
|
684
|
+
|
|
685
|
+
### `~delete`
|
|
686
|
+
|
|
687
|
+
```json
|
|
688
|
+
{ "~delete": ["/a", "/b"] }
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
→
|
|
692
|
+
|
|
693
|
+
```json
|
|
694
|
+
{ "op": "delete", "path": "/a" }
|
|
695
|
+
{ "op": "delete", "path": "/b" }
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
### `~assert`
|
|
699
|
+
|
|
700
|
+
```json
|
|
701
|
+
{ "~assert": { "/x": 10 } }
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
→
|
|
705
|
+
|
|
706
|
+
```json
|
|
707
|
+
{ "op": "assert", "path": "/x", "equals": 10 }
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
### `field[]` append
|
|
711
|
+
|
|
712
|
+
```json
|
|
713
|
+
{ "items[]": 123 }
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
→
|
|
717
|
+
|
|
718
|
+
```json
|
|
719
|
+
{ "op": "set", "path": "/items/-", "value": 123 }
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
### pointer assignment
|
|
723
|
+
|
|
724
|
+
If a value is a string that starts with `/`, it becomes a `copy`:
|
|
725
|
+
|
|
726
|
+
```json
|
|
727
|
+
{ "name": "/user/fullName" }
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
→
|
|
731
|
+
|
|
732
|
+
```json
|
|
733
|
+
{ "op": "copy", "from": "/user/fullName", "path": "/name", "ignore_missing": true }
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
---
|
|
737
|
+
|
|
738
|
+
## Extending J-Perm
|
|
739
|
+
|
|
740
|
+
### Custom operations
|
|
741
|
+
|
|
742
|
+
```python
|
|
743
|
+
from ..op_handler import OpRegistry
|
|
744
|
+
|
|
745
|
+
@OpRegistry.register("my_op")
|
|
746
|
+
def my_op(step, dest, src, engine):
|
|
747
|
+
return dest
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
---
|
|
751
|
+
|
|
752
|
+
### Custom special constructs
|
|
753
|
+
|
|
754
|
+
```python
|
|
755
|
+
from j_perm import SpecialRegistry
|
|
756
|
+
|
|
757
|
+
@SpecialRegistry.register("$upper")
|
|
758
|
+
def sp_upper(node, src, resolver):
|
|
759
|
+
value = resolver.substitutor.substitute(node["$upper"], src)
|
|
760
|
+
return str(value).upper()
|
|
761
|
+
```
|
|
762
|
+
|
|
763
|
+
---
|
|
764
|
+
|
|
765
|
+
### Custom JMESPath functions
|
|
766
|
+
|
|
767
|
+
```python
|
|
768
|
+
from j_perm import JpFuncRegistry
|
|
769
|
+
from jmespath import functions as jp_funcs
|
|
770
|
+
|
|
771
|
+
@JpFuncRegistry.register("subtract")
|
|
772
|
+
@jp_funcs.signature({"types": ["number"]}, {"types": ["number"]})
|
|
773
|
+
def _subtract(self, a, b):
|
|
774
|
+
return a - b
|
|
775
|
+
```
|
|
776
|
+
|
|
777
|
+
Usage in DSL:
|
|
778
|
+
|
|
779
|
+
```text
|
|
780
|
+
${? subtract(price, tax) }
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
---
|
|
784
|
+
|
|
785
|
+
### Custom casters
|
|
786
|
+
|
|
787
|
+
```python
|
|
788
|
+
from j_perm import CasterRegistry
|
|
789
|
+
|
|
790
|
+
@CasterRegistry.register("json")
|
|
791
|
+
def cast_json(x):
|
|
792
|
+
return json.loads(x)
|
|
793
|
+
```
|
|
794
|
+
|
|
795
|
+
Usage:
|
|
796
|
+
|
|
797
|
+
```text
|
|
798
|
+
${json:/raw_payload}
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
---
|
|
802
|
+
|
|
803
|
+
## Plugin loading
|
|
804
|
+
|
|
805
|
+
Registries collect definitions **at import time**.
|
|
806
|
+
To enable “use all registered components”, ensure that modules defining custom ops, specials, casters, or JMESPath functions are imported before engine construction.
|
|
807
|
+
|
|
808
|
+
A common pattern is to import all plugins in one place:
|
|
809
|
+
|
|
810
|
+
```python
|
|
811
|
+
import my_project.jperm_plugins
|
|
812
|
+
```
|
|
813
|
+
|
|
814
|
+
---
|
|
815
|
+
|
|
816
|
+
## License
|
|
817
|
+
|
|
818
|
+
This package is provided as-is; feel free to adapt it to your project structure.
|