Module: EcsRails::Presence::Entity

Extended by:
ActiveSupport::Concern
Included in:
Entity
Defined in:
lib/ecs_rails/presence.rb

Overview

The entity side of presence: add / has? / remove.

Included into Entity, so every entity answers these. The per-component <reader>? predicate (user.moderator?) is generated by DSL and is exactly #has?.

Examples:

user.add(Moderator)     # persist the row now (idempotent, validated)
user.has?(Moderator)    # => true
user.remove(Moderator)  # destroy the row (idempotent)

Instance Method Summary collapse

Instance Method Details

#add(component_class) ⇒ EcsRails::Component

Ensures a row exists for component_class on this entity and returns the now-persisted instance (RFC-0009).

Immediate, not deferred to the next save: the whole reason add exists is that the save cascade never persists a marker. Idempotent: an existing row is returned untouched, never a second INSERT — the unique entity_id index (ADR-0005) would forbid one, and correctness should not depend on catching that. Validated: it uses save!, so add(Email) raises RecordInvalid (address is required) while add(Moderator) always succeeds. You cannot add an empty required component (ADR-0009).

Examples:

A marker, which the save cascade would never write

user.add(Moderator)     # INSERTs the row now
user.moderator?         # => true

Validation still applies

user.add(Email)         # raises: Email#address is required

Parameters:

Returns:

Raises:

  • (EcsRails::InvalidComponent)

    if the entity does not declare it

  • (ActiveRecord::RecordInvalid)

    if the component is invalid — you cannot add an empty required component (ADR-0009)

See Also:



70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/ecs_rails/presence.rb', line 70

def add(component_class)
  name = ecs_presence_reader(component_class)

  # The reader goes through RFC-0006's memo. When no row exists on this
  # instance it loads one (a SELECT) or builds a virtual; when a row does
  # exist — including one this instance already added — it is already the
  # persisted instance, so we return it and no second INSERT is attempted.
  component = public_send(name)
  return component if component.persisted?

  # A virtual instance: persist it. entity_id is restamped for the same
  # reason the cascade does (RFC-0006) — on a freshly built virtual for a
  # new entity it may not have been set yet. The memo now holds this same,
  # persisted instance, so a later `entity.moderator` answers from it with
  # no further query.
  component.entity_id = id
  component.save!
  component
end

#has?(component_class) ⇒ Boolean

True iff a row exists for component_class on this entity (RFC-0009).

Side-effect-free, like valid? (RFC-0007): it never materialises, persists, or dirties anything. It consults RFC-0006’s memo first — a component dirtied-and-saved on this instance is present without a fresh query — and otherwise issues a bare existence check (SELECT … EXISTS), a load of nothing.

The DSL also generates a per-component predicate, so user.moderator? asks exactly this question.

Examples:

user.has?(Moderator)  # => true
user.moderator?       # => the same question, generated per component

Parameters:

Returns:

  • (Boolean)

    whether a persisted row exists

Raises:

See Also:



111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/ecs_rails/presence.rb', line 111

def has?(component_class)
  name = ecs_presence_reader(component_class)

  # Memo first: if this instance already loaded (and persisted) the
  # component, the answer is known without touching the database. A merely
  # virtual memo entry is *not* proof of absence — a row could have been
  # written elsewhere — so it falls through to the existence check rather
  # than answering false from memory.
  cached = @ecs_components&.[](name)
  return true if cached&.persisted?

  # Bare existence check: no row is instantiated, so nothing is loaded and
  # nothing is dirtied. One query per component, as everywhere else in the
  # gem (architecture.md §7 non-goal: query optimisation).
  component_class.where(entity_id: id).exists?
end

#remove(component_class) ⇒ self

Destroys the row for component_class if present, and resets the reader to a virtual default instance — exactly RFC-0006’s component.destroy semantics. Idempotent when absent. Returns the entity (RFC-0009).

Examples:

user.remove(Moderator)  # DELETEs the row; no-op if there was none
user.moderator          # => #<Moderator> — virtual again, not nil

Parameters:

Returns:

  • (self)

    the entity, for chaining

Raises:

See Also:



142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/ecs_rails/presence.rb', line 142

def remove(component_class)
  name = ecs_presence_reader(component_class)

  # Reach the component through the reader so a present row is loaded with
  # its callbacks intact. `component.destroy` fires Lazy::Component's
  # after_destroy, which calls #ecs_forget_component here — dropping the
  # memo and nilling the association, so the next read rebuilds a virtual.
  # When nothing is there to destroy the read leaves a virtual in the memo,
  # which is already the reset state, so remove is idempotent.
  component = public_send(name)
  component.destroy if component.persisted?
  self
end