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.
Files changed (45) hide show
  1. {j_perm-0.2.0.1 → j_perm-0.2.1.1}/PKG-INFO +75 -14
  2. {j_perm-0.2.0.1 → j_perm-0.2.1.1}/README.md +74 -13
  3. {j_perm-0.2.0.1 → j_perm-0.2.1.1}/pyproject.toml +1 -1
  4. {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/__init__.py +8 -4
  5. {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/constructs/eval.py +1 -1
  6. {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/constructs/ref.py +2 -2
  7. {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/engine.py +5 -51
  8. {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/funcs/subtract.py +1 -1
  9. j_perm-0.2.1.1/src/j_perm/normalizer.py +88 -0
  10. {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/ops/__init__.py +1 -0
  11. {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/ops/_assert.py +3 -3
  12. j_perm-0.2.1.1/src/j_perm/ops/_assert_d.py +27 -0
  13. {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/ops/_exec.py +2 -3
  14. j_perm-0.2.1.1/src/j_perm/shorthands/__init__.py +3 -0
  15. j_perm-0.2.1.1/src/j_perm/shorthands/_assert.py +17 -0
  16. j_perm-0.2.1.1/src/j_perm/shorthands/assign_or_append.py +17 -0
  17. j_perm-0.2.1.1/src/j_perm/shorthands/delete.py +10 -0
  18. {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm.egg-info/PKG-INFO +75 -14
  19. {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm.egg-info/SOURCES.txt +6 -0
  20. {j_perm-0.2.0.1 → j_perm-0.2.1.1}/setup.cfg +0 -0
  21. {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/casters/__init__.py +0 -0
  22. {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/casters/_bool.py +0 -0
  23. {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/casters/_float.py +0 -0
  24. {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/casters/_int.py +0 -0
  25. {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/casters/_str.py +0 -0
  26. {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/constructs/__init__.py +0 -0
  27. {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/funcs/__init__.py +0 -0
  28. {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/op_handler.py +0 -0
  29. {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/ops/_if.py +0 -0
  30. {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/ops/copy.py +0 -0
  31. {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/ops/copy_d.py +0 -0
  32. {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/ops/delete.py +0 -0
  33. {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/ops/distinct.py +0 -0
  34. {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/ops/foreach.py +0 -0
  35. {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/ops/replace_root.py +0 -0
  36. {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/ops/set.py +0 -0
  37. {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/ops/update.py +0 -0
  38. {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/schema/__init__.py +0 -0
  39. {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/special_resolver.py +0 -0
  40. {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/subst.py +0 -0
  41. {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/utils/__init__.py +0 -0
  42. {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm/utils/pointers.py +0 -0
  43. {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm.egg-info/dependency_links.txt +0 -0
  44. {j_perm-0.2.0.1 → j_perm-0.2.1.1}/src/j_perm.egg-info/requires.txt +0 -0
  45. {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.0.1
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 expansion (built-in)
681
+ ## Shorthand normalization
680
682
 
681
- Normalization expands shorthand syntax into explicit operation steps:
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
- ### `~delete`
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
- ### `~assert`
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
- ### `field[]` append
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
- ### pointer assignment
759
+ ---
760
+
761
+ #### Pointer assignment shorthand
721
762
 
722
- If a value is a string that starts with `/`, it becomes a `copy`:
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
- { "op": "copy", "from": "/user/fullName", "path": "/name", "ignore_missing": true }
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 expansion (built-in)
668
+ ## Shorthand normalization
667
669
 
668
- Normalization expands shorthand syntax into explicit operation steps:
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
- ### `~delete`
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
- ### `~assert`
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
- ### `field[]` append
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
- ### pointer assignment
746
+ ---
747
+
748
+ #### Pointer assignment shorthand
708
749
 
709
- If a value is a string that starts with `/`, it becomes a `copy`:
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
- { "op": "copy", "from": "/user/fullName", "path": "/name", "ignore_missing": true }
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**.
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
6
6
 
7
7
  [project]
8
8
  name = "j-perm"
9
- version = "0.2.0.1"
9
+ version = "0.2.1.1"
10
10
  description = "json permutation library"
11
11
  authors = [
12
12
  { name = "Roman", email = "kuschanow@gmail.com" },
@@ -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 .subst import JpFuncRegistry, CasterRegistry, TemplateSubstitutor
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
- "JpFuncRegistry",
15
- "CasterRegistry",
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
  ]
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from typing import Mapping, Any
4
4
 
5
- from j_perm.special_resolver import SpecialRegistry
5
+ from ..special_resolver import SpecialRegistry
6
6
 
7
7
 
8
8
  @SpecialRegistry.register("$eval")
@@ -3,8 +3,8 @@ from __future__ import annotations
3
3
  import copy
4
4
  from typing import Mapping, Any
5
5
 
6
- from j_perm.special_resolver import _MISSING, SpecialRegistry
7
- from j_perm.utils.pointers import maybe_slice
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
- - resolve_special_in_actions: enable resolving special constructs inside the actions spec
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._expand_shorthand(item))
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._expand_shorthand(spec)
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()
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from jmespath import functions as _jp_funcs
4
4
 
5
- from j_perm.subst import JpFuncRegistry
5
+ from ..subst import JpFuncRegistry
6
6
 
7
7
 
8
8
  @JpFuncRegistry.register("subtract")
@@ -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
@@ -1,4 +1,5 @@
1
1
  from ._assert import op_assert
2
+ from ._assert_d import op_assert_d
2
3
  from ._exec import op_exec
3
4
  from ._if import op_if
4
5
  from .copy import op_copy
@@ -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(dest, path)
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,3 @@
1
+ from ._assert import rule_assert
2
+ from .assign_or_append import rule_assign_or_append
3
+ from .delete import rule_delete
@@ -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.0.1
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 expansion (built-in)
681
+ ## Shorthand normalization
680
682
 
681
- Normalization expands shorthand syntax into explicit operation steps:
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
- ### `~delete`
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
- ### `~assert`
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
- ### `field[]` append
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
- ### pointer assignment
759
+ ---
760
+
761
+ #### Pointer assignment shorthand
721
762
 
722
- If a value is a string that starts with `/`, it becomes a `copy`:
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
- { "op": "copy", "from": "/user/fullName", "path": "/name", "ignore_missing": true }
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