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
|
@@ -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) ||
|
|
@@ -31,20 +31,34 @@ module Hecks
|
|
|
31
31
|
# holding it when they mint a brand-new record). See
|
|
32
32
|
# `materialize_identity!` for why a composite identity needs it.
|
|
33
33
|
#
|
|
34
|
-
# `hydrate:` —
|
|
34
|
+
# `hydrate:` — on by default, and every existing caller keeps getting
|
|
35
35
|
# exactly what it always got: `state` re-walked through
|
|
36
36
|
# `hydrate_with_defaults` (declared defaults filled, every attribute
|
|
37
37
|
# re-coerced through `Value.for_attribute`, an entity list's every
|
|
38
38
|
# element rebuilt and re-validated). `false` is for exactly one
|
|
39
39
|
# caller (`Adapters::Memory#build_instance`, judge-bootstrapping
|
|
40
40
|
# only — see its own header) that already knows `state` needs none
|
|
41
|
-
# of that: it is a shallow dup of an
|
|
41
|
+
# of that: it is a shallow dup of an already-hydrated, already-
|
|
42
42
|
# validated live `Instance`'s own state, not a raw value pulled off
|
|
43
43
|
# a wire. Skipping the re-walk is what turns a `list_of` entity's Nth
|
|
44
44
|
# save from O(N) (re-hydrating every element saved so far, for every
|
|
45
45
|
# save) into O(1) — the quadratic cost `Adapters::Memory`'s own
|
|
46
46
|
# header traces start to finish. `CodecBoundary.check_state!` still
|
|
47
47
|
# runs either way ; only the re-hydration is skipped.
|
|
48
|
+
#
|
|
49
|
+
# @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct this
|
|
50
|
+
# record's state is declared by
|
|
51
|
+
# @param id [String, nil] this record's identity; nil for a record whose identity
|
|
52
|
+
# is not yet resolvable (e.g. an entity element hydrated before its own view exists)
|
|
53
|
+
# @param state [Hash{Symbol => Object}, nil] the record's stored (or partial)
|
|
54
|
+
# attribute values; nil for a brand-new record, hydrated entirely from declared
|
|
55
|
+
# defaults
|
|
56
|
+
# @param args [Hash{Symbol => Object}, nil] the original command payload, offered
|
|
57
|
+
# only by a fresh creation; materializes a composite identity's own head
|
|
58
|
+
# attributes
|
|
59
|
+
# @param hydrate [Boolean] whether `state` is re-walked through
|
|
60
|
+
# `hydrate_with_defaults`; false only for a caller that already knows `state` is
|
|
61
|
+
# already hydrated and validated
|
|
48
62
|
def initialize(aggregate:, id:, state: nil, args: nil, hydrate: true)
|
|
49
63
|
@aggregate = aggregate
|
|
50
64
|
@id = id
|
|
@@ -68,6 +82,12 @@ module Hecks
|
|
|
68
82
|
# written — arrives filled instead of nil. Only declared defaults
|
|
69
83
|
# fill in; an attribute with no default stays absent, exactly as
|
|
70
84
|
# stored.
|
|
85
|
+
#
|
|
86
|
+
# @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct whose
|
|
87
|
+
# declared attributes and defaults `state` is hydrated against
|
|
88
|
+
# @param state [Hash{Symbol => Object}] the raw stored (or partial) attribute values
|
|
89
|
+
# @return [Hash{Symbol => Object}] `state` coerced through every declared attribute,
|
|
90
|
+
# with any missing declared-default attribute filled in
|
|
71
91
|
def self.hydrate_with_defaults(aggregate, state)
|
|
72
92
|
hydrated = Value.hydrate(aggregate, state)
|
|
73
93
|
defaults(aggregate).each do |name, value|
|
|
@@ -76,6 +96,13 @@ module Hecks
|
|
|
76
96
|
hydrated
|
|
77
97
|
end
|
|
78
98
|
|
|
99
|
+
# Builds a fresh record's starting state: every declared attribute's
|
|
100
|
+
# default value, an empty frozen Array for a `list_of` attribute, and
|
|
101
|
+
# the lifecycle field's declared starting value when `aggregate` has one.
|
|
102
|
+
#
|
|
103
|
+
# @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct whose
|
|
104
|
+
# declared attributes and lifecycle are read
|
|
105
|
+
# @return [Hash{Symbol => Object}] the default state, keyed by attribute name
|
|
79
106
|
def self.defaults(aggregate)
|
|
80
107
|
state = aggregate.attributes.to_h do |attr|
|
|
81
108
|
# Frozen, like a list that has had something appended to it.
|
|
@@ -88,6 +115,16 @@ module Hecks
|
|
|
88
115
|
state
|
|
89
116
|
end
|
|
90
117
|
|
|
118
|
+
# Resolves one attribute's default value: its own declared `default:`
|
|
119
|
+
# if it has one, otherwise a value object built entirely from its own
|
|
120
|
+
# members' defaults, when every member declares one.
|
|
121
|
+
#
|
|
122
|
+
# @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct
|
|
123
|
+
# `attribute` belongs to
|
|
124
|
+
# @param attribute [Bluebook::Attribute] the attribute to resolve a default for
|
|
125
|
+
# @return [Runtime::Value, Object, nil] the coerced default value; nil when
|
|
126
|
+
# `attribute` declares no default and either names no value object or names
|
|
127
|
+
# one with a member that itself declares no default
|
|
91
128
|
def self.default_for(aggregate, attribute)
|
|
92
129
|
return Value.for_attribute(aggregate, attribute, attribute.default) unless attribute.default.nil?
|
|
93
130
|
# An entity's members hydrate through the same path but an entity
|
|
@@ -100,10 +137,24 @@ module Hecks
|
|
|
100
137
|
Value.build(value_object, {}, aggregate)
|
|
101
138
|
end
|
|
102
139
|
|
|
140
|
+
# Reads one state field by name.
|
|
141
|
+
#
|
|
142
|
+
# @param name [String, Symbol] the declared attribute name to read
|
|
143
|
+
# @return [Object, nil] the field's current value; nil if `name` is not a key
|
|
144
|
+
# of `state`
|
|
103
145
|
def [](name) = @state[name.to_sym]
|
|
104
146
|
|
|
147
|
+
# Reports whether `state` holds a value for `name`.
|
|
148
|
+
#
|
|
149
|
+
# @param name [String, Symbol] the declared attribute name to check
|
|
150
|
+
# @return [Boolean] true if `state` has a key for `name`
|
|
105
151
|
def key?(name) = @state.key?(name.to_sym)
|
|
106
152
|
|
|
153
|
+
# Writes one state field by name.
|
|
154
|
+
#
|
|
155
|
+
# @param name [String, Symbol] the declared attribute name to write
|
|
156
|
+
# @param value [Object] the value to store
|
|
157
|
+
# @return [Object] `value`, unchanged
|
|
107
158
|
def []=(name, value)
|
|
108
159
|
@state[name.to_sym] = value
|
|
109
160
|
end
|
|
@@ -118,6 +169,8 @@ module Hecks
|
|
|
118
169
|
@state.key?(name) || super
|
|
119
170
|
end
|
|
120
171
|
|
|
172
|
+
# Renders this record's wire/storage shape, state plus its identity.
|
|
173
|
+
#
|
|
121
174
|
# `id: @id` last, not first — see Facade::Handle#to_h's own comment
|
|
122
175
|
# for the full story (the same fix, landed there first): an
|
|
123
176
|
# aggregate free to declare its own attribute literally named `id`
|
|
@@ -125,8 +178,13 @@ module Hecks
|
|
|
125
178
|
# attribute's own wrapped value object sitting in `@state[:id]` —
|
|
126
179
|
# merging `@state` on top of `{ id: @id }` let it silently clobber
|
|
127
180
|
# the correct bare identity. `@id` merged last always wins.
|
|
181
|
+
#
|
|
182
|
+
# @return [Hash{Symbol => Object}] `state`, with `id` merged in last
|
|
128
183
|
def to_h = @state.merge(id: @id)
|
|
129
184
|
|
|
185
|
+
# Copies this record, deep enough that a mutation on the copy cannot
|
|
186
|
+
# reach the original's own state.
|
|
187
|
+
#
|
|
130
188
|
# A copy a mutation may touch. Every adapter but Memory hands `find`
|
|
131
189
|
# a freshly-decoded Instance already; Memory's holds the record it
|
|
132
190
|
# eventually saves — the same state Hash, aliased. Before `ensures`
|
|
@@ -139,6 +197,8 @@ module Hecks
|
|
|
139
197
|
# record through this, never through the adapter's own return value
|
|
140
198
|
# directly, so a refused ensures leaves the stored record untouched
|
|
141
199
|
# regardless of which adapter is holding it.
|
|
200
|
+
#
|
|
201
|
+
# @return [Runtime::Instance] a copy of this record, with its own state Hash
|
|
142
202
|
def dup
|
|
143
203
|
copy = super
|
|
144
204
|
copy.state = @state.dup
|
|
@@ -157,11 +217,11 @@ module Hecks
|
|
|
157
217
|
# `identified_by` to fall back to `:id` for — `@aggregate.identified_by`
|
|
158
218
|
# is nil the moment there is more than one head (`Behaviour::Identified
|
|
159
219
|
# #derive_identity`), so the single-head branch below never runs for
|
|
160
|
-
# it at all.
|
|
161
|
-
# attributes but doesn't also `sets` them (redundant with
|
|
162
|
-
# the command's own args already named)
|
|
163
|
-
# nil — the id correctly
|
|
164
|
-
# attributes
|
|
220
|
+
# it at all. Without this, a creating command that declares those heads
|
|
221
|
+
# as ordinary attributes but doesn't also `sets` them (redundant with
|
|
222
|
+
# the identity the command's own args already named) would persist
|
|
223
|
+
# every head as nil — the id correctly naming the record, but the
|
|
224
|
+
# record's own attributes forgetting what named it.
|
|
165
225
|
#
|
|
166
226
|
# Filled from `args`, never from splitting `@id` back apart — the
|
|
167
227
|
# same reason the single-head branch below won't guess a multi-path
|
|
@@ -8,11 +8,19 @@ module Hecks
|
|
|
8
8
|
# in CommandInterpreter and EntityInterpreter, where they could only
|
|
9
9
|
# ever drift.
|
|
10
10
|
module Interpreting
|
|
11
|
+
# Ruby's module-inclusion hook. Gives `interpreter` its own `trace`
|
|
12
|
+
# accessor, since a class including this module needs its own copy
|
|
13
|
+
# rather than one shared across every interpreter.
|
|
14
|
+
#
|
|
11
15
|
# Each including interpreter gets its own `trace` — set by a spec to
|
|
12
16
|
# observe dispatch order (Vocabulary::AggregateDispatchOrder and
|
|
13
17
|
# Vocabulary::EntityDispatchOrder in language/bluebook/vocabulary.bluebook);
|
|
14
18
|
# nil in production, always — one array push and a nil check per step
|
|
15
19
|
# is the entire cost of leaving this in.
|
|
20
|
+
#
|
|
21
|
+
# @param interpreter [Class] the class (`CommandInterpreter`, `EntityInterpreter`)
|
|
22
|
+
# including this module
|
|
23
|
+
# @return [void]
|
|
16
24
|
def self.included(interpreter)
|
|
17
25
|
interpreter.singleton_class.attr_accessor :trace
|
|
18
26
|
end
|
|
@@ -32,9 +40,9 @@ module Hecks
|
|
|
32
40
|
# read off its own generated Vocabulary::*DispatchOrder table —
|
|
33
41
|
# vocabulary.bluebook, via lib/hecks/vocabulary.rb) by `send`ing
|
|
34
42
|
# each declared step name against the including interpreter's own
|
|
35
|
-
# `step_<name>` handler, in declared order.
|
|
36
|
-
#
|
|
37
|
-
# tracing a real dispatch and comparing it to the declaration is
|
|
43
|
+
# `step_<name>` handler, in declared order. Data drives that sequence,
|
|
44
|
+
# rather than `call` spelling it out as a literal sequence of method
|
|
45
|
+
# calls — tracing a real dispatch and comparing it to the declaration is
|
|
38
46
|
# tautological once `call` mechanically follows the declaration; a
|
|
39
47
|
# conditional step (assign_creation_attributes, advance_lifecycle) still
|
|
40
48
|
# has to guard itself at the top of its own handler and skip tracing
|
|
@@ -116,8 +124,8 @@ module Hecks
|
|
|
116
124
|
# refuse_absent_arguments are separate DISPATCH_ORDER steps now (the
|
|
117
125
|
# declared vocabulary lists all three as flat, sequential members, not
|
|
118
126
|
# one nesting the other two), and EntityInterpreter never had them here
|
|
119
|
-
# at all (an entity inherits its aggregate's own gate). One
|
|
120
|
-
#
|
|
127
|
+
# at all (an entity inherits its aggregate's own gate). One shared
|
|
128
|
+
# copy, rather than two identical ones that can only ever drift apart.
|
|
121
129
|
def normalize_args(aggregate, command, args)
|
|
122
130
|
coerce_declared_arguments(aggregate, command, args)
|
|
123
131
|
end
|
|
@@ -9,7 +9,8 @@ module Hecks
|
|
|
9
9
|
# (`target`, a `Routing::Envelope` or nil), and every fact the caller did
|
|
10
10
|
# or did not offer. Built once per dispatch by `Invocation.from_call`, the
|
|
11
11
|
# only place the runtime interprets the shape of a call (`to:` vs `with:`
|
|
12
|
-
# vs
|
|
12
|
+
# vs the flat facts hash `dispatch_flat` carries, a port operation's
|
|
13
|
+
# reference attribute
|
|
13
14
|
# lifted into `to:`). `Runtime::Routing.envelope`/`.payload` delegate here
|
|
14
15
|
# and no longer hold that logic themselves.
|
|
15
16
|
#
|
|
@@ -21,12 +22,12 @@ module Hecks
|
|
|
21
22
|
# - `Invocation::Present` — the key was offered with a value
|
|
22
23
|
#
|
|
23
24
|
# Keys are kept exactly as offered: `with:` keys are symbolized (as they
|
|
24
|
-
# always were),
|
|
25
|
-
#
|
|
25
|
+
# always were), a flat facts hash keeps whatever key the wire used. Undeclared keys a caller
|
|
26
|
+
# offered are kept too — refusing them is
|
|
26
27
|
# still `refuse_unknown_arguments`' job, a dispatch step, not this one.
|
|
27
28
|
#
|
|
28
|
-
#
|
|
29
|
-
# which is `#to_args` — the same Hash `Routing.payload`
|
|
29
|
+
# I1 changes no behavior: every interpreter still reads `ctx.args`,
|
|
30
|
+
# which is `#to_args` — the same Hash `Routing.payload` returns.
|
|
30
31
|
# Roadmap I2 moves the Ruby `decode_arguments` step onto `facts` itself.
|
|
31
32
|
Invocation = Data.define(:verb, :target, :facts)
|
|
32
33
|
|
|
@@ -41,6 +42,7 @@ module Hecks
|
|
|
41
42
|
# The class of the two frozen marker singletons below — never
|
|
42
43
|
# instantiated anywhere else.
|
|
43
44
|
class Marker
|
|
45
|
+
# @param name [String] the marker's own label, used by #inspect/#to_s
|
|
44
46
|
def initialize(name)
|
|
45
47
|
@name = name
|
|
46
48
|
freeze
|
|
@@ -54,22 +56,51 @@ module Hecks
|
|
|
54
56
|
Absent = Marker.new("Absent")
|
|
55
57
|
Null = Marker.new("Null")
|
|
56
58
|
|
|
59
|
+
# @param verb [String] the fully qualified verb this invocation dispatches
|
|
60
|
+
# @param target [Routing::Envelope, nil] the resolved receiver, or nil when
|
|
61
|
+
# the facts carry the identity themselves
|
|
62
|
+
# @param facts [Hash{String, Symbol => Object}] every candidate fact, keyed by
|
|
63
|
+
# offered (or declared) name; frozen, deep-duped, before being stored
|
|
57
64
|
def initialize(verb:, target:, facts:)
|
|
58
65
|
super(verb: verb, target: target, facts: facts.dup.freeze)
|
|
59
66
|
end
|
|
60
67
|
|
|
68
|
+
# Reads the fact recorded under `name`.
|
|
69
|
+
#
|
|
61
70
|
# The fact recorded under `name` — `Absent` for a name never offered
|
|
62
71
|
# and never declared either.
|
|
72
|
+
#
|
|
73
|
+
# @param name [String, Symbol] the fact name to look up
|
|
74
|
+
# @return [Invocation::Present, Invocation::Null, Invocation::Absent] the
|
|
75
|
+
# recorded marker for `name`
|
|
63
76
|
def fact(name) = facts.fetch(name, Absent)
|
|
64
77
|
|
|
78
|
+
# Reports whether `name` was offered with a real, non-nil value.
|
|
79
|
+
#
|
|
80
|
+
# @param name [String, Symbol] the fact name to check
|
|
81
|
+
# @return [Boolean] true if `name` was offered with a non-nil value
|
|
65
82
|
def present?(name) = fact(name).is_a?(Present)
|
|
83
|
+
|
|
84
|
+
# Reports whether `name` was offered with an explicit nil.
|
|
85
|
+
#
|
|
86
|
+
# @param name [String, Symbol] the fact name to check
|
|
87
|
+
# @return [Boolean] true if `name` was offered with an explicit nil
|
|
66
88
|
def null?(name) = fact(name).equal?(Null)
|
|
89
|
+
|
|
90
|
+
# Reports whether `name` was never offered.
|
|
91
|
+
#
|
|
92
|
+
# @param name [String, Symbol] the fact name to check
|
|
93
|
+
# @return [Boolean] true if `name` was never offered (and, if declared, left out)
|
|
67
94
|
def absent?(name) = fact(name).equal?(Absent)
|
|
68
95
|
|
|
69
96
|
# A Present fact's value; nil for an explicit Null. Raises KeyError for
|
|
70
97
|
# an Absent fact — "never offered" has no value, and answering nil
|
|
71
98
|
# would re-conflate it with an explicit null, the very ambiguity this
|
|
72
99
|
# type exists to remove. Ask `absent?`/`present?` first.
|
|
100
|
+
#
|
|
101
|
+
# @param name [String, Symbol] the fact name to read
|
|
102
|
+
# @return [Object, nil] the offered value; nil for an explicit Null fact
|
|
103
|
+
# @raise [KeyError] if `name` names an Absent fact (never offered)
|
|
73
104
|
def value(name)
|
|
74
105
|
case (found = fact(name))
|
|
75
106
|
when Present then found.value
|
|
@@ -78,9 +109,12 @@ module Hecks
|
|
|
78
109
|
end
|
|
79
110
|
end
|
|
80
111
|
|
|
81
|
-
# The
|
|
112
|
+
# The flat args hash, byte for byte what `Routing.payload` returned
|
|
82
113
|
# before this type existed: offered keys in offered order, Absent keys
|
|
83
114
|
# omitted, Null keys mapped to nil. A fresh Hash every call.
|
|
115
|
+
#
|
|
116
|
+
# @return [Hash{String, Symbol => Object}] the offered facts by name; a fact
|
|
117
|
+
# offered as nil is kept as nil
|
|
84
118
|
def to_args
|
|
85
119
|
facts.each_with_object({}) do |(name, found), args|
|
|
86
120
|
next if found.equal?(Absent)
|
|
@@ -90,7 +124,7 @@ module Hecks
|
|
|
90
124
|
end
|
|
91
125
|
|
|
92
126
|
class << self
|
|
93
|
-
#
|
|
127
|
+
# The one reading of a call's shape. `receiver:` picks which of the
|
|
94
128
|
# three dispatch shapes this is, because each has always checked its
|
|
95
129
|
# parts in its own order and a malformed call's refusal depends on
|
|
96
130
|
# that order:
|
|
@@ -107,24 +141,57 @@ module Hecks
|
|
|
107
141
|
# the point in that order where the caller always resolved it.
|
|
108
142
|
# `aggregate:` is the owning aggregate construct, read for `:port`
|
|
109
143
|
# only.
|
|
110
|
-
|
|
144
|
+
#
|
|
145
|
+
# @param verb [String] the fully qualified verb being dispatched
|
|
146
|
+
# @param to [String, Hash, nil] a bare aggregate identity, or a Hash with
|
|
147
|
+
# `aggregate:` and `entity:`/`entities:`; nil for no explicit receiver
|
|
148
|
+
# @param with [Hash, nil] the command's facts, keyed by attribute name; nil
|
|
149
|
+
# when the caller offers `flat` instead
|
|
150
|
+
# @param flat [Hash] the command's facts as a flat args hash, used when `with`
|
|
151
|
+
# is nil; for `:port`, may also carry the receiver identity under a
|
|
152
|
+
# reference attribute's own name
|
|
153
|
+
# @param receiver [Symbol] which of the three dispatch shapes this is:
|
|
154
|
+
# `:aggregate`, `:entity` or `:port`
|
|
155
|
+
# @param entity_depth [Integer] the number of entity-hop identities `to:`
|
|
156
|
+
# must carry, for `:entity` only
|
|
157
|
+
# @param aggregate [Bluebook::Aggregate, nil] the owning aggregate construct,
|
|
158
|
+
# read for `:port` only
|
|
159
|
+
# @yieldreturn [Class] the declaring command or port-operation class (anything
|
|
160
|
+
# with `hecks_name` and `attributes`)
|
|
161
|
+
# @return [Runtime::Invocation] the built invocation
|
|
162
|
+
# @raise [Runtime::TypeMismatch] if `to:` is malformed, its entity count is
|
|
163
|
+
# wrong, or (`:port` only) the operation resolves no receiving aggregate
|
|
164
|
+
# @raise [Runtime::UnknownArgument] if `with:` names an attribute the command
|
|
165
|
+
# does not declare
|
|
166
|
+
# @raise [Runtime::AbsentArgument] if `with:` omits a required attribute
|
|
167
|
+
# @raise [ArgumentError] if `receiver` is none of `:aggregate`, `:entity` or `:port`
|
|
168
|
+
def from_call(verb, to:, with:, flat:, receiver: :aggregate, entity_depth: 0, aggregate: nil, &declaring)
|
|
111
169
|
case receiver
|
|
112
170
|
when :aggregate
|
|
113
171
|
command = declaring.call
|
|
114
|
-
facts = facts_for(command, with: with,
|
|
172
|
+
facts = facts_for(command, with: with, flat: flat)
|
|
115
173
|
new(verb: verb, target: route(to), facts: facts)
|
|
116
174
|
when :entity
|
|
117
175
|
target = route(to, entity_depth: entity_depth)
|
|
118
176
|
command = declaring.call
|
|
119
|
-
new(verb: verb, target: target, facts: facts_for(command, with: with,
|
|
177
|
+
new(verb: verb, target: target, facts: facts_for(command, with: with, flat: flat))
|
|
120
178
|
when :port
|
|
121
|
-
port_call(verb, aggregate, declaring.call, to: to, with: with,
|
|
179
|
+
port_call(verb, aggregate, declaring.call, to: to, with: with, flat: flat)
|
|
122
180
|
else
|
|
123
181
|
raise ArgumentError, "unknown receiver #{receiver.inspect}"
|
|
124
182
|
end
|
|
125
183
|
end
|
|
126
184
|
|
|
127
185
|
# `to:` as a `Routing::Envelope`, or nil when no `to:` was given.
|
|
186
|
+
#
|
|
187
|
+
# @param to [String, Hash, nil] a bare aggregate identity, or a Hash with
|
|
188
|
+
# `aggregate:` and `entity:`/`entities:`; nil for no receiver
|
|
189
|
+
# @param entity_depth [Integer] the number of entity-hop identities `to`
|
|
190
|
+
# must carry
|
|
191
|
+
# @return [Routing::Envelope, nil] the resolved envelope, or nil when `to`
|
|
192
|
+
# is nil
|
|
193
|
+
# @raise [Runtime::TypeMismatch] if `to` is malformed, or its entity count
|
|
194
|
+
# does not match `entity_depth`
|
|
128
195
|
def route(to, entity_depth: 0)
|
|
129
196
|
return nil if to.nil?
|
|
130
197
|
|
|
@@ -146,8 +213,23 @@ module Hecks
|
|
|
146
213
|
# The offered facts for `declaring`, as Absent/Null/Present — offered
|
|
147
214
|
# keys first, in offered order, then every declared attribute that was
|
|
148
215
|
# not offered, as Absent.
|
|
149
|
-
|
|
150
|
-
|
|
216
|
+
#
|
|
217
|
+
# @param declaring [Class] the command or port-operation class (anything
|
|
218
|
+
# with `hecks_name` and `attributes`) whose declared attributes are read
|
|
219
|
+
# @param with [Hash, nil] the command's facts, keyed by attribute name; nil
|
|
220
|
+
# when the caller offers `flat` instead
|
|
221
|
+
# @param flat [Hash] the command's facts as a flat args hash, used when `with`
|
|
222
|
+
# is nil
|
|
223
|
+
# @return [Hash{String, Symbol => Invocation::Absent, Invocation::Null,
|
|
224
|
+
# Invocation::Present}] each candidate fact keyed by its offered (or
|
|
225
|
+
# declared) name
|
|
226
|
+
# @raise [Runtime::TypeMismatch] if both `with` and a non-empty `flat` are
|
|
227
|
+
# given, or if `with` is not a Hash
|
|
228
|
+
# @raise [Runtime::UnknownArgument] if `with:` names an attribute `declaring`
|
|
229
|
+
# does not declare
|
|
230
|
+
# @raise [Runtime::AbsentArgument] if `with:` omits a required attribute
|
|
231
|
+
def facts_for(declaring, with:, flat:)
|
|
232
|
+
offered = offered_facts(declaring, with: with, flat: flat)
|
|
151
233
|
facts = offered.each_with_object({}) do |(name, value), found|
|
|
152
234
|
found[name] = value.nil? ? Null : Present.new(value: value)
|
|
153
235
|
end
|
|
@@ -160,10 +242,10 @@ module Hecks
|
|
|
160
242
|
|
|
161
243
|
private
|
|
162
244
|
|
|
163
|
-
# **The port operation shape** —
|
|
245
|
+
# **The port operation shape** — moved from `Dispatcher#port_invocation`.
|
|
164
246
|
#
|
|
165
247
|
# A Reference-typed attribute naming the owning aggregate is routing,
|
|
166
|
-
# not a fact: lifted out of the
|
|
248
|
+
# not a fact: lifted out of the flat facts into `to:` when no `to:`
|
|
167
249
|
# was given. A `to:`-declared operation carries no Reference
|
|
168
250
|
# attribute at all (PortOperationBuilder#initialize's own comment),
|
|
169
251
|
# so its receiver is read — not removed — from the plain attribute
|
|
@@ -173,30 +255,30 @@ module Hecks
|
|
|
173
255
|
# with attribute"; a real AbsentArgument confirmed this before `[]`
|
|
174
256
|
# replaced `delete`). Composite identity is not attempted — `.first`
|
|
175
257
|
# only, no domain in the corpus needs more for a port operation.
|
|
176
|
-
def port_call(verb, aggregate, operation, to:, with:,
|
|
177
|
-
|
|
258
|
+
def port_call(verb, aggregate, operation, to:, with:, flat:)
|
|
259
|
+
flat = flat.dup
|
|
178
260
|
identity = operation.identity_attribute(aggregate.hecks_name)
|
|
179
|
-
if to.nil? && identity &&
|
|
180
|
-
to =
|
|
261
|
+
if to.nil? && identity && flat.key?(identity.name)
|
|
262
|
+
to = flat.delete(identity.name)
|
|
181
263
|
elsif to.nil? && operation.to == aggregate.hecks_name
|
|
182
264
|
identity_name = Array(aggregate.identified_by).first
|
|
183
|
-
to =
|
|
265
|
+
to = flat[identity_name] if identity_name && flat.key?(identity_name)
|
|
184
266
|
end
|
|
185
267
|
|
|
186
268
|
target = route(to)
|
|
187
269
|
raise TypeMismatch, "#{operation.hecks_name} requires its receiving aggregate in to:" unless target
|
|
188
270
|
|
|
189
|
-
new(verb: verb, target: target, facts: facts_for(operation, with: with,
|
|
271
|
+
new(verb: verb, target: target, facts: facts_for(operation, with: with, flat: flat))
|
|
190
272
|
end
|
|
191
273
|
|
|
192
274
|
# BUG#7 — a non-Hash `to:` must be a String, matching Rust's
|
|
193
275
|
# `RoutingEnvelope::from_json` (kernel/routing.rs), which refuses
|
|
194
276
|
# anything neither a JSON string nor object before the domain payload
|
|
195
277
|
# is examined. Surfaced on `examples/roster`'s `Mark`, whose own
|
|
196
|
-
# attribute is literally named `to`: a flat-
|
|
197
|
-
# that key into this parameter, and an out-of-range
|
|
198
|
-
# accepted as the aggregate identity, leaving Mark's
|
|
199
|
-
# (AbsentArgument in Ruby, TypeMismatch in Rust). See
|
|
278
|
+
# attribute is literally named `to`: a flat-facts dispatch steals
|
|
279
|
+
# that key into this parameter, and without this check an out-of-range
|
|
280
|
+
# Integer would be accepted as the aggregate identity, leaving Mark's
|
|
281
|
+
# `to` fact absent (AbsentArgument in Ruby, TypeMismatch in Rust). See
|
|
200
282
|
# spec/runtime/routing_envelope_shape_spec.rb.
|
|
201
283
|
def scalar_envelope(to)
|
|
202
284
|
return [to, []] if to.is_a?(String)
|
|
@@ -214,11 +296,11 @@ module Hecks
|
|
|
214
296
|
|
|
215
297
|
# BUG#18 — an entity route naming no entity (`entities: []`, or
|
|
216
298
|
# neither key) refuses here, unconditionally, before entity_depth is
|
|
217
|
-
# consulted: for an aggregate-level command (depth 0)
|
|
218
|
-
#
|
|
219
|
-
# the command's own validation. Rust's `RoutingEnvelope::
|
|
220
|
-
# always refused it at this point. A bare aggregate identity
|
|
221
|
-
# remains the ordinary aggregate-command shape.
|
|
299
|
+
# consulted: for an aggregate-level command (depth 0), without this,
|
|
300
|
+
# `[].size == 0` would satisfy the depth check and let the degenerate
|
|
301
|
+
# Hash reach the command's own validation. Rust's `RoutingEnvelope::
|
|
302
|
+
# from_json` always refused it at this point. A bare aggregate identity
|
|
303
|
+
# String remains the ordinary aggregate-command shape.
|
|
222
304
|
def entity_identities(hash)
|
|
223
305
|
raise TypeMismatch, "to: takes entity: or entities:, not both" if hash.key?(:entities) && hash.key?(:entity)
|
|
224
306
|
|
|
@@ -230,16 +312,16 @@ module Hecks
|
|
|
230
312
|
|
|
231
313
|
# `with:` is deliberately strict: a caller choosing the explicit
|
|
232
314
|
# envelope cannot smuggle receiver identity back into the payload,
|
|
233
|
-
# and may not mix it with
|
|
234
|
-
# (nil or false) the
|
|
315
|
+
# and may not mix it with a flat facts hash. Without `with:`
|
|
316
|
+
# (nil or false) the flat facts are the facts, unread —
|
|
235
317
|
# whether `to:` was given never enters this decision (BUG#17).
|
|
236
|
-
def offered_facts(declaring, with:,
|
|
237
|
-
if with && !
|
|
318
|
+
def offered_facts(declaring, with:, flat:)
|
|
319
|
+
if with && !flat.empty?
|
|
238
320
|
raise TypeMismatch,
|
|
239
|
-
"dispatch takes command facts in with:, not both with: and
|
|
321
|
+
"dispatch takes command facts in with:, not both with: and a flat facts hash"
|
|
240
322
|
end
|
|
241
323
|
|
|
242
|
-
return
|
|
324
|
+
return flat unless with
|
|
243
325
|
raise TypeMismatch, "with: must be a hash of command facts" unless with.is_a?(Hash)
|
|
244
326
|
|
|
245
327
|
offered = with.transform_keys(&:to_sym)
|