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
|
@@ -7,6 +7,8 @@ require_relative "../ports/persistence/binding_policy"
|
|
|
7
7
|
|
|
8
8
|
# Hecks::Behaviors::Expectations
|
|
9
9
|
#
|
|
10
|
+
# ## What it does
|
|
11
|
+
#
|
|
10
12
|
# One test case, start to finish: take the suite's own runtime (booted
|
|
11
13
|
# once for exactly what its `loads` names, reset to nothing between
|
|
12
14
|
# tests — `runtime_for`), replay `setup` dispatches, dispatch (or query)
|
|
@@ -14,6 +16,8 @@ require_relative "../ports/persistence/binding_policy"
|
|
|
14
16
|
# runner.rb the same way the file-count/sweep concern is split from a
|
|
15
17
|
# single test's own execution.
|
|
16
18
|
#
|
|
19
|
+
# ## Deviations from a prior port
|
|
20
|
+
#
|
|
17
21
|
# Two deliberate deviations from a prior port of this same idea (read
|
|
18
22
|
# before writing this, not reinvented):
|
|
19
23
|
#
|
|
@@ -46,6 +50,15 @@ module Hecks
|
|
|
46
50
|
REFUSAL_CLASSES = Hecks::Runtime::DOMAIN_REFUSALS
|
|
47
51
|
SPECIAL_KEYS = %i[ok refused emits count].freeze
|
|
48
52
|
|
|
53
|
+
# Runs one test case: replays its `setup` dispatches, dispatches (or queries)
|
|
54
|
+
# the command under test, and checks its `expect`.
|
|
55
|
+
#
|
|
56
|
+
# @param test [Behaviors::TestCase] the test case to run
|
|
57
|
+
# @param suite [Behaviors::BehaviorsSuite] the suite `test` belongs to
|
|
58
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher, nil] an
|
|
59
|
+
# already-booted runtime to reuse and reset; nil boots (or reuses the cached
|
|
60
|
+
# boot of) `suite.loads` via `runtime_for`
|
|
61
|
+
# @return [Result] the test's pass, fail, or error outcome
|
|
49
62
|
def run_one(test, suite, runtime: nil)
|
|
50
63
|
runtime ||= runtime_for(suite)
|
|
51
64
|
runtime.registry.reset_runtime_state!
|
|
@@ -85,6 +98,14 @@ module Hecks
|
|
|
85
98
|
RUNTIMES_LOCK = Mutex.new
|
|
86
99
|
private_constant :RUNTIMES, :RUNTIMES_LOCK
|
|
87
100
|
|
|
101
|
+
# Boots (or reuses the cached boot of) the runtime a suite's `loads` names.
|
|
102
|
+
#
|
|
103
|
+
# @param suite [Behaviors::BehaviorsSuite] the suite whose `loads` files boot
|
|
104
|
+
# the runtime
|
|
105
|
+
# @return [Runtime::Dispatcher, Runtime::RemoteDispatcher] the cached or
|
|
106
|
+
# freshly booted, Memory-only-guarded runtime
|
|
107
|
+
# @raise [Malformed] if any aggregate the suite boots is not bound to the
|
|
108
|
+
# default (Memory) adapter
|
|
88
109
|
def runtime_for(suite)
|
|
89
110
|
files = Array(suite.loads).map { |path| File.expand_path(path) }
|
|
90
111
|
key = files.map { |file| [file, File.exist?(file) ? File.mtime(file).to_f : nil] }
|
|
@@ -104,12 +125,25 @@ module Hecks
|
|
|
104
125
|
# of guard `BindingPolicy` already applies to a missing bind — the
|
|
105
126
|
# project's identity is refusing bad wiring up front, not
|
|
106
127
|
# discovering it mid-suite.
|
|
128
|
+
#
|
|
129
|
+
# @param files [Array<String>] absolute paths to the bluebook/hecksagon/world
|
|
130
|
+
# files to boot
|
|
131
|
+
# @return [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted,
|
|
132
|
+
# Memory-only-guarded runtime
|
|
133
|
+
# @raise [Malformed] if any aggregate is bound to a non-Memory adapter
|
|
107
134
|
def boot_and_guard(files)
|
|
108
135
|
runtime = Hecks::Runtime::Loader.boot_files(files, install_facade: false)
|
|
109
136
|
guard_memory_only!(runtime)
|
|
110
137
|
runtime
|
|
111
138
|
end
|
|
112
139
|
|
|
140
|
+
# Refuses a runtime where any aggregate is bound to anything other than the
|
|
141
|
+
# default (Memory) adapter, so tests can never leak state or touch a real store.
|
|
142
|
+
#
|
|
143
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted
|
|
144
|
+
# runtime to check
|
|
145
|
+
# @return [void]
|
|
146
|
+
# @raise [Malformed] if any aggregate is bound to a non-Memory adapter
|
|
113
147
|
def guard_memory_only!(runtime)
|
|
114
148
|
runtime.registry.bluebooks.each_value do |bluebook|
|
|
115
149
|
bluebook.aggregates.each do |aggregate|
|
|
@@ -127,6 +161,15 @@ module Hecks
|
|
|
127
161
|
end
|
|
128
162
|
end
|
|
129
163
|
|
|
164
|
+
# Qualifies and dispatches (or queries) the command under test, catching a
|
|
165
|
+
# domain refusal as the test's own outcome rather than an error.
|
|
166
|
+
#
|
|
167
|
+
# @param test [Behaviors::TestCase] the test case to run
|
|
168
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the suite's
|
|
169
|
+
# booted runtime
|
|
170
|
+
# @param bluebooks [Array<Bluebook::Chapter>] every chapter the suite booted,
|
|
171
|
+
# searched to qualify a bare command/query name
|
|
172
|
+
# @return [Result] the test's pass, fail, or error outcome
|
|
130
173
|
def run_tested(test, runtime, bluebooks)
|
|
131
174
|
verb = qualify(test.tests_command, test.on_aggregate, bluebooks, kind: test.kind)
|
|
132
175
|
|
|
@@ -139,6 +182,13 @@ module Hecks
|
|
|
139
182
|
check_refusal(test, e)
|
|
140
183
|
end
|
|
141
184
|
|
|
185
|
+
# Dispatches the command under test and checks its `expect`.
|
|
186
|
+
#
|
|
187
|
+
# @param test [Behaviors::TestCase] the test case to run
|
|
188
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the suite's
|
|
189
|
+
# booted runtime
|
|
190
|
+
# @param verb [String] the command's dotted FQN
|
|
191
|
+
# @return [Result] the test's pass or fail outcome
|
|
142
192
|
def run_command(test, runtime, verb)
|
|
143
193
|
before = runtime.registry.event_log.length
|
|
144
194
|
result = dispatch_command(runtime, verb, test.input)
|
|
@@ -170,6 +220,15 @@ module Hecks
|
|
|
170
220
|
# `emits:` saw MoveCountBumped in the same test). The repository
|
|
171
221
|
# holds the settled record; read it back by the id the dispatch
|
|
172
222
|
# itself answered with.
|
|
223
|
+
#
|
|
224
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the suite's
|
|
225
|
+
# booted runtime
|
|
226
|
+
# @param verb [String] the dispatched command's dotted FQN
|
|
227
|
+
# @param result [Runtime::Dispatcher::Result, Runtime::RemoteDispatcher::Result]
|
|
228
|
+
# the dispatch's own result
|
|
229
|
+
# @return [Hash{Symbol => Object}] the settled record's current state, read
|
|
230
|
+
# back from the repository; `result.state` (or `{}`) when the result has no
|
|
231
|
+
# id or the repository no longer has that aggregate/record
|
|
173
232
|
def settled_state(runtime, verb, result)
|
|
174
233
|
return result.state || {} unless result.respond_to?(:id) && result.id
|
|
175
234
|
|
|
@@ -202,13 +261,24 @@ module Hecks
|
|
|
202
261
|
# expects a command's own declared attributes at the top level,
|
|
203
262
|
# not a port operation's already-wrapped `to:`/`with:` shape.
|
|
204
263
|
#
|
|
205
|
-
#
|
|
206
|
-
#
|
|
207
|
-
# asked it to resolve
|
|
208
|
-
# `trigger` a port operation
|
|
209
|
-
# own port-operation branch),
|
|
210
|
-
#
|
|
211
|
-
#
|
|
264
|
+
# Checks for a port operation directly rather than leaning on
|
|
265
|
+
# `resolve_target` to raise `UnknownVerb` for one — a refusal that held
|
|
266
|
+
# only so long as nothing else ever asked it to resolve a port-operation
|
|
267
|
+
# verb. Now that a `policy` can legitimately `trigger` a port operation
|
|
268
|
+
# (`ReactionInvocation#resolve_target`'s own port-operation branch),
|
|
269
|
+
# that raise no longer happens, so this checks directly instead.
|
|
270
|
+
#
|
|
271
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the suite's
|
|
272
|
+
# booted runtime
|
|
273
|
+
# @param verb [String] the command's dotted FQN, or a port operation's
|
|
274
|
+
# @param args [Hash{Symbol => Object}] the command's (or setup's) facts, mixing
|
|
275
|
+
# receiver identity and declared arguments
|
|
276
|
+
# @return [Runtime::Dispatcher::Result, Runtime::RemoteDispatcher::Result] the
|
|
277
|
+
# dispatch's own result
|
|
278
|
+
# @raise [Runtime::UnknownVerb] if `verb` is not fully qualified or names an
|
|
279
|
+
# undeclared domain, aggregate, command, entity or port operation
|
|
280
|
+
# @raise [StandardError] any class in `Runtime::DOMAIN_REFUSALS` when the
|
|
281
|
+
# domain refuses the call
|
|
212
282
|
def dispatch_command(runtime, verb, args)
|
|
213
283
|
return runtime.dispatch_flat(verb, args) if port_operation?(runtime, verb)
|
|
214
284
|
|
|
@@ -231,6 +301,11 @@ module Hecks
|
|
|
231
301
|
# `ReactionInvocation#resolve_target` both already check — a bare
|
|
232
302
|
# domain/aggregate lookup plus a port-name lookup, no command
|
|
233
303
|
# resolution needed since all this asks is whether one exists.
|
|
304
|
+
#
|
|
305
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the suite's
|
|
306
|
+
# booted runtime
|
|
307
|
+
# @param verb [String] the verb to check, dotted FQN shaped
|
|
308
|
+
# @return [Boolean] true if `verb` names a port operation on a declared aggregate
|
|
234
309
|
def port_operation?(runtime, verb)
|
|
235
310
|
domain, aggregate_name, command_path = Naming.split_verb(verb)
|
|
236
311
|
return false unless command_path
|
|
@@ -242,6 +317,13 @@ module Hecks
|
|
|
242
317
|
rest && !!aggregate.port(head)
|
|
243
318
|
end
|
|
244
319
|
|
|
320
|
+
# Runs the query under test and checks its `expect`.
|
|
321
|
+
#
|
|
322
|
+
# @param test [Behaviors::TestCase] the test case to run
|
|
323
|
+
# @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the suite's
|
|
324
|
+
# booted runtime
|
|
325
|
+
# @param verb [String] the query's dotted FQN
|
|
326
|
+
# @return [Result] the test's pass or fail outcome
|
|
245
327
|
def run_query(test, runtime, verb)
|
|
246
328
|
rows = runtime.query(verb, **test.input)
|
|
247
329
|
|
|
@@ -267,6 +349,11 @@ module Hecks
|
|
|
267
349
|
check_ok(test) || check_fields(test, row) || pass_result(test)
|
|
268
350
|
end
|
|
269
351
|
|
|
352
|
+
# Whether `test.expect` names a field, not just one of the special keys.
|
|
353
|
+
#
|
|
354
|
+
# @param test [Behaviors::TestCase] the test case to check
|
|
355
|
+
# @return [Boolean] true if `test.expect` has any key besides `ok`, `refused`,
|
|
356
|
+
# `emits` or `count`
|
|
270
357
|
def field_expectations?(test)
|
|
271
358
|
test.expect.keys.any? { |key| !SPECIAL_KEYS.include?(key) }
|
|
272
359
|
end
|
|
@@ -277,6 +364,11 @@ module Hecks
|
|
|
277
364
|
# ambiguous about which row it's describing. `check_fields` itself
|
|
278
365
|
# stays row-shaped (it already is, for `run_command`'s settled
|
|
279
366
|
# state); this picks which row it reads.
|
|
367
|
+
#
|
|
368
|
+
# @param test [Behaviors::TestCase] the test case, for the fail message
|
|
369
|
+
# @param rows [Array<Hash>, Hash] the query's own result
|
|
370
|
+
# @return [Hash, Result] `rows` itself when it isn't an Array; its one row when
|
|
371
|
+
# it holds exactly one; otherwise a `fail_result` naming the row count
|
|
280
372
|
def query_row(test, rows)
|
|
281
373
|
return rows unless rows.is_a?(Array)
|
|
282
374
|
|
|
@@ -288,6 +380,11 @@ module Hecks
|
|
|
288
380
|
end
|
|
289
381
|
end
|
|
290
382
|
|
|
383
|
+
# Checks an `expect ok: true` expectation.
|
|
384
|
+
#
|
|
385
|
+
# @param test [Behaviors::TestCase] the test case to check
|
|
386
|
+
# @return [Result, nil] nil if `test.expect` has no `ok` key or expects `true`;
|
|
387
|
+
# a `fail_result` if it names anything else
|
|
291
388
|
def check_ok(test)
|
|
292
389
|
return unless test.expect.key?(:ok)
|
|
293
390
|
|
|
@@ -297,6 +394,13 @@ module Hecks
|
|
|
297
394
|
fail_result(test, "expect ok: only accepts true — got #{expected.inspect}")
|
|
298
395
|
end
|
|
299
396
|
|
|
397
|
+
# Checks every field-name expectation in `test.expect` against `state`.
|
|
398
|
+
#
|
|
399
|
+
# @param test [Behaviors::TestCase] the test case to check
|
|
400
|
+
# @param state [Hash{Symbol, String => Object}] the settled record's (or query
|
|
401
|
+
# row's) fields
|
|
402
|
+
# @return [Result, nil] nil if every expected field matches; a `fail_result` for
|
|
403
|
+
# the first field that names none of `state`'s keys or doesn't match
|
|
300
404
|
def check_fields(test, state)
|
|
301
405
|
test.expect.each do |key, expected|
|
|
302
406
|
next if SPECIAL_KEYS.include?(key)
|
|
@@ -314,6 +418,12 @@ module Hecks
|
|
|
314
418
|
nil
|
|
315
419
|
end
|
|
316
420
|
|
|
421
|
+
# Checks a caught domain refusal against `test.expect[:refused]`.
|
|
422
|
+
#
|
|
423
|
+
# @param test [Behaviors::TestCase] the test case to check
|
|
424
|
+
# @param error [StandardError] the caught refusal, a member of `REFUSAL_CLASSES`
|
|
425
|
+
# @return [Result] a `pass_result` if `test` expected this refusal's message, an
|
|
426
|
+
# `error_result` if it expected none, or a `fail_result` if the message doesn't match
|
|
317
427
|
def check_refusal(test, error)
|
|
318
428
|
expected = test.expect[:refused]
|
|
319
429
|
return error_result(test, "unexpected refusal (#{error.class}): #{error.message}") unless expected
|
|
@@ -333,6 +443,11 @@ module Hecks
|
|
|
333
443
|
# field always comes back as a `Hecks::Runtime::Value`;
|
|
334
444
|
# normalizing both sides to the same bare-scalar-or-plain-hash
|
|
335
445
|
# shape is the one comparison that accepts either spelling.
|
|
446
|
+
#
|
|
447
|
+
# @param value [Object] a stored field's value, or an `expect` value to compare
|
|
448
|
+
# it against
|
|
449
|
+
# @return [Object] the bare underlying value: unwrapped from a `Runtime::Value`,
|
|
450
|
+
# or from a `{value: ...}` Hash; unchanged otherwise
|
|
336
451
|
def normalize(value)
|
|
337
452
|
return Hecks::Runtime::Value.materialize_unwrapped(value) if value.is_a?(Hecks::Runtime::Value)
|
|
338
453
|
return normalize(value[:value]) if value.is_a?(Hash) && value.keys == [:value]
|
|
@@ -347,6 +462,15 @@ module Hecks
|
|
|
347
462
|
# the tested command — `setup` never receives it, see the DSL
|
|
348
463
|
# contract) narrows the search to one aggregate by name instead of
|
|
349
464
|
# searching all of them.
|
|
465
|
+
#
|
|
466
|
+
# @param command [String, Symbol] a bare verb, or an already-dotted FQN
|
|
467
|
+
# @param on_aggregate [String, Symbol, nil] the aggregate to search, or nil to
|
|
468
|
+
# search every aggregate of every bluebook
|
|
469
|
+
# @param bluebooks [Array<Bluebook::Chapter>] every chapter the suite booted
|
|
470
|
+
# @param kind [Symbol] `:command` or `:query`
|
|
471
|
+
# @return [String] `command` unchanged if already dotted, otherwise its resolved
|
|
472
|
+
# dotted FQN
|
|
473
|
+
# @raise [ArgumentError] if no aggregate declares `command`, or more than one does
|
|
350
474
|
def qualify(command, on_aggregate, bluebooks, kind:)
|
|
351
475
|
return command.to_s if command.to_s.include?(".")
|
|
352
476
|
|
|
@@ -354,9 +478,19 @@ module Hecks
|
|
|
354
478
|
disambiguate_qualified_name(candidates, command, kind, bluebooks)
|
|
355
479
|
end
|
|
356
480
|
|
|
481
|
+
# Finds every aggregate that could be what a bare command/query name refers to.
|
|
482
|
+
#
|
|
357
483
|
# **The search** — every (bluebook, aggregate) pair that declares a
|
|
358
484
|
# command/query named `command`, narrowed to `on_aggregate` by name
|
|
359
485
|
# when given.
|
|
486
|
+
#
|
|
487
|
+
# @param command [String, Symbol] the bare verb to search for
|
|
488
|
+
# @param on_aggregate [String, Symbol, nil] the aggregate to search, or nil to
|
|
489
|
+
# search every aggregate of every bluebook
|
|
490
|
+
# @param bluebooks [Array<Bluebook::Chapter>] every chapter the suite booted
|
|
491
|
+
# @param kind [Symbol] `:command` or `:query`
|
|
492
|
+
# @return [Array<Array(Bluebook::Chapter, Bluebook::Aggregate)>] every matching
|
|
493
|
+
# (chapter, aggregate) pair
|
|
360
494
|
def qualify_candidates(command, on_aggregate, bluebooks, kind)
|
|
361
495
|
members = kind == :query ? :queries : :commands
|
|
362
496
|
pairs =
|
|
@@ -368,9 +502,20 @@ module Hecks
|
|
|
368
502
|
pairs.select { |_, agg| agg.public_send(members).any? { |m| m.hecks_name == command.to_s } }
|
|
369
503
|
end
|
|
370
504
|
|
|
505
|
+
# Resolves a search's candidates to exactly one dotted FQN, or refuses.
|
|
506
|
+
#
|
|
371
507
|
# **The report** — zero candidates and more-than-one candidates both
|
|
372
508
|
# refuse (with a different message); exactly one resolves to its
|
|
373
509
|
# dotted FQN.
|
|
510
|
+
#
|
|
511
|
+
# @param candidates [Array<Array(Bluebook::Chapter, Bluebook::Aggregate)>] the
|
|
512
|
+
# matching (chapter, aggregate) pairs found by `qualify_candidates`
|
|
513
|
+
# @param command [String, Symbol] the bare verb that was searched for
|
|
514
|
+
# @param kind [Symbol] `:command` or `:query`, for the refusal message
|
|
515
|
+
# @param bluebooks [Array<Bluebook::Chapter>] every chapter the suite booted,
|
|
516
|
+
# for the refusal message
|
|
517
|
+
# @return [String] the one candidate's dotted FQN
|
|
518
|
+
# @raise [ArgumentError] if `candidates` is empty, or holds more than one
|
|
374
519
|
def disambiguate_qualified_name(candidates, command, kind, bluebooks)
|
|
375
520
|
case candidates.size
|
|
376
521
|
when 0
|
|
@@ -386,8 +531,24 @@ module Hecks
|
|
|
386
531
|
end
|
|
387
532
|
end
|
|
388
533
|
|
|
534
|
+
# Builds a passing result.
|
|
535
|
+
#
|
|
536
|
+
# @param test [Behaviors::TestCase] the test that passed
|
|
537
|
+
# @return [Result] a `:pass` result
|
|
389
538
|
def pass_result(test) = Result.new(description: test.description, status: :pass, message: nil)
|
|
539
|
+
|
|
540
|
+
# Builds a failing result.
|
|
541
|
+
#
|
|
542
|
+
# @param test [Behaviors::TestCase] the test whose expectation was not met
|
|
543
|
+
# @param message [String] what was expected versus what happened
|
|
544
|
+
# @return [Result] a `:fail` result
|
|
390
545
|
def fail_result(test, message) = Result.new(description: test.description, status: :fail, message: message)
|
|
546
|
+
|
|
547
|
+
# Builds an errored result.
|
|
548
|
+
#
|
|
549
|
+
# @param test [Behaviors::TestCase] the test that could not run to a conclusion
|
|
550
|
+
# @param message [String] what went wrong
|
|
551
|
+
# @return [Result] an `:error` result
|
|
391
552
|
def error_result(test, message) = Result.new(description: test.description, status: :error, message: message)
|
|
392
553
|
|
|
393
554
|
Result = Struct.new(:description, :status, :message, keyword_init: true)
|
data/lib/hecks/behaviors/ir.rb
CHANGED
|
@@ -8,18 +8,29 @@
|
|
|
8
8
|
# round-trip machinery a real bluebook construct does. It is a test
|
|
9
9
|
# artifact a runner reads on demand, not a domain a boot needs.
|
|
10
10
|
module Hecks
|
|
11
|
+
# The `.behaviors` toolkit: the DSL a `.behaviors` file is written against
|
|
12
|
+
# (`Hecks.behaviors`, dsl.rb), the plain-Struct IR it builds (this file), the runner
|
|
13
|
+
# that boots a suite's domain and checks each test (runner.rb, expectations.rb), and
|
|
14
|
+
# an rspec shim for a consuming app's own suite (rspec.rb).
|
|
11
15
|
module Behaviors
|
|
12
16
|
TestSetup = Struct.new(:command, :args, keyword_init: true)
|
|
13
17
|
|
|
14
18
|
TestCase = Struct.new(:description, :tests_command, :on_aggregate, :kind,
|
|
15
19
|
:setups, :input, :expect, keyword_init: true) do
|
|
20
|
+
# Tells whether `tests_command` is already a fully qualified verb name.
|
|
21
|
+
#
|
|
16
22
|
# An already-dotted tests_command is a literal FQN; otherwise `on:`
|
|
17
23
|
# composes with the domain name resolved once setups/the tested
|
|
18
24
|
# dispatch actually run (see Expectations — the domain isn't known
|
|
19
25
|
# until `loads` is booted, so it's a runtime concern, not a field
|
|
20
26
|
# here).
|
|
27
|
+
#
|
|
28
|
+
# @return [Boolean] true when `tests_command` contains a `.`
|
|
21
29
|
def dotted? = tests_command.to_s.include?(".")
|
|
22
30
|
|
|
31
|
+
# Tells whether this test case exercises a query rather than a command.
|
|
32
|
+
#
|
|
33
|
+
# @return [Boolean] true when `kind` is `:query`
|
|
23
34
|
def query? = kind == :query
|
|
24
35
|
end
|
|
25
36
|
|
|
@@ -21,6 +21,14 @@ module Hecks
|
|
|
21
21
|
module RSpec
|
|
22
22
|
module_function
|
|
23
23
|
|
|
24
|
+
# Parses one `.behaviors` file and registers an rspec example group for it, one
|
|
25
|
+
# `it` per test named by the test's own description.
|
|
26
|
+
#
|
|
27
|
+
# @param path [String] the `.behaviors` file's path
|
|
28
|
+
# @return [void]
|
|
29
|
+
# @raise [RuntimeError] not raised by this call itself; wraps `parsed.parse_error`'s
|
|
30
|
+
# message and is raised only when rspec later runs the generated "loads without a
|
|
31
|
+
# parse error" example, so it surfaces as that example's failure
|
|
24
32
|
def describe_file(path)
|
|
25
33
|
parsed = Behaviors.parse(path)
|
|
26
34
|
|
|
@@ -45,6 +45,10 @@ module Hecks
|
|
|
45
45
|
# suite from whatever loaded before it in a sweep (a real bug in a
|
|
46
46
|
# prior port of this idea: compared only against nil, so after the
|
|
47
47
|
# first successful file in a sweep it stayed non-nil forever).
|
|
48
|
+
# @param path [String] the `.behaviors` file's path
|
|
49
|
+
# @return [Behaviors::ParseResult] `suite` holding the built `BehaviorsSuite` and
|
|
50
|
+
# `parse_error` nil on success; `suite` nil and `parse_error` a String describing
|
|
51
|
+
# a raised exception, or the file loading without calling `Hecks.behaviors`
|
|
48
52
|
def parse(path)
|
|
49
53
|
path = File.expand_path(path)
|
|
50
54
|
previous_path = loading_path
|
|
@@ -66,6 +70,11 @@ module Hecks
|
|
|
66
70
|
end
|
|
67
71
|
|
|
68
72
|
# One `.behaviors` file → `FileResult`, every test actually run.
|
|
73
|
+
#
|
|
74
|
+
# @param path [String] the `.behaviors` file's path
|
|
75
|
+
# @return [Behaviors::FileResult] `parse_error` and empty `runs` on a parse
|
|
76
|
+
# failure; otherwise `parse_error` nil and `runs` one `Expectations::Result`
|
|
77
|
+
# per test
|
|
69
78
|
def run(path)
|
|
70
79
|
parsed = parse(path)
|
|
71
80
|
return FileResult.new(path: parsed.path, parse_error: parsed.parse_error, runs: []) if parsed.parse_error
|
|
@@ -78,12 +87,22 @@ module Hecks
|
|
|
78
87
|
# it actually found — a sweep that goes green without saying how
|
|
79
88
|
# much it looked at is indistinguishable from one that found
|
|
80
89
|
# nothing to look at.
|
|
90
|
+
#
|
|
91
|
+
# @param dir [String] the directory to search, recursively, for `.behaviors` files
|
|
92
|
+
# @return [Behaviors::SweepResult] `files_swept` the count found, `files` one
|
|
93
|
+
# `FileResult` per file, and `summary` the aggregate counts `summarize` returns
|
|
81
94
|
def run_all(dir)
|
|
82
95
|
files = Dir.glob(File.join(dir, "**", "*.behaviors"))
|
|
83
96
|
results = files.map { |path| run(path) }
|
|
84
97
|
SweepResult.new(root: dir, files_swept: files.size, files: results, summary: summarize(results))
|
|
85
98
|
end
|
|
86
99
|
|
|
100
|
+
# Tallies a sweep's results into counts by outcome.
|
|
101
|
+
#
|
|
102
|
+
# @param results [Array<Behaviors::FileResult>] the swept files' results
|
|
103
|
+
# @return [Hash{Symbol => Integer}] `:files` the file count, `:parse_errors` files
|
|
104
|
+
# that failed to parse, `:total` tests actually run across every file, `:passed`,
|
|
105
|
+
# `:failed` and `:errored` counting each `Expectations::Result#status`
|
|
87
106
|
def summarize(results)
|
|
88
107
|
runs = results.flat_map(&:runs)
|
|
89
108
|
{
|
data/lib/hecks/behaviors.rb
CHANGED
|
@@ -13,6 +13,14 @@ module Hecks
|
|
|
13
13
|
# behaviors suite is a test artifact a runner reads on demand, never
|
|
14
14
|
# a thing a live domain boot needs, so it has no business landing in
|
|
15
15
|
# `Runtime.current_registry`.
|
|
16
|
+
#
|
|
17
|
+
# @param name [String] the suite's declared name
|
|
18
|
+
# @yield the suite's body, evaluated against a `Behaviors::BehaviorsBuilder`
|
|
19
|
+
# @return [Behaviors::BehaviorsSuite] the built suite, also stashed as
|
|
20
|
+
# `Behaviors.last_suite`
|
|
21
|
+
# @raise [Behaviors::LoadOutsideRunner] if called outside `Behaviors.run(path)`
|
|
22
|
+
# @raise [Behaviors::Malformed] if the suite declares no `vision` or `loads`, or
|
|
23
|
+
# any of its `test` blocks is malformed
|
|
16
24
|
def behaviors(name, &)
|
|
17
25
|
path = Behaviors.loading_path or
|
|
18
26
|
raise Behaviors::LoadOutsideRunner,
|
|
@@ -80,6 +80,36 @@ module Hecks
|
|
|
80
80
|
# Assigns what the language declares, then hands off to the
|
|
81
81
|
# behaviour's own `settle` — derived identity, name indexes and
|
|
82
82
|
# owner stamping, none of which the declaration states.
|
|
83
|
+
#
|
|
84
|
+
# @param name [String, Symbol] the aggregate's declared name
|
|
85
|
+
# @param description [String, nil] the aggregate's declared prose description
|
|
86
|
+
# @param attributes [Array<Bluebook::Attribute>] the aggregate's declared fields
|
|
87
|
+
# @param value_objects [Array<Class>] the value object classes (`Bluebook::ValueObject`
|
|
88
|
+
# subclasses) declared on this aggregate
|
|
89
|
+
# @param commands [Array<Class>] the command classes (`Bluebook::Command` subclasses)
|
|
90
|
+
# declared on this aggregate
|
|
91
|
+
# @param invariants [Array<Bluebook::Invariant>] the aggregate-level rules that must
|
|
92
|
+
# always hold
|
|
93
|
+
# @param preconditions [Array<Bluebook::Given>] the aggregate's own named `given`s, a
|
|
94
|
+
# referencing command's own resolved `givens` entry can point back at
|
|
95
|
+
# @param projected_fields [Array<Bluebook::ProjectedField>] the declared `projects`
|
|
96
|
+
# fields, copied from another aggregate's own state by the rebuild sweep
|
|
97
|
+
# @param identified_by [String, Symbol, Array<String, Symbol>] the identity path(s)
|
|
98
|
+
# this aggregate is addressed by
|
|
99
|
+
# @param lifecycle [Bluebook::Lifecycle, nil] the aggregate's declared state machine,
|
|
100
|
+
# or `nil` if it declares none
|
|
101
|
+
# @param entities [Array<Class>] the entity classes (`Bluebook::Entity` subclasses)
|
|
102
|
+
# nested directly under this aggregate
|
|
103
|
+
# @param queries [Array<Bluebook::Query>] the queries declared directly on this
|
|
104
|
+
# aggregate
|
|
105
|
+
# @param policies [Array<Bluebook::Policy>] the reactions hoisted onto this aggregate
|
|
106
|
+
# from the chapter that assembled it
|
|
107
|
+
# @param ports [Array<Bluebook::DomainPort>] the aggregate-scoped ports attached after
|
|
108
|
+
# this aggregate was built
|
|
109
|
+
# @param reference_targets [Array<String>] the name of every aggregate this one points
|
|
110
|
+
# at with an aggregate-level `reference_to`
|
|
111
|
+
# @param provenance [Object, nil] the aggregate's declared canonical source, captured
|
|
112
|
+
# exactly as written, or `nil` if it declares none
|
|
83
113
|
def initialize(name:, description: nil, attributes: [], value_objects: [],
|
|
84
114
|
commands: [], invariants: [], preconditions: [], projected_fields: [], identified_by: [], lifecycle: nil,
|
|
85
115
|
entities: [], queries: [], policies: [], ports: [], reference_targets: [],
|
|
@@ -10,10 +10,17 @@ module Hecks
|
|
|
10
10
|
# It decides nothing. Whether a declaration is admissible was settled on the
|
|
11
11
|
# way in, by the language.
|
|
12
12
|
class AggregateAssembly
|
|
13
|
+
# @param row [Hash{Symbol => Object}] one declared aggregate's raw contract data,
|
|
14
|
+
# as `@declaration[:aggregates]` carries it
|
|
13
15
|
def initialize(row)
|
|
14
16
|
@row = row
|
|
15
17
|
end
|
|
16
18
|
|
|
19
|
+
# Builds the aggregate's whole owner-chain graph — its value objects, commands,
|
|
20
|
+
# entities, queries and lifecycle — from its raw declaration.
|
|
21
|
+
#
|
|
22
|
+
# @return [Bluebook::Aggregate] the built aggregate, with every reference stamped
|
|
23
|
+
# to resolve against it
|
|
17
24
|
def aggregate
|
|
18
25
|
shapes = Array(@row[:value_objects]).map { |shape| value_object(shape) }
|
|
19
26
|
commands = Array(@row[:commands]).map { |verb| Build.call("Command", verb) }
|
|
@@ -3,10 +3,10 @@ module Hecks
|
|
|
3
3
|
class Assembly
|
|
4
4
|
# One way to build a construct, for every construct.
|
|
5
5
|
#
|
|
6
|
-
#
|
|
7
|
-
# `
|
|
8
|
-
#
|
|
9
|
-
#
|
|
6
|
+
# Not a method per category — `value_object(row)`, `command(row)`,
|
|
7
|
+
# `policy(row)`, each gathering the same keywords the contract already
|
|
8
|
+
# names. This reads the contract instead, so adding a field to the
|
|
9
|
+
# language and forgetting to assemble it is caught by the coverage gate
|
|
10
10
|
# rather than by nobody.
|
|
11
11
|
#
|
|
12
12
|
# `make` is the only branch, and it is a real one: a construct that became a
|
|
@@ -16,6 +16,16 @@ module Hecks
|
|
|
16
16
|
module Build
|
|
17
17
|
module_function
|
|
18
18
|
|
|
19
|
+
# Builds one construct from its declared row, reading which keywords it takes
|
|
20
|
+
# and how to read each one off `Assembly.contract(category)`.
|
|
21
|
+
#
|
|
22
|
+
# @param category [String] the contract's category name, such as `"Aggregate"`
|
|
23
|
+
# or `"Command"`
|
|
24
|
+
# @param row [Hash{Symbol => Object}] the construct's own declared row
|
|
25
|
+
# @param extra [Hash{Symbol => Object}] keywords the caller supplies directly,
|
|
26
|
+
# such as already-built children the contract itself cannot derive
|
|
27
|
+
# @return [Object] the built construct: an instance for a category whose
|
|
28
|
+
# `make` is `:new`, or a class for one whose `make` is `:declare`
|
|
19
29
|
def call(category, row, extra = {})
|
|
20
30
|
contract = Assembly.contract(category)
|
|
21
31
|
keywords = contract.fields.to_h { |keyword, (key, reader)| [keyword, read(reader, row[key])] }
|
|
@@ -23,12 +33,23 @@ module Hecks
|
|
|
23
33
|
holder(contract).public_send(contract.make, **keywords, **extra)
|
|
24
34
|
end
|
|
25
35
|
|
|
36
|
+
# Resolves the class or module a contract's construct is built through.
|
|
37
|
+
#
|
|
38
|
+
# @param contract [Bluebook::Assembly::Contract] the category's field contract
|
|
39
|
+
# @return [Module] the holder that answers `contract.make` (`.new` or `.declare`)
|
|
40
|
+
# @raise [ArgumentError] if the contract names no holder to build through
|
|
26
41
|
def holder(contract)
|
|
27
42
|
contract.holder or raise ArgumentError, "#{contract} holds nothing that can be built"
|
|
28
43
|
end
|
|
29
44
|
|
|
30
45
|
# A reader is a Marks method, a list of them, or one of three spellings that
|
|
31
46
|
# need no decoding at all.
|
|
47
|
+
#
|
|
48
|
+
# @param reader [Symbol, Array, nil] the contract field's reader: `:plain`,
|
|
49
|
+
# `:identity`, `:flag`, `[:each, marks_method]`, `[:option, name]`, or a bare
|
|
50
|
+
# `Marks` method name
|
|
51
|
+
# @param value [Object] the raw declared value to read
|
|
52
|
+
# @return [Object] the value read through `reader`
|
|
32
53
|
def read(reader, value)
|
|
33
54
|
case reader
|
|
34
55
|
when :plain then value
|