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
|
@@ -33,6 +33,9 @@ module Hecks
|
|
|
33
33
|
UNMATCHABLE = Object.new.freeze
|
|
34
34
|
private_constant :UNMATCHABLE
|
|
35
35
|
|
|
36
|
+
# Walks `chain`, one hop per entry, and returns the located element (or
|
|
37
|
+
# `instance` itself, when `chain` is empty).
|
|
38
|
+
#
|
|
36
39
|
# **One hop per chain entry**. `container` starts as `instance` (the root
|
|
37
40
|
# aggregate record) and becomes each just-located element in turn —
|
|
38
41
|
# Dispatch's own element is found inside the Handler element
|
|
@@ -52,6 +55,22 @@ module Hecks
|
|
|
52
55
|
# split's own consequence: an entity's own identity may arrive via
|
|
53
56
|
# `to:` rather than duplicated into `args`, so each hop is offered
|
|
54
57
|
# its routed identity ahead of falling back to `args`.
|
|
58
|
+
#
|
|
59
|
+
# @param root_aggregate [Bluebook::Aggregate] the root aggregate record's
|
|
60
|
+
# own construct, held constant across every hop for coercion
|
|
61
|
+
# @param chain [Array<Bluebook::Entity>] the entity chain to walk, root-first
|
|
62
|
+
# @param instance [Runtime::Instance] the root aggregate record to walk from
|
|
63
|
+
# @param args [Hash{String, Symbol => Object}] the offered command arguments,
|
|
64
|
+
# read for each hop's own identity when `route` does not supply it
|
|
65
|
+
# @param command_name [String] the command name, quoted in a refusal
|
|
66
|
+
# @param route [Runtime::Routing::Envelope, nil] the call's resolved routing
|
|
67
|
+
# envelope, if any; its own `entities` supply each hop's identity first
|
|
68
|
+
# @return [Runtime::Instance, Hash{Symbol => Object}] the located element;
|
|
69
|
+
# `instance` itself, unchanged, when `chain` is empty
|
|
70
|
+
# @raise [Runtime::UnknownVerb] if a hop's owner declares no list attribute
|
|
71
|
+
# for that entity
|
|
72
|
+
# @raise [Runtime::NotFound] if a hop's identity is absent from `args`, or no
|
|
73
|
+
# element matches it
|
|
55
74
|
def locate_chain(root_aggregate, chain, instance, args, command_name, route = nil)
|
|
56
75
|
container = instance
|
|
57
76
|
owner = root_aggregate
|
|
@@ -81,6 +100,24 @@ module Hecks
|
|
|
81
100
|
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
82
101
|
# rubocop:disable-next Metrics/PerceivedComplexity
|
|
83
102
|
# rubocop:disable-next Metrics/MethodLength
|
|
103
|
+
#
|
|
104
|
+
# @param root_aggregate [Bluebook::Aggregate] the root aggregate record's own
|
|
105
|
+
# construct, used for value-object coercion
|
|
106
|
+
# @param owner [Bluebook::Aggregate, Bluebook::Entity] the construct whose own
|
|
107
|
+
# attribute declares the list `entity` is searched in
|
|
108
|
+
# @param entity [Bluebook::Entity] the entity type being located
|
|
109
|
+
# @param command_name [String] the command name, quoted in a refusal
|
|
110
|
+
# @param container [Runtime::Instance, Hash{Symbol => Object}] the record or
|
|
111
|
+
# element holding the list to search
|
|
112
|
+
# @param args [Hash{String, Symbol => Object}] the offered command arguments,
|
|
113
|
+
# read for the element's own identity when `routed_identity` is nil
|
|
114
|
+
# @param routed_identity [String, nil] the routing envelope's own identity
|
|
115
|
+
# string for this hop, matched directly instead of deriving one from `args`
|
|
116
|
+
# @return [Hash{Symbol => Object}] a fresh copy of the located element; the
|
|
117
|
+
# owning list inside `container` is replaced with a fresh copy too
|
|
118
|
+
# @raise [Runtime::UnknownVerb] if `owner` declares no list attribute for `entity`
|
|
119
|
+
# @raise [Runtime::NotFound] if an identity part is absent from `args`
|
|
120
|
+
# (`routed_identity` nil only), or no element matches
|
|
84
121
|
def element_of(root_aggregate, owner, entity, command_name, container, args, routed_identity = nil)
|
|
85
122
|
entity_name = entity.hecks_name
|
|
86
123
|
list_attr = owner.attributes.find { |a| a.list? && a.type.to_s == entity_name } ||
|
|
@@ -173,6 +210,12 @@ module Hecks
|
|
|
173
210
|
# that does not name its fields is refused when the bluebook loads ("an
|
|
174
211
|
# entity says what it is known by", "an identity part names something"),
|
|
175
212
|
# so by the time a dispatch arrives here there is always a path to dig.
|
|
213
|
+
#
|
|
214
|
+
# @param entity [Bluebook::Entity] the entity type whose identity paths
|
|
215
|
+
# are read
|
|
216
|
+
# @param element [Hash{Symbol => Object}] the stored element to read an
|
|
217
|
+
# identity off
|
|
218
|
+
# @return [String] the element's identity, joined from its declared parts
|
|
176
219
|
def element_identity(entity, element)
|
|
177
220
|
parts = entity.identity_paths.map do |path|
|
|
178
221
|
head = path.to_s.split(".").first.to_sym
|
|
@@ -215,8 +258,32 @@ module Hecks
|
|
|
215
258
|
# intentional mirroring, which is what lets the two be diffed
|
|
216
259
|
# against each other when one gets a fix the other needs too.
|
|
217
260
|
# rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
261
|
+
# Applies one declared mutation to an entity element, in place.
|
|
262
|
+
#
|
|
218
263
|
# `pre` — the element as it was before this command (C4.2): every
|
|
219
264
|
# read below goes through it, every write lands on `element`.
|
|
265
|
+
#
|
|
266
|
+
# @param rules [Runtime::CommandRules] the shared rules engine `resolve_source`/
|
|
267
|
+
# `arithmetic`/`multiply`/`clamp`/`sign_of` are read through
|
|
268
|
+
# @param aggregate [Bluebook::Aggregate] the root aggregate, for value-object
|
|
269
|
+
# coercion
|
|
270
|
+
# @param entity [Bluebook::Entity] the entity type `element` is an instance of
|
|
271
|
+
# @param element [Hash{Symbol => Object}] the element to mutate; written in place
|
|
272
|
+
# @param mutation [Bluebook::Mutation] the declared mutation to apply
|
|
273
|
+
# @param args [Hash{String, Symbol => Object}] the offered command arguments,
|
|
274
|
+
# a mutation's source may read from
|
|
275
|
+
# @param pre [Hash{Symbol => Object}] the element as it stood before this
|
|
276
|
+
# command; every read goes through this, every write lands on `element`
|
|
277
|
+
# @return [void]
|
|
278
|
+
# @raise [Runtime::TypeMismatch] if a mutation's own value cannot be coerced,
|
|
279
|
+
# or an arithmetic op's operands are not numeric or compatible value objects
|
|
280
|
+
# @raise [Runtime::InvariantViolation] if a coerced or arithmetic-derived value
|
|
281
|
+
# object breaks one of its own invariants
|
|
282
|
+
# @raise [Runtime::AlreadyExists] if an `:append` mints a nested entity whose
|
|
283
|
+
# identity collides with an existing element
|
|
284
|
+
# @raise [Bluebook::Expression::EvaluationError] if an arithmetic op's product
|
|
285
|
+
# does not fit a signed 64-bit Integer, or is a non-finite Float
|
|
286
|
+
# @raise [Runtime::WiringError] if `mutation.op` names no handled mutation kind
|
|
220
287
|
def apply_to_element(rules, aggregate, entity, element, mutation, args, pre = element)
|
|
221
288
|
case mutation.op
|
|
222
289
|
when :set
|
|
@@ -269,22 +336,47 @@ module Hecks
|
|
|
269
336
|
end
|
|
270
337
|
end
|
|
271
338
|
|
|
339
|
+
# Rewraps a plain-Numeric arithmetic result into `attribute`'s own declared
|
|
340
|
+
# value-object type, when the arithmetic itself ran unwrapped.
|
|
341
|
+
#
|
|
272
342
|
# `MutationApplier#rewrap_arithmetic_result`'s own entity-scoped
|
|
273
343
|
# twin, byte-for-byte the same fix — see that method's own
|
|
274
344
|
# comment for the full "phantom-field asymmetric wrapping" story.
|
|
275
345
|
# A no-op whenever `current` was already a Value (the arithmetic
|
|
276
346
|
# call already returned one) or the mutation targets no declared
|
|
277
347
|
# attribute at all.
|
|
348
|
+
#
|
|
349
|
+
# @param aggregate [Bluebook::Aggregate] the root aggregate, for value-object
|
|
350
|
+
# coercion
|
|
351
|
+
# @param attribute [Bluebook::Attribute, nil] the mutated field's own declared
|
|
352
|
+
# attribute; a no-op when nil (targets no declared attribute)
|
|
353
|
+
# @param current [Object] the field's pre-mutation value, as read off `pre`
|
|
354
|
+
# @param result [Object] the arithmetic op's own result
|
|
355
|
+
# @return [Runtime::Value, Object] `result` unchanged when already a `Value`,
|
|
356
|
+
# `current` is a `Value`, or `attribute` is nil; otherwise `result` coerced
|
|
357
|
+
# into `attribute`'s own declared type
|
|
278
358
|
def rewrap_arithmetic_result(aggregate, attribute, current, result)
|
|
279
359
|
return result if current.is_a?(Value) || attribute.nil? || result.is_a?(Value)
|
|
280
360
|
|
|
281
361
|
Value.for_attribute(aggregate, attribute, result)
|
|
282
362
|
end
|
|
283
363
|
|
|
364
|
+
# Resolves an `:append` mutation's own field source against the offered
|
|
365
|
+
# arguments, falling back to the element's own current field.
|
|
366
|
+
#
|
|
284
367
|
# `MutationApplier#resolve_append_source`'s own entity-scoped
|
|
285
368
|
# twin — a caller-supplied arg first, falling back to the
|
|
286
369
|
# element's own current field (never the parent instance's) when
|
|
287
370
|
# it isn't one.
|
|
371
|
+
#
|
|
372
|
+
# @param source [Symbol, Object] the mutation's own field source: a Symbol
|
|
373
|
+
# names a command argument or, failing that, an element field; anything
|
|
374
|
+
# else is returned as is
|
|
375
|
+
# @param element [Hash{Symbol => Object}] the element to fall back to reading
|
|
376
|
+
# from
|
|
377
|
+
# @param args [Hash{String, Symbol => Object}] the offered command arguments
|
|
378
|
+
# @return [Object, nil] the named argument's value; failing that, the named
|
|
379
|
+
# element field's value; `source` itself when it is not a Symbol
|
|
288
380
|
def resolve_element_append_source(source, element, args)
|
|
289
381
|
return source unless source.is_a?(Symbol)
|
|
290
382
|
return args[source] if args.key?(source)
|
|
@@ -314,6 +406,20 @@ module Hecks
|
|
|
314
406
|
# every declared attribute the append mapping doesn't name gets its
|
|
315
407
|
# own default the same way a fresh aggregate's own attributes
|
|
316
408
|
# already do (`Instance.defaults`), whichever branch built `fields`.
|
|
409
|
+
#
|
|
410
|
+
# @param aggregate [Bluebook::Aggregate] the root aggregate, for value-object
|
|
411
|
+
# coercion
|
|
412
|
+
# @param entity [Bluebook::Entity] the entity type `element`'s mutated list
|
|
413
|
+
# attribute belongs to
|
|
414
|
+
# @param element [Hash{Symbol => Object}] the element being appended to, read
|
|
415
|
+
# as the source for a field the mutation's own map does not supply directly
|
|
416
|
+
# @param mutation [Bluebook::Mutation] the declared `:append` mutation
|
|
417
|
+
# @param args [Hash{String, Symbol => Object}] the offered command arguments
|
|
418
|
+
# @return [Array] `element[mutation.target]`'s existing elements, frozen deep,
|
|
419
|
+
# with the newly built element (a `Runtime::Value`, or a Hash for an entity
|
|
420
|
+
# or untyped element) appended last
|
|
421
|
+
# @raise [Runtime::AlreadyExists] if the appended element is a nested entity
|
|
422
|
+
# whose identity collides with an existing one
|
|
317
423
|
def appended_to_element(aggregate, entity, element, mutation, args)
|
|
318
424
|
fields = mutation.source.transform_values { |source| resolve_element_append_source(source, element, args) }
|
|
319
425
|
element_type = entity.attribute(mutation.target)&.type
|
|
@@ -389,6 +495,15 @@ module Hecks
|
|
|
389
495
|
# what "the default" means. Additive only — a key `fields` already
|
|
390
496
|
# holds (the append mapping, an auto-minted identity, a lifecycle
|
|
391
497
|
# default) is never overwritten.
|
|
498
|
+
#
|
|
499
|
+
# @param aggregate [Bluebook::Aggregate] the root aggregate, for value-object
|
|
500
|
+
# coercion
|
|
501
|
+
# @param entity [Bluebook::Entity] the entity type whose declared attributes
|
|
502
|
+
# are defaulted
|
|
503
|
+
# @param fields [Hash{Symbol => Object}] the element fields already resolved;
|
|
504
|
+
# written in place
|
|
505
|
+
# @return [Hash{Symbol => Object}] `fields`, with every declared attribute it
|
|
506
|
+
# did not already hold filled with its own default
|
|
392
507
|
def fill_declared_defaults(aggregate, entity, fields)
|
|
393
508
|
entity.attributes.each do |attribute|
|
|
394
509
|
next if fields.key?(attribute.name)
|
|
@@ -404,6 +519,18 @@ module Hecks
|
|
|
404
519
|
# the list this targets is itself entity-typed — see
|
|
405
520
|
# `list_element_match?`, below, which both this and
|
|
406
521
|
# `MutationApplier#removed` now share.
|
|
522
|
+
#
|
|
523
|
+
# @param rules [Runtime::CommandRules] the shared rules engine `resolve_source`
|
|
524
|
+
# is read through
|
|
525
|
+
# @param aggregate [Bluebook::Aggregate] the root aggregate, for value-object
|
|
526
|
+
# coercion
|
|
527
|
+
# @param entity [Bluebook::Entity] the entity type `element`'s mutated list
|
|
528
|
+
# attribute belongs to
|
|
529
|
+
# @param element [Hash{Symbol => Object}] the element being removed from
|
|
530
|
+
# @param mutation [Bluebook::Mutation] the declared `:remove` mutation
|
|
531
|
+
# @param args [Hash{String, Symbol => Object}] the offered command arguments
|
|
532
|
+
# @return [Array] `element[mutation.target]`'s existing elements, with any
|
|
533
|
+
# matching the resolved remove target left out
|
|
407
534
|
def removed_from_element(rules, aggregate, entity, element, mutation, args)
|
|
408
535
|
value = rules.resolve_source(mutation.source, args)
|
|
409
536
|
attribute = entity.attribute(mutation.target)
|
|
@@ -443,6 +570,17 @@ module Hecks
|
|
|
443
570
|
# "matches" means — the same reasoning this file's own header
|
|
444
571
|
# gives for centralizing `locate_chain`/`element_of` once rather
|
|
445
572
|
# than twice.
|
|
573
|
+
#
|
|
574
|
+
# @param aggregate [Bluebook::Aggregate] the root aggregate, to resolve
|
|
575
|
+
# whether `attribute`'s own type is an entity
|
|
576
|
+
# @param attribute [Bluebook::Attribute, nil] the list attribute `element`
|
|
577
|
+
# belongs to
|
|
578
|
+
# @param element [Object] the stored list element to check: a Hash for an
|
|
579
|
+
# entity-typed list, a `Runtime::Value` otherwise
|
|
580
|
+
# @param value [Object] the `remove:` target to match `element` against
|
|
581
|
+
# @return [Boolean] whole-value equality for a non-entity-typed list;
|
|
582
|
+
# identity-field equality for an entity-typed one with a single identity
|
|
583
|
+
# head; false for a composite or absent identity
|
|
446
584
|
def list_element_match?(aggregate, attribute, element, value)
|
|
447
585
|
entity = attribute&.list? ? Value.find_entity(aggregate, attribute.type.to_s) : nil
|
|
448
586
|
return element == value unless entity
|
|
@@ -453,24 +591,27 @@ module Hecks
|
|
|
453
591
|
element.is_a?(Hash) && element[head] == value
|
|
454
592
|
end
|
|
455
593
|
|
|
456
|
-
#
|
|
594
|
+
# Refuses a caller-supplied or composite identity that already names an
|
|
595
|
+
# element on `current`.
|
|
596
|
+
#
|
|
597
|
+
# BUG#13 — the same check #hydrate gives every creating
|
|
457
598
|
# aggregate command (`repository.find(id)`,
|
|
458
|
-
# `command_interpreter.rb`), one level down.
|
|
459
|
-
# `MutationApplier` (mutation_applier.rb), called
|
|
599
|
+
# `command_interpreter.rb`), one level down. Shared with
|
|
600
|
+
# `MutationApplier` (mutation_applier.rb), called from
|
|
460
601
|
# `#entity_element` — an aggregate's own entity list (`Workspace.
|
|
461
|
-
# boards`, `Ledger.entries`)
|
|
602
|
+
# boards`, `Ledger.entries`) — and, moved here (BUG#145), from `#appended_
|
|
462
603
|
# to_element`, above — an entity's own nested entity list one hop
|
|
463
|
-
# further in (`Board.cards`) —
|
|
464
|
-
#
|
|
604
|
+
# further in (`Board.cards`) — so neither call site reimplements
|
|
605
|
+
# it a second time, the same way `#list_element_
|
|
465
606
|
# match?` already avoids that split for `remove:`.
|
|
466
607
|
#
|
|
467
608
|
# Reached, at the aggregate-owned call site, only on the two
|
|
468
609
|
# branches that do not auto-mint: a caller-supplied identity (the
|
|
469
610
|
# field is already in the append's own field map) or a composite
|
|
470
611
|
# one (`entity.identified_by` is nil for those — Runtime::
|
|
471
|
-
# Identified#derive_identity).
|
|
612
|
+
# Identified#derive_identity). Without this, neither checks the sibling
|
|
472
613
|
# list at all: a second LogVisit with the same date+sequence, or a
|
|
473
|
-
# second IssueKey with the same serial,
|
|
614
|
+
# second IssueKey with the same serial, would append a silent
|
|
474
615
|
# duplicate — worse than an ordinary duplicate row, because
|
|
475
616
|
# `EntityElement#element_of`'s own `find_index` always matches the
|
|
476
617
|
# first match, so the second becomes permanently unaddressable by
|
|
@@ -491,6 +632,18 @@ module Hecks
|
|
|
491
632
|
# hops up). Used for that naming purpose only (`owner.hecks_name`) —
|
|
492
633
|
# never for `Value`/namespace resolution, which is why an `Entity`
|
|
493
634
|
# (not just an `Aggregate`) is a valid thing to pass here.
|
|
635
|
+
#
|
|
636
|
+
# @param owner [Bluebook::Aggregate, Bluebook::Entity] the construct named
|
|
637
|
+
# in a refusal as what the duplicate "already exists on"
|
|
638
|
+
# @param entity [Bluebook::Entity] the entity type being checked for a
|
|
639
|
+
# colliding identity
|
|
640
|
+
# @param current [Array<Hash>, Object] the entity's own existing elements;
|
|
641
|
+
# coerced through `Array()`, so a single element or nil is also accepted
|
|
642
|
+
# @param fields [Hash{Symbol => Object}] the new element's own fields, whose
|
|
643
|
+
# identity heads are checked against every element in `current`
|
|
644
|
+
# @return [void]
|
|
645
|
+
# @raise [Runtime::AlreadyExists] if `current` already holds an element
|
|
646
|
+
# whose identity heads match `fields`'s own
|
|
494
647
|
def check_entity_collision(owner, entity, current, fields)
|
|
495
648
|
heads = entity.identity_heads
|
|
496
649
|
return if heads.empty?
|
|
@@ -24,8 +24,8 @@ module Hecks
|
|
|
24
24
|
class EntityInterpreter
|
|
25
25
|
include Interpreting
|
|
26
26
|
# The same payload gate aggregate commands and port operations already
|
|
27
|
-
# run — bug audit H1 (docs/audits/2026-08-10-main-bug-audit.md):
|
|
28
|
-
#
|
|
27
|
+
# run — bug audit H1 (docs/audits/2026-08-10-main-bug-audit.md): without
|
|
28
|
+
# this, this class runs neither refuse_unknown_arguments nor
|
|
29
29
|
# refuse_absent_arguments, on a comment claiming "an entity inherits
|
|
30
30
|
# its aggregate's own gate." Nothing on the entity dispatch path ever
|
|
31
31
|
# ran one — confirmed live, `LedgerEntry.Reverse` accepted an
|
|
@@ -83,7 +83,19 @@ module Hecks
|
|
|
83
83
|
# reads the declaring command — after `to:` is parsed, before the
|
|
84
84
|
# facts are.
|
|
85
85
|
Resolution = Data.define(:entity_names, :chain, :command_name, :command) do
|
|
86
|
+
# Resolves a dotted entity verb against `aggregate` into the entity
|
|
87
|
+
# chain it walks and the command it names.
|
|
88
|
+
#
|
|
86
89
|
# Refuses UnknownVerb for an unknown entity or command.
|
|
90
|
+
#
|
|
91
|
+
# @param aggregate [Bluebook::Aggregate] the root aggregate the dotted verb
|
|
92
|
+
# is resolved against
|
|
93
|
+
# @param dotted [String, Symbol] the entity verb, with the leading
|
|
94
|
+
# "Domain::Aggregate." already stripped (e.g. `"Handler.Dispatch.Bind"`)
|
|
95
|
+
# @return [EntityInterpreter::Resolution] the resolved entity names, chain,
|
|
96
|
+
# command name and command class
|
|
97
|
+
# @raise [Runtime::UnknownVerb] if `dotted` names no entity, or the located
|
|
98
|
+
# entity declares no such command
|
|
87
99
|
def self.of(aggregate, dotted)
|
|
88
100
|
*entity_names, command_name = dotted.to_s.split(".")
|
|
89
101
|
if entity_names.empty?
|
|
@@ -119,6 +131,9 @@ module Hecks
|
|
|
119
131
|
private_class_method :walk
|
|
120
132
|
end
|
|
121
133
|
|
|
134
|
+
# @param registry [Runtime::Registry] the booted registry this interpreter reads
|
|
135
|
+
# @param rules [Runtime::CommandRules] the shared rules engine (admissibility,
|
|
136
|
+
# references, arithmetic, authorization, emission) dispatch runs through
|
|
122
137
|
def initialize(registry, rules:)
|
|
123
138
|
@registry = registry
|
|
124
139
|
@rules = rules
|
|
@@ -136,6 +151,26 @@ module Hecks
|
|
|
136
151
|
# `resolution` is #resolve's answer; `invocation` the
|
|
137
152
|
# `Runtime::Invocation` `Dispatcher` built — `ctx.args` is its
|
|
138
153
|
# `to_args`, `ctx.route` its `target`.
|
|
154
|
+
#
|
|
155
|
+
# @param domain [String, Symbol] the domain `aggregate` belongs to
|
|
156
|
+
# @param aggregate [Bluebook::Aggregate] the root aggregate owning the entity
|
|
157
|
+
# chain the command targets
|
|
158
|
+
# @param resolution [EntityInterpreter::Resolution] the resolved entity chain
|
|
159
|
+
# and command, as `Resolution.of` builds it
|
|
160
|
+
# @param invocation [Runtime::Invocation] the invocation `Dispatcher` built for
|
|
161
|
+
# this call
|
|
162
|
+
# @param dry_run [Boolean] whether to run every step through validation without
|
|
163
|
+
# saving, emitting or enqueueing
|
|
164
|
+
# @return [Array(Runtime::Instance, Array<Runtime::Event>,
|
|
165
|
+
# Runtime::DependencyPlanning::Plan, Ports::Persistence::Execution,
|
|
166
|
+
# Array<Runtime::Outbox::Row>)] the settled parent aggregate instance, emitted
|
|
167
|
+
# events, execution plan, persistence outcome and outbox rows — the last
|
|
168
|
+
# three nil on a dry run, which skips save/emit/outbox
|
|
169
|
+
# @raise [StandardError] any class in `Runtime::DOMAIN_REFUSALS` when a
|
|
170
|
+
# given/ensures/invariant/authorization/admissibility rule refuses
|
|
171
|
+
# @raise [Runtime::StaleWrite] if concurrent writers beat this one through every
|
|
172
|
+
# retry (`MAX_STALE_WRITE_RETRIES`)
|
|
173
|
+
# @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
|
|
139
174
|
def call(domain, aggregate, resolution, invocation, dry_run: false)
|
|
140
175
|
chain = resolution.chain
|
|
141
176
|
entity = chain.last
|
|
@@ -239,7 +274,7 @@ module Hecks
|
|
|
239
274
|
# command's `corrects` names even exist" is checked here too, once,
|
|
240
275
|
# before the entity's own `given`s.
|
|
241
276
|
#
|
|
242
|
-
#
|
|
277
|
+
# Admissibility is checked against the parent/root, not the entity —
|
|
243
278
|
# deliberately `ctx.instance`/`ctx.aggregate` (the parent aggregate
|
|
244
279
|
# record and the root aggregate construct), never `ctx.view`/
|
|
245
280
|
# `ctx.entity` (the entity's own pre-mutation view/construct). This
|
|
@@ -399,12 +434,12 @@ module Hecks
|
|
|
399
434
|
end
|
|
400
435
|
|
|
401
436
|
# `locate_chain`/`element_of`/`element_identity`/`apply_to_element` and
|
|
402
|
-
# their own helpers
|
|
403
|
-
# (see that file's own header)
|
|
404
|
-
# `delegate_to_entity` step can locate and
|
|
405
|
-
# same way, against an aggregate record
|
|
406
|
-
# above, and every `step_*` method
|
|
407
|
-
#
|
|
437
|
+
# their own helpers live in `Runtime::EntityElement`
|
|
438
|
+
# (see that file's own header), shared rather than kept here, so
|
|
439
|
+
# `CommandInterpreter`'s own `delegate_to_entity` step can locate and
|
|
440
|
+
# mutate the same element the same way, against an aggregate record
|
|
441
|
+
# already held in memory. `call`, above, and every `step_*` method
|
|
442
|
+
# reach them through that module.
|
|
408
443
|
end
|
|
409
444
|
end
|
|
410
445
|
end
|
data/lib/hecks/runtime/errors.rb
CHANGED
|
@@ -2,6 +2,10 @@ require_relative "value/invariant_violation"
|
|
|
2
2
|
require_relative "../vocabulary"
|
|
3
3
|
|
|
4
4
|
module Hecks
|
|
5
|
+
# What runs a booted domain: dispatch, the command/entity/query/policy/saga
|
|
6
|
+
# interpreters, the registry a boot assembles, and the errors below —
|
|
7
|
+
# everything downstream of a `.bluebook`/`.hecksagon`/`.world` declaration.
|
|
8
|
+
# See `lib/hecks/runtime.rb` for the module's own facade and boot entry points.
|
|
5
9
|
module Runtime
|
|
6
10
|
class UnknownVerb < StandardError; end
|
|
7
11
|
class EnsuresNotMet < StandardError; end
|
|
@@ -21,11 +25,21 @@ module Hecks
|
|
|
21
25
|
class GivenNotMet < StandardError
|
|
22
26
|
attr_reader :detail
|
|
23
27
|
|
|
28
|
+
# @param message [String, nil] the refusal text, pinned byte-for-byte across specs
|
|
29
|
+
# @param detail [String, nil] the failing comparison's resolved operands ("left: X,
|
|
30
|
+
# right: Y"), or nil when the given's top-level shape is not a bare comparison
|
|
24
31
|
def initialize(message = nil, detail: nil)
|
|
25
32
|
super(message)
|
|
26
33
|
@detail = detail
|
|
27
34
|
end
|
|
28
35
|
|
|
36
|
+
# Renders `message` with `detail` appended, for a human reading an unhandled refusal.
|
|
37
|
+
#
|
|
38
|
+
# @param highlight [Boolean] unused; accepted for compatibility with `Exception#
|
|
39
|
+
# detailed_message`'s own signature
|
|
40
|
+
# @param opts [Hash] unused; accepted for compatibility with `Exception#
|
|
41
|
+
# detailed_message`'s own signature
|
|
42
|
+
# @return [String] `message`, with `" (#{detail})"` appended when `detail` is present
|
|
29
43
|
def detailed_message(highlight: false, **opts)
|
|
30
44
|
base = super
|
|
31
45
|
detail ? "#{base} (#{detail})" : base
|
|
@@ -119,10 +133,10 @@ module Hecks
|
|
|
119
133
|
#
|
|
120
134
|
# Everything else is a defect : a NoMethodError in an interpreter, a
|
|
121
135
|
# NameError from a missing constant, a TypeError from a bad assumption. A
|
|
122
|
-
# blanket `rescue StandardError`
|
|
123
|
-
# `delivered: false, reason: "..."` —
|
|
124
|
-
# indistinguishable from a rule doing its job,
|
|
125
|
-
# in the log.
|
|
136
|
+
# blanket `rescue StandardError` would fold both into one line —
|
|
137
|
+
# `delivered: false, reason: "..."` — making a crash in the runtime
|
|
138
|
+
# indistinguishable from a rule doing its job, reading as normal
|
|
139
|
+
# operation in the log.
|
|
126
140
|
#
|
|
127
141
|
# UnknownVerb is one of these, and deliberately : a cross-domain policy
|
|
128
142
|
# (`across "Notifications"`) fires in deployments where that domain is not
|
data/lib/hecks/runtime/event.rb
CHANGED
|
@@ -11,20 +11,25 @@ module Hecks
|
|
|
11
11
|
# one correlating on a different field. Absent for any event no saga
|
|
12
12
|
# dispatch caused, which is most of them.
|
|
13
13
|
Event = Struct.new(:name, :aggregate, :id, :payload, :occurred_at, :correlation, keyword_init: true) do
|
|
14
|
+
# Freezes the event deep, so nothing about it can change after it happens.
|
|
15
|
+
#
|
|
14
16
|
# An emitted event is a record of something that happened, and a
|
|
15
17
|
# mutable audit trail is not one. The payload — the domain fact the
|
|
16
18
|
# event carries — is frozen through on emission: freezing the Hash
|
|
17
19
|
# alone would leave every value in it editable in place, which is
|
|
18
20
|
# the shape all four previous freezing bugs had.
|
|
19
21
|
#
|
|
20
|
-
# The whole event, not just its payload. Correlation
|
|
21
|
-
# merged
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
22
|
+
# The whole event, not just its payload. Correlation is set at
|
|
23
|
+
# construction rather than merged in here by `Dispatcher#dispatch`
|
|
24
|
+
# after the event already exists, because it is part of the
|
|
25
|
+
# transaction and known from `dispatch`'s own argument before
|
|
26
|
+
# anything is emitted — that is what keeps an event immutable once
|
|
27
|
+
# it exists.
|
|
25
28
|
#
|
|
26
29
|
# The log stays appendable: new events are still recorded. It is
|
|
27
30
|
# each event that stops changing once it exists.
|
|
31
|
+
#
|
|
32
|
+
# @return [void]
|
|
28
33
|
def emit!
|
|
29
34
|
Freezer.deep(payload)
|
|
30
35
|
Freezer.deep(correlation)
|
|
@@ -23,19 +23,38 @@ module Hecks
|
|
|
23
23
|
module Identity
|
|
24
24
|
module_function
|
|
25
25
|
|
|
26
|
+
# Reads one key off `hash` by whichever of its Symbol/String spelling
|
|
27
|
+
# is actually present, favoring the Symbol spelling.
|
|
28
|
+
#
|
|
26
29
|
# A hash read that decides which spelling of a key answers by
|
|
27
30
|
# presence, never by `||` — a bare `||` treats a genuinely-held
|
|
28
31
|
# `false` the same as an absent key and falls through to the other
|
|
29
32
|
# spelling, landing on `nil` instead of the real, stored answer.
|
|
33
|
+
#
|
|
34
|
+
# @param hash [Hash] the hash to read, potentially keyed by either spelling
|
|
35
|
+
# @param key [String, Symbol] the key to look up
|
|
36
|
+
# @return [Object, nil] the value under `key.to_sym` if that key is present,
|
|
37
|
+
# otherwise the value under `key` as given; nil if neither is a key of `hash`
|
|
30
38
|
def hash_lookup(hash, key)
|
|
31
39
|
sym = key.to_sym
|
|
32
40
|
hash.key?(sym) ? hash[sym] : hash[key]
|
|
33
41
|
end
|
|
34
42
|
|
|
43
|
+
# Digs `path`'s fields out of `held`, past the head a caller already
|
|
44
|
+
# consumed to look `held` up.
|
|
45
|
+
#
|
|
35
46
|
# The head names the attribute and is consumed by whoever looked the value
|
|
36
47
|
# up; what is left is the walk down into it. A path with no fields to walk
|
|
37
48
|
# — an aggregate that declares no identity and falls back to `id` — hands
|
|
38
49
|
# back what it was given, because there is nothing declared to dig for.
|
|
50
|
+
#
|
|
51
|
+
# @param path [String, Symbol] the dotted identity path (`"number.value"`) or a
|
|
52
|
+
# bare head (`:id`); only the segments after the head are walked
|
|
53
|
+
# @param held [Object] the value already looked up for the path's head — a
|
|
54
|
+
# `Runtime::Value`, a Hash, or a plain scalar
|
|
55
|
+
# @return [Object, nil] `held` unchanged when `path` has no fields past its head;
|
|
56
|
+
# otherwise the value found by walking those fields, or nil when a segment
|
|
57
|
+
# is missing or the value being dug into is not a Hash
|
|
39
58
|
def scalar(path, held)
|
|
40
59
|
_head, *fields = path.to_s.split(".")
|
|
41
60
|
return held if fields.empty?
|
|
@@ -45,6 +64,9 @@ module Hecks
|
|
|
45
64
|
end
|
|
46
65
|
end
|
|
47
66
|
|
|
67
|
+
# Derives `construct`'s identity string from `args`, joining every
|
|
68
|
+
# declared identity part in declaration order.
|
|
69
|
+
#
|
|
48
70
|
# The identity is the join of its parts, in declaration order. Shared by
|
|
49
71
|
# `CommandInterpreter` (an aggregate acting on itself) and
|
|
50
72
|
# `EntityInterpreter` (a piece addressed through its aggregate) — a piece
|
|
@@ -59,23 +81,49 @@ module Hecks
|
|
|
59
81
|
# rather than half of one. Half an identity names nothing, and joining what
|
|
60
82
|
# did arrive would silently name a different record on every dispatch — the
|
|
61
83
|
# precise failure that minting an id caused, arrived at by another road.
|
|
84
|
+
#
|
|
85
|
+
# @param construct [Bluebook::Aggregate, Bluebook::Entity] the construct whose
|
|
86
|
+
# `identity_paths` are resolved
|
|
87
|
+
# @param args [Hash{Symbol => Object}] the offered payload to resolve each
|
|
88
|
+
# identity path against
|
|
89
|
+
# @param value_owner [Bluebook::Aggregate, Bluebook::Entity] the construct whose
|
|
90
|
+
# namespace a value-object identity part is coerced against; `construct` itself
|
|
91
|
+
# unless the caller passes the owning aggregate for an entity
|
|
92
|
+
# @return [String, nil] the joined identity string, or nil when `construct`
|
|
93
|
+
# declares no identity path or any resolved part is nil or blank
|
|
62
94
|
def of(construct, args, value_owner: construct)
|
|
63
95
|
paths = construct.identity_paths
|
|
64
96
|
return nil if paths.empty?
|
|
65
97
|
|
|
66
98
|
parts = paths.map { |path| from(construct, args, path, value_owner: value_owner) }
|
|
67
99
|
# A blank part names nothing, the same as an absent one — an ID is a
|
|
68
|
-
# scalar, and "" is not a fact about anything.
|
|
69
|
-
#
|
|
70
|
-
# source did not survive extraction)
|
|
100
|
+
# scalar, and "" is not a fact about anything. Checking only `nil?`
|
|
101
|
+
# would let a canonical text extracted as "" (an expression whose
|
|
102
|
+
# source did not survive extraction) resolve to a real, empty-string
|
|
71
103
|
# identity — a record addressable by an id no caller could have meant.
|
|
72
104
|
return nil if parts.any? { |part| part.nil? || (part.respond_to?(:empty?) && part.empty?) }
|
|
73
105
|
|
|
74
106
|
Naming.identity(parts)
|
|
75
107
|
end
|
|
76
108
|
|
|
109
|
+
# Resolves one identity path (or a bare head such as `:id`) against `args`.
|
|
110
|
+
#
|
|
77
111
|
# A path digs into the value object that carries the identity, so what is
|
|
78
112
|
# stored is the scalar inside it rather than the object serialised whole.
|
|
113
|
+
#
|
|
114
|
+
# @param construct [Bluebook::Aggregate, Bluebook::Entity] the construct `key` is
|
|
115
|
+
# checked against when it is a bare head
|
|
116
|
+
# @param args [Hash{Symbol => Object}] the offered payload to resolve `key` against
|
|
117
|
+
# @param key [String, Symbol, nil] the identity path to resolve, dotted
|
|
118
|
+
# (`"number.value"`) or bare (`:id`); nil resolves to nil
|
|
119
|
+
# @param value_owner [Bluebook::Aggregate, Bluebook::Entity] the construct whose
|
|
120
|
+
# namespace a value-object head is coerced against; `construct` itself unless
|
|
121
|
+
# the caller passes the owning aggregate for an entity
|
|
122
|
+
# @return [String, Object, nil] the resolved identity text — a String once dug
|
|
123
|
+
# through a dotted path or coerced through a declared value-object attribute;
|
|
124
|
+
# the raw `args[key]` value, unconverted, when the bare head names no declared
|
|
125
|
+
# identity attribute of `construct`; nil when `key` is nil, absent from `args`,
|
|
126
|
+
# or a dotted walk finds nothing
|
|
79
127
|
def from(construct, args, key, value_owner: construct)
|
|
80
128
|
return nil unless key
|
|
81
129
|
|
|
@@ -127,12 +175,21 @@ module Hecks
|
|
|
127
175
|
Value.materialize_unwrapped(Value.for_attribute(value_owner, attribute, raw)).to_s
|
|
128
176
|
end
|
|
129
177
|
|
|
178
|
+
# Renders `construct`'s identity paths for a refusal message.
|
|
179
|
+
#
|
|
130
180
|
# How an identity reads when the runtime has to name it in a refusal — the
|
|
131
181
|
# paths as they were declared, so the message quotes the bluebook back.
|
|
182
|
+
#
|
|
183
|
+
# @param construct [Bluebook::Aggregate, Bluebook::Entity] the construct whose
|
|
184
|
+
# declared identity paths are rendered
|
|
185
|
+
# @return [String] `construct`'s identity paths, comma-separated, exactly as declared
|
|
132
186
|
def reading(construct)
|
|
133
187
|
construct.identity_paths.join(", ")
|
|
134
188
|
end
|
|
135
189
|
|
|
190
|
+
# Resolves the best identity string available for `construct` from `args`,
|
|
191
|
+
# without raising, for use as a lock key only.
|
|
192
|
+
#
|
|
136
193
|
# Best-effort, for a lock key only — `Runtime::AggregateLock`'s own
|
|
137
194
|
# per-record striping needs some id to key on before dispatch has run
|
|
138
195
|
# far enough to hydrate for real, so this walks the identical chain
|
|
@@ -143,6 +200,17 @@ module Hecks
|
|
|
143
200
|
# from the raw, pre-normalized payload this runs against" — the
|
|
144
201
|
# caller locks by aggregate type alone in that case (coarser, still
|
|
145
202
|
# correct, just less concurrent).
|
|
203
|
+
#
|
|
204
|
+
# @param construct [Bluebook::Aggregate, Bluebook::Entity] the construct being
|
|
205
|
+
# located for the lock key
|
|
206
|
+
# @param args [Hash{Symbol => Object}] the raw, pre-normalized payload to resolve
|
|
207
|
+
# an identity from
|
|
208
|
+
# @param route [Runtime::Routing::Envelope, nil] the call's resolved routing
|
|
209
|
+
# envelope, if any; its own `aggregate` identity is tried first
|
|
210
|
+
# @param reference_key [Symbol, nil] the command's reference-key fallback,
|
|
211
|
+
# tried last if given
|
|
212
|
+
# @return [String, Object, nil] the best-effort identity to lock by, or nil
|
|
213
|
+
# when nothing resolves (the caller then locks by aggregate type alone)
|
|
146
214
|
def best_effort(construct, args, route = nil, reference_key: nil)
|
|
147
215
|
route&.aggregate ||
|
|
148
216
|
of(construct, args) ||
|