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
|
@@ -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)
|
data/lib/hecks/runtime/loader.rb
CHANGED
|
@@ -37,6 +37,20 @@ module Hecks
|
|
|
37
37
|
# its own env var name and passes the resulting string straight
|
|
38
38
|
# through, e.g. `Hecks.boot(path, environment:
|
|
39
39
|
# ENV.fetch("MYAPP_ENV", "development"))`.
|
|
40
|
+
#
|
|
41
|
+
# @param path [String] a domain directory to boot, resolved through
|
|
42
|
+
# `Ports::Loading#bluebook_directory`
|
|
43
|
+
# @param shared [String, nil] an explicit shared ports/adapters root override; nil
|
|
44
|
+
# resolves it by walking up from the domain directory
|
|
45
|
+
# @param install_facade [Boolean] whether to install the `Widget::Item.Add(...)`
|
|
46
|
+
# facade sugar
|
|
47
|
+
# @param environment [String, nil] the environment overlay to load after the
|
|
48
|
+
# domain's own files (e.g. `"production"`); nil loads none
|
|
49
|
+
# @return [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted dispatcher
|
|
50
|
+
# @raise [Errno::ENOENT] if `path` names neither a domain directory nor one holding
|
|
51
|
+
# a `bluebook/` subdirectory
|
|
52
|
+
# @raise [Runtime::WiringError] if a boot gate finds a wiring problem (an undeclared
|
|
53
|
+
# bind, a compute/rekey rule with no persistence plugin loaded, …)
|
|
40
54
|
def self.boot(path, shared: nil, install_facade: true, environment: nil)
|
|
41
55
|
loading = Ports::Loading.bootstrap
|
|
42
56
|
directory = loading.bluebook_directory(path)
|
|
@@ -52,6 +66,7 @@ module Hecks
|
|
|
52
66
|
run_boot_gates!(registry, directory)
|
|
53
67
|
dispatcher = dispatcher_for(registry)
|
|
54
68
|
redrive_outbox!(dispatcher)
|
|
69
|
+
seed_privacy_markings!(dispatcher, registry)
|
|
55
70
|
install_facade ? bind_runtime(dispatcher) : dispatcher
|
|
56
71
|
end
|
|
57
72
|
|
|
@@ -63,12 +78,43 @@ module Hecks
|
|
|
63
78
|
# provably never started; `claimed` rows are surfaced, never
|
|
64
79
|
# auto-redriven — see `Runtime::Outbox`. A remote dispatcher has
|
|
65
80
|
# no local stores to scan.
|
|
81
|
+
#
|
|
82
|
+
# @param dispatcher [Runtime::Dispatcher, Runtime::RemoteDispatcher] the
|
|
83
|
+
# just-booted dispatcher; a no-op unless it exposes an `outbox`
|
|
84
|
+
# @return [void]
|
|
66
85
|
def self.redrive_outbox!(dispatcher)
|
|
67
86
|
return unless dispatcher.respond_to?(:outbox)
|
|
68
87
|
|
|
69
88
|
dispatcher.outbox.redrive!
|
|
70
89
|
end
|
|
71
90
|
|
|
91
|
+
# Turns every `.hecksagon`-declared `AggregateDoor#mark_sensitive` fact into a real
|
|
92
|
+
# `Privacy::Marking.Mark` — after the dispatcher exists (dispatch needs a bound
|
|
93
|
+
# repository), idempotent across reboots (a marking already present is never
|
|
94
|
+
# re-dispatched, the same "provably never started" restraint `redrive_outbox!`
|
|
95
|
+
# holds itself to above). A no-op when nothing declared one, or when the domain
|
|
96
|
+
# never attached Privacy at all.
|
|
97
|
+
#
|
|
98
|
+
# @param dispatcher [Runtime::Dispatcher, Runtime::RemoteDispatcher] the just-booted
|
|
99
|
+
# dispatcher `Privacy::Marking.Mark` dispatches through
|
|
100
|
+
# @param registry [Runtime::Registry] the just-booted registry `pending_privacy_markings`
|
|
101
|
+
# was recorded on
|
|
102
|
+
# @return [void]
|
|
103
|
+
def self.seed_privacy_markings!(dispatcher, registry)
|
|
104
|
+
return if registry.pending_privacy_markings.empty?
|
|
105
|
+
return unless registry.bluebook("Privacy")
|
|
106
|
+
|
|
107
|
+
already_marked_by_domain = registry.pending_privacy_markings.map { |marking| marking[:domain] }.uniq.to_h do |domain|
|
|
108
|
+
[domain, dispatcher.query("Privacy::Marking.ForDomain", domain: domain).map { |row| row[:attribute_path][:value] }]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
registry.pending_privacy_markings.each do |marking|
|
|
112
|
+
next if already_marked_by_domain[marking[:domain]].include?(marking[:attribute_path])
|
|
113
|
+
|
|
114
|
+
dispatcher.dispatch_flat("Privacy::Marking.Mark", marking)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
72
118
|
# The explicit-file form — `paths` names the exact bluebook/hecksagon/
|
|
73
119
|
# world files to boot, in place, wherever they actually live. `boot`
|
|
74
120
|
# above only ever takes a directory and globs it; that is the right
|
|
@@ -91,6 +137,18 @@ module Hecks
|
|
|
91
137
|
# first real path in `paths` — genuinely on disk, not a copy — so
|
|
92
138
|
# every downstream path (`EraCheck`, `persisted_by`, `shared_root`)
|
|
93
139
|
# resolves exactly as an ordinary directory boot's would.
|
|
140
|
+
#
|
|
141
|
+
# @param paths [Array<String>, String] the exact bluebook/hecksagon/world file paths
|
|
142
|
+
# to boot, in the order they should load
|
|
143
|
+
# @param shared [String, nil] an explicit shared ports/adapters root override; nil
|
|
144
|
+
# resolves it by walking up from `File.dirname` of the first path
|
|
145
|
+
# @param install_facade [Boolean] whether to install the `Widget::Item.Add(...)`
|
|
146
|
+
# facade sugar
|
|
147
|
+
# @param environment [String, nil] the environment overlay to load after the
|
|
148
|
+
# named files (e.g. `"production"`); nil loads none
|
|
149
|
+
# @return [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted dispatcher
|
|
150
|
+
# @raise [Runtime::WiringError] if a boot gate finds a wiring problem (an undeclared
|
|
151
|
+
# bind, a compute/rekey rule with no persistence plugin loaded, …)
|
|
94
152
|
def self.boot_files(paths, shared: nil, install_facade: true, environment: nil)
|
|
95
153
|
loading = Ports::Loading.bootstrap
|
|
96
154
|
files = Array(paths).map { |path| File.expand_path(path) }
|
|
@@ -109,9 +167,11 @@ module Hecks
|
|
|
109
167
|
install_facade ? bind_runtime(dispatcher) : dispatcher
|
|
110
168
|
end
|
|
111
169
|
|
|
112
|
-
#
|
|
113
|
-
#
|
|
114
|
-
#
|
|
170
|
+
# Runs every registered boot gate against `registry`, in order.
|
|
171
|
+
#
|
|
172
|
+
# ADR 0031 — a per-boot `BootGates` instance holds exactly the gates this
|
|
173
|
+
# registry's own bound adapters have a capability for, rather than two
|
|
174
|
+
# hardcoded, unconditional calls. Ordering is
|
|
115
175
|
# preserved: era-checking (when a persistence plugin contributes one)
|
|
116
176
|
# still runs before `verify!`, saga rehydration still runs after
|
|
117
177
|
# (conservative — see `SagaPersistence#rehydrate_sagas!`'s own
|
|
@@ -124,6 +184,13 @@ module Hecks
|
|
|
124
184
|
# `:post_verify` gates generically; `:saga_rehydration` is the one
|
|
125
185
|
# gate core still registers directly, because ADR 0031 already
|
|
126
186
|
# proved it's not era-specific.
|
|
187
|
+
#
|
|
188
|
+
# @param registry [Runtime::Registry] the registry mid-boot, with its bluebooks,
|
|
189
|
+
# hecksagons, ports, adapters and worlds already loaded
|
|
190
|
+
# @param directory [String] the domain directory the gates report against in a refusal
|
|
191
|
+
# @return [Runtime::BootGates] the gates instance built and run for this boot
|
|
192
|
+
# @raise [Runtime::WiringError] if `registry.verify!` or any contributed gate finds
|
|
193
|
+
# a wiring problem
|
|
127
194
|
def self.run_boot_gates!(registry, directory)
|
|
128
195
|
gates = BootGates.new
|
|
129
196
|
Ports::Persistence.each_plugin { |plugin| plugin.contribute_boot_gates(registry, gates) }
|
|
@@ -149,6 +216,12 @@ module Hecks
|
|
|
149
216
|
# the real, adapter-aware version of this check and refuses by name
|
|
150
217
|
# ("...is bound to Memory") long before this ever would; this only
|
|
151
218
|
# fires when nothing did, because nothing was loaded to.
|
|
219
|
+
#
|
|
220
|
+
# @param registry [Runtime::Registry] the registry mid-boot, whose declared
|
|
221
|
+
# `translations` are checked
|
|
222
|
+
# @return [void]
|
|
223
|
+
# @raise [Runtime::WiringError] if any translation declares a `computes`/`rekeys`
|
|
224
|
+
# rule while no persistence plugin is loaded to interpret it
|
|
152
225
|
def self.check_compute_rules_backstop!(registry)
|
|
153
226
|
return if Ports::Persistence.plugins_loaded?
|
|
154
227
|
|
|
@@ -183,6 +256,12 @@ module Hecks
|
|
|
183
256
|
# this landed. A domain opts in explicitly, the same way
|
|
184
257
|
# `persisted_by("PostgresEra")` is never inferred from anything
|
|
185
258
|
# else either.
|
|
259
|
+
#
|
|
260
|
+
# @param registry [Runtime::Registry] the just-booted registry, whose just-booted
|
|
261
|
+
# domain's `.world` is checked for `dispatched_by("Lambda")`
|
|
262
|
+
# @return [Runtime::Dispatcher, Runtime::RemoteDispatcher] a `RemoteDispatcher`
|
|
263
|
+
# bound to `registry` if the domain declares `dispatched_by("Lambda")`, a plain
|
|
264
|
+
# `Dispatcher` otherwise
|
|
186
265
|
def self.dispatcher_for(registry)
|
|
187
266
|
domain = registry.bluebooks.keys.first
|
|
188
267
|
settings = registry.world(domain)&.for_verb("dispatched_by") || {}
|
|
@@ -191,11 +270,18 @@ module Hecks
|
|
|
191
270
|
RemoteDispatcher.new(registry, region: settings.fetch(:region, "us-east-1"), function: settings[:function])
|
|
192
271
|
end
|
|
193
272
|
|
|
194
|
-
#
|
|
195
|
-
# dispatcher
|
|
196
|
-
#
|
|
197
|
-
#
|
|
198
|
-
# binding lives in the surface a boot
|
|
273
|
+
# Installs the facade sugar (`Widget::Item.Add(...)`), closed over
|
|
274
|
+
# `dispatcher`, and hands `dispatcher` back.
|
|
275
|
+
#
|
|
276
|
+
# The door is installed here, not stamped: the facade's modules close
|
|
277
|
+
# over this dispatcher, so the binding lives in the surface a boot
|
|
278
|
+
# installs rather than in a class-level global (`ruby_class.runtime =`
|
|
279
|
+
# on every aggregate's class) that would make two boots in one process
|
|
280
|
+
# share one name.
|
|
281
|
+
#
|
|
282
|
+
# @param dispatcher [Runtime::Dispatcher, Runtime::RemoteDispatcher] the
|
|
283
|
+
# just-booted dispatcher the facade should call into
|
|
284
|
+
# @return [Runtime::Dispatcher, Runtime::RemoteDispatcher] `dispatcher`, unchanged
|
|
199
285
|
def self.bind_runtime(dispatcher)
|
|
200
286
|
Facade::Surface.install(dispatcher)
|
|
201
287
|
dispatcher
|