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