Recording Events
After reading this guide, you will know how to record an event in a Rails application — and see how naturally it fits alongside the patterns you already use.
Table of contents
Funes is designed to feel like Rails. The patterns on this page — ActiveModel validations, form_with, strong parameters, the persisted?/errors cycle — are ones you use every day. Recording an event means learning one new concept, not a new framework.
Defining the event
Events are ActiveModel objects — no database table, no migration. You define attributes and validations just as you would on any Rails model:
# app/events/debt/issued.rb
module Debt
class Issued < Funes::Event
attribute :amount, :decimal
attribute :interest_rate, :decimal
attribute :at, :datetime
validates_presence_of :at
validates :amount, numericality: { greater_than: 0 }
validates :interest_rate, numericality: { greater_than_or_equal_to: 0 }
end
end
For a deeper look at what events are and how they differ from ActiveRecord models, see the Events guide.
Rendering the form
Since events are ActiveModel objects, form_with and the standard error helpers work exactly as they do with ActiveRecord. Pass the event as the model and Rails takes care of field population and error display:
<%# app/views/debts/new.html.erb %>
<%= form_with model: @event, url: debt_events_path do |f| %>
<% if @event.errors.any? %>
<ul>
<% @event.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
<% end %>
<div>
<%= f.label :amount %>
<%= f.number_field :amount %>
</div>
<div>
<%= f.label :interest_rate, "Interest rate" %>
<%= f.number_field :interest_rate, step: "0.01" %>
</div>
<div>
<%= f.label :at, "Issued at" %>
<%= f.datetime_field :at %>
</div>
<%= f.submit "Issue debt" %>
<% end %>
Appending from a controller
To record an event, build it and call .append on the stream for the relevant entity. The result behaves exactly like saving an ActiveRecord model:
# app/controllers/debts_controller.rb
class DebtsController < ApplicationController
def new
@event = Debt::Issued.new
end
def create
@event = Debt::Issued.new(event_params)
event_stream = DebtEventStream.for(debt_id).append(@event)
if @event.persisted?
redirect_to debt_path(event_stream) # event streams are routable
else
render :new, status: :unprocessable_entity
end
end
private
def event_params
params.require(:debt_issued).permit(:amount, :interest_rate, :at)
end
def debt_id
# Your logic to define the stream id - it can use UUID, ULID, nanoid, etc
end
end
@event.persisted? returns true when the event was written to the log. On failure, the controller re-renders the form and @event.errors.full_messages surfaces the validation messages — the same way an invalid ActiveRecord model would.
Note: The string passed to
.foris the stream identifier — it groups all events for that entity together. You can learn more about how streams work in the Event Streams guide.