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