j-perm 0.1.2__tar.gz → 0.1.3.1__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.
- j_perm-0.1.3.1/PKG-INFO +766 -0
- j_perm-0.1.3.1/README.md +753 -0
- {j_perm-0.1.2 → j_perm-0.1.3.1}/pyproject.toml +1 -1
- {j_perm-0.1.2 → j_perm-0.1.3.1}/src/j_perm/ops/distinct.py +10 -2
- j_perm-0.1.3.1/src/j_perm.egg-info/PKG-INFO +766 -0
- j_perm-0.1.2/PKG-INFO +0 -116
- j_perm-0.1.2/README.md +0 -103
- j_perm-0.1.2/src/j_perm.egg-info/PKG-INFO +0 -116
- {j_perm-0.1.2 → j_perm-0.1.3.1}/setup.cfg +0 -0
- {j_perm-0.1.2 → j_perm-0.1.3.1}/src/j_perm/__init__.py +0 -0
- {j_perm-0.1.2 → j_perm-0.1.3.1}/src/j_perm/engine.py +0 -0
- {j_perm-0.1.2 → j_perm-0.1.3.1}/src/j_perm/jmes_ext.py +0 -0
- {j_perm-0.1.2 → j_perm-0.1.3.1}/src/j_perm/ops/__init__.py +0 -0
- {j_perm-0.1.2 → j_perm-0.1.3.1}/src/j_perm/ops/_exec.py +0 -0
- {j_perm-0.1.2 → j_perm-0.1.3.1}/src/j_perm/ops/_if.py +0 -0
- {j_perm-0.1.2 → j_perm-0.1.3.1}/src/j_perm/ops/assert.py +0 -0
- {j_perm-0.1.2 → j_perm-0.1.3.1}/src/j_perm/ops/copy.py +0 -0
- {j_perm-0.1.2 → j_perm-0.1.3.1}/src/j_perm/ops/copy_d.py +0 -0
- {j_perm-0.1.2 → j_perm-0.1.3.1}/src/j_perm/ops/delete.py +0 -0
- {j_perm-0.1.2 → j_perm-0.1.3.1}/src/j_perm/ops/foreach.py +0 -0
- {j_perm-0.1.2 → j_perm-0.1.3.1}/src/j_perm/ops/replace_root.py +0 -0
- {j_perm-0.1.2 → j_perm-0.1.3.1}/src/j_perm/ops/set.py +0 -0
- {j_perm-0.1.2 → j_perm-0.1.3.1}/src/j_perm/ops/update.py +0 -0
- {j_perm-0.1.2 → j_perm-0.1.3.1}/src/j_perm/registry.py +0 -0
- {j_perm-0.1.2 → j_perm-0.1.3.1}/src/j_perm/schema/__init__.py +0 -0
- {j_perm-0.1.2 → j_perm-0.1.3.1}/src/j_perm/utils/__init__.py +0 -0
- {j_perm-0.1.2 → j_perm-0.1.3.1}/src/j_perm/utils/pointers.py +0 -0
- {j_perm-0.1.2 → j_perm-0.1.3.1}/src/j_perm/utils/special.py +0 -0
- {j_perm-0.1.2 → j_perm-0.1.3.1}/src/j_perm/utils/subst.py +0 -0
- {j_perm-0.1.2 → j_perm-0.1.3.1}/src/j_perm/utils/tuples.py +0 -0
- {j_perm-0.1.2 → j_perm-0.1.3.1}/src/j_perm.egg-info/SOURCES.txt +0 -0
- {j_perm-0.1.2 → j_perm-0.1.3.1}/src/j_perm.egg-info/dependency_links.txt +0 -0
- {j_perm-0.1.2 → j_perm-0.1.3.1}/src/j_perm.egg-info/requires.txt +0 -0
- {j_perm-0.1.2 → j_perm-0.1.3.1}/src/j_perm.egg-info/top_level.txt +0 -0
j_perm-0.1.3.1/PKG-INFO
ADDED
|
@@ -0,0 +1,766 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: j-perm
|
|
3
|
+
Version: 0.1.3.1
|
|
4
|
+
Summary: jsom 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
|
+
---
|
|
21
|
+
|
|
22
|
+
## Features
|
|
23
|
+
|
|
24
|
+
* JSON Pointer read/write with support for:
|
|
25
|
+
|
|
26
|
+
* root pointers (`""`, `"/"`, `"."`)
|
|
27
|
+
* relative `..` segments
|
|
28
|
+
* list slices like `/items[1:3]`
|
|
29
|
+
* Interpolation templates:
|
|
30
|
+
|
|
31
|
+
* `${/path/to/node}` — JSON Pointer lookup
|
|
32
|
+
* `${int:/path}` / `${float:/path}` / `${bool:/path}` — simple type casters
|
|
33
|
+
* `${? some.jmespath(expression) }` — JMESPath with custom functions
|
|
34
|
+
* Special values:
|
|
35
|
+
|
|
36
|
+
* `$ref` — reference into the source document
|
|
37
|
+
* `$eval` — nested DSL evaluation with optional `$select`
|
|
38
|
+
* Built-in operations:
|
|
39
|
+
|
|
40
|
+
* `set`, `copy`, `copyD`, `delete`, `assert`
|
|
41
|
+
* `foreach`, `if`, `distinct`
|
|
42
|
+
* `replace_root`, `exec`, `update`
|
|
43
|
+
* Shorthand syntax for concise scripts (`~delete`, `~assert`, `field[]`, pointer assignments)
|
|
44
|
+
* Schema helper: approximate JSON Schema generation for a given DSL script.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Core API
|
|
49
|
+
|
|
50
|
+
### `apply_actions`
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from j_perm import apply_actions
|
|
54
|
+
|
|
55
|
+
result = apply_actions(actions, dest, source)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Signature:**
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
apply_actions(
|
|
62
|
+
actions: Any,
|
|
63
|
+
*,
|
|
64
|
+
dest: MutableMapping[str, Any] | List[Any],
|
|
65
|
+
source: Mapping[str, Any] | List[Any],
|
|
66
|
+
) -> Mapping[str, Any]
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
* **`actions`** — DSL script (list or mapping).
|
|
70
|
+
Internally normalized via `normalize_actions()` and shorthand expansion.
|
|
71
|
+
* **`dest`** — initial destination document to transform (typically a dict or list).
|
|
72
|
+
* **`source`** — source document or context; available to pointers, interpolation, `$ref`, `$eval`, and nested operations.
|
|
73
|
+
* Returns a **deep copy** of the final `dest`.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Basic usage
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from j_perm import apply_actions
|
|
81
|
+
|
|
82
|
+
source = {
|
|
83
|
+
"users": [
|
|
84
|
+
{"name": "Alice", "age": 17},
|
|
85
|
+
{"name": "Bob", "age": 22}
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
actions = [
|
|
90
|
+
# Start with empty list
|
|
91
|
+
{"op": "replace_root", "value": []},
|
|
92
|
+
|
|
93
|
+
# For each user - build a simplified object
|
|
94
|
+
{
|
|
95
|
+
"op": "foreach",
|
|
96
|
+
"in": "/users",
|
|
97
|
+
"as": "u",
|
|
98
|
+
"do": [
|
|
99
|
+
{
|
|
100
|
+
"op": "set",
|
|
101
|
+
"path": "/-",
|
|
102
|
+
"value": {
|
|
103
|
+
"name": "${/u/name}",
|
|
104
|
+
"is_adult": {
|
|
105
|
+
"$eval": [
|
|
106
|
+
{"op": "replace_root", "value": False},
|
|
107
|
+
{
|
|
108
|
+
"op": "if",
|
|
109
|
+
"cond": "${?`${/u/age}` >= `18`}",
|
|
110
|
+
"then": [{"op": "replace_root", "value": True}]
|
|
111
|
+
}
|
|
112
|
+
]
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
]
|
|
117
|
+
}
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
result = apply_actions(actions, dest={}, source=source)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
# Interpolation & expression system (`${...}`)
|
|
126
|
+
|
|
127
|
+
Interpolation is handled by the substitution utility used throughout operations such as `set`, `copy`, `exec`, `update`, schema building, etc.
|
|
128
|
+
|
|
129
|
+
### 1. JSON Pointer interpolation
|
|
130
|
+
|
|
131
|
+
* **Form:** `${/path/to/value}`
|
|
132
|
+
* Meaning: resolve `/path/to/value` against the current **source context** (the original `source` plus any loop variables, etc.) and inject the value.
|
|
133
|
+
|
|
134
|
+
Example:
|
|
135
|
+
|
|
136
|
+
```json
|
|
137
|
+
{
|
|
138
|
+
"op": "set",
|
|
139
|
+
"path": "/user/name",
|
|
140
|
+
"value": "Hello, ${/user/first_name}!"
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### 2. Casters
|
|
145
|
+
|
|
146
|
+
* `${int:/age}` → cast value at `/age` to `int`
|
|
147
|
+
* `${float:/height}` → cast to `float`
|
|
148
|
+
* `${bool:/flag}` → cast to `bool`
|
|
149
|
+
|
|
150
|
+
If the cast fails, an exception is raised.
|
|
151
|
+
|
|
152
|
+
### 3. JMESPath expressions
|
|
153
|
+
|
|
154
|
+
* **Form:** `${? <expression> }`
|
|
155
|
+
* Evaluated against the **source context**, with access to any custom JMESPath functions wired into the engine.
|
|
156
|
+
|
|
157
|
+
Example:
|
|
158
|
+
|
|
159
|
+
```json
|
|
160
|
+
{
|
|
161
|
+
"op": "set",
|
|
162
|
+
"path": "/expensiveNames",
|
|
163
|
+
"value": "${? items[?price > `10`].name }"
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### 4. Multiple templates in one string
|
|
168
|
+
|
|
169
|
+
Any string can contain multiple `${...}` segments, which are resolved left-to-right.
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
# Special values: `$ref` and `$eval`
|
|
174
|
+
|
|
175
|
+
Special values are resolved by `resolve_special()` before normal interpolation/substitution in operations like `set`, `update`, `exec`, `replace_root`.
|
|
176
|
+
|
|
177
|
+
### `$ref`
|
|
178
|
+
|
|
179
|
+
`{"$ref": "/path"}` means *“use the value at this path from the source”*.
|
|
180
|
+
|
|
181
|
+
Example:
|
|
182
|
+
|
|
183
|
+
```json
|
|
184
|
+
{
|
|
185
|
+
"op": "set",
|
|
186
|
+
"path": "/user",
|
|
187
|
+
"value": { "$ref": "/rawUser" }
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
This behaves similarly to a `copy` from `/rawUser`, but can be nested into larger structures and combined with `$eval` and interpolation.
|
|
192
|
+
|
|
193
|
+
### `$eval`
|
|
194
|
+
|
|
195
|
+
`{"$eval": [...]}` means *“execute this nested DSL script and use its result here”*.
|
|
196
|
+
|
|
197
|
+
Example:
|
|
198
|
+
|
|
199
|
+
```json
|
|
200
|
+
{
|
|
201
|
+
"op": "set",
|
|
202
|
+
"path": "/flag",
|
|
203
|
+
"value": {
|
|
204
|
+
"$eval": [
|
|
205
|
+
{ "op": "replace_root", "value": false },
|
|
206
|
+
{
|
|
207
|
+
"op": "if",
|
|
208
|
+
"cond": "${? some.expression }",
|
|
209
|
+
"then": [{ "op": "replace_root", "value": true }]
|
|
210
|
+
}
|
|
211
|
+
]
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
The nested script has the same `source` context as the outer script, and its final result becomes the value injected at `/flag`.
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
# Shorthand syntax (shortcuts)
|
|
221
|
+
|
|
222
|
+
Shorthand is expanded by `normalize_actions()` into explicit operation steps.
|
|
223
|
+
|
|
224
|
+
### How shorthand works internally
|
|
225
|
+
|
|
226
|
+
* `actions` may be:
|
|
227
|
+
|
|
228
|
+
* a list of steps,
|
|
229
|
+
* a mapping without `"op"` — in that case it’s treated as shorthand and expanded.
|
|
230
|
+
|
|
231
|
+
Expansion is done by `_expand_shorthand()`.
|
|
232
|
+
|
|
233
|
+
### 1. Delete shorthand: `~delete`
|
|
234
|
+
|
|
235
|
+
```json
|
|
236
|
+
{ "~delete": "/path" }
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
If value is a list:
|
|
240
|
+
|
|
241
|
+
```json
|
|
242
|
+
{ "~delete": ["/a", "/b"] }
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Expands into:
|
|
246
|
+
|
|
247
|
+
```json
|
|
248
|
+
{ "op": "delete", "path": "/a" }
|
|
249
|
+
{ "op": "delete", "path": "/b" }
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### 2. Assert shorthand: `~assert`
|
|
253
|
+
|
|
254
|
+
If value is a mapping:
|
|
255
|
+
|
|
256
|
+
```json
|
|
257
|
+
{ "~assert": { "/x": 10, "/y": 20 } }
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Expands into:
|
|
261
|
+
|
|
262
|
+
```json
|
|
263
|
+
{ "op": "assert", "path": "/x", "equals": 10 }
|
|
264
|
+
{ "op": "assert", "path": "/y", "equals": 20 }
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
If value is a string or list of strings:
|
|
268
|
+
|
|
269
|
+
```json
|
|
270
|
+
{ "~assert": "/x" }
|
|
271
|
+
# or
|
|
272
|
+
{ "~assert": ["/x", "/y"] }
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Expands into assertions that only check **existence** at those paths.
|
|
276
|
+
|
|
277
|
+
### 3. Append shorthand: `field[]`
|
|
278
|
+
|
|
279
|
+
A key ending with `[]` means “append to list at this path”.
|
|
280
|
+
|
|
281
|
+
```json
|
|
282
|
+
{ "items[]": 123 }
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Expands into:
|
|
286
|
+
|
|
287
|
+
```json
|
|
288
|
+
{ "op": "set", "path": "/items/-", "value": 123 }
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### 4. Pointer assignment shorthand
|
|
292
|
+
|
|
293
|
+
If a value is a **string that starts with `/`**, it’s treated as a pointer and expanded into a `copy` operation:
|
|
294
|
+
|
|
295
|
+
```json
|
|
296
|
+
{ "name": "/user/fullName" }
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Expands into:
|
|
300
|
+
|
|
301
|
+
```json
|
|
302
|
+
{
|
|
303
|
+
"op": "copy",
|
|
304
|
+
"path": "/name",
|
|
305
|
+
"from": "/user/fullName",
|
|
306
|
+
"ignore_missing": true
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
(See `_is_pointer_string()` and `_expand_shorthand()` for details. )
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
# Built-in operations: signatures & parameters
|
|
315
|
+
|
|
316
|
+
Below are all core operations with:
|
|
317
|
+
|
|
318
|
+
* the **step shape**,
|
|
319
|
+
* **required** vs **optional** parameters,
|
|
320
|
+
* **default values**.
|
|
321
|
+
|
|
322
|
+
All defaults are taken directly from the Python implementations.
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## `set`
|
|
327
|
+
|
|
328
|
+
Set or append a value at a JSON Pointer path in `dest`.
|
|
329
|
+
|
|
330
|
+
### Signature
|
|
331
|
+
|
|
332
|
+
```jsonc
|
|
333
|
+
{
|
|
334
|
+
"op": "set", // required
|
|
335
|
+
"path": "/pointer", // required
|
|
336
|
+
"value": <any>, // required
|
|
337
|
+
"create": true, // optional, default: true
|
|
338
|
+
"extend": true // optional, default: true
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### Parameters
|
|
343
|
+
|
|
344
|
+
* **`path`** (required)
|
|
345
|
+
JSON Pointer path where the value is written.
|
|
346
|
+
|
|
347
|
+
* If the path ends with `"/-"`, the value is **appended** to a list.
|
|
348
|
+
* **`value`** (required)
|
|
349
|
+
Any value, including special `$ref` / `$eval` objects and interpolated strings.
|
|
350
|
+
Resolved by `resolve_special()` and then `substitute()`.
|
|
351
|
+
* **`create`** (optional, default **`true`**)
|
|
352
|
+
If `true`, missing parent containers are created.
|
|
353
|
+
* **`extend`** (optional, default **`true`**)
|
|
354
|
+
If `path` is `"/-"` and `value` is a list:
|
|
355
|
+
|
|
356
|
+
* `extend=true` ⇒ list is extended with elements of `value`
|
|
357
|
+
* `extend=false` ⇒ `value` is appended as a single item (nested list)
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
## `copy`
|
|
362
|
+
|
|
363
|
+
Copy a value from `source` (or extended source context) into `dest`. Internally uses `set`.
|
|
364
|
+
|
|
365
|
+
### Signature
|
|
366
|
+
|
|
367
|
+
```jsonc
|
|
368
|
+
{
|
|
369
|
+
"op": "copy", // required
|
|
370
|
+
"from": "/source/pointer", // required
|
|
371
|
+
"path": "/target/pointer", // required
|
|
372
|
+
"create": true, // optional, default: true
|
|
373
|
+
"extend": true, // optional, default: true
|
|
374
|
+
"ignore_missing": false, // optional, default: false
|
|
375
|
+
"default": <any> // optional
|
|
376
|
+
}
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### Parameters
|
|
380
|
+
|
|
381
|
+
* **`from`** (required)
|
|
382
|
+
JSON Pointer into the **source** context (after interpolation).
|
|
383
|
+
* **`path`** (required)
|
|
384
|
+
Destination path; same semantics as in `set`.
|
|
385
|
+
* **`create`** (optional, default **`true`**)
|
|
386
|
+
Passed through to `set` — create missing parents.
|
|
387
|
+
* **`extend`** (optional, default **`true`**)
|
|
388
|
+
Passed through to `set` — controls list extension on appends.
|
|
389
|
+
* **`ignore_missing`** (optional, default **`false`**)
|
|
390
|
+
If `true` and `from` cannot be resolved, the operation is a no-op.
|
|
391
|
+
* **`default`** (optional)
|
|
392
|
+
Used when `from` cannot be resolved and `ignore_missing` is **not** set.
|
|
393
|
+
If provided, `default` is copied into `path` instead of raising.
|
|
394
|
+
|
|
395
|
+
---
|
|
396
|
+
|
|
397
|
+
## `copyD`
|
|
398
|
+
|
|
399
|
+
Copy a value from the **current `dest`** (self) into another path in `dest`. Internally uses `set`.
|
|
400
|
+
|
|
401
|
+
This is useful for rearranging or duplicating data that has already been built in `dest`, without going back to the `source`.
|
|
402
|
+
|
|
403
|
+
### Signature
|
|
404
|
+
|
|
405
|
+
```jsonc
|
|
406
|
+
{
|
|
407
|
+
"op": "copyD", // required
|
|
408
|
+
"from": "/source/pointer", // required
|
|
409
|
+
"path": "/target/pointer", // required
|
|
410
|
+
"create": true, // optional, default: true
|
|
411
|
+
"ignore_missing": false, // optional, default: false
|
|
412
|
+
"default": <any> // optional
|
|
413
|
+
}
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
### Parameters
|
|
417
|
+
|
|
418
|
+
* **`from`** (required)
|
|
419
|
+
JSON Pointer evaluated against **`dest`** (not `source`). The pointer itself is first interpolated with `src` (source context), but resolution uses `dest`.
|
|
420
|
+
* **`path`** (required)
|
|
421
|
+
Destination path in `dest`, same semantics as in `set`.
|
|
422
|
+
* **`create`** (optional, default **`true`**)
|
|
423
|
+
Passed to `set` — whether to create missing parent containers.
|
|
424
|
+
* **`ignore_missing`** (optional, default **`false`**)
|
|
425
|
+
If `true` and the `from` pointer cannot be resolved in `dest`, the operation becomes a no-op.
|
|
426
|
+
* **`default`** (optional)
|
|
427
|
+
Used when `from` cannot be resolved and `ignore_missing` is **not** set.
|
|
428
|
+
If provided, that default is deep-copied into `path` instead of raising.
|
|
429
|
+
|
|
430
|
+
---
|
|
431
|
+
|
|
432
|
+
## `delete`
|
|
433
|
+
|
|
434
|
+
Delete a node at a JSON Pointer path in `dest`.
|
|
435
|
+
|
|
436
|
+
### Signature
|
|
437
|
+
|
|
438
|
+
```jsonc
|
|
439
|
+
{
|
|
440
|
+
"op": "delete", // required
|
|
441
|
+
"path": "/pointer", // required
|
|
442
|
+
"ignore_missing": true // optional, default: true
|
|
443
|
+
}
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
### Parameters
|
|
447
|
+
|
|
448
|
+
* **`path`** (required)
|
|
449
|
+
JSON Pointer to the node to delete.
|
|
450
|
+
Must not end with `"-"`.
|
|
451
|
+
* **`ignore_missing`** (optional, default **`true`**)
|
|
452
|
+
If `false`, a missing path raises an error;
|
|
453
|
+
if `true`, missing path is silently ignored.
|
|
454
|
+
|
|
455
|
+
---
|
|
456
|
+
|
|
457
|
+
## `assert`
|
|
458
|
+
|
|
459
|
+
Assert node existence and optional equality in `dest`.
|
|
460
|
+
|
|
461
|
+
### Signature
|
|
462
|
+
|
|
463
|
+
```jsonc
|
|
464
|
+
{
|
|
465
|
+
"op": "assert", // required
|
|
466
|
+
"path": "/pointer", // required
|
|
467
|
+
"equals": <any> // optional
|
|
468
|
+
}
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
### Parameters
|
|
472
|
+
|
|
473
|
+
* **`path`** (required)
|
|
474
|
+
JSON Pointer to check in `dest`.
|
|
475
|
+
If it does not exist, an `AssertionError` is raised.
|
|
476
|
+
* **`equals`** (optional)
|
|
477
|
+
If provided, the value at `path` is compared with `equals`;
|
|
478
|
+
mismatch raises `AssertionError`.
|
|
479
|
+
|
|
480
|
+
---
|
|
481
|
+
|
|
482
|
+
## `foreach`
|
|
483
|
+
|
|
484
|
+
Iterate over an array (or mapping) in the source context and execute nested actions.
|
|
485
|
+
|
|
486
|
+
### Signature
|
|
487
|
+
|
|
488
|
+
```jsonc
|
|
489
|
+
{
|
|
490
|
+
"op": "foreach", // required
|
|
491
|
+
"in": "/array/path", // required
|
|
492
|
+
"do": [ ... ], // required
|
|
493
|
+
"as": "item", // optional, default: "item"
|
|
494
|
+
"default": [], // optional, default: []
|
|
495
|
+
"skip_empty": true // optional, default: true
|
|
496
|
+
}
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
### Parameters
|
|
500
|
+
|
|
501
|
+
* **`in`** (required)
|
|
502
|
+
JSON Pointer to the array in the source context (after interpolation).
|
|
503
|
+
* **`do`** (required)
|
|
504
|
+
Nested DSL script (list or single op) executed for each element.
|
|
505
|
+
* **`as`** (optional, default **`"item"`**)
|
|
506
|
+
Name of the variable bound to the current element in the extended source context.
|
|
507
|
+
* **`default`** (optional, default **`[]`**)
|
|
508
|
+
Used when the pointer in `"in"` cannot be resolved.
|
|
509
|
+
* **`skip_empty`** (optional, default **`true`**)
|
|
510
|
+
If `true` and the resolved array is empty, the loop is skipped.
|
|
511
|
+
|
|
512
|
+
Additional behavior:
|
|
513
|
+
|
|
514
|
+
* If the resolved object is a **dict**, it’s converted to a list of `(key, value)` pairs.
|
|
515
|
+
* On exception in the body, `dest` is restored from a deep copy snapshot.
|
|
516
|
+
|
|
517
|
+
---
|
|
518
|
+
|
|
519
|
+
## `if`
|
|
520
|
+
|
|
521
|
+
Conditionally execute nested actions.
|
|
522
|
+
|
|
523
|
+
### Signature (path-based condition)
|
|
524
|
+
|
|
525
|
+
```jsonc
|
|
526
|
+
{
|
|
527
|
+
"op": "if", // required
|
|
528
|
+
"path": "/pointer", // required in this mode
|
|
529
|
+
"equals": <any>, // optional
|
|
530
|
+
"exists": true, // optional
|
|
531
|
+
"then": [ ... ], // optional
|
|
532
|
+
"else": [ ... ], // optional
|
|
533
|
+
"do": [ ... ] // optional (fallback success branch)
|
|
534
|
+
}
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
### Signature (expression-based condition)
|
|
538
|
+
|
|
539
|
+
```jsonc
|
|
540
|
+
{
|
|
541
|
+
"op": "if", // required
|
|
542
|
+
"cond": "${?...}", // required in this mode
|
|
543
|
+
"then": [ ... ], // optional
|
|
544
|
+
"else": [ ... ], // optional
|
|
545
|
+
"do": [ ... ] // optional (fallback success branch)
|
|
546
|
+
}
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
### Parameters
|
|
550
|
+
|
|
551
|
+
At least **one** of `path` or `cond` must be supplied:
|
|
552
|
+
|
|
553
|
+
* **`path`** (required in path-mode)
|
|
554
|
+
|
|
555
|
+
* If combined with `equals`, condition is `dest[path] == equals` and path must exist.
|
|
556
|
+
* If combined with `exists` (truthy), condition is “path exists”.
|
|
557
|
+
* Else, condition is `bool(dest[path])` (path must exist).
|
|
558
|
+
* **`equals`** (optional, path-mode only)
|
|
559
|
+
Expected value for equality check.
|
|
560
|
+
* **`exists`** (optional, path-mode only)
|
|
561
|
+
If truthy, check is presence of path only.
|
|
562
|
+
* **`cond`** (required in expression-mode)
|
|
563
|
+
Arbitrary interpolated value or expression; `bool(cond)` is used.
|
|
564
|
+
|
|
565
|
+
Branches:
|
|
566
|
+
|
|
567
|
+
* **`then`** (optional)
|
|
568
|
+
Executed when condition is **true**.
|
|
569
|
+
* **`else`** (optional)
|
|
570
|
+
Executed when condition is **false**.
|
|
571
|
+
* **`do`** (optional)
|
|
572
|
+
If condition is **true** and `"then"` is missing, `"do"` is used as the success branch.
|
|
573
|
+
|
|
574
|
+
If no branch is present for the chosen condition result, `dest` is returned unchanged.
|
|
575
|
+
|
|
576
|
+
Error handling:
|
|
577
|
+
|
|
578
|
+
* Before running branch actions, a deep copy snapshot of `dest` is taken.
|
|
579
|
+
* On exception, `dest` is restored to the snapshot.
|
|
580
|
+
|
|
581
|
+
---
|
|
582
|
+
|
|
583
|
+
## `distinct`
|
|
584
|
+
|
|
585
|
+
Remove duplicates in a list at the given path, preserving order.
|
|
586
|
+
|
|
587
|
+
### Signature
|
|
588
|
+
|
|
589
|
+
```jsonc
|
|
590
|
+
{
|
|
591
|
+
"op": "distinct", // required
|
|
592
|
+
"path": "/list/path", // required
|
|
593
|
+
"key": "/key/pointer" // optional
|
|
594
|
+
}
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
### Parameters
|
|
598
|
+
|
|
599
|
+
* **`path`** (required)
|
|
600
|
+
JSON Pointer to a **list** in `dest`.
|
|
601
|
+
If target is not a list, a `TypeError` is raised.
|
|
602
|
+
* **`key`** (optional)
|
|
603
|
+
JSON Pointer evaluated per item to compute the *deduplication key*.
|
|
604
|
+
|
|
605
|
+
* If provided: uniqueness is based on `jptr_get(item, key_path)`.
|
|
606
|
+
* If omitted: the whole `item` is used as the key.
|
|
607
|
+
|
|
608
|
+
---
|
|
609
|
+
|
|
610
|
+
## `replace_root`
|
|
611
|
+
|
|
612
|
+
Replace the whole `dest` root value with a new one.
|
|
613
|
+
|
|
614
|
+
### Signature
|
|
615
|
+
|
|
616
|
+
```jsonc
|
|
617
|
+
{
|
|
618
|
+
"op": "replace_root", // required
|
|
619
|
+
"value": <any> // required
|
|
620
|
+
}
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
### Parameters
|
|
624
|
+
|
|
625
|
+
* **`value`** (required)
|
|
626
|
+
Value to become the new root.
|
|
627
|
+
|
|
628
|
+
* Special values (`$ref`, `$eval`) are resolved via `resolve_special()`.
|
|
629
|
+
* Strings/lists/dicts are then passed through interpolation `substitute()`.
|
|
630
|
+
* The final value is deep-copied.
|
|
631
|
+
|
|
632
|
+
Result of `replace_root` is the new `dest`.
|
|
633
|
+
|
|
634
|
+
---
|
|
635
|
+
|
|
636
|
+
## `exec`
|
|
637
|
+
|
|
638
|
+
Execute a nested DSL script held inline or at a pointer.
|
|
639
|
+
|
|
640
|
+
Exactly one of `from` or `actions` must be provided.
|
|
641
|
+
|
|
642
|
+
### Signature (script from pointer)
|
|
643
|
+
|
|
644
|
+
```jsonc
|
|
645
|
+
{
|
|
646
|
+
"op": "exec", // required
|
|
647
|
+
"from": "/script/path", // required in this mode
|
|
648
|
+
"default": <any>, // optional
|
|
649
|
+
"merge": false // optional, default: false
|
|
650
|
+
}
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
### Signature (inline script)
|
|
654
|
+
|
|
655
|
+
```jsonc
|
|
656
|
+
{
|
|
657
|
+
"op": "exec", // required
|
|
658
|
+
"actions": [ ... ], // required in this mode
|
|
659
|
+
"merge": false // optional, default: false
|
|
660
|
+
}
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
### Parameters
|
|
664
|
+
|
|
665
|
+
* **`from`** (required in pointer-mode)
|
|
666
|
+
Pointer (possibly interpolated) to the actions in the source context.
|
|
667
|
+
If resolution fails:
|
|
668
|
+
|
|
669
|
+
* and `default` is present ⇒ use `default` as the script (after resolving specials and interpolation if it’s str/list/dict),
|
|
670
|
+
* else ⇒ raise `ValueError`.
|
|
671
|
+
* **`actions`** (required in inline-mode)
|
|
672
|
+
Inline DSL script (list or mapping). Special values and templates are resolved.
|
|
673
|
+
* **`merge`** (optional, default **`false`**)
|
|
674
|
+
|
|
675
|
+
* `merge=false` (default): nested script runs with `dest={}`, result **replaces** current `dest`.
|
|
676
|
+
* `merge=true`: nested script runs **on current dest** and the result is returned (like a sub-call to `apply_actions` on same `dest`).
|
|
677
|
+
* **`default`** (optional, pointer-mode only)
|
|
678
|
+
Fallback script if `from` cannot be resolved.
|
|
679
|
+
|
|
680
|
+
---
|
|
681
|
+
|
|
682
|
+
## `update`
|
|
683
|
+
|
|
684
|
+
Update a mapping at the given path using either inline value or source mapping.
|
|
685
|
+
|
|
686
|
+
Exactly one of `from` or `value` must be provided.
|
|
687
|
+
|
|
688
|
+
### Signature
|
|
689
|
+
|
|
690
|
+
```jsonc
|
|
691
|
+
{
|
|
692
|
+
"op": "update", // required
|
|
693
|
+
"path": "/target/path", // required
|
|
694
|
+
"from": "/source/path", // required in from-mode
|
|
695
|
+
"value": { ... }, // required in value-mode
|
|
696
|
+
"default": { ... }, // optional (from-mode only)
|
|
697
|
+
"create": true, // optional, default: true
|
|
698
|
+
"deep": false // optional, default: false
|
|
699
|
+
}
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
### Parameters
|
|
703
|
+
|
|
704
|
+
* **`path`** (required)
|
|
705
|
+
JSON Pointer path to the **mapping** to update.
|
|
706
|
+
* **`from`** (required in from-mode)
|
|
707
|
+
Pointer to mapping in source; deep-copied.
|
|
708
|
+
If resolution fails:
|
|
709
|
+
|
|
710
|
+
* and `default` is provided ⇒ use `default`,
|
|
711
|
+
* else ⇒ raise.
|
|
712
|
+
* **`value`** (required in value-mode)
|
|
713
|
+
Inline mapping; resolved through `resolve_special` and `substitute` if needed.
|
|
714
|
+
* **`default`** (optional, from-mode only)
|
|
715
|
+
Mapping used when `from` cannot be resolved.
|
|
716
|
+
* **`create`** (optional, default **`true`**)
|
|
717
|
+
If `true`, missing containers at `path` are created (including the leaf if needed).
|
|
718
|
+
* **`deep`** (optional, default **`false`**)
|
|
719
|
+
|
|
720
|
+
* `false`: shallow update via `dict.update()`.
|
|
721
|
+
* `true`: recursive deep-merge of nested mappings; leaves and non-mappings are overwritten via deep copy.
|
|
722
|
+
|
|
723
|
+
Constraints:
|
|
724
|
+
|
|
725
|
+
* The resolved update value **must be a mapping**; otherwise `TypeError`.
|
|
726
|
+
* The target at `path` must be a mutable mapping; otherwise `TypeError`.
|
|
727
|
+
|
|
728
|
+
---
|
|
729
|
+
|
|
730
|
+
# Schema generation
|
|
731
|
+
|
|
732
|
+
```python
|
|
733
|
+
from j_perm import build_schema
|
|
734
|
+
|
|
735
|
+
schema = build_schema(script)
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
`schema` is a JSON-Schema-like structure inferred by scanning the script:
|
|
739
|
+
|
|
740
|
+
* Tracks `replace_root`, `set`, `update`, nested `$eval`, etc.
|
|
741
|
+
* Infers basic JSON types from literal values.
|
|
742
|
+
|
|
743
|
+
---
|
|
744
|
+
|
|
745
|
+
## Extending with custom operations
|
|
746
|
+
|
|
747
|
+
```python
|
|
748
|
+
from j_perm import register_op
|
|
749
|
+
|
|
750
|
+
@register_op("my_op")
|
|
751
|
+
def my_op(step, dest, src):
|
|
752
|
+
# implement your logic
|
|
753
|
+
return dest
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
Any registered operation can be used in scripts via:
|
|
757
|
+
|
|
758
|
+
```json
|
|
759
|
+
{ "op": "my_op", ... }
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
---
|
|
763
|
+
|
|
764
|
+
## License
|
|
765
|
+
|
|
766
|
+
This package is provided as-is; feel free to adapt it to your project structure.
|