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,13 +1,17 @@
1
- use super::{active_session_email, instances_for, last_refusal, percent_decode, respond};
1
+ use super::{
2
+ active_session_email, instances_for, json_error, last_refusal, percent_decode, respond,
3
+ site_url,
4
+ };
2
5
  use crate::auth;
6
+ use crate::clock::unix_now;
7
+ use crate::commerce_ir::{
8
+ newsletter_issues_provider, newsletter_provider, NewsletterIssuesProvider, NewsletterProvider,
9
+ };
3
10
  use crate::dispatch;
4
- use crate::commerce_ir::{newsletter_issues_provider, newsletter_provider, NewsletterIssuesProvider, NewsletterProvider};
5
- use crate::journal::LineageConfig;
6
- use crate::lambda_client::LambdaInvoker;
11
+ use crate::host::Host;
7
12
  use crate::resend::{Email, Mailer};
8
13
  use serde_json::{json, Value};
9
14
  use std::collections::HashMap;
10
- use std::path::Path;
11
15
  use tokio::sync::Mutex;
12
16
  use tokio_postgres::Client;
13
17
 
@@ -15,7 +19,6 @@ use tokio_postgres::Client;
15
19
  // Admin/Owner. `newsletter_issues` marks issues sent; `newsletter` holds subscribers.
16
20
 
17
21
  const UNSUBSCRIBE_TOKEN: &str = "{{UNSUBSCRIBE_URL}}";
18
- const DEFAULT_SITE_URL: &str = "http://localhost:4321";
19
22
 
20
23
  // The purpose keys an unsubscribe token apart from a confirm token, so neither
21
24
  // verifies as the other. The lifetime is the Newsletter bluebook's
@@ -49,7 +52,6 @@ pub(super) fn issue_action(method: &str, path: &str) -> Option<(String, IssueAct
49
52
 
50
53
  // `None` when the path doesn't match, or this domain declares neither
51
54
  // `newsletter_issues` nor `newsletter`.
52
- #[allow(clippy::too_many_arguments)]
53
55
  pub(super) async fn issue_route(
54
56
  method: &str,
55
57
  path: &str,
@@ -57,11 +59,9 @@ pub(super) async fn issue_route(
57
59
  raw_body: &str,
58
60
  cookies: &HashMap<String, String>,
59
61
  secret: &str,
60
- client: &Mutex<Client>,
61
- wasm_path: &Path,
62
- config: &LineageConfig,
63
- invoker: &dyn LambdaInvoker,
62
+ host: Host<'_>,
64
63
  ) -> Option<Value> {
64
+ let Host { client, .. } = host;
65
65
  let (slug, action) = issue_action(method, path)?;
66
66
  let issues = newsletter_issues_provider(domain_ir)?;
67
67
  let subscribers = newsletter_provider(domain_ir)?;
@@ -71,26 +71,30 @@ pub(super) async fn issue_route(
71
71
  Err(response) => return Some(response),
72
72
  };
73
73
  Some(match action {
74
- IssueAction::Send => send_issue(&slug, &issues, &subscribers, &mailer, secret, client, wasm_path, config, invoker).await,
74
+ IssueAction::Send => send_issue(&slug, &issues, &subscribers, &mailer, secret, host).await,
75
75
  IssueAction::SendTest => send_test(raw_body, &mailer, secret).await,
76
76
  })
77
77
  }
78
78
 
79
- fn json_error(status: u16, message: &str) -> Value {
80
- respond(status, "application/json", &json!({ "error": message }).to_string())
81
- }
82
-
83
79
  // Every email carries a signed unsubscribe link, so nothing may go out (or
84
80
  // be marked sent) without a secret to sign it with.
85
81
  fn require_signing_secret(secret: &str) -> Result<(), Value> {
86
82
  if secret.is_empty() {
87
- return Err(json_error(503, "unsubscribe links cannot be signed: set SESSION_SECRET"));
83
+ return Err(json_error(
84
+ 503,
85
+ "unsubscribe links cannot be signed: set SESSION_SECRET",
86
+ ));
88
87
  }
89
88
  Ok(())
90
89
  }
91
90
 
92
91
  // An active account cookie whose person holds Admin or Owner.
93
- pub(super) async fn require_admin(domain_ir: &Value, cookies: &HashMap<String, String>, secret: &str, client: &Mutex<Client>) -> Result<(), Value> {
92
+ pub(super) async fn require_admin(
93
+ domain_ir: &Value,
94
+ cookies: &HashMap<String, String>,
95
+ secret: &str,
96
+ client: &Mutex<Client>,
97
+ ) -> Result<(), Value> {
94
98
  let email = active_session_email(domain_ir, cookies, secret, client).await?;
95
99
  match auth::caller_is_admin(client, domain_ir, &email).await {
96
100
  Ok(true) => Ok(()),
@@ -101,34 +105,55 @@ pub(super) async fn require_admin(domain_ir: &Value, cookies: &HashMap<String, S
101
105
 
102
106
  // Refuses (before anything is marked sent) when the caller isn't an
103
107
  // Admin/Owner, mail isn't configured, or there's no signing secret.
104
- async fn authorize(domain_ir: &Value, cookies: &HashMap<String, String>, secret: &str, client: &Mutex<Client>) -> Result<Mailer, Value> {
108
+ async fn authorize(
109
+ domain_ir: &Value,
110
+ cookies: &HashMap<String, String>,
111
+ secret: &str,
112
+ client: &Mutex<Client>,
113
+ ) -> Result<Mailer, Value> {
105
114
  require_admin(domain_ir, cookies, secret, client).await?;
106
115
  require_signing_secret(secret)?;
107
116
  match Mailer::from_env() {
108
117
  Ok(Some(mailer)) => Ok(mailer),
109
- Ok(None) => Err(json_error(503, "email delivery is not configured: set RESEND_API_KEY and RESEND_FROM")),
118
+ Ok(None) => Err(json_error(
119
+ 503,
120
+ "email delivery is not configured: set RESEND_API_KEY and RESEND_FROM",
121
+ )),
110
122
  Err(e) => Err(json_error(500, &e)),
111
123
  }
112
124
  }
113
125
 
114
- pub(super) fn site_url() -> String {
115
- std::env::var("SITE_URL").unwrap_or_else(|_| DEFAULT_SITE_URL.to_string())
116
- }
117
-
118
126
  pub(super) fn unsubscribe_token(secret: &str, email: &str) -> String {
119
- auth::purpose_token(secret, UNSUBSCRIBE_PURPOSE, json!({ "email": email }), crate::commerce_ir::newsletter_windows_binding().1)
127
+ auth::purpose_token(
128
+ secret,
129
+ UNSUBSCRIBE_PURPOSE,
130
+ json!({ "email": email }),
131
+ crate::commerce_ir::newsletter_windows_binding().1,
132
+ )
120
133
  }
121
134
 
122
135
  pub(super) fn unsubscribe_token_matches(secret: &str, token: &str, email: &str) -> bool {
123
136
  auth::verify_purpose_token(secret, UNSUBSCRIBE_PURPOSE, token)
124
- .and_then(|claims| claims.get("email").and_then(|v| v.as_str()).map(|signed| signed == email))
137
+ .and_then(|claims| {
138
+ claims
139
+ .get("email")
140
+ .and_then(|v| v.as_str())
141
+ .map(|signed| signed == email)
142
+ })
125
143
  .unwrap_or(false)
126
144
  }
127
145
 
128
146
  // Percent-encodes the address so a '+' in the local part survives the URL.
129
147
  pub(super) fn unsubscribe_url(site_url: &str, email: &str, token: &str) -> String {
130
- let mut url = reqwest::Url::parse(&format!("{site_url}/newsletter-unsubscribed.html")).unwrap_or_else(|_| reqwest::Url::parse("http://invalid.invalid/").unwrap());
131
- url.query_pairs_mut().append_pair("email", email).append_pair("token", token);
148
+ // An unparseable site URL degrades to a placeholder host rather than failing the send.
149
+ let Ok(mut url) = reqwest::Url::parse(&format!("{site_url}/newsletter-unsubscribed.html"))
150
+ .or_else(|_| reqwest::Url::parse("http://invalid.invalid/"))
151
+ else {
152
+ return String::new();
153
+ };
154
+ url.query_pairs_mut()
155
+ .append_pair("email", email)
156
+ .append_pair("token", token);
132
157
  url.to_string()
133
158
  }
134
159
 
@@ -138,7 +163,11 @@ pub(super) fn personalize(body: &str, unsubscribe_url: &str) -> String {
138
163
 
139
164
  // An aggregate field that is a one-attribute value object: `{"value": ...}`.
140
165
  fn value_of<'a>(state: &'a Value, field: &str) -> &'a str {
141
- state.get(field).and_then(|v| v.get("value")).and_then(|v| v.as_str()).unwrap_or("")
166
+ state
167
+ .get(field)
168
+ .and_then(|v| v.get("value"))
169
+ .and_then(|v| v.as_str())
170
+ .unwrap_or("")
142
171
  }
143
172
 
144
173
  // Every address in a state that receives issues (the lifecycle's `receives_issues` mark), alphabetical so a send is repeatable.
@@ -151,39 +180,53 @@ fn confirmed_emails(provider: &NewsletterProvider, subscribers: &[(String, Value
151
180
  emails.sort();
152
181
  emails
153
182
  }
154
-
155
- fn unix_now() -> i64 {
156
- std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs() as i64).unwrap_or(0)
157
- }
158
-
159
- #[allow(clippy::too_many_arguments)]
160
183
  async fn send_issue(
161
184
  slug: &str,
162
185
  issues: &NewsletterIssuesProvider,
163
186
  subscribers: &NewsletterProvider,
164
187
  mailer: &Mailer,
165
188
  secret: &str,
166
- client: &Mutex<Client>,
167
- wasm_path: &Path,
168
- config: &LineageConfig,
169
- invoker: &dyn LambdaInvoker,
189
+ host: Host<'_>,
170
190
  ) -> Value {
191
+ let Host {
192
+ client, wasm_path, ..
193
+ } = host;
171
194
  let read = match dispatch::read(client, wasm_path).await {
172
195
  Ok(r) => r,
173
196
  Err(e) => return respond(500, "text/plain", &format!("{e:#}")),
174
197
  };
175
- let Some((_, issue)) = instances_for(&read, &issues.issue_prefix()).into_iter().find(|(id, _)| id == slug) else {
198
+ let Some((_, issue)) = instances_for(&read, &issues.issue_prefix())
199
+ .into_iter()
200
+ .find(|(id, _)| id == slug)
201
+ else {
176
202
  return json_error(404, "no such issue");
177
203
  };
178
204
  let subject = value_of(&issue, "subject").to_string();
179
205
  let body = value_of(&issue, "body").to_string();
180
- let recipients = confirmed_emails(subscribers, &instances_for(&read, &subscribers.instance_prefix()));
206
+ let recipients = confirmed_emails(
207
+ subscribers,
208
+ &instances_for(&read, &subscribers.instance_prefix()),
209
+ );
181
210
 
182
211
  // Marked sent before mailing: a second click then refuses ("only a draft
183
212
  // issue can be sent") instead of mailing everyone twice.
184
- match dispatch::handle_routed(client, wasm_path, &issues.send_issue, json!(slug), json!({ "sent_at": { "value": unix_now() } }), None, config, invoker).await {
213
+ match dispatch::handle_routed(
214
+ host,
215
+ &issues.send_issue,
216
+ json!(slug),
217
+ json!({ "sent_at": { "value": unix_now() } }),
218
+ None,
219
+ )
220
+ .await
221
+ {
185
222
  Err(e) => return respond(500, "text/plain", &format!("{e:#}")),
186
- Ok(outcome) if !outcome.accepted => return respond(422, "application/json", &last_refusal(&outcome.result).to_string()),
223
+ Ok(outcome) if !outcome.accepted => {
224
+ return respond(
225
+ 422,
226
+ "application/json",
227
+ &last_refusal(&outcome.result).to_string(),
228
+ )
229
+ }
187
230
  Ok(_) => {}
188
231
  }
189
232
 
@@ -194,7 +237,14 @@ async fn send_issue(
194
237
  for email in &recipients {
195
238
  let unsubscribe = unsubscribe_url(&site, email, &unsubscribe_token(secret, email));
196
239
  let personalized = personalize(&body, &unsubscribe);
197
- let delivery = mailer.deliver(&Email { to: email, subject: &subject, body: &personalized, unsubscribe_url: Some(&unsubscribe) }).await;
240
+ let delivery = mailer
241
+ .deliver(&Email {
242
+ to: email,
243
+ subject: &subject,
244
+ body: &personalized,
245
+ unsubscribe_url: Some(&unsubscribe),
246
+ })
247
+ .await;
198
248
  if !delivery.ok {
199
249
  failures.push(json!({ "email": email, "reason": delivery.reason }));
200
250
  continue;
@@ -207,13 +257,19 @@ async fn send_issue(
207
257
  "subscriber_email": { "value": email },
208
258
  "message_id": { "value": delivery.message_id.unwrap_or_default() },
209
259
  });
210
- match dispatch::handle_facts(client, wasm_path, &issues.record_delivery, facts, None, config, invoker).await {
260
+ match dispatch::handle_facts(host, &issues.record_delivery, facts, None).await {
211
261
  Ok(outcome) if outcome.accepted => {}
212
262
  other => {
213
263
  // The email went out; only its record is missing. Surface it
214
264
  // in the response and the log rather than fail the whole send.
215
265
  unrecorded += 1;
216
- eprintln!("newsletter send {slug}: delivered to {email} but could not record it: {}", other.map(|o| last_refusal(&o.result).to_string()).unwrap_or_else(|e| format!("{e:#}")));
266
+ eprintln!(
267
+ "newsletter send {slug}: delivered to {email} but could not record it: {}",
268
+ other.map_or_else(
269
+ |e| format!("{e:#}"),
270
+ |o| last_refusal(&o.result).to_string()
271
+ )
272
+ );
217
273
  }
218
274
  }
219
275
  }
@@ -229,17 +285,29 @@ async fn send_test(raw_body: &str, mailer: &Mailer, secret: &str) -> Value {
229
285
  Err(e) => return json_error(400, &format!("invalid JSON: {e}")),
230
286
  };
231
287
  let field = |name: &str| body.get(name).and_then(|v| v.as_str());
232
- let (Some(to), Some(subject), Some(text)) = (field("to"), field("subject"), field("body")) else {
288
+ let (Some(to), Some(subject), Some(text)) = (field("to"), field("subject"), field("body"))
289
+ else {
233
290
  return json_error(400, "to, subject and body are required");
234
291
  };
235
292
 
236
293
  let unsubscribe = unsubscribe_url(&site_url(), to, &unsubscribe_token(secret, to));
237
294
  let subject = format!("[TEST] {subject}");
238
- let delivery = mailer.deliver(&Email { to, subject: &subject, body: &personalize(text, &unsubscribe), unsubscribe_url: Some(&unsubscribe) }).await;
295
+ let delivery = mailer
296
+ .deliver(&Email {
297
+ to,
298
+ subject: &subject,
299
+ body: &personalize(text, &unsubscribe),
300
+ unsubscribe_url: Some(&unsubscribe),
301
+ })
302
+ .await;
239
303
  if delivery.ok {
240
304
  respond(200, "application/json", &json!({ "to": to }).to_string())
241
305
  } else {
242
- respond(502, "application/json", &json!({ "error": "delivery failed", "reason": delivery.reason }).to_string())
306
+ respond(
307
+ 502,
308
+ "application/json",
309
+ &json!({ "error": "delivery failed", "reason": delivery.reason }).to_string(),
310
+ )
243
311
  }
244
312
  }
245
313
 
@@ -249,8 +317,14 @@ mod tests {
249
317
 
250
318
  #[test]
251
319
  fn send_and_send_test_are_the_only_issue_routes() {
252
- assert_eq!(issue_action("POST", "/newsletter/issues/spring/send"), Some(("spring".to_string(), IssueAction::Send)));
253
- assert_eq!(issue_action("POST", "/newsletter/issues/spring/send-test"), Some(("spring".to_string(), IssueAction::SendTest)));
320
+ assert_eq!(
321
+ issue_action("POST", "/newsletter/issues/spring/send"),
322
+ Some(("spring".to_string(), IssueAction::Send))
323
+ );
324
+ assert_eq!(
325
+ issue_action("POST", "/newsletter/issues/spring/send-test"),
326
+ Some(("spring".to_string(), IssueAction::SendTest))
327
+ );
254
328
  assert_eq!(issue_action("GET", "/newsletter/issues/spring/send"), None);
255
329
  assert_eq!(issue_action("POST", "/newsletter/issues/spring/edit"), None);
256
330
  assert_eq!(issue_action("POST", "/newsletter/issues//send"), None);
@@ -260,12 +334,20 @@ mod tests {
260
334
 
261
335
  #[test]
262
336
  fn the_slug_is_percent_decoded() {
263
- assert_eq!(issue_action("POST", "/newsletter/issues/spring%20news/send").unwrap().0, "spring news");
337
+ assert_eq!(
338
+ issue_action("POST", "/newsletter/issues/spring%20news/send")
339
+ .unwrap()
340
+ .0,
341
+ "spring news"
342
+ );
264
343
  }
265
344
 
266
345
  #[test]
267
346
  fn the_unsubscribe_token_is_replaced_wherever_it_appears() {
268
- let out = personalize("<a href=\"{{UNSUBSCRIBE_URL}}\">leave</a> {{UNSUBSCRIBE_URL}}", "https://x/u");
347
+ let out = personalize(
348
+ "<a href=\"{{UNSUBSCRIBE_URL}}\">leave</a> {{UNSUBSCRIBE_URL}}",
349
+ "https://x/u",
350
+ );
269
351
  assert_eq!(out, "<a href=\"https://x/u\">leave</a> https://x/u");
270
352
  }
271
353
 
@@ -279,42 +361,100 @@ mod tests {
279
361
 
280
362
  #[test]
281
363
  fn a_signed_unsubscribe_url_carries_a_token_that_verifies_for_its_address() {
282
- let url = reqwest::Url::parse(&unsubscribe_url("https://example.com", "a+b@example.com", &unsubscribe_token("s3cret-value", "a+b@example.com"))).unwrap();
283
- let query: HashMap<String, String> = url.query_pairs().map(|(k, v)| (k.to_string(), v.to_string())).collect();
364
+ let url = reqwest::Url::parse(&unsubscribe_url(
365
+ "https://example.com",
366
+ "a+b@example.com",
367
+ &unsubscribe_token("s3cret-value", "a+b@example.com"),
368
+ ))
369
+ .unwrap();
370
+ let query: HashMap<String, String> = url
371
+ .query_pairs()
372
+ .map(|(k, v)| (k.to_string(), v.to_string()))
373
+ .collect();
284
374
  assert_eq!(query["email"], "a+b@example.com");
285
- assert!(unsubscribe_token_matches("s3cret-value", &query["token"], &query["email"]));
375
+ assert!(unsubscribe_token_matches(
376
+ "s3cret-value",
377
+ &query["token"],
378
+ &query["email"]
379
+ ));
286
380
  }
287
381
 
288
382
  #[test]
289
383
  fn an_unsubscribe_token_verifies_only_for_the_address_it_was_minted_for() {
290
384
  let token = unsubscribe_token("s3cret-value", "a@example.com");
291
- assert!(unsubscribe_token_matches("s3cret-value", &token, "a@example.com"));
292
- assert!(!unsubscribe_token_matches("s3cret-value", &token, "b@example.com"));
293
- assert!(!unsubscribe_token_matches("another-secret", &token, "a@example.com"));
385
+ assert!(unsubscribe_token_matches(
386
+ "s3cret-value",
387
+ &token,
388
+ "a@example.com"
389
+ ));
390
+ assert!(!unsubscribe_token_matches(
391
+ "s3cret-value",
392
+ &token,
393
+ "b@example.com"
394
+ ));
395
+ assert!(!unsubscribe_token_matches(
396
+ "another-secret",
397
+ &token,
398
+ "a@example.com"
399
+ ));
294
400
  }
295
401
 
296
402
  #[test]
297
403
  fn an_unsubscribe_token_and_a_confirm_token_are_not_interchangeable() {
298
404
  let unsubscribe = unsubscribe_token("s3cret-value", "a@example.com");
299
- let confirm = auth::purpose_token("s3cret-value", "newsletter-confirm", json!({ "email": "a@example.com" }), 60);
300
- assert!(auth::verify_purpose_token("s3cret-value", "newsletter-confirm", &unsubscribe).is_none());
301
- assert!(!unsubscribe_token_matches("s3cret-value", &confirm, "a@example.com"));
405
+ let confirm = auth::purpose_token(
406
+ "s3cret-value",
407
+ "newsletter-confirm",
408
+ json!({ "email": "a@example.com" }),
409
+ 60,
410
+ );
411
+ assert!(
412
+ auth::verify_purpose_token("s3cret-value", "newsletter-confirm", &unsubscribe)
413
+ .is_none()
414
+ );
415
+ assert!(!unsubscribe_token_matches(
416
+ "s3cret-value",
417
+ &confirm,
418
+ "a@example.com"
419
+ ));
302
420
  }
303
421
 
304
422
  #[test]
305
423
  fn an_unsubscribe_token_outlives_the_confirm_window_by_two_years() {
306
- let claims = auth::verify_purpose_token("s3cret-value", UNSUBSCRIBE_PURPOSE, &unsubscribe_token("s3cret-value", "a@example.com")).unwrap();
424
+ let claims = auth::verify_purpose_token(
425
+ "s3cret-value",
426
+ UNSUBSCRIBE_PURPOSE,
427
+ &unsubscribe_token("s3cret-value", "a@example.com"),
428
+ )
429
+ .unwrap();
307
430
  let remaining = claims["exp"].as_u64().unwrap() - unix_now() as u64;
308
431
  assert!(remaining > 729 * 24 * 60 * 60 && remaining <= 730 * 24 * 60 * 60);
309
432
  }
310
433
 
311
434
  #[test]
312
435
  fn an_expired_or_garbled_unsubscribe_token_is_refused() {
313
- let expired = auth::purpose_token("s3cret-value", UNSUBSCRIBE_PURPOSE, json!({ "email": "a@example.com" }), 0);
436
+ let expired = auth::purpose_token(
437
+ "s3cret-value",
438
+ UNSUBSCRIBE_PURPOSE,
439
+ json!({ "email": "a@example.com" }),
440
+ 0,
441
+ );
314
442
  std::thread::sleep(std::time::Duration::from_millis(1100));
315
- assert!(!unsubscribe_token_matches("s3cret-value", &expired, "a@example.com"));
316
- assert!(!unsubscribe_token_matches("s3cret-value", "not-a-token", "a@example.com"));
317
- assert!(!unsubscribe_token_matches("s3cret-value", "", "a@example.com"));
443
+ assert!(!unsubscribe_token_matches(
444
+ "s3cret-value",
445
+ &expired,
446
+ "a@example.com"
447
+ ));
448
+ assert!(!unsubscribe_token_matches(
449
+ "s3cret-value",
450
+ "not-a-token",
451
+ "a@example.com"
452
+ ));
453
+ assert!(!unsubscribe_token_matches(
454
+ "s3cret-value",
455
+ "",
456
+ "a@example.com"
457
+ ));
318
458
  }
319
459
 
320
460
  #[test]
@@ -323,7 +463,7 @@ mod tests {
323
463
  assert!(require_signing_secret("s3cret-value").is_ok());
324
464
  }
325
465
 
326
- fn provider_with(marks: serde_json::Value) -> NewsletterProvider {
466
+ fn provider_with(marks: &serde_json::Value) -> NewsletterProvider {
327
467
  let mut fact = json!({
328
468
  "provider": "Newsletter", "subscribe": "N::S.Subscribe", "add_name": "N::S.AddName",
329
469
  "confirm": "N::S.Confirm", "unsubscribe": "N::S.Unsubscribe", "aggregate": "N::S"
@@ -336,26 +476,45 @@ mod tests {
336
476
 
337
477
  fn rows() -> Vec<(String, Value)> {
338
478
  vec![
339
- ("zed@example.com".to_string(), json!({ "status": "confirmed" })),
340
- ("pending@example.com".to_string(), json!({ "status": "pending" })),
341
- ("amy@example.com".to_string(), json!({ "status": "confirmed" })),
479
+ (
480
+ "zed@example.com".to_string(),
481
+ json!({ "status": "confirmed" }),
482
+ ),
483
+ (
484
+ "pending@example.com".to_string(),
485
+ json!({ "status": "pending" }),
486
+ ),
487
+ (
488
+ "amy@example.com".to_string(),
489
+ json!({ "status": "confirmed" }),
490
+ ),
342
491
  ("vip@example.com".to_string(), json!({ "status": "vip" })),
343
- ("gone@example.com".to_string(), json!({ "status": "unsubscribed" })),
492
+ (
493
+ "gone@example.com".to_string(),
494
+ json!({ "status": "unsubscribed" }),
495
+ ),
344
496
  ]
345
497
  }
346
498
 
347
499
  #[test]
348
500
  fn legacy_default_sends_to_confirmed_subscribers_in_alphabetical_order() {
349
- let provider = provider_with(json!({}));
350
- assert_eq!(confirmed_emails(&provider, &rows()), vec!["amy@example.com".to_string(), "zed@example.com".to_string()]);
501
+ let provider = provider_with(&json!({}));
502
+ assert_eq!(
503
+ confirmed_emails(&provider, &rows()),
504
+ vec!["amy@example.com".to_string(), "zed@example.com".to_string()]
505
+ );
351
506
  }
352
507
 
353
508
  #[test]
354
509
  fn the_declared_receives_issues_mark_selects_the_recipients() {
355
- let provider = provider_with(json!({ "receives_issues": ["vip", "confirmed"] }));
510
+ let provider = provider_with(&json!({ "receives_issues": ["vip", "confirmed"] }));
356
511
  assert_eq!(
357
512
  confirmed_emails(&provider, &rows()),
358
- vec!["amy@example.com".to_string(), "vip@example.com".to_string(), "zed@example.com".to_string()]
513
+ vec![
514
+ "amy@example.com".to_string(),
515
+ "vip@example.com".to_string(),
516
+ "zed@example.com".to_string()
517
+ ]
359
518
  );
360
519
  }
361
520
 
@@ -374,14 +533,27 @@ mod tests {
374
533
 
375
534
  #[tokio::test]
376
535
  async fn a_test_send_goes_to_the_one_address_with_a_test_subject() {
377
- let response = send_test(r#"{"to":"a@b.com","subject":"Spring","body":"<p>hi</p>"}"#, &Mailer::Mock, "s3cret-value").await;
536
+ let response = send_test(
537
+ r#"{"to":"a@b.com","subject":"Spring","body":"<p>hi</p>"}"#,
538
+ &Mailer::Mock,
539
+ "s3cret-value",
540
+ )
541
+ .await;
378
542
  assert_eq!(response["statusCode"], 200);
379
- assert_eq!(serde_json::from_str::<Value>(response["body"].as_str().unwrap()).unwrap(), json!({ "to": "a@b.com" }));
543
+ assert_eq!(
544
+ serde_json::from_str::<Value>(response["body"].as_str().unwrap()).unwrap(),
545
+ json!({ "to": "a@b.com" })
546
+ );
380
547
  }
381
548
 
382
549
  #[tokio::test]
383
550
  async fn a_test_send_the_provider_refuses_is_a_502() {
384
- let response = send_test(r#"{"to":"bounce@example.com","subject":"Spring","body":"x"}"#, &Mailer::Mock, "s3cret-value").await;
551
+ let response = send_test(
552
+ r#"{"to":"bounce@example.com","subject":"Spring","body":"x"}"#,
553
+ &Mailer::Mock,
554
+ "s3cret-value",
555
+ )
556
+ .await;
385
557
  assert_eq!(response["statusCode"], 502);
386
558
  }
387
559
  }
@@ -13,7 +13,10 @@ fn the_built_in_receipt_names_the_event_the_first_name_and_the_amount() {
13
13
  assert!(body.starts_with("Hi Ada,"));
14
14
  assert!(body.contains("You're registered for Yogadelics."));
15
15
  assert!(body.contains("$108.00"));
16
- assert_eq!(receipt_subject("Yogadelics"), "You're registered for Yogadelics");
16
+ assert_eq!(
17
+ receipt_subject("Yogadelics"),
18
+ "You're registered for Yogadelics"
19
+ );
17
20
  }
18
21
 
19
22
  #[test]
@@ -28,12 +31,21 @@ fn the_sites_finished_email_is_read() {
28
31
  let answer = serde_json::json!({"subject": "You're registered for Yogadelics", "body": "Hi Ada,\n\nWhere: Somewhere\n"});
29
32
  assert_eq!(
30
33
  site_email_from_json(&answer),
31
- Some(SiteEmail { subject: "You're registered for Yogadelics".into(), body: "Hi Ada,\n\nWhere: Somewhere\n".into() })
34
+ Some(SiteEmail {
35
+ subject: "You're registered for Yogadelics".into(),
36
+ body: "Hi Ada,\n\nWhere: Somewhere\n".into()
37
+ })
32
38
  );
33
39
  }
34
40
 
35
41
  #[test]
36
42
  fn an_incomplete_answer_from_the_site_falls_back_to_the_built_in_wording() {
37
- assert_eq!(site_email_from_json(&serde_json::json!({"error": "no such session"})), None);
38
- assert_eq!(site_email_from_json(&serde_json::json!({"subject": "Hi", "body": " "})), None);
43
+ assert_eq!(
44
+ site_email_from_json(&serde_json::json!({"error": "no such session"})),
45
+ None
46
+ );
47
+ assert_eq!(
48
+ site_email_from_json(&serde_json::json!({"subject": "Hi", "body": " "})),
49
+ None
50
+ );
39
51
  }