hecks 1.4.0 → 1.5.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 +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.hecksagon +19 -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/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 +58 -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/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/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 +29 -5
- data/lib/hecks/projector.rb +74 -6
- 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 +11 -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
|
@@ -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
|
|
@@ -32,6 +32,12 @@ module Hecks
|
|
|
32
32
|
# `save` only runs when a projected value would differ from what
|
|
33
33
|
# is already stored, so re-running a sweep with nothing having
|
|
34
34
|
# moved on the target side touches the append log not at all.
|
|
35
|
+
#
|
|
36
|
+
# @param registry [Runtime::Registry] the booted registry to read repositories from
|
|
37
|
+
# @param domain [String] the domain the aggregate belongs to
|
|
38
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate whose `projected_fields` to
|
|
39
|
+
# refresh
|
|
40
|
+
# @return [Integer] the number of records actually changed and saved
|
|
35
41
|
def call(registry, domain, aggregate)
|
|
36
42
|
return 0 if aggregate.projected_fields.empty?
|
|
37
43
|
|
|
@@ -39,6 +45,16 @@ module Hecks
|
|
|
39
45
|
repository.all.count { |record| refresh(registry, domain, aggregate, record, repository) }
|
|
40
46
|
end
|
|
41
47
|
|
|
48
|
+
# Refreshes one record's own projected fields in place, saving it if any changed.
|
|
49
|
+
#
|
|
50
|
+
# @param registry [Runtime::Registry] the booted registry to read the target's
|
|
51
|
+
# repository from
|
|
52
|
+
# @param domain [String] the domain the aggregate belongs to
|
|
53
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate `record` is an instance of
|
|
54
|
+
# @param record [Runtime::Instance] the record to refresh, mutated in place
|
|
55
|
+
# @param repository [Ports::Persistence::AppendOnly] the repository to save `record`
|
|
56
|
+
# through when it changes
|
|
57
|
+
# @return [Boolean] true if any projected field's value changed and `record` was saved
|
|
42
58
|
def refresh(registry, domain, aggregate, record, repository)
|
|
43
59
|
changed = false
|
|
44
60
|
|
|
@@ -55,10 +71,22 @@ module Hecks
|
|
|
55
71
|
changed
|
|
56
72
|
end
|
|
57
73
|
|
|
74
|
+
# Reads the current value of one projected field's remote target field.
|
|
75
|
+
#
|
|
58
76
|
# `nil` when the reference itself does not resolve in this
|
|
59
77
|
# chapter (a cross-domain target left "unfollowed" the same way
|
|
60
78
|
# References#dereference already leaves one) or when the record
|
|
61
79
|
# names no target at all — an optional reference nobody set.
|
|
80
|
+
#
|
|
81
|
+
# @param registry [Runtime::Registry] the booted registry to read the target's
|
|
82
|
+
# repository from
|
|
83
|
+
# @param domain [String] the domain the aggregate belongs to
|
|
84
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate declaring `field`
|
|
85
|
+
# @param record [Runtime::Instance] the record holding the reference to follow
|
|
86
|
+
# @param field [Bluebook::ProjectedField] the projected field to resolve
|
|
87
|
+
# @return [Object, nil] the target record's own `field.remote_field` value; nil if the
|
|
88
|
+
# reference type does not resolve, the record names no target, or the target record
|
|
89
|
+
# cannot be found
|
|
62
90
|
def remote_value(registry, domain, aggregate, record, field)
|
|
63
91
|
target = aggregate.attribute(field.reference)&.type&.resolve
|
|
64
92
|
return nil unless target
|
|
@@ -31,6 +31,21 @@ module Hecks
|
|
|
31
31
|
module ReferenceHop
|
|
32
32
|
module_function
|
|
33
33
|
|
|
34
|
+
# Folds every hop clause in `declared.wheres` into a synthetic local `in` clause.
|
|
35
|
+
#
|
|
36
|
+
# @param declared [Bluebook::Query, Runtime::TenantScope::Scoped,
|
|
37
|
+
# QuerySpecification::Common::Options] the declared query specification to fold hop
|
|
38
|
+
# clauses of
|
|
39
|
+
# @param args [Hash] the query's arguments, read when resolving each hop's own query
|
|
40
|
+
# @param registry [Runtime::Registry] the booted registry to resolve each hop's target
|
|
41
|
+
# repository from
|
|
42
|
+
# @param domain [String] the domain `aggregate` belongs to
|
|
43
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate `declared` queries
|
|
44
|
+
# @return [Bluebook::Query, Runtime::TenantScope::Scoped, QuerySpecification::Common::
|
|
45
|
+
# Options, Hecks::Runtime::ReferenceHop::Folded] `declared` unchanged when it has no
|
|
46
|
+
# hop clauses; otherwise a `Folded` wrapper whose `#wheres` replaces each hop clause
|
|
47
|
+
# with its folded `in` clause
|
|
48
|
+
# @raise [Runtime::WiringError] if a hop's target no longer resolves (see `fold`)
|
|
34
49
|
def apply(declared, args, registry:, domain:, aggregate:)
|
|
35
50
|
hopped, local = declared.wheres.partition { |clause| QuerySpecification::HopPath.hop_head?(clause.field, aggregate.attributes) }
|
|
36
51
|
return declared if hopped.empty?
|
|
@@ -39,6 +54,18 @@ module Hecks
|
|
|
39
54
|
Folded.new(declared, local + folded)
|
|
40
55
|
end
|
|
41
56
|
|
|
57
|
+
# Folds one hop clause into a synthetic `in` clause over the hop attribute's own ids.
|
|
58
|
+
#
|
|
59
|
+
# @param clause [QuerySpecification::Common::WhereClause] the hop clause to fold; its
|
|
60
|
+
# `field` names the hop path, dotted past the first segment
|
|
61
|
+
# @param args [Hash] the query's arguments, read when resolving the inner query
|
|
62
|
+
# @param registry [Runtime::Registry] the booted registry to resolve the hop's target
|
|
63
|
+
# repository from
|
|
64
|
+
# @param domain [String] the domain `aggregate` belongs to
|
|
65
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate `clause` is declared against
|
|
66
|
+
# @return [QuerySpecification::Common::WhereClause] a synthetic `in` clause on the hop
|
|
67
|
+
# attribute's name, whose value is every id the inner clause admits on the target
|
|
68
|
+
# @raise [Runtime::WiringError] if the hop's target aggregate no longer resolves
|
|
42
69
|
def fold(clause, args, registry:, domain:, aggregate:)
|
|
43
70
|
step = QuerySpecification::HopPath.next_hop(clause.field, aggregate.attributes)
|
|
44
71
|
hop, rest = step
|
|
@@ -70,6 +97,16 @@ module Hecks
|
|
|
70
97
|
# aggregate is actually bound to (which may not be the engine
|
|
71
98
|
# the outer aggregate is bound to at all) rather than by a
|
|
72
99
|
# second reading of the comparators.
|
|
100
|
+
#
|
|
101
|
+
# @param domain [String] the domain `target` belongs to
|
|
102
|
+
# @param target [Bluebook::Aggregate] the hop's target aggregate to query
|
|
103
|
+
# @param wheres [Array<QuerySpecification::Common::WhereClause>] the inner clause(s) to
|
|
104
|
+
# run against `target`
|
|
105
|
+
# @param args [Hash] the outer query's arguments, read when resolving the inner query
|
|
106
|
+
# @param registry [Runtime::Registry] the booted registry to resolve `target`'s
|
|
107
|
+
# repository from
|
|
108
|
+
# @return [Array<String>] every distinct id the inner clause(s) admit on `target`
|
|
109
|
+
# @raise [Runtime::WiringError] if a hop nested inside `wheres` no longer resolves
|
|
73
110
|
def matching_ids(domain, target, wheres, args, registry:)
|
|
74
111
|
spec = apply(QuerySpecification::Common::Options.new(wheres: wheres), args,
|
|
75
112
|
registry: registry, domain: domain, aggregate: target)
|
|
@@ -87,6 +124,11 @@ module Hecks
|
|
|
87
124
|
# same reason: SimpleDelegator only intercepts calls made
|
|
88
125
|
# directly on the wrapper.
|
|
89
126
|
class Folded < SimpleDelegator
|
|
127
|
+
# @param declared [Bluebook::Query, Runtime::TenantScope::Scoped,
|
|
128
|
+
# QuerySpecification::Common::Options] the wrapped query specification, delegated to
|
|
129
|
+
# for everything but `#wheres`
|
|
130
|
+
# @param wheres [Array<QuerySpecification::Common::WhereClause>] the replacement
|
|
131
|
+
# where-clauses, hop clauses folded to synthetic `in` clauses
|
|
90
132
|
def initialize(declared, wheres)
|
|
91
133
|
super(declared)
|
|
92
134
|
@wheres = wheres
|
|
@@ -31,9 +31,18 @@ module Hecks
|
|
|
31
31
|
|
|
32
32
|
module_function
|
|
33
33
|
|
|
34
|
+
# Renders `refusal`/`site`'s template, substituting `values` verbatim.
|
|
35
|
+
#
|
|
34
36
|
# Plain text substitution, never expression syntax — a template is
|
|
35
37
|
# read, not evaluated. Values arrive already formatted; prefer
|
|
36
38
|
# `render_site`, which formats them off the declared rows.
|
|
39
|
+
#
|
|
40
|
+
# @param refusal [String] the `DomainRefusal` class name, such as `"UnknownArgument"`
|
|
41
|
+
# @param site [String] the template site within `refusal`, such as `"unknown_args"`
|
|
42
|
+
# @param values [Hash{Symbol => #to_s}] each `{name}` placeholder's already-formatted
|
|
43
|
+
# replacement text
|
|
44
|
+
# @return [String] the rendered refusal message
|
|
45
|
+
# @raise [KeyError] if no `RefusalTemplate` row declares `refusal`/`site`
|
|
37
46
|
def render(refusal, site, **values)
|
|
38
47
|
substitute(template(refusal, site), values)
|
|
39
48
|
end
|
|
@@ -46,6 +55,16 @@ module Hecks
|
|
|
46
55
|
# RefusalWording.render_site("UnknownArgument", "unknown_args",
|
|
47
56
|
# command: "Close", unknown: [:parcel], declared: [])
|
|
48
57
|
# # => "Close does not declare parcel — it takes none"
|
|
58
|
+
#
|
|
59
|
+
# @param refusal [String] the `DomainRefusal` class name, such as `"UnknownArgument"`
|
|
60
|
+
# @param site [String] the template site within `refusal`, such as `"unknown_args"`
|
|
61
|
+
# @param arguments [Hash{Symbol => Object}] raw values, one per `RefusalSiteArgument`
|
|
62
|
+
# row declared for `refusal`/`site`; formatted per row before substitution
|
|
63
|
+
# @return [String] the rendered refusal message
|
|
64
|
+
# @raise [ArgumentError] if `arguments` is missing a declared argument or offers one
|
|
65
|
+
# `refusal`/`site` does not declare
|
|
66
|
+
# @raise [KeyError] if no `RefusalTemplate`/`RefusalSiteArgument` rows declare
|
|
67
|
+
# `refusal`/`site`
|
|
49
68
|
def render_site(refusal, site, **arguments)
|
|
50
69
|
specs = argument_rows(refusal, site)
|
|
51
70
|
declared = specs.map { |spec| spec["argument"].to_sym }
|
|
@@ -62,6 +81,12 @@ module Hecks
|
|
|
62
81
|
# `render_site` without the registry lookups: a template, its
|
|
63
82
|
# argument rows, and raw values. The Rust projection calls this with
|
|
64
83
|
# the chapter's own rows to compute the expected wording it pins.
|
|
84
|
+
#
|
|
85
|
+
# @param template [String] the raw template, `{name}` placeholders unsubstituted
|
|
86
|
+
# @param specs [Array<Hash>] the `RefusalSiteArgument` rows to format `arguments`
|
|
87
|
+
# against, one per declared argument name
|
|
88
|
+
# @param arguments [Hash{Symbol => Object}] raw values, one per entry in `specs`
|
|
89
|
+
# @return [String] the rendered refusal message
|
|
65
90
|
def render_with(template, specs, arguments)
|
|
66
91
|
values = specs.to_h do |spec|
|
|
67
92
|
name = spec["argument"].to_sym
|
|
@@ -73,6 +98,12 @@ module Hecks
|
|
|
73
98
|
# One argument, written the way its row says. A list is sorted first
|
|
74
99
|
# (before quoting), then each item quoted, then joined; an empty list
|
|
75
100
|
# reads `when_empty`. A scalar is quoted or taken as its own text.
|
|
101
|
+
#
|
|
102
|
+
# @param spec [Hash] the argument's `RefusalSiteArgument` row (`"shape"`, `"quoting"`,
|
|
103
|
+
# and, for a list, `"sorted"`, `"separator"`, `"when_empty"`)
|
|
104
|
+
# @param value [Object, Array] the raw value to format; an Array (or anything
|
|
105
|
+
# `Array()`-coercible) for a `"list"`-shaped spec, a scalar otherwise
|
|
106
|
+
# @return [String] the formatted text
|
|
76
107
|
def format_argument(spec, value)
|
|
77
108
|
inspect = spec.fetch("quoting") == "inspect"
|
|
78
109
|
return inspect ? value.inspect : value.to_s unless spec.fetch("shape") == "list"
|
|
@@ -83,10 +114,22 @@ module Hecks
|
|
|
83
114
|
items.empty? ? spec.fetch("when_empty") : items.join(spec.fetch("separator"))
|
|
84
115
|
end
|
|
85
116
|
|
|
117
|
+
# Replaces each `{name}` placeholder in `template` with its value's text.
|
|
118
|
+
#
|
|
119
|
+
# @param template [String] the raw template, `{name}` placeholders unsubstituted
|
|
120
|
+
# @param values [Hash{Symbol => #to_s}] each placeholder name mapped to its
|
|
121
|
+
# replacement text
|
|
122
|
+
# @return [String] `template` with every `{name}` placeholder substituted
|
|
86
123
|
def substitute(template, values)
|
|
87
124
|
values.reduce(template) { |text, (key, value)| text.gsub("{#{key}}", value.to_s) }
|
|
88
125
|
end
|
|
89
126
|
|
|
127
|
+
# Looks up the raw template text declared for one refusal site.
|
|
128
|
+
#
|
|
129
|
+
# @param refusal [String] the `DomainRefusal` class name, such as `"UnknownArgument"`
|
|
130
|
+
# @param site [String] the template site within `refusal`, such as `"unknown_args"`
|
|
131
|
+
# @return [String] the raw `RefusalTemplate` text, `{name}` placeholders unsubstituted
|
|
132
|
+
# @raise [KeyError] if no `RefusalTemplate` row declares `refusal`/`site`
|
|
90
133
|
def template(refusal, site)
|
|
91
134
|
TEMPLATES.fetch([refusal, site]) do
|
|
92
135
|
raise KeyError, "no refusal template for #{refusal}/#{site} — declare it in " \
|
|
@@ -94,9 +137,16 @@ module Hecks
|
|
|
94
137
|
end
|
|
95
138
|
end
|
|
96
139
|
|
|
140
|
+
# The declared arguments for one refusal site.
|
|
141
|
+
#
|
|
97
142
|
# Read lazily, not into a constant: bin/project_vocabulary boots
|
|
98
143
|
# `hecks` (and so this file) before it writes the table a newly
|
|
99
144
|
# declared site's rows live in.
|
|
145
|
+
#
|
|
146
|
+
# @param refusal [String] the `DomainRefusal` class name, such as `"UnknownArgument"`
|
|
147
|
+
# @param site [String] the template site within `refusal`, such as `"unknown_args"`
|
|
148
|
+
# @return [Array<Hash>] the `RefusalSiteArgument` rows declared for `refusal`/`site`
|
|
149
|
+
# @raise [KeyError] if no `RefusalSiteArgument` rows declare `refusal`/`site`
|
|
100
150
|
def argument_rows(refusal, site)
|
|
101
151
|
@argument_rows ||= Hecks::Vocabulary.rows("RefusalSiteArgument")
|
|
102
152
|
.group_by { |row| [row["refusal"], row["site"]] }
|
|
@@ -40,6 +40,15 @@ module Hecks
|
|
|
40
40
|
# state lands in). Double-checked locking against a dedicated mutex
|
|
41
41
|
# — never `@saga_mutex` — see `Registry#initialize`'s own comment for
|
|
42
42
|
# why reusing that one would deadlock.
|
|
43
|
+
# Resolves and memoizes the adapter a domain's sagas persist through.
|
|
44
|
+
#
|
|
45
|
+
# @param domain [String, Symbol] the domain to resolve saga persistence for
|
|
46
|
+
# @return [Adapters::Heki, Adapters::Postgres, Adapters::Sqlite, Adapters::D1,
|
|
47
|
+
# Ports::Persistence::Plugins::Era::PostgresEra, Ports::Persistence::NullSagaStore]
|
|
48
|
+
# the same adapter instance the domain's anchor aggregate persists through, when it
|
|
49
|
+
# implements `save_saga`; `NULL_SAGA_STORE` for a domain with no anchor aggregate, an
|
|
50
|
+
# adapter that does not implement the capability, a `RemoteRuntime`-shaped adapter,
|
|
51
|
+
# or a `Runtime::WiringError` resolving the anchor's own bind
|
|
43
52
|
def saga_persistence(domain)
|
|
44
53
|
key = domain.to_s
|
|
45
54
|
@saga_persistence[key] || @saga_persistence_mutex.synchronize do
|
|
@@ -67,6 +76,8 @@ module Hecks
|
|
|
67
76
|
# points inside `@saga_mutex.synchronize` blocks (`saga_interpreter.
|
|
68
77
|
# rb`), which genuinely do and are guarded accordingly.
|
|
69
78
|
# rubocop:disable-next Hecks/ThreadSharedIvarMutation
|
|
79
|
+
#
|
|
80
|
+
# @return [Hecks::Runtime::Registry] self
|
|
70
81
|
def rehydrate_sagas!
|
|
71
82
|
@hecksagons.each_key do |domain|
|
|
72
83
|
saga_persistence(domain).each_saga do |process_manager, correlation, state, memory, completed_compensations = []|
|