hecks 3.14.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (427) hide show
  1. checksums.yaml +4 -4
  2. data/examples/pizzas/adapters/fake_processor.rb +34 -0
  3. data/examples/pizzas/adapters/payment_signature.rb +66 -0
  4. data/examples/pizzas/adapters/payment_webhook.rb +87 -0
  5. data/examples/pizzas/bluebook/pizzas.behaviors +33 -0
  6. data/examples/pizzas/bluebook/pizzas.hecksagon +5 -3
  7. data/examples/pizzas/config.ru +22 -0
  8. data/exe/hecks +1 -1
  9. data/lib/hecks/adapters/driven/prism.rb +0 -2
  10. data/lib/hecks/adapters/driven.rb +0 -2
  11. data/lib/hecks/adapters/driving/json.rb +9 -16
  12. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -2
  13. data/lib/hecks/bluebook/dsl/derivation_words.rb +58 -0
  14. data/lib/hecks/bluebook/dsl/translation_builder.rb +6 -37
  15. data/lib/hecks/bluebook/expression/ast_json.rb +0 -9
  16. data/lib/hecks/bluebook/hecksagon.rb +1 -1
  17. data/lib/hecks/bluebook/meta_validator/translation_judge/rules.rb +7 -10
  18. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +4 -8
  19. data/lib/hecks/bluebook/meta_validator.rb +0 -11
  20. data/lib/hecks/bluebook/translation.rb +18 -10
  21. data/lib/hecks/bluebook/translation_expression/evaluator.rb +63 -0
  22. data/lib/hecks/bluebook/translation_expression/parser.rb +127 -0
  23. data/lib/hecks/bluebook/translation_expression.rb +39 -0
  24. data/lib/hecks/bluebook/translation_plan.rb +70 -0
  25. data/lib/hecks/bluebook.rb +2 -0
  26. data/lib/hecks/chapters.rb +3 -3
  27. data/lib/hecks/cli/project_cli/launcher_source.rb +1 -1
  28. data/lib/hecks/composer/adapters/claude_composer.adapter +3 -0
  29. data/lib/hecks/composer/adapters/claude_composer.rb +83 -0
  30. data/lib/hecks/composer/adapters/fake_agent.adapter +3 -0
  31. data/lib/hecks/composer/adapters/fake_agent.rb +55 -0
  32. data/lib/hecks/composer/adapters/jev.adapter +3 -0
  33. data/lib/hecks/composer/adapters/jev.rb +66 -0
  34. data/lib/hecks/composer/agent.rb +113 -0
  35. data/lib/hecks/composer/assistant.rb +110 -0
  36. data/lib/hecks/composer/behavior_run.rb +53 -0
  37. data/lib/hecks/composer/bluebook/composer.bluebook +178 -0
  38. data/lib/hecks/composer/bluebook/composer.hecksagon +16 -0
  39. data/lib/hecks/composer/board.rb +65 -0
  40. data/lib/hecks/composer/board_edit/command_text.rb +39 -0
  41. data/lib/hecks/composer/board_edit/source.rb +84 -0
  42. data/lib/hecks/composer/board_edit.rb +144 -0
  43. data/lib/hecks/composer/check.rb +89 -0
  44. data/lib/hecks/composer/domains.rb +81 -0
  45. data/lib/hecks/composer/jobs.rb +134 -0
  46. data/lib/hecks/composer/layout.rb +37 -0
  47. data/lib/hecks/composer/live.rb +150 -0
  48. data/lib/hecks/composer/outline.rb +48 -0
  49. data/lib/hecks/composer/page.html +989 -0
  50. data/lib/hecks/composer/panels.rb +142 -0
  51. data/lib/hecks/composer/parse.rb +83 -0
  52. data/lib/hecks/composer/plain.rb +28 -0
  53. data/lib/hecks/composer/ports/composer_agent.port +7 -0
  54. data/lib/hecks/composer/session.rb +141 -0
  55. data/lib/hecks/composer/toolbox.rb +166 -0
  56. data/lib/hecks/composer/web.rb +156 -0
  57. data/lib/hecks/composer/workbench.rb +51 -0
  58. data/lib/hecks/composer/workspace.rb +81 -0
  59. data/lib/hecks/composer.rb +17 -0
  60. data/lib/hecks/corpus/adapters.rb +22 -0
  61. data/lib/hecks/corpus/rust.rb +1 -1
  62. data/lib/hecks/corpus/worlds.rb +19 -0
  63. data/lib/hecks/corpus.rb +5 -0
  64. data/lib/hecks/forms/app/commands.rb +2 -3
  65. data/lib/hecks/forms/app/queries.rb +2 -3
  66. data/lib/hecks/forms/app/records.rb +17 -5
  67. data/lib/hecks/forms/app/responses.rb +3 -1
  68. data/lib/hecks/forms/command_form_renderer.rb +42 -16
  69. data/lib/hecks/forms/field_controls.rb +2 -2
  70. data/lib/hecks/forms/field_renderer.rb +1 -1
  71. data/lib/hecks/forms/field_shape.rb +36 -17
  72. data/lib/hecks/forms/index_renderer.rb +2 -1
  73. data/lib/hecks/forms/page.rb +1 -1
  74. data/lib/hecks/forms/params.rb +0 -13
  75. data/lib/hecks/forms/query_form_renderer.rb +7 -6
  76. data/lib/hecks/forms/record_renderer.rb +11 -7
  77. data/lib/hecks/forms/rule_words.rb +100 -0
  78. data/lib/hecks/forms/value_object_shape.rb +2 -1
  79. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -3
  80. data/lib/hecks/fuzzing/isolated_boot/worlds.rb +3 -6
  81. data/lib/hecks/fuzzing/replay/filters.rb +2 -6
  82. data/lib/hecks/gate/stages.yml +25 -0
  83. data/lib/hecks/grammar/evolve/seeds.rb +0 -6
  84. data/lib/hecks/grammar/evolve.rb +0 -3
  85. data/lib/hecks/grammar/translation.bluebook +1 -1
  86. data/lib/hecks/hecks/adapters/codebase/composing.rb +56 -0
  87. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +3 -1
  88. data/lib/hecks/hecks/adapters/first_admin.rb +3 -3
  89. data/lib/hecks/hecks/adapters/journal_store/changes.rb +0 -26
  90. data/lib/hecks/hecks/adapters/journal_store/examination.rb +2 -23
  91. data/lib/hecks/hecks/adapters/journal_store/readings.rb +3 -11
  92. data/lib/hecks/hecks/adapters/journal_store.rb +0 -11
  93. data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +4 -11
  94. data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +0 -9
  95. data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +0 -4
  96. data/lib/hecks/hecks/codebase.behaviors +6 -0
  97. data/lib/hecks/hecks/codebase.bluebook +21 -0
  98. data/lib/hecks/hecks/composer.behaviors +68 -0
  99. data/lib/hecks/hecks/context_map.hecksagon +1 -1
  100. data/lib/hecks/hecks/custodian.behaviors +0 -6
  101. data/lib/hecks/hecks/custodian.bluebook +3 -80
  102. data/lib/hecks/hecks/hecks.hecksagon +2 -2
  103. data/lib/hecks/hecks/hecks.world +1 -1
  104. data/lib/hecks/hecks/rehearsal.bluebook +7 -13
  105. data/lib/hecks/kernel_capabilities/templates.rb +4 -4
  106. data/lib/hecks/kernel_capabilities.rb +1 -1
  107. data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -3
  108. data/lib/hecks/language/translation/translation_aggregate.bluebook +21 -24
  109. data/lib/hecks/ports/access_control.rb +2 -2
  110. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -41
  111. data/lib/hecks/ports/persistence/plugins/era/lineage/coverage.rb +7 -7
  112. data/lib/hecks/ports/persistence/plugins/era/lineage/rewrite.rb +25 -2
  113. data/lib/hecks/ports/persistence/plugins/era/lineage/rules.rb +4 -4
  114. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +17 -25
  115. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/base_sql.rb +0 -12
  116. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/domain_rename.rb +1 -1
  117. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -22
  118. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +0 -1
  119. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +9 -3
  120. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +3 -29
  121. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/refusals.rb +2 -26
  122. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -33
  123. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -3
  124. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +2 -4
  125. data/lib/hecks/ports/persistence/plugins/era/translation/expression_sql.rb +52 -0
  126. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +23 -29
  127. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +1 -1
  128. data/lib/hecks/ports/persistence/plugins/era/translation.rb +0 -1
  129. data/lib/hecks/ports/persistence/plugins/era.rb +3 -6
  130. data/lib/hecks/ports.rb +0 -1
  131. data/lib/hecks/projection_files.rb +3 -1
  132. data/lib/hecks/projections/deploy/templates/lambda/nat_comment_oauth.tmpl +1 -1
  133. data/lib/hecks/projections/glossary/entries.rb +10 -4
  134. data/lib/hecks/projections/glossary/groups.rb +42 -0
  135. data/lib/hecks/projections/glossary/html/page.rb +7 -2
  136. data/lib/hecks/projections/glossary/html/parser.rb +1 -1
  137. data/lib/hecks/projections/glossary/html/sections.rb +31 -11
  138. data/lib/hecks/projections/glossary/html.rb +1 -2
  139. data/lib/hecks/projections/glossary/markdown.rb +31 -28
  140. data/lib/hecks/projections/glossary/mermaid.rb +3 -0
  141. data/lib/hecks/projections/glossary/page.css +143 -153
  142. data/lib/hecks/projections/glossary/page.js +63 -3
  143. data/lib/hecks/projections/glossary/sentences/actions.rb +88 -0
  144. data/lib/hecks/projections/glossary/sentences/paths.rb +77 -0
  145. data/lib/hecks/projections/glossary/sentences/queries.rb +90 -0
  146. data/lib/hecks/projections/glossary/sentences/value_objects.rb +32 -5
  147. data/lib/hecks/projections/glossary/sentences.rb +13 -3
  148. data/lib/hecks/projections/glossary/slugs.rb +4 -1
  149. data/lib/hecks/projections/glossary.rb +2 -1
  150. data/lib/hecks/projections/plain_english.rb +97 -0
  151. data/lib/hecks/projections/rust_vocabulary/arguments.rb +2 -2
  152. data/lib/hecks/projections/rust_vocabulary/enum_body.rb +20 -25
  153. data/lib/hecks/projections/rust_vocabulary/tests.rb +2 -2
  154. data/lib/hecks/projections/rust_vocabulary.rb +14 -6
  155. data/lib/hecks/projections/site/cms_editor/color.rb +0 -6
  156. data/lib/hecks/projections/site/cms_editor/templates/src/host.ts.tmpl +7 -2
  157. data/lib/hecks/projections/statements/actions.rb +99 -0
  158. data/lib/hecks/projections/statements/relationships.rb +86 -0
  159. data/lib/hecks/projections/statements.rb +15 -24
  160. data/lib/hecks/projector/exporter/adapters.rb +112 -0
  161. data/lib/hecks/projector/exporter/translations.rb +18 -35
  162. data/lib/hecks/projector/exporter/worlds.rb +103 -0
  163. data/lib/hecks/projector/exporter.rb +5 -1
  164. data/lib/hecks/quality_control/adapters/agent.rb +6 -7
  165. data/lib/hecks/quality_control/adapters/agent_profile.rb +16 -114
  166. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +1 -1
  167. data/lib/hecks/runtime/loader/boot_steps.rb +0 -20
  168. data/lib/hecks/runtime/outbox.rb +2 -9
  169. data/lib/hecks/runtime/port_operation_interpreter.rb +1 -9
  170. data/lib/hecks/runtime/remote_dispatcher.rb +2 -2
  171. data/lib/hecks/runtime/value/unwrapping.rb +60 -0
  172. data/lib/hecks/runtime/value.rb +10 -21
  173. data/lib/hecks/rust_build/project_rust/target_ir.rb +23 -4
  174. data/lib/hecks/rust_build/wasm.rb +0 -12
  175. data/lib/hecks/rust_build/write_if_changed.rb +12 -12
  176. data/lib/hecks/rust_format.rb +77 -0
  177. data/lib/hecks/tools/fuzz_sweep/findings.rb +3 -9
  178. data/lib/hecks/tools/fuzz_sweep.rb +1 -3
  179. data/lib/hecks/tools/translation_audit/verdicts.rb +3 -28
  180. data/lib/hecks/tools/translation_audit.rb +8 -10
  181. data/lib/hecks/tools/translation_scaffold/reporting.rb +1 -1
  182. data/lib/hecks/version.rb +1 -1
  183. data/lib/hecks/vocabulary.rb +2 -1
  184. data/lib/hecks.rb +3 -0
  185. data/prebuilt/{1a6f596be124228be93eba1d86dbc9d2933190fa389b284331241cb9f27115a0.marshal → ad68f9e06c8068c2a4176b57abf3083d591d5d8980c2b3e897b97331f965b643.marshal} +0 -0
  186. data/prebuilt/{a883a7e568667aa5ac904c82daff356bdf9e996252a36af6def4a4a43efb967e.marshal → ee22ec8642c4006717ba2b80f46e5efb81de459eef9dfec043e838c0ad4fd720.marshal} +0 -0
  187. data/prebuilt/{verdicts-872993b1aa26d73fe48f28251374b8585fed145d02aab42be0f00de386b6ff80.json → verdicts-b69f5dcbba79c20daa0493476fa7c317004a30aa2bc3d3dc6a722ea70ae2b43b.json} +1 -1
  188. data/rust/Cargo.toml +40 -0
  189. data/rust/build/Cargo.toml +36 -0
  190. data/rust/build/clippy.toml +4 -0
  191. data/rust/build/src/adapters_pass.rs +196 -0
  192. data/rust/build/src/binding_pass.rs +54 -13
  193. data/rust/build/src/build_artifact.rs +23 -5
  194. data/rust/build/src/cargo_sync.rs +69 -26
  195. data/rust/build/src/lineage_pass.rs +101 -87
  196. data/rust/build/src/main.rs +11 -2
  197. data/rust/build/src/pipeline.rs +80 -19
  198. data/rust/build/src/reserved_names.rs +6 -50
  199. data/rust/build/src/resolve.rs +82 -35
  200. data/rust/build/src/rustfmt.rs +100 -0
  201. data/rust/build/src/seams_pass.rs +85 -16
  202. data/rust/build/src/subprocess.rs +30 -6
  203. data/rust/build/src/tmp.rs +5 -2
  204. data/rust/build/src/translation_pass.rs +72 -22
  205. data/rust/build/src/world_pass.rs +109 -0
  206. data/rust/clippy.toml +4 -0
  207. data/rust/codegen/Cargo.toml +36 -0
  208. data/rust/codegen/clippy.toml +4 -0
  209. data/rust/codegen/src/asks.rs +40 -14
  210. data/rust/codegen/src/attr.rs +2 -2
  211. data/rust/codegen/src/bridging.rs +676 -117
  212. data/rust/codegen/src/commands.rs +979 -373
  213. data/rust/codegen/src/constraints.rs +88 -25
  214. data/rust/codegen/src/dependency_planning.rs +128 -34
  215. data/rust/codegen/src/domain_generator.rs +306 -170
  216. data/rust/codegen/src/exemplar.rs +87 -32
  217. data/rust/codegen/src/expr_emitter.rs +54 -6
  218. data/rust/codegen/src/fielded.rs +193 -41
  219. data/rust/codegen/src/hecks_naming.rs +124 -16
  220. data/rust/codegen/src/hecks_naming_tests.rs +81 -0
  221. data/rust/codegen/src/json.rs +80 -44
  222. data/rust/codegen/src/json_codec.rs +585 -149
  223. data/rust/codegen/src/literal.rs +104 -138
  224. data/rust/codegen/src/main.rs +151 -52
  225. data/rust/codegen/src/manifest.rs +76 -14
  226. data/rust/codegen/src/mutations.rs +1212 -239
  227. data/rust/codegen/src/naming.rs +111 -60
  228. data/rust/codegen/src/optional_pass.rs +90 -28
  229. data/rust/codegen/src/ports.rs +5 -4
  230. data/rust/codegen/src/prelude.rs +139 -49
  231. data/rust/codegen/src/presenters.rs +335 -0
  232. data/rust/codegen/src/queries.rs +601 -226
  233. data/rust/codegen/src/reactions.rs +268 -74
  234. data/rust/codegen/src/read_models.rs +574 -229
  235. data/rust/codegen/src/reference_specs.rs +16 -3
  236. data/rust/codegen/src/registry.rs +128 -79
  237. data/rust/codegen/src/reserved_names.rs +6 -50
  238. data/rust/codegen/src/shared.rs +71 -22
  239. data/rust/codegen/src/sidecars.rs +15 -5
  240. data/rust/codegen/src/skip_reason.rs +8 -2
  241. data/rust/codegen/src/types.rs +306 -108
  242. data/rust/host/Cargo.lock +1 -0
  243. data/rust/host/Cargo.toml +40 -4
  244. data/rust/host/HECKS_RELEASE +1 -1
  245. data/rust/host/clippy.toml +4 -0
  246. data/rust/host/src/api.rs +698 -218
  247. data/rust/host/src/auth.rs +491 -214
  248. data/rust/host/src/b64.rs +91 -0
  249. data/rust/host/src/bin/expr_harness.rs +6 -2
  250. data/rust/host/src/bin/lineage_harness.rs +47 -11
  251. data/rust/host/src/bin/mint_harness.rs +70 -37
  252. data/rust/host/src/bin/validate_harness.rs +16 -8
  253. data/rust/host/src/boot.rs +144 -68
  254. data/rust/host/src/boundary_fuzz/api.rs +130 -32
  255. data/rust/host/src/boundary_fuzz/auth.rs +108 -23
  256. data/rust/host/src/boundary_fuzz/expr_json.rs +85 -11
  257. data/rust/host/src/boundary_fuzz/ir.rs +164 -12
  258. data/rust/host/src/boundary_fuzz/server.rs +283 -36
  259. data/rust/host/src/boundary_fuzz/web.rs +56 -8
  260. data/rust/host/src/checkout.rs +78 -32
  261. data/rust/host/src/clock.rs +8 -0
  262. data/rust/host/src/commerce_ir.rs +333 -95
  263. data/rust/host/src/crypto.rs +33 -0
  264. data/rust/host/src/dispatch.rs +1037 -331
  265. data/rust/host/src/expr_json.rs +1021 -273
  266. data/rust/host/src/extension.rs +24 -4
  267. data/rust/host/src/field_hints.rs +11 -7
  268. data/rust/host/src/fuzz_support.rs +85 -13
  269. data/rust/host/src/host.rs +100 -0
  270. data/rust/host/src/http.rs +22 -0
  271. data/rust/host/src/ir.rs +326 -16
  272. data/rust/host/src/journal.rs +277 -176
  273. data/rust/host/src/lambda_client.rs +219 -53
  274. data/rust/host/src/lib.rs +16 -4
  275. data/rust/host/src/log.rs +62 -11
  276. data/rust/host/src/mint.rs +1153 -326
  277. data/rust/host/src/needs.rs +185 -44
  278. data/rust/host/src/payments/keystore.rs +197 -45
  279. data/rust/host/src/payments/roles.rs +39 -7
  280. data/rust/host/src/payments/tests.rs +1427 -282
  281. data/rust/host/src/payments.rs +391 -112
  282. data/rust/host/src/platform_signin/attack_tests.rs +5 -5
  283. data/rust/host/src/platform_signin/config.rs +11 -11
  284. data/rust/host/src/platform_signin/cookie.rs +2 -2
  285. data/rust/host/src/platform_signin/extension.rs +2 -1
  286. data/rust/host/src/platform_signin/handler.rs +9 -8
  287. data/rust/host/src/platform_signin/jti.rs +4 -2
  288. data/rust/host/src/platform_signin/jwks.rs +2 -1
  289. data/rust/host/src/platform_signin/jws.rs +2 -37
  290. data/rust/host/src/platform_signin/known_answer_tests.rs +5 -1
  291. data/rust/host/src/platform_signin/mode_tests.rs +5 -3
  292. data/rust/host/src/platform_signin/pages.rs +20 -20
  293. data/rust/host/src/platform_signin/session.rs +8 -1
  294. data/rust/host/src/platform_signin/settings.rs +6 -6
  295. data/rust/host/src/platform_signin/strict_json.rs +12 -9
  296. data/rust/host/src/platform_signin/verify.rs +41 -41
  297. data/rust/host/src/platform_signin/wiring_support.rs +13 -18
  298. data/rust/host/src/presentation.rs +207 -63
  299. data/rust/host/src/presentation_write.rs +502 -187
  300. data/rust/host/src/query_step.rs +69 -18
  301. data/rust/host/src/rate_limit.rs +541 -142
  302. data/rust/host/src/reference_transform.rs +77 -25
  303. data/rust/host/src/reference_validate.rs +443 -104
  304. data/rust/host/src/resend.rs +190 -55
  305. data/rust/host/src/secrets.rs +116 -21
  306. data/rust/host/src/server.rs +555 -104
  307. data/rust/host/src/storage_shape.rs +88 -21
  308. data/rust/host/src/test_pg.rs +17 -7
  309. data/rust/host/src/ui_schema.rs +495 -207
  310. data/rust/host/src/wasm_runner.rs +21 -6
  311. data/rust/host/src/web/commerce.rs +101 -23
  312. data/rust/host/src/web/newsletter/cooldown.rs +10 -3
  313. data/rust/host/src/web/newsletter/template.rs +26 -7
  314. data/rust/host/src/web/newsletter/tests.rs +112 -23
  315. data/rust/host/src/web/newsletter.rs +209 -59
  316. data/rust/host/src/web/newsletter_send.rs +251 -79
  317. data/rust/host/src/web/registration_receipt/tests.rs +16 -4
  318. data/rust/host/src/web/registration_receipt.rs +100 -24
  319. data/rust/host/src/web/registrations.rs +1384 -332
  320. data/rust/host/src/web/resend_webhook.rs +138 -80
  321. data/rust/host/src/web.rs +2416 -508
  322. data/rust/lsp/Cargo.toml +36 -0
  323. data/rust/lsp/clippy.toml +4 -0
  324. data/rust/lsp/src/json.rs +29 -22
  325. data/rust/lsp/src/main.rs +81 -31
  326. data/rust/lsp/src/outline.rs +23 -7
  327. data/rust/lsp/src/rpc.rs +17 -4
  328. data/rust/parser/Cargo.toml +36 -0
  329. data/rust/parser/clippy.toml +4 -0
  330. data/rust/parser/src/build/closed_sets.rs +6 -1
  331. data/rust/parser/src/build/identity.rs +4 -3
  332. data/rust/parser/src/build/mod.rs +1 -1
  333. data/rust/parser/src/build/named_pattern.rs +4 -1
  334. data/rust/parser/src/build/naming.rs +2 -80
  335. data/rust/parser/src/build/pattern_subset.rs +6 -8
  336. data/rust/parser/src/build/query_derive.rs +3 -1
  337. data/rust/parser/src/build/query_inference.rs +14 -5
  338. data/rust/parser/src/build/read_model.rs +4 -1
  339. data/rust/parser/src/build/references.rs +4 -3
  340. data/rust/parser/src/canonical.rs +34 -15
  341. data/rust/parser/src/emit.rs +187 -105
  342. data/rust/parser/src/expr/ast_json.rs +134 -42
  343. data/rust/parser/src/expr/evaluator.rs +27 -9
  344. data/rust/parser/src/expr/mod.rs +54 -13
  345. data/rust/parser/src/expr/resolver.rs +299 -92
  346. data/rust/parser/src/ir.rs +35 -3
  347. data/rust/parser/src/keywords.rs +7168 -516
  348. data/rust/parser/src/lex.rs +11 -25
  349. data/rust/parser/src/lib.rs +1 -0
  350. data/rust/parser/src/main.rs +65 -6
  351. data/rust/parser/src/parse/adapter.rs +157 -0
  352. data/rust/parser/src/parse/aggregate.rs +19 -10
  353. data/rust/parser/src/parse/chapter.rs +16 -17
  354. data/rust/parser/src/parse/command.rs +35 -16
  355. data/rust/parser/src/parse/domain_port.rs +4 -7
  356. data/rust/parser/src/parse/entity.rs +21 -11
  357. data/rust/parser/src/parse/hecksagon.rs +10 -4
  358. data/rust/parser/src/parse/lifecycle.rs +9 -5
  359. data/rust/parser/src/parse/mod.rs +22 -91
  360. data/rust/parser/src/parse/needs.rs +21 -12
  361. data/rust/parser/src/parse/policy.rs +2 -2
  362. data/rust/parser/src/parse/process_manager.rs +9 -11
  363. data/rust/parser/src/parse/query.rs +8 -4
  364. data/rust/parser/src/parse/read_model.rs +57 -16
  365. data/rust/parser/src/parse/value_object.rs +7 -2
  366. data/rust/parser/src/parse/world.rs +322 -0
  367. data/rust/parser/src/ruby_value.rs +7 -6
  368. data/rust/src/exemplar/json.rs +4 -4
  369. data/rust/src/exemplar/mutations.rs +18 -0
  370. data/rust/src/exemplar/read_models.rs +1 -10
  371. data/rust/src/kernel/attribute_shapes/composite.rs +54 -14
  372. data/rust/src/kernel/attribute_shapes/list.rs +2 -2
  373. data/rust/src/kernel/attribute_shapes/mod.rs +8 -8
  374. data/rust/src/kernel/attribute_shapes/scalar.rs +2 -2
  375. data/rust/src/kernel/cli.rs +244 -114
  376. data/rust/src/kernel/dispatch.rs +323 -115
  377. data/rust/src/kernel/dispatch_guard_tests.rs +327 -65
  378. data/rust/src/kernel/expr.rs +203 -86
  379. data/rust/src/kernel/expression_operators/arithmetic.rs +138 -25
  380. data/rust/src/kernel/expression_operators/comparison.rs +87 -2
  381. data/rust/src/kernel/expression_operators/enumeration.rs +106 -22
  382. data/rust/src/kernel/expression_operators/logical.rs +9 -3
  383. data/rust/src/kernel/expression_operators/membership.rs +3 -1
  384. data/rust/src/kernel/expression_operators/mod.rs +34 -21
  385. data/rust/src/kernel/expression_operators/pattern_match.rs +46 -11
  386. data/rust/src/kernel/expression_operators/positional.rs +84 -30
  387. data/rust/src/kernel/expression_operators/presence.rs +52 -13
  388. data/rust/src/kernel/expression_operators/sign_test.rs +3 -1
  389. data/rust/src/kernel/expression_operators/sized.rs +24 -8
  390. data/rust/src/kernel/expression_operators/text.rs +120 -27
  391. data/rust/src/kernel/expression_operators/to_string.rs +10 -4
  392. data/rust/src/kernel/json.rs +244 -148
  393. data/rust/src/kernel/mod.rs +47 -33
  394. data/rust/src/kernel/named_query.rs +63 -15
  395. data/rust/src/kernel/needs.rs +112 -25
  396. data/rust/src/kernel/orchestrate.rs +980 -581
  397. data/rust/src/kernel/orchestrate_guard_tests.rs +538 -140
  398. data/rust/src/kernel/pattern.rs +107 -34
  399. data/rust/src/kernel/present.rs +261 -0
  400. data/rust/src/kernel/query_comparators.rs +118 -41
  401. data/rust/src/kernel/query_ordering.rs +13 -5
  402. data/rust/src/kernel/read_model.rs +479 -181
  403. data/rust/src/kernel/reference_lookup.rs +122 -26
  404. data/rust/src/kernel/repository.rs +293 -66
  405. data/rust/src/kernel/routing.rs +58 -42
  406. data/rust/src/kernel/ruby_float.rs +139 -0
  407. data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +57 -57
  408. data/rust/src/kernel/vocab/entity_dispatch_order.rs +54 -54
  409. data/rust/src/kernel/vocab/field_hint.rs +19 -25
  410. data/rust/src/kernel/vocab/mod.rs +6 -6
  411. data/rust/src/kernel/vocab/query_comparator.rs +23 -24
  412. data/rust/src/kernel/vocab/refusal_template.rs +501 -297
  413. data/rust/src/lib.rs +50 -0
  414. data/rust/src/main.rs +4 -1
  415. data/rust/web/Cargo.toml +36 -0
  416. data/rust/web/clippy.toml +4 -0
  417. metadata +85 -15
  418. data/examples/pizzas/bluebook/hecksagon/mock_stripe_payment_adapter.rb +0 -36
  419. data/lib/hecks/adapters/driven/google_authentication.adapter +0 -3
  420. data/lib/hecks/adapters/driven/google_authentication.rb +0 -99
  421. data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +0 -3
  422. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +0 -32
  423. data/lib/hecks/ports/authentication.port +0 -6
  424. data/lib/hecks/ports/authentication.rb +0 -67
  425. data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +0 -255
  426. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +0 -22
  427. data/rust/host/src/approval.rs +0 -652
@@ -62,9 +62,17 @@ type Compiled = Mutex<Option<(Engine, Module)>>;
62
62
  /// leaves the cell empty for the next caller to retry.
63
63
  fn engine_and_module(wasm_path: &Path) -> anyhow::Result<(Engine, Module)> {
64
64
  let cache = ENGINE_AND_MODULE.get_or_init(|| Mutex::new(HashMap::new()));
65
- let cell = Arc::clone(cache.lock().unwrap().entry(wasm_path.to_path_buf()).or_default());
66
-
67
- let mut compiled = cell.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
65
+ let cell = Arc::clone(
66
+ cache
67
+ .lock()
68
+ .unwrap_or_else(std::sync::PoisonError::into_inner)
69
+ .entry(wasm_path.to_path_buf())
70
+ .or_default(),
71
+ );
72
+
73
+ let mut compiled = cell
74
+ .lock()
75
+ .unwrap_or_else(std::sync::PoisonError::into_inner);
68
76
  if let Some((engine, module)) = compiled.as_ref() {
69
77
  return Ok((engine.clone(), module.clone()));
70
78
  }
@@ -84,7 +92,12 @@ static COMPILED_PATHS: Mutex<Vec<PathBuf>> = Mutex::new(Vec::new());
84
92
  /// How many times `wasm_path` has been compiled in this process.
85
93
  #[cfg(test)]
86
94
  pub(crate) fn compile_count(wasm_path: &Path) -> usize {
87
- COMPILED_PATHS.lock().unwrap().iter().filter(|compiled| compiled.as_path() == wasm_path).count()
95
+ COMPILED_PATHS
96
+ .lock()
97
+ .unwrap()
98
+ .iter()
99
+ .filter(|compiled| compiled.as_path() == wasm_path)
100
+ .count()
88
101
  }
89
102
 
90
103
  /// Compiles `wasm_path` at boot, before the host starts listening, so
@@ -139,8 +152,10 @@ pub(crate) mod tests {
139
152
  /// A copy of the checkout fixture wasm at a path no other test uses, so
140
153
  /// its first call is a real cold compile whatever order tests run in.
141
154
  pub(crate) fn cold_copy_of_fixture(name: &str) -> PathBuf {
142
- let source = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../dist/checkout_fixture.wasm");
143
- let dir = std::env::temp_dir().join(format!("hecks_wasm_runner_{}_{name}", std::process::id()));
155
+ let source =
156
+ PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../dist/checkout_fixture.wasm");
157
+ let dir =
158
+ std::env::temp_dir().join(format!("hecks_wasm_runner_{}_{name}", std::process::id()));
144
159
  std::fs::create_dir_all(&dir).unwrap();
145
160
  let copy = dir.join("checkout_fixture.wasm");
146
161
  std::fs::copy(&source, &copy).expect("hecks build_wasm writes checkout_fixture.wasm");
@@ -3,7 +3,8 @@
3
3
  // named in `render` and `route`.
4
4
 
5
5
  use super::{
6
- checkout_enabled, newsletter, newsletter_send, payments_routes, registrations_list_route, resend_webhook, respond, session_secret,
6
+ checkout_enabled, newsletter, newsletter_send, payments_routes, registrations_list_route,
7
+ resend_webhook, respond, session_secret,
7
8
  };
8
9
  use crate::extension::{Ctx, HostExtension, RateRule};
9
10
  use crate::payments;
@@ -32,22 +33,45 @@ impl HostExtension for Commerce {
32
33
  // is checked first since it needs none of the Payments::Payment/Event context checkout
33
34
  // requires. The subscriber list is PII (email, names, status): only an Admin or Owner
34
35
  // holding the account cookie may read it, like sending does.
35
- if ctx.method == "GET" && ctx.path == "/newsletter/subscribers" && ir.and_then(crate::commerce_ir::newsletter_provider).is_some() {
36
+ if ctx.method == "GET"
37
+ && ctx.path == "/newsletter/subscribers"
38
+ && ir
39
+ .and_then(crate::commerce_ir::newsletter_provider)
40
+ .is_some()
41
+ {
36
42
  let Some(domain_ir) = ir else {
37
- return Some(respond(500, "text/plain", "HECKS_IR_PATH not set or unreadable — this domain has no web layer configured"));
43
+ return Some(respond(
44
+ 500,
45
+ "text/plain",
46
+ "HECKS_IR_PATH not set or unreadable — this domain has no web layer configured",
47
+ ));
38
48
  };
39
- if let Err(response) = newsletter_send::require_admin(domain_ir, ctx.cookies, &session_secret(), ctx.client).await {
49
+ if let Err(response) = newsletter_send::require_admin(
50
+ domain_ir,
51
+ ctx.cookies,
52
+ &session_secret(),
53
+ ctx.client,
54
+ )
55
+ .await
56
+ {
40
57
  return Some(response);
41
58
  }
42
59
  }
43
60
  let headers = ctx.event.and_then(|e| e.get("headers"));
44
- if let Some(response) =
45
- resend_webhook::resend_webhook_route(ctx.method, ctx.path, ctx.raw_body, headers, ctx.client, ctx.wasm_path, ctx.config, ctx.invoker).await
61
+ if let Some(response) = resend_webhook::resend_webhook_route(
62
+ ctx.method,
63
+ ctx.path,
64
+ ctx.raw_body,
65
+ headers,
66
+ ctx.host(),
67
+ )
68
+ .await
46
69
  {
47
70
  return Some(response);
48
71
  }
49
72
  if let Some(response) =
50
- newsletter::newsletter_route(ctx.method, ctx.path, ctx.query, ctx.raw_body, ctx.client, ctx.wasm_path, ctx.config, ctx.invoker).await
73
+ newsletter::newsletter_route(ctx.method, ctx.path, ctx.query, ctx.raw_body, ctx.host())
74
+ .await
51
75
  {
52
76
  return Some(response);
53
77
  }
@@ -57,7 +81,15 @@ impl HostExtension for Commerce {
57
81
  .and_then(|h| h.get("stripe-signature"))
58
82
  .and_then(|v| v.as_str())
59
83
  .unwrap_or("");
60
- payments_routes(ir, ctx.method, ctx.path, ctx.raw_body, stripe_signature, ctx.client, ctx.wasm_path, ctx.config, ctx.invoker).await
84
+ payments_routes(
85
+ ir,
86
+ ctx.method,
87
+ ctx.path,
88
+ ctx.raw_body,
89
+ stripe_signature,
90
+ ctx.host(),
91
+ )
92
+ .await
61
93
  }
62
94
 
63
95
  async fn account_route(&self, ctx: &Ctx<'_>) -> Option<Value> {
@@ -67,20 +99,44 @@ impl HostExtension for Commerce {
67
99
  // account cookie, unlike the guest newsletter routes served ahead of the account gate.
68
100
  if newsletter_send::issue_action(ctx.method, ctx.path).is_some() {
69
101
  return newsletter_send::issue_route(
70
- ctx.method, ctx.path, domain_ir, ctx.raw_body, ctx.cookies, &secret, ctx.client, ctx.wasm_path, ctx.config, ctx.invoker,
102
+ ctx.method,
103
+ ctx.path,
104
+ domain_ir,
105
+ ctx.raw_body,
106
+ ctx.cookies,
107
+ &secret,
108
+ ctx.host(),
71
109
  )
72
110
  .await;
73
111
  }
74
112
  if ctx.method == "GET" && ctx.path == "/registrations" {
75
- return Some(registrations_list_route(domain_ir, ctx.cookies, &secret, ctx.client, ctx.wasm_path, ctx.config).await);
113
+ return Some(
114
+ registrations_list_route(
115
+ domain_ir,
116
+ ctx.cookies,
117
+ &secret,
118
+ ctx.client,
119
+ ctx.wasm_path,
120
+ ctx.config,
121
+ )
122
+ .await,
123
+ );
76
124
  }
77
125
  // The tenant's payment connection: same gate as the checkout routes, since only the
78
126
  // HECKS_CHECKOUT_DOMAIN domain carries one — any other domain served by this binary falls
79
127
  // through as unknown.
80
- if payments::owns(ctx.method, ctx.path) && checkout_enabled(checkout_domain().as_deref(), &ctx.config.domain) {
128
+ if payments::owns(ctx.method, ctx.path)
129
+ && checkout_enabled(checkout_domain().as_deref(), &ctx.config.domain)
130
+ {
81
131
  return payments::route(
82
- ctx.method, ctx.path, ctx.raw_body, ctx.cookies, &secret, domain_ir, &payments::PlatformConfig::load().await,
83
- ctx.client, ctx.wasm_path, ctx.config, ctx.invoker,
132
+ ctx.method,
133
+ ctx.path,
134
+ ctx.raw_body,
135
+ ctx.cookies,
136
+ &secret,
137
+ domain_ir,
138
+ &payments::PlatformConfig::load().await,
139
+ ctx.host(),
84
140
  )
85
141
  .await;
86
142
  }
@@ -89,8 +145,20 @@ impl HostExtension for Commerce {
89
145
 
90
146
  fn rate_rules(&self) -> Vec<RateRule> {
91
147
  vec![
92
- RateRule { name: "subscribe", method: "POST", path: "/newsletter/subscribers", limit_env: "HECKS_RATE_LIMIT_SUBSCRIBE", default_limit: 10 },
93
- RateRule { name: "register", method: "POST", path: "/registrations", limit_env: "HECKS_RATE_LIMIT_REGISTER", default_limit: 15 },
148
+ RateRule {
149
+ name: "subscribe",
150
+ method: "POST",
151
+ path: "/newsletter/subscribers",
152
+ limit_env: "HECKS_RATE_LIMIT_SUBSCRIBE",
153
+ default_limit: 10,
154
+ },
155
+ RateRule {
156
+ name: "register",
157
+ method: "POST",
158
+ path: "/registrations",
159
+ limit_env: "HECKS_RATE_LIMIT_REGISTER",
160
+ default_limit: 15,
161
+ },
94
162
  ]
95
163
  }
96
164
 
@@ -105,17 +173,27 @@ impl HostExtension for Commerce {
105
173
  // secret may also carry `webhook_secret`, the signing secret of the Resend webhook that
106
174
  // POST /webhooks/resend verifies; without it that route answers 503.
107
175
  async fn boot_secrets(&self, fetcher: &AwsSecretFetcher) {
108
- let Ok(resend_secret_id) = std::env::var("RESEND_SECRET_ID") else { return };
109
- let fetched = fetcher.fetch_secret_string(&resend_secret_id).await.map_err(|e| format!("{e:#}"));
110
- let read = fetched.and_then(|json| secrets::extract_field(&json, "api_key").map(|api_key| (api_key, secrets::optional_field(&json, "webhook_secret"))));
176
+ let Ok(resend_secret_id) = std::env::var("RESEND_SECRET_ID") else {
177
+ return;
178
+ };
179
+ let fetched = fetcher
180
+ .fetch_secret_string(&resend_secret_id)
181
+ .await
182
+ .map_err(|e| format!("{e:#}"));
183
+ let read = fetched.and_then(|json| {
184
+ secrets::extract_field(&json, "api_key")
185
+ .map(|api_key| (api_key, secrets::optional_field(&json, "webhook_secret")))
186
+ });
111
187
  match read {
112
- Ok((api_key, webhook_secret)) => unsafe {
113
- std::env::set_var("RESEND_API_KEY", api_key);
188
+ Ok((api_key, webhook_secret)) => {
189
+ secrets::provide("RESEND_API_KEY", api_key);
114
190
  if let Some(webhook_secret) = webhook_secret {
115
- std::env::set_var("RESEND_WEBHOOK_SECRET", webhook_secret);
191
+ secrets::provide("RESEND_WEBHOOK_SECRET", webhook_secret);
116
192
  }
117
- },
118
- Err(e) => eprintln!("RESEND_SECRET_ID could not be read, so email sending stays off: {e}"),
193
+ }
194
+ Err(e) => {
195
+ eprintln!("RESEND_SECRET_ID could not be read, so email sending stays off: {e}");
196
+ }
119
197
  }
120
198
  }
121
199
  }
@@ -3,7 +3,7 @@ use std::sync::Mutex;
3
3
  use std::time::{Duration, Instant};
4
4
 
5
5
  /// How long an address is left alone after a confirmation email went to it.
6
- pub(super) const WINDOW: Duration = Duration::from_secs(10 * 60);
6
+ pub(super) const WINDOW: Duration = Duration::from_mins(10);
7
7
 
8
8
  /// The most addresses remembered; past it a new address is refused, not evicting a live one.
9
9
  pub(super) const CAPACITY: usize = 10_000;
@@ -19,7 +19,11 @@ pub(super) struct ConfirmationCooldown {
19
19
 
20
20
  impl ConfirmationCooldown {
21
21
  pub(super) fn new(window: Duration, capacity: usize) -> Self {
22
- Self { window, capacity, last_sent: Mutex::new(HashMap::new()) }
22
+ Self {
23
+ window,
24
+ capacity,
25
+ last_sent: Mutex::new(HashMap::new()),
26
+ }
23
27
  }
24
28
 
25
29
  /// Whether an email may go to `email` now (keyed by lowercased address). A `true` records the
@@ -31,7 +35,10 @@ impl ConfirmationCooldown {
31
35
  /// `try_acquire` with the clock supplied, so tests need not sleep.
32
36
  pub(super) fn try_acquire_at(&self, email: &str, now: Instant) -> bool {
33
37
  let key = email.to_lowercase();
34
- let mut last_sent = self.last_sent.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
38
+ let mut last_sent = self
39
+ .last_sent
40
+ .lock()
41
+ .unwrap_or_else(std::sync::PoisonError::into_inner);
35
42
  let window = self.window;
36
43
  last_sent.retain(|_, sent| now.saturating_duration_since(*sent) < window);
37
44
  if last_sent.contains_key(&key) || last_sent.len() >= self.capacity {
@@ -10,19 +10,30 @@ pub(super) const MAX_TEMPLATE_BYTES: usize = 256 * 1024;
10
10
 
11
11
  // Escapes a URL for an HTML attribute or text position.
12
12
  fn escape_html(value: &str) -> String {
13
- value.replace('&', "&amp;").replace('<', "&lt;").replace('>', "&gt;").replace('"', "&quot;").replace('\'', "&#39;")
13
+ value
14
+ .replace('&', "&amp;")
15
+ .replace('<', "&lt;")
16
+ .replace('>', "&gt;")
17
+ .replace('"', "&quot;")
18
+ .replace('\'', "&#39;")
14
19
  }
15
20
 
16
21
  // Fills the placeholders with HTML-escaped URLs. `Err` names why the
17
22
  // template is unusable: without {{CONFIRM_URL}} the email would carry no
18
23
  // link. {{UNSUBSCRIBE_URL}} is optional. The result starts at its first
19
24
  // non-blank character so the mailer sends it as HTML.
20
- pub(super) fn fill_template(template: &str, confirm_url: &str, unsubscribe_url: &str) -> Result<String, &'static str> {
25
+ pub(super) fn fill_template(
26
+ template: &str,
27
+ confirm_url: &str,
28
+ unsubscribe_url: &str,
29
+ ) -> Result<String, &'static str> {
21
30
  if !template.contains(CONFIRM_TOKEN) {
22
31
  return Err("the template has no {{CONFIRM_URL}} placeholder");
23
32
  }
24
33
  let filled = template.replace(CONFIRM_TOKEN, &escape_html(confirm_url));
25
- Ok(personalize(&filled, &escape_html(unsubscribe_url)).trim_start().to_string())
34
+ Ok(personalize(&filled, &escape_html(unsubscribe_url))
35
+ .trim_start()
36
+ .to_string())
26
37
  }
27
38
 
28
39
  // Reads a response body, stopping as soon as it passes the cap.
@@ -31,7 +42,9 @@ async fn read_capped(mut response: reqwest::Response) -> Result<String, String>
31
42
  while let Some(chunk) = response.chunk().await.map_err(|e| e.to_string())? {
32
43
  bytes.extend_from_slice(&chunk);
33
44
  if bytes.len() > MAX_TEMPLATE_BYTES {
34
- return Err(format!("the template is larger than {MAX_TEMPLATE_BYTES} bytes"));
45
+ return Err(format!(
46
+ "the template is larger than {MAX_TEMPLATE_BYTES} bytes"
47
+ ));
35
48
  }
36
49
  }
37
50
  String::from_utf8(bytes).map_err(|_| "the template is not UTF-8".to_string())
@@ -40,8 +53,12 @@ async fn read_capped(mut response: reqwest::Response) -> Result<String, String>
40
53
  // GETs the template (5 second timeout, size capped); a non-2xx answer or
41
54
  // any transport trouble is an `Err` with the reason.
42
55
  async fn fetch_template(url: &str) -> Result<String, String> {
43
- let http = reqwest::Client::builder().timeout(FETCH_TIMEOUT).build().map_err(|e| e.to_string())?;
44
- let response = http.get(url).send().await.map_err(|e| e.to_string())?;
56
+ let response = crate::http::client()
57
+ .get(url)
58
+ .timeout(FETCH_TIMEOUT)
59
+ .send()
60
+ .await
61
+ .map_err(|e| e.to_string())?;
45
62
  if !response.status().is_success() {
46
63
  return Err(format!("the template answered {}", response.status()));
47
64
  }
@@ -51,7 +68,9 @@ async fn fetch_template(url: &str) -> Result<String, String> {
51
68
  // The HTML body when NEWSLETTER_CONFIRMATION_TEMPLATE_URL names a usable
52
69
  // template; None (after logging why) when the caller should send plain text.
53
70
  pub(super) async fn branded_body(confirm_url: &str, unsubscribe_url: &str) -> Option<String> {
54
- let url = std::env::var("NEWSLETTER_CONFIRMATION_TEMPLATE_URL").ok().filter(|u| !u.trim().is_empty())?;
71
+ let url = std::env::var("NEWSLETTER_CONFIRMATION_TEMPLATE_URL")
72
+ .ok()
73
+ .filter(|u| !u.trim().is_empty())?;
55
74
  let template = match fetch_template(url.trim()).await {
56
75
  Ok(t) => t,
57
76
  Err(e) => {
@@ -3,58 +3,105 @@ use super::*;
3
3
  #[test]
4
4
  fn a_confirm_token_verifies_only_for_the_address_it_was_minted_for() {
5
5
  let token = confirm_token("s3cret-value", "a@example.com");
6
- assert!(confirm_token_matches("s3cret-value", &token, "a@example.com"));
7
- assert!(!confirm_token_matches("s3cret-value", &token, "b@example.com"));
6
+ assert!(confirm_token_matches(
7
+ "s3cret-value",
8
+ &token,
9
+ "a@example.com"
10
+ ));
11
+ assert!(!confirm_token_matches(
12
+ "s3cret-value",
13
+ &token,
14
+ "b@example.com"
15
+ ));
8
16
  }
9
17
 
10
18
  #[test]
11
19
  fn a_confirm_token_signed_with_another_secret_is_refused() {
12
20
  let token = confirm_token("one-secret", "a@example.com");
13
- assert!(!confirm_token_matches("another-secret", &token, "a@example.com"));
21
+ assert!(!confirm_token_matches(
22
+ "another-secret",
23
+ &token,
24
+ "a@example.com"
25
+ ));
14
26
  }
15
27
 
16
28
  #[test]
17
29
  fn a_token_minted_for_another_purpose_is_not_a_confirm_token() {
18
- let other = auth::purpose_token("s3cret-value", "account-sso", json!({ "email": "a@example.com" }), 60);
19
- assert!(!confirm_token_matches("s3cret-value", &other, "a@example.com"));
30
+ let other = auth::purpose_token(
31
+ "s3cret-value",
32
+ "account-sso",
33
+ json!({ "email": "a@example.com" }),
34
+ 60,
35
+ );
36
+ assert!(!confirm_token_matches(
37
+ "s3cret-value",
38
+ &other,
39
+ "a@example.com"
40
+ ));
20
41
  }
21
42
 
22
43
  #[test]
23
44
  fn an_expired_or_garbled_token_is_refused() {
24
- let expired = auth::purpose_token("s3cret-value", CONFIRM_PURPOSE, json!({ "email": "a@example.com" }), 0);
45
+ let expired = auth::purpose_token(
46
+ "s3cret-value",
47
+ CONFIRM_PURPOSE,
48
+ json!({ "email": "a@example.com" }),
49
+ 0,
50
+ );
25
51
  std::thread::sleep(std::time::Duration::from_millis(1100));
26
- assert!(!confirm_token_matches("s3cret-value", &expired, "a@example.com"));
27
- assert!(!confirm_token_matches("s3cret-value", "not-a-token", "a@example.com"));
52
+ assert!(!confirm_token_matches(
53
+ "s3cret-value",
54
+ &expired,
55
+ "a@example.com"
56
+ ));
57
+ assert!(!confirm_token_matches(
58
+ "s3cret-value",
59
+ "not-a-token",
60
+ "a@example.com"
61
+ ));
28
62
  assert!(!confirm_token_matches("s3cret-value", "", "a@example.com"));
29
63
  }
30
64
 
31
65
  #[test]
32
66
  fn the_confirm_url_carries_the_encoded_address_and_the_token() {
33
67
  let url = confirm_url("https://example.com", "a+b@example.com", "tok.en");
34
- assert_eq!(url, "https://example.com/newsletter-confirmed.html?email=a%2Bb%40example.com&token=tok.en");
68
+ assert_eq!(
69
+ url,
70
+ "https://example.com/newsletter-confirmed.html?email=a%2Bb%40example.com&token=tok.en"
71
+ );
35
72
  }
36
73
 
37
74
  #[test]
38
75
  fn the_confirmation_email_holds_the_link_and_says_it_can_be_ignored() {
39
- let body = confirmation_body("https://example.com/newsletter-confirmed.html?email=a%40b.c&token=t");
76
+ let body =
77
+ confirmation_body("https://example.com/newsletter-confirmed.html?email=a%40b.c&token=t");
40
78
  assert!(body.contains("https://example.com/newsletter-confirmed.html?email=a%40b.c&token=t"));
41
79
  assert!(body.contains("ignore this email"));
42
80
  }
43
81
 
44
82
  fn unsubscribe_query(pairs: &[(&str, &str)]) -> HashMap<String, String> {
45
- pairs.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect()
83
+ pairs
84
+ .iter()
85
+ .map(|(k, v)| (k.to_string(), v.to_string()))
86
+ .collect()
46
87
  }
47
88
 
48
89
  fn status_and_error(response: &Value) -> (u64, String) {
49
90
  let body: Value = serde_json::from_str(response["body"].as_str().unwrap()).unwrap();
50
- (response["statusCode"].as_u64().unwrap(), body["error"].as_str().unwrap_or("").to_string())
91
+ (
92
+ response["statusCode"].as_u64().unwrap(),
93
+ body["error"].as_str().unwrap_or("").to_string(),
94
+ )
51
95
  }
52
96
 
53
97
  #[test]
54
98
  fn an_unsubscribe_request_with_a_token_for_its_own_address_is_authorized() {
55
99
  let token = unsubscribe_token("s3cret-value", "a@example.com");
56
100
  let query = unsubscribe_query(&[("email", "a@example.com"), ("token", &token)]);
57
- assert_eq!(unsubscribe_authorized(Some("s3cret-value"), &query).unwrap(), "a@example.com");
101
+ assert_eq!(
102
+ unsubscribe_authorized(Some("s3cret-value"), &query).unwrap(),
103
+ "a@example.com"
104
+ );
58
105
  }
59
106
 
60
107
  #[test]
@@ -63,7 +110,12 @@ fn an_unsubscribe_request_without_a_valid_token_is_a_403() {
63
110
  let own = unsubscribe_token("s3cret-value", "a@example.com");
64
111
  let other = unsubscribe_token("s3cret-value", "b@example.com");
65
112
  let confirm = confirm_token("s3cret-value", "a@example.com");
66
- let expired = auth::purpose_token("s3cret-value", "newsletter-unsubscribe", json!({ "email": "a@example.com" }), 0);
113
+ let expired = auth::purpose_token(
114
+ "s3cret-value",
115
+ "newsletter-unsubscribe",
116
+ json!({ "email": "a@example.com" }),
117
+ 0,
118
+ );
67
119
  std::thread::sleep(std::time::Duration::from_millis(1100));
68
120
  let cases = [
69
121
  unsubscribe_query(&[("email", "a@example.com")]),
@@ -76,7 +128,11 @@ fn an_unsubscribe_request_without_a_valid_token_is_a_403() {
76
128
  ];
77
129
  for query in &cases {
78
130
  let err = unsubscribe_authorized(Some("s3cret-value"), query).unwrap_err();
79
- assert_eq!(status_and_error(&err), (403, refusal.to_string()), "{query:?}");
131
+ assert_eq!(
132
+ status_and_error(&err),
133
+ (403, refusal.to_string()),
134
+ "{query:?}"
135
+ );
80
136
  assert_eq!(err["body"], json!({ "error": refusal }).to_string());
81
137
  }
82
138
  }
@@ -85,13 +141,20 @@ fn an_unsubscribe_request_without_a_valid_token_is_a_403() {
85
141
  fn an_unsubscribe_request_is_refused_when_there_is_no_secret_to_check_against() {
86
142
  let token = unsubscribe_token("s3cret-value", "a@example.com");
87
143
  let query = unsubscribe_query(&[("email", "a@example.com"), ("token", &token)]);
88
- assert_eq!(unsubscribe_authorized(None, &query).unwrap_err()["statusCode"], 403);
144
+ assert_eq!(
145
+ unsubscribe_authorized(None, &query).unwrap_err()["statusCode"],
146
+ 403
147
+ );
89
148
  }
90
149
 
91
150
  #[test]
92
151
  fn an_unsubscribe_request_without_an_address_is_a_400() {
93
152
  let token = unsubscribe_token("s3cret-value", "a@example.com");
94
- let err = unsubscribe_authorized(Some("s3cret-value"), &unsubscribe_query(&[("token", &token)])).unwrap_err();
153
+ let err = unsubscribe_authorized(
154
+ Some("s3cret-value"),
155
+ &unsubscribe_query(&[("token", &token)]),
156
+ )
157
+ .unwrap_err();
95
158
  assert_eq!(err["statusCode"], 400);
96
159
  }
97
160
 
@@ -99,8 +162,16 @@ fn an_unsubscribe_request_without_an_address_is_a_400() {
99
162
  fn a_confirm_token_is_not_an_unsubscribe_token_and_the_reverse() {
100
163
  let confirm = confirm_token("s3cret-value", "a@example.com");
101
164
  let unsubscribe = unsubscribe_token("s3cret-value", "a@example.com");
102
- assert!(!unsubscribe_token_matches("s3cret-value", &confirm, "a@example.com"));
103
- assert!(!confirm_token_matches("s3cret-value", &unsubscribe, "a@example.com"));
165
+ assert!(!unsubscribe_token_matches(
166
+ "s3cret-value",
167
+ &confirm,
168
+ "a@example.com"
169
+ ));
170
+ assert!(!confirm_token_matches(
171
+ "s3cret-value",
172
+ &unsubscribe,
173
+ "a@example.com"
174
+ ));
104
175
  }
105
176
 
106
177
  use template::fill_template;
@@ -110,7 +181,12 @@ const UNSUBSCRIBE: &str = "https://example.com/newsletter-unsubscribed.html?emai
110
181
 
111
182
  #[test]
112
183
  fn a_template_gets_the_confirm_link_in_place_of_its_placeholder() {
113
- let body = fill_template("<p><a href=\"{{CONFIRM_URL}}\">Confirm</a></p>", CONFIRM, UNSUBSCRIBE).unwrap();
184
+ let body = fill_template(
185
+ "<p><a href=\"{{CONFIRM_URL}}\">Confirm</a></p>",
186
+ CONFIRM,
187
+ UNSUBSCRIBE,
188
+ )
189
+ .unwrap();
114
190
  assert_eq!(body, "<p><a href=\"https://example.com/newsletter-confirmed.html?email=a%40b.c&amp;token=t\">Confirm</a></p>");
115
191
  }
116
192
 
@@ -123,15 +199,28 @@ fn a_template_without_the_confirm_placeholder_is_rejected() {
123
199
  #[test]
124
200
  fn the_unsubscribe_placeholder_is_optional_and_replaced_wherever_present() {
125
201
  assert!(fill_template("<a href=\"{{CONFIRM_URL}}\">x</a>", CONFIRM, UNSUBSCRIBE).is_ok());
126
- let body = fill_template("<a href=\"{{CONFIRM_URL}}\">x</a>{{UNSUBSCRIBE_URL}}|{{UNSUBSCRIBE_URL}}", CONFIRM, UNSUBSCRIBE).unwrap();
202
+ let body = fill_template(
203
+ "<a href=\"{{CONFIRM_URL}}\">x</a>{{UNSUBSCRIBE_URL}}|{{UNSUBSCRIBE_URL}}",
204
+ CONFIRM,
205
+ UNSUBSCRIBE,
206
+ )
207
+ .unwrap();
127
208
  assert!(!body.contains("{{"));
128
209
  assert_eq!(body.matches("newsletter-unsubscribed.html").count(), 2);
129
210
  }
130
211
 
131
212
  #[test]
132
213
  fn ampersands_and_quotes_in_a_url_are_escaped_inside_the_html() {
133
- let body = fill_template("<a href=\"{{CONFIRM_URL}}\">x</a>", "https://e.com/?a=1&b=\"2\"<>'", UNSUBSCRIBE).unwrap();
134
- assert_eq!(body, "<a href=\"https://e.com/?a=1&amp;b=&quot;2&quot;&lt;&gt;&#39;\">x</a>");
214
+ let body = fill_template(
215
+ "<a href=\"{{CONFIRM_URL}}\">x</a>",
216
+ "https://e.com/?a=1&b=\"2\"<>'",
217
+ UNSUBSCRIBE,
218
+ )
219
+ .unwrap();
220
+ assert_eq!(
221
+ body,
222
+ "<a href=\"https://e.com/?a=1&amp;b=&quot;2&quot;&lt;&gt;&#39;\">x</a>"
223
+ );
135
224
  }
136
225
 
137
226
  #[test]