hecks 3.14.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/examples/pizzas/adapters/fake_processor.rb +34 -0
- data/examples/pizzas/adapters/payment_signature.rb +66 -0
- data/examples/pizzas/adapters/payment_webhook.rb +87 -0
- data/examples/pizzas/bluebook/pizzas.behaviors +33 -0
- data/examples/pizzas/bluebook/pizzas.hecksagon +5 -3
- data/examples/pizzas/config.ru +22 -0
- data/lib/hecks/adapters/driven/prism.rb +0 -2
- data/lib/hecks/adapters/driven.rb +0 -2
- data/lib/hecks/adapters/driving/json.rb +1 -7
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -2
- data/lib/hecks/bluebook/dsl/derivation_words.rb +58 -0
- data/lib/hecks/bluebook/dsl/translation_builder.rb +6 -37
- data/lib/hecks/bluebook/expression/ast_json.rb +0 -9
- data/lib/hecks/bluebook/hecksagon.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/translation_judge/rules.rb +7 -10
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +4 -8
- data/lib/hecks/bluebook/meta_validator.rb +0 -11
- data/lib/hecks/bluebook/translation.rb +18 -10
- data/lib/hecks/bluebook/translation_expression/evaluator.rb +63 -0
- data/lib/hecks/bluebook/translation_expression/parser.rb +127 -0
- data/lib/hecks/bluebook/translation_expression.rb +39 -0
- data/lib/hecks/bluebook/translation_plan.rb +70 -0
- data/lib/hecks/bluebook.rb +2 -0
- data/lib/hecks/chapters.rb +3 -3
- data/lib/hecks/composer/adapters/claude_composer.adapter +3 -0
- data/lib/hecks/composer/adapters/claude_composer.rb +83 -0
- data/lib/hecks/composer/adapters/fake_agent.adapter +3 -0
- data/lib/hecks/composer/adapters/fake_agent.rb +55 -0
- data/lib/hecks/composer/adapters/jev.adapter +3 -0
- data/lib/hecks/composer/adapters/jev.rb +66 -0
- data/lib/hecks/composer/agent.rb +113 -0
- data/lib/hecks/composer/assistant.rb +110 -0
- data/lib/hecks/composer/behavior_run.rb +53 -0
- data/lib/hecks/composer/bluebook/composer.bluebook +178 -0
- data/lib/hecks/composer/bluebook/composer.hecksagon +16 -0
- data/lib/hecks/composer/board.rb +65 -0
- data/lib/hecks/composer/board_edit/command_text.rb +39 -0
- data/lib/hecks/composer/board_edit/source.rb +84 -0
- data/lib/hecks/composer/board_edit.rb +144 -0
- data/lib/hecks/composer/check.rb +89 -0
- data/lib/hecks/composer/domains.rb +81 -0
- data/lib/hecks/composer/jobs.rb +134 -0
- data/lib/hecks/composer/layout.rb +37 -0
- data/lib/hecks/composer/live.rb +150 -0
- data/lib/hecks/composer/outline.rb +48 -0
- data/lib/hecks/composer/page.html +989 -0
- data/lib/hecks/composer/panels.rb +142 -0
- data/lib/hecks/composer/parse.rb +83 -0
- data/lib/hecks/composer/plain.rb +28 -0
- data/lib/hecks/composer/ports/composer_agent.port +7 -0
- data/lib/hecks/composer/session.rb +141 -0
- data/lib/hecks/composer/toolbox.rb +166 -0
- data/lib/hecks/composer/web.rb +156 -0
- data/lib/hecks/composer/workbench.rb +51 -0
- data/lib/hecks/composer/workspace.rb +81 -0
- data/lib/hecks/composer.rb +17 -0
- data/lib/hecks/corpus/adapters.rb +22 -0
- data/lib/hecks/corpus/rust.rb +1 -1
- data/lib/hecks/corpus/worlds.rb +19 -0
- data/lib/hecks/corpus.rb +5 -0
- data/lib/hecks/forms/app/commands.rb +2 -3
- data/lib/hecks/forms/app/queries.rb +2 -3
- data/lib/hecks/forms/app/records.rb +17 -5
- data/lib/hecks/forms/command_form_renderer.rb +42 -16
- data/lib/hecks/forms/field_controls.rb +2 -2
- data/lib/hecks/forms/field_renderer.rb +1 -1
- data/lib/hecks/forms/field_shape.rb +36 -17
- data/lib/hecks/forms/index_renderer.rb +2 -1
- data/lib/hecks/forms/page.rb +1 -1
- data/lib/hecks/forms/params.rb +0 -13
- data/lib/hecks/forms/query_form_renderer.rb +7 -6
- data/lib/hecks/forms/record_renderer.rb +11 -7
- data/lib/hecks/forms/rule_words.rb +100 -0
- data/lib/hecks/forms/value_object_shape.rb +2 -1
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -3
- data/lib/hecks/fuzzing/isolated_boot/worlds.rb +3 -6
- data/lib/hecks/fuzzing/replay/filters.rb +2 -6
- data/lib/hecks/gate/stages.yml +25 -0
- data/lib/hecks/grammar/evolve/seeds.rb +0 -6
- data/lib/hecks/grammar/evolve.rb +0 -3
- data/lib/hecks/grammar/translation.bluebook +1 -1
- data/lib/hecks/hecks/adapters/codebase/composing.rb +56 -0
- data/lib/hecks/hecks/adapters/codebase/source_tree.rb +3 -1
- data/lib/hecks/hecks/adapters/journal_store/changes.rb +0 -26
- data/lib/hecks/hecks/adapters/journal_store/examination.rb +2 -23
- data/lib/hecks/hecks/adapters/journal_store/readings.rb +3 -11
- data/lib/hecks/hecks/adapters/journal_store.rb +0 -11
- data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +4 -11
- data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +0 -9
- data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +0 -4
- data/lib/hecks/hecks/codebase.behaviors +6 -0
- data/lib/hecks/hecks/codebase.bluebook +21 -0
- data/lib/hecks/hecks/composer.behaviors +68 -0
- data/lib/hecks/hecks/context_map.hecksagon +1 -1
- data/lib/hecks/hecks/custodian.behaviors +0 -6
- data/lib/hecks/hecks/custodian.bluebook +3 -80
- data/lib/hecks/hecks/hecks.hecksagon +2 -2
- data/lib/hecks/hecks/hecks.world +1 -1
- data/lib/hecks/hecks/rehearsal.bluebook +7 -13
- data/lib/hecks/kernel_capabilities/templates.rb +4 -4
- data/lib/hecks/kernel_capabilities.rb +1 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -3
- data/lib/hecks/language/translation/translation_aggregate.bluebook +21 -24
- data/lib/hecks/ports/access_control.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -41
- data/lib/hecks/ports/persistence/plugins/era/lineage/coverage.rb +7 -7
- data/lib/hecks/ports/persistence/plugins/era/lineage/rewrite.rb +25 -2
- data/lib/hecks/ports/persistence/plugins/era/lineage/rules.rb +4 -4
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +17 -25
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/base_sql.rb +0 -12
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/domain_rename.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -22
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +0 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +9 -3
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +3 -29
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/refusals.rb +2 -26
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -33
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +2 -4
- data/lib/hecks/ports/persistence/plugins/era/translation/expression_sql.rb +52 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +23 -29
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/translation.rb +0 -1
- data/lib/hecks/ports/persistence/plugins/era.rb +3 -6
- data/lib/hecks/ports.rb +0 -1
- data/lib/hecks/projection_files.rb +3 -1
- data/lib/hecks/projections/deploy/templates/lambda/nat_comment_oauth.tmpl +1 -1
- data/lib/hecks/projections/glossary/entries.rb +10 -4
- data/lib/hecks/projections/glossary/groups.rb +42 -0
- data/lib/hecks/projections/glossary/html/page.rb +7 -2
- data/lib/hecks/projections/glossary/html/parser.rb +1 -1
- data/lib/hecks/projections/glossary/html/sections.rb +31 -11
- data/lib/hecks/projections/glossary/html.rb +1 -2
- data/lib/hecks/projections/glossary/markdown.rb +31 -28
- data/lib/hecks/projections/glossary/mermaid.rb +3 -0
- data/lib/hecks/projections/glossary/page.css +143 -153
- data/lib/hecks/projections/glossary/page.js +63 -3
- data/lib/hecks/projections/glossary/sentences/actions.rb +88 -0
- data/lib/hecks/projections/glossary/sentences/paths.rb +77 -0
- data/lib/hecks/projections/glossary/sentences/queries.rb +90 -0
- data/lib/hecks/projections/glossary/sentences/value_objects.rb +32 -5
- data/lib/hecks/projections/glossary/sentences.rb +13 -3
- data/lib/hecks/projections/glossary/slugs.rb +4 -1
- data/lib/hecks/projections/glossary.rb +2 -1
- data/lib/hecks/projections/plain_english.rb +97 -0
- data/lib/hecks/projections/rust_vocabulary/arguments.rb +2 -2
- data/lib/hecks/projections/rust_vocabulary/enum_body.rb +20 -25
- data/lib/hecks/projections/rust_vocabulary/tests.rb +2 -2
- data/lib/hecks/projections/rust_vocabulary.rb +14 -6
- data/lib/hecks/projections/site/cms_editor/color.rb +0 -6
- data/lib/hecks/projections/statements/actions.rb +99 -0
- data/lib/hecks/projections/statements/relationships.rb +86 -0
- data/lib/hecks/projections/statements.rb +15 -24
- data/lib/hecks/projector/exporter/adapters.rb +112 -0
- data/lib/hecks/projector/exporter/translations.rb +18 -35
- data/lib/hecks/projector/exporter/worlds.rb +103 -0
- data/lib/hecks/projector/exporter.rb +5 -1
- data/lib/hecks/quality_control/adapters/agent.rb +6 -7
- data/lib/hecks/quality_control/adapters/agent_profile.rb +16 -114
- data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +1 -1
- data/lib/hecks/runtime/loader/boot_steps.rb +0 -20
- data/lib/hecks/runtime/outbox.rb +2 -9
- data/lib/hecks/runtime/port_operation_interpreter.rb +1 -9
- data/lib/hecks/runtime/remote_dispatcher.rb +2 -2
- data/lib/hecks/runtime/value.rb +13 -0
- data/lib/hecks/rust_build/project_rust/target_ir.rb +23 -4
- data/lib/hecks/rust_build/wasm.rb +0 -12
- data/lib/hecks/rust_build/write_if_changed.rb +12 -12
- data/lib/hecks/rust_format.rb +77 -0
- data/lib/hecks/tools/fuzz_sweep/findings.rb +3 -9
- data/lib/hecks/tools/fuzz_sweep.rb +1 -3
- data/lib/hecks/tools/translation_audit/verdicts.rb +3 -28
- data/lib/hecks/tools/translation_audit.rb +8 -10
- data/lib/hecks/tools/translation_scaffold/reporting.rb +1 -1
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +2 -1
- data/lib/hecks.rb +3 -0
- data/prebuilt/{a883a7e568667aa5ac904c82daff356bdf9e996252a36af6def4a4a43efb967e.marshal → 02c09c73b272c1fb5a9ae6d87bb5e5e529208e00f6b4b597dc2c0fd70a9c0964.marshal} +0 -0
- data/prebuilt/{1a6f596be124228be93eba1d86dbc9d2933190fa389b284331241cb9f27115a0.marshal → bb84a835cf6a15136bad5ca03aa5bf93a5b92f5f72444618d3ef2412b987fb32.marshal} +0 -0
- data/prebuilt/{verdicts-872993b1aa26d73fe48f28251374b8585fed145d02aab42be0f00de386b6ff80.json → verdicts-f1f10da14c8e8f01b4c9b7e3e2d74be8cd2bf4676364ed17fa16975d2a076d75.json} +1 -1
- data/rust/Cargo.toml +40 -0
- data/rust/build/Cargo.toml +36 -0
- data/rust/build/clippy.toml +4 -0
- data/rust/build/src/adapters_pass.rs +196 -0
- data/rust/build/src/binding_pass.rs +54 -13
- data/rust/build/src/build_artifact.rs +23 -5
- data/rust/build/src/cargo_sync.rs +69 -26
- data/rust/build/src/lineage_pass.rs +101 -87
- data/rust/build/src/main.rs +11 -2
- data/rust/build/src/pipeline.rs +80 -19
- data/rust/build/src/reserved_names.rs +6 -50
- data/rust/build/src/resolve.rs +82 -35
- data/rust/build/src/rustfmt.rs +100 -0
- data/rust/build/src/seams_pass.rs +85 -16
- data/rust/build/src/subprocess.rs +30 -6
- data/rust/build/src/tmp.rs +5 -2
- data/rust/build/src/translation_pass.rs +72 -22
- data/rust/build/src/world_pass.rs +109 -0
- data/rust/clippy.toml +4 -0
- data/rust/codegen/Cargo.toml +36 -0
- data/rust/codegen/clippy.toml +4 -0
- data/rust/codegen/src/asks.rs +40 -14
- data/rust/codegen/src/attr.rs +2 -2
- data/rust/codegen/src/bridging.rs +676 -117
- data/rust/codegen/src/commands.rs +979 -373
- data/rust/codegen/src/constraints.rs +88 -25
- data/rust/codegen/src/dependency_planning.rs +128 -34
- data/rust/codegen/src/domain_generator.rs +300 -170
- data/rust/codegen/src/exemplar.rs +87 -32
- data/rust/codegen/src/expr_emitter.rs +54 -6
- data/rust/codegen/src/fielded.rs +193 -41
- data/rust/codegen/src/hecks_naming.rs +124 -16
- data/rust/codegen/src/hecks_naming_tests.rs +81 -0
- data/rust/codegen/src/json.rs +80 -44
- data/rust/codegen/src/json_codec.rs +585 -149
- data/rust/codegen/src/literal.rs +104 -138
- data/rust/codegen/src/main.rs +145 -52
- data/rust/codegen/src/manifest.rs +76 -14
- data/rust/codegen/src/mutations.rs +1212 -239
- data/rust/codegen/src/naming.rs +111 -60
- data/rust/codegen/src/optional_pass.rs +90 -28
- data/rust/codegen/src/ports.rs +5 -4
- data/rust/codegen/src/prelude.rs +139 -49
- data/rust/codegen/src/queries.rs +601 -226
- data/rust/codegen/src/reactions.rs +268 -74
- data/rust/codegen/src/read_models.rs +606 -205
- data/rust/codegen/src/reference_specs.rs +16 -3
- data/rust/codegen/src/registry.rs +128 -79
- data/rust/codegen/src/reserved_names.rs +6 -50
- data/rust/codegen/src/shared.rs +71 -22
- data/rust/codegen/src/sidecars.rs +15 -5
- data/rust/codegen/src/skip_reason.rs +8 -2
- data/rust/codegen/src/types.rs +306 -108
- data/rust/host/Cargo.lock +1 -0
- data/rust/host/Cargo.toml +40 -4
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/host/clippy.toml +4 -0
- data/rust/host/src/api.rs +698 -218
- data/rust/host/src/auth.rs +491 -214
- data/rust/host/src/b64.rs +91 -0
- data/rust/host/src/bin/expr_harness.rs +6 -2
- data/rust/host/src/bin/lineage_harness.rs +47 -11
- data/rust/host/src/bin/mint_harness.rs +70 -37
- data/rust/host/src/bin/validate_harness.rs +16 -8
- data/rust/host/src/boot.rs +144 -68
- data/rust/host/src/boundary_fuzz/api.rs +130 -32
- data/rust/host/src/boundary_fuzz/auth.rs +108 -23
- data/rust/host/src/boundary_fuzz/expr_json.rs +85 -11
- data/rust/host/src/boundary_fuzz/ir.rs +164 -12
- data/rust/host/src/boundary_fuzz/server.rs +283 -36
- data/rust/host/src/boundary_fuzz/web.rs +56 -8
- data/rust/host/src/checkout.rs +78 -32
- data/rust/host/src/clock.rs +8 -0
- data/rust/host/src/commerce_ir.rs +333 -95
- data/rust/host/src/crypto.rs +33 -0
- data/rust/host/src/dispatch.rs +990 -331
- data/rust/host/src/expr_json.rs +1021 -273
- data/rust/host/src/extension.rs +24 -4
- data/rust/host/src/field_hints.rs +11 -7
- data/rust/host/src/fuzz_support.rs +85 -13
- data/rust/host/src/host.rs +100 -0
- data/rust/host/src/http.rs +22 -0
- data/rust/host/src/ir.rs +326 -16
- data/rust/host/src/journal.rs +277 -176
- data/rust/host/src/lambda_client.rs +219 -53
- data/rust/host/src/lib.rs +16 -4
- data/rust/host/src/log.rs +62 -11
- data/rust/host/src/mint.rs +1153 -326
- data/rust/host/src/needs.rs +185 -44
- data/rust/host/src/payments/keystore.rs +197 -45
- data/rust/host/src/payments/roles.rs +39 -7
- data/rust/host/src/payments/tests.rs +1427 -282
- data/rust/host/src/payments.rs +391 -112
- data/rust/host/src/platform_signin/attack_tests.rs +5 -5
- data/rust/host/src/platform_signin/config.rs +11 -11
- data/rust/host/src/platform_signin/cookie.rs +2 -2
- data/rust/host/src/platform_signin/extension.rs +2 -1
- data/rust/host/src/platform_signin/handler.rs +9 -8
- data/rust/host/src/platform_signin/jti.rs +4 -2
- data/rust/host/src/platform_signin/jwks.rs +2 -1
- data/rust/host/src/platform_signin/jws.rs +2 -37
- data/rust/host/src/platform_signin/known_answer_tests.rs +5 -1
- data/rust/host/src/platform_signin/mode_tests.rs +5 -3
- data/rust/host/src/platform_signin/pages.rs +20 -20
- data/rust/host/src/platform_signin/session.rs +8 -1
- data/rust/host/src/platform_signin/settings.rs +6 -6
- data/rust/host/src/platform_signin/strict_json.rs +12 -9
- data/rust/host/src/platform_signin/verify.rs +41 -41
- data/rust/host/src/platform_signin/wiring_support.rs +13 -18
- data/rust/host/src/presentation.rs +207 -63
- data/rust/host/src/presentation_write.rs +502 -187
- data/rust/host/src/query_step.rs +69 -18
- data/rust/host/src/rate_limit.rs +541 -142
- data/rust/host/src/reference_transform.rs +77 -25
- data/rust/host/src/reference_validate.rs +443 -104
- data/rust/host/src/resend.rs +190 -55
- data/rust/host/src/secrets.rs +116 -21
- data/rust/host/src/server.rs +483 -104
- data/rust/host/src/storage_shape.rs +88 -21
- data/rust/host/src/test_pg.rs +17 -7
- data/rust/host/src/ui_schema.rs +495 -207
- data/rust/host/src/wasm_runner.rs +21 -6
- data/rust/host/src/web/commerce.rs +101 -23
- data/rust/host/src/web/newsletter/cooldown.rs +10 -3
- data/rust/host/src/web/newsletter/template.rs +26 -7
- data/rust/host/src/web/newsletter/tests.rs +112 -23
- data/rust/host/src/web/newsletter.rs +209 -59
- data/rust/host/src/web/newsletter_send.rs +251 -79
- data/rust/host/src/web/registration_receipt/tests.rs +16 -4
- data/rust/host/src/web/registration_receipt.rs +100 -24
- data/rust/host/src/web/registrations.rs +1384 -332
- data/rust/host/src/web/resend_webhook.rs +138 -80
- data/rust/host/src/web.rs +2416 -508
- data/rust/lsp/Cargo.toml +36 -0
- data/rust/lsp/clippy.toml +4 -0
- data/rust/lsp/src/json.rs +29 -22
- data/rust/lsp/src/main.rs +81 -31
- data/rust/lsp/src/outline.rs +23 -7
- data/rust/lsp/src/rpc.rs +17 -4
- data/rust/parser/Cargo.toml +36 -0
- data/rust/parser/clippy.toml +4 -0
- data/rust/parser/src/build/closed_sets.rs +6 -1
- data/rust/parser/src/build/identity.rs +4 -3
- data/rust/parser/src/build/mod.rs +1 -1
- data/rust/parser/src/build/named_pattern.rs +4 -1
- data/rust/parser/src/build/naming.rs +2 -80
- data/rust/parser/src/build/pattern_subset.rs +6 -8
- data/rust/parser/src/build/query_derive.rs +3 -1
- data/rust/parser/src/build/query_inference.rs +14 -5
- data/rust/parser/src/build/read_model.rs +4 -1
- data/rust/parser/src/build/references.rs +4 -3
- data/rust/parser/src/canonical.rs +34 -15
- data/rust/parser/src/emit.rs +187 -105
- data/rust/parser/src/expr/ast_json.rs +134 -42
- data/rust/parser/src/expr/evaluator.rs +27 -9
- data/rust/parser/src/expr/mod.rs +54 -13
- data/rust/parser/src/expr/resolver.rs +299 -92
- data/rust/parser/src/ir.rs +35 -3
- data/rust/parser/src/keywords.rs +7168 -516
- data/rust/parser/src/lex.rs +11 -25
- data/rust/parser/src/lib.rs +1 -0
- data/rust/parser/src/main.rs +65 -6
- data/rust/parser/src/parse/adapter.rs +157 -0
- data/rust/parser/src/parse/aggregate.rs +19 -10
- data/rust/parser/src/parse/chapter.rs +16 -17
- data/rust/parser/src/parse/command.rs +35 -16
- data/rust/parser/src/parse/domain_port.rs +4 -7
- data/rust/parser/src/parse/entity.rs +21 -11
- data/rust/parser/src/parse/hecksagon.rs +10 -4
- data/rust/parser/src/parse/lifecycle.rs +9 -5
- data/rust/parser/src/parse/mod.rs +22 -91
- data/rust/parser/src/parse/needs.rs +21 -12
- data/rust/parser/src/parse/policy.rs +2 -2
- data/rust/parser/src/parse/process_manager.rs +9 -11
- data/rust/parser/src/parse/query.rs +8 -4
- data/rust/parser/src/parse/read_model.rs +57 -16
- data/rust/parser/src/parse/value_object.rs +7 -2
- data/rust/parser/src/parse/world.rs +322 -0
- data/rust/parser/src/ruby_value.rs +7 -6
- data/rust/src/exemplar/json.rs +4 -4
- data/rust/src/exemplar/mutations.rs +18 -0
- data/rust/src/exemplar/read_models.rs +1 -10
- data/rust/src/kernel/attribute_shapes/composite.rs +54 -14
- data/rust/src/kernel/attribute_shapes/list.rs +2 -2
- data/rust/src/kernel/attribute_shapes/mod.rs +8 -8
- data/rust/src/kernel/attribute_shapes/scalar.rs +2 -2
- data/rust/src/kernel/cli.rs +229 -112
- data/rust/src/kernel/dispatch.rs +323 -115
- data/rust/src/kernel/dispatch_guard_tests.rs +327 -65
- data/rust/src/kernel/expr.rs +203 -86
- data/rust/src/kernel/expression_operators/arithmetic.rs +138 -25
- data/rust/src/kernel/expression_operators/comparison.rs +87 -2
- data/rust/src/kernel/expression_operators/enumeration.rs +106 -22
- data/rust/src/kernel/expression_operators/logical.rs +9 -3
- data/rust/src/kernel/expression_operators/membership.rs +3 -1
- data/rust/src/kernel/expression_operators/mod.rs +34 -21
- data/rust/src/kernel/expression_operators/pattern_match.rs +46 -11
- data/rust/src/kernel/expression_operators/positional.rs +84 -30
- data/rust/src/kernel/expression_operators/presence.rs +52 -13
- data/rust/src/kernel/expression_operators/sign_test.rs +3 -1
- data/rust/src/kernel/expression_operators/sized.rs +24 -8
- data/rust/src/kernel/expression_operators/text.rs +120 -27
- data/rust/src/kernel/expression_operators/to_string.rs +10 -4
- data/rust/src/kernel/json.rs +230 -148
- data/rust/src/kernel/mod.rs +46 -33
- data/rust/src/kernel/named_query.rs +63 -15
- data/rust/src/kernel/needs.rs +112 -25
- data/rust/src/kernel/orchestrate.rs +980 -581
- data/rust/src/kernel/orchestrate_guard_tests.rs +538 -140
- data/rust/src/kernel/pattern.rs +107 -34
- data/rust/src/kernel/query_comparators.rs +118 -41
- data/rust/src/kernel/query_ordering.rs +13 -5
- data/rust/src/kernel/read_model.rs +479 -181
- data/rust/src/kernel/reference_lookup.rs +122 -26
- data/rust/src/kernel/repository.rs +293 -66
- data/rust/src/kernel/routing.rs +58 -42
- data/rust/src/kernel/ruby_float.rs +139 -0
- data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +57 -57
- data/rust/src/kernel/vocab/entity_dispatch_order.rs +54 -54
- data/rust/src/kernel/vocab/field_hint.rs +19 -25
- data/rust/src/kernel/vocab/mod.rs +6 -6
- data/rust/src/kernel/vocab/query_comparator.rs +23 -24
- data/rust/src/kernel/vocab/refusal_template.rs +501 -297
- data/rust/src/lib.rs +50 -0
- data/rust/src/main.rs +4 -1
- data/rust/web/Cargo.toml +36 -0
- data/rust/web/clippy.toml +4 -0
- metadata +82 -15
- data/examples/pizzas/bluebook/hecksagon/mock_stripe_payment_adapter.rb +0 -36
- data/lib/hecks/adapters/driven/google_authentication.adapter +0 -3
- data/lib/hecks/adapters/driven/google_authentication.rb +0 -99
- data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +0 -3
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +0 -32
- data/lib/hecks/ports/authentication.port +0 -6
- data/lib/hecks/ports/authentication.rb +0 -67
- data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +0 -255
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +0 -22
- data/rust/host/src/approval.rs +0 -652
data/rust/src/kernel/dispatch.rs
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
//! Generic dispatch loops over the declared `AggregateStep`/`EntityStep` order.
|
|
2
2
|
//! Argument gates run in `decode_*_arguments`, before the router resolves identity.
|
|
3
3
|
|
|
4
|
-
use super::expr::{
|
|
4
|
+
use super::expr::{
|
|
5
|
+
interpret, EvalContext, Expr, Field, Fielded, NoFields, StateFirst, Value, WithOld, WithParent,
|
|
6
|
+
};
|
|
5
7
|
use super::refusal_wording::{
|
|
6
|
-
AlreadyExistsCreatingDuplicateArgs, LifecycleRefusedTransitionBlockedArgs,
|
|
7
|
-
NotFoundRecordMissingArgs,
|
|
8
|
+
AlreadyExistsCreatingDuplicateArgs, LifecycleRefusedTransitionBlockedArgs,
|
|
9
|
+
NotFoundCreatingNoIdentityArgs, NotFoundEntityElementMissingArgs, NotFoundRecordMissingArgs,
|
|
8
10
|
};
|
|
9
11
|
use super::vocab::{AggregateStep, EntityStep};
|
|
10
12
|
use super::{Event, Json, MutationRecord, Refusal, Repository, SetProjectedField, ToJson};
|
|
@@ -34,7 +36,7 @@ pub struct EntityInvariants {
|
|
|
34
36
|
pub name: &'static str,
|
|
35
37
|
pub list_field: &'static str,
|
|
36
38
|
pub specs: Vec<InvariantSpec>,
|
|
37
|
-
pub nested: Vec<
|
|
39
|
+
pub nested: Vec<Self>,
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
pub struct InvariantSet {
|
|
@@ -42,11 +44,21 @@ pub struct InvariantSet {
|
|
|
42
44
|
pub entities: Vec<EntityInvariants>,
|
|
43
45
|
}
|
|
44
46
|
|
|
45
|
-
pub fn enforce_invariants(
|
|
47
|
+
pub fn enforce_invariants(
|
|
48
|
+
record: &dyn Fielded,
|
|
49
|
+
aggregate_name: &str,
|
|
50
|
+
set: &InvariantSet,
|
|
51
|
+
) -> Result<(), Refusal> {
|
|
46
52
|
for rule in &set.aggregate {
|
|
47
|
-
let ctx = EvalContext {
|
|
53
|
+
let ctx = EvalContext {
|
|
54
|
+
args: &NoFields,
|
|
55
|
+
instance: record,
|
|
56
|
+
};
|
|
48
57
|
if !interpret(&rule.expr, &ctx)?.truthy() {
|
|
49
|
-
return Err(Refusal::InvariantViolation(format!(
|
|
58
|
+
return Err(Refusal::InvariantViolation(format!(
|
|
59
|
+
"{aggregate_name} refused — {}",
|
|
60
|
+
rule.description
|
|
61
|
+
)));
|
|
50
62
|
}
|
|
51
63
|
}
|
|
52
64
|
for entity in &set.entities {
|
|
@@ -55,15 +67,31 @@ pub fn enforce_invariants(record: &dyn Fielded, aggregate_name: &str, set: &Inva
|
|
|
55
67
|
Ok(())
|
|
56
68
|
}
|
|
57
69
|
|
|
58
|
-
fn enforce_entity_invariants(
|
|
59
|
-
|
|
70
|
+
fn enforce_entity_invariants(
|
|
71
|
+
owner: &dyn Fielded,
|
|
72
|
+
entity: &EntityInvariants,
|
|
73
|
+
) -> Result<(), Refusal> {
|
|
74
|
+
let Some(elements) = owner.items(entity.list_field) else {
|
|
75
|
+
return Ok(());
|
|
76
|
+
};
|
|
60
77
|
for element in elements {
|
|
61
|
-
let Field::Nested(element) = element else {
|
|
62
|
-
|
|
78
|
+
let Field::Nested(element) = element else {
|
|
79
|
+
continue;
|
|
80
|
+
};
|
|
81
|
+
let with_parent = WithParent {
|
|
82
|
+
args: &NoFields,
|
|
83
|
+
parent: owner,
|
|
84
|
+
};
|
|
63
85
|
for rule in &entity.specs {
|
|
64
|
-
let ctx = EvalContext {
|
|
86
|
+
let ctx = EvalContext {
|
|
87
|
+
args: &with_parent,
|
|
88
|
+
instance: element,
|
|
89
|
+
};
|
|
65
90
|
if !interpret(&rule.expr, &ctx)?.truthy() {
|
|
66
|
-
return Err(Refusal::InvariantViolation(format!(
|
|
91
|
+
return Err(Refusal::InvariantViolation(format!(
|
|
92
|
+
"{} refused — {}",
|
|
93
|
+
entity.name, rule.description
|
|
94
|
+
)));
|
|
67
95
|
}
|
|
68
96
|
}
|
|
69
97
|
for nested in &entity.nested {
|
|
@@ -91,12 +119,17 @@ pub enum Hydrate<'a, T> {
|
|
|
91
119
|
///
|
|
92
120
|
/// `state_independent` defers the `AlreadyExists` check to just before `repo.save`, for
|
|
93
121
|
/// commands whose givens, ensures and mutations never read the aggregate's own state.
|
|
94
|
-
Create {
|
|
122
|
+
Create {
|
|
123
|
+
id: String,
|
|
124
|
+
build: Box<dyn FnOnce() -> T + 'a>,
|
|
125
|
+
state_independent: bool,
|
|
126
|
+
},
|
|
95
127
|
/// An acting command: the identity already names a record.
|
|
96
128
|
Act { id: String },
|
|
97
129
|
}
|
|
98
130
|
|
|
99
|
-
|
|
131
|
+
// reason: the generated callers own `transition`/`payload` and pass them by value.
|
|
132
|
+
#[allow(clippy::too_many_arguments, clippy::needless_pass_by_value)]
|
|
100
133
|
pub fn dispatch<'a, T, R>(
|
|
101
134
|
repo: &mut R,
|
|
102
135
|
hydrate: Hydrate<'a, T>,
|
|
@@ -152,7 +185,7 @@ where
|
|
|
152
185
|
| AggregateStep::RefuseRoleMismatch
|
|
153
186
|
| AggregateStep::ResolveReferences => {}
|
|
154
187
|
AggregateStep::Hydrate => {
|
|
155
|
-
let hydrate = hydrate.take()
|
|
188
|
+
let hydrate = ordered(hydrate.take(), ONCE)?;
|
|
156
189
|
hydrated = Some(hydrate_record(
|
|
157
190
|
repo,
|
|
158
191
|
hydrate,
|
|
@@ -163,21 +196,32 @@ where
|
|
|
163
196
|
)?);
|
|
164
197
|
}
|
|
165
198
|
AggregateStep::EnforceGivens => {
|
|
166
|
-
let (id, record) = hydrated.as_ref()
|
|
167
|
-
enforce_givens(
|
|
199
|
+
let (id, record) = ordered(hydrated.as_ref(), HYDRATED)?;
|
|
200
|
+
enforce_givens(
|
|
201
|
+
record,
|
|
202
|
+
args,
|
|
203
|
+
givens,
|
|
204
|
+
command_name,
|
|
205
|
+
aggregate_qualified_name,
|
|
206
|
+
id,
|
|
207
|
+
)?;
|
|
168
208
|
}
|
|
169
209
|
AggregateStep::AdmissibleTransition => {
|
|
170
|
-
let (_, record) = hydrated.as_ref()
|
|
210
|
+
let (_, record) = ordered(hydrated.as_ref(), HYDRATED)?;
|
|
171
211
|
admissible_transition(record, transition.as_ref(), command_name)?;
|
|
172
212
|
}
|
|
173
213
|
// Folded into `Hydrate::Create.build`: givens already saw the built record.
|
|
174
214
|
AggregateStep::AssignCreationAttributes => {}
|
|
175
215
|
AggregateStep::ApplyMutations => {
|
|
176
|
-
let (_, record) = hydrated.as_mut()
|
|
216
|
+
let (_, record) = ordered(hydrated.as_mut(), HYDRATED)?;
|
|
177
217
|
// The state as the givens saw it — `old` inside an `ensures`,
|
|
178
218
|
// `old` for `ensures`: the state before the mutation, cloned only if needed.
|
|
179
|
-
old_snapshot = if ensures.is_empty() {
|
|
180
|
-
|
|
219
|
+
old_snapshot = if ensures.is_empty() {
|
|
220
|
+
None
|
|
221
|
+
} else {
|
|
222
|
+
Some(record.clone())
|
|
223
|
+
};
|
|
224
|
+
(ordered(apply_mutations.take(), ONCE)?)(record)?;
|
|
181
225
|
}
|
|
182
226
|
// Written by the generated `apply_mutations` closure as its last line.
|
|
183
227
|
AggregateStep::AdvanceLifecycle => {}
|
|
@@ -185,21 +229,21 @@ where
|
|
|
185
229
|
// `apply_entity_command` on the record in hand.
|
|
186
230
|
AggregateStep::DelegateToEntity => {}
|
|
187
231
|
AggregateStep::EnforceEnsures => {
|
|
188
|
-
let (_, record) = hydrated.as_ref()
|
|
232
|
+
let (_, record) = ordered(hydrated.as_ref(), HYDRATED)?;
|
|
189
233
|
if let Some(old) = &old_snapshot {
|
|
190
234
|
enforce_ensures(record, old, args, ensures, command_name)?;
|
|
191
235
|
}
|
|
192
236
|
}
|
|
193
237
|
AggregateStep::EnforceInvariants => {
|
|
194
|
-
let (_, record) = hydrated.as_ref()
|
|
238
|
+
let (_, record) = ordered(hydrated.as_ref(), HYDRATED)?;
|
|
195
239
|
enforce_invariants(record, aggregate_name, invariants)?;
|
|
196
240
|
}
|
|
197
241
|
AggregateStep::Save => {
|
|
198
|
-
let (id, record) = hydrated.as_mut()
|
|
242
|
+
let (id, record) = ordered(hydrated.as_mut(), HYDRATED)?;
|
|
199
243
|
// Tenant boundary first: it precedes `seed_projections` and the write.
|
|
200
|
-
tenant_boundary_check.take()
|
|
244
|
+
ordered(tenant_boundary_check.take(), ONCE)??;
|
|
201
245
|
|
|
202
|
-
for (field, value) in seed_projections.take()
|
|
246
|
+
for (field, value) in ordered(seed_projections.take(), ONCE)? {
|
|
203
247
|
record.set_projected_field(field, value);
|
|
204
248
|
}
|
|
205
249
|
|
|
@@ -220,13 +264,13 @@ where
|
|
|
220
264
|
persist(repo, id, record, aggregate_qualified_name, mutations);
|
|
221
265
|
}
|
|
222
266
|
AggregateStep::Emit => {
|
|
223
|
-
let (id, _) = hydrated.as_ref()
|
|
267
|
+
let (id, _) = ordered(hydrated.as_ref(), HYDRATED)?;
|
|
224
268
|
events = emitted(emits, aggregate_qualified_name, id, &payload);
|
|
225
269
|
}
|
|
226
270
|
}
|
|
227
271
|
}
|
|
228
272
|
|
|
229
|
-
let (_, record) = hydrated
|
|
273
|
+
let (_, record) = ordered(hydrated, HYDRATED)?;
|
|
230
274
|
Ok((record, events))
|
|
231
275
|
}
|
|
232
276
|
|
|
@@ -245,13 +289,21 @@ where
|
|
|
245
289
|
R: Repository<T>,
|
|
246
290
|
{
|
|
247
291
|
match hydrate {
|
|
248
|
-
Hydrate::Create {
|
|
292
|
+
Hydrate::Create {
|
|
293
|
+
id,
|
|
294
|
+
build,
|
|
295
|
+
state_independent,
|
|
296
|
+
} => {
|
|
249
297
|
// A blank identity names nothing; refuse before any lookup rather than mint a
|
|
250
298
|
// record keyed by "".
|
|
251
299
|
if id.is_empty() {
|
|
252
300
|
return Err(Refusal::NotFound(
|
|
253
|
-
NotFoundCreatingNoIdentityArgs {
|
|
254
|
-
|
|
301
|
+
NotFoundCreatingNoIdentityArgs {
|
|
302
|
+
command: command_name,
|
|
303
|
+
aggregate: aggregate_name,
|
|
304
|
+
identity: identity_reading,
|
|
305
|
+
}
|
|
306
|
+
.render_args(),
|
|
255
307
|
));
|
|
256
308
|
}
|
|
257
309
|
// State-independent creates defer this check to `Save`.
|
|
@@ -274,8 +326,12 @@ where
|
|
|
274
326
|
// Shared with `dispatch_entity`'s parent lookup: both raise `record_missing`.
|
|
275
327
|
let record = repo.find(&id).ok_or_else(|| {
|
|
276
328
|
Refusal::NotFound(
|
|
277
|
-
NotFoundRecordMissingArgs {
|
|
278
|
-
|
|
329
|
+
NotFoundRecordMissingArgs {
|
|
330
|
+
aggregate: aggregate_name,
|
|
331
|
+
identity: identity_reading,
|
|
332
|
+
offered: &format!("{id:?}"),
|
|
333
|
+
}
|
|
334
|
+
.render_args(),
|
|
279
335
|
)
|
|
280
336
|
})?;
|
|
281
337
|
Ok((id, record))
|
|
@@ -293,7 +349,10 @@ fn enforce_givens(
|
|
|
293
349
|
id: &str,
|
|
294
350
|
) -> Result<(), Refusal> {
|
|
295
351
|
for given in givens {
|
|
296
|
-
let ctx = EvalContext {
|
|
352
|
+
let ctx = EvalContext {
|
|
353
|
+
args,
|
|
354
|
+
instance: record,
|
|
355
|
+
};
|
|
297
356
|
if !interpret(&given.expr, &ctx)?.truthy() {
|
|
298
357
|
// The `corrects` wording interpolates the record id, so it is built here.
|
|
299
358
|
if let Some(event_name) = given.corrects_event {
|
|
@@ -302,7 +361,10 @@ fn enforce_givens(
|
|
|
302
361
|
"{command_name} refused — corrects {event_name}, but {aggregate_qualified_name} #{id} has never emitted it"
|
|
303
362
|
)));
|
|
304
363
|
}
|
|
305
|
-
return Err(Refusal::GivenNotMet(format!(
|
|
364
|
+
return Err(Refusal::GivenNotMet(format!(
|
|
365
|
+
"{command_name} refused — {}",
|
|
366
|
+
given.description
|
|
367
|
+
)));
|
|
306
368
|
}
|
|
307
369
|
}
|
|
308
370
|
Ok(())
|
|
@@ -310,8 +372,14 @@ fn enforce_givens(
|
|
|
310
372
|
|
|
311
373
|
/// `admissible_transition` — shared by the aggregate record and an entity
|
|
312
374
|
/// element, which check it identically.
|
|
313
|
-
fn admissible_transition(
|
|
314
|
-
|
|
375
|
+
fn admissible_transition(
|
|
376
|
+
instance: &dyn Fielded,
|
|
377
|
+
transition: Option<&TransitionCheck>,
|
|
378
|
+
command_name: &str,
|
|
379
|
+
) -> Result<(), Refusal> {
|
|
380
|
+
let Some(check) = transition else {
|
|
381
|
+
return Ok(());
|
|
382
|
+
};
|
|
315
383
|
match instance.field(check.field) {
|
|
316
384
|
Some(Field::Value(Value::Str(current))) => {
|
|
317
385
|
if !check.from_states.contains(¤t.as_str()) {
|
|
@@ -348,19 +416,33 @@ fn enforce_ensures(
|
|
|
348
416
|
) -> Result<(), Refusal> {
|
|
349
417
|
// C2.3 — the settled state first; see `StateFirst`.
|
|
350
418
|
let state_first = StateFirst { args, settled };
|
|
351
|
-
let with_old = WithOld {
|
|
419
|
+
let with_old = WithOld {
|
|
420
|
+
args: &state_first,
|
|
421
|
+
old,
|
|
422
|
+
};
|
|
352
423
|
for rule in ensures {
|
|
353
|
-
let ctx = EvalContext {
|
|
424
|
+
let ctx = EvalContext {
|
|
425
|
+
args: &with_old,
|
|
426
|
+
instance: settled,
|
|
427
|
+
};
|
|
354
428
|
if !interpret(&rule.expr, &ctx)?.truthy() {
|
|
355
|
-
return Err(Refusal::EnsuresNotMet(format!(
|
|
429
|
+
return Err(Refusal::EnsuresNotMet(format!(
|
|
430
|
+
"{command_name} refused — {}",
|
|
431
|
+
rule.description
|
|
432
|
+
)));
|
|
356
433
|
}
|
|
357
434
|
}
|
|
358
435
|
Ok(())
|
|
359
436
|
}
|
|
360
437
|
|
|
361
438
|
/// The write half of `save`: the repository write and its mutation record.
|
|
362
|
-
fn persist<T, R>(
|
|
363
|
-
|
|
439
|
+
fn persist<T, R>(
|
|
440
|
+
repo: &mut R,
|
|
441
|
+
id: &str,
|
|
442
|
+
record: &T,
|
|
443
|
+
aggregate_qualified_name: &str,
|
|
444
|
+
mutations: &mut Vec<MutationRecord>,
|
|
445
|
+
) where
|
|
364
446
|
T: Clone + ToJson,
|
|
365
447
|
R: Repository<T>,
|
|
366
448
|
{
|
|
@@ -374,7 +456,12 @@ where
|
|
|
374
456
|
}
|
|
375
457
|
|
|
376
458
|
/// `emit` — the declared events, in declared order.
|
|
377
|
-
fn emitted(
|
|
459
|
+
fn emitted(
|
|
460
|
+
emits: &[&'static str],
|
|
461
|
+
aggregate_qualified_name: &str,
|
|
462
|
+
id: &str,
|
|
463
|
+
payload: &Json,
|
|
464
|
+
) -> Vec<Event> {
|
|
378
465
|
emits
|
|
379
466
|
.iter()
|
|
380
467
|
.map(|name| Event {
|
|
@@ -475,7 +562,8 @@ struct ElementHalf<'a, 's, T, E, GetList, GetListMut, Matches, Apply> {
|
|
|
475
562
|
old_snapshot: Option<E>,
|
|
476
563
|
}
|
|
477
564
|
|
|
478
|
-
impl<
|
|
565
|
+
impl<T, E, GetList, GetListMut, Matches, Apply>
|
|
566
|
+
ElementHalf<'_, '_, T, E, GetList, GetListMut, Matches, Apply>
|
|
479
567
|
where
|
|
480
568
|
T: Fielded + Clone,
|
|
481
569
|
E: Fielded + Clone,
|
|
@@ -497,12 +585,17 @@ where
|
|
|
497
585
|
| EntityStep::ResolveReferences => Ok(()),
|
|
498
586
|
// The parent half: `dispatch_entity`'s own arms, or the delegating aggregate's
|
|
499
587
|
// `dispatch`.
|
|
500
|
-
EntityStep::HydrateParent
|
|
588
|
+
EntityStep::HydrateParent
|
|
589
|
+
| EntityStep::EnforceInvariants
|
|
590
|
+
| EntityStep::Save
|
|
591
|
+
| EntityStep::Emit => Ok(()),
|
|
501
592
|
EntityStep::LocateElement => self.locate_element(record),
|
|
502
593
|
EntityStep::EnforceGivens => self.enforce_givens(),
|
|
503
|
-
EntityStep::AdmissibleTransition =>
|
|
504
|
-
|
|
505
|
-
|
|
594
|
+
EntityStep::AdmissibleTransition => admissible_transition(
|
|
595
|
+
ordered(self.element.as_ref(), LOCATED)?,
|
|
596
|
+
self.transition.as_ref(),
|
|
597
|
+
self.command_name,
|
|
598
|
+
),
|
|
506
599
|
EntityStep::ApplyMutations => self.apply_mutations(record),
|
|
507
600
|
// Written by the generated `apply_mutations` closure (the entity's own lifecycle).
|
|
508
601
|
EntityStep::AdvanceLifecycle => Ok(()),
|
|
@@ -511,18 +604,21 @@ where
|
|
|
511
604
|
}
|
|
512
605
|
|
|
513
606
|
fn locate_element(&mut self, record: &T) -> Result<(), Refusal> {
|
|
514
|
-
let position = (self.get_list)(record)
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
607
|
+
let position = (self.get_list)(record)
|
|
608
|
+
.iter()
|
|
609
|
+
.position(|el| (self.matches)(el))
|
|
610
|
+
.ok_or_else(|| {
|
|
611
|
+
Refusal::NotFound(
|
|
612
|
+
NotFoundEntityElementMissingArgs {
|
|
613
|
+
entity: self.entity_name,
|
|
614
|
+
identity: self.entity_identity_reading,
|
|
615
|
+
wants: self.wants,
|
|
616
|
+
aggregate: self.aggregate_name,
|
|
617
|
+
parent_id: &format!("{:?}", self.parent_id),
|
|
618
|
+
}
|
|
619
|
+
.render_args(),
|
|
620
|
+
)
|
|
621
|
+
})?;
|
|
526
622
|
self.element = Some((self.get_list)(record)[position].clone());
|
|
527
623
|
self.position = Some(position);
|
|
528
624
|
self.parent_before = Some(record.clone());
|
|
@@ -530,15 +626,20 @@ where
|
|
|
530
626
|
}
|
|
531
627
|
|
|
532
628
|
fn enforce_givens(&self) -> Result<(), Refusal> {
|
|
533
|
-
let parent_before = self.parent_before.as_ref()
|
|
534
|
-
let element = self.element.as_ref()
|
|
629
|
+
let parent_before = ordered(self.parent_before.as_ref(), LOCATED)?;
|
|
630
|
+
let element = ordered(self.element.as_ref(), LOCATED)?;
|
|
535
631
|
|
|
536
632
|
// Entity-level `corrects` is asked of the parent record and root aggregate, since an
|
|
537
633
|
// entity has no event stream. It runs after the element lookup, before declared givens,
|
|
538
634
|
// and proves the parent emitted the event, not that this element did.
|
|
539
635
|
for given in self.givens {
|
|
540
|
-
let Some(event_name) = given.corrects_event else {
|
|
541
|
-
|
|
636
|
+
let Some(event_name) = given.corrects_event else {
|
|
637
|
+
continue;
|
|
638
|
+
};
|
|
639
|
+
let ctx = EvalContext {
|
|
640
|
+
args: self.args,
|
|
641
|
+
instance: parent_before,
|
|
642
|
+
};
|
|
542
643
|
if !interpret(&given.expr, &ctx)?.truthy() {
|
|
543
644
|
return Err(Refusal::NothingToCorrect(format!(
|
|
544
645
|
"{} refused — corrects {event_name}, but {} #{} has never emitted it",
|
|
@@ -547,42 +648,69 @@ where
|
|
|
547
648
|
}
|
|
548
649
|
}
|
|
549
650
|
|
|
550
|
-
let with_parent = WithParent {
|
|
551
|
-
|
|
651
|
+
let with_parent = WithParent {
|
|
652
|
+
args: self.args,
|
|
653
|
+
parent: parent_before,
|
|
654
|
+
};
|
|
655
|
+
let given_args: &dyn Fielded = if self.parent_in_args {
|
|
656
|
+
&with_parent
|
|
657
|
+
} else {
|
|
658
|
+
self.args
|
|
659
|
+
};
|
|
552
660
|
for given in self.givens {
|
|
553
661
|
// Handled above; the `corrects` flag field lives on the parent, not the element.
|
|
554
662
|
if given.corrects_event.is_some() {
|
|
555
663
|
continue;
|
|
556
664
|
}
|
|
557
|
-
let ctx = EvalContext {
|
|
665
|
+
let ctx = EvalContext {
|
|
666
|
+
args: given_args,
|
|
667
|
+
instance: element,
|
|
668
|
+
};
|
|
558
669
|
if !interpret(&given.expr, &ctx)?.truthy() {
|
|
559
|
-
return Err(Refusal::GivenNotMet(format!(
|
|
670
|
+
return Err(Refusal::GivenNotMet(format!(
|
|
671
|
+
"{} refused — {}",
|
|
672
|
+
self.command_name, given.description
|
|
673
|
+
)));
|
|
560
674
|
}
|
|
561
675
|
}
|
|
562
676
|
Ok(())
|
|
563
677
|
}
|
|
564
678
|
|
|
565
679
|
fn apply_mutations(&mut self, record: &mut T) -> Result<(), Refusal> {
|
|
566
|
-
let position = self.position
|
|
567
|
-
let mut element = self.element.take()
|
|
568
|
-
self.old_snapshot = if self.ensures.is_empty() {
|
|
569
|
-
|
|
570
|
-
|
|
680
|
+
let position = ordered(self.position, LOCATED)?;
|
|
681
|
+
let mut element = ordered(self.element.take(), LOCATED)?;
|
|
682
|
+
self.old_snapshot = if self.ensures.is_empty() {
|
|
683
|
+
None
|
|
684
|
+
} else {
|
|
685
|
+
Some(element.clone())
|
|
686
|
+
};
|
|
687
|
+
(ordered(self.apply_mutations.take(), ONCE)?)(&mut element)?;
|
|
688
|
+
(ordered(self.get_list_mut.take(), ONCE)?)(record)[position] = element;
|
|
571
689
|
Ok(())
|
|
572
690
|
}
|
|
573
691
|
|
|
574
692
|
fn enforce_ensures(&self, record: &T) -> Result<(), Refusal> {
|
|
575
|
-
let Some(old) = &self.old_snapshot else {
|
|
576
|
-
|
|
693
|
+
let Some(old) = &self.old_snapshot else {
|
|
694
|
+
return Ok(());
|
|
695
|
+
};
|
|
696
|
+
let position = ordered(self.position, LOCATED)?;
|
|
577
697
|
let parent_after = record.clone();
|
|
578
|
-
let with_parent = WithParent {
|
|
579
|
-
|
|
698
|
+
let with_parent = WithParent {
|
|
699
|
+
args: self.args,
|
|
700
|
+
parent: &parent_after,
|
|
701
|
+
};
|
|
702
|
+
let ensures_args: &dyn Fielded = if self.parent_in_args {
|
|
703
|
+
&with_parent
|
|
704
|
+
} else {
|
|
705
|
+
self.args
|
|
706
|
+
};
|
|
580
707
|
let settled = &(self.get_list)(record)[position];
|
|
581
708
|
enforce_ensures(settled, old, ensures_args, self.ensures, self.command_name)
|
|
582
709
|
}
|
|
583
710
|
}
|
|
584
711
|
|
|
585
|
-
|
|
712
|
+
// reason: the generated callers own `payload` and pass it by value.
|
|
713
|
+
#[allow(clippy::too_many_arguments, clippy::needless_pass_by_value)]
|
|
586
714
|
pub fn dispatch_entity<'a, T, E, R>(
|
|
587
715
|
repo: &mut R,
|
|
588
716
|
parent_id: &str,
|
|
@@ -670,14 +798,18 @@ where
|
|
|
670
798
|
| EntityStep::ApplyMutations
|
|
671
799
|
| EntityStep::AdvanceLifecycle
|
|
672
800
|
| EntityStep::EnforceEnsures => {
|
|
673
|
-
element.run(step, hydrated.as_mut()
|
|
801
|
+
element.run(step, ordered(hydrated.as_mut(), HYDRATED)?)?;
|
|
674
802
|
}
|
|
675
803
|
EntityStep::EnforceInvariants => {
|
|
676
|
-
enforce_invariants(
|
|
804
|
+
enforce_invariants(
|
|
805
|
+
ordered(hydrated.as_ref(), HYDRATED)?,
|
|
806
|
+
aggregate_name,
|
|
807
|
+
invariants,
|
|
808
|
+
)?;
|
|
677
809
|
}
|
|
678
810
|
EntityStep::Save => {
|
|
679
|
-
let record = hydrated.as_mut()
|
|
680
|
-
for (field, value) in seed_projections.take()
|
|
811
|
+
let record = ordered(hydrated.as_mut(), HYDRATED)?;
|
|
812
|
+
for (field, value) in ordered(seed_projections.take(), ONCE)? {
|
|
681
813
|
record.set_projected_field(field, value);
|
|
682
814
|
}
|
|
683
815
|
persist(repo, parent_id, record, aggregate_qualified_name, mutations);
|
|
@@ -688,7 +820,13 @@ where
|
|
|
688
820
|
}
|
|
689
821
|
}
|
|
690
822
|
|
|
691
|
-
Ok((hydrated
|
|
823
|
+
Ok((ordered(hydrated, HYDRATED)?, events))
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/// Turns a slot the declared step order guarantees filled into a value, or a `Fault` naming the
|
|
827
|
+
/// order that was broken; the const assertions below make the `Err` unreachable.
|
|
828
|
+
fn ordered<V>(slot: Option<V>, because: &str) -> Result<V, Refusal> {
|
|
829
|
+
slot.ok_or_else(|| Refusal::Fault(because.to_string()))
|
|
692
830
|
}
|
|
693
831
|
|
|
694
832
|
const HYDRATED: &str = "the declared order hydrates before any step that reads the record (const-asserted in this file)";
|
|
@@ -732,7 +870,9 @@ impl<A> ArgumentGates<'_, A> {
|
|
|
732
870
|
Ok(())
|
|
733
871
|
}
|
|
734
872
|
ArgumentGate::RefuseRoleMismatch => (self.refuse_role_mismatch)(),
|
|
735
|
-
ArgumentGate::ResolveReferences =>
|
|
873
|
+
ArgumentGate::ResolveReferences => {
|
|
874
|
+
(self.resolve_references)(ordered(args.as_ref(), NORMALIZED)?)
|
|
875
|
+
}
|
|
736
876
|
}
|
|
737
877
|
}
|
|
738
878
|
}
|
|
@@ -740,7 +880,10 @@ impl<A> ArgumentGates<'_, A> {
|
|
|
740
880
|
/// `AggregateStep::ORDER`'s argument gates over one aggregate command's facts:
|
|
741
881
|
/// the normalized `<Args>`, or the first refusal in declared order. Every
|
|
742
882
|
/// other step is `dispatch`'s, entered once the router has resolved identity.
|
|
743
|
-
pub fn decode_aggregate_arguments<A>(
|
|
883
|
+
pub fn decode_aggregate_arguments<A>(
|
|
884
|
+
facts: &Json,
|
|
885
|
+
gates: &ArgumentGates<'_, A>,
|
|
886
|
+
) -> Result<A, Refusal> {
|
|
744
887
|
let mut args = None;
|
|
745
888
|
for step in AggregateStep::ORDER {
|
|
746
889
|
#[deny(clippy::wildcard_enum_match_arm)]
|
|
@@ -765,12 +908,15 @@ pub fn decode_aggregate_arguments<A>(facts: &Json, gates: &ArgumentGates<'_, A>)
|
|
|
765
908
|
};
|
|
766
909
|
gates.run(gate, facts, &mut args)?;
|
|
767
910
|
}
|
|
768
|
-
|
|
911
|
+
ordered(args, NORMALIZED)
|
|
769
912
|
}
|
|
770
913
|
|
|
771
914
|
/// `EntityStep::ORDER`'s argument gates over one entity command's facts — see
|
|
772
915
|
/// `decode_aggregate_arguments`. Every other step is `dispatch_entity`'s.
|
|
773
|
-
pub fn decode_entity_arguments<A>(
|
|
916
|
+
pub fn decode_entity_arguments<A>(
|
|
917
|
+
facts: &Json,
|
|
918
|
+
gates: &ArgumentGates<'_, A>,
|
|
919
|
+
) -> Result<A, Refusal> {
|
|
774
920
|
let mut args = None;
|
|
775
921
|
for step in EntityStep::ORDER {
|
|
776
922
|
#[deny(clippy::wildcard_enum_match_arm)]
|
|
@@ -794,10 +940,11 @@ pub fn decode_entity_arguments<A>(facts: &Json, gates: &ArgumentGates<'_, A>) ->
|
|
|
794
940
|
};
|
|
795
941
|
gates.run(gate, facts, &mut args)?;
|
|
796
942
|
}
|
|
797
|
-
|
|
943
|
+
ordered(args, NORMALIZED)
|
|
798
944
|
}
|
|
799
945
|
|
|
800
946
|
/// A step's index in `AggregateStep::ORDER`, usable in a const context.
|
|
947
|
+
#[allow(clippy::panic)] // reason: evaluated at compile time; a missing step fails the build.
|
|
801
948
|
const fn aggregate_position(step: AggregateStep) -> usize {
|
|
802
949
|
let mut index = 0;
|
|
803
950
|
while index < AggregateStep::ORDER.len() {
|
|
@@ -810,6 +957,7 @@ const fn aggregate_position(step: AggregateStep) -> usize {
|
|
|
810
957
|
}
|
|
811
958
|
|
|
812
959
|
/// A step's index in `EntityStep::ORDER`, usable in a const context.
|
|
960
|
+
#[allow(clippy::panic)] // reason: evaluated at compile time; a missing step fails the build.
|
|
813
961
|
const fn entity_position(step: EntityStep) -> usize {
|
|
814
962
|
let mut index = 0;
|
|
815
963
|
while index < EntityStep::ORDER.len() {
|
|
@@ -825,6 +973,7 @@ const fn entity_position(step: EntityStep) -> usize {
|
|
|
825
973
|
// vocabulary.bluebook fails the build instead of panicking at dispatch time.
|
|
826
974
|
const _: () = {
|
|
827
975
|
use AggregateStep as A;
|
|
976
|
+
use EntityStep as E;
|
|
828
977
|
let hydrate = aggregate_position(A::Hydrate);
|
|
829
978
|
assert!(hydrate < aggregate_position(A::EnforceGivens));
|
|
830
979
|
assert!(hydrate < aggregate_position(A::AdmissibleTransition));
|
|
@@ -847,7 +996,6 @@ const _: () = {
|
|
|
847
996
|
assert!(aggregate_position(A::ResolveReferences) < hydrate);
|
|
848
997
|
assert!(aggregate_position(A::NormalizeArgs) < aggregate_position(A::ResolveReferences));
|
|
849
998
|
|
|
850
|
-
use EntityStep as E;
|
|
851
999
|
let hydrate_parent = entity_position(E::HydrateParent);
|
|
852
1000
|
assert!(hydrate_parent < entity_position(E::LocateElement));
|
|
853
1001
|
assert!(hydrate_parent < entity_position(E::EnforceInvariants));
|
|
@@ -914,12 +1062,18 @@ pub const fn aggregate_step_site(step: AggregateStep) -> StepSite {
|
|
|
914
1062
|
A::EnforceGivens => StepSite::Kernel,
|
|
915
1063
|
A::AdmissibleTransition => StepSite::Kernel,
|
|
916
1064
|
// `Hydrate::Create.build`.
|
|
917
|
-
A::AssignCreationAttributes =>
|
|
1065
|
+
A::AssignCreationAttributes => {
|
|
1066
|
+
StepSite::FoldedInto(AggregateStepOrEntityStep::Aggregate(A::Hydrate))
|
|
1067
|
+
}
|
|
918
1068
|
A::ApplyMutations => StepSite::Kernel,
|
|
919
1069
|
// The generated `apply_mutations` closure's last line.
|
|
920
|
-
A::AdvanceLifecycle =>
|
|
1070
|
+
A::AdvanceLifecycle => {
|
|
1071
|
+
StepSite::FoldedInto(AggregateStepOrEntityStep::Aggregate(A::ApplyMutations))
|
|
1072
|
+
}
|
|
921
1073
|
// The generated `apply_mutations` closure → `apply_entity_command`.
|
|
922
|
-
A::DelegateToEntity =>
|
|
1074
|
+
A::DelegateToEntity => {
|
|
1075
|
+
StepSite::FoldedInto(AggregateStepOrEntityStep::Aggregate(A::ApplyMutations))
|
|
1076
|
+
}
|
|
923
1077
|
A::EnforceEnsures => StepSite::Kernel,
|
|
924
1078
|
A::EnforceInvariants => StepSite::Kernel,
|
|
925
1079
|
A::Save => StepSite::Kernel,
|
|
@@ -944,7 +1098,9 @@ pub const fn entity_step_site(step: EntityStep) -> StepSite {
|
|
|
944
1098
|
E::EnforceGivens => StepSite::Kernel,
|
|
945
1099
|
E::AdmissibleTransition => StepSite::Kernel,
|
|
946
1100
|
E::ApplyMutations => StepSite::Kernel,
|
|
947
|
-
E::AdvanceLifecycle =>
|
|
1101
|
+
E::AdvanceLifecycle => {
|
|
1102
|
+
StepSite::FoldedInto(AggregateStepOrEntityStep::Entity(E::ApplyMutations))
|
|
1103
|
+
}
|
|
948
1104
|
E::EnforceEnsures => StepSite::Kernel,
|
|
949
1105
|
E::EnforceInvariants => StepSite::Kernel,
|
|
950
1106
|
E::Save => StepSite::Kernel,
|
|
@@ -980,18 +1136,33 @@ mod step_order_tests {
|
|
|
980
1136
|
|
|
981
1137
|
#[test]
|
|
982
1138
|
fn the_kernel_performs_exactly_these_aggregate_steps() {
|
|
983
|
-
let kernel: Vec<&str> =
|
|
984
|
-
|
|
1139
|
+
let kernel: Vec<&str> = AggregateStep::ORDER
|
|
1140
|
+
.iter()
|
|
1141
|
+
.filter(|s| aggregate_step_site(**s) == StepSite::Kernel)
|
|
1142
|
+
.map(AggregateStep::step)
|
|
1143
|
+
.collect();
|
|
985
1144
|
assert_eq!(
|
|
986
1145
|
kernel,
|
|
987
|
-
[
|
|
1146
|
+
[
|
|
1147
|
+
"hydrate",
|
|
1148
|
+
"enforce_givens",
|
|
1149
|
+
"admissible_transition",
|
|
1150
|
+
"apply_mutations",
|
|
1151
|
+
"enforce_ensures",
|
|
1152
|
+
"enforce_invariants",
|
|
1153
|
+
"save",
|
|
1154
|
+
"emit"
|
|
1155
|
+
]
|
|
988
1156
|
);
|
|
989
1157
|
}
|
|
990
1158
|
|
|
991
1159
|
#[test]
|
|
992
1160
|
fn the_kernel_performs_exactly_these_entity_steps() {
|
|
993
|
-
let kernel: Vec<&str> =
|
|
994
|
-
|
|
1161
|
+
let kernel: Vec<&str> = EntityStep::ORDER
|
|
1162
|
+
.iter()
|
|
1163
|
+
.filter(|s| entity_step_site(**s) == StepSite::Kernel)
|
|
1164
|
+
.map(EntityStep::step)
|
|
1165
|
+
.collect();
|
|
995
1166
|
assert_eq!(
|
|
996
1167
|
kernel,
|
|
997
1168
|
[
|
|
@@ -1012,16 +1183,30 @@ mod step_order_tests {
|
|
|
1012
1183
|
fn argument_gates_all_precede_the_first_kernel_step() {
|
|
1013
1184
|
// The router resolves identity between the two loops, so every
|
|
1014
1185
|
// argument gate sits ahead of the first kernel step.
|
|
1015
|
-
let first_kernel = AggregateStep::ORDER
|
|
1186
|
+
let first_kernel = AggregateStep::ORDER
|
|
1187
|
+
.iter()
|
|
1188
|
+
.position(|s| aggregate_step_site(*s) == StepSite::Kernel)
|
|
1189
|
+
.unwrap();
|
|
1016
1190
|
for step in AggregateStep::ORDER {
|
|
1017
1191
|
if aggregate_step_site(step) == StepSite::ArgumentGate {
|
|
1018
|
-
assert!(
|
|
1192
|
+
assert!(
|
|
1193
|
+
step.position() < first_kernel,
|
|
1194
|
+
"{} is an argument gate but is declared after hydrate",
|
|
1195
|
+
step.step()
|
|
1196
|
+
);
|
|
1019
1197
|
}
|
|
1020
1198
|
}
|
|
1021
|
-
let first_kernel = EntityStep::ORDER
|
|
1199
|
+
let first_kernel = EntityStep::ORDER
|
|
1200
|
+
.iter()
|
|
1201
|
+
.position(|s| entity_step_site(*s) == StepSite::Kernel)
|
|
1202
|
+
.unwrap();
|
|
1022
1203
|
for step in EntityStep::ORDER {
|
|
1023
1204
|
if entity_step_site(step) == StepSite::ArgumentGate {
|
|
1024
|
-
assert!(
|
|
1205
|
+
assert!(
|
|
1206
|
+
step.position() < first_kernel,
|
|
1207
|
+
"{} is an argument gate but is declared after hydrate_parent",
|
|
1208
|
+
step.step()
|
|
1209
|
+
);
|
|
1025
1210
|
}
|
|
1026
1211
|
}
|
|
1027
1212
|
}
|
|
@@ -1029,7 +1214,10 @@ mod step_order_tests {
|
|
|
1029
1214
|
use std::cell::RefCell;
|
|
1030
1215
|
|
|
1031
1216
|
/// Hooks that record which step ran, refusing at the steps named in `refuse`.
|
|
1032
|
-
fn recorded<R>(
|
|
1217
|
+
fn recorded<R>(
|
|
1218
|
+
refuse: &[&'static str],
|
|
1219
|
+
body: impl FnOnce(&ArgumentGates<'_, i64>) -> R,
|
|
1220
|
+
) -> (R, Vec<&'static str>) {
|
|
1033
1221
|
let ran = RefCell::new(Vec::new());
|
|
1034
1222
|
let hit = |step: &'static str| -> Result<(), Refusal> {
|
|
1035
1223
|
ran.borrow_mut().push(step);
|
|
@@ -1045,7 +1233,10 @@ mod step_order_tests {
|
|
|
1045
1233
|
let normalize = |_: &Json| hit("normalize_args").map(|()| 7);
|
|
1046
1234
|
let role = || hit("refuse_role_mismatch");
|
|
1047
1235
|
let references = |args: &i64| {
|
|
1048
|
-
assert_eq!(
|
|
1236
|
+
assert_eq!(
|
|
1237
|
+
*args, 7,
|
|
1238
|
+
"resolve_references reads what normalize_args produced"
|
|
1239
|
+
);
|
|
1049
1240
|
hit("resolve_references")
|
|
1050
1241
|
};
|
|
1051
1242
|
let gates = ArgumentGates {
|
|
@@ -1065,29 +1256,46 @@ mod step_order_tests {
|
|
|
1065
1256
|
let facts = Json::Object(Vec::new());
|
|
1066
1257
|
let (result, ran) = recorded(&[], |gates| decode_aggregate_arguments(&facts, gates));
|
|
1067
1258
|
assert_eq!(result.unwrap(), 7);
|
|
1068
|
-
let declared: Vec<&str> =
|
|
1069
|
-
|
|
1259
|
+
let declared: Vec<&str> = AggregateStep::ORDER
|
|
1260
|
+
.iter()
|
|
1261
|
+
.filter(|s| aggregate_step_site(**s) == StepSite::ArgumentGate)
|
|
1262
|
+
.map(AggregateStep::step)
|
|
1263
|
+
.collect();
|
|
1070
1264
|
assert_eq!(ran, declared);
|
|
1071
1265
|
|
|
1072
1266
|
let (result, ran) = recorded(&[], |gates| decode_entity_arguments(&facts, gates));
|
|
1073
1267
|
assert_eq!(result.unwrap(), 7);
|
|
1074
|
-
let declared: Vec<&str> =
|
|
1075
|
-
|
|
1268
|
+
let declared: Vec<&str> = EntityStep::ORDER
|
|
1269
|
+
.iter()
|
|
1270
|
+
.filter(|s| entity_step_site(**s) == StepSite::ArgumentGate)
|
|
1271
|
+
.map(EntityStep::step)
|
|
1272
|
+
.collect();
|
|
1076
1273
|
assert_eq!(ran, declared);
|
|
1077
1274
|
}
|
|
1078
1275
|
|
|
1079
1276
|
#[test]
|
|
1080
1277
|
fn the_earliest_declared_violated_gate_wins_and_later_hooks_never_run() {
|
|
1081
1278
|
let facts = Json::Object(Vec::new());
|
|
1082
|
-
let gates: Vec<&'static str> =
|
|
1083
|
-
|
|
1279
|
+
let gates: Vec<&'static str> = AggregateStep::ORDER
|
|
1280
|
+
.iter()
|
|
1281
|
+
.filter(|s| aggregate_step_site(**s) == StepSite::ArgumentGate)
|
|
1282
|
+
.map(AggregateStep::step)
|
|
1283
|
+
.collect();
|
|
1084
1284
|
for (i, earlier) in gates.iter().enumerate() {
|
|
1085
1285
|
for later in &gates[i + 1..] {
|
|
1086
|
-
let (result, ran) =
|
|
1087
|
-
|
|
1286
|
+
let (result, ran) =
|
|
1287
|
+
recorded(&[earlier, later], |g| decode_aggregate_arguments(&facts, g));
|
|
1288
|
+
assert_eq!(
|
|
1289
|
+
result.unwrap_err().to_string(),
|
|
1290
|
+
Refusal::TypeMismatch(earlier.to_string()).to_string()
|
|
1291
|
+
);
|
|
1088
1292
|
assert_eq!(ran.last(), Some(earlier));
|
|
1089
|
-
let (result, _) =
|
|
1090
|
-
|
|
1293
|
+
let (result, _) =
|
|
1294
|
+
recorded(&[earlier, later], |g| decode_entity_arguments(&facts, g));
|
|
1295
|
+
assert_eq!(
|
|
1296
|
+
result.unwrap_err().to_string(),
|
|
1297
|
+
Refusal::TypeMismatch(earlier.to_string()).to_string()
|
|
1298
|
+
);
|
|
1091
1299
|
}
|
|
1092
1300
|
}
|
|
1093
1301
|
}
|