Class: Servus::Event

Inherits:
Object
  • Object
show all
Defined in:
lib/servus/event.rb

Overview

Base class for event definitions.

Event classes live in app/events/ and serve three purposes:

  1. Contract — declares the event exists and defines its name

  2. Validator — schema enforcement on any emission

  3. *Declarative routing* — optional invoke declarations

The event name can be set explicitly via event_name or inferred from the class name (e.g. OrderPlaced becomes :order_placed). Call ensure_registered! to trigger inference for classes that don’t declare an explicit name.

Examples:

Event with explicit name and invoke declarations

class UserCreated < Servus::Event
  event_name :user_created

  schema payload: { type: 'object', required: ['user_id'] }

  invoke SendWelcomeEmail::Service, async: true do |payload|
    { user_id: payload[:user_id] }
  end
end

Event with inferred name (no invoke — schema-only contract)

class OrderPlaced < Servus::Event
  schema payload: { type: 'object', required: ['order_id'] }
end

Event that passes full payload through (no mapper block)

class AuditLogCreated < Servus::Event
  event_name :audit_log_created

  invoke AuditLogger::Service, async: true
end

See Also:

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.payload_schemaHash? (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns the payload schema.

Returns:

  • (Hash, nil)

    the payload schema or nil if not defined



164
165
166
# File 'lib/servus/event.rb', line 164

def payload_schema
  @payload_schema
end

Class Method Details

.emit(payload) ⇒ void

This method returns an undefined value.

Emits this event via the Bus.

Provides a type-safe, discoverable way to emit events from anywhere in the application (controllers, jobs, rake tasks) without creating a service.

Examples:

Emit from controller

class UsersController
  def create
    user = User.create!(params)
    UserCreated.emit({ user_id: user.id, email: user.email })
    redirect_to user
  end
end

Emit from background job

class ProcessDataJob
  def perform(data_id)
    result = process_data(data_id)
    DataProcessed.emit({ data_id: data_id, status: result })
  end
end

Parameters:

  • payload (Hash)

    the event payload

Raises:

  • (RuntimeError)

    if no event name configured



191
192
193
194
195
196
197
# File 'lib/servus/event.rb', line 191

def emit(payload)
  raise 'No event configured. Call event_name :name first.' unless @event_name

  Servus::Support::Validator.validate_event_payload!(self, payload)

  Servus::Events::Bus.emit(@event_name, payload)
end

.ensure_registered!void

This method returns an undefined value.

Infers and registers the event name from the class name if not already set explicitly. Safe to call multiple times — does nothing if already registered. Skips anonymous classes.



85
86
87
88
89
90
# File 'lib/servus/event.rb', line 85

def ensure_registered!
  return if @event_name
  return if name.nil?

  event_name(name.demodulize.underscore.to_sym)
end

.event_name(name) ⇒ void .event_nameSymbol?

Declares or returns the event name.

When called with an argument, sets the event name and registers with the Bus. When called without arguments, returns the current event name.

If never called explicitly, use ensure_registered! to infer the name from the class name.

Examples:

Explicit name

class UserCreated < Servus::Event
  event_name :user_created
end

Inferred name (via ensure_registered!)

class OrderPlaced < Servus::Event; end
OrderPlaced.ensure_registered!
OrderPlaced.event_name # => :order_placed

Overloads:

  • .event_name(name) ⇒ void

    This method returns an undefined value.

    Parameters:

    • name (Symbol)

      the event name to register

    Raises:

    • (RuntimeError)

      if called twice with different names

  • .event_nameSymbol?

    Returns the event name or nil if not configured.

    Returns:

    • (Symbol, nil)

      the event name or nil if not configured



71
72
73
74
75
76
77
78
# File 'lib/servus/event.rb', line 71

def event_name(name = nil)
  return @event_name if name.nil?

  raise "Event already subscribed to :#{@event_name}. Cannot subscribe to :#{name}" if @event_name

  @event_name = name
  Servus::Events::Bus.register_event(name, self)
end

.handle(payload) ⇒ Array

Handles an event by resolving and executing all invocations.

Parameters:

  • payload (Hash)

    the event payload

Returns:

  • (Array)

    results from all invoked services



220
221
222
# File 'lib/servus/event.rb', line 220

def handle(payload)
  invocations_for(payload).map(&:execute)
end

.invocationsArray<Hash>

Returns all service invocations declared for this event.

Returns:

  • (Array<Hash>)

    array of invocation configurations



134
135
136
# File 'lib/servus/event.rb', line 134

def invocations
  @invocations || []
end

.invocations_for(payload) ⇒ Array<Servus::Events::Invocation>

Returns Invocation objects for the given payload, with conditions already evaluated. This is what routers call to resolve actions.

Parameters:

  • payload (Hash)

    the event payload

Returns:



204
205
206
207
208
209
210
211
212
213
214
# File 'lib/servus/event.rb', line 204

def invocations_for(payload)
  invocations.filter_map do |inv|
    next unless should_invoke?(payload, inv[:options])

    Servus::Events::Invocation.new(
      service: inv[:service_class],
      params: inv[:mapper].call(payload),
      options: inv[:options].except(:if, :unless)
    )
  end
end

.invoke(service_class, options = {}) {|payload| ... } ⇒ void

This method returns an undefined value.

Declares a service invocation in response to the event.

Multiple invocations can be declared for a single event. Each invocation requires a block that maps the event payload to the service’s arguments.

Examples:

Basic invocation

invoke SendEmail::Service do |payload|
  { user_id: payload[:user_id], email: payload[:email] }
end

Async invocation with queue

invoke SendEmail::Service, async: true, queue: :mailers do |payload|
  { user_id: payload[:user_id] }
end

Conditional invocation

invoke GrantRewards::Service, if: ->(p) { p[:premium] } do |payload|
  { user_id: payload[:user_id] }
end

Parameters:

  • service_class (Class)

    the service class to invoke (must inherit from Servus::Base)

  • options (Hash) (defaults to: {})

    invocation options

Options Hash (options):

  • :async (Boolean)

    invoke the service asynchronously via call_async

  • :queue (Symbol)

    the queue name for async jobs

  • :if (Proc)

    condition that must return true for invocation

  • :unless (Proc)

    condition that must return false for invocation

Yields:

  • (payload)

    block that maps event payload to service arguments

Yield Parameters:

  • payload (Hash)

    the event payload

Yield Returns:

  • (Hash)

    keyword arguments for the service’s initialize method



122
123
124
125
126
127
128
129
# File 'lib/servus/event.rb', line 122

def invoke(service_class, options = {}, &block)
  @invocations ||= []
  @invocations << {
    service_class: service_class,
    options: options,
    mapper: block || ->(payload) { payload }
  }
end

.schema(payload: nil) ⇒ void

This method returns an undefined value.

Defines the JSON schema for validating event payloads.

Examples:

class UserCreated < Servus::Event
  event_name :user_created

  schema payload: {
    type: 'object',
    required: ['user_id', 'email'],
    properties: {
      user_id: { type: 'integer' },
      email: { type: 'string', format: 'email' }
    }
  }
end

Parameters:

  • payload (Hash, nil) (defaults to: nil)

    JSON schema for validating event payloads



156
157
158
# File 'lib/servus/event.rb', line 156

def schema(payload: nil)
  @payload_schema = payload.with_indifferent_access if payload
end