hecks 3.14.0 → 4.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 (419) 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/lib/hecks/adapters/driven/prism.rb +0 -2
  9. data/lib/hecks/adapters/driven.rb +0 -2
  10. data/lib/hecks/adapters/driving/json.rb +1 -7
  11. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -2
  12. data/lib/hecks/bluebook/dsl/derivation_words.rb +58 -0
  13. data/lib/hecks/bluebook/dsl/translation_builder.rb +6 -37
  14. data/lib/hecks/bluebook/expression/ast_json.rb +0 -9
  15. data/lib/hecks/bluebook/hecksagon.rb +1 -1
  16. data/lib/hecks/bluebook/meta_validator/translation_judge/rules.rb +7 -10
  17. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +4 -8
  18. data/lib/hecks/bluebook/meta_validator.rb +0 -11
  19. data/lib/hecks/bluebook/translation.rb +18 -10
  20. data/lib/hecks/bluebook/translation_expression/evaluator.rb +63 -0
  21. data/lib/hecks/bluebook/translation_expression/parser.rb +127 -0
  22. data/lib/hecks/bluebook/translation_expression.rb +39 -0
  23. data/lib/hecks/bluebook/translation_plan.rb +70 -0
  24. data/lib/hecks/bluebook.rb +2 -0
  25. data/lib/hecks/chapters.rb +3 -3
  26. data/lib/hecks/composer/adapters/claude_composer.adapter +3 -0
  27. data/lib/hecks/composer/adapters/claude_composer.rb +83 -0
  28. data/lib/hecks/composer/adapters/fake_agent.adapter +3 -0
  29. data/lib/hecks/composer/adapters/fake_agent.rb +55 -0
  30. data/lib/hecks/composer/adapters/jev.adapter +3 -0
  31. data/lib/hecks/composer/adapters/jev.rb +66 -0
  32. data/lib/hecks/composer/agent.rb +113 -0
  33. data/lib/hecks/composer/assistant.rb +110 -0
  34. data/lib/hecks/composer/behavior_run.rb +53 -0
  35. data/lib/hecks/composer/bluebook/composer.bluebook +178 -0
  36. data/lib/hecks/composer/bluebook/composer.hecksagon +16 -0
  37. data/lib/hecks/composer/board.rb +65 -0
  38. data/lib/hecks/composer/board_edit/command_text.rb +39 -0
  39. data/lib/hecks/composer/board_edit/source.rb +84 -0
  40. data/lib/hecks/composer/board_edit.rb +144 -0
  41. data/lib/hecks/composer/check.rb +89 -0
  42. data/lib/hecks/composer/domains.rb +81 -0
  43. data/lib/hecks/composer/jobs.rb +134 -0
  44. data/lib/hecks/composer/layout.rb +37 -0
  45. data/lib/hecks/composer/live.rb +150 -0
  46. data/lib/hecks/composer/outline.rb +48 -0
  47. data/lib/hecks/composer/page.html +989 -0
  48. data/lib/hecks/composer/panels.rb +142 -0
  49. data/lib/hecks/composer/parse.rb +83 -0
  50. data/lib/hecks/composer/plain.rb +28 -0
  51. data/lib/hecks/composer/ports/composer_agent.port +7 -0
  52. data/lib/hecks/composer/session.rb +141 -0
  53. data/lib/hecks/composer/toolbox.rb +166 -0
  54. data/lib/hecks/composer/web.rb +156 -0
  55. data/lib/hecks/composer/workbench.rb +51 -0
  56. data/lib/hecks/composer/workspace.rb +81 -0
  57. data/lib/hecks/composer.rb +17 -0
  58. data/lib/hecks/corpus/adapters.rb +22 -0
  59. data/lib/hecks/corpus/rust.rb +1 -1
  60. data/lib/hecks/corpus/worlds.rb +19 -0
  61. data/lib/hecks/corpus.rb +5 -0
  62. data/lib/hecks/forms/app/commands.rb +2 -3
  63. data/lib/hecks/forms/app/queries.rb +2 -3
  64. data/lib/hecks/forms/app/records.rb +17 -5
  65. data/lib/hecks/forms/command_form_renderer.rb +42 -16
  66. data/lib/hecks/forms/field_controls.rb +2 -2
  67. data/lib/hecks/forms/field_renderer.rb +1 -1
  68. data/lib/hecks/forms/field_shape.rb +36 -17
  69. data/lib/hecks/forms/index_renderer.rb +2 -1
  70. data/lib/hecks/forms/page.rb +1 -1
  71. data/lib/hecks/forms/params.rb +0 -13
  72. data/lib/hecks/forms/query_form_renderer.rb +7 -6
  73. data/lib/hecks/forms/record_renderer.rb +11 -7
  74. data/lib/hecks/forms/rule_words.rb +100 -0
  75. data/lib/hecks/forms/value_object_shape.rb +2 -1
  76. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -3
  77. data/lib/hecks/fuzzing/isolated_boot/worlds.rb +3 -6
  78. data/lib/hecks/fuzzing/replay/filters.rb +2 -6
  79. data/lib/hecks/gate/stages.yml +25 -0
  80. data/lib/hecks/grammar/evolve/seeds.rb +0 -6
  81. data/lib/hecks/grammar/evolve.rb +0 -3
  82. data/lib/hecks/grammar/translation.bluebook +1 -1
  83. data/lib/hecks/hecks/adapters/codebase/composing.rb +56 -0
  84. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +3 -1
  85. data/lib/hecks/hecks/adapters/journal_store/changes.rb +0 -26
  86. data/lib/hecks/hecks/adapters/journal_store/examination.rb +2 -23
  87. data/lib/hecks/hecks/adapters/journal_store/readings.rb +3 -11
  88. data/lib/hecks/hecks/adapters/journal_store.rb +0 -11
  89. data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +4 -11
  90. data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +0 -9
  91. data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +0 -4
  92. data/lib/hecks/hecks/codebase.behaviors +6 -0
  93. data/lib/hecks/hecks/codebase.bluebook +21 -0
  94. data/lib/hecks/hecks/composer.behaviors +68 -0
  95. data/lib/hecks/hecks/context_map.hecksagon +1 -1
  96. data/lib/hecks/hecks/custodian.behaviors +0 -6
  97. data/lib/hecks/hecks/custodian.bluebook +3 -80
  98. data/lib/hecks/hecks/hecks.hecksagon +2 -2
  99. data/lib/hecks/hecks/hecks.world +1 -1
  100. data/lib/hecks/hecks/rehearsal.bluebook +7 -13
  101. data/lib/hecks/kernel_capabilities/templates.rb +4 -4
  102. data/lib/hecks/kernel_capabilities.rb +1 -1
  103. data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -3
  104. data/lib/hecks/language/translation/translation_aggregate.bluebook +21 -24
  105. data/lib/hecks/ports/access_control.rb +2 -2
  106. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -41
  107. data/lib/hecks/ports/persistence/plugins/era/lineage/coverage.rb +7 -7
  108. data/lib/hecks/ports/persistence/plugins/era/lineage/rewrite.rb +25 -2
  109. data/lib/hecks/ports/persistence/plugins/era/lineage/rules.rb +4 -4
  110. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +17 -25
  111. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/base_sql.rb +0 -12
  112. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/domain_rename.rb +1 -1
  113. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -22
  114. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +0 -1
  115. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +9 -3
  116. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +3 -29
  117. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/refusals.rb +2 -26
  118. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -33
  119. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -3
  120. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +2 -4
  121. data/lib/hecks/ports/persistence/plugins/era/translation/expression_sql.rb +52 -0
  122. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +23 -29
  123. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +1 -1
  124. data/lib/hecks/ports/persistence/plugins/era/translation.rb +0 -1
  125. data/lib/hecks/ports/persistence/plugins/era.rb +3 -6
  126. data/lib/hecks/ports.rb +0 -1
  127. data/lib/hecks/projection_files.rb +3 -1
  128. data/lib/hecks/projections/deploy/templates/lambda/nat_comment_oauth.tmpl +1 -1
  129. data/lib/hecks/projections/glossary/entries.rb +10 -4
  130. data/lib/hecks/projections/glossary/groups.rb +42 -0
  131. data/lib/hecks/projections/glossary/html/page.rb +7 -2
  132. data/lib/hecks/projections/glossary/html/parser.rb +1 -1
  133. data/lib/hecks/projections/glossary/html/sections.rb +31 -11
  134. data/lib/hecks/projections/glossary/html.rb +1 -2
  135. data/lib/hecks/projections/glossary/markdown.rb +31 -28
  136. data/lib/hecks/projections/glossary/mermaid.rb +3 -0
  137. data/lib/hecks/projections/glossary/page.css +143 -153
  138. data/lib/hecks/projections/glossary/page.js +63 -3
  139. data/lib/hecks/projections/glossary/sentences/actions.rb +88 -0
  140. data/lib/hecks/projections/glossary/sentences/paths.rb +77 -0
  141. data/lib/hecks/projections/glossary/sentences/queries.rb +90 -0
  142. data/lib/hecks/projections/glossary/sentences/value_objects.rb +32 -5
  143. data/lib/hecks/projections/glossary/sentences.rb +13 -3
  144. data/lib/hecks/projections/glossary/slugs.rb +4 -1
  145. data/lib/hecks/projections/glossary.rb +2 -1
  146. data/lib/hecks/projections/plain_english.rb +97 -0
  147. data/lib/hecks/projections/rust_vocabulary/arguments.rb +2 -2
  148. data/lib/hecks/projections/rust_vocabulary/enum_body.rb +20 -25
  149. data/lib/hecks/projections/rust_vocabulary/tests.rb +2 -2
  150. data/lib/hecks/projections/rust_vocabulary.rb +14 -6
  151. data/lib/hecks/projections/site/cms_editor/color.rb +0 -6
  152. data/lib/hecks/projections/statements/actions.rb +99 -0
  153. data/lib/hecks/projections/statements/relationships.rb +86 -0
  154. data/lib/hecks/projections/statements.rb +15 -24
  155. data/lib/hecks/projector/exporter/adapters.rb +112 -0
  156. data/lib/hecks/projector/exporter/translations.rb +18 -35
  157. data/lib/hecks/projector/exporter/worlds.rb +103 -0
  158. data/lib/hecks/projector/exporter.rb +5 -1
  159. data/lib/hecks/quality_control/adapters/agent.rb +6 -7
  160. data/lib/hecks/quality_control/adapters/agent_profile.rb +16 -114
  161. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +1 -1
  162. data/lib/hecks/runtime/loader/boot_steps.rb +0 -20
  163. data/lib/hecks/runtime/outbox.rb +2 -9
  164. data/lib/hecks/runtime/port_operation_interpreter.rb +1 -9
  165. data/lib/hecks/runtime/remote_dispatcher.rb +2 -2
  166. data/lib/hecks/runtime/value.rb +13 -0
  167. data/lib/hecks/rust_build/project_rust/target_ir.rb +23 -4
  168. data/lib/hecks/rust_build/wasm.rb +0 -12
  169. data/lib/hecks/rust_build/write_if_changed.rb +12 -12
  170. data/lib/hecks/rust_format.rb +77 -0
  171. data/lib/hecks/tools/fuzz_sweep/findings.rb +3 -9
  172. data/lib/hecks/tools/fuzz_sweep.rb +1 -3
  173. data/lib/hecks/tools/translation_audit/verdicts.rb +3 -28
  174. data/lib/hecks/tools/translation_audit.rb +8 -10
  175. data/lib/hecks/tools/translation_scaffold/reporting.rb +1 -1
  176. data/lib/hecks/version.rb +1 -1
  177. data/lib/hecks/vocabulary.rb +2 -1
  178. data/lib/hecks.rb +3 -0
  179. data/prebuilt/{a883a7e568667aa5ac904c82daff356bdf9e996252a36af6def4a4a43efb967e.marshal → 02c09c73b272c1fb5a9ae6d87bb5e5e529208e00f6b4b597dc2c0fd70a9c0964.marshal} +0 -0
  180. data/prebuilt/{1a6f596be124228be93eba1d86dbc9d2933190fa389b284331241cb9f27115a0.marshal → bb84a835cf6a15136bad5ca03aa5bf93a5b92f5f72444618d3ef2412b987fb32.marshal} +0 -0
  181. data/prebuilt/{verdicts-872993b1aa26d73fe48f28251374b8585fed145d02aab42be0f00de386b6ff80.json → verdicts-f1f10da14c8e8f01b4c9b7e3e2d74be8cd2bf4676364ed17fa16975d2a076d75.json} +1 -1
  182. data/rust/Cargo.toml +40 -0
  183. data/rust/build/Cargo.toml +36 -0
  184. data/rust/build/clippy.toml +4 -0
  185. data/rust/build/src/adapters_pass.rs +196 -0
  186. data/rust/build/src/binding_pass.rs +54 -13
  187. data/rust/build/src/build_artifact.rs +23 -5
  188. data/rust/build/src/cargo_sync.rs +69 -26
  189. data/rust/build/src/lineage_pass.rs +101 -87
  190. data/rust/build/src/main.rs +11 -2
  191. data/rust/build/src/pipeline.rs +80 -19
  192. data/rust/build/src/reserved_names.rs +6 -50
  193. data/rust/build/src/resolve.rs +82 -35
  194. data/rust/build/src/rustfmt.rs +100 -0
  195. data/rust/build/src/seams_pass.rs +85 -16
  196. data/rust/build/src/subprocess.rs +30 -6
  197. data/rust/build/src/tmp.rs +5 -2
  198. data/rust/build/src/translation_pass.rs +72 -22
  199. data/rust/build/src/world_pass.rs +109 -0
  200. data/rust/clippy.toml +4 -0
  201. data/rust/codegen/Cargo.toml +36 -0
  202. data/rust/codegen/clippy.toml +4 -0
  203. data/rust/codegen/src/asks.rs +40 -14
  204. data/rust/codegen/src/attr.rs +2 -2
  205. data/rust/codegen/src/bridging.rs +676 -117
  206. data/rust/codegen/src/commands.rs +979 -373
  207. data/rust/codegen/src/constraints.rs +88 -25
  208. data/rust/codegen/src/dependency_planning.rs +128 -34
  209. data/rust/codegen/src/domain_generator.rs +300 -170
  210. data/rust/codegen/src/exemplar.rs +87 -32
  211. data/rust/codegen/src/expr_emitter.rs +54 -6
  212. data/rust/codegen/src/fielded.rs +193 -41
  213. data/rust/codegen/src/hecks_naming.rs +124 -16
  214. data/rust/codegen/src/hecks_naming_tests.rs +81 -0
  215. data/rust/codegen/src/json.rs +80 -44
  216. data/rust/codegen/src/json_codec.rs +585 -149
  217. data/rust/codegen/src/literal.rs +104 -138
  218. data/rust/codegen/src/main.rs +145 -52
  219. data/rust/codegen/src/manifest.rs +76 -14
  220. data/rust/codegen/src/mutations.rs +1212 -239
  221. data/rust/codegen/src/naming.rs +111 -60
  222. data/rust/codegen/src/optional_pass.rs +90 -28
  223. data/rust/codegen/src/ports.rs +5 -4
  224. data/rust/codegen/src/prelude.rs +139 -49
  225. data/rust/codegen/src/queries.rs +601 -226
  226. data/rust/codegen/src/reactions.rs +268 -74
  227. data/rust/codegen/src/read_models.rs +606 -205
  228. data/rust/codegen/src/reference_specs.rs +16 -3
  229. data/rust/codegen/src/registry.rs +128 -79
  230. data/rust/codegen/src/reserved_names.rs +6 -50
  231. data/rust/codegen/src/shared.rs +71 -22
  232. data/rust/codegen/src/sidecars.rs +15 -5
  233. data/rust/codegen/src/skip_reason.rs +8 -2
  234. data/rust/codegen/src/types.rs +306 -108
  235. data/rust/host/Cargo.lock +1 -0
  236. data/rust/host/Cargo.toml +40 -4
  237. data/rust/host/HECKS_RELEASE +1 -1
  238. data/rust/host/clippy.toml +4 -0
  239. data/rust/host/src/api.rs +698 -218
  240. data/rust/host/src/auth.rs +491 -214
  241. data/rust/host/src/b64.rs +91 -0
  242. data/rust/host/src/bin/expr_harness.rs +6 -2
  243. data/rust/host/src/bin/lineage_harness.rs +47 -11
  244. data/rust/host/src/bin/mint_harness.rs +70 -37
  245. data/rust/host/src/bin/validate_harness.rs +16 -8
  246. data/rust/host/src/boot.rs +144 -68
  247. data/rust/host/src/boundary_fuzz/api.rs +130 -32
  248. data/rust/host/src/boundary_fuzz/auth.rs +108 -23
  249. data/rust/host/src/boundary_fuzz/expr_json.rs +85 -11
  250. data/rust/host/src/boundary_fuzz/ir.rs +164 -12
  251. data/rust/host/src/boundary_fuzz/server.rs +283 -36
  252. data/rust/host/src/boundary_fuzz/web.rs +56 -8
  253. data/rust/host/src/checkout.rs +78 -32
  254. data/rust/host/src/clock.rs +8 -0
  255. data/rust/host/src/commerce_ir.rs +333 -95
  256. data/rust/host/src/crypto.rs +33 -0
  257. data/rust/host/src/dispatch.rs +990 -331
  258. data/rust/host/src/expr_json.rs +1021 -273
  259. data/rust/host/src/extension.rs +24 -4
  260. data/rust/host/src/field_hints.rs +11 -7
  261. data/rust/host/src/fuzz_support.rs +85 -13
  262. data/rust/host/src/host.rs +100 -0
  263. data/rust/host/src/http.rs +22 -0
  264. data/rust/host/src/ir.rs +326 -16
  265. data/rust/host/src/journal.rs +277 -176
  266. data/rust/host/src/lambda_client.rs +219 -53
  267. data/rust/host/src/lib.rs +16 -4
  268. data/rust/host/src/log.rs +62 -11
  269. data/rust/host/src/mint.rs +1153 -326
  270. data/rust/host/src/needs.rs +185 -44
  271. data/rust/host/src/payments/keystore.rs +197 -45
  272. data/rust/host/src/payments/roles.rs +39 -7
  273. data/rust/host/src/payments/tests.rs +1427 -282
  274. data/rust/host/src/payments.rs +391 -112
  275. data/rust/host/src/platform_signin/attack_tests.rs +5 -5
  276. data/rust/host/src/platform_signin/config.rs +11 -11
  277. data/rust/host/src/platform_signin/cookie.rs +2 -2
  278. data/rust/host/src/platform_signin/extension.rs +2 -1
  279. data/rust/host/src/platform_signin/handler.rs +9 -8
  280. data/rust/host/src/platform_signin/jti.rs +4 -2
  281. data/rust/host/src/platform_signin/jwks.rs +2 -1
  282. data/rust/host/src/platform_signin/jws.rs +2 -37
  283. data/rust/host/src/platform_signin/known_answer_tests.rs +5 -1
  284. data/rust/host/src/platform_signin/mode_tests.rs +5 -3
  285. data/rust/host/src/platform_signin/pages.rs +20 -20
  286. data/rust/host/src/platform_signin/session.rs +8 -1
  287. data/rust/host/src/platform_signin/settings.rs +6 -6
  288. data/rust/host/src/platform_signin/strict_json.rs +12 -9
  289. data/rust/host/src/platform_signin/verify.rs +41 -41
  290. data/rust/host/src/platform_signin/wiring_support.rs +13 -18
  291. data/rust/host/src/presentation.rs +207 -63
  292. data/rust/host/src/presentation_write.rs +502 -187
  293. data/rust/host/src/query_step.rs +69 -18
  294. data/rust/host/src/rate_limit.rs +541 -142
  295. data/rust/host/src/reference_transform.rs +77 -25
  296. data/rust/host/src/reference_validate.rs +443 -104
  297. data/rust/host/src/resend.rs +190 -55
  298. data/rust/host/src/secrets.rs +116 -21
  299. data/rust/host/src/server.rs +483 -104
  300. data/rust/host/src/storage_shape.rs +88 -21
  301. data/rust/host/src/test_pg.rs +17 -7
  302. data/rust/host/src/ui_schema.rs +495 -207
  303. data/rust/host/src/wasm_runner.rs +21 -6
  304. data/rust/host/src/web/commerce.rs +101 -23
  305. data/rust/host/src/web/newsletter/cooldown.rs +10 -3
  306. data/rust/host/src/web/newsletter/template.rs +26 -7
  307. data/rust/host/src/web/newsletter/tests.rs +112 -23
  308. data/rust/host/src/web/newsletter.rs +209 -59
  309. data/rust/host/src/web/newsletter_send.rs +251 -79
  310. data/rust/host/src/web/registration_receipt/tests.rs +16 -4
  311. data/rust/host/src/web/registration_receipt.rs +100 -24
  312. data/rust/host/src/web/registrations.rs +1384 -332
  313. data/rust/host/src/web/resend_webhook.rs +138 -80
  314. data/rust/host/src/web.rs +2416 -508
  315. data/rust/lsp/Cargo.toml +36 -0
  316. data/rust/lsp/clippy.toml +4 -0
  317. data/rust/lsp/src/json.rs +29 -22
  318. data/rust/lsp/src/main.rs +81 -31
  319. data/rust/lsp/src/outline.rs +23 -7
  320. data/rust/lsp/src/rpc.rs +17 -4
  321. data/rust/parser/Cargo.toml +36 -0
  322. data/rust/parser/clippy.toml +4 -0
  323. data/rust/parser/src/build/closed_sets.rs +6 -1
  324. data/rust/parser/src/build/identity.rs +4 -3
  325. data/rust/parser/src/build/mod.rs +1 -1
  326. data/rust/parser/src/build/named_pattern.rs +4 -1
  327. data/rust/parser/src/build/naming.rs +2 -80
  328. data/rust/parser/src/build/pattern_subset.rs +6 -8
  329. data/rust/parser/src/build/query_derive.rs +3 -1
  330. data/rust/parser/src/build/query_inference.rs +14 -5
  331. data/rust/parser/src/build/read_model.rs +4 -1
  332. data/rust/parser/src/build/references.rs +4 -3
  333. data/rust/parser/src/canonical.rs +34 -15
  334. data/rust/parser/src/emit.rs +187 -105
  335. data/rust/parser/src/expr/ast_json.rs +134 -42
  336. data/rust/parser/src/expr/evaluator.rs +27 -9
  337. data/rust/parser/src/expr/mod.rs +54 -13
  338. data/rust/parser/src/expr/resolver.rs +299 -92
  339. data/rust/parser/src/ir.rs +35 -3
  340. data/rust/parser/src/keywords.rs +7168 -516
  341. data/rust/parser/src/lex.rs +11 -25
  342. data/rust/parser/src/lib.rs +1 -0
  343. data/rust/parser/src/main.rs +65 -6
  344. data/rust/parser/src/parse/adapter.rs +157 -0
  345. data/rust/parser/src/parse/aggregate.rs +19 -10
  346. data/rust/parser/src/parse/chapter.rs +16 -17
  347. data/rust/parser/src/parse/command.rs +35 -16
  348. data/rust/parser/src/parse/domain_port.rs +4 -7
  349. data/rust/parser/src/parse/entity.rs +21 -11
  350. data/rust/parser/src/parse/hecksagon.rs +10 -4
  351. data/rust/parser/src/parse/lifecycle.rs +9 -5
  352. data/rust/parser/src/parse/mod.rs +22 -91
  353. data/rust/parser/src/parse/needs.rs +21 -12
  354. data/rust/parser/src/parse/policy.rs +2 -2
  355. data/rust/parser/src/parse/process_manager.rs +9 -11
  356. data/rust/parser/src/parse/query.rs +8 -4
  357. data/rust/parser/src/parse/read_model.rs +57 -16
  358. data/rust/parser/src/parse/value_object.rs +7 -2
  359. data/rust/parser/src/parse/world.rs +322 -0
  360. data/rust/parser/src/ruby_value.rs +7 -6
  361. data/rust/src/exemplar/json.rs +4 -4
  362. data/rust/src/exemplar/mutations.rs +18 -0
  363. data/rust/src/exemplar/read_models.rs +1 -10
  364. data/rust/src/kernel/attribute_shapes/composite.rs +54 -14
  365. data/rust/src/kernel/attribute_shapes/list.rs +2 -2
  366. data/rust/src/kernel/attribute_shapes/mod.rs +8 -8
  367. data/rust/src/kernel/attribute_shapes/scalar.rs +2 -2
  368. data/rust/src/kernel/cli.rs +229 -112
  369. data/rust/src/kernel/dispatch.rs +323 -115
  370. data/rust/src/kernel/dispatch_guard_tests.rs +327 -65
  371. data/rust/src/kernel/expr.rs +203 -86
  372. data/rust/src/kernel/expression_operators/arithmetic.rs +138 -25
  373. data/rust/src/kernel/expression_operators/comparison.rs +87 -2
  374. data/rust/src/kernel/expression_operators/enumeration.rs +106 -22
  375. data/rust/src/kernel/expression_operators/logical.rs +9 -3
  376. data/rust/src/kernel/expression_operators/membership.rs +3 -1
  377. data/rust/src/kernel/expression_operators/mod.rs +34 -21
  378. data/rust/src/kernel/expression_operators/pattern_match.rs +46 -11
  379. data/rust/src/kernel/expression_operators/positional.rs +84 -30
  380. data/rust/src/kernel/expression_operators/presence.rs +52 -13
  381. data/rust/src/kernel/expression_operators/sign_test.rs +3 -1
  382. data/rust/src/kernel/expression_operators/sized.rs +24 -8
  383. data/rust/src/kernel/expression_operators/text.rs +120 -27
  384. data/rust/src/kernel/expression_operators/to_string.rs +10 -4
  385. data/rust/src/kernel/json.rs +230 -148
  386. data/rust/src/kernel/mod.rs +46 -33
  387. data/rust/src/kernel/named_query.rs +63 -15
  388. data/rust/src/kernel/needs.rs +112 -25
  389. data/rust/src/kernel/orchestrate.rs +980 -581
  390. data/rust/src/kernel/orchestrate_guard_tests.rs +538 -140
  391. data/rust/src/kernel/pattern.rs +107 -34
  392. data/rust/src/kernel/query_comparators.rs +118 -41
  393. data/rust/src/kernel/query_ordering.rs +13 -5
  394. data/rust/src/kernel/read_model.rs +479 -181
  395. data/rust/src/kernel/reference_lookup.rs +122 -26
  396. data/rust/src/kernel/repository.rs +293 -66
  397. data/rust/src/kernel/routing.rs +58 -42
  398. data/rust/src/kernel/ruby_float.rs +139 -0
  399. data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +57 -57
  400. data/rust/src/kernel/vocab/entity_dispatch_order.rs +54 -54
  401. data/rust/src/kernel/vocab/field_hint.rs +19 -25
  402. data/rust/src/kernel/vocab/mod.rs +6 -6
  403. data/rust/src/kernel/vocab/query_comparator.rs +23 -24
  404. data/rust/src/kernel/vocab/refusal_template.rs +501 -297
  405. data/rust/src/lib.rs +50 -0
  406. data/rust/src/main.rs +4 -1
  407. data/rust/web/Cargo.toml +36 -0
  408. data/rust/web/clippy.toml +4 -0
  409. metadata +82 -15
  410. data/examples/pizzas/bluebook/hecksagon/mock_stripe_payment_adapter.rb +0 -36
  411. data/lib/hecks/adapters/driven/google_authentication.adapter +0 -3
  412. data/lib/hecks/adapters/driven/google_authentication.rb +0 -99
  413. data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +0 -3
  414. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +0 -32
  415. data/lib/hecks/ports/authentication.port +0 -6
  416. data/lib/hecks/ports/authentication.rb +0 -67
  417. data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +0 -255
  418. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +0 -22
  419. data/rust/host/src/approval.rs +0 -652
@@ -10,16 +10,24 @@ use std::collections::HashMap;
10
10
 
11
11
  /// The aggregate's invariant set, generated once per aggregate file and passed to every dispatch.
12
12
  pub fn invariants_fn_name(aggregate: &Json) -> String {
13
- format!("{}_invariants", naming::rust_ident_field(aggregate.get("name").and_then(Json::as_str).unwrap_or("")).to_lowercase())
13
+ format!(
14
+ "{}_invariants",
15
+ naming::rust_ident_field(aggregate.get("name").and_then(Json::as_str).unwrap_or(""))
16
+ .to_lowercase()
17
+ )
14
18
  }
15
19
 
16
20
  pub fn emit_invariants_fn(aggregate: &Json) -> String {
17
- let aggregate_vec = invariant_specs_vec(aggregate.get("invariants").map(Json::each).unwrap_or(&[]), 8);
21
+ let aggregate_vec =
22
+ invariant_specs_vec(aggregate.get("invariants").map_or_default(Json::each), 8);
18
23
  let entities_vec = entity_invariants_vec(aggregate, 8);
19
24
  // `Expr` is imported only when an invariant literal was emitted; otherwise it is unused.
20
25
  let needs_expr = aggregate_vec.contains("Expr::") || entities_vec.contains("Expr::");
21
26
  [
22
- Some(format!("fn {}() -> crate::kernel::InvariantSet {{", invariants_fn_name(aggregate))),
27
+ Some(format!(
28
+ "fn {}() -> crate::kernel::InvariantSet {{",
29
+ invariants_fn_name(aggregate)
30
+ )),
23
31
  needs_expr.then(|| " use crate::kernel::Expr;".to_string()),
24
32
  Some(" crate::kernel::InvariantSet {".to_string()),
25
33
  Some(format!(" aggregate: {aggregate_vec},")),
@@ -33,6 +41,10 @@ pub fn emit_invariants_fn(aggregate: &Json) -> String {
33
41
  .join("\n")
34
42
  }
35
43
 
44
+ #[allow(
45
+ clippy::panic,
46
+ reason = "a drifted exemplar or invalid IR aborts generation on purpose"
47
+ )]
36
48
  fn invariant_specs_vec(rules: &[Json], indent: usize) -> String {
37
49
  if rules.is_empty() {
38
50
  return "vec![]".to_string();
@@ -42,7 +54,9 @@ fn invariant_specs_vec(rules: &[Json], indent: usize) -> String {
42
54
  .iter()
43
55
  .map(|rule| {
44
56
  let description = rule.get("description").and_then(Json::as_str).unwrap_or("");
45
- let ast = rule.get("ast").unwrap_or_else(|| panic!("invariant row has no ast: {rule:?}"));
57
+ let ast = rule
58
+ .get("ast")
59
+ .unwrap_or_else(|| panic!("invariant row has no ast: {rule:?}"));
46
60
  format!(
47
61
  "{pad}crate::kernel::InvariantSpec {{ description: {}, expr: {} }},",
48
62
  naming::ruby_inspect_string(description),
@@ -54,18 +68,23 @@ fn invariant_specs_vec(rules: &[Json], indent: usize) -> String {
54
68
  }
55
69
 
56
70
  fn entity_invariants_vec(owner: &Json, indent: usize) -> String {
57
- let entities = owner.get("entities").map(Json::each).unwrap_or(&[]);
58
- let attributes = owner.get("attributes").map(Json::each).unwrap_or(&[]);
71
+ let entities = owner.get("entities").map_or_default(Json::each);
72
+ let attributes = owner.get("attributes").map_or_default(Json::each);
59
73
  let pieces: Vec<(&Json, String)> = entities
60
74
  .iter()
61
75
  .filter_map(|entity| {
62
- if entity.get("invariants").map(Json::each).unwrap_or(&[]).is_empty() {
76
+ if entity
77
+ .get("invariants")
78
+ .map_or_default(Json::each)
79
+ .is_empty()
80
+ {
63
81
  return None;
64
82
  }
65
83
  let name = entity.get("name").map(Json::to_s).unwrap_or_default();
66
- let list = attributes
67
- .iter()
68
- .find(|a| a.get("list").map(Json::as_bool).unwrap_or(false) && a.get("type").map(Json::to_s).unwrap_or_default() == name)?;
84
+ let list = attributes.iter().find(|a| {
85
+ a.get("list").is_some_and(Json::as_bool)
86
+ && a.get("type").map(Json::to_s).unwrap_or_default() == name
87
+ })?;
69
88
  Some((entity, list.get("name").map(Json::to_s).unwrap_or_default()))
70
89
  })
71
90
  .collect();
@@ -80,7 +99,7 @@ fn entity_invariants_vec(owner: &Json, indent: usize) -> String {
80
99
  "{pad}crate::kernel::EntityInvariants {{ name: {}, list_field: {}, specs: {}, nested: {} }},",
81
100
  naming::ruby_inspect_string(&entity.get("name").map(Json::to_s).unwrap_or_default()),
82
101
  naming::ruby_inspect_string(list_field),
83
- invariant_specs_vec(entity.get("invariants").map(Json::each).unwrap_or(&[]), indent + 4),
102
+ invariant_specs_vec(entity.get("invariants").map_or_default(Json::each), indent + 4),
84
103
  entity_invariants_vec(entity, indent + 4)
85
104
  )
86
105
  })
@@ -89,14 +108,158 @@ fn entity_invariants_vec(owner: &Json, indent: usize) -> String {
89
108
  }
90
109
 
91
110
  /// Extra parameters of every generated `dispatch_*`/`dispatch_entity_*`, resolved by the router.
92
- fn deref_params() -> [&'static str; 2] {
93
- ["owner_deref: Vec<(&'static str, crate::kernel::DerefNode)>", "command_deref: Vec<(&'static str, crate::kernel::DerefNode)>"]
111
+ const fn deref_params() -> [&'static str; 2] {
112
+ [
113
+ "owner_deref: Vec<(&'static str, crate::kernel::DerefNode)>",
114
+ "command_deref: Vec<(&'static str, crate::kernel::DerefNode)>",
115
+ ]
94
116
  }
95
117
 
96
118
  /// Write-side tenant boundary result, threaded through aggregate commands only.
97
119
  /// The router always passes `Ok(())`: no domain generated from `ir.json` declares a boundary.
98
120
  const TENANT_BOUNDARY_PARAM: &str = "tenant_boundary_check: Result<(), crate::kernel::Refusal>";
99
121
 
122
+ /// A node's `name`, or "" when it has none.
123
+ fn name_of(node: &Json) -> &str {
124
+ node.get("name").and_then(Json::as_str).unwrap_or("")
125
+ }
126
+
127
+ /// The paths a node is identified by, joined the way a dispatch fn names them.
128
+ fn identity_reading(node: &Json) -> String {
129
+ node.get("identified_by")
130
+ .map_or_default(Json::each)
131
+ .iter()
132
+ .map(Json::to_s)
133
+ .collect::<Vec<_>>()
134
+ .join(", ")
135
+ }
136
+
137
+ /// The first segment of each path a node is identified by.
138
+ fn identity_heads(node: &Json) -> Vec<String> {
139
+ node.get("identified_by")
140
+ .map_or_default(Json::each)
141
+ .iter()
142
+ .map(|p| p.to_s().split('.').next().unwrap_or("").to_string())
143
+ .collect()
144
+ }
145
+
146
+ /// The Rust field of `owner` that holds `held` as a list.
147
+ #[allow(
148
+ clippy::panic,
149
+ reason = "a drifted exemplar or invalid IR aborts generation on purpose"
150
+ )]
151
+ fn list_field_of(owner: &Json, held: &Json) -> String {
152
+ let held_name = name_of(held);
153
+ let list_attr = owner
154
+ .get("attributes")
155
+ .map_or_default(Json::each)
156
+ .iter()
157
+ .find(|a| crate::attr::list(a) && crate::attr::type_name(a) == held_name)
158
+ .unwrap_or_else(|| panic!("{held_name}: no list attribute on {} holds it — unsupported_attribute_types should have caught this", name_of(owner)));
159
+ naming::rust_ident_field(crate::attr::name(list_attr))
160
+ }
161
+
162
+ /// The `pub struct <struct_name> { .. }` text of a command's argument struct.
163
+ fn args_struct_text(exemplar: &Exemplar, struct_name: &str, attrs: &[Json]) -> String {
164
+ let fields = attrs.iter().map(|attr| {
165
+ let mut ty = naming::rust_type(crate::attr::type_name(attr), crate::attr::list(attr));
166
+ if crate::attr::optional(attr) {
167
+ ty = format!("Option<{ty}>");
168
+ }
169
+ format!(
170
+ " {}",
171
+ exemplar.render(
172
+ "struct_field",
173
+ &[
174
+ ("TmplFieldType", ty),
175
+ (
176
+ "tmpl_field",
177
+ naming::rust_ident_field(crate::attr::name(attr))
178
+ ),
179
+ ]
180
+ )
181
+ )
182
+ });
183
+ std::iter::once(format!("pub struct {struct_name} {{"))
184
+ .chain(fields)
185
+ .chain(std::iter::once("}".to_string()))
186
+ .collect::<Vec<_>>()
187
+ .join("\n")
188
+ }
189
+
190
+ /// One `GivenSpec` line per `given` of `command`, each prefixed with `indent`.
191
+ #[allow(
192
+ clippy::panic,
193
+ reason = "a drifted exemplar or invalid IR aborts generation on purpose"
194
+ )]
195
+ fn given_spec_lines(command: &Json, indent: &str) -> Vec<String> {
196
+ command
197
+ .get("givens")
198
+ .map_or_default(Json::each)
199
+ .iter()
200
+ .map(|g| {
201
+ let description = g.get("description").and_then(Json::as_str).unwrap_or("");
202
+ let ast = g.get("ast").unwrap_or_else(|| panic!("given row has no ast: {g:?}"));
203
+ format!("{indent}crate::kernel::GivenSpec {{ description: {}, expr: {}, corrects_event: None }},", naming::ruby_inspect_string(description), crate::expr_emitter::emit_ast(ast))
204
+ })
205
+ .collect()
206
+ }
207
+
208
+ /// One `EnsuresSpec` line per `ensures` of `command`, each prefixed with `indent`.
209
+ #[allow(
210
+ clippy::panic,
211
+ reason = "a drifted exemplar or invalid IR aborts generation on purpose"
212
+ )]
213
+ fn ensures_spec_lines(command: &Json, indent: &str) -> Vec<String> {
214
+ command
215
+ .get("ensures")
216
+ .map_or_default(Json::each)
217
+ .iter()
218
+ .map(|e| {
219
+ let description = e.get("description").and_then(Json::as_str).unwrap_or("");
220
+ let ast = e
221
+ .get("ast")
222
+ .unwrap_or_else(|| panic!("ensures row has no ast: {e:?}"));
223
+ format!(
224
+ "{indent}crate::kernel::EnsuresSpec {{ description: {}, expr: {} }},",
225
+ naming::ruby_inspect_string(description),
226
+ crate::expr_emitter::emit_ast(ast)
227
+ )
228
+ })
229
+ .collect()
230
+ }
231
+
232
+ /// The event names `command` emits, as a comma-separated list of Rust string literals.
233
+ fn emits_expr_of(command: &Json) -> String {
234
+ command
235
+ .get("emits")
236
+ .map_or_default(Json::each)
237
+ .iter()
238
+ .map(|e| naming::ruby_inspect_string(&e.to_s()))
239
+ .collect::<Vec<_>>()
240
+ .join(", ")
241
+ }
242
+
243
+ /// Ends a command's mutation lines: the lifecycle transition's new state, or `let _ = record;`
244
+ /// when there is nothing to write.
245
+ fn close_mutation_lines(
246
+ mut lines: Vec<String>,
247
+ transition: Option<&mutations::Transition>,
248
+ indent: &str,
249
+ ) -> Vec<String> {
250
+ if let Some(t) = transition.filter(|t| !t.to_state.is_empty()) {
251
+ lines.push(format!(
252
+ "{indent}record.{} = {}.to_string();",
253
+ naming::rust_ident_field(&t.field),
254
+ naming::ruby_inspect_string(&t.to_state)
255
+ ));
256
+ }
257
+ if lines.is_empty() {
258
+ lines.push(format!("{indent}let _ = record;"));
259
+ }
260
+ lines
261
+ }
262
+
100
263
  fn with_references_binding() -> String {
101
264
  "let with_references = crate::kernel::WithReferences { command_deref: &command_deref, args: &args, owner_deref: &owner_deref };".to_string()
102
265
  }
@@ -107,12 +270,19 @@ fn seed_projections_binding(aggregate: &Json) -> String {
107
270
  format!("let seed_projections = crate::kernel::seeded_projections(&with_references, {table}_PROJECTED_FIELDS);")
108
271
  }
109
272
 
110
- fn target_type_for<'a>(target: &str, aggregate: &'a Json, lifecycle_field: Option<&str>) -> Option<&'a str> {
273
+ fn target_type_for<'a>(
274
+ target: &str,
275
+ aggregate: &'a Json,
276
+ lifecycle_field: Option<&str>,
277
+ ) -> Option<&'a str> {
111
278
  if lifecycle_field == Some(target) {
112
279
  return Some("String");
113
280
  }
114
- let attrs = aggregate.get("attributes").map(Json::each).unwrap_or(&[]);
115
- attrs.iter().find(|a| crate::attr::name(a) == target).map(crate::attr::type_name)
281
+ let attrs = aggregate.get("attributes").map_or_default(Json::each);
282
+ attrs
283
+ .iter()
284
+ .find(|a| crate::attr::name(a) == target)
285
+ .map(crate::attr::type_name)
116
286
  }
117
287
 
118
288
  // The lifecycle field is never a list; `false` when the target is not found.
@@ -120,24 +290,49 @@ fn target_list_for(target: &str, aggregate: &Json, lifecycle_field: Option<&str>
120
290
  if lifecycle_field == Some(target) {
121
291
  return false;
122
292
  }
123
- let attrs = aggregate.get("attributes").map(Json::each).unwrap_or(&[]);
124
- attrs.iter().find(|a| crate::attr::name(a) == target).map(crate::attr::list).unwrap_or(false)
293
+ let attrs = aggregate.get("attributes").map_or_default(Json::each);
294
+ attrs
295
+ .iter()
296
+ .find(|a| crate::attr::name(a) == target)
297
+ .is_some_and(crate::attr::list)
125
298
  }
126
299
 
127
- pub fn command_skip_reason(command: &Json, aggregate: &Json, value_objects_by_name: &HashMap<String, &Json>) -> Option<SkipReason> {
300
+ pub fn command_skip_reason(
301
+ command: &Json,
302
+ aggregate: &Json,
303
+ value_objects_by_name: &HashMap<String, &Json>,
304
+ ) -> Option<SkipReason> {
128
305
  command_skip_reason_with(command, aggregate, value_objects_by_name, true)
129
306
  }
130
307
 
131
308
  /// `creating_possible` is false for an entity command: its optional identity argument is not an
132
309
  /// identity source.
133
- fn command_skip_reason_with(command: &Json, aggregate: &Json, value_objects_by_name: &HashMap<String, &Json>, creating_possible: bool) -> Option<SkipReason> {
134
- let mutations_list = command.get("mutations").map(Json::each).unwrap_or(&[]);
310
+ fn command_skip_reason_with(
311
+ command: &Json,
312
+ aggregate: &Json,
313
+ value_objects_by_name: &HashMap<String, &Json>,
314
+ creating_possible: bool,
315
+ ) -> Option<SkipReason> {
316
+ let mutations_list = command.get("mutations").map_or_default(Json::each);
135
317
 
136
318
  let mut unsupported_ops: Vec<String> = Vec::new();
137
319
  for m in mutations_list {
138
320
  let op = m.get("op").map(Json::to_s).unwrap_or_default();
139
321
  // `remove` passes here; `mutations::remove_field_problems` narrows it to entity lists.
140
- if !["append", "set", "increment", "decrement", "multiply", "clamp", "remove", "delegate", "corrects"].contains(&op.as_str()) && !unsupported_ops.contains(&op) {
322
+ if ![
323
+ "append",
324
+ "set",
325
+ "increment",
326
+ "decrement",
327
+ "multiply",
328
+ "clamp",
329
+ "remove",
330
+ "delegate",
331
+ "corrects",
332
+ ]
333
+ .contains(&op.as_str())
334
+ && !unsupported_ops.contains(&op)
335
+ {
141
336
  unsupported_ops.push(op);
142
337
  }
143
338
  }
@@ -148,7 +343,11 @@ fn command_skip_reason_with(command: &Json, aggregate: &Json, value_objects_by_n
148
343
  if let Some(corrects) = crate::bridging::corrects_of(command) {
149
344
  // The IR is already derived, so only a command carrying nothing but its `corrects` is
150
345
  // refused.
151
- if crate::bridging::corrects_reverses(corrects) && mutations_list.iter().all(|m| m.get("op").map(Json::to_s).unwrap_or_default() == "corrects") {
346
+ if crate::bridging::corrects_reverses(corrects)
347
+ && mutations_list
348
+ .iter()
349
+ .all(|m| m.get("op").map(Json::to_s).unwrap_or_default() == "corrects")
350
+ {
152
351
  let event = corrects.get("target").map(Json::to_s).unwrap_or_default();
153
352
  return Some(skip("corrects_reverses", format!("corrects {event}, reverses: true — the derived append/remove-reversal shape is a real, separate gap Ruby's own authors haven't finished designing (AggregateBuilder#seal_correction_targets's own comment) — not generated yet")));
154
353
  }
@@ -158,27 +357,49 @@ fn command_skip_reason_with(command: &Json, aggregate: &Json, value_objects_by_n
158
357
  return Some(skip("delegate", problem));
159
358
  }
160
359
 
161
- let append_problems = mutations::append_field_problems(command, aggregate, value_objects_by_name);
360
+ let append_problems =
361
+ mutations::append_field_problems(command, aggregate, value_objects_by_name);
162
362
  if !append_problems.is_empty() {
163
- return Some(skip("append_field", format!("sets append field(s): {}", append_problems.join("; "))));
363
+ return Some(skip(
364
+ "append_field",
365
+ format!("sets append field(s): {}", append_problems.join("; ")),
366
+ ));
164
367
  }
165
368
 
166
- let state_problems = mutations::state_source_problems(command, aggregate, value_objects_by_name);
369
+ let state_problems =
370
+ mutations::state_source_problems(command, aggregate, value_objects_by_name);
167
371
  if !state_problems.is_empty() {
168
- return Some(skip("state_source", format!("sets state source(s): {}", state_problems.join("; "))));
372
+ return Some(skip(
373
+ "state_source",
374
+ format!("sets state source(s): {}", state_problems.join("; ")),
375
+ ));
169
376
  }
170
377
 
171
- let expression_problems = mutations::expression_source_problems(command, aggregate, value_objects_by_name);
378
+ let expression_problems =
379
+ mutations::expression_source_problems(command, aggregate, value_objects_by_name);
172
380
  if !expression_problems.is_empty() {
173
- return Some(skip("expression_source", format!("sets expression source(s): {}", expression_problems.join("; "))));
381
+ return Some(skip(
382
+ "expression_source",
383
+ format!(
384
+ "sets expression source(s): {}",
385
+ expression_problems.join("; ")
386
+ ),
387
+ ));
174
388
  }
175
389
 
176
- let remove_problems = mutations::remove_field_problems(command, aggregate, value_objects_by_name);
390
+ let remove_problems =
391
+ mutations::remove_field_problems(command, aggregate, value_objects_by_name);
177
392
  if !remove_problems.is_empty() {
178
- return Some(skip("remove_field", format!("sets remove field(s): {}", remove_problems.join("; "))));
393
+ return Some(skip(
394
+ "remove_field",
395
+ format!("sets remove field(s): {}", remove_problems.join("; ")),
396
+ ));
179
397
  }
180
398
 
181
- let lifecycle_field = aggregate.get("lifecycle").and_then(|l| l.get("field")).map(Json::to_s);
399
+ let lifecycle_field = aggregate
400
+ .get("lifecycle")
401
+ .and_then(|l| l.get("field"))
402
+ .map(Json::to_s);
182
403
 
183
404
  let literal_set_targets: Vec<String> = mutations_list
184
405
  .iter()
@@ -186,14 +407,20 @@ fn command_skip_reason_with(command: &Json, aggregate: &Json, value_objects_by_n
186
407
  if m.get("op").map(Json::to_s).unwrap_or_default() != "set" {
187
408
  return false;
188
409
  }
189
- let Some(source) = m.get("source") else { return false };
410
+ let Some(source) = m.get("source") else {
411
+ return false;
412
+ };
190
413
  if source.get("kind").map(Json::to_s).unwrap_or_default() != "literal" {
191
414
  return false;
192
415
  }
193
416
  let value = source.get("value").unwrap_or(&Json::Null);
194
417
  let target = m.get("target").map(Json::to_s).unwrap_or_default();
195
418
  let target_type = target_type_for(&target, aggregate, lifecycle_field.as_deref());
196
- !crate::bridging::literal_set_bridgeable(&Literal::from_json(value), target_type, value_objects_by_name)
419
+ !crate::bridging::literal_set_bridgeable(
420
+ &Literal::from_json(value),
421
+ target_type,
422
+ value_objects_by_name,
423
+ )
197
424
  })
198
425
  .map(|m| m.get("target").map(Json::to_s).unwrap_or_default())
199
426
  .collect();
@@ -207,21 +434,26 @@ fn command_skip_reason_with(command: &Json, aggregate: &Json, value_objects_by_n
207
434
  if m.get("op").map(Json::to_s).unwrap_or_default() != "set" {
208
435
  return false;
209
436
  }
210
- let Some(source) = m.get("source") else { return false };
437
+ let Some(source) = m.get("source") else {
438
+ return false;
439
+ };
211
440
  if source.get("kind").map(Json::to_s).unwrap_or_default() != "argument" {
212
441
  return false;
213
442
  }
214
443
  let target = m.get("target").map(Json::to_s).unwrap_or_default();
215
444
  let target_type = target_type_for(&target, aggregate, lifecycle_field.as_deref());
216
445
  let source_name = source.get("name").map(Json::to_s).unwrap_or_default();
217
- let cmd_attrs = command.get("attributes").map(Json::each).unwrap_or(&[]);
218
- let source_attr = cmd_attrs.iter().find(|a| crate::attr::name(a) == source_name);
446
+ let cmd_attrs = command.get("attributes").map_or_default(Json::each);
447
+ let source_attr = cmd_attrs
448
+ .iter()
449
+ .find(|a| crate::attr::name(a) == source_name);
219
450
  let source_type = source_attr.map(crate::attr::type_name);
220
451
  // List-ness must match too: element-type bridging alone would collapse a `Vec` to a
221
452
  // scalar.
222
453
  match (target_type, source_type, source_attr) {
223
454
  (Some(t), Some(s), Some(source_attr)) => {
224
- target_list_for(&target, aggregate, lifecycle_field.as_deref()) != crate::attr::list(source_attr)
455
+ target_list_for(&target, aggregate, lifecycle_field.as_deref())
456
+ != crate::attr::list(source_attr)
225
457
  || !crate::bridging::bridgeable_value_types(s, t, value_objects_by_name)
226
458
  }
227
459
  _ => false,
@@ -233,13 +465,37 @@ fn command_skip_reason_with(command: &Json, aggregate: &Json, value_objects_by_n
233
465
  return Some(skip("set_argument_bridge", format!("sets :{} sources an argument no single-field rewrap can bridge to the target's type — not generated yet", mismatched_sets.join(", "))));
234
466
  }
235
467
 
236
- let arithmetic_targets: Vec<&Json> = mutations_list.iter().filter(|m| ["increment", "decrement", "multiply"].contains(&m.get("op").map(Json::to_s).unwrap_or_default().as_str())).collect();
468
+ let arithmetic_targets: Vec<&Json> = mutations_list
469
+ .iter()
470
+ .filter(|m| {
471
+ ["increment", "decrement", "multiply"]
472
+ .contains(&m.get("op").map(Json::to_s).unwrap_or_default().as_str())
473
+ })
474
+ .collect();
237
475
  let unsupported_arithmetic: Vec<String> = arithmetic_targets
238
476
  .iter()
239
477
  .filter(|m| {
240
- let target = crate::bridging::arithmetic_target_field(m, aggregate, value_objects_by_name);
478
+ let target =
479
+ crate::bridging::arithmetic_target_field(m, aggregate, value_objects_by_name);
241
480
  match target {
242
- Some((_, field)) => crate::bridging::arithmetic_amount_expr(m.get("source").unwrap_or(&Json::Null), command, value_objects_by_name, &field).is_none(),
481
+ Some((_, field, kind)) => {
482
+ crate::bridging::arithmetic_amount_expr(
483
+ m.get("source").unwrap_or(&Json::Null),
484
+ command,
485
+ value_objects_by_name,
486
+ &field,
487
+ kind,
488
+ )
489
+ .is_none()
490
+ && crate::bridging::text_operand_expr(
491
+ m,
492
+ aggregate,
493
+ command,
494
+ value_objects_by_name,
495
+ &field,
496
+ )
497
+ .is_none()
498
+ }
243
499
  None => true,
244
500
  }
245
501
  })
@@ -250,104 +506,165 @@ fn command_skip_reason_with(command: &Json, aggregate: &Json, value_objects_by_n
250
506
  }
251
507
 
252
508
  // `:clamp`: target as for increment/decrement/multiply; bounds via `clamp_bounds_ints`.
253
- let clamp_targets: Vec<&Json> = mutations_list.iter().filter(|m| m.get("op").map(Json::to_s).unwrap_or_default() == "clamp").collect();
509
+ let clamp_targets: Vec<&Json> = mutations_list
510
+ .iter()
511
+ .filter(|m| m.get("op").map(Json::to_s).unwrap_or_default() == "clamp")
512
+ .collect();
254
513
  let unsupported_clamp: Vec<String> = clamp_targets
255
514
  .iter()
256
515
  .filter(|m| {
257
- let target = crate::bridging::arithmetic_target_field(m, aggregate, value_objects_by_name);
516
+ let target = crate::bridging::integer_target_field(m, aggregate, value_objects_by_name);
258
517
  let bounds = m.get("source").and_then(crate::bridging::clamp_bounds_ints);
259
518
  target.is_none() || bounds.is_none()
260
519
  })
261
520
  .map(|m| m.get("target").map(Json::to_s).unwrap_or_default())
262
521
  .collect();
263
522
  if !unsupported_clamp.is_empty() {
264
- return Some(skip("clamp", format!("sets :{} clamp target field or bounds isn't bridgeable — not generated yet", unsupported_clamp.join(", "))));
523
+ return Some(skip(
524
+ "clamp",
525
+ format!(
526
+ "sets :{} clamp target field or bounds isn't bridgeable — not generated yet",
527
+ unsupported_clamp.join(", ")
528
+ ),
529
+ ));
265
530
  }
266
531
 
267
- let optional_problems = optional_source_mismatches_with(command, aggregate, value_objects_by_name, creating_possible);
532
+ let optional_problems = optional_source_mismatches_with(
533
+ command,
534
+ aggregate,
535
+ value_objects_by_name,
536
+ creating_possible,
537
+ );
268
538
  if !optional_problems.is_empty() {
269
- return Some(skip("optional_source", format!("optional argument feeds a non-optional target: {} — not generated yet", optional_problems.join("; "))));
539
+ return Some(skip(
540
+ "optional_source",
541
+ format!(
542
+ "optional argument feeds a non-optional target: {} — not generated yet",
543
+ optional_problems.join("; ")
544
+ ),
545
+ ));
270
546
  }
271
547
 
272
548
  None
273
549
  }
274
550
 
275
- pub fn optional_source_mismatches(command: &Json, aggregate: &Json, value_objects_by_name: &HashMap<String, &Json>) -> Vec<String> {
276
- optional_source_mismatches_with(command, aggregate, value_objects_by_name, true)
277
- }
278
-
279
- fn optional_source_mismatches_with(command: &Json, aggregate: &Json, value_objects_by_name: &HashMap<String, &Json>, creating_possible: bool) -> Vec<String> {
280
- let lifecycle_field = aggregate.get("lifecycle").and_then(|l| l.get("field")).map(Json::to_s);
551
+ fn optional_source_mismatches_with(
552
+ command: &Json,
553
+ aggregate: &Json,
554
+ value_objects_by_name: &HashMap<String, &Json>,
555
+ creating_possible: bool,
556
+ ) -> Vec<String> {
557
+ let lifecycle_field = aggregate
558
+ .get("lifecycle")
559
+ .and_then(|l| l.get("field"))
560
+ .map(Json::to_s);
281
561
  let mut problems: Vec<String> = Vec::new();
282
- let cmd_attrs = command.get("attributes").map(Json::each).unwrap_or(&[]);
562
+ let cmd_attrs = command.get("attributes").map_or_default(Json::each);
283
563
 
284
- if creating_possible && crate::shared::creates_owner(aggregate, command, value_objects_by_name) {
285
- let identified_by = aggregate.get("identified_by").map(Json::each).unwrap_or(&[]);
564
+ if creating_possible && crate::shared::creates_owner(aggregate, command) {
565
+ let identified_by = aggregate.get("identified_by").map_or_default(Json::each);
286
566
  for path in identified_by {
287
567
  let path = path.as_str().unwrap_or("");
288
568
  let head = path.split('.').next().unwrap_or("");
289
569
  if let Some(source_attr) = cmd_attrs.iter().find(|a| crate::attr::name(a) == head) {
290
570
  if crate::attr::optional(source_attr) {
291
- problems.push(format!("identified_by :{path} sources optional argument {}", crate::attr::name(source_attr)));
571
+ problems.push(format!(
572
+ "identified_by :{path} sources optional argument {}",
573
+ crate::attr::name(source_attr)
574
+ ));
292
575
  }
293
576
  }
294
577
  }
295
578
  }
296
579
 
297
- let mutations_list = command.get("mutations").map(Json::each).unwrap_or(&[]);
580
+ let mutations_list = command.get("mutations").map_or_default(Json::each);
298
581
  for m in mutations_list {
299
582
  let op = m.get("op").map(Json::to_s).unwrap_or_default();
300
583
  let target = m.get("target").map(Json::to_s).unwrap_or_default();
301
584
  match op.as_str() {
302
585
  "set" => {
303
- let Some(source) = m.get("source") else { continue };
586
+ let Some(source) = m.get("source") else {
587
+ continue;
588
+ };
304
589
  if source.get("kind").map(Json::to_s).unwrap_or_default() != "argument" {
305
590
  continue;
306
591
  }
307
592
  let source_name = source.get("name").map(Json::to_s).unwrap_or_default();
308
- let Some(source_attr) = cmd_attrs.iter().find(|a| crate::attr::name(a) == source_name) else { continue };
593
+ let Some(source_attr) = cmd_attrs
594
+ .iter()
595
+ .find(|a| crate::attr::name(a) == source_name)
596
+ else {
597
+ continue;
598
+ };
309
599
  if !crate::attr::optional(source_attr) {
310
600
  continue;
311
601
  }
312
602
 
313
603
  if lifecycle_field.as_deref() == Some(target.as_str()) {
314
- problems.push(format!("sets :{target} sources optional argument {} into the lifecycle field", crate::attr::name(source_attr)));
604
+ problems.push(format!(
605
+ "sets :{target} sources optional argument {} into the lifecycle field",
606
+ crate::attr::name(source_attr)
607
+ ));
315
608
  continue;
316
609
  }
317
610
 
318
- let attrs = aggregate.get("attributes").map(Json::each).unwrap_or(&[]);
611
+ let attrs = aggregate.get("attributes").map_or_default(Json::each);
319
612
  let target_attr = attrs.iter().find(|a| crate::attr::name(a) == target);
320
- if target_attr.map(crate::attr::list).unwrap_or(false) {
613
+ if target_attr.is_some_and(crate::attr::list) {
321
614
  continue;
322
615
  }
323
616
  // An aggregate command's record fields are all `Option<T>`.
324
617
  if creating_possible {
325
618
  continue;
326
619
  }
327
- if !target_attr.map(crate::attr::optional).unwrap_or(false) {
328
- problems.push(format!("sets :{target} sources optional argument {}", crate::attr::name(source_attr)));
620
+ if !target_attr.is_some_and(crate::attr::optional) {
621
+ problems.push(format!(
622
+ "sets :{target} sources optional argument {}",
623
+ crate::attr::name(source_attr)
624
+ ));
329
625
  }
330
626
  }
331
627
  "append" => {
332
- let attrs = aggregate.get("attributes").map(Json::each).unwrap_or(&[]);
333
- let Some(target_attr) = attrs.iter().find(|a| crate::attr::name(a) == target) else { continue };
334
- let Some(element) = mutations::append_element(aggregate, crate::attr::type_name(target_attr), value_objects_by_name) else { continue };
628
+ let attrs = aggregate.get("attributes").map_or_default(Json::each);
629
+ let Some(target_attr) = attrs.iter().find(|a| crate::attr::name(a) == target)
630
+ else {
631
+ continue;
632
+ };
633
+ let Some(element) = mutations::append_element(
634
+ aggregate,
635
+ crate::attr::type_name(target_attr),
636
+ value_objects_by_name,
637
+ ) else {
638
+ continue;
639
+ };
335
640
 
336
641
  let fields = match m.get("fields") {
337
642
  Some(Json::Object(pairs)) => pairs.clone(),
338
643
  _ => Vec::new(),
339
644
  };
340
- let element_attrs = element.get("attributes").map(Json::each).unwrap_or(&[]);
645
+ let element_attrs = element.get("attributes").map_or_default(Json::each);
341
646
  for (field_name, source) in &fields {
342
647
  let parsed = mutations::append_field_source(&source.to_s());
343
- let Literal::Symbol(arg_name) = &parsed else { continue };
344
- let Some(source_attr) = cmd_attrs.iter().find(|a| crate::attr::name(a) == arg_name.as_str()) else { continue };
648
+ let Literal::Symbol(arg_name) = &parsed else {
649
+ continue;
650
+ };
651
+ let Some(source_attr) = cmd_attrs
652
+ .iter()
653
+ .find(|a| crate::attr::name(a) == arg_name.as_str())
654
+ else {
655
+ continue;
656
+ };
345
657
  if !crate::attr::optional(source_attr) {
346
658
  continue;
347
659
  }
348
- let field_attr = element_attrs.iter().find(|a| crate::attr::name(a) == field_name.as_str());
349
- if !field_attr.map(crate::attr::optional).unwrap_or(false) {
350
- problems.push(format!("sets append {target}.{field_name} sources optional argument {}", crate::attr::name(source_attr)));
660
+ let field_attr = element_attrs
661
+ .iter()
662
+ .find(|a| crate::attr::name(a) == field_name.as_str());
663
+ if !field_attr.is_some_and(crate::attr::optional) {
664
+ problems.push(format!(
665
+ "sets append {target}.{field_name} sources optional argument {}",
666
+ crate::attr::name(source_attr)
667
+ ));
351
668
  }
352
669
  }
353
670
  }
@@ -361,19 +678,31 @@ fn optional_source_mismatches_with(command: &Json, aggregate: &Json, value_objec
361
678
  /// One attribute's `admits:` check and value-object invariant check against `value_expr`.
362
679
  /// `pattern:` is not checked: Ruby never enforces it on a bare command attribute (ADR 0051).
363
680
  /// List attributes get no `admits:` check either, matching Ruby.
364
- pub fn argument_check_lines(exemplar: &Exemplar, attr: &Json, value_expr: &str, aggregates_by_name: &HashMap<String, &Json>, value_objects_by_name: &HashMap<String, &Json>) -> Vec<String> {
681
+ pub fn argument_check_lines(
682
+ exemplar: &Exemplar,
683
+ attr: &Json,
684
+ value_expr: &str,
685
+ aggregates_by_name: &HashMap<String, &Json>,
686
+ value_objects_by_name: &HashMap<String, &Json>,
687
+ ) -> Vec<String> {
365
688
  let mut lines: Vec<String> = Vec::new();
366
689
 
367
690
  if !crate::attr::list(attr) {
368
691
  // Raw field expression: `constraints.rs` handles optionals itself.
369
- if let Some(c) = crate::constraints::emit_admits_check(exemplar, value_expr, attr, aggregates_by_name, value_objects_by_name) {
692
+ if let Some(c) = crate::constraints::emit_admits_check(
693
+ exemplar,
694
+ value_expr,
695
+ attr,
696
+ aggregates_by_name,
697
+ value_objects_by_name,
698
+ ) {
370
699
  lines.push(format!(" {c}"));
371
700
  }
372
701
  }
373
702
 
374
703
  let vo = value_objects_by_name.get(crate::attr::type_name(attr));
375
704
  if let Some(vo) = vo {
376
- if !vo.get("closed_set").map(Json::as_bool).unwrap_or(false) {
705
+ if !vo.get("closed_set").is_some_and(Json::as_bool) {
377
706
  let line = if crate::attr::optional(attr) {
378
707
  if crate::attr::list(attr) {
379
708
  format!(" if let Some(items) = &{value_expr} {{ for item in items {{ item.check_invariants()?; }} }}")
@@ -393,65 +722,68 @@ pub fn argument_check_lines(exemplar: &Exemplar, attr: &Json, value_expr: &str,
393
722
  }
394
723
 
395
724
  /// Invariant checks against the built `args.{field}`, redundant with `emit_from_json_flat`'s copy.
396
- pub fn invariant_checks_for(exemplar: &Exemplar, command: &Json, aggregates_by_name: &HashMap<String, &Json>, value_objects_by_name: &HashMap<String, &Json>) -> Vec<String> {
397
- let attrs = command.get("attributes").map(Json::each).unwrap_or(&[]);
725
+ pub fn invariant_checks_for(
726
+ exemplar: &Exemplar,
727
+ command: &Json,
728
+ aggregates_by_name: &HashMap<String, &Json>,
729
+ value_objects_by_name: &HashMap<String, &Json>,
730
+ ) -> Vec<String> {
731
+ let attrs = command.get("attributes").map_or_default(Json::each);
398
732
  let mut lines: Vec<String> = Vec::new();
399
733
 
400
734
  for attr in attrs {
401
735
  let field = naming::rust_ident_field(crate::attr::name(attr));
402
736
  let value_expr = format!("args.{field}");
403
- lines.extend(argument_check_lines(exemplar, attr, &value_expr, aggregates_by_name, value_objects_by_name));
737
+ lines.extend(argument_check_lines(
738
+ exemplar,
739
+ attr,
740
+ &value_expr,
741
+ aggregates_by_name,
742
+ value_objects_by_name,
743
+ ));
404
744
  }
405
745
 
406
746
  lines
407
747
  }
408
748
 
409
749
  /// One emitter for every command shape `kernel::dispatch` can run.
410
- pub fn emit_command(exemplar: &Exemplar, command: &Json, aggregate: &Json, domain_name: &str, value_objects_by_name: &HashMap<String, &Json>, aggregates_by_name: &HashMap<String, &Json>) -> String {
411
- let record = naming::rust_ident(aggregate.get("name").and_then(Json::as_str).unwrap_or(""));
412
- let cmd = naming::rust_ident(command.get("name").and_then(Json::as_str).unwrap_or(""));
413
- let creates = crate::shared::creates_owner(aggregate, command, value_objects_by_name);
750
+ #[allow(
751
+ clippy::option_if_let_else,
752
+ clippy::panic,
753
+ reason = "a drifted exemplar or invalid IR aborts generation on purpose; a map_or_else closure would bury this long body"
754
+ )]
755
+ pub fn emit_command(
756
+ exemplar: &Exemplar,
757
+ command: &Json,
758
+ aggregate: &Json,
759
+ domain_name: &str,
760
+ value_objects_by_name: &HashMap<String, &Json>,
761
+ aggregates_by_name: &HashMap<String, &Json>,
762
+ ) -> String {
763
+ let record = naming::rust_ident(name_of(aggregate));
764
+ let cmd = naming::rust_ident(name_of(command));
765
+ let creates = crate::shared::creates_owner(aggregate, command);
414
766
  let identity = mutations::identity_components(aggregate, command);
415
- let identity_extra_params: Vec<String> = identity.iter().filter_map(|c| c.param.clone()).collect();
767
+ let identity_extra_params: Vec<String> =
768
+ identity.iter().filter_map(|c| c.param.clone()).collect();
416
769
 
417
- let aggregate_name = aggregate.get("name").and_then(Json::as_str).unwrap_or("").to_string();
418
- let identity_reading = aggregate.get("identified_by").map(Json::each).unwrap_or(&[]).iter().map(Json::to_s).collect::<Vec<_>>().join(", ");
770
+ let aggregate_name = name_of(aggregate).to_string();
771
+ let identity_reading = identity_reading(aggregate);
419
772
 
420
- let attrs = command.get("attributes").map(Json::each).unwrap_or(&[]);
421
- let mut args_struct = vec![format!("pub struct {cmd}Args {{")];
422
- for attr in attrs {
423
- let mut ty = naming::rust_type(crate::attr::type_name(attr), crate::attr::list(attr));
424
- if crate::attr::optional(attr) {
425
- ty = format!("Option<{ty}>");
426
- }
427
- args_struct.push(format!(" {}", exemplar.render("struct_field", &[("TmplFieldType", ty), ("tmpl_field", naming::rust_ident_field(crate::attr::name(attr)))])));
428
- }
429
- args_struct.push("}".to_string());
773
+ let attrs = command.get("attributes").map_or_default(Json::each);
774
+ let args_struct = args_struct_text(exemplar, &format!("{cmd}Args"), attrs);
430
775
 
431
- let invariant_checks = invariant_checks_for(exemplar, command, aggregates_by_name, value_objects_by_name);
776
+ let invariant_checks =
777
+ invariant_checks_for(exemplar, command, aggregates_by_name, value_objects_by_name);
432
778
 
433
- let givens = command.get("givens").map(Json::each).unwrap_or(&[]);
434
779
  let mut given_specs: Vec<String> = crate::bridging::corrects_given_specs(command);
435
- given_specs.extend(givens.iter().map(|g| {
436
- let description = g.get("description").and_then(Json::as_str).unwrap_or("");
437
- let ast = g.get("ast").unwrap_or_else(|| panic!("given row has no ast: {g:?}"));
438
- format!(" crate::kernel::GivenSpec {{ description: {}, expr: {}, corrects_event: None }},", naming::ruby_inspect_string(description), crate::expr_emitter::emit_ast(ast))
439
- }));
440
-
441
- let ensures = command.get("ensures").map(Json::each).unwrap_or(&[]);
442
- let ensures_specs: Vec<String> = ensures
443
- .iter()
444
- .map(|e| {
445
- let description = e.get("description").and_then(Json::as_str).unwrap_or("");
446
- let ast = e.get("ast").unwrap_or_else(|| panic!("ensures row has no ast: {e:?}"));
447
- format!(" crate::kernel::EnsuresSpec {{ description: {}, expr: {} }},", naming::ruby_inspect_string(description), crate::expr_emitter::emit_ast(ast))
448
- })
449
- .collect();
780
+ given_specs.extend(given_spec_lines(command, " "));
781
+ let ensures_specs = ensures_spec_lines(command, " ");
450
782
 
451
783
  let transition = mutations::lifecycle_transition_for(command, aggregate);
452
784
  let transition_arg = mutations::transition_check_arg(transition.as_ref());
453
785
 
454
- let mutations_list = command.get("mutations").map(Json::each).unwrap_or(&[]);
786
+ let mutations_list = command.get("mutations").map_or_default(Json::each);
455
787
  // A `delegate` mutation is rendered by `delegation_of`, which overwrites `mutation_lines`;
456
788
  // `mutations::emit_mutation_line` panics on the op, so it must be excluded here.
457
789
  let mut mutation_lines: Vec<String> = mutations_list
@@ -460,7 +792,16 @@ pub fn emit_command(exemplar: &Exemplar, command: &Json, aggregate: &Json, domai
460
792
  let op = m.get("op").map(Json::to_s).unwrap_or_default();
461
793
  op != "corrects" && op != "delegate"
462
794
  })
463
- .map(|m| mutations::emit_mutation_line(exemplar, m, aggregate, command, value_objects_by_name, true))
795
+ .map(|m| {
796
+ mutations::emit_mutation_line(
797
+ exemplar,
798
+ m,
799
+ aggregate,
800
+ command,
801
+ value_objects_by_name,
802
+ true,
803
+ )
804
+ })
464
805
  .collect();
465
806
  if mutations::reads_pre_state(mutations_list) {
466
807
  mutation_lines.insert(0, mutations::pre_state_line());
@@ -468,7 +809,12 @@ pub fn emit_command(exemplar: &Exemplar, command: &Json, aggregate: &Json, domai
468
809
  // Stamp the fact forward for every command whose `emits` names an event a `corrects` targets.
469
810
  let correctable = crate::bridging::correctable_event_names(aggregate);
470
811
  let mut emitted_names: Vec<String> = Vec::new();
471
- for name in command.get("emits").map(Json::each).unwrap_or(&[]).iter().map(Json::to_s) {
812
+ for name in command
813
+ .get("emits")
814
+ .map_or_default(Json::each)
815
+ .iter()
816
+ .map(Json::to_s)
817
+ {
472
818
  // `.uniq`: first-occurrence order, not sorted.
473
819
  if !emitted_names.contains(&name) {
474
820
  emitted_names.push(name);
@@ -476,30 +822,46 @@ pub fn emit_command(exemplar: &Exemplar, command: &Json, aggregate: &Json, domai
476
822
  }
477
823
  for name in &emitted_names {
478
824
  if correctable.contains(name) {
479
- mutation_lines.push(format!(" record.{} = true;", crate::bridging::corrects_flag_field(name)));
480
- }
481
- }
482
- if let Some(t) = &transition {
483
- if !t.to_state.is_empty() {
484
- mutation_lines.push(format!(" record.{} = {}.to_string();", naming::rust_ident_field(&t.field), naming::ruby_inspect_string(&t.to_state)));
825
+ mutation_lines.push(format!(
826
+ " record.{} = true;",
827
+ crate::bridging::corrects_flag_field(name)
828
+ ));
485
829
  }
486
830
  }
487
- if mutation_lines.is_empty() {
488
- mutation_lines = vec![" let _ = record;".to_string()];
489
- }
490
- let delegation = delegation_of(exemplar, command, aggregate, value_objects_by_name, domain_name);
831
+ let mut mutation_lines = close_mutation_lines(mutation_lines, transition.as_ref(), " ");
832
+ let delegation = delegation_of(
833
+ exemplar,
834
+ command,
835
+ aggregate,
836
+ value_objects_by_name,
837
+ domain_name,
838
+ );
491
839
  if let Some(d) = &delegation {
492
- assert!(!creates, "{cmd}: a creating command cannot delegate — nothing exists to delegate to");
840
+ assert!(
841
+ !creates,
842
+ "{cmd}: a creating command cannot delegate — nothing exists to delegate to"
843
+ );
493
844
  // `d.element` runs first, inside the closure `dispatch` calls after a successful hydrate.
494
845
  mutation_lines = vec![d.element.clone(), d.apply.clone()];
495
846
  }
496
- let prelude = delegation.as_ref().map(|d| d.prelude.clone()).unwrap_or_default();
497
- let payload = if delegation.is_some() { "delegate_facts.clone()," } else { "args.to_json()," }.to_string();
847
+ let prelude = delegation
848
+ .as_ref()
849
+ .map(|d| d.prelude.clone())
850
+ .unwrap_or_default();
851
+ let payload = if delegation.is_some() {
852
+ "delegate_facts.clone(),"
853
+ } else {
854
+ "args.to_json(),"
855
+ }
856
+ .to_string();
498
857
 
499
858
  let (hydrate, fn_signature);
500
859
  // A `projects` field is never a command argument; `seed_projections` populates it before save.
501
860
  // Owned because the pseudo-attributes are freshly built.
502
- let mut record_agg_attrs_owned: Vec<Json> = aggregate.get("attributes").map(Json::each).unwrap_or(&[]).to_vec();
861
+ let mut record_agg_attrs_owned: Vec<Json> = aggregate
862
+ .get("attributes")
863
+ .map_or_default(Json::each)
864
+ .to_vec();
503
865
  record_agg_attrs_owned.extend(crate::types::projected_field_pseudo_attributes(aggregate));
504
866
  let record_agg_attrs = record_agg_attrs_owned.as_slice();
505
867
 
@@ -507,20 +869,37 @@ pub fn emit_command(exemplar: &Exemplar, command: &Json, aggregate: &Json, domai
507
869
  let record_fields: Vec<String> = record_agg_attrs
508
870
  .iter()
509
871
  .map(|attr| {
510
- let matched = attrs.iter().find(|a| crate::attr::name(a) == crate::attr::name(attr));
872
+ let matched = attrs
873
+ .iter()
874
+ .find(|a| crate::attr::name(a) == crate::attr::name(attr));
511
875
  let field = naming::rust_ident_field(crate::attr::name(attr));
512
876
  if let Some(matched) = matched {
513
877
  let matched_type = crate::attr::type_name(matched);
514
878
  let attr_type = crate::attr::type_name(attr);
515
879
  if crate::attr::optional(matched) {
516
- if crate::attr::list(attr) && !crate::shared::list_attr_creation_optional(aggregate, crate::attr::name(attr), value_objects_by_name) {
517
- format!(" {field}: args.{field}.clone().unwrap_or_default(),")
880
+ if crate::attr::list(attr)
881
+ && !crate::shared::list_attr_creation_optional(
882
+ aggregate,
883
+ crate::attr::name(attr),
884
+ )
885
+ {
886
+ format!(
887
+ " {field}: args.{field}.clone().unwrap_or_default(),"
888
+ )
518
889
  } else if crate::attr::list(attr) || matched_type == attr_type {
519
890
  format!(" {field}: args.{field}.clone(),")
520
891
  } else {
521
892
  // A cross-aggregate argument named like the owner's field but of
522
893
  // another type needs a bridge.
523
- format!(" {field}: {},", mutations::optional_value_rhs(&format!("args.{field}"), matched_type, attr_type, value_objects_by_name))
894
+ format!(
895
+ " {field}: {},",
896
+ mutations::optional_value_rhs(
897
+ &format!("args.{field}"),
898
+ matched_type,
899
+ attr_type,
900
+ value_objects_by_name
901
+ )
902
+ )
524
903
  }
525
904
  } else if crate::attr::list(attr) || matched_type == attr_type {
526
905
  if crate::attr::list(attr) {
@@ -529,15 +908,23 @@ pub fn emit_command(exemplar: &Exemplar, command: &Json, aggregate: &Json, domai
529
908
  format!(" {field}: Some(args.{field}.clone()),")
530
909
  }
531
910
  } else {
532
- format!(" {field}: Some({}),", crate::bridging::value_rhs(&format!("args.{field}"), matched_type, attr_type, value_objects_by_name))
911
+ format!(
912
+ " {field}: Some({}),",
913
+ crate::bridging::value_rhs(
914
+ &format!("args.{field}"),
915
+ matched_type,
916
+ attr_type,
917
+ value_objects_by_name
918
+ )
919
+ )
533
920
  }
534
921
  } else if crate::attr::list(attr) {
535
922
  format!(" {field}: vec![],")
536
923
  } else {
537
- match crate::bridging::creation_default_rhs(attr, value_objects_by_name) {
538
- Some(rhs) => format!(" {field}: Some({rhs}),"),
539
- None => format!(" {field}: None,"),
540
- }
924
+ crate::bridging::creation_default_rhs(attr, value_objects_by_name).map_or_else(
925
+ || format!(" {field}: None,"),
926
+ |rhs| format!(" {field}: Some({rhs}),"),
927
+ )
541
928
  }
542
929
  })
543
930
  .collect();
@@ -545,14 +932,25 @@ pub fn emit_command(exemplar: &Exemplar, command: &Json, aggregate: &Json, domai
545
932
  if let Some(lifecycle) = aggregate.get("lifecycle") {
546
933
  let field = lifecycle.get("field").and_then(Json::as_str).unwrap_or("");
547
934
  let default = lifecycle.get("default").map(Json::to_s).unwrap_or_default();
548
- record_fields.push(format!(" {}: {}.to_string(),", naming::rust_ident_field(field), naming::ruby_inspect_string(&default)));
935
+ record_fields.push(format!(
936
+ " {}: {}.to_string(),",
937
+ naming::rust_ident_field(field),
938
+ naming::ruby_inspect_string(&default)
939
+ ));
549
940
  }
550
941
  for ev in crate::bridging::correctable_event_names(aggregate) {
551
- record_fields.push(format!(" {}: false,", crate::bridging::corrects_flag_field(&ev)));
942
+ record_fields.push(format!(
943
+ " {}: false,",
944
+ crate::bridging::corrects_flag_field(&ev)
945
+ ));
552
946
  }
553
947
 
554
948
  // Mirrors `CommandInterpreter#step_hydrate`: `complete_state? && state_independent?`.
555
- let state_independent = crate::dependency_planning::state_independent_creation(aggregate, command, value_objects_by_name);
949
+ let state_independent = crate::dependency_planning::state_independent_creation(
950
+ aggregate,
951
+ command,
952
+ value_objects_by_name,
953
+ );
556
954
  let create_block = format!(
557
955
  "crate::kernel::Hydrate::Create {{\n id: __hydrate_id,\n build: Box::new(|| {record} {{\n{}\n }}),\n state_independent: {state_independent},\n }}",
558
956
  record_fields.join("\n")
@@ -560,7 +958,11 @@ pub fn emit_command(exemplar: &Exemplar, command: &Json, aggregate: &Json, domai
560
958
 
561
959
  // A route on a creating command is checked against its derived identity (`TypeMismatch`)
562
960
  // only when `complete_state_creation` holds; otherwise it forces find-or-`NotFound`.
563
- let complete_state = crate::dependency_planning::complete_state_creation(aggregate, command, value_objects_by_name);
961
+ let complete_state = crate::dependency_planning::complete_state_creation(
962
+ aggregate,
963
+ command,
964
+ value_objects_by_name,
965
+ );
564
966
  let derived_id_expr = mutations::build_identity_expr(&identity);
565
967
  let route_mismatch_message = format!(
566
968
  "{} routes to {{:?}}, but its identity facts name {{:?}}",
@@ -584,7 +986,7 @@ pub fn emit_command(exemplar: &Exemplar, command: &Json, aggregate: &Json, domai
584
986
  sig_parts.extend(identity_extra_params.iter().cloned());
585
987
  sig_parts.push(format!("args: {cmd}Args"));
586
988
  sig_parts.push("mutations: &mut Vec<crate::kernel::MutationRecord>".to_string());
587
- sig_parts.extend(deref_params().iter().map(|s| s.to_string()));
989
+ sig_parts.extend(deref_params().iter().map(std::string::ToString::to_string));
588
990
  sig_parts.push(TENANT_BOUNDARY_PARAM.to_string());
589
991
  fn_signature = sig_parts.join(", ");
590
992
  } else {
@@ -594,15 +996,20 @@ pub fn emit_command(exemplar: &Exemplar, command: &Json, aggregate: &Json, domai
594
996
  format!("args: {cmd}Args"),
595
997
  "mutations: &mut Vec<crate::kernel::MutationRecord>".to_string(),
596
998
  ];
597
- sig_parts.extend(deref_params().iter().map(|s| s.to_string()));
999
+ sig_parts.extend(deref_params().iter().map(std::string::ToString::to_string));
598
1000
  sig_parts.push(TENANT_BOUNDARY_PARAM.to_string());
599
1001
  hydrate = "crate::kernel::Hydrate::Act { id: id.to_string() }".to_string();
600
1002
  fn_signature = sig_parts.join(", ");
601
1003
  }
602
1004
 
603
1005
  let emits_expr = match &delegation {
604
- Some(d) => d.emits.iter().map(|e| naming::ruby_inspect_string(e)).collect::<Vec<_>>().join(", "),
605
- None => command.get("emits").map(Json::each).unwrap_or(&[]).iter().map(|e| naming::ruby_inspect_string(&e.to_s())).collect::<Vec<_>>().join(", "),
1006
+ Some(d) => d
1007
+ .emits
1008
+ .iter()
1009
+ .map(|e| naming::ruby_inspect_string(e))
1010
+ .collect::<Vec<_>>()
1011
+ .join(", "),
1012
+ None => emits_expr_of(command),
606
1013
  };
607
1014
 
608
1015
  let dispatch_fn = exemplar.render(
@@ -635,85 +1042,145 @@ pub fn emit_command(exemplar: &Exemplar, command: &Json, aggregate: &Json, domai
635
1042
  ],
636
1043
  );
637
1044
 
638
- format!("{}\n\n#[derive(Debug, Clone)]\n{}\n\n{dispatch_fn}", crate::fielded::emit_fielded_flat(exemplar, &format!("{cmd}Args"), attrs, value_objects_by_name, &[]), args_struct.join("\n"))
1045
+ format!(
1046
+ "{}\n\n#[derive(Debug, Clone)]\n{}\n\n{dispatch_fn}",
1047
+ crate::fielded::emit_fielded_flat(
1048
+ exemplar,
1049
+ &format!("{cmd}Args"),
1050
+ attrs,
1051
+ value_objects_by_name,
1052
+ &[]
1053
+ ),
1054
+ args_struct
1055
+ )
639
1056
  }
640
1057
 
641
1058
  /// The `:delegate` mutation (`delegates_to`).
642
1059
  fn delegate_of(command: &Json) -> Option<&Json> {
643
- command.get("mutations").map(Json::each).unwrap_or(&[]).iter().find(|m| m.get("op").map(Json::to_s).unwrap_or_default() == "delegate")
1060
+ command
1061
+ .get("mutations")
1062
+ .map_or_default(Json::each)
1063
+ .iter()
1064
+ .find(|m| m.get("op").map(Json::to_s).unwrap_or_default() == "delegate")
644
1065
  }
645
1066
 
646
1067
  fn delegate_mapping(delegation: &Json) -> Vec<(String, String)> {
647
1068
  match delegation.get("fields") {
648
- Some(Json::Object(pairs)) => pairs.iter().map(|(k, v)| (k.clone(), v.to_s().trim_start_matches(':').to_string())).collect(),
1069
+ Some(Json::Object(pairs)) => pairs
1070
+ .iter()
1071
+ .map(|(k, v)| (k.clone(), v.to_s().trim_start_matches(':').to_string()))
1072
+ .collect(),
649
1073
  _ => Vec::new(),
650
1074
  }
651
1075
  }
652
1076
 
653
- fn delegate_target<'a>(delegation: &Json, aggregate: &'a Json) -> (Option<&'a Json>, Option<&'a Json>) {
1077
+ fn delegate_target<'a>(
1078
+ delegation: &Json,
1079
+ aggregate: &'a Json,
1080
+ ) -> (Option<&'a Json>, Option<&'a Json>) {
654
1081
  let target = delegation.get("target").map(Json::to_s).unwrap_or_default();
655
- let (entity_name, command_name) = match target.rfind('.') {
656
- Some(dot) => (&target[..dot], &target[dot + 1..]),
657
- None => ("", target.as_str()),
658
- };
659
- let entity = aggregate.get("entities").map(Json::each).unwrap_or(&[]).iter().find(|e| e.get("name").map(Json::to_s).unwrap_or_default() == entity_name);
660
- let command = entity.and_then(|e| e.get("commands").map(Json::each).unwrap_or(&[]).iter().find(|c| c.get("name").map(Json::to_s).unwrap_or_default() == command_name));
1082
+ let (entity_name, command_name) = target.rfind('.').map_or(("", target.as_str()), |dot| {
1083
+ (&target[..dot], &target[dot + 1..])
1084
+ });
1085
+ let entity = aggregate
1086
+ .get("entities")
1087
+ .map_or_default(Json::each)
1088
+ .iter()
1089
+ .find(|e| e.get("name").map(Json::to_s).unwrap_or_default() == entity_name);
1090
+ let command = entity.and_then(|e| {
1091
+ e.get("commands")
1092
+ .map_or_default(Json::each)
1093
+ .iter()
1094
+ .find(|c| c.get("name").map(Json::to_s).unwrap_or_default() == command_name)
1095
+ });
661
1096
  (entity, command)
662
1097
  }
663
1098
 
664
- pub fn delegate_skip_reason(command: &Json, aggregate: &Json, value_objects_by_name: &HashMap<String, &Json>) -> Option<String> {
1099
+ pub fn delegate_skip_reason(
1100
+ command: &Json,
1101
+ aggregate: &Json,
1102
+ value_objects_by_name: &HashMap<String, &Json>,
1103
+ ) -> Option<String> {
665
1104
  let delegation = delegate_of(command)?;
666
- let label = format!("delegates_to {}", delegation.get("target").map(Json::to_s).unwrap_or_default());
667
- if command.get("mutations").map(Json::each).unwrap_or(&[]).len() > 1 {
1105
+ let label = format!(
1106
+ "delegates_to {}",
1107
+ delegation.get("target").map(Json::to_s).unwrap_or_default()
1108
+ );
1109
+ if command.get("mutations").map_or_default(Json::each).len() > 1 {
668
1110
  return Some(format!("{label} alongside other sets — not generated yet"));
669
1111
  }
670
1112
  let (entity, target) = delegate_target(delegation, aggregate);
671
1113
  let Some(entity) = entity else {
672
- return Some(format!("{label}: {} has no such entity", aggregate.get("name").map(Json::to_s).unwrap_or_default()));
1114
+ return Some(format!(
1115
+ "{label}: {} has no such entity",
1116
+ aggregate.get("name").map(Json::to_s).unwrap_or_default()
1117
+ ));
673
1118
  };
674
1119
  let entity_name = entity.get("name").map(Json::to_s).unwrap_or_default();
675
1120
  let Some(target) = target else {
676
1121
  return Some(format!("{label}: {entity_name} declares no such command"));
677
1122
  };
678
1123
  if !crate::json_codec::extract_id_supported(entity) {
679
- return Some(format!("{label}: {entity_name} cannot be addressed by identity"));
1124
+ return Some(format!(
1125
+ "{label}: {entity_name} cannot be addressed by identity"
1126
+ ));
680
1127
  }
681
1128
  if let Some(problem) = entity_command_skip_reason(target, entity, value_objects_by_name) {
682
1129
  return Some(format!("{label}: {problem}"));
683
1130
  }
684
1131
  let mapping = delegate_mapping(delegation);
685
- let source_name_for = |name: &str| mapping.iter().find(|(t, _)| t == name).map(|(_, s)| s.clone()).unwrap_or_else(|| name.to_string());
686
- let argument_attrs = command.get("attributes").map(Json::each).unwrap_or(&[]);
687
- for attr in target.get("attributes").map(Json::each).unwrap_or(&[]) {
1132
+ let source_name_for = |name: &str| {
1133
+ mapping
1134
+ .iter()
1135
+ .find(|(t, _)| t == name)
1136
+ .map_or_else(|| name.to_string(), |(_, s)| s.clone())
1137
+ };
1138
+ let argument_attrs = command.get("attributes").map_or_default(Json::each);
1139
+ for attr in target.get("attributes").map_or_default(Json::each) {
688
1140
  let name = crate::attr::name(attr);
689
1141
  let source_name = source_name_for(name);
690
- let source = argument_attrs.iter().find(|a| crate::attr::name(a) == source_name);
1142
+ let source = argument_attrs
1143
+ .iter()
1144
+ .find(|a| crate::attr::name(a) == source_name);
691
1145
  let Some(source) = source else {
692
1146
  if crate::attr::optional(attr) {
693
1147
  continue;
694
1148
  }
695
- return Some(format!("{label}: target argument {name} has no source on the entry point"));
1149
+ return Some(format!(
1150
+ "{label}: target argument {name} has no source on the entry point"
1151
+ ));
696
1152
  };
697
1153
  if crate::attr::list(source) != crate::attr::list(attr) {
698
1154
  return Some(format!("{label}: entry point argument {source_name} is a list, target wants a scalar (or vice versa) — not generated yet"));
699
1155
  }
700
1156
  // Bridgeable, not exact type-name equality: the alias-then-deserialize delegate never
701
1157
  // compares declared type names (docs/decisions/0045).
702
- if !crate::bridging::bridgeable_value_types(crate::attr::type_name(source), crate::attr::type_name(attr), value_objects_by_name) {
1158
+ if !crate::bridging::bridgeable_value_types(
1159
+ crate::attr::type_name(source),
1160
+ crate::attr::type_name(attr),
1161
+ value_objects_by_name,
1162
+ ) {
703
1163
  return Some(format!("{label}: entry point argument {source_name} is {}, target wants {} — not generated yet", crate::attr::type_name(source), crate::attr::type_name(attr)));
704
1164
  }
705
1165
  if crate::attr::optional(source) && !crate::attr::optional(attr) {
706
- return Some(format!("{label}: optional entry point argument {source_name} feeds required {name}"));
1166
+ return Some(format!(
1167
+ "{label}: optional entry point argument {source_name} feeds required {name}"
1168
+ ));
707
1169
  }
708
1170
  }
709
- for path in entity.get("identified_by").map(Json::each).unwrap_or(&[]) {
1171
+ for path in entity.get("identified_by").map_or_default(Json::each) {
710
1172
  let path = path.to_s();
711
1173
  let head = path.split('.').next().unwrap_or("").to_string();
712
1174
  let source_name = source_name_for(&head);
713
- if argument_attrs.iter().any(|a| crate::attr::name(a) == source_name && !crate::attr::optional(a)) {
1175
+ if argument_attrs
1176
+ .iter()
1177
+ .any(|a| crate::attr::name(a) == source_name && !crate::attr::optional(a))
1178
+ {
714
1179
  continue;
715
1180
  }
716
- return Some(format!("{label}: the element's identity {head} has no source on the entry point"));
1181
+ return Some(format!(
1182
+ "{label}: the element's identity {head} has no source on the entry point"
1183
+ ));
717
1184
  }
718
1185
  None
719
1186
  }
@@ -727,30 +1194,59 @@ struct Delegation {
727
1194
  }
728
1195
 
729
1196
  fn indent_block(text: &str, indent: &str) -> String {
730
- text.lines().map(|l| format!("{indent}{l}")).collect::<Vec<_>>().join("\n").trim_end().to_string()
1197
+ text.lines()
1198
+ .map(|l| format!("{indent}{l}"))
1199
+ .collect::<Vec<_>>()
1200
+ .join("\n")
1201
+ .trim_end()
1202
+ .to_string()
731
1203
  }
732
1204
 
733
- fn delegation_of(exemplar: &Exemplar, command: &Json, aggregate: &Json, value_objects_by_name: &HashMap<String, &Json>, domain_name: &str) -> Option<Delegation> {
1205
+ #[allow(
1206
+ clippy::expect_used,
1207
+ clippy::panic,
1208
+ reason = "a drifted exemplar or invalid IR aborts generation on purpose; the message names an invariant an earlier check in this generator established"
1209
+ )]
1210
+ fn delegation_of(
1211
+ exemplar: &Exemplar,
1212
+ command: &Json,
1213
+ aggregate: &Json,
1214
+ value_objects_by_name: &HashMap<String, &Json>,
1215
+ domain_name: &str,
1216
+ ) -> Option<Delegation> {
734
1217
  let delegation = delegate_of(command)?;
735
1218
  let (entity, target) = delegate_target(delegation, aggregate);
736
- let (entity, target) = (entity.expect("delegate_skip_reason admitted the entity"), target.expect("delegate_skip_reason admitted the target"));
1219
+ let (entity, target) = (
1220
+ entity.expect("delegate_skip_reason admitted the entity"),
1221
+ target.expect("delegate_skip_reason admitted the target"),
1222
+ );
737
1223
  let entity_name = entity.get("name").map(Json::to_s).unwrap_or_default();
738
1224
  let aggregate_name = aggregate.get("name").map(Json::to_s).unwrap_or_default();
739
1225
  let list_attr = aggregate
740
1226
  .get("attributes")
741
- .map(Json::each)
742
- .unwrap_or(&[])
1227
+ .map_or_default(Json::each)
743
1228
  .iter()
744
1229
  .find(|a| crate::attr::list(a) && crate::attr::type_name(a) == entity_name)
745
1230
  .unwrap_or_else(|| panic!("{entity_name}: no list attribute on {aggregate_name} holds it"));
746
1231
 
747
1232
  let element_record = naming::rust_ident(&entity_name);
748
- let target_args_name = format!("{element_record}{}EntityArgs", naming::rust_ident(&target.get("name").map(Json::to_s).unwrap_or_default()));
749
- let aliases: Vec<String> = delegate_mapping(delegation).iter().map(|(t, s)| format!("({}, {})", naming::ruby_inspect_string(t), naming::ruby_inspect_string(s))).collect();
1233
+ let target_args_name = format!(
1234
+ "{element_record}{}EntityArgs",
1235
+ naming::rust_ident(&target.get("name").map(Json::to_s).unwrap_or_default())
1236
+ );
1237
+ let aliases: Vec<String> = delegate_mapping(delegation)
1238
+ .iter()
1239
+ .map(|(t, s)| {
1240
+ format!(
1241
+ "({}, {})",
1242
+ naming::ruby_inspect_string(t),
1243
+ naming::ruby_inspect_string(s)
1244
+ )
1245
+ })
1246
+ .collect();
750
1247
  let given_specs: Vec<String> = target
751
1248
  .get("givens")
752
- .map(Json::each)
753
- .unwrap_or(&[])
1249
+ .map_or_default(Json::each)
754
1250
  .iter()
755
1251
  .map(|g| {
756
1252
  let description = g.get("description").and_then(Json::as_str).unwrap_or("");
@@ -760,55 +1256,103 @@ fn delegation_of(exemplar: &Exemplar, command: &Json, aggregate: &Json, value_ob
760
1256
  .collect();
761
1257
  let ensures_specs: Vec<String> = target
762
1258
  .get("ensures")
763
- .map(Json::each)
764
- .unwrap_or(&[])
1259
+ .map_or_default(Json::each)
765
1260
  .iter()
766
1261
  .map(|e| {
767
1262
  let description = e.get("description").and_then(Json::as_str).unwrap_or("");
768
- let ast = e.get("ast").unwrap_or_else(|| panic!("ensures row has no ast: {e:?}"));
769
- format!(" crate::kernel::EnsuresSpec {{ description: {}, expr: {} }},", naming::ruby_inspect_string(description), crate::expr_emitter::emit_ast(ast))
1263
+ let ast = e
1264
+ .get("ast")
1265
+ .unwrap_or_else(|| panic!("ensures row has no ast: {e:?}"));
1266
+ format!(
1267
+ " crate::kernel::EnsuresSpec {{ description: {}, expr: {} }},",
1268
+ naming::ruby_inspect_string(description),
1269
+ crate::expr_emitter::emit_ast(ast)
1270
+ )
770
1271
  })
771
1272
  .collect();
772
1273
  let transition = mutations::lifecycle_transition_for(target, entity);
773
1274
  let transition_arg = mutations::transition_check_arg(transition.as_ref());
774
1275
  let mut mutation_lines: Vec<String> = target
775
1276
  .get("mutations")
776
- .map(Json::each)
777
- .unwrap_or(&[])
1277
+ .map_or_default(Json::each)
778
1278
  .iter()
779
- .map(|m| mutations::emit_mutation_line(exemplar, m, entity, target, value_objects_by_name, false))
1279
+ .map(|m| {
1280
+ mutations::emit_mutation_line(exemplar, m, entity, target, value_objects_by_name, false)
1281
+ })
780
1282
  .collect();
781
- if mutations::reads_pre_state(target.get("mutations").map(Json::each).unwrap_or(&[])) {
1283
+ if mutations::reads_pre_state(target.get("mutations").map_or_default(Json::each)) {
782
1284
  mutation_lines.insert(0, mutations::pre_state_line());
783
1285
  }
784
1286
  if let Some(t) = &transition {
785
1287
  if !t.to_state.is_empty() {
786
- mutation_lines.push(format!(" record.{} = {}.to_string();", naming::rust_ident_field(&t.field), naming::ruby_inspect_string(&t.to_state)));
1288
+ mutation_lines.push(format!(
1289
+ " record.{} = {}.to_string();",
1290
+ naming::rust_ident_field(&t.field),
1291
+ naming::ruby_inspect_string(&t.to_state)
1292
+ ));
787
1293
  }
788
1294
  }
789
1295
  if mutation_lines.is_empty() {
790
1296
  mutation_lines = vec![" let _ = record;".to_string()];
791
1297
  }
792
- let identity_reading = entity.get("identified_by").map(Json::each).unwrap_or(&[]).iter().map(Json::to_s).collect::<Vec<_>>().join(", ");
1298
+ let identity_reading = entity
1299
+ .get("identified_by")
1300
+ .map_or_default(Json::each)
1301
+ .iter()
1302
+ .map(Json::to_s)
1303
+ .collect::<Vec<_>>()
1304
+ .join(", ");
793
1305
 
794
- let prelude = exemplar.render("delegate_prelude", &[("tmpl_aliases_placeholder()", aliases.join(", ")), ("TmplTargetArgs", target_args_name)]);
795
- let element = exemplar.render("delegate_element", &[("TmplElement", element_record.clone())]);
1306
+ let prelude = exemplar.render(
1307
+ "delegate_prelude",
1308
+ &[
1309
+ ("tmpl_aliases_placeholder()", aliases.join(", ")),
1310
+ ("TmplTargetArgs", target_args_name),
1311
+ ],
1312
+ );
1313
+ let element = exemplar.render(
1314
+ "delegate_element",
1315
+ &[("TmplElement", element_record.clone())],
1316
+ );
796
1317
  let apply = exemplar.render(
797
1318
  "delegate_apply",
798
1319
  &[
799
1320
  ("TmplRecord", naming::rust_ident(&aggregate_name)),
800
- ("tmpl_list_field", naming::rust_ident_field(crate::attr::name(list_attr))),
1321
+ (
1322
+ "tmpl_list_field",
1323
+ naming::rust_ident_field(crate::attr::name(list_attr)),
1324
+ ),
801
1325
  ("TmplElement", element_record),
802
1326
  // Bare, not entity-qualified: it feeds refusal-message text, as in Ruby.
803
- ("\"TmplQualifiedCommandName\"", naming::ruby_inspect_string(&target.get("name").map(Json::to_s).unwrap_or_default())),
1327
+ (
1328
+ "\"TmplQualifiedCommandName\"",
1329
+ naming::ruby_inspect_string(
1330
+ &target.get("name").map(Json::to_s).unwrap_or_default(),
1331
+ ),
1332
+ ),
804
1333
  // Lets `apply_entity_command` (kernel/dispatch.rs) judge `corrects` admissibility.
805
- ("\"TmplQualifiedName\"", naming::ruby_inspect_string(&format!("{domain_name}::{aggregate_name}"))),
806
- ("\"TmplAggregateName\"", naming::ruby_inspect_string(&aggregate_name)),
807
- ("\"TmplEntityName\"", naming::ruby_inspect_string(&entity_name)),
808
- ("\"TmplEntityIdentityReading\"", naming::ruby_inspect_string(&identity_reading)),
1334
+ (
1335
+ "\"TmplQualifiedName\"",
1336
+ naming::ruby_inspect_string(&format!("{domain_name}::{aggregate_name}")),
1337
+ ),
1338
+ (
1339
+ "\"TmplAggregateName\"",
1340
+ naming::ruby_inspect_string(&aggregate_name),
1341
+ ),
1342
+ (
1343
+ "\"TmplEntityName\"",
1344
+ naming::ruby_inspect_string(&entity_name),
1345
+ ),
1346
+ (
1347
+ "\"TmplEntityIdentityReading\"",
1348
+ naming::ruby_inspect_string(&identity_reading),
1349
+ ),
809
1350
  ("tmpl_given_spec_placeholder(),", given_specs.join("\n")),
810
1351
  ("tmpl_transition_placeholder()", transition_arg),
811
- ("tmpl_entity_mutation_lines_placeholder(record);", mutation_lines.join("\n")),
1352
+ (
1353
+ "tmpl_entity_mutation_lines_placeholder(record);",
1354
+ mutation_lines.join("\n"),
1355
+ ),
812
1356
  ("tmpl_ensures_spec_placeholder(),", ensures_specs.join("\n")),
813
1357
  ],
814
1358
  );
@@ -816,16 +1360,30 @@ fn delegation_of(exemplar: &Exemplar, command: &Json, aggregate: &Json, value_ob
816
1360
  prelude: indent_block(&prelude, " "),
817
1361
  element: indent_block(&element, " "),
818
1362
  apply: indent_block(&apply, " "),
819
- emits: target.get("emits").map(Json::each).unwrap_or(&[]).iter().map(Json::to_s).collect(),
1363
+ emits: target
1364
+ .get("emits")
1365
+ .map_or_default(Json::each)
1366
+ .iter()
1367
+ .map(Json::to_s)
1368
+ .collect(),
820
1369
  })
821
1370
  }
822
1371
 
823
1372
  /// `command_skip_reason` for an entity command; an entity has the same IR shape as an aggregate.
824
- pub fn entity_command_skip_reason(command: &Json, entity: &Json, value_objects_by_name: &HashMap<String, &Json>) -> Option<SkipReason> {
1373
+ pub fn entity_command_skip_reason(
1374
+ command: &Json,
1375
+ entity: &Json,
1376
+ value_objects_by_name: &HashMap<String, &Json>,
1377
+ ) -> Option<SkipReason> {
825
1378
  command_skip_reason_with(command, entity, value_objects_by_name, false)
826
1379
  }
827
1380
 
828
1381
  /// Emits the dispatch function for an entity command.
1382
+ #[allow(clippy::too_many_arguments)] // established emitter signature
1383
+ #[allow(
1384
+ clippy::panic,
1385
+ reason = "a drifted exemplar or invalid IR aborts generation on purpose"
1386
+ )]
829
1387
  pub fn emit_entity_command(
830
1388
  exemplar: &Exemplar,
831
1389
  command: &Json,
@@ -836,135 +1394,180 @@ pub fn emit_entity_command(
836
1394
  aggregates_by_name: &HashMap<String, &Json>,
837
1395
  process_managers: &[Json],
838
1396
  ) -> String {
839
- let parent_record = naming::rust_ident(parent_aggregate.get("name").and_then(Json::as_str).unwrap_or(""));
840
- let element_record = naming::rust_ident(entity.get("name").and_then(Json::as_str).unwrap_or(""));
841
- let cmd = naming::rust_ident(command.get("name").and_then(Json::as_str).unwrap_or(""));
1397
+ let parent_record = naming::rust_ident(name_of(parent_aggregate));
1398
+ let element_record = naming::rust_ident(name_of(entity));
1399
+ let cmd = naming::rust_ident(name_of(command));
842
1400
 
843
- let aggregate_name = parent_aggregate.get("name").and_then(Json::as_str).unwrap_or("").to_string();
844
- let parent_identity_reading = parent_aggregate.get("identified_by").map(Json::each).unwrap_or(&[]).iter().map(Json::to_s).collect::<Vec<_>>().join(", ");
845
- let entity_name = entity.get("name").and_then(Json::as_str).unwrap_or("").to_string();
846
- let entity_identity_reading = entity.get("identified_by").map(Json::each).unwrap_or(&[]).iter().map(Json::to_s).collect::<Vec<_>>().join(", ");
1401
+ let aggregate_name = name_of(parent_aggregate).to_string();
1402
+ let parent_identity_reading = identity_reading(parent_aggregate);
1403
+ let entity_name = name_of(entity).to_string();
1404
+ let entity_identity_reading = identity_reading(entity);
847
1405
 
848
- let parent_attrs = parent_aggregate.get("attributes").map(Json::each).unwrap_or(&[]);
849
- let list_attr = parent_attrs
850
- .iter()
851
- .find(|a| crate::attr::list(a) && crate::attr::type_name(a) == entity.get("name").and_then(Json::as_str).unwrap_or(""))
852
- .unwrap_or_else(|| panic!("{entity_name}: no list attribute on {aggregate_name} holds it — unsupported_attribute_types should have caught this"));
853
- let list_field = naming::rust_ident_field(crate::attr::name(list_attr));
1406
+ let list_field = list_field_of(parent_aggregate, entity);
854
1407
  let args_struct_name = format!("{element_record}{cmd}EntityArgs");
855
1408
 
856
- let attrs = command.get("attributes").map(Json::each).unwrap_or(&[]);
857
- let mut args_struct = vec![format!("pub struct {args_struct_name} {{")];
858
- for attr in attrs {
859
- let mut ty = naming::rust_type(crate::attr::type_name(attr), crate::attr::list(attr));
860
- if crate::attr::optional(attr) {
861
- ty = format!("Option<{ty}>");
862
- }
863
- args_struct.push(format!(" {}", exemplar.render("struct_field", &[("TmplFieldType", ty), ("tmpl_field", naming::rust_ident_field(crate::attr::name(attr)))])));
864
- }
865
- args_struct.push("}".to_string());
1409
+ let attrs = command.get("attributes").map_or_default(Json::each);
1410
+ let args_struct = args_struct_text(exemplar, &args_struct_name, attrs);
866
1411
 
867
- let invariant_checks = invariant_checks_for(exemplar, command, aggregates_by_name, value_objects_by_name);
1412
+ let invariant_checks =
1413
+ invariant_checks_for(exemplar, command, aggregates_by_name, value_objects_by_name);
868
1414
 
869
1415
  // `corrects_given_specs` is prepended as in `emit_command`; `apply_entity_command` evaluates
870
1416
  // it against the parent record, never the entity's element.
871
- let givens = command.get("givens").map(Json::each).unwrap_or(&[]);
872
1417
  let mut given_specs: Vec<String> = crate::bridging::corrects_given_specs(command);
873
- given_specs.extend(givens.iter().map(|g| {
874
- let description = g.get("description").and_then(Json::as_str).unwrap_or("");
875
- let ast = g.get("ast").unwrap_or_else(|| panic!("given row has no ast: {g:?}"));
876
- format!(" crate::kernel::GivenSpec {{ description: {}, expr: {}, corrects_event: None }},", naming::ruby_inspect_string(description), crate::expr_emitter::emit_ast(ast))
877
- }));
878
-
879
- let ensures = command.get("ensures").map(Json::each).unwrap_or(&[]);
880
- let ensures_specs: Vec<String> = ensures
881
- .iter()
882
- .map(|e| {
883
- let description = e.get("description").and_then(Json::as_str).unwrap_or("");
884
- let ast = e.get("ast").unwrap_or_else(|| panic!("ensures row has no ast: {e:?}"));
885
- format!(" crate::kernel::EnsuresSpec {{ description: {}, expr: {} }},", naming::ruby_inspect_string(description), crate::expr_emitter::emit_ast(ast))
886
- })
887
- .collect();
1418
+ given_specs.extend(given_spec_lines(command, " "));
1419
+ let ensures_specs = ensures_spec_lines(command, " ");
888
1420
 
889
1421
  let transition = mutations::lifecycle_transition_for(command, entity);
890
1422
  let transition_arg = mutations::transition_check_arg(transition.as_ref());
891
1423
 
892
- let mutations_list = command.get("mutations").map(Json::each).unwrap_or(&[]);
893
- let mut mutation_lines: Vec<String> = mutations_list.iter().map(|m| mutations::emit_mutation_line(exemplar, m, entity, command, value_objects_by_name, false)).collect();
1424
+ let mutations_list = command.get("mutations").map_or_default(Json::each);
1425
+ let mut mutation_lines: Vec<String> = mutations_list
1426
+ .iter()
1427
+ .map(|m| {
1428
+ mutations::emit_mutation_line(
1429
+ exemplar,
1430
+ m,
1431
+ entity,
1432
+ command,
1433
+ value_objects_by_name,
1434
+ false,
1435
+ )
1436
+ })
1437
+ .collect();
894
1438
  if mutations::reads_pre_state(mutations_list) {
895
1439
  mutation_lines.insert(0, mutations::pre_state_line());
896
1440
  }
897
- if let Some(t) = &transition {
898
- if !t.to_state.is_empty() {
899
- mutation_lines.push(format!(" record.{} = {}.to_string();", naming::rust_ident_field(&t.field), naming::ruby_inspect_string(&t.to_state)));
900
- }
901
- }
902
- if mutation_lines.is_empty() {
903
- mutation_lines = vec![" let _ = record;".to_string()];
904
- }
1441
+ let mutation_lines = close_mutation_lines(mutation_lines, transition.as_ref(), " ");
905
1442
 
906
1443
  // Bare, not entity-qualified, as in `delegate_apply`.
907
- let qualified_command_name = command.get("name").and_then(Json::as_str).unwrap_or("").to_string();
908
- let emits = command.get("emits").map(Json::each).unwrap_or(&[]);
909
- let emits_expr = emits.iter().map(|e| naming::ruby_inspect_string(&e.to_s())).collect::<Vec<_>>().join(", ");
1444
+ let qualified_command_name = name_of(command).to_string();
1445
+ let emits_expr = emits_expr_of(command);
910
1446
 
911
1447
  let entity_dispatch_fn = exemplar.render(
912
1448
  "entity_dispatch_fn",
913
1449
  &[
914
- ("dispatch_entity_tmpl", format!("dispatch_entity_{}_{}", entity.get("name").and_then(Json::as_str).unwrap_or("").to_lowercase(), naming::dispatch_fn_name(&cmd))),
1450
+ (
1451
+ "dispatch_entity_tmpl",
1452
+ format!(
1453
+ "dispatch_entity_{}_{}",
1454
+ entity
1455
+ .get("name")
1456
+ .and_then(Json::as_str)
1457
+ .unwrap_or("")
1458
+ .to_lowercase(),
1459
+ naming::dispatch_fn_name(&cmd)
1460
+ ),
1461
+ ),
915
1462
  ("TmplRecord", parent_record),
916
1463
  ("TmplArgs", args_struct_name.clone()),
917
- ("tmpl_deref_params_placeholder: ()", deref_params().join(", ")),
918
- ("tmpl_invariant_check_placeholder()?;", invariant_checks.join("\n")),
919
- ("let tmpl_eval_fielded = tmpl_with_references_placeholder();", with_references_binding()),
1464
+ (
1465
+ "tmpl_deref_params_placeholder: ()",
1466
+ deref_params().join(", "),
1467
+ ),
1468
+ (
1469
+ "tmpl_invariant_check_placeholder()?;",
1470
+ invariant_checks.join("\n"),
1471
+ ),
1472
+ (
1473
+ "let tmpl_eval_fielded = tmpl_with_references_placeholder();",
1474
+ with_references_binding(),
1475
+ ),
920
1476
  ("&tmpl_eval_fielded,", "&with_references,".to_string()),
921
- ("let tmpl_seed_projections = tmpl_seed_projections_placeholder();", seed_projections_binding(parent_aggregate)),
1477
+ (
1478
+ "let tmpl_seed_projections = tmpl_seed_projections_placeholder();",
1479
+ seed_projections_binding(parent_aggregate),
1480
+ ),
922
1481
  ("tmpl_seed_projections,", "seed_projections,".to_string()),
923
1482
  ("tmpl_list_field", list_field),
924
1483
  ("TmplElement", element_record),
925
- ("\"TmplQualifiedCommandName\"", naming::ruby_inspect_string(&qualified_command_name)),
926
- ("\"TmplQualifiedName\"", naming::ruby_inspect_string(&format!("{domain_name}::{aggregate_name}"))),
927
- ("\"TmplAggregateName\"", naming::ruby_inspect_string(&aggregate_name)),
928
- ("\"TmplParentIdentityReading\"", naming::ruby_inspect_string(&parent_identity_reading)),
929
- ("\"TmplEntityName\"", naming::ruby_inspect_string(&entity_name)),
930
- ("\"TmplEntityIdentityReading\"", naming::ruby_inspect_string(&entity_identity_reading)),
1484
+ (
1485
+ "\"TmplQualifiedCommandName\"",
1486
+ naming::ruby_inspect_string(&qualified_command_name),
1487
+ ),
1488
+ (
1489
+ "\"TmplQualifiedName\"",
1490
+ naming::ruby_inspect_string(&format!("{domain_name}::{aggregate_name}")),
1491
+ ),
1492
+ (
1493
+ "\"TmplAggregateName\"",
1494
+ naming::ruby_inspect_string(&aggregate_name),
1495
+ ),
1496
+ (
1497
+ "\"TmplParentIdentityReading\"",
1498
+ naming::ruby_inspect_string(&parent_identity_reading),
1499
+ ),
1500
+ (
1501
+ "\"TmplEntityName\"",
1502
+ naming::ruby_inspect_string(&entity_name),
1503
+ ),
1504
+ (
1505
+ "\"TmplEntityIdentityReading\"",
1506
+ naming::ruby_inspect_string(&entity_identity_reading),
1507
+ ),
931
1508
  ("tmpl_given_spec_placeholder(),", given_specs.join("\n")),
932
1509
  ("tmpl_transition_placeholder()", transition_arg),
933
- ("tmpl_entity_mutation_lines_placeholder(record);", mutation_lines.join("\n")),
1510
+ (
1511
+ "tmpl_entity_mutation_lines_placeholder(record);",
1512
+ mutation_lines.join("\n"),
1513
+ ),
934
1514
  ("tmpl_ensures_spec_placeholder(),", ensures_specs.join("\n")),
935
- ("tmpl_invariants_placeholder()", format!("{}()", invariants_fn_name(parent_aggregate))),
1515
+ (
1516
+ "tmpl_invariants_placeholder()",
1517
+ format!("{}()", invariants_fn_name(parent_aggregate)),
1518
+ ),
936
1519
  ("tmpl_emit_placeholder()", emits_expr),
937
1520
  ],
938
1521
  );
939
1522
 
940
1523
  [
941
- crate::fielded::emit_fielded_flat(exemplar, &args_struct_name, attrs, value_objects_by_name, &[]),
942
- format!("#[derive(Debug, Clone)]\n{}", args_struct.join("\n")),
943
- crate::json_codec::emit_to_json_flat_sparse(exemplar, &args_struct_name, attrs, value_objects_by_name),
1524
+ crate::fielded::emit_fielded_flat(
1525
+ exemplar,
1526
+ &args_struct_name,
1527
+ attrs,
1528
+ value_objects_by_name,
1529
+ &[],
1530
+ ),
1531
+ format!("#[derive(Debug, Clone)]\n{args_struct}"),
1532
+ crate::json_codec::emit_to_json_flat_sparse(exemplar, &args_struct_name, attrs),
944
1533
  {
945
1534
  // `unknown_argument_allowlist:` as for an aggregate command, plus the entity's
946
1535
  // identity head (`extra_identity_heads:` in Ruby's `ArgumentGate`).
947
- let entity_identity_heads: Vec<String> = entity
948
- .get("identified_by")
949
- .map(Json::each)
950
- .unwrap_or(&[])
951
- .iter()
952
- .map(|p| p.to_s().split('.').next().unwrap_or("").to_string())
953
- .collect();
954
- let allowlist = crate::json_codec::command_argument_allowlist(parent_aggregate, command, process_managers, &entity_identity_heads);
955
- crate::json_codec::emit_from_json_flat(exemplar, &args_struct_name, attrs, value_objects_by_name, Some(&allowlist), Some(&qualified_command_name), true, true, Some(aggregates_by_name))
1536
+ let entity_identity_heads = identity_heads(entity);
1537
+ let allowlist = crate::json_codec::command_argument_allowlist(
1538
+ parent_aggregate,
1539
+ command,
1540
+ process_managers,
1541
+ &entity_identity_heads,
1542
+ );
1543
+ crate::json_codec::emit_from_json_flat(
1544
+ exemplar,
1545
+ &args_struct_name,
1546
+ attrs,
1547
+ value_objects_by_name,
1548
+ Some(&allowlist),
1549
+ Some(&qualified_command_name),
1550
+ true,
1551
+ true,
1552
+ Some(aggregates_by_name),
1553
+ None,
1554
+ )
956
1555
  },
957
1556
  // Argument gates, run by `kernel::decode_entity_arguments` in `EntityStep::ORDER`.
958
1557
  {
959
- let entity_identity_heads: Vec<String> = entity
960
- .get("identified_by")
961
- .map(Json::each)
962
- .unwrap_or(&[])
963
- .iter()
964
- .map(|p| p.to_s().split('.').next().unwrap_or("").to_string())
965
- .collect();
966
- let allowlist = crate::json_codec::command_argument_allowlist(parent_aggregate, command, process_managers, &entity_identity_heads);
967
- crate::json_codec::emit_argument_gates(&args_struct_name, &qualified_command_name, attrs, Some(&allowlist))
1558
+ let entity_identity_heads = identity_heads(entity);
1559
+ let allowlist = crate::json_codec::command_argument_allowlist(
1560
+ parent_aggregate,
1561
+ command,
1562
+ process_managers,
1563
+ &entity_identity_heads,
1564
+ );
1565
+ crate::json_codec::emit_argument_gates(
1566
+ &args_struct_name,
1567
+ &qualified_command_name,
1568
+ attrs,
1569
+ Some(&allowlist),
1570
+ )
968
1571
  },
969
1572
  entity_dispatch_fn,
970
1573
  ]
@@ -973,6 +1576,11 @@ pub fn emit_entity_command(
973
1576
 
974
1577
  /// Emits the dispatch function for a command on an entity nested two levels deep.
975
1578
  /// Takes plain `&str` ids per hop, so routed and flat-args callers can both use it.
1579
+ #[allow(clippy::too_many_arguments)] // established emitter signature
1580
+ #[allow(
1581
+ clippy::panic,
1582
+ reason = "a drifted exemplar or invalid IR aborts generation on purpose"
1583
+ )]
976
1584
  pub fn emit_nested_entity_command(
977
1585
  exemplar: &Exemplar,
978
1586
  command: &Json,
@@ -984,96 +1592,66 @@ pub fn emit_nested_entity_command(
984
1592
  aggregates_by_name: &HashMap<String, &Json>,
985
1593
  process_managers: &[Json],
986
1594
  ) -> String {
987
- let parent_record = naming::rust_ident(parent_aggregate.get("name").and_then(Json::as_str).unwrap_or(""));
988
- let entity_record = naming::rust_ident(entity.get("name").and_then(Json::as_str).unwrap_or(""));
989
- let nested_record = naming::rust_ident(nested.get("name").and_then(Json::as_str).unwrap_or(""));
990
- let cmd = naming::rust_ident(command.get("name").and_then(Json::as_str).unwrap_or(""));
991
-
992
- let aggregate_name = parent_aggregate.get("name").and_then(Json::as_str).unwrap_or("").to_string();
993
- let parent_identity_reading = parent_aggregate.get("identified_by").map(Json::each).unwrap_or(&[]).iter().map(Json::to_s).collect::<Vec<_>>().join(", ");
994
- let entity_name = entity.get("name").and_then(Json::as_str).unwrap_or("").to_string();
995
- let entity_identity_reading = entity.get("identified_by").map(Json::each).unwrap_or(&[]).iter().map(Json::to_s).collect::<Vec<_>>().join(", ");
996
- let nested_name = nested.get("name").and_then(Json::as_str).unwrap_or("").to_string();
997
- let nested_identity_reading = nested.get("identified_by").map(Json::each).unwrap_or(&[]).iter().map(Json::to_s).collect::<Vec<_>>().join(", ");
998
-
999
- let parent_attrs = parent_aggregate.get("attributes").map(Json::each).unwrap_or(&[]);
1000
- let list_attr1 = parent_attrs
1001
- .iter()
1002
- .find(|a| crate::attr::list(a) && crate::attr::type_name(a) == entity.get("name").and_then(Json::as_str).unwrap_or(""))
1003
- .unwrap_or_else(|| panic!("{entity_name}: no list attribute on {aggregate_name} holds it — unsupported_attribute_types should have caught this"));
1004
- let list_field1 = naming::rust_ident_field(crate::attr::name(list_attr1));
1595
+ let parent_record = naming::rust_ident(name_of(parent_aggregate));
1596
+ let entity_record = naming::rust_ident(name_of(entity));
1597
+ let nested_record = naming::rust_ident(name_of(nested));
1598
+ let cmd = naming::rust_ident(name_of(command));
1005
1599
 
1006
- let entity_attrs = entity.get("attributes").map(Json::each).unwrap_or(&[]);
1007
- let list_attr2 = entity_attrs
1008
- .iter()
1009
- .find(|a| crate::attr::list(a) && crate::attr::type_name(a) == nested.get("name").and_then(Json::as_str).unwrap_or(""))
1010
- .unwrap_or_else(|| panic!("{nested_name}: no list attribute on {entity_name} holds it — unsupported_attribute_types should have caught this"));
1011
- let list_field2 = naming::rust_ident_field(crate::attr::name(list_attr2));
1600
+ let aggregate_name = name_of(parent_aggregate).to_string();
1601
+ let parent_identity_reading = identity_reading(parent_aggregate);
1602
+ let entity_name = name_of(entity).to_string();
1603
+ let entity_identity_reading = identity_reading(entity);
1604
+ let nested_name = name_of(nested).to_string();
1605
+ let nested_identity_reading = identity_reading(nested);
1012
1606
 
1013
- let args_struct_name = format!("{nested_record}{cmd}NestedEntityArgs");
1607
+ let list_field1 = list_field_of(parent_aggregate, entity);
1608
+ let list_field2 = list_field_of(entity, nested);
1014
1609
 
1015
- let attrs = command.get("attributes").map(Json::each).unwrap_or(&[]);
1016
- let mut args_struct = vec![format!("pub struct {args_struct_name} {{")];
1017
- for attr in attrs {
1018
- let mut ty = naming::rust_type(crate::attr::type_name(attr), crate::attr::list(attr));
1019
- if crate::attr::optional(attr) {
1020
- ty = format!("Option<{ty}>");
1021
- }
1022
- args_struct.push(format!(" {}", exemplar.render("struct_field", &[("TmplFieldType", ty), ("tmpl_field", naming::rust_ident_field(crate::attr::name(attr)))])));
1023
- }
1024
- args_struct.push("}".to_string());
1610
+ let args_struct_name = format!("{nested_record}{cmd}NestedEntityArgs");
1025
1611
 
1026
- let invariant_checks = invariant_checks_for(exemplar, command, aggregates_by_name, value_objects_by_name);
1612
+ let attrs = command.get("attributes").map_or_default(Json::each);
1613
+ let args_struct = args_struct_text(exemplar, &args_struct_name, attrs);
1027
1614
 
1028
- let givens = command.get("givens").map(Json::each).unwrap_or(&[]);
1029
- let given_specs: Vec<String> = givens
1030
- .iter()
1031
- .map(|g| {
1032
- let description = g.get("description").and_then(Json::as_str).unwrap_or("");
1033
- let ast = g.get("ast").unwrap_or_else(|| panic!("given row has no ast: {g:?}"));
1034
- format!(" crate::kernel::GivenSpec {{ description: {}, expr: {}, corrects_event: None }},", naming::ruby_inspect_string(description), crate::expr_emitter::emit_ast(ast))
1035
- })
1036
- .collect();
1615
+ let invariant_checks =
1616
+ invariant_checks_for(exemplar, command, aggregates_by_name, value_objects_by_name);
1037
1617
 
1038
- let ensures = command.get("ensures").map(Json::each).unwrap_or(&[]);
1039
- let ensures_specs: Vec<String> = ensures
1040
- .iter()
1041
- .map(|e| {
1042
- let description = e.get("description").and_then(Json::as_str).unwrap_or("");
1043
- let ast = e.get("ast").unwrap_or_else(|| panic!("ensures row has no ast: {e:?}"));
1044
- format!(" crate::kernel::EnsuresSpec {{ description: {}, expr: {} }},", naming::ruby_inspect_string(description), crate::expr_emitter::emit_ast(ast))
1045
- })
1046
- .collect();
1618
+ let given_specs = given_spec_lines(command, " ");
1619
+ let ensures_specs = ensures_spec_lines(command, " ");
1047
1620
 
1048
1621
  // The nested entity's lifecycle, as in `emit_entity_command`.
1049
1622
  let transition = mutations::lifecycle_transition_for(command, nested);
1050
1623
  let transition_arg = mutations::transition_check_arg(transition.as_ref());
1051
1624
 
1052
- let mutations_list = command.get("mutations").map(Json::each).unwrap_or(&[]);
1053
- let mut mutation_lines: Vec<String> = mutations_list.iter().map(|m| mutations::emit_mutation_line(exemplar, m, nested, command, value_objects_by_name, false)).collect();
1625
+ let mutations_list = command.get("mutations").map_or_default(Json::each);
1626
+ let mut mutation_lines: Vec<String> = mutations_list
1627
+ .iter()
1628
+ .map(|m| {
1629
+ mutations::emit_mutation_line(
1630
+ exemplar,
1631
+ m,
1632
+ nested,
1633
+ command,
1634
+ value_objects_by_name,
1635
+ false,
1636
+ )
1637
+ })
1638
+ .collect();
1054
1639
  if mutations::reads_pre_state(mutations_list) {
1055
1640
  mutation_lines.insert(0, mutations::pre_state_line());
1056
1641
  }
1057
- if let Some(t) = &transition {
1058
- if !t.to_state.is_empty() {
1059
- mutation_lines.push(format!(" record.{} = {}.to_string();", naming::rust_ident_field(&t.field), naming::ruby_inspect_string(&t.to_state)));
1060
- }
1061
- }
1062
- if mutation_lines.is_empty() {
1063
- mutation_lines = vec![" let _ = record;".to_string()];
1064
- }
1642
+ let mutation_lines =
1643
+ close_mutation_lines(mutation_lines, transition.as_ref(), " ");
1065
1644
 
1066
1645
  // Bare, as in `emit_entity_command`.
1067
- let qualified_command_name = command.get("name").and_then(Json::as_str).unwrap_or("").to_string();
1646
+ let qualified_command_name = name_of(command).to_string();
1068
1647
  // The hop-2 `apply_entity_command` gets the root aggregate's qualified name only to satisfy
1069
1648
  // the signature: its `record` is the hop-1 entity, so `corrects` admissibility is not checked
1070
1649
  // there. Its trailing `parent_in_args` literal is `true`, byte for byte as Ruby emits.
1071
- let emits = command.get("emits").map(Json::each).unwrap_or(&[]);
1072
- let emits_expr = emits.iter().map(|e| naming::ruby_inspect_string(&e.to_s())).collect::<Vec<_>>().join(", ");
1650
+ let emits_expr = emits_expr_of(command);
1073
1651
  let fn_name = format!(
1074
1652
  "dispatch_entity_{}_{}_{}",
1075
- entity.get("name").and_then(Json::as_str).unwrap_or("").to_lowercase(),
1076
- nested.get("name").and_then(Json::as_str).unwrap_or("").to_lowercase(),
1653
+ name_of(entity).to_lowercase(),
1654
+ name_of(nested).to_lowercase(),
1077
1655
  naming::dispatch_fn_name(&cmd)
1078
1656
  );
1079
1657
 
@@ -1154,48 +1732,76 @@ pub fn emit_nested_entity_command(
1154
1732
  );
1155
1733
 
1156
1734
  [
1157
- crate::fielded::emit_fielded_flat(exemplar, &args_struct_name, attrs, value_objects_by_name, &[]),
1158
- format!("#[derive(Debug, Clone)]\n{}", args_struct.join("\n")),
1159
- crate::json_codec::emit_to_json_flat_sparse(exemplar, &args_struct_name, attrs, value_objects_by_name),
1735
+ crate::fielded::emit_fielded_flat(
1736
+ exemplar,
1737
+ &args_struct_name,
1738
+ attrs,
1739
+ value_objects_by_name,
1740
+ &[],
1741
+ ),
1742
+ format!("#[derive(Debug, Clone)]\n{args_struct}"),
1743
+ crate::json_codec::emit_to_json_flat_sparse(exemplar, &args_struct_name, attrs),
1160
1744
  {
1161
1745
  // Both hops' identity heads, as `ctx.chain.flat_map(&:identity_heads)` in Ruby.
1162
1746
  let mut identity_heads: Vec<String> = entity
1163
1747
  .get("identified_by")
1164
- .map(Json::each)
1165
- .unwrap_or(&[])
1748
+ .map_or_default(Json::each)
1166
1749
  .iter()
1167
1750
  .map(|p| p.to_s().split('.').next().unwrap_or("").to_string())
1168
1751
  .collect();
1169
1752
  identity_heads.extend(
1170
1753
  nested
1171
1754
  .get("identified_by")
1172
- .map(Json::each)
1173
- .unwrap_or(&[])
1755
+ .map_or_default(Json::each)
1174
1756
  .iter()
1175
1757
  .map(|p| p.to_s().split('.').next().unwrap_or("").to_string()),
1176
1758
  );
1177
- let allowlist = crate::json_codec::command_argument_allowlist(parent_aggregate, command, process_managers, &identity_heads);
1178
- crate::json_codec::emit_from_json_flat(exemplar, &args_struct_name, attrs, value_objects_by_name, Some(&allowlist), Some(&qualified_command_name), true, true, Some(aggregates_by_name))
1759
+ let allowlist = crate::json_codec::command_argument_allowlist(
1760
+ parent_aggregate,
1761
+ command,
1762
+ process_managers,
1763
+ &identity_heads,
1764
+ );
1765
+ crate::json_codec::emit_from_json_flat(
1766
+ exemplar,
1767
+ &args_struct_name,
1768
+ attrs,
1769
+ value_objects_by_name,
1770
+ Some(&allowlist),
1771
+ Some(&qualified_command_name),
1772
+ true,
1773
+ true,
1774
+ Some(aggregates_by_name),
1775
+ None,
1776
+ )
1179
1777
  },
1180
1778
  // Argument gates, as in the one-hop entity command.
1181
1779
  {
1182
1780
  let mut identity_heads: Vec<String> = entity
1183
1781
  .get("identified_by")
1184
- .map(Json::each)
1185
- .unwrap_or(&[])
1782
+ .map_or_default(Json::each)
1186
1783
  .iter()
1187
1784
  .map(|p| p.to_s().split('.').next().unwrap_or("").to_string())
1188
1785
  .collect();
1189
1786
  identity_heads.extend(
1190
1787
  nested
1191
1788
  .get("identified_by")
1192
- .map(Json::each)
1193
- .unwrap_or(&[])
1789
+ .map_or_default(Json::each)
1194
1790
  .iter()
1195
1791
  .map(|p| p.to_s().split('.').next().unwrap_or("").to_string()),
1196
1792
  );
1197
- let allowlist = crate::json_codec::command_argument_allowlist(parent_aggregate, command, process_managers, &identity_heads);
1198
- crate::json_codec::emit_argument_gates(&args_struct_name, &qualified_command_name, attrs, Some(&allowlist))
1793
+ let allowlist = crate::json_codec::command_argument_allowlist(
1794
+ parent_aggregate,
1795
+ command,
1796
+ process_managers,
1797
+ &identity_heads,
1798
+ );
1799
+ crate::json_codec::emit_argument_gates(
1800
+ &args_struct_name,
1801
+ &qualified_command_name,
1802
+ attrs,
1803
+ Some(&allowlist),
1804
+ )
1199
1805
  },
1200
1806
  nested_dispatch_fn,
1201
1807
  ]