hecks 3.14.0 → 5.0.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 (427) hide show
  1. checksums.yaml +4 -4
  2. data/examples/pizzas/adapters/fake_processor.rb +34 -0
  3. data/examples/pizzas/adapters/payment_signature.rb +66 -0
  4. data/examples/pizzas/adapters/payment_webhook.rb +87 -0
  5. data/examples/pizzas/bluebook/pizzas.behaviors +33 -0
  6. data/examples/pizzas/bluebook/pizzas.hecksagon +5 -3
  7. data/examples/pizzas/config.ru +22 -0
  8. data/exe/hecks +1 -1
  9. data/lib/hecks/adapters/driven/prism.rb +0 -2
  10. data/lib/hecks/adapters/driven.rb +0 -2
  11. data/lib/hecks/adapters/driving/json.rb +9 -16
  12. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -2
  13. data/lib/hecks/bluebook/dsl/derivation_words.rb +58 -0
  14. data/lib/hecks/bluebook/dsl/translation_builder.rb +6 -37
  15. data/lib/hecks/bluebook/expression/ast_json.rb +0 -9
  16. data/lib/hecks/bluebook/hecksagon.rb +1 -1
  17. data/lib/hecks/bluebook/meta_validator/translation_judge/rules.rb +7 -10
  18. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +4 -8
  19. data/lib/hecks/bluebook/meta_validator.rb +0 -11
  20. data/lib/hecks/bluebook/translation.rb +18 -10
  21. data/lib/hecks/bluebook/translation_expression/evaluator.rb +63 -0
  22. data/lib/hecks/bluebook/translation_expression/parser.rb +127 -0
  23. data/lib/hecks/bluebook/translation_expression.rb +39 -0
  24. data/lib/hecks/bluebook/translation_plan.rb +70 -0
  25. data/lib/hecks/bluebook.rb +2 -0
  26. data/lib/hecks/chapters.rb +3 -3
  27. data/lib/hecks/cli/project_cli/launcher_source.rb +1 -1
  28. data/lib/hecks/composer/adapters/claude_composer.adapter +3 -0
  29. data/lib/hecks/composer/adapters/claude_composer.rb +83 -0
  30. data/lib/hecks/composer/adapters/fake_agent.adapter +3 -0
  31. data/lib/hecks/composer/adapters/fake_agent.rb +55 -0
  32. data/lib/hecks/composer/adapters/jev.adapter +3 -0
  33. data/lib/hecks/composer/adapters/jev.rb +66 -0
  34. data/lib/hecks/composer/agent.rb +113 -0
  35. data/lib/hecks/composer/assistant.rb +110 -0
  36. data/lib/hecks/composer/behavior_run.rb +53 -0
  37. data/lib/hecks/composer/bluebook/composer.bluebook +178 -0
  38. data/lib/hecks/composer/bluebook/composer.hecksagon +16 -0
  39. data/lib/hecks/composer/board.rb +65 -0
  40. data/lib/hecks/composer/board_edit/command_text.rb +39 -0
  41. data/lib/hecks/composer/board_edit/source.rb +84 -0
  42. data/lib/hecks/composer/board_edit.rb +144 -0
  43. data/lib/hecks/composer/check.rb +89 -0
  44. data/lib/hecks/composer/domains.rb +81 -0
  45. data/lib/hecks/composer/jobs.rb +134 -0
  46. data/lib/hecks/composer/layout.rb +37 -0
  47. data/lib/hecks/composer/live.rb +150 -0
  48. data/lib/hecks/composer/outline.rb +48 -0
  49. data/lib/hecks/composer/page.html +989 -0
  50. data/lib/hecks/composer/panels.rb +142 -0
  51. data/lib/hecks/composer/parse.rb +83 -0
  52. data/lib/hecks/composer/plain.rb +28 -0
  53. data/lib/hecks/composer/ports/composer_agent.port +7 -0
  54. data/lib/hecks/composer/session.rb +141 -0
  55. data/lib/hecks/composer/toolbox.rb +166 -0
  56. data/lib/hecks/composer/web.rb +156 -0
  57. data/lib/hecks/composer/workbench.rb +51 -0
  58. data/lib/hecks/composer/workspace.rb +81 -0
  59. data/lib/hecks/composer.rb +17 -0
  60. data/lib/hecks/corpus/adapters.rb +22 -0
  61. data/lib/hecks/corpus/rust.rb +1 -1
  62. data/lib/hecks/corpus/worlds.rb +19 -0
  63. data/lib/hecks/corpus.rb +5 -0
  64. data/lib/hecks/forms/app/commands.rb +2 -3
  65. data/lib/hecks/forms/app/queries.rb +2 -3
  66. data/lib/hecks/forms/app/records.rb +17 -5
  67. data/lib/hecks/forms/app/responses.rb +3 -1
  68. data/lib/hecks/forms/command_form_renderer.rb +42 -16
  69. data/lib/hecks/forms/field_controls.rb +2 -2
  70. data/lib/hecks/forms/field_renderer.rb +1 -1
  71. data/lib/hecks/forms/field_shape.rb +36 -17
  72. data/lib/hecks/forms/index_renderer.rb +2 -1
  73. data/lib/hecks/forms/page.rb +1 -1
  74. data/lib/hecks/forms/params.rb +0 -13
  75. data/lib/hecks/forms/query_form_renderer.rb +7 -6
  76. data/lib/hecks/forms/record_renderer.rb +11 -7
  77. data/lib/hecks/forms/rule_words.rb +100 -0
  78. data/lib/hecks/forms/value_object_shape.rb +2 -1
  79. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -3
  80. data/lib/hecks/fuzzing/isolated_boot/worlds.rb +3 -6
  81. data/lib/hecks/fuzzing/replay/filters.rb +2 -6
  82. data/lib/hecks/gate/stages.yml +25 -0
  83. data/lib/hecks/grammar/evolve/seeds.rb +0 -6
  84. data/lib/hecks/grammar/evolve.rb +0 -3
  85. data/lib/hecks/grammar/translation.bluebook +1 -1
  86. data/lib/hecks/hecks/adapters/codebase/composing.rb +56 -0
  87. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +3 -1
  88. data/lib/hecks/hecks/adapters/first_admin.rb +3 -3
  89. data/lib/hecks/hecks/adapters/journal_store/changes.rb +0 -26
  90. data/lib/hecks/hecks/adapters/journal_store/examination.rb +2 -23
  91. data/lib/hecks/hecks/adapters/journal_store/readings.rb +3 -11
  92. data/lib/hecks/hecks/adapters/journal_store.rb +0 -11
  93. data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +4 -11
  94. data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +0 -9
  95. data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +0 -4
  96. data/lib/hecks/hecks/codebase.behaviors +6 -0
  97. data/lib/hecks/hecks/codebase.bluebook +21 -0
  98. data/lib/hecks/hecks/composer.behaviors +68 -0
  99. data/lib/hecks/hecks/context_map.hecksagon +1 -1
  100. data/lib/hecks/hecks/custodian.behaviors +0 -6
  101. data/lib/hecks/hecks/custodian.bluebook +3 -80
  102. data/lib/hecks/hecks/hecks.hecksagon +2 -2
  103. data/lib/hecks/hecks/hecks.world +1 -1
  104. data/lib/hecks/hecks/rehearsal.bluebook +7 -13
  105. data/lib/hecks/kernel_capabilities/templates.rb +4 -4
  106. data/lib/hecks/kernel_capabilities.rb +1 -1
  107. data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -3
  108. data/lib/hecks/language/translation/translation_aggregate.bluebook +21 -24
  109. data/lib/hecks/ports/access_control.rb +2 -2
  110. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -41
  111. data/lib/hecks/ports/persistence/plugins/era/lineage/coverage.rb +7 -7
  112. data/lib/hecks/ports/persistence/plugins/era/lineage/rewrite.rb +25 -2
  113. data/lib/hecks/ports/persistence/plugins/era/lineage/rules.rb +4 -4
  114. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +17 -25
  115. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/base_sql.rb +0 -12
  116. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/domain_rename.rb +1 -1
  117. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -22
  118. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +0 -1
  119. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +9 -3
  120. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +3 -29
  121. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/refusals.rb +2 -26
  122. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -33
  123. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -3
  124. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +2 -4
  125. data/lib/hecks/ports/persistence/plugins/era/translation/expression_sql.rb +52 -0
  126. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +23 -29
  127. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +1 -1
  128. data/lib/hecks/ports/persistence/plugins/era/translation.rb +0 -1
  129. data/lib/hecks/ports/persistence/plugins/era.rb +3 -6
  130. data/lib/hecks/ports.rb +0 -1
  131. data/lib/hecks/projection_files.rb +3 -1
  132. data/lib/hecks/projections/deploy/templates/lambda/nat_comment_oauth.tmpl +1 -1
  133. data/lib/hecks/projections/glossary/entries.rb +10 -4
  134. data/lib/hecks/projections/glossary/groups.rb +42 -0
  135. data/lib/hecks/projections/glossary/html/page.rb +7 -2
  136. data/lib/hecks/projections/glossary/html/parser.rb +1 -1
  137. data/lib/hecks/projections/glossary/html/sections.rb +31 -11
  138. data/lib/hecks/projections/glossary/html.rb +1 -2
  139. data/lib/hecks/projections/glossary/markdown.rb +31 -28
  140. data/lib/hecks/projections/glossary/mermaid.rb +3 -0
  141. data/lib/hecks/projections/glossary/page.css +143 -153
  142. data/lib/hecks/projections/glossary/page.js +63 -3
  143. data/lib/hecks/projections/glossary/sentences/actions.rb +88 -0
  144. data/lib/hecks/projections/glossary/sentences/paths.rb +77 -0
  145. data/lib/hecks/projections/glossary/sentences/queries.rb +90 -0
  146. data/lib/hecks/projections/glossary/sentences/value_objects.rb +32 -5
  147. data/lib/hecks/projections/glossary/sentences.rb +13 -3
  148. data/lib/hecks/projections/glossary/slugs.rb +4 -1
  149. data/lib/hecks/projections/glossary.rb +2 -1
  150. data/lib/hecks/projections/plain_english.rb +97 -0
  151. data/lib/hecks/projections/rust_vocabulary/arguments.rb +2 -2
  152. data/lib/hecks/projections/rust_vocabulary/enum_body.rb +20 -25
  153. data/lib/hecks/projections/rust_vocabulary/tests.rb +2 -2
  154. data/lib/hecks/projections/rust_vocabulary.rb +14 -6
  155. data/lib/hecks/projections/site/cms_editor/color.rb +0 -6
  156. data/lib/hecks/projections/site/cms_editor/templates/src/host.ts.tmpl +7 -2
  157. data/lib/hecks/projections/statements/actions.rb +99 -0
  158. data/lib/hecks/projections/statements/relationships.rb +86 -0
  159. data/lib/hecks/projections/statements.rb +15 -24
  160. data/lib/hecks/projector/exporter/adapters.rb +112 -0
  161. data/lib/hecks/projector/exporter/translations.rb +18 -35
  162. data/lib/hecks/projector/exporter/worlds.rb +103 -0
  163. data/lib/hecks/projector/exporter.rb +5 -1
  164. data/lib/hecks/quality_control/adapters/agent.rb +6 -7
  165. data/lib/hecks/quality_control/adapters/agent_profile.rb +16 -114
  166. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +1 -1
  167. data/lib/hecks/runtime/loader/boot_steps.rb +0 -20
  168. data/lib/hecks/runtime/outbox.rb +2 -9
  169. data/lib/hecks/runtime/port_operation_interpreter.rb +1 -9
  170. data/lib/hecks/runtime/remote_dispatcher.rb +2 -2
  171. data/lib/hecks/runtime/value/unwrapping.rb +60 -0
  172. data/lib/hecks/runtime/value.rb +10 -21
  173. data/lib/hecks/rust_build/project_rust/target_ir.rb +23 -4
  174. data/lib/hecks/rust_build/wasm.rb +0 -12
  175. data/lib/hecks/rust_build/write_if_changed.rb +12 -12
  176. data/lib/hecks/rust_format.rb +77 -0
  177. data/lib/hecks/tools/fuzz_sweep/findings.rb +3 -9
  178. data/lib/hecks/tools/fuzz_sweep.rb +1 -3
  179. data/lib/hecks/tools/translation_audit/verdicts.rb +3 -28
  180. data/lib/hecks/tools/translation_audit.rb +8 -10
  181. data/lib/hecks/tools/translation_scaffold/reporting.rb +1 -1
  182. data/lib/hecks/version.rb +1 -1
  183. data/lib/hecks/vocabulary.rb +2 -1
  184. data/lib/hecks.rb +3 -0
  185. data/prebuilt/{1a6f596be124228be93eba1d86dbc9d2933190fa389b284331241cb9f27115a0.marshal → ad68f9e06c8068c2a4176b57abf3083d591d5d8980c2b3e897b97331f965b643.marshal} +0 -0
  186. data/prebuilt/{a883a7e568667aa5ac904c82daff356bdf9e996252a36af6def4a4a43efb967e.marshal → ee22ec8642c4006717ba2b80f46e5efb81de459eef9dfec043e838c0ad4fd720.marshal} +0 -0
  187. data/prebuilt/{verdicts-872993b1aa26d73fe48f28251374b8585fed145d02aab42be0f00de386b6ff80.json → verdicts-b69f5dcbba79c20daa0493476fa7c317004a30aa2bc3d3dc6a722ea70ae2b43b.json} +1 -1
  188. data/rust/Cargo.toml +40 -0
  189. data/rust/build/Cargo.toml +36 -0
  190. data/rust/build/clippy.toml +4 -0
  191. data/rust/build/src/adapters_pass.rs +196 -0
  192. data/rust/build/src/binding_pass.rs +54 -13
  193. data/rust/build/src/build_artifact.rs +23 -5
  194. data/rust/build/src/cargo_sync.rs +69 -26
  195. data/rust/build/src/lineage_pass.rs +101 -87
  196. data/rust/build/src/main.rs +11 -2
  197. data/rust/build/src/pipeline.rs +80 -19
  198. data/rust/build/src/reserved_names.rs +6 -50
  199. data/rust/build/src/resolve.rs +82 -35
  200. data/rust/build/src/rustfmt.rs +100 -0
  201. data/rust/build/src/seams_pass.rs +85 -16
  202. data/rust/build/src/subprocess.rs +30 -6
  203. data/rust/build/src/tmp.rs +5 -2
  204. data/rust/build/src/translation_pass.rs +72 -22
  205. data/rust/build/src/world_pass.rs +109 -0
  206. data/rust/clippy.toml +4 -0
  207. data/rust/codegen/Cargo.toml +36 -0
  208. data/rust/codegen/clippy.toml +4 -0
  209. data/rust/codegen/src/asks.rs +40 -14
  210. data/rust/codegen/src/attr.rs +2 -2
  211. data/rust/codegen/src/bridging.rs +676 -117
  212. data/rust/codegen/src/commands.rs +979 -373
  213. data/rust/codegen/src/constraints.rs +88 -25
  214. data/rust/codegen/src/dependency_planning.rs +128 -34
  215. data/rust/codegen/src/domain_generator.rs +306 -170
  216. data/rust/codegen/src/exemplar.rs +87 -32
  217. data/rust/codegen/src/expr_emitter.rs +54 -6
  218. data/rust/codegen/src/fielded.rs +193 -41
  219. data/rust/codegen/src/hecks_naming.rs +124 -16
  220. data/rust/codegen/src/hecks_naming_tests.rs +81 -0
  221. data/rust/codegen/src/json.rs +80 -44
  222. data/rust/codegen/src/json_codec.rs +585 -149
  223. data/rust/codegen/src/literal.rs +104 -138
  224. data/rust/codegen/src/main.rs +151 -52
  225. data/rust/codegen/src/manifest.rs +76 -14
  226. data/rust/codegen/src/mutations.rs +1212 -239
  227. data/rust/codegen/src/naming.rs +111 -60
  228. data/rust/codegen/src/optional_pass.rs +90 -28
  229. data/rust/codegen/src/ports.rs +5 -4
  230. data/rust/codegen/src/prelude.rs +139 -49
  231. data/rust/codegen/src/presenters.rs +335 -0
  232. data/rust/codegen/src/queries.rs +601 -226
  233. data/rust/codegen/src/reactions.rs +268 -74
  234. data/rust/codegen/src/read_models.rs +574 -229
  235. data/rust/codegen/src/reference_specs.rs +16 -3
  236. data/rust/codegen/src/registry.rs +128 -79
  237. data/rust/codegen/src/reserved_names.rs +6 -50
  238. data/rust/codegen/src/shared.rs +71 -22
  239. data/rust/codegen/src/sidecars.rs +15 -5
  240. data/rust/codegen/src/skip_reason.rs +8 -2
  241. data/rust/codegen/src/types.rs +306 -108
  242. data/rust/host/Cargo.lock +1 -0
  243. data/rust/host/Cargo.toml +40 -4
  244. data/rust/host/HECKS_RELEASE +1 -1
  245. data/rust/host/clippy.toml +4 -0
  246. data/rust/host/src/api.rs +698 -218
  247. data/rust/host/src/auth.rs +491 -214
  248. data/rust/host/src/b64.rs +91 -0
  249. data/rust/host/src/bin/expr_harness.rs +6 -2
  250. data/rust/host/src/bin/lineage_harness.rs +47 -11
  251. data/rust/host/src/bin/mint_harness.rs +70 -37
  252. data/rust/host/src/bin/validate_harness.rs +16 -8
  253. data/rust/host/src/boot.rs +144 -68
  254. data/rust/host/src/boundary_fuzz/api.rs +130 -32
  255. data/rust/host/src/boundary_fuzz/auth.rs +108 -23
  256. data/rust/host/src/boundary_fuzz/expr_json.rs +85 -11
  257. data/rust/host/src/boundary_fuzz/ir.rs +164 -12
  258. data/rust/host/src/boundary_fuzz/server.rs +283 -36
  259. data/rust/host/src/boundary_fuzz/web.rs +56 -8
  260. data/rust/host/src/checkout.rs +78 -32
  261. data/rust/host/src/clock.rs +8 -0
  262. data/rust/host/src/commerce_ir.rs +333 -95
  263. data/rust/host/src/crypto.rs +33 -0
  264. data/rust/host/src/dispatch.rs +1037 -331
  265. data/rust/host/src/expr_json.rs +1021 -273
  266. data/rust/host/src/extension.rs +24 -4
  267. data/rust/host/src/field_hints.rs +11 -7
  268. data/rust/host/src/fuzz_support.rs +85 -13
  269. data/rust/host/src/host.rs +100 -0
  270. data/rust/host/src/http.rs +22 -0
  271. data/rust/host/src/ir.rs +326 -16
  272. data/rust/host/src/journal.rs +277 -176
  273. data/rust/host/src/lambda_client.rs +219 -53
  274. data/rust/host/src/lib.rs +16 -4
  275. data/rust/host/src/log.rs +62 -11
  276. data/rust/host/src/mint.rs +1153 -326
  277. data/rust/host/src/needs.rs +185 -44
  278. data/rust/host/src/payments/keystore.rs +197 -45
  279. data/rust/host/src/payments/roles.rs +39 -7
  280. data/rust/host/src/payments/tests.rs +1427 -282
  281. data/rust/host/src/payments.rs +391 -112
  282. data/rust/host/src/platform_signin/attack_tests.rs +5 -5
  283. data/rust/host/src/platform_signin/config.rs +11 -11
  284. data/rust/host/src/platform_signin/cookie.rs +2 -2
  285. data/rust/host/src/platform_signin/extension.rs +2 -1
  286. data/rust/host/src/platform_signin/handler.rs +9 -8
  287. data/rust/host/src/platform_signin/jti.rs +4 -2
  288. data/rust/host/src/platform_signin/jwks.rs +2 -1
  289. data/rust/host/src/platform_signin/jws.rs +2 -37
  290. data/rust/host/src/platform_signin/known_answer_tests.rs +5 -1
  291. data/rust/host/src/platform_signin/mode_tests.rs +5 -3
  292. data/rust/host/src/platform_signin/pages.rs +20 -20
  293. data/rust/host/src/platform_signin/session.rs +8 -1
  294. data/rust/host/src/platform_signin/settings.rs +6 -6
  295. data/rust/host/src/platform_signin/strict_json.rs +12 -9
  296. data/rust/host/src/platform_signin/verify.rs +41 -41
  297. data/rust/host/src/platform_signin/wiring_support.rs +13 -18
  298. data/rust/host/src/presentation.rs +207 -63
  299. data/rust/host/src/presentation_write.rs +502 -187
  300. data/rust/host/src/query_step.rs +69 -18
  301. data/rust/host/src/rate_limit.rs +541 -142
  302. data/rust/host/src/reference_transform.rs +77 -25
  303. data/rust/host/src/reference_validate.rs +443 -104
  304. data/rust/host/src/resend.rs +190 -55
  305. data/rust/host/src/secrets.rs +116 -21
  306. data/rust/host/src/server.rs +555 -104
  307. data/rust/host/src/storage_shape.rs +88 -21
  308. data/rust/host/src/test_pg.rs +17 -7
  309. data/rust/host/src/ui_schema.rs +495 -207
  310. data/rust/host/src/wasm_runner.rs +21 -6
  311. data/rust/host/src/web/commerce.rs +101 -23
  312. data/rust/host/src/web/newsletter/cooldown.rs +10 -3
  313. data/rust/host/src/web/newsletter/template.rs +26 -7
  314. data/rust/host/src/web/newsletter/tests.rs +112 -23
  315. data/rust/host/src/web/newsletter.rs +209 -59
  316. data/rust/host/src/web/newsletter_send.rs +251 -79
  317. data/rust/host/src/web/registration_receipt/tests.rs +16 -4
  318. data/rust/host/src/web/registration_receipt.rs +100 -24
  319. data/rust/host/src/web/registrations.rs +1384 -332
  320. data/rust/host/src/web/resend_webhook.rs +138 -80
  321. data/rust/host/src/web.rs +2416 -508
  322. data/rust/lsp/Cargo.toml +36 -0
  323. data/rust/lsp/clippy.toml +4 -0
  324. data/rust/lsp/src/json.rs +29 -22
  325. data/rust/lsp/src/main.rs +81 -31
  326. data/rust/lsp/src/outline.rs +23 -7
  327. data/rust/lsp/src/rpc.rs +17 -4
  328. data/rust/parser/Cargo.toml +36 -0
  329. data/rust/parser/clippy.toml +4 -0
  330. data/rust/parser/src/build/closed_sets.rs +6 -1
  331. data/rust/parser/src/build/identity.rs +4 -3
  332. data/rust/parser/src/build/mod.rs +1 -1
  333. data/rust/parser/src/build/named_pattern.rs +4 -1
  334. data/rust/parser/src/build/naming.rs +2 -80
  335. data/rust/parser/src/build/pattern_subset.rs +6 -8
  336. data/rust/parser/src/build/query_derive.rs +3 -1
  337. data/rust/parser/src/build/query_inference.rs +14 -5
  338. data/rust/parser/src/build/read_model.rs +4 -1
  339. data/rust/parser/src/build/references.rs +4 -3
  340. data/rust/parser/src/canonical.rs +34 -15
  341. data/rust/parser/src/emit.rs +187 -105
  342. data/rust/parser/src/expr/ast_json.rs +134 -42
  343. data/rust/parser/src/expr/evaluator.rs +27 -9
  344. data/rust/parser/src/expr/mod.rs +54 -13
  345. data/rust/parser/src/expr/resolver.rs +299 -92
  346. data/rust/parser/src/ir.rs +35 -3
  347. data/rust/parser/src/keywords.rs +7168 -516
  348. data/rust/parser/src/lex.rs +11 -25
  349. data/rust/parser/src/lib.rs +1 -0
  350. data/rust/parser/src/main.rs +65 -6
  351. data/rust/parser/src/parse/adapter.rs +157 -0
  352. data/rust/parser/src/parse/aggregate.rs +19 -10
  353. data/rust/parser/src/parse/chapter.rs +16 -17
  354. data/rust/parser/src/parse/command.rs +35 -16
  355. data/rust/parser/src/parse/domain_port.rs +4 -7
  356. data/rust/parser/src/parse/entity.rs +21 -11
  357. data/rust/parser/src/parse/hecksagon.rs +10 -4
  358. data/rust/parser/src/parse/lifecycle.rs +9 -5
  359. data/rust/parser/src/parse/mod.rs +22 -91
  360. data/rust/parser/src/parse/needs.rs +21 -12
  361. data/rust/parser/src/parse/policy.rs +2 -2
  362. data/rust/parser/src/parse/process_manager.rs +9 -11
  363. data/rust/parser/src/parse/query.rs +8 -4
  364. data/rust/parser/src/parse/read_model.rs +57 -16
  365. data/rust/parser/src/parse/value_object.rs +7 -2
  366. data/rust/parser/src/parse/world.rs +322 -0
  367. data/rust/parser/src/ruby_value.rs +7 -6
  368. data/rust/src/exemplar/json.rs +4 -4
  369. data/rust/src/exemplar/mutations.rs +18 -0
  370. data/rust/src/exemplar/read_models.rs +1 -10
  371. data/rust/src/kernel/attribute_shapes/composite.rs +54 -14
  372. data/rust/src/kernel/attribute_shapes/list.rs +2 -2
  373. data/rust/src/kernel/attribute_shapes/mod.rs +8 -8
  374. data/rust/src/kernel/attribute_shapes/scalar.rs +2 -2
  375. data/rust/src/kernel/cli.rs +244 -114
  376. data/rust/src/kernel/dispatch.rs +323 -115
  377. data/rust/src/kernel/dispatch_guard_tests.rs +327 -65
  378. data/rust/src/kernel/expr.rs +203 -86
  379. data/rust/src/kernel/expression_operators/arithmetic.rs +138 -25
  380. data/rust/src/kernel/expression_operators/comparison.rs +87 -2
  381. data/rust/src/kernel/expression_operators/enumeration.rs +106 -22
  382. data/rust/src/kernel/expression_operators/logical.rs +9 -3
  383. data/rust/src/kernel/expression_operators/membership.rs +3 -1
  384. data/rust/src/kernel/expression_operators/mod.rs +34 -21
  385. data/rust/src/kernel/expression_operators/pattern_match.rs +46 -11
  386. data/rust/src/kernel/expression_operators/positional.rs +84 -30
  387. data/rust/src/kernel/expression_operators/presence.rs +52 -13
  388. data/rust/src/kernel/expression_operators/sign_test.rs +3 -1
  389. data/rust/src/kernel/expression_operators/sized.rs +24 -8
  390. data/rust/src/kernel/expression_operators/text.rs +120 -27
  391. data/rust/src/kernel/expression_operators/to_string.rs +10 -4
  392. data/rust/src/kernel/json.rs +244 -148
  393. data/rust/src/kernel/mod.rs +47 -33
  394. data/rust/src/kernel/named_query.rs +63 -15
  395. data/rust/src/kernel/needs.rs +112 -25
  396. data/rust/src/kernel/orchestrate.rs +980 -581
  397. data/rust/src/kernel/orchestrate_guard_tests.rs +538 -140
  398. data/rust/src/kernel/pattern.rs +107 -34
  399. data/rust/src/kernel/present.rs +261 -0
  400. data/rust/src/kernel/query_comparators.rs +118 -41
  401. data/rust/src/kernel/query_ordering.rs +13 -5
  402. data/rust/src/kernel/read_model.rs +479 -181
  403. data/rust/src/kernel/reference_lookup.rs +122 -26
  404. data/rust/src/kernel/repository.rs +293 -66
  405. data/rust/src/kernel/routing.rs +58 -42
  406. data/rust/src/kernel/ruby_float.rs +139 -0
  407. data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +57 -57
  408. data/rust/src/kernel/vocab/entity_dispatch_order.rs +54 -54
  409. data/rust/src/kernel/vocab/field_hint.rs +19 -25
  410. data/rust/src/kernel/vocab/mod.rs +6 -6
  411. data/rust/src/kernel/vocab/query_comparator.rs +23 -24
  412. data/rust/src/kernel/vocab/refusal_template.rs +501 -297
  413. data/rust/src/lib.rs +50 -0
  414. data/rust/src/main.rs +4 -1
  415. data/rust/web/Cargo.toml +36 -0
  416. data/rust/web/clippy.toml +4 -0
  417. metadata +85 -15
  418. data/examples/pizzas/bluebook/hecksagon/mock_stripe_payment_adapter.rb +0 -36
  419. data/lib/hecks/adapters/driven/google_authentication.adapter +0 -3
  420. data/lib/hecks/adapters/driven/google_authentication.rb +0 -99
  421. data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +0 -3
  422. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +0 -32
  423. data/lib/hecks/ports/authentication.port +0 -6
  424. data/lib/hecks/ports/authentication.rb +0 -67
  425. data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +0 -255
  426. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +0 -22
  427. data/rust/host/src/approval.rs +0 -652
@@ -3,86 +3,151 @@
3
3
 
4
4
  use crate::exemplar::Exemplar;
5
5
  use crate::json::Json;
6
+ use crate::shared::{lifecycle_extra_field, puts_blank, puts_str};
6
7
  use crate::{json_codec, types};
7
8
  use std::collections::HashMap;
8
9
 
9
- // Mirrors Ruby's `puts`: appends "\n" only when the string does not already end in one.
10
- fn puts_str(out: &mut String, s: &str) {
11
- out.push_str(s);
12
- if !s.ends_with('\n') {
13
- out.push('\n');
14
- }
15
- }
16
-
17
- fn puts_blank(out: &mut String) {
18
- out.push('\n');
19
- }
20
-
21
- fn lifecycle_extra_field(node: &Json) -> Vec<(String, String)> {
22
- match node.get("lifecycle") {
23
- None => vec![],
24
- Some(lifecycle) => {
25
- let field = lifecycle.get("field").and_then(Json::as_str).unwrap_or("");
26
- let ident = crate::naming::rust_ident_field(field);
27
- vec![(crate::naming::rust_field(field), format!("crate::kernel::Json::Str(self.{ident}.clone())"))]
28
- }
29
- }
30
- }
31
-
32
10
  /// One aggregate's prelude text, or `None` when the aggregate has unsupported attribute
33
11
  /// types and no `.rs` file is generated for it.
34
- pub fn aggregate_prelude(exemplar: &Exemplar, ir: &Json, aggregate: &Json, source_label: &str) -> Option<String> {
35
- let value_objects = aggregate.get("value_objects").map(Json::each).unwrap_or(&[]);
36
- let value_objects_by_name: HashMap<String, &Json> = value_objects.iter().map(|vo| (vo.get("name").and_then(Json::as_str).unwrap_or("").to_string(), vo)).collect();
12
+ pub fn aggregate_prelude(
13
+ exemplar: &Exemplar,
14
+ ir: &Json,
15
+ aggregate: &Json,
16
+ source_label: &str,
17
+ ) -> Option<String> {
18
+ let value_objects = aggregate.get("value_objects").map_or_default(Json::each);
19
+ let value_objects_by_name: HashMap<String, &Json> = value_objects
20
+ .iter()
21
+ .map(|vo| {
22
+ (
23
+ vo.get("name")
24
+ .and_then(Json::as_str)
25
+ .unwrap_or("")
26
+ .to_string(),
27
+ vo,
28
+ )
29
+ })
30
+ .collect();
37
31
 
38
32
  if !types::unsupported_attribute_types(aggregate, &value_objects_by_name).is_empty() {
39
33
  return None;
40
34
  }
41
35
 
42
- let aggregates_by_name: HashMap<String, &Json> = ir.get("aggregates").map(Json::each).unwrap_or(&[]).iter().map(|a| (a.get("name").and_then(Json::as_str).unwrap_or("").to_string(), a)).collect();
36
+ let aggregates_by_name: HashMap<String, &Json> = ir
37
+ .get("aggregates")
38
+ .map_or_default(Json::each)
39
+ .iter()
40
+ .map(|a| {
41
+ (
42
+ a.get("name")
43
+ .and_then(Json::as_str)
44
+ .unwrap_or("")
45
+ .to_string(),
46
+ a,
47
+ )
48
+ })
49
+ .collect();
43
50
 
44
51
  let mut out = String::new();
45
- puts_str(&mut out, &format!("// GENERATED by hecks project_rust from {source_label}'s canonical IR."));
46
- puts_str(&mut out, "// Do not hand-edit — re-run hecks project_rust instead.");
52
+ puts_str(
53
+ &mut out,
54
+ &format!("// GENERATED by hecks project_rust from {source_label}'s canonical IR."),
55
+ );
56
+ puts_str(
57
+ &mut out,
58
+ "// Do not hand-edit — re-run hecks project_rust instead.",
59
+ );
47
60
  puts_str(&mut out, "#![allow(dead_code, unused_variables)]");
48
61
  puts_str(&mut out, "use crate::kernel::Expr;");
49
62
  puts_blank(&mut out);
50
63
 
51
64
  for vo in value_objects {
52
- puts_str(&mut out, &types::emit_value_object(exemplar, vo, &value_objects_by_name, &aggregates_by_name));
65
+ puts_str(
66
+ &mut out,
67
+ &types::emit_value_object(exemplar, vo, &value_objects_by_name, &aggregates_by_name),
68
+ );
53
69
  puts_blank(&mut out);
54
70
 
55
- let closed_set = vo.get("closed_set").map(Json::as_bool).unwrap_or(false);
56
- let attrs = vo.get("attributes").map(Json::each).unwrap_or(&[]);
71
+ let closed_set = vo.get("closed_set").is_some_and(Json::as_bool);
72
+ let attrs = vo.get("attributes").map_or_default(Json::each);
57
73
  if closed_set && attrs.len() == 1 {
58
74
  puts_str(&mut out, &json_codec::emit_closed_set_codec(exemplar, vo));
59
75
  } else if closed_set {
60
- puts_str(&mut out, &json_codec::emit_closed_set_table_codec(exemplar, vo));
76
+ puts_str(
77
+ &mut out,
78
+ &json_codec::emit_closed_set_table_codec(exemplar, vo),
79
+ );
61
80
  } else {
62
- let name = crate::naming::rust_ident(vo.get("name").and_then(Json::as_str).unwrap_or(""));
63
- puts_str(&mut out, &json_codec::emit_to_json_flat(exemplar, &name, attrs, &value_objects_by_name, false, &[], None));
81
+ let name =
82
+ crate::naming::rust_ident(vo.get("name").and_then(Json::as_str).unwrap_or(""));
83
+ puts_str(
84
+ &mut out,
85
+ &json_codec::emit_to_json_flat(exemplar, &name, attrs, false, &[], None),
86
+ );
64
87
  puts_blank(&mut out);
65
- puts_str(&mut out, &json_codec::emit_from_json_flat(exemplar, &name, attrs, &value_objects_by_name, None, None, false, false, None));
88
+ puts_str(
89
+ &mut out,
90
+ &json_codec::emit_from_json_flat(
91
+ exemplar,
92
+ &name,
93
+ attrs,
94
+ &value_objects_by_name,
95
+ None,
96
+ None,
97
+ false,
98
+ false,
99
+ None,
100
+ None,
101
+ ),
102
+ );
66
103
  }
67
104
  puts_blank(&mut out);
68
105
  }
69
106
 
70
- for entity in aggregate.get("entities").map(Json::each).unwrap_or(&[]) {
71
- puts_str(&mut out, &types::emit_entity(exemplar, entity, &value_objects_by_name));
107
+ for entity in aggregate.get("entities").map_or_default(Json::each) {
108
+ puts_str(
109
+ &mut out,
110
+ &types::emit_entity(exemplar, entity, &value_objects_by_name),
111
+ );
72
112
  puts_blank(&mut out);
73
113
 
74
- let entity_name = crate::naming::rust_ident(entity.get("name").and_then(Json::as_str).unwrap_or(""));
75
- let entity_attrs = entity.get("attributes").map(Json::each).unwrap_or(&[]);
114
+ let entity_name =
115
+ crate::naming::rust_ident(entity.get("name").and_then(Json::as_str).unwrap_or(""));
116
+ let entity_attrs = entity.get("attributes").map_or_default(Json::each);
76
117
  let extra = lifecycle_extra_field(entity);
77
- puts_str(&mut out, &json_codec::emit_to_json_flat(exemplar, &entity_name, entity_attrs, &value_objects_by_name, false, &extra, None));
118
+ puts_str(
119
+ &mut out,
120
+ &json_codec::emit_to_json_flat(
121
+ exemplar,
122
+ &entity_name,
123
+ entity_attrs,
124
+ false,
125
+ &extra,
126
+ None,
127
+ ),
128
+ );
78
129
  puts_blank(&mut out);
79
- puts_str(&mut out, &json_codec::emit_from_json_state(exemplar, &entity_name, entity_attrs, &value_objects_by_name, false, &extra, None));
130
+ puts_str(
131
+ &mut out,
132
+ &json_codec::emit_from_json_state(
133
+ exemplar,
134
+ &entity_name,
135
+ entity_attrs,
136
+ &value_objects_by_name,
137
+ false,
138
+ &extra,
139
+ None,
140
+ ),
141
+ );
80
142
  puts_blank(&mut out);
81
143
 
82
144
  if json_codec::extract_id_supported(entity) {
83
145
  puts_str(&mut out, &json_codec::emit_extract_id(exemplar, entity));
84
146
  puts_blank(&mut out);
85
- puts_str(&mut out, &json_codec::emit_extract_id_lenient(exemplar, entity));
147
+ puts_str(
148
+ &mut out,
149
+ &json_codec::emit_extract_id_lenient(exemplar, entity),
150
+ );
86
151
  puts_blank(&mut out);
87
152
  puts_str(&mut out, &json_codec::emit_extract_wants(exemplar, entity));
88
153
  puts_blank(&mut out);
@@ -91,19 +156,44 @@ pub fn aggregate_prelude(exemplar: &Exemplar, ir: &Json, aggregate: &Json, sourc
91
156
  }
92
157
  }
93
158
 
94
- puts_str(&mut out, &types::emit_record(exemplar, aggregate, &value_objects_by_name));
159
+ puts_str(
160
+ &mut out,
161
+ &types::emit_record(exemplar, aggregate, &value_objects_by_name),
162
+ );
95
163
  puts_blank(&mut out);
96
164
 
97
- let record_name = crate::naming::rust_ident(aggregate.get("name").and_then(Json::as_str).unwrap_or(""));
98
- let record_attrs = aggregate.get("attributes").map(Json::each).unwrap_or(&[]);
165
+ let record_name =
166
+ crate::naming::rust_ident(aggregate.get("name").and_then(Json::as_str).unwrap_or(""));
167
+ let record_attrs = aggregate.get("attributes").map_or_default(Json::each);
99
168
  let extra = lifecycle_extra_field(aggregate);
100
- puts_str(&mut out, &json_codec::emit_to_json_flat(exemplar, &record_name, record_attrs, &value_objects_by_name, true, &extra, Some(aggregate)));
169
+ puts_str(
170
+ &mut out,
171
+ &json_codec::emit_to_json_flat(
172
+ exemplar,
173
+ &record_name,
174
+ record_attrs,
175
+ true,
176
+ &extra,
177
+ Some(aggregate),
178
+ ),
179
+ );
101
180
  puts_blank(&mut out);
102
- puts_str(&mut out, &json_codec::emit_from_json_state(exemplar, &record_name, record_attrs, &value_objects_by_name, true, &extra, Some(aggregate)));
181
+ puts_str(
182
+ &mut out,
183
+ &json_codec::emit_from_json_state(
184
+ exemplar,
185
+ &record_name,
186
+ record_attrs,
187
+ &value_objects_by_name,
188
+ true,
189
+ &extra,
190
+ Some(aggregate),
191
+ ),
192
+ );
103
193
  puts_blank(&mut out);
104
194
  puts_str(
105
195
  &mut out,
106
- &format!("impl crate::kernel::ToJson for {record_name} {{\n fn to_json(&self) -> crate::kernel::Json {{\n {record_name}::to_json(self)\n }}\n}}\n"),
196
+ &format!("impl crate::kernel::ToJson for {record_name} {{\n fn to_json(&self) -> crate::kernel::Json {{\n Self::to_json(self)\n }}\n}}\n"),
107
197
  );
108
198
  puts_blank(&mut out);
109
199
 
@@ -0,0 +1,335 @@
1
+ //! The bare answer shape, generated: a port of Ruby's `Value.materialize_unwrapped`, emitted as
2
+ //! type-directed Rust over `crate::kernel::Json`.
3
+ //!
4
+ //! A single-attribute value object answers as its sole field's value, a multi-field one as an
5
+ //! object of its fields, each unwrapped the same way; a list maps item-wise and an entity's
6
+ //! fields are unwrapped. Stored state stays wrapped: this is only what a caller is answered with
7
+ //! (`group_by` rows, and the kernel's `{"present": answer}` input).
8
+
9
+ use crate::json::Json;
10
+ use crate::registry::AggregateEntry;
11
+ use std::collections::HashMap;
12
+
13
+ /// Emits an expression unwrapping the `Json` named `expr` as a `type_name` (a `list` of them
14
+ /// when `list`), resolving value objects in `value_objects_by_name` and entities in `aggregate`.
15
+ pub fn unwrap_json_expr(
16
+ expr: &str,
17
+ type_name: &str,
18
+ list: bool,
19
+ aggregate: &Json,
20
+ value_objects_by_name: &HashMap<String, &Json>,
21
+ ) -> String {
22
+ Unwrapper {
23
+ aggregate,
24
+ value_objects_by_name,
25
+ }
26
+ .expr(expr, type_name, list, &mut Vec::new())
27
+ }
28
+
29
+ /// Emits `pub fn present(state) -> Json` for one aggregate's module: its state (or one of its
30
+ /// events' payloads) with every field it declares unwrapped, other keys passed through.
31
+ ///
32
+ /// The fields are the aggregate's attributes and projected fields, then every aggregate and
33
+ /// entity command argument not already named, since an event payload carries a command's
34
+ /// arguments. A name declared with more than one type unwraps by whichever type's field names
35
+ /// the value's own keys fit, first declared first.
36
+ pub fn emit_aggregate_presenter(
37
+ aggregate: &Json,
38
+ value_objects_by_name: &HashMap<String, &Json>,
39
+ ) -> String {
40
+ let unwrapper = Unwrapper {
41
+ aggregate,
42
+ value_objects_by_name,
43
+ };
44
+ let arms: Vec<String> = presented_fields(aggregate)
45
+ .iter()
46
+ .map(|(name, candidates)| format!("{name:?} => {},", unwrapper.candidates("v", candidates)))
47
+ .collect();
48
+ if arms.is_empty() {
49
+ return format!("{PRESENT_DOC}pub fn present(state: crate::kernel::Json) -> crate::kernel::Json {{\n state\n}}\n");
50
+ }
51
+ let arms = arms.join(" ");
52
+ format!(
53
+ "{PRESENT_DOC}pub fn present(state: crate::kernel::Json) -> crate::kernel::Json {{\n match state {{ crate::kernel::Json::Object(fields) => crate::kernel::Json::Object(fields.into_iter().map(|(k, v)| {{ let new_v = match k.as_str() {{ {arms} _ => v }}; (k, new_v) }}).collect()), other => other }}\n}}\n"
54
+ )
55
+ }
56
+
57
+ const PRESENT_DOC: &str = "/// This aggregate's state, or one of its events' payloads, as a caller is answered with\n/// it: each single-field value object as its bare field (the shape a command takes it in).\n";
58
+
59
+ /// Emits the merged target's `present(aggregate, state)`: one arm per generated aggregate across
60
+ /// every chapter, keyed by its qualified `Domain::Aggregate` name; any other name passes through.
61
+ pub fn emit_present_router(aggregates: &[AggregateEntry]) -> String {
62
+ let arms: Vec<String> = aggregates
63
+ .iter()
64
+ .map(|a| {
65
+ format!(
66
+ " {} => crate::generated::{}::{}::present(state),",
67
+ crate::naming::ruby_inspect_string(&format!("{}::{}", a.domain_name, a.name)),
68
+ a.chapter_mod,
69
+ a.module_name
70
+ )
71
+ })
72
+ .collect();
73
+ format!(
74
+ "/// One `\"Domain::Aggregate\"` state as a caller is answered with it (the kernel's\n/// `{{\"present\": answer}}` input); stored state stays wrapped. An unknown name passes through.\npub fn present(aggregate: &str, state: crate::kernel::Json) -> crate::kernel::Json {{\n match aggregate {{\n{}\n _ => state,\n }}\n}}\n",
75
+ arms.join("\n")
76
+ )
77
+ }
78
+
79
+ // Each presented name with its distinct declared (type, list) pairs, in declaration order.
80
+ fn presented_fields(aggregate: &Json) -> Vec<(String, Vec<(String, bool)>)> {
81
+ let mut attributes: Vec<Json> = aggregate
82
+ .get("attributes")
83
+ .map_or_default(Json::each)
84
+ .to_vec();
85
+ attributes.extend(crate::types::projected_field_pseudo_attributes(aggregate));
86
+ let commands = aggregate
87
+ .get("commands")
88
+ .map_or_default(Json::each)
89
+ .iter()
90
+ .chain(
91
+ aggregate
92
+ .get("entities")
93
+ .map_or_default(Json::each)
94
+ .iter()
95
+ .flat_map(|e| e.get("commands").map_or_default(Json::each)),
96
+ );
97
+ for command in commands {
98
+ attributes.extend(
99
+ command
100
+ .get("attributes")
101
+ .map_or_default(Json::each)
102
+ .iter()
103
+ .cloned(),
104
+ );
105
+ }
106
+
107
+ let mut fields: Vec<(String, Vec<(String, bool)>)> = Vec::new();
108
+ for attribute in &attributes {
109
+ let name = crate::attr::name(attribute).to_string();
110
+ let candidate = (
111
+ crate::attr::type_name(attribute).to_string(),
112
+ crate::attr::list(attribute),
113
+ );
114
+ match fields.iter_mut().find(|(n, _)| *n == name) {
115
+ Some((_, candidates)) if !candidates.contains(&candidate) => candidates.push(candidate),
116
+ Some(_) => {}
117
+ None => fields.push((name, vec![candidate])),
118
+ }
119
+ }
120
+ fields
121
+ }
122
+
123
+ struct Unwrapper<'a> {
124
+ aggregate: &'a Json,
125
+ value_objects_by_name: &'a HashMap<String, &'a Json>,
126
+ }
127
+
128
+ impl Unwrapper<'_> {
129
+ // The declared fields of a value object or entity type, and whether it is a value object.
130
+ fn fields_of(&self, type_name: &str) -> Option<(&[Json], bool)> {
131
+ if let Some(vo) = self.value_objects_by_name.get(type_name) {
132
+ return vo.get("attributes").map(|a| (Json::each(a), true));
133
+ }
134
+ self.aggregate
135
+ .get("entities")
136
+ .map_or_default(Json::each)
137
+ .iter()
138
+ .find(|e| e.get("name").map(Json::to_s).unwrap_or_default() == type_name)
139
+ .and_then(|e| e.get("attributes").map(|a| (Json::each(a), false)))
140
+ }
141
+
142
+ // `seen` holds the types being expanded, so a type that contains itself stops there and
143
+ // passes its nested value through rather than recursing without end.
144
+ fn expr(&self, expr: &str, type_name: &str, list: bool, seen: &mut Vec<String>) -> String {
145
+ if list {
146
+ let inner = self.expr("item", type_name, false, seen);
147
+ return format!("match {expr} {{ crate::kernel::Json::Array(items) => crate::kernel::Json::Array(items.into_iter().map(|item| {inner}).collect()), other => other }}");
148
+ }
149
+ let Some((fields_meta, is_value_object)) = self.fields_of(type_name) else {
150
+ return expr.to_string();
151
+ };
152
+ if seen.iter().any(|s| s == type_name) {
153
+ return expr.to_string();
154
+ }
155
+ seen.push(type_name.to_string());
156
+ let out = if is_value_object && fields_meta.len() == 1 {
157
+ let field = &fields_meta[0];
158
+ let unwrapped = self.expr(
159
+ "field_value",
160
+ crate::attr::type_name(field),
161
+ crate::attr::list(field),
162
+ seen,
163
+ );
164
+ let field_name = crate::attr::name(field);
165
+ format!("match {expr} {{ crate::kernel::Json::Object(fields) => fields.into_iter().find(|(k, _)| k == {field_name:?}).map_or(crate::kernel::Json::Null, |(_, field_value)| {unwrapped}), other => other }}")
166
+ } else {
167
+ let inner_arms: Vec<String> = fields_meta
168
+ .iter()
169
+ .map(|f| {
170
+ let unwrapped =
171
+ self.expr("v", crate::attr::type_name(f), crate::attr::list(f), seen);
172
+ format!("{:?} => {unwrapped},", crate::attr::name(f))
173
+ })
174
+ .collect();
175
+ let inner_arms = inner_arms.join(" ");
176
+ format!("match {expr} {{ crate::kernel::Json::Object(fields) => crate::kernel::Json::Object(fields.into_iter().map(|(k, v)| {{ let new_v = match k.as_str() {{ {inner_arms} _ => v }}; (k, new_v) }}).collect()), other => other }}")
177
+ };
178
+ seen.pop();
179
+ out
180
+ }
181
+
182
+ // One declared type unwraps unconditionally; several are told apart by the value's keys.
183
+ fn candidates(&self, expr: &str, candidates: &[(String, bool)]) -> String {
184
+ if let [(type_name, list)] = candidates {
185
+ return self.expr(expr, type_name, *list, &mut Vec::new());
186
+ }
187
+ let branches: Vec<String> = candidates
188
+ .iter()
189
+ .filter_map(|(type_name, list)| {
190
+ let guard = self.fits(&format!("&{expr}"), type_name, *list)?;
191
+ let unwrapped = self.expr(expr, type_name, *list, &mut Vec::new());
192
+ Some(format!("if {guard} {{ {unwrapped} }}"))
193
+ })
194
+ .collect();
195
+ if branches.is_empty() {
196
+ return expr.to_string();
197
+ }
198
+ format!("{} else {{ {expr} }}", branches.join(" else "))
199
+ }
200
+
201
+ // Whether the value at `expr` (a `&Json`) has the shape of `type_name`: an object whose keys
202
+ // are all its declared fields. `None` for a type with no fields to unwrap.
203
+ fn fits(&self, expr: &str, type_name: &str, list: bool) -> Option<String> {
204
+ if list {
205
+ let item = self.fits("item", type_name, false)?;
206
+ return Some(format!("matches!({expr}, crate::kernel::Json::Array(items) if items.iter().all(|item| {item}))"));
207
+ }
208
+ let (fields_meta, _) = self.fields_of(type_name)?;
209
+ let names: Vec<String> = fields_meta
210
+ .iter()
211
+ .map(|f| format!("{:?}", crate::attr::name(f)))
212
+ .collect();
213
+ Some(format!("matches!({expr}, crate::kernel::Json::Object(fields) if fields.iter().all(|(k, _)| matches!(k.as_str(), {})))", names.join(" | ")))
214
+ }
215
+ }
216
+
217
+ #[cfg(test)]
218
+ mod tests {
219
+ use super::*;
220
+
221
+ fn attribute(name: &str, type_name: &str, list: bool) -> String {
222
+ format!(r#"{{"name": "{name}", "type": "{type_name}", "list": {list}}}"#)
223
+ }
224
+
225
+ fn value_object(name: &str, attributes: &[String]) -> String {
226
+ format!(
227
+ r#"{{"name": "{name}", "attributes": [{}]}}"#,
228
+ attributes.join(", ")
229
+ )
230
+ }
231
+
232
+ // Pizzas' `Order`, cut down: a single-field name, a multi-field pizza holding two
233
+ // single-field value objects, a list of single-field tags, and two commands that both
234
+ // take an `amount` of a different single-field type.
235
+ fn order() -> Json {
236
+ let text = format!(
237
+ r#"{{"name": "Order",
238
+ "attributes": [{}, {}, {}],
239
+ "value_objects": [{}, {}, {}, {}, {}, {}],
240
+ "commands": [
241
+ {{"name": "CreatePizza", "attributes": [{}, {}]}},
242
+ {{"name": "AddTopping", "attributes": [{}]}},
243
+ {{"name": "Purchase", "attributes": [{}]}}
244
+ ]}}"#,
245
+ attribute("name", "PizzaName", false),
246
+ attribute("pizza", "Pizza", false),
247
+ attribute("tags", "Tag", true),
248
+ value_object("PizzaName", &[attribute("value", "String", false)]),
249
+ value_object("Price", &[attribute("cents", "Integer", false)]),
250
+ value_object("Size", &[attribute("value", "String", false)]),
251
+ value_object("Tag", &[attribute("value", "String", false)]),
252
+ value_object("ToppingAmount", &[attribute("value", "Integer", false)]),
253
+ value_object(
254
+ "Pizza",
255
+ &[
256
+ attribute("price_cents", "Price", false),
257
+ attribute("size", "Size", false)
258
+ ]
259
+ ),
260
+ attribute("name", "PizzaName", false),
261
+ attribute("pizza", "Pizza", false),
262
+ attribute("amount", "ToppingAmount", false),
263
+ attribute("amount", "Price", false),
264
+ );
265
+ Json::parse(&text).unwrap()
266
+ }
267
+
268
+ fn presenter() -> String {
269
+ let order = order();
270
+ let vos: Vec<&Json> = order
271
+ .get("value_objects")
272
+ .map_or_default(Json::each)
273
+ .iter()
274
+ .collect();
275
+ let by_name: HashMap<String, &Json> = vos
276
+ .iter()
277
+ .map(|vo| (vo.get("name").map(Json::to_s).unwrap_or_default(), *vo))
278
+ .collect();
279
+ emit_aggregate_presenter(&order, &by_name)
280
+ }
281
+
282
+ #[test]
283
+ fn a_single_field_attribute_presents_as_its_sole_field() {
284
+ assert!(presenter().contains(r#""name" => match v { crate::kernel::Json::Object(fields) => fields.into_iter().find(|(k, _)| k == "value")"#));
285
+ }
286
+
287
+ #[test]
288
+ fn a_multi_field_value_object_keeps_its_object_and_unwraps_each_field() {
289
+ let text = presenter();
290
+ assert!(text.contains(r#""pizza" => match v { crate::kernel::Json::Object(fields) => crate::kernel::Json::Object("#));
291
+ assert!(text.contains(r#""price_cents" => match v { crate::kernel::Json::Object(fields) => fields.into_iter().find(|(k, _)| k == "cents")"#));
292
+ assert!(text.contains(r#""size" => match v { crate::kernel::Json::Object(fields) => fields.into_iter().find(|(k, _)| k == "value")"#));
293
+ }
294
+
295
+ #[test]
296
+ fn a_list_of_single_field_value_objects_maps_item_wise() {
297
+ assert!(presenter().contains(r#""tags" => match v { crate::kernel::Json::Array(items) => crate::kernel::Json::Array(items.into_iter().map(|item| match item { crate::kernel::Json::Object(fields) => fields.into_iter().find(|(k, _)| k == "value")"#));
298
+ }
299
+
300
+ #[test]
301
+ fn a_name_declared_with_two_types_is_told_apart_by_its_keys() {
302
+ let text = presenter();
303
+ let arm = text.split(r#""amount" => "#).nth(1).unwrap();
304
+ assert!(arm.starts_with(r#"if matches!(&v, crate::kernel::Json::Object(fields) if fields.iter().all(|(k, _)| matches!(k.as_str(), "value")))"#));
305
+ assert!(arm.contains(r#"else if matches!(&v, crate::kernel::Json::Object(fields) if fields.iter().all(|(k, _)| matches!(k.as_str(), "cents")))"#));
306
+ }
307
+
308
+ #[test]
309
+ fn a_command_argument_already_named_by_an_attribute_adds_no_second_arm() {
310
+ assert_eq!(presenter().matches(r#""name" => "#).count(), 1);
311
+ }
312
+
313
+ #[test]
314
+ fn the_router_names_each_aggregate_qualified_and_passes_any_other_through() {
315
+ let entry = AggregateEntry {
316
+ name: "Order".to_string(),
317
+ module_name: "order".to_string(),
318
+ record: "Order".to_string(),
319
+ commands: vec![],
320
+ entity_commands: vec![],
321
+ nested_entity_commands: vec![],
322
+ ports: vec![],
323
+ chapter_mod: "pizzas".to_string(),
324
+ domain_name: "Pizzas".to_string(),
325
+ reference_specs: vec![],
326
+ identified_by: vec![],
327
+ entities: vec![],
328
+ };
329
+ let text = emit_present_router(&[entry]);
330
+ assert!(
331
+ text.contains(r#""Pizzas::Order" => crate::generated::pizzas::order::present(state),"#)
332
+ );
333
+ assert!(text.contains("_ => state,"));
334
+ }
335
+ }