servus 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servus
4
+ module Events
5
+ # Default router that reads +invoke+ declarations from Event classes.
6
+ #
7
+ # ClassRouter is what ships with Servus and is the default when no
8
+ # routers are configured. It resolves invocations by looking up all
9
+ # Event classes registered for the given event name and calling
10
+ # +invocations_for+ on each — which evaluates if/unless conditions
11
+ # and returns Invocation objects for actions that should run.
12
+ #
13
+ # Applications can add additional routers (e.g. a data-driven router
14
+ # backed by a database table) alongside the ClassRouter:
15
+ #
16
+ # Servus.configure do |config|
17
+ # config.routers = [
18
+ # Servus::Events::ClassRouter.new,
19
+ # MyApp::DataDrivenRouter.new
20
+ # ]
21
+ # end
22
+ #
23
+ # @see Servus::Events::Router
24
+ # @see Servus::Event#invocations_for
25
+ class ClassRouter < Router
26
+ # Resolves invocations by reading +invoke+ declarations from all
27
+ # Event classes registered for the given event name.
28
+ #
29
+ # @param event_name [Symbol] the name of the emitted event
30
+ # @param payload [Hash] the event payload
31
+ # @return [Array<Servus::Events::Invocation>] invocations to execute
32
+ def resolve(event_name, payload)
33
+ event_class = Bus.event_for(event_name)
34
+ return [] unless event_class
35
+
36
+ event_class.invocations_for(payload)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -69,24 +69,24 @@ module Servus
69
69
  #
70
70
  # @note Best Practice: Services should typically emit ONE event per trigger
71
71
  # that represents their core concern. Multiple downstream reactions should
72
- # be coordinated by EventHandler classes, not by emitting multiple events
72
+ # be coordinated by Event classes, not by emitting multiple events
73
73
  # from the service. This maintains separation of concerns.
74
74
  #
75
- # @example Recommended pattern (one event, multiple handlers)
75
+ # @example Recommended pattern (one event, multiple reactions)
76
76
  # # Service emits one event
77
77
  # class CreateUser < Servus::Base
78
78
  # emits :user_created, on: :success
79
79
  # end
80
80
  #
81
- # # Handler coordinates multiple reactions
82
- # class UserCreatedHandler < Servus::EventHandler
83
- # handles :user_created
81
+ # # Event coordinates multiple reactions
82
+ # class UserCreated < Servus::Event
83
+ # event_name :user_created
84
84
  # invoke SendWelcomeEmail::Service, async: true
85
85
  # invoke TrackAnalytics::Service, async: true
86
86
  # end
87
87
  #
88
88
  # @see Servus::Events::Bus
89
- # @see Servus::EventHandler
89
+ # @see Servus::Event
90
90
  def emits(event_name, on:, with: nil, &block)
91
91
  valid_triggers = %i[success failure error!]
92
92
 
@@ -128,7 +128,6 @@ module Servus
128
128
  self.class.emissions_for(trigger).each do |emission|
129
129
  payload = build_event_payload(emission, result)
130
130
  validate_event_payload!(emission[:event_name], payload)
131
- Servus::Support::Logger.log_event(emission[:event_name], payload)
132
131
  Servus::Events::Bus.emit(emission[:event_name], payload)
133
132
  end
134
133
  end
@@ -136,7 +135,7 @@ module Servus
136
135
  # Instance methods for emitting events during service execution
137
136
  private
138
137
 
139
- # Validates the payload against all handler schemas registered for the event.
138
+ # Validates the payload against the Event class's schema registered for the event.
140
139
  #
141
140
  # @param event_name [Symbol] the event name
142
141
  # @param payload [Hash] the event payload
@@ -144,9 +143,10 @@ module Servus
144
143
  # @raise [Servus::Support::Errors::ValidationError] if payload fails validation
145
144
  # @api private
146
145
  def validate_event_payload!(event_name, payload)
147
- Servus::Events::Bus.handlers_for(event_name).each do |handler_class|
148
- Servus::Support::Validator.validate_event_payload!(handler_class, payload)
149
- end
146
+ event_class = Servus::Events::Bus.event_for(event_name)
147
+ return unless event_class
148
+
149
+ Servus::Support::Validator.validate_event_payload!(event_class, payload)
150
150
  end
151
151
 
152
152
  # Builds the event payload using the configured payload builder or defaults.
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+ require 'json'
5
+
6
+ module Servus
7
+ module Events
8
+ # A normalized, executable representation of "call this service with
9
+ # these params."
10
+ #
11
+ # Routers return arrays of Invocation objects. The Bus collects them,
12
+ # deduplicates by +#key+ (first wins), and calls +#execute+ on each.
13
+ #
14
+ # An Invocation separates *identity* (service + params) from
15
+ # *execution strategy* (async, queue, priority, etc.). The +#key+
16
+ # is derived only from the identity — two invocations that call the
17
+ # same service with the same params are considered duplicates
18
+ # regardless of their options.
19
+ #
20
+ # @example Sync invocation
21
+ # Invocation.new(
22
+ # service: Rewards::Grant::Service,
23
+ # params: { user_id: "abc-123" },
24
+ # options: {}
25
+ # )
26
+ #
27
+ # @example Async invocation with scheduling options
28
+ # Invocation.new(
29
+ # service: Notifications::Send::Service,
30
+ # params: { user_id: "abc-123" },
31
+ # options: { async: true, queue: :mailers, priority: 5 }
32
+ # )
33
+ #
34
+ # @see Servus::Events::Router
35
+ # @see Servus::Events::Bus
36
+ class Invocation
37
+ # @return [Class] the service class to call (must respond to +.call+ or +.call_async+)
38
+ attr_reader :service
39
+
40
+ # @return [Hash] keyword arguments passed to the service
41
+ attr_reader :params
42
+
43
+ # @return [Hash] execution options — +async+, +queue+, +wait+,
44
+ # +wait_until+, +priority+, +job_options+
45
+ attr_reader :options
46
+
47
+ # @param service [Class] the service class
48
+ # @param params [Hash] keyword arguments for the service
49
+ # @param options [Hash] execution options
50
+ def initialize(service:, params:, options: {})
51
+ @service = service
52
+ @params = params
53
+ @options = options
54
+ end
55
+
56
+ # Executes the invocation.
57
+ #
58
+ # Delegates to +service.call+ for synchronous invocations or
59
+ # +service.call_async+ for asynchronous ones. Async scheduling
60
+ # options (queue, wait, priority, etc.) are merged into the
61
+ # call_async kwargs.
62
+ #
63
+ # @return [Servus::Support::Response, void]
64
+ def execute
65
+ if options[:async]
66
+ service.call_async(**params, **async_options)
67
+ else
68
+ service.call(**params)
69
+ end
70
+ end
71
+
72
+ # A deterministic deduplication key derived from the service class
73
+ # and params. Two invocations with the same key are considered
74
+ # duplicates — the Bus keeps the first and skips the rest.
75
+ #
76
+ # Options are intentionally excluded: identity is *what* to call,
77
+ # not *how* to call it.
78
+ #
79
+ # @return [String] SHA-256 hex digest
80
+ def key
81
+ Digest::SHA256.hexdigest("#{service}:#{params.to_json}")
82
+ end
83
+
84
+ private
85
+
86
+ # Extracts scheduling options for +call_async+.
87
+ #
88
+ # @return [Hash]
89
+ def async_options
90
+ options.slice(:queue, :wait, :wait_until, :priority, :job_options).compact
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servus
4
+ module Events
5
+ # Abstract base class for event routers.
6
+ #
7
+ # Routers resolve which services should be invoked when an event fires.
8
+ # The Bus iterates all configured routers (in order), collects the
9
+ # Invocation objects they return, deduplicates by key, and executes.
10
+ #
11
+ # Servus ships one built-in router — ClassRouter — which reads the
12
+ # +invoke+ declarations from Event classes. Applications can add their
13
+ # own routers (e.g. a data-driven router backed by a database table)
14
+ # by subclassing Router and implementing +#resolve+.
15
+ #
16
+ # Configure routers in the Servus initializer:
17
+ #
18
+ # Servus.configure do |config|
19
+ # config.routers = [
20
+ # Servus::Events::ClassRouter.new,
21
+ # MyApp::DataDrivenRouter.new
22
+ # ]
23
+ # end
24
+ #
25
+ # @see Servus::Events::ClassRouter
26
+ # @see Servus::Events::Invocation
27
+ # @see Servus::Events::Bus
28
+ class Router
29
+ # Resolves which service invocations should run for the given event.
30
+ #
31
+ # Implementations must evaluate any conditions (if/unless) internally
32
+ # and return only invocations that *will* run. The Bus does not
33
+ # perform further filtering.
34
+ #
35
+ # @param event_name [Symbol] the name of the emitted event
36
+ # @param payload [Hash] the event payload
37
+ # @return [Array<Servus::Events::Invocation>] invocations to execute
38
+ # @raise [NotImplementedError] when called on the abstract base class
39
+ def resolve(event_name, payload)
40
+ raise NotImplementedError, "#{self.class}#resolve must be implemented"
41
+ end
42
+ end
43
+ end
44
+ end
@@ -25,7 +25,11 @@ module Servus
25
25
  end
26
26
  end
27
27
 
28
- # Load guards and event handlers, clear caches on reload
28
+ initializer 'servus.event_logging' do
29
+ Servus::Events::Bus.enable_logging!
30
+ end
31
+
32
+ # Load guards and event classes, clear caches on reload
29
33
  config.to_prepare do
30
34
  # Load custom guards from guards_dir
31
35
  guards_path = Rails.root.join(Servus.config.guards_dir)
@@ -37,16 +41,14 @@ module Servus
37
41
 
38
42
  Servus::Events::Bus.clear if Rails.env.development?
39
43
 
40
- # Eager load all event handlers
44
+ # Eager load all event classes
41
45
  events_path = Rails.root.join(Servus.config.events_dir)
42
- Dir[File.join(events_path, '**/*_handler.rb')].each do |file|
46
+ Dir[File.join(events_path, '**/*_event.rb')].each do |file|
43
47
  require_dependency file
44
48
  end
45
- end
46
49
 
47
- # NOTE: Event validation is available but not run automatically due to load order issues.
48
- # To validate handlers match emitted events, call manually:
49
- # Servus::EventHandler.validate_all_handlers!
50
- # Or create a rake task for CI validation.
50
+ # Infer and register event names for classes that didn't call event_name explicitly
51
+ Servus::Event.descendants.each(&:ensure_registered!)
52
+ end
51
53
  end
52
54
  end
@@ -267,7 +267,7 @@ module Servus
267
267
  def api_error = { code: http_status, message: message }
268
268
  end
269
269
 
270
- # Raised when a service or event handler is invoked without a required schema.
270
+ # Raised when a service or Event class is used without a required schema.
271
271
  #
272
272
  # Triggered by the +require_service_arguments_schema+,
273
273
  # +require_service_result_schema+, or +require_event_payload_schema+
@@ -63,12 +63,14 @@ module Servus
63
63
  logger.warn("#{service_class.name} guard failed: #{error.message}")
64
64
  end
65
65
 
66
- # Logs an event emission
66
+ # Logs an event emission with correlation ID and duration.
67
67
  #
68
68
  # @param event_name [Symbol] The event name
69
69
  # @param payload [Hash] The event payload
70
- def self.log_event(event_name, payload)
71
- logger.info("Event :#{event_name} emitted with payload: #{payload.inspect}")
70
+ # @param event_id [String] The unique event correlation ID
71
+ # @param duration_ms [Float] The dispatch duration in milliseconds
72
+ def self.log_event(event_name, payload, event_id:, duration_ms:)
73
+ logger.info("[#{event_id}] Event :#{event_name} (#{duration_ms.round(1)}ms) #{payload.inspect}")
72
74
  end
73
75
 
74
76
  # Logs a validation error from a service
@@ -102,27 +102,26 @@ module Servus
102
102
  end
103
103
  end
104
104
 
105
- # Validates event payload against the handler's payload schema.
105
+ # Validates event payload against the Event class's payload schema.
106
106
  #
107
- # @param handler_class [Class] the event handler class
107
+ # @param event_class [Class] the Event subclass
108
108
  # @param payload [Hash] the event payload to validate
109
109
  # @return [Boolean] true if validation passes
110
110
  # @raise [Servus::Support::Errors::ValidationError] if payload fails validation
111
111
  #
112
- #
113
112
  # @example
114
- # Validator.validate_event_payload!(MyEventHandler, { user_id: 123 })
113
+ # Validator.validate_event_payload!(UserCreated, { user_id: 123 })
115
114
  #
116
115
  # @api private
117
- def self.validate_event_payload!(handler_class, payload)
118
- schema = handler_class.payload_schema
119
- enforce_schema_presence!(schema, handler_class, :require_event_payload_schema)
116
+ def self.validate_event_payload!(event_class, payload)
117
+ schema = event_class.payload_schema
118
+ enforce_schema_presence!(schema, event_class, :require_event_payload_schema)
120
119
  return true unless schema
121
120
 
122
121
  validate_data_against_schema!(
123
122
  payload,
124
123
  schema,
125
- "Invalid payload for event :#{handler_class.event_name}"
124
+ "Invalid payload for event :#{event_class.event_name}"
126
125
  )
127
126
 
128
127
  true
@@ -210,7 +209,7 @@ module Servus
210
209
  # Returns the schema if present. Raises if absent and the config flag is enabled.
211
210
  #
212
211
  # @param schema [Hash, nil] the loaded schema
213
- # @param klass [Class] the service or handler class
212
+ # @param klass [Class] the service or Event class
214
213
  # @param config_flag [Symbol] the config method to check
215
214
  # @return [Hash, nil] the schema
216
215
  # @raise [Servus::Support::Errors::SchemaRequiredError] if schema is nil and enforcement is enabled
@@ -12,7 +12,7 @@ module Servus
12
12
  end
13
13
 
14
14
  # Matcher for asserting event emission
15
- RSpec::Matchers.define :emit_event do |handler_class_or_symbol|
15
+ RSpec::Matchers.define :emit_event do |event_class_or_symbol|
16
16
  supports_block_expectations
17
17
 
18
18
  chain :with do |payload|
@@ -30,10 +30,10 @@ RSpec::Matchers.define :emit_event do |handler_class_or_symbol|
30
30
  block.call
31
31
 
32
32
  # Determine event name
33
- @event_name = if handler_class_or_symbol.is_a?(Symbol)
34
- handler_class_or_symbol
33
+ @event_name = if event_class_or_symbol.is_a?(Symbol)
34
+ event_class_or_symbol
35
35
  else
36
- handler_class_or_symbol.event_name
36
+ event_class_or_symbol.event_name
37
37
  end
38
38
 
39
39
  @matching_event = @captured_events.find { |e| e[:name] == @event_name }
@@ -86,7 +86,7 @@ RSpec::Matchers.define :call_service do |service_class|
86
86
  end
87
87
  end
88
88
 
89
- # Matcher for asserting schema presence on a service or event handler
89
+ # Matcher for asserting schema presence on a service or Event class
90
90
  RSpec::Matchers.define :have_schema do |schema_type|
91
91
  match do |klass|
92
92
  if schema_type.to_s == 'payload'
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Servus
4
- VERSION = '0.4.0'
4
+ VERSION = '0.5.0'
5
5
  end
data/lib/servus.rb CHANGED
@@ -29,10 +29,14 @@ require_relative 'servus/support/lockdown'
29
29
  require_relative 'servus/support/message_resolver'
30
30
 
31
31
  # Events
32
- require_relative 'servus/events/errors'
33
32
  require_relative 'servus/events/bus'
34
33
  require_relative 'servus/events/emitter'
35
- require_relative 'servus/event_handler'
34
+ require_relative 'servus/event'
35
+
36
+ # Routing
37
+ require_relative 'servus/events/router'
38
+ require_relative 'servus/events/invocation'
39
+ require_relative 'servus/events/class_router'
36
40
 
37
41
  # Guards (guards.rb loads defaults based on config)
38
42
  require_relative 'servus/guard'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: servus
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sebastian Scholl
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-04-30 00:00:00.000000000 Z
11
+ date: 2026-05-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: active_model_serializers
@@ -75,9 +75,9 @@ executables: []
75
75
  extensions: []
76
76
  extra_rdoc_files: []
77
77
  files:
78
- - lib/generators/servus/event_handler/event_handler_generator.rb
79
- - lib/generators/servus/event_handler/templates/handler.rb.erb
80
- - lib/generators/servus/event_handler/templates/handler_spec.rb.erb
78
+ - lib/generators/servus/event/event_generator.rb
79
+ - lib/generators/servus/event/templates/event.rb.erb
80
+ - lib/generators/servus/event/templates/event_spec.rb.erb
81
81
  - lib/generators/servus/guard/guard_generator.rb
82
82
  - lib/generators/servus/guard/templates/guard.rb.erb
83
83
  - lib/generators/servus/guard/templates/guard_spec.rb.erb
@@ -89,10 +89,12 @@ files:
89
89
  - lib/servus.rb
90
90
  - lib/servus/base.rb
91
91
  - lib/servus/config.rb
92
- - lib/servus/event_handler.rb
92
+ - lib/servus/event.rb
93
93
  - lib/servus/events/bus.rb
94
+ - lib/servus/events/class_router.rb
94
95
  - lib/servus/events/emitter.rb
95
- - lib/servus/events/errors.rb
96
+ - lib/servus/events/invocation.rb
97
+ - lib/servus/events/router.rb
96
98
  - lib/servus/extensions/async/call.rb
97
99
  - lib/servus/extensions/async/errors.rb
98
100
  - lib/servus/extensions/async/ext.rb
@@ -1,59 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Servus
4
- module Generators
5
- # Rails generator for creating Servus event handlers.
6
- #
7
- # Generates an event handler class and spec file.
8
- #
9
- # @example Generate an event handler
10
- # rails g servus:event_handler user_created
11
- #
12
- # @example Generated files
13
- # app/events/user_created_handler.rb
14
- # spec/app/events/user_created_handler_spec.rb
15
- #
16
- # @see https://guides.rubyonrails.org/generators.html
17
- class EventHandlerGenerator < Rails::Generators::NamedBase
18
- source_root File.expand_path('templates', __dir__)
19
-
20
- class_option :no_docs, type: :boolean,
21
- default: false,
22
- desc: 'Skip documentation comments in generated files'
23
-
24
- # Creates the event handler and spec files.
25
- #
26
- # @return [void]
27
- def create_handler_file
28
- template 'handler.rb.erb', handler_path
29
- template 'handler_spec.rb.erb', handler_spec_path
30
- end
31
-
32
- private
33
-
34
- # Returns the path for the handler file.
35
- #
36
- # @return [String] handler file path
37
- # @api private
38
- def handler_path
39
- File.join(Servus.config.events_dir, "#{file_name}_handler.rb")
40
- end
41
-
42
- # Returns the path for the handler spec file.
43
- #
44
- # @return [String] spec file path
45
- # @api private
46
- def handler_spec_path
47
- File.join(Servus.config.tests_dir, Servus.config.events_dir, "#{file_name}_handler_spec.rb")
48
- end
49
-
50
- # Returns the handler class name.
51
- #
52
- # @return [String] handler class name
53
- # @api private
54
- def handler_class_name
55
- "#{class_name}Handler"
56
- end
57
- end
58
- end
59
- end
@@ -1,86 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- <%- unless options[:no_docs] -%>
4
- # Handles the :<%= file_name %> event by invoking configured services.
5
- #
6
- # EventHandlers subscribe to a single event and declaratively map it to one or
7
- # more service invocations. This provides clean separation between event emission
8
- # (what happened) and event handling (what to do about it).
9
- #
10
- # @example Basic usage
11
- # class <%= handler_class_name %> < Servus::EventHandler
12
- # handles :<%= file_name %>
13
- #
14
- # # Invoke a service when this event fires
15
- # invoke SendEmail::Service, async: true do |payload|
16
- # { user_id: payload[:user_id], email: payload[:email] }
17
- # end
18
- # end
19
- #
20
- # @example Multiple service invocations
21
- # invoke SendWelcomeEmail::Service, async: true, queue: :mailers do |payload|
22
- # { user_id: payload[:user_id] }
23
- # end
24
- #
25
- # invoke TrackAnalytics::Service, async: true do |payload|
26
- # { event: '<%= file_name %>', user_id: payload[:user_id] }
27
- # end
28
- #
29
- # @example Conditional invocation
30
- # invoke GrantRewards::Service, if: ->(payload) { payload[:premium] } do |payload|
31
- # { user_id: payload[:user_id] }
32
- # end
33
- #
34
- # @example With payload schema validation
35
- # schema payload: {
36
- # type: 'object',
37
- # required: ['user_id'],
38
- # properties: {
39
- # user_id: { type: 'integer' },
40
- # email: { type: 'string', format: 'email' }
41
- # }
42
- # }
43
- #
44
- # @example Emit this event from anywhere
45
- # # From controllers, jobs, rake tasks, etc.
46
- # <%= handler_class_name %>.emit({ user_id: 123, email: 'user@example.com' })
47
- #
48
- # Available options for `invoke`:
49
- # - async: true - Invoke service asynchronously via ActiveJob
50
- # - queue: :queue_name - Specify ActiveJob queue (requires async: true)
51
- # - if: ->(payload) {} - Condition that must be true to invoke
52
- # - unless: ->(payload) {} - Condition that must be false to invoke
53
- #
54
- # @see Servus::EventHandler
55
- # @see Servus::Events::Bus
56
- <%- end -%>
57
- class <%= handler_class_name %> < Servus::EventHandler
58
- handles :<%= file_name %>
59
-
60
- <%- unless options[:no_docs] -%>
61
- # TODO: Define payload schema (optional but recommended)
62
- # schema payload: {
63
- # type: 'object',
64
- # required: ['required_field'],
65
- # properties: {
66
- # required_field: { type: 'string' }
67
- # }
68
- # }
69
-
70
- # TODO: Add service invocations
71
- # invoke YourService, async: true do |payload|
72
- # {
73
- # # Map event payload to service arguments
74
- # argument_name: payload[:field_name]
75
- # }
76
- # end
77
- <%- end -%>
78
- schema payload: {
79
- type: 'object',
80
- description: 'JSON schema for the <%= handler_class_name %> event payload',
81
- }
82
-
83
- # invoke ExampleService, async: true do |payload|
84
- # { example_arg: payload[:example_field] }
85
- # end
86
- end
@@ -1,48 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'rails_helper'
4
-
5
- RSpec.describe <%= handler_class_name %> do
6
- <%- unless options[:no_docs] -%>
7
- # Test that the handler invokes the correct services with properly mapped arguments.
8
- #
9
- # Example test pattern:
10
- # it 'invokes YourService with correct arguments' do
11
- # expect(YourService).to receive(:call_async).with(user_id: 123)
12
- # described_class.handle({ user_id: 123, email: 'test@example.com' })
13
- # end
14
- #
15
- # For testing event emission from controllers/jobs:
16
- # include Servus::Testing::EventHelpers
17
- #
18
- # it 'emits <%= file_name %> event' do
19
- # servus_expect_event(:<%= file_name %>)
20
- # .with_payload(hash_including(user_id: 123))
21
- # .when { YourController.new.create }
22
- # end
23
-
24
- <%- end -%>
25
- let(:payload) do
26
- {
27
- # TODO: Add sample payload fields
28
- # user_id: 123,
29
- # email: 'test@example.com'
30
- }
31
- end
32
-
33
- <%- unless options[:no_docs] -%>
34
- # TODO: Add tests for service invocations
35
- # it 'invokes YourService with mapped arguments' do
36
- # expect(YourService).to receive(:call_async).with(user_id: payload[:user_id])
37
- # described_class.handle(payload)
38
- # end
39
-
40
- # TODO: Test conditional logic if using :if or :unless
41
- # it 'skips invocation when condition is false' do
42
- # expect(YourService).not_to receive(:call_async)
43
- # described_class.handle(payload.merge(premium: false))
44
- # end
45
- <%- else -%>
46
- # Add your tests here
47
- <%- end -%>
48
- end