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) ⇒ Instance

Returns a new instance of Instance.



9
10
11
12
13
14
# File 'lib/hecks/runtime/instance.rb', line 9

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

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(name, *args) ⇒ Object



62
63
64
65
66
# File 'lib/hecks/runtime/instance.rb', line 62

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.



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

def aggregate
  @aggregate
end

#idObject (readonly)

Returns the value of attribute id.



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

def id
  @id
end

#stateObject

Returns the value of attribute state.



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

def state
  @state
end

Class Method Details

.default_for(aggregate, attribute) ⇒ Object



42
43
44
45
46
47
48
49
50
51
52
# File 'lib/hecks/runtime/instance.rb', line 42

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



30
31
32
33
34
35
36
37
38
39
40
# File 'lib/hecks/runtime/instance.rb', line 30

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.



22
23
24
25
26
27
28
# File 'lib/hecks/runtime/instance.rb', line 22

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



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

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

#[]=(name, value) ⇒ Object



58
59
60
# File 'lib/hecks/runtime/instance.rb', line 58

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.



93
94
95
96
97
# File 'lib/hecks/runtime/instance.rb', line 93

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

#inspectObject



99
100
101
102
# File 'lib/hecks/runtime/instance.rb', line 99

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

#key?(name) ⇒ Boolean

Returns:

  • (Boolean)


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

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

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

Returns:

  • (Boolean)


68
69
70
# File 'lib/hecks/runtime/instance.rb', line 68

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.



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

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