j-perm 0.2.0.1__tar.gz → 0.2.1.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.2.0.1 → j_perm-0.2.1.1}/PKG-INFO +75 -14
- {j_perm-0.2.0.1 → j_perm-0.2.1.1}/README.md +74 -13
- {j_perm-0.2.0.1 → j_perm-0.2.1.1}/pyproject.toml +1 -1
- {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/__init__.py +8 -4
- {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/constructs/eval.py +1 -1
- {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/constructs/ref.py +2 -2
- {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/engine.py +5 -51
- {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/funcs/subtract.py +1 -1
- j_perm-0.2.1.1/src/j_perm/normalizer.py +88 -0
- {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/ops/__init__.py +1 -0
- {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/ops/_assert.py +3 -3
- j_perm-0.2.1.1/src/j_perm/ops/_assert_d.py +27 -0
- {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/ops/_exec.py +2 -3
- j_perm-0.2.1.1/src/j_perm/shorthands/__init__.py +3 -0
- j_perm-0.2.1.1/src/j_perm/shorthands/_assert.py +17 -0
- j_perm-0.2.1.1/src/j_perm/shorthands/assign_or_append.py +17 -0
- j_perm-0.2.1.1/src/j_perm/shorthands/delete.py +10 -0
- {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm.egg-info/PKG-INFO +75 -14
- {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm.egg-info/SOURCES.txt +6 -0
- {j_perm-0.2.0.1 → j_perm-0.2.1.1}/setup.cfg +0 -0
- {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/casters/__init__.py +0 -0
- {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/casters/_bool.py +0 -0
- {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/casters/_float.py +0 -0
- {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/casters/_int.py +0 -0
- {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/casters/_str.py +0 -0
- {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/constructs/__init__.py +0 -0
- {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/funcs/__init__.py +0 -0
- {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/op_handler.py +0 -0
- {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/ops/_if.py +0 -0
- {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/ops/copy.py +0 -0
- {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/ops/copy_d.py +0 -0
- {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/ops/delete.py +0 -0
- {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/ops/distinct.py +0 -0
- {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/ops/foreach.py +0 -0
- {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/ops/replace_root.py +0 -0
- {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/ops/set.py +0 -0
- {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/ops/update.py +0 -0
- {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/schema/__init__.py +0 -0
- {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/special_resolver.py +0 -0
- {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/subst.py +0 -0
- {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/utils/__init__.py +0 -0
- {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/utils/pointers.py +0 -0
- {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm.egg-info/dependency_links.txt +0 -0
- {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm.egg-info/requires.txt +0 -0
- {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: j-perm
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1.1
|
|
4
4
|
Summary: json permutation library
|
|
5
5
|
Author-email: Roman <kuschanow@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -84,11 +84,13 @@ Typical setup:
|
|
|
84
84
|
substitutor = TemplateSubstitutor()
|
|
85
85
|
special = SpecialResolver()
|
|
86
86
|
handlers = Handlers()
|
|
87
|
+
normalizer = Normalizer()
|
|
87
88
|
|
|
88
89
|
engine = ActionEngine(
|
|
89
90
|
handlers=handlers,
|
|
90
91
|
special=special,
|
|
91
92
|
substitutor=substitutor,
|
|
93
|
+
normalizer=normalizer,
|
|
92
94
|
)
|
|
93
95
|
```
|
|
94
96
|
|
|
@@ -676,59 +678,103 @@ Update a mapping at `path` using either source mapping (`from`) or inline mappin
|
|
|
676
678
|
|
|
677
679
|
---
|
|
678
680
|
|
|
679
|
-
## Shorthand
|
|
681
|
+
## Shorthand normalization
|
|
680
682
|
|
|
681
|
-
|
|
683
|
+
In addition to explicit DSL steps, J-Perm supports a *shorthand syntax* for more concise scripts.
|
|
684
|
+
Shorthand forms are expanded into regular operation steps **before execution**.
|
|
682
685
|
|
|
683
|
-
|
|
686
|
+
Shorthand expansion is implemented as a pluggable normalization layer, similar to operations and special constructs.
|
|
687
|
+
|
|
688
|
+
### How it works
|
|
689
|
+
|
|
690
|
+
During normalization, each mapping entry is processed by a chain of registered *shorthand rules*:
|
|
691
|
+
|
|
692
|
+
1. Each rule decides whether it can handle a given `(key, value)` pair.
|
|
693
|
+
2. If a rule matches, it expands the entry into one or more explicit operation steps.
|
|
694
|
+
3. The first matching rule wins.
|
|
695
|
+
4. If no rule matches, normalization fails with an error.
|
|
696
|
+
|
|
697
|
+
The resulting list of steps is then executed by the engine as a normal DSL script.
|
|
698
|
+
|
|
699
|
+
---
|
|
700
|
+
|
|
701
|
+
### Built-in shorthand rules
|
|
702
|
+
|
|
703
|
+
The following shorthand forms are enabled by default.
|
|
704
|
+
|
|
705
|
+
#### Delete shorthand (`~delete`)
|
|
684
706
|
|
|
685
707
|
```json
|
|
686
708
|
{ "~delete": ["/a", "/b"] }
|
|
687
709
|
```
|
|
688
710
|
|
|
689
|
-
|
|
711
|
+
Expands into:
|
|
690
712
|
|
|
691
713
|
```json
|
|
692
714
|
{ "op": "delete", "path": "/a" }
|
|
693
715
|
{ "op": "delete", "path": "/b" }
|
|
694
716
|
```
|
|
695
717
|
|
|
696
|
-
|
|
718
|
+
---
|
|
719
|
+
|
|
720
|
+
#### Assert shorthand (`~assert`)
|
|
721
|
+
|
|
722
|
+
Mapping form:
|
|
697
723
|
|
|
698
724
|
```json
|
|
699
|
-
{ "~assert": { "/x": 10 } }
|
|
725
|
+
{ "~assert": { "/x": 10, "/y": 20 } }
|
|
700
726
|
```
|
|
701
727
|
|
|
702
|
-
|
|
728
|
+
Expands into:
|
|
703
729
|
|
|
704
730
|
```json
|
|
705
731
|
{ "op": "assert", "path": "/x", "equals": 10 }
|
|
732
|
+
{ "op": "assert", "path": "/y", "equals": 20 }
|
|
706
733
|
```
|
|
707
734
|
|
|
708
|
-
|
|
735
|
+
List / string form:
|
|
736
|
+
|
|
737
|
+
```json
|
|
738
|
+
{ "~assert": ["/x", "/y"] }
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
Expands into existence-only assertions.
|
|
742
|
+
|
|
743
|
+
---
|
|
744
|
+
|
|
745
|
+
#### Append shorthand (`field[]`)
|
|
746
|
+
|
|
747
|
+
A key ending with `[]` means *append to a list* at that path:
|
|
709
748
|
|
|
710
749
|
```json
|
|
711
750
|
{ "items[]": 123 }
|
|
712
751
|
```
|
|
713
752
|
|
|
714
|
-
|
|
753
|
+
Expands into:
|
|
715
754
|
|
|
716
755
|
```json
|
|
717
756
|
{ "op": "set", "path": "/items/-", "value": 123 }
|
|
718
757
|
```
|
|
719
758
|
|
|
720
|
-
|
|
759
|
+
---
|
|
760
|
+
|
|
761
|
+
#### Pointer assignment shorthand
|
|
721
762
|
|
|
722
|
-
If a value is a string that starts with `/`, it
|
|
763
|
+
If a value is a string that starts with `/`, it is treated as a source pointer:
|
|
723
764
|
|
|
724
765
|
```json
|
|
725
766
|
{ "name": "/user/fullName" }
|
|
726
767
|
```
|
|
727
768
|
|
|
728
|
-
|
|
769
|
+
Expands into:
|
|
729
770
|
|
|
730
771
|
```json
|
|
731
|
-
{
|
|
772
|
+
{
|
|
773
|
+
"op": "copy",
|
|
774
|
+
"from": "/user/fullName",
|
|
775
|
+
"path": "/name",
|
|
776
|
+
"ignore_missing": true
|
|
777
|
+
}
|
|
732
778
|
```
|
|
733
779
|
|
|
734
780
|
---
|
|
@@ -798,6 +844,21 @@ ${json:/raw_payload}
|
|
|
798
844
|
|
|
799
845
|
---
|
|
800
846
|
|
|
847
|
+
### Custom shorthand rules
|
|
848
|
+
|
|
849
|
+
```python
|
|
850
|
+
from j_perm import ShorthandRegistry, ExpandResult
|
|
851
|
+
|
|
852
|
+
@ShorthandRegistry.register("name", priority=10)
|
|
853
|
+
def my_shorthand_rule(key: str, value: Any) -> ExpandResult | None:
|
|
854
|
+
if key.startswith("my_prefix_"):
|
|
855
|
+
# expand into steps
|
|
856
|
+
steps = [ ... ]
|
|
857
|
+
return steps
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
---
|
|
861
|
+
|
|
801
862
|
## Plugin loading
|
|
802
863
|
|
|
803
864
|
Registries collect definitions **at import time**.
|
|
@@ -71,11 +71,13 @@ Typical setup:
|
|
|
71
71
|
substitutor = TemplateSubstitutor()
|
|
72
72
|
special = SpecialResolver()
|
|
73
73
|
handlers = Handlers()
|
|
74
|
+
normalizer = Normalizer()
|
|
74
75
|
|
|
75
76
|
engine = ActionEngine(
|
|
76
77
|
handlers=handlers,
|
|
77
78
|
special=special,
|
|
78
79
|
substitutor=substitutor,
|
|
80
|
+
normalizer=normalizer,
|
|
79
81
|
)
|
|
80
82
|
```
|
|
81
83
|
|
|
@@ -663,59 +665,103 @@ Update a mapping at `path` using either source mapping (`from`) or inline mappin
|
|
|
663
665
|
|
|
664
666
|
---
|
|
665
667
|
|
|
666
|
-
## Shorthand
|
|
668
|
+
## Shorthand normalization
|
|
667
669
|
|
|
668
|
-
|
|
670
|
+
In addition to explicit DSL steps, J-Perm supports a *shorthand syntax* for more concise scripts.
|
|
671
|
+
Shorthand forms are expanded into regular operation steps **before execution**.
|
|
669
672
|
|
|
670
|
-
|
|
673
|
+
Shorthand expansion is implemented as a pluggable normalization layer, similar to operations and special constructs.
|
|
674
|
+
|
|
675
|
+
### How it works
|
|
676
|
+
|
|
677
|
+
During normalization, each mapping entry is processed by a chain of registered *shorthand rules*:
|
|
678
|
+
|
|
679
|
+
1. Each rule decides whether it can handle a given `(key, value)` pair.
|
|
680
|
+
2. If a rule matches, it expands the entry into one or more explicit operation steps.
|
|
681
|
+
3. The first matching rule wins.
|
|
682
|
+
4. If no rule matches, normalization fails with an error.
|
|
683
|
+
|
|
684
|
+
The resulting list of steps is then executed by the engine as a normal DSL script.
|
|
685
|
+
|
|
686
|
+
---
|
|
687
|
+
|
|
688
|
+
### Built-in shorthand rules
|
|
689
|
+
|
|
690
|
+
The following shorthand forms are enabled by default.
|
|
691
|
+
|
|
692
|
+
#### Delete shorthand (`~delete`)
|
|
671
693
|
|
|
672
694
|
```json
|
|
673
695
|
{ "~delete": ["/a", "/b"] }
|
|
674
696
|
```
|
|
675
697
|
|
|
676
|
-
|
|
698
|
+
Expands into:
|
|
677
699
|
|
|
678
700
|
```json
|
|
679
701
|
{ "op": "delete", "path": "/a" }
|
|
680
702
|
{ "op": "delete", "path": "/b" }
|
|
681
703
|
```
|
|
682
704
|
|
|
683
|
-
|
|
705
|
+
---
|
|
706
|
+
|
|
707
|
+
#### Assert shorthand (`~assert`)
|
|
708
|
+
|
|
709
|
+
Mapping form:
|
|
684
710
|
|
|
685
711
|
```json
|
|
686
|
-
{ "~assert": { "/x": 10 } }
|
|
712
|
+
{ "~assert": { "/x": 10, "/y": 20 } }
|
|
687
713
|
```
|
|
688
714
|
|
|
689
|
-
|
|
715
|
+
Expands into:
|
|
690
716
|
|
|
691
717
|
```json
|
|
692
718
|
{ "op": "assert", "path": "/x", "equals": 10 }
|
|
719
|
+
{ "op": "assert", "path": "/y", "equals": 20 }
|
|
693
720
|
```
|
|
694
721
|
|
|
695
|
-
|
|
722
|
+
List / string form:
|
|
723
|
+
|
|
724
|
+
```json
|
|
725
|
+
{ "~assert": ["/x", "/y"] }
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
Expands into existence-only assertions.
|
|
729
|
+
|
|
730
|
+
---
|
|
731
|
+
|
|
732
|
+
#### Append shorthand (`field[]`)
|
|
733
|
+
|
|
734
|
+
A key ending with `[]` means *append to a list* at that path:
|
|
696
735
|
|
|
697
736
|
```json
|
|
698
737
|
{ "items[]": 123 }
|
|
699
738
|
```
|
|
700
739
|
|
|
701
|
-
|
|
740
|
+
Expands into:
|
|
702
741
|
|
|
703
742
|
```json
|
|
704
743
|
{ "op": "set", "path": "/items/-", "value": 123 }
|
|
705
744
|
```
|
|
706
745
|
|
|
707
|
-
|
|
746
|
+
---
|
|
747
|
+
|
|
748
|
+
#### Pointer assignment shorthand
|
|
708
749
|
|
|
709
|
-
If a value is a string that starts with `/`, it
|
|
750
|
+
If a value is a string that starts with `/`, it is treated as a source pointer:
|
|
710
751
|
|
|
711
752
|
```json
|
|
712
753
|
{ "name": "/user/fullName" }
|
|
713
754
|
```
|
|
714
755
|
|
|
715
|
-
|
|
756
|
+
Expands into:
|
|
716
757
|
|
|
717
758
|
```json
|
|
718
|
-
{
|
|
759
|
+
{
|
|
760
|
+
"op": "copy",
|
|
761
|
+
"from": "/user/fullName",
|
|
762
|
+
"path": "/name",
|
|
763
|
+
"ignore_missing": true
|
|
764
|
+
}
|
|
719
765
|
```
|
|
720
766
|
|
|
721
767
|
---
|
|
@@ -785,6 +831,21 @@ ${json:/raw_payload}
|
|
|
785
831
|
|
|
786
832
|
---
|
|
787
833
|
|
|
834
|
+
### Custom shorthand rules
|
|
835
|
+
|
|
836
|
+
```python
|
|
837
|
+
from j_perm import ShorthandRegistry, ExpandResult
|
|
838
|
+
|
|
839
|
+
@ShorthandRegistry.register("name", priority=10)
|
|
840
|
+
def my_shorthand_rule(key: str, value: Any) -> ExpandResult | None:
|
|
841
|
+
if key.startswith("my_prefix_"):
|
|
842
|
+
# expand into steps
|
|
843
|
+
steps = [ ... ]
|
|
844
|
+
return steps
|
|
845
|
+
```
|
|
846
|
+
|
|
847
|
+
---
|
|
848
|
+
|
|
788
849
|
## Plugin loading
|
|
789
850
|
|
|
790
851
|
Registries collect definitions **at import time**.
|
|
@@ -3,19 +3,23 @@ from . import casters as _builtin_casters # noqa: F401
|
|
|
3
3
|
from . import constructs as _builtin_constructs # noqa: F401
|
|
4
4
|
from . import funcs as _builtin_funcs # noqa: F401
|
|
5
5
|
from . import ops as _builtin_ops # noqa: F401
|
|
6
|
+
from . import shorthands as _builtin_shorthands # noqa: F401
|
|
6
7
|
from .engine import apply_actions, ActionEngine
|
|
7
|
-
from .
|
|
8
|
+
from .normalizer import Normalizer, ShorthandRegistry
|
|
8
9
|
from .op_handler import OpRegistry, Handlers
|
|
9
10
|
from .special_resolver import SpecialRegistry, SpecialResolver
|
|
11
|
+
from .subst import JpFuncRegistry, CasterRegistry, TemplateSubstitutor
|
|
10
12
|
|
|
11
13
|
__all__ = [
|
|
12
14
|
"apply_actions",
|
|
13
15
|
"ActionEngine",
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"TemplateSubstitutor",
|
|
16
|
+
"Normalizer",
|
|
17
|
+
"ShorthandRegistry",
|
|
17
18
|
"OpRegistry",
|
|
18
19
|
"Handlers",
|
|
19
20
|
"SpecialRegistry",
|
|
20
21
|
"SpecialResolver",
|
|
22
|
+
"JpFuncRegistry",
|
|
23
|
+
"CasterRegistry",
|
|
24
|
+
"TemplateSubstitutor",
|
|
21
25
|
]
|
|
@@ -3,8 +3,8 @@ from __future__ import annotations
|
|
|
3
3
|
import copy
|
|
4
4
|
from typing import Mapping, Any
|
|
5
5
|
|
|
6
|
-
from
|
|
7
|
-
from
|
|
6
|
+
from ..special_resolver import _MISSING, SpecialRegistry
|
|
7
|
+
from ..utils.pointers import maybe_slice
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
@SpecialRegistry.register("$ref")
|
|
@@ -4,6 +4,7 @@ import copy
|
|
|
4
4
|
from dataclasses import dataclass, field
|
|
5
5
|
from typing import Any, List, Mapping, TypeAlias, MutableMapping, Union
|
|
6
6
|
|
|
7
|
+
from .normalizer import Normalizer
|
|
7
8
|
from .op_handler import Handlers
|
|
8
9
|
from .special_resolver import SpecialResolver
|
|
9
10
|
from .subst import TemplateSubstitutor
|
|
@@ -17,14 +18,6 @@ JsonLikeDest: TypeAlias = Union[MutableMapping[str, Any], List[Any]]
|
|
|
17
18
|
# Small utilities
|
|
18
19
|
# =============================================================================
|
|
19
20
|
|
|
20
|
-
def _is_pointer_string(v: Any) -> bool:
|
|
21
|
-
return isinstance(v, str) and v.startswith("/")
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def _to_list(x: Any) -> List[Any]:
|
|
25
|
-
return x if isinstance(x, list) else [x]
|
|
26
|
-
|
|
27
|
-
|
|
28
21
|
def tuples_to_lists(obj: Any) -> Any:
|
|
29
22
|
"""Recursively convert all tuples into lists so JMESPath indexers work reliably."""
|
|
30
23
|
if isinstance(obj, tuple):
|
|
@@ -48,22 +41,13 @@ class ActionEngine:
|
|
|
48
41
|
- handlers: a Handlers object (has get_handler)
|
|
49
42
|
- special: a SpecialResolver (resolves $ref/$eval/... inside the actions spec, if you want)
|
|
50
43
|
- substitutor: a TemplateSubstitutor (optional: expand ${...} in action specs)
|
|
51
|
-
-
|
|
52
|
-
- substitute_templates_in_actions: enable template expansion inside the actions spec
|
|
53
|
-
|
|
54
|
-
Notes on order:
|
|
55
|
-
1) Optionally substitute templates in 'actions' using 'source' as context
|
|
56
|
-
2) Optionally resolve special constructs in 'actions' using 'source' as context
|
|
57
|
-
3) Normalize to flat step list
|
|
58
|
-
4) Execute handlers
|
|
44
|
+
- normalizer: a Normalizer (for shorthand expansion)
|
|
59
45
|
"""
|
|
60
46
|
|
|
61
47
|
handlers: Handlers = field(default_factory=Handlers)
|
|
62
48
|
special: SpecialResolver = field(default_factory=SpecialResolver)
|
|
63
49
|
substitutor: TemplateSubstitutor = field(default_factory=TemplateSubstitutor)
|
|
64
|
-
|
|
65
|
-
resolve_special_in_actions: bool = True
|
|
66
|
-
substitute_templates_in_actions: bool = True
|
|
50
|
+
normalizer: Normalizer = field(default_factory=Normalizer)
|
|
67
51
|
|
|
68
52
|
def apply_actions(
|
|
69
53
|
self,
|
|
@@ -107,46 +91,16 @@ class ActionEngine:
|
|
|
107
91
|
out: List[dict] = []
|
|
108
92
|
for item in spec:
|
|
109
93
|
if isinstance(item, Mapping) and "op" not in item:
|
|
110
|
-
out.extend(self.
|
|
94
|
+
out.extend(self.normalizer.expand_shorthand(item))
|
|
111
95
|
else:
|
|
112
96
|
out.append(item)
|
|
113
97
|
return out
|
|
114
98
|
|
|
115
99
|
if isinstance(spec, Mapping):
|
|
116
|
-
return self.
|
|
100
|
+
return self.normalizer.expand_shorthand(spec)
|
|
117
101
|
|
|
118
102
|
raise TypeError("spec must be dict or list")
|
|
119
103
|
|
|
120
|
-
@staticmethod
|
|
121
|
-
def _expand_shorthand(obj: Mapping[str, Any]) -> List[dict]:
|
|
122
|
-
"""Expand shorthand mapping form into explicit op steps."""
|
|
123
|
-
steps: List[dict] = []
|
|
124
|
-
|
|
125
|
-
for key, val in obj.items():
|
|
126
|
-
if key == "~delete":
|
|
127
|
-
for p in _to_list(val):
|
|
128
|
-
steps.append({"op": "delete", "path": p})
|
|
129
|
-
continue
|
|
130
|
-
|
|
131
|
-
if key == "~assert":
|
|
132
|
-
if isinstance(val, Mapping):
|
|
133
|
-
for p, eq in val.items():
|
|
134
|
-
steps.append({"op": "assert", "path": p, "equals": eq})
|
|
135
|
-
else:
|
|
136
|
-
for p in _to_list(val):
|
|
137
|
-
steps.append({"op": "assert", "path": p})
|
|
138
|
-
continue
|
|
139
|
-
|
|
140
|
-
append = isinstance(key, str) and key.endswith("[]")
|
|
141
|
-
dst = f"{key[:-2]}/-" if append else key
|
|
142
|
-
|
|
143
|
-
if _is_pointer_string(val):
|
|
144
|
-
steps.append({"op": "copy", "from": val, "path": dst, "ignore_missing": True})
|
|
145
|
-
else:
|
|
146
|
-
steps.append({"op": "set", "path": dst, "value": val})
|
|
147
|
-
|
|
148
|
-
return steps
|
|
149
|
-
|
|
150
104
|
|
|
151
105
|
# Create a default engine instance for convenience
|
|
152
106
|
default_engine = ActionEngine()
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any, Callable, Mapping, Optional, List
|
|
5
|
+
|
|
6
|
+
ExpandResult = Optional[List[dict]]
|
|
7
|
+
Rule = Callable[[str, Any], ExpandResult]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ShorthandRegistry:
|
|
11
|
+
_rules: dict[str, Rule] = {}
|
|
12
|
+
_default_priority: dict[str, int] = {} # name -> order (10, 5, 0, -5, -10...)
|
|
13
|
+
|
|
14
|
+
@classmethod
|
|
15
|
+
def register(cls, name: str, *, priority: int = 0):
|
|
16
|
+
def deco(fn: Rule) -> Rule:
|
|
17
|
+
if name in cls._rules:
|
|
18
|
+
raise ValueError(f"shorthand rule already registered: {name!r}")
|
|
19
|
+
cls._rules[name] = fn
|
|
20
|
+
cls._default_priority[name] = priority
|
|
21
|
+
return fn
|
|
22
|
+
return deco
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def ordered(
|
|
26
|
+
cls,
|
|
27
|
+
rules: Mapping[str, Rule] | None = None,
|
|
28
|
+
*,
|
|
29
|
+
priority: Mapping[str, int] | None = None,
|
|
30
|
+
) -> list[Rule]:
|
|
31
|
+
rules = dict(cls._rules if rules is None else rules)
|
|
32
|
+
|
|
33
|
+
_priority = dict(cls._default_priority)
|
|
34
|
+
if priority:
|
|
35
|
+
_priority.update(priority)
|
|
36
|
+
|
|
37
|
+
names = sorted(
|
|
38
|
+
rules.keys(),
|
|
39
|
+
key=lambda n: (_priority.get(n, 0), n),
|
|
40
|
+
reverse=True,
|
|
41
|
+
)
|
|
42
|
+
return [rules[n] for n in names]
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def get(cls, name: str) -> Rule:
|
|
46
|
+
try:
|
|
47
|
+
return cls._rules[name]
|
|
48
|
+
except KeyError:
|
|
49
|
+
raise ValueError(f"Unknown rule {name!r}") from None
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def all(cls) -> dict[str, Rule]:
|
|
53
|
+
return dict(cls._rules)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(slots=True)
|
|
57
|
+
class Normalizer:
|
|
58
|
+
rules: list[str] | Mapping[str, Rule] | None = None
|
|
59
|
+
priority: Mapping[str, int] | None = None
|
|
60
|
+
|
|
61
|
+
_rules: list[Rule] = field(init=False)
|
|
62
|
+
|
|
63
|
+
def __post_init__(self) -> None:
|
|
64
|
+
if self.rules is None:
|
|
65
|
+
rules = ShorthandRegistry.all()
|
|
66
|
+
elif isinstance(self.rules, Mapping):
|
|
67
|
+
rules = dict(self.rules)
|
|
68
|
+
else:
|
|
69
|
+
all_rules = ShorthandRegistry.all()
|
|
70
|
+
rules = {}
|
|
71
|
+
for n in self.rules:
|
|
72
|
+
if n not in all_rules:
|
|
73
|
+
raise ValueError(f"Unknown rule {n!r}")
|
|
74
|
+
rules[n] = all_rules[n]
|
|
75
|
+
|
|
76
|
+
self._rules = ShorthandRegistry.ordered(rules, priority=self.priority)
|
|
77
|
+
|
|
78
|
+
def expand_shorthand(self, obj: Mapping[str, Any]) -> List[dict]:
|
|
79
|
+
steps: List[dict] = []
|
|
80
|
+
for k, v in obj.items():
|
|
81
|
+
for rule in self._rules:
|
|
82
|
+
out = rule(k, v)
|
|
83
|
+
if out is not None:
|
|
84
|
+
steps.extend(out)
|
|
85
|
+
break
|
|
86
|
+
else:
|
|
87
|
+
raise ValueError(f"Unhandled shorthand key: {k!r}")
|
|
88
|
+
return steps
|
|
@@ -17,11 +17,11 @@ def op_assert(
|
|
|
17
17
|
path = engine.substitutor.substitute(step["path"], src)
|
|
18
18
|
|
|
19
19
|
try:
|
|
20
|
-
current = jptr_get(
|
|
20
|
+
current = jptr_get(src, path)
|
|
21
21
|
except Exception:
|
|
22
|
-
raise AssertionError(f"{path} does not exist")
|
|
22
|
+
raise AssertionError(f"'{path}' does not exist in source")
|
|
23
23
|
|
|
24
24
|
if "equals" in step and current != step["equals"]:
|
|
25
|
-
raise AssertionError(f"{path} != {step['equals']!r}")
|
|
25
|
+
raise AssertionError(f"'{path}' != '{step['equals']!r}'")
|
|
26
26
|
|
|
27
27
|
return dest
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import MutableMapping, Any, Mapping
|
|
4
|
+
|
|
5
|
+
from ..op_handler import OpRegistry
|
|
6
|
+
from ..utils.pointers import jptr_get
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@OpRegistry.register("assertD")
|
|
10
|
+
def op_assert_d(
|
|
11
|
+
step: dict,
|
|
12
|
+
dest: MutableMapping[str, Any],
|
|
13
|
+
src: Mapping[str, Any],
|
|
14
|
+
engine: "ActionEngine",
|
|
15
|
+
) -> MutableMapping[str, Any]:
|
|
16
|
+
"""Assert node existence and/or value at JSON Pointer path in dest."""
|
|
17
|
+
path = engine.substitutor.substitute(step["path"], dest)
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
current = jptr_get(dest, path)
|
|
21
|
+
except Exception:
|
|
22
|
+
raise AssertionError(f"'{path}' does not exist in destination")
|
|
23
|
+
|
|
24
|
+
if "equals" in step and current != step["equals"]:
|
|
25
|
+
raise AssertionError(f"'{path}' != '{step['equals']!r}'")
|
|
26
|
+
|
|
27
|
+
return dest
|
|
@@ -3,7 +3,6 @@ from __future__ import annotations
|
|
|
3
3
|
from typing import MutableMapping, Any, Mapping
|
|
4
4
|
|
|
5
5
|
from ..op_handler import OpRegistry
|
|
6
|
-
from ..engine import apply_actions
|
|
7
6
|
from ..utils.pointers import maybe_slice
|
|
8
7
|
|
|
9
8
|
|
|
@@ -43,10 +42,10 @@ def op_exec(
|
|
|
43
42
|
merge = bool(step.get("merge", False))
|
|
44
43
|
|
|
45
44
|
if merge:
|
|
46
|
-
result = apply_actions(actions, dest=dest, source=src)
|
|
45
|
+
result = engine.apply_actions(actions, dest=dest, source=src)
|
|
47
46
|
return result
|
|
48
47
|
else:
|
|
49
|
-
result = apply_actions(actions, dest={}, source=src)
|
|
48
|
+
result = engine.apply_actions(actions, dest={}, source=src)
|
|
50
49
|
dest.clear()
|
|
51
50
|
if isinstance(dest, list):
|
|
52
51
|
dest.extend(result) # type: ignore[arg-type]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from ..normalizer import ShorthandRegistry, ExpandResult
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@ShorthandRegistry.register("assert")
|
|
8
|
+
def rule_assert(key: str, val: Any) -> ExpandResult:
|
|
9
|
+
if key != "~assert" and key != "~assertD":
|
|
10
|
+
return None
|
|
11
|
+
if key == "~assertD":
|
|
12
|
+
op = "assertD"
|
|
13
|
+
else:
|
|
14
|
+
op = "assert"
|
|
15
|
+
if isinstance(val, Mapping):
|
|
16
|
+
return [{"op": op, "path": p, "equals": eq} for p, eq in val.items()]
|
|
17
|
+
return [{"op": op, "path": p} for p in (val if isinstance(val, list) else [val])]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from ..normalizer import ExpandResult, ShorthandRegistry
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@ShorthandRegistry.register("assign_or_append", priority=-1)
|
|
7
|
+
def rule_assign_or_append(key: str, val: Any) -> ExpandResult:
|
|
8
|
+
# Fallback rule: handles:
|
|
9
|
+
# - append shorthand: field[] -> "/field/-"
|
|
10
|
+
# - pointer assignment -> copy
|
|
11
|
+
# - literal assignment -> set
|
|
12
|
+
append = key.endswith("[]")
|
|
13
|
+
dst = f"{key[:-2]}/-" if append else key
|
|
14
|
+
|
|
15
|
+
if isinstance(val, str) and val.startswith("/"):
|
|
16
|
+
return [{"op": "copy", "from": val, "path": dst, "ignore_missing": True}]
|
|
17
|
+
return [{"op": "set", "path": dst, "value": val}]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from ..normalizer import ShorthandRegistry, ExpandResult
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@ShorthandRegistry.register("delete")
|
|
7
|
+
def rule_delete(key: str, val: Any) -> ExpandResult:
|
|
8
|
+
if key != "~delete":
|
|
9
|
+
return None
|
|
10
|
+
return [{"op": "delete", "path": p} for p in (val if isinstance(val, list) else [val])]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: j-perm
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1.1
|
|
4
4
|
Summary: json permutation library
|
|
5
5
|
Author-email: Roman <kuschanow@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -84,11 +84,13 @@ Typical setup:
|
|
|
84
84
|
substitutor = TemplateSubstitutor()
|
|
85
85
|
special = SpecialResolver()
|
|
86
86
|
handlers = Handlers()
|
|
87
|
+
normalizer = Normalizer()
|
|
87
88
|
|
|
88
89
|
engine = ActionEngine(
|
|
89
90
|
handlers=handlers,
|
|
90
91
|
special=special,
|
|
91
92
|
substitutor=substitutor,
|
|
93
|
+
normalizer=normalizer,
|
|
92
94
|
)
|
|
93
95
|
```
|
|
94
96
|
|
|
@@ -676,59 +678,103 @@ Update a mapping at `path` using either source mapping (`from`) or inline mappin
|
|
|
676
678
|
|
|
677
679
|
---
|
|
678
680
|
|
|
679
|
-
## Shorthand
|
|
681
|
+
## Shorthand normalization
|
|
680
682
|
|
|
681
|
-
|
|
683
|
+
In addition to explicit DSL steps, J-Perm supports a *shorthand syntax* for more concise scripts.
|
|
684
|
+
Shorthand forms are expanded into regular operation steps **before execution**.
|
|
682
685
|
|
|
683
|
-
|
|
686
|
+
Shorthand expansion is implemented as a pluggable normalization layer, similar to operations and special constructs.
|
|
687
|
+
|
|
688
|
+
### How it works
|
|
689
|
+
|
|
690
|
+
During normalization, each mapping entry is processed by a chain of registered *shorthand rules*:
|
|
691
|
+
|
|
692
|
+
1. Each rule decides whether it can handle a given `(key, value)` pair.
|
|
693
|
+
2. If a rule matches, it expands the entry into one or more explicit operation steps.
|
|
694
|
+
3. The first matching rule wins.
|
|
695
|
+
4. If no rule matches, normalization fails with an error.
|
|
696
|
+
|
|
697
|
+
The resulting list of steps is then executed by the engine as a normal DSL script.
|
|
698
|
+
|
|
699
|
+
---
|
|
700
|
+
|
|
701
|
+
### Built-in shorthand rules
|
|
702
|
+
|
|
703
|
+
The following shorthand forms are enabled by default.
|
|
704
|
+
|
|
705
|
+
#### Delete shorthand (`~delete`)
|
|
684
706
|
|
|
685
707
|
```json
|
|
686
708
|
{ "~delete": ["/a", "/b"] }
|
|
687
709
|
```
|
|
688
710
|
|
|
689
|
-
|
|
711
|
+
Expands into:
|
|
690
712
|
|
|
691
713
|
```json
|
|
692
714
|
{ "op": "delete", "path": "/a" }
|
|
693
715
|
{ "op": "delete", "path": "/b" }
|
|
694
716
|
```
|
|
695
717
|
|
|
696
|
-
|
|
718
|
+
---
|
|
719
|
+
|
|
720
|
+
#### Assert shorthand (`~assert`)
|
|
721
|
+
|
|
722
|
+
Mapping form:
|
|
697
723
|
|
|
698
724
|
```json
|
|
699
|
-
{ "~assert": { "/x": 10 } }
|
|
725
|
+
{ "~assert": { "/x": 10, "/y": 20 } }
|
|
700
726
|
```
|
|
701
727
|
|
|
702
|
-
|
|
728
|
+
Expands into:
|
|
703
729
|
|
|
704
730
|
```json
|
|
705
731
|
{ "op": "assert", "path": "/x", "equals": 10 }
|
|
732
|
+
{ "op": "assert", "path": "/y", "equals": 20 }
|
|
706
733
|
```
|
|
707
734
|
|
|
708
|
-
|
|
735
|
+
List / string form:
|
|
736
|
+
|
|
737
|
+
```json
|
|
738
|
+
{ "~assert": ["/x", "/y"] }
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
Expands into existence-only assertions.
|
|
742
|
+
|
|
743
|
+
---
|
|
744
|
+
|
|
745
|
+
#### Append shorthand (`field[]`)
|
|
746
|
+
|
|
747
|
+
A key ending with `[]` means *append to a list* at that path:
|
|
709
748
|
|
|
710
749
|
```json
|
|
711
750
|
{ "items[]": 123 }
|
|
712
751
|
```
|
|
713
752
|
|
|
714
|
-
|
|
753
|
+
Expands into:
|
|
715
754
|
|
|
716
755
|
```json
|
|
717
756
|
{ "op": "set", "path": "/items/-", "value": 123 }
|
|
718
757
|
```
|
|
719
758
|
|
|
720
|
-
|
|
759
|
+
---
|
|
760
|
+
|
|
761
|
+
#### Pointer assignment shorthand
|
|
721
762
|
|
|
722
|
-
If a value is a string that starts with `/`, it
|
|
763
|
+
If a value is a string that starts with `/`, it is treated as a source pointer:
|
|
723
764
|
|
|
724
765
|
```json
|
|
725
766
|
{ "name": "/user/fullName" }
|
|
726
767
|
```
|
|
727
768
|
|
|
728
|
-
|
|
769
|
+
Expands into:
|
|
729
770
|
|
|
730
771
|
```json
|
|
731
|
-
{
|
|
772
|
+
{
|
|
773
|
+
"op": "copy",
|
|
774
|
+
"from": "/user/fullName",
|
|
775
|
+
"path": "/name",
|
|
776
|
+
"ignore_missing": true
|
|
777
|
+
}
|
|
732
778
|
```
|
|
733
779
|
|
|
734
780
|
---
|
|
@@ -798,6 +844,21 @@ ${json:/raw_payload}
|
|
|
798
844
|
|
|
799
845
|
---
|
|
800
846
|
|
|
847
|
+
### Custom shorthand rules
|
|
848
|
+
|
|
849
|
+
```python
|
|
850
|
+
from j_perm import ShorthandRegistry, ExpandResult
|
|
851
|
+
|
|
852
|
+
@ShorthandRegistry.register("name", priority=10)
|
|
853
|
+
def my_shorthand_rule(key: str, value: Any) -> ExpandResult | None:
|
|
854
|
+
if key.startswith("my_prefix_"):
|
|
855
|
+
# expand into steps
|
|
856
|
+
steps = [ ... ]
|
|
857
|
+
return steps
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
---
|
|
861
|
+
|
|
801
862
|
## Plugin loading
|
|
802
863
|
|
|
803
864
|
Registries collect definitions **at import time**.
|
|
@@ -2,6 +2,7 @@ README.md
|
|
|
2
2
|
pyproject.toml
|
|
3
3
|
src/j_perm/__init__.py
|
|
4
4
|
src/j_perm/engine.py
|
|
5
|
+
src/j_perm/normalizer.py
|
|
5
6
|
src/j_perm/op_handler.py
|
|
6
7
|
src/j_perm/special_resolver.py
|
|
7
8
|
src/j_perm/subst.py
|
|
@@ -22,6 +23,7 @@ src/j_perm/funcs/__init__.py
|
|
|
22
23
|
src/j_perm/funcs/subtract.py
|
|
23
24
|
src/j_perm/ops/__init__.py
|
|
24
25
|
src/j_perm/ops/_assert.py
|
|
26
|
+
src/j_perm/ops/_assert_d.py
|
|
25
27
|
src/j_perm/ops/_exec.py
|
|
26
28
|
src/j_perm/ops/_if.py
|
|
27
29
|
src/j_perm/ops/copy.py
|
|
@@ -33,5 +35,9 @@ src/j_perm/ops/replace_root.py
|
|
|
33
35
|
src/j_perm/ops/set.py
|
|
34
36
|
src/j_perm/ops/update.py
|
|
35
37
|
src/j_perm/schema/__init__.py
|
|
38
|
+
src/j_perm/shorthands/__init__.py
|
|
39
|
+
src/j_perm/shorthands/_assert.py
|
|
40
|
+
src/j_perm/shorthands/assign_or_append.py
|
|
41
|
+
src/j_perm/shorthands/delete.py
|
|
36
42
|
src/j_perm/utils/__init__.py
|
|
37
43
|
src/j_perm/utils/pointers.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|