hecks 3.9.1 → 3.11.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.
- checksums.yaml +4 -4
- data/examples/pizzas/bluebook/pizzas.bluebook +29 -0
- data/examples/pizzas/bluebook/pizzas.hecksagon +7 -0
- data/exe/hecks +4 -4
- data/lib/hecks/adapters/driven/d1.rb +2 -10
- data/lib/hecks/adapters/driven/heki.adapter +1 -1
- data/lib/hecks/adapters/driven/heki.rb +2 -12
- data/lib/hecks/adapters/driven/lambda.adapter +1 -1
- data/lib/hecks/adapters/driven/lambda.rb +3 -18
- data/lib/hecks/adapters/driven/postgres.rb +4 -11
- data/lib/hecks/adapters/driven/sqlite.adapter +1 -1
- data/lib/hecks/adapters/driven/sqlite.rb +2 -10
- data/lib/hecks/adapters/driving/admission.rb +46 -0
- data/lib/hecks/adapters/driving/cli/nesting.rb +36 -0
- data/lib/hecks/adapters/driving/cli/shorthand.rb +88 -0
- data/lib/hecks/adapters/driving/cli.rb +127 -0
- data/lib/hecks/adapters/driving/cli_runner/answers.rb +160 -0
- data/lib/hecks/adapters/driving/cli_runner/resolution.rb +155 -0
- data/lib/hecks/adapters/driving/cli_runner/settling.rb +120 -0
- data/lib/hecks/adapters/driving/cli_runner/streaming.rb +93 -0
- data/lib/hecks/adapters/driving/cli_runner/suggestions.rb +47 -0
- data/lib/hecks/adapters/driving/cli_runner.rb +69 -0
- data/lib/hecks/adapters/driving/command_request.rb +121 -0
- data/lib/hecks/adapters/driving/handle.rb +214 -0
- data/lib/hecks/adapters/driving/json.rb +136 -0
- data/lib/hecks/adapters/driving/launcher_options.rb +267 -0
- data/lib/hecks/adapters/driving/mcp/properties.rb +49 -0
- data/lib/hecks/adapters/driving/mcp/tool_calls.rb +116 -0
- data/lib/hecks/adapters/driving/mcp/tool_definitions/auditing.rb +53 -0
- data/lib/hecks/adapters/driving/mcp/tool_definitions/calling.rb +71 -0
- data/lib/hecks/adapters/driving/mcp/tool_definitions/reading.rb +99 -0
- data/lib/hecks/adapters/driving/mcp/tool_definitions.rb +18 -0
- data/lib/hecks/adapters/driving/mcp.rb +159 -0
- data/lib/hecks/adapters/driving/mcp_guide_cache.rb +78 -0
- data/lib/hecks/adapters/driving/mcp_scope/argument_policy.rb +97 -0
- data/lib/hecks/adapters/driving/mcp_scope/env_problems.rb +69 -0
- data/lib/hecks/adapters/driving/mcp_scope/presenter.rb +83 -0
- data/lib/hecks/adapters/driving/mcp_scope.rb +213 -0
- data/lib/hecks/adapters/driving/ruby/aggregate_module.rb +171 -0
- data/lib/hecks/adapters/driving/ruby/chapter.rb +91 -0
- data/lib/hecks/adapters/driving/ruby.rb +83 -0
- data/lib/hecks/adapters/driving/usage_cache.rb +144 -0
- data/lib/hecks/{doors.rb → adapters/driving.rb} +10 -6
- data/lib/hecks/behaviors/expectations/boot.rb +1 -1
- data/lib/hecks/bench/ruby_runner.rb +1 -1
- data/lib/hecks/bluebook/ask_resolution.rb +130 -0
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +8 -2
- data/lib/hecks/bluebook/assembly/contracts/rules.rb +4 -2
- data/lib/hecks/bluebook/assembly/contracts/structure.rb +3 -1
- data/lib/hecks/bluebook/assembly/marks.rb +1 -0
- data/lib/hecks/bluebook/attribute.rb +1 -1
- data/lib/hecks/bluebook/behaviour/command.rb +13 -8
- data/lib/hecks/bluebook/behaviour/policy.rb +30 -0
- data/lib/hecks/bluebook/capabilities/attribute_values.rb +72 -0
- data/lib/hecks/bluebook/capabilities.rb +91 -9
- data/lib/hecks/bluebook/domain_port.rb +16 -4
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +18 -6
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +13 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation/computed_sources.rb +98 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation/fan_out_rows.rb +42 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation/projected_fields.rb +14 -2
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation/provisions.rb +69 -3
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation/with_projections.rb +17 -10
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +5 -0
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -0
- data/lib/hecks/bluebook/dsl/command_builder/mutations.rb +8 -1
- data/lib/hecks/bluebook/dsl/command_builder/operands.rb +121 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +2 -0
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +10 -1
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +45 -2
- data/lib/hecks/bluebook/dsl/policy_builder.rb +47 -10
- data/lib/hecks/bluebook/dsl/rule_reference.rb +7 -4
- data/lib/hecks/bluebook/dsl/world_builder.rb +1 -1
- data/lib/hecks/bluebook/expression/ast_json/emitters.rb +6 -0
- data/lib/hecks/bluebook/expression/ast_json.rb +9 -6
- data/lib/hecks/bluebook/expression/ast_reader.rb +14 -9
- data/lib/hecks/bluebook/expression/projection.json +36 -0
- data/lib/hecks/bluebook/expression/resolver/operations.rb +31 -0
- data/lib/hecks/bluebook/expression/resolver/parsing.rb +38 -5
- data/lib/hecks/bluebook/expression/resolver/scanning.rb +22 -5
- data/lib/hecks/bluebook/expression/resolver/values.rb +10 -0
- data/lib/hecks/bluebook/expression/resolver.rb +20 -1
- data/lib/hecks/bluebook/hecksagon.rb +23 -3
- data/lib/hecks/bluebook/lifecycle.rb +22 -3
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +13 -1
- data/lib/hecks/bluebook/meta_validator/modes.rb +4 -1
- data/lib/hecks/bluebook/meta_validator/readings.rb +18 -1
- data/lib/hecks/bluebook/meta_validator/reconstruction/heads.rb +2 -1
- data/lib/hecks/bluebook/meta_validator/shapes.rb +5 -0
- data/lib/hecks/bluebook/meta_validator/verdict_cache.rb +1 -1
- data/lib/hecks/bluebook/meta_validator.rb +12 -12
- data/lib/hecks/bluebook/model_check/ask_checks.rb +71 -0
- data/lib/hecks/bluebook/model_check/policy_checks.rb +2 -1
- data/lib/hecks/bluebook/model_check/reference_checks.rb +70 -0
- data/lib/hecks/bluebook/model_check.rb +8 -2
- data/lib/hecks/bluebook/policy.rb +4 -2
- data/lib/hecks/bluebook/smoke_test.rb +1 -1
- data/lib/hecks/bluebook/synthesizer.rb +2 -0
- data/lib/hecks/bluebook.rb +1 -0
- data/lib/hecks/cli/command_table.rb +1 -1
- data/lib/hecks/cli/console.rb +2 -1
- data/lib/hecks/cli/document.rb +1 -1
- data/lib/hecks/cli/launcher_words.rb +4 -4
- data/lib/hecks/cli/mcp.rb +4 -4
- data/lib/hecks/cli/project_cli/launcher_source.rb +2 -2
- data/lib/hecks/cli/project_cli/templates.rb +5 -5
- data/lib/hecks/cli/project_cli.rb +9 -6
- data/lib/hecks/cli/run.rb +2 -2
- data/lib/hecks/cli/smoke_http/checks.rb +4 -1
- data/lib/hecks/cli/smoke_http/transport.rb +3 -3
- data/lib/hecks/cli/smoke_http.rb +3 -0
- data/lib/hecks/codemod.rb +5 -2
- data/lib/hecks/corpus/routes.rb +7 -0
- data/lib/hecks/corpus/rust.rb +3 -2
- data/lib/hecks/deploy/bluebook/deploy.bluebook +124 -2
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +8 -0
- data/lib/hecks/deploy/oidc.json +16 -3
- data/lib/hecks/doc/reference/rendering.rb +6 -3
- data/lib/hecks/forms/app/commands.rb +8 -7
- data/lib/hecks/forms/app/queries.rb +2 -1
- data/lib/hecks/forms/app.rb +1 -1
- data/lib/hecks/forms/record_renderer.rb +1 -1
- data/lib/hecks/framework/bluebook/privacy.bluebook +11 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +4 -1
- data/lib/hecks/fuzzing/failure_triage.rb +2 -1
- data/lib/hecks/fuzzing/mutation/run.rb +11 -4
- data/lib/hecks/fuzzing/properties/catalog.rb +6 -4
- data/lib/hecks/fuzzing/properties/guaranteed_by_construction.rb +18 -11
- data/lib/hecks/fuzzing/properties/guards.rb +1 -1
- data/lib/hecks/fuzzing/self_consistency/saga_redelivery.rb +1 -1
- data/lib/hecks/fuzzing/self_consistency.rb +6 -2
- data/lib/hecks/fuzzing/shrinker.rb +3 -1
- data/lib/hecks/fuzzing/value_generator.rb +19 -5
- data/lib/hecks/gate/stages.yml +4 -10
- data/lib/hecks/grammar/evolve.rb +1 -1
- data/lib/hecks/grammar/expression_operators.json +372 -0
- data/lib/hecks/grammar/operators.rb +3 -1
- data/lib/hecks/hecks/adapters/codebase/corpus_tasks.rb +10 -10
- data/lib/hecks/hecks/adapters/codebase/inspection/rubycritic.rb +35 -0
- data/lib/hecks/hecks/adapters/codebase/inspection.rb +104 -0
- data/lib/hecks/hecks/adapters/codebase/publishing.rb +2 -1
- data/lib/hecks/hecks/adapters/codebase/release_facts.rb +12 -2
- data/lib/hecks/hecks/adapters/codebase/source_tree.rb +2 -1
- data/lib/hecks/hecks/adapters/codebase/test_runner.rb +1 -1
- data/lib/hecks/hecks/adapters/cost_explorer.rb +8 -5
- data/lib/hecks/hecks/adapters/deploy_toolchain/clearing.rb +51 -0
- data/lib/hecks/hecks/adapters/deploy_toolchain/statuses.rb +2 -1
- data/lib/hecks/hecks/adapters/deploy_toolchain.rb +2 -0
- data/lib/hecks/hecks/adapters/finding_github.rb +13 -6
- data/lib/hecks/hecks/adapters/first_admin.rb +2 -2
- data/lib/hecks/hecks/adapters/github_rulesets.rb +6 -3
- data/lib/hecks/hecks/adapters/in_process_boot.rb +1 -1
- data/lib/hecks/hecks/adapters/journal_store/compaction.rb +1 -1
- data/lib/hecks/hecks/adapters/journal_store/edge_audit.rb +10 -20
- data/lib/hecks/hecks/adapters/journal_store/held_domain.rb +28 -5
- data/lib/hecks/hecks/adapters/local_files.rb +2 -2
- data/lib/hecks/hecks/adapters/rehearsal/audit_report.rb +107 -0
- data/lib/hecks/hecks/adapters/rehearsal/census.rb +71 -0
- data/lib/hecks/hecks/adapters/rehearsal/declaration.rb +133 -0
- data/lib/hecks/hecks/adapters/rehearsal/environment.rb +83 -0
- data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +142 -0
- data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +96 -0
- data/lib/hecks/hecks/adapters/rehearsal/postgres_census.rb +100 -0
- data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +69 -0
- data/lib/hecks/hecks/adapters/rehearsal/postgres_store.rb +129 -0
- data/lib/hecks/hecks/adapters/rehearsal/rows.rb +41 -0
- data/lib/hecks/hecks/adapters/rehearsal/rust_audit.rb +83 -0
- data/lib/hecks/hecks/adapters/rehearsal/subject.rb +104 -0
- data/lib/hecks/hecks/adapters/rehearsal.rb +54 -0
- data/lib/hecks/hecks/adapters/rehearsal_store.adapter +7 -0
- data/lib/hecks/hecks/adapters/rehearsal_store.rb +81 -0
- data/lib/hecks/hecks/adapters/site_toolchain/role_prober.rb +78 -0
- data/lib/hecks/hecks/adapters/site_toolchain.rb +29 -4
- data/lib/hecks/hecks/adapters/terminal.rb +13 -11
- data/lib/hecks/hecks/adapters/world_clearing.rb +65 -0
- data/lib/hecks/hecks/codebase.behaviors +13 -1
- data/lib/hecks/hecks/codebase.bluebook +239 -5
- data/lib/hecks/hecks/custodian.behaviors +24 -6
- data/lib/hecks/hecks/custodian.bluebook +6 -1227
- data/lib/hecks/hecks/deploy.behaviors +1 -1
- data/lib/hecks/hecks/deploy.world +1 -3
- data/lib/hecks/hecks/hecks.behaviors +1 -1
- data/lib/hecks/hecks/hecks.hecksagon +61 -18
- data/lib/hecks/hecks/hecks.world +11 -10
- data/lib/hecks/hecks/rehearsal.bluebook +477 -0
- data/lib/hecks/hecks/site.behaviors +1 -1
- data/lib/hecks/hecks/site.world +0 -1
- data/lib/hecks/hecks/tooling.bluebook +1238 -0
- data/lib/hecks/indifferent_key.rb +20 -0
- data/lib/hecks/language/adapter.bluebook +13 -3
- data/lib/hecks/language/bluebook/aggregate.bluebook +23 -0
- data/lib/hecks/language/bluebook/bluebook.bluebook +15 -0
- data/lib/hecks/language/bluebook/entity.bluebook +18 -0
- data/lib/hecks/language/bluebook/policy.bluebook +9 -1
- data/lib/hecks/language/bluebook/query.bluebook +1 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +11 -4
- data/lib/hecks/language/hecksagon/domain_port.bluebook +1 -1
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +22 -0
- data/lib/hecks/language/oidc.json +10 -0
- data/lib/hecks/language/port.bluebook +4 -4
- data/lib/hecks/literal.rb +12 -3
- data/lib/hecks/ports/key_vault.rb +7 -2
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +21 -10
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/boot.rb +4 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +2 -1
- data/lib/hecks/ports/persistence/repository_factory.rb +6 -3
- data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +29 -0
- data/lib/hecks/projections/deploy/box/templates/deploy-service.sh.tmpl +1 -1
- data/lib/hecks/projections/deploy/fargate/settings/tuning.rb +15 -0
- data/lib/hecks/projections/deploy/fargate/settings.rb +3 -1
- data/lib/hecks/projections/deploy/fargate/stack/readers.rb +1 -0
- data/lib/hecks/projections/deploy/preview/preview.sh.erb +1 -1
- data/lib/hecks/projections/deploy/shared.rb +5 -2
- data/lib/hecks/projections/deploy/templates/fargate/Makefile.tmpl +7 -4
- data/lib/hecks/projections/model/deviations.rb +2 -2
- data/lib/hecks/projections/model.rb +2 -2
- data/lib/hecks/projections/oidc.rb +13 -1
- data/lib/hecks/projections/shape.rb +5 -2
- data/lib/hecks/projections/site/cms_editor/attributes.rb +95 -0
- data/lib/hecks/projections/site/cms_editor/blocks.rb +157 -0
- data/lib/hecks/projections/site/cms_editor/checks.rb +77 -0
- data/lib/hecks/projections/site/cms_editor/clearing.rb +70 -0
- data/lib/hecks/projections/site/cms_editor/color.rb +124 -0
- data/lib/hecks/projections/site/cms_editor/composition.rb +42 -0
- data/lib/hecks/projections/site/cms_editor/destructive.rb +61 -0
- data/lib/hecks/projections/site/cms_editor/drafts.rb +102 -0
- data/lib/hecks/projections/site/cms_editor/hints.rb +74 -0
- data/lib/hecks/projections/site/cms_editor/icons.rb +86 -0
- data/lib/hecks/projections/site/cms_editor/key_kinds.rb +31 -0
- data/lib/hecks/projections/site/cms_editor/limits.rb +72 -0
- data/lib/hecks/projections/site/cms_editor/media.rb +85 -0
- data/lib/hecks/projections/site/cms_editor/moment.rb +34 -0
- data/lib/hecks/projections/site/cms_editor/palette.rb +115 -0
- data/lib/hecks/projections/site/cms_editor/pickers.rb +85 -0
- data/lib/hecks/projections/site/cms_editor/pretty.rb +40 -0
- data/lib/hecks/projections/site/cms_editor/preview.rb +91 -0
- data/lib/hecks/projections/site/cms_editor/relations.rb +92 -0
- data/lib/hecks/projections/site/cms_editor/scale_checks.rb +66 -0
- data/lib/hecks/projections/site/cms_editor/scheduling.rb +150 -0
- data/lib/hecks/projections/site/cms_editor/schema.rb +161 -0
- data/lib/hecks/projections/site/cms_editor/setting.rb +130 -0
- data/lib/hecks/projections/site/cms_editor/targets.rb +86 -0
- data/lib/hecks/projections/site/cms_editor/templates/package.json.tmpl +36 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/app.ts.tmpl +370 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/assets.ts.tmpl +98 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/auth/membership.ts.tmpl +87 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/auth/session.ts.tmpl +36 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/auth/sso.ts.tmpl +20 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/app.css.tmpl +412 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/autosave.js.tmpl +144 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/blocks.js.tmpl +500 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/body_doc.js.tmpl +175 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/body_editor.js.tmpl +418 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/epoch.js.tmpl +61 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/forms.js.tmpl +210 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/lists.js.tmpl +40 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/main.js.tmpl +26 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/media_picker.js.tmpl +232 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/moments.js.tmpl +73 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/pickers.js.tmpl +152 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/preview.js.tmpl +98 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/shell.js.tmpl +140 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/browser/toast.js.tmpl +66 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/choices.ts.tmpl +99 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/commands.ts.tmpl +133 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/config.ts.tmpl +29 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/flash.ts.tmpl +52 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/host.ts.tmpl +43 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/media/handler.ts.tmpl +177 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/media/multipart.ts.tmpl +53 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/media/sniff.ts.tmpl +94 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/media/storage.ts.tmpl +50 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/names.ts.tmpl +36 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/schema.ts.tmpl +214 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/server.ts.tmpl +65 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/availability.ts.tmpl +15 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/block_list.js.tmpl +213 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/blocks.ts.tmpl +275 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/body_model.js.tmpl +123 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/body_parse.js.tmpl +320 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/detail.ts.tmpl +88 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/dialogs.ts.tmpl +43 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/fields.ts.tmpl +275 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/html.ts.tmpl +61 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/icons.ts.tmpl +15 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/input.ts.tmpl +92 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/links.ts.tmpl +100 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/moments.ts.tmpl +44 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/outline.ts.tmpl +63 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/pages.ts.tmpl +213 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/paging.ts.tmpl +82 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/pickers.ts.tmpl +110 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/preview.ts.tmpl +87 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/related.ts.tmpl +97 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/schedule.ts.tmpl +160 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/shell.ts.tmpl +211 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/table.ts.tmpl +151 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/theme.js.tmpl +16 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/values.ts.tmpl +69 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/words.ts.tmpl +97 -0
- data/lib/hecks/projections/site/cms_editor/templates/src/ui/world.ts.tmpl +35 -0
- data/lib/hecks/projections/site/cms_editor/templates/tsconfig.json.tmpl +17 -0
- data/lib/hecks/projections/site/cms_editor/theme.rb +29 -0
- data/lib/hecks/projections/site/cms_editor/theme_checks.rb +57 -0
- data/lib/hecks/projections/site/cms_editor.rb +133 -0
- data/lib/hecks/projections/site/role_probe.rb +74 -0
- data/lib/hecks/projector/cli_audience.rb +2 -1
- data/lib/hecks/projector/cli_projector/listing.rb +1 -1
- data/lib/hecks/projector/cli_projector/option_specs.rb +1 -1
- data/lib/hecks/projector/docs_projector/closing.rb +1 -1
- data/lib/hecks/projector/docs_projector.rb +1 -1
- data/lib/hecks/projector/exporter/bindings.rb +40 -10
- data/lib/hecks/projector/exporter/capability_values.rb +55 -0
- data/lib/hecks/projector/narrate_projector/reactions.rb +1 -1
- data/lib/hecks/projector/target.rb +11 -0
- data/lib/hecks/projector.rb +32 -5
- data/lib/hecks/quality_control/adapters/github_checks.rb +12 -8
- data/lib/hecks/quality_control/adapters/github_issues.rb +8 -4
- data/lib/hecks/quality_control/cli/qa_open_pr.rb +2 -2
- data/lib/hecks/quality_control/cli/qa_sweep/checks/shrinking.rb +1 -1
- data/lib/hecks/quality_control/cli/qa_sweep/finding_report.rb +1 -1
- data/lib/hecks/quality_control/cli/qa_sweep.rb +1 -1
- data/lib/hecks/quality_control/quality_control.bluebook +6 -2
- data/lib/hecks/release/approval.rb +64 -0
- data/lib/hecks/router/{door.rb → aggregate_reader.rb} +5 -4
- data/lib/hecks/router/namespace_installer.rb +16 -16
- data/lib/hecks/router.rb +1 -1
- data/lib/hecks/runtime/best_effort.rb +22 -0
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +12 -2
- data/lib/hecks/runtime/command_interpreter.rb +3 -2
- data/lib/hecks/runtime/command_rules/emission.rb +1 -1
- data/lib/hecks/runtime/dispatcher.rb +4 -4
- data/lib/hecks/runtime/entity_element/location.rb +1 -1
- data/lib/hecks/runtime/event.rb +5 -0
- data/lib/hecks/runtime/identity.rb +7 -6
- data/lib/hecks/runtime/interpreting.rb +2 -1
- data/lib/hecks/runtime/invocation_builder.rb +1 -1
- data/lib/hecks/runtime/loader/boot_steps.rb +8 -5
- data/lib/hecks/runtime/loader.rb +10 -10
- data/lib/hecks/runtime/policy_interpreter/arguments.rb +7 -4
- data/lib/hecks/runtime/policy_interpreter/fan_out.rb +3 -3
- data/lib/hecks/runtime/policy_interpreter.rb +6 -6
- data/lib/hecks/runtime/port_operation_interpreter.rb +1 -1
- data/lib/hecks/runtime/reaction_invocation.rb +1 -1
- data/lib/hecks/runtime/rebuild_sweep.rb +11 -1
- data/lib/hecks/runtime/registry/ask_verification.rb +35 -0
- data/lib/hecks/runtime/registry/hecksagon_rules.rb +10 -0
- data/lib/hecks/runtime/registry/merging.rb +1 -1
- data/lib/hecks/runtime/registry/providers.rb +16 -0
- data/lib/hecks/runtime/registry/read_repository.rb +6 -4
- data/lib/hecks/runtime/registry/verification.rb +4 -1
- data/lib/hecks/runtime/registry/world_defaults.rb +29 -0
- data/lib/hecks/runtime/registry.rb +1 -1
- data/lib/hecks/runtime/saga_interpreter/compensation.rb +1 -1
- data/lib/hecks/runtime/saga_interpreter/delivery.rb +4 -4
- data/lib/hecks/runtime/saga_interpreter/transitions.rb +1 -1
- data/lib/hecks/runtime/saga_interpreter.rb +3 -3
- data/lib/hecks/runtime/tenant_check.rb +5 -4
- data/lib/hecks/runtime/value/coercion.rb +2 -2
- data/lib/hecks/runtime/value/validation.rb +16 -4
- data/lib/hecks/runtime.rb +9 -9
- data/lib/hecks/rust_build/project_rust/target_ir.rb +1 -1
- data/lib/hecks/rust_build/wasm.rb +1 -1
- data/lib/hecks/rust_build/wasm_browser.rb +1 -1
- data/lib/hecks/rust_build.rb +11 -0
- data/lib/hecks/served_options.rb +53 -0
- data/lib/hecks/site/bluebook/site.bluebook +29 -1
- data/lib/hecks/site/bluebook/site.ports.hecksagon +6 -0
- data/lib/hecks/storehouse/audit.rb +1 -1
- data/lib/hecks/storehouse/dispatching.rb +8 -6
- data/lib/hecks/storehouse/introspection.rb +2 -2
- data/lib/hecks/storehouse/querying.rb +6 -4
- data/lib/hecks/storehouse.rb +4 -4
- data/lib/hecks/tools/comment_style/source_file.rb +4 -1
- data/lib/hecks/tools/deploy_recipe_lint/fixtures.rb +4 -1
- data/lib/hecks/tools/lanes/rows.rb +6 -0
- data/lib/hecks/tools/oidc_manifests.rb +4 -5
- data/lib/hecks/tools/regeneration_run.rb +11 -4
- data/lib/hecks/tools/site_routes/cli.rb +1 -1
- data/lib/hecks/tools/site_routes/editor_files.rb +45 -0
- data/lib/hecks/tools/site_routes.rb +14 -7
- data/lib/hecks/tools/tenant_provisioning.rb +2 -2
- data/lib/hecks/tools/tools_doc.rb +4 -4
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +4 -4
- data/lib/hecks.rb +15 -15
- data/lib/rubocop/cop/hecks/env_dump.rb +69 -0
- data/lib/rubocop/cop/hecks/monotonic_duration.rb +53 -0
- data/lib/rubocop/cop/hecks/no_wall_clock_in_runtime.rb +34 -0
- data/lib/rubocop/cop/hecks/raw_shell_interpolation.rb +79 -0
- data/lib/rubocop/cop/hecks/sleep_in_spec.rb +34 -0
- data/lib/rubocop/cop/hecks/swallowed_rescue.rb +37 -0
- data/lib/rubocop/cop/hecks/symbol_string_key_mix.rb +50 -0
- data/lib/rubocop/cop/hecks/untyped_raise.rb +37 -0
- data/prebuilt/05c358d38d661a3774f813d49c66f5b68939af94432d66257a56f49ee2598994.marshal +0 -0
- data/prebuilt/26c40c61f09b09b11651c64668eda3ceb180f83cf2a3802d3033d2fd99ac841b.marshal +0 -0
- data/prebuilt/verdicts-230ac18c25abfd3d92ede032bee2661a4c2cd2773ab0cf0e2ae52546ba2f7af9.json +1 -0
- data/rust/.gitignore +6 -0
- data/rust/Cargo.toml +9 -1
- data/rust/build/src/binding_pass.rs +103 -0
- data/rust/build/src/cargo_sync.rs +6 -0
- data/rust/build/src/lineage_pass.rs +3 -3
- data/rust/build/src/main.rs +17 -5
- data/rust/build/src/pipeline.rs +48 -5
- data/rust/build/src/seams_pass.rs +123 -0
- data/rust/build/src/translation_pass.rs +167 -0
- data/rust/codegen/src/asks.rs +103 -0
- data/rust/codegen/src/bridging.rs +1 -1
- data/rust/codegen/src/commands.rs +13 -8
- data/rust/codegen/src/constraints.rs +25 -0
- data/rust/codegen/src/domain_generator.rs +11 -9
- data/rust/codegen/src/expr_emitter.rs +13 -0
- data/rust/codegen/src/json_codec.rs +54 -15
- data/rust/codegen/src/main.rs +2 -1
- data/rust/codegen/src/mutations.rs +174 -4
- data/rust/codegen/src/naming.rs +30 -0
- data/rust/codegen/src/queries.rs +11 -2
- data/rust/codegen/src/reactions.rs +1 -1
- data/rust/codegen/src/types.rs +166 -8
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/host/src/api.rs +4 -4
- data/rust/host/src/bin/validate_harness.rs +43 -0
- data/rust/host/src/boundary_fuzz/expr_json.rs +2 -2
- data/rust/host/src/boundary_fuzz/ir.rs +1 -0
- data/rust/host/src/boundary_fuzz/server.rs +1 -1
- data/rust/host/src/checkout.rs +35 -19
- data/rust/host/src/commerce_ir.rs +408 -2
- data/rust/host/src/expr_json.rs +118 -0
- data/rust/host/src/mint.rs +1 -1
- data/rust/host/src/payments/tests.rs +27 -1
- data/rust/host/src/payments.rs +12 -4
- data/rust/host/src/query_step.rs +22 -4
- data/rust/host/src/reference_validate.rs +653 -108
- data/rust/host/src/resend.rs +2 -0
- data/rust/host/src/server.rs +19 -6
- data/rust/host/src/ui_schema.rs +132 -5
- data/rust/host/src/web/newsletter.rs +7 -8
- data/rust/host/src/web/newsletter_send.rs +38 -13
- data/rust/host/src/web/registrations.rs +229 -26
- data/rust/host/src/web.rs +3 -3
- data/rust/parser/src/emit.rs +38 -6
- data/rust/parser/src/expr/ast_json.rs +7 -1
- data/rust/parser/src/expr/resolver.rs +171 -6
- data/rust/parser/src/ir.rs +7 -0
- data/rust/parser/src/keywords.rs +24 -0
- data/rust/parser/src/main.rs +3 -0
- data/rust/parser/src/parse/aggregate.rs +2 -1
- data/rust/parser/src/parse/chapter.rs +2 -2
- data/rust/parser/src/parse/command.rs +2 -0
- data/rust/parser/src/parse/hecksagon.rs +5 -2
- data/rust/parser/src/parse/lifecycle.rs +81 -0
- data/rust/parser/src/parse/mod.rs +15 -1
- data/rust/parser/src/parse/policy.rs +13 -2
- data/rust/parser/src/parse/value_object.rs +13 -8
- data/rust/parser/src/ruby_value.rs +1 -1
- data/rust/src/exemplar/commands.rs +4 -4
- data/rust/src/exemplar/constraints.rs +1 -1
- data/rust/src/exemplar/json.rs +43 -1
- data/rust/src/kernel/cli.rs +7 -5
- data/rust/src/kernel/dispatch.rs +2 -2
- data/rust/src/kernel/dispatch_guard_tests.rs +1 -1
- data/rust/src/kernel/expr.rs +73 -4
- data/rust/src/kernel/expression_operators/arithmetic.rs +130 -5
- data/rust/src/kernel/expression_operators/presence.rs +41 -3
- data/rust/src/kernel/expression_operators/text.rs +26 -2
- data/rust/src/kernel/json.rs +15 -0
- data/rust/src/kernel/mod.rs +1 -1
- data/rust/src/kernel/orchestrate.rs +54 -5
- data/rust/src/kernel/orchestrate_guard_tests.rs +67 -0
- data/rust/src/kernel/pattern.rs +60 -0
- data/rust/src/kernel/reference_lookup.rs +90 -7
- metadata +174 -36
- data/lib/hecks/doors/cli_door/nesting.rb +0 -34
- data/lib/hecks/doors/cli_door/shorthand.rb +0 -85
- data/lib/hecks/doors/cli_door.rb +0 -124
- data/lib/hecks/doors/cli_runner/answers.rb +0 -138
- data/lib/hecks/doors/cli_runner/resolution.rb +0 -146
- data/lib/hecks/doors/cli_runner/settling.rb +0 -115
- data/lib/hecks/doors/cli_runner/streaming.rb +0 -90
- data/lib/hecks/doors/cli_runner/suggestions.rb +0 -43
- data/lib/hecks/doors/cli_runner.rb +0 -66
- data/lib/hecks/doors/command_request.rb +0 -119
- data/lib/hecks/doors/handle.rb +0 -208
- data/lib/hecks/doors/json_door.rb +0 -132
- data/lib/hecks/doors/launcher_options.rb +0 -247
- data/lib/hecks/doors/mcp_door/properties.rb +0 -47
- data/lib/hecks/doors/mcp_door/tool_calls.rb +0 -113
- data/lib/hecks/doors/mcp_door/tool_definitions/auditing.rb +0 -51
- data/lib/hecks/doors/mcp_door/tool_definitions/calling.rb +0 -69
- data/lib/hecks/doors/mcp_door/tool_definitions/reading.rb +0 -97
- data/lib/hecks/doors/mcp_door/tool_definitions.rb +0 -15
- data/lib/hecks/doors/mcp_door.rb +0 -156
- data/lib/hecks/doors/mcp_door_scope/argument_policy.rb +0 -92
- data/lib/hecks/doors/mcp_door_scope/env_problems.rb +0 -55
- data/lib/hecks/doors/mcp_door_scope/presenter.rb +0 -78
- data/lib/hecks/doors/mcp_door_scope.rb +0 -201
- data/lib/hecks/doors/mcp_guide_cache.rb +0 -75
- data/lib/hecks/doors/ruby_door/aggregate_door.rb +0 -162
- data/lib/hecks/doors/ruby_door/chapter.rb +0 -89
- data/lib/hecks/doors/ruby_door.rb +0 -80
- data/lib/hecks/doors/usage_cache.rb +0 -139
- data/prebuilt/0ee7f83cba59ce7d1c45c65ba166b071779f70143449f584f33ff7989f64eccb.marshal +0 -0
- data/prebuilt/6e4a6bcf0aa880e2e6b056e1846b70e5a768090b9dc998bbadeaf5d6bcfd3db8.marshal +0 -0
- data/prebuilt/verdicts-91aaf93ce8981bd9a754948f075d95650dc2ec6435015376f5c60992b36a24b1.json +0 -1
data/rust/host/src/checkout.rs
CHANGED
|
@@ -7,10 +7,6 @@ use sha2::Sha256;
|
|
|
7
7
|
|
|
8
8
|
type HmacSha256 = Hmac<Sha256>;
|
|
9
9
|
|
|
10
|
-
// docs.stripe.com/webhooks#verify-events — 5 minutes, guards against a
|
|
11
|
-
// replayed old payload even against a leaked (not yet rotated) secret.
|
|
12
|
-
const TOLERANCE_SECONDS: i64 = 300;
|
|
13
|
-
|
|
14
10
|
pub struct SignatureError(pub String);
|
|
15
11
|
|
|
16
12
|
impl std::fmt::Display for SignatureError {
|
|
@@ -22,7 +18,10 @@ impl std::fmt::Display for SignatureError {
|
|
|
22
18
|
// Signs `<timestamp>.<raw body>`, the exact bytes Stripe sent -- re-serializing
|
|
23
19
|
// the JSON would disagree on whitespace or key order and break every match.
|
|
24
20
|
// `now` is a parameter, not read internally, so a test can hold time fixed.
|
|
25
|
-
|
|
21
|
+
// `tolerance_seconds` is the freshness window the Checkout bluebook declares
|
|
22
|
+
// (`WebhookReceipt.tolerance`, read from the IR): it guards against a replayed
|
|
23
|
+
// old payload even against a leaked (not yet rotated) secret.
|
|
24
|
+
pub fn verify_signature(payload: &str, header: &str, secret: &str, now: i64, tolerance_seconds: i64) -> Result<(), SignatureError> {
|
|
26
25
|
let mut timestamp: Option<i64> = None;
|
|
27
26
|
let mut signatures: Vec<&str> = Vec::new();
|
|
28
27
|
for part in header.split(',') {
|
|
@@ -39,9 +38,9 @@ pub fn verify_signature(payload: &str, header: &str, secret: &str, now: i64) ->
|
|
|
39
38
|
if signatures.is_empty() {
|
|
40
39
|
return Err(SignatureError("Stripe-Signature header has no v1= signature".to_string()));
|
|
41
40
|
}
|
|
42
|
-
if (now - timestamp).abs() >
|
|
41
|
+
if (now - timestamp).abs() > tolerance_seconds {
|
|
43
42
|
return Err(SignatureError(format!(
|
|
44
|
-
"timestamp {timestamp} is outside the {
|
|
43
|
+
"timestamp {timestamp} is outside the {tolerance_seconds}s tolerance (now: {now})"
|
|
45
44
|
)));
|
|
46
45
|
}
|
|
47
46
|
|
|
@@ -98,11 +97,19 @@ pub struct EmbeddedSession {
|
|
|
98
97
|
pub client_secret: String,
|
|
99
98
|
}
|
|
100
99
|
|
|
101
|
-
// 31, not 30: Stripe requires an expiry at least 30 minutes out.
|
|
102
|
-
|
|
100
|
+
// 31, not 30: Stripe requires an expiry at least 30 minutes out. This floor is a
|
|
101
|
+
// fact about the processor, so it lives here in the adapter; the domain declares
|
|
102
|
+
// only how long a session holds its seat (`CheckoutSession.hold`), and a hold
|
|
103
|
+
// shorter than the floor is raised to it.
|
|
104
|
+
pub const STRIPE_MIN_HOLD_SECONDS: i64 = 31 * 60;
|
|
105
|
+
|
|
106
|
+
/// The hold actually sent to Stripe for a domain hold of `hold_seconds`.
|
|
107
|
+
pub fn effective_hold_seconds(hold_seconds: i64) -> i64 {
|
|
108
|
+
hold_seconds.max(STRIPE_MIN_HOLD_SECONDS)
|
|
109
|
+
}
|
|
103
110
|
|
|
104
|
-
pub fn session_expires_at(now: i64) -> i64 {
|
|
105
|
-
now +
|
|
111
|
+
pub fn session_expires_at(now: i64, hold_seconds: i64) -> i64 {
|
|
112
|
+
now + effective_hold_seconds(hold_seconds)
|
|
106
113
|
}
|
|
107
114
|
|
|
108
115
|
// Errors carry Stripe's message when it refuses, never a secret.
|
|
@@ -158,6 +165,8 @@ pub async fn create_checkout_session(
|
|
|
158
165
|
mod tests {
|
|
159
166
|
use super::*;
|
|
160
167
|
|
|
168
|
+
const TOLERANCE_SECONDS: i64 = 300;
|
|
169
|
+
|
|
161
170
|
fn sign(secret: &str, timestamp: i64, payload: &str) -> String {
|
|
162
171
|
let signed_payload = format!("{timestamp}.{payload}");
|
|
163
172
|
let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).unwrap();
|
|
@@ -195,7 +204,7 @@ mod tests {
|
|
|
195
204
|
let now = 1_700_000_000;
|
|
196
205
|
let header = format!("t={now},v1={}", sign(secret, now, payload));
|
|
197
206
|
|
|
198
|
-
assert!(verify_signature(payload, &header, secret, now).is_ok());
|
|
207
|
+
assert!(verify_signature(payload, &header, secret, now, TOLERANCE_SECONDS).is_ok());
|
|
199
208
|
}
|
|
200
209
|
|
|
201
210
|
#[test]
|
|
@@ -207,7 +216,7 @@ mod tests {
|
|
|
207
216
|
// Same header, different body — exactly what an attacker
|
|
208
217
|
// intercepting and rewriting the request in transit would send.
|
|
209
218
|
let tampered = r#"{"type":"checkout.session.expired"}"#;
|
|
210
|
-
assert!(verify_signature(tampered, &header, secret, now).is_err());
|
|
219
|
+
assert!(verify_signature(tampered, &header, secret, now, TOLERANCE_SECONDS).is_err());
|
|
211
220
|
}
|
|
212
221
|
|
|
213
222
|
#[test]
|
|
@@ -216,7 +225,7 @@ mod tests {
|
|
|
216
225
|
let now = 1_700_000_000;
|
|
217
226
|
let header = format!("t={now},v1={}", sign("whsec_real", now, payload));
|
|
218
227
|
|
|
219
|
-
assert!(verify_signature(payload, &header, "whsec_wrong", now).is_err());
|
|
228
|
+
assert!(verify_signature(payload, &header, "whsec_wrong", now, TOLERANCE_SECONDS).is_err());
|
|
220
229
|
}
|
|
221
230
|
|
|
222
231
|
#[test]
|
|
@@ -231,7 +240,14 @@ mod tests {
|
|
|
231
240
|
// to be a separate, independent gate rather than folded into
|
|
232
241
|
// "does the signature verify at all."
|
|
233
242
|
let much_later = signed_at + TOLERANCE_SECONDS + 1;
|
|
234
|
-
assert!(verify_signature(payload, &header, secret, much_later).is_err());
|
|
243
|
+
assert!(verify_signature(payload, &header, secret, much_later, TOLERANCE_SECONDS).is_err());
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
#[test]
|
|
247
|
+
fn a_hold_shorter_than_stripes_floor_is_raised_to_it() {
|
|
248
|
+
assert_eq!(effective_hold_seconds(1800), STRIPE_MIN_HOLD_SECONDS);
|
|
249
|
+
assert_eq!(effective_hold_seconds(3600), 3600);
|
|
250
|
+
assert_eq!(session_expires_at(1_000, 1800), 1_000 + STRIPE_MIN_HOLD_SECONDS);
|
|
235
251
|
}
|
|
236
252
|
|
|
237
253
|
#[test]
|
|
@@ -246,20 +262,20 @@ mod tests {
|
|
|
246
262
|
sign(new_secret, now, payload)
|
|
247
263
|
);
|
|
248
264
|
|
|
249
|
-
assert!(verify_signature(payload, &header, old_secret, now).is_ok());
|
|
250
|
-
assert!(verify_signature(payload, &header, new_secret, now).is_ok());
|
|
265
|
+
assert!(verify_signature(payload, &header, old_secret, now, TOLERANCE_SECONDS).is_ok());
|
|
266
|
+
assert!(verify_signature(payload, &header, new_secret, now, TOLERANCE_SECONDS).is_ok());
|
|
251
267
|
}
|
|
252
268
|
|
|
253
269
|
#[test]
|
|
254
270
|
fn a_header_with_no_v1_at_all_is_rejected() {
|
|
255
271
|
let now = 1_700_000_000;
|
|
256
272
|
let header = format!("t={now}");
|
|
257
|
-
assert!(verify_signature("{}", &header, "whsec_test", now).is_err());
|
|
273
|
+
assert!(verify_signature("{}", &header, "whsec_test", now, TOLERANCE_SECONDS).is_err());
|
|
258
274
|
}
|
|
259
275
|
|
|
260
276
|
#[test]
|
|
261
277
|
fn a_header_with_no_timestamp_at_all_is_rejected() {
|
|
262
278
|
let header = "v1=deadbeef".to_string();
|
|
263
|
-
assert!(verify_signature("{}", &header, "whsec_test", 1_700_000_000).is_err());
|
|
279
|
+
assert!(verify_signature("{}", &header, "whsec_test", 1_700_000_000, TOLERANCE_SECONDS).is_err());
|
|
264
280
|
}
|
|
265
281
|
}
|
|
@@ -20,9 +20,152 @@ pub struct NewsletterProvider {
|
|
|
20
20
|
pub unsubscribe: String,
|
|
21
21
|
/// Qualified subscribing aggregate, e.g. `Newsletter::Subscriber`.
|
|
22
22
|
pub aggregate: String,
|
|
23
|
+
/// Subscriber states awaiting the emailed confirm link: the
|
|
24
|
+
/// `awaiting_confirmation` list of the `newsletter` fact (the lifecycle's
|
|
25
|
+
/// mark), or [`LEGACY_AWAITING_CONFIRMATION`] when the chapter declares none.
|
|
26
|
+
pub awaiting_confirmation: Vec<String>,
|
|
27
|
+
/// Subscriber states that receive each issue: the `receives_issues` list,
|
|
28
|
+
/// or [`LEGACY_RECEIVES_ISSUES`] when the chapter declares none.
|
|
29
|
+
pub receives_issues: Vec<String>,
|
|
30
|
+
/// Subscriber states of someone who has left: the `left` list, or
|
|
31
|
+
/// [`LEGACY_LEFT`] when the chapter declares none.
|
|
32
|
+
pub left: Vec<String>,
|
|
33
|
+
/// Seconds the emailed confirm link stays valid: the `confirm_window` of
|
|
34
|
+
/// the `newsletter` fact (ADR 0098), or [`LEGACY_CONFIRM_WINDOW_SECS`].
|
|
35
|
+
pub confirm_window: u64,
|
|
36
|
+
/// Seconds an emailed unsubscribe link stays valid: the `unsubscribe_window`,
|
|
37
|
+
/// or [`LEGACY_UNSUBSCRIBE_WINDOW_SECS`].
|
|
38
|
+
pub unsubscribe_window: u64,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// LEGACY DEFAULT: how long the emailed confirm link lasts (14 days), used
|
|
42
|
+
/// while the `newsletter` fact carries no `confirm_window`, because the
|
|
43
|
+
/// Newsletter chapter predates ADR 0098 and does not declare
|
|
44
|
+
/// `provides "newsletter", ..., confirm_window: "Subscriber.confirm_window"`.
|
|
45
|
+
/// Delete this (and the fallback in `newsletter_provider`) once every shipped
|
|
46
|
+
/// Newsletter bluebook does.
|
|
47
|
+
pub const LEGACY_CONFIRM_WINDOW_SECS: u64 = 14 * 24 * 60 * 60;
|
|
48
|
+
|
|
49
|
+
/// LEGACY DEFAULT: how long an emailed unsubscribe link lasts (730 days), used
|
|
50
|
+
/// while the `newsletter` fact carries no `unsubscribe_window` (see
|
|
51
|
+
/// [`LEGACY_CONFIRM_WINDOW_SECS`]).
|
|
52
|
+
pub const LEGACY_UNSUBSCRIBE_WINDOW_SECS: u64 = 730 * 24 * 60 * 60;
|
|
53
|
+
|
|
54
|
+
/// The whole seconds the fact `fact` lists under `key`: an attribute default
|
|
55
|
+
/// resolved by the exporter (ADR 0098). `None` when the fact omits it.
|
|
56
|
+
fn declared_seconds(fact: &Value, key: &str) -> Option<u64> {
|
|
57
|
+
fact.get(key)?.as_u64().filter(|seconds| *seconds > 0)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/// The declared seconds for `key`, else `legacy` with a single warning per process.
|
|
61
|
+
fn seconds_or_legacy(fact: &Value, capability: &str, key: &str, legacy: u64, warned: &std::sync::Once) -> u64 {
|
|
62
|
+
declared_seconds(fact, key).unwrap_or_else(|| {
|
|
63
|
+
warned.call_once(|| eprintln!("{capability} capability declares no {key}; using built-in default"));
|
|
64
|
+
legacy
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/// The checkout boundary's windows, read from `ir.json`'s own `checkout` key
|
|
69
|
+
/// (ADR 0098). Always present: a window the chapter does not declare takes its
|
|
70
|
+
/// labelled legacy default.
|
|
71
|
+
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
72
|
+
pub struct CheckoutWindows {
|
|
73
|
+
/// Seconds either side of now a signed webhook's timestamp may sit.
|
|
74
|
+
pub webhook_tolerance: i64,
|
|
75
|
+
/// Seconds a checkout session holds its seat (before the processor's own
|
|
76
|
+
/// minimum is applied by the checkout adapter).
|
|
77
|
+
pub session_hold: i64,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// LEGACY DEFAULT: the webhook freshness window (5 minutes), used while the
|
|
81
|
+
/// `checkout` fact carries no `webhook_tolerance`, because no shipped Checkout
|
|
82
|
+
/// bluebook declares `provides "checkout", webhook_tolerance: "WebhookReceipt.tolerance"`.
|
|
83
|
+
/// Delete this (and the fallback in `checkout_windows`) once every one does.
|
|
84
|
+
pub const LEGACY_WEBHOOK_TOLERANCE_SECS: i64 = 300;
|
|
85
|
+
|
|
86
|
+
/// LEGACY DEFAULT: the seat hold of a session (30 minutes), used while the
|
|
87
|
+
/// `checkout` fact carries no `session_hold` (see [`LEGACY_WEBHOOK_TOLERANCE_SECS`]).
|
|
88
|
+
pub const LEGACY_SESSION_HOLD_SECS: i64 = 30 * 60;
|
|
89
|
+
|
|
90
|
+
/// The checkout windows `domain_ir` declares, each falling back to its legacy default.
|
|
91
|
+
pub fn checkout_windows(domain_ir: &Value) -> CheckoutWindows {
|
|
92
|
+
static TOLERANCE_WARNED: std::sync::Once = std::sync::Once::new();
|
|
93
|
+
static HOLD_WARNED: std::sync::Once = std::sync::Once::new();
|
|
94
|
+
let fact = domain_ir.get("checkout").unwrap_or(&Value::Null);
|
|
95
|
+
let seconds = |key, legacy: i64, warned| seconds_or_legacy(fact, "checkout", key, legacy as u64, warned) as i64;
|
|
96
|
+
CheckoutWindows {
|
|
97
|
+
webhook_tolerance: seconds("webhook_tolerance", LEGACY_WEBHOOK_TOLERANCE_SECS, &TOLERANCE_WARNED),
|
|
98
|
+
session_hold: seconds("session_hold", LEGACY_SESSION_HOLD_SECS, &HOLD_WARNED),
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/// The checkout windows of the IR this host loaded (legacy defaults without one).
|
|
103
|
+
pub fn checkout_windows_binding() -> CheckoutWindows {
|
|
104
|
+
checkout_windows(ir().unwrap_or(&Value::Null))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/// The confirm-link and unsubscribe-link windows of the IR this host loaded:
|
|
108
|
+
/// `(confirm, unsubscribe)` seconds, legacy defaults without a `newsletter` fact.
|
|
109
|
+
pub fn newsletter_windows_binding() -> (u64, u64) {
|
|
110
|
+
ir().and_then(newsletter_provider)
|
|
111
|
+
.map(|p| (p.confirm_window, p.unsubscribe_window))
|
|
112
|
+
.unwrap_or((LEGACY_CONFIRM_WINDOW_SECS, LEGACY_UNSUBSCRIBE_WINDOW_SECS))
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/// LEGACY DEFAULT: the states a new subscriber waits in, used while the
|
|
116
|
+
/// `newsletter` fact carries no `awaiting_confirmation` list, because the
|
|
117
|
+
/// Newsletter chapter predates the marks and does not declare
|
|
118
|
+
/// `provides "newsletter", ..., awaiting_confirmation: "Subscriber.awaiting_confirmation"`.
|
|
119
|
+
/// Delete this (and the fallback in `newsletter_provider`) once every shipped
|
|
120
|
+
/// Newsletter bluebook does.
|
|
121
|
+
pub const LEGACY_AWAITING_CONFIRMATION: [&str; 1] = ["pending"];
|
|
122
|
+
|
|
123
|
+
/// LEGACY DEFAULT: the states that receive an issue, used while the
|
|
124
|
+
/// `newsletter` fact carries no `receives_issues` list (see
|
|
125
|
+
/// [`LEGACY_AWAITING_CONFIRMATION`]).
|
|
126
|
+
pub const LEGACY_RECEIVES_ISSUES: [&str; 1] = ["confirmed"];
|
|
127
|
+
|
|
128
|
+
/// LEGACY DEFAULT: the states of a subscriber who has left, used while the
|
|
129
|
+
/// `newsletter` fact carries no `left` list (see [`LEGACY_AWAITING_CONFIRMATION`]).
|
|
130
|
+
pub const LEGACY_LEFT: [&str; 1] = ["unsubscribed"];
|
|
131
|
+
|
|
132
|
+
/// The states the `newsletter` fact lists under `key`: a lifecycle mark
|
|
133
|
+
/// resolved by the exporter. `None` when the fact omits it.
|
|
134
|
+
fn declared_states(fact: &Value, key: &str) -> Option<Vec<String>> {
|
|
135
|
+
let states = fact.get(key)?.as_array()?;
|
|
136
|
+
Some(states.iter().filter_map(|s| s.as_str().map(String::from)).collect())
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/// The declared states for `key`, else `legacy` with a single warning per process.
|
|
140
|
+
fn states_or_legacy(fact: &Value, key: &str, legacy: &[&str], warned: &std::sync::Once) -> Vec<String> {
|
|
141
|
+
declared_states(fact, key).unwrap_or_else(|| {
|
|
142
|
+
warned.call_once(|| eprintln!("newsletter capability declares no {key}; using built-in default"));
|
|
143
|
+
legacy.iter().map(|s| s.to_string()).collect()
|
|
144
|
+
})
|
|
23
145
|
}
|
|
24
146
|
|
|
25
147
|
impl NewsletterProvider {
|
|
148
|
+
/// Whether `status` is a state awaiting confirmation.
|
|
149
|
+
pub fn is_awaiting_confirmation(&self, status: Option<&str>) -> bool {
|
|
150
|
+
status.is_some_and(|s| self.awaiting_confirmation.iter().any(|m| m == s))
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/// Whether `status` is a state that receives issues.
|
|
154
|
+
pub fn receives_issues(&self, status: Option<&str>) -> bool {
|
|
155
|
+
status.is_some_and(|s| self.receives_issues.iter().any(|m| m == s))
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/// Whether `status` is a state of a subscriber who has left.
|
|
159
|
+
pub fn has_left(&self, status: Option<&str>) -> bool {
|
|
160
|
+
status.is_some_and(|s| self.left.iter().any(|m| m == s))
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/// The state a subscriber is reported in when none is recorded yet: the
|
|
164
|
+
/// first awaiting-confirmation state.
|
|
165
|
+
pub fn initial_status(&self) -> &str {
|
|
166
|
+
self.awaiting_confirmation.first().map(String::as_str).unwrap_or("")
|
|
167
|
+
}
|
|
168
|
+
|
|
26
169
|
/// The prefix every subscriber's key in a `dispatch::read` `instances`
|
|
27
170
|
/// map starts with, e.g. `Newsletter::Subscriber#`.
|
|
28
171
|
pub fn instance_prefix(&self) -> String {
|
|
@@ -49,8 +192,18 @@ pub fn command_declares(domain_ir: &Value, aggregate: &str, command: &str, field
|
|
|
49
192
|
}
|
|
50
193
|
|
|
51
194
|
pub fn newsletter_provider(domain_ir: &Value) -> Option<NewsletterProvider> {
|
|
195
|
+
static AWAITING_WARNED: std::sync::Once = std::sync::Once::new();
|
|
196
|
+
static RECEIVES_WARNED: std::sync::Once = std::sync::Once::new();
|
|
197
|
+
static LEFT_WARNED: std::sync::Once = std::sync::Once::new();
|
|
198
|
+
static CONFIRM_WINDOW_WARNED: std::sync::Once = std::sync::Once::new();
|
|
199
|
+
static UNSUBSCRIBE_WINDOW_WARNED: std::sync::Once = std::sync::Once::new();
|
|
52
200
|
let fact = domain_ir.get("newsletter")?;
|
|
53
201
|
Some(NewsletterProvider {
|
|
202
|
+
confirm_window: seconds_or_legacy(fact, "newsletter", "confirm_window", LEGACY_CONFIRM_WINDOW_SECS, &CONFIRM_WINDOW_WARNED),
|
|
203
|
+
unsubscribe_window: seconds_or_legacy(fact, "newsletter", "unsubscribe_window", LEGACY_UNSUBSCRIBE_WINDOW_SECS, &UNSUBSCRIBE_WINDOW_WARNED),
|
|
204
|
+
awaiting_confirmation: states_or_legacy(fact, "awaiting_confirmation", &LEGACY_AWAITING_CONFIRMATION, &AWAITING_WARNED),
|
|
205
|
+
receives_issues: states_or_legacy(fact, "receives_issues", &LEGACY_RECEIVES_ISSUES, &RECEIVES_WARNED),
|
|
206
|
+
left: states_or_legacy(fact, "left", &LEGACY_LEFT, &LEFT_WARNED),
|
|
54
207
|
provider: fact.get("provider")?.as_str()?.to_string(),
|
|
55
208
|
subscribe: fact.get("subscribe")?.as_str()?.to_string(),
|
|
56
209
|
add_name: fact.get("add_name")?.as_str()?.to_string(),
|
|
@@ -108,6 +261,49 @@ pub struct PaymentsProvider {
|
|
|
108
261
|
pub failed: String,
|
|
109
262
|
/// Qualified paying aggregate, e.g. `Payments::Payment`.
|
|
110
263
|
pub aggregate: String,
|
|
264
|
+
/// Payment states whose registration still holds a seat: the `holds_seat`
|
|
265
|
+
/// list of the `payments` fact (the payment lifecycle's mark), or
|
|
266
|
+
/// [`LEGACY_HOLDS_SEAT`] when the chapter declares none.
|
|
267
|
+
pub holds_seat: Vec<String>,
|
|
268
|
+
/// The failure reason a lapsed checkout hold records: the `lapse_reason` of
|
|
269
|
+
/// the `payments` fact (ADR 0099), or [`LEGACY_LAPSE_REASON`] when the
|
|
270
|
+
/// chapter declares none.
|
|
271
|
+
pub lapse_reason: String,
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/// LEGACY DEFAULT: the failure reason a lapsed hold records, used while the
|
|
275
|
+
/// `payments` capability fact carries no `lapse_reason` because the Payments
|
|
276
|
+
/// chapter predates `provides "payments", lapse_reason: "Payment.lapse_reason"`.
|
|
277
|
+
/// Delete this (and the fallback in `payments_provider`) once every shipped
|
|
278
|
+
/// Payments bluebook declares it.
|
|
279
|
+
pub const LEGACY_LAPSE_REASON: &str = "checkout_expired";
|
|
280
|
+
|
|
281
|
+
/// LEGACY DEFAULT: the registration attributes guessed to hold the timestamp, in
|
|
282
|
+
/// order, used while the `registrations` fact carries no `registered_at` because
|
|
283
|
+
/// the chapter predates `provides "registrations", registered_at: "Registration.requested_at"`.
|
|
284
|
+
/// Delete this (and the fallback in `registrations_provider`) once every shipped
|
|
285
|
+
/// bluebook declares it.
|
|
286
|
+
pub const LEGACY_REGISTERED_AT: [&str; 4] = ["created_at", "registered_at", "requested_at", "occurred_at"];
|
|
287
|
+
|
|
288
|
+
/// LEGACY DEFAULT: the seat-holding payment states used while the `payments`
|
|
289
|
+
/// capability fact carries no `holds_seat` list, because the Payments chapter
|
|
290
|
+
/// predates `mark :holds_seat` and does not declare
|
|
291
|
+
/// `provides "payments", holds_seat: "Payment.holds_seat"`. Delete this (and the
|
|
292
|
+
/// fallback in `payments_provider`) once every shipped Payments bluebook does.
|
|
293
|
+
pub const LEGACY_HOLDS_SEAT: [&str; 4] = ["pending", "succeeded", "refunding", "disputed"];
|
|
294
|
+
|
|
295
|
+
/// LEGACY DEFAULT: the registration state that gives its seat back, used while the
|
|
296
|
+
/// `registrations` capability fact carries no `frees_seat` list, because the chapter
|
|
297
|
+
/// predates `mark :frees_seat` and does not declare
|
|
298
|
+
/// `provides "registrations", frees_seat: "Registration.frees_seat"`. Delete this (and the
|
|
299
|
+
/// fallback in `registrations_provider`) once every shipped bluebook declares it.
|
|
300
|
+
pub const LEGACY_FREES_SEAT: [&str; 1] = ["archived"];
|
|
301
|
+
|
|
302
|
+
/// The states the `payments` fact lists under `holds_seat`: the aggregate's
|
|
303
|
+
/// lifecycle mark, resolved by the exporter. `None` when the fact omits it.
|
|
304
|
+
fn declared_holds_seat(fact: &Value) -> Option<Vec<String>> {
|
|
305
|
+
let states = fact.get("holds_seat")?.as_array()?;
|
|
306
|
+
Some(states.iter().filter_map(|s| s.as_str().map(String::from)).collect())
|
|
111
307
|
}
|
|
112
308
|
|
|
113
309
|
impl PaymentsProvider {
|
|
@@ -120,12 +316,24 @@ impl PaymentsProvider {
|
|
|
120
316
|
|
|
121
317
|
pub fn payments_provider(domain_ir: &Value) -> Option<PaymentsProvider> {
|
|
122
318
|
let fact = domain_ir.get("payments")?;
|
|
319
|
+
let aggregate = fact.get("aggregate")?.as_str()?.to_string();
|
|
320
|
+
let holds_seat = declared_holds_seat(fact).unwrap_or_else(|| {
|
|
321
|
+
static WARNED: std::sync::Once = std::sync::Once::new();
|
|
322
|
+
WARNED.call_once(|| eprintln!("payments capability declares no holds_seat; using built-in default"));
|
|
323
|
+
LEGACY_HOLDS_SEAT.iter().map(|s| s.to_string()).collect()
|
|
324
|
+
});
|
|
123
325
|
Some(PaymentsProvider {
|
|
124
326
|
provider: fact.get("provider")?.as_str()?.to_string(),
|
|
125
327
|
initiate: fact.get("initiate")?.as_str()?.to_string(),
|
|
126
328
|
succeeded: fact.get("succeeded")?.as_str()?.to_string(),
|
|
127
329
|
failed: fact.get("failed")?.as_str()?.to_string(),
|
|
128
|
-
aggregate
|
|
330
|
+
aggregate,
|
|
331
|
+
holds_seat,
|
|
332
|
+
lapse_reason: fact.get("lapse_reason").and_then(|v| v.as_str()).filter(|v| !v.is_empty()).map(String::from).unwrap_or_else(|| {
|
|
333
|
+
static WARNED: std::sync::Once = std::sync::Once::new();
|
|
334
|
+
WARNED.call_once(|| eprintln!("payments capability declares no lapse_reason; using built-in default"));
|
|
335
|
+
LEGACY_LAPSE_REASON.to_string()
|
|
336
|
+
}),
|
|
129
337
|
})
|
|
130
338
|
}
|
|
131
339
|
|
|
@@ -143,6 +351,14 @@ pub struct RegistrationsProvider {
|
|
|
143
351
|
pub event_aggregate: String,
|
|
144
352
|
/// Qualified registration aggregate, e.g. `Studio::Registration`.
|
|
145
353
|
pub registration_aggregate: String,
|
|
354
|
+
/// The registration attributes that may hold when it was asked for, in the order
|
|
355
|
+
/// to try them: the one `registered_at` of the `registrations` fact names
|
|
356
|
+
/// (ADR 0099), or [`LEGACY_REGISTERED_AT`] when the chapter declares none.
|
|
357
|
+
pub timestamp_keys: Vec<String>,
|
|
358
|
+
/// Registration states that give their seat back: the `frees_seat` list of the
|
|
359
|
+
/// `registrations` fact (the registration lifecycle's mark), or [`LEGACY_FREES_SEAT`]
|
|
360
|
+
/// when the chapter declares none.
|
|
361
|
+
pub frees_seat: Vec<String>,
|
|
146
362
|
}
|
|
147
363
|
|
|
148
364
|
impl RegistrationsProvider {
|
|
@@ -155,6 +371,8 @@ impl RegistrationsProvider {
|
|
|
155
371
|
request: format!("{domain}::Registration.Request"),
|
|
156
372
|
event_aggregate: format!("{domain}::Event"),
|
|
157
373
|
registration_aggregate: format!("{domain}::Registration"),
|
|
374
|
+
timestamp_keys: LEGACY_REGISTERED_AT.iter().map(|k| k.to_string()).collect(),
|
|
375
|
+
frees_seat: LEGACY_FREES_SEAT.iter().map(|s| s.to_string()).collect(),
|
|
158
376
|
}
|
|
159
377
|
}
|
|
160
378
|
|
|
@@ -177,14 +395,35 @@ impl RegistrationsProvider {
|
|
|
177
395
|
}
|
|
178
396
|
}
|
|
179
397
|
|
|
398
|
+
/// The states the `registrations` fact lists under `frees_seat`: the aggregate's lifecycle
|
|
399
|
+
/// mark, resolved by the exporter. `None` when the fact omits it.
|
|
400
|
+
fn declared_frees_seat(fact: &Value) -> Option<Vec<String>> {
|
|
401
|
+
let states = fact.get("frees_seat")?.as_array()?;
|
|
402
|
+
Some(states.iter().filter_map(|s| s.as_str().map(String::from)).collect())
|
|
403
|
+
}
|
|
404
|
+
|
|
180
405
|
pub fn registrations_provider(domain_ir: &Value) -> Option<RegistrationsProvider> {
|
|
181
406
|
let fact = domain_ir.get("registrations")?;
|
|
407
|
+
let frees_seat = declared_frees_seat(fact).unwrap_or_else(|| {
|
|
408
|
+
static WARNED: std::sync::Once = std::sync::Once::new();
|
|
409
|
+
WARNED.call_once(|| eprintln!("registrations capability declares no frees_seat; using built-in default"));
|
|
410
|
+
LEGACY_FREES_SEAT.iter().map(|s| s.to_string()).collect()
|
|
411
|
+
});
|
|
182
412
|
Some(RegistrationsProvider {
|
|
183
413
|
provider: fact.get("provider")?.as_str()?.to_string(),
|
|
184
414
|
schedule: fact.get("schedule")?.as_str()?.to_string(),
|
|
185
415
|
request: fact.get("request")?.as_str()?.to_string(),
|
|
186
416
|
event_aggregate: fact.get("event_aggregate")?.as_str()?.to_string(),
|
|
187
417
|
registration_aggregate: fact.get("registration_aggregate")?.as_str()?.to_string(),
|
|
418
|
+
timestamp_keys: match fact.get("registered_at").and_then(|v| v.as_str()).filter(|v| !v.is_empty()) {
|
|
419
|
+
Some(declared) => vec![declared.to_string()],
|
|
420
|
+
None => {
|
|
421
|
+
static WARNED: std::sync::Once = std::sync::Once::new();
|
|
422
|
+
WARNED.call_once(|| eprintln!("registrations capability declares no registered_at; using built-in default"));
|
|
423
|
+
LEGACY_REGISTERED_AT.iter().map(|k| k.to_string()).collect()
|
|
424
|
+
}
|
|
425
|
+
},
|
|
426
|
+
frees_seat,
|
|
188
427
|
})
|
|
189
428
|
}
|
|
190
429
|
|
|
@@ -325,6 +564,84 @@ mod tests {
|
|
|
325
564
|
assert!(newsletter_provider(&serde_json::json!({ "name": "Pizzas" })).is_none());
|
|
326
565
|
}
|
|
327
566
|
|
|
567
|
+
fn newsletter_ir(marks: serde_json::Value) -> Value {
|
|
568
|
+
let mut fact = serde_json::json!({
|
|
569
|
+
"provider": "Newsletter",
|
|
570
|
+
"subscribe": "Newsletter::Subscriber.Subscribe",
|
|
571
|
+
"add_name": "Newsletter::Subscriber.AddName",
|
|
572
|
+
"confirm": "Newsletter::Subscriber.Confirm",
|
|
573
|
+
"unsubscribe": "Newsletter::Subscriber.Unsubscribe",
|
|
574
|
+
"aggregate": "Newsletter::Subscriber"
|
|
575
|
+
});
|
|
576
|
+
for (key, states) in marks.as_object().into_iter().flatten() {
|
|
577
|
+
fact[key] = states.clone();
|
|
578
|
+
}
|
|
579
|
+
serde_json::json!({ "name": "Studio", "newsletter": fact })
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
#[test]
|
|
583
|
+
fn newsletter_provider_reads_the_marks_from_the_fact() {
|
|
584
|
+
let ir = newsletter_ir(serde_json::json!({
|
|
585
|
+
"awaiting_confirmation": ["invited"],
|
|
586
|
+
"receives_issues": ["active", "vip"],
|
|
587
|
+
"left": ["gone"]
|
|
588
|
+
}));
|
|
589
|
+
let provider = newsletter_provider(&ir).expect("newsletter");
|
|
590
|
+
assert_eq!(provider.awaiting_confirmation, vec!["invited"]);
|
|
591
|
+
assert_eq!(provider.receives_issues, vec!["active", "vip"]);
|
|
592
|
+
assert_eq!(provider.left, vec!["gone"]);
|
|
593
|
+
assert!(provider.is_awaiting_confirmation(Some("invited")));
|
|
594
|
+
assert!(!provider.is_awaiting_confirmation(Some("pending")));
|
|
595
|
+
assert!(provider.receives_issues(Some("vip")));
|
|
596
|
+
assert!(!provider.receives_issues(Some("confirmed")));
|
|
597
|
+
assert!(provider.has_left(Some("gone")));
|
|
598
|
+
assert_eq!(provider.initial_status(), "invited");
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
#[test]
|
|
602
|
+
fn newsletter_provider_falls_back_to_the_legacy_defaults_without_marks() {
|
|
603
|
+
let provider = newsletter_provider(&newsletter_ir(serde_json::json!({}))).expect("newsletter");
|
|
604
|
+
assert_eq!(provider.awaiting_confirmation, LEGACY_AWAITING_CONFIRMATION);
|
|
605
|
+
assert_eq!(provider.receives_issues, LEGACY_RECEIVES_ISSUES);
|
|
606
|
+
assert_eq!(provider.left, LEGACY_LEFT);
|
|
607
|
+
assert!(provider.is_awaiting_confirmation(Some("pending")));
|
|
608
|
+
assert!(provider.receives_issues(Some("confirmed")));
|
|
609
|
+
assert!(provider.has_left(Some("unsubscribed")));
|
|
610
|
+
assert!(!provider.has_left(None));
|
|
611
|
+
assert_eq!(provider.initial_status(), "pending");
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
#[test]
|
|
615
|
+
fn newsletter_provider_reads_the_link_windows_from_the_fact() {
|
|
616
|
+
let ir = newsletter_ir(serde_json::json!({ "confirm_window": 3600, "unsubscribe_window": 7200 }));
|
|
617
|
+
let provider = newsletter_provider(&ir).expect("newsletter");
|
|
618
|
+
assert_eq!((provider.confirm_window, provider.unsubscribe_window), (3600, 7200));
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
#[test]
|
|
622
|
+
fn newsletter_provider_falls_back_to_the_legacy_windows_without_them() {
|
|
623
|
+
let provider = newsletter_provider(&newsletter_ir(serde_json::json!({}))).expect("newsletter");
|
|
624
|
+
assert_eq!(provider.confirm_window, LEGACY_CONFIRM_WINDOW_SECS);
|
|
625
|
+
assert_eq!(provider.unsubscribe_window, LEGACY_UNSUBSCRIBE_WINDOW_SECS);
|
|
626
|
+
assert_eq!((LEGACY_CONFIRM_WINDOW_SECS, LEGACY_UNSUBSCRIBE_WINDOW_SECS), (1_209_600, 63_072_000));
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
#[test]
|
|
630
|
+
fn checkout_windows_read_the_fact_and_fall_back_per_window() {
|
|
631
|
+
let declared = serde_json::json!({ "checkout": { "provider": "Checkout", "webhook_tolerance": 120, "session_hold": 2400 } });
|
|
632
|
+
assert_eq!(checkout_windows(&declared), CheckoutWindows { webhook_tolerance: 120, session_hold: 2400 });
|
|
633
|
+
let partial = serde_json::json!({ "checkout": { "provider": "Checkout", "session_hold": 2400 } });
|
|
634
|
+
assert_eq!(checkout_windows(&partial), CheckoutWindows { webhook_tolerance: LEGACY_WEBHOOK_TOLERANCE_SECS, session_hold: 2400 });
|
|
635
|
+
let none = checkout_windows(&serde_json::json!({ "name": "Pizzas" }));
|
|
636
|
+
assert_eq!(none, CheckoutWindows { webhook_tolerance: 300, session_hold: 1800 });
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
#[test]
|
|
640
|
+
fn a_zero_or_non_numeric_window_is_not_a_declaration() {
|
|
641
|
+
let ir = serde_json::json!({ "checkout": { "webhook_tolerance": 0, "session_hold": "soon" } });
|
|
642
|
+
assert_eq!(checkout_windows(&ir), CheckoutWindows { webhook_tolerance: 300, session_hold: 1800 });
|
|
643
|
+
}
|
|
644
|
+
|
|
328
645
|
#[test]
|
|
329
646
|
fn newsletter_issues_provider_reads_the_declared_capability() {
|
|
330
647
|
let ir = serde_json::json!({
|
|
@@ -365,6 +682,39 @@ mod tests {
|
|
|
365
682
|
assert!(payments_provider(&serde_json::json!({ "name": "Pizzas" })).is_none());
|
|
366
683
|
}
|
|
367
684
|
|
|
685
|
+
fn payments_ir(holds_seat: Option<serde_json::Value>) -> Value {
|
|
686
|
+
let mut fact = serde_json::json!({
|
|
687
|
+
"provider": "Payments", "initiate": "Payments::Payment.Initiate",
|
|
688
|
+
"succeeded": "Payments::Payment.PaymentGateway.Succeeded",
|
|
689
|
+
"failed": "Payments::Payment.PaymentGateway.Failed", "aggregate": "Payments::Payment"
|
|
690
|
+
});
|
|
691
|
+
if let Some(states) = holds_seat {
|
|
692
|
+
fact["holds_seat"] = states;
|
|
693
|
+
}
|
|
694
|
+
serde_json::json!({"name": "Studio", "payments": fact})
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
#[test]
|
|
698
|
+
fn payments_provider_reads_holds_seat_from_the_fact() {
|
|
699
|
+
let ir = payments_ir(Some(serde_json::json!(["pending", "paid"])));
|
|
700
|
+
assert_eq!(declared_holds_seat(&ir["payments"]), Some(vec!["pending".to_string(), "paid".to_string()]));
|
|
701
|
+
assert_eq!(payments_provider(&ir).expect("payments").holds_seat, vec!["pending", "paid"]);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
#[test]
|
|
705
|
+
fn the_checkout_fixture_ir_carries_holds_seat_with_no_fallback() {
|
|
706
|
+
let ir = fixture_ir();
|
|
707
|
+
assert_eq!(declared_holds_seat(&ir["payments"]), Some(vec!["pending".to_string(), "succeeded".to_string()]));
|
|
708
|
+
assert_eq!(fixture_payments().holds_seat, vec!["pending", "succeeded"]);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
#[test]
|
|
712
|
+
fn payments_provider_falls_back_to_the_legacy_default_without_holds_seat() {
|
|
713
|
+
assert_eq!(declared_holds_seat(&payments_ir(None)["payments"]), None);
|
|
714
|
+
let held = payments_provider(&payments_ir(None)).expect("payments").holds_seat;
|
|
715
|
+
assert_eq!(held, LEGACY_HOLDS_SEAT);
|
|
716
|
+
}
|
|
717
|
+
|
|
368
718
|
#[test]
|
|
369
719
|
fn registrations_provider_reads_the_declared_capability() {
|
|
370
720
|
let provider = registrations_provider(&fixture_ir()).expect("the checkout fixture provides registrations");
|
|
@@ -377,7 +727,63 @@ mod tests {
|
|
|
377
727
|
|
|
378
728
|
#[test]
|
|
379
729
|
fn the_conventional_registrations_names_match_what_the_fixture_declares() {
|
|
380
|
-
|
|
730
|
+
let declared = registrations_provider(&fixture_ir()).expect("registrations");
|
|
731
|
+
let conventional = RegistrationsProvider {
|
|
732
|
+
timestamp_keys: declared.timestamp_keys.clone(),
|
|
733
|
+
frees_seat: declared.frees_seat.clone(),
|
|
734
|
+
..RegistrationsProvider::conventional("CheckoutFixture") };
|
|
735
|
+
assert_eq!(declared, conventional);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
#[test]
|
|
739
|
+
fn the_registration_timestamp_is_the_declared_attribute_or_the_legacy_guess() {
|
|
740
|
+
assert_eq!(registrations_provider(&fixture_ir()).expect("registrations").timestamp_keys, vec!["requested_at"]);
|
|
741
|
+
let undeclared = serde_json::json!({"registrations": {
|
|
742
|
+
"provider": "B", "schedule": "B::E.S", "request": "B::R.Q",
|
|
743
|
+
"event_aggregate": "B::E", "registration_aggregate": "B::R"
|
|
744
|
+
}});
|
|
745
|
+
assert_eq!(registrations_provider(&undeclared).expect("registrations").timestamp_keys, LEGACY_REGISTERED_AT);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
fn registrations_ir(frees_seat: Option<serde_json::Value>) -> Value {
|
|
749
|
+
let mut fact = serde_json::json!({
|
|
750
|
+
"provider": "B", "schedule": "B::E.S", "request": "B::R.Q",
|
|
751
|
+
"event_aggregate": "B::E", "registration_aggregate": "B::R"
|
|
752
|
+
});
|
|
753
|
+
if let Some(states) = frees_seat {
|
|
754
|
+
fact["frees_seat"] = states;
|
|
755
|
+
}
|
|
756
|
+
serde_json::json!({"registrations": fact})
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
#[test]
|
|
760
|
+
fn registrations_provider_reads_frees_seat_from_the_fact() {
|
|
761
|
+
let ir = registrations_ir(Some(serde_json::json!(["cancelled", "archived"])));
|
|
762
|
+
assert_eq!(declared_frees_seat(&ir["registrations"]), Some(vec!["cancelled".to_string(), "archived".to_string()]));
|
|
763
|
+
assert_eq!(registrations_provider(&ir).expect("registrations").frees_seat, vec!["cancelled", "archived"]);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
#[test]
|
|
767
|
+
fn the_checkout_fixture_ir_carries_frees_seat_with_no_fallback() {
|
|
768
|
+
let ir = fixture_ir();
|
|
769
|
+
assert_eq!(declared_frees_seat(&ir["registrations"]), Some(vec!["archived".to_string(), "cancelled".to_string()]));
|
|
770
|
+
assert_eq!(registrations_provider(&ir).expect("registrations").frees_seat, vec!["archived", "cancelled"]);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
#[test]
|
|
774
|
+
fn registrations_provider_falls_back_to_the_legacy_default_without_frees_seat() {
|
|
775
|
+
assert_eq!(declared_frees_seat(®istrations_ir(None)["registrations"]), None);
|
|
776
|
+
let freed = registrations_provider(®istrations_ir(None)).expect("registrations").frees_seat;
|
|
777
|
+
assert_eq!(freed, LEGACY_FREES_SEAT);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
#[test]
|
|
781
|
+
fn the_lapse_reason_is_the_declared_word_or_the_legacy_default() {
|
|
782
|
+
assert_eq!(fixture_payments().lapse_reason, "checkout_expired");
|
|
783
|
+
let mut ir = payments_ir(None);
|
|
784
|
+
assert_eq!(payments_provider(&ir).expect("payments").lapse_reason, LEGACY_LAPSE_REASON);
|
|
785
|
+
ir["payments"]["lapse_reason"] = serde_json::json!("hold_lapsed");
|
|
786
|
+
assert_eq!(payments_provider(&ir).expect("payments").lapse_reason, "hold_lapsed");
|
|
381
787
|
}
|
|
382
788
|
|
|
383
789
|
#[test]
|