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", )  # => "acct-1"

Class Method Summary collapse

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