Class: Hecks::Runtime::Instance

Inherits:
Object
  • Object
show all
Defined in:
lib/hecks/runtime/instance.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(aggregate:, id:, state: nil, args: nil) ⇒ Instance

args: — THE ORIGINAL COMMAND PAYLOAD, offered only by a fresh creation (CommandInterpreter#hydrate_legacy_creation/ #hydrate_complete_state/#hydrate_prior_or_initial, each already holding it when they mint a brand-new record). See materialize_identity! for why a composite identity needs it.



26
27
28
29
30
31
32
# File 'lib/hecks/runtime/instance.rb', line 26

def initialize(aggregate:, id:, state: nil, args: nil)
  @aggregate = aggregate
  @id        = id
  @state     = state ? self.class.hydrate_with_defaults(aggregate, state) : self.class.defaults(aggregate)
  @version   = nil
  materialize_identity!(args)
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(name, *args) ⇒ Object



80
81
82
83
84
# File 'lib/hecks/runtime/instance.rb', line 80

def method_missing(name, *args)
  return @state[name] if @state.key?(name)

  super
end

Instance Attribute Details

#aggregateObject (readonly)

Returns the value of attribute aggregate.



7
8
9
# File 'lib/hecks/runtime/instance.rb', line 7

def aggregate
  @aggregate
end

#idObject (readonly)

Returns the value of attribute id.



7
8
9
# File 'lib/hecks/runtime/instance.rb', line 7

def id
  @id
end

#stateObject

Returns the value of attribute state.



8
9
10
# File 'lib/hecks/runtime/instance.rb', line 8

def state
  @state
end

#versionObject

OUT-OF-BAND ADAPTER BOOKKEEPING, NOT DOMAIN STATE — the optimistic- concurrency version a CAS-capable adapter (Postgres today) stamps on a record it reads/writes, so a later save can assert "commit only if nobody has written since". Deliberately absent from to_h/[]/[]=/method_missing : a domain author never declares this, a given/ensures/invariant can never read it, and no adapter that doesn't understand it (Memory, Heki) ever sets it — nil there just means "no CAS attempted", which is exactly what a plain save already does. See docs/decisions/ (concurrency-control ADR) for the full mechanism.



19
20
21
# File 'lib/hecks/runtime/instance.rb', line 19

def version
  @version
end

Class Method Details

.default_for(aggregate, attribute) ⇒ Object



60
61
62
63
64
65
66
67
68
69
70
# File 'lib/hecks/runtime/instance.rb', line 60

def self.default_for(aggregate, attribute)
  return Value.for_attribute(aggregate, attribute, attribute.default) unless attribute.default.nil?
  # An entity's members hydrate through the same path but an entity
  # declares no value objects of its own — nothing to default-build.
  return nil unless aggregate.respond_to?(:value_object)

  value_object = aggregate.value_object(attribute.type)
  return nil unless value_object && value_object.attributes.all? { |field| !field.default.nil? }

  Value.build(value_object, {}, aggregate)
end

.defaults(aggregate) ⇒ Object



48
49
50
51
52
53
54
55
56
57
58
# File 'lib/hecks/runtime/instance.rb', line 48

def self.defaults(aggregate)
  state = aggregate.attributes.each_with_object({}) do |attr, acc|
    # FROZEN, like a list that has had something appended to it.
    # An untouched list is the easiest one to miss and the easiest
    # to mutate: nothing has replaced it yet, so a caller pushing
    # into it writes straight into the aggregate's own state.
    acc[attr.name] = attr.list? ? Freezer.deep([]) : default_for(aggregate, attr)
  end
  state[aggregate.lifecycle.field.to_sym] = aggregate.lifecycle.default if aggregate.lifecycle
  state
end

.hydrate_with_defaults(aggregate, state) ⇒ Object

Loading existing state runs the same default-fill a fresh instance gets: an attribute the record predates — a newly-required field with a declared default:, a list added since the record was written — arrives filled instead of nil. Only declared defaults fill in; an attribute with no default stays absent, exactly as stored.



40
41
42
43
44
45
46
# File 'lib/hecks/runtime/instance.rb', line 40

def self.hydrate_with_defaults(aggregate, state)
  hydrated = Value.hydrate(aggregate, state)
  defaults(aggregate).each do |name, value|
    hydrated[name] = value unless value.nil? || hydrated.key?(name)
  end
  hydrated
end

Instance Method Details

#[](name) ⇒ Object



72
# File 'lib/hecks/runtime/instance.rb', line 72

def [](name) = @state[name.to_sym]

#[]=(name, value) ⇒ Object



76
77
78
# File 'lib/hecks/runtime/instance.rb', line 76

def []=(name, value)
  @state[name.to_sym] = value
end

#dupObject

A COPY A MUTATION MAY TOUCH. Every adapter but Memory hands find a freshly-decoded Instance already; Memory's holds the record it eventually saves — the SAME state Hash, aliased. Before ensures existed, nothing could refuse between apply_mutations and save, so that aliasing was invisible: a dispatch either ran to completion or raised before touching state at all. ensures is the first refusal to sit AFTER mutation, and it found the bug the moment it did — an in-memory record left half-mutated by a dispatch that then refused. command_interpreter/entity_interpreter hydrate an EXISTING record through this, never through the adapter's own return value directly, so a refused ensures leaves the stored record untouched regardless of which adapter is holding it.



111
112
113
114
115
# File 'lib/hecks/runtime/instance.rb', line 111

def dup
  copy = super
  copy.state = @state.dup
  copy
end

#inspectObject



117
118
119
120
# File 'lib/hecks/runtime/instance.rb', line 117

def inspect
  fields = @state.map { |k, v| "#{k}=#{v.inspect}" }.join(" ")
  "#<#{@aggregate.hecks_name} #{@id} #{fields}>"
end

#key?(name) ⇒ Boolean

Returns:

  • (Boolean)


74
# File 'lib/hecks/runtime/instance.rb', line 74

def key?(name) = @state.key?(name.to_sym)

#respond_to_missing?(name, include_private = false) ⇒ Boolean

Returns:

  • (Boolean)


86
87
88
# File 'lib/hecks/runtime/instance.rb', line 86

def respond_to_missing?(name, include_private = false)
  @state.key?(name) || super
end

#to_hObject

id: @id LAST, not first — see Facade::Handle#to_h's own comment for the full story (the same fix, landed there first): an aggregate free to declare its own attribute literally named id (BurningManPrep's Item, attribute :id, ItemId) has that attribute's own wrapped value object sitting in @state[:id] — merging @state on top of { id: @id } let it silently clobber the correct bare identity. @id merged last always wins.



97
# File 'lib/hecks/runtime/instance.rb', line 97

def to_h = @state.merge(id: @id)