hecks 1.3.0 → 1.4.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/lib/hecks/adapters/driven/claude_code.rb +7 -7
- data/lib/hecks/adapters/driven/d1.rb +187 -23
- data/lib/hecks/adapters/driven/folder.rb +10 -10
- data/lib/hecks/adapters/driven/google_authentication.rb +8 -8
- data/lib/hecks/adapters/driven/governance_authorization.rb +31 -13
- data/lib/hecks/adapters/driven/heki/journal.rb +5 -4
- data/lib/hecks/adapters/driven/heki/saga_store.rb +5 -5
- data/lib/hecks/adapters/driven/heki.rb +13 -7
- data/lib/hecks/adapters/driven/identity_registry.rb +2 -2
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +3 -3
- data/lib/hecks/adapters/driven/lambda/client.rb +34 -9
- data/lib/hecks/adapters/driven/lambda.rb +39 -33
- data/lib/hecks/adapters/driven/local_storage.rb +17 -10
- data/lib/hecks/adapters/driven/memory.rb +205 -9
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +1 -1
- data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
- data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
- data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
- data/lib/hecks/adapters/driven/postgres.rb +175 -28
- data/lib/hecks/adapters/driven/prism.rb +4 -4
- data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
- data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
- data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
- data/lib/hecks/adapters/driven/sqlite.rb +181 -21
- data/lib/hecks/adapters/driven.rb +4 -4
- data/lib/hecks/adapters/driving/github_webhook.rb +18 -18
- data/lib/hecks/behaviors/dsl.rb +2 -2
- data/lib/hecks/behaviors/expectations.rb +23 -23
- data/lib/hecks/behaviors/ir.rb +1 -1
- data/lib/hecks/behaviors/rspec.rb +1 -1
- data/lib/hecks/behaviors/runner.rb +2 -2
- data/lib/hecks/behaviors.rb +1 -1
- data/lib/hecks/bluebook/aggregate.rb +13 -13
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +10 -10
- data/lib/hecks/bluebook/assembly/build.rb +1 -1
- data/lib/hecks/bluebook/assembly/contract.rb +39 -16
- data/lib/hecks/bluebook/assembly/contracts.rb +40 -38
- data/lib/hecks/bluebook/assembly/marks.rb +18 -18
- data/lib/hecks/bluebook/assembly/specializer.rb +19 -21
- data/lib/hecks/bluebook/assembly.rb +9 -9
- data/lib/hecks/bluebook/attribute.rb +9 -9
- data/lib/hecks/bluebook/behaviour/aggregate.rb +11 -11
- data/lib/hecks/bluebook/behaviour/attribute.rb +5 -5
- data/lib/hecks/bluebook/behaviour/chapter.rb +23 -5
- data/lib/hecks/bluebook/behaviour/command.rb +23 -23
- data/lib/hecks/bluebook/behaviour/domain_port.rb +7 -7
- data/lib/hecks/bluebook/behaviour/entity.rb +8 -8
- data/lib/hecks/bluebook/behaviour/hexagon.rb +4 -4
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +5 -5
- data/lib/hecks/bluebook/behaviour/policy.rb +12 -12
- data/lib/hecks/bluebook/behaviour/process_manager.rb +7 -7
- data/lib/hecks/bluebook/behaviour/query.rb +1 -1
- data/lib/hecks/bluebook/behaviour/read_model.rb +8 -8
- data/lib/hecks/bluebook/behaviour/traits.rb +12 -12
- data/lib/hecks/bluebook/behaviour/value_object.rb +6 -6
- data/lib/hecks/bluebook/capabilities.rb +27 -0
- data/lib/hecks/bluebook/chapter.rb +28 -9
- data/lib/hecks/bluebook/command.rb +12 -12
- data/lib/hecks/bluebook/domain_port.rb +9 -9
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +96 -96
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +41 -41
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +22 -2
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +111 -74
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +48 -30
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +103 -103
- data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
- data/lib/hecks/bluebook/dsl/entity_builder.rb +56 -56
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +89 -30
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +17 -17
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
- data/lib/hecks/bluebook/dsl/policy_builder.rb +30 -21
- data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +35 -35
- data/lib/hecks/bluebook/dsl/query_builder.rb +5 -5
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +34 -34
- data/lib/hecks/bluebook/dsl/rule_reference.rb +41 -39
- data/lib/hecks/bluebook/dsl/translation_builder.rb +9 -9
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +16 -16
- data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
- data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
- data/lib/hecks/bluebook/entity.rb +11 -11
- data/lib/hecks/bluebook/expression/ast_json.rb +20 -20
- data/lib/hecks/bluebook/expression/ast_reader.rb +3 -3
- data/lib/hecks/bluebook/expression/canonical_form.rb +9 -9
- data/lib/hecks/bluebook/expression/evaluator.rb +18 -18
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +18 -18
- data/lib/hecks/bluebook/expression/resolver.rb +60 -62
- data/lib/hecks/bluebook/hexagon.rb +1 -1
- data/lib/hecks/bluebook/lifecycle.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/judge.rb +101 -87
- data/lib/hecks/bluebook/meta_validator/plan.rb +39 -39
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -2
- data/lib/hecks/bluebook/meta_validator/readings.rb +43 -43
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +44 -37
- data/lib/hecks/bluebook/meta_validator/shapes.rb +25 -21
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +145 -31
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +6 -6
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +5 -5
- data/lib/hecks/bluebook/meta_validator.rb +70 -70
- data/lib/hecks/bluebook/model_check.rb +170 -84
- data/lib/hecks/bluebook/pattern_subset.rb +9 -9
- data/lib/hecks/bluebook/policy.rb +15 -13
- data/lib/hecks/bluebook/process_manager.rb +14 -14
- data/lib/hecks/bluebook/project_register.rb +6 -6
- data/lib/hecks/bluebook/query.rb +4 -4
- data/lib/hecks/bluebook/read_model.rb +14 -14
- data/lib/hecks/bluebook/reference.rb +8 -8
- data/lib/hecks/bluebook/smoke_test.rb +19 -19
- data/lib/hecks/bluebook/synthesizer.rb +12 -12
- data/lib/hecks/bluebook/translation.rb +4 -4
- data/lib/hecks/bluebook/value_object.rb +6 -6
- data/lib/hecks/bluebook.rb +2 -2
- data/lib/hecks/codemod/legacy_dispatch_args.rb +299 -0
- data/lib/hecks/codemod/legacy_dispatch_recorder.rb +186 -0
- data/lib/hecks/codemod.rb +36 -35
- data/lib/hecks/construct.rb +6 -6
- data/lib/hecks/corpus.rb +317 -0
- data/lib/hecks/deprecation.rb +95 -0
- data/lib/hecks/doc/reference.rb +19 -19
- data/lib/hecks/embryonaut_bluebook.rb +11 -11
- data/lib/hecks/facade/cli_door.rb +69 -10
- data/lib/hecks/facade/cli_runner.rb +105 -24
- data/lib/hecks/facade/command_request.rb +23 -0
- data/lib/hecks/facade/handle.rb +79 -32
- data/lib/hecks/facade/json_door.rb +106 -25
- data/lib/hecks/facade/surface/aggregate_door.rb +42 -27
- data/lib/hecks/facade/surface/chapter.rb +26 -17
- data/lib/hecks/facade/surface.rb +16 -3
- data/lib/hecks/facade.rb +15 -4
- data/lib/hecks/forms/app.rb +46 -30
- data/lib/hecks/forms/command_form_renderer.rb +70 -9
- data/lib/hecks/forms/field_renderer.rb +142 -6
- data/lib/hecks/forms/field_shape.rb +180 -19
- data/lib/hecks/forms/html.rb +51 -7
- data/lib/hecks/forms/index_renderer.rb +14 -2
- data/lib/hecks/forms/params.rb +120 -23
- data/lib/hecks/forms/port_argument.rb +2 -2
- data/lib/hecks/forms/query_form_renderer.rb +2 -2
- data/lib/hecks/forms/record_renderer.rb +2 -2
- data/lib/hecks/forms/record_table.rb +1 -1
- data/lib/hecks/forms/value_object_shape.rb +3 -3
- data/lib/hecks/forms.rb +24 -4
- data/lib/hecks/fqn.rb +1 -1
- data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
- data/lib/hecks/framework.rb +48 -17
- data/lib/hecks/freezer.rb +11 -11
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +90 -90
- data/lib/hecks/fuzzing/combination_miner.rb +119 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +140 -45
- data/lib/hecks/fuzzing/coverage_campaign.rb +118 -0
- data/lib/hecks/fuzzing/differential.rb +158 -0
- data/lib/hecks/fuzzing/domain_generator.rb +694 -0
- data/lib/hecks/fuzzing/era_boundary.rb +37 -18
- data/lib/hecks/fuzzing/form_census.rb +35 -20
- data/lib/hecks/fuzzing/generated_domain_check.rb +95 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +6 -6
- data/lib/hecks/fuzzing/isolated_boot.rb +62 -62
- data/lib/hecks/fuzzing/nondeterministic.rb +67 -0
- data/lib/hecks/fuzzing/persistence_parity.rb +19 -21
- data/lib/hecks/fuzzing/properties/corrections.rb +9 -9
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +154 -49
- data/lib/hecks/fuzzing/properties/guards.rb +42 -42
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +32 -32
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +22 -35
- data/lib/hecks/fuzzing/properties/outbox.rb +24 -24
- data/lib/hecks/fuzzing/properties/querying.rb +17 -17
- data/lib/hecks/fuzzing/properties.rb +64 -40
- data/lib/hecks/fuzzing/qa_settings.rb +152 -0
- data/lib/hecks/fuzzing/replay.rb +114 -114
- data/lib/hecks/fuzzing/rotation_priority.rb +10 -10
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +113 -0
- data/lib/hecks/fuzzing/self_consistency.rb +141 -112
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +35 -35
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +58 -31
- data/lib/hecks/fuzzing/sequence_generator.rb +111 -23
- data/lib/hecks/fuzzing/shrinker.rb +197 -0
- data/lib/hecks/fuzzing/structural_skips.rb +23 -130
- data/lib/hecks/fuzzing/sweep_depth.rb +4 -4
- data/lib/hecks/fuzzing/target_capabilities.rb +49 -18
- data/lib/hecks/fuzzing/value_generator.rb +14 -14
- data/lib/hecks/fuzzing.rb +5 -0
- data/lib/hecks/grammar/evolve.rb +10 -10
- data/lib/hecks/grammar.rb +7 -7
- data/lib/hecks/ir.rb +13 -13
- data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
- data/lib/hecks/language/bluebook/policy.bluebook +11 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +348 -13
- data/lib/hecks/language/oidc.json +5 -0
- data/lib/hecks/literal.rb +9 -9
- data/lib/hecks/naming.rb +25 -25
- data/lib/hecks/ports/access_control.rb +58 -2
- data/lib/hecks/ports/agent/answers.rb +83 -6
- data/lib/hecks/ports/agent.rb +119 -35
- data/lib/hecks/ports/authentication.rb +44 -4
- data/lib/hecks/ports/authorization.rb +53 -11
- data/lib/hecks/ports/clock.rb +42 -23
- data/lib/hecks/ports/extraction.rb +16 -0
- data/lib/hecks/ports/identity_assignment.rb +24 -2
- data/lib/hecks/ports/identity_generation.rb +17 -3
- data/lib/hecks/ports/identity_resolution.rb +18 -1
- data/lib/hecks/ports/loading.rb +4 -0
- data/lib/hecks/ports/persistence/append_only.rb +172 -8
- data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
- data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
- data/lib/hecks/ports/persistence/execution.rb +4 -0
- data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
- data/lib/hecks/ports/persistence/plugin.rb +42 -4
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
- data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +118 -66
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +28 -5
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +33 -9
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +57 -4
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
- data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +42 -8
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -19
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +5 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
- data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
- data/lib/hecks/ports/persistence/repository_factory.rb +23 -3
- data/lib/hecks/ports/persistence/state_codec.rb +319 -0
- data/lib/hecks/ports/persistence.rb +36 -1
- data/lib/hecks/ports/projection.rb +61 -7
- data/lib/hecks/ports/query/in_memory.rb +3 -3
- data/lib/hecks/ports/query/ordering.rb +6 -6
- data/lib/hecks/ports/query.rb +35 -0
- data/lib/hecks/projections/bootstrap_table.rb +112 -0
- data/lib/hecks/projections/diagrams.rb +75 -75
- data/lib/hecks/projections/glossary/html.rb +3 -3
- data/lib/hecks/projections/glossary/markdown.rb +3 -3
- data/lib/hecks/projections/glossary/mermaid.rb +1 -1
- data/lib/hecks/projections/glossary/sections.rb +1 -1
- data/lib/hecks/projections/glossary/sentences.rb +5 -5
- data/lib/hecks/projections/glossary.rb +15 -15
- data/lib/hecks/projections/ir.rb +1 -1
- data/lib/hecks/projections/model/deviations.rb +18 -17
- data/lib/hecks/projections/model.rb +25 -21
- data/lib/hecks/projections/oidc.rb +7 -7
- data/lib/hecks/projections/parser_table.rb +5 -5
- data/lib/hecks/projections/reference.rb +3 -3
- data/lib/hecks/projections/rust_vocabulary.rb +443 -0
- data/lib/hecks/projections/shape.rb +2 -2
- data/lib/hecks/projections/statements.rb +11 -11
- data/lib/hecks/projections/vocabulary.rb +9 -9
- data/lib/hecks/projections.rb +5 -3
- data/lib/hecks/projector/cli_projector.rb +29 -29
- data/lib/hecks/projector/docs_projector.rb +13 -13
- data/lib/hecks/projector/exporter.rb +42 -21
- data/lib/hecks/projector/ir_projector.rb +1 -1
- data/lib/hecks/projector/narrate_projector.rb +11 -11
- data/lib/hecks/projector/target.rb +13 -13
- data/lib/hecks/projector.rb +15 -15
- data/lib/hecks/query_ir.rb +47 -47
- data/lib/hecks/query_specification/common/comparators.rb +19 -3
- data/lib/hecks/query_specification/common/comparison.rb +109 -27
- data/lib/hecks/query_specification/common/dsl.rb +65 -9
- data/lib/hecks/query_specification/common/null_policy.rb +57 -13
- data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
- data/lib/hecks/query_specification/common/options.rb +25 -0
- data/lib/hecks/query_specification/field_path.rb +69 -15
- data/lib/hecks/query_specification/hop_path.rb +57 -20
- data/lib/hecks/query_specification/read_model/specification.rb +4 -0
- data/lib/hecks/rendering.rb +3 -3
- data/lib/hecks/router/namespace_installer.rb +3 -3
- data/lib/hecks/router.rb +1 -1
- data/lib/hecks/runtime/aggregate_lock.rb +11 -11
- data/lib/hecks/runtime/boot_gates.rb +3 -3
- data/lib/hecks/runtime/caller.rb +8 -8
- data/lib/hecks/runtime/capability_graph.rb +2 -2
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +22 -25
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +42 -63
- data/lib/hecks/runtime/command_interpreter.rb +159 -102
- data/lib/hecks/runtime/command_rules/admissibility.rb +69 -69
- data/lib/hecks/runtime/command_rules/arithmetic.rb +137 -91
- data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
- data/lib/hecks/runtime/command_rules/emission.rb +18 -1
- data/lib/hecks/runtime/command_rules/references.rb +35 -35
- data/lib/hecks/runtime/command_rules.rb +3 -0
- data/lib/hecks/runtime/dependency_planning.rb +11 -11
- data/lib/hecks/runtime/dispatcher.rb +283 -90
- data/lib/hecks/runtime/entity_element.rb +151 -67
- data/lib/hecks/runtime/entity_interpreter.rb +108 -82
- data/lib/hecks/runtime/errors.rb +19 -19
- data/lib/hecks/runtime/event.rb +6 -6
- data/lib/hecks/runtime/identity.rb +22 -22
- data/lib/hecks/runtime/instance.rb +39 -14
- data/lib/hecks/runtime/interpreting.rb +12 -12
- data/lib/hecks/runtime/invocation.rb +276 -0
- data/lib/hecks/runtime/loader.rb +14 -14
- data/lib/hecks/runtime/outbox.rb +23 -23
- data/lib/hecks/runtime/policy_interpreter.rb +54 -54
- data/lib/hecks/runtime/port_operation_interpreter.rb +22 -19
- data/lib/hecks/runtime/query_interpreter.rb +55 -64
- data/lib/hecks/runtime/reaction_invocation.rb +20 -20
- data/lib/hecks/runtime/read_model_interpreter.rb +40 -40
- data/lib/hecks/runtime/rebuild_sweep.rb +4 -4
- data/lib/hecks/runtime/reference_hop.rb +6 -6
- data/lib/hecks/runtime/refusal_wording.rb +92 -115
- data/lib/hecks/runtime/registry/saga_persistence.rb +21 -21
- data/lib/hecks/runtime/registry/verification.rb +36 -26
- data/lib/hecks/runtime/registry.rb +45 -28
- data/lib/hecks/runtime/remote_dispatcher.rb +38 -23
- data/lib/hecks/runtime/routing.rb +10 -153
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +17 -17
- data/lib/hecks/runtime/saga_interpreter.rb +64 -64
- data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
- data/lib/hecks/runtime/tenant_check.rb +9 -9
- data/lib/hecks/runtime/tenant_scope.rb +5 -5
- data/lib/hecks/runtime/value/admission.rb +75 -30
- data/lib/hecks/runtime/value/coercion.rb +318 -127
- data/lib/hecks/runtime/value/entity_list_coercion.rb +34 -34
- data/lib/hecks/runtime/value.rb +21 -21
- data/lib/hecks/runtime.rb +7 -7
- data/lib/hecks/storehouse.rb +64 -64
- data/lib/hecks/version.rb +3 -3
- data/lib/hecks/vocabulary.rb +205 -4
- data/lib/hecks.rb +13 -11
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +11 -11
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +14 -12
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +11 -11
- metadata +22 -2
|
@@ -3,9 +3,9 @@ require_relative "value"
|
|
|
3
3
|
|
|
4
4
|
module Hecks
|
|
5
5
|
module Runtime
|
|
6
|
-
#
|
|
6
|
+
# **The scalar an identity path names**.
|
|
7
7
|
#
|
|
8
|
-
# An identity is
|
|
8
|
+
# An identity is declared as a path — `identified_by :number` — and
|
|
9
9
|
# this is the one place that reads one. It follows the path and nothing else.
|
|
10
10
|
#
|
|
11
11
|
# What it replaced was `Value.identifier`, which opened a one-field value
|
|
@@ -24,7 +24,7 @@ module Hecks
|
|
|
24
24
|
module_function
|
|
25
25
|
|
|
26
26
|
# A hash read that decides which spelling of a key answers by
|
|
27
|
-
#
|
|
27
|
+
# presence, never by `||` — a bare `||` treats a genuinely-held
|
|
28
28
|
# `false` the same as an absent key and falls through to the other
|
|
29
29
|
# spelling, landing on `nil` instead of the real, stored answer.
|
|
30
30
|
def hash_lookup(hash, key)
|
|
@@ -32,7 +32,7 @@ module Hecks
|
|
|
32
32
|
hash.key?(sym) ? hash[sym] : hash[key]
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
-
# The head names the
|
|
35
|
+
# The head names the attribute and is consumed by whoever looked the value
|
|
36
36
|
# up; what is left is the walk down into it. A path with no fields to walk
|
|
37
37
|
# — an aggregate that declares no identity and falls back to `id` — hands
|
|
38
38
|
# back what it was given, because there is nothing declared to dig for.
|
|
@@ -45,17 +45,17 @@ module Hecks
|
|
|
45
45
|
end
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
-
#
|
|
48
|
+
# The identity is the join of its parts, in declaration order. Shared by
|
|
49
49
|
# `CommandInterpreter` (an aggregate acting on itself) and
|
|
50
50
|
# `EntityInterpreter` (a piece addressed through its aggregate) — a piece
|
|
51
51
|
# declares an identity the same shape a head does, so it derives one the
|
|
52
52
|
# same way. `construct` answers `identity_paths` / `identity_heads` /
|
|
53
53
|
# `attribute` (an Aggregate or an Entity, either one) ; `value_owner`
|
|
54
54
|
# answers for coercion (`Value.for_attribute`'s first argument), which for
|
|
55
|
-
# an entity is its
|
|
55
|
+
# an entity is its owning aggregate — an entity's value objects resolve
|
|
56
56
|
# through the aggregate's namespace, not its own.
|
|
57
57
|
#
|
|
58
|
-
# A part the payload does not carry makes the
|
|
58
|
+
# A part the payload does not carry makes the whole identity unresolvable,
|
|
59
59
|
# rather than half of one. Half an identity names nothing, and joining what
|
|
60
60
|
# did arrive would silently name a different record on every dispatch — the
|
|
61
61
|
# precise failure that minting an id caused, arrived at by another road.
|
|
@@ -64,10 +64,10 @@ module Hecks
|
|
|
64
64
|
return nil if paths.empty?
|
|
65
65
|
|
|
66
66
|
parts = paths.map { |path| from(construct, args, path, value_owner: value_owner) }
|
|
67
|
-
# A
|
|
68
|
-
#
|
|
67
|
+
# A blank part names nothing, the same as an absent one — an ID is a
|
|
68
|
+
# scalar, and "" is not a fact about anything. This used to check only
|
|
69
69
|
# `nil?`, so a canonical text extracted as "" (an expression whose
|
|
70
|
-
# source did not survive extraction) resolved to a
|
|
70
|
+
# source did not survive extraction) resolved to a real, empty-string
|
|
71
71
|
# identity — a record addressable by an id no caller could have meant.
|
|
72
72
|
return nil if parts.any? { |part| part.nil? || (part.respond_to?(:empty?) && part.empty?) }
|
|
73
73
|
|
|
@@ -75,7 +75,7 @@ module Hecks
|
|
|
75
75
|
end
|
|
76
76
|
|
|
77
77
|
# A path digs into the value object that carries the identity, so what is
|
|
78
|
-
# stored is the
|
|
78
|
+
# stored is the scalar inside it rather than the object serialised whole.
|
|
79
79
|
def from(construct, args, key, value_owner: construct)
|
|
80
80
|
return nil unless key
|
|
81
81
|
|
|
@@ -86,7 +86,7 @@ module Hecks
|
|
|
86
86
|
unless rest.empty?
|
|
87
87
|
held = args[head]
|
|
88
88
|
held = held.to_h if held.respond_to?(:to_h)
|
|
89
|
-
#
|
|
89
|
+
# **An ID is always a scalar**. The path says which field carries it, so a
|
|
90
90
|
# caller may hand that field's value straight over — a string or a
|
|
91
91
|
# number, never a serialised object. Only a value object that actually
|
|
92
92
|
# arrived whole has to be opened.
|
|
@@ -95,18 +95,18 @@ module Hecks
|
|
|
95
95
|
return rest.reduce(held) { |h, f| h.is_a?(Hash) ? hash_lookup(h, f) : nil }&.to_s
|
|
96
96
|
end
|
|
97
97
|
|
|
98
|
-
# Coerced against the identity
|
|
98
|
+
# Coerced against the identity attribute only when the caller actually
|
|
99
99
|
# named it. A saga addresses an aggregate by its correlation key, and
|
|
100
|
-
# that key carries the id
|
|
100
|
+
# that key carries the id already resolved — coercing "w1" against a
|
|
101
101
|
# WireReference asked the caller to pass fields for a value object they
|
|
102
102
|
# never mentioned.
|
|
103
103
|
attribute = construct.identity_heads.include?(head) ? construct.attribute(head) : nil
|
|
104
104
|
raw = args[head]
|
|
105
105
|
return raw unless attribute
|
|
106
106
|
|
|
107
|
-
#
|
|
108
|
-
# already keeps, just reached a different way here: a
|
|
109
|
-
# (undotted) identity path names one of
|
|
107
|
+
# **An ID is always a scalar** — same contract the dotted branch above
|
|
108
|
+
# already keeps, just reached a different way here: a bare
|
|
109
|
+
# (undotted) identity path names one of this construct's own
|
|
110
110
|
# declared attributes directly, and when that attribute's type is
|
|
111
111
|
# a value object (Translation's own compound `identified_by
|
|
112
112
|
# :domain, :from, :to`, each typed `TranslationDomainName`/
|
|
@@ -118,23 +118,23 @@ module Hecks
|
|
|
118
118
|
# memory address (`#<Hecks::Runtime::Value:0x...>`) into
|
|
119
119
|
# every refusal quoting this identity — found live via bin/fuzz on
|
|
120
120
|
# the self-hosted "translation" domain (replay_is_deterministic:
|
|
121
|
-
# the
|
|
121
|
+
# the same address never repeats, so two replays of the
|
|
122
122
|
# identical steps produced different histories the moment a
|
|
123
123
|
# Translation went missing). `materialize_unwrapped` is the
|
|
124
|
-
#
|
|
124
|
+
# same single-field-VO-recurses-to-its-bare-scalar helper
|
|
125
125
|
# `read_model_interpreter.rb` already uses for exactly this
|
|
126
126
|
# unwrap; passthrough for anything that isn't a Value at all.
|
|
127
127
|
Value.materialize_unwrapped(Value.for_attribute(value_owner, attribute, raw)).to_s
|
|
128
128
|
end
|
|
129
129
|
|
|
130
|
-
# How an identity
|
|
130
|
+
# How an identity reads when the runtime has to name it in a refusal — the
|
|
131
131
|
# paths as they were declared, so the message quotes the bluebook back.
|
|
132
132
|
def reading(construct)
|
|
133
133
|
construct.identity_paths.join(", ")
|
|
134
134
|
end
|
|
135
135
|
|
|
136
|
-
#
|
|
137
|
-
# per-record striping needs
|
|
136
|
+
# Best-effort, for a lock key only — `Runtime::AggregateLock`'s own
|
|
137
|
+
# per-record striping needs some id to key on before dispatch has run
|
|
138
138
|
# far enough to hydrate for real, so this walks the identical chain
|
|
139
139
|
# `CommandInterpreter#hydrate_existing`/`#hydrate_prior_or_initial`
|
|
140
140
|
# and `EntityInterpreter#parent` already use to locate the real
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require_relative "value"
|
|
2
2
|
require_relative "identity"
|
|
3
|
+
require_relative "../ports/persistence/codec_boundary"
|
|
3
4
|
|
|
4
5
|
module Hecks
|
|
5
6
|
module Runtime
|
|
@@ -12,7 +13,7 @@ module Hecks
|
|
|
12
13
|
class Instance
|
|
13
14
|
attr_reader :aggregate, :id
|
|
14
15
|
attr_accessor :state
|
|
15
|
-
#
|
|
16
|
+
# **Out-of-band adapter bookkeeping, not domain state** — the optimistic-
|
|
16
17
|
# concurrency version a CAS-capable adapter (Postgres today) stamps
|
|
17
18
|
# on a record it reads/writes, so a later `save` can assert "commit
|
|
18
19
|
# only if nobody has written since". Deliberately absent from
|
|
@@ -24,16 +25,40 @@ module Hecks
|
|
|
24
25
|
# ADR) for the full mechanism.
|
|
25
26
|
attr_accessor :version
|
|
26
27
|
|
|
27
|
-
# `args:` —
|
|
28
|
+
# `args:` — the original command payload, offered only by a fresh
|
|
28
29
|
# creation (`CommandInterpreter#hydrate_legacy_creation`/
|
|
29
30
|
# `#hydrate_complete_state`/`#hydrate_prior_or_initial`, each already
|
|
30
31
|
# holding it when they mint a brand-new record). See
|
|
31
32
|
# `materialize_identity!` for why a composite identity needs it.
|
|
32
|
-
|
|
33
|
+
#
|
|
34
|
+
# `hydrate:` — ON BY DEFAULT, and every existing caller keeps getting
|
|
35
|
+
# exactly what it always got: `state` re-walked through
|
|
36
|
+
# `hydrate_with_defaults` (declared defaults filled, every attribute
|
|
37
|
+
# re-coerced through `Value.for_attribute`, an entity list's every
|
|
38
|
+
# element rebuilt and re-validated). `false` is for exactly one
|
|
39
|
+
# caller (`Adapters::Memory#build_instance`, judge-bootstrapping
|
|
40
|
+
# only — see its own header) that already knows `state` needs none
|
|
41
|
+
# of that: it is a shallow dup of an ALREADY-hydrated, ALREADY-
|
|
42
|
+
# validated live `Instance`'s own state, not a raw value pulled off
|
|
43
|
+
# a wire. Skipping the re-walk is what turns a `list_of` entity's Nth
|
|
44
|
+
# save from O(N) (re-hydrating every element saved so far, for every
|
|
45
|
+
# save) into O(1) — the quadratic cost `Adapters::Memory`'s own
|
|
46
|
+
# header traces start to finish. `CodecBoundary.check_state!` still
|
|
47
|
+
# runs either way ; only the re-hydration is skipped.
|
|
48
|
+
def initialize(aggregate:, id:, state: nil, args: nil, hydrate: true)
|
|
33
49
|
@aggregate = aggregate
|
|
34
50
|
@id = id
|
|
35
|
-
|
|
36
|
-
|
|
51
|
+
# Inside a persistence adapter call this refuses undecoded stored
|
|
52
|
+
# state (Ports::Persistence::CodecBoundary); everywhere else, no-op.
|
|
53
|
+
Ports::Persistence::CodecBoundary.check_state!(aggregate, state) if state
|
|
54
|
+
@state = if !hydrate
|
|
55
|
+
state || self.class.defaults(aggregate)
|
|
56
|
+
elsif state
|
|
57
|
+
self.class.hydrate_with_defaults(aggregate, state)
|
|
58
|
+
else
|
|
59
|
+
self.class.defaults(aggregate)
|
|
60
|
+
end
|
|
61
|
+
@version = nil
|
|
37
62
|
materialize_identity!(args)
|
|
38
63
|
end
|
|
39
64
|
|
|
@@ -53,7 +78,7 @@ module Hecks
|
|
|
53
78
|
|
|
54
79
|
def self.defaults(aggregate)
|
|
55
80
|
state = aggregate.attributes.to_h do |attr|
|
|
56
|
-
#
|
|
81
|
+
# Frozen, like a list that has had something appended to it.
|
|
57
82
|
# An untouched list is the easiest one to miss and the easiest
|
|
58
83
|
# to mutate: nothing has replaced it yet, so a caller pushing
|
|
59
84
|
# into it writes straight into the aggregate's own state.
|
|
@@ -93,7 +118,7 @@ module Hecks
|
|
|
93
118
|
@state.key?(name) || super
|
|
94
119
|
end
|
|
95
120
|
|
|
96
|
-
# `id: @id`
|
|
121
|
+
# `id: @id` last, not first — see Facade::Handle#to_h's own comment
|
|
97
122
|
# for the full story (the same fix, landed there first): an
|
|
98
123
|
# aggregate free to declare its own attribute literally named `id`
|
|
99
124
|
# (BurningManPrep's `Item`, `attribute :id, ItemId`) has that
|
|
@@ -102,15 +127,15 @@ module Hecks
|
|
|
102
127
|
# the correct bare identity. `@id` merged last always wins.
|
|
103
128
|
def to_h = @state.merge(id: @id)
|
|
104
129
|
|
|
105
|
-
# A
|
|
130
|
+
# A copy a mutation may touch. Every adapter but Memory hands `find`
|
|
106
131
|
# a freshly-decoded Instance already; Memory's holds the record it
|
|
107
|
-
# eventually saves — the
|
|
132
|
+
# eventually saves — the same state Hash, aliased. Before `ensures`
|
|
108
133
|
# existed, nothing could refuse between apply_mutations and save, so
|
|
109
134
|
# that aliasing was invisible: a dispatch either ran to completion or
|
|
110
135
|
# raised before touching state at all. `ensures` is the first refusal
|
|
111
|
-
# to sit
|
|
136
|
+
# to sit after mutation, and it found the bug the moment it did — an
|
|
112
137
|
# in-memory record left half-mutated by a dispatch that then refused.
|
|
113
|
-
# `command_interpreter`/`entity_interpreter` hydrate an
|
|
138
|
+
# `command_interpreter`/`entity_interpreter` hydrate an existing
|
|
114
139
|
# record through this, never through the adapter's own return value
|
|
115
140
|
# directly, so a refused ensures leaves the stored record untouched
|
|
116
141
|
# regardless of which adapter is holding it.
|
|
@@ -127,13 +152,13 @@ module Hecks
|
|
|
127
152
|
|
|
128
153
|
private
|
|
129
154
|
|
|
130
|
-
# M17 — a
|
|
155
|
+
# M17 — a composite identity (`identity_heads.size > 1`, e.g.
|
|
131
156
|
# `identified_by :branch_code, :box_number`) has no single
|
|
132
157
|
# `identified_by` to fall back to `:id` for — `@aggregate.identified_by`
|
|
133
158
|
# is nil the moment there is more than one head (`Behaviour::Identified
|
|
134
159
|
# #derive_identity`), so the single-head branch below never runs for
|
|
135
160
|
# it at all. A creating command that declares those heads as ordinary
|
|
136
|
-
# attributes but doesn't
|
|
161
|
+
# attributes but doesn't also `sets` them (redundant with the identity
|
|
137
162
|
# the command's own args already named) used to persist every head as
|
|
138
163
|
# nil — the id correctly named the record, but the record's own
|
|
139
164
|
# attributes forgot what named it.
|
|
@@ -142,7 +167,7 @@ module Hecks
|
|
|
142
167
|
# same reason the single-head branch below won't guess a multi-path
|
|
143
168
|
# identifier from its joined string: `@id` is a display key, not a
|
|
144
169
|
# reversible serialization, and a composite's own separator can
|
|
145
|
-
# collide with a part's own text. `args` is only offered by a
|
|
170
|
+
# collide with a part's own text. `args` is only offered by a fresh
|
|
146
171
|
# creation (`Instance.new`'s own `args:` comment); an existing record
|
|
147
172
|
# read back from storage has no args to lean on, and doesn't need
|
|
148
173
|
# one since a correctly-persisted record already carries its own
|
|
@@ -19,7 +19,7 @@ module Hecks
|
|
|
19
19
|
|
|
20
20
|
private
|
|
21
21
|
|
|
22
|
-
# Logged
|
|
22
|
+
# Logged after the step's own work, so a step that wraps sub-steps logs
|
|
23
23
|
# itself once everything inside it has already logged — trace order is
|
|
24
24
|
# completion order, which is dispatch order.
|
|
25
25
|
def step(name)
|
|
@@ -28,21 +28,21 @@ module Hecks
|
|
|
28
28
|
result
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
31
|
+
# Drives `DISPATCH_ORDER` (CommandInterpreter/EntityInterpreter, each
|
|
32
|
+
# read off its own generated Vocabulary::*DispatchOrder table —
|
|
33
|
+
# vocabulary.bluebook, via lib/hecks/vocabulary.rb) by `send`ing
|
|
34
34
|
# each declared step name against the including interpreter's own
|
|
35
35
|
# `step_<name>` handler, in declared order. What used to be `call`'s own
|
|
36
|
-
# literal sequence of method calls is now
|
|
36
|
+
# literal sequence of method calls is now data driving that sequence —
|
|
37
37
|
# tracing a real dispatch and comparing it to the declaration is
|
|
38
38
|
# tautological once `call` mechanically follows the declaration; a
|
|
39
39
|
# conditional step (assign_creation_attributes, advance_lifecycle) still
|
|
40
|
-
# has to guard
|
|
40
|
+
# has to guard itself at the top of its own handler and skip tracing
|
|
41
41
|
# when it does not fire, rather than the caller branching around it —
|
|
42
42
|
# see CommandInterpreter#step_assign_creation_attributes.
|
|
43
|
-
#
|
|
43
|
+
# The commit boundary. Every step up to `save` runs as before;
|
|
44
44
|
# `save`, `emit`, and the outbox enqueue that follows them run
|
|
45
|
-
# inside
|
|
45
|
+
# inside one `repository.transaction` — so the aggregate row, its
|
|
46
46
|
# journal entry, the recorded event, and the outbox rows naming
|
|
47
47
|
# who is owed a reaction commit together or not at all. No new
|
|
48
48
|
# step is added to the vocabulary's dispatch order (the step list
|
|
@@ -71,20 +71,20 @@ module Hecks
|
|
|
71
71
|
ctx.outbox_rows = @registry.outbox.enqueue(ctx.repository, Array(ctx.result), ctx.domain)
|
|
72
72
|
end
|
|
73
73
|
|
|
74
|
-
#
|
|
74
|
+
# The concurrency-control split — see docs/decisions/ (concurrency
|
|
75
75
|
# control ADR) for the full mechanism. A repository that declares
|
|
76
76
|
# `:optimistic_concurrency` (Postgres today) already closes the
|
|
77
77
|
# lost-update gap itself, via `step_save`'s CAS + `#call`'s own
|
|
78
78
|
# `StaleWrite` retry loop — an extra in-process lock here would be
|
|
79
79
|
# pointless overhead, not incorrect, so it's skipped for clarity.
|
|
80
80
|
# A repository that declares `:cross_process_lock` (PostgresEra —
|
|
81
|
-
# ADR 0036) holds a
|
|
81
|
+
# ADR 0036) holds a real Postgres advisory lock for the whole
|
|
82
82
|
# dispatch order instead: unlike Heki/Memory (confirmed
|
|
83
83
|
# process-local, never a second process writing the same store),
|
|
84
84
|
# PostgresEra's own tables can be dispatched against concurrently
|
|
85
85
|
# by `rust/host` from a separate OS process, and an in-process
|
|
86
|
-
# `Mutex` is invisible to that. Every
|
|
87
|
-
# striped `Mutex` below, held for the
|
|
86
|
+
# `Mutex` is invisible to that. Every other repository gets the
|
|
87
|
+
# striped `Mutex` below, held for the whole dispatch-order run, so
|
|
88
88
|
# a second thread's own hydrate can't start until the first
|
|
89
89
|
# thread's save has landed. `lock_key_id` is best-effort
|
|
90
90
|
# (`Identity.best_effort`) — `nil` still locks correctly, just
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
require_relative "errors"
|
|
2
|
+
require_relative "refusal_wording"
|
|
3
|
+
require_relative "routing"
|
|
4
|
+
require_relative "../rendering"
|
|
5
|
+
|
|
6
|
+
module Hecks
|
|
7
|
+
module Runtime
|
|
8
|
+
# **One dispatch, as data** — the verb, the receiver it is addressed to
|
|
9
|
+
# (`target`, a `Routing::Envelope` or nil), and every fact the caller did
|
|
10
|
+
# or did not offer. Built once per dispatch by `Invocation.from_call`, the
|
|
11
|
+
# only place the runtime interprets the shape of a call (`to:` vs `with:`
|
|
12
|
+
# vs loose keyword arguments, a port operation's reference attribute
|
|
13
|
+
# lifted into `to:`). `Runtime::Routing.envelope`/`.payload` delegate here
|
|
14
|
+
# and no longer hold that logic themselves.
|
|
15
|
+
#
|
|
16
|
+
# `facts` maps a fact name to exactly one of:
|
|
17
|
+
#
|
|
18
|
+
# - `Invocation::Absent` — the key was never offered (every declared
|
|
19
|
+
# attribute the caller left out appears so)
|
|
20
|
+
# - `Invocation::Null` — the key was offered with an explicit nil
|
|
21
|
+
# - `Invocation::Present` — the key was offered with a value
|
|
22
|
+
#
|
|
23
|
+
# Keys are kept exactly as offered: `with:` keys are symbolized (as they
|
|
24
|
+
# always were), loose keyword arguments keep whatever key the caller
|
|
25
|
+
# used. Undeclared keys a caller offered are kept too — refusing them is
|
|
26
|
+
# still `refuse_unknown_arguments`' job, a dispatch step, not this one.
|
|
27
|
+
#
|
|
28
|
+
# PR I1 changes no behavior: every interpreter still reads `ctx.args`,
|
|
29
|
+
# which is `#to_args` — the same Hash `Routing.payload` used to return.
|
|
30
|
+
# Roadmap I2 moves the Ruby `decode_arguments` step onto `facts` itself.
|
|
31
|
+
Invocation = Data.define(:verb, :target, :facts)
|
|
32
|
+
|
|
33
|
+
# The fact markers, the fact readers, and `from_call` — see above.
|
|
34
|
+
class Invocation
|
|
35
|
+
# A fact the caller offered with a real (non-nil) value.
|
|
36
|
+
Present = Data.define(:value) do
|
|
37
|
+
def inspect = "#<Invocation::Present #{value.inspect}>"
|
|
38
|
+
alias_method :to_s, :inspect
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# The class of the two frozen marker singletons below — never
|
|
42
|
+
# instantiated anywhere else.
|
|
43
|
+
class Marker
|
|
44
|
+
def initialize(name)
|
|
45
|
+
@name = name
|
|
46
|
+
freeze
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def inspect = "Invocation::#{@name}"
|
|
50
|
+
alias to_s inspect
|
|
51
|
+
end
|
|
52
|
+
private_constant :Marker
|
|
53
|
+
|
|
54
|
+
Absent = Marker.new("Absent")
|
|
55
|
+
Null = Marker.new("Null")
|
|
56
|
+
|
|
57
|
+
def initialize(verb:, target:, facts:)
|
|
58
|
+
super(verb: verb, target: target, facts: facts.dup.freeze)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# The fact recorded under `name` — `Absent` for a name never offered
|
|
62
|
+
# and never declared either.
|
|
63
|
+
def fact(name) = facts.fetch(name, Absent)
|
|
64
|
+
|
|
65
|
+
def present?(name) = fact(name).is_a?(Present)
|
|
66
|
+
def null?(name) = fact(name).equal?(Null)
|
|
67
|
+
def absent?(name) = fact(name).equal?(Absent)
|
|
68
|
+
|
|
69
|
+
# A Present fact's value; nil for an explicit Null. Raises KeyError for
|
|
70
|
+
# an Absent fact — "never offered" has no value, and answering nil
|
|
71
|
+
# would re-conflate it with an explicit null, the very ambiguity this
|
|
72
|
+
# type exists to remove. Ask `absent?`/`present?` first.
|
|
73
|
+
def value(name)
|
|
74
|
+
case (found = fact(name))
|
|
75
|
+
when Present then found.value
|
|
76
|
+
when Null then nil
|
|
77
|
+
else raise KeyError, "#{verb} was not given #{name.inspect}"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# The legacy args hash, byte for byte what `Routing.payload` returned
|
|
82
|
+
# before this type existed: offered keys in offered order, Absent keys
|
|
83
|
+
# omitted, Null keys mapped to nil. A fresh Hash every call.
|
|
84
|
+
def to_args
|
|
85
|
+
facts.each_with_object({}) do |(name, found), args|
|
|
86
|
+
next if found.equal?(Absent)
|
|
87
|
+
|
|
88
|
+
args[name] = found.equal?(Null) ? nil : found.value
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
class << self
|
|
93
|
+
# **The one reading of a call's shape**. `receiver:` picks which of the
|
|
94
|
+
# three dispatch shapes this is, because each has always checked its
|
|
95
|
+
# parts in its own order and a malformed call's refusal depends on
|
|
96
|
+
# that order:
|
|
97
|
+
#
|
|
98
|
+
# :aggregate — facts first (`with:` checks), then `to:`
|
|
99
|
+
# :entity — `to:` first, then the block (which resolves the
|
|
100
|
+
# entity chain, UnknownVerb), then facts
|
|
101
|
+
# :port — a reference attribute / identity field lifted into
|
|
102
|
+
# `to:`, then `to:` (which a port operation requires),
|
|
103
|
+
# then facts
|
|
104
|
+
#
|
|
105
|
+
# The block answers the declaring command or port operation (anything
|
|
106
|
+
# with `hecks_name` and `attributes`); it is called exactly once, at
|
|
107
|
+
# the point in that order where the caller always resolved it.
|
|
108
|
+
# `aggregate:` is the owning aggregate construct, read for `:port`
|
|
109
|
+
# only.
|
|
110
|
+
def from_call(verb, to:, with:, legacy:, receiver: :aggregate, entity_depth: 0, aggregate: nil, &declaring)
|
|
111
|
+
case receiver
|
|
112
|
+
when :aggregate
|
|
113
|
+
command = declaring.call
|
|
114
|
+
facts = facts_for(command, with: with, legacy: legacy)
|
|
115
|
+
new(verb: verb, target: route(to), facts: facts)
|
|
116
|
+
when :entity
|
|
117
|
+
target = route(to, entity_depth: entity_depth)
|
|
118
|
+
command = declaring.call
|
|
119
|
+
new(verb: verb, target: target, facts: facts_for(command, with: with, legacy: legacy))
|
|
120
|
+
when :port
|
|
121
|
+
port_call(verb, aggregate, declaring.call, to: to, with: with, legacy: legacy)
|
|
122
|
+
else
|
|
123
|
+
raise ArgumentError, "unknown receiver #{receiver.inspect}"
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# `to:` as a `Routing::Envelope`, or nil when no `to:` was given.
|
|
128
|
+
def route(to, entity_depth: 0)
|
|
129
|
+
return nil if to.nil?
|
|
130
|
+
|
|
131
|
+
aggregate, entities = to.is_a?(Hash) ? envelope_hash(to) : scalar_envelope(to)
|
|
132
|
+
|
|
133
|
+
raise TypeMismatch, "to: must name the receiving aggregate identity" if aggregate.nil? || aggregate.to_s.empty?
|
|
134
|
+
if entities.size != entity_depth
|
|
135
|
+
raise TypeMismatch,
|
|
136
|
+
"to: for an entity command needs #{entity_depth} entity " \
|
|
137
|
+
"#{entity_depth == 1 ? 'identity' : 'identities'} after the aggregate — got #{entities.size}"
|
|
138
|
+
end
|
|
139
|
+
raise TypeMismatch, "to: contains a blank entity identity" if entities.any? do |identity|
|
|
140
|
+
identity.nil? || identity.to_s.empty?
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
Routing::Envelope.new(aggregate: aggregate, entities: entities)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# The offered facts for `declaring`, as Absent/Null/Present — offered
|
|
147
|
+
# keys first, in offered order, then every declared attribute that was
|
|
148
|
+
# not offered, as Absent.
|
|
149
|
+
def facts_for(declaring, with:, legacy:)
|
|
150
|
+
offered = offered_facts(declaring, with: with, legacy: legacy)
|
|
151
|
+
facts = offered.each_with_object({}) do |(name, value), found|
|
|
152
|
+
found[name] = value.nil? ? Null : Present.new(value: value)
|
|
153
|
+
end
|
|
154
|
+
declaring.attributes.each do |attribute|
|
|
155
|
+
name = attribute.name.to_sym
|
|
156
|
+
facts[name] = Absent unless facts.key?(name)
|
|
157
|
+
end
|
|
158
|
+
facts
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
private
|
|
162
|
+
|
|
163
|
+
# **The port operation shape** — formerly `Dispatcher#port_invocation`.
|
|
164
|
+
#
|
|
165
|
+
# A Reference-typed attribute naming the owning aggregate is routing,
|
|
166
|
+
# not a fact: lifted out of the loose kwargs into `to:` when no `to:`
|
|
167
|
+
# was given. A `to:`-declared operation carries no Reference
|
|
168
|
+
# attribute at all (PortOperationBuilder#initialize's own comment),
|
|
169
|
+
# so its receiver is read — not removed — from the plain attribute
|
|
170
|
+
# named for the owner's first `identified_by` field: that attribute
|
|
171
|
+
# is still a declared external fact `refuse_absent_arguments`
|
|
172
|
+
# expects to find (Rust's own comment: "declare only external facts
|
|
173
|
+
# with attribute"; a real AbsentArgument confirmed this before `[]`
|
|
174
|
+
# replaced `delete`). Composite identity is not attempted — `.first`
|
|
175
|
+
# only, no domain in the corpus needs more for a port operation.
|
|
176
|
+
def port_call(verb, aggregate, operation, to:, with:, legacy:)
|
|
177
|
+
legacy = legacy.dup
|
|
178
|
+
identity = operation.identity_attribute(aggregate.hecks_name)
|
|
179
|
+
if to.nil? && identity && legacy.key?(identity.name)
|
|
180
|
+
to = legacy.delete(identity.name)
|
|
181
|
+
elsif to.nil? && operation.to == aggregate.hecks_name
|
|
182
|
+
identity_name = Array(aggregate.identified_by).first
|
|
183
|
+
to = legacy[identity_name] if identity_name && legacy.key?(identity_name)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
target = route(to)
|
|
187
|
+
raise TypeMismatch, "#{operation.hecks_name} requires its receiving aggregate in to:" unless target
|
|
188
|
+
|
|
189
|
+
new(verb: verb, target: target, facts: facts_for(operation, with: with, legacy: legacy))
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# BUG#7 — a non-Hash `to:` must be a String, matching Rust's
|
|
193
|
+
# `RoutingEnvelope::from_json` (kernel/routing.rs), which refuses
|
|
194
|
+
# anything neither a JSON string nor object before the domain payload
|
|
195
|
+
# is examined. Surfaced on `examples/roster`'s `Mark`, whose own
|
|
196
|
+
# attribute is literally named `to`: a flat-kwargs dispatch steals
|
|
197
|
+
# that key into this parameter, and an out-of-range Integer used to be
|
|
198
|
+
# accepted as the aggregate identity, leaving Mark's `to` fact absent
|
|
199
|
+
# (AbsentArgument in Ruby, TypeMismatch in Rust). See
|
|
200
|
+
# spec/runtime/routing_envelope_shape_spec.rb.
|
|
201
|
+
def scalar_envelope(to)
|
|
202
|
+
return [to, []] if to.is_a?(String)
|
|
203
|
+
|
|
204
|
+
raise TypeMismatch, "to: must be a string aggregate identity or an entity route, got #{Rendering.describe(to)}"
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def envelope_hash(to)
|
|
208
|
+
hash = to.transform_keys(&:to_sym)
|
|
209
|
+
unknown = hash.keys - %i[aggregate entity entities]
|
|
210
|
+
raise TypeMismatch, "to: does not recognize #{unknown.sort.join(', ')}" unless unknown.empty?
|
|
211
|
+
|
|
212
|
+
[hash[:aggregate], entity_identities(hash)]
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# BUG#18 — an entity route naming no entity (`entities: []`, or
|
|
216
|
+
# neither key) refuses here, unconditionally, before entity_depth is
|
|
217
|
+
# consulted: for an aggregate-level command (depth 0) `[].size == 0`
|
|
218
|
+
# used to satisfy the depth check and let the degenerate Hash reach
|
|
219
|
+
# the command's own validation. Rust's `RoutingEnvelope::from_json`
|
|
220
|
+
# always refused it at this point. A bare aggregate identity String
|
|
221
|
+
# remains the ordinary aggregate-command shape.
|
|
222
|
+
def entity_identities(hash)
|
|
223
|
+
raise TypeMismatch, "to: takes entity: or entities:, not both" if hash.key?(:entities) && hash.key?(:entity)
|
|
224
|
+
|
|
225
|
+
identities = hash.key?(:entities) ? Array(hash[:entities]) : Array(hash[:entity])
|
|
226
|
+
raise TypeMismatch, "to: entity route requires at least one entity identity" if identities.empty?
|
|
227
|
+
|
|
228
|
+
identities
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# `with:` is deliberately strict: a caller choosing the explicit
|
|
232
|
+
# envelope cannot smuggle receiver identity back into the payload,
|
|
233
|
+
# and may not mix it with loose keyword arguments. Without `with:`
|
|
234
|
+
# (nil or false) the loose keyword arguments are the facts, unread —
|
|
235
|
+
# whether `to:` was given never enters this decision (BUG#17).
|
|
236
|
+
def offered_facts(declaring, with:, legacy:)
|
|
237
|
+
if with && !legacy.empty?
|
|
238
|
+
raise TypeMismatch,
|
|
239
|
+
"dispatch takes command facts in with:, not both with: and loose keyword arguments"
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
return legacy unless with
|
|
243
|
+
raise TypeMismatch, "with: must be a hash of command facts" unless with.is_a?(Hash)
|
|
244
|
+
|
|
245
|
+
offered = with.transform_keys(&:to_sym)
|
|
246
|
+
declared = declaring.attributes.map { |attribute| attribute.name.to_sym }
|
|
247
|
+
refuse_unknown_facts!(declaring, offered, declared)
|
|
248
|
+
refuse_absent_facts!(declaring, offered, declared)
|
|
249
|
+
offered
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Unknown before absent — a `with:` that is both still refuses
|
|
253
|
+
# UnknownArgument first.
|
|
254
|
+
def refuse_unknown_facts!(declaring, offered, declared)
|
|
255
|
+
unknown = (offered.keys - declared).sort
|
|
256
|
+
return if unknown.empty?
|
|
257
|
+
|
|
258
|
+
raise UnknownArgument,
|
|
259
|
+
RefusalWording.render_site("UnknownArgument", "unknown_args",
|
|
260
|
+
command: declaring.hecks_name, unknown: unknown,
|
|
261
|
+
declared: declared)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def refuse_absent_facts!(declaring, offered, declared)
|
|
265
|
+
absent = declaring.attributes.reject(&:optional?).map { |attribute| attribute.name.to_sym } - offered.keys
|
|
266
|
+
return if absent.empty?
|
|
267
|
+
|
|
268
|
+
raise AbsentArgument,
|
|
269
|
+
RefusalWording.render_site("AbsentArgument", "absent_args",
|
|
270
|
+
command: declaring.hecks_name, absent: absent,
|
|
271
|
+
declared: declared)
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
end
|