Class: Funes::Event

Inherits:
Object
  • Object
show all
Includes:
ActiveModel::Attributes, ActiveModel::Model, Associations, Inspection
Defined in:
app/models/funes/event.rb

Overview

Base class for all events in the Funes event sourcing framework.

Events are immutable facts that represent something that happened in the system. They use ActiveModel for attributes and validations, making them familiar to Rails developers.

Event Validation

Events support three types of validation:

  • Own validation: Standard ActiveModel validations defined on the event class itself.
  • Adjacent state validation: Validation errors from consistency projections that check if the event would lead to an invalid state.
  • Interpretation errors: Errors added via event.errors.add(...) inside interpretation blocks. When used in a consistency projection, these are automatically transferred to interpretation_errors and cause the event to be rejected.

The valid? method returns true only if both validations pass. The errors method merges both types of errors for display.

Defining Events

Events inherit from Funes::Event and define attributes using ActiveModel::Attributes:

Associating Events With Other Models

Use refers_to to relate an event to another model. Only the referenced record's id is stored in the event payload; the record itself is loaded lazily and is readable at interpretation time:

Examples:

Define a simple event

class Order::Placed < Funes::Event
  attribute :total, :decimal
  attribute :customer_id, :string
  attribute :at, :datetime, default: -> { Time.current }

  validates :total, presence: true, numericality: { greater_than: 0 }
  validates :customer_id, presence: true
end

Using the event

event = Order::Placed.new(total: 99.99, customer_id: "cust-123")
stream.append(event)

Reference another model and read it in an interpretation

class Order::Placed < Funes::Event
  refers_to :customer
  attribute :total, :decimal
end

stream.append(Order::Placed.new(customer: customer, total: 99.99))

interpretation_for Order::Placed do |state, event, _at|
  state.customer = event.customer  # loaded by id from props
  state
end

Handling validation errors

event = stream.append(Order::Placed.new(total: -10))
unless event.valid?
  puts event.own_errors.full_messages      # => Event's own validation errors
  puts event.state_errors.full_messages    # => Consistency projection errors
  puts event.errors.full_messages          # => All errors merged
end

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Inspection

#attribute_for_inspect, #full_inspect, #inspect, #pretty_print

Instance Attribute Details

#_adjacent_state_errorsObject

Returns the value of attribute _adjacent_state_errors.



72
73
74
# File 'app/models/funes/event.rb', line 72

def _adjacent_state_errors
  @_adjacent_state_errors
end

#_event_entryFunes::EventEntry?

Returns The persisted EventEntry record (internal use).

Returns:



80
81
82
# File 'app/models/funes/event.rb', line 80

def _event_entry
  @_event_entry
end

#_interpretation_errorsObject

Returns the value of attribute _interpretation_errors.



76
77
78
# File 'app/models/funes/event.rb', line 76

def _interpretation_errors
  @_interpretation_errors
end

#adjacent_state_errorsActiveModel::Errors

Returns Validation errors from consistency projections.

Returns:

  • (ActiveModel::Errors)

    Validation errors from consistency projections.



72
# File 'app/models/funes/event.rb', line 72

attr_accessor :_adjacent_state_errors

#interpretation_errorsActiveModel::Errors

Returns Explicit rejection errors from consistency projection interpretation blocks.

Returns:

  • (ActiveModel::Errors)

    Explicit rejection errors from consistency projection interpretation blocks.



76
# File 'app/models/funes/event.rb', line 76

attr_accessor :_interpretation_errors

Instance Method Details

#created_atTime?

Returns the timestamp when the event was persisted.

Examples:

event = Order::Placed.new(total: 99.99)
event.created_at  # => nil

stream.append(event)
event.created_at  # => 2026-02-18 12:00:00 UTC

Returns:

  • (Time, nil)

    The time the event was created, or nil if not yet persisted.



128
# File 'app/models/funes/event.rb', line 128

def created_at = _event_entry&.created_at

#errorsActiveModel::Errors

Get all validation errors (both event and state errors merged).

This method merges the event's own validation errors with any errors from consistency projections, prefixing state errors with a localized message.

Examples:

event.errors.full_messages
# => ["Total must be greater than 0", "Led to invalid state: Quantity on hand must be >= 0"]

Returns:

  • (ActiveModel::Errors)

    All validation errors combined.



219
220
221
222
223
224
225
226
227
228
# File 'app/models/funes/event.rb', line 219

def errors
  return super unless !_adjacent_state_errors.empty? || !_interpretation_errors.empty?

  tmp_errors = ActiveModel::Errors.new(self)
  tmp_errors.merge!(super)
  merge_errors_into(tmp_errors, _adjacent_state_errors, state_errors: true)
  merge_errors_into(tmp_errors, _interpretation_errors)

  tmp_errors
end

#invalid?Boolean

Check if the event is invalid.

An event is invalid if any of its own validations fail, it leads to an invalid state (adjacent_state_errors), or it has been explicitly rejected via interpretation_errors.

Examples:

event = Order::Placed.new(total: -10)
event.invalid?  # => true (own validation failed)

Returns:

  • (Boolean)

    true if the event is invalid, false otherwise.



178
# File 'app/models/funes/event.rb', line 178

def invalid? = !valid?

#occurred_atTime?

Returns the timestamp when the event actually occurred.

When an event is recorded retroactively (with an explicit at: on append), this returns the actual time of the event. Otherwise, it equals created_at.

Examples:

event = stream.append(Salary::Raised.new(amount: 6500), at: Time.new(2025, 2, 15))
event.occurred_at  # => 2025-02-15 00:00:00 UTC

Returns:

  • (Time, nil)

    The actual time of the event, or nil if not yet persisted.



140
# File 'app/models/funes/event.rb', line 140

def occurred_at = _event_entry&.occurred_at

#own_errorsActiveModel::Errors

Get the event's own validation errors (excluding state errors).

Examples:

event = Order::Placed.new(total: -10)
event.own_errors.full_messages  # => ["Total must be greater than 0"]

Returns:

  • (ActiveModel::Errors)

    Only the event's own validation errors.



201
202
203
204
205
206
207
# File 'app/models/funes/event.rb', line 201

def own_errors
  tmp_errors = ActiveModel::Errors.new(self)
  tmp_errors.merge!(base_errors)
  merge_errors_into(tmp_errors, _interpretation_errors)

  tmp_errors
end

#persisted?Boolean

Check if the event has been persisted to the database.

An event is considered persisted if it was either saved via EventStream#append or reconstructed from an Funes::EventEntry via to_klass_instance.

Examples:

event = Order::Placed.new(total: 99.99)
event.persisted?  # => false

stream.append(event)
event.persisted?  # => true (if no validation errors)

Returns:

  • (Boolean)

    true if the event has been persisted, false otherwise.



114
115
116
# File 'app/models/funes/event.rb', line 114

def persisted?
  _event_entry.present?
end

#state_errorsActiveModel::Errors

Get validation errors from consistency projections.

These are errors that indicate the event would lead to an invalid state, even if the event itself is valid.

Examples:

event = stream.append(Inventory::ItemShipped.new(quantity: 9999))
event.state_errors.full_messages  # => ["Quantity on hand must be >= 0"]

Returns:

  • (ActiveModel::Errors)

    Errors from consistency projection validation.



190
191
192
# File 'app/models/funes/event.rb', line 190

def state_errors
  _adjacent_state_errors
end

#valid?Boolean

Check if the event is valid.

An event is valid only if both its own validations pass AND it doesn't lead to an invalid state (no adjacent_state_errors from consistency projections).

Examples:

event = Order::Placed.new(total: 99.99, customer_id: "cust-123")
event.valid?  # => true or false

Returns:

  • (Boolean)

    true if the event is valid, false otherwise.



164
165
166
# File 'app/models/funes/event.rb', line 164

def valid?
  super && _adjacent_state_errors.empty? && _interpretation_errors.empty?
end

#versionInteger?

Returns the version number of the event within its stream.

Each event in a stream gets an incrementing version number used for optimistic concurrency control. The version is assigned when the event is persisted.

Examples:

event = stream.append(Order::Placed.new(total: 99.99))
event.version  # => 1

Returns:

  • (Integer, nil)

    The version number, or nil if not yet persisted.



152
# File 'app/models/funes/event.rb', line 152

def version = _event_entry&.version