hecks 1.4.0 → 1.5.1
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 +65 -0
- data/lib/hecks/adapters/driven/folder.rb +73 -0
- data/lib/hecks/adapters/driven/google_authentication.rb +25 -4
- data/lib/hecks/adapters/driven/governance_authorization.rb +57 -6
- data/lib/hecks/adapters/driven/heki/journal.rb +8 -0
- data/lib/hecks/adapters/driven/heki/saga_store.rb +53 -7
- data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
- data/lib/hecks/adapters/driven/heki.rb +93 -2
- data/lib/hecks/adapters/driven/identity_registry.rb +10 -0
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +22 -0
- data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
- data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
- data/lib/hecks/adapters/driven/lambda/client.rb +35 -7
- data/lib/hecks/adapters/driven/lambda.rb +43 -2
- data/lib/hecks/adapters/driven/local_storage.rb +67 -1
- data/lib/hecks/adapters/driven/memory.rb +13 -13
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +20 -0
- data/lib/hecks/adapters/driven/prism.rb +46 -7
- data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
- data/lib/hecks/adapters/driven/system_clock.rb +3 -0
- data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
- data/lib/hecks/adapters/driven.rb +2 -0
- data/lib/hecks/adapters/driving/github_webhook.rb +14 -1
- data/lib/hecks/behaviors/dsl.rb +58 -0
- data/lib/hecks/behaviors/expectations.rb +168 -7
- data/lib/hecks/behaviors/ir.rb +11 -0
- data/lib/hecks/behaviors/rspec.rb +8 -0
- data/lib/hecks/behaviors/runner.rb +19 -0
- data/lib/hecks/behaviors.rb +8 -0
- data/lib/hecks/bluebook/aggregate.rb +30 -0
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +7 -0
- data/lib/hecks/bluebook/assembly/build.rb +25 -4
- data/lib/hecks/bluebook/assembly/contract.rb +66 -14
- data/lib/hecks/bluebook/assembly/contracts.rb +24 -19
- data/lib/hecks/bluebook/assembly/marks.rb +143 -14
- data/lib/hecks/bluebook/assembly/specializer.rb +19 -0
- data/lib/hecks/bluebook/assembly.rb +25 -7
- data/lib/hecks/bluebook/attribute.rb +17 -3
- data/lib/hecks/bluebook/behaviour/aggregate.rb +32 -0
- data/lib/hecks/bluebook/behaviour/attribute.rb +13 -0
- data/lib/hecks/bluebook/behaviour/chapter.rb +55 -2
- data/lib/hecks/bluebook/behaviour/command.rb +33 -3
- data/lib/hecks/bluebook/behaviour/domain_port.rb +20 -0
- data/lib/hecks/bluebook/behaviour/entity.rb +12 -0
- data/lib/hecks/bluebook/behaviour/hexagon.rb +26 -0
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +23 -2
- data/lib/hecks/bluebook/behaviour/policy.rb +34 -9
- data/lib/hecks/bluebook/behaviour/process_manager.rb +32 -1
- data/lib/hecks/bluebook/behaviour/query.rb +5 -0
- data/lib/hecks/bluebook/behaviour/read_model.rb +21 -0
- data/lib/hecks/bluebook/behaviour/traits.rb +36 -0
- data/lib/hecks/bluebook/behaviour/value_object.rb +17 -5
- data/lib/hecks/bluebook/chapter.rb +23 -0
- data/lib/hecks/bluebook/command.rb +53 -8
- data/lib/hecks/bluebook/domain_port.rb +25 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +196 -37
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +79 -42
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +59 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +385 -53
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +135 -21
- data/lib/hecks/bluebook/dsl/command_builder.rb +189 -27
- data/lib/hecks/bluebook/dsl/entity_builder.rb +139 -9
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +41 -0
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +23 -6
- data/lib/hecks/bluebook/dsl/policy_builder.rb +60 -19
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +79 -15
- data/lib/hecks/bluebook/dsl/query_builder.rb +33 -4
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +102 -27
- data/lib/hecks/bluebook/dsl/rule_reference.rb +60 -8
- data/lib/hecks/bluebook/dsl/translation_builder.rb +146 -40
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +55 -7
- data/lib/hecks/bluebook/entity.rb +29 -0
- data/lib/hecks/bluebook/expression/ast_json.rb +115 -23
- data/lib/hecks/bluebook/expression/ast_reader.rb +29 -0
- data/lib/hecks/bluebook/expression/canonical_form.rb +48 -9
- data/lib/hecks/bluebook/expression/evaluator.rb +207 -29
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +36 -0
- data/lib/hecks/bluebook/expression/resolver.rb +320 -77
- data/lib/hecks/bluebook/hexagon.rb +34 -0
- data/lib/hecks/bluebook/lifecycle.rb +11 -0
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -0
- data/lib/hecks/bluebook/meta_validator/judge.rb +30 -26
- data/lib/hecks/bluebook/meta_validator/plan.rb +43 -8
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +1 -0
- data/lib/hecks/bluebook/meta_validator/readings.rb +157 -7
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +24 -4
- data/lib/hecks/bluebook/meta_validator/shapes.rb +141 -0
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +149 -23
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +5 -4
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +1 -0
- data/lib/hecks/bluebook/meta_validator.rb +180 -84
- data/lib/hecks/bluebook/model_check.rb +268 -24
- data/lib/hecks/bluebook/pattern_subset.rb +23 -1
- data/lib/hecks/bluebook/process_manager.rb +13 -0
- data/lib/hecks/bluebook/project_discovery.rb +5 -0
- data/lib/hecks/bluebook/project_loader.rb +40 -0
- data/lib/hecks/bluebook/project_register.rb +44 -0
- data/lib/hecks/bluebook/query.rb +27 -0
- data/lib/hecks/bluebook/read_model.rb +21 -1
- data/lib/hecks/bluebook/reference.rb +21 -8
- data/lib/hecks/bluebook/smoke_test.rb +29 -6
- data/lib/hecks/bluebook/synthesizer.rb +34 -0
- data/lib/hecks/bluebook/translation.rb +30 -1
- data/lib/hecks/bluebook/value_object.rb +23 -5
- data/lib/hecks/bluebook.rb +3 -4
- data/lib/hecks/codemod.rb +107 -20
- data/lib/hecks/construct.rb +15 -1
- data/lib/hecks/corpus.rb +146 -25
- data/lib/hecks/deploy/bluebook/deploy.bluebook +105 -0
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
- data/lib/hecks/deploy/oidc.json +5 -0
- data/lib/hecks/doc/reference.rb +185 -16
- data/lib/hecks/embryonaut_bluebook.rb +32 -9
- data/lib/hecks/facade/handle.rb +76 -3
- data/lib/hecks/facade/surface/aggregate_door.rb +8 -0
- data/lib/hecks/forms/field_shape.rb +3 -0
- data/lib/hecks/forms/page.rb +14 -0
- data/lib/hecks/forms/port_argument.rb +12 -0
- data/lib/hecks/forms/query_form_renderer.rb +63 -0
- data/lib/hecks/forms/record_renderer.rb +58 -0
- data/lib/hecks/forms/record_table.rb +27 -0
- data/lib/hecks/forms/reference_options.rb +24 -0
- data/lib/hecks/forms/value_object_shape.rb +10 -0
- data/lib/hecks/fqn.rb +58 -0
- data/lib/hecks/framework/bluebook/compliance.bluebook +221 -0
- data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
- data/lib/hecks/framework/oidc.json +15 -0
- data/lib/hecks/framework.rb +43 -20
- data/lib/hecks/freezer.rb +17 -1
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +159 -24
- data/lib/hecks/fuzzing/combination_miner.rb +59 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +109 -8
- data/lib/hecks/fuzzing/coverage_campaign.rb +56 -13
- data/lib/hecks/fuzzing/differential.rb +34 -0
- data/lib/hecks/fuzzing/domain_generator.rb +188 -11
- data/lib/hecks/fuzzing/era_boundary.rb +45 -15
- data/lib/hecks/fuzzing/form_census.rb +86 -0
- data/lib/hecks/fuzzing/generated_domain_check.rb +76 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +39 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +79 -22
- data/lib/hecks/fuzzing/nondeterministic.rb +13 -1
- data/lib/hecks/fuzzing/persistence_parity.rb +95 -3
- data/lib/hecks/fuzzing/properties/corrections.rb +25 -0
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +158 -14
- data/lib/hecks/fuzzing/properties/guards.rb +44 -0
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +48 -0
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +18 -0
- data/lib/hecks/fuzzing/properties/outbox.rb +49 -11
- data/lib/hecks/fuzzing/properties/querying.rb +68 -14
- data/lib/hecks/fuzzing/properties.rb +24 -15
- data/lib/hecks/fuzzing/qa_settings.rb +12 -0
- data/lib/hecks/fuzzing/replay.rb +137 -29
- data/lib/hecks/fuzzing/rotation_priority.rb +41 -21
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +46 -20
- data/lib/hecks/fuzzing/self_consistency.rb +189 -40
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +12 -6
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +1 -1
- data/lib/hecks/fuzzing/sequence_generator.rb +47 -0
- data/lib/hecks/fuzzing/shrinker.rb +114 -2
- data/lib/hecks/fuzzing/structural_skips.rb +18 -4
- data/lib/hecks/fuzzing/sweep_depth.rb +8 -0
- data/lib/hecks/fuzzing/target_capabilities.rb +61 -14
- data/lib/hecks/fuzzing/value_generator.rb +98 -10
- data/lib/hecks/grammar/evolve.rb +178 -2
- data/lib/hecks/grammar.rb +46 -0
- data/lib/hecks/ir.rb +38 -7
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
- data/lib/hecks/literal.rb +32 -0
- data/lib/hecks/naming.rb +88 -7
- data/lib/hecks/ports/access_control.rb +5 -10
- data/lib/hecks/ports/authorization.rb +3 -6
- data/lib/hecks/ports/identity_assignment.rb +1 -2
- data/lib/hecks/ports/identity_resolution.rb +1 -2
- data/lib/hecks/ports/key_vault.port +6 -0
- data/lib/hecks/ports/key_vault.rb +85 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +6 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +38 -2
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +40 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +92 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +8 -0
- data/lib/hecks/ports/query/in_memory.rb +39 -6
- data/lib/hecks/ports/query/ordering.rb +15 -0
- data/lib/hecks/ports.rb +1 -0
- data/lib/hecks/projections/bootstrap_table.rb +43 -8
- data/lib/hecks/projections/deploy/fargate.rb +666 -0
- data/lib/hecks/projections/deploy/lambda.rb +2423 -0
- data/lib/hecks/projections/deploy/shared.rb +624 -0
- data/lib/hecks/projections/diagrams.rb +243 -7
- data/lib/hecks/projections/glossary/html.rb +88 -0
- data/lib/hecks/projections/glossary/markdown.rb +47 -0
- data/lib/hecks/projections/glossary/mermaid.rb +48 -0
- data/lib/hecks/projections/glossary/sentences.rb +105 -0
- data/lib/hecks/projections/glossary.rb +161 -19
- data/lib/hecks/projections/model/deviations.rb +44 -0
- data/lib/hecks/projections/model.rb +51 -1
- data/lib/hecks/projections/oidc.rb +40 -3
- data/lib/hecks/projections/parser_table.rb +49 -8
- data/lib/hecks/projections/reference.rb +12 -0
- data/lib/hecks/projections/rust_vocabulary.rb +219 -16
- data/lib/hecks/projections/shape.rb +8 -0
- data/lib/hecks/projections/statements.rb +63 -16
- data/lib/hecks/projections/vocabulary.rb +17 -0
- data/lib/hecks/projections.rb +3 -0
- data/lib/hecks/projector/cli_projector.rb +218 -10
- data/lib/hecks/projector/docs_projector.rb +145 -19
- data/lib/hecks/projector/exporter.rb +65 -11
- data/lib/hecks/projector/ir_projector.rb +6 -0
- data/lib/hecks/projector/narrate_projector.rb +136 -15
- data/lib/hecks/projector/target.rb +47 -10
- data/lib/hecks/projector.rb +101 -19
- data/lib/hecks/query_ir.rb +47 -0
- data/lib/hecks/query_specification/common/null_policy.rb +5 -3
- data/lib/hecks/rendering.rb +6 -0
- data/lib/hecks/router/namespace_installer.rb +13 -0
- data/lib/hecks/router.rb +55 -0
- data/lib/hecks/runtime/aggregate_lock.rb +9 -0
- data/lib/hecks/runtime/boot_gates.rb +18 -0
- data/lib/hecks/runtime/caller.rb +32 -0
- data/lib/hecks/runtime/capability_graph.rb +11 -0
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +23 -21
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +14 -15
- data/lib/hecks/runtime/command_interpreter.rb +42 -17
- data/lib/hecks/runtime/command_rules/admissibility.rb +165 -14
- data/lib/hecks/runtime/command_rules/arithmetic.rb +17 -5
- data/lib/hecks/runtime/command_rules/references.rb +118 -28
- data/lib/hecks/runtime/dependency_planning.rb +45 -0
- data/lib/hecks/runtime/dispatcher.rb +28 -50
- data/lib/hecks/runtime/entity_element.rb +161 -8
- data/lib/hecks/runtime/entity_interpreter.rb +44 -9
- data/lib/hecks/runtime/errors.rb +18 -4
- data/lib/hecks/runtime/event.rb +10 -5
- data/lib/hecks/runtime/identity.rb +71 -3
- data/lib/hecks/runtime/instance.rb +67 -7
- data/lib/hecks/runtime/interpreting.rb +13 -5
- data/lib/hecks/runtime/invocation.rb +118 -36
- data/lib/hecks/runtime/loader.rb +94 -8
- data/lib/hecks/runtime/outbox.rb +145 -7
- data/lib/hecks/runtime/policy_interpreter.rb +22 -9
- data/lib/hecks/runtime/port_operation_interpreter.rb +20 -0
- data/lib/hecks/runtime/query_interpreter.rb +40 -12
- data/lib/hecks/runtime/reaction_invocation.rb +53 -8
- data/lib/hecks/runtime/read_model_interpreter.rb +23 -7
- data/lib/hecks/runtime/rebuild_sweep.rb +28 -0
- data/lib/hecks/runtime/reference_hop.rb +42 -0
- data/lib/hecks/runtime/refusal_wording.rb +50 -0
- data/lib/hecks/runtime/registry/saga_persistence.rb +11 -0
- data/lib/hecks/runtime/registry/verification.rb +119 -4
- data/lib/hecks/runtime/registry.rb +157 -4
- data/lib/hecks/runtime/remote_dispatcher.rb +92 -6
- data/lib/hecks/runtime/routing.rb +27 -2
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +10 -12
- data/lib/hecks/runtime/saga_interpreter.rb +27 -13
- data/lib/hecks/runtime/tenant_check.rb +26 -6
- data/lib/hecks/runtime/tenant_scope.rb +18 -0
- data/lib/hecks/runtime/value/coercion.rb +255 -33
- data/lib/hecks/runtime/value/entity_list_coercion.rb +102 -30
- data/lib/hecks/runtime/value.rb +50 -0
- data/lib/hecks/runtime.rb +32 -0
- data/lib/hecks/storehouse.rb +305 -9
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
- data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks.rb +79 -1
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -0
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +12 -2
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +29 -5
- metadata +14 -5
- data/lib/hecks/codemod/legacy_dispatch_args.rb +0 -299
- data/lib/hecks/codemod/legacy_dispatch_recorder.rb +0 -186
- data/lib/hecks/deprecation.rb +0 -95
- data/lib/hecks/framework/bluebook/compliance.bluebook +0 -1
data/lib/hecks/runtime/outbox.rb
CHANGED
|
@@ -13,7 +13,9 @@ module Hecks
|
|
|
13
13
|
# outbound port operation) the external effects those reactions
|
|
14
14
|
# cause. `future-features.md` item 8, built.
|
|
15
15
|
#
|
|
16
|
-
#
|
|
16
|
+
# ## The shape
|
|
17
|
+
#
|
|
18
|
+
# One row per (event, consumer). A consumer is a named
|
|
17
19
|
# policy or process manager that would react to the event — resolved
|
|
18
20
|
# at enqueue time from the registry (`Fanout`), so the outbox records
|
|
19
21
|
# who was owed what, not just that an event happened. Rows move
|
|
@@ -33,18 +35,22 @@ module Hecks
|
|
|
33
35
|
# tracks delivery, not the domain's answer.
|
|
34
36
|
# failed the consumer raised a defect (non-refusal error).
|
|
35
37
|
#
|
|
36
|
-
#
|
|
38
|
+
# ## Delivery is inline by default
|
|
39
|
+
#
|
|
40
|
+
# The dispatcher drains the rows it
|
|
37
41
|
# just wrote, in the same call, in the order C10.2 fixes (per event
|
|
38
42
|
# in `emits` order: that event's policy rows, then its saga rows —
|
|
39
43
|
# the emitting domain's own policies before other domains'). Nothing about the
|
|
40
44
|
# happy path is deferred or asynchronous; a caller still sees every
|
|
41
45
|
# reaction settled when `dispatch` returns. What changes is the
|
|
42
|
-
# crash window: a process that dies between
|
|
43
|
-
#
|
|
44
|
-
# `Relay#redrive!` — run at boot by `Loader.run_boot_gates!` —
|
|
46
|
+
# crash window: without a durable row, a process that dies between
|
|
47
|
+
# commit and reaction loses the reaction silently. The row survives,
|
|
48
|
+
# and `Relay#redrive!` — run at boot by `Loader.run_boot_gates!` —
|
|
45
49
|
# finds it.
|
|
46
50
|
#
|
|
47
|
-
#
|
|
51
|
+
# ## What redrive does, and deliberately doesn't
|
|
52
|
+
#
|
|
53
|
+
# A `pending` row is
|
|
48
54
|
# redriven: its consumer provably never started (claiming is the
|
|
49
55
|
# first thing delivery does), so running it now is exactly-once by
|
|
50
56
|
# construction. A `claimed` row is not auto-redriven: the consumer
|
|
@@ -57,7 +63,9 @@ module Hecks
|
|
|
57
63
|
# unique per store, so a re-enqueue of the same fact to the same
|
|
58
64
|
# consumer is a no-op rather than a second row.
|
|
59
65
|
#
|
|
60
|
-
#
|
|
66
|
+
# ## Which adapters
|
|
67
|
+
#
|
|
68
|
+
# Memory (in-process rows — visible to specs,
|
|
61
69
|
# gone with the process, exactly like everything else Memory holds),
|
|
62
70
|
# Sqlite and Postgres (a `hecks_outbox` table in the aggregate's own
|
|
63
71
|
# database — the only way the enqueue can share the save's
|
|
@@ -71,13 +79,30 @@ module Hecks
|
|
|
71
79
|
|
|
72
80
|
Row = Struct.new(:id, :delivery_id, :event_uid, :aggregate, :domain, :kind, :consumer, :event,
|
|
73
81
|
:status, :attempts, :error, keyword_init: true) do
|
|
82
|
+
# Reports whether this row is still waiting to be claimed.
|
|
83
|
+
#
|
|
84
|
+
# @return [Boolean] true if this row's status is `"pending"`
|
|
74
85
|
def pending? = status == "pending"
|
|
86
|
+
|
|
87
|
+
# Reports whether this row's consumer is currently running.
|
|
88
|
+
#
|
|
89
|
+
# @return [Boolean] true if this row's status is `"claimed"`
|
|
75
90
|
def claimed? = status == "claimed"
|
|
91
|
+
|
|
92
|
+
# Reports whether this row's consumer ran to completion.
|
|
93
|
+
#
|
|
94
|
+
# @return [Boolean] true if this row's status is `"delivered"`
|
|
76
95
|
def delivered? = status == "delivered"
|
|
96
|
+
|
|
97
|
+
# Reports whether this row's consumer raised a defect.
|
|
98
|
+
#
|
|
99
|
+
# @return [Boolean] true if this row's status is `"failed"`
|
|
77
100
|
def failed? = status == "failed"
|
|
78
101
|
|
|
79
102
|
# Wire-shaped — what an adapter persists. `event` is the event's
|
|
80
103
|
# own `to_h` plus correlation; `Row.event_from` reverses it.
|
|
104
|
+
#
|
|
105
|
+
# @return [Hash{Symbol => Object}] this row's own fields, keyed by name
|
|
81
106
|
def to_h
|
|
82
107
|
{ id: id, delivery_id: delivery_id, event_uid: event_uid, aggregate: aggregate, domain: domain,
|
|
83
108
|
kind: kind, consumer: consumer, event: event, status: status, attempts: attempts, error: error }
|
|
@@ -89,6 +114,12 @@ module Hecks
|
|
|
89
114
|
|
|
90
115
|
module_function
|
|
91
116
|
|
|
117
|
+
# Renders an event as the wire-shaped Hash an outbox row's own `event`
|
|
118
|
+
# field stores.
|
|
119
|
+
#
|
|
120
|
+
# @param event [Runtime::Event] the event to serialize
|
|
121
|
+
# @return [Hash{Symbol => Object}] `event`'s own `to_h`, with `correlation`
|
|
122
|
+
# merged in
|
|
92
123
|
def serialize_event(event)
|
|
93
124
|
event.to_h.merge(correlation: event.correlation)
|
|
94
125
|
end
|
|
@@ -96,11 +127,21 @@ module Hecks
|
|
|
96
127
|
# The emitting domain's own bluebook first, then the rest in load
|
|
97
128
|
# order (C10.2) — the one policy ordering both `PolicyInterpreter#
|
|
98
129
|
# policies_for` and `Fanout.policies` read.
|
|
130
|
+
#
|
|
131
|
+
# @param registry [Runtime::Registry] the booted registry whose loaded
|
|
132
|
+
# bluebooks are ordered
|
|
133
|
+
# @param domain [String, Symbol] the emitting domain, sorted first
|
|
134
|
+
# @return [Array<Bluebook::Chapter>] every loaded chapter, `domain`'s own first
|
|
99
135
|
def bluebooks_home_first(registry, domain)
|
|
100
136
|
home, others = registry.bluebooks.each_value.partition { |bluebook| bluebook.name == domain }
|
|
101
137
|
home + others
|
|
102
138
|
end
|
|
103
139
|
|
|
140
|
+
# Rebuilds a frozen event from an outbox row's own stored `event` field.
|
|
141
|
+
#
|
|
142
|
+
# @param hash [Hash{String, Symbol => Object}] the wire-shaped event Hash,
|
|
143
|
+
# as `serialize_event` built it (String or Symbol keys either way)
|
|
144
|
+
# @return [Runtime::Event] the rebuilt, frozen event
|
|
104
145
|
def event_from(hash)
|
|
105
146
|
hash = hash.transform_keys(&:to_sym)
|
|
106
147
|
Event.new(
|
|
@@ -113,6 +154,12 @@ module Hecks
|
|
|
113
154
|
).emit!
|
|
114
155
|
end
|
|
115
156
|
|
|
157
|
+
# Recursively symbolizes every Hash key reachable from `value`.
|
|
158
|
+
#
|
|
159
|
+
# @param value [Object] the value to symbolize; typically a Hash or Array,
|
|
160
|
+
# possibly nested
|
|
161
|
+
# @return [Object] `value` with every Hash key (at any depth) turned into a
|
|
162
|
+
# Symbol; a non-Hash, non-Array value passes through unchanged
|
|
116
163
|
def deep_symbolize(value)
|
|
117
164
|
case value
|
|
118
165
|
when Hash then value.to_h { |k, v| [k.to_sym, deep_symbolize(v)] }
|
|
@@ -137,6 +184,12 @@ module Hecks
|
|
|
137
184
|
# this consumer".
|
|
138
185
|
# Row order is delivery order (C10.2): per event, in `emits`
|
|
139
186
|
# order — that event's policy rows, then its saga rows.
|
|
187
|
+
#
|
|
188
|
+
# @param registry [Runtime::Registry] the booted registry every candidate
|
|
189
|
+
# consumer is resolved against
|
|
190
|
+
# @param events [Array<Runtime::Event>] the just-emitted events to build rows for
|
|
191
|
+
# @param domain [String, Symbol] the emitting domain
|
|
192
|
+
# @return [Array<Runtime::Outbox::Row>] one pending row per (event, consumer)
|
|
140
193
|
def rows_for(registry, events, domain)
|
|
141
194
|
uids = events.to_h { |event| [event, SecureRandom.uuid] }
|
|
142
195
|
events.flat_map do |event|
|
|
@@ -144,6 +197,16 @@ module Hecks
|
|
|
144
197
|
end
|
|
145
198
|
end
|
|
146
199
|
|
|
200
|
+
# Builds one pending row per policy `event` triggers.
|
|
201
|
+
#
|
|
202
|
+
# @param registry [Runtime::Registry] the booted registry policies are
|
|
203
|
+
# resolved against
|
|
204
|
+
# @param event [Runtime::Event] the just-emitted event
|
|
205
|
+
# @param domain [String, Symbol] the emitting domain
|
|
206
|
+
# @param uid [String] this event's own enqueue-time UID, shared by every row
|
|
207
|
+
# built for it
|
|
208
|
+
# @return [Array<Runtime::Outbox::Row>] one pending row per matching policy,
|
|
209
|
+
# emitting domain's own bluebook first
|
|
147
210
|
def policies(registry, event, domain, uid)
|
|
148
211
|
emitting = Naming.demodulise(event.aggregate)
|
|
149
212
|
Outbox.bluebooks_home_first(registry, domain).flat_map do |bluebook|
|
|
@@ -159,6 +222,18 @@ module Hecks
|
|
|
159
222
|
end
|
|
160
223
|
end
|
|
161
224
|
|
|
225
|
+
# Builds one pending row per process manager `event` advances, in `domain`
|
|
226
|
+
# only (a saga never reacts across domains).
|
|
227
|
+
#
|
|
228
|
+
# @param registry [Runtime::Registry] the booted registry process managers
|
|
229
|
+
# are resolved against
|
|
230
|
+
# @param event [Runtime::Event] the just-emitted event
|
|
231
|
+
# @param domain [String, Symbol] the domain whose declared process managers
|
|
232
|
+
# are checked
|
|
233
|
+
# @param uid [String] this event's own enqueue-time UID, shared by every row
|
|
234
|
+
# built for it
|
|
235
|
+
# @return [Array<Runtime::Outbox::Row>] one pending row per process manager
|
|
236
|
+
# `event` starts, ends, or advances
|
|
162
237
|
def sagas(registry, event, domain, uid)
|
|
163
238
|
bluebook = registry.bluebook(domain)
|
|
164
239
|
return [] unless bluebook
|
|
@@ -170,6 +245,13 @@ module Hecks
|
|
|
170
245
|
end
|
|
171
246
|
end
|
|
172
247
|
|
|
248
|
+
# Reports whether `event` starts, ends, or advances `process_manager`.
|
|
249
|
+
#
|
|
250
|
+
# @param process_manager [Bluebook::ProcessManager] the declared process
|
|
251
|
+
# manager to check
|
|
252
|
+
# @param event [Runtime::Event] the just-emitted event
|
|
253
|
+
# @return [Boolean] true if `event` names `process_manager`'s own
|
|
254
|
+
# `starts_on`, `ends_on`, or a declared handler
|
|
173
255
|
def listens?(process_manager, event)
|
|
174
256
|
process_manager.starts_on == event.name || process_manager.ends_on == event.name ||
|
|
175
257
|
!process_manager.handler_for(event.name).nil?
|
|
@@ -179,6 +261,14 @@ module Hecks
|
|
|
179
261
|
# operation — the row is the durable record that an external
|
|
180
262
|
# call was owed, claimed right before the adapter is asked and
|
|
181
263
|
# settled right after. Everything else is a plain "reaction".
|
|
264
|
+
#
|
|
265
|
+
# @param registry [Runtime::Registry] the booted registry the trigger's own
|
|
266
|
+
# aggregate/port are resolved against
|
|
267
|
+
# @param policy [Bluebook::Policy] the policy whose trigger is classified
|
|
268
|
+
# @param home_domain [String, Symbol] the domain `policy` is declared in,
|
|
269
|
+
# used when `policy` declares no `target_domain`
|
|
270
|
+
# @return [String] `"effect"` when the trigger resolves to an outbound port
|
|
271
|
+
# operation, `"reaction"` otherwise
|
|
182
272
|
def kind_for(registry, policy, home_domain)
|
|
183
273
|
target = "#{policy.target_domain || home_domain}::#{policy.trigger_command}"
|
|
184
274
|
parsed = Naming.split_verb(target)
|
|
@@ -204,11 +294,16 @@ module Hecks
|
|
|
204
294
|
# by the Rust kernel (`spec/rust_conformance_spec.rb`); this is
|
|
205
295
|
# an additive, Ruby-only log, the same rule `saga_dispatch_log`
|
|
206
296
|
# and `policy_dispatch_log` already follow.
|
|
297
|
+
#
|
|
298
|
+
# @param registry [Runtime::Registry] the booted registry this relay drains
|
|
299
|
+
# rows for
|
|
207
300
|
def initialize(registry)
|
|
208
301
|
@registry = registry
|
|
209
302
|
@log = []
|
|
210
303
|
end
|
|
211
304
|
|
|
305
|
+
# Attaches the interpreters a delivered row's own consumer runs through.
|
|
306
|
+
#
|
|
212
307
|
# A Dispatcher hands over the interpreters a consumer runs
|
|
213
308
|
# through (`Dispatcher#initialize`). Until then this relay can
|
|
214
309
|
# enqueue (that needs only the registry) but not deliver — and
|
|
@@ -216,20 +311,38 @@ module Hecks
|
|
|
216
311
|
# to. The registry holds one relay for its lifetime; a second
|
|
217
312
|
# dispatcher fronting the same registry re-attaches, which is
|
|
218
313
|
# fine because both dispatchers share every log and store.
|
|
314
|
+
#
|
|
315
|
+
# @param policies [Runtime::PolicyInterpreter] the interpreter a `"policy:"`
|
|
316
|
+
# consumer reacts through
|
|
317
|
+
# @param sagas [Runtime::SagaInterpreter] the interpreter a `"saga:"` consumer
|
|
318
|
+
# advances through
|
|
319
|
+
# @return [Runtime::Outbox::Relay] self
|
|
219
320
|
def attach(policies:, sagas:)
|
|
220
321
|
@policies = policies
|
|
221
322
|
@sagas = sagas
|
|
222
323
|
self
|
|
223
324
|
end
|
|
224
325
|
|
|
326
|
+
# Reports whether a dispatcher has attached its interpreters.
|
|
327
|
+
#
|
|
328
|
+
# @return [Boolean] true once `attach` has run
|
|
225
329
|
def attached? = !@policies.nil?
|
|
226
330
|
|
|
331
|
+
# Writes one pending row per (event, consumer) `events` owes a reaction to.
|
|
332
|
+
#
|
|
227
333
|
# Called inside the save transaction by `Interpreting` for the
|
|
228
334
|
# command/entity paths, and outside one by `Dispatcher` for port
|
|
229
335
|
# operations (which save nothing, so there is no transaction to
|
|
230
336
|
# share). Returns the rows as stored (ids assigned), or nil when
|
|
231
337
|
# the repository has no outbox — the dispatcher then reacts
|
|
232
338
|
# directly, exactly as before.
|
|
339
|
+
#
|
|
340
|
+
# @param repository [Persistence::AppendOnly] the repository whose store the
|
|
341
|
+
# rows are enqueued into; a no-op unless it has an outbox
|
|
342
|
+
# @param events [Array<Runtime::Event>] the events this dispatch just emitted
|
|
343
|
+
# @param domain [String, Symbol] the emitting domain
|
|
344
|
+
# @return [Array<Runtime::Outbox::Row>, nil] the stored rows, `id` assigned;
|
|
345
|
+
# `nil` when `repository` has no outbox; `[]` when `events` is empty
|
|
233
346
|
def enqueue(repository, events, domain)
|
|
234
347
|
return nil unless repository.outbox?
|
|
235
348
|
return [] if events.empty?
|
|
@@ -241,6 +354,15 @@ module Hecks
|
|
|
241
354
|
|
|
242
355
|
# Drain the rows a dispatch just committed. `rows` nil means "no
|
|
243
356
|
# outbox here" — react directly, the pre-outbox path.
|
|
357
|
+
#
|
|
358
|
+
# @param rows [Array<Runtime::Outbox::Row>, nil] the just-enqueued rows to
|
|
359
|
+
# deliver; nil to react directly instead (no outbox on this repository)
|
|
360
|
+
# @param events [Array<Runtime::Event>] the events this dispatch just emitted,
|
|
361
|
+
# read when `rows` is nil
|
|
362
|
+
# @param domain [String, Symbol] the emitting domain
|
|
363
|
+
# @param repository [Persistence::AppendOnly] the repository `rows` were
|
|
364
|
+
# enqueued into
|
|
365
|
+
# @return [void]
|
|
244
366
|
def deliver(rows, events, domain, repository)
|
|
245
367
|
if rows.nil?
|
|
246
368
|
# Per event, in `emits` order — its policies, then its sagas
|
|
@@ -258,6 +380,11 @@ module Hecks
|
|
|
258
380
|
|
|
259
381
|
# One row: claim, run its consumer, settle. A claim that fails
|
|
260
382
|
# means another relay (or this one, re-entrantly) already has it.
|
|
383
|
+
#
|
|
384
|
+
# @param row [Runtime::Outbox::Row] the row to claim and deliver
|
|
385
|
+
# @param repository [Persistence::AppendOnly] the repository `row` is stored in
|
|
386
|
+
# @return [Boolean] true when this call claimed and delivered `row`; false
|
|
387
|
+
# when the claim failed, or the consumer raised a defect (recorded on `log`)
|
|
261
388
|
def deliver_row(row, repository)
|
|
262
389
|
return false unless repository.outbox_claim(row.id)
|
|
263
390
|
|
|
@@ -283,13 +410,24 @@ module Hecks
|
|
|
283
410
|
end
|
|
284
411
|
|
|
285
412
|
# Every row in every bound store, newest last. `status:` narrows.
|
|
413
|
+
#
|
|
414
|
+
# @param status [String, Symbol, nil] keep only rows with this status; nil
|
|
415
|
+
# for every row
|
|
416
|
+
# @return [Array<Runtime::Outbox::Row>] copies of the matching rows, across
|
|
417
|
+
# every store with an outbox
|
|
286
418
|
def rows(status: nil)
|
|
287
419
|
stores.flat_map { |repository| repository.outbox_rows(status: status) }
|
|
288
420
|
end
|
|
289
421
|
|
|
422
|
+
# Runs the boot-time reconciliation over every bound store.
|
|
423
|
+
#
|
|
290
424
|
# **Boot-time reconciliation**. Redrives `pending` rows (never
|
|
291
425
|
# claimed — safe by construction); surfaces `claimed` rows and
|
|
292
426
|
# redrives them only when told to (`claimed: true`).
|
|
427
|
+
#
|
|
428
|
+
# @param claimed [Boolean] whether to also redrive `claimed` rows (an
|
|
429
|
+
# explicit operator decision); false leaves them surfaced and untouched
|
|
430
|
+
# @return [Array<Runtime::Outbox::Row>] every row this call actually delivered
|
|
293
431
|
def redrive!(claimed: false)
|
|
294
432
|
redriven = []
|
|
295
433
|
stores.each do |repository|
|
|
@@ -16,11 +16,18 @@ module Hecks
|
|
|
16
16
|
class PolicyInterpreter
|
|
17
17
|
attr_reader :registry
|
|
18
18
|
|
|
19
|
+
# @param registry [Runtime::Registry] the booted registry whose loaded
|
|
20
|
+
# bluebooks are scanned for candidate policies
|
|
21
|
+
# @param door [Runtime::Dispatcher] the dispatcher reactions re-enter through,
|
|
22
|
+
# and whose reaction-depth guard is checked before each delivery
|
|
19
23
|
def initialize(registry, door:)
|
|
20
24
|
@registry = registry
|
|
21
25
|
@door = door
|
|
22
26
|
end
|
|
23
27
|
|
|
28
|
+
# Fires every declared policy `event` triggers, recording each outcome on
|
|
29
|
+
# the registry's reaction log.
|
|
30
|
+
#
|
|
24
31
|
# `deliver` returns `nil` for a policy whose `where` did not hold —
|
|
25
32
|
# silently, the same as a policy `policies_for` never selected at all
|
|
26
33
|
# (an `event_qualifier` miss carries no reaction_log entry either) —
|
|
@@ -32,6 +39,13 @@ module Hecks
|
|
|
32
39
|
# way of running exactly the consumer a row names (`Runtime::
|
|
33
40
|
# Outbox::Relay#run_consumer`) instead of every policy that
|
|
34
41
|
# matches the event. Selection is otherwise identical.
|
|
42
|
+
#
|
|
43
|
+
# @param event [Runtime::Event] the just-emitted event to react to
|
|
44
|
+
# @param domain [String, Symbol] the domain `event`'s own aggregate belongs
|
|
45
|
+
# to, the emitting domain's policies fire first
|
|
46
|
+
# @param only [Array(Bluebook::Policy, String), nil] one `[policy, home_domain]`
|
|
47
|
+
# pair to run exactly, instead of scanning every loaded bluebook for candidates
|
|
48
|
+
# @return [void]
|
|
35
49
|
def react(event, domain, only: nil)
|
|
36
50
|
selected = only ? [only] : policies_for(event, domain)
|
|
37
51
|
selected.each do |policy, home_domain|
|
|
@@ -195,14 +209,13 @@ module Hecks
|
|
|
195
209
|
# The event's own identity is a fact too, not only its payload. A
|
|
196
210
|
# for_each query commonly filters by the emitting record's own
|
|
197
211
|
# identity (`OpenForCustomer`'s own `reference:`, scoping by the
|
|
198
|
-
# very customer who was just suspended) —
|
|
199
|
-
#
|
|
200
|
-
#
|
|
201
|
-
#
|
|
202
|
-
#
|
|
203
|
-
#
|
|
204
|
-
#
|
|
205
|
-
# customer" the whole reaction exists to catch.
|
|
212
|
+
# very customer who was just suspended) — a fact routing keeps
|
|
213
|
+
# separate from payload (`to:`/`with:`, what the facade's own
|
|
214
|
+
# bang-methods always use), so `event.payload` alone does not carry
|
|
215
|
+
# it. Without this, the query would silently see neither the field
|
|
216
|
+
# it needs nor any error saying why — an empty result read as
|
|
217
|
+
# "nothing to freeze" instead of "the customer" the whole reaction
|
|
218
|
+
# exists to catch.
|
|
206
219
|
#
|
|
207
220
|
# Merged in only when the query declares an argument by that exact
|
|
208
221
|
# name and the emitting aggregate's own identity is genuinely what
|
|
@@ -261,7 +274,7 @@ module Hecks
|
|
|
261
274
|
label: "#{policy.name}'s trigger"
|
|
262
275
|
)
|
|
263
276
|
|
|
264
|
-
#
|
|
277
|
+
# The raw inputs `args` was resolved from — same additive,
|
|
265
278
|
# Ruby-only shape SagaInterpreter#deliver_saga_dispatch's own
|
|
266
279
|
# saga_dispatch_log gets, for Properties.dispatch_binding_
|
|
267
280
|
# fidelity's own independent re-derivation of Policy#with_spec's
|
|
@@ -25,6 +25,10 @@ module Hecks
|
|
|
25
25
|
refuse_unknown_arguments refuse_absent_arguments normalize_args resolve_references resolve_route emit
|
|
26
26
|
].freeze
|
|
27
27
|
|
|
28
|
+
# @param registry [Runtime::Registry] the booted registry this interpreter dispatches
|
|
29
|
+
# against
|
|
30
|
+
# @param rules [Runtime::CommandRules] the shared command-rule checks (references) this
|
|
31
|
+
# interpreter's steps call
|
|
28
32
|
def initialize(registry, rules:)
|
|
29
33
|
@registry = registry
|
|
30
34
|
@rules = rules
|
|
@@ -32,6 +36,22 @@ module Hecks
|
|
|
32
36
|
|
|
33
37
|
# `invocation` — the `Runtime::Invocation` `Dispatcher` built;
|
|
34
38
|
# `ctx.args` is its `to_args`, `ctx.route` its `target`.
|
|
39
|
+
#
|
|
40
|
+
# @param domain [String] the domain the aggregate belongs to
|
|
41
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate the port operation belongs to
|
|
42
|
+
# @param operation [Bluebook::PortOperation] the port operation to dispatch
|
|
43
|
+
# @param invocation [Runtime::Invocation] the invocation `Dispatcher` built for this call
|
|
44
|
+
# @return [Array<Runtime::Event>] the events recorded: the operation's own `emits` for
|
|
45
|
+
# an inbound operation, or a single `answers`/`refuses` event for an outbound one
|
|
46
|
+
# (an adapter failure is recorded as a `refuses` event, not raised)
|
|
47
|
+
# @raise [Runtime::UnknownArgument] if `invocation` offers an argument the operation
|
|
48
|
+
# does not declare
|
|
49
|
+
# @raise [Runtime::AbsentArgument] if `invocation` omits a non-optional declared
|
|
50
|
+
# argument
|
|
51
|
+
# @raise [Runtime::TypeMismatch] if an offered argument does not coerce to its declared
|
|
52
|
+
# type
|
|
53
|
+
# @raise [Runtime::NotFound] if an offered reference or the operation's own receiving
|
|
54
|
+
# record cannot be found
|
|
35
55
|
def call(domain, aggregate, operation, invocation)
|
|
36
56
|
ctx = Context.new(domain, aggregate, operation, invocation.to_args)
|
|
37
57
|
ctx.invocation = invocation
|
|
@@ -22,10 +22,26 @@ module Hecks
|
|
|
22
22
|
class QueryInterpreter
|
|
23
23
|
attr_reader :registry
|
|
24
24
|
|
|
25
|
+
# @param registry [Runtime::Registry] the booted registry queries are answered
|
|
26
|
+
# against
|
|
25
27
|
def initialize(registry)
|
|
26
28
|
@registry = registry
|
|
27
29
|
end
|
|
28
30
|
|
|
31
|
+
# Answers one declared aggregate or entity query, preferring a native adapter
|
|
32
|
+
# hook and falling back to interpreting the query over every loaded record.
|
|
33
|
+
#
|
|
34
|
+
# @param domain [String, Symbol] the domain `aggregate` belongs to
|
|
35
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate the query is declared on
|
|
36
|
+
# @param query_name [String] the query's declared name, or an entity query's
|
|
37
|
+
# dotted `"Entity.Query"` name
|
|
38
|
+
# @param args [Hash{Symbol => Object}] the query's declared arguments
|
|
39
|
+
# @return [Array<Hash>] one frozen Hash per matching record, its state with
|
|
40
|
+
# `:id` merged in last
|
|
41
|
+
# @raise [Runtime::UnknownVerb] if `query_name` names no declared query
|
|
42
|
+
# @raise [Runtime::TypeMismatch] if an argument cannot be coerced to its
|
|
43
|
+
# declared type
|
|
44
|
+
# @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
|
|
29
45
|
def call(domain, aggregate, query_name, args)
|
|
30
46
|
return entity_rows(domain, aggregate, query_name, args) if query_name.include?(".")
|
|
31
47
|
|
|
@@ -55,7 +71,7 @@ module Hecks
|
|
|
55
71
|
# Instance#to_h's own comment: an aggregate free to declare its
|
|
56
72
|
# own attribute literally named `id` has that attribute's own
|
|
57
73
|
# wrapped value sitting in `record.state[:id]` already; merging
|
|
58
|
-
# it over a `{id:}.merge(state)`
|
|
74
|
+
# it over a `{id:}.merge(state)` would let it silently
|
|
59
75
|
# clobber the correct bare identity this row is supposed to
|
|
60
76
|
# carry.
|
|
61
77
|
# A query row is an answer, not a handle. Mutating one edits
|
|
@@ -81,6 +97,18 @@ module Hecks
|
|
|
81
97
|
# adapters, but the fold itself — the empty candidate set, a
|
|
82
98
|
# duplicate id, a dangling reference, a chain's inside-out
|
|
83
99
|
# resolution order — would only ever be compared against itself.
|
|
100
|
+
#
|
|
101
|
+
# @param domain [String, Symbol] the domain `aggregate` belongs to
|
|
102
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate the query is declared on
|
|
103
|
+
# @param query_name [String] the query's declared name, or an entity query's
|
|
104
|
+
# dotted `"Entity.Query"` name
|
|
105
|
+
# @param args [Hash{Symbol => Object}] the query's declared arguments
|
|
106
|
+
# @return [Array<Hash>] one Hash per matching record, its state with `:id`
|
|
107
|
+
# merged in last
|
|
108
|
+
# @raise [Runtime::UnknownVerb] if `query_name` names no declared query
|
|
109
|
+
# @raise [Runtime::TypeMismatch] if an argument cannot be coerced to its
|
|
110
|
+
# declared type
|
|
111
|
+
# @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
|
|
84
112
|
def reference_call(domain, aggregate, query_name, args)
|
|
85
113
|
return entity_rows(domain, aggregate, query_name, args) if query_name.include?(".")
|
|
86
114
|
|
|
@@ -104,9 +132,9 @@ module Hecks
|
|
|
104
132
|
ordered = ordered(matched, declared.order_by, declared.null_semantics)
|
|
105
133
|
# **Offset first, then limit** — the order SQL means by `LIMIT n
|
|
106
134
|
# OFFSET m`, and the order Ports::Query::InMemory#execute already
|
|
107
|
-
# applies (see that file's own comment).
|
|
108
|
-
#
|
|
109
|
-
#
|
|
135
|
+
# applies (see that file's own comment). Without reading
|
|
136
|
+
# `declared.offset`, offset would silently vanish for any query
|
|
137
|
+
# answered here, not just come out reversed.
|
|
110
138
|
skipped = declared.offset ? ordered.drop(resolve_query_value(declared.offset.value, args).to_i) : ordered
|
|
111
139
|
capped = declared.limit ? skipped.first(resolve_query_value(declared.limit.value, args).to_i) : skipped
|
|
112
140
|
|
|
@@ -249,12 +277,12 @@ module Hecks
|
|
|
249
277
|
end
|
|
250
278
|
|
|
251
279
|
# The comparator table itself lives in
|
|
252
|
-
# QuerySpecification::Common::Comparison
|
|
253
|
-
# Ports::Query::InMemory#holds?
|
|
254
|
-
# two drifted
|
|
255
|
-
# `comparable` disagreed about value objects with two
|
|
256
|
-
# members. What stays here is how a value is reached for
|
|
257
|
-
# path: the registry is instance state rather than an argument.
|
|
280
|
+
# QuerySpecification::Common::Comparison, shared with
|
|
281
|
+
# Ports::Query::InMemory#holds? rather than each carrying its own
|
|
282
|
+
# copy — two copies once drifted: `none_in_state` reached only one
|
|
283
|
+
# of them, and `comparable` disagreed about value objects with two
|
|
284
|
+
# numeric members. What stays here is how a value is reached for
|
|
285
|
+
# this path: the registry is instance state rather than an argument.
|
|
258
286
|
def holds?(clause, held, args, record: nil, domain: nil)
|
|
259
287
|
QuerySpecification::Common::Comparison.holds?(
|
|
260
288
|
clause.op, comparable(held), comparable(resolve_query_value(clause.value, args)), registry: @registry
|
|
@@ -273,8 +301,8 @@ module Hecks
|
|
|
273
301
|
# the same way a command argument's own is, so a `nil` offered for
|
|
274
302
|
# a non-optional value-object-typed query attribute
|
|
275
303
|
# (Governance::RoleAssignment.AssignmentsForActor's `actor_id`, say)
|
|
276
|
-
# has to refuse — passing it through unchecked
|
|
277
|
-
#
|
|
304
|
+
# has to refuse — passing it through unchecked would let it through
|
|
305
|
+
# as a silent, unfiltered query instead, a real
|
|
278
306
|
# Ruby/Rust divergence the fuzzer caught (QualityControl BUG#2).
|
|
279
307
|
#
|
|
280
308
|
# `checked_vo?` true is handled by `null_vo_argument!` directly,
|
|
@@ -6,10 +6,10 @@ require_relative "value"
|
|
|
6
6
|
module Hecks
|
|
7
7
|
module Runtime
|
|
8
8
|
# Turns facts selected by a policy or process manager into the same
|
|
9
|
-
# receiver/payload envelope an outside caller uses.
|
|
10
|
-
#
|
|
11
|
-
# compatibility seam that separates
|
|
12
|
-
#
|
|
9
|
+
# receiver/payload envelope an outside caller uses. A reaction declares
|
|
10
|
+
# both receiver identities and facts through one `with:` map, so this is
|
|
11
|
+
# the one compatibility seam that separates them after resolving the
|
|
12
|
+
# declaration and before re-entering the dispatcher.
|
|
13
13
|
module ReactionInvocation
|
|
14
14
|
Target = Struct.new(:aggregate, :entities, :command, keyword_init: true)
|
|
15
15
|
# :facts, not :values — Struct.new already defines #values (every
|
|
@@ -19,10 +19,17 @@ module Hecks
|
|
|
19
19
|
|
|
20
20
|
module_function
|
|
21
21
|
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
22
|
+
# Reports whether a reaction declared an explicit `with:` projection.
|
|
23
|
+
#
|
|
24
|
+
# An omitted projection and an explicitly empty `with: {}` both hold
|
|
25
|
+
# as the same empty array on the holding IR. Builders preserve
|
|
26
|
+
# declaration presence off-wire, in `@projection_declared`;
|
|
27
|
+
# reconstructed/legacy IR without that ivar falls back to reading
|
|
28
|
+
# presence off whether `with_spec` is non-empty.
|
|
29
|
+
#
|
|
30
|
+
# @param declaration [Bluebook::Policy, Bluebook::DispatchSpec] the reacting
|
|
31
|
+
# declaration to check
|
|
32
|
+
# @return [Boolean] true if the declaration names an explicit `with:` projection
|
|
26
33
|
def projection_declared?(declaration)
|
|
27
34
|
if declaration.instance_variable_defined?(:@projection_declared)
|
|
28
35
|
declaration.instance_variable_get(:@projection_declared)
|
|
@@ -31,11 +38,27 @@ module Hecks
|
|
|
31
38
|
end
|
|
32
39
|
end
|
|
33
40
|
|
|
41
|
+
# Resolves a declared `with:` projection against the scopes and bindings visible to it.
|
|
42
|
+
#
|
|
34
43
|
# A reaction's source names resolve lexically, not globally. Policies
|
|
35
44
|
# supply one event/row scope. Process managers supply current event then
|
|
36
45
|
# opening-event memory, while correlation is an explicit binding ahead
|
|
37
46
|
# of both. Missing names are refused here rather than materialized as nil
|
|
38
47
|
# and accidentally presented as target command facts.
|
|
48
|
+
#
|
|
49
|
+
# @param with_spec [Hash{Symbol => Object}] each target fact name mapped to its source:
|
|
50
|
+
# a Symbol naming a fact visible in `bindings` or `scopes`, or any other value taken
|
|
51
|
+
# as a literal
|
|
52
|
+
# @param scopes [Array<Hecks::Runtime::ReactionInvocation::Scope, Array(String,
|
|
53
|
+
# Hash)>] the named fact scopes to resolve a Symbol source against, checked in order;
|
|
54
|
+
# a bare `[name, facts]` pair is wrapped into a `Scope`
|
|
55
|
+
# @param bindings [Hash] explicit bindings (such as a saga's correlation key), checked
|
|
56
|
+
# before any scope
|
|
57
|
+
# @param label [String] names this resolution in an `UnknownArgument` refusal
|
|
58
|
+
# @return [Hash{Symbol => Object}] `with_spec`'s keys mapped to their resolved,
|
|
59
|
+
# materialized values
|
|
60
|
+
# @raise [Runtime::UnknownArgument] if a Symbol source names a fact visible in no
|
|
61
|
+
# binding and no scope
|
|
39
62
|
def resolve_mapping(with_spec:, scopes:, bindings: {}, label: "reaction")
|
|
40
63
|
normalized_bindings = bindings.transform_keys(&:to_sym)
|
|
41
64
|
normalized_scopes = scopes.map do |scope|
|
|
@@ -86,6 +109,28 @@ module Hecks
|
|
|
86
109
|
# piece that is self-contained; what remains is the sequencing
|
|
87
110
|
# itself, which further splitting would only relocate, not remove.
|
|
88
111
|
# rubocop:disable-next Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
112
|
+
#
|
|
113
|
+
# @param registry [Runtime::Registry] the booted registry to resolve `verb` against
|
|
114
|
+
# @param verb [String] the fully qualified target command verb
|
|
115
|
+
# @param projected [Hash] the facts to send, already resolved (e.g. by
|
|
116
|
+
# `resolve_mapping`) or, for a legacy reaction, the raw event/row payload
|
|
117
|
+
# @param explicit [Boolean] true when the reaction declared its own `with:` projection
|
|
118
|
+
# (`projection_declared?`); false forwards `projected` wholesale as legacy args
|
|
119
|
+
# @param passthrough [Array<String, Symbol>] extra fact names allowed to ride along
|
|
120
|
+
# unconsumed, beyond the receiver identity and declared command facts
|
|
121
|
+
# @param source_receiver [Hash{Symbol => Object}, nil] the triggering event's own
|
|
122
|
+
# `{aggregate:, identity:}`, offered as a same-aggregate receiver when nothing else
|
|
123
|
+
# supplies one; nil when there is no such event to inherit from
|
|
124
|
+
# @return [Hash{Symbol => Object}] `{to:, with:}` for an explicit projection targeting
|
|
125
|
+
# a non-creating command (`with:` only for a creating command); otherwise `projected`
|
|
126
|
+
# (with `to:` merged in when a receiver could be inherited)
|
|
127
|
+
# @raise [Runtime::UnknownVerb] if `verb` does not resolve to a declared command,
|
|
128
|
+
# entity command, or port operation (only when `explicit` is true; a legacy call
|
|
129
|
+
# resolving `verb` only to check inheritance swallows this and forwards unchanged)
|
|
130
|
+
# @raise [Runtime::TypeMismatch] if an explicit projection resolves no receiver
|
|
131
|
+
# identity for the target aggregate or one of its entities
|
|
132
|
+
# @raise [Runtime::UnknownArgument] if an explicit projection's facts include a name
|
|
133
|
+
# that is neither a consumed receiver identity nor a declared command fact
|
|
89
134
|
def build(registry:, verb:, projected:, explicit:, passthrough: [], source_receiver: nil)
|
|
90
135
|
args = projected.transform_keys(&:to_sym)
|
|
91
136
|
unless explicit
|
|
@@ -16,16 +16,32 @@ module Hecks
|
|
|
16
16
|
# model is simple enough for one; otherwise runs the whole join
|
|
17
17
|
# in-process against loaded records.
|
|
18
18
|
class ReadModelInterpreter
|
|
19
|
+
# @param registry [Runtime::Registry] the booted registry whose repositories
|
|
20
|
+
# this interpreter reads
|
|
19
21
|
def initialize(registry) = @registry = registry
|
|
20
22
|
|
|
23
|
+
# Runs one declared read model and returns its projected rows.
|
|
24
|
+
#
|
|
25
|
+
# @param domain [String, Symbol] the domain the read model is declared in
|
|
26
|
+
# @param model [Bluebook::ReadModel] the read model to run
|
|
27
|
+
# @param args [Hash{Symbol => Object}] the query's declared arguments
|
|
28
|
+
# @return [Array<Hash>] a one-element Array holding a Hash of head name to
|
|
29
|
+
# projected rows (or a single row, for a non-`:many` head)
|
|
30
|
+
# @raise [Runtime::TypeMismatch] if the reference argument is offered as a whole
|
|
31
|
+
# object rather than a plain identity, or a `median` field is not numeric
|
|
32
|
+
# @raise [Runtime::NotFound] if the reference argument names no record
|
|
33
|
+
# @raise [KeyError] if a rooted read model is asked without its reference argument
|
|
34
|
+
# @raise [ArgumentError] if `group_by` or `median` names a field its target
|
|
35
|
+
# aggregate does not declare
|
|
36
|
+
# @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
|
|
21
37
|
def call(domain, model, args)
|
|
22
38
|
project(domain, model, args)
|
|
23
39
|
end
|
|
24
40
|
|
|
25
41
|
private
|
|
26
42
|
|
|
27
|
-
#
|
|
28
|
-
# each step's own comment names a real,
|
|
43
|
+
# Root-first, then the SQLite escape hatch, then the join loop —
|
|
44
|
+
# each step's own comment names a real, already-shipped bug the
|
|
29
45
|
# current order fixes (the reference/TenantScope refusal ordering
|
|
30
46
|
# above, the root-first head processing below). Splitting this
|
|
31
47
|
# into smaller methods would scatter that ordering across method
|
|
@@ -77,11 +93,11 @@ module Hecks
|
|
|
77
93
|
# bluebook. `read_model_builder.rb`'s own `include` is
|
|
78
94
|
# documented "Order-independent" (the `:many` flag is resolved
|
|
79
95
|
# at build time, once `@reference_target` is known), but that
|
|
80
|
-
# promise
|
|
81
|
-
# their literal declared order and
|
|
82
|
-
# against whatever was already in `projected`
|
|
83
|
-
# very first time through, if a many-side head happened
|
|
84
|
-
# declared before the root. A real, live bug (not a guess):
|
|
96
|
+
# promise is not kept without this: running heads in
|
|
97
|
+
# their literal declared order and matching each "many" head
|
|
98
|
+
# against whatever was already in `projected` would leave it
|
|
99
|
+
# empty, the very first time through, if a many-side head happened
|
|
100
|
+
# to be declared before the root. A real, live bug (not a guess):
|
|
85
101
|
# `include Promotion` before `include Item` on a read model
|
|
86
102
|
# whose root is Item silently returned an empty array for
|
|
87
103
|
# Promotion — no error, just a wrong, too-small answer — while
|