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
data/rust/host/src/api.rs CHANGED
@@ -3,8 +3,8 @@
3
3
 
4
4
  use crate::auth::Session;
5
5
  use crate::dispatch;
6
+ use crate::host::Host;
6
7
  use crate::journal::LineageConfig;
7
- use crate::lambda_client::LambdaInvoker;
8
8
  use crate::presentation;
9
9
  use crate::presentation_write;
10
10
  use crate::ui_schema;
@@ -19,7 +19,7 @@ use tokio_postgres::Client;
19
19
  /// Every `/api/...` request, answered. `session` is `None` only in tests —
20
20
  /// `web::route` runs `auth_gate` first in production — but it stays an
21
21
  /// `Option` because `/api/me` itself has a real empty-session answer.
22
- #[allow(clippy::too_many_arguments)]
22
+ #[allow(clippy::implicit_hasher)] // reason: the query map always comes from this crate's own default-hashed parser
23
23
  pub async fn route(
24
24
  domain_ir: &Value,
25
25
  method: &str,
@@ -27,11 +27,14 @@ pub async fn route(
27
27
  query: &HashMap<String, String>,
28
28
  raw_body: &str,
29
29
  session: Option<&Session>,
30
- client: &Mutex<Client>,
31
- wasm_path: &Path,
32
- config: &LineageConfig,
33
- invoker: &dyn LambdaInvoker,
30
+ host: Host<'_>,
34
31
  ) -> Value {
32
+ let Host {
33
+ client,
34
+ wasm_path,
35
+ config,
36
+ ..
37
+ } = host;
35
38
  match (method, path) {
36
39
  ("GET", "/api/me") => ok(&me(session)),
37
40
 
@@ -56,9 +59,7 @@ pub async fn route(
56
59
  Err(e) => internal_error(&e.to_string()),
57
60
  },
58
61
 
59
- ("PUT", "/api/presentation") => {
60
- presentation_save(domain_ir, raw_body, client, wasm_path, config, invoker).await
61
- }
62
+ ("PUT", "/api/presentation") => presentation_save(domain_ir, raw_body, host).await,
62
63
 
63
64
  // Collection routes (`/api/:coll[/:id]`), matched last so `:coll`
64
65
  // doesn't swallow `me`/`schema`. Segments are percent-decoded: a
@@ -76,12 +77,14 @@ pub async fn route(
76
77
  ("GET", [collection]) => {
77
78
  collection_index(domain_ir, collection, query, client, wasm_path, config).await
78
79
  }
79
- ("GET", [collection, id]) => record_show(domain_ir, collection, id, client, wasm_path, config).await,
80
+ ("GET", [collection, id]) => {
81
+ record_show(domain_ir, collection, id, client, wasm_path, config).await
82
+ }
80
83
  ("POST", [collection]) => {
81
- collection_create(domain_ir, collection, raw_body, client, wasm_path, config, invoker).await
84
+ collection_create(domain_ir, collection, raw_body, host).await
82
85
  }
83
86
  ("POST", [collection, id, command]) => {
84
- command_route(domain_ir, collection, id, command, raw_body, client, wasm_path, config, invoker).await
87
+ command_route(domain_ir, collection, id, command, raw_body, host).await
85
88
  }
86
89
  _ => not_found(&format!("no API route for {method} {path}")),
87
90
  }
@@ -109,7 +112,10 @@ async fn collection_index(
109
112
  Err(refusal) => return refusal,
110
113
  };
111
114
 
112
- let wanted = params.get("query").map(String::as_str).filter(|name| !name.is_empty());
115
+ let wanted = params
116
+ .get("query")
117
+ .map(String::as_str)
118
+ .filter(|name| !name.is_empty());
113
119
  let named = wanted.and_then(|name| find_query(aggregate, name));
114
120
  if let Some(named) = named {
115
121
  if let Some(args) = query_args_from_params(aggregate, named, params) {
@@ -121,8 +127,16 @@ async fn collection_index(
121
127
  Ok(records) => records,
122
128
  Err(refusal) => return refusal,
123
129
  };
124
- sort_records(&mut records, &resolve_sort(domain_ir, &config, collection, aggregate, params));
125
- ok(&Value::Array(records.into_iter().map(|(id, state)| with_id(&id, &state)).collect()))
130
+ sort_records(
131
+ &mut records,
132
+ resolve_sort(domain_ir, &config, collection, aggregate, params).as_ref(),
133
+ );
134
+ ok(&Value::Array(
135
+ records
136
+ .into_iter()
137
+ .map(|(id, state)| with_id(&id, &state))
138
+ .collect(),
139
+ ))
126
140
  }
127
141
 
128
142
  /// `GET /api/:coll/:id` — one record's full state, or the Ruby
@@ -149,14 +163,22 @@ async fn record_show(
149
163
  };
150
164
  match records.into_iter().find(|(record_id, _)| record_id == id) {
151
165
  Some((record_id, state)) => ok(&with_id(&record_id, &state)),
152
- None => not_found(&format!("no {} found for id {:?}", ui_schema::agg_name(aggregate), id)),
166
+ None => not_found(&format!(
167
+ "no {} found for id {:?}",
168
+ ui_schema::agg_name(aggregate),
169
+ id
170
+ )),
153
171
  }
154
172
  }
155
173
 
156
174
  /// A collection key is presentation, not domain (`collections.<Name>.key`
157
175
  /// can rename it), so this resolves through the same `collection_key`
158
176
  /// `/api/ui-schema` hands the client, regardless of lifecycle.
159
- fn resolve_collection<'a>(domain_ir: &'a Value, config: &Value, collection: &str) -> Result<&'a Value, Value> {
177
+ fn resolve_collection<'a>(
178
+ domain_ir: &'a Value,
179
+ config: &Value,
180
+ collection: &str,
181
+ ) -> Result<&'a Value, Value> {
160
182
  ui_schema::aggregates(domain_ir)
161
183
  .into_iter()
162
184
  .find(|aggregate| ui_schema::collection_key(aggregate, config) == collection)
@@ -179,7 +201,9 @@ async fn read_records(
179
201
  client: &Mutex<Client>,
180
202
  wasm_path: &Path,
181
203
  ) -> Result<Vec<(String, Value)>, Value> {
182
- let result = dispatch::read(client, wasm_path).await.map_err(|e| internal_error(&format!("{e:#}")))?;
204
+ let result = dispatch::read(client, wasm_path)
205
+ .await
206
+ .map_err(|e| internal_error(&format!("{e:#}")))?;
183
207
  let prefix = format!(
184
208
  "{}::{}#",
185
209
  domain_ir.get("name").and_then(|v| v.as_str()).unwrap_or(""),
@@ -191,7 +215,10 @@ async fn read_records(
191
215
  .map(|instances| {
192
216
  instances
193
217
  .iter()
194
- .filter_map(|(key, state)| key.strip_prefix(&prefix).map(|id| (id.to_string(), state.clone())))
218
+ .filter_map(|(key, state)| {
219
+ key.strip_prefix(&prefix)
220
+ .map(|id| (id.to_string(), state.clone()))
221
+ })
195
222
  .collect()
196
223
  })
197
224
  .unwrap_or_default())
@@ -225,28 +252,40 @@ async fn named_query_rows(
225
252
  Ok(result) => result,
226
253
  Err(e) => return internal_error(&format!("{e:#}")),
227
254
  };
228
- let answered = result.get("queries").and_then(|v| v.as_array()).and_then(|queries| queries.first()).cloned();
255
+ let answered = result
256
+ .get("queries")
257
+ .and_then(|v| v.as_array())
258
+ .and_then(|queries| queries.first())
259
+ .cloned();
229
260
  let Some(answered) = answered else {
230
261
  return internal_error(&format!("the kernel answered no query step for {question}"));
231
262
  };
232
- match answered.get("rows").filter(|rows| !rows.is_null()) {
233
- Some(rows) => ok(rows),
234
- // A refused query, not a missing one — the refusal lands in the
235
- // kernel's own top-level `refusals` array, read the same way a
236
- // refused command is.
237
- None => domain_refusal(&result),
238
- }
263
+ // No rows is a refused query, not a missing one — the refusal lands in
264
+ // the kernel's own top-level `refusals` array, read the same way a
265
+ // refused command is.
266
+ answered
267
+ .get("rows")
268
+ .filter(|rows| !rows.is_null())
269
+ .map_or_else(|| domain_refusal(&result), ok)
239
270
  }
240
271
 
241
272
  /// Same-named params, shaped per argument (bare for a reference/primitive,
242
273
  /// wrapped for a value object). `None` the moment a required one is missing.
243
- fn query_args_from_params(aggregate: &Value, named: &Value, params: &HashMap<String, String>) -> Option<Value> {
274
+ fn query_args_from_params(
275
+ aggregate: &Value,
276
+ named: &Value,
277
+ params: &HashMap<String, String>,
278
+ ) -> Option<Value> {
244
279
  let mut args = Map::new();
245
280
  for attribute in named.get("attributes").and_then(|v| v.as_array())? {
246
281
  let name = attribute.get("name").and_then(|v| v.as_str()).unwrap_or("");
247
282
  let raw = params.get(name).filter(|value| !value.is_empty());
248
283
  let Some(raw) = raw else {
249
- if attribute.get("optional").and_then(|v| v.as_bool()).unwrap_or(false) {
284
+ if attribute
285
+ .get("optional")
286
+ .and_then(serde_json::Value::as_bool)
287
+ .unwrap_or(false)
288
+ {
250
289
  continue;
251
290
  }
252
291
  return None;
@@ -272,7 +311,10 @@ fn value_object_field(aggregate: &Value, type_name: &str) -> Option<String> {
272
311
  .and_then(|v| v.as_array())?
273
312
  .iter()
274
313
  .find(|v| v.get("name").and_then(|n| n.as_str()) == Some(type_name))?;
275
- let first = value_object.get("attributes").and_then(|v| v.as_array())?.first()?;
314
+ let first = value_object
315
+ .get("attributes")
316
+ .and_then(|v| v.as_array())?
317
+ .first()?;
276
318
  first.get("name").and_then(|v| v.as_str()).map(String::from)
277
319
  }
278
320
 
@@ -282,16 +324,18 @@ fn value_object_field(aggregate: &Value, type_name: &str) -> Option<String> {
282
324
  /// console does around it: minting an identity (`apply_identity`) and
283
325
  /// checking a cross-aggregate precondition a `given` can't express
284
326
  /// (`check_preconditions`), both driven by `collections.<Name>` config.
285
- #[allow(clippy::too_many_arguments)]
286
327
  async fn collection_create(
287
328
  domain_ir: &Value,
288
329
  collection: &str,
289
330
  raw_body: &str,
290
- client: &Mutex<Client>,
291
- wasm_path: &Path,
292
- config: &LineageConfig,
293
- invoker: &dyn LambdaInvoker,
331
+ host: Host<'_>,
294
332
  ) -> Value {
333
+ let Host {
334
+ client,
335
+ wasm_path,
336
+ config,
337
+ ..
338
+ } = host;
295
339
  let presentation = match presentation::load(client, wasm_path, config).await {
296
340
  Ok(presentation) => presentation,
297
341
  Err(e) => return internal_error(&e.to_string()),
@@ -305,7 +349,10 @@ async fn collection_create(
305
349
  Err(refusal) => return refusal,
306
350
  };
307
351
  let name = ui_schema::agg_name(aggregate);
308
- let Some(creating) = ui_schema::commands(aggregate).into_iter().find(|c| ui_schema::creates(c)) else {
352
+ let Some(creating) = ui_schema::commands(aggregate)
353
+ .into_iter()
354
+ .find(|c| ui_schema::creates(c))
355
+ else {
309
356
  return not_found(&format!("{name} declares no creating command"));
310
357
  };
311
358
 
@@ -316,12 +363,25 @@ async fn collection_create(
316
363
  if let Err(refusal) = apply_identity(&presentation, aggregate, &mut args, records.len()) {
317
364
  return refusal;
318
365
  }
319
- if let Err(refusal) = check_preconditions(domain_ir, &presentation, aggregate, &args, client, wasm_path).await {
366
+ if let Err(refusal) = check_preconditions(
367
+ domain_ir,
368
+ &presentation,
369
+ aggregate,
370
+ &args,
371
+ client,
372
+ wasm_path,
373
+ )
374
+ .await
375
+ {
320
376
  return refusal;
321
377
  }
322
378
 
323
- let verb = format!("{}::{name}.{}", domain_name(domain_ir), command_name(creating));
324
- let outcome = match dispatch::handle_facts(client, wasm_path, &verb, args, None, config, invoker).await {
379
+ let verb = format!(
380
+ "{}::{name}.{}",
381
+ domain_name(domain_ir),
382
+ command_name(creating)
383
+ );
384
+ let outcome = match dispatch::handle_facts(host, &verb, args, None).await {
325
385
  Ok(outcome) => outcome,
326
386
  Err(e) => return internal_error(&format!("{e:#}")),
327
387
  };
@@ -338,18 +398,20 @@ async fn collection_create(
338
398
  /// `POST /api/:coll/:id/:command` — a command against an existing record.
339
399
  /// Order matters: find the record first (404), then the command (404),
340
400
  /// then dispatch — the Ruby engine's own order.
341
- #[allow(clippy::too_many_arguments)]
342
401
  async fn command_route(
343
402
  domain_ir: &Value,
344
403
  collection: &str,
345
404
  id: &str,
346
405
  command: &str,
347
406
  raw_body: &str,
348
- client: &Mutex<Client>,
349
- wasm_path: &Path,
350
- config: &LineageConfig,
351
- invoker: &dyn LambdaInvoker,
407
+ host: Host<'_>,
352
408
  ) -> Value {
409
+ let Host {
410
+ client,
411
+ wasm_path,
412
+ config,
413
+ ..
414
+ } = host;
353
415
  let presentation = match presentation::load(client, wasm_path, config).await {
354
416
  Ok(presentation) => presentation,
355
417
  Err(e) => return internal_error(&e.to_string()),
@@ -374,8 +436,12 @@ async fn command_route(
374
436
  Err(refusal) => return refusal,
375
437
  };
376
438
 
377
- let verb = format!("{}::{name}.{}", domain_name(domain_ir), command_name(declared));
378
- let outcome = match dispatch::handle_routed(client, wasm_path, &verb, json!(id), args, None, config, invoker).await {
439
+ let verb = format!(
440
+ "{}::{name}.{}",
441
+ domain_name(domain_ir),
442
+ command_name(declared)
443
+ );
444
+ let outcome = match dispatch::handle_routed(host, &verb, json!(id), args, None).await {
379
445
  Ok(outcome) => outcome,
380
446
  Err(e) => return internal_error(&format!("{e:#}")),
381
447
  };
@@ -392,7 +458,7 @@ fn dispatchable_command<'a>(aggregate: &'a Value, wanted: &str) -> Option<&'a Va
392
458
  ui_schema::commands(aggregate)
393
459
  .into_iter()
394
460
  .filter(|command| !ui_schema::creates(command))
395
- .find(|command| ui_schema::snake(command_name(command)) == wanted)
461
+ .find(|command| crate::hecks_naming::snake(command_name(command)) == wanted)
396
462
  }
397
463
 
398
464
  fn command_name(command: &Value) -> &str {
@@ -412,7 +478,11 @@ fn parsed_body(raw: &str) -> Result<Value, Value> {
412
478
  }
413
479
  match serde_json::from_str::<Value>(raw) {
414
480
  Ok(Value::Object(body)) => Ok(Value::Object(body)),
415
- _ => Err(refusal(400, "MalformedBody", "request body is not valid JSON")),
481
+ _ => Err(refusal(
482
+ 400,
483
+ "MalformedBody",
484
+ "request body is not valid JSON",
485
+ )),
416
486
  }
417
487
  }
418
488
 
@@ -432,13 +502,24 @@ fn created_id(result: &Value) -> Option<String> {
432
502
  /// The record as it stands after the command, read straight out of the
433
503
  /// kernel's post-dispatch `instances` rather than re-reading the journal.
434
504
  fn created_record(domain_ir: &Value, aggregate: &Value, result: &Value, id: &str) -> Value {
435
- let key = format!("{}::{}#{id}", domain_name(domain_ir), ui_schema::agg_name(aggregate));
436
- match result.get("instances").and_then(|instances| instances.get(&key)) {
437
- Some(state) => ok(&with_id(id, state)),
438
- // Accepted, but this host cannot see the record it just wrote
439
- // — a real inconsistency, never a 200 with an empty body.
440
- None => internal_error(&format!("{key} was accepted but is not in the resulting state")),
441
- }
505
+ let key = format!(
506
+ "{}::{}#{id}",
507
+ domain_name(domain_ir),
508
+ ui_schema::agg_name(aggregate)
509
+ );
510
+ // A missing record was accepted, but this host cannot see what it just
511
+ // wrote — a real inconsistency, never a 200 with an empty body.
512
+ result
513
+ .get("instances")
514
+ .and_then(|instances| instances.get(&key))
515
+ .map_or_else(
516
+ || {
517
+ internal_error(&format!(
518
+ "{key} was accepted but is not in the resulting state"
519
+ ))
520
+ },
521
+ |state| ok(&with_id(id, state)),
522
+ )
442
523
  }
443
524
 
444
525
  /// A refused command, in the Ruby engine's own envelope (422, refusal
@@ -446,12 +527,21 @@ fn created_record(domain_ir: &Value, aggregate: &Value, result: &Value, id: &str
446
527
  /// replays the whole rehydrated history, and every earlier step already
447
528
  /// succeeded once.
448
529
  fn domain_refusal(result: &Value) -> Value {
449
- let last = result.get("refusals").and_then(|r| r.as_array()).and_then(|refusals| refusals.last());
450
- let Some(last) = last else { return refusal(422, "Refused", "the command was refused") };
530
+ let last = result
531
+ .get("refusals")
532
+ .and_then(|r| r.as_array())
533
+ .and_then(|refusals| refusals.last());
534
+ let Some(last) = last else {
535
+ return refusal(422, "Refused", "the command was refused");
536
+ };
451
537
  refusal(
452
538
  422,
453
- last.get("kind").and_then(|v| v.as_str()).unwrap_or("Refused"),
454
- last.get("error").and_then(|v| v.as_str()).unwrap_or("the command was refused"),
539
+ last.get("kind")
540
+ .and_then(|v| v.as_str())
541
+ .unwrap_or("Refused"),
542
+ last.get("error")
543
+ .and_then(|v| v.as_str())
544
+ .unwrap_or("the command was refused"),
455
545
  )
456
546
  }
457
547
 
@@ -461,16 +551,29 @@ fn domain_refusal(result: &Value) -> Value {
461
551
  /// derives it from another submitted field, `sequence` pads the next
462
552
  /// count. Only fires when the field is genuinely missing; a caller that
463
553
  /// supplies one is left alone.
464
- fn apply_identity(presentation: &Value, aggregate: &Value, args: &mut Value, existing: usize) -> Result<(), Value> {
554
+ fn apply_identity(
555
+ presentation: &Value,
556
+ aggregate: &Value,
557
+ args: &mut Value,
558
+ existing: usize,
559
+ ) -> Result<(), Value> {
465
560
  let name = ui_schema::agg_name(aggregate);
466
- let Some(rule) = presentation.get("collections").and_then(|c| c.get(name)).and_then(|c| c.get("identity")) else {
561
+ let Some(rule) = presentation
562
+ .get("collections")
563
+ .and_then(|c| c.get(name))
564
+ .and_then(|c| c.get("identity"))
565
+ else {
566
+ return Ok(());
567
+ };
568
+ let Some(field) = rule.get("field").and_then(|v| v.as_str()) else {
467
569
  return Ok(());
468
570
  };
469
- let Some(field) = rule.get("field").and_then(|v| v.as_str()) else { return Ok(()) };
470
571
  if args.get(field).is_some() {
471
572
  return Ok(());
472
573
  }
473
- let Some(wire_key) = identity_wire_key(aggregate, field) else { return Ok(()) };
574
+ let Some(wire_key) = identity_wire_key(aggregate, field) else {
575
+ return Ok(());
576
+ };
474
577
 
475
578
  let minted = match rule.get("strategy").and_then(|v| v.as_str()) {
476
579
  Some("slug") => {
@@ -479,7 +582,7 @@ fn apply_identity(presentation: &Value, aggregate: &Value, args: &mut Value, exi
479
582
  }
480
583
  Some("sequence") => {
481
584
  let prefix = rule.get("prefix").and_then(|v| v.as_str()).unwrap_or("");
482
- let pad = rule.get("pad").and_then(|v| v.as_u64()).unwrap_or(3);
585
+ let pad = rule.get("pad").and_then(serde_json::Value::as_u64).unwrap_or(3);
483
586
  // `pad` rides in as free-form config JSON, and a width like 10^15 would have the
484
587
  // formatter allocate that many bytes: refuse it rather than abort the process.
485
588
  if pad > MAX_IDENTITY_PAD {
@@ -497,7 +600,9 @@ fn apply_identity(presentation: &Value, aggregate: &Value, args: &mut Value, exi
497
600
  }
498
601
  _ => None,
499
602
  };
500
- let Some(minted) = minted.filter(|value| !value.is_empty()) else { return Ok(()) };
603
+ let Some(minted) = minted.filter(|value| !value.is_empty()) else {
604
+ return Ok(());
605
+ };
501
606
  if let Some(object) = args.as_object_mut() {
502
607
  object.insert(field.to_string(), json!({ wire_key: minted }));
503
608
  }
@@ -521,7 +626,11 @@ fn identity_wire_key(aggregate: &Value, field: &str) -> Option<String> {
521
626
 
522
627
  fn dig_source(args: &Value, source: &str) -> String {
523
628
  match args.get(source) {
524
- Some(Value::Object(fields)) => fields.values().next().map(scalar_to_string).unwrap_or_default(),
629
+ Some(Value::Object(fields)) => fields
630
+ .values()
631
+ .next()
632
+ .map(scalar_to_string)
633
+ .unwrap_or_default(),
525
634
  Some(value) => scalar_to_string(value),
526
635
  None => String::new(),
527
636
  }
@@ -580,8 +689,13 @@ async fn check_preconditions(
580
689
  return Ok(());
581
690
  }
582
691
 
583
- let instances = dispatch::read(client, wasm_path).await.map_err(|e| internal_error(&format!("{e:#}")))?;
584
- let instances = instances.get("instances").cloned().unwrap_or_else(|| json!({}));
692
+ let instances = dispatch::read(client, wasm_path)
693
+ .await
694
+ .map_err(|e| internal_error(&format!("{e:#}")))?;
695
+ let instances = instances
696
+ .get("instances")
697
+ .cloned()
698
+ .unwrap_or_else(|| json!({}));
585
699
 
586
700
  for rule in &rules {
587
701
  check_precondition(domain_ir, aggregate, rule, args, &instances)?;
@@ -589,11 +703,21 @@ async fn check_preconditions(
589
703
  Ok(())
590
704
  }
591
705
 
592
- fn check_precondition(domain_ir: &Value, aggregate: &Value, rule: &Value, args: &Value, instances: &Value) -> Result<(), Value> {
593
- let Some(field) = rule.get("field").and_then(|v| v.as_str()) else { return Ok(()) };
706
+ fn check_precondition(
707
+ domain_ir: &Value,
708
+ aggregate: &Value,
709
+ rule: &Value,
710
+ args: &Value,
711
+ instances: &Value,
712
+ ) -> Result<(), Value> {
713
+ let Some(field) = rule.get("field").and_then(|v| v.as_str()) else {
714
+ return Ok(());
715
+ };
594
716
  // A missing required reference is the command's own validation to
595
717
  // refuse, not this one's.
596
- let Some(target_id) = args.get(field).and_then(|v| v.as_str()) else { return Ok(()) };
718
+ let Some(target_id) = args.get(field).and_then(|v| v.as_str()) else {
719
+ return Ok(());
720
+ };
597
721
  // Config named a field that isn't a reference at all — caught at
598
722
  // save time by the console's own validation, not at dispatch time.
599
723
  let Some(target_name) = ui_schema::find_attribute(aggregate, field)
@@ -612,8 +736,12 @@ fn check_precondition(domain_ir: &Value, aggregate: &Value, rule: &Value, args:
612
736
  ));
613
737
  };
614
738
 
615
- let Some(expected) = rule.get("state").and_then(|v| v.as_str()) else { return Ok(()) };
616
- let target_aggregate = ui_schema::aggregates(domain_ir).into_iter().find(|a| ui_schema::agg_name(a) == target_name);
739
+ let Some(expected) = rule.get("state").and_then(|v| v.as_str()) else {
740
+ return Ok(());
741
+ };
742
+ let target_aggregate = ui_schema::aggregates(domain_ir)
743
+ .into_iter()
744
+ .find(|a| ui_schema::agg_name(a) == target_name);
617
745
  // Config named a state precondition against an aggregate with no
618
746
  // lifecycle — again a save-time concern, not a dispatch-time one.
619
747
  let Some(state_field) = target_aggregate
@@ -624,22 +752,42 @@ fn check_precondition(domain_ir: &Value, aggregate: &Value, rule: &Value, args:
624
752
  return Ok(());
625
753
  };
626
754
 
627
- let actual = target.get(&state_field).and_then(|v| v.as_str()).unwrap_or("");
755
+ let actual = target
756
+ .get(&state_field)
757
+ .and_then(|v| v.as_str())
758
+ .unwrap_or("");
628
759
  if actual == expected {
629
760
  return Ok(());
630
761
  }
631
- Err(precondition_failed(rule, &format!("{target_name} is at {actual}, not {expected}")))
762
+ Err(precondition_failed(
763
+ rule,
764
+ &format!("{target_name} is at {actual}, not {expected}"),
765
+ ))
632
766
  }
633
767
 
634
768
  fn precondition_failed(rule: &Value, derived: &str) -> Value {
635
- refusal(422, "PreconditionFailed", rule.get("message").and_then(|v| v.as_str()).unwrap_or(derived))
769
+ refusal(
770
+ 422,
771
+ "PreconditionFailed",
772
+ rule.get("message")
773
+ .and_then(|v| v.as_str())
774
+ .unwrap_or(derived),
775
+ )
636
776
  }
637
777
 
638
778
  // ---- sorting ---------------------------------------------------------
639
779
 
640
780
  /// Column shapes a JSONB order path resolves correctly. A fold over a
641
781
  /// list or a cross-aggregate join is excluded and stays client-side.
642
- const SQL_SORTABLE_SHAPES: &[&str] = &["text_value", "text", "address", "number", "money", "enum", "pill"];
782
+ const SQL_SORTABLE_SHAPES: &[&str] = &[
783
+ "text_value",
784
+ "text",
785
+ "address",
786
+ "number",
787
+ "money",
788
+ "enum",
789
+ "pill",
790
+ ];
643
791
 
644
792
  struct SortSpec {
645
793
  field: String,
@@ -676,14 +824,19 @@ fn resolve_sort(
676
824
  let field = if key == "__state__" {
677
825
  collection_schema.get("stateField")?.as_str()?.to_string()
678
826
  } else {
679
- key.to_string()
827
+ key.clone()
680
828
  };
681
829
  // Config could still name a field the aggregate doesn't have.
682
- if ui_schema::find_attribute(aggregate, &field).is_none() && lifecycle_field(aggregate) != Some(field.as_str()) {
830
+ if ui_schema::find_attribute(aggregate, &field).is_none()
831
+ && lifecycle_field(aggregate) != Some(field.as_str())
832
+ {
683
833
  return None;
684
834
  }
685
835
 
686
- Some(SortSpec { field, descending: params.get("direction").map(String::as_str) == Some("desc") })
836
+ Some(SortSpec {
837
+ field,
838
+ descending: params.get("direction").map(String::as_str) == Some("desc"),
839
+ })
687
840
  }
688
841
 
689
842
  fn lifecycle_field(aggregate: &Value) -> Option<&str> {
@@ -692,11 +845,15 @@ fn lifecycle_field(aggregate: &Value) -> Option<&str> {
692
845
 
693
846
  /// In memory, not SQL: there's no ORDER BY to push into, so this follows
694
847
  /// Postgres's own null ordering, with a stable id fallback for ties.
695
- fn sort_records(records: &mut [(String, Value)], spec: &Option<SortSpec>) {
848
+ fn sort_records(records: &mut [(String, Value)], spec: Option<&SortSpec>) {
696
849
  match spec {
697
850
  None => records.sort_by(|(a, _), (b, _)| a.cmp(b)),
698
851
  Some(spec) => records.sort_by(|(a_id, a), (b_id, b)| {
699
- let ordering = compare_sort_keys(sort_key(a, &spec.field), sort_key(b, &spec.field), spec.descending);
852
+ let ordering = compare_sort_keys(
853
+ sort_key(a, &spec.field),
854
+ sort_key(b, &spec.field),
855
+ spec.descending,
856
+ );
700
857
  ordering.then_with(|| a_id.cmp(b_id))
701
858
  }),
702
859
  }
@@ -706,7 +863,9 @@ fn sort_records(records: &mut [(String, Value)], spec: &Option<SortSpec>) {
706
863
  /// a money value object, or a single-attribute value object's member.
707
864
  fn sort_key<'a>(state: &'a Value, field: &str) -> Option<&'a Value> {
708
865
  let value = state.get(field).filter(|v| !v.is_null())?;
709
- let Some(object) = value.as_object() else { return Some(value) };
866
+ let Some(object) = value.as_object() else {
867
+ return Some(value);
868
+ };
710
869
  if let Some(cents) = object.get("cents") {
711
870
  return Some(cents);
712
871
  }
@@ -739,7 +898,7 @@ fn compare_sort_keys(left: Option<&Value>, right: Option<&Value>, descending: bo
739
898
  /// hold mixed types, and `sort_by` panics on a comparison that is not a total order: comparing a
740
899
  /// number with a string as `""` made `5`, `""` and `3` each equal to a neighbour but not to each other.
741
900
  fn compare_values(left: &Value, right: &Value) -> Ordering {
742
- fn rank(value: &Value) -> u8 {
901
+ const fn rank(value: &Value) -> u8 {
743
902
  match value {
744
903
  Value::Number(_) => 0,
745
904
  Value::String(_) => 1,
@@ -747,7 +906,10 @@ fn compare_values(left: &Value, right: &Value) -> Ordering {
747
906
  }
748
907
  }
749
908
  match (left, right) {
750
- (Value::Number(a), Value::Number(b)) => a.as_f64().unwrap_or(0.0).total_cmp(&b.as_f64().unwrap_or(0.0)),
909
+ (Value::Number(a), Value::Number(b)) => a
910
+ .as_f64()
911
+ .unwrap_or(0.0)
912
+ .total_cmp(&b.as_f64().unwrap_or(0.0)),
751
913
  (Value::String(a), Value::String(b)) => a.cmp(b),
752
914
  _ => rank(left).cmp(&rank(right)),
753
915
  }
@@ -757,7 +919,9 @@ fn compare_values(left: &Value, right: &Value) -> Ordering {
757
919
  /// `session`, or `{}` with no session, which index.html treats as
758
920
  /// "don't render the signed-in banner".
759
921
  fn me(session: Option<&Session>) -> Value {
760
- let Some(session) = session else { return json!({}) };
922
+ let Some(session) = session else {
923
+ return json!({});
924
+ };
761
925
  json!({
762
926
  "email": session.email,
763
927
  "name": session.name,
@@ -770,24 +934,20 @@ fn me(session: Option<&Session>) -> Value {
770
934
  /// against this domain's live shape, answered with the reloaded config.
771
935
  /// 200 on save; 400 `MalformedBody`; 422 `Malformed` or a domain refusal;
772
936
  /// 501 when this host's kernel carries no ConsoleSettings chapter.
773
- #[allow(clippy::too_many_arguments)]
774
- async fn presentation_save(
775
- domain_ir: &Value,
776
- raw_body: &str,
777
- client: &Mutex<Client>,
778
- wasm_path: &Path,
779
- config: &LineageConfig,
780
- invoker: &dyn LambdaInvoker,
781
- ) -> Value {
937
+ async fn presentation_save(domain_ir: &Value, raw_body: &str, host: Host<'_>) -> Value {
782
938
  let submitted = match parsed_body(raw_body) {
783
939
  Ok(submitted) => submitted,
784
940
  Err(refusal) => return refusal,
785
941
  };
786
942
 
787
- match presentation_write::save(domain_ir, &submitted, client, wasm_path, config, invoker).await {
943
+ match presentation_write::save(domain_ir, &submitted, host).await {
788
944
  Ok(saved) => ok(&saved),
789
- Err(presentation_write::SaveRefusal::NoKernel) => not_implemented(PRESENTATION_WRITE_REFUSAL),
790
- Err(presentation_write::SaveRefusal::Malformed(message)) => refusal(422, "Malformed", &message),
945
+ Err(presentation_write::SaveRefusal::NoKernel) => {
946
+ not_implemented(PRESENTATION_WRITE_REFUSAL)
947
+ }
948
+ Err(presentation_write::SaveRefusal::Malformed(message)) => {
949
+ refusal(422, "Malformed", &message)
950
+ }
791
951
  Err(presentation_write::SaveRefusal::Domain(result)) => domain_refusal(&result),
792
952
  Err(presentation_write::SaveRefusal::Internal(message)) => internal_error(&message),
793
953
  }
@@ -821,7 +981,11 @@ fn not_implemented(message: &str) -> Value {
821
981
  }
822
982
 
823
983
  pub(crate) fn refusal(status: u16, error: &str, message: &str) -> Value {
824
- respond(status, "application/json", &json!({"error": error, "message": message}).to_string())
984
+ respond(
985
+ status,
986
+ "application/json",
987
+ &json!({"error": error, "message": message}).to_string(),
988
+ )
825
989
  }
826
990
 
827
991
  #[cfg(test)]
@@ -885,7 +1049,11 @@ mod tests {
885
1049
  // pinned against app.rb's own `json({ error:, message: })`.
886
1050
  #[test]
887
1051
  fn a_refusal_carries_exactly_error_and_message() {
888
- let response = refusal(422, "PreconditionFailed", "the proposal hasn't been accepted yet");
1052
+ let response = refusal(
1053
+ 422,
1054
+ "PreconditionFailed",
1055
+ "the proposal hasn't been accepted yet",
1056
+ );
889
1057
  assert_eq!(response["statusCode"], 422);
890
1058
  assert_eq!(
891
1059
  response["body"],
@@ -933,7 +1101,10 @@ mod tests {
933
1101
  }
934
1102
 
935
1103
  fn params(pairs: &[(&str, &str)]) -> HashMap<String, String> {
936
- pairs.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect()
1104
+ pairs
1105
+ .iter()
1106
+ .map(|(k, v)| (k.to_string(), v.to_string()))
1107
+ .collect()
937
1108
  }
938
1109
 
939
1110
  #[test]
@@ -950,10 +1121,14 @@ mod tests {
950
1121
 
951
1122
  #[test]
952
1123
  fn an_unknown_collection_is_the_ruby_engines_own_not_found_message() {
953
- let refusal = resolve_collection(&domain(), &json!({}), "nope").expect_err("unknown collections refuse");
1124
+ let refusal = resolve_collection(&domain(), &json!({}), "nope")
1125
+ .expect_err("unknown collections refuse");
954
1126
 
955
1127
  assert_eq!(refusal["statusCode"], 404);
956
- assert_eq!(body(&refusal), json!({"error": "NotFound", "message": "no such collection: nope"}));
1128
+ assert_eq!(
1129
+ body(&refusal),
1130
+ json!({"error": "NotFound", "message": "no such collection: nope"})
1131
+ );
957
1132
  }
958
1133
 
959
1134
  #[test]
@@ -973,7 +1148,11 @@ mod tests {
973
1148
  let by_name = find_query(&aggregate, "ByName").expect("declared");
974
1149
 
975
1150
  assert_eq!(
976
- query_args_from_params(&aggregate, by_owner, &params(&[("owner", "chris@example.com")])),
1151
+ query_args_from_params(
1152
+ &aggregate,
1153
+ by_owner,
1154
+ &params(&[("owner", "chris@example.com")])
1155
+ ),
977
1156
  Some(json!({"owner": "chris@example.com"}))
978
1157
  );
979
1158
  assert_eq!(
@@ -987,10 +1166,16 @@ mod tests {
987
1166
  let aggregate = client_aggregate();
988
1167
  let by_owner = find_query(&aggregate, "ByOwner").expect("declared");
989
1168
 
990
- assert_eq!(query_args_from_params(&aggregate, by_owner, &params(&[])), None);
1169
+ assert_eq!(
1170
+ query_args_from_params(&aggregate, by_owner, &params(&[])),
1171
+ None
1172
+ );
991
1173
  // An empty param is the same as an absent one, matching Ruby's
992
1174
  // own `raw.nil? || raw.empty?`.
993
- assert_eq!(query_args_from_params(&aggregate, by_owner, &params(&[("owner", "")])), None);
1175
+ assert_eq!(
1176
+ query_args_from_params(&aggregate, by_owner, &params(&[("owner", "")])),
1177
+ None
1178
+ );
994
1179
  }
995
1180
 
996
1181
  #[test]
@@ -998,8 +1183,14 @@ mod tests {
998
1183
  let aggregate = client_aggregate();
999
1184
  let since = find_query(&aggregate, "Since").expect("declared");
1000
1185
 
1001
- assert_eq!(query_args_from_params(&aggregate, since, &params(&[])), Some(json!({})));
1002
- assert_eq!(query_args_from_params(&aggregate, since, &params(&[("year", "2026")])), Some(json!({"year": "2026"})));
1186
+ assert_eq!(
1187
+ query_args_from_params(&aggregate, since, &params(&[])),
1188
+ Some(json!({}))
1189
+ );
1190
+ assert_eq!(
1191
+ query_args_from_params(&aggregate, since, &params(&[("year", "2026")])),
1192
+ Some(json!({"year": "2026"}))
1193
+ );
1003
1194
  }
1004
1195
 
1005
1196
  #[test]
@@ -1007,7 +1198,10 @@ mod tests {
1007
1198
  let aggregate = client_aggregate();
1008
1199
  let active = find_query(&aggregate, "Active").expect("declared");
1009
1200
 
1010
- assert_eq!(query_args_from_params(&aggregate, active, &params(&[])), Some(json!({})));
1201
+ assert_eq!(
1202
+ query_args_from_params(&aggregate, active, &params(&[])),
1203
+ Some(json!({}))
1204
+ );
1011
1205
  }
1012
1206
 
1013
1207
  #[test]
@@ -1021,27 +1215,61 @@ mod tests {
1021
1215
  fn a_sortable_column_of_a_pushable_shape_sorts_and_anything_else_falls_back() {
1022
1216
  let domain = domain();
1023
1217
  let aggregate = client_aggregate();
1024
- let spec = resolve_sort(&domain, &json!({}), "clients", &aggregate, &params(&[("sort", "name")]));
1218
+ let spec = resolve_sort(
1219
+ &domain,
1220
+ &json!({}),
1221
+ "clients",
1222
+ &aggregate,
1223
+ &params(&[("sort", "name")]),
1224
+ );
1025
1225
  let spec = spec.expect("a text_value column is sortable");
1026
1226
  assert_eq!(spec.field, "name");
1027
1227
  assert!(!spec.descending);
1028
1228
 
1029
1229
  // A reference column is deliberately excluded — it would need a
1030
1230
  // cross-aggregate join no order clause wires.
1031
- assert!(resolve_sort(&domain, &json!({}), "clients", &aggregate, &params(&[("sort", "owner")])).is_none());
1231
+ assert!(resolve_sort(
1232
+ &domain,
1233
+ &json!({}),
1234
+ "clients",
1235
+ &aggregate,
1236
+ &params(&[("sort", "owner")])
1237
+ )
1238
+ .is_none());
1032
1239
  // …as is a column config explicitly opts out of.
1033
- let opted_out = json!({"collections": {"Client": {"columns": [{"field": "name", "sortable": false}]}}});
1034
- assert!(resolve_sort(&domain, &opted_out, "clients", &aggregate, &params(&[("sort", "name")])).is_none());
1240
+ let opted_out =
1241
+ json!({"collections": {"Client": {"columns": [{"field": "name", "sortable": false}]}}});
1242
+ assert!(resolve_sort(
1243
+ &domain,
1244
+ &opted_out,
1245
+ "clients",
1246
+ &aggregate,
1247
+ &params(&[("sort", "name")])
1248
+ )
1249
+ .is_none());
1035
1250
  // …and a column key that isn't in this collection at all.
1036
- assert!(resolve_sort(&domain, &json!({}), "clients", &aggregate, &params(&[("sort", "nope")])).is_none());
1251
+ assert!(resolve_sort(
1252
+ &domain,
1253
+ &json!({}),
1254
+ "clients",
1255
+ &aggregate,
1256
+ &params(&[("sort", "nope")])
1257
+ )
1258
+ .is_none());
1037
1259
  }
1038
1260
 
1039
1261
  #[test]
1040
1262
  fn sorting_by_the_state_column_sorts_by_whatever_the_lifecycle_field_is_really_called() {
1041
1263
  let domain = domain();
1042
1264
  let aggregate = client_aggregate();
1043
- let spec = resolve_sort(&domain, &json!({}), "clients", &aggregate, &params(&[("sort", "__state__"), ("direction", "desc")]))
1044
- .expect("the derived state pill is sortable");
1265
+ let spec = resolve_sort(
1266
+ &domain,
1267
+ &json!({}),
1268
+ "clients",
1269
+ &aggregate,
1270
+ &params(&[("sort", "__state__"), ("direction", "desc")]),
1271
+ )
1272
+ .expect("the derived state pill is sortable");
1045
1273
 
1046
1274
  assert_eq!(spec.field, "status");
1047
1275
  assert!(spec.descending);
@@ -1053,25 +1281,55 @@ mod tests {
1053
1281
  ("b".to_string(), json!({"name": {"value": "Zed"}})),
1054
1282
  ("a".to_string(), json!({"name": {"value": "Ann"}})),
1055
1283
  ];
1056
- sort_records(&mut records, &None);
1284
+ sort_records(&mut records, None);
1057
1285
 
1058
- assert_eq!(records.iter().map(|(id, _)| id.as_str()).collect::<Vec<&str>>(), vec!["a", "b"]);
1286
+ assert_eq!(
1287
+ records
1288
+ .iter()
1289
+ .map(|(id, _)| id.as_str())
1290
+ .collect::<Vec<&str>>(),
1291
+ vec!["a", "b"]
1292
+ );
1059
1293
  }
1060
1294
 
1061
1295
  #[test]
1062
1296
  fn sorting_drills_through_a_single_attribute_value_object_and_through_money_to_its_cents() {
1063
1297
  let mut records = vec![
1064
- ("c".to_string(), json!({"name": {"value": "Cara"}, "fee": {"cents": 300, "currency": "USD"}})),
1065
- ("a".to_string(), json!({"name": {"value": "Ann"}, "fee": {"cents": 1000, "currency": "USD"}})),
1298
+ (
1299
+ "c".to_string(),
1300
+ json!({"name": {"value": "Cara"}, "fee": {"cents": 300, "currency": "USD"}}),
1301
+ ),
1302
+ (
1303
+ "a".to_string(),
1304
+ json!({"name": {"value": "Ann"}, "fee": {"cents": 1000, "currency": "USD"}}),
1305
+ ),
1066
1306
  ];
1067
1307
 
1068
- sort_records(&mut records, &Some(SortSpec { field: "name".to_string(), descending: false }));
1308
+ sort_records(
1309
+ &mut records,
1310
+ Some(&SortSpec {
1311
+ field: "name".to_string(),
1312
+ descending: false,
1313
+ }),
1314
+ );
1069
1315
  assert_eq!(records[0].0, "a");
1070
1316
 
1071
1317
  // Money sorts numerically by cents — 300 before 1000, which a
1072
1318
  // string comparison would get backwards.
1073
- sort_records(&mut records, &Some(SortSpec { field: "fee".to_string(), descending: false }));
1074
- assert_eq!(records.iter().map(|(id, _)| id.as_str()).collect::<Vec<&str>>(), vec!["c", "a"]);
1319
+ sort_records(
1320
+ &mut records,
1321
+ Some(&SortSpec {
1322
+ field: "fee".to_string(),
1323
+ descending: false,
1324
+ }),
1325
+ );
1326
+ assert_eq!(
1327
+ records
1328
+ .iter()
1329
+ .map(|(id, _)| id.as_str())
1330
+ .collect::<Vec<&str>>(),
1331
+ vec!["c", "a"]
1332
+ );
1075
1333
  }
1076
1334
 
1077
1335
  #[test]
@@ -1081,11 +1339,35 @@ mod tests {
1081
1339
  ("ann".to_string(), json!({"name": {"value": "Ann"}})),
1082
1340
  ];
1083
1341
 
1084
- sort_records(&mut records, &Some(SortSpec { field: "name".to_string(), descending: false }));
1085
- assert_eq!(records.iter().map(|(id, _)| id.as_str()).collect::<Vec<&str>>(), vec!["ann", "missing"]);
1342
+ sort_records(
1343
+ &mut records,
1344
+ Some(&SortSpec {
1345
+ field: "name".to_string(),
1346
+ descending: false,
1347
+ }),
1348
+ );
1349
+ assert_eq!(
1350
+ records
1351
+ .iter()
1352
+ .map(|(id, _)| id.as_str())
1353
+ .collect::<Vec<&str>>(),
1354
+ vec!["ann", "missing"]
1355
+ );
1086
1356
 
1087
- sort_records(&mut records, &Some(SortSpec { field: "name".to_string(), descending: true }));
1088
- assert_eq!(records.iter().map(|(id, _)| id.as_str()).collect::<Vec<&str>>(), vec!["missing", "ann"]);
1357
+ sort_records(
1358
+ &mut records,
1359
+ Some(&SortSpec {
1360
+ field: "name".to_string(),
1361
+ descending: true,
1362
+ }),
1363
+ );
1364
+ assert_eq!(
1365
+ records
1366
+ .iter()
1367
+ .map(|(id, _)| id.as_str())
1368
+ .collect::<Vec<&str>>(),
1369
+ vec!["missing", "ann"]
1370
+ );
1089
1371
  }
1090
1372
 
1091
1373
  #[test]
@@ -1093,7 +1375,10 @@ mod tests {
1093
1375
  // `JSON_ADAPTER.find!`'s own message, `id` inspected the way Ruby
1094
1376
  // inspects it.
1095
1377
  let refusal = not_found(&format!("no {} found for id {:?}", "Client", "acme"));
1096
- assert_eq!(body(&refusal), json!({"error": "NotFound", "message": "no Client found for id \"acme\""}));
1378
+ assert_eq!(
1379
+ body(&refusal),
1380
+ json!({"error": "NotFound", "message": "no Client found for id \"acme\""})
1381
+ );
1097
1382
  }
1098
1383
 
1099
1384
  // ---- write routes ------------------------------------------------
@@ -1102,11 +1387,17 @@ mod tests {
1102
1387
  fn an_empty_body_is_an_empty_argument_hash_and_anything_unparseable_refuses() {
1103
1388
  assert_eq!(parsed_body(""), Ok(json!({})));
1104
1389
  assert_eq!(parsed_body(" "), Ok(json!({})));
1105
- assert_eq!(parsed_body(r#"{"name":{"value":"Acme"}}"#), Ok(json!({"name": {"value": "Acme"}})));
1390
+ assert_eq!(
1391
+ parsed_body(r#"{"name":{"value":"Acme"}}"#),
1392
+ Ok(json!({"name": {"value": "Acme"}}))
1393
+ );
1106
1394
 
1107
1395
  let refusal = parsed_body("not json").expect_err("a malformed body refuses");
1108
1396
  assert_eq!(refusal["statusCode"], 400);
1109
- assert_eq!(body(&refusal), json!({"error": "MalformedBody", "message": "request body is not valid JSON"}));
1397
+ assert_eq!(
1398
+ body(&refusal),
1399
+ json!({"error": "MalformedBody", "message": "request body is not valid JSON"})
1400
+ );
1110
1401
  // A body that parses but isn't an object has no arguments to
1111
1402
  // splat — refused the same way rather than dispatched.
1112
1403
  assert!(parsed_body("[1,2,3]").is_err());
@@ -1122,7 +1413,10 @@ mod tests {
1122
1413
  ]
1123
1414
  });
1124
1415
 
1125
- assert_eq!(command_name(dispatchable_command(&aggregate, "advance_cycle").expect("declared")), "AdvanceCycle");
1416
+ assert_eq!(
1417
+ command_name(dispatchable_command(&aggregate, "advance_cycle").expect("declared")),
1418
+ "AdvanceCycle"
1419
+ );
1126
1420
  // The creating command is reachable through POST /api/:coll,
1127
1421
  // never here — accepting it would pass this gate and then fail
1128
1422
  // as something far less legible.
@@ -1160,7 +1454,7 @@ mod tests {
1160
1454
 
1161
1455
  // ---- identity minting --------------------------------------------
1162
1456
 
1163
- fn client_with_identity(identity: Value) -> (Value, Value) {
1457
+ fn client_with_identity(identity: &Value) -> (Value, Value) {
1164
1458
  let aggregate = json!({
1165
1459
  "name": "Client",
1166
1460
  "identified_by": ["reference.value"],
@@ -1181,7 +1475,9 @@ mod tests {
1181
1475
 
1182
1476
  #[test]
1183
1477
  fn a_slug_identity_is_minted_from_another_submitted_field() {
1184
- let (aggregate, presentation) = client_with_identity(json!({"field": "reference", "strategy": "slug", "source": "name"}));
1478
+ let (aggregate, presentation) = client_with_identity(
1479
+ &json!({"field": "reference", "strategy": "slug", "source": "name"}),
1480
+ );
1185
1481
  let mut args = json!({"name": {"value": "Acme Corp."}});
1186
1482
 
1187
1483
  apply_identity(&presentation, &aggregate, &mut args, 0).expect("slugging never refuses");
@@ -1191,8 +1487,9 @@ mod tests {
1191
1487
 
1192
1488
  #[test]
1193
1489
  fn a_sequence_identity_counts_the_records_already_there_and_pads_to_the_configured_width() {
1194
- let (aggregate, presentation) =
1195
- client_with_identity(json!({"field": "reference", "strategy": "sequence", "prefix": "C-", "pad": 4}));
1490
+ let (aggregate, presentation) = client_with_identity(
1491
+ &json!({"field": "reference", "strategy": "sequence", "prefix": "C-", "pad": 4}),
1492
+ );
1196
1493
  let mut args = json!({"name": {"value": "Acme"}});
1197
1494
 
1198
1495
  apply_identity(&presentation, &aggregate, &mut args, 41).expect("sequencing never refuses");
@@ -1202,17 +1499,21 @@ mod tests {
1202
1499
 
1203
1500
  #[test]
1204
1501
  fn an_identity_the_caller_supplied_is_left_exactly_as_it_came() {
1205
- let (aggregate, presentation) = client_with_identity(json!({"field": "reference", "strategy": "slug", "source": "name"}));
1502
+ let (aggregate, presentation) = client_with_identity(
1503
+ &json!({"field": "reference", "strategy": "slug", "source": "name"}),
1504
+ );
1206
1505
  let mut args = json!({"reference": {"value": "chosen-by-hand"}, "name": {"value": "Acme"}});
1207
1506
 
1208
- apply_identity(&presentation, &aggregate, &mut args, 0).expect("supplied identities never refuse");
1507
+ apply_identity(&presentation, &aggregate, &mut args, 0)
1508
+ .expect("supplied identities never refuse");
1209
1509
 
1210
1510
  assert_eq!(args["reference"], json!({"value": "chosen-by-hand"}));
1211
1511
  }
1212
1512
 
1213
1513
  #[test]
1214
1514
  fn an_identity_rule_naming_a_reference_field_mints_nothing_rather_than_guessing_a_wire_shape() {
1215
- let (aggregate, presentation) = client_with_identity(json!({"field": "owner", "strategy": "slug", "source": "name"}));
1515
+ let (aggregate, presentation) =
1516
+ client_with_identity(&json!({"field": "owner", "strategy": "slug", "source": "name"}));
1216
1517
  let mut args = json!({"name": {"value": "Acme"}});
1217
1518
 
1218
1519
  apply_identity(&presentation, &aggregate, &mut args, 0).expect("no refusal");
@@ -1224,14 +1525,19 @@ mod tests {
1224
1525
  // surfacing a confusing "absent argument" from the kernel.
1225
1526
  #[test]
1226
1527
  fn a_port_identity_strategy_refuses_in_its_own_words_rather_than_dispatching_without_one() {
1227
- let (aggregate, presentation) = client_with_identity(json!({"field": "reference", "strategy": "port"}));
1528
+ let (aggregate, presentation) =
1529
+ client_with_identity(&json!({"field": "reference", "strategy": "port"}));
1228
1530
  let mut args = json!({"name": {"value": "Acme"}});
1229
1531
 
1230
- let refusal = apply_identity(&presentation, &aggregate, &mut args, 0).expect_err("the port strategy refuses");
1532
+ let refusal = apply_identity(&presentation, &aggregate, &mut args, 0)
1533
+ .expect_err("the port strategy refuses");
1231
1534
 
1232
1535
  assert_eq!(refusal["statusCode"], 501);
1233
1536
  assert!(
1234
- body(&refusal)["message"].as_str().expect("a message").contains("identity_assignment"),
1537
+ body(&refusal)["message"]
1538
+ .as_str()
1539
+ .expect("a message")
1540
+ .contains("identity_assignment"),
1235
1541
  "{refusal}"
1236
1542
  );
1237
1543
  }
@@ -1280,7 +1586,14 @@ mod tests {
1280
1586
  let instances = json!({"SampleApp::Proposal#P-001": {"status": "accepted"}});
1281
1587
  let rule = json!({"field": "proposal_id", "state": "accepted"});
1282
1588
 
1283
- assert!(check_precondition(&precondition_domain(), &contract(), &rule, &json!({"proposal_id": "P-001"}), &instances).is_ok());
1589
+ assert!(check_precondition(
1590
+ &precondition_domain(),
1591
+ &contract(),
1592
+ &rule,
1593
+ &json!({"proposal_id": "P-001"}),
1594
+ &instances
1595
+ )
1596
+ .is_ok());
1284
1597
  }
1285
1598
 
1286
1599
  #[test]
@@ -1288,8 +1601,14 @@ mod tests {
1288
1601
  let instances = json!({"SampleApp::Proposal#P-001": {"status": "sent"}});
1289
1602
  let rule = json!({"field": "proposal_id", "state": "accepted"});
1290
1603
 
1291
- let refusal = check_precondition(&precondition_domain(), &contract(), &rule, &json!({"proposal_id": "P-001"}), &instances)
1292
- .expect_err("a proposal that isn't accepted refuses");
1604
+ let refusal = check_precondition(
1605
+ &precondition_domain(),
1606
+ &contract(),
1607
+ &rule,
1608
+ &json!({"proposal_id": "P-001"}),
1609
+ &instances,
1610
+ )
1611
+ .expect_err("a proposal that isn't accepted refuses");
1293
1612
 
1294
1613
  assert_eq!(refusal["statusCode"], 422);
1295
1614
  assert_eq!(
@@ -1303,20 +1622,38 @@ mod tests {
1303
1622
  let rule = json!({"field": "proposal_id", "state": "accepted", "message": "the proposal hasn't been accepted yet"});
1304
1623
  let instances = json!({"SampleApp::Proposal#P-001": {"status": "sent"}});
1305
1624
 
1306
- let refusal = check_precondition(&precondition_domain(), &contract(), &rule, &json!({"proposal_id": "P-001"}), &instances)
1307
- .expect_err("refuses");
1625
+ let refusal = check_precondition(
1626
+ &precondition_domain(),
1627
+ &contract(),
1628
+ &rule,
1629
+ &json!({"proposal_id": "P-001"}),
1630
+ &instances,
1631
+ )
1632
+ .expect_err("refuses");
1308
1633
 
1309
- assert_eq!(body(&refusal)["message"], "the proposal hasn't been accepted yet");
1634
+ assert_eq!(
1635
+ body(&refusal)["message"],
1636
+ "the proposal hasn't been accepted yet"
1637
+ );
1310
1638
  }
1311
1639
 
1312
1640
  #[test]
1313
1641
  fn a_precondition_refuses_a_reference_to_a_record_that_does_not_exist_at_all() {
1314
1642
  let rule = json!({"field": "proposal_id", "state": "accepted"});
1315
1643
 
1316
- let refusal = check_precondition(&precondition_domain(), &contract(), &rule, &json!({"proposal_id": "ghost"}), &json!({}))
1317
- .expect_err("refuses");
1644
+ let refusal = check_precondition(
1645
+ &precondition_domain(),
1646
+ &contract(),
1647
+ &rule,
1648
+ &json!({"proposal_id": "ghost"}),
1649
+ &json!({}),
1650
+ )
1651
+ .expect_err("refuses");
1318
1652
 
1319
- assert_eq!(body(&refusal), json!({"error": "PreconditionFailed", "message": "no proposal with that id"}));
1653
+ assert_eq!(
1654
+ body(&refusal),
1655
+ json!({"error": "PreconditionFailed", "message": "no proposal with that id"})
1656
+ );
1320
1657
  }
1321
1658
 
1322
1659
  #[test]
@@ -1325,14 +1662,28 @@ mod tests {
1325
1662
  // to refuse, in the domain's own words — not this check's.
1326
1663
  let rule = json!({"field": "proposal_id", "state": "accepted"});
1327
1664
 
1328
- assert!(check_precondition(&precondition_domain(), &contract(), &rule, &json!({}), &json!({})).is_ok());
1665
+ assert!(check_precondition(
1666
+ &precondition_domain(),
1667
+ &contract(),
1668
+ &rule,
1669
+ &json!({}),
1670
+ &json!({})
1671
+ )
1672
+ .is_ok());
1329
1673
  }
1330
1674
 
1331
1675
  #[test]
1332
1676
  fn a_precondition_naming_a_field_that_is_not_a_reference_simply_does_not_fire() {
1333
1677
  let rule = json!({"field": "number", "state": "accepted"});
1334
1678
 
1335
- assert!(check_precondition(&precondition_domain(), &contract(), &rule, &json!({"number": "C-001"}), &json!({})).is_ok());
1679
+ assert!(check_precondition(
1680
+ &precondition_domain(),
1681
+ &contract(),
1682
+ &rule,
1683
+ &json!({"number": "C-001"}),
1684
+ &json!({})
1685
+ )
1686
+ .is_ok());
1336
1687
  }
1337
1688
 
1338
1689
  // ---- both write routes, end to end --------------------------------
@@ -1343,9 +1694,13 @@ mod tests {
1343
1694
  // is the derived one ("customers").
1344
1695
 
1345
1696
  fn banking_ir() -> Value {
1346
- let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../dist/banking.ir.json");
1347
- serde_json::from_str(&std::fs::read_to_string(path).expect("hecks build_wasm writes banking.ir.json beside the wasm"))
1348
- .expect("valid IR")
1697
+ let path =
1698
+ std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../dist/banking.ir.json");
1699
+ serde_json::from_str(
1700
+ &std::fs::read_to_string(path)
1701
+ .expect("hecks build_wasm writes banking.ir.json beside the wasm"),
1702
+ )
1703
+ .expect("valid IR")
1349
1704
  }
1350
1705
 
1351
1706
  /// A fixture whose `.wasm` carries a ConsoleSettings chapter, the
@@ -1377,20 +1732,36 @@ mod tests {
1377
1732
  &*client.lock().await,
1378
1733
  "CheckoutFixture",
1379
1734
  1,
1380
- &["Event", "Registration", "StateStyle", "Collection", "Overview"],
1735
+ &[
1736
+ "Event",
1737
+ "Registration",
1738
+ "StateStyle",
1739
+ "Collection",
1740
+ "Overview",
1741
+ ],
1381
1742
  )
1382
1743
  .await;
1383
1744
  let (wasm, ir) = console_fixture();
1384
- let lineage = LineageConfig { domain: "CheckoutFixture".to_string(), era: Some(1), mirrored: None };
1745
+ let lineage = LineageConfig {
1746
+ domain: "CheckoutFixture".to_string(),
1747
+ era: Some(1),
1748
+ mirrored: None,
1749
+ };
1385
1750
  let invoker = crate::lambda_client::NeverInvoker;
1386
1751
 
1387
1752
  // Nothing saved yet — every section empty, the same answer the
1388
1753
  // Ruby engine gives a console that has never saved.
1389
- let before = presentation::load(&client, &wasm, &lineage).await.expect("a config");
1390
- assert_eq!(before, json!({"states": {}, "collections": {}, "overview": {}}));
1754
+ let before = presentation::load(&client, &wasm, &lineage)
1755
+ .await
1756
+ .expect("a config");
1757
+ assert_eq!(
1758
+ before,
1759
+ json!({"states": {}, "collections": {}, "overview": {}})
1760
+ );
1391
1761
 
1392
1762
  let mut submitted = every_state_styled();
1393
- submitted["states"]["Event"]["open"] = json!({"tone": "good", "attention": true, "label": "Open for signups"});
1763
+ submitted["states"]["Event"]["open"] =
1764
+ json!({"tone": "good", "attention": true, "label": "Open for signups"});
1394
1765
  submitted["collections"] = json!({
1395
1766
  "Event": {
1396
1767
  "label": "Sessions",
@@ -1405,7 +1776,12 @@ mod tests {
1405
1776
  submitted["overview"] = json!({"stats": [{"label": "Open", "collection": "events",
1406
1777
  "where": {"state": "open"}}]});
1407
1778
 
1408
- let saved = presentation_save(&ir, &submitted.to_string(), &client, &wasm, &lineage, &invoker).await;
1779
+ let saved = presentation_save(
1780
+ &ir,
1781
+ &submitted.to_string(),
1782
+ Host::new(&client, &wasm, &lineage, &invoker),
1783
+ )
1784
+ .await;
1409
1785
  assert_eq!(saved["statusCode"], 200, "{saved}");
1410
1786
  let stored = body(&saved);
1411
1787
 
@@ -1415,46 +1791,90 @@ mod tests {
1415
1791
  assert_eq!(stored["states"]["Event"]["open"]["attention"], json!(true));
1416
1792
  // A field this chapter does not model individually still
1417
1793
  // round-trips, through `extra_json`.
1418
- assert_eq!(stored["states"]["Event"]["open"]["label"], "Open for signups");
1794
+ assert_eq!(
1795
+ stored["states"]["Event"]["open"]["label"],
1796
+ "Open for signups"
1797
+ );
1419
1798
  assert_eq!(stored["states"]["Event"]["closed"]["tone"], "muted");
1420
- assert_eq!(stored["states"]["Registration"]["archived"]["tone"], "muted");
1799
+ assert_eq!(
1800
+ stored["states"]["Registration"]["archived"]["tone"],
1801
+ "muted"
1802
+ );
1421
1803
  assert_eq!(stored["collections"]["Event"]["label"], "Sessions");
1422
1804
  assert_eq!(stored["collections"]["Event"]["nav_order"], json!(1));
1423
1805
  assert_eq!(stored["collections"]["Event"]["noun_sing"], "session");
1424
- assert_eq!(stored["collections"]["Event"]["columns"][1]["sortable"], json!(true));
1425
- assert_eq!(stored["collections"]["Event"]["columns"][1]["head"], "Title");
1806
+ assert_eq!(
1807
+ stored["collections"]["Event"]["columns"][1]["sortable"],
1808
+ json!(true)
1809
+ );
1810
+ assert_eq!(
1811
+ stored["collections"]["Event"]["columns"][1]["head"],
1812
+ "Title"
1813
+ );
1426
1814
  assert_eq!(stored["collections"]["Event"]["identity"]["pad"], json!(3));
1427
- assert_eq!(stored["collections"]["Event"]["field_formats"], json!({"capacity": "percent"}));
1428
- assert_eq!(stored["overview"]["stats"][0]["where"], json!({"state": "open"}));
1815
+ assert_eq!(
1816
+ stored["collections"]["Event"]["field_formats"],
1817
+ json!({"capacity": "percent"})
1818
+ );
1819
+ assert_eq!(
1820
+ stored["overview"]["stats"][0]["where"],
1821
+ json!({"state": "open"})
1822
+ );
1429
1823
 
1430
1824
  // A fresh read agrees, proving the rows are in the kernel's store.
1431
- assert_eq!(presentation::load(&client, &wasm, &lineage).await.expect("a config"), stored);
1825
+ assert_eq!(
1826
+ presentation::load(&client, &wasm, &lineage)
1827
+ .await
1828
+ .expect("a config"),
1829
+ stored
1830
+ );
1432
1831
 
1433
1832
  // Saving again is safe: an existing row is a `Set*`, never a
1434
1833
  // second `Declare` (which would refuse `AlreadyExists`).
1435
- let again = presentation_save(&ir, &submitted.to_string(), &client, &wasm, &lineage, &invoker).await;
1834
+ let again = presentation_save(
1835
+ &ir,
1836
+ &submitted.to_string(),
1837
+ Host::new(&client, &wasm, &lineage, &invoker),
1838
+ )
1839
+ .await;
1436
1840
  assert_eq!(again["statusCode"], 200, "{again}");
1437
1841
  assert_eq!(body(&again), stored);
1438
1842
  }
1439
1843
 
1440
1844
  #[tokio::test]
1441
1845
  async fn a_config_that_breaks_a_rule_refuses_422_malformed_and_writes_nothing() {
1442
- let client = crate::dispatch::tests::scratch_db("rust_host_api_presentation_malformed").await;
1846
+ let client =
1847
+ crate::dispatch::tests::scratch_db("rust_host_api_presentation_malformed").await;
1443
1848
  crate::dispatch::tests::provision_lineage(
1444
1849
  &*client.lock().await,
1445
1850
  "CheckoutFixture",
1446
1851
  1,
1447
- &["Event", "Registration", "StateStyle", "Collection", "Overview"],
1852
+ &[
1853
+ "Event",
1854
+ "Registration",
1855
+ "StateStyle",
1856
+ "Collection",
1857
+ "Overview",
1858
+ ],
1448
1859
  )
1449
1860
  .await;
1450
1861
  let (wasm, ir) = console_fixture();
1451
- let lineage = LineageConfig { domain: "CheckoutFixture".to_string(), era: Some(1), mirrored: None };
1862
+ let lineage = LineageConfig {
1863
+ domain: "CheckoutFixture".to_string(),
1864
+ era: Some(1),
1865
+ mirrored: None,
1866
+ };
1452
1867
  let invoker = crate::lambda_client::NeverInvoker;
1453
1868
 
1454
1869
  let mut bad = every_state_styled();
1455
1870
  bad["states"]["Event"]["open"] = json!({"tone": "chartreuse"});
1456
1871
 
1457
- let refused = presentation_save(&ir, &bad.to_string(), &client, &wasm, &lineage, &invoker).await;
1872
+ let refused = presentation_save(
1873
+ &ir,
1874
+ &bad.to_string(),
1875
+ Host::new(&client, &wasm, &lineage, &invoker),
1876
+ )
1877
+ .await;
1458
1878
 
1459
1879
  assert_eq!(refused["statusCode"], 422, "{refused}");
1460
1880
  assert_eq!(body(&refused)["error"], "Malformed");
@@ -1465,25 +1885,40 @@ mod tests {
1465
1885
 
1466
1886
  // Nothing written: `validate` runs before the first dispatch.
1467
1887
  assert_eq!(
1468
- presentation::load(&client, &wasm, &lineage).await.expect("a config"),
1888
+ presentation::load(&client, &wasm, &lineage)
1889
+ .await
1890
+ .expect("a config"),
1469
1891
  json!({"states": {}, "collections": {}, "overview": {}})
1470
1892
  );
1471
1893
  }
1472
1894
 
1473
1895
  #[tokio::test]
1474
1896
  async fn a_body_that_is_not_json_at_all_is_a_400_before_anything_is_validated() {
1475
- let client = crate::dispatch::tests::scratch_db("rust_host_api_presentation_bad_body").await;
1476
- crate::dispatch::tests::provision_lineage(&*client.lock().await, "CheckoutFixture", 1, &["Event"]).await;
1897
+ let client =
1898
+ crate::dispatch::tests::scratch_db("rust_host_api_presentation_bad_body").await;
1899
+ crate::dispatch::tests::provision_lineage(
1900
+ &*client.lock().await,
1901
+ "CheckoutFixture",
1902
+ 1,
1903
+ &["Event"],
1904
+ )
1905
+ .await;
1477
1906
  let (wasm, ir) = console_fixture();
1478
- let lineage = LineageConfig { domain: "CheckoutFixture".to_string(), era: Some(1), mirrored: None };
1907
+ let lineage = LineageConfig {
1908
+ domain: "CheckoutFixture".to_string(),
1909
+ era: Some(1),
1910
+ mirrored: None,
1911
+ };
1479
1912
 
1480
1913
  let refused = presentation_save(
1481
1914
  &ir,
1482
1915
  "not json",
1483
- &client,
1484
- &wasm,
1485
- &lineage,
1486
- &crate::lambda_client::NeverInvoker,
1916
+ Host::new(
1917
+ &client,
1918
+ &wasm,
1919
+ &lineage,
1920
+ &crate::lambda_client::NeverInvoker,
1921
+ ),
1487
1922
  )
1488
1923
  .await;
1489
1924
 
@@ -1495,26 +1930,42 @@ mod tests {
1495
1930
  /// asks the kernel rather than inferring it from anything else.
1496
1931
  #[tokio::test]
1497
1932
  async fn a_host_whose_kernel_has_no_console_settings_chapter_still_refuses_501() {
1498
- let client = crate::dispatch::tests::scratch_db("rust_host_api_presentation_no_chapter").await;
1499
- crate::dispatch::tests::provision_lineage(&*client.lock().await, "Banking", 1, &["Customer"]).await;
1933
+ let client =
1934
+ crate::dispatch::tests::scratch_db("rust_host_api_presentation_no_chapter").await;
1935
+ crate::dispatch::tests::provision_lineage(
1936
+ &*client.lock().await,
1937
+ "Banking",
1938
+ 1,
1939
+ &["Customer"],
1940
+ )
1941
+ .await;
1500
1942
  let wasm = crate::dispatch::tests::wasm_path();
1501
1943
  let ir = banking_ir();
1502
- let lineage = LineageConfig { domain: "Banking".to_string(), era: Some(1), mirrored: None };
1944
+ let lineage = LineageConfig {
1945
+ domain: "Banking".to_string(),
1946
+ era: Some(1),
1947
+ mirrored: None,
1948
+ };
1503
1949
 
1504
1950
  let refused = presentation_save(
1505
1951
  &ir,
1506
1952
  r#"{"states":{}}"#,
1507
- &client,
1508
- &wasm,
1509
- &lineage,
1510
- &crate::lambda_client::NeverInvoker,
1953
+ Host::new(
1954
+ &client,
1955
+ &wasm,
1956
+ &lineage,
1957
+ &crate::lambda_client::NeverInvoker,
1958
+ ),
1511
1959
  )
1512
1960
  .await;
1513
1961
 
1514
1962
  assert_eq!(refused["statusCode"], 501, "{refused}");
1515
1963
  assert_eq!(body(&refused)["error"], "NotImplemented");
1516
1964
  assert!(
1517
- body(&refused)["message"].as_str().expect("a message").contains("ConsoleSettings"),
1965
+ body(&refused)["message"]
1966
+ .as_str()
1967
+ .expect("a message")
1968
+ .contains("ConsoleSettings"),
1518
1969
  "the refusal has to name what is missing: {refused}"
1519
1970
  );
1520
1971
  }
@@ -1522,21 +1973,23 @@ mod tests {
1522
1973
  #[tokio::test]
1523
1974
  async fn a_record_is_created_then_commanded_then_read_back_through_the_api_routes() {
1524
1975
  let client = crate::dispatch::tests::scratch_db("rust_host_api_write_routes").await;
1525
- crate::dispatch::tests::provision_lineage(&*client.lock().await, "Banking", 1, &["Customer"]).await;
1976
+ crate::dispatch::tests::provision_lineage(
1977
+ &*client.lock().await,
1978
+ "Banking",
1979
+ 1,
1980
+ &["Customer"],
1981
+ )
1982
+ .await;
1526
1983
  let wasm = crate::dispatch::tests::wasm_path();
1527
1984
  let ir = banking_ir();
1528
- let lineage = LineageConfig { domain: "Banking".to_string(), era: Some(1), mirrored: None };
1985
+ let lineage = LineageConfig {
1986
+ domain: "Banking".to_string(),
1987
+ era: Some(1),
1988
+ mirrored: None,
1989
+ };
1529
1990
  let invoker = crate::lambda_client::NeverInvoker;
1530
1991
 
1531
- let created = collection_create(
1532
- &ir,
1533
- "customers",
1534
- r#"{"reference":{"value":"CUST-9001"},"name":{"given":"Ada","family":"Lovelace"},"email":{"address":"ada@example.com"}}"#,
1535
- &client,
1536
- &wasm,
1537
- &lineage,
1538
- &invoker,
1539
- )
1992
+ let created = collection_create(&ir, "customers", r#"{"reference":{"value":"CUST-9001"},"name":{"given":"Ada","family":"Lovelace"},"email":{"address":"ada@example.com"}}"#, Host::new(&client, &wasm, &lineage, &invoker))
1540
1993
  .await;
1541
1994
 
1542
1995
  assert_eq!(created["statusCode"], 200, "{created}");
@@ -1545,7 +1998,8 @@ mod tests {
1545
1998
  assert_eq!(record["reference"]["value"], "CUST-9001");
1546
1999
 
1547
2000
  // …and it is there to read, both ways.
1548
- let index = collection_index(&ir, "customers", &params(&[]), &client, &wasm, &lineage).await;
2001
+ let index =
2002
+ collection_index(&ir, "customers", &params(&[]), &client, &wasm, &lineage).await;
1549
2003
  assert_eq!(body(&index).as_array().expect("an array").len(), 1);
1550
2004
  let shown = record_show(&ir, "customers", "CUST-9001", &client, &wasm, &lineage).await;
1551
2005
  assert_eq!(body(&shown)["id"], "CUST-9001");
@@ -1558,10 +2012,7 @@ mod tests {
1558
2012
  "CUST-9001",
1559
2013
  "suspend",
1560
2014
  r#"{"standing":{"value":"watch"}}"#,
1561
- &client,
1562
- &wasm,
1563
- &lineage,
1564
- &invoker,
2015
+ Host::new(&client, &wasm, &lineage, &invoker),
1565
2016
  )
1566
2017
  .await;
1567
2018
 
@@ -1577,29 +2028,55 @@ mod tests {
1577
2028
  "CUST-9001",
1578
2029
  "suspend",
1579
2030
  r#"{"standing":{"value":"watch"}}"#,
1580
- &client,
1581
- &wasm,
1582
- &lineage,
1583
- &invoker,
2031
+ Host::new(&client, &wasm, &lineage, &invoker),
1584
2032
  )
1585
2033
  .await;
1586
2034
 
1587
2035
  assert_eq!(again["statusCode"], 422, "{again}");
1588
2036
  let refused = body(&again);
1589
- assert!(!refused["error"].as_str().expect("a refusal class").is_empty(), "{refused}");
1590
- assert!(!refused["message"].as_str().expect("a message").is_empty(), "{refused}");
2037
+ assert!(
2038
+ !refused["error"]
2039
+ .as_str()
2040
+ .expect("a refusal class")
2041
+ .is_empty(),
2042
+ "{refused}"
2043
+ );
2044
+ assert!(
2045
+ !refused["message"].as_str().expect("a message").is_empty(),
2046
+ "{refused}"
2047
+ );
1591
2048
 
1592
2049
  // An unknown command, and an unknown record, both refuse the
1593
2050
  // way Json refuses them.
1594
- let unknown_command =
1595
- command_route(&ir, "customers", "CUST-9001", "nope", "", &client, &wasm, &lineage, &invoker).await;
2051
+ let unknown_command = command_route(
2052
+ &ir,
2053
+ "customers",
2054
+ "CUST-9001",
2055
+ "nope",
2056
+ "",
2057
+ Host::new(&client, &wasm, &lineage, &invoker),
2058
+ )
2059
+ .await;
1596
2060
  assert_eq!(unknown_command["statusCode"], 404);
1597
- assert_eq!(body(&unknown_command)["message"], "Customer declares no command named \"nope\"");
2061
+ assert_eq!(
2062
+ body(&unknown_command)["message"],
2063
+ "Customer declares no command named \"nope\""
2064
+ );
1598
2065
 
1599
- let unknown_record =
1600
- command_route(&ir, "customers", "ghost", "suspend", "", &client, &wasm, &lineage, &invoker).await;
2066
+ let unknown_record = command_route(
2067
+ &ir,
2068
+ "customers",
2069
+ "ghost",
2070
+ "suspend",
2071
+ "",
2072
+ Host::new(&client, &wasm, &lineage, &invoker),
2073
+ )
2074
+ .await;
1601
2075
  assert_eq!(unknown_record["statusCode"], 404);
1602
- assert_eq!(body(&unknown_record)["message"], "no Customer found for id \"ghost\"");
2076
+ assert_eq!(
2077
+ body(&unknown_record)["message"],
2078
+ "no Customer found for id \"ghost\""
2079
+ );
1603
2080
  }
1604
2081
 
1605
2082
  #[test]
@@ -1608,7 +2085,10 @@ mod tests {
1608
2085
  assert_eq!(response["statusCode"], 501);
1609
2086
  assert_eq!(body(&response)["error"], "NotImplemented");
1610
2087
  assert!(
1611
- body(&response)["message"].as_str().expect("a message").contains("ConsoleSettings"),
2088
+ body(&response)["message"]
2089
+ .as_str()
2090
+ .expect("a message")
2091
+ .contains("ConsoleSettings"),
1612
2092
  "the refusal has to name what is missing: {response}"
1613
2093
  );
1614
2094
  }