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
-
.best_effort(construct, args, route = nil, reference_key: nil) ⇒ Object
BEST-EFFORT, FOR A LOCK KEY ONLY —
Runtime::AggregateLock's own per-record striping needs SOME id to key on before dispatch has run far enough to hydrate for real, so this walks the identical chainCommandInterpreter#hydrate_existing/#hydrate_prior_or_initialandEntityInterpreter#parentalready use to locate the real record — but wrapped to never raise. -
.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.
-
.hash_lookup(hash, key) ⇒ Object
A hash read that decides which spelling of a key answers by PRESENCE, never by
||— a bare||treats a genuinely-heldfalsethe same as an absent key and falls through to the other spelling, landing onnilinstead of the real, stored answer. -
.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
.best_effort(construct, args, route = nil, reference_key: nil) ⇒ Object
BEST-EFFORT, FOR A LOCK KEY ONLY — Runtime::AggregateLock's own
per-record striping needs SOME id to key on before dispatch has run
far enough to hydrate for real, so this walks the identical chain
CommandInterpreter#hydrate_existing/#hydrate_prior_or_initial
and EntityInterpreter#parent already use to locate the real
record — but wrapped to never raise. Choosing which Mutex to hold
must never itself become a crash. nil means "could not resolve
from the raw, pre-normalized payload this runs against" — the
caller locks by aggregate type alone in that case (coarser, still
correct, just less concurrent).
146 147 148 149 150 151 152 153 |
# File 'lib/hecks/runtime/identity.rb', line 146 def best_effort(construct, args, route = nil, reference_key: nil) route&.aggregate || of(construct, args) || from(construct, args, :id) || (reference_key && from(construct, args, reference_key)) rescue StandardError nil end |
.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.
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 120 121 122 123 124 125 126 127 128 |
# File 'lib/hecks/runtime/identity.rb', line 79 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) ? hash_lookup(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 |
.hash_lookup(hash, key) ⇒ Object
A hash read that decides which spelling of a key answers by
PRESENCE, never by || — a bare || treats a genuinely-held
false the same as an absent key and falls through to the other
spelling, landing on nil instead of the real, stored answer.
30 31 32 33 |
# File 'lib/hecks/runtime/identity.rb', line 30 def hash_lookup(hash, key) sym = key.to_sym hash.key?(sym) ? hash[sym] : hash[key] 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.
62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
# File 'lib/hecks/runtime/identity.rb', line 62 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.
132 133 134 |
# File 'lib/hecks/runtime/identity.rb', line 132 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.
39 40 41 42 43 44 45 46 |
# File 'lib/hecks/runtime/identity.rb', line 39 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) ? hash_lookup(dug, field) : nil end end |