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
|
@@ -17,25 +17,34 @@ module Hecks
|
|
|
17
17
|
# one of these is a pure function of its own explicit arguments
|
|
18
18
|
# (an assembled `Bluebook::Chapter`, an aggregate list, ...), never
|
|
19
19
|
# builder-instance state (no `@ivar` read anywhere in this
|
|
20
|
-
# module
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
20
|
+
# module), and every caller reaches these as
|
|
21
|
+
# `BluebookBuilder.validate_assembled!(...)` class-level calls,
|
|
22
|
+
# `#build`'s own `self.class.validate_assembled!` included.
|
|
23
|
+
# `extend` is `include`'s class-level counterpart: the same "one
|
|
24
|
+
# class, its methods, filed across files" pattern
|
|
25
25
|
# `Runtime::Registry` already uses (`include Verification`), just
|
|
26
26
|
# turning each method into a class (singleton) method instead of
|
|
27
|
-
# an instance method,
|
|
28
|
-
#
|
|
27
|
+
# an instance method, the same role `def self.foo` fills on a
|
|
28
|
+
# class that keeps its methods in one file.
|
|
29
29
|
module Validation
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
30
|
+
# Runs every whole-chapter check against one fully assembled chapter.
|
|
31
|
+
#
|
|
32
|
+
# **Every whole-chapter check, in one place** — a pure function of an
|
|
33
|
+
# assembled `Bluebook::Chapter`, callable both from `#build` (right
|
|
34
|
+
# after assembly) and from `MetaValidator.judge_deferred!` (once, on
|
|
35
|
+
# a chapter whose files have all loaded — see `#build`'s own comment
|
|
36
|
+
# for why that split exists at all). Public, not
|
|
37
|
+
# `private_class_method`'d, for exactly that second caller —
|
|
38
|
+
# `MetaValidator` needs to reach this with no builder instance in
|
|
39
|
+
# hand, only the chapter `judge_deferred!` already read back out of
|
|
40
|
+
# the registry.
|
|
41
|
+
#
|
|
42
|
+
# @param bluebook [Bluebook::Chapter] the fully assembled chapter to validate
|
|
43
|
+
# @return [void]
|
|
44
|
+
# @raise [Bluebook::DSL::Malformed] if any of the reference, event-shape, `with:`,
|
|
45
|
+
# query-hop, projected-field or provision checks below finds a violation
|
|
46
|
+
# @raise [Bluebook::DSL::ProcessManagerBuilder::InvalidProcessManager] if a process
|
|
47
|
+
# manager's `correlates_by` resolves to something other than a scalar field
|
|
39
48
|
def validate_assembled!(bluebook)
|
|
40
49
|
# moved to the language: an attribute type is a reference to its Shape,
|
|
41
50
|
# so an undeclared value object fails reference resolution
|
|
@@ -67,11 +76,20 @@ module Hecks
|
|
|
67
76
|
validate_provisions!(bluebook)
|
|
68
77
|
end
|
|
69
78
|
|
|
79
|
+
# Checks every `provides` row against `Capabilities::CONTRACTS`.
|
|
80
|
+
#
|
|
70
81
|
# **What a declared capability must name**. A `provides` row is only
|
|
71
82
|
# worth trusting in place of a name check if it is checked: an
|
|
72
83
|
# unknown capability, a missing or extra key, or a verb that is
|
|
73
84
|
# not this chapter's own command/query of the right kind would
|
|
74
85
|
# otherwise wire a role check to nothing, in silence.
|
|
86
|
+
#
|
|
87
|
+
# @param bluebook [Bluebook::Chapter] the chapter whose `provides` rows to check
|
|
88
|
+
# @return [void]
|
|
89
|
+
# @raise [Bluebook::DSL::Malformed] if a row names a capability
|
|
90
|
+
# `Capabilities::CONTRACTS` does not know, its keys do not match the
|
|
91
|
+
# capability's contract exactly, or a verb names no real command/query
|
|
92
|
+
# of the declared kind
|
|
75
93
|
def validate_provisions!(bluebook)
|
|
76
94
|
bluebook.provides.group_by(&:capability).each do |capability, rows|
|
|
77
95
|
contract = Capabilities::CONTRACTS.fetch(capability) do
|
|
@@ -89,6 +107,17 @@ module Hecks
|
|
|
89
107
|
end
|
|
90
108
|
end
|
|
91
109
|
|
|
110
|
+
# Checks that one `provides` row's verb names a real command or query.
|
|
111
|
+
#
|
|
112
|
+
# @param bluebook [Bluebook::Chapter] the chapter the row was declared on
|
|
113
|
+
# @param capability [String] the capability the row belongs to, such as
|
|
114
|
+
# `"authorization"`
|
|
115
|
+
# @param row [Bluebook::Chapter::Provision] the one declared row to check
|
|
116
|
+
# @param kind [Symbol] `:command` or `:query`, the kind of verb this row's key
|
|
117
|
+
# must name (from `Capabilities::CONTRACTS`)
|
|
118
|
+
# @return [void]
|
|
119
|
+
# @raise [Bluebook::DSL::Malformed] if `row.verb` does not split into a real
|
|
120
|
+
# aggregate and a member of the right kind that aggregate declares
|
|
92
121
|
def validate_provided_verb!(bluebook, capability, row, kind)
|
|
93
122
|
aggregate_name, member = row.verb.split(".", 2)
|
|
94
123
|
aggregate = bluebook.aggregate(aggregate_name)
|
|
@@ -98,15 +127,21 @@ module Hecks
|
|
|
98
127
|
"which names no #{kind} this chapter declares (spelled \"Aggregate.#{kind.capitalize}\")"
|
|
99
128
|
end
|
|
100
129
|
|
|
130
|
+
# Names of the members a `provides` verb of the given kind may address.
|
|
131
|
+
#
|
|
132
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate the verb is scoped to
|
|
133
|
+
# @param kind [Symbol] `:command` or `:query`
|
|
134
|
+
# @return [Array<String>] every command's `hecks_name` when `kind` is `:command`,
|
|
135
|
+
# else every query's declared `name`
|
|
101
136
|
def provided_member_names(aggregate, kind)
|
|
102
137
|
kind == :command ? aggregate.commands.map(&:hecks_name) : aggregate.queries.map(&:name)
|
|
103
138
|
end
|
|
104
139
|
|
|
105
|
-
#
|
|
140
|
+
# Refuses an entity command that names itself as its own root.
|
|
106
141
|
#
|
|
107
|
-
# That is the whole of what is left here, and it needs saying plainly
|
|
108
|
-
#
|
|
109
|
-
#
|
|
142
|
+
# That is the whole of what is left here, and it needs saying plainly:
|
|
143
|
+
# the refusal message once read "references must target aggregate
|
|
144
|
+
# heads," a claim broader than what this actually checks.
|
|
110
145
|
#
|
|
111
146
|
# `CommandBuilder#reference_to` sets `references` only when the target's
|
|
112
147
|
# bare name equals the owner's ; anything else becomes a reference
|
|
@@ -123,6 +158,13 @@ module Hecks
|
|
|
123
158
|
# Reference attributes are the language's business now — offered as the
|
|
124
159
|
# head's own id and resolved as references, so `Aggregate.Reference` and
|
|
125
160
|
# `Command.Reference` refuse an undeclared head with no predicate at all.
|
|
161
|
+
#
|
|
162
|
+
# @param aggregates [Array<Bluebook::Aggregate>] the chapter's declared aggregates
|
|
163
|
+
# @return [void]
|
|
164
|
+
# @raise [Bluebook::DSL::Malformed] if an entity command's `references` names
|
|
165
|
+
# something other than one of this chapter's own aggregate heads — which, given
|
|
166
|
+
# how `CommandBuilder#reference_to` sets it, only happens when the entity command
|
|
167
|
+
# names its own entity as its root
|
|
126
168
|
def validate_reference_value_objects!(aggregates)
|
|
127
169
|
heads = aggregates.map(&:hecks_name)
|
|
128
170
|
|
|
@@ -159,6 +201,11 @@ module Hecks
|
|
|
159
201
|
# claim about what the payload holds, so two emitting commands
|
|
160
202
|
# are free to differ there without actually disagreeing about
|
|
161
203
|
# the event's own shape.
|
|
204
|
+
#
|
|
205
|
+
# @param aggregates [Array<Bluebook::Aggregate>] the chapter's declared aggregates
|
|
206
|
+
# @return [void]
|
|
207
|
+
# @raise [Bluebook::DSL::Malformed] if two or more commands `emits` the same event
|
|
208
|
+
# name with different structural shapes
|
|
162
209
|
def validate_event_shapes!(aggregates)
|
|
163
210
|
event_emitters(aggregates).each do |event_name, pairs|
|
|
164
211
|
next if pairs.size == 1
|
|
@@ -196,6 +243,16 @@ module Hecks
|
|
|
196
243
|
# (does the dispatched command actually declare the field) still
|
|
197
244
|
# runs, since that half is true regardless of where the value
|
|
198
245
|
# came from.
|
|
246
|
+
#
|
|
247
|
+
# @param policies [Array<Bluebook::Policy>] every reaction hoisted onto the
|
|
248
|
+
# chapter's own aggregates
|
|
249
|
+
# @param process_managers [Array<Bluebook::ProcessManager>] the chapter's declared
|
|
250
|
+
# process managers
|
|
251
|
+
# @param aggregates [Array<Bluebook::Aggregate>] the chapter's declared aggregates
|
|
252
|
+
# @return [void]
|
|
253
|
+
# @raise [Bluebook::DSL::Malformed] if a policy's or a saga dispatch's `with:`
|
|
254
|
+
# names a field its target command does not declare, or reads a source field
|
|
255
|
+
# its event does not carry (see `check_with_spec!` for the full rule)
|
|
199
256
|
def validate_with_projections!(policies, process_managers, aggregates)
|
|
200
257
|
lookup = command_lookup(aggregates)
|
|
201
258
|
heads = correlation_heads(process_managers)
|
|
@@ -240,6 +297,27 @@ module Hecks
|
|
|
240
297
|
# source_shape, memory_shape, correlation, identity_sources, ...)
|
|
241
298
|
# into a new method for no gain: each `next`/`raise` already
|
|
242
299
|
# reads as its own rule at its own site.
|
|
300
|
+
#
|
|
301
|
+
# @param command_ref [String] the dotted name of the command `with:` dispatches,
|
|
302
|
+
# such as `"Account.Debit"` or `"Account.Piece.Move"`
|
|
303
|
+
# @param event_name [String, nil] the triggering event's name, or `nil` for a
|
|
304
|
+
# FOR_EACH policy (whose source is a query row, not an event)
|
|
305
|
+
# @param with_spec [Array<Array(Symbol, Object)>] the declared `with:` pairs,
|
|
306
|
+
# field name to source — a Symbol naming a field to read, or a literal value
|
|
307
|
+
# @param lookup [Hash{String => Class}] every command in the chapter, keyed by
|
|
308
|
+
# `"owner.hecks_name"` (`command_lookup`'s own shape)
|
|
309
|
+
# @param label [String] a human-readable description of what declared this
|
|
310
|
+
# `with:`, used only in a raised message
|
|
311
|
+
# @param aggregates [Array<Bluebook::Aggregate>] the chapter's declared aggregates
|
|
312
|
+
# @param correlation_heads [Array<Symbol>] every saga's own `correlates_by` head
|
|
313
|
+
# in this chapter, a legal addressing key alongside a command's own attributes
|
|
314
|
+
# @param process_manager [Bluebook::ProcessManager, nil] the process manager this
|
|
315
|
+
# `with:` dispatches from, or `nil` for a policy's own trigger
|
|
316
|
+
# @return [void]
|
|
317
|
+
# @raise [Bluebook::DSL::Malformed] if `with:` names a field the target command
|
|
318
|
+
# does not declare, or reads a Symbol source that resolves to neither the
|
|
319
|
+
# correlation key, the emitter's identity, nor a field the triggering event or
|
|
320
|
+
# (for a saga leg) the starting event's shape carries
|
|
243
321
|
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
244
322
|
# rubocop:disable-next Metrics/PerceivedComplexity
|
|
245
323
|
def check_with_spec!(command_ref, event_name, with_spec, lookup, label, aggregates, correlation_heads,
|
|
@@ -280,6 +358,8 @@ module Hecks
|
|
|
280
358
|
end
|
|
281
359
|
end
|
|
282
360
|
|
|
361
|
+
# Decides whether `field` is a legal `with:` target on `command`.
|
|
362
|
+
#
|
|
283
363
|
# A command's own `reference_to` (bare, no `as:`) never lands in
|
|
284
364
|
# `attributes` — `CommandBuilder#reference_to`'s self-reference
|
|
285
365
|
# branch sets `command.references` instead (S2), and mints no new
|
|
@@ -296,6 +376,16 @@ module Hecks
|
|
|
296
376
|
# legal here : this mirrors that gate rather than re-deriving a
|
|
297
377
|
# narrower rule that would refuse one of two real, already-shipped
|
|
298
378
|
# dispatch conventions.
|
|
379
|
+
#
|
|
380
|
+
# @param command [Class<Bluebook::Command>] the command `with:` dispatches
|
|
381
|
+
# @param field [Symbol] the `with:` key to check
|
|
382
|
+
# @param aggregates [Array<Bluebook::Aggregate>] the chapter's declared aggregates,
|
|
383
|
+
# searched when `field` might name `command`'s own reference key
|
|
384
|
+
# @param correlation_heads [Array<Symbol>] every saga's own `correlates_by` head
|
|
385
|
+
# in this chapter
|
|
386
|
+
# @return [Boolean] `true` when `field` is one of `command`'s own attributes,
|
|
387
|
+
# `:id`, a correlation head, or the identity/reference key of the aggregate
|
|
388
|
+
# `command.references` names
|
|
299
389
|
def command_declares?(command, field, aggregates, correlation_heads)
|
|
300
390
|
return true if command.attributes.any? { |a| a.name == field }
|
|
301
391
|
return true if field == :id
|
|
@@ -316,15 +406,30 @@ module Hecks
|
|
|
316
406
|
# passthrough, not an addressing key"). A command declaring none of
|
|
317
407
|
# its attributes named this is not a gap; the correlation key rides
|
|
318
408
|
# through commands that never read it, same as it does at runtime.
|
|
409
|
+
#
|
|
410
|
+
# @param process_managers [Array<Bluebook::ProcessManager>] the chapter's declared
|
|
411
|
+
# process managers
|
|
412
|
+
# @return [Array<Symbol>] the `correlation_head` of every process manager that
|
|
413
|
+
# declares a `correlates_by`
|
|
319
414
|
def correlation_heads(process_managers)
|
|
320
415
|
process_managers.filter_map { |pm| pm.correlates_by && pm.correlation_head }
|
|
321
416
|
end
|
|
322
417
|
|
|
418
|
+
# Yields every command this chapter declares, paired with its owner's name.
|
|
419
|
+
#
|
|
323
420
|
# Every command this chapter declares, an aggregate's own and
|
|
324
421
|
# every entity nested inside one, paired with a name for what
|
|
325
422
|
# declares it — shared by `validate_event_shapes!` and
|
|
326
423
|
# `validate_with_projections!`'s own command lookup, the same
|
|
327
424
|
# reach `HecksagonBuilder#commands_in` needs one level up (S8).
|
|
425
|
+
#
|
|
426
|
+
# @param aggregates [Array<Bluebook::Aggregate>] the chapter's declared aggregates
|
|
427
|
+
# @yieldparam owner [String] the declaring aggregate's `hecks_name`, or
|
|
428
|
+
# `"Aggregate.Entity"` for a command declared on a nested entity
|
|
429
|
+
# @yieldparam command [Class<Bluebook::Command>] the command class
|
|
430
|
+
# @return [Enumerator<Array(String, Class)>] an enumerator over the same pairs
|
|
431
|
+
# when no block is given; otherwise the value of the underlying `each`, unused
|
|
432
|
+
# by any caller
|
|
328
433
|
def each_command(aggregates)
|
|
329
434
|
return enum_for(:each_command, aggregates) unless block_given?
|
|
330
435
|
|
|
@@ -336,21 +441,29 @@ module Hecks
|
|
|
336
441
|
end
|
|
337
442
|
end
|
|
338
443
|
|
|
339
|
-
#
|
|
340
|
-
#
|
|
341
|
-
#
|
|
342
|
-
#
|
|
343
|
-
#
|
|
344
|
-
#
|
|
345
|
-
#
|
|
346
|
-
#
|
|
347
|
-
#
|
|
444
|
+
# Groups every command in the chapter by the event name(s) it emits.
|
|
445
|
+
#
|
|
446
|
+
# **Not memoised** — caching this as `@event_emitters ||=` on the builder
|
|
447
|
+
# instance would be safe for a one-file chapter but wrong for one split
|
|
448
|
+
# across several: the first file's `build()` call would compute and cache
|
|
449
|
+
# it from whatever `@aggregates` held at that moment, and every later
|
|
450
|
+
# file's own validation would keep reading that same stale snapshot,
|
|
451
|
+
# silently missing any command a later file adds. Recomputed fresh every
|
|
452
|
+
# call instead — this walks the whole chapter once per `#build`, not a
|
|
453
|
+
# hot path worth memoising at that cost.
|
|
454
|
+
#
|
|
455
|
+
# @param aggregates [Array<Bluebook::Aggregate>] the chapter's declared aggregates
|
|
456
|
+
# @return [Hash{String => Array<Array(String, Class)>}] every event name mapped
|
|
457
|
+
# to the `[owner, command]` pairs (`each_command`'s own shape) that `emits` it;
|
|
458
|
+
# an unqueried event name answers `[]`
|
|
348
459
|
def event_emitters(aggregates)
|
|
349
460
|
each_command(aggregates).with_object(Hash.new { |h, k| h[k] = [] }) do |(owner, command), index|
|
|
350
461
|
command.emits.each { |event_name| index[event_name] << [owner, command] }
|
|
351
462
|
end
|
|
352
463
|
end
|
|
353
464
|
|
|
465
|
+
# The comparable shape of one command's attributes, value objects unwrapped.
|
|
466
|
+
#
|
|
354
467
|
# **Structural, not nominal**. Two commands on two different
|
|
355
468
|
# aggregates that both `emits "SameEvent"` are free to type a
|
|
356
469
|
# field through two different, locally-scoped wrapper value
|
|
@@ -372,22 +485,48 @@ module Hecks
|
|
|
372
485
|
# same-named VO private to themselves, so the unwrap has to ask
|
|
373
486
|
# the same aggregate the field's own command belongs to, never a
|
|
374
487
|
# neighbor's.
|
|
488
|
+
#
|
|
489
|
+
# @param command [Class<Bluebook::Command>] the command whose attribute shape
|
|
490
|
+
# to compute
|
|
491
|
+
# @param owner [Bluebook::Aggregate, nil] the aggregate whose `value_object`
|
|
492
|
+
# lookup resolves any wrapper type by name; `nil` when the owner could not
|
|
493
|
+
# be resolved, in which case nothing is unwrapped
|
|
494
|
+
# @return [Array<Array>] one `[name, unwrapped_type, list, optional]` tuple per
|
|
495
|
+
# attribute, sorted; `unwrapped_type` is a type name String for a primitive
|
|
496
|
+
# or an unresolved type, or a recursively-unwrapped Array of the same shape
|
|
497
|
+
# for a value object
|
|
375
498
|
def event_shape(command, owner)
|
|
376
499
|
command.attributes.map { |a| [a.name, unwrap_shape(owner, a.type.to_s), a.list?, a.optional?] }.sort
|
|
377
500
|
end
|
|
378
501
|
|
|
502
|
+
# Recursively unwraps a type name to a primitive or a value object's own shape.
|
|
503
|
+
#
|
|
504
|
+
# @param owner [Bluebook::Aggregate, nil] the aggregate whose `value_object`
|
|
505
|
+
# lookup resolves `type_name`; `nil` answers `type_name` unchanged
|
|
506
|
+
# @param type_name [String] the attribute type to unwrap
|
|
507
|
+
# @param seen [Array<String>] type names already unwrapped on this path, so a
|
|
508
|
+
# self-referential value object bottoms out on its own name instead of
|
|
509
|
+
# recursing forever
|
|
510
|
+
# @return [String, Array<Array>] `type_name` itself when `owner` is `nil`, when
|
|
511
|
+
# it is already a primitive, when it is already in `seen`, or when it names no
|
|
512
|
+
# value object `owner` declares; otherwise the same `[name, unwrapped_type,
|
|
513
|
+
# list, optional]` shape `event_shape` returns, one tuple per member
|
|
379
514
|
def unwrap_shape(owner, type_name, seen = [])
|
|
380
|
-
|
|
515
|
+
# owner couldn't be resolved, so compare by name: nothing left to unwrap
|
|
516
|
+
return type_name if owner.nil?
|
|
381
517
|
return type_name if Attribute::PRIMITIVES.include?(type_name)
|
|
382
518
|
# a self-referential VO bottoms out on its own name, not an infinite unwrap
|
|
383
519
|
return type_name if seen.include?(type_name)
|
|
384
520
|
|
|
385
521
|
shape = owner.value_object(type_name)
|
|
386
|
-
|
|
522
|
+
# not this owner's own VO (a reference type, say): nothing further to unwrap
|
|
523
|
+
return type_name unless shape
|
|
387
524
|
|
|
388
525
|
shape.attributes.map { |a| [a.name, unwrap_shape(owner, a.type.to_s, seen + [type_name]), a.list?, a.optional?] }.sort
|
|
389
526
|
end
|
|
390
527
|
|
|
528
|
+
# Finds the aggregate an `each_command` owner string names.
|
|
529
|
+
#
|
|
391
530
|
# `owner` (from `each_command`) is a plain string — the aggregate's
|
|
392
531
|
# `hecks_name` alone, or `"Aggregate.Entity"` for an entity's own
|
|
393
532
|
# command. Either way the value objects a command's fields can be
|
|
@@ -396,10 +535,22 @@ module Hecks
|
|
|
396
535
|
# already resolves hop/type lookups only at the aggregate level,
|
|
397
536
|
# e.g. `validate_hop_tail!`'s `target.value_object(type)`), so only
|
|
398
537
|
# the first segment ever matters here.
|
|
538
|
+
#
|
|
539
|
+
# @param owner [String] an `each_command` owner string, such as `"Game"` or
|
|
540
|
+
# `"Game.Knight"`
|
|
541
|
+
# @param aggregates [Array<Bluebook::Aggregate>] the chapter's declared aggregates
|
|
542
|
+
# @return [Bluebook::Aggregate, nil] the aggregate whose `hecks_name` matches
|
|
543
|
+
# `owner`'s first dotted segment, or `nil` if none does
|
|
399
544
|
def owner_aggregate(owner, aggregates)
|
|
400
545
|
aggregates.find { |a| a.hecks_name == owner.to_s.split(".").first }
|
|
401
546
|
end
|
|
402
547
|
|
|
548
|
+
# The comparable attribute shape of whichever command first emits `event_name`.
|
|
549
|
+
#
|
|
550
|
+
# @param event_name [String, Symbol] the event name to look up
|
|
551
|
+
# @param aggregates [Array<Bluebook::Aggregate>] the chapter's declared aggregates
|
|
552
|
+
# @return [Array<Array>, nil] the first emitting command's `event_shape`, or `nil`
|
|
553
|
+
# if no command in the chapter emits this event
|
|
403
554
|
def event_shape_for(event_name, aggregates)
|
|
404
555
|
pairs = event_emitters(aggregates).fetch(event_name.to_s, [])
|
|
405
556
|
return nil if pairs.empty?
|
|
@@ -412,6 +563,12 @@ module Hecks
|
|
|
412
563
|
# entity's event is stamped with its owning aggregate's identity
|
|
413
564
|
# (`Event#id` is the parent's), so an owner spelled "Game.Knight"
|
|
414
565
|
# answers Game's heads.
|
|
566
|
+
#
|
|
567
|
+
# @param event_name [String, Symbol] the event name to look up
|
|
568
|
+
# @param aggregates [Array<Bluebook::Aggregate>] the chapter's declared aggregates
|
|
569
|
+
# @return [Array<Symbol>] the emitting aggregate's own `identity_heads`, plus the
|
|
570
|
+
# emitting entity's own `identity_heads` when the event is an entity's; `[]` if
|
|
571
|
+
# no command in the chapter emits this event, or its owner cannot be resolved
|
|
415
572
|
def event_identity_heads_for(event_name, aggregates)
|
|
416
573
|
pairs = event_emitters(aggregates).fetch(event_name.to_s, [])
|
|
417
574
|
return [] if pairs.empty?
|
|
@@ -431,6 +588,11 @@ module Hecks
|
|
|
431
588
|
heads + (entity ? entity.identity_heads.map(&:to_sym) : [])
|
|
432
589
|
end
|
|
433
590
|
|
|
591
|
+
# Every command in the chapter, keyed by its fully dotted address.
|
|
592
|
+
#
|
|
593
|
+
# @param aggregates [Array<Bluebook::Aggregate>] the chapter's declared aggregates
|
|
594
|
+
# @return [Hash{String => Class}] `"owner.hecks_name"` (`each_command`'s owner
|
|
595
|
+
# string joined with the command's own `hecks_name`) mapped to the command class
|
|
434
596
|
def command_lookup(aggregates)
|
|
435
597
|
each_command(aggregates).with_object({}) do |(owner, command), index|
|
|
436
598
|
index["#{owner}.#{command.hecks_name}"] = command
|
|
@@ -451,18 +613,22 @@ module Hecks
|
|
|
451
613
|
# aggregate finishes building long before it can know whether
|
|
452
614
|
# some later aggregate in the same file points back at it.
|
|
453
615
|
#
|
|
454
|
-
# Acyclic within a chapter (ADR 0025, "References") —
|
|
455
|
-
#
|
|
456
|
-
#
|
|
457
|
-
#
|
|
458
|
-
#
|
|
459
|
-
#
|
|
460
|
-
#
|
|
461
|
-
#
|
|
462
|
-
#
|
|
463
|
-
#
|
|
464
|
-
#
|
|
465
|
-
#
|
|
616
|
+
# Acyclic within a chapter (ADR 0025, "References") — catches any
|
|
617
|
+
# ring, however long (A -> B -> C -> A), not only a direct pair
|
|
618
|
+
# (A -> B -> A), the same DFS coloring a reference graph needs
|
|
619
|
+
# for any cycle. A cross-chapter reference is unreachable here
|
|
620
|
+
# rather than unchecked: `Reference#resolve` is scoped to its
|
|
621
|
+
# own chapter by construction, so a target this chapter never
|
|
622
|
+
# declares is a dangling name, not an edge — `edges.key?` below
|
|
623
|
+
# is what keeps the walk from ever leaving this chapter's own
|
|
624
|
+
# aggregates. Self-reference stays legal (`parent.parent.name`
|
|
625
|
+
# for a hierarchy is real and safe) — excluded the same way a
|
|
626
|
+
# direct pair is.
|
|
627
|
+
#
|
|
628
|
+
# @param aggregates [Array<Bluebook::Aggregate>] the chapter's declared aggregates
|
|
629
|
+
# @return [void]
|
|
630
|
+
# @raise [Bluebook::DSL::Malformed] if the chapter's own aggregate-to-aggregate
|
|
631
|
+
# reference graph contains a cycle
|
|
466
632
|
def validate_no_bidirectional_references!(aggregates)
|
|
467
633
|
edges = aggregates.to_h do |aggregate|
|
|
468
634
|
[aggregate.hecks_name, aggregate.reference_targets.uniq.reject { |target| target == aggregate.hecks_name }]
|
|
@@ -482,6 +648,11 @@ module Hecks
|
|
|
482
648
|
# Plain DFS with a visiting/done coloring, over the reference
|
|
483
649
|
# graph this chapter's own aggregates declare. Returns the ring
|
|
484
650
|
# itself (in the order it closes), or nil.
|
|
651
|
+
#
|
|
652
|
+
# @param edges [Hash{String => Array<String>}] each aggregate's `hecks_name`
|
|
653
|
+
# mapped to the names of the aggregates it references
|
|
654
|
+
# @return [Array<String>, nil] the aggregate names forming a cycle, in the order
|
|
655
|
+
# the walk closes it, or `nil` if the graph has none
|
|
485
656
|
def find_reference_cycle(edges)
|
|
486
657
|
state = {}
|
|
487
658
|
|
|
@@ -493,6 +664,17 @@ module Hecks
|
|
|
493
664
|
nil
|
|
494
665
|
end
|
|
495
666
|
|
|
667
|
+
# One DFS step of `find_reference_cycle`, from `node`.
|
|
668
|
+
#
|
|
669
|
+
# @param node [String] the aggregate name to walk from
|
|
670
|
+
# @param edges [Hash{String => Array<String>}] the whole reference graph, as
|
|
671
|
+
# `find_reference_cycle` receives it
|
|
672
|
+
# @param state [Hash{String => Symbol}] each visited node's `:visiting` or
|
|
673
|
+
# `:done` color, mutated in place across the whole walk
|
|
674
|
+
# @param path [Array<String>] the nodes visited on the current path, mutated
|
|
675
|
+
# in place as the walk descends and backtracks
|
|
676
|
+
# @return [Array<String>, nil] the cycle closing back on `node`, or `nil` if
|
|
677
|
+
# no cycle is reachable from here
|
|
496
678
|
def reference_cycle_from(node, edges, state, path)
|
|
497
679
|
return nil if state[node] == :done
|
|
498
680
|
return path[path.index(node)..] if state[node] == :visiting
|
|
@@ -501,7 +683,8 @@ module Hecks
|
|
|
501
683
|
path.push(node)
|
|
502
684
|
|
|
503
685
|
edges[node].each do |target|
|
|
504
|
-
|
|
686
|
+
# a name this chapter never declares is dangling, not an edge
|
|
687
|
+
next unless edges.key?(target)
|
|
505
688
|
|
|
506
689
|
found = reference_cycle_from(target, edges, state, path)
|
|
507
690
|
return found if found
|
|
@@ -512,6 +695,8 @@ module Hecks
|
|
|
512
695
|
nil
|
|
513
696
|
end
|
|
514
697
|
|
|
698
|
+
# Resolves and checks every `where` hop deferred at aggregate-seal time.
|
|
699
|
+
#
|
|
515
700
|
# **The other half of a hop** — AggregateBuilder#seal_query_field
|
|
516
701
|
# recognised the head of a dotted where-field that names one of
|
|
517
702
|
# its own references and deferred it here, unable to check
|
|
@@ -519,24 +704,28 @@ module Hecks
|
|
|
519
704
|
# This runs once every aggregate exists in one chapter, so it
|
|
520
705
|
# can.
|
|
521
706
|
#
|
|
522
|
-
# Only
|
|
707
|
+
# Only `where` clauses ever reach here — a hop on ORDER BY is
|
|
523
708
|
# refused outright, immediately, back in seal_query_field
|
|
524
709
|
# itself (that answer never needed the target's shape).
|
|
525
710
|
#
|
|
526
|
-
# An entity's own queries
|
|
527
|
-
#
|
|
528
|
-
#
|
|
529
|
-
#
|
|
530
|
-
#
|
|
531
|
-
#
|
|
532
|
-
# to check the deferral — a bad hop, or even a well-formed one,
|
|
533
|
-
# built silently and then matched nothing at runtime
|
|
711
|
+
# An entity's own queries do reach `EntityBuilder#reference_to` —
|
|
712
|
+
# tier-1 sealing (`AggregateBuilder#query_surfaces`) already
|
|
713
|
+
# recognises a hop on an entity's own field and defers it exactly
|
|
714
|
+
# like an aggregate's — but nothing walks entity queries at tier 2
|
|
715
|
+
# to check the deferral, so a bad hop, or even a well-formed one,
|
|
716
|
+
# would otherwise build silently and then match nothing at runtime
|
|
534
717
|
# (`QueryInterpreter#entity_rows` reads an element's fields by
|
|
535
718
|
# literal hash key, never follows a reference). Refused outright
|
|
536
719
|
# here instead of taught to follow the hop for real: no corpus
|
|
537
720
|
# member needs an entity query to cross a reference yet, and a
|
|
538
721
|
# named refusal beats a runtime that resolves nothing while
|
|
539
722
|
# looking like it might.
|
|
723
|
+
#
|
|
724
|
+
# @param bluebook [Bluebook::Chapter] the fully assembled chapter to check
|
|
725
|
+
# @return [void]
|
|
726
|
+
# @raise [Bluebook::DSL::Malformed] if an aggregate's `where` hop resolves to
|
|
727
|
+
# nothing, chains too deep, or lands somewhere illegal, or if an entity's own
|
|
728
|
+
# query hops through a reference at all
|
|
540
729
|
def validate_query_hops!(bluebook)
|
|
541
730
|
bluebook.aggregates.each do |aggregate|
|
|
542
731
|
aggregate.queries.each do |query|
|
|
@@ -551,11 +740,19 @@ module Hecks
|
|
|
551
740
|
end
|
|
552
741
|
end
|
|
553
742
|
|
|
743
|
+
# Mints an implicit query attribute for every unresolved symbolic hop comparison.
|
|
744
|
+
#
|
|
554
745
|
# The chapter-wide half of AggregateBuilder's local query-argument
|
|
555
746
|
# inference. A hop cannot resolve while its aggregate is still being
|
|
556
747
|
# built; here every Reference has an owner and target, so a symbolic
|
|
557
748
|
# comparison can inherit the type of the scalar it compares without a
|
|
558
749
|
# duplicate query-local declaration.
|
|
750
|
+
#
|
|
751
|
+
# @param bluebook [Bluebook::Chapter] the fully assembled chapter to infer
|
|
752
|
+
# query arguments over
|
|
753
|
+
# @return [void]
|
|
754
|
+
# @raise [Bluebook::DSL::Malformed] if a hop's reference has no `declared_in`
|
|
755
|
+
# aggregate to resolve its target through
|
|
559
756
|
def infer_hop_query_arguments!(bluebook)
|
|
560
757
|
bluebook.aggregates.each do |aggregate|
|
|
561
758
|
aggregate.queries.each do |query|
|
|
@@ -577,6 +774,8 @@ module Hecks
|
|
|
577
774
|
end
|
|
578
775
|
end
|
|
579
776
|
|
|
777
|
+
# Builds the query attribute a resolved hop plan implies for `name`.
|
|
778
|
+
#
|
|
580
779
|
# The leaf `infer_hop_query_arguments!` infers for one resolved
|
|
581
780
|
# hop plan — a pure function of `plan` and the symbolic `name`
|
|
582
781
|
# it is naming, pulled out because it is a self-contained
|
|
@@ -584,6 +783,14 @@ module Hecks
|
|
|
584
783
|
# iteration state (it neither reads nor mutates anything about
|
|
585
784
|
# `bluebook`/`aggregate`/`query` beyond what `plan` already
|
|
586
785
|
# carries).
|
|
786
|
+
#
|
|
787
|
+
# @param name [Symbol] the symbolic comparison value the query names, and the
|
|
788
|
+
# attribute name to mint
|
|
789
|
+
# @param plan [QuerySpecification::HopPath::Plan] the resolved hop chain whose
|
|
790
|
+
# target's shape `name` is inferred against
|
|
791
|
+
# @return [Bluebook::Attribute, nil] a `String`-typed attribute when the tail
|
|
792
|
+
# lands on the target's lifecycle field; otherwise an attribute copying the
|
|
793
|
+
# resolved leaf's type and list-ness; `nil` when the tail resolves to nothing
|
|
587
794
|
def inferred_hop_leaf(name, plan)
|
|
588
795
|
target = plan.hops.last.target
|
|
589
796
|
head, *nested = plan.tail.to_s.split(".")
|
|
@@ -598,6 +805,14 @@ module Hecks
|
|
|
598
805
|
end
|
|
599
806
|
end
|
|
600
807
|
|
|
808
|
+
# Refuses any `where` clause on an entity's own query that hops through a reference.
|
|
809
|
+
#
|
|
810
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate `entity` is nested under,
|
|
811
|
+
# named only in a raised message
|
|
812
|
+
# @param entity [Class<Bluebook::Entity>] the entity whose queries to check
|
|
813
|
+
# @return [void]
|
|
814
|
+
# @raise [Bluebook::DSL::Malformed] if any of `entity`'s queries has a `where`
|
|
815
|
+
# clause that hops through one of the entity's own references
|
|
601
816
|
def refuse_entity_query_hops!(aggregate, entity)
|
|
602
817
|
entity.queries.each do |query|
|
|
603
818
|
query.wheres.each do |clause|
|
|
@@ -612,6 +827,18 @@ module Hecks
|
|
|
612
827
|
end
|
|
613
828
|
end
|
|
614
829
|
|
|
830
|
+
# Resolves one `where` hop's target and checks where its tail lands.
|
|
831
|
+
#
|
|
832
|
+
# @param aggregate [Bluebook::Aggregate] the querying aggregate, named in a
|
|
833
|
+
# raised message
|
|
834
|
+
# @param query [Bluebook::Query] the query `clause` belongs to, named in a
|
|
835
|
+
# raised message
|
|
836
|
+
# @param clause [QuerySpecification::Common::WhereClause] the `where` clause whose
|
|
837
|
+
# `field` hops through one of `aggregate`'s own references
|
|
838
|
+
# @return [void]
|
|
839
|
+
# @raise [Bluebook::DSL::Malformed] if the hop's target is not declared in this
|
|
840
|
+
# chapter, the hop chain exceeds `QuerySpecification::HopPath::MAX_HOPS`, or
|
|
841
|
+
# the resolved tail fails `validate_hop_tail!`'s own checks
|
|
615
842
|
def validate_hop_clause!(aggregate, query, clause)
|
|
616
843
|
plan = QuerySpecification::HopPath.plan(clause.field, aggregate.attributes)
|
|
617
844
|
|
|
@@ -639,11 +866,26 @@ module Hecks
|
|
|
639
866
|
validate_hop_tail!(aggregate, query, clause, target, plan.tail)
|
|
640
867
|
end
|
|
641
868
|
|
|
869
|
+
# Checks that a resolved hop's tail lands on a real scalar field of its target.
|
|
870
|
+
#
|
|
642
871
|
# The same three-way answer seal_query_field gives for its own
|
|
643
872
|
# aggregate's fields — landing on a real scalar (fine), landing
|
|
644
873
|
# on a value object (refused by name), or naming nothing at all
|
|
645
874
|
# (refused by name) — asked instead of the hop's target aggregate,
|
|
646
875
|
# since that is whose shape the tail actually has to answer for.
|
|
876
|
+
#
|
|
877
|
+
# @param aggregate [Bluebook::Aggregate] the querying aggregate, named in a
|
|
878
|
+
# raised message
|
|
879
|
+
# @param query [Bluebook::Query] the query `clause` belongs to, named in a
|
|
880
|
+
# raised message
|
|
881
|
+
# @param clause [QuerySpecification::Common::WhereClause] the `where` clause the
|
|
882
|
+
# hop was resolved from, named in a raised message
|
|
883
|
+
# @param target [Bluebook::Aggregate] the aggregate the hop resolved to
|
|
884
|
+
# @param tail [String] the dotted field path remaining after the hop, such as
|
|
885
|
+
# `"status"` or `"amount.currency"`
|
|
886
|
+
# @return [void]
|
|
887
|
+
# @raise [Bluebook::DSL::Malformed] if `tail` lands on a value object rather
|
|
888
|
+
# than a scalar, or names no field `target` declares
|
|
647
889
|
def validate_hop_tail!(aggregate, query, clause, target, tail)
|
|
648
890
|
name, *nested = tail.to_s.split(".")
|
|
649
891
|
attribute = target.attributes.find { |candidate| candidate.name.to_s == name }
|
|
@@ -669,11 +911,30 @@ module Hecks
|
|
|
669
911
|
"not exist matches nothing and refuses nothing"
|
|
670
912
|
end
|
|
671
913
|
|
|
914
|
+
# Checks that an ordered comparator over a hopped field lands on a number.
|
|
915
|
+
#
|
|
672
916
|
# A where hop with an ordered comparator is legitimate ("client
|
|
673
917
|
# whose balance > 500") — AggregateBuilder#seal_ordered_comparator
|
|
674
918
|
# already deferred this exact check for the same reason every
|
|
675
919
|
# other hop check is deferred, and this is where it gets asked,
|
|
676
920
|
# against the hop's target instead of the querying aggregate.
|
|
921
|
+
#
|
|
922
|
+
# @param aggregate [Bluebook::Aggregate] the querying aggregate, named in a
|
|
923
|
+
# raised message
|
|
924
|
+
# @param query [Bluebook::Query] the query `clause` belongs to, named in a
|
|
925
|
+
# raised message
|
|
926
|
+
# @param clause [QuerySpecification::Common::WhereClause] the `where` clause
|
|
927
|
+
# whose `op` and `field` are named in a raised message
|
|
928
|
+
# @param target [Bluebook::Aggregate] the aggregate the hop resolved to, named
|
|
929
|
+
# in a raised message
|
|
930
|
+
# @param attribute [Bluebook::Attribute, nil] the tail's root attribute on
|
|
931
|
+
# `target`, or `nil` when the tail lands on the lifecycle field
|
|
932
|
+
# @param nested [Array<String>] the tail's remaining dotted segments past
|
|
933
|
+
# `attribute`, `[]` for a bare field
|
|
934
|
+
# @return [void]
|
|
935
|
+
# @raise [Bluebook::DSL::Malformed] if `clause.op` is one of
|
|
936
|
+
# `AggregateBuilder::ORDERED_COMPARATORS` and the field it lands on, after
|
|
937
|
+
# `attribute`/`nested`, is not numeric
|
|
677
938
|
def validate_hop_comparator!(aggregate, query, clause, target, attribute, nested)
|
|
678
939
|
return unless AggregateBuilder::ORDERED_COMPARATORS.include?(clause.op.to_s.to_sym)
|
|
679
940
|
return if attribute &&
|
|
@@ -702,12 +963,19 @@ module Hecks
|
|
|
702
963
|
# one resolution primitive. A single hop can never reach
|
|
703
964
|
# HopPath::MAX_HOPS, so :too_deep is structurally unreachable
|
|
704
965
|
# here and is not special-cased.
|
|
966
|
+
#
|
|
967
|
+
# @param bluebook [Bluebook::Chapter] the fully assembled chapter to check
|
|
968
|
+
# @return [void]
|
|
969
|
+
# @raise [Bluebook::DSL::Malformed] if any aggregate's `projects` field fails
|
|
970
|
+
# `validate_projected_field!`'s own checks
|
|
705
971
|
def validate_projected_fields!(bluebook)
|
|
706
972
|
bluebook.aggregates.each do |aggregate|
|
|
707
973
|
aggregate.projected_fields.each { |field| validate_projected_field!(aggregate, field) }
|
|
708
974
|
end
|
|
709
975
|
end
|
|
710
976
|
|
|
977
|
+
# Checks that one `projects` field's reference resolves to a real scalar.
|
|
978
|
+
#
|
|
711
979
|
# A linear decision tree of validation rules over one resolved
|
|
712
980
|
# hop plan, each already explained by its own comment above
|
|
713
981
|
# (the lifecycle fallback, the chained-projection fallback, the
|
|
@@ -716,6 +984,13 @@ module Hecks
|
|
|
716
984
|
# land," not several unrelated concerns. Splitting it would
|
|
717
985
|
# scatter `plan`/`target`/`remote_attribute` across new methods
|
|
718
986
|
# that would each need most of them anyway.
|
|
987
|
+
#
|
|
988
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate that declared `field`
|
|
989
|
+
# @param field [Bluebook::ProjectedField] the declared `projects` field to check
|
|
990
|
+
# @return [void]
|
|
991
|
+
# @raise [Bluebook::DSL::Malformed] if `field.reference` hops to an aggregate
|
|
992
|
+
# this chapter never declares, or `field.remote_field` names nothing on the
|
|
993
|
+
# target, or names something other than a scalar
|
|
719
994
|
# rubocop:disable-next Metrics/AbcSize
|
|
720
995
|
def validate_projected_field!(aggregate, field)
|
|
721
996
|
plan = QuerySpecification::HopPath.plan("#{field.reference}/#{field.remote_field}", aggregate.attributes)
|
|
@@ -770,6 +1045,13 @@ module Hecks
|
|
|
770
1045
|
"value, never a reference, a value object, or a list"
|
|
771
1046
|
end
|
|
772
1047
|
|
|
1048
|
+
# Decides whether a declared attribute is a plain, single scalar value.
|
|
1049
|
+
#
|
|
1050
|
+
# @param target [Bluebook::Aggregate] the aggregate `attribute` is declared on,
|
|
1051
|
+
# whose `value_object` lookup resolves `attribute`'s type
|
|
1052
|
+
# @param attribute [Bluebook::Attribute] the attribute to check
|
|
1053
|
+
# @return [Boolean] `true` when `attribute` is not a list, not a reference, and
|
|
1054
|
+
# its type names no value object `target` declares
|
|
773
1055
|
def projectable_scalar?(target, attribute)
|
|
774
1056
|
!attribute.list? && !attribute.reference? && target.value_object(attribute.type).nil?
|
|
775
1057
|
end
|
|
@@ -794,6 +1076,14 @@ module Hecks
|
|
|
794
1076
|
# aggregate's own reference key; saga_interpreter/correlation.rb), so
|
|
795
1077
|
# an absent field is not this check's business. Only a field that
|
|
796
1078
|
# resolves, and resolves to something other than a scalar, is.
|
|
1079
|
+
#
|
|
1080
|
+
# @param process_managers [Array<Bluebook::ProcessManager>] the chapter's
|
|
1081
|
+
# declared process managers
|
|
1082
|
+
# @param aggregates [Array<Bluebook::Aggregate>] the chapter's declared aggregates
|
|
1083
|
+
# @return [void]
|
|
1084
|
+
# @raise [Bluebook::DSL::ProcessManagerBuilder::InvalidProcessManager] if a
|
|
1085
|
+
# process manager's `correlates_by` path resolves, on any command that emits
|
|
1086
|
+
# an event it reacts to, to something other than a scalar
|
|
797
1087
|
def validate_correlation_keys!(process_managers, aggregates)
|
|
798
1088
|
process_managers.each do |pm|
|
|
799
1089
|
next unless pm.correlates_by
|
|
@@ -806,6 +1096,15 @@ module Hecks
|
|
|
806
1096
|
end
|
|
807
1097
|
end
|
|
808
1098
|
|
|
1099
|
+
# Finds why a process manager's `correlates_by` path is not a scalar, if it isn't.
|
|
1100
|
+
#
|
|
1101
|
+
# @param process_manager [Bluebook::ProcessManager] the process manager whose
|
|
1102
|
+
# `correlates_by` to check
|
|
1103
|
+
# @param aggregates [Array<Bluebook::Aggregate>] the chapter's declared aggregates
|
|
1104
|
+
# @return [String, nil] the reason the path is not a scalar, on the first
|
|
1105
|
+
# emitting command whose first segment resolves; `nil` if every command that
|
|
1106
|
+
# emits a reacted-to event either omits the first segment, or resolves to
|
|
1107
|
+
# a scalar
|
|
809
1108
|
def correlation_key_violation(process_manager, aggregates)
|
|
810
1109
|
head, *rest = process_manager.correlates_by.to_s.split(".")
|
|
811
1110
|
events = reacted_events(process_manager)
|
|
@@ -821,6 +1120,12 @@ module Hecks
|
|
|
821
1120
|
nil
|
|
822
1121
|
end
|
|
823
1122
|
|
|
1123
|
+
# The demodulised names of every event a process manager reacts to.
|
|
1124
|
+
#
|
|
1125
|
+
# @param process_manager [Bluebook::ProcessManager] the process manager whose
|
|
1126
|
+
# `starts_on`, `ends_on` and handler event types to collect
|
|
1127
|
+
# @return [Array<String>] the unique demodulised event names, `REFUSED` (the
|
|
1128
|
+
# compensation trigger, not a real event) excluded
|
|
824
1129
|
def reacted_events(process_manager)
|
|
825
1130
|
([process_manager.starts_on, process_manager.ends_on] + process_manager.handlers.map(&:event_type))
|
|
826
1131
|
.compact
|
|
@@ -829,6 +1134,13 @@ module Hecks
|
|
|
829
1134
|
.uniq
|
|
830
1135
|
end
|
|
831
1136
|
|
|
1137
|
+
# Every command across the chapter that emits one of the given events.
|
|
1138
|
+
#
|
|
1139
|
+
# @param events [Array<String>] the demodulised event names to match against
|
|
1140
|
+
# each command's own `emits`
|
|
1141
|
+
# @param aggregates [Array<Bluebook::Aggregate>] the chapter's declared aggregates
|
|
1142
|
+
# @return [Array<Array(Bluebook::Aggregate, Class)>] one `[aggregate, command]`
|
|
1143
|
+
# pair per matching command, an aggregate's own or one of its entities'
|
|
832
1144
|
def emitting_commands(events, aggregates)
|
|
833
1145
|
aggregates.flat_map do |aggregate|
|
|
834
1146
|
commands = aggregate.commands + aggregate.entities.flat_map(&:commands)
|
|
@@ -837,6 +1149,16 @@ module Hecks
|
|
|
837
1149
|
end
|
|
838
1150
|
end
|
|
839
1151
|
|
|
1152
|
+
# Decides whether one command attribute can serve as a correlation key.
|
|
1153
|
+
#
|
|
1154
|
+
# @param owner [Bluebook::Aggregate] the aggregate whose `value_object` lookup
|
|
1155
|
+
# resolves `attribute`'s type, for `walk_scalar`
|
|
1156
|
+
# @param attribute [Bluebook::Attribute] the command attribute the correlation
|
|
1157
|
+
# path's first segment named
|
|
1158
|
+
# @param segments [Array<String>] the path's remaining dotted segments, `[]` for
|
|
1159
|
+
# a bare field
|
|
1160
|
+
# @return [String, nil] why `attribute` cannot serve as a correlation key, or
|
|
1161
|
+
# `nil` if it can — see `walk_scalar` for what a non-`nil` reason means
|
|
840
1162
|
def list_or_scalar_violation(owner, attribute, segments)
|
|
841
1163
|
if attribute.list?
|
|
842
1164
|
return "#{attribute.name} is a list — a correlation key must name one instance's own field, " \
|
|
@@ -853,6 +1175,16 @@ module Hecks
|
|
|
853
1175
|
# a value object this domain never declared, a field that value
|
|
854
1176
|
# object does not have, or a segment left over after already
|
|
855
1177
|
# reaching a scalar.
|
|
1178
|
+
#
|
|
1179
|
+
# @param owner [Bluebook::Aggregate] the aggregate whose `value_object` lookup
|
|
1180
|
+
# resolves each stepped-into type name
|
|
1181
|
+
# @param type_name [String] the type to check or step into
|
|
1182
|
+
# @param segments [Array<String>] the dotted path segments remaining to walk
|
|
1183
|
+
# @return [String, nil] `nil` when the walk bottoms out cleanly on a scalar;
|
|
1184
|
+
# otherwise why it cannot: `type_name` is a value object with no segments
|
|
1185
|
+
# left, `type_name` is already a scalar with segments left over, `type_name`
|
|
1186
|
+
# names no value object `owner` declares, the next segment names no field of
|
|
1187
|
+
# that value object, or that field is a list
|
|
856
1188
|
def walk_scalar(owner, type_name, segments)
|
|
857
1189
|
if segments.empty?
|
|
858
1190
|
return nil if Attribute::PRIMITIVES.include?(type_name)
|