Module: Hecks::Runtime::CommandRules::References
- Included in:
- Hecks::Runtime::CommandRules
- Defined in:
- lib/hecks/runtime/command_rules/references.rb
Overview
A reference must point at something that EXISTS.
reference_to Customer is the one guarantee an aggregate reference is
for, and it was declared 14 times across banking and enforced nowhere :
an Account could belong to a customer who was never registered, and
every gate stayed green because
no corpus step ever passed a dangling reference.
Instance Method Summary collapse
- #dereference(domain, owner, source, depth: DEREFERENCE_DEPTH) ⇒ Object
-
#reference_key(value) ⇒ Object
valueis the referenced record's own id, EXACTLY asIdentity.ofwould build it for that record — a bare scalar for a single-field identity (the overwhelming common case; Banking's own plainreference_to Customerholds one already, soValue. materialize_unwrappedis a no-op passthrough here), or aNaming.identity-joined string for a compound one (belongs_to Translation, as: :translation_ref— Translation's ownidentified_by :domain, :from, :to, THREE fields). -
#referenced_aggregate(attribute) ⇒ Object
The reference RESOLVES itself — through the chapter's own IR, so the bluebook's declared heads are the index.
-
#resolve_references(domain, command, args) ⇒ Object
Resolved here rather than in coercion because coercion is pure — it holds no repository.
-
#resolve_state_references(domain, construct, state) ⇒ Object
Structural references are checked again against the settled state.
- #validate_reference_values(domain, target, held, list:) ⇒ Object
-
#validate_relationship_cardinality(construct, attribute, held) ⇒ Object
has_oneandbelongs_tomean exactly one target unless the declaration explicitly says optional.
Instance Method Details
#dereference(domain, owner, source, depth: DEREFERENCE_DEPTH) ⇒ Object
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 |
# File 'lib/hecks/runtime/command_rules/references.rb', line 159 def dereference(domain, owner, source, depth: DEREFERENCE_DEPTH) return {} if depth <= 0 || owner.nil? owner.attributes.each_with_object({}) do |attribute, hydrated| next unless attribute.reference? id = source[attribute.name] next if id.nil? target = referenced_aggregate(attribute) next unless target record = @registry.repository(domain, target).find(id.to_s) next unless record name = attribute.name.to_s.sub(/_id\z/, "").to_sym hydrated[name] = record.state.merge(dereference(domain, target, record.state, depth: depth - 1)) end end |
#reference_key(value) ⇒ Object
value is the referenced record's own id, EXACTLY as Identity.of
would build it for that record — a bare scalar for a single-field
identity (the overwhelming common case; Banking's own plain
reference_to Customer holds one already, so Value. materialize_unwrapped is a no-op passthrough here), or a
Naming.identity-joined string for a compound one
(belongs_to Translation, as: :translation_ref — Translation's
own identified_by :domain, :from, :to, THREE fields). Before
this, plain value.to_s on that compound case's own coerced
Value hit Ruby's default Object#to_s (a raw, run-to-run-random
memory address) instead of joining the record's real id — found
live via bin/fuzz on the self-hosted "translation" domain
(replay_is_deterministic), the SAME class of gap Identity.from
already had for a compound identified_by's own bare (undotted)
attribute paths. materialize_unwrapped recurses a multi-
attribute value object to a plain Hash keyed by attribute name,
in declaration order — Naming.identity on .values reproduces
the identical join Identity.of itself would produce for the
SAME fields.
124 125 126 127 128 129 |
# File 'lib/hecks/runtime/command_rules/references.rb', line 124 def reference_key(value) unwrapped = Value.materialize_unwrapped(value) return Naming.identity(unwrapped.values).to_s if unwrapped.is_a?(Hash) unwrapped.to_s end |
#referenced_aggregate(attribute) ⇒ Object
The reference RESOLVES itself — through the chapter's own IR, so the
bluebook's declared heads are the index. This used to regex the target's
name out of "Referenceregistry.bluebook(domain).aggregates for it — and later reached the
target through Ruby's constant tree, a class thrown away for its .ir
the moment it was found.
87 88 89 |
# File 'lib/hecks/runtime/command_rules/references.rb', line 87 def referenced_aggregate(attribute) attribute.type.resolve end |
#resolve_references(domain, command, args) ⇒ Object
Resolved here rather than in coercion because coercion is pure — it
holds no repository. A reference INTO ANOTHER DOMAIN is left alone : a
cross-domain target may legitimately not be loaded, which is the same
reading across policies already get.
Shared by CommandInterpreter and EntityInterpreter — an entity command can declare a reference-typed attribute the same way an aggregate command can, even though nothing in the real corpus does yet.
24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
# File 'lib/hecks/runtime/command_rules/references.rb', line 24 def resolve_references(domain, command, args) command.attributes.each do |attribute| next unless attribute.reference? next unless args.key?(attribute.name) held = args[attribute.name] next if held.nil? target = referenced_aggregate(attribute) next unless target validate_reference_values(domain, target, held, list: attribute.list?) end end |
#resolve_state_references(domain, construct, state) ⇒ Object
Structural references are checked again against the settled state.
This is what makes a has_many declared on an aggregate honest even
when a command supplies its list through an ordinary typed argument.
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
# File 'lib/hecks/runtime/command_rules/references.rb', line 42 def resolve_state_references(domain, construct, state) construct.attributes.each do |attribute| next unless attribute.reference? held = state[attribute.name] validate_relationship_cardinality(construct, attribute, held) next if held.nil? target = referenced_aggregate(attribute) next unless target validate_reference_values(domain, target, held, list: attribute.list?) end Array(construct.entities).each do |entity| field = construct.attribute(Naming.snake(entity.hecks_name).to_sym) || construct.attributes.find { |attribute| attribute.type.to_s == entity.hecks_name.to_s } next unless field Array(state[field.name]).each { |row| resolve_state_references(domain, entity, row) } end end |
#validate_reference_values(domain, target, held, list:) ⇒ Object
91 92 93 94 95 96 97 98 99 100 101 102 103 |
# File 'lib/hecks/runtime/command_rules/references.rb', line 91 def validate_reference_values(domain, target, held, list:) values = list ? Array(held) : [held] values.each do |value| key = reference_key(value) next if key.empty? next if @registry.repository(domain, target).find(key) raise NotFound, RefusalWording.render("NotFound", "reference_target_missing", target: target.name, heads: target.identity_heads.join(", "), key: key.inspect) end end |
#validate_relationship_cardinality(construct, attribute, held) ⇒ Object
has_one and belongs_to mean exactly one target unless the
declaration explicitly says optional. State validation owns this
boundary because a command may leave an aggregate field untouched;
checking only command arguments would let a required relationship be
persisted as nil. has_many admits zero members, so its empty list is
already a valid cardinality and needs no presence refusal.
71 72 73 74 75 76 77 78 79 |
# File 'lib/hecks/runtime/command_rules/references.rb', line 71 def validate_relationship_cardinality(construct, attribute, held) return if attribute.relationship.nil? || attribute.list? return unless held.nil? && !attribute.optional? raise TypeMismatch, "#{construct.hecks_name}.#{attribute.name} is a required " \ "#{attribute.relationship} relationship — expected one " \ "#{attribute.type.target_name} identity, got nil" end |