hecks 2.9.0 → 3.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.
- checksums.yaml +4 -4
- data/exe/hecks +51 -6
- data/lib/hecks/adapters/driven/heki/journal.rb +1 -1
- data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
- data/lib/hecks/adapters/driven/lambda.rb +1 -1
- data/lib/hecks/adapters/driven/memory.rb +16 -3
- data/lib/hecks/adapters/driven/postgres/reconnect.rb +6 -2
- data/lib/hecks/adapters/driven/postgres/shared_connection.rb +180 -0
- data/lib/hecks/adapters/driven/postgres.rb +4 -2
- data/lib/hecks/adapters/driven/prism.rb +41 -8
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +73 -16
- data/lib/hecks/behaviors/expectations.rb +1 -1
- data/lib/hecks/behaviors.rb +1 -1
- data/lib/hecks/bench/cli.rb +62 -0
- data/lib/hecks/bench/environment.rb +71 -0
- data/lib/hecks/bench/postgres_probe.rb +30 -0
- data/lib/hecks/bench/report.rb +76 -0
- data/lib/hecks/bench/ruby_runner.rb +93 -0
- data/lib/hecks/bench/run.rb +55 -0
- data/lib/hecks/bench/rust_runner.rb +101 -0
- data/lib/hecks/bench/stats.rb +46 -0
- data/lib/hecks/bench/suite.rb +98 -0
- data/lib/hecks/bench/workload.rb +89 -0
- data/lib/hecks/bench.rb +17 -0
- data/lib/hecks/bluebook/assembly/contracts.rb +2 -0
- data/lib/hecks/bluebook/attribute.rb +1 -1
- data/lib/hecks/bluebook/behaviour/aggregate.rb +30 -1
- data/lib/hecks/bluebook/behaviour/domain_port.rb +7 -0
- data/lib/hecks/bluebook/chapter.rb +6 -2
- data/lib/hecks/bluebook/domain_port.rb +36 -5
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +10 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +8 -0
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -1
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +31 -3
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +122 -3
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/query_builder.rb +27 -1
- data/lib/hecks/bluebook/dsl/rule_reference.rb +1 -1
- data/lib/hecks/bluebook/dsl/world_builder.rb +29 -2
- data/lib/hecks/bluebook/expression/canonical_form.rb +1 -1
- data/lib/hecks/bluebook/expression/resolver.rb +3 -2
- data/lib/hecks/bluebook/hexagon.rb +13 -5
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +1 -0
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +4 -1
- data/lib/hecks/bluebook/meta_validator/verdict_cache.rb +246 -0
- data/lib/hecks/bluebook/meta_validator.rb +9 -4
- data/lib/hecks/bluebook/model_check.rb +23 -0
- data/lib/hecks/bluebook/pattern_subset.rb +38 -0
- data/lib/hecks/bluebook/query.rb +20 -5
- data/lib/hecks/bluebook/smoke_test.rb +1 -1
- data/lib/hecks/canonical_json.rb +30 -0
- data/lib/hecks/chapters.rb +102 -0
- data/lib/hecks/cli/behaviors.rb +61 -0
- data/lib/hecks/cli/canonicalise.rb +25 -0
- data/lib/hecks/cli/check_engine_agreement.rb +32 -0
- data/lib/hecks/cli/check_era.rb +88 -0
- data/lib/hecks/cli/console.rb +66 -0
- data/lib/hecks/cli/deploy_template_diff.rb +40 -0
- data/lib/hecks/cli/doc_coverage.rb +32 -0
- data/lib/hecks/cli/document.rb +2 -2
- data/lib/hecks/cli/expression_projection.rb +41 -0
- data/lib/hecks/cli/follow.rb +88 -0
- data/lib/hecks/cli/history.rb +44 -0
- data/lib/hecks/cli/ir.rb +1 -1
- data/lib/hecks/cli/mcp.rb +3 -3
- data/lib/hecks/cli/model_check.rb +7 -4
- data/lib/hecks/cli/pattern_cases.rb +47 -0
- data/lib/hecks/cli/present.rb +39 -0
- data/lib/hecks/cli/project_cli.rb +224 -32
- data/lib/hecks/cli/project_diagrams.rb +4 -3
- data/lib/hecks/cli/project_glossary.rb +44 -0
- data/lib/hecks/cli/reference.rb +46 -0
- data/lib/hecks/cli/refresh_projections.rb +44 -0
- data/lib/hecks/cli/refresh_rspec_runtime_baseline.rb +150 -0
- data/lib/hecks/cli/release.rb +70 -0
- data/lib/hecks/cli/release_gem.rb +59 -0
- data/lib/hecks/cli/rspec_io_parallel_files.rb +137 -0
- data/lib/hecks/cli/rspec_shard_files.rb +66 -0
- data/lib/hecks/cli/run.rb +3 -3
- data/lib/hecks/cli/seed_semantics_corpus.rb +71 -0
- data/lib/hecks/cli/serve_query_ir_mcp.rb +30 -0
- data/lib/hecks/cli/shape.rb +70 -0
- data/lib/hecks/cli/smoke_http.rb +230 -0
- data/lib/hecks/cli/smoke_test.rb +5 -3
- data/lib/hecks/cli/spec_example.rb +27 -0
- data/lib/hecks/cli/statements.rb +32 -0
- data/lib/hecks/cli/stores.rb +4 -5
- data/lib/hecks/cli/stress_concurrency_specs.rb +177 -0
- data/lib/hecks/cli.rb +201 -52
- data/lib/hecks/codemod.rb +324 -0
- data/lib/hecks/corpus.rb +423 -0
- data/lib/hecks/deploy/bluebook/deploy.bluebook +591 -23
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +41 -4
- data/lib/hecks/deploy/oidc.json +82 -2
- data/lib/hecks/doc/reference.rb +351 -0
- data/lib/hecks/doors/cli_door.rb +196 -0
- data/lib/hecks/doors/cli_runner.rb +297 -0
- data/lib/hecks/{facade → doors}/command_request.rb +1 -1
- data/lib/hecks/{facade → doors}/handle.rb +2 -2
- data/lib/hecks/{facade → doors}/json_door.rb +4 -4
- data/lib/hecks/doors/launcher_options.rb +129 -0
- data/lib/hecks/{cli → doors}/mcp_door.rb +7 -7
- data/lib/hecks/doors/mcp_door_scope.rb +97 -0
- data/lib/hecks/{facade/surface → doors/ruby_door}/aggregate_door.rb +8 -3
- data/lib/hecks/{facade/surface → doors/ruby_door}/chapter.rb +3 -3
- data/lib/hecks/{facade/surface.rb → doors/ruby_door.rb} +36 -5
- data/lib/hecks/doors.rb +61 -0
- data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +1 -1
- data/lib/hecks/embryonaut_bluebook.rb +1 -1
- data/lib/hecks/engine_agreement.rb +156 -0
- data/lib/hecks/facade.rb +24 -37
- data/lib/hecks/forms/app.rb +6 -6
- data/lib/hecks/forms/banking_presentation.rb +50 -0
- data/lib/hecks/forms/field_shape.rb +2 -1
- data/lib/hecks/forms/port_argument.rb +1 -1
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +393 -0
- data/lib/hecks/fuzzing/combination_miner.rb +86 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +214 -0
- data/lib/hecks/fuzzing/coverage_campaign.rb +143 -0
- data/lib/hecks/fuzzing/differential.rb +156 -0
- data/lib/hecks/fuzzing/domain_generator.rb +615 -0
- data/lib/hecks/fuzzing/era_boundary.rb +71 -0
- data/lib/hecks/fuzzing/form_census.rb +136 -0
- data/lib/hecks/fuzzing/generated_domain_check.rb +93 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +64 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +301 -0
- data/lib/hecks/fuzzing/nondeterministic.rb +51 -0
- data/lib/hecks/fuzzing/outside_world.rb +45 -0
- data/lib/hecks/fuzzing/persistence_parity.rb +102 -0
- data/lib/hecks/fuzzing/properties/corrections.rb +88 -0
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +314 -0
- data/lib/hecks/fuzzing/properties/guards.rb +190 -0
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +300 -0
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +93 -0
- data/lib/hecks/fuzzing/properties/outbox.rb +89 -0
- data/lib/hecks/fuzzing/properties/querying.rb +175 -0
- data/lib/hecks/fuzzing/properties.rb +162 -0
- data/lib/hecks/fuzzing/qa_settings.rb +123 -0
- data/lib/hecks/fuzzing/replay.rb +505 -0
- data/lib/hecks/fuzzing/rotation_priority.rb +48 -0
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +108 -0
- data/lib/hecks/fuzzing/self_consistency.rb +339 -0
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +385 -0
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +132 -0
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +95 -0
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +77 -0
- data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +109 -0
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +237 -0
- data/lib/hecks/fuzzing/sequence_generator.rb +189 -0
- data/lib/hecks/fuzzing/shrinker.rb +166 -0
- data/lib/hecks/fuzzing/structural_skips.rb +33 -0
- data/lib/hecks/fuzzing/sweep_depth.rb +43 -0
- data/lib/hecks/fuzzing/target_capabilities.rb +163 -0
- data/lib/hecks/fuzzing/value_generator.rb +216 -0
- data/lib/hecks/fuzzing.rb +23 -0
- data/lib/hecks/grammar/evolve.rb +336 -0
- data/lib/hecks/hecks/adapters/codebase/codemods.rb +45 -0
- data/lib/hecks/hecks/adapters/codebase/conformance.rb +71 -0
- data/lib/hecks/hecks/adapters/codebase/corpus_tasks.rb +202 -0
- data/lib/hecks/hecks/adapters/codebase/gem_registry.rb +80 -0
- data/lib/hecks/hecks/adapters/codebase/kernel_tables.rb +54 -0
- data/lib/hecks/hecks/adapters/codebase/language.rb +240 -0
- data/lib/hecks/hecks/adapters/codebase/npm_registry.rb +102 -0
- data/lib/hecks/hecks/adapters/codebase/publishing.rb +142 -0
- data/lib/hecks/hecks/adapters/codebase/regeneration.rb +67 -0
- data/lib/hecks/hecks/adapters/codebase/release_facts.rb +90 -0
- data/lib/hecks/hecks/adapters/codebase/ruby_child.rb +83 -0
- data/lib/hecks/hecks/adapters/codebase/secret_vault.rb +36 -0
- data/lib/hecks/hecks/adapters/codebase/source_tree.adapter +7 -0
- data/lib/hecks/hecks/adapters/codebase/source_tree.rb +189 -0
- data/lib/hecks/hecks/adapters/codebase/sqlite_fixture.rb +71 -0
- data/lib/hecks/hecks/adapters/codebase/style.rb +171 -0
- data/lib/hecks/hecks/adapters/codebase/test_runner.rb +55 -0
- data/lib/hecks/hecks/adapters/codebase/test_suite.rb +165 -0
- data/lib/hecks/hecks/adapters/codebase/tree.rb +111 -0
- data/lib/hecks/hecks/adapters/console_capture.rb +79 -0
- data/lib/hecks/hecks/adapters/deploy_toolchain.adapter +7 -0
- data/lib/hecks/hecks/adapters/deploy_toolchain.rb +108 -0
- data/lib/hecks/hecks/adapters/git.adapter +7 -0
- data/lib/hecks/hecks/adapters/git.rb +102 -0
- data/lib/hecks/hecks/adapters/host_http.adapter +7 -0
- data/lib/hecks/hecks/adapters/host_http.rb +65 -0
- data/lib/hecks/hecks/adapters/in_process_boot.adapter +7 -0
- data/lib/hecks/hecks/adapters/in_process_boot.rb +197 -0
- data/lib/hecks/hecks/adapters/in_process_operations.rb +202 -0
- data/lib/hecks/hecks/adapters/journal_store/changes.rb +143 -0
- data/lib/hecks/hecks/adapters/journal_store/compaction.rb +145 -0
- data/lib/hecks/hecks/adapters/journal_store/edge_audit.rb +132 -0
- data/lib/hecks/hecks/adapters/journal_store/examination.rb +135 -0
- data/lib/hecks/hecks/adapters/journal_store/held_domain.rb +163 -0
- data/lib/hecks/hecks/adapters/journal_store/readings.rb +187 -0
- data/lib/hecks/hecks/adapters/journal_store.adapter +7 -0
- data/lib/hecks/hecks/adapters/journal_store.rb +64 -0
- data/lib/hecks/hecks/adapters/local_files.adapter +7 -0
- data/lib/hecks/hecks/adapters/local_files.rb +46 -0
- data/lib/hecks/hecks/adapters/pg_admin.adapter +7 -0
- data/lib/hecks/hecks/adapters/pg_admin.rb +245 -0
- data/lib/hecks/hecks/adapters/process_pool.adapter +7 -0
- data/lib/hecks/hecks/adapters/process_pool.rb +119 -0
- data/lib/hecks/hecks/adapters/rust_toolchain.adapter +7 -0
- data/lib/hecks/hecks/adapters/rust_toolchain.rb +179 -0
- data/lib/hecks/hecks/adapters/rust_workspace.rb +150 -0
- data/lib/hecks/hecks/adapters/shell.adapter +7 -0
- data/lib/hecks/hecks/adapters/shell.rb +49 -0
- data/lib/hecks/hecks/adapters/terminal.adapter +7 -0
- data/lib/hecks/hecks/adapters/terminal.rb +74 -0
- data/lib/hecks/hecks/codebase.behaviors +172 -0
- data/lib/hecks/hecks/codebase.bluebook +2453 -0
- data/lib/hecks/hecks/context_map.hecksagon +13 -0
- data/lib/hecks/hecks/custodian.behaviors +179 -0
- data/lib/hecks/hecks/custodian.bluebook +1847 -0
- data/lib/hecks/hecks/deploy.behaviors +74 -0
- data/lib/hecks/hecks/deploy.world +12 -0
- data/lib/hecks/hecks/environments/memory.world +10 -0
- data/lib/hecks/hecks/governance.world +7 -0
- data/lib/hecks/hecks/hecks.behaviors +62 -0
- data/lib/hecks/hecks/hecks.bluebook +124 -0
- data/lib/hecks/hecks/hecks.hecksagon +390 -0
- data/lib/hecks/hecks/hecks.world +42 -0
- data/lib/hecks/kernel_capabilities.rb +194 -0
- data/lib/hecks/language/bluebook/bluebook.bluebook +11 -0
- data/lib/hecks/language/bluebook/query.bluebook +7 -0
- data/lib/hecks/language/bluebook/vocabulary.bluebook +4 -4
- data/lib/hecks/language/hecksagon/domain_port.bluebook +10 -0
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +20 -0
- data/lib/hecks/mcp_stdio_guard.rb +2 -2
- data/lib/hecks/persistence_legacy_fixture/regenerate.rb +161 -0
- data/lib/hecks/persistence_legacy_fixture.rb +113 -0
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +8 -5
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +4 -1
- data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +15 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +32 -15
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +6 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +227 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +43 -24
- data/lib/hecks/ports/persistence/plugins/era/translation.rb +1 -0
- data/lib/hecks/ports/persistence/state_codec.rb +24 -5
- data/lib/hecks/projection_files.rb +285 -0
- data/lib/hecks/projections/bootstrap_table.rb +1 -1
- data/lib/hecks/projections/deploy/fargate.rb +5 -5
- data/lib/hecks/projections/deploy/lambda.rb +43 -32
- data/lib/hecks/projections/deploy/preview/preview.sh.erb +1 -1
- data/lib/hecks/projections/deploy/preview/template.rb +1 -1
- data/lib/hecks/projections/deploy/scripts/deploy-service.sh.tmpl +1 -1
- data/lib/hecks/projections/deploy/scripts/expected-era.header +1 -1
- data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +3 -3
- data/lib/hecks/projections/deploy/scripts/smoke-after-deploy.sh.tmpl +1 -1
- data/lib/hecks/projections/deploy/shared.rb +1 -1
- data/lib/hecks/projections/diagrams.rb +3 -3
- data/lib/hecks/projections/model/deviations.rb +1 -1
- data/lib/hecks/projections/parser_table.rb +4 -4
- data/lib/hecks/projections/reference.rb +2 -3
- data/lib/hecks/projections/rust_vocabulary.rb +1 -1
- data/lib/hecks/projector/cli_projector.rb +106 -37
- data/lib/hecks/projector.rb +1 -1
- data/lib/hecks/quality_control/adapters/agent.adapter +7 -0
- data/lib/hecks/quality_control/adapters/agent.rb +58 -0
- data/lib/hecks/quality_control/adapters/angle_tools.adapter +7 -0
- data/lib/hecks/quality_control/adapters/angle_tools.rb +15 -0
- data/lib/hecks/quality_control/adapters/clearance_recorder.rb +36 -0
- data/lib/hecks/quality_control/adapters/clearance_tools.adapter +7 -0
- data/lib/hecks/quality_control/adapters/clearance_tools.rb +17 -0
- data/lib/hecks/quality_control/adapters/git_pr.adapter +7 -0
- data/lib/hecks/quality_control/adapters/git_pr.rb +226 -0
- data/lib/hecks/quality_control/adapters/github_checks.adapter +9 -0
- data/lib/hecks/quality_control/adapters/github_checks.rb +83 -0
- data/lib/hecks/quality_control/adapters/github_ci_webhook.rb +56 -0
- data/lib/hecks/quality_control/adapters/github_issues.adapter +7 -0
- data/lib/hecks/quality_control/adapters/github_issues.rb +49 -0
- data/lib/hecks/quality_control/adapters/qa_tool.rb +59 -0
- data/lib/hecks/quality_control/adapters/sweep_tools.adapter +7 -0
- data/lib/hecks/quality_control/adapters/sweep_tools.rb +51 -0
- data/lib/hecks/quality_control/adapters/target_tools.adapter +7 -0
- data/lib/hecks/quality_control/adapters/target_tools.rb +43 -0
- data/lib/hecks/quality_control/cli/child.rb +45 -0
- data/lib/hecks/quality_control/cli/qa_concurrency_racer.rb +45 -0
- data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +248 -0
- data/lib/hecks/quality_control/cli/qa_domain_novelty.rb +163 -0
- data/lib/hecks/quality_control/cli/qa_generated_domains.rb +450 -0
- data/lib/hecks/quality_control/cli/qa_log_bug.rb +195 -0
- data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +220 -0
- data/lib/hecks/quality_control/cli/qa_open_pr.rb +305 -0
- data/lib/hecks/quality_control/cli/qa_postgres_migrate.rb +154 -0
- data/lib/hecks/quality_control/cli/qa_postgres_role.rb +159 -0
- data/lib/hecks/quality_control/cli/qa_pr_check.rb +232 -0
- data/lib/hecks/quality_control/cli/qa_seed_angles.rb +142 -0
- data/lib/hecks/quality_control/cli/qa_seed_targets.rb +69 -0
- data/lib/hecks/quality_control/cli/qa_sweep/all_mode.rb +280 -0
- data/lib/hecks/quality_control/cli/qa_sweep/checks.rb +329 -0
- data/lib/hecks/quality_control/cli/qa_sweep/dials.rb +92 -0
- data/lib/hecks/quality_control/cli/qa_sweep/finding_report.rb +117 -0
- data/lib/hecks/quality_control/cli/qa_sweep/parsing.rb +175 -0
- data/lib/hecks/quality_control/cli/qa_sweep/release_mode.rb +60 -0
- data/lib/hecks/quality_control/cli/qa_sweep.rb +611 -0
- data/lib/hecks/quality_control/cli/qa_tick.rb +274 -0
- data/lib/hecks/quality_control/quality_control.bluebook +2224 -0
- data/lib/hecks/quality_control/quality_control.hecksagon +13 -0
- data/lib/hecks/quality_control/quality_control.ports.hecksagon +54 -0
- data/lib/hecks/query_ir.rb +346 -0
- data/lib/hecks/query_ir_mcp.rb +198 -0
- data/lib/hecks/release/runner/ci_publisher.rb +2 -2
- data/lib/hecks/release/runner/clean_tree.rb +42 -0
- data/lib/hecks/release/runner/console.rb +6 -0
- data/lib/hecks/release/runner/gem_publisher.rb +14 -7
- data/lib/hecks/release/runner/npm_publisher.rb +6 -36
- data/lib/hecks/release/runner/preflight.rb +26 -2
- data/lib/hecks/release/runner/published.rb +8 -26
- data/lib/hecks/release/runner/tagger.rb +30 -15
- data/lib/hecks/release/runner.rb +57 -6
- data/lib/hecks/router/namespace_installer.rb +3 -3
- data/lib/hecks/runtime/adapter_lookup.rb +124 -0
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +4 -0
- data/lib/hecks/runtime/dependency_planning.rb +20 -2
- data/lib/hecks/runtime/dispatcher.rb +53 -3
- data/lib/hecks/runtime/interpreting.rb +3 -0
- data/lib/hecks/runtime/loader.rb +65 -12
- data/lib/hecks/runtime/outbox.rb +1 -1
- data/lib/hecks/runtime/policy_interpreter.rb +5 -2
- data/lib/hecks/runtime/port_operation_interpreter.rb +2 -10
- data/lib/hecks/runtime/query_interpreter.rb +77 -0
- data/lib/hecks/runtime/reaction_outcome.rb +89 -0
- data/lib/hecks/runtime/refusal_wording.rb +23 -1
- data/lib/hecks/runtime/registry/verification.rb +163 -4
- data/lib/hecks/runtime/registry.rb +57 -3
- data/lib/hecks/runtime/remote_dispatcher.rb +31 -2
- data/lib/hecks/runtime/value/coercion.rb +23 -135
- data/lib/hecks/runtime/value/field_checks.rb +172 -0
- data/lib/hecks/runtime/value.rb +4 -1
- data/lib/hecks/runtime.rb +20 -6
- data/lib/hecks/rust_build/conformance.rb +158 -0
- data/lib/hecks/rust_build/conformance_fuzz.rb +71 -0
- data/lib/hecks/rust_build/coverage.rb +218 -0
- data/lib/hecks/rust_build/native_build.rb +124 -0
- data/lib/hecks/rust_build/project_rust/root_mod.rb +120 -0
- data/lib/hecks/rust_build/project_rust.rb +332 -0
- data/lib/hecks/rust_build/wasm.rb +105 -0
- data/lib/hecks/rust_build/wasm_browser.rb +113 -0
- data/lib/hecks/rust_build.rb +144 -0
- data/lib/hecks/storehouse.rb +16 -16
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +2 -2
- data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +1 -1
- data/lib/hecks/three_zero/forms.yml +127 -0
- data/lib/hecks/three_zero.rb +12 -0
- data/lib/hecks/tools/argument_gate_matrix.rb +316 -0
- data/lib/hecks/tools/comment_style.rb +1070 -0
- data/lib/hecks/tools/corpus_report.rb +87 -0
- data/lib/hecks/tools/deploy_recipe.rb +255 -0
- data/lib/hecks/tools/deploy_recipe_lint.rb +282 -0
- data/lib/hecks/tools/drop_implicit_append_fields.rb +95 -0
- data/lib/hecks/tools/era_projection_backfill.rb +94 -0
- data/lib/hecks/tools/era_reattest.rb +146 -0
- data/lib/hecks/tools/evolve_run.rb +189 -0
- data/lib/hecks/tools/fuzz_sweep.rb +373 -0
- data/lib/hecks/tools/heki_compaction.rb +100 -0
- data/lib/hecks/tools/hoist_local_givens.rb +246 -0
- data/lib/hecks/tools/journal_compaction.rb +89 -0
- data/lib/hecks/tools/oidc_manifests.rb +70 -0
- data/lib/hecks/tools/query_ir_run.rb +81 -0
- data/lib/hecks/tools/regeneration_run.rb +166 -0
- data/lib/hecks/tools/rust_comment_style.rb +642 -0
- data/lib/hecks/tools/sequence_script.rb +62 -0
- data/lib/hecks/tools/tail_merge.rb +117 -0
- data/lib/hecks/tools/tenant_provisioning.rb +142 -0
- data/lib/hecks/tools/translation_audit.rb +208 -0
- data/lib/hecks/tools/translation_scaffold.rb +131 -0
- data/lib/hecks/tools.rb +86 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks.rb +32 -15
- data/qa/settings.yml +66 -0
- data/rust/.cargo/config.toml +6 -0
- data/rust/.gitignore +2 -0
- data/rust/Cargo.lock +54 -0
- data/rust/Cargo.toml +61 -0
- data/rust/build/.cargo/config.toml +3 -0
- data/rust/build/.gitignore +1 -0
- data/rust/build/Cargo.lock +7 -0
- data/rust/build/Cargo.toml +19 -0
- data/rust/build/src/build_artifact.rs +47 -0
- data/rust/build/src/cargo_sync.rs +248 -0
- data/rust/build/src/json.rs +421 -0
- data/rust/build/src/lineage_pass.rs +285 -0
- data/rust/build/src/main.rs +51 -0
- data/rust/build/src/optional_pass.rs +150 -0
- data/rust/build/src/pipeline.rs +264 -0
- data/rust/build/src/reserved_names.rs +76 -0
- data/rust/build/src/resolve.rs +124 -0
- data/rust/build/src/root.rs +25 -0
- data/rust/build/src/sidecars.rs +73 -0
- data/rust/build/src/subprocess.rs +51 -0
- data/rust/build/src/tmp.rs +31 -0
- data/rust/codegen/.cargo/config.toml +3 -0
- data/rust/codegen/.gitignore +1 -0
- data/rust/codegen/Cargo.lock +7 -0
- data/rust/codegen/Cargo.toml +20 -0
- data/rust/codegen/src/attr.rs +32 -0
- data/rust/codegen/src/bridging.rs +481 -0
- data/rust/codegen/src/commands.rs +1226 -0
- data/rust/codegen/src/constraints.rs +109 -0
- data/rust/codegen/src/dependency_planning.rs +209 -0
- data/rust/codegen/src/domain_generator.rs +1288 -0
- data/rust/codegen/src/exemplar.rs +230 -0
- data/rust/codegen/src/expr_emitter.rs +87 -0
- data/rust/codegen/src/fielded.rs +192 -0
- data/rust/codegen/src/hecks_naming.rs +33 -0
- data/rust/codegen/src/json.rs +335 -0
- data/rust/codegen/src/json_codec.rs +697 -0
- data/rust/codegen/src/literal.rs +207 -0
- data/rust/codegen/src/main.rs +290 -0
- data/rust/codegen/src/manifest.rs +227 -0
- data/rust/codegen/src/mutations.rs +784 -0
- data/rust/codegen/src/naming.rs +286 -0
- data/rust/codegen/src/ports.rs +208 -0
- data/rust/codegen/src/prelude.rs +116 -0
- data/rust/codegen/src/queries.rs +786 -0
- data/rust/codegen/src/reactions.rs +532 -0
- data/rust/codegen/src/read_models.rs +697 -0
- data/rust/codegen/src/reference_specs.rs +40 -0
- data/rust/codegen/src/registry.rs +966 -0
- data/rust/codegen/src/reserved_names.rs +76 -0
- data/rust/codegen/src/shared.rs +93 -0
- data/rust/codegen/src/skip_reason.rs +25 -0
- data/rust/codegen/src/types.rs +362 -0
- data/rust/host/.cargo/config.toml +3 -0
- data/rust/host/.gitignore +1 -0
- data/rust/host/Cargo.lock +4297 -0
- data/rust/host/Cargo.toml +61 -0
- data/rust/host/HECKS_RELEASE +1 -0
- data/rust/host/rds-ca-bundle.pem +2736 -0
- data/rust/host/src/api.rs +1596 -0
- data/rust/host/src/approval.rs +652 -0
- data/rust/host/src/auth.rs +1287 -0
- data/rust/host/src/bin/expr_harness.rs +46 -0
- data/rust/host/src/bin/lineage_harness.rs +119 -0
- data/rust/host/src/bin/mint_harness.rs +135 -0
- data/rust/host/src/checkout.rs +265 -0
- data/rust/host/src/dispatch.rs +1443 -0
- data/rust/host/src/expr_json.rs +1117 -0
- data/rust/host/src/field_hints.rs +52 -0
- data/rust/host/src/ir.rs +733 -0
- data/rust/host/src/journal.rs +874 -0
- data/rust/host/src/lambda_client.rs +378 -0
- data/rust/host/src/log.rs +147 -0
- data/rust/host/src/main.rs +474 -0
- data/rust/host/src/mint.rs +1865 -0
- data/rust/host/src/payments/keystore.rs +330 -0
- data/rust/host/src/payments/tests.rs +1400 -0
- data/rust/host/src/payments.rs +767 -0
- data/rust/host/src/presentation.rs +698 -0
- data/rust/host/src/presentation_write.rs +1431 -0
- data/rust/host/src/query_step.rs +149 -0
- data/rust/host/src/rate_limit.rs +904 -0
- data/rust/host/src/reference_transform.rs +241 -0
- data/rust/host/src/reference_validate.rs +338 -0
- data/rust/host/src/resend.rs +296 -0
- data/rust/host/src/secrets.rs +106 -0
- data/rust/host/src/server.rs +663 -0
- data/rust/host/src/storage_shape.rs +175 -0
- data/rust/host/src/test_pg.rs +40 -0
- data/rust/host/src/ui_schema.rs +1351 -0
- data/rust/host/src/wasm_runner.rs +191 -0
- data/rust/host/src/web/newsletter/cooldown.rs +126 -0
- data/rust/host/src/web/newsletter/tests.rs +104 -0
- data/rust/host/src/web/newsletter.rs +310 -0
- data/rust/host/src/web/newsletter_send.rs +357 -0
- data/rust/host/src/web/registrations.rs +1505 -0
- data/rust/host/src/web.rs +2937 -0
- data/rust/lsp/.gitignore +1 -0
- data/rust/lsp/Cargo.lock +7 -0
- data/rust/lsp/Cargo.toml +16 -0
- data/rust/lsp/README.md +133 -0
- data/rust/lsp/src/diagnostics.rs +165 -0
- data/rust/lsp/src/json.rs +385 -0
- data/rust/lsp/src/main.rs +399 -0
- data/rust/lsp/src/outline.rs +177 -0
- data/rust/lsp/src/rpc.rs +67 -0
- data/rust/parser/.cargo/config.toml +5 -0
- data/rust/parser/.gitignore +1 -0
- data/rust/parser/Cargo.lock +7 -0
- data/rust/parser/Cargo.toml +21 -0
- data/rust/parser/src/build/closed_sets.rs +27 -0
- data/rust/parser/src/build/identity.rs +276 -0
- data/rust/parser/src/build/mod.rs +12 -0
- data/rust/parser/src/build/naming.rs +141 -0
- data/rust/parser/src/build/pattern_subset.rs +161 -0
- data/rust/parser/src/build/query_derive.rs +72 -0
- data/rust/parser/src/build/query_inference.rs +269 -0
- data/rust/parser/src/build/query_options.rs +100 -0
- data/rust/parser/src/build/read_model.rs +94 -0
- data/rust/parser/src/build/references.rs +32 -0
- data/rust/parser/src/canonical.rs +114 -0
- data/rust/parser/src/diag.rs +55 -0
- data/rust/parser/src/emit.rs +953 -0
- data/rust/parser/src/expr/ast_json.rs +147 -0
- data/rust/parser/src/expr/evaluator.rs +81 -0
- data/rust/parser/src/expr/mod.rs +84 -0
- data/rust/parser/src/expr/resolver.rs +497 -0
- data/rust/parser/src/ir.rs +387 -0
- data/rust/parser/src/keywords.rs +585 -0
- data/rust/parser/src/lex.rs +662 -0
- data/rust/parser/src/main.rs +252 -0
- data/rust/parser/src/parse/aggregate.rs +612 -0
- data/rust/parser/src/parse/chapter.rs +517 -0
- data/rust/parser/src/parse/command.rs +637 -0
- data/rust/parser/src/parse/domain_port.rs +127 -0
- data/rust/parser/src/parse/entity.rs +527 -0
- data/rust/parser/src/parse/file.rs +67 -0
- data/rust/parser/src/parse/hecksagon.rs +231 -0
- data/rust/parser/src/parse/lifecycle.rs +162 -0
- data/rust/parser/src/parse/mod.rs +1262 -0
- data/rust/parser/src/parse/policy.rs +87 -0
- data/rust/parser/src/parse/process_manager.rs +305 -0
- data/rust/parser/src/parse/query.rs +100 -0
- data/rust/parser/src/parse/read_model.rs +214 -0
- data/rust/parser/src/parse/value_object.rs +250 -0
- data/rust/parser/src/ruby_value.rs +422 -0
- data/rust/project/bridging.rb +257 -0
- data/rust/project/commands.rb +877 -0
- data/rust/project/constraints.rb +86 -0
- data/rust/project/dependency_planning.rb +178 -0
- data/rust/project/domain_generator.rb +980 -0
- data/rust/project/exemplar.rb +148 -0
- data/rust/project/expr_emitter.rb +72 -0
- data/rust/project/fielded.rb +141 -0
- data/rust/project/json_codec.rb +641 -0
- data/rust/project/mutations.rb +721 -0
- data/rust/project/naming.rb +205 -0
- data/rust/project/ports.rb +65 -0
- data/rust/project/queries.rb +493 -0
- data/rust/project/reactions.rb +317 -0
- data/rust/project/read_models.rb +690 -0
- data/rust/project/reference_specs.rb +27 -0
- data/rust/project/registry.rb +396 -0
- data/rust/project/skip_reason.rb +29 -0
- data/rust/project/types.rb +225 -0
- data/rust/project/write_if_changed.rb +83 -0
- data/rust/project.rb +30 -0
- data/rust/project_rust_pipeline.rb +383 -0
- data/rust/src/exemplar/commands.rs +279 -0
- data/rust/src/exemplar/constraints.rs +18 -0
- data/rust/src/exemplar/fielded.rs +280 -0
- data/rust/src/exemplar/json.rs +277 -0
- data/rust/src/exemplar/mod.rs +17 -0
- data/rust/src/exemplar/mutations.rs +80 -0
- data/rust/src/exemplar/queries.rs +33 -0
- data/rust/src/exemplar/reactions.rs +88 -0
- data/rust/src/exemplar/read_models.rs +48 -0
- data/rust/src/exemplar/registry.rs +166 -0
- data/rust/src/exemplar/types.rs +44 -0
- data/rust/src/kernel/attribute_shapes/composite.rs +88 -0
- data/rust/src/kernel/attribute_shapes/list.rs +18 -0
- data/rust/src/kernel/attribute_shapes/mod.rs +44 -0
- data/rust/src/kernel/attribute_shapes/optional.rs +9 -0
- data/rust/src/kernel/attribute_shapes/scalar.rs +39 -0
- data/rust/src/kernel/cli.rs +585 -0
- data/rust/src/kernel/dispatch.rs +1090 -0
- data/rust/src/kernel/expr.rs +458 -0
- data/rust/src/kernel/expression_operators/arithmetic.rs +128 -0
- data/rust/src/kernel/expression_operators/comparison.rs +48 -0
- data/rust/src/kernel/expression_operators/enumeration.rs +232 -0
- data/rust/src/kernel/expression_operators/logical.rs +13 -0
- data/rust/src/kernel/expression_operators/membership.rs +18 -0
- data/rust/src/kernel/expression_operators/mod.rs +68 -0
- data/rust/src/kernel/expression_operators/pattern_match.rs +73 -0
- data/rust/src/kernel/expression_operators/positional.rs +126 -0
- data/rust/src/kernel/expression_operators/presence.rs +101 -0
- data/rust/src/kernel/expression_operators/sign_test.rs +16 -0
- data/rust/src/kernel/expression_operators/sized.rs +35 -0
- data/rust/src/kernel/expression_operators/text.rs +79 -0
- data/rust/src/kernel/expression_operators/to_string.rs +18 -0
- data/rust/src/kernel/json.rs +779 -0
- data/rust/src/kernel/mod.rs +133 -0
- data/rust/src/kernel/named_query.rs +219 -0
- data/rust/src/kernel/orchestrate.rs +1810 -0
- data/rust/src/kernel/pattern.rs +307 -0
- data/rust/src/kernel/query_comparators.rs +206 -0
- data/rust/src/kernel/query_ordering.rs +103 -0
- data/rust/src/kernel/read_model.rs +914 -0
- data/rust/src/kernel/reference_lookup.rs +149 -0
- data/rust/src/kernel/repository.rs +468 -0
- data/rust/src/kernel/routing.rs +612 -0
- data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +118 -0
- data/rust/src/kernel/vocab/entity_dispatch_order.rs +114 -0
- data/rust/src/kernel/vocab/field_hint.rs +70 -0
- data/rust/src/kernel/vocab/mod.rs +20 -0
- data/rust/src/kernel/vocab/query_comparator.rs +65 -0
- data/rust/src/kernel/vocab/refusal_template.rs +1592 -0
- data/rust/src/lib.rs +8 -0
- data/rust/src/main.rs +16 -0
- data/rust/web/.gitignore +1 -0
- data/rust/web/Cargo.lock +119 -0
- data/rust/web/Cargo.toml +18 -0
- data/rust/web/src/lib.rs +8 -0
- metadata +477 -12
- data/lib/hecks/facade/cli_door.rb +0 -96
- data/lib/hecks/facade/cli_runner.rb +0 -127
- data/lib/hecks/mcp_door_scope.rb +0 -95
|
@@ -0,0 +1,1090 @@
|
|
|
1
|
+
//! Generic dispatch loops over the declared `AggregateStep`/`EntityStep` order.
|
|
2
|
+
//! Argument gates run in `decode_*_arguments`, before the router resolves identity.
|
|
3
|
+
|
|
4
|
+
use super::expr::{interpret, EvalContext, Expr, Field, Fielded, NoFields, StateFirst, Value, WithOld, WithParent};
|
|
5
|
+
use super::refusal_wording::{
|
|
6
|
+
AlreadyExistsCreatingDuplicateArgs, LifecycleRefusedTransitionBlockedArgs, NotFoundCreatingNoIdentityArgs, NotFoundEntityElementMissingArgs,
|
|
7
|
+
NotFoundRecordMissingArgs,
|
|
8
|
+
};
|
|
9
|
+
use super::vocab::{AggregateStep, EntityStep};
|
|
10
|
+
use super::{Event, Json, MutationRecord, Refusal, Repository, SetProjectedField, ToJson};
|
|
11
|
+
|
|
12
|
+
pub struct GivenSpec {
|
|
13
|
+
pub description: &'static str,
|
|
14
|
+
pub expr: Expr,
|
|
15
|
+
// Carries the event name a `corrects` given checks, so the refusal can interpolate the
|
|
16
|
+
// record's id (the static `description` cannot). `None` for every ordinary given.
|
|
17
|
+
pub corrects_event: Option<&'static str>,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/// An `ensures` rule, checked after `apply_mutations`, with `old` bound to the prior state.
|
|
21
|
+
pub struct EnsuresSpec {
|
|
22
|
+
pub description: &'static str,
|
|
23
|
+
pub expr: Expr,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/// An invariant rule, checked after `ensures` and before save on the candidate record.
|
|
27
|
+
/// Entity invariants run per list element with `parent` bound to the owner.
|
|
28
|
+
pub struct InvariantSpec {
|
|
29
|
+
pub description: &'static str,
|
|
30
|
+
pub expr: Expr,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
pub struct EntityInvariants {
|
|
34
|
+
pub name: &'static str,
|
|
35
|
+
pub list_field: &'static str,
|
|
36
|
+
pub specs: Vec<InvariantSpec>,
|
|
37
|
+
pub nested: Vec<EntityInvariants>,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
pub struct InvariantSet {
|
|
41
|
+
pub aggregate: Vec<InvariantSpec>,
|
|
42
|
+
pub entities: Vec<EntityInvariants>,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
pub fn enforce_invariants(record: &dyn Fielded, aggregate_name: &str, set: &InvariantSet) -> Result<(), Refusal> {
|
|
46
|
+
for rule in &set.aggregate {
|
|
47
|
+
let ctx = EvalContext { args: &NoFields, instance: record };
|
|
48
|
+
if !interpret(&rule.expr, &ctx)?.truthy() {
|
|
49
|
+
return Err(Refusal::InvariantViolation(format!("{aggregate_name} refused — {}", rule.description)));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
for entity in &set.entities {
|
|
53
|
+
enforce_entity_invariants(record, entity)?;
|
|
54
|
+
}
|
|
55
|
+
Ok(())
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
fn enforce_entity_invariants(owner: &dyn Fielded, entity: &EntityInvariants) -> Result<(), Refusal> {
|
|
59
|
+
let Some(elements) = owner.items(entity.list_field) else { return Ok(()) };
|
|
60
|
+
for element in elements {
|
|
61
|
+
let Field::Nested(element) = element else { continue };
|
|
62
|
+
let with_parent = WithParent { args: &NoFields, parent: owner };
|
|
63
|
+
for rule in &entity.specs {
|
|
64
|
+
let ctx = EvalContext { args: &with_parent, instance: element };
|
|
65
|
+
if !interpret(&rule.expr, &ctx)?.truthy() {
|
|
66
|
+
return Err(Refusal::InvariantViolation(format!("{} refused — {}", entity.name, rule.description)));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
for nested in &entity.nested {
|
|
70
|
+
enforce_entity_invariants(element, nested)?;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
Ok(())
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// The check half of a lifecycle transition: the current state must be in `from_states`.
|
|
77
|
+
///
|
|
78
|
+
/// The write half is folded into the generated `apply_mutations` closure as its last line.
|
|
79
|
+
pub struct TransitionCheck {
|
|
80
|
+
pub field: &'static str,
|
|
81
|
+
pub from_states: &'static [&'static str],
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// How a dispatch obtains its starting record: `Create` when the command declares no
|
|
85
|
+
/// `references`, `Act` when it addresses an existing record.
|
|
86
|
+
///
|
|
87
|
+
/// `'a` lets `build` borrow `args`, which is also borrowed as `&dyn Fielded` in the same call.
|
|
88
|
+
pub enum Hydrate<'a, T> {
|
|
89
|
+
/// A creating command: mint the identity, build a fresh record if nothing answers to it.
|
|
90
|
+
/// `build` assigns creation attributes (implicit, name-matched), not a `sets`.
|
|
91
|
+
///
|
|
92
|
+
/// `state_independent` defers the `AlreadyExists` check to just before `repo.save`, for
|
|
93
|
+
/// commands whose givens, ensures and mutations never read the aggregate's own state.
|
|
94
|
+
Create { id: String, build: Box<dyn FnOnce() -> T + 'a>, state_independent: bool },
|
|
95
|
+
/// An acting command: the identity already names a record.
|
|
96
|
+
Act { id: String },
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
#[allow(clippy::too_many_arguments)]
|
|
100
|
+
pub fn dispatch<'a, T, R>(
|
|
101
|
+
repo: &mut R,
|
|
102
|
+
hydrate: Hydrate<'a, T>,
|
|
103
|
+
command_name: &'static str,
|
|
104
|
+
aggregate_qualified_name: &'static str,
|
|
105
|
+
// Bare `hecks_name` and the declared identity reading, as refusal wording quotes them;
|
|
106
|
+
// `aggregate_qualified_name` (`{domain}::{aggregate}`) is what events and records use.
|
|
107
|
+
aggregate_name: &'static str,
|
|
108
|
+
identity_reading: &'static str,
|
|
109
|
+
args: &'a dyn Fielded,
|
|
110
|
+
givens: &[GivenSpec],
|
|
111
|
+
transition: Option<TransitionCheck>,
|
|
112
|
+
// Captures the typed args struct by reference (not `move`, like `Hydrate::Create.build`);
|
|
113
|
+
// writing a typed field needs the real type, which `&dyn Fielded` erases.
|
|
114
|
+
apply_mutations: impl FnOnce(&mut T) -> Result<(), Refusal> + 'a,
|
|
115
|
+
ensures: &[EnsuresSpec],
|
|
116
|
+
invariants: &InvariantSet,
|
|
117
|
+
emits: &[&'static str],
|
|
118
|
+
payload: Json,
|
|
119
|
+
mutations: &mut Vec<MutationRecord>,
|
|
120
|
+
// The synchronous half of `projects`, applied right before `repo.save`.
|
|
121
|
+
seed_projections: Vec<(&'static str, Option<String>)>,
|
|
122
|
+
// The write-side tenant boundary. The router computes it (it needs every aggregate's repo)
|
|
123
|
+
// but it is applied here, first in `Save`, before `seed_projections`.
|
|
124
|
+
tenant_boundary_check: Result<(), Refusal>,
|
|
125
|
+
) -> Result<(T, Vec<Event>), Refusal>
|
|
126
|
+
where
|
|
127
|
+
T: Fielded + Clone + ToJson + SetProjectedField,
|
|
128
|
+
R: Repository<T>,
|
|
129
|
+
{
|
|
130
|
+
// Set by a state-independent `Hydrate::Create`; checked in `Save`, just before `repo.save`.
|
|
131
|
+
let mut defer_existence_check = false;
|
|
132
|
+
|
|
133
|
+
// Each owned or `FnOnce` input is consumed by one arm via `take()`; the const assertions
|
|
134
|
+
// below guarantee `hydrated` is set before any arm reads it.
|
|
135
|
+
let mut hydrate = Some(hydrate);
|
|
136
|
+
let mut apply_mutations = Some(apply_mutations);
|
|
137
|
+
let mut tenant_boundary_check = Some(tenant_boundary_check);
|
|
138
|
+
let mut seed_projections = Some(seed_projections);
|
|
139
|
+
let mut hydrated: Option<(String, T)> = None;
|
|
140
|
+
let mut old_snapshot: Option<T> = None;
|
|
141
|
+
let mut events: Vec<Event> = Vec::new();
|
|
142
|
+
|
|
143
|
+
for step in AggregateStep::ORDER {
|
|
144
|
+
#[deny(clippy::wildcard_enum_match_arm)]
|
|
145
|
+
match step {
|
|
146
|
+
// Already run by `decode_aggregate_arguments`; see `aggregate_step_site`.
|
|
147
|
+
// `aggregate_step_site`.
|
|
148
|
+
AggregateStep::DecodeArguments
|
|
149
|
+
| AggregateStep::RefuseUnknownArguments
|
|
150
|
+
| AggregateStep::RefuseAbsentArguments
|
|
151
|
+
| AggregateStep::NormalizeArgs
|
|
152
|
+
| AggregateStep::RefuseRoleMismatch
|
|
153
|
+
| AggregateStep::ResolveReferences => {}
|
|
154
|
+
AggregateStep::Hydrate => {
|
|
155
|
+
let hydrate = hydrate.take().expect(ONCE);
|
|
156
|
+
hydrated = Some(hydrate_record(
|
|
157
|
+
repo,
|
|
158
|
+
hydrate,
|
|
159
|
+
command_name,
|
|
160
|
+
aggregate_name,
|
|
161
|
+
identity_reading,
|
|
162
|
+
&mut defer_existence_check,
|
|
163
|
+
)?);
|
|
164
|
+
}
|
|
165
|
+
AggregateStep::EnforceGivens => {
|
|
166
|
+
let (id, record) = hydrated.as_ref().expect(HYDRATED);
|
|
167
|
+
enforce_givens(record, args, givens, command_name, aggregate_qualified_name, id)?;
|
|
168
|
+
}
|
|
169
|
+
AggregateStep::AdmissibleTransition => {
|
|
170
|
+
let (_, record) = hydrated.as_ref().expect(HYDRATED);
|
|
171
|
+
admissible_transition(record, transition.as_ref(), command_name)?;
|
|
172
|
+
}
|
|
173
|
+
// Folded into `Hydrate::Create.build`: givens already saw the built record.
|
|
174
|
+
AggregateStep::AssignCreationAttributes => {}
|
|
175
|
+
AggregateStep::ApplyMutations => {
|
|
176
|
+
let (_, record) = hydrated.as_mut().expect(HYDRATED);
|
|
177
|
+
// The state as the givens saw it — `old` inside an `ensures`,
|
|
178
|
+
// `old` for `ensures`: the state before the mutation, cloned only if needed.
|
|
179
|
+
old_snapshot = if ensures.is_empty() { None } else { Some(record.clone()) };
|
|
180
|
+
(apply_mutations.take().expect(ONCE))(record)?;
|
|
181
|
+
}
|
|
182
|
+
// Written by the generated `apply_mutations` closure as its last line.
|
|
183
|
+
AggregateStep::AdvanceLifecycle => {}
|
|
184
|
+
// Performed inside the generated `apply_mutations` closure, which calls
|
|
185
|
+
// `apply_entity_command` on the record in hand.
|
|
186
|
+
AggregateStep::DelegateToEntity => {}
|
|
187
|
+
AggregateStep::EnforceEnsures => {
|
|
188
|
+
let (_, record) = hydrated.as_ref().expect(HYDRATED);
|
|
189
|
+
if let Some(old) = &old_snapshot {
|
|
190
|
+
enforce_ensures(record, old, args, ensures, command_name)?;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
AggregateStep::EnforceInvariants => {
|
|
194
|
+
let (_, record) = hydrated.as_ref().expect(HYDRATED);
|
|
195
|
+
enforce_invariants(record, aggregate_name, invariants)?;
|
|
196
|
+
}
|
|
197
|
+
AggregateStep::Save => {
|
|
198
|
+
let (id, record) = hydrated.as_mut().expect(HYDRATED);
|
|
199
|
+
// Tenant boundary first: it precedes `seed_projections` and the write.
|
|
200
|
+
tenant_boundary_check.take().expect(ONCE)?;
|
|
201
|
+
|
|
202
|
+
for (field, value) in seed_projections.take().expect(ONCE) {
|
|
203
|
+
record.set_projected_field(field, value);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Deferred `AlreadyExists` check for state-independent creates: runs after
|
|
207
|
+
// every other step, immediately before the write.
|
|
208
|
+
if defer_existence_check && repo.find(id.as_str()).is_some() {
|
|
209
|
+
return Err(Refusal::AlreadyExists(
|
|
210
|
+
AlreadyExistsCreatingDuplicateArgs {
|
|
211
|
+
command: command_name,
|
|
212
|
+
aggregate: aggregate_name,
|
|
213
|
+
identity: identity_reading,
|
|
214
|
+
offered: &format!("{id:?}"),
|
|
215
|
+
}
|
|
216
|
+
.render_args(),
|
|
217
|
+
));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
persist(repo, id, record, aggregate_qualified_name, mutations);
|
|
221
|
+
}
|
|
222
|
+
AggregateStep::Emit => {
|
|
223
|
+
let (id, _) = hydrated.as_ref().expect(HYDRATED);
|
|
224
|
+
events = emitted(emits, aggregate_qualified_name, id, &payload);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
let (_, record) = hydrated.expect(HYDRATED);
|
|
230
|
+
Ok((record, events))
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/// `hydrate` — how this dispatch obtains its starting record (see
|
|
234
|
+
/// `Hydrate`'s own comment for the branch it takes).
|
|
235
|
+
fn hydrate_record<T, R>(
|
|
236
|
+
repo: &R,
|
|
237
|
+
hydrate: Hydrate<'_, T>,
|
|
238
|
+
command_name: &'static str,
|
|
239
|
+
aggregate_name: &'static str,
|
|
240
|
+
identity_reading: &'static str,
|
|
241
|
+
defer_existence_check: &mut bool,
|
|
242
|
+
) -> Result<(String, T), Refusal>
|
|
243
|
+
where
|
|
244
|
+
T: Clone,
|
|
245
|
+
R: Repository<T>,
|
|
246
|
+
{
|
|
247
|
+
match hydrate {
|
|
248
|
+
Hydrate::Create { id, build, state_independent } => {
|
|
249
|
+
// A blank identity names nothing; refuse before any lookup rather than mint a
|
|
250
|
+
// record keyed by "".
|
|
251
|
+
if id.is_empty() {
|
|
252
|
+
return Err(Refusal::NotFound(
|
|
253
|
+
NotFoundCreatingNoIdentityArgs { command: command_name, aggregate: aggregate_name, identity: identity_reading }
|
|
254
|
+
.render_args(),
|
|
255
|
+
));
|
|
256
|
+
}
|
|
257
|
+
// State-independent creates defer this check to `Save`.
|
|
258
|
+
if state_independent {
|
|
259
|
+
*defer_existence_check = true;
|
|
260
|
+
} else if repo.find(&id).is_some() {
|
|
261
|
+
return Err(Refusal::AlreadyExists(
|
|
262
|
+
AlreadyExistsCreatingDuplicateArgs {
|
|
263
|
+
command: command_name,
|
|
264
|
+
aggregate: aggregate_name,
|
|
265
|
+
identity: identity_reading,
|
|
266
|
+
offered: &format!("{id:?}"),
|
|
267
|
+
}
|
|
268
|
+
.render_args(),
|
|
269
|
+
));
|
|
270
|
+
}
|
|
271
|
+
Ok((id, build()))
|
|
272
|
+
}
|
|
273
|
+
Hydrate::Act { id } => {
|
|
274
|
+
// Shared with `dispatch_entity`'s parent lookup: both raise `record_missing`.
|
|
275
|
+
let record = repo.find(&id).ok_or_else(|| {
|
|
276
|
+
Refusal::NotFound(
|
|
277
|
+
NotFoundRecordMissingArgs { aggregate: aggregate_name, identity: identity_reading, offered: &format!("{id:?}") }
|
|
278
|
+
.render_args(),
|
|
279
|
+
)
|
|
280
|
+
})?;
|
|
281
|
+
Ok((id, record))
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/// `enforce_givens` on an aggregate record.
|
|
287
|
+
fn enforce_givens(
|
|
288
|
+
record: &dyn Fielded,
|
|
289
|
+
args: &dyn Fielded,
|
|
290
|
+
givens: &[GivenSpec],
|
|
291
|
+
command_name: &str,
|
|
292
|
+
aggregate_qualified_name: &str,
|
|
293
|
+
id: &str,
|
|
294
|
+
) -> Result<(), Refusal> {
|
|
295
|
+
for given in givens {
|
|
296
|
+
let ctx = EvalContext { args, instance: record };
|
|
297
|
+
if !interpret(&given.expr, &ctx)?.truthy() {
|
|
298
|
+
// The `corrects` wording interpolates the record id, so it is built here.
|
|
299
|
+
if let Some(event_name) = given.corrects_event {
|
|
300
|
+
// `NothingToCorrect`, not `GivenNotMet`: the conformance corpus compares kinds.
|
|
301
|
+
return Err(Refusal::NothingToCorrect(format!(
|
|
302
|
+
"{command_name} refused — corrects {event_name}, but {aggregate_qualified_name} #{id} has never emitted it"
|
|
303
|
+
)));
|
|
304
|
+
}
|
|
305
|
+
return Err(Refusal::GivenNotMet(format!("{command_name} refused — {}", given.description)));
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
Ok(())
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/// `admissible_transition` — shared by the aggregate record and an entity
|
|
312
|
+
/// element, which check it identically.
|
|
313
|
+
fn admissible_transition(instance: &dyn Fielded, transition: Option<&TransitionCheck>, command_name: &str) -> Result<(), Refusal> {
|
|
314
|
+
let Some(check) = transition else { return Ok(()) };
|
|
315
|
+
match instance.field(check.field) {
|
|
316
|
+
Some(Field::Value(Value::Str(current))) => {
|
|
317
|
+
if !check.from_states.contains(¤t.as_str()) {
|
|
318
|
+
// `allowed` is `from_states`; `render_args` quotes each and joins with " or ".
|
|
319
|
+
return Err(Refusal::LifecycleRefused(
|
|
320
|
+
LifecycleRefusedTransitionBlockedArgs {
|
|
321
|
+
command: command_name,
|
|
322
|
+
field: check.field,
|
|
323
|
+
current: &format!("{current:?}"),
|
|
324
|
+
allowed: check.from_states,
|
|
325
|
+
}
|
|
326
|
+
.render_args(),
|
|
327
|
+
));
|
|
328
|
+
}
|
|
329
|
+
Ok(())
|
|
330
|
+
}
|
|
331
|
+
// Unreachable for generated records (the lifecycle field is always a string): a
|
|
332
|
+
// codegen bug, surfaced as TypeMismatch with no `RefusalSite` template.
|
|
333
|
+
_ => Err(Refusal::TypeMismatch(format!(
|
|
334
|
+
"{command_name}: lifecycle field {:?} missing or not a string — a codegen bug",
|
|
335
|
+
check.field
|
|
336
|
+
))),
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/// `enforce_ensures` against the settled record (or element), `old` merged
|
|
341
|
+
/// into the args it reads.
|
|
342
|
+
fn enforce_ensures(
|
|
343
|
+
settled: &dyn Fielded,
|
|
344
|
+
old: &dyn Fielded,
|
|
345
|
+
args: &dyn Fielded,
|
|
346
|
+
ensures: &[EnsuresSpec],
|
|
347
|
+
command_name: &str,
|
|
348
|
+
) -> Result<(), Refusal> {
|
|
349
|
+
// C2.3 — the settled state first; see `StateFirst`.
|
|
350
|
+
let state_first = StateFirst { args, settled };
|
|
351
|
+
let with_old = WithOld { args: &state_first, old };
|
|
352
|
+
for rule in ensures {
|
|
353
|
+
let ctx = EvalContext { args: &with_old, instance: settled };
|
|
354
|
+
if !interpret(&rule.expr, &ctx)?.truthy() {
|
|
355
|
+
return Err(Refusal::EnsuresNotMet(format!("{command_name} refused — {}", rule.description)));
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
Ok(())
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/// The write half of `save`: the repository write and its mutation record.
|
|
362
|
+
fn persist<T, R>(repo: &mut R, id: &str, record: &T, aggregate_qualified_name: &str, mutations: &mut Vec<MutationRecord>)
|
|
363
|
+
where
|
|
364
|
+
T: Clone + ToJson,
|
|
365
|
+
R: Repository<T>,
|
|
366
|
+
{
|
|
367
|
+
repo.save(id, record.clone());
|
|
368
|
+
mutations.push(MutationRecord {
|
|
369
|
+
aggregate: aggregate_qualified_name.to_string(),
|
|
370
|
+
id: id.to_string(),
|
|
371
|
+
operation: "save",
|
|
372
|
+
state: record.to_json(),
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/// `emit` — the declared events, in declared order.
|
|
377
|
+
fn emitted(emits: &[&'static str], aggregate_qualified_name: &str, id: &str, payload: &Json) -> Vec<Event> {
|
|
378
|
+
emits
|
|
379
|
+
.iter()
|
|
380
|
+
.map(|name| Event {
|
|
381
|
+
name: name.to_string(),
|
|
382
|
+
aggregate: aggregate_qualified_name.to_string(),
|
|
383
|
+
id: id.to_string(),
|
|
384
|
+
payload: payload.clone(),
|
|
385
|
+
// Stamped later by `orchestrate`; this constructor has no clock.
|
|
386
|
+
occurred_at: None,
|
|
387
|
+
correlation: None,
|
|
388
|
+
})
|
|
389
|
+
.collect()
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/// The element half of an entity command on a parent record already in hand, nothing saved:
|
|
393
|
+
/// locate, givens, transition, mutate, ensures.
|
|
394
|
+
///
|
|
395
|
+
/// Shared by `dispatch_entity` and delegating aggregate commands so a refusal reads the same
|
|
396
|
+
/// through either. `parent_in_args` binds `parent:` (the live owning record) into given and
|
|
397
|
+
/// ensures scope, so an ensures sees the parent after the mutation. Both callers pass `true`:
|
|
398
|
+
/// the routing layer's `parent_deref` snapshot is never refreshed after `apply_mutations`.
|
|
399
|
+
#[allow(clippy::too_many_arguments)]
|
|
400
|
+
pub fn apply_entity_command<'a, T, E>(
|
|
401
|
+
record: &mut T,
|
|
402
|
+
parent_id: &str,
|
|
403
|
+
get_list: impl Fn(&T) -> &Vec<E>,
|
|
404
|
+
get_list_mut: impl FnOnce(&mut T) -> &mut Vec<E>,
|
|
405
|
+
matches: impl Fn(&E) -> bool,
|
|
406
|
+
command_name: &'static str,
|
|
407
|
+
// Renders `NothingToCorrect` wording, which qualifies the aggregate name.
|
|
408
|
+
aggregate_qualified_name: &'static str,
|
|
409
|
+
aggregate_name: &'static str,
|
|
410
|
+
entity_name: &'static str,
|
|
411
|
+
entity_identity_reading: &'static str,
|
|
412
|
+
wants: &str,
|
|
413
|
+
args: &'a dyn Fielded,
|
|
414
|
+
givens: &[GivenSpec],
|
|
415
|
+
transition: Option<TransitionCheck>,
|
|
416
|
+
apply_mutations: impl FnOnce(&mut E) -> Result<(), Refusal> + 'a,
|
|
417
|
+
ensures: &[EnsuresSpec],
|
|
418
|
+
parent_in_args: bool,
|
|
419
|
+
) -> Result<(), Refusal>
|
|
420
|
+
where
|
|
421
|
+
T: Fielded + Clone,
|
|
422
|
+
E: Fielded + Clone,
|
|
423
|
+
{
|
|
424
|
+
let mut element = ElementHalf {
|
|
425
|
+
parent_id,
|
|
426
|
+
get_list,
|
|
427
|
+
get_list_mut: Some(get_list_mut),
|
|
428
|
+
matches,
|
|
429
|
+
command_name,
|
|
430
|
+
aggregate_qualified_name,
|
|
431
|
+
aggregate_name,
|
|
432
|
+
entity_name,
|
|
433
|
+
entity_identity_reading,
|
|
434
|
+
wants,
|
|
435
|
+
args,
|
|
436
|
+
givens,
|
|
437
|
+
transition,
|
|
438
|
+
apply_mutations: Some(apply_mutations),
|
|
439
|
+
ensures,
|
|
440
|
+
parent_in_args,
|
|
441
|
+
position: None,
|
|
442
|
+
element: None,
|
|
443
|
+
parent_before: None,
|
|
444
|
+
old_snapshot: None,
|
|
445
|
+
};
|
|
446
|
+
for step in EntityStep::ORDER {
|
|
447
|
+
element.run(step, record)?;
|
|
448
|
+
}
|
|
449
|
+
Ok(())
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/// The element half's state, carried across `EntityStep::ORDER` — what
|
|
453
|
+
/// `apply_entity_command` and `dispatch_entity` both drive, one step at a
|
|
454
|
+
/// time, so a refusal reads identically through either caller.
|
|
455
|
+
struct ElementHalf<'a, 's, T, E, GetList, GetListMut, Matches, Apply> {
|
|
456
|
+
parent_id: &'s str,
|
|
457
|
+
get_list: GetList,
|
|
458
|
+
get_list_mut: Option<GetListMut>,
|
|
459
|
+
matches: Matches,
|
|
460
|
+
command_name: &'static str,
|
|
461
|
+
aggregate_qualified_name: &'static str,
|
|
462
|
+
aggregate_name: &'static str,
|
|
463
|
+
entity_name: &'static str,
|
|
464
|
+
entity_identity_reading: &'static str,
|
|
465
|
+
wants: &'s str,
|
|
466
|
+
args: &'a dyn Fielded,
|
|
467
|
+
givens: &'s [GivenSpec],
|
|
468
|
+
transition: Option<TransitionCheck>,
|
|
469
|
+
apply_mutations: Option<Apply>,
|
|
470
|
+
ensures: &'s [EnsuresSpec],
|
|
471
|
+
parent_in_args: bool,
|
|
472
|
+
position: Option<usize>,
|
|
473
|
+
element: Option<E>,
|
|
474
|
+
parent_before: Option<T>,
|
|
475
|
+
old_snapshot: Option<E>,
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
impl<'a, 's, T, E, GetList, GetListMut, Matches, Apply> ElementHalf<'a, 's, T, E, GetList, GetListMut, Matches, Apply>
|
|
479
|
+
where
|
|
480
|
+
T: Fielded + Clone,
|
|
481
|
+
E: Fielded + Clone,
|
|
482
|
+
GetList: Fn(&T) -> &Vec<E>,
|
|
483
|
+
GetListMut: FnOnce(&mut T) -> &mut Vec<E>,
|
|
484
|
+
Matches: Fn(&E) -> bool,
|
|
485
|
+
Apply: FnOnce(&mut E) -> Result<(), Refusal>,
|
|
486
|
+
{
|
|
487
|
+
/// One declared entity step against the parent `record` already in hand.
|
|
488
|
+
fn run(&mut self, step: EntityStep, record: &mut T) -> Result<(), Refusal> {
|
|
489
|
+
#[deny(clippy::wildcard_enum_match_arm)]
|
|
490
|
+
match step {
|
|
491
|
+
// Already run by `decode_entity_arguments`; see `entity_step_site`.
|
|
492
|
+
EntityStep::DecodeArguments
|
|
493
|
+
| EntityStep::RefuseUnknownArguments
|
|
494
|
+
| EntityStep::RefuseAbsentArguments
|
|
495
|
+
| EntityStep::NormalizeArgs
|
|
496
|
+
| EntityStep::RefuseRoleMismatch
|
|
497
|
+
| EntityStep::ResolveReferences => Ok(()),
|
|
498
|
+
// The parent half: `dispatch_entity`'s own arms, or the delegating aggregate's
|
|
499
|
+
// `dispatch`.
|
|
500
|
+
EntityStep::HydrateParent | EntityStep::EnforceInvariants | EntityStep::Save | EntityStep::Emit => Ok(()),
|
|
501
|
+
EntityStep::LocateElement => self.locate_element(record),
|
|
502
|
+
EntityStep::EnforceGivens => self.enforce_givens(),
|
|
503
|
+
EntityStep::AdmissibleTransition => {
|
|
504
|
+
admissible_transition(self.element.as_ref().expect(LOCATED), self.transition.as_ref(), self.command_name)
|
|
505
|
+
}
|
|
506
|
+
EntityStep::ApplyMutations => self.apply_mutations(record),
|
|
507
|
+
// Written by the generated `apply_mutations` closure (the entity's own lifecycle).
|
|
508
|
+
EntityStep::AdvanceLifecycle => Ok(()),
|
|
509
|
+
EntityStep::EnforceEnsures => self.enforce_ensures(record),
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
fn locate_element(&mut self, record: &T) -> Result<(), Refusal> {
|
|
514
|
+
let position = (self.get_list)(record).iter().position(|el| (self.matches)(el)).ok_or_else(|| {
|
|
515
|
+
Refusal::NotFound(
|
|
516
|
+
NotFoundEntityElementMissingArgs {
|
|
517
|
+
entity: self.entity_name,
|
|
518
|
+
identity: self.entity_identity_reading,
|
|
519
|
+
wants: self.wants,
|
|
520
|
+
aggregate: self.aggregate_name,
|
|
521
|
+
parent_id: &format!("{:?}", self.parent_id),
|
|
522
|
+
}
|
|
523
|
+
.render_args(),
|
|
524
|
+
)
|
|
525
|
+
})?;
|
|
526
|
+
self.element = Some((self.get_list)(record)[position].clone());
|
|
527
|
+
self.position = Some(position);
|
|
528
|
+
self.parent_before = Some(record.clone());
|
|
529
|
+
Ok(())
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
fn enforce_givens(&self) -> Result<(), Refusal> {
|
|
533
|
+
let parent_before = self.parent_before.as_ref().expect(LOCATED);
|
|
534
|
+
let element = self.element.as_ref().expect(LOCATED);
|
|
535
|
+
|
|
536
|
+
// Entity-level `corrects` is asked of the parent record and root aggregate, since an
|
|
537
|
+
// entity has no event stream. It runs after the element lookup, before declared givens,
|
|
538
|
+
// and proves the parent emitted the event, not that this element did.
|
|
539
|
+
for given in self.givens {
|
|
540
|
+
let Some(event_name) = given.corrects_event else { continue };
|
|
541
|
+
let ctx = EvalContext { args: self.args, instance: parent_before };
|
|
542
|
+
if !interpret(&given.expr, &ctx)?.truthy() {
|
|
543
|
+
return Err(Refusal::NothingToCorrect(format!(
|
|
544
|
+
"{} refused — corrects {event_name}, but {} #{} has never emitted it",
|
|
545
|
+
self.command_name, self.aggregate_qualified_name, self.parent_id
|
|
546
|
+
)));
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
let with_parent = WithParent { args: self.args, parent: parent_before };
|
|
551
|
+
let given_args: &dyn Fielded = if self.parent_in_args { &with_parent } else { self.args };
|
|
552
|
+
for given in self.givens {
|
|
553
|
+
// Handled above; the `corrects` flag field lives on the parent, not the element.
|
|
554
|
+
if given.corrects_event.is_some() {
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
let ctx = EvalContext { args: given_args, instance: element };
|
|
558
|
+
if !interpret(&given.expr, &ctx)?.truthy() {
|
|
559
|
+
return Err(Refusal::GivenNotMet(format!("{} refused — {}", self.command_name, given.description)));
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
Ok(())
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
fn apply_mutations(&mut self, record: &mut T) -> Result<(), Refusal> {
|
|
566
|
+
let position = self.position.expect(LOCATED);
|
|
567
|
+
let mut element = self.element.take().expect(LOCATED);
|
|
568
|
+
self.old_snapshot = if self.ensures.is_empty() { None } else { Some(element.clone()) };
|
|
569
|
+
(self.apply_mutations.take().expect(ONCE))(&mut element)?;
|
|
570
|
+
(self.get_list_mut.take().expect(ONCE))(record)[position] = element;
|
|
571
|
+
Ok(())
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
fn enforce_ensures(&self, record: &T) -> Result<(), Refusal> {
|
|
575
|
+
let Some(old) = &self.old_snapshot else { return Ok(()) };
|
|
576
|
+
let position = self.position.expect(LOCATED);
|
|
577
|
+
let parent_after = record.clone();
|
|
578
|
+
let with_parent = WithParent { args: self.args, parent: &parent_after };
|
|
579
|
+
let ensures_args: &dyn Fielded = if self.parent_in_args { &with_parent } else { self.args };
|
|
580
|
+
let settled = &(self.get_list)(record)[position];
|
|
581
|
+
enforce_ensures(settled, old, ensures_args, self.ensures, self.command_name)
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
#[allow(clippy::too_many_arguments)]
|
|
586
|
+
pub fn dispatch_entity<'a, T, E, R>(
|
|
587
|
+
repo: &mut R,
|
|
588
|
+
parent_id: &str,
|
|
589
|
+
get_list: impl Fn(&T) -> &Vec<E>,
|
|
590
|
+
get_list_mut: impl FnOnce(&mut T) -> &mut Vec<E>,
|
|
591
|
+
matches: impl Fn(&E) -> bool,
|
|
592
|
+
command_name: &'static str,
|
|
593
|
+
aggregate_qualified_name: &'static str,
|
|
594
|
+
aggregate_name: &'static str,
|
|
595
|
+
parent_identity_reading: &'static str,
|
|
596
|
+
entity_name: &'static str,
|
|
597
|
+
entity_identity_reading: &'static str,
|
|
598
|
+
wants: &str,
|
|
599
|
+
args: &'a dyn Fielded,
|
|
600
|
+
givens: &[GivenSpec],
|
|
601
|
+
transition: Option<TransitionCheck>,
|
|
602
|
+
apply_mutations: impl FnOnce(&mut E) -> Result<(), Refusal> + 'a,
|
|
603
|
+
ensures: &[EnsuresSpec],
|
|
604
|
+
invariants: &InvariantSet,
|
|
605
|
+
emits: &[&'static str],
|
|
606
|
+
payload: Json,
|
|
607
|
+
mutations: &mut Vec<MutationRecord>,
|
|
608
|
+
// As in `dispatch`: an entity command still ends in a parent aggregate save.
|
|
609
|
+
seed_projections: Vec<(&'static str, Option<String>)>,
|
|
610
|
+
) -> Result<(T, Vec<Event>), Refusal>
|
|
611
|
+
where
|
|
612
|
+
T: Fielded + Clone + ToJson + SetProjectedField,
|
|
613
|
+
E: Fielded + Clone,
|
|
614
|
+
R: Repository<T>,
|
|
615
|
+
{
|
|
616
|
+
let mut element = ElementHalf {
|
|
617
|
+
parent_id,
|
|
618
|
+
get_list,
|
|
619
|
+
get_list_mut: Some(get_list_mut),
|
|
620
|
+
matches,
|
|
621
|
+
command_name,
|
|
622
|
+
aggregate_qualified_name,
|
|
623
|
+
aggregate_name,
|
|
624
|
+
entity_name,
|
|
625
|
+
entity_identity_reading,
|
|
626
|
+
wants,
|
|
627
|
+
args,
|
|
628
|
+
givens,
|
|
629
|
+
transition,
|
|
630
|
+
apply_mutations: Some(apply_mutations),
|
|
631
|
+
ensures,
|
|
632
|
+
// See `apply_entity_command` on `parent_in_args`.
|
|
633
|
+
parent_in_args: true,
|
|
634
|
+
position: None,
|
|
635
|
+
element: None,
|
|
636
|
+
parent_before: None,
|
|
637
|
+
old_snapshot: None,
|
|
638
|
+
};
|
|
639
|
+
let mut seed_projections = Some(seed_projections);
|
|
640
|
+
let mut hydrated: Option<T> = None;
|
|
641
|
+
let mut events: Vec<Event> = Vec::new();
|
|
642
|
+
|
|
643
|
+
for step in EntityStep::ORDER {
|
|
644
|
+
#[deny(clippy::wildcard_enum_match_arm)]
|
|
645
|
+
match step {
|
|
646
|
+
// Already run by `decode_entity_arguments`; see `entity_step_site`.
|
|
647
|
+
EntityStep::DecodeArguments
|
|
648
|
+
| EntityStep::RefuseUnknownArguments
|
|
649
|
+
| EntityStep::RefuseAbsentArguments
|
|
650
|
+
| EntityStep::NormalizeArgs
|
|
651
|
+
| EntityStep::RefuseRoleMismatch
|
|
652
|
+
| EntityStep::ResolveReferences => {}
|
|
653
|
+
EntityStep::HydrateParent => {
|
|
654
|
+
// Same `record_missing` site as `hydrate_record`.
|
|
655
|
+
hydrated = Some(repo.find(parent_id).ok_or_else(|| {
|
|
656
|
+
Refusal::NotFound(
|
|
657
|
+
NotFoundRecordMissingArgs {
|
|
658
|
+
aggregate: aggregate_name,
|
|
659
|
+
identity: parent_identity_reading,
|
|
660
|
+
offered: &format!("{parent_id:?}"),
|
|
661
|
+
}
|
|
662
|
+
.render_args(),
|
|
663
|
+
)
|
|
664
|
+
})?);
|
|
665
|
+
}
|
|
666
|
+
// The element half: the same per-step body `apply_entity_command` runs.
|
|
667
|
+
EntityStep::LocateElement
|
|
668
|
+
| EntityStep::EnforceGivens
|
|
669
|
+
| EntityStep::AdmissibleTransition
|
|
670
|
+
| EntityStep::ApplyMutations
|
|
671
|
+
| EntityStep::AdvanceLifecycle
|
|
672
|
+
| EntityStep::EnforceEnsures => {
|
|
673
|
+
element.run(step, hydrated.as_mut().expect(HYDRATED))?;
|
|
674
|
+
}
|
|
675
|
+
EntityStep::EnforceInvariants => {
|
|
676
|
+
enforce_invariants(hydrated.as_ref().expect(HYDRATED), aggregate_name, invariants)?;
|
|
677
|
+
}
|
|
678
|
+
EntityStep::Save => {
|
|
679
|
+
let record = hydrated.as_mut().expect(HYDRATED);
|
|
680
|
+
for (field, value) in seed_projections.take().expect(ONCE) {
|
|
681
|
+
record.set_projected_field(field, value);
|
|
682
|
+
}
|
|
683
|
+
persist(repo, parent_id, record, aggregate_qualified_name, mutations);
|
|
684
|
+
}
|
|
685
|
+
EntityStep::Emit => {
|
|
686
|
+
events = emitted(emits, aggregate_qualified_name, parent_id, &payload);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
Ok((hydrated.expect(HYDRATED), events))
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const HYDRATED: &str = "the declared order hydrates before any step that reads the record (const-asserted in this file)";
|
|
695
|
+
const LOCATED: &str = "the declared order locates the element before any step that reads it (const-asserted in this file)";
|
|
696
|
+
const ONCE: &str = "each declared step appears exactly once in its ORDER";
|
|
697
|
+
const NORMALIZED: &str = "the declared order normalizes arguments before resolving references (const-asserted in this file)";
|
|
698
|
+
|
|
699
|
+
/// One generated hook per argument-gate step, called in declared order by
|
|
700
|
+
/// `decode_aggregate_arguments` and `decode_entity_arguments`.
|
|
701
|
+
///
|
|
702
|
+
/// `normalize_args` produces the typed `<Args>` that `resolve_references` reads.
|
|
703
|
+
pub struct ArgumentGates<'g, A> {
|
|
704
|
+
pub decode_arguments: &'g dyn Fn(&Json) -> Result<(), Refusal>,
|
|
705
|
+
pub refuse_unknown_arguments: &'g dyn Fn(&Json) -> Result<(), Refusal>,
|
|
706
|
+
pub refuse_absent_arguments: &'g dyn Fn(&Json) -> Result<(), Refusal>,
|
|
707
|
+
pub normalize_args: &'g dyn Fn(&Json) -> Result<A, Refusal>,
|
|
708
|
+
pub refuse_role_mismatch: &'g dyn Fn() -> Result<(), Refusal>,
|
|
709
|
+
pub resolve_references: &'g dyn Fn(&A) -> Result<(), Refusal>,
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/// The argument-gate steps the aggregate and entity orders share, so one
|
|
713
|
+
/// body (`ArgumentGates::run`) serves both loops.
|
|
714
|
+
#[derive(Debug, Clone, Copy)]
|
|
715
|
+
enum ArgumentGate {
|
|
716
|
+
DecodeArguments,
|
|
717
|
+
RefuseUnknownArguments,
|
|
718
|
+
RefuseAbsentArguments,
|
|
719
|
+
NormalizeArgs,
|
|
720
|
+
RefuseRoleMismatch,
|
|
721
|
+
ResolveReferences,
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
impl<A> ArgumentGates<'_, A> {
|
|
725
|
+
fn run(&self, gate: ArgumentGate, facts: &Json, args: &mut Option<A>) -> Result<(), Refusal> {
|
|
726
|
+
match gate {
|
|
727
|
+
ArgumentGate::DecodeArguments => (self.decode_arguments)(facts),
|
|
728
|
+
ArgumentGate::RefuseUnknownArguments => (self.refuse_unknown_arguments)(facts),
|
|
729
|
+
ArgumentGate::RefuseAbsentArguments => (self.refuse_absent_arguments)(facts),
|
|
730
|
+
ArgumentGate::NormalizeArgs => {
|
|
731
|
+
*args = Some((self.normalize_args)(facts)?);
|
|
732
|
+
Ok(())
|
|
733
|
+
}
|
|
734
|
+
ArgumentGate::RefuseRoleMismatch => (self.refuse_role_mismatch)(),
|
|
735
|
+
ArgumentGate::ResolveReferences => (self.resolve_references)(args.as_ref().expect(NORMALIZED)),
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/// `AggregateStep::ORDER`'s argument gates over one aggregate command's facts:
|
|
741
|
+
/// the normalized `<Args>`, or the first refusal in declared order. Every
|
|
742
|
+
/// other step is `dispatch`'s, entered once the router has resolved identity.
|
|
743
|
+
pub fn decode_aggregate_arguments<A>(facts: &Json, gates: &ArgumentGates<'_, A>) -> Result<A, Refusal> {
|
|
744
|
+
let mut args = None;
|
|
745
|
+
for step in AggregateStep::ORDER {
|
|
746
|
+
#[deny(clippy::wildcard_enum_match_arm)]
|
|
747
|
+
let gate = match step {
|
|
748
|
+
AggregateStep::DecodeArguments => ArgumentGate::DecodeArguments,
|
|
749
|
+
AggregateStep::RefuseUnknownArguments => ArgumentGate::RefuseUnknownArguments,
|
|
750
|
+
AggregateStep::RefuseAbsentArguments => ArgumentGate::RefuseAbsentArguments,
|
|
751
|
+
AggregateStep::NormalizeArgs => ArgumentGate::NormalizeArgs,
|
|
752
|
+
AggregateStep::RefuseRoleMismatch => ArgumentGate::RefuseRoleMismatch,
|
|
753
|
+
AggregateStep::ResolveReferences => ArgumentGate::ResolveReferences,
|
|
754
|
+
AggregateStep::Hydrate
|
|
755
|
+
| AggregateStep::EnforceGivens
|
|
756
|
+
| AggregateStep::AdmissibleTransition
|
|
757
|
+
| AggregateStep::AssignCreationAttributes
|
|
758
|
+
| AggregateStep::ApplyMutations
|
|
759
|
+
| AggregateStep::AdvanceLifecycle
|
|
760
|
+
| AggregateStep::DelegateToEntity
|
|
761
|
+
| AggregateStep::EnforceEnsures
|
|
762
|
+
| AggregateStep::EnforceInvariants
|
|
763
|
+
| AggregateStep::Save
|
|
764
|
+
| AggregateStep::Emit => continue,
|
|
765
|
+
};
|
|
766
|
+
gates.run(gate, facts, &mut args)?;
|
|
767
|
+
}
|
|
768
|
+
Ok(args.expect(NORMALIZED))
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/// `EntityStep::ORDER`'s argument gates over one entity command's facts — see
|
|
772
|
+
/// `decode_aggregate_arguments`. Every other step is `dispatch_entity`'s.
|
|
773
|
+
pub fn decode_entity_arguments<A>(facts: &Json, gates: &ArgumentGates<'_, A>) -> Result<A, Refusal> {
|
|
774
|
+
let mut args = None;
|
|
775
|
+
for step in EntityStep::ORDER {
|
|
776
|
+
#[deny(clippy::wildcard_enum_match_arm)]
|
|
777
|
+
let gate = match step {
|
|
778
|
+
EntityStep::DecodeArguments => ArgumentGate::DecodeArguments,
|
|
779
|
+
EntityStep::RefuseUnknownArguments => ArgumentGate::RefuseUnknownArguments,
|
|
780
|
+
EntityStep::RefuseAbsentArguments => ArgumentGate::RefuseAbsentArguments,
|
|
781
|
+
EntityStep::NormalizeArgs => ArgumentGate::NormalizeArgs,
|
|
782
|
+
EntityStep::RefuseRoleMismatch => ArgumentGate::RefuseRoleMismatch,
|
|
783
|
+
EntityStep::ResolveReferences => ArgumentGate::ResolveReferences,
|
|
784
|
+
EntityStep::HydrateParent
|
|
785
|
+
| EntityStep::LocateElement
|
|
786
|
+
| EntityStep::EnforceGivens
|
|
787
|
+
| EntityStep::AdmissibleTransition
|
|
788
|
+
| EntityStep::ApplyMutations
|
|
789
|
+
| EntityStep::AdvanceLifecycle
|
|
790
|
+
| EntityStep::EnforceEnsures
|
|
791
|
+
| EntityStep::EnforceInvariants
|
|
792
|
+
| EntityStep::Save
|
|
793
|
+
| EntityStep::Emit => continue,
|
|
794
|
+
};
|
|
795
|
+
gates.run(gate, facts, &mut args)?;
|
|
796
|
+
}
|
|
797
|
+
Ok(args.expect(NORMALIZED))
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/// A step's index in `AggregateStep::ORDER`, usable in a const context.
|
|
801
|
+
const fn aggregate_position(step: AggregateStep) -> usize {
|
|
802
|
+
let mut index = 0;
|
|
803
|
+
while index < AggregateStep::ORDER.len() {
|
|
804
|
+
if AggregateStep::ORDER[index] as usize == step as usize {
|
|
805
|
+
return index;
|
|
806
|
+
}
|
|
807
|
+
index += 1;
|
|
808
|
+
}
|
|
809
|
+
panic!("step missing from AggregateStep::ORDER")
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/// A step's index in `EntityStep::ORDER`, usable in a const context.
|
|
813
|
+
const fn entity_position(step: EntityStep) -> usize {
|
|
814
|
+
let mut index = 0;
|
|
815
|
+
while index < EntityStep::ORDER.len() {
|
|
816
|
+
if EntityStep::ORDER[index] as usize == step as usize {
|
|
817
|
+
return index;
|
|
818
|
+
}
|
|
819
|
+
index += 1;
|
|
820
|
+
}
|
|
821
|
+
panic!("step missing from EntityStep::ORDER")
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// Pairs where an arm consumes what an earlier arm produced; reordering them in
|
|
825
|
+
// vocabulary.bluebook fails the build instead of panicking at dispatch time.
|
|
826
|
+
const _: () = {
|
|
827
|
+
use AggregateStep as A;
|
|
828
|
+
let hydrate = aggregate_position(A::Hydrate);
|
|
829
|
+
assert!(hydrate < aggregate_position(A::EnforceGivens));
|
|
830
|
+
assert!(hydrate < aggregate_position(A::AdmissibleTransition));
|
|
831
|
+
assert!(hydrate < aggregate_position(A::ApplyMutations));
|
|
832
|
+
assert!(hydrate < aggregate_position(A::EnforceEnsures));
|
|
833
|
+
assert!(hydrate < aggregate_position(A::EnforceInvariants));
|
|
834
|
+
assert!(hydrate < aggregate_position(A::Save));
|
|
835
|
+
assert!(hydrate < aggregate_position(A::Emit));
|
|
836
|
+
// `old_snapshot` is taken in ApplyMutations; an EnforceEnsures before it
|
|
837
|
+
// would see none and skip every ensures.
|
|
838
|
+
assert!(aggregate_position(A::ApplyMutations) < aggregate_position(A::EnforceEnsures));
|
|
839
|
+
// The router resolves identity only after `decode_aggregate_arguments`
|
|
840
|
+
// returns, so every argument gate must be declared ahead of `hydrate`;
|
|
841
|
+
// `resolve_references` reads the `<Args>` `normalize_args` produced.
|
|
842
|
+
assert!(aggregate_position(A::DecodeArguments) < hydrate);
|
|
843
|
+
assert!(aggregate_position(A::RefuseUnknownArguments) < hydrate);
|
|
844
|
+
assert!(aggregate_position(A::RefuseAbsentArguments) < hydrate);
|
|
845
|
+
assert!(aggregate_position(A::NormalizeArgs) < hydrate);
|
|
846
|
+
assert!(aggregate_position(A::RefuseRoleMismatch) < hydrate);
|
|
847
|
+
assert!(aggregate_position(A::ResolveReferences) < hydrate);
|
|
848
|
+
assert!(aggregate_position(A::NormalizeArgs) < aggregate_position(A::ResolveReferences));
|
|
849
|
+
|
|
850
|
+
use EntityStep as E;
|
|
851
|
+
let hydrate_parent = entity_position(E::HydrateParent);
|
|
852
|
+
assert!(hydrate_parent < entity_position(E::LocateElement));
|
|
853
|
+
assert!(hydrate_parent < entity_position(E::EnforceInvariants));
|
|
854
|
+
assert!(hydrate_parent < entity_position(E::Save));
|
|
855
|
+
let locate = entity_position(E::LocateElement);
|
|
856
|
+
assert!(locate < entity_position(E::EnforceGivens));
|
|
857
|
+
assert!(locate < entity_position(E::AdmissibleTransition));
|
|
858
|
+
assert!(locate < entity_position(E::ApplyMutations));
|
|
859
|
+
// ApplyMutations moves the element back into the parent's list.
|
|
860
|
+
assert!(entity_position(E::EnforceGivens) < entity_position(E::ApplyMutations));
|
|
861
|
+
assert!(entity_position(E::AdmissibleTransition) < entity_position(E::ApplyMutations));
|
|
862
|
+
assert!(entity_position(E::ApplyMutations) < entity_position(E::EnforceEnsures));
|
|
863
|
+
// See the aggregate half: identity (parent, then element) is resolved
|
|
864
|
+
// only after `decode_entity_arguments` returns.
|
|
865
|
+
assert!(entity_position(E::DecodeArguments) < hydrate_parent);
|
|
866
|
+
assert!(entity_position(E::RefuseUnknownArguments) < hydrate_parent);
|
|
867
|
+
assert!(entity_position(E::RefuseAbsentArguments) < hydrate_parent);
|
|
868
|
+
assert!(entity_position(E::NormalizeArgs) < hydrate_parent);
|
|
869
|
+
assert!(entity_position(E::RefuseRoleMismatch) < hydrate_parent);
|
|
870
|
+
assert!(entity_position(E::ResolveReferences) < hydrate_parent);
|
|
871
|
+
assert!(entity_position(E::NormalizeArgs) < entity_position(E::ResolveReferences));
|
|
872
|
+
};
|
|
873
|
+
|
|
874
|
+
/// Where a declared dispatch step is actually performed in the Rust port.
|
|
875
|
+
/// The dispatch loops above follow this mapping.
|
|
876
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
877
|
+
pub enum StepSite {
|
|
878
|
+
/// An arm of the kernel's own dispatch loop does the work.
|
|
879
|
+
Kernel,
|
|
880
|
+
/// `decode_aggregate_arguments`/`decode_entity_arguments` call this
|
|
881
|
+
/// step's generated hook (`ArgumentGates`) in its declared position,
|
|
882
|
+
/// before the router resolves identity. `dispatch`'s own arm is a no-op.
|
|
883
|
+
ArgumentGate,
|
|
884
|
+
/// Folded into a generated closure the kernel already calls in another
|
|
885
|
+
/// step's arm (named here). The loop's own arm is a no-op.
|
|
886
|
+
FoldedInto(AggregateStepOrEntityStep),
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/// The step a `StepSite::FoldedInto` step rides inside.
|
|
890
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
891
|
+
pub enum AggregateStepOrEntityStep {
|
|
892
|
+
Aggregate(AggregateStep),
|
|
893
|
+
Entity(EntityStep),
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/// See `StepSite`. Exhaustive, no wildcard — a step the vocabulary gains
|
|
897
|
+
/// must be placed here before the crate compiles.
|
|
898
|
+
#[deny(clippy::wildcard_enum_match_arm)]
|
|
899
|
+
pub const fn aggregate_step_site(step: AggregateStep) -> StepSite {
|
|
900
|
+
use AggregateStep as A;
|
|
901
|
+
match step {
|
|
902
|
+
// `<Args>::decode_arguments` (the facts are an object).
|
|
903
|
+
A::DecodeArguments => StepSite::ArgumentGate,
|
|
904
|
+
// `<Args>::refuse_unknown_arguments`/`<Args>::refuse_absent_arguments`.
|
|
905
|
+
A::RefuseUnknownArguments | A::RefuseAbsentArguments => StepSite::ArgumentGate,
|
|
906
|
+
// `<Args>::from_json`'s coercion + the router's invariant checks.
|
|
907
|
+
A::NormalizeArgs => StepSite::ArgumentGate,
|
|
908
|
+
// `kernel::check_role_via`.
|
|
909
|
+
A::RefuseRoleMismatch => StepSite::ArgumentGate,
|
|
910
|
+
// `kernel::check_reference`; the router computes the tenant boundary
|
|
911
|
+
// after it, but the kernel's `Save` arm applies it.
|
|
912
|
+
A::ResolveReferences => StepSite::ArgumentGate,
|
|
913
|
+
A::Hydrate => StepSite::Kernel,
|
|
914
|
+
A::EnforceGivens => StepSite::Kernel,
|
|
915
|
+
A::AdmissibleTransition => StepSite::Kernel,
|
|
916
|
+
// `Hydrate::Create.build`.
|
|
917
|
+
A::AssignCreationAttributes => StepSite::FoldedInto(AggregateStepOrEntityStep::Aggregate(A::Hydrate)),
|
|
918
|
+
A::ApplyMutations => StepSite::Kernel,
|
|
919
|
+
// The generated `apply_mutations` closure's last line.
|
|
920
|
+
A::AdvanceLifecycle => StepSite::FoldedInto(AggregateStepOrEntityStep::Aggregate(A::ApplyMutations)),
|
|
921
|
+
// The generated `apply_mutations` closure → `apply_entity_command`.
|
|
922
|
+
A::DelegateToEntity => StepSite::FoldedInto(AggregateStepOrEntityStep::Aggregate(A::ApplyMutations)),
|
|
923
|
+
A::EnforceEnsures => StepSite::Kernel,
|
|
924
|
+
A::EnforceInvariants => StepSite::Kernel,
|
|
925
|
+
A::Save => StepSite::Kernel,
|
|
926
|
+
A::Emit => StepSite::Kernel,
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
/// See `StepSite`. Exhaustive, no wildcard.
|
|
931
|
+
#[deny(clippy::wildcard_enum_match_arm)]
|
|
932
|
+
pub const fn entity_step_site(step: EntityStep) -> StepSite {
|
|
933
|
+
use EntityStep as E;
|
|
934
|
+
match step {
|
|
935
|
+
// The `<EntityArgs>` functions and checks named in
|
|
936
|
+
// `aggregate_step_site`, through `decode_entity_arguments`.
|
|
937
|
+
E::DecodeArguments => StepSite::ArgumentGate,
|
|
938
|
+
E::RefuseUnknownArguments | E::RefuseAbsentArguments => StepSite::ArgumentGate,
|
|
939
|
+
E::NormalizeArgs => StepSite::ArgumentGate,
|
|
940
|
+
E::RefuseRoleMismatch => StepSite::ArgumentGate,
|
|
941
|
+
E::ResolveReferences => StepSite::ArgumentGate,
|
|
942
|
+
E::HydrateParent => StepSite::Kernel,
|
|
943
|
+
E::LocateElement => StepSite::Kernel,
|
|
944
|
+
E::EnforceGivens => StepSite::Kernel,
|
|
945
|
+
E::AdmissibleTransition => StepSite::Kernel,
|
|
946
|
+
E::ApplyMutations => StepSite::Kernel,
|
|
947
|
+
E::AdvanceLifecycle => StepSite::FoldedInto(AggregateStepOrEntityStep::Entity(E::ApplyMutations)),
|
|
948
|
+
E::EnforceEnsures => StepSite::Kernel,
|
|
949
|
+
E::EnforceInvariants => StepSite::Kernel,
|
|
950
|
+
E::Save => StepSite::Kernel,
|
|
951
|
+
E::Emit => StepSite::Kernel,
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// No `match step` above has a wildcard arm, so a step added to the vocabulary fails to
|
|
956
|
+
// compile (E0004) until placed. The tests pin the current kernel/argument-gate mapping.
|
|
957
|
+
#[cfg(test)]
|
|
958
|
+
mod step_order_tests {
|
|
959
|
+
use super::*;
|
|
960
|
+
|
|
961
|
+
#[test]
|
|
962
|
+
fn decode_arguments_leads_both_orders() {
|
|
963
|
+
assert_eq!(AggregateStep::ORDER[0], AggregateStep::DecodeArguments);
|
|
964
|
+
assert_eq!(EntityStep::ORDER[0], EntityStep::DecodeArguments);
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
#[test]
|
|
968
|
+
fn const_positions_agree_with_the_generated_ones() {
|
|
969
|
+
for step in AggregateStep::ORDER {
|
|
970
|
+
assert_eq!(aggregate_position(step), step.position());
|
|
971
|
+
}
|
|
972
|
+
for step in EntityStep::ORDER {
|
|
973
|
+
assert_eq!(entity_position(step), step.position());
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
#[test]
|
|
978
|
+
fn the_kernel_performs_exactly_these_aggregate_steps() {
|
|
979
|
+
let kernel: Vec<&str> =
|
|
980
|
+
AggregateStep::ORDER.iter().filter(|s| aggregate_step_site(**s) == StepSite::Kernel).map(|s| s.step()).collect();
|
|
981
|
+
assert_eq!(
|
|
982
|
+
kernel,
|
|
983
|
+
["hydrate", "enforce_givens", "admissible_transition", "apply_mutations", "enforce_ensures", "enforce_invariants", "save", "emit"]
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
#[test]
|
|
988
|
+
fn the_kernel_performs_exactly_these_entity_steps() {
|
|
989
|
+
let kernel: Vec<&str> =
|
|
990
|
+
EntityStep::ORDER.iter().filter(|s| entity_step_site(**s) == StepSite::Kernel).map(|s| s.step()).collect();
|
|
991
|
+
assert_eq!(
|
|
992
|
+
kernel,
|
|
993
|
+
[
|
|
994
|
+
"hydrate_parent",
|
|
995
|
+
"locate_element",
|
|
996
|
+
"enforce_givens",
|
|
997
|
+
"admissible_transition",
|
|
998
|
+
"apply_mutations",
|
|
999
|
+
"enforce_ensures",
|
|
1000
|
+
"enforce_invariants",
|
|
1001
|
+
"save",
|
|
1002
|
+
"emit"
|
|
1003
|
+
]
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
#[test]
|
|
1008
|
+
fn argument_gates_all_precede_the_first_kernel_step() {
|
|
1009
|
+
// The router resolves identity between the two loops, so every
|
|
1010
|
+
// argument gate sits ahead of the first kernel step.
|
|
1011
|
+
let first_kernel = AggregateStep::ORDER.iter().position(|s| aggregate_step_site(*s) == StepSite::Kernel).unwrap();
|
|
1012
|
+
for step in AggregateStep::ORDER {
|
|
1013
|
+
if aggregate_step_site(step) == StepSite::ArgumentGate {
|
|
1014
|
+
assert!(step.position() < first_kernel, "{} is an argument gate but is declared after hydrate", step.step());
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
let first_kernel = EntityStep::ORDER.iter().position(|s| entity_step_site(*s) == StepSite::Kernel).unwrap();
|
|
1018
|
+
for step in EntityStep::ORDER {
|
|
1019
|
+
if entity_step_site(step) == StepSite::ArgumentGate {
|
|
1020
|
+
assert!(step.position() < first_kernel, "{} is an argument gate but is declared after hydrate_parent", step.step());
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
use std::cell::RefCell;
|
|
1026
|
+
|
|
1027
|
+
/// Hooks that record which step ran, refusing at the steps named in `refuse`.
|
|
1028
|
+
fn recorded<R>(refuse: &[&'static str], body: impl FnOnce(&ArgumentGates<'_, i64>) -> R) -> (R, Vec<&'static str>) {
|
|
1029
|
+
let ran = RefCell::new(Vec::new());
|
|
1030
|
+
let hit = |step: &'static str| -> Result<(), Refusal> {
|
|
1031
|
+
ran.borrow_mut().push(step);
|
|
1032
|
+
if refuse.contains(&step) {
|
|
1033
|
+
Err(Refusal::TypeMismatch(step.to_string()))
|
|
1034
|
+
} else {
|
|
1035
|
+
Ok(())
|
|
1036
|
+
}
|
|
1037
|
+
};
|
|
1038
|
+
let decode = |_: &Json| hit("decode_arguments");
|
|
1039
|
+
let unknown = |_: &Json| hit("refuse_unknown_arguments");
|
|
1040
|
+
let absent = |_: &Json| hit("refuse_absent_arguments");
|
|
1041
|
+
let normalize = |_: &Json| hit("normalize_args").map(|()| 7);
|
|
1042
|
+
let role = || hit("refuse_role_mismatch");
|
|
1043
|
+
let references = |args: &i64| {
|
|
1044
|
+
assert_eq!(*args, 7, "resolve_references reads what normalize_args produced");
|
|
1045
|
+
hit("resolve_references")
|
|
1046
|
+
};
|
|
1047
|
+
let gates = ArgumentGates {
|
|
1048
|
+
decode_arguments: &decode,
|
|
1049
|
+
refuse_unknown_arguments: &unknown,
|
|
1050
|
+
refuse_absent_arguments: &absent,
|
|
1051
|
+
normalize_args: &normalize,
|
|
1052
|
+
refuse_role_mismatch: &role,
|
|
1053
|
+
resolve_references: &references,
|
|
1054
|
+
};
|
|
1055
|
+
let result = body(&gates);
|
|
1056
|
+
(result, ran.into_inner())
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
#[test]
|
|
1060
|
+
fn the_argument_gate_loops_call_every_hook_in_declared_order() {
|
|
1061
|
+
let facts = Json::Object(Vec::new());
|
|
1062
|
+
let (result, ran) = recorded(&[], |gates| decode_aggregate_arguments(&facts, gates));
|
|
1063
|
+
assert_eq!(result.unwrap(), 7);
|
|
1064
|
+
let declared: Vec<&str> =
|
|
1065
|
+
AggregateStep::ORDER.iter().filter(|s| aggregate_step_site(**s) == StepSite::ArgumentGate).map(|s| s.step()).collect();
|
|
1066
|
+
assert_eq!(ran, declared);
|
|
1067
|
+
|
|
1068
|
+
let (result, ran) = recorded(&[], |gates| decode_entity_arguments(&facts, gates));
|
|
1069
|
+
assert_eq!(result.unwrap(), 7);
|
|
1070
|
+
let declared: Vec<&str> =
|
|
1071
|
+
EntityStep::ORDER.iter().filter(|s| entity_step_site(**s) == StepSite::ArgumentGate).map(|s| s.step()).collect();
|
|
1072
|
+
assert_eq!(ran, declared);
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
#[test]
|
|
1076
|
+
fn the_earliest_declared_violated_gate_wins_and_later_hooks_never_run() {
|
|
1077
|
+
let facts = Json::Object(Vec::new());
|
|
1078
|
+
let gates: Vec<&'static str> =
|
|
1079
|
+
AggregateStep::ORDER.iter().filter(|s| aggregate_step_site(**s) == StepSite::ArgumentGate).map(|s| s.step()).collect();
|
|
1080
|
+
for (i, earlier) in gates.iter().enumerate() {
|
|
1081
|
+
for later in &gates[i + 1..] {
|
|
1082
|
+
let (result, ran) = recorded(&[earlier, later], |g| decode_aggregate_arguments(&facts, g));
|
|
1083
|
+
assert_eq!(result.unwrap_err().to_string(), Refusal::TypeMismatch(earlier.to_string()).to_string());
|
|
1084
|
+
assert_eq!(ran.last(), Some(earlier));
|
|
1085
|
+
let (result, _) = recorded(&[earlier, later], |g| decode_entity_arguments(&facts, g));
|
|
1086
|
+
assert_eq!(result.unwrap_err().to_string(), Refusal::TypeMismatch(earlier.to_string()).to_string());
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
}
|