j-perm 0.1.3.1__tar.gz → 0.2.0__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 (51) hide show
  1. j_perm-0.2.0/PKG-INFO +818 -0
  2. j_perm-0.2.0/README.md +805 -0
  3. {j_perm-0.1.3.1 → j_perm-0.2.0}/pyproject.toml +2 -2
  4. j_perm-0.2.0/src/j_perm/__init__.py +21 -0
  5. j_perm-0.2.0/src/j_perm/casters/__init__.py +4 -0
  6. j_perm-0.2.0/src/j_perm/casters/_bool.py +9 -0
  7. j_perm-0.2.0/src/j_perm/casters/_float.py +8 -0
  8. j_perm-0.2.0/src/j_perm/casters/_int.py +8 -0
  9. j_perm-0.2.0/src/j_perm/casters/_str.py +8 -0
  10. j_perm-0.2.0/src/j_perm/constructs/__init__.py +2 -0
  11. j_perm-0.2.0/src/j_perm/constructs/eval.py +16 -0
  12. j_perm-0.2.0/src/j_perm/constructs/ref.py +21 -0
  13. j_perm-0.2.0/src/j_perm/engine.py +162 -0
  14. j_perm-0.2.0/src/j_perm/funcs/__init__.py +1 -0
  15. j_perm-0.2.0/src/j_perm/funcs/subtract.py +11 -0
  16. j_perm-0.2.0/src/j_perm/op_handler.py +57 -0
  17. {j_perm-0.1.3.1 → j_perm-0.2.0}/src/j_perm/ops/__init__.py +1 -0
  18. j_perm-0.1.3.1/src/j_perm/ops/assert.py → j_perm-0.2.0/src/j_perm/ops/_assert.py +4 -4
  19. {j_perm-0.1.3.1 → j_perm-0.2.0}/src/j_perm/ops/_exec.py +8 -9
  20. {j_perm-0.1.3.1 → j_perm-0.2.0}/src/j_perm/ops/_if.py +8 -9
  21. {j_perm-0.1.3.1 → j_perm-0.2.0}/src/j_perm/ops/copy.py +5 -5
  22. {j_perm-0.1.3.1 → j_perm-0.2.0}/src/j_perm/ops/copy_d.py +5 -5
  23. {j_perm-0.1.3.1 → j_perm-0.2.0}/src/j_perm/ops/delete.py +4 -4
  24. {j_perm-0.1.3.1 → j_perm-0.2.0}/src/j_perm/ops/distinct.py +5 -5
  25. {j_perm-0.1.3.1 → j_perm-0.2.0}/src/j_perm/ops/foreach.py +6 -7
  26. j_perm-0.2.0/src/j_perm/ops/replace_root.py +14 -0
  27. {j_perm-0.1.3.1 → j_perm-0.2.0}/src/j_perm/ops/set.py +6 -7
  28. {j_perm-0.1.3.1 → j_perm-0.2.0}/src/j_perm/ops/update.py +7 -8
  29. j_perm-0.2.0/src/j_perm/schema/__init__.py +109 -0
  30. j_perm-0.2.0/src/j_perm/special_resolver.py +115 -0
  31. j_perm-0.2.0/src/j_perm/subst.py +300 -0
  32. j_perm-0.2.0/src/j_perm.egg-info/PKG-INFO +818 -0
  33. {j_perm-0.1.3.1 → j_perm-0.2.0}/src/j_perm.egg-info/SOURCES.txt +15 -7
  34. j_perm-0.1.3.1/PKG-INFO +0 -766
  35. j_perm-0.1.3.1/README.md +0 -753
  36. j_perm-0.1.3.1/src/j_perm/__init__.py +0 -13
  37. j_perm-0.1.3.1/src/j_perm/engine.py +0 -86
  38. j_perm-0.1.3.1/src/j_perm/jmes_ext.py +0 -17
  39. j_perm-0.1.3.1/src/j_perm/ops/replace_root.py +0 -16
  40. j_perm-0.1.3.1/src/j_perm/registry.py +0 -30
  41. j_perm-0.1.3.1/src/j_perm/schema/__init__.py +0 -109
  42. j_perm-0.1.3.1/src/j_perm/utils/special.py +0 -41
  43. j_perm-0.1.3.1/src/j_perm/utils/subst.py +0 -133
  44. j_perm-0.1.3.1/src/j_perm/utils/tuples.py +0 -17
  45. j_perm-0.1.3.1/src/j_perm.egg-info/PKG-INFO +0 -766
  46. {j_perm-0.1.3.1 → j_perm-0.2.0}/setup.cfg +0 -0
  47. {j_perm-0.1.3.1 → j_perm-0.2.0}/src/j_perm/utils/__init__.py +0 -0
  48. {j_perm-0.1.3.1 → j_perm-0.2.0}/src/j_perm/utils/pointers.py +0 -0
  49. {j_perm-0.1.3.1 → j_perm-0.2.0}/src/j_perm.egg-info/dependency_links.txt +0 -0
  50. {j_perm-0.1.3.1 → j_perm-0.2.0}/src/j_perm.egg-info/requires.txt +0 -0
  51. {j_perm-0.1.3.1 → j_perm-0.2.0}/src/j_perm.egg-info/top_level.txt +0 -0
j_perm-0.2.0/PKG-INFO ADDED
@@ -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.