Module: Hecks::Runtime::Identity
- Defined in:
- lib/hecks/runtime/identity.rb
Overview
THE SCALAR AN IDENTITY PATH NAMES.
An identity is DECLARED as a path — identified_by :number — and
this is the one place that reads one. It follows the path and nothing else.
What it replaced was Value.identifier, which opened a one-field value
object and took whatever was inside : that let identified_by :number pass
for an identity, with the runtime guessing which field had been meant. The
guess is gone. A declaration that names no field is now refused when the
bluebook loads (an aggregate that is identified names a field, an entity is known by a field), so by the time anything is dispatched there is
always a path here to follow.
Usage:
Identity.scalar("number.value", account_number_value_object) # => "acct-1"
Class Method Summary collapse
-
.from(construct, args, key, value_owner: construct) ⇒ Object
A path digs into the value object that carries the identity, so what is stored is the SCALAR inside it rather than the object serialised whole.
-
.of(construct, args, value_owner: construct) ⇒ Object
THE IDENTITY IS THE JOIN OF ITS PARTS, in declaration order.
-
.reading(construct) ⇒ Object
How an identity READS when the runtime has to name it in a refusal — the paths as they were declared, so the message quotes the bluebook back.
-
.scalar(path, held) ⇒ Object
The head names the ATTRIBUTE and is consumed by whoever looked the value up; what is left is the walk down into it.
Class Method Details
.from(construct, args, key, value_owner: construct) ⇒ Object
A path digs into the value object that carries the identity, so what is stored is the SCALAR inside it rather than the object serialised whole.
70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 |
# File 'lib/hecks/runtime/identity.rb', line 70 def from(construct, args, key, value_owner: construct) return nil unless key head, *rest = key.to_s.split(".") head = head.to_sym return nil unless args.key?(head) unless rest.empty? held = args[head] held = held.to_h if held.respond_to?(:to_h) # AN ID IS ALWAYS A SCALAR. The path says WHICH FIELD carries it, so a # caller may hand that field's value straight over — a string or a # number, never a serialised object. Only a value object that actually # arrived whole has to be opened. return held.to_s unless held.is_a?(Hash) return rest.reduce(held) { |h, f| h.is_a?(Hash) ? (h[f.to_sym] || h[f]) : nil }&.to_s end # Coerced against the identity ATTRIBUTE only when the caller actually # named it. A saga addresses an aggregate by its correlation key, and # that key carries the id ALREADY RESOLVED — coercing "w1" against a # WireReference asked the caller to pass fields for a value object they # never mentioned. attribute = construct.identity_heads.include?(head) ? construct.attribute(head) : nil raw = args[head] return raw unless attribute # AN ID IS ALWAYS A SCALAR — same contract the dotted branch above # already keeps, just reached a different way here: a BARE # (undotted) identity path names one of THIS construct's own # declared attributes directly, and when that attribute's type is # a value object (Translation's own compound `identified_by # :domain, :from, :to`, each typed `TranslationDomainName`/ # `TranslationEraName`), `Value.for_attribute` coerces it into a # real single-field Value wrapper — never unwrapped before this, # so `Naming.identity`'s own plain `Array#join` (`Naming.identity`'s # own header: parts must already be scalars) fell through to # Ruby's default `Object#to_s`, leaking a raw, run-to-run-random # memory address (`#<Hecks::Runtime::Value:0x...>`) into # every refusal quoting this identity — found live via bin/fuzz on # the self-hosted "translation" domain (replay_is_deterministic: # the SAME address never repeats, so two replays of the # identical steps produced different histories the moment a # Translation went missing). `materialize_unwrapped` is the # SAME single-field-VO-recurses-to-its-bare-scalar helper # `read_model_interpreter.rb` already uses for exactly this # unwrap; passthrough for anything that isn't a Value at all. Value.materialize_unwrapped(Value.for_attribute(value_owner, attribute, raw)).to_s end |
.of(construct, args, value_owner: construct) ⇒ Object
THE IDENTITY IS THE JOIN OF ITS PARTS, in declaration order. Shared by
CommandInterpreter (an aggregate acting on itself) and
EntityInterpreter (a piece addressed through its aggregate) — a piece
declares an identity the same shape a head does, so it derives one the
same way. construct answers identity_paths / identity_heads /
attribute (an Aggregate or an Entity, either one) ; value_owner
answers for coercion (Value.for_attribute's first argument), which for
an entity is its OWNING aggregate — an entity's value objects resolve
through the aggregate's namespace, not its own.
A part the payload does not carry makes the WHOLE identity unresolvable, rather than half of one. Half an identity names nothing, and joining what did arrive would silently name a different record on every dispatch — the precise failure that minting an id caused, arrived at by another road.
53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
# File 'lib/hecks/runtime/identity.rb', line 53 def of(construct, args, value_owner: construct) paths = construct.identity_paths return nil if paths.empty? parts = paths.map { |path| from(construct, args, path, value_owner: value_owner) } # A BLANK PART NAMES NOTHING, the same as an ABSENT one — AN ID IS A # SCALAR, and "" is not a fact about anything. This used to check only # `nil?`, so a canonical text extracted as "" (an expression whose # source did not survive extraction) resolved to a REAL, empty-string # identity — a record addressable by an id no caller could have meant. return nil if parts.any? { |part| part.nil? || (part.respond_to?(:empty?) && part.empty?) } Naming.identity(parts) end |
.reading(construct) ⇒ Object
How an identity READS when the runtime has to name it in a refusal — the paths as they were declared, so the message quotes the bluebook back.
123 124 125 |
# File 'lib/hecks/runtime/identity.rb', line 123 def reading(construct) construct.identity_paths.join(", ") end |
.scalar(path, held) ⇒ Object
The head names the ATTRIBUTE and is consumed by whoever looked the value
up; what is left is the walk down into it. A path with no fields to walk
— an aggregate that declares no identity and falls back to id — hands
back what it was given, because there is nothing declared to dig for.
30 31 32 33 34 35 36 37 |
# File 'lib/hecks/runtime/identity.rb', line 30 def scalar(path, held) _head, *fields = path.to_s.split(".") return held if fields.empty? fields.reduce(Value.materialize(held)) do |dug, field| dug.is_a?(Hash) ? (dug[field.to_sym] || dug[field]) : nil end end |