@voxgig/sdkgen 0.35.2 → 0.37.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 (498) hide show
  1. package/bin/voxgig-sdkgen +1 -1
  2. package/dist/cmp/Main.js +0 -12
  3. package/dist/cmp/Main.js.map +1 -1
  4. package/dist/cmp/Readme.js +5 -5
  5. package/dist/cmp/Readme.js.map +1 -1
  6. package/dist/cmp/ReadmeEntity.js +74 -24
  7. package/dist/cmp/ReadmeEntity.js.map +1 -1
  8. package/dist/cmp/ReadmeExplanation.d.ts +2 -0
  9. package/dist/cmp/ReadmeExplanation.js +308 -0
  10. package/dist/cmp/ReadmeExplanation.js.map +1 -0
  11. package/dist/cmp/ReadmeHowto.d.ts +2 -0
  12. package/dist/cmp/ReadmeHowto.js +18 -0
  13. package/dist/cmp/ReadmeHowto.js.map +1 -0
  14. package/dist/cmp/ReadmeIntro.js +8 -23
  15. package/dist/cmp/ReadmeIntro.js.map +1 -1
  16. package/dist/cmp/ReadmeModel.js +55 -91
  17. package/dist/cmp/ReadmeModel.js.map +1 -1
  18. package/dist/cmp/ReadmeOptions.js +35 -11
  19. package/dist/cmp/ReadmeOptions.js.map +1 -1
  20. package/dist/cmp/ReadmeQuick.js +4 -1
  21. package/dist/cmp/ReadmeQuick.js.map +1 -1
  22. package/dist/cmp/ReadmeRef.js +1042 -40
  23. package/dist/cmp/ReadmeRef.js.map +1 -1
  24. package/dist/cmp/ReadmeTop.d.ts +2 -0
  25. package/dist/cmp/ReadmeTop.js +171 -0
  26. package/dist/cmp/ReadmeTop.js.map +1 -0
  27. package/dist/sdkgen.d.ts +7 -1
  28. package/dist/sdkgen.js +13 -1
  29. package/dist/sdkgen.js.map +1 -1
  30. package/dist/tsconfig.tsbuildinfo +1 -1
  31. package/package.json +2 -2
  32. package/project/.sdk/model/feature/feature-index.jsonic +5 -0
  33. package/project/.sdk/model/feature/log.jsonic +5 -1
  34. package/project/.sdk/model/feature/test.jsonic +1 -1
  35. package/project/.sdk/model/target/lua.jsonic +23 -0
  36. package/project/.sdk/model/target/php.jsonic +22 -0
  37. package/project/.sdk/model/target/py.jsonic +23 -0
  38. package/project/.sdk/model/target/rb.jsonic +23 -0
  39. package/project/.sdk/src/cmp/go/Config_go.ts +6 -2
  40. package/project/.sdk/src/cmp/go/Main_go.ts +2 -6
  41. package/project/.sdk/src/cmp/go/ReadmeExplanation_go.ts +42 -0
  42. package/project/.sdk/src/cmp/go/ReadmeHowto_go.ts +112 -0
  43. package/project/.sdk/src/cmp/go/ReadmeInstall_go.ts +29 -0
  44. package/project/.sdk/src/cmp/go/ReadmeModel_go.ts +129 -0
  45. package/project/.sdk/src/cmp/go/ReadmeQuick_go.ts +163 -0
  46. package/project/.sdk/src/cmp/go/ReadmeTopHowto_go.ts +24 -0
  47. package/project/.sdk/src/cmp/go/ReadmeTopQuick_go.ts +67 -0
  48. package/project/.sdk/src/cmp/go/ReadmeTopTest_go.ts +40 -0
  49. package/project/.sdk/src/cmp/go/TestDirect_go.ts +46 -2
  50. package/project/.sdk/src/cmp/go/TestEntity_go.ts +360 -160
  51. package/project/.sdk/src/cmp/go/Test_go.ts +24 -1
  52. package/project/.sdk/src/cmp/js/EntityBase_js.ts +34 -0
  53. package/project/.sdk/src/cmp/js/Main_js.ts +10 -0
  54. package/project/.sdk/src/cmp/js/ReadmeExplanation_js.ts +33 -0
  55. package/project/.sdk/src/cmp/js/ReadmeHowto_js.ts +123 -0
  56. package/project/.sdk/src/cmp/js/ReadmeModel_js.ts +152 -0
  57. package/project/.sdk/src/cmp/js/ReadmeQuick_js.ts +1 -1
  58. package/project/.sdk/src/cmp/js/ReadmeTopHowto_js.ts +25 -0
  59. package/project/.sdk/src/cmp/js/ReadmeTopQuick_js.ts +65 -0
  60. package/project/.sdk/src/cmp/js/ReadmeTopTest_js.ts +36 -0
  61. package/project/.sdk/src/cmp/js/TestDirect_js.ts +53 -5
  62. package/project/.sdk/src/cmp/js/TestEntity_js.ts +114 -20
  63. package/project/.sdk/src/cmp/js/fragment/Entity.fragment.js +7 -139
  64. package/project/.sdk/src/cmp/js/fragment/EntityBase.fragment.js +149 -0
  65. package/project/.sdk/src/cmp/js/fragment/EntityCreateOp.fragment.js +6 -10
  66. package/project/.sdk/src/cmp/js/fragment/EntityListOp.fragment.js +6 -10
  67. package/project/.sdk/src/cmp/js/fragment/EntityLoadOp.fragment.js +7 -11
  68. package/project/.sdk/src/cmp/js/fragment/EntityRemoveOp.fragment.js +7 -11
  69. package/project/.sdk/src/cmp/js/fragment/EntityUpdateOp.fragment.js +7 -11
  70. package/project/.sdk/src/cmp/js/fragment/Main.fragment.js +2 -0
  71. package/project/.sdk/src/cmp/js/fragment/SdkError.fragment.js +0 -2
  72. package/project/.sdk/src/cmp/lua/Config_lua.ts +116 -0
  73. package/project/.sdk/src/cmp/lua/EntityOperation_lua.ts +44 -0
  74. package/project/.sdk/src/cmp/lua/Entity_lua.ts +62 -0
  75. package/project/.sdk/src/cmp/lua/MainEntity_lua.ts +23 -0
  76. package/project/.sdk/src/cmp/lua/Main_lua.ts +133 -0
  77. package/project/.sdk/src/cmp/lua/Package_lua.ts +80 -0
  78. package/project/.sdk/src/cmp/lua/ReadmeExplanation_lua.ts +42 -0
  79. package/project/.sdk/src/cmp/lua/ReadmeHowto_lua.ts +100 -0
  80. package/project/.sdk/src/cmp/lua/ReadmeInstall_lua.ts +26 -0
  81. package/project/.sdk/src/cmp/lua/ReadmeModel_lua.ts +129 -0
  82. package/project/.sdk/src/cmp/lua/ReadmeQuick_lua.ts +99 -0
  83. package/project/.sdk/src/cmp/lua/ReadmeTopHowto_lua.ts +24 -0
  84. package/project/.sdk/src/cmp/lua/ReadmeTopQuick_lua.ts +55 -0
  85. package/project/.sdk/src/cmp/lua/ReadmeTopTest_lua.ts +38 -0
  86. package/project/.sdk/src/cmp/lua/TestDirect_lua.ts +261 -0
  87. package/project/.sdk/src/cmp/lua/TestEntity_lua.ts +485 -0
  88. package/project/.sdk/src/cmp/lua/Test_lua.ts +49 -0
  89. package/project/.sdk/src/cmp/lua/fragment/Entity.fragment.lua +147 -0
  90. package/project/.sdk/src/cmp/lua/fragment/EntityCreateOp.fragment.lua +27 -0
  91. package/project/.sdk/src/cmp/lua/fragment/EntityListOp.fragment.lua +24 -0
  92. package/project/.sdk/src/cmp/lua/fragment/EntityLoadOp.fragment.lua +30 -0
  93. package/project/.sdk/src/cmp/lua/fragment/EntityRemoveOp.fragment.lua +30 -0
  94. package/project/.sdk/src/cmp/lua/fragment/EntityUpdateOp.fragment.lua +30 -0
  95. package/project/.sdk/src/cmp/lua/fragment/Main.fragment.lua +256 -0
  96. package/project/.sdk/src/cmp/lua/fragment/SdkError.fragment.lua +26 -0
  97. package/project/.sdk/src/cmp/lua/tsconfig.json +15 -0
  98. package/project/.sdk/src/cmp/lua/utility_lua.ts +84 -0
  99. package/project/.sdk/src/cmp/php/Config_php.ts +111 -0
  100. package/project/.sdk/src/cmp/php/EntityOperation_php.ts +44 -0
  101. package/project/.sdk/src/cmp/php/Entity_php.ts +62 -0
  102. package/project/.sdk/src/cmp/php/MainEntity_php.ts +24 -0
  103. package/project/.sdk/src/cmp/php/Main_php.ts +135 -0
  104. package/project/.sdk/src/cmp/php/Package_php.ts +87 -0
  105. package/project/.sdk/src/cmp/php/ReadmeExplanation_php.ts +42 -0
  106. package/project/.sdk/src/cmp/php/ReadmeHowto_php.ts +101 -0
  107. package/project/.sdk/src/cmp/php/ReadmeInstall_php.ts +19 -0
  108. package/project/.sdk/src/cmp/php/ReadmeModel_php.ts +129 -0
  109. package/project/.sdk/src/cmp/php/ReadmeQuick_php.ts +100 -0
  110. package/project/.sdk/src/cmp/php/ReadmeTopHowto_php.ts +24 -0
  111. package/project/.sdk/src/cmp/php/ReadmeTopQuick_php.ts +56 -0
  112. package/project/.sdk/src/cmp/php/ReadmeTopTest_php.ts +38 -0
  113. package/project/.sdk/src/cmp/php/TestDirect_php.ts +270 -0
  114. package/project/.sdk/src/cmp/php/TestEntity_php.ts +479 -0
  115. package/project/.sdk/src/cmp/php/Test_php.ts +56 -0
  116. package/project/.sdk/src/cmp/php/fragment/Entity.fragment.php +146 -0
  117. package/project/.sdk/src/cmp/php/fragment/EntityCreateOp.fragment.php +26 -0
  118. package/project/.sdk/src/cmp/php/fragment/EntityListOp.fragment.php +23 -0
  119. package/project/.sdk/src/cmp/php/fragment/EntityLoadOp.fragment.php +29 -0
  120. package/project/.sdk/src/cmp/php/fragment/EntityRemoveOp.fragment.php +29 -0
  121. package/project/.sdk/src/cmp/php/fragment/EntityUpdateOp.fragment.php +29 -0
  122. package/project/.sdk/src/cmp/php/fragment/Main.fragment.php +233 -0
  123. package/project/.sdk/src/cmp/php/fragment/SdkError.fragment.php +24 -0
  124. package/project/.sdk/src/cmp/php/tsconfig.json +15 -0
  125. package/project/.sdk/src/cmp/php/utility_php.ts +85 -0
  126. package/project/.sdk/src/cmp/py/Config_py.ts +97 -0
  127. package/project/.sdk/src/cmp/py/EntityOperation_py.ts +44 -0
  128. package/project/.sdk/src/cmp/py/Entity_py.ts +62 -0
  129. package/project/.sdk/src/cmp/py/MainEntity_py.ts +22 -0
  130. package/project/.sdk/src/cmp/py/Main_py.ts +156 -0
  131. package/project/.sdk/src/cmp/py/Package_py.ts +75 -0
  132. package/project/.sdk/src/cmp/py/ReadmeExplanation_py.ts +41 -0
  133. package/project/.sdk/src/cmp/py/ReadmeHowto_py.ts +98 -0
  134. package/project/.sdk/src/cmp/py/ReadmeInstall_py.ts +25 -0
  135. package/project/.sdk/src/cmp/py/ReadmeModel_py.ts +130 -0
  136. package/project/.sdk/src/cmp/py/ReadmeQuick_py.ts +100 -0
  137. package/project/.sdk/src/cmp/py/ReadmeTopHowto_py.ts +24 -0
  138. package/project/.sdk/src/cmp/py/ReadmeTopQuick_py.ts +56 -0
  139. package/project/.sdk/src/cmp/py/ReadmeTopTest_py.ts +38 -0
  140. package/project/.sdk/src/cmp/py/TestDirect_py.ts +248 -0
  141. package/project/.sdk/src/cmp/py/TestEntity_py.ts +477 -0
  142. package/project/.sdk/src/cmp/py/Test_py.ts +55 -0
  143. package/project/.sdk/src/cmp/py/fragment/Entity.fragment.py +112 -0
  144. package/project/.sdk/src/cmp/py/fragment/EntityCreateOp.fragment.py +25 -0
  145. package/project/.sdk/src/cmp/py/fragment/EntityListOp.fragment.py +22 -0
  146. package/project/.sdk/src/cmp/py/fragment/EntityLoadOp.fragment.py +27 -0
  147. package/project/.sdk/src/cmp/py/fragment/EntityRemoveOp.fragment.py +27 -0
  148. package/project/.sdk/src/cmp/py/fragment/EntityUpdateOp.fragment.py +27 -0
  149. package/project/.sdk/src/cmp/py/fragment/Main.fragment.py +222 -0
  150. package/project/.sdk/src/cmp/py/fragment/SdkError.fragment.py +16 -0
  151. package/project/.sdk/src/cmp/py/tsconfig.json +15 -0
  152. package/project/.sdk/src/cmp/py/utility_py.ts +84 -0
  153. package/project/.sdk/src/cmp/rb/Config_rb.ts +105 -0
  154. package/project/.sdk/src/cmp/rb/EntityOperation_rb.ts +44 -0
  155. package/project/.sdk/src/cmp/rb/Entity_rb.ts +62 -0
  156. package/project/.sdk/src/cmp/rb/MainEntity_rb.ts +23 -0
  157. package/project/.sdk/src/cmp/rb/Main_rb.ts +130 -0
  158. package/project/.sdk/src/cmp/rb/Package_rb.ts +111 -0
  159. package/project/.sdk/src/cmp/rb/ReadmeExplanation_rb.ts +42 -0
  160. package/project/.sdk/src/cmp/rb/ReadmeHowto_rb.ts +98 -0
  161. package/project/.sdk/src/cmp/rb/ReadmeInstall_rb.ts +31 -0
  162. package/project/.sdk/src/cmp/rb/ReadmeModel_rb.ts +129 -0
  163. package/project/.sdk/src/cmp/rb/ReadmeQuick_rb.ts +99 -0
  164. package/project/.sdk/src/cmp/rb/ReadmeTopHowto_rb.ts +24 -0
  165. package/project/.sdk/src/cmp/rb/ReadmeTopQuick_rb.ts +55 -0
  166. package/project/.sdk/src/cmp/rb/ReadmeTopTest_rb.ts +38 -0
  167. package/project/.sdk/src/cmp/rb/TestDirect_rb.ts +260 -0
  168. package/project/.sdk/src/cmp/rb/TestEntity_rb.ts +476 -0
  169. package/project/.sdk/src/cmp/rb/Test_rb.ts +50 -0
  170. package/project/.sdk/src/cmp/rb/fragment/Entity.fragment.rb +116 -0
  171. package/project/.sdk/src/cmp/rb/fragment/EntityCreateOp.fragment.rb +25 -0
  172. package/project/.sdk/src/cmp/rb/fragment/EntityListOp.fragment.rb +20 -0
  173. package/project/.sdk/src/cmp/rb/fragment/EntityLoadOp.fragment.rb +26 -0
  174. package/project/.sdk/src/cmp/rb/fragment/EntityRemoveOp.fragment.rb +26 -0
  175. package/project/.sdk/src/cmp/rb/fragment/EntityUpdateOp.fragment.rb +26 -0
  176. package/project/.sdk/src/cmp/rb/fragment/Main.fragment.rb +203 -0
  177. package/project/.sdk/src/cmp/rb/fragment/SdkError.fragment.rb +16 -0
  178. package/project/.sdk/src/cmp/rb/tsconfig.json +15 -0
  179. package/project/.sdk/src/cmp/rb/utility_rb.ts +84 -0
  180. package/project/.sdk/src/cmp/ts/Main_ts.ts +7 -0
  181. package/project/.sdk/src/cmp/ts/ReadmeExplanation_ts.ts +34 -0
  182. package/project/.sdk/src/cmp/ts/ReadmeHowto_ts.ts +123 -0
  183. package/project/.sdk/src/cmp/ts/ReadmeInstall_ts.ts +1 -1
  184. package/project/.sdk/src/cmp/ts/ReadmeModel_ts.ts +159 -0
  185. package/project/.sdk/src/cmp/ts/ReadmeQuick_ts.ts +69 -54
  186. package/project/.sdk/src/cmp/ts/ReadmeTopHowto_ts.ts +25 -0
  187. package/project/.sdk/src/cmp/ts/ReadmeTopQuick_ts.ts +65 -0
  188. package/project/.sdk/src/cmp/ts/ReadmeTopTest_ts.ts +36 -0
  189. package/project/.sdk/tm/go/feature/log_feature.go +133 -0
  190. package/project/.sdk/tm/go/src/feature/log/.gitkeep +0 -0
  191. package/project/.sdk/tm/go/test/runner_test.go +16 -2
  192. package/project/.sdk/tm/js/src/Context.js +142 -0
  193. package/project/.sdk/tm/js/src/Control.js +16 -0
  194. package/project/.sdk/tm/js/src/Operation.js +19 -0
  195. package/project/.sdk/tm/js/src/Point.js +24 -0
  196. package/project/.sdk/tm/js/src/README.md +1 -0
  197. package/project/.sdk/tm/js/src/Response.js +19 -0
  198. package/project/.sdk/tm/js/src/Result.js +21 -0
  199. package/project/.sdk/tm/js/src/Spec.js +26 -0
  200. package/project/.sdk/tm/js/src/feature/README.md +1 -0
  201. package/project/.sdk/tm/js/src/feature/base/BaseFeature.js +45 -0
  202. package/project/.sdk/tm/js/src/feature/log/LogFeature.js +46 -47
  203. package/project/.sdk/tm/js/src/feature/test/TestFeature.js +207 -0
  204. package/project/.sdk/tm/js/src/types.js +22 -0
  205. package/project/.sdk/tm/js/src/utility/CleanUtility.js +31 -0
  206. package/project/.sdk/tm/js/src/utility/DoneUtility.js +11 -4
  207. package/project/.sdk/tm/js/src/utility/FeatureAddUtility.js +42 -0
  208. package/project/.sdk/tm/js/src/utility/FeatureHookUtility.js +25 -0
  209. package/project/.sdk/tm/js/src/utility/FeatureInitUtility.js +11 -0
  210. package/project/.sdk/tm/js/src/utility/FetcherUtility.js +28 -0
  211. package/project/.sdk/tm/js/src/utility/MakeContextUtility.js +11 -0
  212. package/project/.sdk/tm/js/src/utility/MakeErrorUtility.js +55 -0
  213. package/project/.sdk/tm/js/src/utility/MakeFetchDefUtility.js +44 -0
  214. package/project/.sdk/tm/js/src/utility/MakeOptionsUtility.js +93 -0
  215. package/project/.sdk/tm/js/src/utility/MakePointUtility.js +77 -0
  216. package/project/.sdk/tm/js/src/utility/MakeRequestUtility.js +63 -0
  217. package/project/.sdk/tm/js/src/utility/MakeResponseUtility.js +55 -0
  218. package/project/.sdk/tm/js/src/utility/MakeResultUtility.js +54 -0
  219. package/project/.sdk/tm/js/src/utility/MakeSpecUtility.js +58 -0
  220. package/project/.sdk/tm/js/src/utility/MakeUrlUtility.js +40 -0
  221. package/project/.sdk/tm/js/src/utility/ParamUtility.js +61 -0
  222. package/project/.sdk/tm/js/src/utility/PrepareAuthUtility.js +41 -0
  223. package/project/.sdk/tm/js/src/utility/PrepareBodyUtility.js +25 -0
  224. package/project/.sdk/tm/js/src/utility/PrepareHeadersUtility.js +18 -0
  225. package/project/.sdk/tm/js/src/utility/{MethodUtility.js → PrepareMethodUtility.js} +7 -7
  226. package/project/.sdk/tm/js/src/utility/PrepareParamsUtility.js +25 -0
  227. package/project/.sdk/tm/js/src/utility/PreparePathUtility.js +13 -0
  228. package/project/.sdk/tm/js/src/utility/PrepareQueryUtility.js +26 -0
  229. package/project/.sdk/tm/js/src/utility/README.md +1 -0
  230. package/project/.sdk/tm/js/src/utility/ResultBasicUtility.js +34 -0
  231. package/project/.sdk/tm/js/src/utility/ResultBodyUtility.js +18 -0
  232. package/project/.sdk/tm/js/src/utility/ResultHeadersUtility.js +22 -0
  233. package/project/.sdk/tm/js/src/utility/StructUtility.js +2219 -1078
  234. package/project/.sdk/tm/js/src/utility/TransformRequestUtility.js +28 -0
  235. package/project/.sdk/tm/js/src/utility/TransformResponseUtility.js +31 -0
  236. package/project/.sdk/tm/js/src/utility/Utility.js +61 -61
  237. package/project/.sdk/tm/js/test/README.md +1 -0
  238. package/project/.sdk/tm/js/test/exists.test.js +16 -0
  239. package/project/.sdk/tm/js/test/runner.js +323 -107
  240. package/project/.sdk/tm/js/test/utility/Custom.test.js +41 -63
  241. package/project/.sdk/tm/js/test/utility/PrimaryUtility.test.js +390 -116
  242. package/project/.sdk/tm/js/test/utility/StructUtility.test.js +728 -175
  243. package/project/.sdk/tm/js/test/utility/index.js +9 -0
  244. package/project/.sdk/tm/js/test/utility.js +72 -0
  245. package/project/.sdk/tm/lua/LICENSE +22 -0
  246. package/project/.sdk/tm/lua/Makefile +10 -0
  247. package/project/.sdk/tm/lua/core/context.lua +208 -0
  248. package/project/.sdk/tm/lua/core/control.lua +17 -0
  249. package/project/.sdk/tm/lua/core/error.lua +30 -0
  250. package/project/.sdk/tm/lua/core/helpers.lua +30 -0
  251. package/project/.sdk/tm/lua/core/operation.lua +51 -0
  252. package/project/.sdk/tm/lua/core/response.lua +45 -0
  253. package/project/.sdk/tm/lua/core/result.lua +58 -0
  254. package/project/.sdk/tm/lua/core/spec.lua +29 -0
  255. package/project/.sdk/tm/lua/core/utility_type.lua +90 -0
  256. package/project/.sdk/tm/lua/feature/base_feature.lua +35 -0
  257. package/project/.sdk/tm/lua/feature/log_feature.lua +80 -0
  258. package/project/.sdk/tm/lua/feature/test_feature.lua +202 -0
  259. package/project/.sdk/tm/lua/src/feature/README.md +3 -0
  260. package/project/.sdk/tm/lua/src/feature/base/.gitkeep +0 -0
  261. package/project/.sdk/tm/lua/src/feature/log/.gitkeep +0 -0
  262. package/project/.sdk/tm/lua/src/feature/test/.gitkeep +0 -0
  263. package/project/.sdk/tm/lua/test/primary_utility_test.lua +1213 -0
  264. package/project/.sdk/tm/lua/test/runner.lua +86 -0
  265. package/project/.sdk/tm/lua/test/struct_runner.lua +602 -0
  266. package/project/.sdk/tm/lua/test/struct_utility_test.lua +959 -0
  267. package/project/.sdk/tm/lua/utility/clean.lua +7 -0
  268. package/project/.sdk/tm/lua/utility/done.lua +19 -0
  269. package/project/.sdk/tm/lua/utility/feature_add.lua +8 -0
  270. package/project/.sdk/tm/lua/utility/feature_hook.lua +21 -0
  271. package/project/.sdk/tm/lua/utility/feature_init.lua +24 -0
  272. package/project/.sdk/tm/lua/utility/fetcher.lua +96 -0
  273. package/project/.sdk/tm/lua/utility/make_context.lua +9 -0
  274. package/project/.sdk/tm/lua/utility/make_error.lua +73 -0
  275. package/project/.sdk/tm/lua/utility/make_fetch_def.lua +43 -0
  276. package/project/.sdk/tm/lua/utility/make_options.lua +116 -0
  277. package/project/.sdk/tm/lua/utility/make_point.lua +92 -0
  278. package/project/.sdk/tm/lua/utility/make_request.lua +58 -0
  279. package/project/.sdk/tm/lua/utility/make_response.lua +44 -0
  280. package/project/.sdk/tm/lua/utility/make_result.lua +51 -0
  281. package/project/.sdk/tm/lua/utility/make_spec.lua +72 -0
  282. package/project/.sdk/tm/lua/utility/make_url.lua +46 -0
  283. package/project/.sdk/tm/lua/utility/param.lua +68 -0
  284. package/project/.sdk/tm/lua/utility/prepare_auth.lua +39 -0
  285. package/project/.sdk/tm/lua/utility/prepare_body.lua +14 -0
  286. package/project/.sdk/tm/lua/utility/prepare_headers.lua +20 -0
  287. package/project/.sdk/tm/lua/utility/prepare_method.lua +17 -0
  288. package/project/.sdk/tm/lua/utility/prepare_params.lua +36 -0
  289. package/project/.sdk/tm/lua/utility/prepare_path.lua +19 -0
  290. package/project/.sdk/tm/lua/utility/prepare_query.lua +41 -0
  291. package/project/.sdk/tm/lua/utility/register.lua +71 -0
  292. package/project/.sdk/tm/lua/utility/result_basic.lua +32 -0
  293. package/project/.sdk/tm/lua/utility/result_body.lua +17 -0
  294. package/project/.sdk/tm/lua/utility/result_headers.lua +22 -0
  295. package/project/.sdk/tm/lua/utility/struct/struct.lua +3427 -0
  296. package/project/.sdk/tm/lua/utility/transform_request.lua +31 -0
  297. package/project/.sdk/tm/lua/utility/transform_response.lua +44 -0
  298. package/project/.sdk/tm/php/LICENSE +22 -0
  299. package/project/.sdk/tm/php/Makefile +10 -0
  300. package/project/.sdk/tm/php/core/Context.php +139 -0
  301. package/project/.sdk/tm/php/core/Control.php +18 -0
  302. package/project/.sdk/tm/php/core/Error.php +37 -0
  303. package/project/.sdk/tm/php/core/Helpers.php +25 -0
  304. package/project/.sdk/tm/php/core/Operation.php +36 -0
  305. package/project/.sdk/tm/php/core/Response.php +30 -0
  306. package/project/.sdk/tm/php/core/Result.php +35 -0
  307. package/project/.sdk/tm/php/core/Spec.php +38 -0
  308. package/project/.sdk/tm/php/core/UtilityType.php +89 -0
  309. package/project/.sdk/tm/php/feature/BaseFeature.php +37 -0
  310. package/project/.sdk/tm/php/feature/LogFeature.php +65 -0
  311. package/project/.sdk/tm/php/feature/TestFeature.php +156 -0
  312. package/project/.sdk/tm/php/src/feature/README.md +3 -0
  313. package/project/.sdk/tm/php/src/feature/base/.gitkeep +0 -0
  314. package/project/.sdk/tm/php/src/feature/log/.gitkeep +0 -0
  315. package/project/.sdk/tm/php/src/feature/test/.gitkeep +0 -0
  316. package/project/.sdk/tm/php/test/PrimaryUtilityTest.php +1309 -0
  317. package/project/.sdk/tm/php/test/Runner.php +112 -0
  318. package/project/.sdk/tm/php/test/StructRunner.php +275 -0
  319. package/project/.sdk/tm/php/test/StructUtilityTest.php +1336 -0
  320. package/project/.sdk/tm/php/utility/Clean.php +12 -0
  321. package/project/.sdk/tm/php/utility/Done.php +26 -0
  322. package/project/.sdk/tm/php/utility/FeatureAdd.php +12 -0
  323. package/project/.sdk/tm/php/utility/FeatureHook.php +23 -0
  324. package/project/.sdk/tm/php/utility/FeatureInit.php +25 -0
  325. package/project/.sdk/tm/php/utility/Fetcher.php +105 -0
  326. package/project/.sdk/tm/php/utility/MakeContext.php +14 -0
  327. package/project/.sdk/tm/php/utility/MakeError.php +59 -0
  328. package/project/.sdk/tm/php/utility/MakeFetchDef.php +36 -0
  329. package/project/.sdk/tm/php/utility/MakeOptions.php +102 -0
  330. package/project/.sdk/tm/php/utility/MakePoint.php +87 -0
  331. package/project/.sdk/tm/php/utility/MakeRequest.php +57 -0
  332. package/project/.sdk/tm/php/utility/MakeResponse.php +43 -0
  333. package/project/.sdk/tm/php/utility/MakeResult.php +53 -0
  334. package/project/.sdk/tm/php/utility/MakeSpec.php +64 -0
  335. package/project/.sdk/tm/php/utility/MakeUrl.php +41 -0
  336. package/project/.sdk/tm/php/utility/Param.php +68 -0
  337. package/project/.sdk/tm/php/utility/PrepareAuth.php +33 -0
  338. package/project/.sdk/tm/php/utility/PrepareBody.php +15 -0
  339. package/project/.sdk/tm/php/utility/PrepareHeaders.php +18 -0
  340. package/project/.sdk/tm/php/utility/PrepareMethod.php +21 -0
  341. package/project/.sdk/tm/php/utility/PrepareParams.php +34 -0
  342. package/project/.sdk/tm/php/utility/PreparePath.php +20 -0
  343. package/project/.sdk/tm/php/utility/PrepareQuery.php +32 -0
  344. package/project/.sdk/tm/php/utility/Register.php +67 -0
  345. package/project/.sdk/tm/php/utility/ResultBasic.php +29 -0
  346. package/project/.sdk/tm/php/utility/ResultBody.php +17 -0
  347. package/project/.sdk/tm/php/utility/ResultHeaders.php +21 -0
  348. package/project/.sdk/tm/php/utility/TransformRequest.php +27 -0
  349. package/project/.sdk/tm/php/utility/TransformResponse.php +42 -0
  350. package/project/.sdk/tm/php/utility/struct/Struct.php +3535 -0
  351. package/project/.sdk/tm/py/Makefile +10 -0
  352. package/project/.sdk/tm/py/core/__init__.py +0 -0
  353. package/project/.sdk/tm/py/core/context.py +199 -0
  354. package/project/.sdk/tm/py/core/control.py +12 -0
  355. package/project/.sdk/tm/py/core/error.py +18 -0
  356. package/project/.sdk/tm/py/core/helpers.py +15 -0
  357. package/project/.sdk/tm/py/core/operation.py +37 -0
  358. package/project/.sdk/tm/py/core/response.py +34 -0
  359. package/project/.sdk/tm/py/core/result.py +44 -0
  360. package/project/.sdk/tm/py/core/spec.py +23 -0
  361. package/project/.sdk/tm/py/core/utility_type.py +82 -0
  362. package/project/.sdk/tm/py/entity/__init__.py +0 -0
  363. package/project/.sdk/tm/py/feature/__init__.py +0 -0
  364. package/project/.sdk/tm/py/feature/base_feature.py +61 -0
  365. package/project/.sdk/tm/py/feature/log_feature.py +84 -0
  366. package/project/.sdk/tm/py/feature/test_feature.py +164 -0
  367. package/project/.sdk/tm/py/src/feature/README.md +3 -0
  368. package/project/.sdk/tm/py/src/feature/base/.gitkeep +0 -0
  369. package/project/.sdk/tm/py/src/feature/log/.gitkeep +0 -0
  370. package/project/.sdk/tm/py/src/feature/test/.gitkeep +0 -0
  371. package/project/.sdk/tm/py/test/__init__.py +0 -0
  372. package/project/.sdk/tm/py/test/runner.py +90 -0
  373. package/project/.sdk/tm/py/test/struct_runner.py +411 -0
  374. package/project/.sdk/tm/py/test/test_primary_utility.py +1101 -0
  375. package/project/.sdk/tm/py/test/test_struct_utility.py +751 -0
  376. package/project/.sdk/tm/py/utility/__init__.py +0 -0
  377. package/project/.sdk/tm/py/utility/clean.py +5 -0
  378. package/project/.sdk/tm/py/utility/done.py +14 -0
  379. package/project/.sdk/tm/py/utility/feature_add.py +6 -0
  380. package/project/.sdk/tm/py/utility/feature_hook.py +15 -0
  381. package/project/.sdk/tm/py/utility/feature_init.py +18 -0
  382. package/project/.sdk/tm/py/utility/fetcher.py +95 -0
  383. package/project/.sdk/tm/py/utility/make_context.py +7 -0
  384. package/project/.sdk/tm/py/utility/make_error.py +64 -0
  385. package/project/.sdk/tm/py/utility/make_fetch_def.py +37 -0
  386. package/project/.sdk/tm/py/utility/make_options.py +103 -0
  387. package/project/.sdk/tm/py/utility/make_point.py +74 -0
  388. package/project/.sdk/tm/py/utility/make_request.py +52 -0
  389. package/project/.sdk/tm/py/utility/make_response.py +36 -0
  390. package/project/.sdk/tm/py/utility/make_result.py +41 -0
  391. package/project/.sdk/tm/py/utility/make_spec.py +68 -0
  392. package/project/.sdk/tm/py/utility/make_url.py +34 -0
  393. package/project/.sdk/tm/py/utility/param.py +55 -0
  394. package/project/.sdk/tm/py/utility/prepare_auth.py +34 -0
  395. package/project/.sdk/tm/py/utility/prepare_body.py +11 -0
  396. package/project/.sdk/tm/py/utility/prepare_headers.py +17 -0
  397. package/project/.sdk/tm/py/utility/prepare_method.py +15 -0
  398. package/project/.sdk/tm/py/utility/prepare_params.py +28 -0
  399. package/project/.sdk/tm/py/utility/prepare_path.py +16 -0
  400. package/project/.sdk/tm/py/utility/prepare_query.py +33 -0
  401. package/project/.sdk/tm/py/utility/register.py +68 -0
  402. package/project/.sdk/tm/py/utility/result_basic.py +26 -0
  403. package/project/.sdk/tm/py/utility/result_body.py +13 -0
  404. package/project/.sdk/tm/py/utility/result_headers.py +17 -0
  405. package/project/.sdk/tm/py/utility/transform_request.py +27 -0
  406. package/project/.sdk/tm/py/utility/transform_response.py +39 -0
  407. package/project/.sdk/tm/py/utility/voxgig_struct/__init__.py +72 -0
  408. package/project/.sdk/tm/py/utility/voxgig_struct/voxgig_struct.py +2770 -0
  409. package/project/.sdk/tm/rb/Gemfile +4 -0
  410. package/project/.sdk/tm/rb/LICENSE +22 -0
  411. package/project/.sdk/tm/rb/Makefile +10 -0
  412. package/project/.sdk/tm/rb/core/context.rb +105 -0
  413. package/project/.sdk/tm/rb/core/control.rb +11 -0
  414. package/project/.sdk/tm/rb/core/error.rb +24 -0
  415. package/project/.sdk/tm/rb/core/helpers.rb +16 -0
  416. package/project/.sdk/tm/rb/core/operation.rb +26 -0
  417. package/project/.sdk/tm/rb/core/response.rb +20 -0
  418. package/project/.sdk/tm/rb/core/result.rb +23 -0
  419. package/project/.sdk/tm/rb/core/spec.rb +23 -0
  420. package/project/.sdk/tm/rb/core/utility_type.rb +32 -0
  421. package/project/.sdk/tm/rb/feature/base_feature.rb +30 -0
  422. package/project/.sdk/tm/rb/feature/log_feature.rb +50 -0
  423. package/project/.sdk/tm/rb/feature/test_feature.rb +154 -0
  424. package/project/.sdk/tm/rb/src/feature/README.md +3 -0
  425. package/project/.sdk/tm/rb/src/feature/base/.gitkeep +0 -0
  426. package/project/.sdk/tm/rb/src/feature/log/.gitkeep +0 -0
  427. package/project/.sdk/tm/rb/src/feature/test/.gitkeep +0 -0
  428. package/project/.sdk/tm/rb/test/primary_utility_test.rb +1083 -0
  429. package/project/.sdk/tm/rb/test/runner.rb +70 -0
  430. package/project/.sdk/tm/rb/test/struct_runner.rb +309 -0
  431. package/project/.sdk/tm/rb/test/struct_utility_test.rb +670 -0
  432. package/project/.sdk/tm/rb/utility/clean.rb +4 -0
  433. package/project/.sdk/tm/rb/utility/done.rb +14 -0
  434. package/project/.sdk/tm/rb/utility/feature_add.rb +6 -0
  435. package/project/.sdk/tm/rb/utility/feature_hook.rb +11 -0
  436. package/project/.sdk/tm/rb/utility/feature_init.rb +16 -0
  437. package/project/.sdk/tm/rb/utility/fetcher.rb +67 -0
  438. package/project/.sdk/tm/rb/utility/make_context.rb +7 -0
  439. package/project/.sdk/tm/rb/utility/make_error.rb +44 -0
  440. package/project/.sdk/tm/rb/utility/make_fetch_def.rb +24 -0
  441. package/project/.sdk/tm/rb/utility/make_options.rb +57 -0
  442. package/project/.sdk/tm/rb/utility/make_point.rb +77 -0
  443. package/project/.sdk/tm/rb/utility/make_request.rb +44 -0
  444. package/project/.sdk/tm/rb/utility/make_response.rb +25 -0
  445. package/project/.sdk/tm/rb/utility/make_result.rb +33 -0
  446. package/project/.sdk/tm/rb/utility/make_spec.rb +50 -0
  447. package/project/.sdk/tm/rb/utility/make_url.rb +32 -0
  448. package/project/.sdk/tm/rb/utility/param.rb +48 -0
  449. package/project/.sdk/tm/rb/utility/prepare_auth.rb +26 -0
  450. package/project/.sdk/tm/rb/utility/prepare_body.rb +6 -0
  451. package/project/.sdk/tm/rb/utility/prepare_headers.rb +11 -0
  452. package/project/.sdk/tm/rb/utility/prepare_method.rb +5 -0
  453. package/project/.sdk/tm/rb/utility/prepare_params.rb +25 -0
  454. package/project/.sdk/tm/rb/utility/prepare_path.rb +13 -0
  455. package/project/.sdk/tm/rb/utility/prepare_query.rb +22 -0
  456. package/project/.sdk/tm/rb/utility/register.rb +63 -0
  457. package/project/.sdk/tm/rb/utility/result_basic.rb +23 -0
  458. package/project/.sdk/tm/rb/utility/result_body.rb +11 -0
  459. package/project/.sdk/tm/rb/utility/result_headers.rb +15 -0
  460. package/project/.sdk/tm/rb/utility/struct/voxgig_struct.rb +2256 -0
  461. package/project/.sdk/tm/rb/utility/transform_request.rb +15 -0
  462. package/project/.sdk/tm/rb/utility/transform_response.rb +23 -0
  463. package/src/cmp/Main.ts +1 -16
  464. package/src/cmp/Readme.ts +5 -5
  465. package/src/cmp/ReadmeEntity.ts +77 -25
  466. package/src/cmp/ReadmeExplanation.ts +333 -0
  467. package/src/cmp/ReadmeHowto.ts +28 -0
  468. package/src/cmp/ReadmeIntro.ts +10 -24
  469. package/src/cmp/ReadmeModel.ts +57 -88
  470. package/src/cmp/ReadmeOptions.ts +40 -11
  471. package/src/cmp/ReadmeQuick.ts +4 -1
  472. package/src/cmp/ReadmeRef.ts +1057 -40
  473. package/src/cmp/ReadmeTop.ts +213 -0
  474. package/src/sdkgen.ts +12 -0
  475. package/project/.sdk/src/cmp/js/Quick_js.ts +0 -78
  476. package/project/.sdk/src/cmp/js/TestAcceptEntity_js.ts +0 -13
  477. package/project/.sdk/src/cmp/js/TestAccept_js.ts +0 -18
  478. package/project/.sdk/tm/go/test/exists_test.go +0 -16
  479. package/project/.sdk/tm/js/src/utility/AuthUtility.js +0 -21
  480. package/project/.sdk/tm/js/src/utility/BodyUtility.js +0 -29
  481. package/project/.sdk/tm/js/src/utility/ErrorUtility.js +0 -33
  482. package/project/.sdk/tm/js/src/utility/FindparamUtility.js +0 -31
  483. package/project/.sdk/tm/js/src/utility/FullurlUtility.js +0 -39
  484. package/project/.sdk/tm/js/src/utility/HeadersUtility.js +0 -13
  485. package/project/.sdk/tm/js/src/utility/JoinurlUtility.js +0 -14
  486. package/project/.sdk/tm/js/src/utility/OperatorUtility.js +0 -44
  487. package/project/.sdk/tm/js/src/utility/OptionsUtility.js +0 -54
  488. package/project/.sdk/tm/js/src/utility/ParamsUtility.js +0 -21
  489. package/project/.sdk/tm/js/src/utility/QueryUtility.js +0 -21
  490. package/project/.sdk/tm/js/src/utility/ReqformUtility.js +0 -32
  491. package/project/.sdk/tm/js/src/utility/RequestUtility.js +0 -48
  492. package/project/.sdk/tm/js/src/utility/ResbasicUtility.js +0 -27
  493. package/project/.sdk/tm/js/src/utility/ResbodyUtility.js +0 -15
  494. package/project/.sdk/tm/js/src/utility/ResformUtility.js +0 -34
  495. package/project/.sdk/tm/js/src/utility/ResheadersUtility.js +0 -19
  496. package/project/.sdk/tm/js/src/utility/ResponseUtility.js +0 -37
  497. package/project/.sdk/tm/js/src/utility/ResultUtility.js +0 -28
  498. package/project/.sdk/tm/js/src/utility/SpecUtility.js +0 -35
@@ -0,0 +1,3535 @@
1
+ <?php
2
+ declare(strict_types=1);
3
+
4
+ namespace Voxgig\Struct;
5
+
6
+ /**
7
+ * Reference-stable wrapper for PHP arrays.
8
+ * PHP arrays are value types (copy-on-write), so storing them in injection
9
+ * state loses reference identity. ListRef wraps the array in an object
10
+ * (reference type) so mutations via setval/delprop propagate through the
11
+ * injection pipeline. Mirrors Go's ListRef[T] strategy.
12
+ */
13
+ class ListRef implements \ArrayAccess, \Countable, \IteratorAggregate
14
+ {
15
+ public array $list;
16
+
17
+ public function __construct(array $list = [])
18
+ {
19
+ $this->list = $list;
20
+ }
21
+
22
+ public function offsetExists(mixed $offset): bool
23
+ {
24
+ return isset($this->list[$offset]);
25
+ }
26
+
27
+ public function offsetGet(mixed $offset): mixed
28
+ {
29
+ return $this->list[$offset] ?? null;
30
+ }
31
+
32
+ public function offsetSet(mixed $offset, mixed $value): void
33
+ {
34
+ if ($offset === null) {
35
+ $this->list[] = $value;
36
+ } else {
37
+ $this->list[$offset] = $value;
38
+ }
39
+ }
40
+
41
+ public function offsetUnset(mixed $offset): void
42
+ {
43
+ array_splice($this->list, (int)$offset, 1);
44
+ }
45
+
46
+ public function count(): int
47
+ {
48
+ return count($this->list);
49
+ }
50
+
51
+ public function getIterator(): \ArrayIterator
52
+ {
53
+ return new \ArrayIterator($this->list);
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Class Struct
59
+ *
60
+ * Utility class for manipulating in-memory JSON-like data structures.
61
+ * These utilities implement functions similar to the TypeScript version,
62
+ * with emphasis on handling nodes, maps, lists, and special "undefined" values.
63
+ */
64
+ class Struct
65
+ {
66
+
67
+ /* =======================
68
+ * String Constants
69
+ * =======================
70
+ */
71
+ private const S_MKEYPRE = 'key:pre';
72
+ private const S_MKEYPOST = 'key:post';
73
+ private const S_MVAL = 'val';
74
+ private const S_MKEY = 'key';
75
+
76
+ private const S_DKEY = '`$KEY`';
77
+ private const S_DMETA = '`$META`';
78
+ private const S_DANNO = '`$ANNO`';
79
+
80
+ // Match TypeScript constants exactly
81
+ private const S_BKEY = '`$KEY`';
82
+ private const S_BANNO = '`$ANNO`';
83
+ private const S_DTOP = '$TOP';
84
+ private const S_DERRS = '$ERRS';
85
+ private const S_ERRS = '$ERRS';
86
+
87
+ private const S_array = 'array';
88
+ private const S_boolean = 'boolean';
89
+ private const S_function = 'function';
90
+ private const S_number = 'number';
91
+ private const S_object = 'object';
92
+ private const S_string = 'string';
93
+ private const S_null = 'null';
94
+ private const S_MT = '';
95
+ private const S_BT = '`';
96
+ private const S_DS = '$';
97
+ private const S_DT = '.';
98
+ private const S_CN = ':';
99
+ private const S_KEY = 'KEY';
100
+ public const S_BASE = 'base';
101
+
102
+ /**
103
+ * Legacy string marker for undefined. Kept for backward compatibility.
104
+ * Internal code uses the $UNDEF sentinel object via undef().
105
+ */
106
+ public const UNDEF = '__UNDEFINED__';
107
+
108
+ /** Sentinel object for undefined — can never collide with real data. */
109
+ private static ?\stdClass $UNDEF = null;
110
+
111
+ /** Return the sentinel object for undefined. */
112
+ public static function undef(): \stdClass
113
+ {
114
+ if (self::$UNDEF === null) {
115
+ self::$UNDEF = new \stdClass();
116
+ }
117
+ return self::$UNDEF;
118
+ }
119
+
120
+ public const T_any = (1 << 31) - 1;
121
+ public const T_noval = 1 << 30;
122
+ public const T_boolean = 1 << 29;
123
+ public const T_decimal = 1 << 28;
124
+ public const T_integer = 1 << 27;
125
+ public const T_number = 1 << 26;
126
+ public const T_string = 1 << 25;
127
+ public const T_function = 1 << 24;
128
+ public const T_symbol = 1 << 23;
129
+ public const T_null = 1 << 22;
130
+ public const T_list = 1 << 14;
131
+ public const T_map = 1 << 13;
132
+ public const T_instance = 1 << 12;
133
+ public const T_scalar = 1 << 7;
134
+ public const T_node = 1 << 6;
135
+
136
+ public const DELETE = ['`$DELETE`' => true];
137
+
138
+ private const S_CM = ',';
139
+
140
+ private const TYPENAME = [
141
+ 'any', 'noval', 'boolean', 'decimal', 'integer', 'number', 'string',
142
+ 'function', 'symbol', 'null',
143
+ '', '', '', '', '', '', '',
144
+ 'list', 'map', 'instance',
145
+ '', '', '', '',
146
+ 'scalar', 'node',
147
+ ];
148
+
149
+ public const SKIP = ['`$SKIP`' => true];
150
+
151
+ // Mode constants (bitfield) matching TypeScript canonical
152
+ public const M_KEYPRE = 1;
153
+ public const M_KEYPOST = 2;
154
+ public const M_VAL = 4;
155
+
156
+ public const MODENAME = [
157
+ self::M_VAL => 'val',
158
+ self::M_KEYPRE => 'key:pre',
159
+ self::M_KEYPOST => 'key:post',
160
+ ];
161
+
162
+ private const PLACEMENT = [
163
+ self::M_VAL => 'value',
164
+ self::M_KEYPRE => 'key',
165
+ self::M_KEYPOST => 'key',
166
+ ];
167
+
168
+ /* =======================
169
+ * Regular expressions for validation and transformation
170
+ * =======================
171
+ */
172
+ private const R_META_PATH = '/^([^$]+)\$([=~])(.+)$/';
173
+ private const R_TRANSFORM_NAME = '/`\$([A-Z]+)`/';
174
+
175
+ /* =======================
176
+ * Private Helpers
177
+ * =======================
178
+ */
179
+
180
+ /**
181
+ * Determines whether an array has sequential integer keys, i.e. a list.
182
+ *
183
+ * @param array $val
184
+ * @return bool True if the array is a list (i.e. sequential keys starting at 0).
185
+ */
186
+ private static function isListHelper(array $val): bool
187
+ {
188
+ return array_keys($val) === range(0, count($val) - 1);
189
+ }
190
+
191
+ /* =======================
192
+ * Type and Existence Checks
193
+ * =======================
194
+ */
195
+
196
+ public static function isnode(mixed $val): bool
197
+ {
198
+ if ($val === self::undef() || $val === null) {
199
+ return false;
200
+ }
201
+ if ($val instanceof \Closure) {
202
+ return false;
203
+ }
204
+ if ($val instanceof ListRef) {
205
+ return true;
206
+ }
207
+ return is_object($val) || is_array($val);
208
+ }
209
+
210
+
211
+
212
+ /**
213
+ * Check if a value is a map (associative array or object) rather than a list.
214
+ *
215
+ * @param mixed $val
216
+ * @return bool
217
+ */
218
+ public static function ismap(mixed $val): bool
219
+ {
220
+ if ($val instanceof ListRef) {
221
+ return false;
222
+ }
223
+ if ($val instanceof \Closure) {
224
+ return false;
225
+ }
226
+ if ($val === self::undef()) {
227
+ return false;
228
+ }
229
+ if (is_object($val)) {
230
+ return true;
231
+ }
232
+ // Any PHP array that isn't a list is a map,
233
+ // but treat *empty* arrays as lists (not maps).
234
+ if (is_array($val)) {
235
+ if (count($val) === 0) {
236
+ return false;
237
+ }
238
+ return !self::islist($val);
239
+ }
240
+ return false;
241
+ }
242
+
243
+
244
+
245
+ /**
246
+ * Check if a value is a list (sequential array).
247
+ *
248
+ * @param mixed $val
249
+ * @return bool
250
+ */
251
+ public static function islist(mixed $val): bool
252
+ {
253
+ if ($val instanceof ListRef) {
254
+ return true;
255
+ }
256
+ if (!is_array($val)) {
257
+ return false;
258
+ }
259
+ $i = 0;
260
+ foreach ($val as $k => $_) {
261
+ if ($k !== $i++) {
262
+ return false;
263
+ }
264
+ }
265
+ return true;
266
+ }
267
+
268
+ /**
269
+ * Check if a key is valid (non-empty string or integer/float).
270
+ *
271
+ * @param mixed $key
272
+ * @return bool
273
+ */
274
+ public static function iskey(mixed $key): bool
275
+ {
276
+ if ($key === self::undef()) { // Explicit check for UNDEF
277
+ return false;
278
+ }
279
+ if (is_string($key)) {
280
+ return strlen($key) > 0;
281
+ }
282
+ return is_int($key) || is_float($key);
283
+ }
284
+ /**
285
+ * Check if a value is empty.
286
+ * Considers undefined, null, empty string, empty array, or empty object.
287
+ *
288
+ * @param mixed $val
289
+ * @return bool
290
+ */
291
+ public static function isempty(mixed $val): bool
292
+ {
293
+ if ($val === self::undef() || $val === null || $val === self::S_MT) {
294
+ return true;
295
+ }
296
+ if (is_array($val) && count($val) === 0) {
297
+ return true;
298
+ }
299
+ if (is_object($val) && count(get_object_vars($val)) === 0) {
300
+ return true;
301
+ }
302
+ return false;
303
+ }
304
+
305
+ /**
306
+ * Check if a value is callable.
307
+ *
308
+ * @param mixed $val
309
+ * @return bool
310
+ */
311
+ public static function isfunc(mixed $val): bool
312
+ {
313
+ return is_callable($val);
314
+ }
315
+
316
+ public static function typify(mixed $value): int
317
+ {
318
+ if ($value === self::undef()) {
319
+ return self::T_noval;
320
+ }
321
+ if ($value === null) {
322
+ return self::T_scalar | self::T_null;
323
+ }
324
+ if (is_bool($value)) {
325
+ return self::T_scalar | self::T_boolean;
326
+ }
327
+ if (is_int($value)) {
328
+ return self::T_scalar | self::T_number | self::T_integer;
329
+ }
330
+ if (is_float($value)) {
331
+ return self::T_scalar | self::T_number | self::T_decimal;
332
+ }
333
+ if (is_string($value)) {
334
+ return self::T_scalar | self::T_string;
335
+ }
336
+ if ($value instanceof \Closure) {
337
+ return self::T_scalar | self::T_function;
338
+ }
339
+ if (is_callable($value) && !is_array($value) && !is_object($value)) {
340
+ return self::T_scalar | self::T_function;
341
+ }
342
+ if ($value instanceof ListRef) {
343
+ return self::T_node | self::T_list;
344
+ }
345
+ if (is_array($value)) {
346
+ if (self::islist($value)) {
347
+ return self::T_node | self::T_list;
348
+ } else {
349
+ return self::T_node | self::T_map;
350
+ }
351
+ }
352
+ if (is_object($value)) {
353
+ return self::T_node | self::T_map;
354
+ }
355
+ return self::T_noval;
356
+ }
357
+
358
+ public static function typename(int $type): string
359
+ {
360
+ if ($type <= 0) {
361
+ return self::TYPENAME[0];
362
+ }
363
+ $clz = 31 - (int) floor(log($type, 2));
364
+ return self::TYPENAME[$clz] ?? self::TYPENAME[0];
365
+ }
366
+
367
+ /**
368
+ * Get a defined value. Returns alt if val is undefined.
369
+ */
370
+ public static function getdef(mixed $val, mixed $alt): mixed
371
+ {
372
+ if ($val === self::undef() || $val === null) {
373
+ return $alt;
374
+ }
375
+ return $val;
376
+ }
377
+
378
+ /**
379
+ * Replace a search string (all), or a regex pattern, in a source string.
380
+ */
381
+ public static function replace(string $s, string|array $from, mixed $to): string
382
+ {
383
+ $rs = $s;
384
+ $ts = self::typify($s);
385
+ if (0 === (self::T_string & $ts)) {
386
+ $rs = self::stringify($s);
387
+ } elseif (0 < ((self::T_noval | self::T_null) & $ts)) {
388
+ $rs = self::S_MT;
389
+ }
390
+ if (is_string($from) && @preg_match($from, '') !== false && $from[0] === '/') {
391
+ return preg_replace($from, (string)$to, $rs);
392
+ }
393
+ return str_replace((string)$from, (string)$to, $rs);
394
+ }
395
+
396
+ /**
397
+ * Define a JSON Object using key-value arguments.
398
+ */
399
+ public static function jm(mixed ...$kv): object
400
+ {
401
+ $kvsize = count($kv);
402
+ $o = new \stdClass();
403
+ for ($i = 0; $i < $kvsize; $i += 2) {
404
+ $k = $kv[$i] ?? ('$KEY' . $i);
405
+ $k = is_string($k) ? $k : self::stringify($k);
406
+ $o->$k = $kv[$i + 1] ?? null;
407
+ }
408
+ return $o;
409
+ }
410
+
411
+ /**
412
+ * Define a JSON Array using arguments.
413
+ */
414
+ public static function jt(mixed ...$v): array
415
+ {
416
+ return array_values($v);
417
+ }
418
+
419
+ public static function getprop(mixed $val, mixed $key, mixed $alt = null): mixed
420
+ {
421
+ $altExplicit = func_num_args() >= 3;
422
+ $out = self::_getprop($val, $key, self::undef());
423
+ if ($out === self::undef()) {
424
+ return $altExplicit ? $alt : null;
425
+ }
426
+ return $out;
427
+ }
428
+
429
+
430
+ // Internal getprop returning the UNDEF sentinel for missing keys so the
431
+ // injection/transform machinery can distinguish "missing" from a stored null.
432
+ // External callers should use getprop(), which normalises UNDEF to null (or $alt).
433
+ public static function _getprop(mixed $val, mixed $key, mixed $alt = null): mixed
434
+ {
435
+ if ($alt === null) { $alt = self::undef(); }
436
+ // 1) undefined‐marker or invalid key → alt
437
+ if ($val === self::undef() || $key === self::undef()) {
438
+ return $alt;
439
+ }
440
+ if (!self::iskey($key)) {
441
+ return $alt;
442
+ }
443
+ if ($val === null) {
444
+ return $alt;
445
+ }
446
+
447
+ // 2) ListRef branch
448
+ if ($val instanceof ListRef) {
449
+ $ki = is_numeric($key) ? (int)$key : -1;
450
+ $out = ($ki >= 0 && $ki < count($val->list)) ? $val->list[$ki] : $alt;
451
+ }
452
+ // 3) array branch
453
+ elseif (is_array($val) && array_key_exists($key, $val)) {
454
+ $out = $val[$key];
455
+ }
456
+ // 4) object branch: cast $key to string
457
+ elseif (is_object($val)) {
458
+ $prop = (string) $key;
459
+ if (property_exists($val, $prop)) {
460
+ $out = $val->$prop;
461
+ } else {
462
+ $out = $alt;
463
+ }
464
+ }
465
+ // 4) fallback
466
+ else {
467
+ $out = $alt;
468
+ }
469
+
470
+ // 5) JSON‐null‐marker check
471
+ return ($out === self::undef() ? $alt : $out);
472
+ }
473
+
474
+
475
+ public static function strkey(mixed $key = null): string
476
+ {
477
+ if ($key === null || $key === self::undef()) {
478
+ return self::S_MT;
479
+ }
480
+ if (is_string($key)) {
481
+ return $key;
482
+ }
483
+ if (is_bool($key)) {
484
+ return self::S_MT;
485
+ }
486
+ if (is_int($key)) {
487
+ return (string) $key;
488
+ }
489
+ if (is_float($key)) {
490
+ return (string) floor($key);
491
+ }
492
+ return self::S_MT;
493
+ }
494
+
495
+ /**
496
+ * Get a sorted list of keys from a node (map or list).
497
+ *
498
+ * @param mixed $val
499
+ * @return array
500
+ */
501
+ public static function keysof(mixed $val): array
502
+ {
503
+ if (!self::isnode($val)) {
504
+ return [];
505
+ }
506
+ if (self::ismap($val)) {
507
+ $keys = is_array($val) ? array_keys($val) : array_keys(get_object_vars($val));
508
+ sort($keys, SORT_STRING);
509
+ return $keys;
510
+ } elseif ($val instanceof ListRef) {
511
+ return array_map('strval', array_keys($val->list));
512
+ } elseif (self::islist($val)) {
513
+ return array_map('strval', array_keys($val));
514
+ }
515
+ return [];
516
+ }
517
+
518
+ /**
519
+ * Determine if a node has a defined property with the given key.
520
+ *
521
+ * @param mixed $val
522
+ * @param mixed $key
523
+ * @return bool
524
+ */
525
+ public static function haskey(mixed $val = null, mixed $key = null): bool
526
+ {
527
+ // 1. Validate $val is a node
528
+ if (!self::isnode($val)) {
529
+ return false;
530
+ }
531
+
532
+ // 2. Validate $key is a valid key
533
+ if (!self::iskey($key)) {
534
+ return false;
535
+ }
536
+
537
+ // 3. Check property existence
538
+ $marker = new \stdClass();
539
+ return self::_getprop($val, $key, $marker) !== $marker;
540
+ }
541
+
542
+ public static function items(mixed $val, ?callable $apply = null): array
543
+ {
544
+ $result = [];
545
+ if (self::islist($val)) {
546
+ foreach ($val as $k => $v) {
547
+ $result[] = [(string) $k, $v];
548
+ }
549
+ } else {
550
+ foreach (self::keysof($val) as $k) {
551
+ $result[] = [$k, self::_getprop($val, $k)];
552
+ }
553
+ }
554
+ if ($apply !== null) {
555
+ $result = array_map($apply, $result);
556
+ }
557
+ return $result;
558
+ }
559
+
560
+ public static function escre(?string $s): string
561
+ {
562
+ $s = $s ?? self::S_MT;
563
+ return preg_quote($s, '/');
564
+ }
565
+
566
+ public static function escurl(?string $s): string
567
+ {
568
+ $s = $s ?? self::S_MT;
569
+ return rawurlencode($s);
570
+ }
571
+
572
+ public static function joinurl(array $sarr): string
573
+ {
574
+ return self::join($sarr, '/', true);
575
+ }
576
+
577
+ public static function filter(mixed $val, callable $check): array
578
+ {
579
+ $all = self::items($val);
580
+ $numall = self::size($all);
581
+ $out = [];
582
+ for ($i = 0; $i < $numall; $i++) {
583
+ if ($check($all[$i])) {
584
+ $out[] = $all[$i][1];
585
+ }
586
+ }
587
+ return $out;
588
+ }
589
+
590
+ public static function join(mixed $arr, ?string $sep = null, ?bool $url = false): string
591
+ {
592
+ $sarr = self::size($arr);
593
+ $sepdef = $sep ?? self::S_CM;
594
+ $sepre = (1 === strlen($sepdef)) ? self::escre($sepdef) : '';
595
+
596
+ $filtered = self::filter($arr, function ($n) {
597
+ return (0 < (self::T_string & self::typify($n[1]))) && self::S_MT !== $n[1];
598
+ });
599
+
600
+ $mapped = self::filter(
601
+ self::items($filtered, function ($n) use ($sepre, $sepdef, $url, $sarr) {
602
+ $i = (int) $n[0];
603
+ $s = $n[1];
604
+
605
+ if ('' !== $sepre && self::S_MT !== $sepre) {
606
+ if ($url && 0 === $i) {
607
+ $s = preg_replace('/' . $sepre . '+$/', self::S_MT, $s);
608
+ return $s;
609
+ }
610
+
611
+ if (0 < $i) {
612
+ $s = preg_replace('/^' . $sepre . '+/', self::S_MT, $s);
613
+ }
614
+
615
+ if ($i < $sarr - 1 || !$url) {
616
+ $s = preg_replace('/' . $sepre . '+$/', self::S_MT, $s);
617
+ }
618
+
619
+ $s = preg_replace('/([^' . $sepre . '])' . $sepre . '+([^' . $sepre . '])/',
620
+ '$1' . $sepdef . '$2', $s);
621
+ }
622
+
623
+ return $s;
624
+ }),
625
+ function ($n) {
626
+ return self::S_MT !== $n[1];
627
+ }
628
+ );
629
+
630
+ return implode($sepdef, $mapped);
631
+ }
632
+
633
+ public static function jsonify(mixed $val, mixed $flags = null): string
634
+ {
635
+ $str = 'null';
636
+
637
+ if ($val !== null && $val !== self::undef() && !($val instanceof \Closure)) {
638
+ if ($val instanceof ListRef) {
639
+ $val = self::cloneUnwrap($val);
640
+ }
641
+ $indent = self::_getprop($flags, 'indent', 2);
642
+ try {
643
+ $encoded = json_encode($val, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
644
+ if ($encoded === false) {
645
+ return '__JSONIFY_FAILED__';
646
+ }
647
+
648
+ // PHP's JSON_PRETTY_PRINT uses 4-space indents; convert to requested indent
649
+ $encoded = preg_replace_callback('/^( +)/m', function ($matches) use ($indent) {
650
+ $level = (int)(strlen($matches[1]) / 4);
651
+ return str_repeat(' ', $level * $indent);
652
+ }, $encoded);
653
+
654
+ $str = $encoded;
655
+
656
+ $offset = self::_getprop($flags, 'offset', 0);
657
+ if (0 < $offset) {
658
+ $lines = explode("\n", $str);
659
+ $rest = array_slice($lines, 1);
660
+ $padded = self::filter(
661
+ self::items($rest, function ($n) use ($offset) {
662
+ return self::pad($n[1], 0 - $offset - self::size($n[1]));
663
+ }),
664
+ function ($n) { return true; }
665
+ );
666
+ $str = "{\n" . implode("\n", $padded);
667
+ }
668
+ } catch (\Exception $e) {
669
+ $str = '__JSONIFY_FAILED__';
670
+ }
671
+ }
672
+
673
+ return $str;
674
+ }
675
+
676
+ /**
677
+ * The integer size of the value. For arrays and strings, the length,
678
+ * for numbers, the integer part, for boolean, true is 1 and false is 0, for all other values, 0.
679
+ */
680
+ public static function size(mixed $val): int
681
+ {
682
+ if ($val === null || $val === self::undef()) {
683
+ return 0;
684
+ }
685
+
686
+ if (self::islist($val)) {
687
+ return count($val);
688
+ } elseif (self::ismap($val)) {
689
+ return count(get_object_vars($val));
690
+ }
691
+
692
+ if (is_string($val)) {
693
+ return strlen($val);
694
+ } elseif (is_numeric($val)) {
695
+ return (int) floor((float) $val);
696
+ } elseif (is_bool($val)) {
697
+ return $val ? 1 : 0;
698
+ } else {
699
+ return 0;
700
+ }
701
+ }
702
+
703
+ /**
704
+ * Extract part of an array or string into a new value, from the start point to the end point.
705
+ * If no end is specified, extract to the full length of the value. Negative arguments count
706
+ * from the end of the value.
707
+ */
708
+ public static function slice(mixed $val, ?int $start = null, ?int $end = null): mixed
709
+ {
710
+ if (is_numeric($val)) {
711
+ $start = $start ?? PHP_INT_MIN;
712
+ $end = ($end ?? PHP_INT_MAX) - 1;
713
+ $result = min(max((float) $val, $start), $end);
714
+ // Return integer if the original value was an integer
715
+ return is_int($val) ? (int) $result : $result;
716
+ }
717
+
718
+ $vlen = self::size($val);
719
+
720
+ if ($end !== null && $start === null) {
721
+ $start = 0;
722
+ }
723
+
724
+ if ($start !== null) {
725
+ if ($start < 0) {
726
+ $end = $vlen + $start;
727
+ if ($end < 0) {
728
+ $end = 0;
729
+ }
730
+ $start = 0;
731
+ } elseif ($end !== null) {
732
+ if ($end < 0) {
733
+ $end = $vlen + $end;
734
+ if ($end < 0) {
735
+ $end = 0;
736
+ }
737
+ } elseif ($vlen < $end) {
738
+ $end = $vlen;
739
+ }
740
+ } else {
741
+ $end = $vlen;
742
+ }
743
+
744
+ if ($vlen < $start) {
745
+ $start = $vlen;
746
+ }
747
+
748
+ if (-1 < $start && $start <= $end && $end <= $vlen) {
749
+ if ($val instanceof ListRef) {
750
+ $val = new ListRef(array_slice($val->list, $start, $end - $start));
751
+ } elseif (self::islist($val)) {
752
+ $val = array_slice($val, $start, $end - $start);
753
+ } elseif (is_string($val)) {
754
+ $val = substr($val, $start, $end - $start);
755
+ }
756
+ } else {
757
+ if ($val instanceof ListRef) {
758
+ $val = new ListRef([]);
759
+ } elseif (self::islist($val)) {
760
+ $val = [];
761
+ } elseif (is_string($val)) {
762
+ $val = self::S_MT;
763
+ }
764
+ }
765
+ }
766
+
767
+ return $val;
768
+ }
769
+
770
+ /**
771
+ * Pad a string with a character to a specified length.
772
+ */
773
+ public static function pad(mixed $str, ?int $padding = null, ?string $padchar = null): string
774
+ {
775
+ $str = self::stringify($str);
776
+ $padding = $padding ?? 44;
777
+ $padchar = $padchar ?? ' ';
778
+ $padchar = ($padchar . ' ')[0]; // Get first character or space as fallback
779
+
780
+ if ($padding >= 0) {
781
+ return str_pad($str, $padding, $padchar, STR_PAD_RIGHT);
782
+ } else {
783
+ return str_pad($str, abs($padding), $padchar, STR_PAD_LEFT);
784
+ }
785
+ }
786
+
787
+ /* =======================
788
+ * Stringification and Cloning
789
+ * =======================
790
+ */
791
+
792
+ /**
793
+ * Recursively sorts a node (array or object) to ensure consistent stringification.
794
+ *
795
+ * @param mixed $val
796
+ * @return mixed
797
+ */
798
+ private static function sort_obj(mixed $val): mixed
799
+ {
800
+ if (is_array($val)) {
801
+ if (self::islist($val)) {
802
+ return array_map([self::class, 'sort_obj'], $val);
803
+ } else {
804
+ ksort($val);
805
+ foreach ($val as $k => $v) {
806
+ $val[$k] = self::sort_obj($v);
807
+ }
808
+ return $val;
809
+ }
810
+ } elseif (is_object($val)) {
811
+ $arr = get_object_vars($val);
812
+ ksort($arr);
813
+ foreach ($arr as $k => $v) {
814
+ $arr[$k] = self::sort_obj($v);
815
+ }
816
+ return $arr;
817
+ }
818
+ return $val;
819
+ }
820
+
821
+ public static function stringify(mixed $val, ?int $maxlen = null, mixed $pretty = null): string
822
+ {
823
+ if ($val === self::undef()) {
824
+ return $pretty ? '<>' : self::S_MT;
825
+ }
826
+
827
+ $valstr = self::S_MT;
828
+
829
+ if (is_string($val)) {
830
+ $valstr = $val;
831
+ } else {
832
+ $original = $val;
833
+ // Unwrap ListRefs before JSON encoding
834
+ if ($val instanceof ListRef) {
835
+ $val = self::cloneUnwrap($val);
836
+ }
837
+ try {
838
+ $sorted = self::sort_obj($val);
839
+ $str = json_encode($sorted);
840
+ if ($str === false) {
841
+ $str = '__STRINGIFY_FAILED__';
842
+ }
843
+ $valstr = str_replace('"', '', $str);
844
+
845
+ if (is_object($original) && $valstr === '[]') {
846
+ $valstr = '{}';
847
+ }
848
+ } catch (\Exception $e) {
849
+ $valstr = '__STRINGIFY_FAILED__';
850
+ }
851
+ }
852
+
853
+ if ($maxlen !== null && $maxlen > -1) {
854
+ $js = substr($valstr, 0, $maxlen);
855
+ $valstr = $maxlen < strlen($valstr)
856
+ ? (substr($js, 0, $maxlen - 3) . '...')
857
+ : $valstr;
858
+ }
859
+
860
+ return $valstr;
861
+ }
862
+
863
+ public static function pathify(mixed $val, ?int $startin = null, ?int $endin = null): string
864
+ {
865
+ $UNDEF = self::undef();
866
+ $S_MT = self::S_MT;
867
+ $S_CN = self::S_CN;
868
+ $S_DT = self::S_DT;
869
+
870
+ if (is_array($val) && (self::islist($val) || count($val) === 0)) {
871
+ $path = $val;
872
+ } elseif (is_string($val) || is_int($val) || is_float($val)) {
873
+ $path = [$val];
874
+ } else {
875
+ $path = $UNDEF;
876
+ }
877
+
878
+ $start = ($startin === null || $startin < 0) ? 0 : $startin;
879
+ $end = ($endin === null || $endin < 0) ? 0 : $endin;
880
+
881
+ $pathstr = $UNDEF;
882
+
883
+ if ($path !== $UNDEF && $start >= 0) {
884
+ $len = count($path);
885
+ $length = max(0, $len - $end - $start);
886
+ $slice = array_slice($path, $start, $length);
887
+
888
+ if (count($slice) === 0) {
889
+ $pathstr = '<root>';
890
+ } else {
891
+ $parts = [];
892
+ foreach ($slice as $p) {
893
+ if (!self::iskey($p)) {
894
+ continue;
895
+ }
896
+ if (is_int($p) || is_float($p)) {
897
+ $parts[] = $S_MT . (string) floor($p);
898
+ } else {
899
+ $parts[] = str_replace('.', $S_MT, (string) $p);
900
+ }
901
+ }
902
+ $pathstr = implode($S_DT, $parts);
903
+ }
904
+ }
905
+
906
+ if ($pathstr === $UNDEF) {
907
+ if ($val === $UNDEF || $val === null) {
908
+ $pathstr = '<unknown-path>';
909
+ } elseif (is_object($val) && count(get_object_vars($val)) === 0) {
910
+ // empty object
911
+ $pathstr = '<unknown-path:{}>';
912
+ } else {
913
+ // booleans, numbers, non-empty objects, etc.
914
+ $pathstr = '<unknown-path' . $S_CN . self::stringify($val, 47) . '>';
915
+ }
916
+ }
917
+
918
+ return $pathstr;
919
+ }
920
+
921
+
922
+ public static function flatten(mixed $list, ?int $depth = null): mixed
923
+ {
924
+ if (!self::islist($list)) {
925
+ return $list;
926
+ }
927
+ $depth = $depth ?? 1;
928
+ $result = [];
929
+ foreach ($list as $item) {
930
+ if (self::islist($item) && $depth > 0) {
931
+ $sub = self::flatten($item, $depth - 1);
932
+ foreach ($sub as $v) {
933
+ $result[] = $v;
934
+ }
935
+ } else {
936
+ $result[] = $item;
937
+ }
938
+ }
939
+ return $result;
940
+ }
941
+
942
+ public static function clone(mixed $val): mixed
943
+ {
944
+ if ($val === self::undef()) {
945
+ return self::undef();
946
+ }
947
+ $refs = [];
948
+ $replacer = function (mixed $v) use (&$refs, &$replacer): mixed {
949
+ if ($v instanceof \Closure || (is_object($v) && !($v instanceof \stdClass) && !($v instanceof ListRef) && method_exists($v, '__invoke'))) {
950
+ $refs[] = $v;
951
+ return '`$FUNCTION:' . (count($refs) - 1) . '`';
952
+ } elseif ($v instanceof ListRef) {
953
+ $newList = [];
954
+ foreach ($v->list as $item) {
955
+ $newList[] = $replacer($item);
956
+ }
957
+ return new ListRef($newList);
958
+ } elseif (is_array($v)) {
959
+ $result = [];
960
+ foreach ($v as $k => $item) {
961
+ $result[$k] = $replacer($item);
962
+ }
963
+ return $result;
964
+ } elseif (is_object($v)) {
965
+ $objVars = get_object_vars($v);
966
+ $result = new \stdClass();
967
+ foreach ($objVars as $k => $item) {
968
+ $result->$k = $replacer($item);
969
+ }
970
+ return $result;
971
+ } else {
972
+ return $v;
973
+ }
974
+ };
975
+ $temp = $replacer($val);
976
+ $reviver = function (mixed $v) use (&$refs, &$reviver): mixed {
977
+ if (is_string($v)) {
978
+ if (preg_match('/^`\$FUNCTION:([0-9]+)`$/', $v, $matches)) {
979
+ $idx = (int) $matches[1];
980
+ if (isset($refs[$idx])) {
981
+ return $refs[$idx];
982
+ }
983
+ }
984
+ return $v;
985
+ } elseif ($v instanceof ListRef) {
986
+ $newList = [];
987
+ foreach ($v->list as $item) {
988
+ $newList[] = $reviver($item);
989
+ }
990
+ return new ListRef($newList);
991
+ } elseif (is_array($v)) {
992
+ $result = [];
993
+ foreach ($v as $k => $item) {
994
+ $result[$k] = $reviver($item);
995
+ }
996
+ return $result;
997
+ } elseif (is_object($v)) {
998
+ $objVars = get_object_vars($v);
999
+ $result = new \stdClass();
1000
+ foreach ($objVars as $k => $item) {
1001
+ $result->$k = $reviver($item);
1002
+ }
1003
+ return $result;
1004
+ } else {
1005
+ return $v;
1006
+ }
1007
+ };
1008
+ return $reviver($temp);
1009
+ }
1010
+
1011
+ /**
1012
+ * Clone a value, wrapping all sequential arrays in ListRef for reference stability.
1013
+ * Mirrors Go's CloneFlags(val, {wrap: true}).
1014
+ */
1015
+ public static function cloneWrap(mixed $val): mixed
1016
+ {
1017
+ if ($val === null || $val === self::undef()) {
1018
+ return $val;
1019
+ }
1020
+ if ($val instanceof \Closure) {
1021
+ return $val;
1022
+ }
1023
+ if ($val instanceof ListRef) {
1024
+ $newList = [];
1025
+ foreach ($val->list as $item) {
1026
+ $newList[] = self::cloneWrap($item);
1027
+ }
1028
+ return new ListRef($newList);
1029
+ }
1030
+ if (is_array($val)) {
1031
+ if (self::isListHelper($val) || empty($val)) {
1032
+ $newList = [];
1033
+ foreach ($val as $item) {
1034
+ $newList[] = self::cloneWrap($item);
1035
+ }
1036
+ return new ListRef($newList);
1037
+ }
1038
+ // Assoc array (map-array) - clone as stdClass
1039
+ $result = new \stdClass();
1040
+ foreach ($val as $k => $v) {
1041
+ $result->$k = self::cloneWrap($v);
1042
+ }
1043
+ return $result;
1044
+ }
1045
+ if ($val instanceof \stdClass) {
1046
+ $result = new \stdClass();
1047
+ foreach (get_object_vars($val) as $k => $v) {
1048
+ $result->$k = self::cloneWrap($v);
1049
+ }
1050
+ return $result;
1051
+ }
1052
+ // Class instances and scalars returned as-is
1053
+ return $val;
1054
+ }
1055
+
1056
+ /**
1057
+ * Clone a value, unwrapping all ListRef back to plain arrays.
1058
+ * Mirrors Go's CloneFlags(val, {unwrap: true}).
1059
+ */
1060
+ public static function cloneUnwrap(mixed $val, int $depth = 0): mixed
1061
+ {
1062
+ if ($depth > 32) {
1063
+ return $val;
1064
+ }
1065
+ if ($val instanceof ListRef) {
1066
+ $result = [];
1067
+ foreach ($val->list as $item) {
1068
+ $result[] = self::cloneUnwrap($item, $depth + 1);
1069
+ }
1070
+ return $result;
1071
+ }
1072
+ if ($val instanceof \stdClass) {
1073
+ $result = new \stdClass();
1074
+ foreach (get_object_vars($val) as $k => $v) {
1075
+ $result->$k = self::cloneUnwrap($v, $depth + 1);
1076
+ }
1077
+ return $result;
1078
+ }
1079
+ if (is_array($val)) {
1080
+ $result = [];
1081
+ foreach ($val as $k => $v) {
1082
+ $result[$k] = self::cloneUnwrap($v, $depth + 1);
1083
+ }
1084
+ return $result;
1085
+ }
1086
+ return $val;
1087
+ }
1088
+
1089
+ /**
1090
+ * @internal
1091
+ * Set a property or list‐index on a "node" (stdClass or PHP array).
1092
+ * Respects undef‐marker removals, numeric vs string keys, and
1093
+ * list‐vs‐map semantics.
1094
+ */
1095
+ public static function setprop(mixed &$parent, mixed $key, mixed $val): mixed
1096
+ {
1097
+ // only valid keys make sense
1098
+ if (!self::iskey($key)) {
1099
+ return $parent;
1100
+ }
1101
+
1102
+ // ─── LISTREF ────────────────────────────────────────────────
1103
+ if ($parent instanceof ListRef) {
1104
+ if (!is_numeric($key)) {
1105
+ return $parent;
1106
+ }
1107
+ $keyI = (int) floor((float) $key);
1108
+ if ($val === self::undef()) {
1109
+ if ($keyI >= 0 && $keyI < count($parent->list)) {
1110
+ array_splice($parent->list, $keyI, 1);
1111
+ }
1112
+ } elseif ($keyI >= 0) {
1113
+ if ($keyI >= count($parent->list)) {
1114
+ $parent->list[] = $val;
1115
+ } else {
1116
+ $parent->list[$keyI] = $val;
1117
+ }
1118
+ } else {
1119
+ array_unshift($parent->list, $val);
1120
+ }
1121
+ return $parent;
1122
+ }
1123
+
1124
+ // ─── OBJECT (map) ───────────────────────────────────────────
1125
+ if (is_object($parent)) {
1126
+ $keyStr = self::strkey($key);
1127
+ if ($val === self::undef()) {
1128
+ unset($parent->$keyStr);
1129
+ } else {
1130
+ $parent->$keyStr = $val;
1131
+ }
1132
+ return $parent;
1133
+ }
1134
+
1135
+ // ─── ARRAY ──────────────────────────────────────────────────
1136
+ if (is_array($parent)) {
1137
+ if (!self::islist($parent)) {
1138
+ // map‐array
1139
+ $keyStr = self::strkey($key);
1140
+ if ($val === self::undef()) {
1141
+ unset($parent[$keyStr]);
1142
+ } elseif (ctype_digit((string) $key)) {
1143
+ // numeric string key: unshift (TS always merges maps by overwriting)
1144
+ $parent = [$keyStr => $val] + $parent;
1145
+ } else {
1146
+ $parent[$keyStr] = $val;
1147
+ }
1148
+ } else {
1149
+ // list‐array
1150
+ if (!is_numeric($key)) {
1151
+ return $parent;
1152
+ }
1153
+ $keyI = (int) floor((float) $key);
1154
+ if ($val === self::undef()) {
1155
+ if ($keyI >= 0 && $keyI < count($parent)) {
1156
+ array_splice($parent, $keyI, 1);
1157
+ }
1158
+ } elseif ($keyI >= 0) {
1159
+ if (count($parent) < $keyI) {
1160
+ $parent[] = $val;
1161
+ } else {
1162
+ $parent[$keyI] = $val;
1163
+ }
1164
+ } else {
1165
+ array_unshift($parent, $val);
1166
+ }
1167
+ }
1168
+ }
1169
+
1170
+ return $parent;
1171
+ }
1172
+
1173
+
1174
+ private const MAXDEPTH = 32;
1175
+
1176
+ public static function walk(
1177
+ mixed $val,
1178
+ ?callable $before = null,
1179
+ ?callable $after = null,
1180
+ ?int $maxdepth = null,
1181
+ mixed $key = null,
1182
+ mixed $parent = null,
1183
+ ?array $path = null
1184
+ ): mixed {
1185
+ if ($path === null) {
1186
+ $path = [];
1187
+ }
1188
+
1189
+ $out = ($before !== null) ? $before($key, $val, $parent, $path) : $val;
1190
+
1191
+ $md = ($maxdepth !== null && $maxdepth >= 0) ? $maxdepth : self::MAXDEPTH;
1192
+ if (0 === $md || (count($path) > 0 && $md <= count($path))) {
1193
+ return $out;
1194
+ }
1195
+
1196
+ if (self::isnode($out)) {
1197
+ foreach (self::items($out) as [$childKey, $childVal]) {
1198
+ $childPath = self::flatten([$path, self::S_MT . $childKey]);
1199
+ $result = self::walk(
1200
+ $childVal, $before, $after, $md, $childKey, $out, $childPath
1201
+ );
1202
+ if (self::ismap($out)) {
1203
+ if (is_object($out)) {
1204
+ $out->{self::strkey($childKey)} = $result;
1205
+ } else {
1206
+ $out[self::strkey($childKey)] = $result;
1207
+ }
1208
+ } else {
1209
+ $out[(int) $childKey] = $result;
1210
+ }
1211
+ }
1212
+ }
1213
+
1214
+ $out = ($after !== null) ? $after($key, $out, $parent, $path) : $out;
1215
+
1216
+ return $out;
1217
+ }
1218
+
1219
+ public static function merge(mixed $val, ?int $maxdepth = null): mixed
1220
+ {
1221
+ $md = self::slice($maxdepth ?? self::MAXDEPTH, 0);
1222
+
1223
+ if (!self::islist($val)) {
1224
+ return $val;
1225
+ }
1226
+
1227
+ $list = $val;
1228
+ $lenlist = count($list);
1229
+
1230
+ if (0 === $lenlist) {
1231
+ return self::undef();
1232
+ } elseif (1 === $lenlist) {
1233
+ return $list[0];
1234
+ }
1235
+
1236
+ $out = self::_getprop($list, 0, new \stdClass());
1237
+
1238
+ for ($oI = 1; $oI < $lenlist; $oI++) {
1239
+ $obj = $list[$oI];
1240
+
1241
+ if (!self::isnode($obj)) {
1242
+ $out = $obj;
1243
+ } else {
1244
+ $cur = [&$out];
1245
+ $dst = [&$out];
1246
+
1247
+ $before = function ($key, $val, $_parent, $path) use (&$cur, &$dst, $md) {
1248
+ $pI = self::size($path);
1249
+
1250
+ if ($md <= $pI) {
1251
+ self::setprop($cur[$pI - 1], $key, $val);
1252
+ } elseif (!self::isnode($val)) {
1253
+ $cur[$pI] = $val;
1254
+ } else {
1255
+ $dst[$pI] = 0 < $pI ? self::_getprop($dst[$pI - 1], $key) : $dst[$pI];
1256
+ $tval = $dst[$pI];
1257
+
1258
+ if (self::undef() === $tval && 0 === (self::T_instance & self::typify($val))) {
1259
+ $cur[$pI] = self::islist($val) ? [] : new \stdClass();
1260
+ } elseif (self::typify($val) === self::typify($tval)) {
1261
+ $cur[$pI] = $tval;
1262
+ } else {
1263
+ $cur[$pI] = $val;
1264
+ $val = self::undef();
1265
+ }
1266
+ }
1267
+
1268
+ return $val;
1269
+ };
1270
+
1271
+ $after = function ($key, $_val, $_parent, $path) use (&$cur) {
1272
+ $cI = self::size($path);
1273
+ $value = $cur[$cI] ?? null;
1274
+ if ($cI > 0) {
1275
+ self::setprop($cur[$cI - 1], $key, $value);
1276
+ }
1277
+ return $value;
1278
+ };
1279
+
1280
+ $out = self::walk($obj, $before, $after, $md);
1281
+ }
1282
+ }
1283
+
1284
+ if (0 === $md) {
1285
+ $out = self::getelem($list, -1);
1286
+ $out = self::islist($out) ? [] : (self::ismap($out) ? new \stdClass() : $out);
1287
+ }
1288
+
1289
+ return $out;
1290
+ }
1291
+
1292
+ public static function getpath(
1293
+ mixed $store,
1294
+ mixed $path,
1295
+ mixed $injdef = null
1296
+ ): mixed {
1297
+ // Convert path to array of parts
1298
+ $parts = is_array($path) ? $path :
1299
+ (is_string($path) ? explode('.', $path) :
1300
+ (is_numeric($path) ? [self::strkey($path)] : self::undef()));
1301
+
1302
+ if ($parts === self::undef()) {
1303
+ return self::undef();
1304
+ }
1305
+
1306
+ $val = $store;
1307
+ $base = self::_getprop($injdef, 'base');
1308
+ $src = self::_getprop($store, $base, $store);
1309
+ $numparts = count($parts);
1310
+ $dparent = self::_getprop($injdef, 'dparent');
1311
+
1312
+ // An empty path (incl empty string) just finds the src (base data)
1313
+ if ($path === null || $store === null || ($numparts === 1 && $parts[0] === '')) {
1314
+ $val = $src;
1315
+ } else if ($numparts > 0) {
1316
+ // Check for $ACTIONs
1317
+ if ($numparts === 1) {
1318
+ $val = self::_getprop($store, $parts[0]);
1319
+ }
1320
+
1321
+ if (!self::isfunc($val)) {
1322
+ $val = $src;
1323
+
1324
+ // Check for meta path in first part
1325
+ if (preg_match('/^([^$]+)\$([=~])(.+)$/', $parts[0], $m) && $injdef && isset($injdef->meta)) {
1326
+ $val = self::_getprop($injdef->meta, $m[1]);
1327
+ $parts[0] = $m[3];
1328
+ }
1329
+
1330
+ $dpath = self::_getprop($injdef, 'dpath');
1331
+
1332
+ for ($pI = 0; $val !== self::undef() && $pI < count($parts); $pI++) {
1333
+ $part = $parts[$pI];
1334
+
1335
+ if ($injdef && $part === '$KEY') {
1336
+ $part = self::_getprop($injdef, 'key');
1337
+ } else if ($injdef && str_starts_with($part, '$GET:')) {
1338
+ // $GET:path$ -> get store value, use as path part (string)
1339
+ $getpath = substr($part, 5, -1);
1340
+ $getval = self::getpath($src, $getpath);
1341
+ $part = self::stringify($getval);
1342
+ } else if ($injdef && str_starts_with($part, '$REF:')) {
1343
+ // $REF:refpath$ -> get spec value, use as path part (string)
1344
+ $refpath = substr($part, 5, -1);
1345
+ $part = self::stringify(self::getpath(self::_getprop($store, '$SPEC'), self::slice($part, 5, -1)));
1346
+ } else if ($injdef && str_starts_with($part, '$META:')) {
1347
+ // $META:metapath$ -> get meta value, use as path part (string)
1348
+ $part = self::stringify(self::getpath(self::_getprop($injdef, 'meta'), substr($part, 6, -1)));
1349
+ }
1350
+
1351
+ // $$ escapes $
1352
+ $part = str_replace('$$', '$', $part);
1353
+
1354
+ if ($part === '') {
1355
+ $ascends = 0;
1356
+ while ($pI + 1 < count($parts) && $parts[$pI + 1] === '') {
1357
+ $ascends++;
1358
+ $pI++;
1359
+ }
1360
+
1361
+ if ($injdef && $ascends > 0) {
1362
+ if ($pI === count($parts) - 1) {
1363
+ $ascends--;
1364
+ }
1365
+
1366
+ if ($ascends === 0) {
1367
+ $val = $dparent;
1368
+ } else {
1369
+ $fullpath = self::flatten([self::slice($dpath, 0 - $ascends), array_slice($parts, $pI + 1)]);
1370
+
1371
+ if (is_array($dpath) && $ascends <= count($dpath)) {
1372
+ $val = self::getpath($store, $fullpath);
1373
+ } else {
1374
+ $val = self::undef();
1375
+ }
1376
+ break;
1377
+ }
1378
+ } else {
1379
+ // Special case for single dot: use dparent if available
1380
+ if ($dparent !== null && $dparent !== self::undef()) {
1381
+ $val = $dparent;
1382
+ } else {
1383
+ $val = $src;
1384
+ }
1385
+ }
1386
+ } else {
1387
+ $val = self::_getprop($val, $part);
1388
+ }
1389
+ }
1390
+ }
1391
+ }
1392
+
1393
+ // Inj may provide a custom handler to modify found value
1394
+ $handler = self::_getprop($injdef, 'handler');
1395
+ if ($injdef !== null && self::isfunc($handler)) {
1396
+ $ref = self::pathify($path);
1397
+ $val = call_user_func($handler, $injdef, $val, $ref, $store);
1398
+ }
1399
+
1400
+ return $val;
1401
+ }
1402
+
1403
+
1404
+ public static function inject(
1405
+ mixed $val,
1406
+ mixed $store,
1407
+ mixed $injdef = null
1408
+ ): mixed {
1409
+ $valtype = gettype($val);
1410
+
1411
+ /** @var Injection $inj */
1412
+ $inj = $injdef;
1413
+
1414
+ // Create state if at root of injection. The input value is placed
1415
+ // inside a virtual parent holder to simplify edge cases.
1416
+ if (self::undef() === $injdef || null === $injdef || !($injdef instanceof Injection)) {
1417
+ $inj = new Injection($val, (object) [self::S_DTOP => $val]);
1418
+ $inj->dparent = $store;
1419
+ $inj->errs = self::_getprop($store, self::S_DERRS, []);
1420
+ if (!isset($inj->meta->__d)) {
1421
+ $inj->meta->__d = 0;
1422
+ }
1423
+
1424
+ if (self::undef() !== $injdef && null !== $injdef) {
1425
+ $inj->modify = (is_object($injdef) && property_exists($injdef, 'modify') && null !== $injdef->modify) ? $injdef->modify : $inj->modify;
1426
+ $inj->extra = (is_object($injdef) && property_exists($injdef, 'extra') && null !== $injdef->extra) ? $injdef->extra : ($inj->extra ?? null);
1427
+ $inj->meta = (is_object($injdef) && property_exists($injdef, 'meta') && null !== $injdef->meta) ? $injdef->meta : $inj->meta;
1428
+ $inj->handler = (is_object($injdef) && property_exists($injdef, 'handler') && null !== $injdef->handler) ? $injdef->handler : $inj->handler;
1429
+ }
1430
+ }
1431
+
1432
+ $inj->descend();
1433
+
1434
+ // Descend into node.
1435
+ if (self::isnode($val)) {
1436
+ $nodekeys = self::keysof($val);
1437
+
1438
+ if (self::ismap($val)) {
1439
+ $nonDollar = [];
1440
+ $dollar = [];
1441
+ foreach ($nodekeys as $nk) {
1442
+ if (str_contains((string) $nk, self::S_DS)) {
1443
+ $dollar[] = $nk;
1444
+ } else {
1445
+ $nonDollar[] = $nk;
1446
+ }
1447
+ }
1448
+ $nodekeys = array_merge($nonDollar, $dollar);
1449
+ } else {
1450
+ $nodekeys = self::keysof($val);
1451
+ }
1452
+
1453
+ for ($nkI = 0; $nkI < count($nodekeys); $nkI++) {
1454
+ $childinj = $inj->child($nkI, $nodekeys);
1455
+ $nodekey = $childinj->key;
1456
+ $childinj->mode = self::M_KEYPRE;
1457
+
1458
+ // Perform the key:pre mode injection on the child key.
1459
+ $prekey = self::_injectstr($nodekey, $store, $childinj);
1460
+
1461
+ // The injection may modify child processing.
1462
+ $nkI = $childinj->keyI;
1463
+ $nodekeys = $childinj->keys;
1464
+
1465
+ // Prevent further processing by returning an undefined prekey
1466
+ if (self::undef() !== $prekey) {
1467
+ $childinj->val = self::_getprop($val, $prekey);
1468
+ $childinj->mode = self::M_VAL;
1469
+
1470
+ // Perform the val mode injection on the child value.
1471
+ // NOTE: return value is not used.
1472
+ self::inject($childinj->val, $store, $childinj);
1473
+
1474
+ // The injection may modify child processing.
1475
+ $nkI = $childinj->keyI;
1476
+ $nodekeys = $childinj->keys;
1477
+
1478
+ // Perform the key:post mode injection on the child key.
1479
+ $childinj->mode = self::M_KEYPOST;
1480
+ self::_injectstr($nodekey, $store, $childinj);
1481
+
1482
+ // The injection may modify child processing.
1483
+ $nkI = $childinj->keyI;
1484
+ $nodekeys = $childinj->keys;
1485
+ }
1486
+
1487
+ // PHP: arrays are value types; propagate child mutations back to val & parent.
1488
+ // Skip sync if a transform modified an ancestor (checked via prior chain).
1489
+ if (is_array($val) && is_array($childinj->parent)) {
1490
+ // Check that the grandparent (inj->parent) still references our list.
1491
+ // If a transform like $REF replaced/deleted it, the stored value will differ.
1492
+ $storedVal = self::_getprop($inj->parent, $inj->key);
1493
+ if (is_array($storedVal)) {
1494
+ $val = $childinj->parent;
1495
+ $inj->val = $val;
1496
+ self::setprop($inj->parent, $inj->key, $val);
1497
+ }
1498
+ }
1499
+ }
1500
+ }
1501
+ // Inject paths into string scalars.
1502
+ else if ($valtype === 'string') {
1503
+ $inj->mode = self::M_VAL;
1504
+ $val = self::_injectstr($val, $store, $inj);
1505
+ if (self::SKIP !== $val) {
1506
+ $inj->setval($val);
1507
+ }
1508
+ }
1509
+
1510
+ // Custom modification.
1511
+ if ($inj->modify && self::SKIP !== $val) {
1512
+ $mkey = $inj->key;
1513
+ $mparent = $inj->parent;
1514
+ $mval = self::_getprop($mparent, $mkey);
1515
+
1516
+ call_user_func(
1517
+ $inj->modify,
1518
+ $mval,
1519
+ $mkey,
1520
+ $mparent,
1521
+ $inj,
1522
+ $store
1523
+ );
1524
+ }
1525
+
1526
+ $inj->val = $val;
1527
+
1528
+ // Original val reference may no longer be correct.
1529
+ // This return value is only used as the top level result.
1530
+ return self::_getprop($inj->parent, self::S_DTOP);
1531
+ }
1532
+
1533
+
1534
+ private static function _injectstr(
1535
+ string $val,
1536
+ mixed $store,
1537
+ ?object $inj = null
1538
+ ): mixed {
1539
+ // Can't inject into non-strings
1540
+ if ($val === self::S_MT) {
1541
+ return self::S_MT;
1542
+ }
1543
+
1544
+ $out = $val;
1545
+
1546
+ // Pattern examples: "`a.b.c`", "`$NAME`", "`$NAME1`", "``"
1547
+ $m = preg_match('/^`(\$[A-Z]+|[^`]*)[0-9]*`$/', $val, $matches);
1548
+
1549
+ // Full string of the val is an injection.
1550
+ if ($m) {
1551
+ if ($inj !== null) {
1552
+ $inj->full = true;
1553
+ }
1554
+ $pathref = $matches[1];
1555
+
1556
+ // Special escapes inside injection.
1557
+ if (strlen($pathref) > 3) {
1558
+ $pathref = str_replace('\\.', '.', $pathref);
1559
+ $pathref = str_replace('$BT', self::S_BT, $pathref);
1560
+ $pathref = str_replace('$DS', self::S_DS, $pathref);
1561
+ }
1562
+
1563
+ // Get the extracted path reference.
1564
+ $out = self::getpath($store, $pathref, $inj);
1565
+ }
1566
+ else {
1567
+ // Check for injections within the string.
1568
+ $out = preg_replace_callback('/`([^`]+)`/', function($matches) use ($store, $inj) {
1569
+ $ref = $matches[1];
1570
+
1571
+ if (strlen($ref) > 3) {
1572
+ $ref = str_replace('\\.', '.', $ref);
1573
+ $ref = str_replace('$BT', self::S_BT, $ref);
1574
+ $ref = str_replace('$DS', self::S_DS, $ref);
1575
+ }
1576
+ if ($inj !== null) {
1577
+ $inj->full = false;
1578
+ }
1579
+
1580
+ $found = self::getpath($store, $ref, $inj);
1581
+
1582
+ // Ensure inject value is a string.
1583
+ if ($found === self::undef()) {
1584
+ return self::S_MT;
1585
+ }
1586
+ if (is_string($found)) {
1587
+ return $found;
1588
+ }
1589
+ return json_encode($found instanceof ListRef ? self::cloneUnwrap($found) : $found);
1590
+ }, $val);
1591
+
1592
+ // Also call the inj handler on the entire string, providing the
1593
+ // option for custom injection.
1594
+ if ($inj !== null && is_callable($inj->handler)) {
1595
+ $inj->full = true;
1596
+ $out = call_user_func($inj->handler, $inj, $out, $val, $store);
1597
+ }
1598
+ }
1599
+
1600
+ return $out;
1601
+ }
1602
+
1603
+
1604
+ public static function _injecthandler(
1605
+ object $inj,
1606
+ mixed $val,
1607
+ string $ref,
1608
+ mixed $store
1609
+ ): mixed {
1610
+ $out = $val;
1611
+
1612
+ // Check if val is a function (command transforms)
1613
+ $iscmd = self::isfunc($val) && (self::undef() === $ref || str_starts_with($ref, self::S_DS));
1614
+
1615
+ // Only call val function if it is a special command ($NAME format).
1616
+ if ($iscmd) {
1617
+ $out = call_user_func($val, $inj, $val, $ref, $store);
1618
+ }
1619
+ // Update parent with value. Ensures references remain in node tree.
1620
+ elseif (self::M_VAL === $inj->mode && $inj->full) {
1621
+ $inj->setval($val);
1622
+ }
1623
+ return $out;
1624
+ }
1625
+
1626
+ private static function _injecthandler_getpath(
1627
+ object $state,
1628
+ mixed $val,
1629
+ string $ref,
1630
+ mixed $store
1631
+ ): mixed {
1632
+ return self::_injecthandler($state, $val, $ref, $store);
1633
+ }
1634
+
1635
+ /**
1636
+ * @internal
1637
+ * Delete a key from a map or list.
1638
+ */
1639
+ public static function transform_DELETE(
1640
+ object $state,
1641
+ mixed $val,
1642
+ mixed $ref,
1643
+ mixed $store
1644
+ ): mixed {
1645
+ // _setparentprop(state, UNDEF)
1646
+ $state->setval(self::undef());
1647
+ return self::undef();
1648
+ }
1649
+
1650
+ /**
1651
+ * @internal
1652
+ * Copy value from source data.
1653
+ */
1654
+ public static function transform_COPY(
1655
+ object $state,
1656
+ mixed $val,
1657
+ mixed $ref,
1658
+ mixed $store
1659
+ ): mixed {
1660
+ if (self::M_VAL !== $state->mode) {
1661
+ return self::undef();
1662
+ }
1663
+
1664
+ $out = self::_getprop($state->dparent, $state->key);
1665
+ $state->setval($out);
1666
+
1667
+ return $out;
1668
+ }
1669
+
1670
+ /**
1671
+ * @internal
1672
+ * As a value, inject the key of the parent node.
1673
+ * As a key, defines the name of the key property in the source object.
1674
+ */
1675
+ public static function transform_KEY(
1676
+ object $state,
1677
+ mixed $val,
1678
+ mixed $ref,
1679
+ mixed $store
1680
+ ): mixed {
1681
+ // only in "val" mode do anything
1682
+ if (self::M_VAL !== $state->mode) {
1683
+ return self::undef();
1684
+ }
1685
+
1686
+ // if parent has a "$KEY" override, use that
1687
+ $keyspec = self::_getprop($state->parent, self::S_DKEY);
1688
+ if ($keyspec !== self::undef()) {
1689
+ // remove the marker
1690
+ self::setprop($state->parent, self::S_DKEY, self::undef());
1691
+ return self::_getprop($state->dparent, $keyspec);
1692
+ }
1693
+
1694
+ // otherwise pull from $ANNO.KEY or fallback to the path index
1695
+ $meta = self::_getprop($state->parent, self::S_BANNO);
1696
+ $idx = count($state->path) - 2;
1697
+ return self::_getprop(
1698
+ $meta,
1699
+ self::S_KEY,
1700
+ self::_getprop($state->path, $idx)
1701
+ );
1702
+ }
1703
+
1704
+ /**
1705
+ * @internal
1706
+ * Store meta data about a node. Does nothing itself, just used by other transforms.
1707
+ */
1708
+ public static function transform_META(
1709
+ object $state,
1710
+ mixed $val,
1711
+ mixed $ref,
1712
+ mixed $store
1713
+ ): mixed {
1714
+ // remove the $META marker
1715
+ self::setprop($state->parent, self::S_DMETA, self::undef());
1716
+ return self::undef();
1717
+ }
1718
+
1719
+ /**
1720
+ * @internal
1721
+ * Store annotation data about a node. Does nothing itself, just used by other transforms.
1722
+ */
1723
+ public static function transform_ANNO(
1724
+ object $state,
1725
+ mixed $val,
1726
+ mixed $ref,
1727
+ mixed $store
1728
+ ): mixed {
1729
+ // remove the $ANNO marker
1730
+ self::setprop($state->parent, self::S_BANNO, self::undef());
1731
+ return self::undef();
1732
+ }
1733
+
1734
+ /**
1735
+ * @internal
1736
+ * Merge a list of objects into the current object.
1737
+ */
1738
+ public static function transform_MERGE(
1739
+ object $state,
1740
+ mixed $val,
1741
+ mixed $ref,
1742
+ mixed $store
1743
+ ): mixed {
1744
+ $mode = $state->mode;
1745
+ $key = $state->key;
1746
+ $parent = $state->parent;
1747
+
1748
+ // Ensures $MERGE is removed from parent list (val mode).
1749
+ $out = self::undef();
1750
+
1751
+ if (self::M_KEYPRE === $mode) {
1752
+ $out = $key;
1753
+ }
1754
+ // Operate after child values have been transformed.
1755
+ elseif (self::M_KEYPOST === $mode) {
1756
+ $out = $key;
1757
+
1758
+ $args = self::_getprop($parent, $key);
1759
+ $args = self::islist($args) ? (($args instanceof ListRef) ? $args->list : $args) : [$args];
1760
+
1761
+ // Remove the $MERGE command from a parent map.
1762
+ $state->setval(self::undef());
1763
+
1764
+ // Literals in the parent have precedence, but we still merge onto
1765
+ // the parent object, so that node tree references are not changed.
1766
+ $mergelist = self::flatten([[$parent], $args, [clone $parent]]);
1767
+
1768
+ self::merge($mergelist);
1769
+ }
1770
+
1771
+ return $out;
1772
+ }
1773
+
1774
+
1775
+ public static function transform_EACH(
1776
+ object $state,
1777
+ mixed $_val,
1778
+ string $_ref,
1779
+ mixed $store
1780
+ ): mixed {
1781
+ // Remove remaining keys to avoid spurious processing.
1782
+ $state->keys = array_slice($state->keys, 0, 1);
1783
+
1784
+ if (self::M_VAL !== $state->mode) {
1785
+ return self::undef();
1786
+ }
1787
+
1788
+ // Get arguments: ['`$EACH`', 'source-path', child-template]
1789
+ $srcpath = self::_getprop($state->parent, 1);
1790
+ $child = self::clone(self::_getprop($state->parent, 2));
1791
+
1792
+ // Source data.
1793
+ $srcstore = self::_getprop($store, $state->base, $store);
1794
+ $src = self::getpath($srcstore, $srcpath, $state);
1795
+
1796
+ // Create parallel data structures: source entries :: child templates
1797
+ $tcur = [];
1798
+ $tval = [];
1799
+
1800
+ $tkey = self::getelem($state->path, -2);
1801
+ $target = self::getelem($state->nodes, -2) ?? self::getelem($state->nodes, -1);
1802
+
1803
+ // Create clones of the child template for each value of the current source.
1804
+ if (self::islist($src)) {
1805
+ $srcArr = ($src instanceof ListRef) ? $src->list : (array) $src;
1806
+ $tval = array_map(function($_) use ($child) {
1807
+ return self::clone($child);
1808
+ }, $srcArr);
1809
+ } elseif (self::ismap($src)) {
1810
+ $tval = [];
1811
+ foreach (self::items($src) as $item) {
1812
+ $template = self::merge([
1813
+ self::clone($child),
1814
+ (object) [self::S_BANNO => (object) [self::S_KEY => $item[0]]]
1815
+ ], 1);
1816
+ $tval[] = $template;
1817
+ }
1818
+ }
1819
+
1820
+ $rval = [];
1821
+
1822
+ if (0 < self::size($tval)) {
1823
+ $tcur = (null == $src) ? self::undef() : ($src instanceof ListRef ? $src->list : array_values((array) $src));
1824
+
1825
+ $ckey = self::getelem($state->path, -2);
1826
+
1827
+ $tpath = self::slice($state->path, -1);
1828
+ $dpath = self::flatten([self::S_DTOP, explode(self::S_DT, $srcpath), '$:' . $ckey]);
1829
+
1830
+ // Parent structure.
1831
+ $tcur = (object) [$ckey => $tcur];
1832
+
1833
+ if (1 < self::size($tpath)) {
1834
+ $pkey = self::getelem($state->path, -3, self::S_DTOP);
1835
+ $tcur = (object) [$pkey => $tcur];
1836
+ $dpath[] = '$:' . $pkey;
1837
+ }
1838
+
1839
+ $tinj = $state->child(0, [$ckey]);
1840
+ $tinj->path = $tpath;
1841
+ $tinj->nodes = self::slice($state->nodes, -1);
1842
+
1843
+ $tinj->parent = self::getelem($tinj->nodes, -1);
1844
+ self::setprop($tinj->parent, $ckey, $tval);
1845
+
1846
+ $tinj->val = $tval;
1847
+ $tinj->dpath = $dpath;
1848
+ $tinj->dparent = $tcur;
1849
+
1850
+ self::inject($tval, $store, $tinj);
1851
+ $rval = $tinj->val;
1852
+ }
1853
+
1854
+ // Update ancestors.
1855
+ self::setprop($target, $tkey, $rval);
1856
+
1857
+ // Prevent callee from damaging first list entry (since we are in `val` mode).
1858
+ return $rval[0] ?? self::undef();
1859
+ }
1860
+
1861
+
1862
+
1863
+
1864
+ /** @internal */
1865
+ public static function transform_PACK(
1866
+ object $state,
1867
+ mixed $_val,
1868
+ mixed $_ref,
1869
+ mixed $store
1870
+ ): mixed {
1871
+ $mode = $state->mode;
1872
+ $key = $state->key;
1873
+ $path = $state->path;
1874
+ $parent = $state->parent;
1875
+ $nodes = $state->nodes;
1876
+
1877
+ // Only run in key:pre mode.
1878
+ if (self::M_KEYPRE !== $mode) {
1879
+ return self::undef();
1880
+ }
1881
+
1882
+ // Get arguments. Spec arrays are wrapped in ListRef by transform() so
1883
+ // accept either a plain array or a ListRef here.
1884
+ $args = self::_getprop($parent, $key);
1885
+ if ($args instanceof ListRef) {
1886
+ $args = $args->list;
1887
+ }
1888
+ if (!is_array($args) || count($args) < 2) {
1889
+ return self::undef();
1890
+ }
1891
+
1892
+ $srcpath = $args[0];
1893
+ $origchildspec = self::clone($args[1]);
1894
+
1895
+ // Find key and target node.
1896
+ $tkey = self::getelem($path, -2);
1897
+ $pathsize = self::size($path);
1898
+ $target = self::getelem($nodes, $pathsize - 2) ?? self::getelem($nodes, $pathsize - 1);
1899
+
1900
+ // Source data
1901
+ $srcstore = self::_getprop($store, $state->base, $store);
1902
+ $src = self::getpath($srcstore, $srcpath, $state);
1903
+
1904
+ // Prepare source as a list.
1905
+ if (!self::islist($src)) {
1906
+ if (self::ismap($src)) {
1907
+ $newSrc = [];
1908
+ foreach (self::items($src) as $item) {
1909
+ self::setprop($item[1], self::S_BANNO, (object) [self::S_KEY => $item[0]]);
1910
+ $newSrc[] = $item[1];
1911
+ }
1912
+ $src = $newSrc;
1913
+ } else {
1914
+ return self::undef();
1915
+ }
1916
+ }
1917
+
1918
+ if (null == $src) {
1919
+ return self::undef();
1920
+ }
1921
+
1922
+ // Get keypath.
1923
+ $keypath = self::_getprop($origchildspec, self::S_BKEY);
1924
+ $childspec = self::delprop($origchildspec, self::S_BKEY);
1925
+
1926
+ $child = $childspec;
1927
+
1928
+ // Build parallel target object.
1929
+ $tval = new \stdClass();
1930
+
1931
+ foreach (self::items($src) as $item) {
1932
+ $srckey = $item[0];
1933
+ $srcnode = $item[1];
1934
+
1935
+ $nkey = $srckey;
1936
+ if (self::undef() !== $keypath) {
1937
+ if (is_string($keypath) && str_starts_with($keypath, '`')) {
1938
+ $nkey = self::inject($keypath, self::merge([new \stdClass(), $store, (object) ['$TOP' => $srcnode]], 1));
1939
+ } else {
1940
+ $nkey = self::getpath($srcnode, $keypath, $state);
1941
+ }
1942
+ }
1943
+
1944
+ $tchild = self::clone($child);
1945
+ self::setprop($tval, $nkey, $tchild);
1946
+
1947
+ $anno = self::_getprop($srcnode, self::S_BANNO);
1948
+ if (self::undef() === $anno) {
1949
+ self::delprop($tchild, self::S_BANNO);
1950
+ } else {
1951
+ self::setprop($tchild, self::S_BANNO, $anno);
1952
+ }
1953
+ }
1954
+
1955
+ $rval = new \stdClass();
1956
+
1957
+ if (!self::isempty($tval)) {
1958
+ // Build parallel source object.
1959
+ $tsrc = new \stdClass();
1960
+ foreach ($src as $i => $n) {
1961
+ $kn = null;
1962
+ if (self::undef() === $keypath) {
1963
+ $kn = $i;
1964
+ } elseif (is_string($keypath) && str_starts_with($keypath, '`')) {
1965
+ $kn = self::inject($keypath, self::merge([new \stdClass(), $store, (object) ['$TOP' => $n]], 1));
1966
+ } else {
1967
+ $kn = self::getpath($n, $keypath, $state);
1968
+ }
1969
+ self::setprop($tsrc, $kn, $n);
1970
+ }
1971
+
1972
+ $tpath = self::slice($state->path, -1);
1973
+
1974
+ $ckey = self::getelem($state->path, -2);
1975
+ $dpath = self::flatten([self::S_DTOP, explode(self::S_DT, $srcpath), '$:' . $ckey]);
1976
+
1977
+ $tcur = (object) [$ckey => $tsrc];
1978
+
1979
+ if (1 < self::size($tpath)) {
1980
+ $pkey = self::getelem($state->path, -3, self::S_DTOP);
1981
+ $tcur = (object) [$pkey => $tcur];
1982
+ $dpath[] = '$:' . $pkey;
1983
+ }
1984
+
1985
+ $tinj = $state->child(0, [$ckey]);
1986
+ $tinj->path = $tpath;
1987
+ $tinj->nodes = self::slice($state->nodes, -1);
1988
+
1989
+ $tinj->parent = self::getelem($tinj->nodes, -1);
1990
+ $tinj->val = $tval;
1991
+
1992
+ $tinj->dpath = $dpath;
1993
+ $tinj->dparent = $tcur;
1994
+
1995
+ self::inject($tval, $store, $tinj);
1996
+ $rval = $tinj->val;
1997
+ }
1998
+
1999
+ // Update ancestors.
2000
+ self::setprop($target, $tkey, $rval);
2001
+
2002
+ // Drop transform key.
2003
+ return self::undef();
2004
+ }
2005
+
2006
+
2007
+ /** @internal */
2008
+ public static function transform_REF(object $state, mixed $_val, string $_ref, mixed $store): mixed
2009
+ {
2010
+ $nodes = $state->nodes;
2011
+
2012
+ if (self::M_VAL !== $state->mode) {
2013
+ return self::undef();
2014
+ }
2015
+
2016
+ // Get arguments: ['`$REF`', 'ref-path'].
2017
+ $refpath = self::_getprop($state->parent, 1);
2018
+ $state->keyI = self::size($state->keys);
2019
+
2020
+ // Spec reference.
2021
+ $specFn = self::_getprop($store, '$SPEC');
2022
+ $spec = is_callable($specFn) ? $specFn() : self::undef();
2023
+
2024
+ $dpath = self::slice($state->path, 1);
2025
+ $ref = self::getpath($spec, $refpath, (object) [
2026
+ 'dpath' => $dpath,
2027
+ 'dparent' => self::getpath($spec, $dpath),
2028
+ ]);
2029
+
2030
+ $hasSubRef = false;
2031
+ if (self::isnode($ref)) {
2032
+ self::walk($ref, function ($_k, $v) use (&$hasSubRef) {
2033
+ if ($v === '`$REF`') {
2034
+ $hasSubRef = true;
2035
+ }
2036
+ return $v;
2037
+ });
2038
+ }
2039
+
2040
+ $tref = self::cloneWrap($ref);
2041
+
2042
+ $cpath = self::slice($state->path, -3);
2043
+ $tpath = self::slice($state->path, -1);
2044
+ $tcur = self::getpath($store, $cpath);
2045
+ $tval = self::getpath($store, $tpath);
2046
+ $rval = self::undef();
2047
+
2048
+ if (!$hasSubRef || self::undef() !== $tval) {
2049
+ $tinj = $state->child(0, [self::getelem($tpath, -1)]);
2050
+
2051
+ $tinj->path = $tpath;
2052
+ $tinj->nodes = self::slice($state->nodes, -1);
2053
+ $tinj->parent = self::getelem($nodes, -2);
2054
+ $tinj->val = $tref;
2055
+
2056
+ $tinj->dpath = self::flatten([$cpath]);
2057
+ $tinj->dparent = $tcur;
2058
+
2059
+ $injResult = self::inject($tref, $store, $tinj);
2060
+
2061
+ // If inject returned SKIP, use tref (mutated in place) not tinj->val (which may be SKIP)
2062
+ if ($injResult === self::SKIP || $tinj->val === self::SKIP) {
2063
+ $rval = is_object($tref) ? $tref : self::undef();
2064
+ } else {
2065
+ $rval = $tinj->val;
2066
+ }
2067
+ } else {
2068
+ $rval = self::undef();
2069
+ }
2070
+
2071
+ $grandparent = $state->setval($rval, 2);
2072
+
2073
+ // PHP: arrays in nodes are copies, so ancestor setval on arrays doesn't propagate.
2074
+ // Sync the prior injection's parent if it's an array.
2075
+ if ($state->prior && is_array($state->prior->parent)) {
2076
+ $akey = self::getelem($state->path, -2);
2077
+ if (self::undef() === $rval) {
2078
+ $state->prior->parent = self::delprop($state->prior->parent, $akey);
2079
+ } else {
2080
+ self::setprop($state->prior->parent, $akey, $rval);
2081
+ }
2082
+ }
2083
+
2084
+ if (self::islist($grandparent) && $state->prior) {
2085
+ $state->prior->keyI--;
2086
+ }
2087
+
2088
+ return $_val;
2089
+ }
2090
+
2091
+
2092
+ private static array $FORMATTER = [];
2093
+
2094
+ private static function _getFormatters(): array
2095
+ {
2096
+ if (empty(self::$FORMATTER)) {
2097
+ self::$FORMATTER = [
2098
+ 'identity' => fn($_k, $v) => $v,
2099
+ 'upper' => fn($_k, $v) => self::isnode($v) ? $v : strtoupper('' . $v),
2100
+ 'lower' => fn($_k, $v) => self::isnode($v) ? $v : strtolower('' . $v),
2101
+ 'string' => fn($_k, $v) => self::isnode($v) ? $v : ('' . $v),
2102
+ 'number' => function ($_k, $v) {
2103
+ if (self::isnode($v)) {
2104
+ return $v;
2105
+ }
2106
+ $n = is_numeric($v) ? $v + 0 : 0;
2107
+ return $n;
2108
+ },
2109
+ 'integer' => function ($_k, $v) {
2110
+ if (self::isnode($v)) {
2111
+ return $v;
2112
+ }
2113
+ $n = is_numeric($v) ? (int)$v : 0;
2114
+ return $n;
2115
+ },
2116
+ 'concat' => function ($k, $v) {
2117
+ if (null === $k && self::islist($v)) {
2118
+ $parts = self::items($v, fn($n) => self::isnode($n[1]) ? '' : ('' . $n[1]));
2119
+ return self::join($parts, '');
2120
+ }
2121
+ return $v;
2122
+ },
2123
+ ];
2124
+ }
2125
+ return self::$FORMATTER;
2126
+ }
2127
+
2128
+
2129
+ /** @internal */
2130
+ public static function transform_FORMAT(object $inj, mixed $_val, string $_ref, mixed $store): mixed
2131
+ {
2132
+ // Remove remaining keys to avoid spurious processing.
2133
+ self::slice($inj->keys, 0, 1, true);
2134
+
2135
+ if (self::M_VAL !== $inj->mode) {
2136
+ return self::undef();
2137
+ }
2138
+
2139
+ // Get arguments: ['`$FORMAT`', 'name', child].
2140
+ $name = self::_getprop($inj->parent, 1);
2141
+ $child = self::_getprop($inj->parent, 2);
2142
+
2143
+ // Source data.
2144
+ $tkey = self::getelem($inj->path, -2);
2145
+ $target = self::getelem($inj->nodes, -2, fn() => self::getelem($inj->nodes, -1));
2146
+
2147
+ $cinj = self::injectChild($child, $store, $inj);
2148
+ $resolved = $cinj->val;
2149
+
2150
+ $formatters = self::_getFormatters();
2151
+ $formatter = (0 < (self::T_function & self::typify($name))) ? $name : ($formatters[$name] ?? self::undef());
2152
+
2153
+ if (self::undef() === $formatter) {
2154
+ $inj->errs[] = '$FORMAT: unknown format: ' . $name . '.';
2155
+ return self::undef();
2156
+ }
2157
+
2158
+ $out = self::walk($resolved, $formatter);
2159
+
2160
+ self::setprop($target, $tkey, $out);
2161
+
2162
+ return $out;
2163
+ }
2164
+
2165
+
2166
+ /** @internal */
2167
+ public static function transform_APPLY(object $inj, mixed $_val, string $_ref, mixed $store): mixed
2168
+ {
2169
+ $ijname = 'APPLY';
2170
+
2171
+ if (!self::checkPlacement(self::M_VAL, $ijname, self::T_list, $inj)) {
2172
+ return self::undef();
2173
+ }
2174
+
2175
+ $args = self::slice($inj->parent, 1);
2176
+ $argsList = [];
2177
+ if (self::islist($args)) {
2178
+ if ($args instanceof ListRef) {
2179
+ $argsList = $args->list;
2180
+ } else {
2181
+ $argsList = $args;
2182
+ }
2183
+ }
2184
+ [$err, $apply, $child] = self::injectorArgs([self::T_function, self::T_any], $argsList);
2185
+ if (self::undef() !== $err) {
2186
+ $inj->errs[] = '$' . $ijname . ': ' . $err;
2187
+ return self::undef();
2188
+ }
2189
+
2190
+ $tkey = self::getelem($inj->path, -2);
2191
+ $target = self::getelem($inj->nodes, -2, fn() => self::getelem($inj->nodes, -1));
2192
+
2193
+ $cinj = self::injectChild($child, $store, $inj);
2194
+ $resolved = $cinj->val;
2195
+
2196
+ $out = call_user_func($apply, $resolved, $store, $cinj);
2197
+
2198
+ self::setprop($target, $tkey, $out);
2199
+
2200
+ return $out;
2201
+ }
2202
+
2203
+
2204
+ /**
2205
+ * Transform data using a spec.
2206
+ *
2207
+ * @param mixed $data Source data (not mutated)
2208
+ * @param mixed $spec Transform spec (JSON-like)
2209
+ * @param array<mixed>|object|null $extra extra transforms or data
2210
+ * @param callable|null $modify optional per-value hook
2211
+ */
2212
+ public static function transform(
2213
+ mixed $data,
2214
+ mixed $spec,
2215
+ mixed $injdef = null
2216
+ ): mixed {
2217
+ // Support injdef object pattern or backward compat (extra data passed directly)
2218
+ $extra = null;
2219
+ $modify = null;
2220
+ $errs = null;
2221
+ if (is_object($injdef) && (
2222
+ property_exists($injdef, 'extra') ||
2223
+ property_exists($injdef, 'modify') ||
2224
+ property_exists($injdef, 'errs') ||
2225
+ property_exists($injdef, 'meta') ||
2226
+ property_exists($injdef, 'handler')
2227
+ )) {
2228
+ // New injdef pattern: { extra, modify, errs, meta, handler }
2229
+ $extra = property_exists($injdef, 'extra') ? $injdef->extra : null;
2230
+ $modify = property_exists($injdef, 'modify') ? $injdef->modify : null;
2231
+ $errs = property_exists($injdef, 'errs') ? $injdef->errs : null;
2232
+ } else {
2233
+ // Backward compat: treat 3rd arg as extra data/store directly
2234
+ $extra = $injdef;
2235
+ }
2236
+
2237
+ // 1) clone spec, wrapping arrays in ListRef for reference stability (Go pattern)
2238
+ $specClone = self::cloneWrap($spec);
2239
+
2240
+ // 2) split extra into data vs transforms
2241
+ $extraTransforms = [];
2242
+ $extraData = [];
2243
+
2244
+ foreach ((array) ($extra ?? []) as $k => $v) {
2245
+ if (str_starts_with((string) $k, self::S_DS)) {
2246
+ $extraTransforms[$k] = $v;
2247
+ } else {
2248
+ $extraData[$k] = $v;
2249
+ }
2250
+ }
2251
+
2252
+ // 3) build the combined store
2253
+ $dataClone = self::merge([
2254
+ self::cloneWrap($extraData),
2255
+ self::cloneWrap($data),
2256
+ ]);
2257
+
2258
+ $store = (object) array_merge(
2259
+ [
2260
+ self::S_DTOP => $dataClone,
2261
+ '$BT' => fn() => self::S_BT,
2262
+ '$DS' => fn() => self::S_DS,
2263
+ '$WHEN' => fn() => (new \DateTime)->format(\DateTime::ATOM),
2264
+ '$DELETE' => [self::class, 'transform_DELETE'],
2265
+ '$COPY' => [self::class, 'transform_COPY'],
2266
+ '$KEY' => [self::class, 'transform_KEY'],
2267
+ '$META' => [self::class, 'transform_META'],
2268
+ '$ANNO' => [self::class, 'transform_ANNO'],
2269
+ '$MERGE' => [self::class, 'transform_MERGE'],
2270
+ '$EACH' => [self::class, 'transform_EACH'],
2271
+ '$PACK' => [self::class, 'transform_PACK'],
2272
+ '$SPEC' => fn() => $spec,
2273
+ '$REF' => [self::class, 'transform_REF'],
2274
+ '$FORMAT' => [self::class, 'transform_FORMAT'],
2275
+ '$APPLY' => [self::class, 'transform_APPLY'],
2276
+ ],
2277
+ $extraTransforms
2278
+ );
2279
+
2280
+ // 4) run inject to do the transform
2281
+ $injectOpts = new \stdClass();
2282
+ if ($modify !== null) {
2283
+ $injectOpts->modify = $modify;
2284
+ }
2285
+ if (is_object($injdef) && property_exists($injdef, 'handler') && $injdef->handler !== null) {
2286
+ $injectOpts->handler = $injdef->handler;
2287
+ }
2288
+ if (is_object($injdef) && property_exists($injdef, 'meta') && $injdef->meta !== null) {
2289
+ $injectOpts->meta = $injdef->meta;
2290
+ }
2291
+ if (is_object($injdef) && property_exists($injdef, 'errs') && $injdef->errs !== null) {
2292
+ $injectOpts->errs = $injdef->errs;
2293
+ }
2294
+ $result = self::inject($specClone, $store, $injectOpts);
2295
+
2296
+ // When a child transform (e.g. $REF) deletes the key, inject returns SKIP; return mutated spec
2297
+ if ($result === self::SKIP) {
2298
+ return self::_stdClassToArray(self::cloneUnwrap($specClone));
2299
+ }
2300
+
2301
+ // Return maps as PHP associative arrays (the native map type) so callers
2302
+ // can use is_array()/array access directly. Internal processing may use
2303
+ // stdClass; the conversion here happens only at the public boundary.
2304
+ return self::_stdClassToArray(self::cloneUnwrap($result));
2305
+ }
2306
+
2307
+
2308
+ // Deeply convert stdClass map nodes to associative arrays, leaving lists
2309
+ // (sequential arrays) and scalar values untouched. Used at the transform()
2310
+ // public boundary so consumers receive PHP-idiomatic arrays.
2311
+ private static function _stdClassToArray(mixed $val, int $depth = 0): mixed
2312
+ {
2313
+ if ($depth > 64) {
2314
+ return $val;
2315
+ }
2316
+ if ($val instanceof \stdClass) {
2317
+ $out = [];
2318
+ foreach (get_object_vars($val) as $k => $v) {
2319
+ $out[$k] = self::_stdClassToArray($v, $depth + 1);
2320
+ }
2321
+ return $out;
2322
+ }
2323
+ if (is_array($val)) {
2324
+ $out = [];
2325
+ foreach ($val as $k => $v) {
2326
+ $out[$k] = self::_stdClassToArray($v, $depth + 1);
2327
+ }
2328
+ return $out;
2329
+ }
2330
+ return $val;
2331
+ }
2332
+
2333
+ /**
2334
+ * Remove unresolved $REF list entries from a list spec.
2335
+ * This handles PHP's value-type arrays where in-place mutation via references doesn't propagate.
2336
+ */
2337
+ private static function _cleanRefEntries(array $list): array {
2338
+ $cleaned = [];
2339
+ foreach ($list as $item) {
2340
+ if (self::islist($item) && count($item) >= 1 && self::_getprop($item, 0) === '`$REF`') {
2341
+ // This is an unresolved $REF entry - remove it
2342
+ continue;
2343
+ }
2344
+ if (self::islist($item)) {
2345
+ $item = self::_cleanRefEntries($item);
2346
+ }
2347
+ $cleaned[] = $item;
2348
+ }
2349
+ return $cleaned;
2350
+ }
2351
+
2352
+ /** @internal */
2353
+ private static function _invalidTypeMsg(array $path, string $needtype, int $vt, mixed $v): string
2354
+ {
2355
+ $missing = ($v === null || $v === self::undef());
2356
+ $vs = $missing ? 'no value' : self::stringify($v);
2357
+ return 'Expected ' .
2358
+ (1 < self::size($path) ? ('field ' . self::pathify($path, 1) . ' to be ') : '') .
2359
+ $needtype . ', but found ' .
2360
+ ($missing ? '' : self::typename($vt) . ': ') . $vs . '.';
2361
+ }
2362
+
2363
+ /* =======================
2364
+ * Validation Functions
2365
+ * =======================
2366
+
2367
+ /**
2368
+ * A required string value.
2369
+ */
2370
+ public static function validate_STRING(object $inj): mixed
2371
+ {
2372
+ $out = self::_getprop($inj->dparent, $inj->key);
2373
+
2374
+ $t = self::typify($out);
2375
+ if (0 === (self::T_string & $t)) {
2376
+ $msg = self::_invalidTypeMsg($inj->path, self::S_string, $t, $out);
2377
+ $inj->errs[] = $msg;
2378
+ return self::undef();
2379
+ }
2380
+
2381
+ if (self::S_MT === $out) {
2382
+ $msg = 'Empty string at ' . self::pathify($inj->path, 1);
2383
+ $inj->errs[] = $msg;
2384
+ return self::undef();
2385
+ }
2386
+
2387
+ return $out;
2388
+ }
2389
+
2390
+ /**
2391
+ * A required number value (int or float).
2392
+ */
2393
+ public static function validate_NUMBER(object $inj): mixed
2394
+ {
2395
+ $out = self::_getprop($inj->dparent, $inj->key);
2396
+
2397
+ $t = self::typify($out);
2398
+ if (0 === (self::T_number & $t)) {
2399
+ $inj->errs[] = self::_invalidTypeMsg($inj->path, self::S_number, $t, $out);
2400
+ return self::undef();
2401
+ }
2402
+
2403
+ return $out;
2404
+ }
2405
+
2406
+ /**
2407
+ * A required boolean value.
2408
+ */
2409
+ public static function validate_BOOLEAN(object $inj): mixed
2410
+ {
2411
+ $out = self::_getprop($inj->dparent, $inj->key);
2412
+
2413
+ $t = self::typify($out);
2414
+ if (0 === (self::T_boolean & $t)) {
2415
+ $inj->errs[] = self::_invalidTypeMsg($inj->path, self::S_boolean, $t, $out);
2416
+ return self::undef();
2417
+ }
2418
+
2419
+ return $out;
2420
+ }
2421
+
2422
+ /**
2423
+ * A required object (map) value (contents not validated).
2424
+ */
2425
+ public static function validate_OBJECT(object $inj): mixed
2426
+ {
2427
+ $out = self::_getprop($inj->dparent, $inj->key);
2428
+
2429
+ $t = self::typify($out);
2430
+ if (0 === (self::T_map & $t)) {
2431
+ $inj->errs[] = self::_invalidTypeMsg($inj->path, self::S_object, $t, $out);
2432
+ return self::undef();
2433
+ }
2434
+
2435
+ return $out;
2436
+ }
2437
+
2438
+ /**
2439
+ * A required array (list) value (contents not validated).
2440
+ */
2441
+ public static function validate_ARRAY(object $inj): mixed
2442
+ {
2443
+ $out = self::_getprop($inj->dparent, $inj->key);
2444
+
2445
+ $t = self::typify($out);
2446
+ if (0 === (self::T_list & $t)) {
2447
+ $inj->errs[] = self::_invalidTypeMsg($inj->path, 'list', $t, $out);
2448
+ return self::undef();
2449
+ }
2450
+
2451
+ return $out;
2452
+ }
2453
+
2454
+ /**
2455
+ * A required function value.
2456
+ */
2457
+ public static function validate_FUNCTION(object $inj): mixed
2458
+ {
2459
+ $out = self::_getprop($inj->dparent, $inj->key);
2460
+
2461
+ $t = self::typify($out);
2462
+ if (0 === (self::T_function & $t)) {
2463
+ $inj->errs[] = self::_invalidTypeMsg($inj->path, self::S_function, $t, $out);
2464
+ return self::undef();
2465
+ }
2466
+
2467
+ return $out;
2468
+ }
2469
+
2470
+ /**
2471
+ * Generic type validator. Validates against any type name via TYPENAME lookup.
2472
+ */
2473
+ public static function validate_TYPE(object $inj, mixed $_val = null, ?string $ref = null): mixed
2474
+ {
2475
+ $tname = strtolower(substr($ref ?? '', 1));
2476
+ $idx = array_search($tname, self::TYPENAME);
2477
+ $typev = ($idx !== false) ? (1 << (31 - $idx)) : 0;
2478
+ $out = self::_getprop($inj->dparent, $inj->key);
2479
+
2480
+ $t = self::typify($out);
2481
+ if (0 === ($t & $typev)) {
2482
+ $inj->errs[] = self::_invalidTypeMsg($inj->path, $tname, $t, $out);
2483
+ return self::undef();
2484
+ }
2485
+
2486
+ return $out;
2487
+ }
2488
+
2489
+ /**
2490
+ * Allow any value.
2491
+ */
2492
+ public static function validate_ANY(object $inj): mixed
2493
+ {
2494
+ $out = self::_getprop($inj->dparent, $inj->key);
2495
+ return $out;
2496
+ }
2497
+
2498
+ /**
2499
+ * Specify child values for map or list.
2500
+ * Map syntax: {'`$CHILD`': child-template }
2501
+ * List syntax: ['`$CHILD`', child-template ]
2502
+ */
2503
+ public static function validate_CHILD(object $inj): mixed
2504
+ {
2505
+ $mode = $inj->mode;
2506
+ $key = $inj->key;
2507
+ $parent = $inj->parent;
2508
+ $keys = $inj->keys ?? [];
2509
+ $path = $inj->path;
2510
+
2511
+ // Map syntax.
2512
+ if (self::M_KEYPRE === $mode) {
2513
+ $childtm = self::_getprop($parent, $key);
2514
+
2515
+ // Get corresponding current object.
2516
+ $pkey = self::_getprop($path, count($path) - 2);
2517
+ $tval = self::_getprop($inj->dparent, $pkey);
2518
+
2519
+ if (self::undef() == $tval) {
2520
+ $tval = new \stdClass();
2521
+ } elseif (!self::ismap($tval)) {
2522
+ $inj->errs[] = self::_invalidTypeMsg(
2523
+ self::slice($inj->path, 0, -1), self::S_object, self::typify($tval), $tval);
2524
+ return self::undef();
2525
+
2526
+ }
2527
+
2528
+ $ckeys = self::keysof($tval);
2529
+ foreach ($ckeys as $ckey) {
2530
+ self::setprop($parent, $ckey, self::clone($childtm));
2531
+ // NOTE: modifying inj! This extends the child value loop in inject.
2532
+ $keys[] = $ckey;
2533
+ }
2534
+ $inj->keys = $keys;
2535
+
2536
+ // Remove $CHILD to cleanup output.
2537
+ $inj->setval(self::undef());
2538
+ return self::undef();
2539
+ }
2540
+
2541
+ // List syntax.
2542
+ if (self::M_VAL === $mode) {
2543
+ if (!self::islist($parent)) {
2544
+ // $CHILD was not inside a list.
2545
+ $inj->errs[] = 'Invalid $CHILD as value';
2546
+ return self::undef();
2547
+ }
2548
+
2549
+ $childtm = self::_getprop($parent, 1);
2550
+
2551
+ if (self::undef() === $inj->dparent) {
2552
+ // Empty list as default.
2553
+ while (count($parent) > 0) {
2554
+ array_pop($parent);
2555
+ }
2556
+ return self::undef();
2557
+ }
2558
+
2559
+ if (!self::islist($inj->dparent)) {
2560
+ $msg = self::_invalidTypeMsg(
2561
+ self::slice($inj->path, 0, -1), self::S_array, self::typify($inj->dparent), $inj->dparent);
2562
+ $inj->errs[] = $msg;
2563
+ $inj->keyI = count($parent);
2564
+ return $inj->dparent;
2565
+ }
2566
+
2567
+ // Clone children and reset inj key index.
2568
+ foreach ($inj->dparent as $i => $n) {
2569
+ $parent[$i] = self::clone($childtm);
2570
+ }
2571
+ // Adjust array length
2572
+ while (count($parent) > count($inj->dparent)) {
2573
+ array_pop($parent);
2574
+ }
2575
+ $inj->keyI = 0;
2576
+ $out = self::_getprop($inj->dparent, 0);
2577
+ return $out;
2578
+ }
2579
+
2580
+ return self::undef();
2581
+ }
2582
+
2583
+ /**
2584
+ * Match at least one of the specified shapes.
2585
+ * Syntax: ['`$ONE`', alt0, alt1, ...]
2586
+ */
2587
+ public static function validate_ONE(
2588
+ object $inj,
2589
+ mixed $_val,
2590
+ string $_ref,
2591
+ mixed $store
2592
+ ): mixed {
2593
+ $mode = $inj->mode;
2594
+ $parent = $inj->parent;
2595
+ $keyI = $inj->keyI;
2596
+
2597
+ // Only operate in val mode, since parent is a list.
2598
+ if (self::M_VAL === $mode) {
2599
+ if (!self::islist($parent) || 0 !== $keyI) {
2600
+ $inj->errs[] = 'The $ONE validator at field ' .
2601
+ self::pathify($inj->path, 1, 1) .
2602
+ ' must be the first element of an array.';
2603
+ return self::undef();
2604
+ }
2605
+
2606
+ $inj->keyI = count($inj->keys ?? []);
2607
+
2608
+ // Clean up structure, replacing [$ONE, ...] with current
2609
+ $inj->setval($inj->dparent, 2);
2610
+
2611
+ $inj->path = self::slice($inj->path, 0, -1);
2612
+ $inj->key = self::getelem($inj->path, -1);
2613
+
2614
+ $tvals = self::slice($parent, 1);
2615
+ if (0 === count($tvals)) {
2616
+ $inj->errs[] = 'The $ONE validator at field ' .
2617
+ self::pathify($inj->path, 1, 1) .
2618
+ ' must have at least one argument.';
2619
+ return self::undef();
2620
+ }
2621
+
2622
+ // See if we can find a match.
2623
+ foreach ($tvals as $tval) {
2624
+ // If match, then errs.length = 0
2625
+ $terrs = [];
2626
+
2627
+ $vstore = array_merge((array) $store, [self::S_DTOP => $inj->dparent]);
2628
+
2629
+ $vcurrent = self::validate($inj->dparent, $tval, (object) [
2630
+ 'extra' => $vstore,
2631
+ 'errs' => $terrs,
2632
+ 'meta' => $inj->meta,
2633
+ ]);
2634
+
2635
+ $inj->setval($vcurrent, 2);
2636
+
2637
+ // Accept current value if there was a match
2638
+ if (0 === count($terrs)) {
2639
+ return self::undef();
2640
+ }
2641
+ }
2642
+
2643
+ // There was no match.
2644
+ $tvArr = ($tvals instanceof ListRef) ? $tvals->list : (is_array($tvals) ? $tvals : []);
2645
+ $valdesc = implode(', ', array_map(fn($v) => self::stringify($v), $tvArr));
2646
+ $valdesc = preg_replace(self::R_TRANSFORM_NAME, '$1', strtolower($valdesc));
2647
+
2648
+ $inj->errs[] = self::_invalidTypeMsg(
2649
+ $inj->path,
2650
+ (1 < count($tvals) ? 'one of ' : '') . $valdesc,
2651
+ self::typify($inj->dparent), $inj->dparent);
2652
+ }
2653
+
2654
+ return self::undef();
2655
+ }
2656
+
2657
+ /**
2658
+ * Match exactly one of the specified values.
2659
+ */
2660
+ public static function validate_EXACT(object $inj): mixed
2661
+ {
2662
+ $mode = $inj->mode;
2663
+ $parent = $inj->parent;
2664
+ $key = $inj->key;
2665
+ $keyI = $inj->keyI;
2666
+
2667
+ // Only operate in val mode, since parent is a list.
2668
+ if (self::M_VAL === $mode) {
2669
+ if (!self::islist($parent) || 0 !== $keyI) {
2670
+ $inj->errs[] = 'The $EXACT validator at field ' .
2671
+ self::pathify($inj->path, 1, 1) .
2672
+ ' must be the first element of an array.';
2673
+ return self::undef();
2674
+ }
2675
+
2676
+ $inj->keyI = count($inj->keys ?? []);
2677
+
2678
+ // Clean up structure, replacing [$EXACT, ...] with current data parent
2679
+ $inj->setval($inj->dparent, 2);
2680
+
2681
+ $inj->path = self::slice($inj->path, 0, count($inj->path) - 1);
2682
+ $inj->key = self::getelem($inj->path, -1);
2683
+
2684
+ $tvals = self::slice($parent, 1);
2685
+ if (0 === count($tvals)) {
2686
+ $inj->errs[] = 'The $EXACT validator at field ' .
2687
+ self::pathify($inj->path, 1, 1) .
2688
+ ' must have at least one argument.';
2689
+ return self::undef();
2690
+ }
2691
+
2692
+ // See if we can find an exact value match.
2693
+ $currentstr = null;
2694
+ foreach ($tvals as $tval) {
2695
+ $exactmatch = $tval === $inj->dparent;
2696
+
2697
+ if (!$exactmatch && self::isnode($tval)) {
2698
+ $currentstr = $currentstr ?? self::stringify($inj->dparent);
2699
+ $tvalstr = self::stringify($tval);
2700
+ $exactmatch = $tvalstr === $currentstr;
2701
+ }
2702
+
2703
+ if ($exactmatch) {
2704
+ return self::undef();
2705
+ }
2706
+ }
2707
+
2708
+ $tvArr = ($tvals instanceof ListRef) ? $tvals->list : (is_array($tvals) ? $tvals : []);
2709
+ $valdesc = implode(', ', array_map(fn($v) => self::stringify($v), $tvArr));
2710
+ $valdesc = preg_replace(self::R_TRANSFORM_NAME, '$1', strtolower($valdesc));
2711
+
2712
+ $inj->errs[] = self::_invalidTypeMsg(
2713
+ $inj->path,
2714
+ (1 < count($inj->path) ? '' : 'value ') .
2715
+ 'exactly equal to ' . (1 === count($tvals) ? '' : 'one of ') . $valdesc,
2716
+ self::typify($inj->dparent), $inj->dparent);
2717
+ } else {
2718
+ self::delprop($parent, $key);
2719
+ }
2720
+
2721
+ return self::undef();
2722
+ }
2723
+
2724
+ /**
2725
+ * This is the "modify" argument to inject. Use this to perform
2726
+ * generic validation. Runs *after* any special commands.
2727
+ */
2728
+ private static function _validation(
2729
+ mixed $pval,
2730
+ mixed $key = null,
2731
+ mixed $parent = null,
2732
+ ?object $inj = null,
2733
+ mixed $store = null
2734
+ ): void {
2735
+ if (self::undef() === $inj) {
2736
+ return;
2737
+ }
2738
+
2739
+ if ($pval === self::SKIP) {
2740
+ return;
2741
+ }
2742
+
2743
+ // select needs exact matches
2744
+ $exact = self::_getprop($inj->meta, '`$EXACT`', false);
2745
+
2746
+ // Current val to verify.
2747
+ $cval = self::_getprop($inj->dparent, $key);
2748
+
2749
+ if (self::undef() === $inj || (!$exact && self::undef() === $cval)) {
2750
+ return;
2751
+ }
2752
+
2753
+ $ptype = self::typify($pval);
2754
+
2755
+ // Delete any special commands remaining.
2756
+ if (0 < (self::T_string & $ptype) && str_contains($pval, self::S_DS)) {
2757
+ return;
2758
+ }
2759
+
2760
+ $ctype = self::typify($cval);
2761
+
2762
+ // PHP empty [] is ambiguous (list vs map). When the spec expects a
2763
+ // map, treat an empty array/ListRef in the data as an empty map so
2764
+ // that validation does not produce a spurious type-mismatch error.
2765
+ // Go/Lua don't hit this because they have distinct map/list types.
2766
+ if (0 < (self::T_map & $ptype) &&
2767
+ (is_array($cval) || $cval instanceof \Voxgig\Struct\ListRef) &&
2768
+ 0 === count($cval)) {
2769
+ $cval = new \stdClass();
2770
+ $ctype = self::typify($cval);
2771
+ }
2772
+
2773
+ // Type mismatch.
2774
+ if ($ptype !== $ctype && self::undef() !== $pval) {
2775
+ $inj->errs[] = self::_invalidTypeMsg($inj->path, self::typename($ptype), $ctype, $cval);
2776
+ return;
2777
+ }
2778
+
2779
+ if (self::ismap($cval)) {
2780
+ if (!self::ismap($pval)) {
2781
+ $inj->errs[] = self::_invalidTypeMsg($inj->path, self::typename($ptype), $ctype, $cval);
2782
+ return;
2783
+ }
2784
+
2785
+ $ckeys = self::keysof($cval);
2786
+ $pkeys = self::keysof($pval);
2787
+
2788
+ // Empty spec object {} means object can be open (any keys).
2789
+ if (0 < count($pkeys) && true !== self::_getprop($pval, '`$OPEN`')) {
2790
+ $badkeys = [];
2791
+ foreach ($ckeys as $ckey) {
2792
+ if (!self::haskey($pval, $ckey)) {
2793
+ $badkeys[] = $ckey;
2794
+ }
2795
+ }
2796
+
2797
+ // Closed object, so reject extra keys not in shape.
2798
+ if (0 < count($badkeys)) {
2799
+ $msg = 'Unexpected keys at field ' . self::pathify($inj->path, 1) . ': ' . implode(', ', $badkeys);
2800
+ $inj->errs[] = $msg;
2801
+ }
2802
+ } else {
2803
+ // Object is open, so merge in extra keys.
2804
+ self::merge([$pval, $cval]);
2805
+ if (self::isnode($pval)) {
2806
+ self::delprop($pval, '`$OPEN`');
2807
+ }
2808
+ }
2809
+ } elseif (self::islist($cval)) {
2810
+ if (!self::islist($pval)) {
2811
+ $inj->errs[] = self::_invalidTypeMsg($inj->path, self::typename($ptype), $ctype, $cval);
2812
+ }
2813
+ } elseif ($exact) {
2814
+ if ($cval !== $pval) {
2815
+ $pathmsg = 1 < self::size($inj->path) ? 'at field ' . self::pathify($inj->path, 1) . ': ' : '';
2816
+ $inj->errs[] = 'Value ' . $pathmsg . $cval . ' should equal ' . $pval . '.';
2817
+ }
2818
+ } else {
2819
+ // Spec value was a default, copy over data
2820
+ self::setprop($parent, $key, $cval);
2821
+ }
2822
+ }
2823
+
2824
+ /**
2825
+ * Validation handler for injection.
2826
+ */
2827
+ private static function _validatehandler(
2828
+ object $inj,
2829
+ mixed $val,
2830
+ string $ref,
2831
+ mixed $store
2832
+ ): mixed {
2833
+ $out = $val;
2834
+
2835
+ $m = preg_match(self::R_META_PATH, $ref, $matches);
2836
+ $ismetapath = null != $m;
2837
+
2838
+ if ($ismetapath) {
2839
+ if ('=' === $matches[2]) {
2840
+ $inj->setval(['`$EXACT`', $val]);
2841
+ } else {
2842
+ $inj->setval($val);
2843
+ }
2844
+ $inj->keyI = -1;
2845
+
2846
+ $out = self::SKIP;
2847
+ } else {
2848
+ $out = self::_injecthandler($inj, $val, $ref, $store);
2849
+ }
2850
+
2851
+ return $out;
2852
+ }
2853
+
2854
+ /**
2855
+ * Validate a data structure against a shape specification.
2856
+ * The shape specification follows the "by example" principle.
2857
+ *
2858
+ * @param mixed $data Source data to validate
2859
+ * @param mixed $spec Validation specification
2860
+ * @param mixed $injdef Optional injection definition with extra validators, etc.
2861
+ * @return mixed Validated data
2862
+ */
2863
+ public static function validate(mixed $data, mixed $spec, mixed $injdef = null): mixed
2864
+ {
2865
+ $extra = is_object($injdef) && property_exists($injdef, 'extra') ? $injdef->extra : null;
2866
+
2867
+ $collect = null != $injdef && property_exists($injdef, 'errs');
2868
+
2869
+ // PHP arrays are value-copied, so a plain array on $injdef->errs would be
2870
+ // detached from $inj->errs deep inside inject. Wrap in ArrayObject so the
2871
+ // same storage is shared through the whole transform/inject chain.
2872
+ $errs = (is_object($injdef) && property_exists($injdef, 'errs')) ? $injdef->errs : [];
2873
+ if (is_array($errs)) {
2874
+ $errs = new \ArrayObject($errs);
2875
+ }
2876
+ if ($collect) {
2877
+ $injdef->errs = $errs;
2878
+ }
2879
+
2880
+ $store = array_merge([
2881
+ // Remove the transform commands.
2882
+ '$DELETE' => null,
2883
+ '$COPY' => null,
2884
+ '$KEY' => null,
2885
+ '$META' => null,
2886
+ '$MERGE' => null,
2887
+ '$EACH' => null,
2888
+ '$PACK' => null,
2889
+
2890
+ '$STRING' => [self::class, 'validate_STRING'],
2891
+ '$NUMBER' => [self::class, 'validate_TYPE'],
2892
+ '$INTEGER' => [self::class, 'validate_TYPE'],
2893
+ '$DECIMAL' => [self::class, 'validate_TYPE'],
2894
+ '$BOOLEAN' => [self::class, 'validate_TYPE'],
2895
+ '$NULL' => [self::class, 'validate_TYPE'],
2896
+ '$NIL' => [self::class, 'validate_TYPE'],
2897
+ '$MAP' => [self::class, 'validate_TYPE'],
2898
+ '$LIST' => [self::class, 'validate_TYPE'],
2899
+ '$FUNCTION' => [self::class, 'validate_TYPE'],
2900
+ '$INSTANCE' => [self::class, 'validate_TYPE'],
2901
+ '$ANY' => [self::class, 'validate_ANY'],
2902
+ '$CHILD' => [self::class, 'validate_CHILD'],
2903
+ '$ONE' => [self::class, 'validate_ONE'],
2904
+ '$EXACT' => [self::class, 'validate_EXACT'],
2905
+
2906
+ // A special top level value to collect errors.
2907
+ '$ERRS' => $errs,
2908
+ ], (array) ($extra ?? []));
2909
+
2910
+ $meta = is_object($injdef) && property_exists($injdef, 'meta') ? $injdef->meta : null;
2911
+
2912
+ $transformOpts = new \stdClass();
2913
+ $transformOpts->extra = $store;
2914
+ $transformOpts->modify = [self::class, '_validation'];
2915
+ $transformOpts->handler = [self::class, '_validatehandler'];
2916
+ if ($meta !== null) {
2917
+ $transformOpts->meta = $meta;
2918
+ }
2919
+ $transformOpts->errs = $errs;
2920
+ $out = self::transform($data, $spec, $transformOpts);
2921
+
2922
+ // Unwrap shared ArrayObject back to a plain array on the caller's injdef
2923
+ // so count()/foreach work as callers (including select) expect.
2924
+ if ($collect && $injdef->errs instanceof \ArrayObject) {
2925
+ $injdef->errs = $injdef->errs->getArrayCopy();
2926
+ }
2927
+
2928
+ $generr = (0 < count($errs) && !$collect);
2929
+ if ($generr) {
2930
+ throw new \Exception('Invalid data: ' . implode(' | ', (array) $errs));
2931
+ }
2932
+
2933
+ return $out;
2934
+ }
2935
+
2936
+ /**
2937
+ * Select children from a top-level object that match a MongoDB-style query.
2938
+ * Supports $and, $or, and equality comparisons.
2939
+ * For arrays, children are elements; for objects, children are values.
2940
+ *
2941
+ * @param mixed $query The query specification
2942
+ * @param mixed $children The object or array to search in
2943
+ * @return array Array of matching children
2944
+ */
2945
+ public static function select(mixed $children, mixed $query): array
2946
+ {
2947
+ if (!self::isnode($children)) {
2948
+ return [];
2949
+ }
2950
+
2951
+ if (self::ismap($children)) {
2952
+ $children = array_map(function($n) {
2953
+ self::setprop($n[1], self::S_DKEY, $n[0]);
2954
+ return $n[1];
2955
+ }, self::items($children));
2956
+ } else {
2957
+ $children = array_map(function($n, $i) {
2958
+ if (self::ismap($n)) {
2959
+ self::setprop($n, self::S_DKEY, $i);
2960
+ }
2961
+ return $n;
2962
+ }, $children, array_keys($children));
2963
+ }
2964
+
2965
+ $results = [];
2966
+ $injdef = (object) [
2967
+ 'errs' => [],
2968
+ 'meta' => (object) ['`$EXACT`' => true],
2969
+ 'extra' => [
2970
+ '$AND' => [self::class, 'select_AND'],
2971
+ '$OR' => [self::class, 'select_OR'],
2972
+ '$NOT' => [self::class, 'select_NOT'],
2973
+ '$GT' => [self::class, 'select_CMP'],
2974
+ '$LT' => [self::class, 'select_CMP'],
2975
+ '$GTE' => [self::class, 'select_CMP'],
2976
+ '$LTE' => [self::class, 'select_CMP'],
2977
+ '$LIKE' => [self::class, 'select_CMP'],
2978
+ ]
2979
+ ];
2980
+
2981
+ $q = self::clone($query);
2982
+ $q = self::_select_add_open($q);
2983
+
2984
+ foreach ($children as $child) {
2985
+ $injdef->errs = [];
2986
+ self::validate($child, self::clone($q), $injdef);
2987
+
2988
+ if (count($injdef->errs) === 0) {
2989
+ $results[] = $child;
2990
+ }
2991
+ }
2992
+
2993
+ return $results;
2994
+ }
2995
+
2996
+ // Recursively add `$OPEN` to all maps in a query for select.
2997
+ // PHP arrays are value types, so walk can't modify in-place.
2998
+ private static function _select_add_open(mixed $val): mixed
2999
+ {
3000
+ if (self::ismap($val)) {
3001
+ if (is_array($val)) {
3002
+ if (!array_key_exists('`$OPEN`', $val)) {
3003
+ $val['`$OPEN`'] = true;
3004
+ }
3005
+ foreach ($val as $k => $v) {
3006
+ $val[$k] = self::_select_add_open($v);
3007
+ }
3008
+ } elseif ($val instanceof \stdClass) {
3009
+ if (!property_exists($val, '`$OPEN`')) {
3010
+ $val->{'`$OPEN`'} = true;
3011
+ }
3012
+ foreach (get_object_vars($val) as $k => $v) {
3013
+ $val->$k = self::_select_add_open($v);
3014
+ }
3015
+ }
3016
+ } elseif (self::islist($val) && is_array($val)) {
3017
+ foreach ($val as $i => $v) {
3018
+ $val[$i] = self::_select_add_open($v);
3019
+ }
3020
+ }
3021
+ return $val;
3022
+ }
3023
+
3024
+ /**
3025
+ * Helper method for $AND operator in select queries
3026
+ */
3027
+ private static function select_AND(object $state, mixed $_val, mixed $_ref, mixed $store): mixed
3028
+ {
3029
+ if (self::M_KEYPRE === $state->mode) {
3030
+ $terms = self::_getprop($state->parent, $state->key);
3031
+
3032
+ $ppath = self::slice($state->path, -1);
3033
+ $point = self::getpath($store, $ppath);
3034
+
3035
+ $vstore = self::merge([(object) [], $store], 1);
3036
+ $vstore->{'$TOP'} = $point;
3037
+
3038
+ foreach ($terms as $term) {
3039
+ $terrs = [];
3040
+ self::validate($point, $term, (object) [
3041
+ 'extra' => $vstore,
3042
+ 'errs' => $terrs,
3043
+ 'meta' => $state->meta,
3044
+ ]);
3045
+
3046
+ if (count($terrs) !== 0) {
3047
+ $state->errs[] = 'AND:' . self::pathify($ppath) . ': ' . self::stringify($point) . ' fail:' . self::stringify($terms);
3048
+ }
3049
+ }
3050
+
3051
+ $gkey = self::getelem($state->path, -2);
3052
+ $gp = self::getelem($state->nodes, -2);
3053
+ self::setprop($gp, $gkey, $point);
3054
+ }
3055
+ return null;
3056
+ }
3057
+
3058
+ /**
3059
+ * Helper method for $OR operator in select queries
3060
+ */
3061
+ private static function select_OR(object $state, mixed $_val, mixed $_ref, mixed $store): mixed
3062
+ {
3063
+ if (self::M_KEYPRE === $state->mode) {
3064
+ $terms = self::_getprop($state->parent, $state->key);
3065
+
3066
+ $ppath = self::slice($state->path, -1);
3067
+ $point = self::getpath($store, $ppath);
3068
+
3069
+ $vstore = self::merge([(object) [], $store], 1);
3070
+ $vstore->{'$TOP'} = $point;
3071
+
3072
+ foreach ($terms as $term) {
3073
+ $terrs = [];
3074
+ self::validate($point, $term, (object) [
3075
+ 'extra' => $vstore,
3076
+ 'errs' => $terrs,
3077
+ 'meta' => $state->meta,
3078
+ ]);
3079
+
3080
+ if (count($terrs) === 0) {
3081
+ $gkey = self::getelem($state->path, -2);
3082
+ $gp = self::getelem($state->nodes, -2);
3083
+ self::setprop($gp, $gkey, $point);
3084
+
3085
+ return null;
3086
+ }
3087
+ }
3088
+
3089
+ $state->errs[] = 'OR:' . self::pathify($ppath) . ': ' . self::stringify($point) . ' fail:' . self::stringify($terms);
3090
+ }
3091
+ return null;
3092
+ }
3093
+
3094
+ /**
3095
+ * Helper method for $NOT operator in select queries
3096
+ */
3097
+ private static function select_NOT(object $state, mixed $_val, mixed $_ref, mixed $store): mixed
3098
+ {
3099
+ if (self::M_KEYPRE === $state->mode) {
3100
+ $term = self::_getprop($state->parent, $state->key);
3101
+
3102
+ $ppath = self::slice($state->path, -1);
3103
+ $point = self::getpath($store, $ppath);
3104
+
3105
+ $vstore = self::merge([(object) [], $store], 1);
3106
+ $vstore->{'$TOP'} = $point;
3107
+
3108
+ $terrs = [];
3109
+ self::validate($point, $term, (object) [
3110
+ 'extra' => $vstore,
3111
+ 'errs' => $terrs,
3112
+ 'meta' => $state->meta,
3113
+ ]);
3114
+
3115
+ if (count($terrs) === 0) {
3116
+ $state->errs[] = 'NOT:' . self::pathify($ppath) . ': ' . self::stringify($point) . ' fail:' . self::stringify($term);
3117
+ }
3118
+
3119
+ $gkey = self::getelem($state->path, -2);
3120
+ $gp = self::getelem($state->nodes, -2);
3121
+ self::setprop($gp, $gkey, $point);
3122
+ }
3123
+ return null;
3124
+ }
3125
+
3126
+ /**
3127
+ * Helper method for comparison operators in select queries
3128
+ */
3129
+ private static function select_CMP(object $state, mixed $_val, mixed $ref, mixed $store): mixed
3130
+ {
3131
+ if (self::M_KEYPRE === $state->mode) {
3132
+ $term = self::_getprop($state->parent, $state->key);
3133
+ $gkey = self::getelem($state->path, -2);
3134
+
3135
+ $ppath = self::slice($state->path, -1);
3136
+ $point = self::getpath($store, $ppath);
3137
+
3138
+ $pass = false;
3139
+
3140
+ if ('$GT' === $ref && $point > $term) {
3141
+ $pass = true;
3142
+ }
3143
+ elseif ('$LT' === $ref && $point < $term) {
3144
+ $pass = true;
3145
+ }
3146
+ elseif ('$GTE' === $ref && $point >= $term) {
3147
+ $pass = true;
3148
+ }
3149
+ elseif ('$LTE' === $ref && $point <= $term) {
3150
+ $pass = true;
3151
+ }
3152
+ elseif ('$LIKE' === $ref && preg_match('/' . $term . '/', self::stringify($point))) {
3153
+ $pass = true;
3154
+ }
3155
+
3156
+ if ($pass) {
3157
+ // Update spec to match found value so that _validate does not complain
3158
+ $gp = self::getelem($state->nodes, -2);
3159
+ self::setprop($gp, $gkey, $point);
3160
+ }
3161
+ else {
3162
+ $state->errs[] = 'CMP: ' . self::pathify($ppath) . ': ' . self::stringify($point) .
3163
+ ' fail:' . $ref . ' ' . self::stringify($term);
3164
+ }
3165
+ }
3166
+ return null;
3167
+ }
3168
+
3169
+ /**
3170
+ * Get element from array by index, supporting negative indices
3171
+ * The key should be an integer, or a string that can parse to an integer only.
3172
+ * Negative integers count from the end of the list.
3173
+ */
3174
+ public static function getelem(mixed $val, mixed $key, mixed $alt = null): mixed
3175
+ {
3176
+ $altIsDefault = (func_num_args() < 3);
3177
+ $out = self::undef();
3178
+
3179
+ if ($val === null || $val === self::undef() || $key === null || $key === self::undef()) {
3180
+ return $altIsDefault ? null : (is_callable($alt) ? $alt() : $alt);
3181
+ }
3182
+
3183
+ if (self::islist($val)) {
3184
+ $listArr = ($val instanceof ListRef) ? $val->list : $val;
3185
+ $listLen = count($listArr);
3186
+ if (is_string($key)) {
3187
+ if (!preg_match('/^[-0-9]+$/', $key)) {
3188
+ $out = self::undef();
3189
+ } else {
3190
+ $nkey = (int) $key;
3191
+ if ($nkey < 0) {
3192
+ $nkey = $listLen + $nkey;
3193
+ }
3194
+ $out = ($nkey >= 0 && $nkey < $listLen) ? $listArr[$nkey] : self::undef();
3195
+ }
3196
+ } elseif (is_int($key)) {
3197
+ $nkey = $key;
3198
+ if ($nkey < 0) {
3199
+ $nkey = $listLen + $nkey;
3200
+ }
3201
+ $out = ($nkey >= 0 && $nkey < $listLen) ? $listArr[$nkey] : self::undef();
3202
+ }
3203
+ }
3204
+
3205
+ if ($out === self::undef()) {
3206
+ if ($altIsDefault) {
3207
+ return null;
3208
+ }
3209
+ return is_callable($alt) ? $alt() : $alt;
3210
+ }
3211
+
3212
+ return $out;
3213
+ }
3214
+
3215
+ /**
3216
+ * Safely delete a property from an object or array element.
3217
+ * Undefined arguments and invalid keys are ignored.
3218
+ * Returns the (possibly modified) parent.
3219
+ * For objects, the property is deleted using unset.
3220
+ * For arrays, the element at the index is removed and remaining elements are shifted down.
3221
+ */
3222
+ public static function delprop(mixed $parent, mixed $key): mixed
3223
+ {
3224
+ if (!self::iskey($key)) {
3225
+ return $parent;
3226
+ }
3227
+
3228
+ if ($parent instanceof ListRef) {
3229
+ $keyI = (int)$key;
3230
+ if (!is_numeric($key)) {
3231
+ return $parent;
3232
+ }
3233
+ if ($keyI >= 0 && $keyI < count($parent->list)) {
3234
+ array_splice($parent->list, $keyI, 1);
3235
+ }
3236
+ return $parent;
3237
+ }
3238
+
3239
+ if (self::ismap($parent)) {
3240
+ $key = self::strkey($key);
3241
+ unset($parent->$key);
3242
+ }
3243
+ elseif (self::islist($parent)) {
3244
+ // Ensure key is an integer
3245
+ $keyI = (int)$key;
3246
+ if (!is_numeric($key) || (string)$keyI !== (string)$key) {
3247
+ return $parent;
3248
+ }
3249
+
3250
+ // Delete list element at position keyI, shifting later elements down
3251
+ if ($keyI >= 0 && $keyI < count($parent)) {
3252
+ for ($pI = $keyI; $pI < count($parent) - 1; $pI++) {
3253
+ $parent[$pI] = $parent[$pI + 1];
3254
+ }
3255
+ array_pop($parent);
3256
+ }
3257
+ }
3258
+
3259
+ return $parent;
3260
+ }
3261
+
3262
+
3263
+ public static function setpath(
3264
+ mixed $store,
3265
+ mixed $path,
3266
+ mixed $val,
3267
+ mixed $injdef = null
3268
+ ): mixed {
3269
+ $pathType = self::typify($path);
3270
+
3271
+ $parts = (0 < (self::T_list & $pathType)) ? $path :
3272
+ ((0 < (self::T_string & $pathType)) ? explode('.', $path) :
3273
+ ((0 < (self::T_number & $pathType)) ? [$path] : self::undef()));
3274
+
3275
+ if (self::undef() === $parts) {
3276
+ return self::undef();
3277
+ }
3278
+
3279
+ $base = self::_getprop($injdef, self::S_BASE);
3280
+ $numparts = self::size($parts);
3281
+ $parent = self::_getprop($store, $base, $store);
3282
+
3283
+ for ($pI = 0; $pI < $numparts - 1; $pI++) {
3284
+ $partKey = self::getelem($parts, $pI);
3285
+ $nextParent = self::_getprop($parent, $partKey);
3286
+ if (!self::isnode($nextParent)) {
3287
+ $nextParent = (0 < (self::T_number & self::typify(self::getelem($parts, $pI + 1))))
3288
+ ? [] : new \stdClass();
3289
+ self::setprop($parent, $partKey, $nextParent);
3290
+ }
3291
+ $parent = $nextParent;
3292
+ }
3293
+
3294
+ if ($val === self::DELETE) {
3295
+ self::delprop($parent, self::getelem($parts, -1));
3296
+ } else {
3297
+ self::setprop($parent, self::getelem($parts, -1), $val);
3298
+ }
3299
+
3300
+ return $parent;
3301
+ }
3302
+
3303
+
3304
+ public static function checkPlacement(
3305
+ int $modes,
3306
+ string $ijname,
3307
+ int $parentTypes,
3308
+ object $inj
3309
+ ): bool {
3310
+ if (0 === ($modes & $inj->mode)) {
3311
+ $modeItems = array_filter(
3312
+ [self::M_KEYPRE, self::M_KEYPOST, self::M_VAL],
3313
+ fn($m) => $modes & $m
3314
+ );
3315
+ $placementNames = array_map(fn($m) => self::PLACEMENT[$m] ?? '', $modeItems);
3316
+ $inj->errs[] = '$' . $ijname . ': invalid placement as ' . (self::PLACEMENT[$inj->mode] ?? '') .
3317
+ ', expected: ' . implode(',', $placementNames) . '.';
3318
+ return false;
3319
+ }
3320
+ if (!self::isempty($parentTypes)) {
3321
+ $ptype = self::typify($inj->parent);
3322
+ if (0 === ($parentTypes & $ptype)) {
3323
+ $inj->errs[] = '$' . $ijname . ': invalid placement in parent ' . self::typename($ptype) .
3324
+ ', expected: ' . self::typename($parentTypes) . '.';
3325
+ return false;
3326
+ }
3327
+ }
3328
+ return true;
3329
+ }
3330
+
3331
+
3332
+ public static function injectorArgs(array $argTypes, array $args): array
3333
+ {
3334
+ $numargs = self::size($argTypes);
3335
+ $found = array_fill(0, 1 + $numargs, self::undef());
3336
+ $found[0] = self::undef();
3337
+ for ($argI = 0; $argI < $numargs; $argI++) {
3338
+ $arg = $args[$argI] ?? self::undef();
3339
+ $argType = self::typify($arg);
3340
+ if (0 === ($argTypes[$argI] & $argType)) {
3341
+ $found[0] = 'invalid argument: ' . self::stringify($arg, 22) .
3342
+ ' (' . self::typename($argType) . ' at position ' . (1 + $argI) .
3343
+ ') is not of type: ' . self::typename($argTypes[$argI]) . '.';
3344
+ break;
3345
+ }
3346
+ $found[1 + $argI] = $arg;
3347
+ }
3348
+ return $found;
3349
+ }
3350
+
3351
+
3352
+ public static function injectChild(mixed $child, mixed $store, object $inj): object
3353
+ {
3354
+ $cinj = $inj;
3355
+
3356
+ // Replace ['`$FORMAT`',...] with child
3357
+ if (null !== $inj->prior) {
3358
+ if (null !== $inj->prior->prior) {
3359
+ $cinj = $inj->prior->prior->child($inj->prior->keyI, $inj->prior->keys);
3360
+ $cinj->val = $child;
3361
+ self::setprop($cinj->parent, $inj->prior->key, $child);
3362
+ }
3363
+ else {
3364
+ $cinj = $inj->prior->child($inj->keyI, $inj->keys);
3365
+ $cinj->val = $child;
3366
+ self::setprop($cinj->parent, $inj->key, $child);
3367
+ }
3368
+ }
3369
+
3370
+ self::inject($child, $store, $cinj);
3371
+
3372
+ return $cinj;
3373
+ }
3374
+
3375
+ }
3376
+
3377
+
3378
+ class Injection
3379
+ {
3380
+
3381
+ public int $mode;
3382
+ public bool $full;
3383
+ public int $keyI;
3384
+ public array $keys;
3385
+ public string $key;
3386
+ public mixed $val;
3387
+ public mixed $parent;
3388
+ public array $path;
3389
+ public array $nodes;
3390
+ /** @var callable */
3391
+ public mixed $handler;
3392
+ // Accepts plain array or ArrayObject. ArrayObject is used by validate/select
3393
+ // so that mutations propagate back through the inject/transform call chain.
3394
+ public array|\ArrayObject $errs;
3395
+ public object $meta;
3396
+ public mixed $dparent;
3397
+ public array $dpath;
3398
+ public string $base;
3399
+ /** @var callable|null */
3400
+ public mixed $modify;
3401
+ public ?Injection $prior;
3402
+ public mixed $extra;
3403
+
3404
+ public function __construct(mixed $val, mixed $parent)
3405
+ {
3406
+ $this->val = $val;
3407
+ $this->parent = $parent;
3408
+ $this->errs = [];
3409
+
3410
+ $this->dparent = Struct::UNDEF;
3411
+ $this->dpath = ['$TOP'];
3412
+
3413
+ $this->mode = Struct::M_VAL;
3414
+ $this->full = false;
3415
+ $this->keyI = 0;
3416
+ $this->keys = ['$TOP'];
3417
+ $this->key = '$TOP';
3418
+ $this->path = ['$TOP'];
3419
+ $this->nodes = [$parent];
3420
+ $this->handler = [Struct::class, '_injecthandler'];
3421
+ $this->base = '$TOP';
3422
+ $this->meta = (object) [];
3423
+ $this->modify = null;
3424
+ $this->prior = null;
3425
+ $this->extra = null;
3426
+ }
3427
+
3428
+
3429
+ public function __toString(): string
3430
+ {
3431
+ return $this->toString();
3432
+ }
3433
+
3434
+ public function toString(?string $prefix = null): string
3435
+ {
3436
+ return 'INJ' . (null === $prefix ? '' : '/' . $prefix) . ':' .
3437
+ Struct::pad(Struct::pathify($this->path, 1)) .
3438
+ (Struct::MODENAME[$this->mode] ?? '') . ($this->full ? '/full' : '') . ':' .
3439
+ 'key=' . $this->keyI . '/' . $this->key . '/' . '[' . implode(',', $this->keys) . ']' .
3440
+ ' p=' . Struct::stringify($this->parent, -1, 1) .
3441
+ ' m=' . Struct::stringify($this->meta, -1, 1) .
3442
+ ' d/' . Struct::pathify($this->dpath, 1) . '=' . Struct::stringify($this->dparent, -1, 1) .
3443
+ ' r=' . Struct::stringify(Struct::_getprop($this->nodes[0] ?? null, '$TOP'), -1, 1);
3444
+ }
3445
+
3446
+
3447
+ public function descend(): mixed
3448
+ {
3449
+ if (!isset($this->meta->__d)) {
3450
+ $this->meta->__d = 0;
3451
+ }
3452
+ $this->meta->__d++;
3453
+ $parentkey = Struct::getelem($this->path, -2);
3454
+
3455
+ // Resolve current node in store for local paths.
3456
+ if (Struct::UNDEF === $this->dparent) {
3457
+
3458
+ // Even if there's no data, dpath should continue to match path, so that
3459
+ // relative paths work properly.
3460
+ if (1 < Struct::size($this->dpath)) {
3461
+ $this->dpath = Struct::flatten([$this->dpath, $parentkey]);
3462
+ }
3463
+ }
3464
+ else {
3465
+ // this->dparent is the containing node of the current store value.
3466
+ if (null !== $parentkey && Struct::UNDEF !== $parentkey) {
3467
+ $this->dparent = Struct::_getprop($this->dparent, $parentkey);
3468
+
3469
+ $lastpart = Struct::getelem($this->dpath, -1);
3470
+ if ($lastpart === '$:' . $parentkey) {
3471
+ $this->dpath = Struct::slice($this->dpath, -1);
3472
+ }
3473
+ else {
3474
+ $this->dpath = Struct::flatten([$this->dpath, $parentkey]);
3475
+ }
3476
+ }
3477
+ }
3478
+
3479
+ return $this->dparent;
3480
+ }
3481
+
3482
+
3483
+ public function child(int $keyI, array $keys): Injection
3484
+ {
3485
+ $key = Struct::strkey($keys[$keyI] ?? null);
3486
+ $val = $this->val;
3487
+
3488
+ $cinj = new Injection(Struct::_getprop($val, $key), $val);
3489
+ $cinj->keyI = $keyI;
3490
+ $cinj->keys = $keys;
3491
+ $cinj->key = $key;
3492
+
3493
+ $cinj->path = Struct::flatten([Struct::getdef($this->path, []), $key]);
3494
+ $cinj->nodes = Struct::flatten([Struct::getdef($this->nodes, []), [$val]]);
3495
+
3496
+ $cinj->mode = $this->mode;
3497
+ $cinj->handler = $this->handler;
3498
+ $cinj->modify = $this->modify;
3499
+ $cinj->base = $this->base;
3500
+ $cinj->meta = $this->meta;
3501
+ $cinj->errs = &$this->errs;
3502
+ $cinj->prior = $this;
3503
+
3504
+ $cinj->dpath = Struct::flatten([$this->dpath]);
3505
+ $cinj->dparent = $this->dparent;
3506
+
3507
+ return $cinj;
3508
+ }
3509
+
3510
+
3511
+ public function setval(mixed $val, ?int $ancestor = null): mixed
3512
+ {
3513
+ $parent = Struct::UNDEF;
3514
+ if (null === $ancestor || $ancestor < 2) {
3515
+ if (Struct::UNDEF === $val) {
3516
+ $this->parent = Struct::delprop($this->parent, $this->key);
3517
+ $parent = $this->parent;
3518
+ } else {
3519
+ $parent = Struct::setprop($this->parent, $this->key, $val);
3520
+ }
3521
+ }
3522
+ else {
3523
+ $aval = Struct::getelem($this->nodes, 0 - $ancestor);
3524
+ $akey = Struct::getelem($this->path, 0 - $ancestor);
3525
+ if (Struct::UNDEF === $val) {
3526
+ $parent = Struct::delprop($aval, $akey);
3527
+ } else {
3528
+ $parent = Struct::setprop($aval, $akey, $val);
3529
+ }
3530
+ }
3531
+
3532
+ return $parent;
3533
+ }
3534
+ }
3535
+ ?>