hecks 3.13.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 (440) 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.bluebook +4 -4
  7. data/examples/pizzas/bluebook/pizzas.hecksagon +5 -3
  8. data/examples/pizzas/config.ru +22 -0
  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 +1 -7
  12. data/lib/hecks/bluebook/dsl/attribute_collector.rb +15 -5
  13. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -2
  14. data/lib/hecks/bluebook/dsl/derivation_words.rb +58 -0
  15. data/lib/hecks/bluebook/dsl/translation_builder.rb +6 -37
  16. data/lib/hecks/bluebook/expression/ast_json.rb +0 -9
  17. data/lib/hecks/bluebook/hecksagon.rb +1 -1
  18. data/lib/hecks/bluebook/meta_validator/translation_judge/rules.rb +7 -10
  19. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +4 -8
  20. data/lib/hecks/bluebook/meta_validator.rb +0 -11
  21. data/lib/hecks/bluebook/named_pattern.rb +33 -0
  22. data/lib/hecks/bluebook/translation.rb +18 -10
  23. data/lib/hecks/bluebook/translation_expression/evaluator.rb +63 -0
  24. data/lib/hecks/bluebook/translation_expression/parser.rb +127 -0
  25. data/lib/hecks/bluebook/translation_expression.rb +39 -0
  26. data/lib/hecks/bluebook/translation_plan.rb +70 -0
  27. data/lib/hecks/bluebook.rb +3 -0
  28. data/lib/hecks/chapters.rb +3 -3
  29. data/lib/hecks/cli/domain_stub.rb +1 -1
  30. data/lib/hecks/cli/interview_aggregate.rb +4 -4
  31. data/lib/hecks/composer/adapters/claude_composer.adapter +3 -0
  32. data/lib/hecks/composer/adapters/claude_composer.rb +83 -0
  33. data/lib/hecks/composer/adapters/fake_agent.adapter +3 -0
  34. data/lib/hecks/composer/adapters/fake_agent.rb +55 -0
  35. data/lib/hecks/composer/adapters/jev.adapter +3 -0
  36. data/lib/hecks/composer/adapters/jev.rb +66 -0
  37. data/lib/hecks/composer/agent.rb +113 -0
  38. data/lib/hecks/composer/assistant.rb +110 -0
  39. data/lib/hecks/composer/behavior_run.rb +53 -0
  40. data/lib/hecks/composer/bluebook/composer.bluebook +178 -0
  41. data/lib/hecks/composer/bluebook/composer.hecksagon +16 -0
  42. data/lib/hecks/composer/board.rb +65 -0
  43. data/lib/hecks/composer/board_edit/command_text.rb +39 -0
  44. data/lib/hecks/composer/board_edit/source.rb +84 -0
  45. data/lib/hecks/composer/board_edit.rb +144 -0
  46. data/lib/hecks/composer/check.rb +89 -0
  47. data/lib/hecks/composer/domains.rb +81 -0
  48. data/lib/hecks/composer/jobs.rb +134 -0
  49. data/lib/hecks/composer/layout.rb +37 -0
  50. data/lib/hecks/composer/live.rb +150 -0
  51. data/lib/hecks/composer/outline.rb +48 -0
  52. data/lib/hecks/composer/page.html +989 -0
  53. data/lib/hecks/composer/panels.rb +142 -0
  54. data/lib/hecks/composer/parse.rb +83 -0
  55. data/lib/hecks/composer/plain.rb +28 -0
  56. data/lib/hecks/composer/ports/composer_agent.port +7 -0
  57. data/lib/hecks/composer/session.rb +141 -0
  58. data/lib/hecks/composer/toolbox.rb +166 -0
  59. data/lib/hecks/composer/web.rb +156 -0
  60. data/lib/hecks/composer/workbench.rb +51 -0
  61. data/lib/hecks/composer/workspace.rb +81 -0
  62. data/lib/hecks/composer.rb +17 -0
  63. data/lib/hecks/corpus/adapters.rb +22 -0
  64. data/lib/hecks/corpus/rust.rb +1 -1
  65. data/lib/hecks/corpus/worlds.rb +19 -0
  66. data/lib/hecks/corpus.rb +5 -0
  67. data/lib/hecks/deploy/bluebook/deploy.bluebook +34 -34
  68. data/lib/hecks/forms/app/commands.rb +2 -3
  69. data/lib/hecks/forms/app/queries.rb +2 -3
  70. data/lib/hecks/forms/app/records.rb +17 -5
  71. data/lib/hecks/forms/command_form_renderer.rb +42 -16
  72. data/lib/hecks/forms/field_controls.rb +2 -2
  73. data/lib/hecks/forms/field_renderer.rb +1 -1
  74. data/lib/hecks/forms/field_shape.rb +36 -17
  75. data/lib/hecks/forms/index_renderer.rb +2 -1
  76. data/lib/hecks/forms/page.rb +1 -1
  77. data/lib/hecks/forms/params.rb +0 -13
  78. data/lib/hecks/forms/query_form_renderer.rb +7 -6
  79. data/lib/hecks/forms/record_renderer.rb +11 -7
  80. data/lib/hecks/forms/rule_words.rb +100 -0
  81. data/lib/hecks/forms/value_object_shape.rb +2 -1
  82. data/lib/hecks/framework/bluebook/change.bluebook +2 -2
  83. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -3
  84. data/lib/hecks/fuzzing/domain_generator/block_rendering.rb +1 -1
  85. data/lib/hecks/fuzzing/isolated_boot/worlds.rb +3 -6
  86. data/lib/hecks/fuzzing/replay/filters.rb +2 -6
  87. data/lib/hecks/gate/stages.yml +25 -0
  88. data/lib/hecks/grammar/evolve/seeds.rb +0 -6
  89. data/lib/hecks/grammar/evolve.rb +0 -3
  90. data/lib/hecks/grammar/translation.bluebook +1 -1
  91. data/lib/hecks/hecks/adapters/codebase/composing.rb +56 -0
  92. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +3 -1
  93. data/lib/hecks/hecks/adapters/journal_store/changes.rb +0 -26
  94. data/lib/hecks/hecks/adapters/journal_store/examination.rb +2 -23
  95. data/lib/hecks/hecks/adapters/journal_store/readings.rb +3 -11
  96. data/lib/hecks/hecks/adapters/journal_store.rb +0 -11
  97. data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +4 -11
  98. data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +0 -9
  99. data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +0 -4
  100. data/lib/hecks/hecks/codebase.behaviors +6 -0
  101. data/lib/hecks/hecks/codebase.bluebook +58 -37
  102. data/lib/hecks/hecks/composer.behaviors +68 -0
  103. data/lib/hecks/hecks/context_map.hecksagon +1 -1
  104. data/lib/hecks/hecks/custodian.behaviors +0 -6
  105. data/lib/hecks/hecks/custodian.bluebook +15 -92
  106. data/lib/hecks/hecks/hecks.bluebook +2 -2
  107. data/lib/hecks/hecks/hecks.hecksagon +2 -2
  108. data/lib/hecks/hecks/hecks.world +1 -1
  109. data/lib/hecks/hecks/rehearsal.bluebook +10 -16
  110. data/lib/hecks/hecks/tooling.bluebook +22 -22
  111. data/lib/hecks/kernel_capabilities/templates.rb +4 -4
  112. data/lib/hecks/kernel_capabilities.rb +1 -1
  113. data/lib/hecks/language/bluebook/aggregate.bluebook +1 -0
  114. data/lib/hecks/language/bluebook/command.bluebook +1 -0
  115. data/lib/hecks/language/bluebook/entity.bluebook +1 -0
  116. data/lib/hecks/language/bluebook/query.bluebook +1 -0
  117. data/lib/hecks/language/bluebook/shape.bluebook +8 -7
  118. data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -3
  119. data/lib/hecks/language/hecksagon/port_operation.bluebook +1 -0
  120. data/lib/hecks/language/translation/translation_aggregate.bluebook +21 -24
  121. data/lib/hecks/ports/access_control.rb +2 -2
  122. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -41
  123. data/lib/hecks/ports/persistence/plugins/era/lineage/coverage.rb +7 -7
  124. data/lib/hecks/ports/persistence/plugins/era/lineage/rewrite.rb +25 -2
  125. data/lib/hecks/ports/persistence/plugins/era/lineage/rules.rb +4 -4
  126. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +17 -25
  127. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/base_sql.rb +0 -12
  128. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/domain_rename.rb +1 -1
  129. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -22
  130. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +0 -1
  131. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +9 -3
  132. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +3 -29
  133. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/refusals.rb +2 -26
  134. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -33
  135. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -3
  136. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +2 -4
  137. data/lib/hecks/ports/persistence/plugins/era/translation/expression_sql.rb +52 -0
  138. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +23 -29
  139. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +1 -1
  140. data/lib/hecks/ports/persistence/plugins/era/translation.rb +0 -1
  141. data/lib/hecks/ports/persistence/plugins/era.rb +3 -6
  142. data/lib/hecks/ports.rb +0 -1
  143. data/lib/hecks/projection_files.rb +3 -1
  144. data/lib/hecks/projections/deploy/templates/lambda/nat_comment_oauth.tmpl +1 -1
  145. data/lib/hecks/projections/glossary/entries.rb +10 -4
  146. data/lib/hecks/projections/glossary/groups.rb +42 -0
  147. data/lib/hecks/projections/glossary/html/page.rb +7 -2
  148. data/lib/hecks/projections/glossary/html/parser.rb +1 -1
  149. data/lib/hecks/projections/glossary/html/sections.rb +31 -11
  150. data/lib/hecks/projections/glossary/html.rb +1 -2
  151. data/lib/hecks/projections/glossary/markdown.rb +31 -28
  152. data/lib/hecks/projections/glossary/mermaid.rb +3 -0
  153. data/lib/hecks/projections/glossary/page.css +143 -153
  154. data/lib/hecks/projections/glossary/page.js +63 -3
  155. data/lib/hecks/projections/glossary/sentences/actions.rb +88 -0
  156. data/lib/hecks/projections/glossary/sentences/paths.rb +77 -0
  157. data/lib/hecks/projections/glossary/sentences/queries.rb +90 -0
  158. data/lib/hecks/projections/glossary/sentences/value_objects.rb +32 -5
  159. data/lib/hecks/projections/glossary/sentences.rb +13 -3
  160. data/lib/hecks/projections/glossary/slugs.rb +4 -1
  161. data/lib/hecks/projections/glossary.rb +2 -1
  162. data/lib/hecks/projections/plain_english.rb +97 -0
  163. data/lib/hecks/projections/rust_vocabulary/arguments.rb +2 -2
  164. data/lib/hecks/projections/rust_vocabulary/enum_body.rb +20 -25
  165. data/lib/hecks/projections/rust_vocabulary/tests.rb +2 -2
  166. data/lib/hecks/projections/rust_vocabulary.rb +14 -6
  167. data/lib/hecks/projections/site/cms_editor/color.rb +0 -6
  168. data/lib/hecks/projections/statements/actions.rb +99 -0
  169. data/lib/hecks/projections/statements/relationships.rb +86 -0
  170. data/lib/hecks/projections/statements.rb +15 -24
  171. data/lib/hecks/projector/exporter/adapters.rb +112 -0
  172. data/lib/hecks/projector/exporter/translations.rb +18 -35
  173. data/lib/hecks/projector/exporter/worlds.rb +103 -0
  174. data/lib/hecks/projector/exporter.rb +5 -1
  175. data/lib/hecks/quality_control/adapters/agent.rb +6 -7
  176. data/lib/hecks/quality_control/adapters/agent_profile.rb +16 -114
  177. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +1 -1
  178. data/lib/hecks/quality_control/quality_control.bluebook +51 -51
  179. data/lib/hecks/runtime/loader/boot_steps.rb +0 -20
  180. data/lib/hecks/runtime/outbox.rb +2 -9
  181. data/lib/hecks/runtime/port_operation_interpreter.rb +1 -9
  182. data/lib/hecks/runtime/remote_dispatcher.rb +2 -2
  183. data/lib/hecks/runtime/value.rb +13 -0
  184. data/lib/hecks/rust_build/project_rust/target_ir.rb +23 -4
  185. data/lib/hecks/rust_build/wasm.rb +0 -12
  186. data/lib/hecks/rust_build/write_if_changed.rb +12 -12
  187. data/lib/hecks/rust_format.rb +77 -0
  188. data/lib/hecks/site/bluebook/site.bluebook +5 -5
  189. data/lib/hecks/sme/bluebook/sme.bluebook +12 -12
  190. data/lib/hecks/tickets/bluebook/tickets.bluebook +3 -3
  191. data/lib/hecks/tools/fuzz_sweep/findings.rb +3 -9
  192. data/lib/hecks/tools/fuzz_sweep.rb +1 -3
  193. data/lib/hecks/tools/translation_audit/verdicts.rb +3 -28
  194. data/lib/hecks/tools/translation_audit.rb +8 -10
  195. data/lib/hecks/tools/translation_scaffold/reporting.rb +1 -1
  196. data/lib/hecks/version.rb +1 -1
  197. data/lib/hecks/vocabulary.rb +2 -1
  198. data/lib/hecks.rb +3 -0
  199. data/prebuilt/{58208399bada10b0db99928c48e38789288fc58f7b71a72b21bfa386680e32ed.marshal → 02c09c73b272c1fb5a9ae6d87bb5e5e529208e00f6b4b597dc2c0fd70a9c0964.marshal} +0 -0
  200. data/prebuilt/{2b59737fbdbd5f44edda0fddce2611f45d76087b3a7c782d1f1a17ea58c77948.marshal → bb84a835cf6a15136bad5ca03aa5bf93a5b92f5f72444618d3ef2412b987fb32.marshal} +0 -0
  201. data/prebuilt/verdicts-f1f10da14c8e8f01b4c9b7e3e2d74be8cd2bf4676364ed17fa16975d2a076d75.json +1 -0
  202. data/rust/Cargo.toml +40 -0
  203. data/rust/build/Cargo.toml +36 -0
  204. data/rust/build/clippy.toml +4 -0
  205. data/rust/build/src/adapters_pass.rs +196 -0
  206. data/rust/build/src/binding_pass.rs +54 -13
  207. data/rust/build/src/build_artifact.rs +23 -5
  208. data/rust/build/src/cargo_sync.rs +69 -26
  209. data/rust/build/src/lineage_pass.rs +101 -87
  210. data/rust/build/src/main.rs +11 -2
  211. data/rust/build/src/pipeline.rs +80 -19
  212. data/rust/build/src/reserved_names.rs +6 -50
  213. data/rust/build/src/resolve.rs +82 -35
  214. data/rust/build/src/rustfmt.rs +100 -0
  215. data/rust/build/src/seams_pass.rs +85 -16
  216. data/rust/build/src/subprocess.rs +30 -6
  217. data/rust/build/src/tmp.rs +5 -2
  218. data/rust/build/src/translation_pass.rs +72 -22
  219. data/rust/build/src/world_pass.rs +109 -0
  220. data/rust/clippy.toml +4 -0
  221. data/rust/codegen/Cargo.toml +36 -0
  222. data/rust/codegen/clippy.toml +4 -0
  223. data/rust/codegen/src/asks.rs +40 -14
  224. data/rust/codegen/src/attr.rs +2 -2
  225. data/rust/codegen/src/bridging.rs +676 -117
  226. data/rust/codegen/src/commands.rs +979 -373
  227. data/rust/codegen/src/constraints.rs +88 -25
  228. data/rust/codegen/src/dependency_planning.rs +128 -34
  229. data/rust/codegen/src/domain_generator.rs +300 -170
  230. data/rust/codegen/src/exemplar.rs +87 -32
  231. data/rust/codegen/src/expr_emitter.rs +54 -6
  232. data/rust/codegen/src/fielded.rs +193 -41
  233. data/rust/codegen/src/hecks_naming.rs +124 -16
  234. data/rust/codegen/src/hecks_naming_tests.rs +81 -0
  235. data/rust/codegen/src/json.rs +80 -44
  236. data/rust/codegen/src/json_codec.rs +585 -149
  237. data/rust/codegen/src/literal.rs +104 -138
  238. data/rust/codegen/src/main.rs +145 -52
  239. data/rust/codegen/src/manifest.rs +76 -14
  240. data/rust/codegen/src/mutations.rs +1212 -239
  241. data/rust/codegen/src/naming.rs +111 -60
  242. data/rust/codegen/src/optional_pass.rs +90 -28
  243. data/rust/codegen/src/ports.rs +5 -4
  244. data/rust/codegen/src/prelude.rs +139 -49
  245. data/rust/codegen/src/queries.rs +601 -226
  246. data/rust/codegen/src/reactions.rs +268 -74
  247. data/rust/codegen/src/read_models.rs +606 -205
  248. data/rust/codegen/src/reference_specs.rs +16 -3
  249. data/rust/codegen/src/registry.rs +128 -79
  250. data/rust/codegen/src/reserved_names.rs +6 -50
  251. data/rust/codegen/src/shared.rs +71 -22
  252. data/rust/codegen/src/sidecars.rs +15 -5
  253. data/rust/codegen/src/skip_reason.rs +8 -2
  254. data/rust/codegen/src/types.rs +306 -108
  255. data/rust/host/Cargo.lock +1 -0
  256. data/rust/host/Cargo.toml +40 -4
  257. data/rust/host/HECKS_RELEASE +1 -1
  258. data/rust/host/clippy.toml +4 -0
  259. data/rust/host/src/api.rs +698 -218
  260. data/rust/host/src/auth.rs +491 -214
  261. data/rust/host/src/b64.rs +91 -0
  262. data/rust/host/src/bin/expr_harness.rs +6 -2
  263. data/rust/host/src/bin/lineage_harness.rs +47 -11
  264. data/rust/host/src/bin/mint_harness.rs +70 -37
  265. data/rust/host/src/bin/validate_harness.rs +16 -8
  266. data/rust/host/src/boot.rs +144 -68
  267. data/rust/host/src/boundary_fuzz/api.rs +130 -32
  268. data/rust/host/src/boundary_fuzz/auth.rs +108 -23
  269. data/rust/host/src/boundary_fuzz/expr_json.rs +85 -11
  270. data/rust/host/src/boundary_fuzz/ir.rs +164 -12
  271. data/rust/host/src/boundary_fuzz/server.rs +283 -36
  272. data/rust/host/src/boundary_fuzz/web.rs +56 -8
  273. data/rust/host/src/checkout.rs +78 -32
  274. data/rust/host/src/clock.rs +8 -0
  275. data/rust/host/src/commerce_ir.rs +333 -95
  276. data/rust/host/src/crypto.rs +33 -0
  277. data/rust/host/src/dispatch.rs +990 -331
  278. data/rust/host/src/expr_json.rs +1021 -273
  279. data/rust/host/src/extension.rs +24 -4
  280. data/rust/host/src/field_hints.rs +11 -7
  281. data/rust/host/src/fuzz_support.rs +85 -13
  282. data/rust/host/src/host.rs +100 -0
  283. data/rust/host/src/http.rs +22 -0
  284. data/rust/host/src/ir.rs +326 -16
  285. data/rust/host/src/journal.rs +277 -176
  286. data/rust/host/src/lambda_client.rs +219 -53
  287. data/rust/host/src/lib.rs +16 -4
  288. data/rust/host/src/log.rs +62 -11
  289. data/rust/host/src/mint.rs +1153 -326
  290. data/rust/host/src/needs.rs +185 -44
  291. data/rust/host/src/payments/keystore.rs +197 -45
  292. data/rust/host/src/payments/roles.rs +191 -0
  293. data/rust/host/src/payments/tests.rs +1445 -275
  294. data/rust/host/src/payments.rs +409 -121
  295. data/rust/host/src/platform_signin/attack_tests.rs +5 -5
  296. data/rust/host/src/platform_signin/config.rs +11 -11
  297. data/rust/host/src/platform_signin/cookie.rs +2 -2
  298. data/rust/host/src/platform_signin/extension.rs +2 -1
  299. data/rust/host/src/platform_signin/handler.rs +9 -8
  300. data/rust/host/src/platform_signin/jti.rs +4 -2
  301. data/rust/host/src/platform_signin/jwks.rs +2 -1
  302. data/rust/host/src/platform_signin/jws.rs +2 -37
  303. data/rust/host/src/platform_signin/known_answer_tests.rs +5 -1
  304. data/rust/host/src/platform_signin/mode_tests.rs +5 -3
  305. data/rust/host/src/platform_signin/pages.rs +20 -20
  306. data/rust/host/src/platform_signin/session.rs +8 -1
  307. data/rust/host/src/platform_signin/settings.rs +6 -6
  308. data/rust/host/src/platform_signin/strict_json.rs +12 -9
  309. data/rust/host/src/platform_signin/verify.rs +41 -41
  310. data/rust/host/src/platform_signin/wiring_support.rs +13 -18
  311. data/rust/host/src/presentation.rs +207 -63
  312. data/rust/host/src/presentation_write.rs +502 -187
  313. data/rust/host/src/query_step.rs +69 -18
  314. data/rust/host/src/rate_limit.rs +541 -142
  315. data/rust/host/src/reference_transform.rs +77 -25
  316. data/rust/host/src/reference_validate.rs +443 -104
  317. data/rust/host/src/resend.rs +190 -55
  318. data/rust/host/src/secrets.rs +116 -21
  319. data/rust/host/src/server.rs +483 -104
  320. data/rust/host/src/storage_shape.rs +88 -21
  321. data/rust/host/src/test_pg.rs +17 -7
  322. data/rust/host/src/ui_schema.rs +495 -207
  323. data/rust/host/src/wasm_runner.rs +21 -6
  324. data/rust/host/src/web/commerce.rs +101 -23
  325. data/rust/host/src/web/newsletter/cooldown.rs +10 -3
  326. data/rust/host/src/web/newsletter/template.rs +26 -7
  327. data/rust/host/src/web/newsletter/tests.rs +112 -23
  328. data/rust/host/src/web/newsletter.rs +209 -59
  329. data/rust/host/src/web/newsletter_send.rs +251 -79
  330. data/rust/host/src/web/registration_receipt/tests.rs +16 -4
  331. data/rust/host/src/web/registration_receipt.rs +100 -24
  332. data/rust/host/src/web/registrations.rs +1384 -332
  333. data/rust/host/src/web/resend_webhook.rs +138 -80
  334. data/rust/host/src/web.rs +2416 -508
  335. data/rust/lsp/Cargo.toml +36 -0
  336. data/rust/lsp/clippy.toml +4 -0
  337. data/rust/lsp/src/json.rs +29 -22
  338. data/rust/lsp/src/main.rs +81 -31
  339. data/rust/lsp/src/outline.rs +23 -7
  340. data/rust/lsp/src/rpc.rs +17 -4
  341. data/rust/parser/Cargo.toml +36 -0
  342. data/rust/parser/clippy.toml +4 -0
  343. data/rust/parser/src/build/closed_sets.rs +6 -1
  344. data/rust/parser/src/build/identity.rs +4 -3
  345. data/rust/parser/src/build/mod.rs +1 -0
  346. data/rust/parser/src/build/named_pattern.rs +26 -0
  347. data/rust/parser/src/build/naming.rs +2 -80
  348. data/rust/parser/src/build/pattern_subset.rs +6 -8
  349. data/rust/parser/src/build/query_derive.rs +3 -1
  350. data/rust/parser/src/build/query_inference.rs +14 -5
  351. data/rust/parser/src/build/read_model.rs +4 -1
  352. data/rust/parser/src/build/references.rs +4 -3
  353. data/rust/parser/src/canonical.rs +34 -15
  354. data/rust/parser/src/emit.rs +187 -105
  355. data/rust/parser/src/expr/ast_json.rs +134 -42
  356. data/rust/parser/src/expr/evaluator.rs +27 -9
  357. data/rust/parser/src/expr/mod.rs +54 -13
  358. data/rust/parser/src/expr/resolver.rs +299 -92
  359. data/rust/parser/src/ir.rs +35 -3
  360. data/rust/parser/src/keywords.rs +7168 -510
  361. data/rust/parser/src/lex.rs +11 -25
  362. data/rust/parser/src/lib.rs +1 -0
  363. data/rust/parser/src/main.rs +65 -6
  364. data/rust/parser/src/parse/adapter.rs +157 -0
  365. data/rust/parser/src/parse/aggregate.rs +19 -10
  366. data/rust/parser/src/parse/chapter.rs +16 -17
  367. data/rust/parser/src/parse/command.rs +35 -16
  368. data/rust/parser/src/parse/domain_port.rs +4 -7
  369. data/rust/parser/src/parse/entity.rs +21 -11
  370. data/rust/parser/src/parse/hecksagon.rs +10 -4
  371. data/rust/parser/src/parse/lifecycle.rs +9 -5
  372. data/rust/parser/src/parse/mod.rs +43 -91
  373. data/rust/parser/src/parse/needs.rs +21 -12
  374. data/rust/parser/src/parse/policy.rs +2 -2
  375. data/rust/parser/src/parse/process_manager.rs +9 -11
  376. data/rust/parser/src/parse/query.rs +8 -4
  377. data/rust/parser/src/parse/read_model.rs +57 -16
  378. data/rust/parser/src/parse/value_object.rs +7 -2
  379. data/rust/parser/src/parse/world.rs +322 -0
  380. data/rust/parser/src/ruby_value.rs +7 -6
  381. data/rust/src/exemplar/json.rs +4 -4
  382. data/rust/src/exemplar/mutations.rs +18 -0
  383. data/rust/src/exemplar/read_models.rs +1 -10
  384. data/rust/src/kernel/attribute_shapes/composite.rs +54 -14
  385. data/rust/src/kernel/attribute_shapes/list.rs +2 -2
  386. data/rust/src/kernel/attribute_shapes/mod.rs +8 -8
  387. data/rust/src/kernel/attribute_shapes/scalar.rs +2 -2
  388. data/rust/src/kernel/cli.rs +229 -112
  389. data/rust/src/kernel/dispatch.rs +323 -115
  390. data/rust/src/kernel/dispatch_guard_tests.rs +327 -65
  391. data/rust/src/kernel/expr.rs +203 -86
  392. data/rust/src/kernel/expression_operators/arithmetic.rs +138 -25
  393. data/rust/src/kernel/expression_operators/comparison.rs +87 -2
  394. data/rust/src/kernel/expression_operators/enumeration.rs +106 -22
  395. data/rust/src/kernel/expression_operators/logical.rs +9 -3
  396. data/rust/src/kernel/expression_operators/membership.rs +3 -1
  397. data/rust/src/kernel/expression_operators/mod.rs +34 -21
  398. data/rust/src/kernel/expression_operators/pattern_match.rs +46 -11
  399. data/rust/src/kernel/expression_operators/positional.rs +84 -30
  400. data/rust/src/kernel/expression_operators/presence.rs +52 -13
  401. data/rust/src/kernel/expression_operators/sign_test.rs +3 -1
  402. data/rust/src/kernel/expression_operators/sized.rs +24 -8
  403. data/rust/src/kernel/expression_operators/text.rs +120 -27
  404. data/rust/src/kernel/expression_operators/to_string.rs +10 -4
  405. data/rust/src/kernel/json.rs +230 -148
  406. data/rust/src/kernel/mod.rs +46 -33
  407. data/rust/src/kernel/named_query.rs +63 -15
  408. data/rust/src/kernel/needs.rs +112 -25
  409. data/rust/src/kernel/orchestrate.rs +980 -581
  410. data/rust/src/kernel/orchestrate_guard_tests.rs +538 -140
  411. data/rust/src/kernel/pattern.rs +107 -34
  412. data/rust/src/kernel/query_comparators.rs +118 -41
  413. data/rust/src/kernel/query_ordering.rs +13 -5
  414. data/rust/src/kernel/read_model.rs +479 -181
  415. data/rust/src/kernel/reference_lookup.rs +122 -26
  416. data/rust/src/kernel/repository.rs +293 -66
  417. data/rust/src/kernel/routing.rs +58 -42
  418. data/rust/src/kernel/ruby_float.rs +139 -0
  419. data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +57 -57
  420. data/rust/src/kernel/vocab/entity_dispatch_order.rs +54 -54
  421. data/rust/src/kernel/vocab/field_hint.rs +19 -25
  422. data/rust/src/kernel/vocab/mod.rs +6 -6
  423. data/rust/src/kernel/vocab/query_comparator.rs +23 -24
  424. data/rust/src/kernel/vocab/refusal_template.rs +501 -297
  425. data/rust/src/lib.rs +50 -0
  426. data/rust/src/main.rs +4 -1
  427. data/rust/web/Cargo.toml +36 -0
  428. data/rust/web/clippy.toml +4 -0
  429. metadata +85 -15
  430. data/examples/pizzas/bluebook/hecksagon/mock_stripe_payment_adapter.rb +0 -36
  431. data/lib/hecks/adapters/driven/google_authentication.adapter +0 -3
  432. data/lib/hecks/adapters/driven/google_authentication.rb +0 -99
  433. data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +0 -3
  434. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +0 -32
  435. data/lib/hecks/ports/authentication.port +0 -6
  436. data/lib/hecks/ports/authentication.rb +0 -67
  437. data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +0 -255
  438. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +0 -22
  439. data/prebuilt/verdicts-48318ca172edd21443b1ad02801c697fff100b746e828f09db987647641a5df2.json +0 -1
  440. data/rust/host/src/approval.rs +0 -652
@@ -1,11 +1,10 @@
1
- use super::newsletter_send::{site_url, unsubscribe_token, unsubscribe_token_matches, unsubscribe_url};
2
- use super::{instances_for, last_refusal, respond};
1
+ use super::newsletter_send::{unsubscribe_token, unsubscribe_token_matches, unsubscribe_url};
2
+ use super::{instances_for, last_refusal, respond, site_url};
3
3
  use crate::auth;
4
+ use crate::commerce_ir::{newsletter_provider, NewsletterProvider};
4
5
  use crate::dispatch;
6
+ use crate::host::Host;
5
7
  use crate::ir::ir;
6
- use crate::commerce_ir::{newsletter_provider, NewsletterProvider};
7
- use crate::journal::LineageConfig;
8
- use crate::lambda_client::LambdaInvoker;
9
8
  use crate::resend::{Email, Mailer};
10
9
  use serde_json::{json, Value};
11
10
  use std::collections::HashMap;
@@ -26,20 +25,28 @@ pub(super) async fn newsletter_route(
26
25
  path: &str,
27
26
  query: &HashMap<String, String>,
28
27
  raw_body: &str,
29
- client: &Mutex<Client>,
30
- wasm_path: &Path,
31
- config: &LineageConfig,
32
- invoker: &dyn LambdaInvoker,
28
+ host: Host<'_>,
33
29
  ) -> Option<Value> {
30
+ let Host {
31
+ client, wasm_path, ..
32
+ } = host;
34
33
  // The chapter that declares `provides "newsletter"` names the verbs
35
34
  // and the subscribing aggregate; a domain that attaches none serves
36
35
  // none of these routes.
37
36
  let provider = ir().and_then(newsletter_provider)?;
38
37
  match (method, path) {
39
- ("POST", "/newsletter/subscribers") => Some(newsletter_subscribe_route(&provider, raw_body, client, wasm_path, config, invoker).await),
40
- ("GET", "/newsletter/subscribers") => Some(newsletter_subscribers_list_route(&provider, client, wasm_path).await),
41
- ("GET", "/newsletter/subscribers/confirm") => Some(newsletter_confirm_route(&provider, query, client, wasm_path, config, invoker).await),
42
- ("GET", "/newsletter/subscribers/unsubscribe") => Some(newsletter_unsubscribe_route(&provider, query, client, wasm_path, config, invoker).await),
38
+ ("POST", "/newsletter/subscribers") => {
39
+ Some(newsletter_subscribe_route(&provider, raw_body, host).await)
40
+ }
41
+ ("GET", "/newsletter/subscribers") => {
42
+ Some(newsletter_subscribers_list_route(&provider, client, wasm_path).await)
43
+ }
44
+ ("GET", "/newsletter/subscribers/confirm") => {
45
+ Some(newsletter_confirm_route(&provider, query, host).await)
46
+ }
47
+ ("GET", "/newsletter/subscribers/unsubscribe") => {
48
+ Some(newsletter_unsubscribe_route(&provider, query, host).await)
49
+ }
43
50
  _ => None,
44
51
  }
45
52
  }
@@ -49,31 +56,51 @@ pub(super) async fn newsletter_route(
49
56
  async fn newsletter_subscribe_route(
50
57
  provider: &NewsletterProvider,
51
58
  raw_body: &str,
52
- client: &Mutex<Client>,
53
- wasm_path: &Path,
54
- config: &LineageConfig,
55
- invoker: &dyn LambdaInvoker,
59
+ host: Host<'_>,
56
60
  ) -> Value {
61
+ let Host {
62
+ client, wasm_path, ..
63
+ } = host;
57
64
  let body: Value = match serde_json::from_str(raw_body) {
58
65
  Ok(v) => v,
59
- Err(e) => return respond(400, "application/json", &json!({"error": format!("invalid JSON: {e}")}).to_string()),
66
+ Err(e) => {
67
+ return respond(
68
+ 400,
69
+ "application/json",
70
+ &json!({"error": format!("invalid JSON: {e}")}).to_string(),
71
+ )
72
+ }
60
73
  };
61
74
  let Some(email) = body.get("email").and_then(|v| v.as_str()) else {
62
- return respond(400, "application/json", &json!({"error": "email"}).to_string());
75
+ return respond(
76
+ 400,
77
+ "application/json",
78
+ &json!({"error": "email"}).to_string(),
79
+ );
63
80
  };
64
- let first_name = body.get("first_name").and_then(|v| v.as_str()).filter(|s| !s.is_empty());
65
- let last_name = body.get("last_name").and_then(|v| v.as_str()).filter(|s| !s.is_empty());
81
+ let first_name = body
82
+ .get("first_name")
83
+ .and_then(|v| v.as_str())
84
+ .filter(|s| !s.is_empty());
85
+ let last_name = body
86
+ .get("last_name")
87
+ .and_then(|v| v.as_str())
88
+ .filter(|s| !s.is_empty());
66
89
 
67
90
  let read = match dispatch::read(client, wasm_path).await {
68
91
  Ok(r) => r,
69
92
  Err(e) => return respond(500, "text/plain", &format!("{e:#}")),
70
93
  };
71
- let existing = instances_for(&read, &provider.instance_prefix()).iter().any(|(id, _)| id == email);
94
+ let existing = instances_for(&read, &provider.instance_prefix())
95
+ .iter()
96
+ .any(|(id, _)| id == email);
72
97
 
73
98
  if existing {
74
99
  if let (Some(first), Some(last)) = (first_name, last_name) {
75
100
  let facts = json!({"first_name": {"value": first}, "last_name": {"value": last}});
76
- if let Err(e) = dispatch::handle_routed(client, wasm_path, &provider.add_name, json!(email), facts, None, config, invoker).await {
101
+ if let Err(e) =
102
+ dispatch::handle_routed(host, &provider.add_name, json!(email), facts, None).await
103
+ {
77
104
  return respond(500, "text/plain", &format!("{e:#}"));
78
105
  }
79
106
  }
@@ -85,12 +112,16 @@ async fn newsletter_subscribe_route(
85
112
  if let Some(last) = last_name {
86
113
  facts["last_name"] = json!({"value": last});
87
114
  }
88
- let outcome = match dispatch::handle_facts(client, wasm_path, &provider.subscribe, facts, None, config, invoker).await {
115
+ let outcome = match dispatch::handle_facts(host, &provider.subscribe, facts, None).await {
89
116
  Ok(o) => o,
90
117
  Err(e) => return respond(500, "text/plain", &format!("{e:#}")),
91
118
  };
92
119
  if !outcome.accepted {
93
- return respond(422, "application/json", &last_refusal(&outcome.result).to_string());
120
+ return respond(
121
+ 422,
122
+ "application/json",
123
+ &last_refusal(&outcome.result).to_string(),
124
+ );
94
125
  }
95
126
  send_confirmation(email).await;
96
127
  }
@@ -104,16 +135,31 @@ async fn newsletter_subscribe_route(
104
135
  };
105
136
  let subscribers = instances_for(&read, &provider.instance_prefix());
106
137
  let Some((_, subscriber)) = subscribers.iter().find(|(id, _)| id == email) else {
107
- return respond(500, "text/plain", "subscriber vanished immediately after being written");
138
+ return respond(
139
+ 500,
140
+ "text/plain",
141
+ "subscriber vanished immediately after being written",
142
+ );
108
143
  };
109
- let status = subscriber.get("status").and_then(|v| v.as_str()).unwrap_or(provider.initial_status());
110
- respond(200, "application/json", &json!({"email": email, "status": status}).to_string())
144
+ let status = subscriber
145
+ .get("status")
146
+ .and_then(|v| v.as_str())
147
+ .unwrap_or_else(|| provider.initial_status());
148
+ respond(
149
+ 200,
150
+ "application/json",
151
+ &json!({"email": email, "status": status}).to_string(),
152
+ )
111
153
  }
112
154
 
113
155
  // Reads directly off `instances`, not `dispatch::query`: Subscriber lives
114
156
  // under the Newsletter chapter, not `config.domain`, so the qualified
115
157
  // question name would not resolve there.
116
- async fn newsletter_subscribers_list_route(provider: &NewsletterProvider, client: &Mutex<Client>, wasm_path: &Path) -> Value {
158
+ async fn newsletter_subscribers_list_route(
159
+ provider: &NewsletterProvider,
160
+ client: &Mutex<Client>,
161
+ wasm_path: &Path,
162
+ ) -> Value {
117
163
  let read = match dispatch::read(client, wasm_path).await {
118
164
  Ok(r) => r,
119
165
  Err(e) => return respond(500, "text/plain", &format!("{e:#}")),
@@ -129,7 +175,12 @@ async fn newsletter_subscribers_list_route(provider: &NewsletterProvider, client
129
175
  })
130
176
  })
131
177
  .collect();
132
- subscribers.sort_by(|a, b| a["email"].as_str().unwrap_or("").cmp(b["email"].as_str().unwrap_or("")));
178
+ subscribers.sort_by(|a, b| {
179
+ a["email"]
180
+ .as_str()
181
+ .unwrap_or("")
182
+ .cmp(b["email"].as_str().unwrap_or(""))
183
+ });
133
184
  respond(200, "application/json", &json!(subscribers).to_string())
134
185
  }
135
186
 
@@ -137,30 +188,50 @@ const CONFIRM_PURPOSE: &str = "newsletter-confirm";
137
188
 
138
189
  // SESSION_SECRET; unset means no confirm link can be signed and none is sent.
139
190
  fn confirm_secret() -> Option<String> {
140
- std::env::var("SESSION_SECRET").ok().filter(|s| !s.is_empty())
191
+ crate::secrets::var("SESSION_SECRET")
192
+ .ok()
193
+ .filter(|s| !s.is_empty())
141
194
  }
142
195
 
143
196
  fn confirm_token(secret: &str, email: &str) -> String {
144
- auth::purpose_token(secret, CONFIRM_PURPOSE, json!({ "email": email }), crate::commerce_ir::newsletter_windows_binding().0)
197
+ auth::purpose_token(
198
+ secret,
199
+ CONFIRM_PURPOSE,
200
+ json!({ "email": email }),
201
+ crate::commerce_ir::newsletter_windows_binding().0,
202
+ )
145
203
  }
146
204
 
147
205
  fn confirm_token_matches(secret: &str, token: &str, email: &str) -> bool {
148
206
  auth::verify_purpose_token(secret, CONFIRM_PURPOSE, token)
149
- .and_then(|claims| claims.get("email").and_then(|v| v.as_str()).map(|signed| signed == email))
207
+ .and_then(|claims| {
208
+ claims
209
+ .get("email")
210
+ .and_then(|v| v.as_str())
211
+ .map(|signed| signed == email)
212
+ })
150
213
  .unwrap_or(false)
151
214
  }
152
215
 
153
216
  // Percent-encodes the address so a '+' in the local part survives the URL.
154
217
  fn confirm_url(site_url: &str, email: &str, token: &str) -> String {
155
- let mut url = reqwest::Url::parse(&format!("{site_url}/newsletter-confirmed.html")).unwrap_or_else(|_| reqwest::Url::parse("http://invalid.invalid/").unwrap());
156
- url.query_pairs_mut().append_pair("email", email).append_pair("token", token);
218
+ // An unparseable site URL degrades to a placeholder host rather than failing the send.
219
+ let Ok(mut url) = reqwest::Url::parse(&format!("{site_url}/newsletter-confirmed.html"))
220
+ .or_else(|_| reqwest::Url::parse("http://invalid.invalid/"))
221
+ else {
222
+ return String::new();
223
+ };
224
+ url.query_pairs_mut()
225
+ .append_pair("email", email)
226
+ .append_pair("token", token);
157
227
  url.to_string()
158
228
  }
159
229
 
160
230
  // Per-process only (not shared across instances); see `ConfirmationCooldown`.
161
231
  fn confirmation_cooldown() -> &'static cooldown::ConfirmationCooldown {
162
232
  static COOLDOWN: OnceLock<cooldown::ConfirmationCooldown> = OnceLock::new();
163
- COOLDOWN.get_or_init(|| cooldown::ConfirmationCooldown::new(cooldown::WINDOW, cooldown::CAPACITY))
233
+ COOLDOWN
234
+ .get_or_init(|| cooldown::ConfirmationCooldown::new(cooldown::WINDOW, cooldown::CAPACITY))
164
235
  }
165
236
 
166
237
  fn confirmation_body(confirm_url: &str) -> String {
@@ -195,21 +266,32 @@ pub(super) async fn send_confirmation(email: &str) {
195
266
  let site = site_url();
196
267
  let link = confirm_url(&site, email, &confirm_token(&secret, email));
197
268
  let unsubscribe = unsubscribe_url(&site, email, &unsubscribe_token(&secret, email));
198
- let body = match template::branded_body(&link, &unsubscribe).await {
199
- Some(html) => html,
200
- None => confirmation_body(&link),
201
- };
269
+ let body = template::branded_body(&link, &unsubscribe)
270
+ .await
271
+ .unwrap_or_else(|| confirmation_body(&link));
202
272
  let delivery = mailer
203
- .deliver(&Email { to: email, subject: "Confirm your newsletter subscription", body: &body, unsubscribe_url: Some(&unsubscribe) })
273
+ .deliver(&Email {
274
+ to: email,
275
+ subject: "Confirm your newsletter subscription",
276
+ body: &body,
277
+ unsubscribe_url: Some(&unsubscribe),
278
+ })
204
279
  .await;
205
280
  if !delivery.ok {
206
- eprintln!("newsletter: the confirmation email was not delivered: {}", delivery.reason.unwrap_or_default());
281
+ eprintln!(
282
+ "newsletter: the confirmation email was not delivered: {}",
283
+ delivery.reason.unwrap_or_default()
284
+ );
207
285
  }
208
286
  }
209
287
 
210
288
  // Only mails when the address is now `pending`; an already-confirmed or
211
289
  // unsubscribed registrant is left alone.
212
- pub(super) async fn send_confirmation_if_pending(email: &str, client: &Mutex<Client>, wasm_path: &Path) {
290
+ pub(super) async fn send_confirmation_if_pending(
291
+ email: &str,
292
+ client: &Mutex<Client>,
293
+ wasm_path: &Path,
294
+ ) {
213
295
  let Some(provider) = ir().and_then(newsletter_provider) else {
214
296
  return;
215
297
  };
@@ -218,7 +300,11 @@ pub(super) async fn send_confirmation_if_pending(email: &str, client: &Mutex<Cli
218
300
  };
219
301
  let pending = instances_for(&read, &provider.instance_prefix())
220
302
  .iter()
221
- .any(|(id, subscriber)| id == email && provider.is_awaiting_confirmation(subscriber.get("status").and_then(|v| v.as_str())));
303
+ .any(|(id, subscriber)| {
304
+ id == email
305
+ && provider
306
+ .is_awaiting_confirmation(subscriber.get("status").and_then(|v| v.as_str()))
307
+ });
222
308
  if pending {
223
309
  send_confirmation(email).await;
224
310
  }
@@ -228,16 +314,31 @@ pub(super) async fn send_confirmation_if_pending(email: &str, client: &Mutex<Cli
228
314
  // a missing, wrong or expired one refuses before any subscriber is touched.
229
315
  // Idempotent: dispatches Confirm only while still `pending`, so a double
230
316
  // click or link-prefetch lands on the same success response, not a 422.
231
- async fn newsletter_confirm_route(provider: &NewsletterProvider, query: &HashMap<String, String>, client: &Mutex<Client>, wasm_path: &Path, config: &LineageConfig, invoker: &dyn LambdaInvoker) -> Value {
317
+ async fn newsletter_confirm_route(
318
+ provider: &NewsletterProvider,
319
+ query: &HashMap<String, String>,
320
+ host: Host<'_>,
321
+ ) -> Value {
322
+ let Host {
323
+ client, wasm_path, ..
324
+ } = host;
232
325
  let Some(email) = query.get("email") else {
233
- return respond(400, "application/json", &json!({"error": "email"}).to_string());
326
+ return respond(
327
+ 400,
328
+ "application/json",
329
+ &json!({"error": "email"}).to_string(),
330
+ );
234
331
  };
235
332
  let signed = match (confirm_secret(), query.get("token")) {
236
333
  (Some(secret), Some(token)) => confirm_token_matches(&secret, token, email),
237
334
  _ => false,
238
335
  };
239
336
  if !signed {
240
- return respond(403, "application/json", &json!({"error": "this confirmation link is invalid or has expired"}).to_string());
337
+ return respond(
338
+ 403,
339
+ "application/json",
340
+ &json!({"error": "this confirmation link is invalid or has expired"}).to_string(),
341
+ );
241
342
  }
242
343
 
243
344
  let read = match dispatch::read(client, wasm_path).await {
@@ -246,11 +347,17 @@ async fn newsletter_confirm_route(provider: &NewsletterProvider, query: &HashMap
246
347
  };
247
348
  let subscribers = instances_for(&read, &provider.instance_prefix());
248
349
  let Some((_, subscriber)) = subscribers.iter().find(|(id, _)| id == email) else {
249
- return respond(404, "application/json", &json!({"error": "no such subscriber"}).to_string());
350
+ return respond(
351
+ 404,
352
+ "application/json",
353
+ &json!({"error": "no such subscriber"}).to_string(),
354
+ );
250
355
  };
251
356
 
252
357
  if provider.is_awaiting_confirmation(subscriber.get("status").and_then(|v| v.as_str())) {
253
- if let Err(e) = dispatch::handle_routed(client, wasm_path, &provider.confirm, json!(email), json!({}), None, config, invoker).await {
358
+ if let Err(e) =
359
+ dispatch::handle_routed(host, &provider.confirm, json!(email), json!({}), None).await
360
+ {
254
361
  return respond(500, "text/plain", &format!("{e:#}"));
255
362
  }
256
363
  }
@@ -260,15 +367,31 @@ async fn newsletter_confirm_route(provider: &NewsletterProvider, query: &HashMap
260
367
  Err(e) => return respond(500, "text/plain", &format!("{e:#}")),
261
368
  };
262
369
  let subscribers = instances_for(&read, &provider.instance_prefix());
263
- let status = subscribers.iter().find(|(id, _)| id == email).and_then(|(_, s)| s.get("status")).and_then(|v| v.as_str()).unwrap_or(provider.initial_status());
264
- respond(200, "application/json", &json!({"email": email, "status": status}).to_string())
370
+ let status = subscribers
371
+ .iter()
372
+ .find(|(id, _)| id == email)
373
+ .and_then(|(_, s)| s.get("status"))
374
+ .and_then(|v| v.as_str())
375
+ .unwrap_or_else(|| provider.initial_status());
376
+ respond(
377
+ 200,
378
+ "application/json",
379
+ &json!({"email": email, "status": status}).to_string(),
380
+ )
265
381
  }
266
382
 
267
383
  // 400 with no address; 403 for a missing, wrong, other-address or expired
268
384
  // token, or no secret to check against. Otherwise Ok(email).
269
- fn unsubscribe_authorized<'a>(secret: Option<&str>, query: &'a HashMap<String, String>) -> Result<&'a str, Value> {
385
+ fn unsubscribe_authorized<'a>(
386
+ secret: Option<&str>,
387
+ query: &'a HashMap<String, String>,
388
+ ) -> Result<&'a str, Value> {
270
389
  let Some(email) = query.get("email") else {
271
- return Err(respond(400, "application/json", &json!({"error": "email"}).to_string()));
390
+ return Err(respond(
391
+ 400,
392
+ "application/json",
393
+ &json!({"error": "email"}).to_string(),
394
+ ));
272
395
  };
273
396
  let signed = match (secret, query.get("token")) {
274
397
  (Some(secret), Some(token)) => unsubscribe_token_matches(secret, token, email),
@@ -277,14 +400,25 @@ fn unsubscribe_authorized<'a>(secret: Option<&str>, query: &'a HashMap<String, S
277
400
  if signed {
278
401
  Ok(email)
279
402
  } else {
280
- Err(respond(403, "application/json", &json!({"error": "this unsubscribe link is invalid or has expired"}).to_string()))
403
+ Err(respond(
404
+ 403,
405
+ "application/json",
406
+ &json!({"error": "this unsubscribe link is invalid or has expired"}).to_string(),
407
+ ))
281
408
  }
282
409
  }
283
410
 
284
411
  // Same token/idempotency shape as newsletter_confirm_route: Unsubscribe's
285
412
  // own `given` accepts a pending or confirmed subscriber, so a repeat click
286
413
  // on an already-unsubscribed row lands on the same success response.
287
- async fn newsletter_unsubscribe_route(provider: &NewsletterProvider, query: &HashMap<String, String>, client: &Mutex<Client>, wasm_path: &Path, config: &LineageConfig, invoker: &dyn LambdaInvoker) -> Value {
414
+ async fn newsletter_unsubscribe_route(
415
+ provider: &NewsletterProvider,
416
+ query: &HashMap<String, String>,
417
+ host: Host<'_>,
418
+ ) -> Value {
419
+ let Host {
420
+ client, wasm_path, ..
421
+ } = host;
288
422
  let email = match unsubscribe_authorized(confirm_secret().as_deref(), query) {
289
423
  Ok(email) => email,
290
424
  Err(refusal) => return refusal,
@@ -296,11 +430,18 @@ async fn newsletter_unsubscribe_route(provider: &NewsletterProvider, query: &Has
296
430
  };
297
431
  let subscribers = instances_for(&read, &provider.instance_prefix());
298
432
  let Some((_, subscriber)) = subscribers.iter().find(|(id, _)| id == email) else {
299
- return respond(404, "application/json", &json!({"error": "no such subscriber"}).to_string());
433
+ return respond(
434
+ 404,
435
+ "application/json",
436
+ &json!({"error": "no such subscriber"}).to_string(),
437
+ );
300
438
  };
301
439
 
302
440
  if !provider.has_left(subscriber.get("status").and_then(|v| v.as_str())) {
303
- if let Err(e) = dispatch::handle_routed(client, wasm_path, &provider.unsubscribe, json!(email), json!({}), None, config, invoker).await {
441
+ if let Err(e) =
442
+ dispatch::handle_routed(host, &provider.unsubscribe, json!(email), json!({}), None)
443
+ .await
444
+ {
304
445
  return respond(500, "text/plain", &format!("{e:#}"));
305
446
  }
306
447
  }
@@ -310,6 +451,15 @@ async fn newsletter_unsubscribe_route(provider: &NewsletterProvider, query: &Has
310
451
  Err(e) => return respond(500, "text/plain", &format!("{e:#}")),
311
452
  };
312
453
  let subscribers = instances_for(&read, &provider.instance_prefix());
313
- let status = subscribers.iter().find(|(id, _)| id == email).and_then(|(_, s)| s.get("status")).and_then(|v| v.as_str()).unwrap_or(provider.initial_status());
314
- respond(200, "application/json", &json!({"email": email, "status": status}).to_string())
454
+ let status = subscribers
455
+ .iter()
456
+ .find(|(id, _)| id == email)
457
+ .and_then(|(_, s)| s.get("status"))
458
+ .and_then(|v| v.as_str())
459
+ .unwrap_or_else(|| provider.initial_status());
460
+ respond(
461
+ 200,
462
+ "application/json",
463
+ &json!({"email": email, "status": status}).to_string(),
464
+ )
315
465
  }