@voxgig/sdkgen 0.25.0 → 0.28.0

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 (213) hide show
  1. package/bin/voxgig-sdkgen +6 -6
  2. package/dist/action/action.js +1 -2
  3. package/dist/action/action.js.map +1 -1
  4. package/dist/action/feature.js +4 -2
  5. package/dist/action/feature.js.map +1 -1
  6. package/dist/action/target.js +4 -2
  7. package/dist/action/target.js.map +1 -1
  8. package/dist/cmp/Entity.js +2 -1
  9. package/dist/cmp/Entity.js.map +1 -1
  10. package/dist/cmp/Feature.js +11 -9
  11. package/dist/cmp/Feature.js.map +1 -1
  12. package/dist/cmp/FeatureHook.js +2 -1
  13. package/dist/cmp/FeatureHook.js.map +1 -1
  14. package/dist/cmp/Main.js +6 -2
  15. package/dist/cmp/Main.js.map +1 -1
  16. package/dist/cmp/ReadmeEntity.js +2 -1
  17. package/dist/cmp/ReadmeEntity.js.map +1 -1
  18. package/dist/cmp/Test.d.ts +2 -0
  19. package/dist/cmp/Test.js +17 -0
  20. package/dist/cmp/Test.js.map +1 -0
  21. package/dist/sdkgen.d.ts +2 -1
  22. package/dist/sdkgen.js +17 -28
  23. package/dist/sdkgen.js.map +1 -1
  24. package/dist/tsconfig.tsbuildinfo +1 -1
  25. package/dist/types.d.ts +3 -1
  26. package/dist/types.js +4 -0
  27. package/dist/types.js.map +1 -1
  28. package/model/sdkgen.jsonic +9 -9
  29. package/package.json +8 -6
  30. package/project/.sdk/model/feature/log.jsonic +3 -3
  31. package/project/.sdk/model/feature/test.jsonic +8 -3
  32. package/project/.sdk/model/target/go.jsonic +19 -4
  33. package/project/.sdk/model/target/js.jsonic +2 -2
  34. package/project/.sdk/model/target/ts.jsonic +5 -5
  35. package/project/.sdk/src/cmp/go/Config_go.ts +119 -0
  36. package/project/.sdk/src/cmp/go/EntityOperation_go.ts +48 -0
  37. package/project/.sdk/src/cmp/go/Entity_go.ts +67 -0
  38. package/project/.sdk/src/cmp/go/MainEntity_go.ts +22 -0
  39. package/project/.sdk/src/cmp/go/Main_go.ts +209 -0
  40. package/project/.sdk/src/cmp/go/Package_go.ts +89 -0
  41. package/project/.sdk/src/cmp/go/TestDirect_go.ts +373 -0
  42. package/project/.sdk/src/cmp/go/TestEntity_go.ts +341 -0
  43. package/project/.sdk/src/cmp/go/Test_go.ts +37 -0
  44. package/project/.sdk/src/cmp/go/fragment/Entity.fragment.go +146 -0
  45. package/project/.sdk/src/cmp/go/fragment/EntityCreateOp.fragment.go +35 -0
  46. package/project/.sdk/src/cmp/go/fragment/EntityListOp.fragment.go +26 -0
  47. package/project/.sdk/src/cmp/go/fragment/EntityLoadOp.fragment.go +38 -0
  48. package/project/.sdk/src/cmp/go/fragment/EntityRemoveOp.fragment.go +38 -0
  49. package/project/.sdk/src/cmp/go/fragment/EntityUpdateOp.fragment.go +38 -0
  50. package/project/.sdk/src/cmp/go/fragment/Main.fragment.go +250 -0
  51. package/project/.sdk/src/cmp/go/fragment/SdkError.fragment.go +22 -0
  52. package/project/.sdk/src/cmp/go/tsconfig.json +15 -0
  53. package/project/.sdk/src/cmp/go/utility_go.ts +88 -0
  54. package/project/.sdk/src/cmp/js/Main_js.ts +3 -3
  55. package/project/.sdk/src/cmp/js/Quick_js.ts +1 -1
  56. package/project/.sdk/src/cmp/js/fragment/EntityCreateOp.fragment.js +6 -6
  57. package/project/.sdk/src/cmp/js/fragment/EntityListOp.fragment.js +6 -6
  58. package/project/.sdk/src/cmp/js/fragment/EntityLoadOp.fragment.js +6 -6
  59. package/project/.sdk/src/cmp/js/fragment/EntityRemoveOp.fragment.js +6 -6
  60. package/project/.sdk/src/cmp/js/fragment/EntityUpdateOp.fragment.js +6 -6
  61. package/project/.sdk/src/cmp/js/fragment/Main.fragment.js +85 -1
  62. package/project/.sdk/src/cmp/ts/Config_ts.ts +53 -6
  63. package/project/.sdk/src/cmp/ts/EntityOperation_ts.ts +3 -21
  64. package/project/.sdk/src/cmp/ts/Entity_ts.ts +3 -5
  65. package/project/.sdk/src/cmp/ts/Main_ts.ts +23 -13
  66. package/project/.sdk/src/cmp/ts/Package_ts.ts +30 -11
  67. package/project/.sdk/src/cmp/ts/SdkError_ts.ts +42 -0
  68. package/project/.sdk/src/cmp/ts/TestDirect_ts.ts +288 -0
  69. package/project/.sdk/src/cmp/ts/TestEntity_ts.ts +349 -2
  70. package/project/.sdk/src/cmp/ts/TestMain_ts.ts +0 -3
  71. package/project/.sdk/src/cmp/ts/Test_ts.ts +23 -3
  72. package/project/.sdk/src/cmp/ts/fragment/Config.fragment.ts +38 -5
  73. package/project/.sdk/src/cmp/ts/fragment/Direct.test.fragment.ts +30 -0
  74. package/project/.sdk/src/cmp/ts/fragment/Entity.fragment.ts +50 -38
  75. package/project/.sdk/src/cmp/ts/fragment/Entity.test.fragment.ts +9 -7
  76. package/project/.sdk/src/cmp/ts/fragment/EntityCreateOp.fragment.ts +47 -31
  77. package/project/.sdk/src/cmp/ts/fragment/EntityListOp.fragment.ts +49 -31
  78. package/project/.sdk/src/cmp/ts/fragment/EntityLoadOp.fragment.ts +50 -33
  79. package/project/.sdk/src/cmp/ts/fragment/EntityRemoveOp.fragment.ts +51 -32
  80. package/project/.sdk/src/cmp/ts/fragment/EntityUpdateOp.fragment.ts +51 -31
  81. package/project/.sdk/src/cmp/ts/fragment/Main.fragment.ts +166 -34
  82. package/project/.sdk/src/cmp/ts/fragment/SdkError.fragment.ts +25 -0
  83. package/project/.sdk/src/cmp/ts/tsconfig.json +15 -0
  84. package/project/.sdk/src/cmp/ts/utility_ts.ts +55 -1
  85. package/project/.sdk/tm/go/Makefile +10 -0
  86. package/project/.sdk/tm/go/core/context.go +267 -0
  87. package/project/.sdk/tm/go/core/control.go +7 -0
  88. package/project/.sdk/tm/go/core/error.go +25 -0
  89. package/project/.sdk/tm/go/core/helpers.go +33 -0
  90. package/project/.sdk/tm/go/core/operation.go +61 -0
  91. package/project/.sdk/tm/go/core/response.go +55 -0
  92. package/project/.sdk/tm/go/core/result.go +73 -0
  93. package/project/.sdk/tm/go/core/spec.go +97 -0
  94. package/project/.sdk/tm/go/core/target.go +102 -0
  95. package/project/.sdk/tm/go/core/types.go +44 -0
  96. package/project/.sdk/tm/go/core/utility_type.go +54 -0
  97. package/project/.sdk/tm/go/feature/base_feature.go +40 -0
  98. package/project/.sdk/tm/go/feature/test_feature.go +196 -0
  99. package/project/.sdk/tm/go/src/feature/README.md +1 -0
  100. package/project/.sdk/tm/go/src/feature/base/.gitkeep +0 -0
  101. package/project/.sdk/tm/go/src/feature/test/.gitkeep +0 -0
  102. package/project/.sdk/tm/go/test/custom_utility_test.go +80 -0
  103. package/project/.sdk/tm/go/test/exists_test.go +16 -0
  104. package/project/.sdk/tm/go/test/primary_utility_test.go +899 -0
  105. package/project/.sdk/tm/go/test/runner_test.go +428 -0
  106. package/project/.sdk/tm/go/test/struct_runner_test.go +1094 -0
  107. package/project/.sdk/tm/go/test/struct_utility_test.go +1423 -0
  108. package/project/.sdk/tm/go/utility/clean.go +7 -0
  109. package/project/.sdk/tm/go/utility/done.go +20 -0
  110. package/project/.sdk/tm/go/utility/feature_add.go +10 -0
  111. package/project/.sdk/tm/go/utility/feature_hook.go +30 -0
  112. package/project/.sdk/tm/go/utility/feature_init.go +30 -0
  113. package/project/.sdk/tm/go/utility/fetcher.go +102 -0
  114. package/project/.sdk/tm/go/utility/make_context.go +7 -0
  115. package/project/.sdk/tm/go/utility/make_error.go +69 -0
  116. package/project/.sdk/tm/go/utility/make_fetch_def.go +44 -0
  117. package/project/.sdk/tm/go/utility/make_options.go +130 -0
  118. package/project/.sdk/tm/go/utility/make_request.go +59 -0
  119. package/project/.sdk/tm/go/utility/make_response.go +46 -0
  120. package/project/.sdk/tm/go/utility/make_result.go +55 -0
  121. package/project/.sdk/tm/go/utility/make_spec.go +68 -0
  122. package/project/.sdk/tm/go/utility/make_target.go +95 -0
  123. package/project/.sdk/tm/go/utility/make_url.go +41 -0
  124. package/project/.sdk/tm/go/utility/param.go +66 -0
  125. package/project/.sdk/tm/go/utility/prepare_auth.go +40 -0
  126. package/project/.sdk/tm/go/utility/prepare_body.go +14 -0
  127. package/project/.sdk/tm/go/utility/prepare_headers.go +22 -0
  128. package/project/.sdk/tm/go/utility/prepare_method.go +21 -0
  129. package/project/.sdk/tm/go/utility/prepare_params.go +41 -0
  130. package/project/.sdk/tm/go/utility/prepare_path.go +21 -0
  131. package/project/.sdk/tm/go/utility/prepare_query.go +47 -0
  132. package/project/.sdk/tm/go/utility/register.go +39 -0
  133. package/project/.sdk/tm/go/utility/result_basic.go +31 -0
  134. package/project/.sdk/tm/go/utility/result_body.go +17 -0
  135. package/project/.sdk/tm/go/utility/result_headers.go +22 -0
  136. package/project/.sdk/tm/go/utility/struct/go.mod +3 -0
  137. package/project/.sdk/tm/go/utility/struct/voxgigstruct.go +4891 -0
  138. package/project/.sdk/tm/go/utility/transform_request.go +32 -0
  139. package/project/.sdk/tm/go/utility/transform_response.go +45 -0
  140. package/project/.sdk/tm/js/src/feature/log/LogFeature.js +2 -2
  141. package/project/.sdk/tm/ts/src/Context.ts +144 -0
  142. package/project/.sdk/tm/ts/src/Control.ts +20 -0
  143. package/project/.sdk/tm/ts/src/Operation.ts +24 -0
  144. package/project/.sdk/tm/ts/src/Response.ts +26 -0
  145. package/project/.sdk/tm/ts/src/Result.ts +30 -0
  146. package/project/.sdk/tm/ts/src/Spec.ts +40 -0
  147. package/project/.sdk/tm/ts/src/Target.ts +36 -0
  148. package/project/.sdk/tm/ts/src/feature/base/BaseFeature.ts +1 -1
  149. package/project/.sdk/tm/ts/src/feature/log/LogFeature.ts +2 -2
  150. package/project/.sdk/tm/ts/src/feature/test/TestFeature.ts +158 -104
  151. package/project/.sdk/tm/ts/src/types.ts +18 -78
  152. package/project/.sdk/tm/ts/src/utility/CleanUtility.ts +17 -31
  153. package/project/.sdk/tm/ts/src/utility/DoneUtility.ts +3 -4
  154. package/project/.sdk/tm/ts/src/utility/{AddfeatureUtility.ts → FeatureAddUtility.ts} +2 -2
  155. package/project/.sdk/tm/ts/src/utility/{FeaturehookUtility.ts → FeatureHookUtility.ts} +8 -5
  156. package/project/.sdk/tm/ts/src/utility/FeatureInitUtility.ts +15 -0
  157. package/project/.sdk/tm/ts/src/utility/FetcherUtility.ts +20 -2
  158. package/project/.sdk/tm/ts/src/utility/MakeContextUtility.ts +27 -0
  159. package/project/.sdk/tm/ts/src/utility/{ErrorUtility.ts → MakeErrorUtility.ts} +10 -6
  160. package/project/.sdk/tm/ts/src/utility/MakeFetchDefUtility.ts +46 -0
  161. package/project/.sdk/tm/ts/src/utility/{OptionsUtility.ts → MakeOptionsUtility.ts} +32 -7
  162. package/project/.sdk/tm/ts/src/utility/MakeRequestUtility.ts +66 -0
  163. package/project/.sdk/tm/ts/src/utility/MakeResponseUtility.ts +61 -0
  164. package/project/.sdk/tm/ts/src/utility/MakeResultUtility.ts +56 -0
  165. package/project/.sdk/tm/ts/src/utility/MakeSpecUtility.ts +61 -0
  166. package/project/.sdk/tm/ts/src/utility/MakeTargetUtility.ts +76 -0
  167. package/project/.sdk/tm/ts/src/utility/MakeUrlUtility.ts +61 -0
  168. package/project/.sdk/tm/ts/src/utility/{FindparamUtility.ts → ParamUtility.ts} +28 -8
  169. package/project/.sdk/tm/ts/src/utility/{AuthUtility.ts → PrepareAuthUtility.ts} +10 -4
  170. package/project/.sdk/tm/ts/src/utility/PrepareBodyUtility.ts +32 -0
  171. package/project/.sdk/tm/ts/src/utility/{HeadersUtility.ts → PrepareHeadersUtility.ts} +5 -7
  172. package/project/.sdk/tm/ts/src/utility/{MethodUtility.ts → PrepareMethodUtility.ts} +6 -6
  173. package/project/.sdk/tm/ts/src/utility/PrepareParamsUtility.ts +37 -0
  174. package/project/.sdk/tm/ts/src/utility/PreparePathUtility.ts +17 -0
  175. package/project/.sdk/tm/ts/src/utility/PrepareQueryUtility.ts +30 -0
  176. package/project/.sdk/tm/ts/src/utility/{ResbasicUtility.ts → ResultBasicUtility.ts} +12 -7
  177. package/project/.sdk/tm/ts/src/utility/ResultBodyUtility.ts +22 -0
  178. package/project/.sdk/tm/ts/src/utility/ResultHeadersUtility.ts +26 -0
  179. package/project/.sdk/tm/ts/src/utility/StructUtility.ts +1113 -525
  180. package/project/.sdk/tm/ts/src/utility/{ReqformUtility.ts → TransformRequestUtility.ts} +9 -7
  181. package/project/.sdk/tm/ts/src/utility/{ResformUtility.ts → TransformResponseUtility.ts} +11 -8
  182. package/project/.sdk/tm/ts/src/utility/Utility.ts +52 -65
  183. package/project/.sdk/tm/ts/test/exists.test.ts +0 -1
  184. package/project/.sdk/tm/ts/test/runner.ts +36 -13
  185. package/project/.sdk/tm/ts/test/utility/Custom.test.ts +30 -29
  186. package/project/.sdk/tm/ts/test/utility/PrimaryUtility.test.ts +385 -168
  187. package/project/.sdk/tm/ts/test/utility/StructUtility.test.ts +433 -189
  188. package/src/action/action.ts +1 -2
  189. package/src/action/feature.ts +7 -2
  190. package/src/action/target.ts +7 -7
  191. package/src/cmp/Entity.ts +7 -1
  192. package/src/cmp/Feature.ts +11 -9
  193. package/src/cmp/FeatureHook.ts +6 -1
  194. package/src/cmp/Main.ts +12 -2
  195. package/src/cmp/ReadmeEntity.ts +6 -1
  196. package/src/cmp/Test.ts +31 -0
  197. package/src/sdkgen.ts +19 -34
  198. package/src/types.ts +10 -1
  199. package/project/.sdk/src/cmp/ts/EntityTest_ts.ts +0 -180
  200. package/project/.sdk/tm/ts/src/utility/BodyUtility.ts +0 -29
  201. package/project/.sdk/tm/ts/src/utility/ContextUtility.ts +0 -67
  202. package/project/.sdk/tm/ts/src/utility/FullurlUtility.ts +0 -46
  203. package/project/.sdk/tm/ts/src/utility/InitfeatureUtility.ts +0 -13
  204. package/project/.sdk/tm/ts/src/utility/JoinurlUtility.ts +0 -15
  205. package/project/.sdk/tm/ts/src/utility/OperatorUtility.ts +0 -90
  206. package/project/.sdk/tm/ts/src/utility/ParamsUtility.ts +0 -37
  207. package/project/.sdk/tm/ts/src/utility/QueryUtility.ts +0 -27
  208. package/project/.sdk/tm/ts/src/utility/RequestUtility.ts +0 -66
  209. package/project/.sdk/tm/ts/src/utility/ResbodyUtility.ts +0 -19
  210. package/project/.sdk/tm/ts/src/utility/ResheadersUtility.ts +0 -23
  211. package/project/.sdk/tm/ts/src/utility/ResponseUtility.ts +0 -30
  212. package/project/.sdk/tm/ts/src/utility/ResultUtility.ts +0 -36
  213. package/project/.sdk/tm/ts/src/utility/SpecUtility.ts +0 -61
@@ -0,0 +1,4891 @@
1
+ /* Copyright (c) 2025 Voxgig Ltd. MIT LICENSE. */
2
+
3
+ /* Voxgig Struct
4
+ * =============
5
+ *
6
+ * Utility functions to manipulate in-memory JSON-like data
7
+ * structures. These structures assumed to be composed of nested
8
+ * "nodes", where a node is a list or map, and has named or indexed
9
+ * fields. The general design principle is "by-example". Transform
10
+ * specifications mirror the desired output. This implementation is
11
+ * designed for porting to multiple language, and to be tolerant of
12
+ * undefined values.
13
+ *
14
+ * Main utilities
15
+ * - getpath: get the value at a key path deep inside an object.
16
+ * - merge: merge multiple nodes, overriding values in earlier nodes.
17
+ * - walk: walk a node tree, applying a function at each node and leaf.
18
+ * - inject: inject values from a data store into a new data structure.
19
+ * - transform: transform a data structure to an example structure.
20
+ * - validate: valiate a data structure against a shape specification.
21
+ *
22
+ * Minor utilities
23
+ * - isnode, islist, ismap, iskey, isfunc: identify value kinds.
24
+ * - isempty: undefined values, or empty nodes.
25
+ * - keysof: sorted list of node keys (ascending).
26
+ * - haskey: true if key value is defined.
27
+ * - clone: create a copy of a JSON-like data structure.
28
+ * - items: list entries of a map or list as [key, value] pairs.
29
+ * - getprop: safely get a property value by key.
30
+ * - setprop: safely set a property value by key.
31
+ * - stringify: human-friendly string version of a value.
32
+ * - escre: escape a regular expresion string.
33
+ * - escurl: escape a url.
34
+ * - joinurl: join parts of a url, merging forward slashes.
35
+ *
36
+ * This set of functions and supporting utilities is designed to work
37
+ * uniformly across many languages, meaning that some code that may be
38
+ * functionally redundant in specific languages is still retained to
39
+ * keep the code human comparable.
40
+ *
41
+ * NOTE: In this code JSON nulls are in general *not* considered the
42
+ * same as the undefined value in the given language. However most
43
+ * JSON parsers do use the undefined value to represent JSON
44
+ * null. This is ambiguous as JSON null is a separate value, not an
45
+ * undefined value. You should convert such values to a special value
46
+ * to represent JSON null, if this ambiguity creates issues
47
+ * (thankfully in most APIs, JSON nulls are not used). For example,
48
+ * the unit tests use the string "__NULL__" where necessary.
49
+ *
50
+ */
51
+
52
+ package voxgigstruct
53
+
54
+ import (
55
+ "encoding/json"
56
+ "fmt"
57
+ "math"
58
+ "math/bits"
59
+ "net/url"
60
+ "reflect"
61
+ "regexp"
62
+ "sort"
63
+ "strconv"
64
+ "strings"
65
+ "time"
66
+ )
67
+
68
+ const Version = "0.1.0"
69
+
70
+ // String constants are explicitly defined.
71
+
72
+ const (
73
+ // Mode value for inject step (bitfield).
74
+ M_KEYPRE = 1
75
+ M_KEYPOST = 2
76
+ M_VAL = 4
77
+
78
+ // Special keys.
79
+ S_DKEY = "$KEY"
80
+ S_DMETA = "`$META`"
81
+ S_DTOP = "$TOP"
82
+ S_DERRS = "$ERRS"
83
+
84
+ // General strings.
85
+ S_any = "any"
86
+ S_noval = "noval"
87
+ S_array = "array"
88
+ S_list = "list"
89
+ S_map = "map"
90
+ S_boolean = "boolean"
91
+ S_decimal = "decimal"
92
+ S_integer = "integer"
93
+ S_function = "function"
94
+ S_symbol = "symbol"
95
+ S_instance = "instance"
96
+ S_number = "number"
97
+ S_object = "object"
98
+ S_string = "string"
99
+ S_scalar = "scalar"
100
+ S_node = "node"
101
+ S_null = "null"
102
+ S_key = "key"
103
+ S_parent = "parent"
104
+ S_MT = ""
105
+ S_SP = " "
106
+ S_BT = "`"
107
+ S_DS = "$"
108
+ S_DT = "."
109
+ S_CN = ":"
110
+ S_KEY = "KEY"
111
+ S_base = "base"
112
+ S_BEXACT = "`$EXACT`"
113
+ S_BOPEN = "`$OPEN`"
114
+ S_BKEY = "`$KEY`"
115
+ S_BANNO = "`$ANNO`"
116
+ S_BVAL = "`$VAL`"
117
+ S_DSPEC = "$SPEC"
118
+ S_VIZ = ": "
119
+ )
120
+
121
+ // Type bits - using bit positions from 31 downward, matching the TS implementation.
122
+ const (
123
+ T_any = (1 << 31) - 1 // All bits set.
124
+ T_noval = 1 << 30 // Absent value (no value at all). NOT a scalar.
125
+ T_boolean = 1 << 29
126
+ T_decimal = 1 << 28
127
+ T_integer = 1 << 27
128
+ T_number = 1 << 26
129
+ T_string = 1 << 25
130
+ T_function = 1 << 24
131
+ T_symbol = 1 << 23
132
+ T_null = 1 << 22
133
+ // 7 bits reserved
134
+ T_list = 1 << 14
135
+ T_map = 1 << 13
136
+ T_instance = 1 << 12
137
+ // 4 bits reserved
138
+ T_scalar = 1 << 7
139
+ T_node = 1 << 6
140
+ )
141
+
142
+ // TYPENAME maps bit position (via leading zeros count) to type name string.
143
+ var TYPENAME = [...]string{
144
+ S_any,
145
+ S_noval,
146
+ S_boolean,
147
+ S_decimal,
148
+ S_integer,
149
+ S_number,
150
+ S_string,
151
+ S_function,
152
+ S_symbol,
153
+ S_null,
154
+ "", "", "",
155
+ "", "", "", "",
156
+ S_list,
157
+ S_map,
158
+ S_instance,
159
+ "", "", "", "",
160
+ S_scalar,
161
+ S_node,
162
+ }
163
+
164
+ // Sentinel values for control flow in inject/transform.
165
+ type _sentinel struct{ name string }
166
+
167
+ var SKIP = &_sentinel{"SKIP"}
168
+ var DELETE = &_sentinel{"DELETE"}
169
+
170
+ // Regex matching integer keys (including negative).
171
+ var reIntegerKey = regexp.MustCompile(`^[-0-9]+$`)
172
+
173
+ // Meta path syntax regex: matches patterns like "q0$=x1" or "q0$~x1"
174
+ var reMetaPath = regexp.MustCompile(`^([^$]+)\$([=~])(.+)$`)
175
+
176
+ // The standard undefined value for this language.
177
+ // NOTE: `nil` must be used directly.
178
+
179
+ // Keys are strings for maps, or integers for lists.
180
+ type PropKey any
181
+
182
+ // Handle value injections using backtick escape sequences:
183
+ // - `a.b.c`: insert value at {a:{b:{c:1}}}
184
+ // - `$FOO`: apply transform FOO
185
+ type Injector func(
186
+ inj *Injection, // Injection inj.
187
+ val any, // Injection value specification.
188
+ ref *string, // Original injection reference string.
189
+ store any, // Current source root value.
190
+ ) any
191
+
192
+ // Injection inj used for recursive injection into JSON-like data structures.
193
+ type Injection struct {
194
+ Mode int // Injection mode: M_KEYPRE, M_VAL, M_KEYPOST (bitfield).
195
+ Full bool // Transform escape was full key name.
196
+ KeyI int // Index of parent key in list of parent keys.
197
+ Keys *ListRef[string] // List of parent keys.
198
+ Key string // Current parent key.
199
+ Val any // Current child value.
200
+ Parent any // Current parent (in transform specification).
201
+ Path *ListRef[string] // Path to current node.
202
+ Nodes *ListRef[any] // Stack of ancestor nodes.
203
+ Handler Injector // Custom handler for injections.
204
+ Errs *ListRef[any] // Error collector.
205
+ Meta map[string]any // Custom meta data.
206
+ Dparent any // Current data parent node (contains current data value).
207
+ Dpath []string // Current data value path.
208
+ Base string // Base key for data in store, if any.
209
+ Modify Modify // Modify injection output.
210
+ Prior *Injection // Parent (aka prior) injection.
211
+ Extra any // Extra data.
212
+ }
213
+
214
+ // Apply a custom modification to injections.
215
+ type Modify func(
216
+ val any, // Value.
217
+ key any, // Value key, if any,
218
+ parent any, // Parent node, if any.
219
+ inj *Injection, // Injection inj, if any.
220
+ store any, // Store, if any
221
+ )
222
+
223
+ // Create a child injection inj sharing errs/meta/modify/handler with parent.
224
+ func (inj *Injection) child(keyI int, keys []string) *Injection {
225
+ key := StrKey(keys[keyI])
226
+ val := inj.Val
227
+
228
+ childPath := make([]string, len(inj.Path.List))
229
+ copy(childPath, inj.Path.List)
230
+ childPath = append(childPath, key)
231
+
232
+ childNodes := make([]any, len(inj.Nodes.List))
233
+ copy(childNodes, inj.Nodes.List)
234
+ childNodes = append(childNodes, val)
235
+
236
+ childDpath := make([]string, len(inj.Dpath))
237
+ copy(childDpath, inj.Dpath)
238
+
239
+ cinj := &Injection{
240
+ Mode: inj.Mode,
241
+ Full: false,
242
+ KeyI: keyI,
243
+ Keys: &ListRef[string]{List: keys},
244
+ Key: key,
245
+ Val: GetProp(val, key),
246
+ Parent: val,
247
+ Path: &ListRef[string]{List: childPath},
248
+ Nodes: &ListRef[any]{List: childNodes},
249
+ Handler: inj.Handler,
250
+ Modify: inj.Modify,
251
+ Base: inj.Base,
252
+ Meta: inj.Meta,
253
+ Errs: inj.Errs,
254
+ Prior: inj,
255
+ Dpath: childDpath,
256
+ Dparent: inj.Dparent,
257
+ }
258
+
259
+ return cinj
260
+ }
261
+
262
+ // Set value in parent or ancestor node.
263
+ func (inj *Injection) setval(val any, ancestor ...int) any {
264
+ anc := 0
265
+ if len(ancestor) > 0 {
266
+ anc = ancestor[0]
267
+ }
268
+
269
+ if anc < 2 {
270
+ if val == nil {
271
+ inj.Parent = DelProp(inj.Parent, inj.Key)
272
+ } else {
273
+ SetProp(inj.Parent, inj.Key, val)
274
+ }
275
+ return inj.Parent
276
+ } else {
277
+ aval := GetElem(inj.Nodes.List, 0-anc)
278
+ akey := GetElem(inj.Path.List, 0-anc)
279
+ if val == nil {
280
+ DelProp(aval, akey)
281
+ } else {
282
+ SetProp(aval, akey, val)
283
+ }
284
+ return aval
285
+ }
286
+ }
287
+
288
+ // Resolve current node in store for local paths.
289
+ func (inj *Injection) descend() any {
290
+ if inj.Meta == nil {
291
+ inj.Meta = map[string]any{}
292
+ }
293
+
294
+ // Increment depth counter
295
+ d, _ := inj.Meta["__d"].(int)
296
+ inj.Meta["__d"] = d + 1
297
+
298
+ parentkey := ""
299
+ if len(inj.Path.List) >= 2 {
300
+ parentkey = inj.Path.List[len(inj.Path.List)-2]
301
+ }
302
+
303
+ if inj.Dparent == nil {
304
+ // Even if there's no data, dpath should continue to match path
305
+ if len(inj.Dpath) > 1 {
306
+ inj.Dpath = append(inj.Dpath, parentkey)
307
+ }
308
+ } else {
309
+ if parentkey != "" {
310
+ inj.Dparent = GetProp(inj.Dparent, parentkey)
311
+
312
+ lastpart := ""
313
+ if len(inj.Dpath) > 0 {
314
+ lastpart = inj.Dpath[len(inj.Dpath)-1]
315
+ }
316
+ if lastpart == "$:"+parentkey {
317
+ inj.Dpath = inj.Dpath[:len(inj.Dpath)-1]
318
+ } else {
319
+ inj.Dpath = append(inj.Dpath, parentkey)
320
+ }
321
+ }
322
+ }
323
+
324
+ return inj.Dparent
325
+ }
326
+
327
+ // String returns a human-readable representation of the Injection inj.
328
+ func (inj *Injection) String(prefix ...string) string {
329
+ pfx := ""
330
+ if len(prefix) > 0 && prefix[0] != "" {
331
+ pfx = "/" + prefix[0]
332
+ }
333
+ fullStr := ""
334
+ if inj.Full {
335
+ fullStr = "/full"
336
+ }
337
+
338
+ pathStr := ""
339
+ if inj.Path != nil {
340
+ pathStr = Pathify(inj.Path.List, 1)
341
+ }
342
+
343
+ keysStr := ""
344
+ if inj.Keys != nil {
345
+ keysStr = fmt.Sprintf("%v", inj.Keys.List)
346
+ }
347
+
348
+ rootVal := ""
349
+ if inj.Nodes != nil && len(inj.Nodes.List) > 0 {
350
+ top := inj.Nodes.List[0]
351
+ if topMap, ok := top.(map[string]any); ok {
352
+ rootVal = Stringify(topMap[S_DTOP], -1, 1)
353
+ }
354
+ }
355
+
356
+ return "INJ" + pfx + ":" +
357
+ Pad(pathStr) +
358
+ MODENAME[inj.Mode] + fullStr + ":" +
359
+ "key=" + fmt.Sprintf("%d", inj.KeyI) + "/" + inj.Key + "/" + keysStr +
360
+ " p=" + Stringify(inj.Parent, -1, 1) +
361
+ " m=" + Stringify(inj.Meta, -1, 1) +
362
+ " d/" + Pathify(inj.Dpath, 1) + "=" + Stringify(inj.Dparent, -1, 1) +
363
+ " r=" + rootVal
364
+ }
365
+
366
+ // Function applied to each node and leaf when walking a node structure depth first.
367
+ type WalkApply func(
368
+ // Map keys are strings, list keys are numbers, top key is nil
369
+ key *string,
370
+ val any,
371
+ parent any,
372
+ path []string,
373
+ ) any
374
+
375
+ // Value is a node - defined, and a map (hash) or list (array).
376
+ func IsNode(val any) bool {
377
+ if val == nil {
378
+ return false
379
+ }
380
+
381
+ return IsMap(val) || IsList(val)
382
+ }
383
+
384
+ // Value is a defined map (hash) with string keys.
385
+ func IsMap(val any) bool {
386
+ if val == nil {
387
+ return false
388
+ }
389
+ _, ok := val.(map[string]any)
390
+ return ok
391
+ }
392
+
393
+ // Value is a defined list (array) with integer keys (indexes).
394
+ func IsList(val any) bool {
395
+ if val == nil {
396
+ return false
397
+ }
398
+ if _, ok := val.(*ListRef[any]); ok {
399
+ return true
400
+ }
401
+ rv := reflect.ValueOf(val)
402
+ kind := rv.Kind()
403
+ return kind == reflect.Slice || kind == reflect.Array
404
+ }
405
+
406
+ // Value is a defined string (non-empty) or integer key.
407
+ func IsKey(val any) bool {
408
+ switch k := val.(type) {
409
+ case string:
410
+ return k != S_MT
411
+ case int, float64, int8, int16, int32, int64:
412
+ return true
413
+ case uint8, uint16, uint32, uint64, uint, float32:
414
+ return true
415
+ default:
416
+ return false
417
+ }
418
+ }
419
+
420
+ // Check for an "empty" value - nil, empty string, array, object.
421
+ func IsEmpty(val any) bool {
422
+ if val == nil {
423
+ return true
424
+ }
425
+ switch vv := val.(type) {
426
+ case string:
427
+ return vv == S_MT
428
+ case *ListRef[any]:
429
+ return len(vv.List) == 0
430
+ case []any:
431
+ return len(vv) == 0
432
+ case map[string]any:
433
+ return len(vv) == 0
434
+ }
435
+ return false
436
+ }
437
+
438
+ // Value is a function.
439
+ func IsFunc(val any) bool {
440
+ return reflect.ValueOf(val).Kind() == reflect.Func
441
+ }
442
+
443
+ // Get a defined value. Returns alt if val is nil.
444
+ func GetDef(val any, alt any) any {
445
+ if nil == val {
446
+ return alt
447
+ }
448
+ return val
449
+ }
450
+
451
+ // Determine the type of a value as a bitset.
452
+ // Use bitwise AND to test: 0 < (T_string & Typify(val))
453
+ // Use Typename to get the string name.
454
+ func Typify(value any) int {
455
+ if value == nil {
456
+ return T_scalar | T_null
457
+ }
458
+
459
+ if _, ok := value.(*ListRef[any]); ok {
460
+ return T_node | T_list
461
+ }
462
+
463
+ val := reflect.ValueOf(value)
464
+ if !val.IsValid() {
465
+ return T_scalar | T_null
466
+ }
467
+
468
+ switch val.Type().Kind() {
469
+ case reflect.Bool:
470
+ return T_scalar | T_boolean
471
+
472
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
473
+ return T_scalar | T_number | T_integer
474
+
475
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
476
+ return T_scalar | T_number | T_integer
477
+
478
+ case reflect.Float32, reflect.Float64:
479
+ f, err := _toFloat64(value)
480
+ if err == nil && f == math.Trunc(f) && !math.IsNaN(f) && !math.IsInf(f, 0) {
481
+ return T_scalar | T_number | T_integer
482
+ }
483
+ if err == nil && math.IsNaN(f) {
484
+ return T_noval
485
+ }
486
+ return T_scalar | T_number | T_decimal
487
+
488
+ case reflect.String:
489
+ return T_scalar | T_string
490
+
491
+ case reflect.Func:
492
+ return T_scalar | T_function
493
+
494
+ case reflect.Slice, reflect.Array:
495
+ return T_node | T_list
496
+
497
+ case reflect.Map:
498
+ return T_node | T_map
499
+
500
+ default:
501
+ return T_node | T_map
502
+ }
503
+ }
504
+
505
+ // Convert a type bitset to its string name using leading zeros count.
506
+ func Typename(t int) string {
507
+ if t <= 0 {
508
+ return S_any
509
+ }
510
+ idx := bits.LeadingZeros32(uint32(t))
511
+ if idx < len(TYPENAME) && TYPENAME[idx] != "" {
512
+ return TYPENAME[idx]
513
+ }
514
+ return S_any
515
+ }
516
+
517
+ // The integer size of the value. For lists and maps, the number of entries.
518
+ // For strings, the length. For numbers, the integer part.
519
+ // For booleans, true is 1 and false is 0. For all other values, 0.
520
+ func Size(val any) int {
521
+ if IsList(val) {
522
+ list, ok := _asList(val)
523
+ if ok {
524
+ return len(list)
525
+ }
526
+ return len(_listify(val))
527
+ } else if IsMap(val) {
528
+ return len(val.(map[string]any))
529
+ }
530
+
531
+ switch v := val.(type) {
532
+ case string:
533
+ return len(v)
534
+ case bool:
535
+ if v {
536
+ return 1
537
+ }
538
+ return 0
539
+ default:
540
+ f, err := _toFloat64(val)
541
+ if err == nil {
542
+ return int(math.Floor(f))
543
+ }
544
+ return 0
545
+ }
546
+ }
547
+
548
+ // Extract part of a list or string into a new value, from the start
549
+ // point to the end point. If no end is specified, extract to the
550
+ // full length. Negative arguments count from the end. For numbers,
551
+ // perform min and max bounding (start inclusive, end exclusive).
552
+ func Slice(val any, args ...any) any {
553
+ var startP, endP *int
554
+ var mutate bool
555
+
556
+ if len(args) > 0 && args[0] != nil {
557
+ if f, err := _toFloat64(args[0]); err == nil {
558
+ i := int(f)
559
+ startP = &i
560
+ }
561
+ }
562
+ if len(args) > 1 && args[1] != nil {
563
+ if f, err := _toFloat64(args[1]); err == nil {
564
+ i := int(f)
565
+ endP = &i
566
+ }
567
+ }
568
+ if len(args) > 2 {
569
+ if b, ok := args[2].(bool); ok {
570
+ mutate = b
571
+ }
572
+ }
573
+
574
+ // Number case: clamp between start (inclusive) and end-1 (exclusive->inclusive).
575
+ if _, ok := val.(string); !ok && !IsNode(val) {
576
+ if f, err := _toFloat64(val); err == nil {
577
+ start := math.MinInt64
578
+ if startP != nil {
579
+ start = *startP
580
+ }
581
+ end := math.MaxInt64
582
+ if endP != nil {
583
+ end = *endP - 1
584
+ }
585
+ result := int(math.Min(math.Max(f, float64(start)), float64(end)))
586
+ return result
587
+ }
588
+ }
589
+
590
+ vlen := Size(val)
591
+
592
+ if endP != nil && startP == nil {
593
+ zero := 0
594
+ startP = &zero
595
+ }
596
+
597
+ if startP == nil {
598
+ return val
599
+ }
600
+
601
+ start := *startP
602
+ end := vlen
603
+
604
+ if start < 0 {
605
+ end = vlen + start
606
+ if end < 0 {
607
+ end = 0
608
+ }
609
+ start = 0
610
+ } else if endP != nil {
611
+ end = *endP
612
+ if end < 0 {
613
+ end = vlen + end
614
+ if end < 0 {
615
+ end = 0
616
+ }
617
+ } else if vlen < end {
618
+ end = vlen
619
+ }
620
+ }
621
+
622
+ if vlen < start {
623
+ start = vlen
624
+ }
625
+
626
+ if start >= 0 && start <= end && end <= vlen {
627
+ if IsList(val) {
628
+ list, _ := _asList(val)
629
+ if list == nil {
630
+ list = _listify(val)
631
+ }
632
+ if mutate {
633
+ for i, j := 0, start; j < end; i, j = i+1, j+1 {
634
+ list[i] = list[j]
635
+ }
636
+ list = list[:end-start]
637
+ if lr, ok := val.(*ListRef[any]); ok {
638
+ lr.List = list
639
+ return lr
640
+ }
641
+ return list
642
+ }
643
+ return append([]any{}, list[start:end]...)
644
+ } else if s, ok := val.(string); ok {
645
+ return s[start:end]
646
+ }
647
+ } else {
648
+ if IsList(val) {
649
+ return []any{}
650
+ } else if _, ok := val.(string); ok {
651
+ return S_MT
652
+ }
653
+ }
654
+
655
+ return val
656
+ }
657
+
658
+ // String padding. Positive padding right-pads, negative left-pads.
659
+ // Default padding is 44, default pad character is space.
660
+ func Pad(str any, args ...any) string {
661
+ var s string
662
+ if ss, ok := str.(string); ok {
663
+ s = ss
664
+ } else {
665
+ s = Stringify(str)
666
+ }
667
+
668
+ padding := 44
669
+ if len(args) > 0 && args[0] != nil {
670
+ if f, err := _toFloat64(args[0]); err == nil {
671
+ padding = int(f)
672
+ }
673
+ }
674
+
675
+ padchar := S_SP
676
+ if len(args) > 1 && args[1] != nil {
677
+ if pc, ok := args[1].(string); ok && len(pc) > 0 {
678
+ padchar = string(pc[0])
679
+ }
680
+ }
681
+
682
+ if padding >= 0 {
683
+ for len(s) < padding {
684
+ s += padchar
685
+ }
686
+ } else {
687
+ target := -padding
688
+ for len(s) < target {
689
+ s = padchar + s
690
+ }
691
+ }
692
+
693
+ return s
694
+ }
695
+
696
+ // Get a list element. The key should be an integer, or a string
697
+ // that parses to an integer. Negative integers count from the end.
698
+ func GetElem(val any, key any, alts ...any) any {
699
+ var alt any
700
+ if len(alts) > 0 {
701
+ alt = alts[0]
702
+ }
703
+
704
+ if nil == val || nil == key {
705
+ return alt
706
+ }
707
+
708
+ var out any
709
+
710
+ if IsList(val) {
711
+ ks := StrKey(key)
712
+ if reIntegerKey.MatchString(ks) {
713
+ nkey, err := strconv.Atoi(ks)
714
+ if err == nil {
715
+ list, ok := _asList(val)
716
+ if !ok {
717
+ list = _listify(val)
718
+ }
719
+ if nkey < 0 {
720
+ nkey = len(list) + nkey
721
+ }
722
+ if nkey >= 0 && nkey < len(list) {
723
+ out = list[nkey]
724
+ }
725
+ }
726
+ }
727
+ }
728
+
729
+ if nil == out {
730
+ if 0 < (T_function & Typify(alt)) {
731
+ fn := reflect.ValueOf(alt)
732
+ results := fn.Call(nil)
733
+ if len(results) > 0 {
734
+ return results[0].Interface()
735
+ }
736
+ return nil
737
+ }
738
+ return alt
739
+ }
740
+
741
+ return out
742
+ }
743
+
744
+ // Safely get a property of a node. Nil arguments return nil.
745
+ // If the key is not found, return the alternative value, if any.
746
+ func GetProp(val any, key any, alts ...any) any {
747
+ var out any
748
+ var alt any
749
+
750
+ if len(alts) > 0 {
751
+ alt = alts[0]
752
+ }
753
+
754
+ if nil == val || nil == key {
755
+ return alt
756
+ }
757
+
758
+ if IsMap(val) {
759
+ ks, ok := key.(string)
760
+ if !ok {
761
+ ks = StrKey(key)
762
+ }
763
+
764
+ v := val.(map[string]any)
765
+ res, has := v[ks]
766
+ if has {
767
+ out = res
768
+ }
769
+
770
+ } else if IsList(val) {
771
+ ki, ok := key.(int)
772
+ if !ok {
773
+ switch kf := key.(type) {
774
+ case float64:
775
+ ki = int(kf)
776
+
777
+ case string:
778
+ ki = -1
779
+ ski, err := strconv.Atoi(key.(string))
780
+ if nil == err {
781
+ ki = ski
782
+ }
783
+ }
784
+ }
785
+
786
+ if lr, isLR := val.(*ListRef[any]); isLR {
787
+ if 0 <= ki && ki < len(lr.List) {
788
+ out = lr.List[ki]
789
+ }
790
+ } else {
791
+ v, ok := val.([]any)
792
+
793
+ if !ok {
794
+ rv := reflect.ValueOf(val)
795
+ if rv.Kind() == reflect.Slice && 0 <= ki && ki < rv.Len() {
796
+ out = rv.Index(ki).Interface()
797
+ }
798
+
799
+ } else {
800
+ if 0 <= ki && ki < len(v) {
801
+ out = v[ki]
802
+ }
803
+ }
804
+ }
805
+
806
+ } else {
807
+ valRef := reflect.ValueOf(val)
808
+ if valRef.Kind() == reflect.Ptr {
809
+ valRef = valRef.Elem()
810
+ }
811
+
812
+ if valRef.Kind() == reflect.Struct {
813
+ ks, ok := key.(string)
814
+ if !ok {
815
+ ks = StrKey(key)
816
+ }
817
+
818
+ field := valRef.FieldByName(ks)
819
+ if field.IsValid() {
820
+ out = field.Interface()
821
+ }
822
+ }
823
+ }
824
+
825
+ if nil == out {
826
+ return alt
827
+ }
828
+
829
+ return out
830
+ }
831
+
832
+ // Sorted keys of a map, or indexes of a list.
833
+ func KeysOf(val any) []string {
834
+ if IsMap(val) {
835
+ m := val.(map[string]any)
836
+
837
+ keys := make([]string, 0, len(m))
838
+ for k := range m {
839
+ keys = append(keys, k)
840
+ }
841
+
842
+ sort.Strings(keys)
843
+
844
+ return keys
845
+
846
+ } else if IsList(val) {
847
+ list, _ := _asList(val)
848
+ if list == nil {
849
+ list = _listify(val)
850
+ }
851
+ keys := make([]string, len(list))
852
+ for i := range list {
853
+ keys[i] = StrKey(i)
854
+ }
855
+ return keys
856
+ }
857
+
858
+ return make([]string, 0)
859
+ }
860
+
861
+
862
+ // Value of property with name key in node val is defined.
863
+ func HasKey(val any, key any) bool {
864
+ return nil != GetProp(val, key)
865
+ }
866
+
867
+
868
+ // List the sorted keys of a map or list as an array of tuples of the form [key, value].
869
+ func Items(val any) [][2]any {
870
+ if IsMap(val) {
871
+ m := val.(map[string]any)
872
+ out := make([][2]any, 0, len(m))
873
+
874
+ keys := KeysOf(val)
875
+ // keys := make([]string, 0, len(m))
876
+ // for k := range m {
877
+ // keys = append(keys, k)
878
+ // }
879
+ // sort.Strings(keys)
880
+
881
+ for _, k := range keys {
882
+ out = append(out, [2]any{k, m[k]})
883
+ }
884
+ return out
885
+
886
+ } else if IsList(val) {
887
+ list, _ := _asList(val)
888
+ if list == nil {
889
+ list = _listify(val)
890
+ }
891
+ out := make([][2]any, 0, len(list))
892
+ for i, v := range list {
893
+ out = append(out, [2]any{strconv.Itoa(i), v})
894
+ }
895
+ return out
896
+ }
897
+
898
+ return make([][2]any, 0, 0)
899
+ }
900
+
901
+ // List items with an optional apply callback that maps over each [key, value] tuple.
902
+ func ItemsApply(val any, apply func([2]any) any) []any {
903
+ items := Items(val)
904
+ out := make([]any, len(items))
905
+ for i, item := range items {
906
+ out[i] = apply(item)
907
+ }
908
+ return out
909
+ }
910
+
911
+ // Flatten a nested list to a given depth (default 1).
912
+ // Non-list inputs are returned as-is.
913
+ func Flatten(list any, depths ...int) any {
914
+ if !IsList(list) {
915
+ return list
916
+ }
917
+
918
+ depth := 1
919
+ if len(depths) > 0 {
920
+ depth = depths[0]
921
+ }
922
+
923
+ arr, ok := _asList(list)
924
+ if !ok {
925
+ arr = _listify(list)
926
+ }
927
+
928
+ return _flattenDepth(arr, depth)
929
+ }
930
+
931
+ func _flattenDepth(arr []any, depth int) []any {
932
+ result := make([]any, 0)
933
+ for _, item := range arr {
934
+ if depth > 0 {
935
+ if sub, ok := _asList(item); ok {
936
+ result = append(result, _flattenDepth(sub, depth-1)...)
937
+ continue
938
+ }
939
+ }
940
+ result = append(result, item)
941
+ }
942
+ return result
943
+ }
944
+
945
+ // Filter item values using check function.
946
+ // Returns values where the check function returns true.
947
+ func Filter(val any, check func([2]any) bool) []any {
948
+ all := Items(val)
949
+ out := make([]any, 0)
950
+ for _, item := range all {
951
+ if check(item) {
952
+ out = append(out, item[1])
953
+ }
954
+ }
955
+ return out
956
+ }
957
+
958
+
959
+ // Escape regular expression.
960
+ func EscRe(s string) string {
961
+ if s == "" {
962
+ return ""
963
+ }
964
+ re := regexp.MustCompile(`[.*+?^${}()|\[\]\\]`)
965
+ return re.ReplaceAllString(s, `\${0}`)
966
+ }
967
+
968
+ // Escape URLs.
969
+ func EscUrl(s string) string {
970
+ return url.QueryEscape(s)
971
+ }
972
+
973
+ var (
974
+ reNonSlashSlash = regexp.MustCompile(`([^/])/+`)
975
+ reTrailingSlash = regexp.MustCompile(`/+$`)
976
+ reLeadingSlash = regexp.MustCompile(`^/+`)
977
+ )
978
+
979
+ // Concatenate url part strings, merging forward slashes as needed.
980
+ func JoinUrl(parts []any) string {
981
+ var filtered []string
982
+ for _, p := range parts {
983
+ if "" != p && nil != p {
984
+ ps, ok := p.(string)
985
+ if !ok {
986
+ ps = Stringify(p)
987
+ }
988
+ filtered = append(filtered, ps)
989
+ }
990
+ }
991
+
992
+ for i, s := range filtered {
993
+ if i == 0 {
994
+ // For the first part, only remove trailing slashes
995
+ s = reTrailingSlash.ReplaceAllString(s, "")
996
+ } else {
997
+ // For remaining parts, handle both leading and trailing slashes
998
+ s = reNonSlashSlash.ReplaceAllString(s, `$1/`)
999
+ s = reLeadingSlash.ReplaceAllString(s, "")
1000
+ s = reTrailingSlash.ReplaceAllString(s, "")
1001
+ }
1002
+ filtered[i] = s
1003
+ }
1004
+
1005
+ finalParts := filtered[:0]
1006
+ for _, s := range filtered {
1007
+ if s != "" {
1008
+ finalParts = append(finalParts, s)
1009
+ }
1010
+ }
1011
+
1012
+ return strings.Join(finalParts, "/")
1013
+ }
1014
+
1015
+
1016
+ // Concatenate string array elements, merging separator chars as needed.
1017
+ // Optional args: sep (string, default ","), url (bool, default false).
1018
+ func Join(arr []any, args ...any) string {
1019
+ sarr := Size(arr)
1020
+
1021
+ sep := ","
1022
+ urlMode := false
1023
+
1024
+ if len(args) > 0 && args[0] != nil {
1025
+ if s, ok := args[0].(string); ok {
1026
+ sep = s
1027
+ }
1028
+ }
1029
+ if len(args) > 1 && args[1] != nil {
1030
+ if b, ok := args[1].(bool); ok {
1031
+ urlMode = b
1032
+ }
1033
+ }
1034
+
1035
+ var sepre string
1036
+ if 1 == len(sep) {
1037
+ sepre = EscRe(sep)
1038
+ }
1039
+
1040
+ // Filter to only non-empty strings
1041
+ filtered := Filter(arr, func(n [2]any) bool {
1042
+ t := Typify(n[1])
1043
+ return (0 < (T_string & t)) && S_MT != n[1]
1044
+ })
1045
+
1046
+ // Process each element for separator handling
1047
+ processed := Items(filtered)
1048
+
1049
+ var parts []string
1050
+ for _, kv := range processed {
1051
+ idx := 0
1052
+ if kstr, ok := kv[0].(string); ok {
1053
+ n, err := strconv.Atoi(kstr)
1054
+ if err == nil {
1055
+ idx = n
1056
+ }
1057
+ }
1058
+ s, ok := kv[1].(string)
1059
+ if !ok {
1060
+ continue
1061
+ }
1062
+
1063
+ if sepre != "" && sepre != S_MT {
1064
+ reTrailing := regexp.MustCompile(sepre + `+$`)
1065
+ reLeading := regexp.MustCompile(`^` + sepre + `+`)
1066
+ reInternal := regexp.MustCompile(`([^` + sepre + `])` + sepre + `+([^` + sepre + `])`)
1067
+
1068
+ if urlMode && 0 == idx {
1069
+ s = reTrailing.ReplaceAllString(s, S_MT)
1070
+ } else {
1071
+ if 0 < idx {
1072
+ s = reLeading.ReplaceAllString(s, S_MT)
1073
+ }
1074
+
1075
+ if idx < sarr-1 || !urlMode {
1076
+ s = reTrailing.ReplaceAllString(s, S_MT)
1077
+ }
1078
+
1079
+ s = reInternal.ReplaceAllString(s, "${1}"+sep+"${2}")
1080
+ }
1081
+ }
1082
+
1083
+ if s != S_MT {
1084
+ parts = append(parts, s)
1085
+ }
1086
+ }
1087
+
1088
+ return strings.Join(parts, sep)
1089
+ }
1090
+
1091
+
1092
+ // Output JSON in a "standard" format, with 2 space indents, each property on a new line,
1093
+ // and spaces after {[: and before ]}. Any "weird" values (NaN, etc) are output as null.
1094
+ // In general, the behavior of JavaScript's JSON.stringify(val,null,2) is followed.
1095
+ func Jsonify(val any, flags ...map[string]any) string {
1096
+ str := S_null
1097
+
1098
+ indent := 2
1099
+ offset := 0
1100
+
1101
+ if len(flags) > 0 && flags[0] != nil {
1102
+ if v, ok := flags[0]["indent"]; ok {
1103
+ if n, ok := v.(int); ok {
1104
+ indent = n
1105
+ }
1106
+ }
1107
+ if v, ok := flags[0]["offset"]; ok {
1108
+ if n, ok := v.(int); ok {
1109
+ offset = n
1110
+ }
1111
+ }
1112
+ }
1113
+
1114
+ if nil != val {
1115
+ indentStr := strings.Repeat(" ", indent)
1116
+ offsetStr := strings.Repeat(" ", offset)
1117
+ b, err := json.MarshalIndent(val, offsetStr, indentStr)
1118
+ if err != nil {
1119
+ str = S_null
1120
+ } else {
1121
+ str = string(b)
1122
+ }
1123
+ }
1124
+
1125
+ return str
1126
+ }
1127
+
1128
+ // Safely stringify a value for humans (NOT JSON!).
1129
+ func Stringify(val any, maxlen ...int) string {
1130
+ if nil == val {
1131
+ return S_MT
1132
+ }
1133
+
1134
+ if lr, ok := val.(*ListRef[any]); ok {
1135
+ return Stringify(lr.List, maxlen...)
1136
+ }
1137
+
1138
+ // Strings are returned directly without JSON serialization.
1139
+ if s, ok := val.(string); ok {
1140
+ jsonStr := s
1141
+ if len(maxlen) > 0 && maxlen[0] > 0 {
1142
+ ml := maxlen[0]
1143
+ if len(jsonStr) > ml {
1144
+ if ml >= 3 {
1145
+ jsonStr = jsonStr[:ml-3] + "..."
1146
+ } else {
1147
+ jsonStr = jsonStr[:ml]
1148
+ }
1149
+ }
1150
+ }
1151
+ return jsonStr
1152
+ }
1153
+
1154
+ // Unwrap any nested ListRefs before marshaling to JSON.
1155
+ val = _unwrapListRefs(val)
1156
+
1157
+ b, err := json.Marshal(val)
1158
+ if err != nil {
1159
+ return "__STRINGIFY_FAILED__"
1160
+ }
1161
+ jsonStr := string(b)
1162
+
1163
+ jsonStr = strings.ReplaceAll(jsonStr, `"`, "")
1164
+
1165
+ if len(maxlen) > 0 && maxlen[0] > 0 {
1166
+ ml := maxlen[0]
1167
+ if len(jsonStr) > ml {
1168
+ if ml >= 3 {
1169
+ jsonStr = jsonStr[:ml-3] + "..."
1170
+ } else {
1171
+ jsonStr = jsonStr[:ml]
1172
+ }
1173
+ }
1174
+ }
1175
+
1176
+ return jsonStr
1177
+ }
1178
+
1179
+ // Build a human friendly path string.
1180
+ func Pathify(val any, from ...int) string {
1181
+ var pathstr *string
1182
+
1183
+ var path []any = nil
1184
+
1185
+ if IsList(val) {
1186
+ path = _listify(val)
1187
+ } else {
1188
+ str, ok := val.(string)
1189
+ if ok {
1190
+ path = append(path, str)
1191
+ } else {
1192
+ num, err := _toFloat64(val)
1193
+ if nil == err {
1194
+ path = append(path, strconv.FormatInt(int64(math.Floor(num)), 10))
1195
+ }
1196
+ }
1197
+ }
1198
+
1199
+ var start int
1200
+ if 0 == len(from) {
1201
+ start = 0
1202
+
1203
+ } else {
1204
+ start = from[0]
1205
+ if start < 0 {
1206
+ start = 0
1207
+ }
1208
+ }
1209
+
1210
+ end := 0
1211
+ if len(from) > 1 {
1212
+ end = from[1]
1213
+ if end < 0 {
1214
+ end = 0
1215
+ }
1216
+ }
1217
+
1218
+ if nil != path && 0 <= start {
1219
+ if len(path) < start {
1220
+ start = len(path)
1221
+ }
1222
+
1223
+ endIdx := len(path) - end
1224
+ if endIdx < start {
1225
+ endIdx = start
1226
+ }
1227
+
1228
+ sliced := path[start:endIdx]
1229
+ if len(sliced) == 0 {
1230
+ root := "<root>"
1231
+ pathstr = &root
1232
+
1233
+ } else {
1234
+ var filtered []any
1235
+ for _, p := range sliced {
1236
+ switch x := p.(type) {
1237
+ case string:
1238
+ filtered = append(filtered, x)
1239
+ case int, int8, int16, int32, int64,
1240
+ float32, float64, uint, uint8, uint16, uint32, uint64:
1241
+ filtered = append(filtered, x)
1242
+ }
1243
+ }
1244
+
1245
+ var mapped []string
1246
+ for _, p := range filtered {
1247
+ switch x := p.(type) {
1248
+ case string:
1249
+ replaced := strings.ReplaceAll(x, S_DT, S_MT)
1250
+ mapped = append(mapped, replaced)
1251
+ default:
1252
+ numVal, err := _toFloat64(x)
1253
+ if err == nil {
1254
+ mapped = append(mapped, S_MT+strconv.FormatInt(int64(math.Floor(numVal)), 10))
1255
+ }
1256
+ }
1257
+ }
1258
+
1259
+ joined := strings.Join(mapped, S_DT)
1260
+ pathstr = &joined
1261
+ }
1262
+ }
1263
+
1264
+ if nil == pathstr {
1265
+ var sb strings.Builder
1266
+ sb.WriteString("<unknown-path")
1267
+ if val == nil {
1268
+ sb.WriteString(S_MT)
1269
+ } else {
1270
+ sb.WriteString(S_CN)
1271
+ sb.WriteString(Stringify(val, 33))
1272
+ }
1273
+ sb.WriteString(">")
1274
+ updesc := sb.String()
1275
+ pathstr = &updesc
1276
+ }
1277
+
1278
+ return *pathstr
1279
+ }
1280
+
1281
+ // Clone a JSON-like data structure.
1282
+ // NOTE: function value references are copied, *not* cloned.
1283
+ func Clone(val any) any {
1284
+ return CloneFlags(val, nil)
1285
+ }
1286
+
1287
+ func CloneFlags(val any, flags map[string]bool) any {
1288
+ if val == nil {
1289
+ return nil
1290
+ }
1291
+
1292
+ if nil == flags {
1293
+ flags = map[string]bool{}
1294
+ }
1295
+
1296
+ if _, ok := flags["func"]; !ok {
1297
+ flags["func"] = true
1298
+ }
1299
+
1300
+ typ := reflect.TypeOf(val)
1301
+ if typ.Kind() == reflect.Func {
1302
+ if flags["func"] {
1303
+ return val
1304
+ }
1305
+ return nil
1306
+ }
1307
+
1308
+ switch v := val.(type) {
1309
+ case map[string]any:
1310
+ newMap := make(map[string]any, len(v))
1311
+ for key, value := range v {
1312
+ newMap[key] = CloneFlags(value, flags)
1313
+ }
1314
+ return newMap
1315
+ case *ListRef[any]:
1316
+ newSlice := make([]any, len(v.List))
1317
+ for i, value := range v.List {
1318
+ newSlice[i] = CloneFlags(value, flags)
1319
+ }
1320
+ if flags["unwrap"] {
1321
+ return newSlice
1322
+ }
1323
+ return &ListRef[any]{List: newSlice}
1324
+ case []any:
1325
+ newSlice := make([]any, len(v))
1326
+ for i, value := range v {
1327
+ newSlice[i] = CloneFlags(value, flags)
1328
+ }
1329
+ if flags["wrap"] {
1330
+ return &ListRef[any]{List: newSlice}
1331
+ }
1332
+ return newSlice
1333
+ default:
1334
+ return v
1335
+ }
1336
+ }
1337
+
1338
+ // Define a JSON Object from alternating key-value arguments.
1339
+ // jo("a", 1, "b", 2) => {"a": 1, "b": 2}
1340
+ func Jo(kv ...any) map[string]any {
1341
+ o := make(map[string]any)
1342
+ kvsize := len(kv)
1343
+ for i := 0; i < kvsize; i += 2 {
1344
+ k := GetProp(kv, i, S_DS+S_KEY+strconv.Itoa(i))
1345
+ ks, ok := k.(string)
1346
+ if !ok {
1347
+ ks = Stringify(k)
1348
+ }
1349
+ o[ks] = GetProp(kv, i+1)
1350
+ }
1351
+ return o
1352
+ }
1353
+
1354
+ // Define a JSON Array from arguments.
1355
+ // ja(1, "x", true) => [1, "x", true]
1356
+ func Ja(v ...any) []any {
1357
+ a := make([]any, len(v))
1358
+ for i := 0; i < len(v); i++ {
1359
+ a[i] = GetProp(v, i)
1360
+ }
1361
+ return a
1362
+ }
1363
+
1364
+ // Safely delete a property from a map or list element.
1365
+ // For maps, the property is deleted. For lists, the element at the
1366
+ // index is removed and remaining elements are shifted down.
1367
+ // Returns the (possibly modified) parent.
1368
+ func DelProp(parent any, key any) any {
1369
+ if !IsKey(key) {
1370
+ return parent
1371
+ }
1372
+
1373
+ if IsMap(parent) {
1374
+ ks := StrKey(key)
1375
+ delete(parent.(map[string]any), ks)
1376
+ } else if IsList(parent) {
1377
+ ks := StrKey(key)
1378
+ ki, err := _parseInt(ks)
1379
+ if err != nil {
1380
+ return parent
1381
+ }
1382
+ ki = int(math.Floor(float64(ki)))
1383
+
1384
+ if lr, isLR := parent.(*ListRef[any]); isLR {
1385
+ psize := len(lr.List)
1386
+ if 0 <= ki && ki < psize {
1387
+ copy(lr.List[ki:], lr.List[ki+1:])
1388
+ lr.List = lr.List[:psize-1]
1389
+ }
1390
+ return parent
1391
+ }
1392
+
1393
+ arr, genarr := parent.([]any)
1394
+ if !genarr {
1395
+ rv := reflect.ValueOf(parent)
1396
+ arr = make([]any, rv.Len())
1397
+ for i := 0; i < rv.Len(); i++ {
1398
+ arr[i] = rv.Index(i).Interface()
1399
+ }
1400
+ }
1401
+
1402
+ psize := len(arr)
1403
+ if 0 <= ki && ki < psize {
1404
+ copy(arr[ki:], arr[ki+1:])
1405
+ arr = arr[:psize-1]
1406
+ }
1407
+
1408
+ if !genarr {
1409
+ return _makeArrayType(arr, parent)
1410
+ }
1411
+ return arr
1412
+ }
1413
+
1414
+ return parent
1415
+ }
1416
+
1417
+ // Safely set a property. Undefined arguments and invalid keys are ignored.
1418
+ // Returns the (possibly modified) parent.
1419
+ // If the value is undefined the key will be deleted from the parent.
1420
+ // If the parent is a list, and the key is negative, prepend the value.
1421
+ // NOTE: If the key is above the list size, append the value; below, prepend.
1422
+ // If the value is undefined, remove the list element at index key, and shift the
1423
+ // remaining elements down. These rules avoid "holes" in the list.
1424
+ func SetProp(parent any, key any, newval any) any {
1425
+ if !IsKey(key) {
1426
+ return parent
1427
+ }
1428
+
1429
+ if IsMap(parent) {
1430
+ m := parent.(map[string]any)
1431
+
1432
+ // Convert key to string
1433
+ ks := ""
1434
+ ks = StrKey(key)
1435
+
1436
+ // Preserve nil values (like JS null). Use DelProp for explicit key removal.
1437
+ m[ks] = newval
1438
+
1439
+ } else if IsList(parent) {
1440
+
1441
+ // Convert key to integer
1442
+ var ki int
1443
+ switch k := key.(type) {
1444
+ case int:
1445
+ ki = k
1446
+ case float64:
1447
+ ki = int(k)
1448
+ case string:
1449
+ kiParsed, e := _parseInt(k)
1450
+ if e == nil {
1451
+ ki = kiParsed
1452
+ } else {
1453
+ // no-op, can't set
1454
+ return parent
1455
+ }
1456
+ default:
1457
+ return parent
1458
+ }
1459
+
1460
+ // ListRef: modify .List in place, return same pointer for reference stability.
1461
+ if lr, isLR := parent.(*ListRef[any]); isLR {
1462
+ if newval == nil {
1463
+ if ki >= 0 && ki < len(lr.List) {
1464
+ copy(lr.List[ki:], lr.List[ki+1:])
1465
+ lr.List = lr.List[:len(lr.List)-1]
1466
+ }
1467
+ return parent
1468
+ }
1469
+ if ki >= 0 {
1470
+ if ki >= len(lr.List) {
1471
+ lr.List = append(lr.List, newval)
1472
+ } else {
1473
+ lr.List[ki] = newval
1474
+ }
1475
+ return parent
1476
+ }
1477
+ if ki < 0 {
1478
+ lr.List = append([]any{newval}, lr.List...)
1479
+ return parent
1480
+ }
1481
+ return parent
1482
+ }
1483
+
1484
+ arr, genarr := parent.([]any)
1485
+
1486
+ // If newval == nil, remove element [shift down].
1487
+
1488
+ if !genarr {
1489
+ rv := reflect.ValueOf(parent)
1490
+ arr = make([]any, rv.Len())
1491
+ for i := 0; i < rv.Len(); i++ {
1492
+ arr[i] = rv.Index(i).Interface()
1493
+ }
1494
+ }
1495
+
1496
+ if newval == nil {
1497
+ if ki >= 0 && ki < len(arr) {
1498
+ copy(arr[ki:], arr[ki+1:])
1499
+ arr = arr[:len(arr)-1]
1500
+ }
1501
+
1502
+ if !genarr {
1503
+ return _makeArrayType(arr, parent)
1504
+ } else {
1505
+
1506
+ return arr
1507
+ }
1508
+ }
1509
+
1510
+ // If ki >= 0, set or append
1511
+ if ki >= 0 {
1512
+ if ki >= len(arr) {
1513
+ arr = append(arr, newval)
1514
+ } else {
1515
+ arr[ki] = newval
1516
+ }
1517
+
1518
+ if !genarr {
1519
+ return _makeArrayType(arr, parent)
1520
+ } else {
1521
+ return arr
1522
+ }
1523
+ }
1524
+
1525
+ // If ki < 0, prepend
1526
+ if ki < 0 {
1527
+ // prepend
1528
+ newarr := make([]any, 0, len(arr)+1)
1529
+ newarr = append(newarr, newval)
1530
+ newarr = append(newarr, arr...)
1531
+ if !genarr {
1532
+ return _makeArrayType(newarr, parent)
1533
+ } else {
1534
+ return newarr
1535
+ }
1536
+ }
1537
+ }
1538
+
1539
+ return parent
1540
+ }
1541
+
1542
+ // Walk a data structure depth first, applying functions to each value.
1543
+ // Walk(val, before) - before callback only (pre-order).
1544
+ // Walk(val, before, after) - both before and after callbacks.
1545
+ // Walk(val, before, after, maxdepth) - with maximum recursion depth.
1546
+ // Pass nil for before or after to skip that callback.
1547
+ // For backward compatibility, Walk(val, apply) applies the callback after children (post-order).
1548
+ func Walk(
1549
+ val any,
1550
+ apply WalkApply,
1551
+ opts ...any,
1552
+ ) any {
1553
+ var after WalkApply
1554
+ var maxdepth int = 32
1555
+
1556
+ if len(opts) > 0 {
1557
+ if opts[0] != nil {
1558
+ if fn, ok := opts[0].(WalkApply); ok {
1559
+ after = fn
1560
+ } else if fn, ok := opts[0].(func(*string, any, any, []string) any); ok {
1561
+ after = fn
1562
+ }
1563
+ }
1564
+ }
1565
+ if len(opts) > 1 {
1566
+ if opts[1] != nil {
1567
+ switch md := opts[1].(type) {
1568
+ case int:
1569
+ maxdepth = md
1570
+ case float64:
1571
+ maxdepth = int(md)
1572
+ }
1573
+ }
1574
+ }
1575
+
1576
+ if after != nil {
1577
+ // Two-callback mode: apply is before, after is after.
1578
+ return _walkDescend(val, apply, after, maxdepth, nil, nil, nil)
1579
+ }
1580
+
1581
+ // Single-callback mode: apply is called before children (pre-order),
1582
+ // matching the TS implementation where walk(val, before) is pre-order.
1583
+ return _walkDescend(val, apply, nil, maxdepth, nil, nil, nil)
1584
+ }
1585
+
1586
+
1587
+ func WalkDescend(
1588
+ val any,
1589
+ apply WalkApply,
1590
+ key *string,
1591
+ parent any,
1592
+ path []string,
1593
+ ) any {
1594
+ return _walkDescend(val, nil, apply, 32, key, parent, path)
1595
+ }
1596
+
1597
+
1598
+ func _walkDescend(
1599
+ val any,
1600
+ before WalkApply,
1601
+ after WalkApply,
1602
+ maxdepth int,
1603
+ key *string,
1604
+ parent any,
1605
+ path []string,
1606
+ ) any {
1607
+
1608
+ out := val
1609
+
1610
+ // Apply before callback.
1611
+ if nil != before {
1612
+ out = before(key, out, parent, path)
1613
+ }
1614
+
1615
+ // Check depth limit.
1616
+ if 0 == maxdepth || (nil != path && 0 < maxdepth && maxdepth <= len(path)) {
1617
+ return out
1618
+ }
1619
+
1620
+ if IsNode(out) {
1621
+ for _, kv := range Items(out) {
1622
+ ckey := kv[0]
1623
+ child := kv[1]
1624
+ ckeyStr := StrKey(ckey)
1625
+ newPath := make([]string, len(path)+1)
1626
+ copy(newPath, path)
1627
+ newPath[len(path)] = ckeyStr
1628
+ newChild := _walkDescend(child, before, after, maxdepth, &ckeyStr, out, newPath)
1629
+ out = SetProp(out, ckey, newChild)
1630
+ }
1631
+
1632
+ if nil != parent && nil != key {
1633
+ SetProp(parent, *key, out)
1634
+ }
1635
+ }
1636
+
1637
+ // Apply after callback.
1638
+ if nil != after {
1639
+ out = after(key, out, parent, path)
1640
+ }
1641
+
1642
+ return out
1643
+ }
1644
+
1645
+ // Merge a list of values into each other. Later values have
1646
+ // precedence. Nodes override scalars. Node kinds (list or map)
1647
+ // override each other, and do *not* merge. The first element is
1648
+ // modified.
1649
+ // Optional maxdepth parameter limits recursion depth.
1650
+ func Merge(val any, maxdepths ...int) any {
1651
+ md := 32
1652
+ if len(maxdepths) > 0 {
1653
+ if maxdepths[0] < 0 {
1654
+ md = 0
1655
+ } else {
1656
+ md = maxdepths[0]
1657
+ }
1658
+ }
1659
+
1660
+ var out any = nil
1661
+
1662
+ if !IsList(val) {
1663
+ return val
1664
+ }
1665
+
1666
+ list := _listify(val)
1667
+ lenlist := len(list)
1668
+
1669
+ if 0 == lenlist {
1670
+ return nil
1671
+ }
1672
+
1673
+ if 1 == lenlist {
1674
+ return list[0]
1675
+ }
1676
+
1677
+ // Merge a list of values.
1678
+ out = GetProp(list, 0, make(map[string]any))
1679
+
1680
+ for i := 1; i < lenlist; i++ {
1681
+ obj := list[i]
1682
+
1683
+ if !IsNode(obj) {
1684
+ // Nodes win.
1685
+ out = obj
1686
+ } else {
1687
+ // Current value at path end in overriding node.
1688
+ cur := make([]any, 33)
1689
+ cur[0] = out
1690
+
1691
+ // Current value at path end in destination node.
1692
+ dst := make([]any, 33)
1693
+ dst[0] = out
1694
+
1695
+ before := func(
1696
+ key *string,
1697
+ val any,
1698
+ _parent any,
1699
+ path []string,
1700
+ ) any {
1701
+ pI := len(path)
1702
+
1703
+ if md <= pI {
1704
+ if key != nil {
1705
+ SetProp(cur[pI-1], *key, val)
1706
+ }
1707
+ } else if !IsNode(val) {
1708
+ // Scalars just override directly.
1709
+ cur[pI] = val
1710
+ } else {
1711
+ // Descend into override node.
1712
+ if 0 < pI && key != nil {
1713
+ dst[pI] = GetProp(dst[pI-1], *key)
1714
+ }
1715
+ tval := dst[pI]
1716
+
1717
+ // Destination empty, create node (unless override is class instance).
1718
+ if nil == tval && 0 == (T_instance&Typify(val)) {
1719
+ if IsList(val) {
1720
+ cur[pI] = make([]any, 0)
1721
+ } else {
1722
+ cur[pI] = make(map[string]any)
1723
+ }
1724
+ } else if Typify(val) == Typify(tval) {
1725
+ // Matching override and destination, continue with their values.
1726
+ cur[pI] = tval
1727
+ } else {
1728
+ // Override wins.
1729
+ cur[pI] = val
1730
+ // No need to descend (destination is discarded).
1731
+ val = nil
1732
+ }
1733
+ }
1734
+
1735
+ return val
1736
+ }
1737
+
1738
+ after := func(
1739
+ key *string,
1740
+ _val any,
1741
+ _parent any,
1742
+ path []string,
1743
+ ) any {
1744
+ cI := len(path)
1745
+
1746
+ // Root node: nothing to set on parent.
1747
+ if nil == key || cI <= 0 {
1748
+ return cur[0]
1749
+ }
1750
+
1751
+ value := cur[cI]
1752
+
1753
+ cur[cI-1] = SetProp(cur[cI-1], *key, value)
1754
+ return value
1755
+ }
1756
+
1757
+ // Walk overriding node, creating paths in output as needed.
1758
+ Walk(obj, before, after, md)
1759
+
1760
+ out = cur[0]
1761
+ }
1762
+ }
1763
+
1764
+ if 0 == md {
1765
+ out = GetElem(list, -1)
1766
+ if IsList(out) {
1767
+ out = make([]any, 0)
1768
+ } else if IsMap(out) {
1769
+ out = make(map[string]any)
1770
+ }
1771
+ }
1772
+
1773
+ return out
1774
+ }
1775
+
1776
+ // Get a value deep inside a node using a key path. For example the
1777
+ // path `a.b` gets the value 1 from {a:{b:1}}. The path can specified
1778
+ // as a dotted string, or a string array. If the path starts with a
1779
+ // dot (or the first element is "), the path is considered local, and
1780
+ // resolved against the `current` argument, if defined. Integer path
1781
+ // parts are used as array indexes. The inj argument allows for
1782
+ // custom handling when called from `inject` or `transform`.
1783
+ func GetPath(path any, store any, injdefs ...*Injection) any {
1784
+ var inj *Injection
1785
+ if len(injdefs) > 0 {
1786
+ inj = injdefs[0]
1787
+ }
1788
+
1789
+ var parts []string
1790
+
1791
+ // Operate on a string array.
1792
+ switch pp := path.(type) {
1793
+ case []string:
1794
+ parts = make([]string, len(pp))
1795
+ copy(parts, pp)
1796
+
1797
+ case string:
1798
+ if pp == "" {
1799
+ parts = []string{S_MT}
1800
+ } else {
1801
+ parts = strings.Split(pp, S_DT)
1802
+ }
1803
+ default:
1804
+ if IsList(path) {
1805
+ parts = _resolveStrings(_listify(path))
1806
+ } else {
1807
+ return nil
1808
+ }
1809
+ }
1810
+
1811
+ val := store
1812
+ var base any = nil
1813
+ if nil != inj {
1814
+ base = inj.Base
1815
+ }
1816
+
1817
+ src := GetProp(store, base, store)
1818
+ var dparent any
1819
+ if inj != nil {
1820
+ dparent = inj.Dparent
1821
+ }
1822
+
1823
+ numparts := len(parts)
1824
+
1825
+ // An empty path (incl empty string) just finds the store.
1826
+ if nil == path || nil == store || (1 == numparts && S_MT == parts[0]) {
1827
+ val = src
1828
+
1829
+ } else if 0 < numparts {
1830
+
1831
+ // Check for $ACTIONs
1832
+ if 1 == numparts {
1833
+ val = GetProp(store, parts[0])
1834
+ }
1835
+
1836
+ if !IsFunc(val) {
1837
+ val = src
1838
+
1839
+ // Meta path syntax: "q0$=x1" or "q0$~x1"
1840
+ m := reMetaPath.FindStringSubmatch(parts[0])
1841
+ if m != nil && inj != nil && inj.Meta != nil {
1842
+ val = GetProp(inj.Meta, m[1])
1843
+ parts[0] = m[3]
1844
+ }
1845
+
1846
+ var dpath []string
1847
+ if inj != nil {
1848
+ dpath = inj.Dpath
1849
+ }
1850
+
1851
+ for pI := 0; val != nil && pI < numparts; pI++ {
1852
+ part := parts[pI]
1853
+
1854
+ if inj != nil && part == "$KEY" {
1855
+ part = inj.Key
1856
+ } else if inj != nil && strings.HasPrefix(part, "$GET:") {
1857
+ // $GET:path$ -> get store value, use as path part
1858
+ subpath := part[5 : len(part)-1]
1859
+ result := GetPath(subpath, src)
1860
+ part = Stringify(result)
1861
+ } else if inj != nil && strings.HasPrefix(part, "$REF:") {
1862
+ // $REF:refpath$ -> get spec value, use as path part
1863
+ subpath := part[5 : len(part)-1]
1864
+ specVal := GetProp(store, S_DSPEC)
1865
+ if specVal != nil {
1866
+ result := GetPath(subpath, specVal)
1867
+ part = Stringify(result)
1868
+ }
1869
+ } else if inj != nil && strings.HasPrefix(part, "$META:") {
1870
+ // $META:metapath$ -> get meta value, use as path part
1871
+ subpath := part[6 : len(part)-1]
1872
+ result := GetPath(subpath, inj.Meta)
1873
+ part = Stringify(result)
1874
+ }
1875
+
1876
+ // $$ escapes $
1877
+ part = strings.ReplaceAll(part, "$$", "$")
1878
+
1879
+ if S_MT == part {
1880
+ ascends := 0
1881
+ for 1+pI < numparts && S_MT == parts[1+pI] {
1882
+ ascends++
1883
+ pI++
1884
+ }
1885
+
1886
+ if inj != nil && 0 < ascends {
1887
+ if pI == numparts-1 {
1888
+ ascends--
1889
+ }
1890
+
1891
+ if 0 == ascends {
1892
+ val = dparent
1893
+ } else {
1894
+ // Build fullpath from dpath + remaining parts
1895
+ cutLen := len(dpath) - ascends
1896
+ if cutLen < 0 {
1897
+ cutLen = 0
1898
+ }
1899
+ fullpath := make([]string, 0)
1900
+ fullpath = append(fullpath, dpath[:cutLen]...)
1901
+ if pI+1 < numparts {
1902
+ fullpath = append(fullpath, parts[pI+1:]...)
1903
+ }
1904
+
1905
+ if ascends <= len(dpath) {
1906
+ val = GetPath(fullpath, store)
1907
+ } else {
1908
+ val = nil
1909
+ }
1910
+ break
1911
+ }
1912
+ } else {
1913
+ val = dparent
1914
+ }
1915
+ } else {
1916
+ val = GetProp(val, part)
1917
+ }
1918
+ }
1919
+ }
1920
+ }
1921
+
1922
+ if nil != inj && inj.Handler != nil {
1923
+ ref := Pathify(path)
1924
+ val = inj.Handler(inj, val, &ref, store)
1925
+ }
1926
+
1927
+ return val
1928
+ }
1929
+
1930
+ // Set a value at a path inside a store. Missing intermediate path
1931
+ // parts are created (maps for string keys, lists for numeric keys).
1932
+ // String paths are split on ".". If val is the DELETE sentinel,
1933
+ // the final key is deleted instead of set.
1934
+ func SetPath(store any, path any, val any, injdefs ...map[string]any) any {
1935
+ pathType := Typify(path)
1936
+
1937
+ var parts []any
1938
+ if 0 < (T_list & pathType) {
1939
+ parts = _listify(path)
1940
+ } else if 0 < (T_string & pathType) {
1941
+ splitParts := strings.Split(path.(string), S_DT)
1942
+ parts = make([]any, len(splitParts))
1943
+ for i, s := range splitParts {
1944
+ parts[i] = s
1945
+ }
1946
+ } else if 0 < (T_number & pathType) {
1947
+ parts = []any{path}
1948
+ } else {
1949
+ return nil
1950
+ }
1951
+
1952
+ var base any
1953
+ if len(injdefs) > 0 && injdefs[0] != nil {
1954
+ base = GetProp(injdefs[0], S_base)
1955
+ }
1956
+
1957
+ numparts := len(parts)
1958
+ parent := GetProp(store, base, store)
1959
+
1960
+ var grandparent any
1961
+ var grandKey any
1962
+
1963
+ for pI := 0; pI < numparts-1; pI++ {
1964
+ partKey := GetElem(parts, pI)
1965
+ nextParent := GetProp(parent, partKey)
1966
+ if !IsNode(nextParent) {
1967
+ nextPartKey := GetElem(parts, pI+1)
1968
+ if 0 < (T_number & Typify(nextPartKey)) {
1969
+ nextParent = []any{}
1970
+ } else {
1971
+ nextParent = map[string]any{}
1972
+ }
1973
+ SetProp(parent, partKey, nextParent)
1974
+ }
1975
+ grandparent = parent
1976
+ grandKey = partKey
1977
+ parent = nextParent
1978
+ }
1979
+
1980
+ lastKey := GetElem(parts, -1)
1981
+ if val == DELETE {
1982
+ newParent := DelProp(parent, lastKey)
1983
+ if grandparent != nil && IsList(parent) {
1984
+ SetProp(grandparent, grandKey, newParent)
1985
+ }
1986
+ return newParent
1987
+ } else {
1988
+ newParent := SetProp(parent, lastKey, val)
1989
+ if grandparent != nil && IsList(parent) {
1990
+ SetProp(grandparent, grandKey, newParent)
1991
+ }
1992
+ return newParent
1993
+ }
1994
+ }
1995
+
1996
+ // Inject store values into a string. Not a public utility - used by
1997
+ // `inject`. Inject are marked with `path` where path is resolved
1998
+ // with getpath against the store or current (if defined)
1999
+ // arguments. See `getpath`. Custom injection handling can be
2000
+ // provided by inj.handler (this is used for transform functions).
2001
+ // The path can also have the special syntax $NAME999 where NAME is
2002
+ // upper case letters only, and 999 is any digits, which are
2003
+ // discarded. This syntax specifies the name of a transform, and
2004
+ // optionally allows transforms to be ordered by alphanumeric sorting.
2005
+ func _injectStr(
2006
+ val string,
2007
+ store any,
2008
+ inj *Injection,
2009
+ ) any {
2010
+ if val == S_MT {
2011
+ return S_MT
2012
+ }
2013
+
2014
+ // Pattern examples: "`a.b.c`", "`$NAME`", "`$NAME1`"
2015
+ // fullRe := regexp.MustCompile("^`([^`]+)[0-9]*`$")
2016
+ fullRe := regexp.MustCompile("^`(\\$[A-Z]+|[^`]*)[0-9]*`$")
2017
+ matches := fullRe.FindStringSubmatch(val)
2018
+
2019
+ // Full string of the val is an injection.
2020
+ if matches != nil {
2021
+ if nil != inj {
2022
+ inj.Full = true
2023
+ }
2024
+ pathref := matches[1]
2025
+
2026
+ // Special escapes inside injection.
2027
+ if len(pathref) > 3 {
2028
+ pathref = strings.ReplaceAll(pathref, "$BT", S_BT)
2029
+ pathref = strings.ReplaceAll(pathref, "$DS", S_DS)
2030
+ }
2031
+
2032
+ // Get the extracted path reference.
2033
+ out := GetPath(pathref, store, inj)
2034
+
2035
+ return out
2036
+ }
2037
+
2038
+ // Check for injections within the string.
2039
+ partialRe := regexp.MustCompile("`([^`]+)`")
2040
+ out := partialRe.ReplaceAllStringFunc(val, func(m string) string {
2041
+ ref := strings.Trim(m, "`")
2042
+
2043
+ // Special escapes inside injection.
2044
+ if 3 < len(ref) {
2045
+ ref = strings.ReplaceAll(ref, "$BT", S_BT)
2046
+ ref = strings.ReplaceAll(ref, "$DS", S_DS)
2047
+ }
2048
+ if nil != inj {
2049
+ inj.Full = false
2050
+ }
2051
+ found := GetPath(ref, store, inj)
2052
+
2053
+ if nil == found {
2054
+ return S_MT
2055
+ }
2056
+ switch fv := found.(type) {
2057
+ case map[string]any, []any:
2058
+ b, _ := json.Marshal(fv)
2059
+ return string(b)
2060
+ case *ListRef[any]:
2061
+ b, _ := json.Marshal(fv.List)
2062
+ return string(b)
2063
+ default:
2064
+ return _stringifyValue(found)
2065
+ }
2066
+ })
2067
+
2068
+ // Also call the inj handler on the entire string, providing the
2069
+ // option for custom injection.
2070
+ if nil != inj && IsFunc(inj.Handler) {
2071
+ inj.Full = true
2072
+ result := inj.Handler(inj, out, &val, store)
2073
+ if s, ok := result.(string); ok {
2074
+ out = s
2075
+ } else {
2076
+ out = fmt.Sprint(result)
2077
+ }
2078
+ }
2079
+
2080
+ return out
2081
+ }
2082
+
2083
+ // Inject values from a data store into a node recursively, resolving
2084
+ // paths against the store, or current if they are local. The modify
2085
+ // argument allows custom modification of the result. The inj
2086
+ // (Injection) argument is used to maintain recursive inj.
2087
+ func Inject(
2088
+ val any,
2089
+ store any,
2090
+ injdefs ...*Injection,
2091
+ ) any {
2092
+ var inj *Injection
2093
+ if len(injdefs) > 0 {
2094
+ inj = injdefs[0]
2095
+ }
2096
+ valType := Typify(val)
2097
+
2098
+ // Create inj if at root of injection. The input value is placed
2099
+ // inside a virtual parent holder to simplify edge cases.
2100
+ if inj == nil || inj.Mode == 0 {
2101
+ parent := map[string]any{
2102
+ S_DTOP: val,
2103
+ }
2104
+
2105
+ newInj := &Injection{
2106
+ Mode: M_VAL,
2107
+ Full: false,
2108
+ KeyI: 0,
2109
+ Keys: &ListRef[string]{List: []string{S_DTOP}},
2110
+ Key: S_DTOP,
2111
+ Val: val,
2112
+ Parent: parent,
2113
+ Path: &ListRef[string]{List: []string{S_DTOP}},
2114
+ Nodes: &ListRef[any]{List: []any{parent}},
2115
+ Handler: injectHandler,
2116
+ Base: S_DTOP,
2117
+ Modify: nil,
2118
+ Errs: GetProp(store, S_DERRS, ListRefCreate[any]()).(*ListRef[any]),
2119
+ Meta: make(map[string]any),
2120
+ Dparent: store,
2121
+ Dpath: []string{S_DTOP},
2122
+ }
2123
+ newInj.Meta["__d"] = 0
2124
+
2125
+ if inj != nil {
2126
+ // Partial init provided (like TS injdef)
2127
+ if inj.Modify != nil {
2128
+ newInj.Modify = inj.Modify
2129
+ }
2130
+ if inj.Extra != nil {
2131
+ newInj.Extra = inj.Extra
2132
+ }
2133
+ if inj.Meta != nil {
2134
+ newInj.Meta = inj.Meta
2135
+ }
2136
+ if inj.Handler != nil {
2137
+ newInj.Handler = inj.Handler
2138
+ }
2139
+ if inj.Errs != nil {
2140
+ newInj.Errs = inj.Errs
2141
+ }
2142
+ if inj.Dparent != nil {
2143
+ newInj.Dparent = inj.Dparent
2144
+ }
2145
+ if inj.Dpath != nil {
2146
+ newInj.Dpath = inj.Dpath
2147
+ }
2148
+ }
2149
+
2150
+ inj = newInj
2151
+ }
2152
+
2153
+ inj.descend()
2154
+
2155
+ // Descend into node
2156
+ if IsNode(val) {
2157
+ childkeys := KeysOf(val)
2158
+
2159
+ // Keys are sorted alphanumerically to ensure determinism.
2160
+ // Injection transforms ($FOO) are processed *after* other keys.
2161
+ var normalKeys []string
2162
+ var transformKeys []string
2163
+ for _, k := range childkeys {
2164
+ if strings.Contains(k, S_DS) {
2165
+ transformKeys = append(transformKeys, k)
2166
+ } else {
2167
+ normalKeys = append(normalKeys, k)
2168
+ }
2169
+ }
2170
+
2171
+ sort.Strings(normalKeys)
2172
+ sort.Strings(transformKeys)
2173
+ nodekeys := append(normalKeys, transformKeys...)
2174
+
2175
+ nkI := 0
2176
+ for nkI < len(nodekeys) {
2177
+ nodekey := nodekeys[nkI]
2178
+
2179
+ childinj := inj.child(nkI, nodekeys)
2180
+ childinj.Mode = M_KEYPRE
2181
+
2182
+ // Perform the key:pre mode injection on the child key.
2183
+ preKey := _injectStr(nodekey, store, childinj)
2184
+
2185
+ // The injection may modify child processing.
2186
+ nkI = childinj.KeyI
2187
+ nodekeys = childinj.Keys.List
2188
+ val = childinj.Parent
2189
+
2190
+ if preKey != nil {
2191
+ childval := GetProp(val, preKey)
2192
+ childinj.Val = childval
2193
+ childinj.Mode = M_VAL
2194
+
2195
+ // Perform the val mode injection on the child value.
2196
+ Inject(childval, store, childinj)
2197
+
2198
+ // The injection may modify child processing.
2199
+ nkI = childinj.KeyI
2200
+ nodekeys = childinj.Keys.List
2201
+ val = childinj.Parent
2202
+
2203
+ // Perform the key:post mode injection on the child key.
2204
+ childinj.Mode = M_KEYPOST
2205
+ _injectStr(nodekey, store, childinj)
2206
+
2207
+ // The injection may modify child processing.
2208
+ nkI = childinj.KeyI
2209
+ nodekeys = childinj.Keys.List
2210
+ val = childinj.Parent
2211
+ }
2212
+
2213
+ nkI = nkI + 1
2214
+ }
2215
+
2216
+ } else if 0 < (T_string & valType) {
2217
+
2218
+ // Inject paths into string scalars.
2219
+ inj.Mode = M_VAL
2220
+ strVal, ok := val.(string)
2221
+ if ok {
2222
+ val = _injectStr(strVal, store, inj)
2223
+ if val != SKIP {
2224
+ inj.setval(val)
2225
+ }
2226
+ }
2227
+ }
2228
+
2229
+ // Custom modification
2230
+ if nil != inj.Modify && val != SKIP {
2231
+ mkey := inj.Key
2232
+ mparent := inj.Parent
2233
+ mval := GetProp(mparent, mkey)
2234
+ inj.Modify(
2235
+ mval,
2236
+ mkey,
2237
+ mparent,
2238
+ inj,
2239
+ store,
2240
+ )
2241
+ }
2242
+
2243
+ inj.Val = val
2244
+
2245
+ // Original val reference may no longer be correct.
2246
+ // This return value is only used as the top level result.
2247
+ rval := GetProp(inj.Parent, S_DTOP)
2248
+
2249
+ return rval
2250
+ }
2251
+
2252
+ // Default inject handler for transforms. If the path resolves to a function,
2253
+ // call the function passing the injection inj. This is how transforms operate.
2254
+ var injectHandler Injector = func(
2255
+ inj *Injection,
2256
+ val any,
2257
+ ref *string,
2258
+ store any,
2259
+ ) any {
2260
+ out := val
2261
+ iscmd := IsFunc(val) && (nil == ref || strings.HasPrefix(*ref, S_DS))
2262
+
2263
+ if iscmd {
2264
+ fnih, ok := val.(Injector)
2265
+
2266
+ if ok {
2267
+ out = fnih(inj, val, ref, store)
2268
+ } else {
2269
+ // In Go, as a convenience, allow injection functions that have no arguments.
2270
+ fn0, ok := val.(func() any)
2271
+ if ok {
2272
+ out = fn0()
2273
+ }
2274
+ }
2275
+ } else if M_VAL == inj.Mode && inj.Full {
2276
+ // Update parent with value. Ensures references remain in node tree.
2277
+ inj.setval(val)
2278
+ }
2279
+
2280
+ return out
2281
+ }
2282
+
2283
+ // The transform_* functions are special command inject handlers (see Injector).
2284
+
2285
+ // Delete a key from a map or list.
2286
+ var Transform_DELETE Injector = func(
2287
+ inj *Injection,
2288
+ val any,
2289
+ ref *string,
2290
+ store any,
2291
+ ) any {
2292
+ inj.setval(nil)
2293
+ return nil
2294
+ }
2295
+
2296
+ // Copy value from source data.
2297
+ var Transform_COPY Injector = func(
2298
+ inj *Injection,
2299
+ val any,
2300
+ ref *string,
2301
+ store any,
2302
+ ) any {
2303
+ if !CheckPlacement(M_VAL, "COPY", T_any, inj) {
2304
+ return nil
2305
+ }
2306
+
2307
+ out := GetProp(inj.Dparent, inj.Key)
2308
+ inj.setval(out)
2309
+
2310
+ return out
2311
+ }
2312
+
2313
+ // As a value, inject the key of the parent node.
2314
+ // As a key, defined the name of the key property in the source object.
2315
+ var Transform_KEY Injector = func(
2316
+ inj *Injection,
2317
+ val any,
2318
+ ref *string,
2319
+ store any,
2320
+ ) any {
2321
+ if inj.Mode != M_VAL {
2322
+ return nil
2323
+ }
2324
+
2325
+ // Key is defined by $KEY meta property.
2326
+ keyspec := GetProp(inj.Parent, S_BKEY)
2327
+ if keyspec != nil {
2328
+ DelProp(inj.Parent, S_BKEY)
2329
+ return GetProp(inj.Dparent, keyspec)
2330
+ }
2331
+
2332
+ // Key is defined within general purpose $ANNO object.
2333
+ anno := GetProp(inj.Parent, S_BANNO)
2334
+ pkey := GetProp(anno, S_KEY)
2335
+ if pkey != nil {
2336
+ return pkey
2337
+ }
2338
+
2339
+ // fallback to the second-last path element
2340
+ if len(inj.Path.List) >= 2 {
2341
+ return inj.Path.List[len(inj.Path.List)-2]
2342
+ }
2343
+
2344
+ return nil
2345
+ }
2346
+
2347
+ // Store meta data about a node. Does nothing itself, just used by
2348
+ // other injectors, and is removed when called.
2349
+ var Transform_META Injector = func(
2350
+ inj *Injection,
2351
+ val any,
2352
+ ref *string,
2353
+ store any,
2354
+ ) any {
2355
+ DelProp(inj.Parent, S_DMETA)
2356
+ return nil
2357
+ }
2358
+
2359
+ // Annotate node. Does nothing itself, just used by other injectors, and is removed when called.
2360
+ var Transform_ANNO Injector = func(
2361
+ inj *Injection,
2362
+ val any,
2363
+ ref *string,
2364
+ store any,
2365
+ ) any {
2366
+ DelProp(inj.Parent, S_BANNO)
2367
+ return nil
2368
+ }
2369
+
2370
+ // Merge a list of objects into the current object.
2371
+ // Must be a key in an object. The value is merged over the current object.
2372
+ // If the value is an array, the elements are first merged using `merge`.
2373
+ // If the value is the empty string, merge the top level store.
2374
+ // Format: { '`$MERGE`': '`source-path`' | ['`source-paths`', ...] }
2375
+ var Transform_MERGE Injector = func(
2376
+ inj *Injection,
2377
+ val any,
2378
+ ref *string,
2379
+ store any,
2380
+ ) any {
2381
+ if M_KEYPRE == inj.Mode {
2382
+ return inj.Key
2383
+ }
2384
+
2385
+ if M_KEYPOST == inj.Mode {
2386
+ args := GetProp(inj.Parent, inj.Key)
2387
+ if S_MT == args {
2388
+ args = []any{GetProp(store, S_DTOP)}
2389
+ } else if IsList(args) {
2390
+ // do nothing
2391
+ } else {
2392
+ // wrap in array
2393
+ args = []any{args}
2394
+ }
2395
+
2396
+ // Remove the $MERGE command from a parent map.
2397
+ DelProp(inj.Parent, inj.Key)
2398
+
2399
+ list, ok := _asList(args)
2400
+ if !ok {
2401
+ return inj.Key
2402
+ }
2403
+
2404
+ // Literals in the parent have precedence, but we still merge onto
2405
+ // the parent object, so that node tree references are not changed.
2406
+ mergeList := []any{inj.Parent}
2407
+ mergeList = append(mergeList, list...)
2408
+ mergeList = append(mergeList, Clone(inj.Parent))
2409
+
2410
+ Merge(mergeList)
2411
+
2412
+ return inj.Key
2413
+ }
2414
+
2415
+ // Ensures $MERGE is removed from parent list.
2416
+ return nil
2417
+ }
2418
+
2419
+
2420
+ // Convert a node to a list.
2421
+ // Format: ['`$EACH`', '`source-path-of-node`', child-template]
2422
+ var Transform_EACH Injector = func(
2423
+ inj *Injection,
2424
+ val any,
2425
+ ref *string,
2426
+ store any,
2427
+ ) any {
2428
+ ijname := "EACH"
2429
+
2430
+ if !CheckPlacement(M_VAL, ijname, T_list, inj) {
2431
+ return nil
2432
+ }
2433
+
2434
+ // Remove remaining keys to avoid spurious processing.
2435
+ if inj.Keys != nil && len(inj.Keys.List) > 0 {
2436
+ inj.Keys.List = inj.Keys.List[:1]
2437
+ }
2438
+
2439
+ // Get arguments: ['`$EACH`', 'source-path', child-template]
2440
+ parentList := _listify(inj.Parent)
2441
+ var sliced []any
2442
+ if len(parentList) > 1 {
2443
+ sliced = parentList[1:]
2444
+ }
2445
+ args := InjectorArgs([]int{T_string, T_any}, sliced)
2446
+ if args[0] != nil {
2447
+ inj.Errs.Append("$" + ijname + ": " + args[0].(string))
2448
+ return nil
2449
+ }
2450
+
2451
+ srcpath := args[1].(string)
2452
+ child := args[2]
2453
+
2454
+ // Source data.
2455
+ srcstore := GetProp(store, inj.Base, store)
2456
+ src := GetPath(srcpath, srcstore, inj)
2457
+ srctype := Typify(src)
2458
+
2459
+ // Create parallel data structures
2460
+ var tcur any
2461
+ var tval any
2462
+
2463
+ tkey := ""
2464
+ if len(inj.Path.List) >= 2 {
2465
+ tkey = inj.Path.List[len(inj.Path.List)-2]
2466
+ }
2467
+ var target any
2468
+ if len(inj.Nodes.List) >= 2 {
2469
+ target = inj.Nodes.List[len(inj.Nodes.List)-2]
2470
+ }
2471
+ if target == nil && len(inj.Nodes.List) > 0 {
2472
+ target = inj.Nodes.List[len(inj.Nodes.List)-1]
2473
+ }
2474
+
2475
+ // Create clones of the child template for each value of the current source.
2476
+ if 0 < (T_list & srctype) {
2477
+ srcList := _listify(src)
2478
+ newlist := make([]any, len(srcList))
2479
+ for i := range srcList {
2480
+ newlist[i] = Clone(child)
2481
+ }
2482
+ tval = newlist
2483
+ } else if 0 < (T_map & srctype) {
2484
+ srcItems := Items(src)
2485
+ newlist := make([]any, len(srcItems))
2486
+ for i, item := range srcItems {
2487
+ cclone := Clone(child)
2488
+ cclone = Merge([]any{
2489
+ cclone,
2490
+ map[string]any{S_BANNO: map[string]any{S_KEY: item[0]}},
2491
+ })
2492
+ newlist[i] = cclone
2493
+ }
2494
+ tval = newlist
2495
+ }
2496
+
2497
+ rval := []any{}
2498
+
2499
+ if tval != nil && len(_listify(tval)) > 0 {
2500
+ if src != nil {
2501
+ srcVals := make([]any, 0)
2502
+ if IsMap(src) {
2503
+ for _, item := range Items(src) {
2504
+ srcVals = append(srcVals, item[1])
2505
+ }
2506
+ } else {
2507
+ srcVals = _listify(src)
2508
+ }
2509
+ tcur = srcVals
2510
+ }
2511
+
2512
+ ckey := ""
2513
+ if len(inj.Path.List) >= 2 {
2514
+ ckey = inj.Path.List[len(inj.Path.List)-2]
2515
+ }
2516
+
2517
+ tpath := make([]string, len(inj.Path.List)-1)
2518
+ copy(tpath, inj.Path.List[:len(inj.Path.List)-1])
2519
+
2520
+ dpath := []string{S_DTOP}
2521
+ for _, p := range strings.Split(srcpath, S_DT) {
2522
+ dpath = append(dpath, p)
2523
+ }
2524
+ dpath = append(dpath, "$:"+ckey)
2525
+
2526
+ // Parent structure.
2527
+ tcur = map[string]any{ckey: tcur}
2528
+
2529
+ if len(tpath) > 1 {
2530
+ pkey := S_DTOP
2531
+ if len(inj.Path.List) >= 3 {
2532
+ pkey = inj.Path.List[len(inj.Path.List)-3]
2533
+ }
2534
+ tcur = map[string]any{pkey: tcur}
2535
+ dpath = append(dpath, "$:"+pkey)
2536
+ }
2537
+
2538
+ tinj := inj.child(0, []string{ckey})
2539
+ tinj.Path = &ListRef[string]{List: tpath}
2540
+
2541
+ tnodeslist := make([]any, 1)
2542
+ copy(tnodeslist, inj.Nodes.List[len(inj.Nodes.List)-1:])
2543
+ tinj.Nodes = &ListRef[any]{List: tnodeslist}
2544
+
2545
+ tinj.Parent = tinj.Nodes.List[len(tinj.Nodes.List)-1]
2546
+ SetProp(tinj.Parent, ckey, tval)
2547
+
2548
+ tinj.Val = tval
2549
+ tinj.Dpath = dpath
2550
+ tinj.Dparent = tcur
2551
+
2552
+ Inject(tval, store, tinj)
2553
+ rval = _listify(tinj.Val)
2554
+ }
2555
+
2556
+ SetProp(target, tkey, rval)
2557
+
2558
+ // Prevent callee from damaging first list entry (since we are in val mode).
2559
+ if len(rval) > 0 {
2560
+ return rval[0]
2561
+ }
2562
+ return nil
2563
+ }
2564
+
2565
+
2566
+ // transform_PACK => `$PACK`
2567
+ var Transform_PACK Injector = func(
2568
+ inj *Injection,
2569
+ val any,
2570
+ ref *string,
2571
+ store any,
2572
+ ) any {
2573
+ ijname := "EACH" // TS uses EACH for checkPlacement name
2574
+
2575
+ if !CheckPlacement(M_KEYPRE, ijname, T_map, inj) {
2576
+ return nil
2577
+ }
2578
+
2579
+ // Get arguments.
2580
+ args := GetProp(inj.Parent, inj.Key)
2581
+ argsList := _listify(args)
2582
+ injArgs := InjectorArgs([]int{T_string, T_any}, argsList)
2583
+ if injArgs[0] != nil {
2584
+ inj.Errs.Append("$" + ijname + ": " + injArgs[0].(string))
2585
+ return nil
2586
+ }
2587
+
2588
+ srcpath := injArgs[1].(string)
2589
+ origchildspec := injArgs[2]
2590
+
2591
+ // Find key and target node.
2592
+ tkey := ""
2593
+ if len(inj.Path.List) >= 2 {
2594
+ tkey = inj.Path.List[len(inj.Path.List)-2]
2595
+ }
2596
+ pathsize := len(inj.Path.List)
2597
+ var target any
2598
+ if pathsize >= 2 {
2599
+ target = inj.Nodes.List[pathsize-2]
2600
+ }
2601
+ if target == nil {
2602
+ target = inj.Nodes.List[pathsize-1]
2603
+ }
2604
+
2605
+ // Source data
2606
+ srcstore := GetProp(store, inj.Base, store)
2607
+ src := GetPath(srcpath, srcstore, inj)
2608
+
2609
+ // Prepare source as a list.
2610
+ if !IsList(src) {
2611
+ if IsMap(src) {
2612
+ srcItems := Items(src)
2613
+ srcList := make([]any, len(srcItems))
2614
+ for i, item := range srcItems {
2615
+ node := item[1]
2616
+ if IsMap(node) {
2617
+ SetProp(node, S_BANNO, map[string]any{S_KEY: item[0]})
2618
+ }
2619
+ srcList[i] = node
2620
+ }
2621
+ src = srcList
2622
+ } else {
2623
+ return nil
2624
+ }
2625
+ }
2626
+
2627
+ if src == nil {
2628
+ return nil
2629
+ }
2630
+
2631
+ // Get keypath.
2632
+ keypath := GetProp(origchildspec, S_BKEY)
2633
+ childspec := DelProp(origchildspec, S_BKEY)
2634
+
2635
+ child := GetProp(childspec, S_BVAL, childspec)
2636
+
2637
+ // Build parallel target object.
2638
+ tval := map[string]any{}
2639
+ srclist := _listify(src)
2640
+
2641
+ // Helper to resolve key for a src item at given index
2642
+ resolveKey := func(srcItem any, idx int) string {
2643
+ if keypath == nil {
2644
+ return strconv.Itoa(idx)
2645
+ }
2646
+ keypathStr, isStr := keypath.(string)
2647
+ if !isStr {
2648
+ return ""
2649
+ }
2650
+ if strings.HasPrefix(keypathStr, "`") {
2651
+ keyStore := Merge([]any{map[string]any{}, store, map[string]any{S_DTOP: srcItem}})
2652
+ keyResult := Inject(keypathStr, keyStore)
2653
+ if ks, ok := keyResult.(string); ok {
2654
+ return ks
2655
+ }
2656
+ } else {
2657
+ kval := GetPath(keypathStr, srcItem, inj)
2658
+ if ks, ok := kval.(string); ok {
2659
+ return ks
2660
+ }
2661
+ }
2662
+ return ""
2663
+ }
2664
+
2665
+ for i, srcItem := range srclist {
2666
+ srcnode := srcItem
2667
+ key := resolveKey(srcnode, i)
2668
+
2669
+ if key == "" {
2670
+ continue
2671
+ }
2672
+
2673
+ tchild := Clone(child)
2674
+ tval[key] = tchild
2675
+
2676
+ anno := GetProp(srcnode, S_BANNO)
2677
+ if anno == nil {
2678
+ if tchildMap, ok := tchild.(map[string]any); ok {
2679
+ delete(tchildMap, S_BANNO)
2680
+ }
2681
+ } else {
2682
+ if IsMap(tchild) {
2683
+ SetProp(tchild, S_BANNO, anno)
2684
+ }
2685
+ }
2686
+ }
2687
+
2688
+ rval := map[string]any{}
2689
+
2690
+ if !IsEmpty(tval) {
2691
+ // Build parallel source object
2692
+ tsrc := map[string]any{}
2693
+ for i, srcItem := range srclist {
2694
+ kn := resolveKey(srcItem, i)
2695
+ if kn != "" {
2696
+ tsrc[kn] = srcItem
2697
+ }
2698
+ }
2699
+
2700
+ tpath := make([]string, len(inj.Path.List)-1)
2701
+ copy(tpath, inj.Path.List[:len(inj.Path.List)-1])
2702
+
2703
+ ckey := ""
2704
+ if len(inj.Path.List) >= 2 {
2705
+ ckey = inj.Path.List[len(inj.Path.List)-2]
2706
+ }
2707
+
2708
+ dpath := []string{S_DTOP}
2709
+ for _, p := range strings.Split(srcpath, S_DT) {
2710
+ dpath = append(dpath, p)
2711
+ }
2712
+ dpath = append(dpath, "$:"+ckey)
2713
+
2714
+ tcur := map[string]any{ckey: tsrc}
2715
+
2716
+ if len(tpath) > 1 {
2717
+ pkey := S_DTOP
2718
+ if len(inj.Path.List) >= 3 {
2719
+ pkey = inj.Path.List[len(inj.Path.List)-3]
2720
+ }
2721
+ tcur = map[string]any{pkey: tcur}
2722
+ dpath = append(dpath, "$:"+pkey)
2723
+ }
2724
+
2725
+ tinj := inj.child(0, []string{ckey})
2726
+ tinj.Path = &ListRef[string]{List: tpath}
2727
+
2728
+ tnodeslist := make([]any, 1)
2729
+ copy(tnodeslist, inj.Nodes.List[len(inj.Nodes.List)-1:])
2730
+ tinj.Nodes = &ListRef[any]{List: tnodeslist}
2731
+
2732
+ tinj.Parent = tinj.Nodes.List[len(tinj.Nodes.List)-1]
2733
+ tinj.Val = tval
2734
+
2735
+ tinj.Dpath = dpath
2736
+ tinj.Dparent = tcur
2737
+
2738
+ Inject(tval, store, tinj)
2739
+ if r, ok := tinj.Val.(map[string]any); ok {
2740
+ rval = r
2741
+ }
2742
+ }
2743
+
2744
+ SetProp(target, tkey, rval)
2745
+
2746
+ // Drop transform key.
2747
+ return nil
2748
+ }
2749
+
2750
+ // transform_APPLY => `$APPLY`
2751
+ // Reference original spec (enables recursive transformations).
2752
+ // Format: ['`$REF`', '`spec-path`']
2753
+ var Transform_REF Injector = func(
2754
+ inj *Injection,
2755
+ val any,
2756
+ ref *string,
2757
+ store any,
2758
+ ) any {
2759
+ if inj.Mode != M_VAL {
2760
+ return nil
2761
+ }
2762
+
2763
+ // Get arguments: ['`$REF`', 'ref-path'].
2764
+ refpath := GetProp(inj.Parent, 1)
2765
+ inj.KeyI = len(inj.Keys.List)
2766
+
2767
+ // Spec reference.
2768
+ specFn := GetProp(store, S_DSPEC)
2769
+ if specFn == nil {
2770
+ return nil
2771
+ }
2772
+ var spec any
2773
+ if fn, ok := specFn.(func() any); ok {
2774
+ spec = fn()
2775
+ } else {
2776
+ return nil
2777
+ }
2778
+
2779
+ dpath := make([]string, 0)
2780
+ if len(inj.Path.List) > 1 {
2781
+ dpath = append(dpath, inj.Path.List[1:]...)
2782
+ }
2783
+ refResult := GetPath(refpath, spec, &Injection{
2784
+ Dpath: dpath,
2785
+ Dparent: GetPath(dpath, spec),
2786
+ })
2787
+
2788
+ hasSubRef := false
2789
+ if IsNode(refResult) {
2790
+ Walk(refResult, func(k *string, v any, parent any, path []string) any {
2791
+ if s, ok := v.(string); ok && s == "`$REF`" {
2792
+ hasSubRef = true
2793
+ }
2794
+ return v
2795
+ })
2796
+ }
2797
+
2798
+ tref := Clone(refResult)
2799
+
2800
+ cpath := make([]string, 0)
2801
+ if len(inj.Path.List) > 3 {
2802
+ cpath = append(cpath, inj.Path.List[:len(inj.Path.List)-3]...)
2803
+ }
2804
+ tpath := make([]string, 0)
2805
+ if len(inj.Path.List) >= 1 {
2806
+ tpath = append(tpath, inj.Path.List[:len(inj.Path.List)]...)
2807
+ }
2808
+ tpath = tpath[:len(tpath)-1]
2809
+
2810
+ tcur := GetPath(cpath, store)
2811
+ tval := GetPath(tpath, store)
2812
+
2813
+ var rval any
2814
+
2815
+ if !hasSubRef || tval != nil {
2816
+ lastPath := S_DTOP
2817
+ if len(tpath) > 0 {
2818
+ lastPath = tpath[len(tpath)-1]
2819
+ }
2820
+ tinj := inj.child(0, []string{lastPath})
2821
+ tinj.Path = &ListRef[string]{List: tpath}
2822
+
2823
+ // TS: tinj.nodes = slice(inj.nodes, -1) → nodes[0:len-1] (all except last)
2824
+ nodesLen := len(inj.Nodes.List)
2825
+ if nodesLen > 1 {
2826
+ tnodeslist := make([]any, nodesLen-1)
2827
+ copy(tnodeslist, inj.Nodes.List[:nodesLen-1])
2828
+ tinj.Nodes = &ListRef[any]{List: tnodeslist}
2829
+ } else {
2830
+ tinj.Nodes = &ListRef[any]{List: []any{}}
2831
+ }
2832
+
2833
+ // TS: tinj.parent = getelem(nodes, -2)
2834
+ if nodesLen >= 2 {
2835
+ tinj.Parent = inj.Nodes.List[nodesLen-2]
2836
+ }
2837
+ tinj.Val = tref
2838
+
2839
+ tinj.Dpath = cpath
2840
+ tinj.Dparent = tcur
2841
+
2842
+ Inject(tref, store, tinj)
2843
+ rval = tinj.Val
2844
+ }
2845
+
2846
+ grandparent := inj.setval(rval, 2)
2847
+
2848
+ if IsList(grandparent) && inj.Prior != nil {
2849
+ inj.Prior.KeyI--
2850
+ }
2851
+
2852
+ return val
2853
+ }
2854
+
2855
+ var Transform_APPLY Injector = func(
2856
+ inj *Injection,
2857
+ val any,
2858
+ ref *string,
2859
+ store any,
2860
+ ) any {
2861
+ ijname := "APPLY"
2862
+
2863
+ // Skip remaining keys
2864
+ if inj.Keys != nil && len(inj.Keys.List) > 0 {
2865
+ inj.Keys.List = inj.Keys.List[:1]
2866
+ }
2867
+
2868
+ if !CheckPlacement(M_VAL, ijname, T_list, inj) {
2869
+ return nil
2870
+ }
2871
+
2872
+ parentList := _listify(inj.Parent)
2873
+ var sliced []any
2874
+ if len(parentList) > 1 {
2875
+ sliced = parentList[1:]
2876
+ }
2877
+ args := InjectorArgs([]int{T_function, T_any}, sliced)
2878
+
2879
+ tkey := ""
2880
+ if len(inj.Path.List) >= 2 {
2881
+ tkey = inj.Path.List[len(inj.Path.List)-2]
2882
+ }
2883
+ var target any
2884
+ if len(inj.Nodes.List) >= 2 {
2885
+ target = inj.Nodes.List[len(inj.Nodes.List)-2]
2886
+ }
2887
+
2888
+ if args[0] != nil {
2889
+ inj.Errs.Append("$" + ijname + ": " + args[0].(string))
2890
+ if target != nil {
2891
+ DelProp(target, tkey)
2892
+ }
2893
+ return nil
2894
+ }
2895
+
2896
+ applyFn := args[1]
2897
+ child := args[2]
2898
+
2899
+ // Resolve child via injection
2900
+ cinj := InjectChild(child, store, inj)
2901
+ resolved := cinj.Val
2902
+
2903
+ // Call the apply function
2904
+ fn := reflect.ValueOf(applyFn)
2905
+ fnType := fn.Type()
2906
+
2907
+ var out any
2908
+ switch fnType.NumIn() {
2909
+ case 1:
2910
+ results := fn.Call([]reflect.Value{reflect.ValueOf(resolved)})
2911
+ if len(results) > 0 {
2912
+ out = results[0].Interface()
2913
+ }
2914
+ case 3:
2915
+ results := fn.Call([]reflect.Value{
2916
+ reflect.ValueOf(resolved),
2917
+ reflect.ValueOf(store),
2918
+ reflect.ValueOf(cinj),
2919
+ })
2920
+ if len(results) > 0 {
2921
+ out = results[0].Interface()
2922
+ }
2923
+ default:
2924
+ results := fn.Call([]reflect.Value{reflect.ValueOf(resolved)})
2925
+ if len(results) > 0 {
2926
+ out = results[0].Interface()
2927
+ }
2928
+ }
2929
+
2930
+ // Set on parent output
2931
+ if target != nil {
2932
+ SetProp(target, tkey, out)
2933
+ }
2934
+
2935
+ return out
2936
+ }
2937
+
2938
+
2939
+ // transform_FORMAT => `$FORMAT`
2940
+ // injectChild resolves a child value via injection, going up the injection chain
2941
+ // to get the correct data context.
2942
+ func InjectChild(child any, store any, inj *Injection) *Injection {
2943
+ cinj := inj
2944
+
2945
+ if inj.Prior != nil {
2946
+ if inj.Prior.Prior != nil {
2947
+ cinj = inj.Prior.Prior.child(inj.Prior.KeyI, inj.Prior.Keys.List)
2948
+ cinj.Val = child
2949
+ SetProp(cinj.Parent, inj.Prior.Key, child)
2950
+ } else {
2951
+ cinj = inj.Prior.child(inj.KeyI, inj.Keys.List)
2952
+ cinj.Val = child
2953
+ SetProp(cinj.Parent, inj.Key, child)
2954
+ }
2955
+ }
2956
+
2957
+ Inject(child, store, cinj)
2958
+
2959
+ return cinj
2960
+ }
2961
+
2962
+ var Transform_FORMAT Injector = func(
2963
+ inj *Injection,
2964
+ val any,
2965
+ ref *string,
2966
+ store any,
2967
+ ) any {
2968
+ // Remove remaining keys to avoid spurious processing.
2969
+ if inj.Keys != nil && len(inj.Keys.List) > 0 {
2970
+ inj.Keys.List = inj.Keys.List[:1]
2971
+ }
2972
+
2973
+ if inj.Mode != M_VAL {
2974
+ return nil
2975
+ }
2976
+
2977
+ // Get arguments: ['`$FORMAT`', 'name', child].
2978
+ name := GetProp(inj.Parent, 1)
2979
+ child := GetProp(inj.Parent, 2)
2980
+
2981
+ // Resolve child via injection using injectChild
2982
+ cinj := InjectChild(child, store, inj)
2983
+ resolved := cinj.Val
2984
+
2985
+ tkey := ""
2986
+ if len(inj.Path.List) >= 2 {
2987
+ tkey = inj.Path.List[len(inj.Path.List)-2]
2988
+ }
2989
+ var target any
2990
+ if len(inj.Nodes.List) >= 2 {
2991
+ target = inj.Nodes.List[len(inj.Nodes.List)-2]
2992
+ }
2993
+ if target == nil && len(inj.Nodes.List) > 0 {
2994
+ target = inj.Nodes.List[len(inj.Nodes.List)-1]
2995
+ }
2996
+
2997
+ // Convert nil to "null" string for formatting purposes
2998
+ _fmtStr := func(v any) string {
2999
+ if v == nil {
3000
+ return "null"
3001
+ }
3002
+ return fmt.Sprint(v)
3003
+ }
3004
+
3005
+ // Get formatter
3006
+ var formatter func(key *string, val any, parent any, path []string) any
3007
+
3008
+ if IsFunc(name) {
3009
+ fn := reflect.ValueOf(name)
3010
+ formatter = func(key *string, val any, parent any, path []string) any {
3011
+ results := fn.Call([]reflect.Value{
3012
+ reflect.ValueOf(key),
3013
+ reflect.ValueOf(val),
3014
+ reflect.ValueOf(parent),
3015
+ reflect.ValueOf(path),
3016
+ })
3017
+ if len(results) > 0 {
3018
+ return results[0].Interface()
3019
+ }
3020
+ return val
3021
+ }
3022
+ } else if nameStr, ok := name.(string); ok {
3023
+ switch nameStr {
3024
+ case "upper":
3025
+ formatter = func(_ *string, val any, _ any, _ []string) any {
3026
+ if IsNode(val) {
3027
+ return val
3028
+ }
3029
+ return strings.ToUpper(_fmtStr(val))
3030
+ }
3031
+ case "lower":
3032
+ formatter = func(_ *string, val any, _ any, _ []string) any {
3033
+ if IsNode(val) {
3034
+ return val
3035
+ }
3036
+ return strings.ToLower(_fmtStr(val))
3037
+ }
3038
+ case "string":
3039
+ formatter = func(_ *string, val any, _ any, _ []string) any {
3040
+ if IsNode(val) {
3041
+ return val
3042
+ }
3043
+ return _fmtStr(val)
3044
+ }
3045
+ case "number":
3046
+ formatter = func(_ *string, val any, _ any, _ []string) any {
3047
+ if IsNode(val) {
3048
+ return val
3049
+ }
3050
+ switch v := val.(type) {
3051
+ case int:
3052
+ return v
3053
+ case float64:
3054
+ return v
3055
+ case string:
3056
+ n, err := strconv.ParseFloat(v, 64)
3057
+ if err != nil {
3058
+ return 0
3059
+ }
3060
+ if n == float64(int(n)) {
3061
+ return int(n)
3062
+ }
3063
+ return n
3064
+ default:
3065
+ return 0
3066
+ }
3067
+ }
3068
+ case "integer":
3069
+ formatter = func(_ *string, val any, _ any, _ []string) any {
3070
+ if IsNode(val) {
3071
+ return val
3072
+ }
3073
+ switch v := val.(type) {
3074
+ case int:
3075
+ return v
3076
+ case float64:
3077
+ return int(v)
3078
+ case string:
3079
+ n, err := strconv.ParseFloat(v, 64)
3080
+ if err != nil {
3081
+ return 0
3082
+ }
3083
+ return int(n)
3084
+ default:
3085
+ return 0
3086
+ }
3087
+ }
3088
+ case "concat":
3089
+ formatter = func(key *string, val any, _ any, _ []string) any {
3090
+ if key == nil && IsList(val) {
3091
+ parts := []string{}
3092
+ list := _listify(val)
3093
+ for _, v := range list {
3094
+ if IsNode(v) {
3095
+ parts = append(parts, "")
3096
+ } else {
3097
+ parts = append(parts, _fmtStr(v))
3098
+ }
3099
+ }
3100
+ return strings.Join(parts, "")
3101
+ }
3102
+ return val
3103
+ }
3104
+ case "identity":
3105
+ formatter = func(_ *string, val any, _ any, _ []string) any {
3106
+ return val
3107
+ }
3108
+ default:
3109
+ inj.Errs.Append("$FORMAT: unknown format: " + nameStr + ".")
3110
+ if target != nil {
3111
+ DelProp(target, tkey)
3112
+ }
3113
+ return nil
3114
+ }
3115
+ } else {
3116
+ inj.Errs.Append("$FORMAT: unknown format: " + Stringify(name) + ".")
3117
+ if target != nil {
3118
+ DelProp(target, tkey)
3119
+ }
3120
+ return nil
3121
+ }
3122
+
3123
+ var out any
3124
+ out = Walk(resolved, formatter)
3125
+
3126
+ // Set on parent output
3127
+ if target != nil {
3128
+ SetProp(target, tkey, out)
3129
+ }
3130
+
3131
+ return out
3132
+ }
3133
+
3134
+
3135
+ // ---------------------------------------------------------------------
3136
+ // Transform function: top-level
3137
+
3138
+ func Transform(
3139
+ data any, // source data
3140
+ spec any, // transform specification
3141
+ injdefs ...*Injection,
3142
+ ) any {
3143
+ if len(injdefs) > 0 && injdefs[0] != nil {
3144
+ injdef := injdefs[0]
3145
+ return TransformModifyHandler(
3146
+ data,
3147
+ spec,
3148
+ injdef.Extra,
3149
+ injdef.Modify,
3150
+ injdef.Handler,
3151
+ injdef.Errs,
3152
+ injdef.Meta,
3153
+ )
3154
+ }
3155
+ return TransformModify(data, spec, nil, nil)
3156
+ }
3157
+
3158
+ // TransformModifyHandler is like TransformModify but allows a custom handler and injection inj.
3159
+ func TransformModifyHandler(
3160
+ data any,
3161
+ spec any,
3162
+ extra any,
3163
+ modify Modify,
3164
+ handler Injector,
3165
+ errs *ListRef[any],
3166
+ meta map[string]any,
3167
+ ) any {
3168
+ // Clone and wrap
3169
+ wrapFlags := map[string]bool{"wrap": true}
3170
+ origspec := spec
3171
+ spec = CloneFlags(spec, wrapFlags)
3172
+
3173
+ // Split extra transforms from extra data
3174
+ extraTransforms := map[string]any{}
3175
+ extraData := map[string]any{}
3176
+
3177
+ if extra != nil {
3178
+ pairs := Items(extra)
3179
+ for _, kv := range pairs {
3180
+ k, _ := kv[0].(string)
3181
+ v := kv[1]
3182
+ if strings.HasPrefix(k, S_DS) {
3183
+ extraTransforms[k] = v
3184
+ } else {
3185
+ extraData[k] = v
3186
+ }
3187
+ }
3188
+ }
3189
+
3190
+ if extraData == nil {
3191
+ extraData = map[string]any{}
3192
+ }
3193
+
3194
+ var dataClone any
3195
+ if data == nil {
3196
+ dataClone = nil
3197
+ } else {
3198
+ dataClone = Merge([]any{
3199
+ CloneFlags(extraData, wrapFlags),
3200
+ CloneFlags(data, wrapFlags),
3201
+ })
3202
+ }
3203
+
3204
+ // Save original spec for $REF
3205
+ _ = origspec
3206
+
3207
+ store := map[string]any{
3208
+ S_DTOP: dataClone,
3209
+ S_DSPEC: func() any { return origspec },
3210
+ "$BT": func() any { return S_BT },
3211
+ "$DS": func() any { return S_DS },
3212
+ "$WHEN": func() any {
3213
+ return time.Now().UTC().Format(time.RFC3339)
3214
+ },
3215
+ "$DELETE": Transform_DELETE,
3216
+ "$COPY": Transform_COPY,
3217
+ "$KEY": Transform_KEY,
3218
+ "$META": Transform_META,
3219
+ "$ANNO": Transform_ANNO,
3220
+ "$MERGE": Transform_MERGE,
3221
+ "$EACH": Transform_EACH,
3222
+ "$PACK": Transform_PACK,
3223
+ "$REF": Transform_REF,
3224
+ "$APPLY": Transform_APPLY,
3225
+ "$FORMAT": Transform_FORMAT,
3226
+ }
3227
+
3228
+ for k, v := range extraTransforms {
3229
+ store[k] = v
3230
+ }
3231
+
3232
+ if errs == nil {
3233
+ errs = ListRefCreate[any]()
3234
+ }
3235
+ store[S_DERRS] = errs
3236
+
3237
+ // Create injection inj with handler
3238
+ injState := &Injection{
3239
+ Modify: modify,
3240
+ Handler: handler,
3241
+ Errs: errs,
3242
+ Meta: meta,
3243
+ }
3244
+
3245
+ out := Inject(spec, store, injState)
3246
+ out = CloneFlags(out, map[string]bool{"unwrap": true})
3247
+ return out
3248
+ }
3249
+
3250
+ // transformModifyCore is the internal implementation that collects errors.
3251
+ func transformModifyCore(
3252
+ data any,
3253
+ spec any,
3254
+ extra any,
3255
+ modify Modify,
3256
+ ) (any, *ListRef[any]) {
3257
+
3258
+ // Clone and wrap: clone the structures and convert bare lists to ListRefs
3259
+ // for reference stability, in a single pass.
3260
+ wrapFlags := map[string]bool{"wrap": true}
3261
+
3262
+ // Save original spec for $REF as a separate wrapped clone (not modified by injection)
3263
+ origspec := CloneFlags(spec, wrapFlags)
3264
+ spec = CloneFlags(spec, wrapFlags)
3265
+
3266
+ // Split extra transforms from extra data
3267
+ extraTransforms := map[string]any{}
3268
+ extraData := map[string]any{}
3269
+
3270
+ if extra != nil {
3271
+ pairs := Items(extra)
3272
+ for _, kv := range pairs {
3273
+ k, _ := kv[0].(string)
3274
+ v := kv[1]
3275
+ if strings.HasPrefix(k, S_DS) {
3276
+ extraTransforms[k] = v
3277
+ } else {
3278
+ extraData[k] = v
3279
+ }
3280
+ }
3281
+ }
3282
+
3283
+ // Create empty maps if nil
3284
+ if extraData == nil {
3285
+ extraData = map[string]any{}
3286
+ }
3287
+ if data == nil {
3288
+ data = map[string]any{}
3289
+ }
3290
+
3291
+ // Merge extraData + data, clone+wrap in one pass
3292
+ dataClone := Merge([]any{
3293
+ CloneFlags(extraData, wrapFlags),
3294
+ CloneFlags(data, wrapFlags),
3295
+ })
3296
+
3297
+ // Collect errors from transform operations
3298
+ errs := ListRefCreate[any]()
3299
+
3300
+ // The injection store with transform functions
3301
+ store := map[string]any{
3302
+ // Merged data is at $TOP
3303
+ S_DTOP: dataClone,
3304
+
3305
+ // Reference to original spec for $REF
3306
+ S_DSPEC: func() any { return origspec },
3307
+
3308
+ // Handy escapes
3309
+ "$BT": func() any { return S_BT },
3310
+ "$DS": func() any { return S_DS },
3311
+
3312
+ // Insert current date/time
3313
+ "$WHEN": func() any {
3314
+ return time.Now().UTC().Format(time.RFC3339)
3315
+ },
3316
+
3317
+ // Built-in transform functions
3318
+ "$DELETE": Transform_DELETE,
3319
+ "$COPY": Transform_COPY,
3320
+ "$KEY": Transform_KEY,
3321
+ "$META": Transform_META,
3322
+ "$ANNO": Transform_ANNO,
3323
+ "$MERGE": Transform_MERGE,
3324
+ "$EACH": Transform_EACH,
3325
+ "$PACK": Transform_PACK,
3326
+ "$REF": Transform_REF,
3327
+ "$APPLY": Transform_APPLY,
3328
+ "$FORMAT": Transform_FORMAT,
3329
+ }
3330
+
3331
+ // Add any extra transforms
3332
+ for k, v := range extraTransforms {
3333
+ store[k] = v
3334
+ }
3335
+
3336
+ // Pass errs via injection inj to avoid Merge converting ListRef to []any
3337
+ injState := &Injection{
3338
+ Modify: modify,
3339
+ Errs: errs,
3340
+ }
3341
+
3342
+ out := Inject(spec, store, injState)
3343
+
3344
+ // Clone output, unwrapping ListRefs back to bare lists.
3345
+ out = CloneFlags(out, map[string]bool{"unwrap": true})
3346
+
3347
+ return out, errs
3348
+ }
3349
+
3350
+ func TransformModify(
3351
+ data any, // source data
3352
+ spec any, // transform specification
3353
+ extra any, // extra store
3354
+ modify Modify, // optional modify
3355
+ ) any {
3356
+ out, _ := transformModifyCore(data, spec, extra, modify)
3357
+ return out
3358
+ }
3359
+
3360
+ // TransformCollect is like Transform but also returns collected error strings.
3361
+ func TransformCollect(
3362
+ data any,
3363
+ spec any,
3364
+ ) (any, []string) {
3365
+ out, errs := transformModifyCore(data, spec, nil, nil)
3366
+ errStrs := make([]string, 0, len(errs.List))
3367
+ for _, e := range errs.List {
3368
+ if s, ok := e.(string); ok {
3369
+ errStrs = append(errStrs, s)
3370
+ }
3371
+ }
3372
+ return out, errStrs
3373
+ }
3374
+
3375
+ var validate_STRING Injector = func(
3376
+ inj *Injection,
3377
+ _val any,
3378
+ ref *string,
3379
+ store any,
3380
+ ) any {
3381
+ out := GetProp(inj.Dparent, inj.Key)
3382
+
3383
+ t := Typify(out)
3384
+ if 0 == (T_string & t) {
3385
+ msg := _invalidTypeMsg(inj.Path.List, S_string, Typename(t), out)
3386
+ inj.Errs.Append(msg)
3387
+ return nil
3388
+ }
3389
+
3390
+ if S_MT == out.(string) {
3391
+ msg := "Empty string at " + Pathify(inj.Path.List, 1)
3392
+ inj.Errs.Append(msg)
3393
+ return nil
3394
+ }
3395
+
3396
+ return out
3397
+ }
3398
+
3399
+ var validate_NUMBER Injector = func(
3400
+ inj *Injection,
3401
+ _val any,
3402
+ ref *string,
3403
+ store any,
3404
+ ) any {
3405
+ out := GetProp(inj.Dparent, inj.Key)
3406
+
3407
+ t := Typify(out)
3408
+ if 0 == (T_number & t) {
3409
+ msg := _invalidTypeMsg(inj.Path.List, S_number, Typename(t), out)
3410
+ inj.Errs.Append(msg)
3411
+ return nil
3412
+ }
3413
+
3414
+ return out
3415
+ }
3416
+
3417
+ var validate_BOOLEAN Injector = func(
3418
+ inj *Injection,
3419
+ _val any,
3420
+ ref *string,
3421
+ store any,
3422
+ ) any {
3423
+ out := GetProp(inj.Dparent, inj.Key)
3424
+
3425
+ t := Typify(out)
3426
+ if 0 == (T_boolean & t) {
3427
+ msg := _invalidTypeMsg(inj.Path.List, S_boolean, Typename(t), out)
3428
+ inj.Errs.Append(msg)
3429
+ return nil
3430
+ }
3431
+
3432
+ return out
3433
+ }
3434
+
3435
+ var validate_OBJECT Injector = func(
3436
+ inj *Injection,
3437
+ _val any,
3438
+ ref *string,
3439
+ store any,
3440
+ ) any {
3441
+ out := GetProp(inj.Dparent, inj.Key)
3442
+
3443
+ t := Typify(out)
3444
+
3445
+ if 0 == (T_map & t) {
3446
+ msg := _invalidTypeMsg(inj.Path.List, S_object, Typename(t), out)
3447
+ inj.Errs.Append(msg)
3448
+
3449
+ return nil
3450
+ }
3451
+
3452
+ return out
3453
+ }
3454
+
3455
+ var validate_ARRAY Injector = func(
3456
+ inj *Injection,
3457
+ _val any,
3458
+ ref *string,
3459
+ store any,
3460
+ ) any {
3461
+ out := GetProp(inj.Dparent, inj.Key)
3462
+
3463
+ t := Typify(out)
3464
+ if 0 == (T_list & t) {
3465
+ msg := _invalidTypeMsg(inj.Path.List, S_array, Typename(t), out)
3466
+ inj.Errs.Append(msg)
3467
+ return nil
3468
+ }
3469
+
3470
+ return out
3471
+ }
3472
+
3473
+ var validate_FUNCTION Injector = func(
3474
+ inj *Injection,
3475
+ _val any,
3476
+ ref *string,
3477
+ store any,
3478
+ ) any {
3479
+ out := GetProp(inj.Dparent, inj.Key)
3480
+
3481
+ t := Typify(out)
3482
+ if 0 == (T_function & t) {
3483
+ msg := _invalidTypeMsg(inj.Path.List, S_function, Typename(t), out)
3484
+ inj.Errs.Append(msg)
3485
+ return nil
3486
+ }
3487
+
3488
+ return out
3489
+ }
3490
+
3491
+ var validate_ANY Injector = func(
3492
+ inj *Injection,
3493
+ _val any,
3494
+ ref *string,
3495
+ store any,
3496
+ ) any {
3497
+ return GetProp(inj.Dparent, inj.Key)
3498
+ }
3499
+
3500
+ // Generic type validator: handles $INTEGER, $DECIMAL, $NULL, $NIL, $MAP, $LIST, $INSTANCE
3501
+ var validate_TYPE Injector = func(
3502
+ inj *Injection,
3503
+ _val any,
3504
+ ref *string,
3505
+ store any,
3506
+ ) any {
3507
+ if ref == nil {
3508
+ return nil
3509
+ }
3510
+
3511
+ tname := strings.ToLower((*ref)[1:]) // e.g. "$DECIMAL" → "decimal"
3512
+
3513
+ // Find the type bit from the TYPENAME array
3514
+ var typev int
3515
+ for i, name := range TYPENAME {
3516
+ if name == tname {
3517
+ typev = 1 << (31 - i)
3518
+ break
3519
+ }
3520
+ }
3521
+
3522
+ out := GetProp(inj.Dparent, inj.Key)
3523
+ t := Typify(out)
3524
+
3525
+ // In Go, nil represents both null and noval (undefined).
3526
+ // $NIL should match nil values, and $NULL should also match nil.
3527
+ if tname == "nil" && out == nil {
3528
+ return out
3529
+ }
3530
+ if tname == "null" && out == nil {
3531
+ return out
3532
+ }
3533
+
3534
+ if 0 == (t & typev) {
3535
+ msg := _invalidTypeMsg(inj.Path.List, tname, Typename(t), out)
3536
+ inj.Errs.Append(msg)
3537
+ return nil
3538
+ }
3539
+
3540
+ return out
3541
+ }
3542
+
3543
+ var validate_CHILD Injector = func(
3544
+ inj *Injection,
3545
+ _val any,
3546
+ ref *string,
3547
+ store any,
3548
+ ) any {
3549
+ // Map syntax
3550
+ if inj.Mode == M_KEYPRE {
3551
+ child := GetProp(inj.Parent, inj.Key)
3552
+
3553
+ pkey := inj.Path.List[len(inj.Path.List)-2]
3554
+ tval := GetProp(inj.Dparent, pkey)
3555
+
3556
+ if nil == tval {
3557
+ tval = map[string]any{}
3558
+
3559
+ } else if !IsMap(tval) {
3560
+ inj.Errs.Append(
3561
+ _invalidTypeMsg(
3562
+ inj.Path.List[:len(inj.Path.List)-1],
3563
+ S_object,
3564
+ Typename(Typify(tval)),
3565
+ tval,
3566
+ ))
3567
+ return nil
3568
+ }
3569
+
3570
+ // For each key in tval, clone the child into parent
3571
+ ckeys := KeysOf(tval)
3572
+ for _, ckey := range ckeys {
3573
+ SetProp(inj.Parent, ckey, Clone(child))
3574
+ inj.Keys.Append(ckey)
3575
+ }
3576
+
3577
+ DelProp(inj.Parent, inj.Key)
3578
+
3579
+ return nil
3580
+ }
3581
+
3582
+ // List syntax
3583
+ if inj.Mode == M_VAL {
3584
+
3585
+ // We expect 'parent' to be a slice of any, like ["`$CHILD`", childTemplate].
3586
+ if !IsList(inj.Parent) {
3587
+ inj.Errs.Append("Invalid $CHILD as value")
3588
+ return nil
3589
+ }
3590
+
3591
+ child := GetProp(inj.Parent, 1)
3592
+ dparent := inj.Dparent
3593
+
3594
+ // If dparent is nil => empty list default
3595
+ if nil == dparent {
3596
+ if lr, ok := inj.Parent.(*ListRef[any]); ok {
3597
+ lr.List = []any{}
3598
+ } else {
3599
+ inj.Parent = []any{}
3600
+ }
3601
+ return nil
3602
+ }
3603
+
3604
+ // If dparent is not a list => error
3605
+ if !IsList(dparent) {
3606
+ inj.Errs.Append(
3607
+ _invalidTypeMsg(
3608
+ inj.Path.List[:len(inj.Path.List)-1],
3609
+ S_array,
3610
+ Typename(Typify(dparent)),
3611
+ dparent,
3612
+ ))
3613
+ parentList := _listify(inj.Parent)
3614
+ inj.KeyI = len(parentList)
3615
+ return dparent
3616
+ }
3617
+
3618
+ // Otherwise, dparent is a list => clone child for each element
3619
+ currentList := _listify(dparent)
3620
+ length := len(currentList)
3621
+
3622
+ // Make a new slice to hold the child clones, sized to length
3623
+ newParent := make([]any, length)
3624
+ for i := 0; i < length; i++ {
3625
+ newParent[i] = Clone(child)
3626
+ }
3627
+
3628
+ // Replace parent with the new slice
3629
+ if lr, ok := inj.Parent.(*ListRef[any]); ok {
3630
+ lr.List = newParent
3631
+ } else {
3632
+ inj.Parent = newParent
3633
+ }
3634
+
3635
+ inj.KeyI = 0
3636
+
3637
+ out := GetProp(dparent, 0)
3638
+ return out
3639
+ }
3640
+
3641
+ return nil
3642
+ }
3643
+
3644
+ // Forward declaration for validate_ONE
3645
+ var validate_ONE Injector
3646
+
3647
+ // Forward declaration for validate_EXACT
3648
+ var validate_EXACT Injector
3649
+
3650
+ // Implementation will be set after Validate is defined
3651
+ func init_validate_ONE() {
3652
+ validate_ONE = func(
3653
+ inj *Injection,
3654
+ _val any,
3655
+ ref *string,
3656
+ store any,
3657
+ ) any {
3658
+ // Only operate in "val mode" (list mode).
3659
+ if inj.Mode == M_VAL {
3660
+ // Validate that parent is a list and we're at the first element
3661
+ if !IsList(inj.Parent) || inj.KeyI != 0 {
3662
+ inj.Errs.Append("The $ONE validator at field " +
3663
+ Pathify(inj.Path.List, 1, 1) +
3664
+ " must be the first element of an array.")
3665
+ return nil
3666
+ }
3667
+
3668
+ // Once we handle `$ONE`, we skip further iteration by setting KeyI to keys.length
3669
+ inj.KeyI = len(inj.Keys.List)
3670
+
3671
+ // The parent is assumed to be a slice: ["`$ONE`", alt0, alt1, ...].
3672
+ parentSlice, ok := _asList(inj.Parent)
3673
+ if !ok {
3674
+ return nil
3675
+ }
3676
+
3677
+ // Get grandparent and grandkey to replace the structure
3678
+ grandparent := inj.Nodes.List[len(inj.Nodes.List)-2]
3679
+ grandkey := inj.Path.List[len(inj.Path.List)-2]
3680
+
3681
+ // Clean up structure by replacing [$ONE, ...] with current value
3682
+ SetProp(grandparent, grandkey, inj.Dparent)
3683
+ inj.Parent = inj.Dparent
3684
+
3685
+ // Adjust the path
3686
+ inj.Path.List = inj.Path.List[:len(inj.Path.List)-1]
3687
+ inj.Key = inj.Path.List[len(inj.Path.List)-1]
3688
+
3689
+ // The shape alternatives are everything after the first element.
3690
+ tvals := parentSlice[1:] // alt0, alt1, ...
3691
+
3692
+ // Ensure we have at least one alternative
3693
+ if len(tvals) == 0 {
3694
+ inj.Errs.Append("The $ONE validator at field " +
3695
+ Pathify(inj.Path.List, 1, 1) +
3696
+ " must have at least one argument.")
3697
+ return nil
3698
+ }
3699
+
3700
+ // Try each alternative shape
3701
+ for _, tval := range tvals {
3702
+ // Collect errors in a temporary slice
3703
+ var terrs = ListRefCreate[any]()
3704
+
3705
+ // Create a new store for validation
3706
+ vstore := Clone(store).(map[string]any)
3707
+ vstore["$TOP"] = inj.Dparent
3708
+
3709
+ // Attempt validation of data with shape `tval`
3710
+ vcurrent, err := Validate(inj.Dparent, tval, &Injection{Extra: vstore, Errs: terrs})
3711
+
3712
+ // Update the value in the grandparent
3713
+ SetProp(grandparent, grandkey, vcurrent)
3714
+
3715
+ // If no errors, we found a match
3716
+ if err == nil && len(terrs.List) == 0 {
3717
+ return nil
3718
+ }
3719
+ }
3720
+
3721
+ // If we get here, there was no match
3722
+ mapped := make([]string, len(tvals))
3723
+ for i, v := range tvals {
3724
+ mapped[i] = Stringify(v)
3725
+ }
3726
+
3727
+ joined := strings.Join(mapped, ", ")
3728
+
3729
+ re := regexp.MustCompile("`\\$([A-Z]+)`")
3730
+ valdesc := re.ReplaceAllStringFunc(joined, func(match string) string {
3731
+ submatches := re.FindStringSubmatch(match)
3732
+ if len(submatches) == 2 {
3733
+ return strings.ToLower(submatches[1])
3734
+ }
3735
+ return match
3736
+ })
3737
+
3738
+ prefix := ""
3739
+ if len(tvals) > 1 {
3740
+ prefix = "one of "
3741
+ }
3742
+
3743
+ msg := _invalidTypeMsg(
3744
+ inj.Path.List,
3745
+ prefix+valdesc,
3746
+ Typename(Typify(inj.Dparent)),
3747
+ inj.Dparent,
3748
+ "V0210",
3749
+ )
3750
+ inj.Errs.Append(msg)
3751
+ }
3752
+
3753
+ return nil
3754
+ }
3755
+ }
3756
+
3757
+ func init_validate_EXACT() {
3758
+ validate_EXACT = func(
3759
+ inj *Injection,
3760
+ _val any,
3761
+ ref *string,
3762
+ _store any,
3763
+ ) any {
3764
+ // Only operate in "val mode" (list mode).
3765
+ if inj.Mode == M_VAL {
3766
+ // Validate that parent is a list and we're at the first element
3767
+ if !IsList(inj.Parent) || inj.KeyI != 0 {
3768
+ inj.Errs.Append("The $EXACT validator at field " +
3769
+ Pathify(inj.Path.List, 1, 1) +
3770
+ " must be the first element of an array.")
3771
+ return nil
3772
+ }
3773
+
3774
+ // Once we handle `$EXACT`, we skip further iteration by setting KeyI to keys.length
3775
+ inj.KeyI = len(inj.Keys.List)
3776
+
3777
+ // The parent is assumed to be a slice: ["`$EXACT`", alt0, alt1, ...].
3778
+ parentSlice, ok := _asList(inj.Parent)
3779
+ if !ok {
3780
+ return nil
3781
+ }
3782
+
3783
+ // Get grandparent and grandkey to replace the structure
3784
+ grandparent := inj.Nodes.List[len(inj.Nodes.List)-2]
3785
+ grandkey := inj.Path.List[len(inj.Path.List)-2]
3786
+
3787
+ // Clean up structure by replacing [$EXACT, ...] with current value
3788
+ SetProp(grandparent, grandkey, inj.Dparent)
3789
+ inj.Parent = inj.Dparent
3790
+
3791
+ // Adjust the path
3792
+ inj.Path.List = inj.Path.List[:len(inj.Path.List)-1]
3793
+ inj.Key = inj.Path.List[len(inj.Path.List)-1]
3794
+
3795
+ // The exact values to match are everything after the first element.
3796
+ tvals := parentSlice[1:] // alt0, alt1, ...
3797
+
3798
+ // Ensure we have at least one alternative
3799
+ if len(tvals) == 0 {
3800
+ inj.Errs.Append("The $EXACT validator at field " +
3801
+ Pathify(inj.Path.List, 1, 1) +
3802
+ " must have at least one argument.")
3803
+ return nil
3804
+ }
3805
+
3806
+ // See if we can find an exact value match
3807
+ var currentStr *string
3808
+ for _, tval := range tvals {
3809
+ exactMatch := false
3810
+
3811
+ if !exactMatch {
3812
+ // Unwrap ListRefs for comparison since data and spec may have
3813
+ // different wrapping levels.
3814
+ unwrapFlags := map[string]bool{"unwrap": true}
3815
+ utval := CloneFlags(tval, unwrapFlags)
3816
+ ucurrent := CloneFlags(inj.Dparent, unwrapFlags)
3817
+ exactMatch = reflect.DeepEqual(utval, ucurrent)
3818
+ }
3819
+
3820
+ if !exactMatch && IsNode(tval) {
3821
+ if nil == currentStr {
3822
+ tmpstr := Stringify(inj.Dparent)
3823
+ currentStr = &tmpstr
3824
+ }
3825
+ tvalStr := Stringify(tval)
3826
+ exactMatch = tvalStr == *currentStr
3827
+ }
3828
+
3829
+ if exactMatch {
3830
+ return nil
3831
+ }
3832
+ }
3833
+
3834
+ // If we get here, there was no match
3835
+ mapped := make([]string, len(tvals))
3836
+ for i, v := range tvals {
3837
+ mapped[i] = Stringify(v)
3838
+ }
3839
+
3840
+ joined := strings.Join(mapped, ", ")
3841
+
3842
+ re := regexp.MustCompile("`\\$([A-Z]+)`")
3843
+ valdesc := re.ReplaceAllStringFunc(joined, func(match string) string {
3844
+ submatches := re.FindStringSubmatch(match)
3845
+ if len(submatches) == 2 {
3846
+ return strings.ToLower(submatches[1])
3847
+ }
3848
+ return match
3849
+ })
3850
+
3851
+ prefix := ""
3852
+ if len(inj.Path.List) <= 1 {
3853
+ prefix = "value "
3854
+ }
3855
+
3856
+ oneOf := ""
3857
+ if len(tvals) > 1 {
3858
+ oneOf = "one of "
3859
+ }
3860
+
3861
+ msg := _invalidTypeMsg(
3862
+ inj.Path.List,
3863
+ prefix+"exactly equal to "+oneOf+valdesc,
3864
+ Typename(Typify(inj.Dparent)),
3865
+ inj.Dparent,
3866
+ "V0110",
3867
+ )
3868
+ inj.Errs.Append(msg)
3869
+ } else {
3870
+ DelProp(inj.Parent, inj.Key)
3871
+ }
3872
+
3873
+ return nil
3874
+ }
3875
+ }
3876
+
3877
+ func makeValidation(exact bool) Modify {
3878
+ return func(
3879
+ val any,
3880
+ key any,
3881
+ parent any,
3882
+ inj *Injection,
3883
+ _store any,
3884
+ ) {
3885
+ if inj == nil {
3886
+ return
3887
+ }
3888
+
3889
+ if val == SKIP {
3890
+ return
3891
+ }
3892
+
3893
+ // Current val to verify — use dparent from injection inj.
3894
+ cval := GetProp(inj.Dparent, key)
3895
+ if !exact && cval == nil {
3896
+ return
3897
+ }
3898
+
3899
+ pval := GetProp(parent, key)
3900
+ ptype := Typify(pval)
3901
+
3902
+ // Delete any special commands remaining.
3903
+ if 0 < (T_string & ptype) && pval != nil {
3904
+ if strVal, ok := pval.(string); ok && strings.Contains(strVal, S_DS) {
3905
+ return
3906
+ }
3907
+ }
3908
+
3909
+ ctype := Typify(cval)
3910
+
3911
+ // Type mismatch.
3912
+ if ptype != ctype && pval != nil {
3913
+ inj.Errs.Append(_invalidTypeMsg(inj.Path.List, Typename(ptype), Typename(ctype), cval))
3914
+ return
3915
+ }
3916
+
3917
+ if IsMap(cval) {
3918
+ if !IsMap(val) {
3919
+ var errType string
3920
+ if IsList(val) {
3921
+ errType = S_array
3922
+ } else {
3923
+ errType = Typename(ptype)
3924
+ }
3925
+ inj.Errs.Append(_invalidTypeMsg(inj.Path.List, errType, Typename(ctype), cval))
3926
+ return
3927
+ }
3928
+
3929
+ ckeys := KeysOf(cval)
3930
+ pkeys := KeysOf(pval)
3931
+
3932
+ // Empty spec object {} means object can be open (any keys).
3933
+ if len(pkeys) > 0 && GetProp(pval, "`$OPEN`") != true {
3934
+ badkeys := []string{}
3935
+ for _, ckey := range ckeys {
3936
+ if !HasKey(val, ckey) {
3937
+ badkeys = append(badkeys, ckey)
3938
+ }
3939
+ }
3940
+
3941
+ // Closed object, so reject extra keys not in shape.
3942
+ if len(badkeys) > 0 {
3943
+ inj.Errs.Append("Unexpected keys at field " + Pathify(inj.Path.List, 1) +
3944
+ ": " + strings.Join(badkeys, ", "))
3945
+ }
3946
+ } else {
3947
+ // Object is open, so merge in extra keys.
3948
+ Merge([]any{pval, cval})
3949
+ if IsNode(pval) {
3950
+ DelProp(pval, "`$OPEN`")
3951
+ }
3952
+ }
3953
+ } else if IsList(cval) {
3954
+ if !IsList(val) {
3955
+ inj.Errs.Append(_invalidTypeMsg(inj.Path.List, Typename(ptype), Typename(ctype), cval))
3956
+ }
3957
+ } else if exact {
3958
+ // Select needs exact matches for scalar values.
3959
+ if cval != pval {
3960
+ pathmsg := ""
3961
+ if len(inj.Path.List) > 1 {
3962
+ pathmsg = "at field " + Pathify(inj.Path.List, 1) + ": "
3963
+ }
3964
+ inj.Errs.Append("Value " + pathmsg + fmt.Sprintf("%v", cval) +
3965
+ " should equal " + fmt.Sprintf("%v", pval) + ".")
3966
+ } else if pval == nil {
3967
+ // Both nil: in Go, nil represents both null and undefined.
3968
+ // For exact matching (Select), verify the key actually exists in the data.
3969
+ keyExists := false
3970
+ if m, ok := inj.Dparent.(map[string]any); ok {
3971
+ if keyStr, ok := key.(string); ok {
3972
+ _, keyExists = m[keyStr]
3973
+ }
3974
+ }
3975
+ if !keyExists {
3976
+ pathmsg := ""
3977
+ if len(inj.Path.List) > 1 {
3978
+ pathmsg = "at field " + Pathify(inj.Path.List, 1) + ": "
3979
+ }
3980
+ inj.Errs.Append("Value " + pathmsg + "undefined" +
3981
+ " should equal " + fmt.Sprintf("%v", pval) + ".")
3982
+ }
3983
+ }
3984
+ } else {
3985
+ // Spec value was a default, copy over data
3986
+ SetProp(parent, key, cval)
3987
+ }
3988
+
3989
+ return
3990
+ }
3991
+ }
3992
+
3993
+ // Default validation modify (non-exact mode).
3994
+ var validation Modify = makeValidation(false)
3995
+
3996
+ // _validatehandler processes meta path operators in validation.
3997
+ var _validatehandler Injector = func(
3998
+ inj *Injection,
3999
+ val any,
4000
+ ref *string,
4001
+ store any,
4002
+ ) any {
4003
+ out := val
4004
+
4005
+ refStr := ""
4006
+ if ref != nil {
4007
+ refStr = *ref
4008
+ }
4009
+ m := reMetaPath.FindStringSubmatch(refStr)
4010
+ ismetapath := m != nil
4011
+
4012
+ if ismetapath {
4013
+ if m[2] == "=" {
4014
+ inj.setval([]any{S_BEXACT, val})
4015
+ } else {
4016
+ inj.setval(val)
4017
+ }
4018
+ inj.KeyI = -1
4019
+ out = SKIP
4020
+ } else {
4021
+ out = injectHandler(inj, val, ref, store)
4022
+ }
4023
+
4024
+ return out
4025
+ }
4026
+
4027
+ func Validate(
4028
+ data any, // The input data
4029
+ spec any, // The shape specification
4030
+ injdefs ...*Injection,
4031
+ ) (any, error) {
4032
+ var extra map[string]any
4033
+ var collecterrs *ListRef[any]
4034
+ if len(injdefs) > 0 && injdefs[0] != nil {
4035
+ if e, ok := injdefs[0].Extra.(map[string]any); ok {
4036
+ extra = e
4037
+ }
4038
+ collecterrs = injdefs[0].Errs
4039
+ }
4040
+
4041
+ // Use the provided error collection or create a new one
4042
+ errs := collecterrs
4043
+ if nil == errs {
4044
+ errs = ListRefCreate[any]()
4045
+ }
4046
+
4047
+
4048
+ // Initialize validate_ONE if not already initialized.
4049
+ // This avoids a circular reference error, validate_ONE calls Validate.
4050
+ if validate_ONE == nil {
4051
+ init_validate_ONE()
4052
+ }
4053
+
4054
+ // Initialize validate_EXACT if not already initialized.
4055
+ if validate_EXACT == nil {
4056
+ init_validate_EXACT()
4057
+ }
4058
+
4059
+ // Create the store with validation commands
4060
+ store := map[string]any{
4061
+ // Remove the transform commands
4062
+ "$DELETE": nil,
4063
+ "$COPY": nil,
4064
+ "$KEY": nil,
4065
+ "$META": nil,
4066
+ "$MERGE": nil,
4067
+ "$EACH": nil,
4068
+ "$PACK": nil,
4069
+ "$BT": nil,
4070
+ "$DS": nil,
4071
+ "$WHEN": nil,
4072
+
4073
+ // Add validation commands
4074
+ "$STRING": validate_STRING,
4075
+ "$NUMBER": validate_TYPE,
4076
+ "$INTEGER": validate_TYPE,
4077
+ "$DECIMAL": validate_TYPE,
4078
+ "$BOOLEAN": validate_TYPE,
4079
+ "$NULL": validate_TYPE,
4080
+ "$NIL": validate_TYPE,
4081
+ "$MAP": validate_TYPE,
4082
+ "$LIST": validate_TYPE,
4083
+ "$FUNCTION": validate_TYPE,
4084
+ "$INSTANCE": validate_TYPE,
4085
+ "$OBJECT": validate_OBJECT,
4086
+ "$ARRAY": validate_ARRAY,
4087
+ "$ANY": validate_ANY,
4088
+ "$CHILD": validate_CHILD,
4089
+ "$ONE": validate_ONE,
4090
+ "$EXACT": validate_EXACT,
4091
+ }
4092
+
4093
+ // Add any extra validation commands
4094
+ if extra != nil {
4095
+ for k, fn := range extra {
4096
+ store[k] = fn
4097
+ }
4098
+ }
4099
+
4100
+ // A special top level value to collect errors
4101
+ store["$ERRS"] = errs
4102
+
4103
+ // Set up meta with exact mode.
4104
+ meta := map[string]any{}
4105
+ if extra != nil {
4106
+ if metaVal, ok := extra["meta"]; ok {
4107
+ if metaMap, ok := metaVal.(map[string]any); ok {
4108
+ meta = metaMap
4109
+ }
4110
+ delete(store, "meta")
4111
+ }
4112
+ }
4113
+ if _, ok := meta[S_BEXACT]; !ok {
4114
+ meta[S_BEXACT] = false
4115
+ }
4116
+
4117
+ // Check if exact mode is requested via meta.
4118
+ validationFn := validation
4119
+ exactVal, _ := meta[S_BEXACT].(bool)
4120
+ if exactVal {
4121
+ validationFn = makeValidation(true)
4122
+ }
4123
+
4124
+ // Run the transformation with validation and _validatehandler
4125
+ out := TransformModifyHandler(data, spec, store, validationFn, _validatehandler, errs, meta)
4126
+
4127
+ // Generate an error if we collected any errors and the caller didn't provide
4128
+ // their own error collection
4129
+ var err error
4130
+ generr := 0 < len(errs.List) && collecterrs == nil
4131
+ if generr {
4132
+ // Join error messages
4133
+ errmsgs := make([]string, len(errs.List))
4134
+ for i, e := range errs.List {
4135
+ if s, ok := e.(string); ok {
4136
+ errmsgs[i] = s
4137
+ } else {
4138
+ errmsgs[i] = fmt.Sprintf("%v", e)
4139
+ }
4140
+ }
4141
+ err = fmt.Errorf("Invalid data: %s", strings.Join(errmsgs, " | "))
4142
+ }
4143
+
4144
+ return out, err
4145
+ }
4146
+
4147
+
4148
+ // Mode names for injection modes.
4149
+ var MODENAME = map[int]string{
4150
+ M_VAL: "val",
4151
+ M_KEYPRE: "key:pre",
4152
+ M_KEYPOST: "key:post",
4153
+ }
4154
+
4155
+ // Placement names for injection modes.
4156
+ var PLACEMENT = map[int]string{
4157
+ M_VAL: "value",
4158
+ M_KEYPRE: S_key,
4159
+ M_KEYPOST: S_key,
4160
+ }
4161
+
4162
+ // Validate that an injector is placed in a valid mode and parent type.
4163
+ func CheckPlacement(modes int, ijname string, parentTypes int, inj *Injection) bool {
4164
+ if 0 == (modes & inj.Mode) {
4165
+ allModes := []int{M_KEYPRE, M_KEYPOST, M_VAL}
4166
+ expected := []string{}
4167
+ for _, m := range allModes {
4168
+ if 0 != (modes & m) {
4169
+ expected = append(expected, PLACEMENT[m])
4170
+ }
4171
+ }
4172
+ inj.Errs.Append("$" + ijname + ": invalid placement as " + PLACEMENT[inj.Mode] +
4173
+ ", expected: " + strings.Join(expected, ",") + ".")
4174
+ return false
4175
+ }
4176
+ if !IsEmpty(parentTypes) {
4177
+ ptype := Typify(inj.Parent)
4178
+ if 0 == (parentTypes & ptype) {
4179
+ inj.Errs.Append("$" + ijname + ": invalid placement in parent " + Typename(ptype) +
4180
+ ", expected: " + Typename(parentTypes) + ".")
4181
+ return false
4182
+ }
4183
+ }
4184
+ return true
4185
+ }
4186
+
4187
+ // Validate and extract injector arguments against expected type bitmasks.
4188
+ // Returns a slice where [0] is nil on success or an error string on failure,
4189
+ // and [1..N] are the validated arguments.
4190
+ func InjectorArgs(argTypes []int, args []any) []any {
4191
+ numargs := len(argTypes)
4192
+ found := make([]any, 1+numargs)
4193
+ found[0] = nil
4194
+ for argI := 0; argI < numargs; argI++ {
4195
+ arg := args[argI]
4196
+ argType := Typify(arg)
4197
+ if 0 == (argTypes[argI] & argType) {
4198
+ found[0] = "invalid argument: " + Stringify(arg, 22) +
4199
+ " (" + Typename(argType) + " at position " + strconv.Itoa(1+argI) +
4200
+ ") is not of type: " + Typename(argTypes[argI]) + "."
4201
+ break
4202
+ }
4203
+ found[1+argI] = arg
4204
+ }
4205
+ return found
4206
+ }
4207
+
4208
+
4209
+ // Select helpers - internal injectors for query matching.
4210
+
4211
+ var select_AND Injector = func(
4212
+ inj *Injection,
4213
+ val any,
4214
+ ref *string,
4215
+ store any,
4216
+ ) any {
4217
+ if M_KEYPRE == inj.Mode {
4218
+ terms := GetProp(inj.Parent, inj.Key)
4219
+
4220
+ pathList := inj.Path.List
4221
+ ppath := pathList[:len(pathList)-1]
4222
+ point := GetPath(ppath, store)
4223
+
4224
+ vstore := Merge([]any{map[string]any{}, store})
4225
+ SetProp(vstore, S_DTOP, point)
4226
+
4227
+ termList, _ := _asList(terms)
4228
+ for _, term := range termList {
4229
+ terrs := ListRefCreate[any]()
4230
+ vstoreMap, _ := vstore.(map[string]any)
4231
+ validateCollectExact(point, term, vstoreMap, terrs)
4232
+ if 0 != len(terrs.List) {
4233
+ inj.Errs.Append("AND:" + Pathify(ppath) + S_VIZ +
4234
+ Stringify(point) + " fail:" + Stringify(terms))
4235
+ }
4236
+ }
4237
+
4238
+ if len(pathList) >= 2 {
4239
+ gkey := pathList[len(pathList)-2]
4240
+ gp := inj.Nodes.List[len(inj.Nodes.List)-2]
4241
+ SetProp(gp, gkey, point)
4242
+ }
4243
+ }
4244
+ return nil
4245
+ }
4246
+
4247
+ var select_OR Injector = func(
4248
+ inj *Injection,
4249
+ val any,
4250
+ ref *string,
4251
+ store any,
4252
+ ) any {
4253
+ if M_KEYPRE == inj.Mode {
4254
+ terms := GetProp(inj.Parent, inj.Key)
4255
+
4256
+ pathList := inj.Path.List
4257
+ ppath := pathList[:len(pathList)-1]
4258
+ point := GetPath(ppath, store)
4259
+
4260
+ vstore := Merge([]any{map[string]any{}, store})
4261
+ SetProp(vstore, S_DTOP, point)
4262
+
4263
+ termList, _ := _asList(terms)
4264
+ for _, term := range termList {
4265
+ terrs := ListRefCreate[any]()
4266
+ vstoreMap, _ := vstore.(map[string]any)
4267
+ validateCollectExact(point, term, vstoreMap, terrs)
4268
+ if 0 == len(terrs.List) {
4269
+ if len(pathList) >= 2 {
4270
+ gkey := pathList[len(pathList)-2]
4271
+ gp := inj.Nodes.List[len(inj.Nodes.List)-2]
4272
+ SetProp(gp, gkey, point)
4273
+ }
4274
+ return nil
4275
+ }
4276
+ }
4277
+
4278
+ inj.Errs.Append("OR:" + Pathify(ppath) + S_VIZ +
4279
+ Stringify(point) + " fail:" + Stringify(terms))
4280
+ }
4281
+ return nil
4282
+ }
4283
+
4284
+ var select_NOT Injector = func(
4285
+ inj *Injection,
4286
+ val any,
4287
+ ref *string,
4288
+ store any,
4289
+ ) any {
4290
+ if M_KEYPRE == inj.Mode {
4291
+ term := GetProp(inj.Parent, inj.Key)
4292
+
4293
+ pathList := inj.Path.List
4294
+ ppath := pathList[:len(pathList)-1]
4295
+ point := GetPath(ppath, store)
4296
+
4297
+ vstore := Merge([]any{map[string]any{}, store})
4298
+ SetProp(vstore, S_DTOP, point)
4299
+
4300
+ terrs := ListRefCreate[any]()
4301
+ vstoreMap, _ := vstore.(map[string]any)
4302
+ validateCollectExact(point, term, vstoreMap, terrs)
4303
+
4304
+ if 0 == len(terrs.List) {
4305
+ inj.Errs.Append("NOT:" + Pathify(ppath) + S_VIZ +
4306
+ Stringify(point) + " fail:" + Stringify(term))
4307
+ }
4308
+
4309
+ if len(pathList) >= 2 {
4310
+ gkey := pathList[len(pathList)-2]
4311
+ gp := inj.Nodes.List[len(inj.Nodes.List)-2]
4312
+ SetProp(gp, gkey, point)
4313
+ }
4314
+ }
4315
+ return nil
4316
+ }
4317
+
4318
+ var select_CMP Injector = func(
4319
+ inj *Injection,
4320
+ val any,
4321
+ ref *string,
4322
+ store any,
4323
+ ) any {
4324
+ if M_KEYPRE == inj.Mode {
4325
+ term := GetProp(inj.Parent, inj.Key)
4326
+
4327
+ pathList := inj.Path.List
4328
+ ppath := pathList[:len(pathList)-1]
4329
+ point := GetPath(ppath, store)
4330
+
4331
+ pass := false
4332
+ refStr := ""
4333
+ if ref != nil {
4334
+ refStr = *ref
4335
+ }
4336
+
4337
+ pf, pErr := _toFloat64(point)
4338
+ tf, tErr := _toFloat64(term)
4339
+
4340
+ switch refStr {
4341
+ case "$GT":
4342
+ if pErr == nil && tErr == nil {
4343
+ pass = pf > tf
4344
+ }
4345
+ case "$LT":
4346
+ if pErr == nil && tErr == nil {
4347
+ pass = pf < tf
4348
+ }
4349
+ case "$GTE":
4350
+ if pErr == nil && tErr == nil {
4351
+ pass = pf >= tf
4352
+ }
4353
+ case "$LTE":
4354
+ if pErr == nil && tErr == nil {
4355
+ pass = pf <= tf
4356
+ }
4357
+ case "$LIKE":
4358
+ if ts, ok := term.(string); ok {
4359
+ re, err := regexp.Compile(ts)
4360
+ if err == nil {
4361
+ pass = re.MatchString(Stringify(point))
4362
+ }
4363
+ }
4364
+ }
4365
+
4366
+ if pass {
4367
+ if len(pathList) >= 2 {
4368
+ gkey := pathList[len(pathList)-2]
4369
+ gp := inj.Nodes.List[len(inj.Nodes.List)-2]
4370
+ SetProp(gp, gkey, point)
4371
+ }
4372
+ } else {
4373
+ inj.Errs.Append("CMP: " + Pathify(ppath) + S_VIZ +
4374
+ Stringify(point) + " fail:" + refStr + " " + Stringify(term))
4375
+ }
4376
+ }
4377
+ return nil
4378
+ }
4379
+
4380
+
4381
+ // Internal exact-mode validation for Select.
4382
+ // Like Validate but uses exact scalar comparison.
4383
+ func validateCollectExact(
4384
+ data any,
4385
+ spec any,
4386
+ extra map[string]any,
4387
+ collecterrs *ListRef[any],
4388
+ ) {
4389
+ errs := collecterrs
4390
+ if nil == errs {
4391
+ errs = ListRefCreate[any]()
4392
+ }
4393
+
4394
+ if validate_ONE == nil {
4395
+ init_validate_ONE()
4396
+ }
4397
+ if validate_EXACT == nil {
4398
+ init_validate_EXACT()
4399
+ }
4400
+
4401
+ store := map[string]any{
4402
+ "$DELETE": nil,
4403
+ "$COPY": nil,
4404
+ "$KEY": nil,
4405
+ "$META": nil,
4406
+ "$MERGE": nil,
4407
+ "$EACH": nil,
4408
+ "$PACK": nil,
4409
+ "$BT": nil,
4410
+ "$DS": nil,
4411
+ "$WHEN": nil,
4412
+
4413
+ "$STRING": validate_STRING,
4414
+ "$NUMBER": validate_TYPE,
4415
+ "$INTEGER": validate_TYPE,
4416
+ "$DECIMAL": validate_TYPE,
4417
+ "$BOOLEAN": validate_TYPE,
4418
+ "$NULL": validate_TYPE,
4419
+ "$NIL": validate_TYPE,
4420
+ "$MAP": validate_TYPE,
4421
+ "$LIST": validate_TYPE,
4422
+ "$FUNCTION": validate_TYPE,
4423
+ "$INSTANCE": validate_TYPE,
4424
+ "$OBJECT": validate_OBJECT,
4425
+ "$ARRAY": validate_ARRAY,
4426
+ "$ANY": validate_ANY,
4427
+ "$CHILD": validate_CHILD,
4428
+ "$ONE": validate_ONE,
4429
+ "$EXACT": validate_EXACT,
4430
+ }
4431
+
4432
+ if extra != nil {
4433
+ for k, fn := range extra {
4434
+ store[k] = fn
4435
+ }
4436
+ }
4437
+
4438
+ store["$ERRS"] = errs
4439
+
4440
+ meta := map[string]any{S_BEXACT: true}
4441
+ TransformModifyHandler(data, spec, store, makeValidation(true), _validatehandler, errs, meta)
4442
+ }
4443
+
4444
+
4445
+ // Select children from a node that match a query.
4446
+ // Uses validate internally with query operators ($AND, $OR, $NOT,
4447
+ // $GT, $LT, $GTE, $LTE, $LIKE).
4448
+ // For maps, children are values (tagged with $KEY). For lists, children are elements.
4449
+ func Select(children any, query any) []any {
4450
+ if !IsNode(children) {
4451
+ return []any{}
4452
+ }
4453
+
4454
+ var childList []any
4455
+
4456
+ if IsMap(children) {
4457
+ pairs := Items(children)
4458
+ childList = make([]any, len(pairs))
4459
+ for i, pair := range pairs {
4460
+ child := pair[1]
4461
+ if IsMap(child) {
4462
+ SetProp(child, "$KEY", pair[0])
4463
+ }
4464
+ childList[i] = child
4465
+ }
4466
+ } else {
4467
+ list, _ := _asList(children)
4468
+ if list == nil {
4469
+ list = _listify(children)
4470
+ }
4471
+ childList = make([]any, len(list))
4472
+ for i, child := range list {
4473
+ if IsMap(child) {
4474
+ SetProp(child, "$KEY", i)
4475
+ }
4476
+ childList[i] = child
4477
+ }
4478
+ }
4479
+
4480
+ results := []any{}
4481
+ extra := map[string]any{
4482
+ "$AND": select_AND,
4483
+ "$OR": select_OR,
4484
+ "$NOT": select_NOT,
4485
+ "$GT": select_CMP,
4486
+ "$LT": select_CMP,
4487
+ "$GTE": select_CMP,
4488
+ "$LTE": select_CMP,
4489
+ "$LIKE": select_CMP,
4490
+ }
4491
+
4492
+ q := Clone(query)
4493
+
4494
+ // Mark all map nodes as open so extra keys don't fail validation.
4495
+ Walk(q, func(key *string, v any, parent any, path []string) any {
4496
+ if IsMap(v) {
4497
+ m := v.(map[string]any)
4498
+ if _, has := m[S_BOPEN]; !has {
4499
+ m[S_BOPEN] = true
4500
+ }
4501
+ }
4502
+ return v
4503
+ })
4504
+
4505
+ for _, child := range childList {
4506
+ errs := ListRefCreate[any]()
4507
+ validateCollectExact(child, Clone(q), extra, errs)
4508
+ if 0 == len(errs.List) {
4509
+ results = append(results, child)
4510
+ }
4511
+ }
4512
+
4513
+ return results
4514
+ }
4515
+
4516
+
4517
+ // Internal utilities
4518
+ // ==================
4519
+
4520
+ type ListRef[T any] struct {
4521
+ List []T
4522
+ }
4523
+
4524
+ func ListRefCreate[T any]() *ListRef[T] {
4525
+ return &ListRef[T]{
4526
+ List: make([]T, 0),
4527
+ }
4528
+ }
4529
+
4530
+
4531
+ func (l *ListRef[T]) Append(elem T) {
4532
+ l.List = append(l.List, elem)
4533
+ }
4534
+
4535
+
4536
+ func (l *ListRef[T]) Prepend(elem T) {
4537
+ l.List = append([]T{elem}, l.List...)
4538
+ }
4539
+
4540
+ func _join(vals []any, sep string) string {
4541
+ strVals := make([]string, len(vals))
4542
+ for i, v := range vals {
4543
+ strVals[i] = fmt.Sprint(v)
4544
+ }
4545
+ return strings.Join(strVals, sep)
4546
+ }
4547
+
4548
+
4549
+ func _invalidTypeMsg(path []string, needtype string, vt string, v any, whence ...string) string {
4550
+ vs := "no value"
4551
+ if v != nil {
4552
+ vs = Stringify(v)
4553
+ }
4554
+
4555
+ fieldPart := ""
4556
+ if len(path) > 1 {
4557
+ fieldPart = "field " + Pathify(path, 1) + " to be "
4558
+ }
4559
+
4560
+ typePart := ""
4561
+ if v != nil {
4562
+ typePart = vt + ": "
4563
+ }
4564
+
4565
+ // Build the main error message
4566
+ message := "Expected " + fieldPart + needtype + ", but found " + typePart + vs
4567
+
4568
+ // Uncomment to help debug validation errors
4569
+ // if len(whence) > 0 {
4570
+ // message += " [" + whence[0] + "]"
4571
+ // }
4572
+
4573
+ return message + "."
4574
+ }
4575
+
4576
+ func _getType(v any) string {
4577
+ if nil == v {
4578
+ return "nil"
4579
+ }
4580
+ return reflect.TypeOf(v).String()
4581
+ }
4582
+
4583
+
4584
+ // StrKey converts different types of keys to string representation.
4585
+ // String keys are returned as is.
4586
+ // Number keys are converted to strings.
4587
+ // Floats are truncated to integers.
4588
+ // Booleans, objects, arrays, null, undefined all return empty string.
4589
+
4590
+ // TODO: rename to _strKey
4591
+ func StrKey(key any) string {
4592
+ if nil == key {
4593
+ return S_MT
4594
+ }
4595
+
4596
+ switch v := key.(type) {
4597
+ case string:
4598
+ return v
4599
+ case *string:
4600
+ if nil != v {
4601
+ return *v
4602
+ }
4603
+ return S_MT
4604
+ case int:
4605
+ return strconv.Itoa(v)
4606
+ case int64:
4607
+ return strconv.FormatInt(v, 10)
4608
+ case int32:
4609
+ return strconv.FormatInt(int64(v), 10)
4610
+ case float64:
4611
+ return strconv.FormatInt(int64(v), 10)
4612
+ case float32:
4613
+ return strconv.FormatInt(int64(v), 10)
4614
+ case bool:
4615
+ return S_MT
4616
+ default:
4617
+ return S_MT
4618
+ }
4619
+ }
4620
+
4621
+
4622
+ func _resolveStrings(input []any) []string {
4623
+ var result []string
4624
+
4625
+ for _, v := range input {
4626
+ if str, ok := v.(string); ok {
4627
+ result = append(result, str)
4628
+ } else {
4629
+ result = append(result, StrKey(v))
4630
+ }
4631
+ }
4632
+
4633
+ return result
4634
+ }
4635
+
4636
+
4637
+ // Extract a bare []any from either a []any or a *ListRef[any].
4638
+ // Recursively unwrap *ListRef[any] to []any for JSON marshaling.
4639
+ func _unwrapListRefs(val any) any {
4640
+ return _unwrapListRefsD(val, 0)
4641
+ }
4642
+
4643
+ func _unwrapListRefsD(val any, depth int) any {
4644
+ if depth > 32 {
4645
+ return val
4646
+ }
4647
+ if lr, ok := val.(*ListRef[any]); ok {
4648
+ out := make([]any, len(lr.List))
4649
+ for i, v := range lr.List {
4650
+ out[i] = _unwrapListRefsD(v, depth+1)
4651
+ }
4652
+ return out
4653
+ }
4654
+ if m, ok := val.(map[string]any); ok {
4655
+ out := make(map[string]any, len(m))
4656
+ for k, v := range m {
4657
+ out[k] = _unwrapListRefsD(v, depth+1)
4658
+ }
4659
+ return out
4660
+ }
4661
+ if list, ok := val.([]any); ok {
4662
+ out := make([]any, len(list))
4663
+ for i, v := range list {
4664
+ out[i] = _unwrapListRefsD(v, depth+1)
4665
+ }
4666
+ return out
4667
+ }
4668
+ return val
4669
+ }
4670
+
4671
+ func _asList(val any) ([]any, bool) {
4672
+ if lr, ok := val.(*ListRef[any]); ok {
4673
+ return lr.List, true
4674
+ }
4675
+ if list, ok := val.([]any); ok {
4676
+ return list, true
4677
+ }
4678
+ return nil, false
4679
+ }
4680
+
4681
+
4682
+ func _listify(src any) []any {
4683
+ if lr, ok := src.(*ListRef[any]); ok {
4684
+ return lr.List
4685
+ }
4686
+
4687
+ if list, ok := src.([]any); ok {
4688
+ return list
4689
+ }
4690
+
4691
+ if src == nil {
4692
+ return nil
4693
+ }
4694
+
4695
+ val := reflect.ValueOf(src)
4696
+ if val.Kind() == reflect.Slice {
4697
+ length := val.Len()
4698
+ result := make([]any, length)
4699
+
4700
+ for i := 0; i < length; i++ {
4701
+ result[i] = val.Index(i).Interface()
4702
+ }
4703
+ return result
4704
+ }
4705
+
4706
+ return nil
4707
+ }
4708
+
4709
+
4710
+ // toFloat64 helps unify numeric types for floor conversion.
4711
+ func _toFloat64(val any) (float64, error) {
4712
+ switch n := val.(type) {
4713
+ case float64:
4714
+ return n, nil
4715
+ case float32:
4716
+ return float64(n), nil
4717
+ case int:
4718
+ return float64(n), nil
4719
+ case int8:
4720
+ return float64(n), nil
4721
+ case int16:
4722
+ return float64(n), nil
4723
+ case int32:
4724
+ return float64(n), nil
4725
+ case int64:
4726
+ return float64(n), nil
4727
+ case uint:
4728
+ return float64(n), nil
4729
+ case uint8:
4730
+ return float64(n), nil
4731
+ case uint16:
4732
+ return float64(n), nil
4733
+ case uint32:
4734
+ return float64(n), nil
4735
+ case uint64:
4736
+ // might overflow if > math.MaxFloat64, but for demonstration that's rare
4737
+ return float64(n), nil
4738
+ default:
4739
+ return 0, fmt.Errorf("not a numeric type")
4740
+ }
4741
+ }
4742
+
4743
+
4744
+ // _parseInt is a helper to convert a string to int safely.
4745
+ func _parseInt(s string) (int, error) {
4746
+ // We'll do a very simple parse:
4747
+ var x int
4748
+ var sign int = 1
4749
+ for i, c := range s {
4750
+ if c == '-' && i == 0 {
4751
+ sign = -1
4752
+ continue
4753
+ }
4754
+ if c < '0' || c > '9' {
4755
+ return 0, &ParseIntError{s}
4756
+ }
4757
+ x = 10*x + int(c-'0')
4758
+ }
4759
+ return x * sign, nil
4760
+ }
4761
+
4762
+
4763
+ type ParseIntError struct{ input string }
4764
+
4765
+
4766
+ func (e *ParseIntError) Error() string {
4767
+ return "cannot parse int from: " + e.input
4768
+ }
4769
+
4770
+
4771
+ func _makeArrayType(values []any, target any) any {
4772
+ targetElem := reflect.TypeOf(target).Elem()
4773
+ out := reflect.MakeSlice(reflect.SliceOf(targetElem), len(values), len(values))
4774
+
4775
+ for i, v := range values {
4776
+ elemVal := reflect.ValueOf(v)
4777
+ if !elemVal.Type().ConvertibleTo(targetElem) {
4778
+ return values
4779
+ }
4780
+
4781
+ out.Index(i).Set(elemVal.Convert(targetElem))
4782
+ }
4783
+
4784
+ return out.Interface()
4785
+ }
4786
+
4787
+
4788
+ func _stringifyValue(v any) string {
4789
+ switch vv := v.(type) {
4790
+ case string:
4791
+ return vv
4792
+ case float64, int, bool:
4793
+ return Stringify(v)
4794
+ default:
4795
+ return Stringify(v)
4796
+ }
4797
+ }
4798
+
4799
+
4800
+
4801
+
4802
+ // DEBUG
4803
+
4804
+ func fdt(data any) string {
4805
+ return fdti(data, "")
4806
+ }
4807
+
4808
+ func fdti(data any, indent string) string {
4809
+ result := ""
4810
+
4811
+ if data == nil {
4812
+ return indent + "nil\n"
4813
+ }
4814
+
4815
+ // Get a pointer for memory address
4816
+ memoryAddr := "0x???"
4817
+ val := reflect.ValueOf(data)
4818
+
4819
+ // For non-pointer types that are addressable, get their pointer
4820
+ if val.Kind() != reflect.Ptr && val.CanAddr() {
4821
+ ptr := val.Addr()
4822
+ memoryAddr = fmt.Sprintf("0x%x", ptr.Pointer())
4823
+ } else if val.Kind() == reflect.Ptr {
4824
+ // For pointer types, use the pointer value directly
4825
+ memoryAddr = fmt.Sprintf("0x%x", val.Pointer())
4826
+ } else if val.Kind() == reflect.Map || val.Kind() == reflect.Slice {
4827
+ // For maps and slices, use the pointer to internal data
4828
+ memoryAddr = fmt.Sprintf("0x%x", val.Pointer())
4829
+ }
4830
+
4831
+ switch v := data.(type) {
4832
+ case map[string]any:
4833
+ result += indent + fmt.Sprintf("{ @%s\n", memoryAddr)
4834
+ for key, value := range v {
4835
+ result += fmt.Sprintf("%s \"%s\": %s", indent, key, fdti(value, indent+" "))
4836
+ }
4837
+ result += indent + "}\n"
4838
+
4839
+ case []any:
4840
+ result += indent + fmt.Sprintf("[ @%s\n", memoryAddr)
4841
+ for _, value := range v {
4842
+ result += fmt.Sprintf("%s - %s", indent, fdti(value, indent+" "))
4843
+ }
4844
+ result += indent + "]\n"
4845
+
4846
+ default:
4847
+ // Check if it's a struct using reflection
4848
+ typ := val.Type()
4849
+
4850
+ // Handle pointers by dereferencing
4851
+ isPtr := false
4852
+ if val.Kind() == reflect.Ptr {
4853
+ isPtr = true
4854
+ if val.IsNil() {
4855
+ return indent + "nil\n"
4856
+ }
4857
+ val = val.Elem()
4858
+ typ = val.Type()
4859
+ }
4860
+
4861
+ if val.Kind() == reflect.Struct {
4862
+ structName := typ.Name()
4863
+ if isPtr {
4864
+ structName = "*" + structName
4865
+ }
4866
+ result += indent + fmt.Sprintf("struct %s @%s {\n", structName, memoryAddr)
4867
+
4868
+ // Iterate over all fields of the struct
4869
+ for i := 0; i < val.NumField(); i++ {
4870
+ field := val.Field(i)
4871
+ fieldType := typ.Field(i)
4872
+
4873
+ // Skip unexported fields (lowercase field names)
4874
+ if !fieldType.IsExported() {
4875
+ continue
4876
+ }
4877
+
4878
+ fieldName := fieldType.Name
4879
+ fieldValue := field.Interface()
4880
+
4881
+ result += fmt.Sprintf("%s %s: %s", indent, fieldName, fdti(fieldValue, indent+" "))
4882
+ }
4883
+ result += indent + "}\n"
4884
+ } else {
4885
+ // For non-struct types, just format value with its type
4886
+ result += fmt.Sprintf("%v (%s) @%s\n", v, reflect.TypeOf(v), memoryAddr)
4887
+ }
4888
+ }
4889
+
4890
+ return result
4891
+ }