hecks 3.14.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (427) hide show
  1. checksums.yaml +4 -4
  2. data/examples/pizzas/adapters/fake_processor.rb +34 -0
  3. data/examples/pizzas/adapters/payment_signature.rb +66 -0
  4. data/examples/pizzas/adapters/payment_webhook.rb +87 -0
  5. data/examples/pizzas/bluebook/pizzas.behaviors +33 -0
  6. data/examples/pizzas/bluebook/pizzas.hecksagon +5 -3
  7. data/examples/pizzas/config.ru +22 -0
  8. data/exe/hecks +1 -1
  9. data/lib/hecks/adapters/driven/prism.rb +0 -2
  10. data/lib/hecks/adapters/driven.rb +0 -2
  11. data/lib/hecks/adapters/driving/json.rb +9 -16
  12. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -2
  13. data/lib/hecks/bluebook/dsl/derivation_words.rb +58 -0
  14. data/lib/hecks/bluebook/dsl/translation_builder.rb +6 -37
  15. data/lib/hecks/bluebook/expression/ast_json.rb +0 -9
  16. data/lib/hecks/bluebook/hecksagon.rb +1 -1
  17. data/lib/hecks/bluebook/meta_validator/translation_judge/rules.rb +7 -10
  18. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +4 -8
  19. data/lib/hecks/bluebook/meta_validator.rb +0 -11
  20. data/lib/hecks/bluebook/translation.rb +18 -10
  21. data/lib/hecks/bluebook/translation_expression/evaluator.rb +63 -0
  22. data/lib/hecks/bluebook/translation_expression/parser.rb +127 -0
  23. data/lib/hecks/bluebook/translation_expression.rb +39 -0
  24. data/lib/hecks/bluebook/translation_plan.rb +70 -0
  25. data/lib/hecks/bluebook.rb +2 -0
  26. data/lib/hecks/chapters.rb +3 -3
  27. data/lib/hecks/cli/project_cli/launcher_source.rb +1 -1
  28. data/lib/hecks/composer/adapters/claude_composer.adapter +3 -0
  29. data/lib/hecks/composer/adapters/claude_composer.rb +83 -0
  30. data/lib/hecks/composer/adapters/fake_agent.adapter +3 -0
  31. data/lib/hecks/composer/adapters/fake_agent.rb +55 -0
  32. data/lib/hecks/composer/adapters/jev.adapter +3 -0
  33. data/lib/hecks/composer/adapters/jev.rb +66 -0
  34. data/lib/hecks/composer/agent.rb +113 -0
  35. data/lib/hecks/composer/assistant.rb +110 -0
  36. data/lib/hecks/composer/behavior_run.rb +53 -0
  37. data/lib/hecks/composer/bluebook/composer.bluebook +178 -0
  38. data/lib/hecks/composer/bluebook/composer.hecksagon +16 -0
  39. data/lib/hecks/composer/board.rb +65 -0
  40. data/lib/hecks/composer/board_edit/command_text.rb +39 -0
  41. data/lib/hecks/composer/board_edit/source.rb +84 -0
  42. data/lib/hecks/composer/board_edit.rb +144 -0
  43. data/lib/hecks/composer/check.rb +89 -0
  44. data/lib/hecks/composer/domains.rb +81 -0
  45. data/lib/hecks/composer/jobs.rb +134 -0
  46. data/lib/hecks/composer/layout.rb +37 -0
  47. data/lib/hecks/composer/live.rb +150 -0
  48. data/lib/hecks/composer/outline.rb +48 -0
  49. data/lib/hecks/composer/page.html +989 -0
  50. data/lib/hecks/composer/panels.rb +142 -0
  51. data/lib/hecks/composer/parse.rb +83 -0
  52. data/lib/hecks/composer/plain.rb +28 -0
  53. data/lib/hecks/composer/ports/composer_agent.port +7 -0
  54. data/lib/hecks/composer/session.rb +141 -0
  55. data/lib/hecks/composer/toolbox.rb +166 -0
  56. data/lib/hecks/composer/web.rb +156 -0
  57. data/lib/hecks/composer/workbench.rb +51 -0
  58. data/lib/hecks/composer/workspace.rb +81 -0
  59. data/lib/hecks/composer.rb +17 -0
  60. data/lib/hecks/corpus/adapters.rb +22 -0
  61. data/lib/hecks/corpus/rust.rb +1 -1
  62. data/lib/hecks/corpus/worlds.rb +19 -0
  63. data/lib/hecks/corpus.rb +5 -0
  64. data/lib/hecks/forms/app/commands.rb +2 -3
  65. data/lib/hecks/forms/app/queries.rb +2 -3
  66. data/lib/hecks/forms/app/records.rb +17 -5
  67. data/lib/hecks/forms/app/responses.rb +3 -1
  68. data/lib/hecks/forms/command_form_renderer.rb +42 -16
  69. data/lib/hecks/forms/field_controls.rb +2 -2
  70. data/lib/hecks/forms/field_renderer.rb +1 -1
  71. data/lib/hecks/forms/field_shape.rb +36 -17
  72. data/lib/hecks/forms/index_renderer.rb +2 -1
  73. data/lib/hecks/forms/page.rb +1 -1
  74. data/lib/hecks/forms/params.rb +0 -13
  75. data/lib/hecks/forms/query_form_renderer.rb +7 -6
  76. data/lib/hecks/forms/record_renderer.rb +11 -7
  77. data/lib/hecks/forms/rule_words.rb +100 -0
  78. data/lib/hecks/forms/value_object_shape.rb +2 -1
  79. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -3
  80. data/lib/hecks/fuzzing/isolated_boot/worlds.rb +3 -6
  81. data/lib/hecks/fuzzing/replay/filters.rb +2 -6
  82. data/lib/hecks/gate/stages.yml +25 -0
  83. data/lib/hecks/grammar/evolve/seeds.rb +0 -6
  84. data/lib/hecks/grammar/evolve.rb +0 -3
  85. data/lib/hecks/grammar/translation.bluebook +1 -1
  86. data/lib/hecks/hecks/adapters/codebase/composing.rb +56 -0
  87. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +3 -1
  88. data/lib/hecks/hecks/adapters/first_admin.rb +3 -3
  89. data/lib/hecks/hecks/adapters/journal_store/changes.rb +0 -26
  90. data/lib/hecks/hecks/adapters/journal_store/examination.rb +2 -23
  91. data/lib/hecks/hecks/adapters/journal_store/readings.rb +3 -11
  92. data/lib/hecks/hecks/adapters/journal_store.rb +0 -11
  93. data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +4 -11
  94. data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +0 -9
  95. data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +0 -4
  96. data/lib/hecks/hecks/codebase.behaviors +6 -0
  97. data/lib/hecks/hecks/codebase.bluebook +21 -0
  98. data/lib/hecks/hecks/composer.behaviors +68 -0
  99. data/lib/hecks/hecks/context_map.hecksagon +1 -1
  100. data/lib/hecks/hecks/custodian.behaviors +0 -6
  101. data/lib/hecks/hecks/custodian.bluebook +3 -80
  102. data/lib/hecks/hecks/hecks.hecksagon +2 -2
  103. data/lib/hecks/hecks/hecks.world +1 -1
  104. data/lib/hecks/hecks/rehearsal.bluebook +7 -13
  105. data/lib/hecks/kernel_capabilities/templates.rb +4 -4
  106. data/lib/hecks/kernel_capabilities.rb +1 -1
  107. data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -3
  108. data/lib/hecks/language/translation/translation_aggregate.bluebook +21 -24
  109. data/lib/hecks/ports/access_control.rb +2 -2
  110. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -41
  111. data/lib/hecks/ports/persistence/plugins/era/lineage/coverage.rb +7 -7
  112. data/lib/hecks/ports/persistence/plugins/era/lineage/rewrite.rb +25 -2
  113. data/lib/hecks/ports/persistence/plugins/era/lineage/rules.rb +4 -4
  114. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +17 -25
  115. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/base_sql.rb +0 -12
  116. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/domain_rename.rb +1 -1
  117. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -22
  118. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +0 -1
  119. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +9 -3
  120. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +3 -29
  121. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/refusals.rb +2 -26
  122. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -33
  123. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -3
  124. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +2 -4
  125. data/lib/hecks/ports/persistence/plugins/era/translation/expression_sql.rb +52 -0
  126. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +23 -29
  127. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +1 -1
  128. data/lib/hecks/ports/persistence/plugins/era/translation.rb +0 -1
  129. data/lib/hecks/ports/persistence/plugins/era.rb +3 -6
  130. data/lib/hecks/ports.rb +0 -1
  131. data/lib/hecks/projection_files.rb +3 -1
  132. data/lib/hecks/projections/deploy/templates/lambda/nat_comment_oauth.tmpl +1 -1
  133. data/lib/hecks/projections/glossary/entries.rb +10 -4
  134. data/lib/hecks/projections/glossary/groups.rb +42 -0
  135. data/lib/hecks/projections/glossary/html/page.rb +7 -2
  136. data/lib/hecks/projections/glossary/html/parser.rb +1 -1
  137. data/lib/hecks/projections/glossary/html/sections.rb +31 -11
  138. data/lib/hecks/projections/glossary/html.rb +1 -2
  139. data/lib/hecks/projections/glossary/markdown.rb +31 -28
  140. data/lib/hecks/projections/glossary/mermaid.rb +3 -0
  141. data/lib/hecks/projections/glossary/page.css +143 -153
  142. data/lib/hecks/projections/glossary/page.js +63 -3
  143. data/lib/hecks/projections/glossary/sentences/actions.rb +88 -0
  144. data/lib/hecks/projections/glossary/sentences/paths.rb +77 -0
  145. data/lib/hecks/projections/glossary/sentences/queries.rb +90 -0
  146. data/lib/hecks/projections/glossary/sentences/value_objects.rb +32 -5
  147. data/lib/hecks/projections/glossary/sentences.rb +13 -3
  148. data/lib/hecks/projections/glossary/slugs.rb +4 -1
  149. data/lib/hecks/projections/glossary.rb +2 -1
  150. data/lib/hecks/projections/plain_english.rb +97 -0
  151. data/lib/hecks/projections/rust_vocabulary/arguments.rb +2 -2
  152. data/lib/hecks/projections/rust_vocabulary/enum_body.rb +20 -25
  153. data/lib/hecks/projections/rust_vocabulary/tests.rb +2 -2
  154. data/lib/hecks/projections/rust_vocabulary.rb +14 -6
  155. data/lib/hecks/projections/site/cms_editor/color.rb +0 -6
  156. data/lib/hecks/projections/site/cms_editor/templates/src/host.ts.tmpl +7 -2
  157. data/lib/hecks/projections/statements/actions.rb +99 -0
  158. data/lib/hecks/projections/statements/relationships.rb +86 -0
  159. data/lib/hecks/projections/statements.rb +15 -24
  160. data/lib/hecks/projector/exporter/adapters.rb +112 -0
  161. data/lib/hecks/projector/exporter/translations.rb +18 -35
  162. data/lib/hecks/projector/exporter/worlds.rb +103 -0
  163. data/lib/hecks/projector/exporter.rb +5 -1
  164. data/lib/hecks/quality_control/adapters/agent.rb +6 -7
  165. data/lib/hecks/quality_control/adapters/agent_profile.rb +16 -114
  166. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +1 -1
  167. data/lib/hecks/runtime/loader/boot_steps.rb +0 -20
  168. data/lib/hecks/runtime/outbox.rb +2 -9
  169. data/lib/hecks/runtime/port_operation_interpreter.rb +1 -9
  170. data/lib/hecks/runtime/remote_dispatcher.rb +2 -2
  171. data/lib/hecks/runtime/value/unwrapping.rb +60 -0
  172. data/lib/hecks/runtime/value.rb +10 -21
  173. data/lib/hecks/rust_build/project_rust/target_ir.rb +23 -4
  174. data/lib/hecks/rust_build/wasm.rb +0 -12
  175. data/lib/hecks/rust_build/write_if_changed.rb +12 -12
  176. data/lib/hecks/rust_format.rb +77 -0
  177. data/lib/hecks/tools/fuzz_sweep/findings.rb +3 -9
  178. data/lib/hecks/tools/fuzz_sweep.rb +1 -3
  179. data/lib/hecks/tools/translation_audit/verdicts.rb +3 -28
  180. data/lib/hecks/tools/translation_audit.rb +8 -10
  181. data/lib/hecks/tools/translation_scaffold/reporting.rb +1 -1
  182. data/lib/hecks/version.rb +1 -1
  183. data/lib/hecks/vocabulary.rb +2 -1
  184. data/lib/hecks.rb +3 -0
  185. data/prebuilt/{1a6f596be124228be93eba1d86dbc9d2933190fa389b284331241cb9f27115a0.marshal → ad68f9e06c8068c2a4176b57abf3083d591d5d8980c2b3e897b97331f965b643.marshal} +0 -0
  186. data/prebuilt/{a883a7e568667aa5ac904c82daff356bdf9e996252a36af6def4a4a43efb967e.marshal → ee22ec8642c4006717ba2b80f46e5efb81de459eef9dfec043e838c0ad4fd720.marshal} +0 -0
  187. data/prebuilt/{verdicts-872993b1aa26d73fe48f28251374b8585fed145d02aab42be0f00de386b6ff80.json → verdicts-b69f5dcbba79c20daa0493476fa7c317004a30aa2bc3d3dc6a722ea70ae2b43b.json} +1 -1
  188. data/rust/Cargo.toml +40 -0
  189. data/rust/build/Cargo.toml +36 -0
  190. data/rust/build/clippy.toml +4 -0
  191. data/rust/build/src/adapters_pass.rs +196 -0
  192. data/rust/build/src/binding_pass.rs +54 -13
  193. data/rust/build/src/build_artifact.rs +23 -5
  194. data/rust/build/src/cargo_sync.rs +69 -26
  195. data/rust/build/src/lineage_pass.rs +101 -87
  196. data/rust/build/src/main.rs +11 -2
  197. data/rust/build/src/pipeline.rs +80 -19
  198. data/rust/build/src/reserved_names.rs +6 -50
  199. data/rust/build/src/resolve.rs +82 -35
  200. data/rust/build/src/rustfmt.rs +100 -0
  201. data/rust/build/src/seams_pass.rs +85 -16
  202. data/rust/build/src/subprocess.rs +30 -6
  203. data/rust/build/src/tmp.rs +5 -2
  204. data/rust/build/src/translation_pass.rs +72 -22
  205. data/rust/build/src/world_pass.rs +109 -0
  206. data/rust/clippy.toml +4 -0
  207. data/rust/codegen/Cargo.toml +36 -0
  208. data/rust/codegen/clippy.toml +4 -0
  209. data/rust/codegen/src/asks.rs +40 -14
  210. data/rust/codegen/src/attr.rs +2 -2
  211. data/rust/codegen/src/bridging.rs +676 -117
  212. data/rust/codegen/src/commands.rs +979 -373
  213. data/rust/codegen/src/constraints.rs +88 -25
  214. data/rust/codegen/src/dependency_planning.rs +128 -34
  215. data/rust/codegen/src/domain_generator.rs +306 -170
  216. data/rust/codegen/src/exemplar.rs +87 -32
  217. data/rust/codegen/src/expr_emitter.rs +54 -6
  218. data/rust/codegen/src/fielded.rs +193 -41
  219. data/rust/codegen/src/hecks_naming.rs +124 -16
  220. data/rust/codegen/src/hecks_naming_tests.rs +81 -0
  221. data/rust/codegen/src/json.rs +80 -44
  222. data/rust/codegen/src/json_codec.rs +585 -149
  223. data/rust/codegen/src/literal.rs +104 -138
  224. data/rust/codegen/src/main.rs +151 -52
  225. data/rust/codegen/src/manifest.rs +76 -14
  226. data/rust/codegen/src/mutations.rs +1212 -239
  227. data/rust/codegen/src/naming.rs +111 -60
  228. data/rust/codegen/src/optional_pass.rs +90 -28
  229. data/rust/codegen/src/ports.rs +5 -4
  230. data/rust/codegen/src/prelude.rs +139 -49
  231. data/rust/codegen/src/presenters.rs +335 -0
  232. data/rust/codegen/src/queries.rs +601 -226
  233. data/rust/codegen/src/reactions.rs +268 -74
  234. data/rust/codegen/src/read_models.rs +574 -229
  235. data/rust/codegen/src/reference_specs.rs +16 -3
  236. data/rust/codegen/src/registry.rs +128 -79
  237. data/rust/codegen/src/reserved_names.rs +6 -50
  238. data/rust/codegen/src/shared.rs +71 -22
  239. data/rust/codegen/src/sidecars.rs +15 -5
  240. data/rust/codegen/src/skip_reason.rs +8 -2
  241. data/rust/codegen/src/types.rs +306 -108
  242. data/rust/host/Cargo.lock +1 -0
  243. data/rust/host/Cargo.toml +40 -4
  244. data/rust/host/HECKS_RELEASE +1 -1
  245. data/rust/host/clippy.toml +4 -0
  246. data/rust/host/src/api.rs +698 -218
  247. data/rust/host/src/auth.rs +491 -214
  248. data/rust/host/src/b64.rs +91 -0
  249. data/rust/host/src/bin/expr_harness.rs +6 -2
  250. data/rust/host/src/bin/lineage_harness.rs +47 -11
  251. data/rust/host/src/bin/mint_harness.rs +70 -37
  252. data/rust/host/src/bin/validate_harness.rs +16 -8
  253. data/rust/host/src/boot.rs +144 -68
  254. data/rust/host/src/boundary_fuzz/api.rs +130 -32
  255. data/rust/host/src/boundary_fuzz/auth.rs +108 -23
  256. data/rust/host/src/boundary_fuzz/expr_json.rs +85 -11
  257. data/rust/host/src/boundary_fuzz/ir.rs +164 -12
  258. data/rust/host/src/boundary_fuzz/server.rs +283 -36
  259. data/rust/host/src/boundary_fuzz/web.rs +56 -8
  260. data/rust/host/src/checkout.rs +78 -32
  261. data/rust/host/src/clock.rs +8 -0
  262. data/rust/host/src/commerce_ir.rs +333 -95
  263. data/rust/host/src/crypto.rs +33 -0
  264. data/rust/host/src/dispatch.rs +1037 -331
  265. data/rust/host/src/expr_json.rs +1021 -273
  266. data/rust/host/src/extension.rs +24 -4
  267. data/rust/host/src/field_hints.rs +11 -7
  268. data/rust/host/src/fuzz_support.rs +85 -13
  269. data/rust/host/src/host.rs +100 -0
  270. data/rust/host/src/http.rs +22 -0
  271. data/rust/host/src/ir.rs +326 -16
  272. data/rust/host/src/journal.rs +277 -176
  273. data/rust/host/src/lambda_client.rs +219 -53
  274. data/rust/host/src/lib.rs +16 -4
  275. data/rust/host/src/log.rs +62 -11
  276. data/rust/host/src/mint.rs +1153 -326
  277. data/rust/host/src/needs.rs +185 -44
  278. data/rust/host/src/payments/keystore.rs +197 -45
  279. data/rust/host/src/payments/roles.rs +39 -7
  280. data/rust/host/src/payments/tests.rs +1427 -282
  281. data/rust/host/src/payments.rs +391 -112
  282. data/rust/host/src/platform_signin/attack_tests.rs +5 -5
  283. data/rust/host/src/platform_signin/config.rs +11 -11
  284. data/rust/host/src/platform_signin/cookie.rs +2 -2
  285. data/rust/host/src/platform_signin/extension.rs +2 -1
  286. data/rust/host/src/platform_signin/handler.rs +9 -8
  287. data/rust/host/src/platform_signin/jti.rs +4 -2
  288. data/rust/host/src/platform_signin/jwks.rs +2 -1
  289. data/rust/host/src/platform_signin/jws.rs +2 -37
  290. data/rust/host/src/platform_signin/known_answer_tests.rs +5 -1
  291. data/rust/host/src/platform_signin/mode_tests.rs +5 -3
  292. data/rust/host/src/platform_signin/pages.rs +20 -20
  293. data/rust/host/src/platform_signin/session.rs +8 -1
  294. data/rust/host/src/platform_signin/settings.rs +6 -6
  295. data/rust/host/src/platform_signin/strict_json.rs +12 -9
  296. data/rust/host/src/platform_signin/verify.rs +41 -41
  297. data/rust/host/src/platform_signin/wiring_support.rs +13 -18
  298. data/rust/host/src/presentation.rs +207 -63
  299. data/rust/host/src/presentation_write.rs +502 -187
  300. data/rust/host/src/query_step.rs +69 -18
  301. data/rust/host/src/rate_limit.rs +541 -142
  302. data/rust/host/src/reference_transform.rs +77 -25
  303. data/rust/host/src/reference_validate.rs +443 -104
  304. data/rust/host/src/resend.rs +190 -55
  305. data/rust/host/src/secrets.rs +116 -21
  306. data/rust/host/src/server.rs +555 -104
  307. data/rust/host/src/storage_shape.rs +88 -21
  308. data/rust/host/src/test_pg.rs +17 -7
  309. data/rust/host/src/ui_schema.rs +495 -207
  310. data/rust/host/src/wasm_runner.rs +21 -6
  311. data/rust/host/src/web/commerce.rs +101 -23
  312. data/rust/host/src/web/newsletter/cooldown.rs +10 -3
  313. data/rust/host/src/web/newsletter/template.rs +26 -7
  314. data/rust/host/src/web/newsletter/tests.rs +112 -23
  315. data/rust/host/src/web/newsletter.rs +209 -59
  316. data/rust/host/src/web/newsletter_send.rs +251 -79
  317. data/rust/host/src/web/registration_receipt/tests.rs +16 -4
  318. data/rust/host/src/web/registration_receipt.rs +100 -24
  319. data/rust/host/src/web/registrations.rs +1384 -332
  320. data/rust/host/src/web/resend_webhook.rs +138 -80
  321. data/rust/host/src/web.rs +2416 -508
  322. data/rust/lsp/Cargo.toml +36 -0
  323. data/rust/lsp/clippy.toml +4 -0
  324. data/rust/lsp/src/json.rs +29 -22
  325. data/rust/lsp/src/main.rs +81 -31
  326. data/rust/lsp/src/outline.rs +23 -7
  327. data/rust/lsp/src/rpc.rs +17 -4
  328. data/rust/parser/Cargo.toml +36 -0
  329. data/rust/parser/clippy.toml +4 -0
  330. data/rust/parser/src/build/closed_sets.rs +6 -1
  331. data/rust/parser/src/build/identity.rs +4 -3
  332. data/rust/parser/src/build/mod.rs +1 -1
  333. data/rust/parser/src/build/named_pattern.rs +4 -1
  334. data/rust/parser/src/build/naming.rs +2 -80
  335. data/rust/parser/src/build/pattern_subset.rs +6 -8
  336. data/rust/parser/src/build/query_derive.rs +3 -1
  337. data/rust/parser/src/build/query_inference.rs +14 -5
  338. data/rust/parser/src/build/read_model.rs +4 -1
  339. data/rust/parser/src/build/references.rs +4 -3
  340. data/rust/parser/src/canonical.rs +34 -15
  341. data/rust/parser/src/emit.rs +187 -105
  342. data/rust/parser/src/expr/ast_json.rs +134 -42
  343. data/rust/parser/src/expr/evaluator.rs +27 -9
  344. data/rust/parser/src/expr/mod.rs +54 -13
  345. data/rust/parser/src/expr/resolver.rs +299 -92
  346. data/rust/parser/src/ir.rs +35 -3
  347. data/rust/parser/src/keywords.rs +7168 -516
  348. data/rust/parser/src/lex.rs +11 -25
  349. data/rust/parser/src/lib.rs +1 -0
  350. data/rust/parser/src/main.rs +65 -6
  351. data/rust/parser/src/parse/adapter.rs +157 -0
  352. data/rust/parser/src/parse/aggregate.rs +19 -10
  353. data/rust/parser/src/parse/chapter.rs +16 -17
  354. data/rust/parser/src/parse/command.rs +35 -16
  355. data/rust/parser/src/parse/domain_port.rs +4 -7
  356. data/rust/parser/src/parse/entity.rs +21 -11
  357. data/rust/parser/src/parse/hecksagon.rs +10 -4
  358. data/rust/parser/src/parse/lifecycle.rs +9 -5
  359. data/rust/parser/src/parse/mod.rs +22 -91
  360. data/rust/parser/src/parse/needs.rs +21 -12
  361. data/rust/parser/src/parse/policy.rs +2 -2
  362. data/rust/parser/src/parse/process_manager.rs +9 -11
  363. data/rust/parser/src/parse/query.rs +8 -4
  364. data/rust/parser/src/parse/read_model.rs +57 -16
  365. data/rust/parser/src/parse/value_object.rs +7 -2
  366. data/rust/parser/src/parse/world.rs +322 -0
  367. data/rust/parser/src/ruby_value.rs +7 -6
  368. data/rust/src/exemplar/json.rs +4 -4
  369. data/rust/src/exemplar/mutations.rs +18 -0
  370. data/rust/src/exemplar/read_models.rs +1 -10
  371. data/rust/src/kernel/attribute_shapes/composite.rs +54 -14
  372. data/rust/src/kernel/attribute_shapes/list.rs +2 -2
  373. data/rust/src/kernel/attribute_shapes/mod.rs +8 -8
  374. data/rust/src/kernel/attribute_shapes/scalar.rs +2 -2
  375. data/rust/src/kernel/cli.rs +244 -114
  376. data/rust/src/kernel/dispatch.rs +323 -115
  377. data/rust/src/kernel/dispatch_guard_tests.rs +327 -65
  378. data/rust/src/kernel/expr.rs +203 -86
  379. data/rust/src/kernel/expression_operators/arithmetic.rs +138 -25
  380. data/rust/src/kernel/expression_operators/comparison.rs +87 -2
  381. data/rust/src/kernel/expression_operators/enumeration.rs +106 -22
  382. data/rust/src/kernel/expression_operators/logical.rs +9 -3
  383. data/rust/src/kernel/expression_operators/membership.rs +3 -1
  384. data/rust/src/kernel/expression_operators/mod.rs +34 -21
  385. data/rust/src/kernel/expression_operators/pattern_match.rs +46 -11
  386. data/rust/src/kernel/expression_operators/positional.rs +84 -30
  387. data/rust/src/kernel/expression_operators/presence.rs +52 -13
  388. data/rust/src/kernel/expression_operators/sign_test.rs +3 -1
  389. data/rust/src/kernel/expression_operators/sized.rs +24 -8
  390. data/rust/src/kernel/expression_operators/text.rs +120 -27
  391. data/rust/src/kernel/expression_operators/to_string.rs +10 -4
  392. data/rust/src/kernel/json.rs +244 -148
  393. data/rust/src/kernel/mod.rs +47 -33
  394. data/rust/src/kernel/named_query.rs +63 -15
  395. data/rust/src/kernel/needs.rs +112 -25
  396. data/rust/src/kernel/orchestrate.rs +980 -581
  397. data/rust/src/kernel/orchestrate_guard_tests.rs +538 -140
  398. data/rust/src/kernel/pattern.rs +107 -34
  399. data/rust/src/kernel/present.rs +261 -0
  400. data/rust/src/kernel/query_comparators.rs +118 -41
  401. data/rust/src/kernel/query_ordering.rs +13 -5
  402. data/rust/src/kernel/read_model.rs +479 -181
  403. data/rust/src/kernel/reference_lookup.rs +122 -26
  404. data/rust/src/kernel/repository.rs +293 -66
  405. data/rust/src/kernel/routing.rs +58 -42
  406. data/rust/src/kernel/ruby_float.rs +139 -0
  407. data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +57 -57
  408. data/rust/src/kernel/vocab/entity_dispatch_order.rs +54 -54
  409. data/rust/src/kernel/vocab/field_hint.rs +19 -25
  410. data/rust/src/kernel/vocab/mod.rs +6 -6
  411. data/rust/src/kernel/vocab/query_comparator.rs +23 -24
  412. data/rust/src/kernel/vocab/refusal_template.rs +501 -297
  413. data/rust/src/lib.rs +50 -0
  414. data/rust/src/main.rs +4 -1
  415. data/rust/web/Cargo.toml +36 -0
  416. data/rust/web/clippy.toml +4 -0
  417. metadata +85 -15
  418. data/examples/pizzas/bluebook/hecksagon/mock_stripe_payment_adapter.rb +0 -36
  419. data/lib/hecks/adapters/driven/google_authentication.adapter +0 -3
  420. data/lib/hecks/adapters/driven/google_authentication.rb +0 -99
  421. data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +0 -3
  422. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +0 -32
  423. data/lib/hecks/ports/authentication.port +0 -6
  424. data/lib/hecks/ports/authentication.rb +0 -67
  425. data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +0 -255
  426. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +0 -22
  427. data/rust/host/src/approval.rs +0 -652
@@ -7,17 +7,43 @@ use std::collections::{HashMap, HashSet};
7
7
 
8
8
  /// `use` lines for value objects this aggregate's commands reference but never declare locally.
9
9
  /// A locally declared type name is never foreign.
10
- pub fn cross_aggregate_vo_imports(aggregate: &Json, domain_value_object_owner: &HashMap<String, String>, mod_name: &str) -> Vec<String> {
11
- let local_names: HashSet<String> = aggregate.get("value_objects").map(Json::each).unwrap_or(&[]).iter().map(|vo| vo.get("name").and_then(Json::as_str).unwrap_or("").to_string()).collect();
10
+ pub fn cross_aggregate_vo_imports(
11
+ aggregate: &Json,
12
+ domain_value_object_owner: &HashMap<String, String>,
13
+ mod_name: &str,
14
+ ) -> Vec<String> {
15
+ let local_names: HashSet<String> = aggregate
16
+ .get("value_objects")
17
+ .map_or_default(Json::each)
18
+ .iter()
19
+ .map(|vo| {
20
+ vo.get("name")
21
+ .and_then(Json::as_str)
22
+ .unwrap_or("")
23
+ .to_string()
24
+ })
25
+ .collect();
12
26
 
13
- let commands = aggregate.get("commands").map(Json::each).unwrap_or(&[]);
14
- let entity_commands = aggregate.get("entities").map(Json::each).unwrap_or(&[]).iter().flat_map(|e| e.get("commands").map(Json::each).unwrap_or(&[]).iter());
15
- let port_operations = aggregate.get("ports").map(Json::each).unwrap_or(&[]).iter().flat_map(|p| p.get("operations").map(Json::each).unwrap_or(&[]).iter());
27
+ let commands = aggregate.get("commands").map_or_default(Json::each);
28
+ let entity_commands = aggregate
29
+ .get("entities")
30
+ .map_or_default(Json::each)
31
+ .iter()
32
+ .flat_map(|e| e.get("commands").map_or_default(Json::each).iter());
33
+ let port_operations = aggregate
34
+ .get("ports")
35
+ .map_or_default(Json::each)
36
+ .iter()
37
+ .flat_map(|p| p.get("operations").map_or_default(Json::each).iter());
16
38
 
17
39
  let mut foreign_types: Vec<String> = Vec::new();
18
40
  let mut seen: HashSet<String> = HashSet::new();
19
- for c in commands.iter().chain(entity_commands).chain(port_operations) {
20
- for attr in c.get("attributes").map(Json::each).unwrap_or(&[]) {
41
+ for c in commands
42
+ .iter()
43
+ .chain(entity_commands)
44
+ .chain(port_operations)
45
+ {
46
+ for attr in c.get("attributes").map_or_default(Json::each) {
21
47
  let type_name = crate::attr::type_name(attr).to_string();
22
48
  if local_names.contains(&type_name) || seen.contains(&type_name) {
23
49
  continue;
@@ -27,39 +53,70 @@ pub fn cross_aggregate_vo_imports(aggregate: &Json, domain_value_object_owner: &
27
53
  }
28
54
  }
29
55
 
30
- let mut pairs: Vec<(String, String)> = foreign_types.into_iter().filter_map(|type_name| domain_value_object_owner.get(&type_name).map(|owner| (type_name, owner.clone()))).collect();
56
+ let mut pairs: Vec<(String, String)> = foreign_types
57
+ .into_iter()
58
+ .filter_map(|type_name| {
59
+ domain_value_object_owner
60
+ .get(&type_name)
61
+ .map(|owner| (type_name, owner.clone()))
62
+ })
63
+ .collect();
31
64
  pairs.sort_by(|(a_type, a_owner), (b_type, b_owner)| (a_owner, a_type).cmp(&(b_owner, b_type)));
32
- pairs.into_iter().map(|(type_name, owner)| format!("use crate::generated::{mod_name}::{}::{};", naming::aggregate_module(&owner), naming::rust_ident(&type_name))).collect()
65
+ pairs
66
+ .into_iter()
67
+ .map(|(type_name, owner)| {
68
+ format!(
69
+ "use crate::generated::{mod_name}::{}::{};",
70
+ naming::aggregate_module(&owner),
71
+ naming::rust_ident(&type_name)
72
+ )
73
+ })
74
+ .collect()
33
75
  }
34
76
 
35
77
  /// True when a value object rebuilds into a differently-named one by matching field names.
36
78
  pub fn vo_field_bridgeable(source_vo: Option<&Json>, target_vo: Option<&Json>) -> bool {
37
- let (Some(source_vo), Some(target_vo)) = (source_vo, target_vo) else { return false };
38
- if source_vo.get("closed_set").map(Json::as_bool).unwrap_or(false) || target_vo.get("closed_set").map(Json::as_bool).unwrap_or(false) {
79
+ let (Some(source_vo), Some(target_vo)) = (source_vo, target_vo) else {
80
+ return false;
81
+ };
82
+ if source_vo.get("closed_set").is_some_and(Json::as_bool)
83
+ || target_vo.get("closed_set").is_some_and(Json::as_bool)
84
+ {
39
85
  return closed_set_bridge_members(source_vo, target_vo).is_some();
40
86
  }
41
87
 
42
- let source_attrs = source_vo.get("attributes").map(Json::each).unwrap_or(&[]);
43
- let target_attrs = target_vo.get("attributes").map(Json::each).unwrap_or(&[]);
88
+ let source_attrs = source_vo.get("attributes").map_or_default(Json::each);
89
+ let target_attrs = target_vo.get("attributes").map_or_default(Json::each);
44
90
  target_attrs.iter().all(|t_attr| {
45
91
  let t_name = crate::attr::name(t_attr);
46
- source_attrs.iter().any(|s_attr| crate::attr::name(s_attr) == t_name) || crate::attr::default(t_attr).is_some()
92
+ source_attrs
93
+ .iter()
94
+ .any(|s_attr| crate::attr::name(s_attr) == t_name)
95
+ || crate::attr::default(t_attr).is_some()
47
96
  })
48
97
  }
49
98
 
50
99
  /// Closed-set to closed-set bridge, admitted only when every source member is a target member.
51
100
  fn closed_set_bridge_members<'a>(source_vo: &'a Json, target_vo: &Json) -> Option<&'a [Json]> {
52
- let closed = |vo: &Json| vo.get("closed_set").map(Json::as_bool).unwrap_or(false);
101
+ let closed = |vo: &Json| vo.get("closed_set").is_some_and(Json::as_bool);
53
102
  if !closed(source_vo) || !closed(target_vo) {
54
103
  return None;
55
104
  }
56
- let single = |vo: &Json| vo.get("attributes").map(Json::each).unwrap_or(&[]).len() == 1;
105
+ let single = |vo: &Json| vo.get("attributes").map_or_default(Json::each).len() == 1;
57
106
  if !single(source_vo) || !single(target_vo) {
58
107
  return None;
59
108
  }
60
- let target_values: Vec<String> = target_vo.get("members").map(Json::each).unwrap_or(&[]).iter().map(closed_set_row_value).collect();
61
- let source_rows = source_vo.get("members").map(Json::each).unwrap_or(&[]);
62
- if source_rows.iter().all(|row| target_values.contains(&closed_set_row_value(row))) {
109
+ let target_values: Vec<String> = target_vo
110
+ .get("members")
111
+ .map_or_default(Json::each)
112
+ .iter()
113
+ .map(closed_set_row_value)
114
+ .collect();
115
+ let source_rows = source_vo.get("members").map_or_default(Json::each);
116
+ if source_rows
117
+ .iter()
118
+ .all(|row| target_values.contains(&closed_set_row_value(row)))
119
+ {
63
120
  Some(source_rows)
64
121
  } else {
65
122
  None
@@ -72,39 +129,73 @@ fn closed_set_row_value(row: &Json) -> String {
72
129
  first.get(1).map(Json::to_s).unwrap_or_default()
73
130
  }
74
131
 
75
- pub fn vo_field_rhs(source_expr: &str, source_vo: &Json, target_type: &str, value_objects_by_name: &HashMap<String, &Json>) -> String {
132
+ #[allow(
133
+ clippy::unwrap_used,
134
+ reason = "the value was checked just before; None would mean invalid IR"
135
+ )]
136
+ pub fn vo_field_rhs(
137
+ source_expr: &str,
138
+ source_vo: &Json,
139
+ target_type: &str,
140
+ value_objects_by_name: &HashMap<String, &Json>,
141
+ ) -> String {
76
142
  let target_vo = value_objects_by_name[target_type];
77
143
  if let Some(rows) = closed_set_bridge_members(source_vo, target_vo) {
78
144
  let source_type = source_vo.get("name").map(Json::to_s).unwrap_or_default();
79
145
  let arms: Vec<String> = rows
80
146
  .iter()
81
- .map(|row| format!("{}::{} => {}::{}", naming::rust_ident(&source_type), closed_set_variant_of_row(row), naming::rust_ident(target_type), closed_set_variant_of_row(row)))
147
+ .map(|row| {
148
+ format!(
149
+ "{}::{} => {}::{}",
150
+ naming::rust_ident(&source_type),
151
+ closed_set_variant_of_row(row),
152
+ naming::rust_ident(target_type),
153
+ closed_set_variant_of_row(row)
154
+ )
155
+ })
82
156
  .collect();
83
157
  return format!("match &{source_expr} {{ {} }}", arms.join(", "));
84
158
  }
85
- let source_attrs = source_vo.get("attributes").map(Json::each).unwrap_or(&[]);
86
- let target_attrs = target_vo.get("attributes").map(Json::each).unwrap_or(&[]);
159
+ let source_attrs = source_vo.get("attributes").map_or_default(Json::each);
160
+ let target_attrs = target_vo.get("attributes").map_or_default(Json::each);
87
161
 
88
162
  let fields: Vec<String> = target_attrs
89
163
  .iter()
90
164
  .map(|t_attr| {
91
165
  let field = naming::rust_ident_field(crate::attr::name(t_attr));
92
166
  let t_name = crate::attr::name(t_attr);
93
- if source_attrs.iter().any(|s_attr| crate::attr::name(s_attr) == t_name) {
167
+ if source_attrs
168
+ .iter()
169
+ .any(|s_attr| crate::attr::name(s_attr) == t_name)
170
+ {
94
171
  format!("{field}: {source_expr}.{field}.clone()")
95
172
  } else {
96
- format!("{field}: {}", naming::literal_rhs(crate::attr::default(t_attr).unwrap()))
173
+ format!(
174
+ "{field}: {}",
175
+ naming::literal_rhs(crate::attr::default(t_attr).unwrap())
176
+ )
97
177
  }
98
178
  })
99
179
  .collect();
100
- format!("{} {{ {} }}", naming::rust_ident(target_type), fields.join(", "))
180
+ format!(
181
+ "{} {{ {} }}",
182
+ naming::rust_ident(target_type),
183
+ fields.join(", ")
184
+ )
101
185
  }
102
186
 
103
- pub fn bridgeable_value_types(source_type: &str, target_type: &str, value_objects_by_name: &HashMap<String, &Json>) -> bool {
187
+ pub fn bridgeable_value_types(
188
+ source_type: &str,
189
+ target_type: &str,
190
+ value_objects_by_name: &HashMap<String, &Json>,
191
+ ) -> bool {
104
192
  if source_type == target_type {
105
193
  return true;
106
194
  }
107
- if let (Some(s), Some(t)) = (naming::effective_scalar_type(source_type), naming::effective_scalar_type(target_type)) {
195
+ if let (Some(s), Some(t)) = (
196
+ naming::effective_scalar_type(source_type),
197
+ naming::effective_scalar_type(target_type),
198
+ ) {
108
199
  if s == t {
109
200
  return true;
110
201
  }
@@ -118,8 +209,8 @@ pub fn bridgeable_value_types(source_type: &str, target_type: &str, value_object
118
209
 
119
210
  match source_vo {
120
211
  Some(vo) => {
121
- let closed = vo.get("closed_set").map(Json::as_bool).unwrap_or(false);
122
- let attrs = vo.get("attributes").map(Json::each).unwrap_or(&[]);
212
+ let closed = vo.get("closed_set").is_some_and(Json::as_bool);
213
+ let attrs = vo.get("attributes").map_or_default(Json::each);
123
214
  if closed || attrs.len() != 1 {
124
215
  return false;
125
216
  }
@@ -134,11 +225,24 @@ pub fn bridgeable_value_types(source_type: &str, target_type: &str, value_object
134
225
  }
135
226
  }
136
227
 
137
- pub fn value_rhs(source_expr: &str, source_type: &str, target_type: &str, value_objects_by_name: &HashMap<String, &Json>) -> String {
228
+ #[allow(
229
+ clippy::expect_used,
230
+ clippy::panic,
231
+ reason = "a drifted exemplar or invalid IR aborts generation on purpose; the message names an invariant an earlier check in this generator established"
232
+ )]
233
+ pub fn value_rhs(
234
+ source_expr: &str,
235
+ source_type: &str,
236
+ target_type: &str,
237
+ value_objects_by_name: &HashMap<String, &Json>,
238
+ ) -> String {
138
239
  if source_type == target_type {
139
240
  return format!("{source_expr}.clone()");
140
241
  }
141
- if let (Some(s), Some(t)) = (naming::effective_scalar_type(source_type), naming::effective_scalar_type(target_type)) {
242
+ if let (Some(s), Some(t)) = (
243
+ naming::effective_scalar_type(source_type),
244
+ naming::effective_scalar_type(target_type),
245
+ ) {
142
246
  if s == t {
143
247
  return format!("{source_expr}.clone()");
144
248
  }
@@ -147,15 +251,21 @@ pub fn value_rhs(source_expr: &str, source_type: &str, target_type: &str, value_
147
251
  let source_vo = value_objects_by_name.get(source_type).copied();
148
252
  let target_vo = value_objects_by_name.get(target_type).copied();
149
253
  if target_vo.is_some() {
150
- return vo_field_rhs(source_expr, source_vo.expect("bridgeable_value_types? should have caught this"), target_type, value_objects_by_name);
254
+ return vo_field_rhs(
255
+ source_expr,
256
+ source_vo.expect("bridgeable_value_types? should have caught this"),
257
+ target_type,
258
+ value_objects_by_name,
259
+ );
151
260
  }
152
261
 
153
262
  let source_vo = source_vo.unwrap_or_else(|| panic!("unsupported coercion {source_type} -> {target_type} — bridgeable_value_types? should have caught this"));
154
- let attrs = source_vo.get("attributes").map(Json::each).unwrap_or(&[]);
155
- if source_vo.get("closed_set").map(Json::as_bool).unwrap_or(false) || attrs.len() != 1 {
156
- panic!("unsupported coercion {source_type} -> {target_type} — bridgeable_value_types? should have caught this");
157
- }
158
- format!("{source_expr}.{}.clone()", naming::rust_ident_field(crate::attr::name(&attrs[0])))
263
+ let attrs = source_vo.get("attributes").map_or_default(Json::each);
264
+ assert!(!(source_vo.get("closed_set").is_some_and(Json::as_bool) || attrs.len() != 1), "unsupported coercion {source_type} -> {target_type} — bridgeable_value_types? should have caught this");
265
+ format!(
266
+ "{source_expr}.{}.clone()",
267
+ naming::rust_ident_field(crate::attr::name(&attrs[0]))
268
+ )
159
269
  }
160
270
 
161
271
  /// True when a list-to-list `:set` needs a per-element rebuild rather than one `.clone()`.
@@ -171,40 +281,67 @@ pub fn list_bridge_requires_element_mapping(source_type: &str, target_type: &str
171
281
  }
172
282
 
173
283
  /// `value_rhs` applied per element, for list-to-list `:set` whose element types differ.
174
- pub fn list_value_rhs(source_expr: &str, source_type: &str, target_type: &str, value_objects_by_name: &HashMap<String, &Json>) -> String {
175
- format!("{source_expr}.iter().map(|item| {}).collect()", value_rhs("item", source_type, target_type, value_objects_by_name))
284
+ pub fn list_value_rhs(
285
+ source_expr: &str,
286
+ source_type: &str,
287
+ target_type: &str,
288
+ value_objects_by_name: &HashMap<String, &Json>,
289
+ ) -> String {
290
+ format!(
291
+ "{source_expr}.iter().map(|item| {}).collect()",
292
+ value_rhs("item", source_type, target_type, value_objects_by_name)
293
+ )
176
294
  }
177
295
 
178
- pub fn literal_set_bridgeable(value: &Literal, target_type: Option<&str>, value_objects_by_name: &HashMap<String, &Json>) -> bool {
296
+ pub fn literal_set_bridgeable(
297
+ value: &Literal,
298
+ target_type: Option<&str>,
299
+ value_objects_by_name: &HashMap<String, &Json>,
300
+ ) -> bool {
179
301
  match value {
180
- Literal::Hash(_) => match target_type {
181
- Some(target_type) => literal_hash_bridgeable(value, target_type, value_objects_by_name),
182
- None => false,
183
- },
302
+ Literal::Hash(_) => target_type.is_some_and(|target_type| {
303
+ literal_hash_bridgeable(value, target_type, value_objects_by_name)
304
+ }),
184
305
  Literal::Str(_) | Literal::Int(_) | Literal::Float(_) | Literal::Bool(_) => {
185
- let Some(target_type) = target_type else { return true };
306
+ let Some(target_type) = target_type else {
307
+ return true;
308
+ };
186
309
  if !value_objects_by_name.contains_key(target_type) {
187
310
  return true;
188
311
  }
189
312
  // A scalar literal into a single-field value object is that field's value.
190
- match crate::json_codec::sole_field_of(target_type, value_objects_by_name) {
191
- Some(sole) => literal_hash_bridgeable(&Literal::Hash(vec![(sole, value.clone())]), target_type, value_objects_by_name),
192
- None => false,
193
- }
313
+ crate::json_codec::sole_field_of(target_type, value_objects_by_name).is_some_and(
314
+ |sole| {
315
+ literal_hash_bridgeable(
316
+ &Literal::Hash(vec![(sole, value.clone())]),
317
+ target_type,
318
+ value_objects_by_name,
319
+ )
320
+ },
321
+ )
194
322
  }
195
323
  _ => false,
196
324
  }
197
325
  }
198
326
 
199
327
  /// Renders a literal as the right-hand side for a target type.
200
- pub fn literal_rhs_for(value: &Literal, target_type: Option<&str>, value_objects_by_name: &HashMap<String, &Json>) -> String {
328
+ pub fn literal_rhs_for(
329
+ value: &Literal,
330
+ target_type: Option<&str>,
331
+ value_objects_by_name: &HashMap<String, &Json>,
332
+ ) -> String {
201
333
  if let Literal::Hash(_) = value {
202
334
  return literal_hash_rhs(value, target_type.unwrap_or(""), value_objects_by_name);
203
335
  }
204
336
  if let Some(target_type) = target_type {
205
337
  if value_objects_by_name.contains_key(target_type) {
206
- if let Some(sole) = crate::json_codec::sole_field_of(target_type, value_objects_by_name) {
207
- return literal_hash_rhs(&Literal::Hash(vec![(sole, value.clone())]), target_type, value_objects_by_name);
338
+ if let Some(sole) = crate::json_codec::sole_field_of(target_type, value_objects_by_name)
339
+ {
340
+ return literal_hash_rhs(
341
+ &Literal::Hash(vec![(sole, value.clone())]),
342
+ target_type,
343
+ value_objects_by_name,
344
+ );
208
345
  }
209
346
  }
210
347
  }
@@ -212,15 +349,25 @@ pub fn literal_rhs_for(value: &Literal, target_type: Option<&str>, value_objects
212
349
  }
213
350
 
214
351
  /// True when a literal Hash fills an ordinary value object or matches one closed-set member row.
215
- pub fn literal_hash_bridgeable(hash: &Literal, target_type: &str, value_objects_by_name: &HashMap<String, &Json>) -> bool {
216
- let Some(vo) = value_objects_by_name.get(target_type) else { return false };
352
+ pub fn literal_hash_bridgeable(
353
+ hash: &Literal,
354
+ target_type: &str,
355
+ value_objects_by_name: &HashMap<String, &Json>,
356
+ ) -> bool {
357
+ let Some(vo) = value_objects_by_name.get(target_type) else {
358
+ return false;
359
+ };
217
360
 
218
- if vo.get("closed_set").map(Json::as_bool).unwrap_or(false) {
219
- let members = vo.get("members").map(Json::each).unwrap_or(&[]);
220
- members.iter().any(|member| member_matches_hash(member, hash))
361
+ if vo.get("closed_set").is_some_and(Json::as_bool) {
362
+ let members = vo.get("members").map_or_default(Json::each);
363
+ members
364
+ .iter()
365
+ .any(|member| member_matches_hash(member, hash))
221
366
  } else {
222
- let attrs = vo.get("attributes").map(Json::each).unwrap_or(&[]);
223
- attrs.iter().all(|attr| hash.get(crate::attr::name(attr)).is_some())
367
+ let attrs = vo.get("attributes").map_or_default(Json::each);
368
+ attrs
369
+ .iter()
370
+ .all(|attr| hash.get(crate::attr::name(attr)).is_some())
224
371
  }
225
372
  }
226
373
 
@@ -230,11 +377,11 @@ fn member_matches_hash(member: &Json, hash: &Literal) -> bool {
230
377
  let pairs = member.as_array().unwrap_or(&[]);
231
378
  pairs.iter().all(|pair| {
232
379
  let kv = pair.as_array().unwrap_or(&[]);
233
- let (Some(field), Some(value)) = (kv.first().and_then(Json::as_str), kv.get(1)) else { return false };
234
- match hash.get(field) {
235
- Some(held) => literal_to_s(held) == value.to_s(),
236
- None => false,
237
- }
380
+ let (Some(field), Some(value)) = (kv.first().and_then(Json::as_str), kv.get(1)) else {
381
+ return false;
382
+ };
383
+ hash.get(field)
384
+ .is_some_and(|held| literal_to_s(held) == value.to_s())
238
385
  })
239
386
  }
240
387
 
@@ -251,20 +398,32 @@ fn literal_to_s(lit: &Literal) -> String {
251
398
  }
252
399
  }
253
400
 
254
- pub fn literal_hash_rhs(hash: &Literal, target_type: &str, value_objects_by_name: &HashMap<String, &Json>) -> String {
401
+ #[allow(
402
+ clippy::panic,
403
+ reason = "a drifted exemplar or invalid IR aborts generation on purpose"
404
+ )]
405
+ pub fn literal_hash_rhs(
406
+ hash: &Literal,
407
+ target_type: &str,
408
+ value_objects_by_name: &HashMap<String, &Json>,
409
+ ) -> String {
255
410
  let vo = value_objects_by_name
256
411
  .get(target_type)
257
412
  .unwrap_or_else(|| panic!("unsupported literal hash source {hash:?} -> {target_type} — literal_hash_bridgeable? should have caught this"));
258
413
 
259
- if vo.get("closed_set").map(Json::as_bool).unwrap_or(false) {
260
- let members = vo.get("members").map(Json::each).unwrap_or(&[]);
414
+ if vo.get("closed_set").is_some_and(Json::as_bool) {
415
+ let members = vo.get("members").map_or_default(Json::each);
261
416
  let row = members
262
417
  .iter()
263
418
  .find(|member| member_matches_hash(member, hash))
264
419
  .unwrap_or_else(|| panic!("literal {hash:?} matches no member of {target_type} — literal_hash_bridgeable? should have caught this"));
265
- format!("{}::{}", naming::rust_ident(target_type), closed_set_variant_of_row(row))
420
+ format!(
421
+ "{}::{}",
422
+ naming::rust_ident(target_type),
423
+ closed_set_variant_of_row(row)
424
+ )
266
425
  } else {
267
- let attrs = vo.get("attributes").map(Json::each).unwrap_or(&[]);
426
+ let attrs = vo.get("attributes").map_or_default(Json::each);
268
427
  let fields: Vec<String> = attrs
269
428
  .iter()
270
429
  .map(|attr| {
@@ -275,7 +434,11 @@ pub fn literal_hash_rhs(hash: &Literal, target_type: &str, value_objects_by_name
275
434
  format!("{}: {}", naming::rust_ident_field(name), crate::literal::literal_rhs(value))
276
435
  })
277
436
  .collect();
278
- format!("{} {{ {} }}", naming::rust_ident(target_type), fields.join(", "))
437
+ format!(
438
+ "{} {{ {} }}",
439
+ naming::rust_ident(target_type),
440
+ fields.join(", ")
441
+ )
279
442
  }
280
443
  }
281
444
 
@@ -287,28 +450,97 @@ fn closed_set_variant_of_row(row: &Json) -> String {
287
450
  }
288
451
 
289
452
  pub fn integer_field_of(vo: Option<&Json>) -> Option<String> {
453
+ scalar_field_of(vo, "Integer")
454
+ }
455
+
456
+ fn scalar_field_of(vo: Option<&Json>, type_name: &str) -> Option<String> {
290
457
  let vo = vo?;
291
- if vo.get("closed_set").map(Json::as_bool).unwrap_or(false) {
458
+ if vo.get("closed_set").is_some_and(Json::as_bool) {
292
459
  return None;
293
460
  }
294
- let attrs = vo.get("attributes").map(Json::each).unwrap_or(&[]);
295
- attrs.iter().find(|a| crate::attr::type_name(a) == "Integer").map(|a| crate::attr::name(a).to_string())
461
+ let attrs = vo.get("attributes").map_or_default(Json::each);
462
+ attrs
463
+ .iter()
464
+ .find(|a| crate::attr::type_name(a) == type_name)
465
+ .map(|a| crate::attr::name(a).to_string())
466
+ }
467
+
468
+ /// Which machine number a value object's arithmetic field is.
469
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
470
+ pub enum NumericKind {
471
+ Integer,
472
+ Float,
473
+ }
474
+
475
+ /// The field arithmetic acts on: the first Integer field, else the first Float field.
476
+ pub fn numeric_field_of(vo: Option<&Json>) -> Option<(String, NumericKind)> {
477
+ integer_field_of(vo)
478
+ .map(|field| (field, NumericKind::Integer))
479
+ .or_else(|| scalar_field_of(vo, "Float").map(|field| (field, NumericKind::Float)))
296
480
  }
297
481
 
298
- /// The target half of an `:increment`/`:decrement` mutation.
299
- pub fn arithmetic_target_field<'a>(mutation: &Json, aggregate: &'a Json, value_objects_by_name: &HashMap<String, &Json>) -> Option<(&'a Json, String)> {
482
+ fn target_attribute<'a>(mutation: &Json, aggregate: &'a Json) -> Option<&'a Json> {
300
483
  let target = mutation.get("target").map(Json::to_s).unwrap_or_default();
301
- let attrs = aggregate.get("attributes").map(Json::each).unwrap_or(&[]);
484
+ let attrs = aggregate.get("attributes").map_or_default(Json::each);
302
485
  let target_attr = attrs.iter().find(|a| crate::attr::name(a) == target)?;
303
486
  if crate::attr::list(target_attr) {
304
487
  return None;
305
488
  }
306
- let field = integer_field_of(value_objects_by_name.get(crate::attr::type_name(target_attr)).copied())?;
489
+ Some(target_attr)
490
+ }
491
+
492
+ /// The target half of an `:increment`/`:decrement`/`:multiply` mutation.
493
+ pub fn arithmetic_target_field<'a>(
494
+ mutation: &Json,
495
+ aggregate: &'a Json,
496
+ value_objects_by_name: &HashMap<String, &Json>,
497
+ ) -> Option<(&'a Json, String, NumericKind)> {
498
+ let target_attr = target_attribute(mutation, aggregate)?;
499
+ let (field, kind) = numeric_field_of(
500
+ value_objects_by_name
501
+ .get(crate::attr::type_name(target_attr))
502
+ .copied(),
503
+ )?;
504
+ Some((target_attr, field, kind))
505
+ }
506
+
507
+ /// The target of a `:clamp`, whose bounds are integers: Integer-field value objects only.
508
+ pub fn integer_target_field<'a>(
509
+ mutation: &Json,
510
+ aggregate: &'a Json,
511
+ value_objects_by_name: &HashMap<String, &Json>,
512
+ ) -> Option<(&'a Json, String)> {
513
+ let target_attr = target_attribute(mutation, aggregate)?;
514
+ let field = integer_field_of(
515
+ value_objects_by_name
516
+ .get(crate::attr::type_name(target_attr))
517
+ .copied(),
518
+ )?;
307
519
  Some((target_attr, field))
308
520
  }
309
521
 
310
- /// The amount half — resolved to a raw Rust integer-typed expression.
311
- pub fn arithmetic_amount_expr(source: &Json, command: &Json, value_objects_by_name: &HashMap<String, &Json>, target_integer_field: &str) -> Option<String> {
522
+ /// The amount half — resolved to a raw Rust expression of the target field's own type.
523
+ pub fn arithmetic_amount_expr(
524
+ source: &Json,
525
+ command: &Json,
526
+ value_objects_by_name: &HashMap<String, &Json>,
527
+ target_field: &str,
528
+ kind: NumericKind,
529
+ ) -> Option<String> {
530
+ match kind {
531
+ NumericKind::Integer => {
532
+ integer_amount_expr(source, command, value_objects_by_name, target_field)
533
+ }
534
+ NumericKind::Float => float_amount_expr(source, command, value_objects_by_name),
535
+ }
536
+ }
537
+
538
+ fn integer_amount_expr(
539
+ source: &Json,
540
+ command: &Json,
541
+ value_objects_by_name: &HashMap<String, &Json>,
542
+ target_integer_field: &str,
543
+ ) -> Option<String> {
312
544
  let kind = source.get("kind").map(Json::to_s).unwrap_or_default();
313
545
  if kind == "literal" {
314
546
  let value = source.get("value")?;
@@ -326,17 +558,131 @@ pub fn arithmetic_amount_expr(source: &Json, command: &Json, value_objects_by_na
326
558
  }
327
559
  if kind == "argument" {
328
560
  let name = source.get("name").map(Json::to_s).unwrap_or_default();
329
- let attrs = command.get("attributes").map(Json::each).unwrap_or(&[]);
561
+ let attrs = command.get("attributes").map_or_default(Json::each);
330
562
  let arg_attr = attrs.iter().find(|a| crate::attr::name(a) == name)?;
331
563
  if crate::attr::type_name(arg_attr) == "Integer" {
332
- return Some(format!("args.{}", naming::rust_ident_field(crate::attr::name(arg_attr))));
564
+ return Some(format!(
565
+ "args.{}",
566
+ naming::rust_ident_field(crate::attr::name(arg_attr))
567
+ ));
333
568
  }
334
- let field = integer_field_of(value_objects_by_name.get(crate::attr::type_name(arg_attr)).copied())?;
335
- return Some(format!("args.{}.{}", naming::rust_ident_field(crate::attr::name(arg_attr)), naming::rust_ident_field(&field)));
569
+ let field = integer_field_of(
570
+ value_objects_by_name
571
+ .get(crate::attr::type_name(arg_attr))
572
+ .copied(),
573
+ )?;
574
+ return Some(format!(
575
+ "args.{}.{}",
576
+ naming::rust_ident_field(crate::attr::name(arg_attr)),
577
+ naming::rust_ident_field(&field)
578
+ ));
336
579
  }
337
580
  None
338
581
  }
339
582
 
583
+ /// The Rust expression for a text operand that can never be added to the target's Integer field.
584
+ ///
585
+ /// Ruby wraps the amount as the target's own type before combining, so a required command
586
+ /// argument carrying text refuses `TypeMismatch` at the mutation step (C4.4). The rule is exact:
587
+ /// the argument is a single, required, non-list value and is either a bare `String` or a value
588
+ /// object whose only field is a `String` named as the target's Integer field (so the rewrap
589
+ /// lands on that field and no other refusal intervenes); the target is a required scalar. Any
590
+ /// other shape keeps its own refusal and so stays ungenerated. The expression yields the `String`.
591
+ pub fn text_operand_expr(
592
+ mutation: &Json,
593
+ aggregate: &Json,
594
+ command: &Json,
595
+ value_objects_by_name: &HashMap<String, &Json>,
596
+ target_integer_field: &str,
597
+ ) -> Option<String> {
598
+ let target = mutation.get("target").map(Json::to_s).unwrap_or_default();
599
+ let aggregate_attrs = aggregate.get("attributes").map_or_default(Json::each);
600
+ let target_attr = aggregate_attrs
601
+ .iter()
602
+ .find(|a| crate::attr::name(a) == target)?;
603
+ if crate::attr::optional(target_attr) {
604
+ return None;
605
+ }
606
+ let source = mutation.get("source")?;
607
+ if source.get("kind").map(Json::to_s).unwrap_or_default() != "argument" {
608
+ return None;
609
+ }
610
+ let name = source.get("name").map(Json::to_s).unwrap_or_default();
611
+ let command_attrs = command.get("attributes").map_or_default(Json::each);
612
+ let arg_attr = command_attrs
613
+ .iter()
614
+ .find(|a| crate::attr::name(a) == name)?;
615
+ if crate::attr::list(arg_attr) || crate::attr::optional(arg_attr) {
616
+ return None;
617
+ }
618
+ let arg = format!("args.{}", naming::rust_ident_field(&name));
619
+ if crate::attr::type_name(arg_attr) == "String" {
620
+ return Some(arg);
621
+ }
622
+ let vo = value_objects_by_name
623
+ .get(crate::attr::type_name(arg_attr))
624
+ .copied()?;
625
+ if vo.get("closed_set").is_some_and(Json::as_bool) {
626
+ return None;
627
+ }
628
+ let fields = vo.get("attributes").map_or_default(Json::each);
629
+ match fields {
630
+ [only]
631
+ if crate::attr::type_name(only) == "String"
632
+ && !crate::attr::list(only)
633
+ && !crate::attr::optional(only)
634
+ && crate::attr::name(only) == target_integer_field =>
635
+ {
636
+ Some(format!(
637
+ "{arg}.{}",
638
+ naming::rust_ident_field(target_integer_field)
639
+ ))
640
+ }
641
+ _ => None,
642
+ }
643
+ }
644
+
645
+ // A Float target takes a Float or Integer amount, an Integer widened to `f64` as Ruby coerces it.
646
+ fn float_amount_expr(
647
+ source: &Json,
648
+ command: &Json,
649
+ value_objects_by_name: &HashMap<String, &Json>,
650
+ ) -> Option<String> {
651
+ let kind = source.get("kind").map(Json::to_s).unwrap_or_default();
652
+ if kind == "literal" {
653
+ let value = source.get("value")?;
654
+ return match value.number()? {
655
+ Number::Int(n) => Some(format!("({n} as f64)")),
656
+ Number::Float(_) => Some(naming::literal_rhs(value)),
657
+ };
658
+ }
659
+ if kind != "argument" {
660
+ return None;
661
+ }
662
+ let name = source.get("name").map(Json::to_s).unwrap_or_default();
663
+ let attrs = command.get("attributes").map_or_default(Json::each);
664
+ let arg_attr = attrs.iter().find(|a| crate::attr::name(a) == name)?;
665
+ let arg = format!(
666
+ "args.{}",
667
+ naming::rust_ident_field(crate::attr::name(arg_attr))
668
+ );
669
+ match crate::attr::type_name(arg_attr) {
670
+ "Float" => return Some(arg),
671
+ "Integer" => return Some(format!("({arg} as f64)")),
672
+ _ => {}
673
+ }
674
+ let (field, field_kind) = numeric_field_of(
675
+ value_objects_by_name
676
+ .get(crate::attr::type_name(arg_attr))
677
+ .copied(),
678
+ )?;
679
+ let access = format!("{arg}.{}", naming::rust_ident_field(&field));
680
+ Some(match field_kind {
681
+ NumericKind::Float => access,
682
+ NumericKind::Integer => format!("({access} as f64)"),
683
+ })
684
+ }
685
+
340
686
  /// `clamp:` bounds as `[min, max]` integers; `None` for anything else.
341
687
  pub fn clamp_bounds_ints(source: &Json) -> Option<(i64, i64)> {
342
688
  if source.get("kind").map(Json::to_s).unwrap_or_default() != "literal" {
@@ -347,38 +693,70 @@ pub fn clamp_bounds_ints(source: &Json) -> Option<(i64, i64)> {
347
693
  if items.len() != 2 {
348
694
  return None;
349
695
  }
350
- let Some(Number::Int(min)) = items[0].number() else { return None };
351
- let Some(Number::Int(max)) = items[1].number() else { return None };
696
+ let Some(Number::Int(min)) = items[0].number() else {
697
+ return None;
698
+ };
699
+ let Some(Number::Int(max)) = items[1].number() else {
700
+ return None;
701
+ };
352
702
  Some((min, max))
353
703
  }
354
704
 
355
705
  /// An aggregate attribute a creating command's arguments never mention.
356
- pub fn creation_default_rhs(attr: &Json, value_objects_by_name: &HashMap<String, &Json>) -> Option<String> {
706
+ #[allow(
707
+ clippy::unwrap_used,
708
+ reason = "the value was checked just before; None would mean invalid IR"
709
+ )]
710
+ pub fn creation_default_rhs(
711
+ attr: &Json,
712
+ value_objects_by_name: &HashMap<String, &Json>,
713
+ ) -> Option<String> {
357
714
  if let Some(default) = crate::attr::default(attr) {
358
715
  return Some(if let Json::Object(_) = default {
359
- literal_hash_rhs(&Literal::from_json(default), crate::attr::type_name(attr), value_objects_by_name)
716
+ literal_hash_rhs(
717
+ &Literal::from_json(default),
718
+ crate::attr::type_name(attr),
719
+ value_objects_by_name,
720
+ )
360
721
  } else {
361
722
  naming::literal_rhs(default)
362
723
  });
363
724
  }
364
725
 
365
726
  let vo = value_objects_by_name.get(crate::attr::type_name(attr))?;
366
- if vo.get("closed_set").map(Json::as_bool).unwrap_or(false) {
727
+ if vo.get("closed_set").is_some_and(Json::as_bool) {
367
728
  return None;
368
729
  }
369
- let attrs = vo.get("attributes").map(Json::each).unwrap_or(&[]);
730
+ let attrs = vo.get("attributes").map_or_default(Json::each);
370
731
  // Vacuously true on an empty list, as Ruby's `all?`; only a present field without a default
371
732
  // fails.
372
733
  if !attrs.iter().all(|f| crate::attr::default(f).is_some()) {
373
734
  return None;
374
735
  }
375
- let fields: Vec<String> = attrs.iter().map(|f| format!("{}: {}", naming::rust_ident_field(crate::attr::name(f)), naming::literal_rhs(crate::attr::default(f).unwrap()))).collect();
376
- Some(format!("{} {{ {} }}", naming::rust_ident(crate::attr::type_name(attr)), fields.join(", ")))
736
+ let fields: Vec<String> = attrs
737
+ .iter()
738
+ .map(|f| {
739
+ format!(
740
+ "{}: {}",
741
+ naming::rust_ident_field(crate::attr::name(f)),
742
+ naming::literal_rhs(crate::attr::default(f).unwrap())
743
+ )
744
+ })
745
+ .collect();
746
+ Some(format!(
747
+ "{} {{ {} }}",
748
+ naming::rust_ident(crate::attr::type_name(attr)),
749
+ fields.join(", ")
750
+ ))
377
751
  }
378
752
 
379
753
  /// The `corrects "EventName"` mutation, if this command declares one.
380
754
  pub fn corrects_of(command: &Json) -> Option<&Json> {
381
- command.get("mutations").map(Json::each).unwrap_or(&[]).iter().find(|m| m.get("op").map(Json::to_s).unwrap_or_default() == "corrects")
755
+ command
756
+ .get("mutations")
757
+ .map_or_default(Json::each)
758
+ .iter()
759
+ .find(|m| m.get("op").map(Json::to_s).unwrap_or_default() == "corrects")
382
760
  }
383
761
 
384
762
  /// True when a `corrects` mutation carries `reverses: true`.
@@ -389,8 +767,7 @@ pub fn corrects_reverses(mutation: &Json) -> bool {
389
767
  .get("source")
390
768
  .and_then(|s| s.get("value"))
391
769
  .and_then(|v| v.get("reverses"))
392
- .map(Json::as_bool)
393
- .unwrap_or(false)
770
+ .is_some_and(Json::as_bool)
394
771
  }
395
772
 
396
773
  /// Synthetic flag field name, e.g. `emitted_fee_applied` from `"FeeApplied"`; must match Ruby's.
@@ -412,8 +789,8 @@ pub fn corrects_flag_field(event_name: &str) -> String {
412
789
  /// Every event name any command on this aggregate, entities included, names in a `corrects`
413
790
  /// mutation. The flag field lives on the parent record whichever level declares it.
414
791
  pub fn correctable_event_names(aggregate: &Json) -> Vec<String> {
415
- let mut names = corrects_targets_of(aggregate.get("commands").map(Json::each).unwrap_or(&[]));
416
- for entity in aggregate.get("entities").map(Json::each).unwrap_or(&[]) {
792
+ let mut names = corrects_targets_of(aggregate.get("commands").map_or_default(Json::each));
793
+ for entity in aggregate.get("entities").map_or_default(Json::each) {
417
794
  for name in entity_correctable_event_names(entity) {
418
795
  if !names.contains(&name) {
419
796
  names.push(name);
@@ -425,8 +802,8 @@ pub fn correctable_event_names(aggregate: &Json) -> Vec<String> {
425
802
 
426
803
  /// The entity-recursive half of `correctable_event_names`.
427
804
  fn entity_correctable_event_names(entity: &Json) -> Vec<String> {
428
- let mut names = corrects_targets_of(entity.get("commands").map(Json::each).unwrap_or(&[]));
429
- for nested in entity.get("entities").map(Json::each).unwrap_or(&[]) {
805
+ let mut names = corrects_targets_of(entity.get("commands").map_or_default(Json::each));
806
+ for nested in entity.get("entities").map_or_default(Json::each) {
430
807
  for name in entity_correctable_event_names(nested) {
431
808
  if !names.contains(&name) {
432
809
  names.push(name);
@@ -439,7 +816,7 @@ fn entity_correctable_event_names(entity: &Json) -> Vec<String> {
439
816
  fn corrects_targets_of(commands: &[Json]) -> Vec<String> {
440
817
  let mut names: Vec<String> = Vec::new();
441
818
  for c in commands {
442
- for m in c.get("mutations").map(Json::each).unwrap_or(&[]) {
819
+ for m in c.get("mutations").map_or_default(Json::each) {
443
820
  if m.get("op").map(Json::to_s).unwrap_or_default() == "corrects" {
444
821
  let name = m.get("target").map(Json::to_s).unwrap_or_default();
445
822
  if !names.contains(&name) {
@@ -453,7 +830,9 @@ fn corrects_targets_of(commands: &[Json]) -> Vec<String> {
453
830
 
454
831
  /// The synthetic `GivenSpec` prepended to a `corrects`-declaring command.
455
832
  pub fn corrects_given_specs(command: &Json) -> Vec<String> {
456
- let Some(corrects) = corrects_of(command) else { return Vec::new() };
833
+ let Some(corrects) = corrects_of(command) else {
834
+ return Vec::new();
835
+ };
457
836
  let event_name = corrects.get("target").map(Json::to_s).unwrap_or_default();
458
837
  vec![format!(
459
838
  " crate::kernel::GivenSpec {{ description: \"\", expr: crate::kernel::Expr::Lookup({:?}), corrects_event: Some({:?}) }},",
@@ -462,20 +841,200 @@ pub fn corrects_given_specs(command: &Json) -> Vec<String> {
462
841
  )]
463
842
  }
464
843
 
465
- /// `(key, to_json-expr, deserialize_rhs)` triples for `corrects`'s per-record flag fields.
466
- pub fn corrects_extra_fields(aggregate: &Json) -> Vec<(String, String, String)> {
467
- correctable_event_names(aggregate)
468
- .iter()
469
- .map(|ev| {
470
- let field = corrects_flag_field(ev);
471
- let aggregate_name = crate::attr::name(aggregate).to_string();
472
- let deserialize_rhs = format!(
473
- "match v.require({:?}, {:?})? {{ crate::kernel::Json::Bool(b) => *b, _ => return Err({}) }}",
474
- field,
475
- aggregate_name,
476
- crate::json_codec::json_type_error(&aggregate_name, &field, "a boolean")
477
- );
478
- (field.clone(), format!("crate::kernel::Json::Bool(self.{field})"), deserialize_rhs)
479
- })
480
- .collect()
844
+ #[cfg(test)]
845
+ mod text_operand_tests {
846
+ use super::*;
847
+
848
+ fn aggregate() -> Json {
849
+ Json::parse(
850
+ r#"{"attributes":[
851
+ {"name":"count","type":"Count","list":false,"optional":false},
852
+ {"name":"limit","type":"Count","list":false,"optional":true}]}"#,
853
+ )
854
+ .unwrap()
855
+ }
856
+
857
+ fn vos() -> Vec<Json> {
858
+ vec![
859
+ Json::parse(
860
+ r#"{"name":"Count","attributes":[{"name":"value","type":"Integer","list":false}]}"#,
861
+ )
862
+ .unwrap(),
863
+ Json::parse(
864
+ r#"{"name":"Label","attributes":[{"name":"value","type":"String","list":false}]}"#,
865
+ )
866
+ .unwrap(),
867
+ Json::parse(
868
+ r#"{"name":"Note","attributes":[{"name":"text","type":"String","list":false}]}"#,
869
+ )
870
+ .unwrap(),
871
+ ]
872
+ }
873
+
874
+ fn by_name(vos: &[Json]) -> HashMap<String, &Json> {
875
+ vos.iter()
876
+ .map(|vo| (vo.get("name").map(Json::to_s).unwrap_or_default(), vo))
877
+ .collect()
878
+ }
879
+
880
+ fn operand(target: &str, arg_type: &str, arg_flags: &str) -> Option<String> {
881
+ let mutation = Json::parse(&format!(
882
+ r#"{{"target":"{target}","op":"increment","source":{{"kind":"argument","name":"by"}}}}"#
883
+ ))
884
+ .unwrap();
885
+ let command = Json::parse(&format!(
886
+ r#"{{"attributes":[{{"name":"by","type":"{arg_type}","list":false{arg_flags}}}]}}"#
887
+ ))
888
+ .unwrap();
889
+ let vos = vos();
890
+ text_operand_expr(&mutation, &aggregate(), &command, &by_name(&vos), "value")
891
+ }
892
+
893
+ #[test]
894
+ fn a_value_object_holding_text_in_the_target_field_name_is_a_text_operand() {
895
+ assert_eq!(
896
+ operand("count", "Label", "").as_deref(),
897
+ Some("args.by.value")
898
+ );
899
+ }
900
+
901
+ #[test]
902
+ fn a_bare_string_argument_is_a_text_operand() {
903
+ assert_eq!(operand("count", "String", "").as_deref(), Some("args.by"));
904
+ }
905
+
906
+ #[test]
907
+ fn a_numeric_operand_is_not_a_text_operand() {
908
+ assert_eq!(operand("count", "Count", ""), None);
909
+ assert_eq!(operand("count", "Integer", ""), None);
910
+ }
911
+
912
+ #[test]
913
+ fn a_text_field_named_otherwise_keeps_its_own_refusal() {
914
+ assert_eq!(operand("count", "Note", ""), None);
915
+ }
916
+
917
+ #[test]
918
+ fn an_optional_target_or_argument_keeps_its_own_behaviour() {
919
+ assert_eq!(operand("limit", "Label", ""), None);
920
+ assert_eq!(operand("count", "Label", r#","optional":true"#), None);
921
+ }
922
+ }
923
+
924
+ #[cfg(test)]
925
+ mod float_arithmetic_tests {
926
+ use super::*;
927
+
928
+ fn vos() -> Vec<Json> {
929
+ ["Count:Integer", "Weight:Float", "Label:String"]
930
+ .iter()
931
+ .map(|pair| {
932
+ let (name, ty) = pair.split_once(':').unwrap();
933
+ Json::parse(&format!(
934
+ r#"{{"name":"{name}","attributes":[{{"name":"value","type":"{ty}","list":false}}]}}"#
935
+ ))
936
+ .unwrap()
937
+ })
938
+ .collect()
939
+ }
940
+
941
+ fn by_name(vos: &[Json]) -> HashMap<String, &Json> {
942
+ vos.iter()
943
+ .map(|vo| (vo.get("name").map(Json::to_s).unwrap_or_default(), vo))
944
+ .collect()
945
+ }
946
+
947
+ fn aggregate() -> Json {
948
+ Json::parse(r#"{"attributes":[{"name":"weight","type":"Weight","list":false},{"name":"count","type":"Count","list":false},{"name":"label","type":"Label","list":false}]}"#).unwrap()
949
+ }
950
+
951
+ fn command() -> Json {
952
+ Json::parse(r#"{"attributes":[{"name":"more","type":"Weight"},{"name":"n","type":"Integer"},{"name":"f","type":"Float"},{"name":"c","type":"Count"}]}"#).unwrap()
953
+ }
954
+
955
+ fn mutation(target: &str) -> Json {
956
+ Json::parse(&format!(r#"{{"target":"{target}","op":"increment"}}"#)).unwrap()
957
+ }
958
+
959
+ fn amount(source: &str, kind: NumericKind) -> Option<String> {
960
+ let vos = vos();
961
+ arithmetic_amount_expr(
962
+ &Json::parse(source).unwrap(),
963
+ &command(),
964
+ &by_name(&vos),
965
+ "value",
966
+ kind,
967
+ )
968
+ }
969
+
970
+ #[test]
971
+ fn a_float_field_value_object_is_a_float_target() {
972
+ let vos = vos();
973
+ let aggregate = aggregate();
974
+ let (_, field, kind) =
975
+ arithmetic_target_field(&mutation("weight"), &aggregate, &by_name(&vos)).unwrap();
976
+ assert_eq!((field.as_str(), kind), ("value", NumericKind::Float));
977
+ }
978
+
979
+ #[test]
980
+ fn an_integer_field_value_object_stays_an_integer_target() {
981
+ let vos = vos();
982
+ let aggregate = aggregate();
983
+ let (_, _, kind) =
984
+ arithmetic_target_field(&mutation("count"), &aggregate, &by_name(&vos)).unwrap();
985
+ assert_eq!(kind, NumericKind::Integer);
986
+ }
987
+
988
+ #[test]
989
+ fn a_text_field_value_object_is_no_arithmetic_target() {
990
+ let vos = vos();
991
+ let aggregate = aggregate();
992
+ assert!(arithmetic_target_field(&mutation("label"), &aggregate, &by_name(&vos)).is_none());
993
+ }
994
+
995
+ #[test]
996
+ fn clamp_still_takes_only_integer_targets() {
997
+ let vos = vos();
998
+ let aggregate = aggregate();
999
+ assert!(integer_target_field(&mutation("weight"), &aggregate, &by_name(&vos)).is_none());
1000
+ assert!(integer_target_field(&mutation("count"), &aggregate, &by_name(&vos)).is_some());
1001
+ }
1002
+
1003
+ #[test]
1004
+ fn a_float_target_reads_a_float_value_object_argument() {
1005
+ let source = r#"{"kind":"argument","name":"more"}"#;
1006
+ assert_eq!(
1007
+ amount(source, NumericKind::Float).as_deref(),
1008
+ Some("args.more.value")
1009
+ );
1010
+ }
1011
+
1012
+ #[test]
1013
+ fn a_float_target_widens_an_integer_argument_and_integer_value_object() {
1014
+ assert_eq!(
1015
+ amount(r#"{"kind":"argument","name":"n"}"#, NumericKind::Float).as_deref(),
1016
+ Some("(args.n as f64)")
1017
+ );
1018
+ assert_eq!(
1019
+ amount(r#"{"kind":"argument","name":"c"}"#, NumericKind::Float).as_deref(),
1020
+ Some("(args.c.value as f64)")
1021
+ );
1022
+ }
1023
+
1024
+ #[test]
1025
+ fn a_float_target_takes_float_and_integer_literals() {
1026
+ assert_eq!(
1027
+ amount(r#"{"kind":"literal","value":2.5}"#, NumericKind::Float).as_deref(),
1028
+ Some("2.5")
1029
+ );
1030
+ assert_eq!(
1031
+ amount(r#"{"kind":"literal","value":3}"#, NumericKind::Float).as_deref(),
1032
+ Some("(3 as f64)")
1033
+ );
1034
+ }
1035
+
1036
+ #[test]
1037
+ fn an_integer_target_still_refuses_a_float_literal() {
1038
+ assert!(amount(r#"{"kind":"literal","value":2.5}"#, NumericKind::Integer).is_none());
1039
+ }
481
1040
  }