hecks 1.0.2 → 1.1.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 +3 -3
- data/lib/hecks/adapters/driven/d1.rb +48 -28
- data/lib/hecks/adapters/driven/folder.rb +10 -3
- data/lib/hecks/adapters/driven/google_authentication.rb +2 -4
- data/lib/hecks/adapters/driven/heki/journal.rb +1 -1
- data/lib/hecks/adapters/driven/heki.rb +3 -5
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +4 -1
- data/lib/hecks/adapters/driven/lambda.rb +2 -2
- data/lib/hecks/adapters/driven/local_storage.adapter +3 -0
- data/lib/hecks/adapters/driven/local_storage.rb +120 -0
- data/lib/hecks/adapters/driven/memory.rb +48 -3
- data/lib/hecks/adapters/driven/postgres/codec.rb +3 -1
- data/lib/hecks/adapters/driven/postgres/outbox.rb +97 -0
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +3 -2
- data/lib/hecks/adapters/driven/postgres.rb +25 -14
- data/lib/hecks/adapters/driven/prism.rb +11 -0
- data/lib/hecks/adapters/driven/sql_query_builder.rb +45 -20
- data/lib/hecks/adapters/driven/sqlite/codec.rb +3 -1
- data/lib/hecks/adapters/driven/sqlite/projection.rb +17 -1
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +23 -1
- data/lib/hecks/adapters/driven/sqlite.rb +87 -8
- data/lib/hecks/adapters/driven.rb +4 -0
- data/lib/hecks/adapters.rb +4 -0
- data/lib/hecks/behaviors/dsl.rb +7 -0
- data/lib/hecks/behaviors/expectations.rb +32 -4
- data/lib/hecks/behaviors/rspec.rb +2 -0
- data/lib/hecks/behaviors/runner.rb +4 -0
- data/lib/hecks/bluebook/aggregate.rb +6 -5
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +4 -4
- data/lib/hecks/bluebook/assembly/build.rb +6 -3
- data/lib/hecks/bluebook/assembly/contract.rb +5 -1
- data/lib/hecks/bluebook/assembly/contracts.rb +4 -0
- data/lib/hecks/bluebook/assembly/marks.rb +2 -1
- data/lib/hecks/bluebook/assembly.rb +1 -3
- data/lib/hecks/bluebook/attribute.rb +6 -0
- data/lib/hecks/bluebook/behaviour/policy.rb +18 -0
- data/lib/hecks/bluebook/behaviour/process_manager.rb +17 -1
- data/lib/hecks/bluebook/behaviour/traits.rb +1 -1
- data/lib/hecks/bluebook/command.rb +6 -5
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +4 -1
- data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +524 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +19 -472
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +14 -6
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +5 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +848 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +25 -784
- data/lib/hecks/bluebook/dsl/command_builder.rb +53 -13
- data/lib/hecks/bluebook/dsl/const_shim.rb +6 -0
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +20 -7
- data/lib/hecks/bluebook/dsl/entity_builder.rb +27 -6
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +12 -2
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +8 -1
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +24 -12
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +40 -1
- data/lib/hecks/bluebook/dsl/policy_builder.rb +10 -1
- data/lib/hecks/bluebook/dsl/port_builder.rb +6 -1
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +33 -12
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +56 -12
- data/lib/hecks/bluebook/dsl/query_builder.rb +7 -1
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +11 -3
- data/lib/hecks/bluebook/dsl/rule_reference.rb +3 -1
- data/lib/hecks/bluebook/dsl/translation_builder.rb +13 -2
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +8 -1
- data/lib/hecks/bluebook/dsl/word_gate.rb +5 -0
- data/lib/hecks/bluebook/dsl/world_builder.rb +13 -3
- data/lib/hecks/bluebook/entity.rb +5 -3
- data/lib/hecks/bluebook/expression/ast_json.rb +88 -9
- data/lib/hecks/bluebook/expression/ast_reader.rb +107 -0
- data/lib/hecks/bluebook/expression/canonical_form.rb +7 -0
- data/lib/hecks/bluebook/expression/evaluator.rb +64 -11
- data/lib/hecks/bluebook/expression/projection.json +12 -0
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +4 -3
- data/lib/hecks/bluebook/expression/resolver.rb +83 -11
- data/lib/hecks/bluebook/expression.rb +1 -0
- data/lib/hecks/bluebook/hexagon.rb +10 -0
- data/lib/hecks/bluebook/lifecycle.rb +9 -0
- data/lib/hecks/bluebook/meta_validator/judge.rb +10 -4
- data/lib/hecks/bluebook/meta_validator/plan.rb +4 -0
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/shapes.rb +3 -3
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +7 -4
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +42 -3
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +1 -1
- data/lib/hecks/bluebook/meta_validator.rb +5 -5
- data/lib/hecks/bluebook/model_check.rb +119 -82
- data/lib/hecks/bluebook/pattern_subset.rb +9 -0
- data/lib/hecks/bluebook/policy.rb +2 -1
- data/lib/hecks/bluebook/process_manager.rb +6 -0
- data/lib/hecks/bluebook/project_register.rb +45 -25
- data/lib/hecks/bluebook/query.rb +4 -0
- data/lib/hecks/bluebook/read_model.rb +2 -2
- data/lib/hecks/bluebook/smoke_test.rb +5 -0
- data/lib/hecks/bluebook/synthesizer.rb +2 -1
- data/lib/hecks/bluebook/translation.rb +3 -0
- data/lib/hecks/bluebook/value_object.rb +2 -2
- data/lib/hecks/codemod.rb +13 -0
- data/lib/hecks/doc/reference.rb +17 -11
- data/lib/hecks/facade/cli_runner.rb +2 -2
- data/lib/hecks/facade/command_request.rb +11 -11
- data/lib/hecks/facade/handle.rb +3 -3
- data/lib/hecks/facade/surface/aggregate_door.rb +52 -38
- data/lib/hecks/facade.rb +4 -0
- data/lib/hecks/forms/app.rb +10 -6
- data/lib/hecks/forms/command_form_renderer.rb +3 -3
- data/lib/hecks/forms/field_renderer.rb +14 -9
- data/lib/hecks/forms/field_shape.rb +7 -2
- data/lib/hecks/forms/html.rb +5 -2
- data/lib/hecks/forms/index_renderer.rb +1 -1
- data/lib/hecks/forms/page.rb +7 -3
- data/lib/hecks/forms/query_form_renderer.rb +3 -2
- data/lib/hecks/forms/record_renderer.rb +22 -16
- data/lib/hecks/forms.rb +4 -0
- data/lib/hecks/fqn.rb +21 -9
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +30 -8
- data/lib/hecks/fuzzing/invalid_value_generator.rb +0 -1
- data/lib/hecks/fuzzing/isolated_boot.rb +14 -6
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +264 -0
- data/lib/hecks/fuzzing/properties/guards.rb +251 -0
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +351 -0
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +99 -0
- data/lib/hecks/fuzzing/properties/querying.rb +206 -0
- data/lib/hecks/fuzzing/properties.rb +41 -1065
- data/lib/hecks/fuzzing/replay.rb +60 -16
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +6 -2
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +2 -1
- data/lib/hecks/fuzzing/sequence_generator.rb +0 -1
- data/lib/hecks/fuzzing/value_generator.rb +2 -2
- data/lib/hecks/grammar/evolve.rb +34 -10
- data/lib/hecks/grammar/expression_operators.json +124 -0
- data/lib/hecks/grammar.rb +15 -5
- data/lib/hecks/ir.rb +9 -0
- data/lib/hecks/language/bluebook/aggregate.bluebook +4 -2
- data/lib/hecks/language/bluebook/command.bluebook +2 -1
- data/lib/hecks/language/bluebook/entity.bluebook +2 -1
- data/lib/hecks/language/bluebook/projection.bluebook +2 -1
- data/lib/hecks/language/bluebook/query.bluebook +4 -2
- data/lib/hecks/language/bluebook/shape.bluebook +2 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -0
- data/lib/hecks/literal.rb +18 -8
- data/lib/hecks/naming.rb +6 -3
- data/lib/hecks/ports/access_control.rb +1 -1
- data/lib/hecks/ports/agent.rb +5 -3
- data/lib/hecks/ports/authentication.rb +3 -2
- data/lib/hecks/ports/authorization.rb +1 -1
- data/lib/hecks/ports/clock.rb +1 -1
- data/lib/hecks/ports/extraction.rb +5 -1
- data/lib/hecks/ports/identity_assignment.rb +1 -1
- data/lib/hecks/ports/identity_generation.rb +1 -1
- data/lib/hecks/ports/identity_resolution.rb +1 -1
- data/lib/hecks/ports/loading.rb +4 -1
- data/lib/hecks/ports/persistence/append_only.rb +40 -4
- data/lib/hecks/ports/persistence/binding_policy.rb +1 -1
- data/lib/hecks/ports/persistence/plugin.rb +3 -0
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -1
- data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +5 -5
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +14 -5
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +4 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +80 -16
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +23 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +43 -5
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +25 -4
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +49 -9
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +10 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +49 -38
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +23 -13
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +19 -21
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +27 -9
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +9 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +25 -10
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +2 -6
- data/lib/hecks/ports/persistence/plugins/era.rb +3 -1
- data/lib/hecks/ports/persistence/remote_runtime.rb +1 -1
- data/lib/hecks/ports/persistence.rb +7 -3
- data/lib/hecks/ports/projection.rb +13 -5
- data/lib/hecks/ports/query/in_memory.rb +8 -2
- data/lib/hecks/ports/query.rb +1 -3
- data/lib/hecks/ports.rb +2 -0
- data/lib/hecks/projections/diagrams.rb +11 -3
- data/lib/hecks/projections/ir.rb +2 -0
- data/lib/hecks/projections/model/deviations.rb +2 -1
- data/lib/hecks/projections/model.rb +15 -10
- data/lib/hecks/projections/vocabulary.rb +1 -3
- data/lib/hecks/projector/cli_projector.rb +49 -29
- data/lib/hecks/projector/docs_projector.rb +21 -12
- data/lib/hecks/projector/exporter.rb +10 -1
- data/lib/hecks/projector/narrate_projector.rb +83 -25
- data/lib/hecks/projector.rb +2 -2
- data/lib/hecks/query_ir.rb +60 -36
- data/lib/hecks/query_specification/common/authorization_spec.rb +1 -1
- data/lib/hecks/query_specification/common/comparators.rb +4 -0
- data/lib/hecks/query_specification/common/comparison.rb +8 -1
- data/lib/hecks/query_specification/common/dsl.rb +5 -0
- data/lib/hecks/query_specification/common/null_policy.rb +6 -2
- data/lib/hecks/query_specification/common/options.rb +17 -5
- data/lib/hecks/query_specification/field_path.rb +4 -4
- data/lib/hecks/query_specification/read_model/specification.rb +5 -2
- data/lib/hecks/router/namespace_installer.rb +22 -10
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +57 -22
- data/lib/hecks/runtime/command_interpreter.rb +104 -65
- data/lib/hecks/runtime/command_rules/admissibility.rb +26 -8
- data/lib/hecks/runtime/command_rules/arithmetic.rb +35 -14
- data/lib/hecks/runtime/dependency_planning.rb +33 -21
- data/lib/hecks/runtime/dispatcher.rb +41 -7
- data/lib/hecks/runtime/entity_element.rb +25 -6
- data/lib/hecks/runtime/entity_interpreter.rb +28 -15
- data/lib/hecks/runtime/errors.rb +28 -2
- data/lib/hecks/runtime/instance.rb +9 -3
- data/lib/hecks/runtime/interpreting.rb +29 -2
- data/lib/hecks/runtime/loader.rb +21 -0
- data/lib/hecks/runtime/outbox.rb +365 -0
- data/lib/hecks/runtime/policy_interpreter.rb +32 -17
- data/lib/hecks/runtime/query_interpreter.rb +36 -16
- data/lib/hecks/runtime/reaction_invocation.rb +35 -22
- data/lib/hecks/runtime/read_model_interpreter.rb +26 -3
- data/lib/hecks/runtime/reference_hop.rb +1 -1
- data/lib/hecks/runtime/refusal_wording.rb +55 -49
- data/lib/hecks/runtime/registry/saga_persistence.rb +3 -1
- data/lib/hecks/runtime/registry/verification.rb +39 -2
- data/lib/hecks/runtime/registry.rb +21 -11
- data/lib/hecks/runtime/remote_dispatcher.rb +3 -1
- data/lib/hecks/runtime/routing.rb +52 -39
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +7 -7
- data/lib/hecks/runtime/saga_interpreter.rb +119 -90
- data/lib/hecks/runtime/saga_pending_dispatch.rb +6 -3
- data/lib/hecks/runtime/tenant_scope.rb +7 -0
- data/lib/hecks/runtime/value/coercion.rb +207 -48
- data/lib/hecks/runtime/value.rb +1 -1
- data/lib/hecks/runtime.rb +7 -2
- data/lib/hecks/storehouse.rb +15 -8
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +1 -0
- data/lib/hecks.rb +17 -13
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +1 -1
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +1 -1
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +1 -1
- metadata +14 -2
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
require_relative "word_gate"
|
|
2
|
+
require_relative "aggregate_builder/sealing"
|
|
2
3
|
module Hecks
|
|
3
4
|
module Bluebook
|
|
4
5
|
module DSL
|
|
6
|
+
# The `aggregate "Name" do ... end` receiver — the largest DSL builder
|
|
7
|
+
# in the language, collecting everything an aggregate declares
|
|
8
|
+
# (attributes, value objects, entities, commands, queries, policies,
|
|
9
|
+
# invariants, preconditions, projected fields, its lifecycle and
|
|
10
|
+
# identity) and assembling the final `Aggregate` IR in `#build`.
|
|
11
|
+
# `entity`/`command`/`query` only QUEUE a descriptor during
|
|
12
|
+
# `instance_eval` (`#drain_pending!` builds them, in order, once the
|
|
13
|
+
# whole block has run) so a later-declared piece can still be
|
|
14
|
+
# referenced by an earlier line — see `#drain_pending!`'s own header.
|
|
5
15
|
class AggregateBuilder
|
|
6
|
-
GRAMMAR_CONTEXT = "Aggregate"
|
|
16
|
+
GRAMMAR_CONTEXT = "Aggregate".freeze
|
|
7
17
|
|
|
8
18
|
include AttributeCollector
|
|
9
19
|
include IdentityDeclaration
|
|
10
20
|
include RuleReference
|
|
11
21
|
include WordGate
|
|
22
|
+
include Sealing
|
|
12
23
|
|
|
13
24
|
def initialize(name, chapter_named_givens: {}, chapter_pending_givens: [],
|
|
14
25
|
chapter_entity_named_givens: {}, chapter_entity_pending_givens: [])
|
|
@@ -156,12 +167,11 @@ module Hecks
|
|
|
156
167
|
# so no BOOTSTRAP_CALLS_FALLBACK entry is needed, unlike
|
|
157
168
|
# `attribute`/`role`.
|
|
158
169
|
def has_many_impl(type, as: nil, **legacy_options)
|
|
159
|
-
if MetaValidator.shadow_parsing?
|
|
160
|
-
return legacy_has_many(type, as: as, optional: legacy_options.fetch(:optional, false))
|
|
161
|
-
end
|
|
170
|
+
return legacy_has_many(type, as: as, optional: legacy_options.fetch(:optional, false)) if MetaValidator.shadow_parsing?
|
|
162
171
|
|
|
163
172
|
unless legacy_options.empty?
|
|
164
|
-
raise Malformed,
|
|
173
|
+
raise Malformed,
|
|
174
|
+
"#{@name}.has_many takes no #{legacy_options.keys.first}: — an empty list already means none"
|
|
165
175
|
end
|
|
166
176
|
|
|
167
177
|
plural = Naming.demodulise(type)
|
|
@@ -189,8 +199,8 @@ module Hecks
|
|
|
189
199
|
optional: optional)
|
|
190
200
|
end
|
|
191
201
|
|
|
192
|
-
def lifecycle_impl(field, default:, &
|
|
193
|
-
@lifecycle = LifecycleBuilder.build(field, default: default, &
|
|
202
|
+
def lifecycle_impl(field, default:, &)
|
|
203
|
+
@lifecycle = LifecycleBuilder.build(field, default: default, &)
|
|
194
204
|
end
|
|
195
205
|
|
|
196
206
|
# A piece is declared IN this aggregate — its owner is stamped by
|
|
@@ -223,8 +233,8 @@ module Hecks
|
|
|
223
233
|
@pending_queries << [name, block]
|
|
224
234
|
end
|
|
225
235
|
|
|
226
|
-
def policy_impl(name, &
|
|
227
|
-
reaction = PolicyBuilder.build(name, &
|
|
236
|
+
def policy_impl(name, &)
|
|
237
|
+
reaction = PolicyBuilder.build(name, &)
|
|
228
238
|
reaction.aggregate = @name
|
|
229
239
|
@policies << reaction
|
|
230
240
|
end
|
|
@@ -578,469 +588,6 @@ module Hecks
|
|
|
578
588
|
def legacy_has_one(type, as:, optional: false)
|
|
579
589
|
reference_to_impl(type, as: as || Naming.snake(Naming.demodulise(type)).to_sym, optional: optional)
|
|
580
590
|
end
|
|
581
|
-
|
|
582
|
-
# Every reference is told which Aggregate declares it, so it can
|
|
583
|
-
# find the chapter and resolve its target.
|
|
584
|
-
#
|
|
585
|
-
# Stamped HERE, at build, rather than at `reference_to`, because a command
|
|
586
|
-
# builder does not hold the aggregate and should not learn to. And
|
|
587
|
-
# deliberately across every list that can carry one — a reference the walk
|
|
588
|
-
# missed would resolve to nil, and `resolve_references` SKIPS a nil target,
|
|
589
|
-
# so the guarantee would go quiet instead of going red. That is the exact
|
|
590
|
-
# shape of the bug that let an Account belong to an unregistered customer
|
|
591
|
-
# fourteen times over.
|
|
592
|
-
def stamp_references(ir)
|
|
593
|
-
reference_bearing_attributes.each { |attribute| attribute.type.declared_in = ir }
|
|
594
|
-
end
|
|
595
|
-
|
|
596
|
-
# AN OWNED PIECE'S OWN `reference_to` IS AN EDGE THIS AGGREGATE
|
|
597
|
-
# POINTS ACROSS TOO (S9, ADR 0025 — "entity/aggregate shared
|
|
598
|
-
# vocabulary") — a ring closing through a contained piece (Board
|
|
599
|
-
# -> Board::Card -> Product -> Board) is the same "no boundary
|
|
600
|
-
# anyone can reason about alone" `validate_no_bidirectional_
|
|
601
|
-
# references!` already refuses for a direct aggregate-to-
|
|
602
|
-
# aggregate ring; it was invisible before this because only
|
|
603
|
-
# `AggregateBuilder#reference_to` ever fed `@reference_targets`,
|
|
604
|
-
# never `EntityBuilder#reference_to`. Command/query reference
|
|
605
|
-
# ARGUMENTS are deliberately excluded — they are data flowing
|
|
606
|
-
# through a dispatch, not persisted state the graph a cycle
|
|
607
|
-
# means anything over.
|
|
608
|
-
def entity_reference_targets
|
|
609
|
-
@entities.flat_map { |entity| entity.attributes.select(&:reference?).map { |a| a.type.target_name.to_s } }
|
|
610
|
-
end
|
|
611
|
-
|
|
612
|
-
def reference_bearing_attributes
|
|
613
|
-
lists = [attributes, *@commands.map(&:attributes), *@queries.map(&:attributes)]
|
|
614
|
-
@entities.each do |entity|
|
|
615
|
-
lists << entity.attributes
|
|
616
|
-
lists.concat(entity.commands.map(&:attributes))
|
|
617
|
-
lists.concat(entity.queries.map(&:attributes))
|
|
618
|
-
end
|
|
619
|
-
|
|
620
|
-
lists.flatten.select(&:reference?)
|
|
621
|
-
end
|
|
622
|
-
|
|
623
|
-
# A mutation must name a field the aggregate actually HAS.
|
|
624
|
-
#
|
|
625
|
-
# NOT moved to the language, and deliberately so. The language says only
|
|
626
|
-
# `given("a mutation names a target") { !target.value.to_s.empty? }` —
|
|
627
|
-
# non-emptiness — because saying more means reaching a list that lives on
|
|
628
|
-
# a DIFFERENT root : a command's changes hang off Command, the fields they
|
|
629
|
-
# name hang off Aggregate, and a given is a closed predicate over its own
|
|
630
|
-
# state. Aggregate.Seal is the right shape and cannot see commands ; the
|
|
631
|
-
# reference trick that rescued "attributes use value-object types" needs a
|
|
632
|
-
# root to point at, and an aggregate's fields are a value-object list, not
|
|
633
|
-
# roots. This is the second rule that cannot port for that reason — the
|
|
634
|
-
# first is read-model uniqueness — and both wait on the same thing : a
|
|
635
|
-
# quantifier, or fields promoted to roots.
|
|
636
|
-
#
|
|
637
|
-
# So it lives here, at build, where every declaration is present. Found by
|
|
638
|
-
# writing `then_set :disputed_by` on CardPayment before the field existed :
|
|
639
|
-
# it wrote into nothing, refused nothing, and every check stayed green.
|
|
640
|
-
# A DEFAULT FILLS THE SHAPE IT IS DECLARED ON, or it fills nothing.
|
|
641
|
-
#
|
|
642
|
-
# `attribute :cover, one_of("covered", "open"), default: "open"` builds
|
|
643
|
-
# cleanly and then refuses EVERY create at dispatch — "cover is a Cover,
|
|
644
|
-
# pass its fields as an object" — because the value object wants its
|
|
645
|
-
# fields and got a bare string. The bluebook is wrong at the line where
|
|
646
|
-
# it is written and says so nowhere near it.
|
|
647
|
-
#
|
|
648
|
-
# It cost a corpus member 33 refusals out of 40 steps, with every gate
|
|
649
|
-
# green throughout: the refusals were perfectly consistent, which is
|
|
650
|
-
# consistency about nothing. `till.bluebook` has always had the right shape
|
|
651
|
-
# — `default: { cents: 0 }`.
|
|
652
|
-
#
|
|
653
|
-
# A PRIMITIVE takes a scalar and a VALUE OBJECT takes its fields, so the
|
|
654
|
-
# test is simply which one the type names. Nothing here guesses at the
|
|
655
|
-
# keys: a default that is a Hash is left to `Value.for_attribute`, which
|
|
656
|
-
# is where a wrong FIELD belongs.
|
|
657
|
-
def seal_defaults
|
|
658
|
-
# `closed_sets` TOO, not only `@value_objects` — the exact gap
|
|
659
|
-
# this method's own comment names: an inline `one_of(...)`
|
|
660
|
-
# synthesises its value object through `closed_sets`
|
|
661
|
-
# (AttributeCollector#synthesise_closed_set), never installed
|
|
662
|
-
# into `@value_objects` until `#build` merges them (see
|
|
663
|
-
# `#build`'s own `@value_objects + closed_sets`, and
|
|
664
|
-
# `declared_value_object`'s identical merge). Checking
|
|
665
|
-
# `@value_objects` alone made this exact attribute — a bare
|
|
666
|
-
# default on an inline closed set — invisible to the one
|
|
667
|
-
# check meant to catch it.
|
|
668
|
-
shapes = (@value_objects + closed_sets).map { |shape| shape.hecks_name.to_s }
|
|
669
|
-
|
|
670
|
-
attributes.each do |attribute|
|
|
671
|
-
next if attribute.default.nil? || attribute.default.is_a?(Hash)
|
|
672
|
-
next unless shapes.include?(attribute.type.to_s)
|
|
673
|
-
|
|
674
|
-
raise Malformed,
|
|
675
|
-
"#{@name}.#{attribute.name} defaults to #{attribute.default.inspect}, but " \
|
|
676
|
-
"#{attribute.type} is a value object — a default fills its FIELDS " \
|
|
677
|
-
"(default: { ... }), and a bare value refuses every create instead"
|
|
678
|
-
end
|
|
679
|
-
end
|
|
680
|
-
|
|
681
|
-
# A command's `from:` guard needs a lifecycle field to check
|
|
682
|
-
# against — declared at BUILD time (S10, ADR 0025), the same
|
|
683
|
-
# point every other "does this actually resolve" check in this
|
|
684
|
-
# file runs, rather than left to crash `enforce_lifecycle_
|
|
685
|
-
# guard` the first time such a command is ever dispatched.
|
|
686
|
-
def seal_lifecycle_guards
|
|
687
|
-
return if @lifecycle
|
|
688
|
-
|
|
689
|
-
@commands.each do |command|
|
|
690
|
-
next unless command.from
|
|
691
|
-
|
|
692
|
-
raise Malformed,
|
|
693
|
-
"#{@name}.#{command.hecks_name} guards from: #{Array(command.from).inspect}, but " \
|
|
694
|
-
"#{@name} declares no lifecycle — from: checks a lifecycle field, and there is " \
|
|
695
|
-
"none here to check"
|
|
696
|
-
end
|
|
697
|
-
end
|
|
698
|
-
|
|
699
|
-
# `projects`'s OWN half of "does this actually resolve" (S12,
|
|
700
|
-
# ADR 0025) — the LOCAL half only: `reference` must name a real
|
|
701
|
-
# reference-typed attribute this aggregate declares, and
|
|
702
|
-
# `name` must not collide with an attribute already declared
|
|
703
|
-
# (a projected field is its own kind of field, never a second
|
|
704
|
-
# spelling of one that already exists). The TARGET aggregate's
|
|
705
|
-
# own field is checked separately, once every aggregate in the
|
|
706
|
-
# chapter is real — see BluebookBuilder#validate_projected_
|
|
707
|
-
# fields!'s own comment for why that half cannot happen here.
|
|
708
|
-
def seal_projected_fields
|
|
709
|
-
declared = attributes.map { |attribute| attribute.name.to_sym }
|
|
710
|
-
|
|
711
|
-
@projected_fields.each do |field|
|
|
712
|
-
if declared.include?(field.name)
|
|
713
|
-
raise Malformed,
|
|
714
|
-
"#{@name}.projects :#{field.name} names a field #{@name} already declares — " \
|
|
715
|
-
"a projected field is never a second spelling of one that already exists"
|
|
716
|
-
end
|
|
717
|
-
|
|
718
|
-
reference_attribute = attributes.find { |attribute| attribute.name == field.reference }
|
|
719
|
-
unless reference_attribute&.reference?
|
|
720
|
-
raise Malformed,
|
|
721
|
-
"#{@name}.projects :#{field.name} reads through #{field.reference.inspect}, which " \
|
|
722
|
-
"#{@name} never declares as a reference_to — projects reads through a REFERENCE, " \
|
|
723
|
-
"never a value object or a scalar"
|
|
724
|
-
end
|
|
725
|
-
end
|
|
726
|
-
end
|
|
727
|
-
|
|
728
|
-
def seal_mutation_targets
|
|
729
|
-
known = attributes.map { |attribute| attribute.name.to_sym }
|
|
730
|
-
known << @lifecycle.field.to_sym if @lifecycle
|
|
731
|
-
|
|
732
|
-
@commands.each do |command|
|
|
733
|
-
command.mutations.each do |mutation|
|
|
734
|
-
# `:delegate` — CommandBuilder#delegates_to's own comment —
|
|
735
|
-
# targets no field of THIS aggregate at all; its `target`
|
|
736
|
-
# names an "Entity.Command" pair instead, checked when the
|
|
737
|
-
# command builds (`delegates_to`'s own `rpartition` guard)
|
|
738
|
-
# and again at dispatch time (`CommandInterpreter
|
|
739
|
-
# #step_delegate_to_entity`, which refuses a real one that
|
|
740
|
-
# names no such entity or command). Sealing THIS check
|
|
741
|
-
# against it would refuse every delegating command outright.
|
|
742
|
-
# `:corrects` — CommandBuilder#corrects_impl's own comment —
|
|
743
|
-
# targets an EVENT name, not a field either; checked instead
|
|
744
|
-
# by `seal_correction_targets`, below.
|
|
745
|
-
next if [:delegate, :corrects].include?(mutation.op)
|
|
746
|
-
next if known.include?(mutation.target.to_sym)
|
|
747
|
-
|
|
748
|
-
raise Malformed,
|
|
749
|
-
"#{@name}.#{command.hecks_name} sets #{mutation.target}, which #{@name} " \
|
|
750
|
-
"never declares — a mutation into a field that does not exist " \
|
|
751
|
-
"writes nothing and refuses nothing"
|
|
752
|
-
end
|
|
753
|
-
end
|
|
754
|
-
end
|
|
755
|
-
|
|
756
|
-
# `corrects` — CommandBuilder#corrects_impl's own comment. Runs
|
|
757
|
-
# once every command in the aggregate is known (the same reason
|
|
758
|
-
# this is a `seal_*` step rather than living in `corrects_impl`
|
|
759
|
-
# itself — a command cannot see its own siblings' `emits` while
|
|
760
|
-
# it is still being built). Two things are checked:
|
|
761
|
-
#
|
|
762
|
-
# 1. The named event must be something a SIBLING command here
|
|
763
|
-
# actually `emits` — naming an event nothing in this aggregate
|
|
764
|
-
# ever announces is a build-time authoring error. (Whether
|
|
765
|
-
# THIS record has actually emitted it YET is the dispatch-time
|
|
766
|
-
# half — CommandRules::Admissibility#enforce_correction_target.)
|
|
767
|
-
#
|
|
768
|
-
# 2. `reverses: true` derives the corrective `sets` from the
|
|
769
|
-
# ORIGINAL command's own mutations, rather than the author
|
|
770
|
-
# writing them — but only when every one of those mutations is
|
|
771
|
-
# STRUCTURALLY invertible with no runtime data: increment/
|
|
772
|
-
# decrement, same argument, opposite verb (`sign_for`'s own
|
|
773
|
-
# +1/-1 pair — CommandRules::Arithmetic applies `current +
|
|
774
|
-
# sign * amount`, so the SAME source with the OPPOSITE sign
|
|
775
|
-
# undoes it exactly). Nothing else qualifies today: `set` has
|
|
776
|
-
# no such rule at all — inverting it needs the SPECIFIC prior
|
|
777
|
-
# value at the moment the original fired, which is per-
|
|
778
|
-
# instance runtime data no build-time derivation can have;
|
|
779
|
-
# `multiply`/`clamp` are lossy by design (a clamped value's
|
|
780
|
-
# own pre-clamp magnitude is not recoverable from the mutation
|
|
781
|
-
# at all); `append`/`remove` LOOK symmetric but are not
|
|
782
|
-
# reliably so — `append`'s source is a per-field binding hash
|
|
783
|
-
# (`append: { name: :name, amount: :amount }`), `remove`'s is
|
|
784
|
-
# a single resolved value to match by equality
|
|
785
|
-
# (MutationApplier#removed), and collapsing one shape into the
|
|
786
|
-
# other correctly needs the target list's own value-object
|
|
787
|
-
# field names, not just the mutation's own recorded shape — a
|
|
788
|
-
# real gap, left for a follow-on round rather than guessed at
|
|
789
|
-
# here. Refuses rather than silently deriving something wrong
|
|
790
|
-
# — see docs/decisions/ for the ADR that draws this exact
|
|
791
|
-
# line.
|
|
792
|
-
def seal_correction_targets
|
|
793
|
-
inverse_op = { increment: :decrement, decrement: :increment }
|
|
794
|
-
emitted_by = Hash.new { |hash, key| hash[key] = [] }
|
|
795
|
-
@commands.each { |command| command.emits.each { |event_name| emitted_by[event_name] << command } }
|
|
796
|
-
|
|
797
|
-
@commands.each do |command|
|
|
798
|
-
correction = command.mutations.find { |mutation| mutation.op == :corrects }
|
|
799
|
-
next unless correction
|
|
800
|
-
|
|
801
|
-
event = correction.target
|
|
802
|
-
sources = emitted_by[event]
|
|
803
|
-
if sources.empty?
|
|
804
|
-
raise Malformed,
|
|
805
|
-
"#{@name}.#{command.hecks_name} corrects #{event.inspect}, but nothing " \
|
|
806
|
-
"declared on #{@name} ever emits it — corrects names a fact this " \
|
|
807
|
-
"aggregate actually announces, not an aspiration"
|
|
808
|
-
end
|
|
809
|
-
|
|
810
|
-
next unless correction.source[:reverses]
|
|
811
|
-
|
|
812
|
-
own_mutations = command.mutations.reject { |mutation| mutation.op == :corrects }
|
|
813
|
-
if own_mutations.any?
|
|
814
|
-
raise Malformed,
|
|
815
|
-
"#{@name}.#{command.hecks_name} declares both corrects #{event.inspect}, " \
|
|
816
|
-
"reverses: true AND its own sets — reverses: true means the correction " \
|
|
817
|
-
"is DERIVED; write one or the other, never both"
|
|
818
|
-
end
|
|
819
|
-
|
|
820
|
-
derived = sources.flat_map(&:mutations).reject { |mutation| mutation.op == :corrects }
|
|
821
|
-
unsupported = derived.reject { |mutation| inverse_op.key?(mutation.op) }
|
|
822
|
-
if unsupported.any?
|
|
823
|
-
raise Malformed,
|
|
824
|
-
"#{@name}.#{command.hecks_name} corrects #{event.inspect}, reverses: " \
|
|
825
|
-
"true, but the command(s) that emit it use " \
|
|
826
|
-
"#{unsupported.map(&:op).uniq.join(', ')} — not statically invertible " \
|
|
827
|
-
"(set needs the specific prior value, multiply/clamp are lossy) — " \
|
|
828
|
-
"declare the corrective sets by hand instead"
|
|
829
|
-
end
|
|
830
|
-
|
|
831
|
-
derived.each do |mutation|
|
|
832
|
-
command.mutations << Mutation.new(target: mutation.target, op: inverse_op.fetch(mutation.op),
|
|
833
|
-
source: mutation.source)
|
|
834
|
-
end
|
|
835
|
-
end
|
|
836
|
-
end
|
|
837
|
-
|
|
838
|
-
# A query must ask about a field the aggregate actually HAS — the same
|
|
839
|
-
# seal `then_set` gets, closing the same silence: a where over a field
|
|
840
|
-
# nothing declares matches nothing and refuses nothing, forever, on
|
|
841
|
-
# every adapter. Three more silences close with it. A dotted path may
|
|
842
|
-
# reach through the value-object graph but must LAND on a scalar
|
|
843
|
-
# member (QuerySpecification::FieldPath is the one walk every engine
|
|
844
|
-
# now shares) — landing on a value object hands SQL a JSON object
|
|
845
|
-
# where the reference interpreter unwraps a hash. An ordered
|
|
846
|
-
# comparator (lt/gt/gte/lte) must land on a numeric leaf — over text
|
|
847
|
-
# the reference interpreter quietly matches no rows while SQL
|
|
848
|
-
# compares lexicographically. And a :symbol value must name one of
|
|
849
|
-
# the query's own declared arguments, or it resolves to nil at
|
|
850
|
-
# dispatch and matches nothing.
|
|
851
|
-
ORDERED_COMPARATORS = %i[lt lte gt gte].freeze
|
|
852
|
-
|
|
853
|
-
def seal_query_targets
|
|
854
|
-
query_surfaces.each do |owner, fields, lifecycle, queries|
|
|
855
|
-
queries.each do |query|
|
|
856
|
-
query.wheres.each do |clause|
|
|
857
|
-
seal_query_field(owner, query, fields, lifecycle, clause.field)
|
|
858
|
-
seal_ordered_comparator(owner, query, fields, clause)
|
|
859
|
-
infer_local_query_argument(query, fields, lifecycle, clause)
|
|
860
|
-
seal_query_argument(owner, query, clause.value) unless clause.field.to_s.include?("/")
|
|
861
|
-
end
|
|
862
|
-
seal_query_field(owner, query, fields, lifecycle, query.order_by.field, ordering: true) if query.order_by
|
|
863
|
-
seal_query_argument(owner, query, query.limit&.value)
|
|
864
|
-
seal_query_argument(owner, query, query.offset&.value)
|
|
865
|
-
end
|
|
866
|
-
end
|
|
867
|
-
end
|
|
868
|
-
|
|
869
|
-
def query_surfaces
|
|
870
|
-
[[@name, attributes, @lifecycle, @queries]] +
|
|
871
|
-
@entities.map { |entity| ["#{@name}::#{entity.hecks_name}", entity.attributes, entity.lifecycle, entity.queries] }
|
|
872
|
-
end
|
|
873
|
-
|
|
874
|
-
# `/` CROSSES INTO ANOTHER RECORD, `.` WALKS FIELDS INSIDE THIS
|
|
875
|
-
# ONE (ADR 0025, "References") — the operator answers which
|
|
876
|
-
# kind of path this is now, not a name collision to arbitrate,
|
|
877
|
-
# so a hop is routed to its own method before any `.`-splitting
|
|
878
|
-
# runs at all; `seal_query_hop` below never sees a field this
|
|
879
|
-
# one would also have tried to resolve as a local dotted walk.
|
|
880
|
-
def seal_query_field(owner, query, fields, lifecycle, field, ordering: false)
|
|
881
|
-
return seal_query_hop(owner, query, fields, field, ordering: ordering) if field.to_s.include?("/")
|
|
882
|
-
|
|
883
|
-
name, *nested = field.to_s.split(".")
|
|
884
|
-
attribute = fields.find { |candidate| candidate.name.to_s == name }
|
|
885
|
-
if nested.empty? && attribute
|
|
886
|
-
refuse_ambiguous_comparison!(owner, query, field, attribute)
|
|
887
|
-
return
|
|
888
|
-
end
|
|
889
|
-
return if nested.empty? && lifecycle&.field.to_s == name
|
|
890
|
-
return if nested.any? && attribute && scalar_path?(attribute, nested)
|
|
891
|
-
|
|
892
|
-
if nested.any? && attribute && resolves?(attribute, nested)
|
|
893
|
-
raise Malformed,
|
|
894
|
-
"#{owner}.#{query.hecks_name} asks about #{field}, which lands on a " \
|
|
895
|
-
"value object, not a scalar — a dotted query path ends on a scalar " \
|
|
896
|
-
"member, or the engines answer it differently"
|
|
897
|
-
end
|
|
898
|
-
|
|
899
|
-
raise Malformed,
|
|
900
|
-
"#{owner}.#{query.hecks_name} asks about #{field}, which #{owner} " \
|
|
901
|
-
"never declares — a query over a field that does not exist " \
|
|
902
|
-
"matches nothing and refuses nothing"
|
|
903
|
-
end
|
|
904
|
-
|
|
905
|
-
# ORDER BY refuses a hop OUTRIGHT, right here — unlike a WHERE
|
|
906
|
-
# hop (deferred below), this doesn't need the target's shape to
|
|
907
|
-
# answer: an ask is ordered by what its own answering rows
|
|
908
|
-
# hold, and a hop answers with a candidate set, not a sort key
|
|
909
|
-
# (see Runtime::ReferenceHop).
|
|
910
|
-
#
|
|
911
|
-
# A WHERE hop is only RECOGNISED here, and CHECKED LATER. The
|
|
912
|
-
# head names one of this aggregate's own references, which is
|
|
913
|
-
# answerable now — a Reference knows its own target_name at
|
|
914
|
-
# declaration. What it points AT is not: stamp_references has
|
|
915
|
-
# already run by this point, but the chapter (Bluebook, and the
|
|
916
|
-
# owning aggregate's OWN place in it) does not exist yet, so
|
|
917
|
-
# Reference#resolve would answer nil for every target in the
|
|
918
|
-
# file, including ones declared above this one. The tail, and
|
|
919
|
-
# whether the target even exists, are BluebookBuilder's
|
|
920
|
-
# business — see validate_query_hops!, which runs once the
|
|
921
|
-
# chapter is real, for exactly the reason
|
|
922
|
-
# validate_no_bidirectional_references! already gives for
|
|
923
|
-
# living at that same later point.
|
|
924
|
-
def seal_query_hop(owner, query, fields, field, ordering:)
|
|
925
|
-
unless QuerySpecification::HopPath.hop_head?(field, fields)
|
|
926
|
-
raise Malformed,
|
|
927
|
-
"#{owner}.#{query.hecks_name} asks about #{field}, which #{owner} " \
|
|
928
|
-
"never declares — a query over a field that does not exist " \
|
|
929
|
-
"matches nothing and refuses nothing"
|
|
930
|
-
end
|
|
931
|
-
|
|
932
|
-
return unless ordering
|
|
933
|
-
|
|
934
|
-
raise Malformed,
|
|
935
|
-
"#{owner}.#{query.hecks_name} orders by #{field}, which hops through " \
|
|
936
|
-
"a reference — an ask is ordered by what its own answering rows " \
|
|
937
|
-
"hold, and a hop answers with a candidate set, not a sort key"
|
|
938
|
-
end
|
|
939
|
-
|
|
940
|
-
def seal_ordered_comparator(owner, query, fields, clause)
|
|
941
|
-
return unless ORDERED_COMPARATORS.include?(clause.op.to_s.to_sym)
|
|
942
|
-
|
|
943
|
-
# A WHERE clause hopping through a reference with an ordered
|
|
944
|
-
# comparator is legitimate ("client whose balance > 500") —
|
|
945
|
-
# unlike ORDER BY (refused outright in seal_query_field, see
|
|
946
|
-
# its own comment), a where-clause hop answers a real
|
|
947
|
-
# candidate set either way, ordered or not. Deferred for the
|
|
948
|
-
# same reason any other hop is: whether the tail is even
|
|
949
|
-
# numeric is BluebookBuilder#validate_query_hops!'s question
|
|
950
|
-
# to ask of the TARGET's shape, not this aggregate's own.
|
|
951
|
-
return if clause.field.to_s.include?("/") && QuerySpecification::HopPath.hop_head?(clause.field, fields)
|
|
952
|
-
|
|
953
|
-
name, *nested = clause.field.to_s.split(".")
|
|
954
|
-
attribute = fields.find { |candidate| candidate.name.to_s == name }
|
|
955
|
-
return if attribute &&
|
|
956
|
-
QuerySpecification::FieldPath.numeric?(attribute, nested) { |type| declared_value_object(type) }
|
|
957
|
-
|
|
958
|
-
held = attribute ? "holds no number" : "is the lifecycle field, which holds text"
|
|
959
|
-
raise Malformed,
|
|
960
|
-
"#{owner}.#{query.hecks_name} compares #{clause.field} with #{clause.op}, " \
|
|
961
|
-
"but #{clause.field} #{held} — an ordered comparison needs a numeric " \
|
|
962
|
-
"field, and over anything else the adapters answer differently or not at all"
|
|
963
|
-
end
|
|
964
|
-
|
|
965
|
-
def seal_query_argument(owner, query, value)
|
|
966
|
-
return unless value.is_a?(Symbol)
|
|
967
|
-
return if query.attribute(value)
|
|
968
|
-
|
|
969
|
-
raise Malformed,
|
|
970
|
-
"#{owner}.#{query.hecks_name} resolves :#{value} from its arguments, " \
|
|
971
|
-
"but declares no #{value} attribute — an argument that does not exist " \
|
|
972
|
-
"resolves to nil and matches nothing"
|
|
973
|
-
end
|
|
974
|
-
|
|
975
|
-
# A symbolic right-hand side is a query input. When the compared path
|
|
976
|
-
# lands on this owner's declared shape, its type is already known and
|
|
977
|
-
# repeating an `attribute` line inside the query adds no information.
|
|
978
|
-
# Reference hops are resolved only after the whole chapter has been
|
|
979
|
-
# owner-stamped; BluebookBuilder performs the identical inference for
|
|
980
|
-
# those deferred paths.
|
|
981
|
-
def infer_local_query_argument(query, fields, lifecycle, clause)
|
|
982
|
-
name = clause.value
|
|
983
|
-
return unless name.is_a?(Symbol)
|
|
984
|
-
return if query.attribute(name)
|
|
985
|
-
return if clause.field.to_s.include?("/")
|
|
986
|
-
|
|
987
|
-
head, *nested = clause.field.to_s.split(".")
|
|
988
|
-
leaf = if nested.empty? && lifecycle&.field.to_s == head
|
|
989
|
-
Attribute.new(name: name, type: String)
|
|
990
|
-
else
|
|
991
|
-
root = fields.find { |candidate| candidate.name.to_s == head }
|
|
992
|
-
found = root && QuerySpecification::FieldPath.leaf_attribute(root, nested) do |type|
|
|
993
|
-
declared_value_object(type)
|
|
994
|
-
end
|
|
995
|
-
found && Attribute.new(name: name, type: found.type, list: found.list?)
|
|
996
|
-
end
|
|
997
|
-
query.attributes << leaf if leaf
|
|
998
|
-
end
|
|
999
|
-
|
|
1000
|
-
# A BARE FIELD NAMING A VALUE OBJECT HAS TO SAY WHICH MEMBER IT
|
|
1001
|
-
# MEANS, when more than one could answer. The dotted case above
|
|
1002
|
-
# already refuses a path that lands on a value object rather than
|
|
1003
|
-
# a scalar; a bare name was returning unconditionally, so
|
|
1004
|
-
# `where(frequency: ...)` against a StatementFrequency
|
|
1005
|
-
# (cadence, retention_months, paper_fee_cents) compiled — and the
|
|
1006
|
-
# engines then disagreed about which member it meant, one taking
|
|
1007
|
-
# the FIRST numeric and another declining to unwrap at all.
|
|
1008
|
-
#
|
|
1009
|
-
# Unambiguous is: exactly one member, whatever its type, or
|
|
1010
|
-
# exactly one NUMERIC member among several (Money's `cents`
|
|
1011
|
-
# beside its `currency` — the reading every engine already
|
|
1012
|
-
# shared, and what the corpus relies on). Anything else names
|
|
1013
|
-
# its member with a dotted path, which already works.
|
|
1014
|
-
#
|
|
1015
|
-
# A list is exempt: `contains` over a `list_of` reads element
|
|
1016
|
-
# membership, not a scalar comparison, and has its own agreed
|
|
1017
|
-
# reading across the engines.
|
|
1018
|
-
def refuse_ambiguous_comparison!(owner, query, field, attribute)
|
|
1019
|
-
return if attribute.list?
|
|
1020
|
-
|
|
1021
|
-
value_object = declared_value_object(attribute.type.to_s)
|
|
1022
|
-
return unless value_object
|
|
1023
|
-
|
|
1024
|
-
members = QuerySpecification::Common::Comparison.ambiguous_members(value_object)
|
|
1025
|
-
return if members.empty?
|
|
1026
|
-
|
|
1027
|
-
raise Malformed,
|
|
1028
|
-
"#{owner}.#{query.hecks_name} asks about #{field}, which names #{attribute.type} — " \
|
|
1029
|
-
"it has #{members.size} members (#{members.join(', ')}) and no single one a " \
|
|
1030
|
-
"comparison can mean; name the member (#{field}.#{members.first})"
|
|
1031
|
-
end
|
|
1032
|
-
|
|
1033
|
-
def scalar_path?(attribute, nested)
|
|
1034
|
-
QuerySpecification::FieldPath.scalar_leaf?(attribute, nested) { |type| declared_value_object(type) }
|
|
1035
|
-
end
|
|
1036
|
-
|
|
1037
|
-
def resolves?(attribute, nested)
|
|
1038
|
-
!QuerySpecification::FieldPath.leaf_attribute(attribute, nested) { |type| declared_value_object(type) }.nil?
|
|
1039
|
-
end
|
|
1040
|
-
|
|
1041
|
-
def declared_value_object(type_name)
|
|
1042
|
-
(@value_objects + closed_sets).find { |shape| shape.hecks_name.to_s == type_name }
|
|
1043
|
-
end
|
|
1044
591
|
end
|
|
1045
592
|
end
|
|
1046
593
|
end
|
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
module Hecks
|
|
2
2
|
module Bluebook
|
|
3
3
|
module DSL
|
|
4
|
+
# Shared mixin for every DSL builder that declares attributes
|
|
5
|
+
# (Aggregate/Entity/Command/Query/PortOperation/ValueObject) — the
|
|
6
|
+
# `attribute`/`list_of`/`one_of` words themselves, closed-set
|
|
7
|
+
# synthesis, duplicate-name and pattern refusals, and the
|
|
8
|
+
# `identified_by` field/type resolution both a construct's own
|
|
9
|
+
# identity and a compound key are built from.
|
|
4
10
|
module AttributeCollector
|
|
5
11
|
ListOf = Struct.new(:type)
|
|
12
|
+
# `:values` shadows Struct#values on purpose — with one member, the
|
|
13
|
+
# generated accessor returns the flat array of permitted values
|
|
14
|
+
# directly (what every caller actually wants), not Struct#values'
|
|
15
|
+
# own `[self.values]`. No caller anywhere reads the built-in
|
|
16
|
+
# behavior; verified before disabling this cop for it.
|
|
17
|
+
# rubocop:disable-next Lint/StructNewOverride
|
|
6
18
|
OneOf = Struct.new(:values)
|
|
7
19
|
|
|
8
20
|
UNSET = Object.new.freeze
|
|
@@ -261,15 +273,11 @@ module Hecks
|
|
|
261
273
|
def resolve_identity_type!(type, as, insert_at, value_objects, context_name)
|
|
262
274
|
target = Naming.demodulise(type.respond_to?(:hecks_name) ? type.hecks_name : type)
|
|
263
275
|
matches = value_objects.select { |value_object| value_object.hecks_name.to_s == target }
|
|
264
|
-
if matches.size > 1
|
|
265
|
-
raise Malformed, "#{context_name}.identified_by names duplicate value object #{target}"
|
|
266
|
-
end
|
|
276
|
+
raise Malformed, "#{context_name}.identified_by names duplicate value object #{target}" if matches.size > 1
|
|
267
277
|
|
|
268
278
|
vo = type.respond_to?(:attributes) ? type : matches.first
|
|
269
279
|
raise Malformed, "#{context_name}.identified_by names #{target}, which is not a declared value object" unless vo
|
|
270
|
-
if vo.attributes.empty?
|
|
271
|
-
raise Malformed, "#{context_name}.identified_by names #{target}, which declares no attributes"
|
|
272
|
-
end
|
|
280
|
+
raise Malformed, "#{context_name}.identified_by names #{target}, which declares no attributes" if vo.attributes.empty?
|
|
273
281
|
|
|
274
282
|
field = (as || Naming.snake(target)).to_sym
|
|
275
283
|
if attributes.any? { |attribute| attribute.name == field }
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
module Hecks
|
|
2
2
|
module Bluebook
|
|
3
3
|
module DSL
|
|
4
|
+
# What a bare `Domain::Aggregate` constant resolves to inside a
|
|
5
|
+
# `.hecksagon`/`.world` block (minted by `.namespace`'s own
|
|
6
|
+
# `const_missing`) — `method_missing` turns a bind like
|
|
7
|
+
# `Payment.persisted_by "Postgres"` into a queued `Bind`, and `#port`
|
|
8
|
+
# is the one real method, for `Aggregate.port("Name") do ... end`.
|
|
4
9
|
class BindingProxy
|
|
5
10
|
def self.namespace(domain, collector)
|
|
6
11
|
Module.new do
|