Class: Servus::Event

Inherits:
Object
  • Object
show all
Extended by:
Schema::Declaration
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'] }

  enqueue SendWelcomeEmail::Service 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

  enqueue AuditLogger::Service
end

See Also:

Class Method Summary collapse

Methods included from Schema::Declaration

declare_schemas, schema, schema_types

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



226
227
228
229
230
231
232
# File 'lib/servus/event.rb', line 226

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

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

This method returns an undefined value.

Declares a service to enqueue in response to the event.

An event can declare as many services as it needs; each is enqueued independently when the event fires. The block maps the event payload to the service's keyword arguments — without one, the full payload is passed through.

Invocation is always asynchronous. A reaction that ran inline would put its latency and its failures back into the emitting service, which is what events exist to avoid. This requires ActiveJob; see Servus::Events::Errors::AsyncBackendMissingError.

Examples:

Enqueue a service

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

Route to a queue

enqueue SendEmail::Service, queue: :mailers do |payload|
  { user_id: payload[:user_id] }
end

Conditional

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

Parameters:

  • service_class (Class)

    the service to enqueue (must inherit from Servus::Base)

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

    invocation options

Options Hash (options):

  • :queue (Symbol)

    the queue to route the job to

  • :wait (ActiveSupport::Duration)

    delay before the job runs

  • :wait_until (Time)

    absolute time to run the job

  • :priority (Integer)

    job priority (adapter-dependent)

  • :job_options (Hash)

    additional ActiveJob options

  • :if (Proc)

    condition that must return true to enqueue

  • :unless (Proc)

    condition that must return false to enqueue

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

Raises:

  • (ArgumentError)

    if the removed async: option is passed



169
170
171
172
173
174
175
176
177
178
# File 'lib/servus/event.rb', line 169

def enqueue(service_class, options = {}, &block)
  reject_async_option!(options)

  @invocations ||= []
  @invocations << {
    service_class: service_class,
    options: options,
    mapper: block || ->(payload) { 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.



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

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



107
108
109
110
111
112
113
114
# File 'lib/servus/event.rb', line 107

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



255
256
257
# File 'lib/servus/event.rb', line 255

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

.invocationsArray<Hash>

Returns all service invocations declared for this event.

Returns:

  • (Array<Hash>)

    array of invocation configurations



197
198
199
# File 'lib/servus/event.rb', line 197

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:



239
240
241
242
243
244
245
246
247
248
249
# File 'lib/servus/event.rb', line 239

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(*_args, **_options) ⇒ Object

Deprecated.

Use #enqueue.

Explains that invoke was renamed, rather than failing as a typo.

Event classes load at boot, so a bare NoMethodError here would read like a misspelling instead of a rename. This covers both changes at once, since the overwhelmingly common declaration was invoke Foo, async: true.

Raises:

  • (NoMethodError)

    always



188
189
190
191
192
# File 'lib/servus/event.rb', line 188

def invoke(*_args, **_options, &)
  raise NoMethodError,
        '`invoke` was renamed to `enqueue` in 1.0.0 — event invocation is always ' \
        'asynchronous. Replace `invoke` with `enqueue`, and drop `async:` if present.'
end

.payload_schemaHash?

Returns the compiled payload schema.

Returns:

  • (Hash, nil)

    the compiled payload schema



78
# File 'lib/servus/event.rb', line 78

declare_schemas :payload

.schema(payload: nil) ⇒ void

This method returns an undefined value.

Declares the JSON schema for this event's payload.

The payload is validated on every emit. Schemas may reference shared fragments registered with Schema.register; refs are resolved on first read.

Omitting the keyword leaves any schema declared earlier — or by a superclass — in place. Passing it explicitly as nil raises.

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) (defaults to: nil)

    JSON schema for the event payload

Raises:

  • (ArgumentError)

    on an unknown keyword or an explicit nil

See Also:



78
# File 'lib/servus/event.rb', line 78

declare_schemas :payload