servus 0.1.5 → 0.2.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.
- checksums.yaml +4 -4
- data/.claude/settings.json +9 -0
- data/CHANGELOG.md +45 -1
- data/READme.md +120 -15
- data/docs/features/6_guards.md +356 -0
- data/docs/features/guards_naming_convention.md +540 -0
- data/docs/integration/1_configuration.md +52 -2
- data/lib/generators/servus/guard/guard_generator.rb +75 -0
- data/lib/generators/servus/guard/templates/guard.rb.erb +69 -0
- data/lib/generators/servus/guard/templates/guard_spec.rb.erb +65 -0
- data/lib/servus/base.rb +9 -1
- data/lib/servus/config.rb +17 -2
- data/lib/servus/events/emitter.rb +3 -3
- data/lib/servus/guard.rb +289 -0
- data/lib/servus/guards/falsey_guard.rb +59 -0
- data/lib/servus/guards/presence_guard.rb +80 -0
- data/lib/servus/guards/state_guard.rb +62 -0
- data/lib/servus/guards/truthy_guard.rb +61 -0
- data/lib/servus/guards.rb +48 -0
- data/lib/servus/helpers/controller_helpers.rb +20 -48
- data/lib/servus/railtie.rb +11 -3
- data/lib/servus/support/errors.rb +69 -140
- data/lib/servus/support/message_resolver.rb +166 -0
- data/lib/servus/version.rb +1 -1
- data/lib/servus.rb +5 -0
- metadata +13 -8
- data/builds/servus-0.0.1.gem +0 -0
- data/builds/servus-0.1.1.gem +0 -0
- data/builds/servus-0.1.2.gem +0 -0
- data/builds/servus-0.1.3.gem +0 -0
- data/builds/servus-0.1.4.gem +0 -0
- data/builds/servus-0.1.5.gem +0 -0
- data/docs/current_focus.md +0 -569
data/docs/current_focus.md
DELETED
|
@@ -1,569 +0,0 @@
|
|
|
1
|
-
# Servus Event Bus Specification
|
|
2
|
-
|
|
3
|
-
## 1. Overview & Philosophy
|
|
4
|
-
|
|
5
|
-
This document specifies the design and API for a native event bus within the Servus gem. The core philosophy is **clean separation of concerns**, ensuring that services remain focused on business logic, while a dedicated, lightweight layer handles the routing of events to service invocations.
|
|
6
|
-
|
|
7
|
-
This approach is defined by three distinct layers:
|
|
8
|
-
|
|
9
|
-
1. **The Emitter (****`Servus::Base`****)**: A service that performs a business function and declares the events it emits upon completion.
|
|
10
|
-
|
|
11
|
-
1. **The Mapper (****`Servus::EventHandler`****)**: A new, lightweight class whose sole responsibility is to subscribe to a single event and declaratively map it to one or more service invocations.
|
|
12
|
-
|
|
13
|
-
1. **The Consumer (****`Servus::Base`****)**: A service that is invoked by the event handler and performs a subsequent business function, without needing any awareness of the eventing system.
|
|
14
|
-
|
|
15
|
-
This design avoids overloading service classes with subscription logic and eliminates the need for auto-generated code, resulting in a system that is explicit, discoverable, and highly maintainable.
|
|
16
|
-
|
|
17
|
-
---
|
|
18
|
-
|
|
19
|
-
## 2. Core Components
|
|
20
|
-
|
|
21
|
-
### 2.1. The Emitter: `Servus::Base`
|
|
22
|
-
|
|
23
|
-
Services that emit events will use a class-level `emits` method to declare them.
|
|
24
|
-
|
|
25
|
-
#### **API: ****`emits(event_name, on:, with: nil)`**
|
|
26
|
-
|
|
27
|
-
- **`event_name`**** (Symbol)**: The unique name of the event (e.g., `:referral_created`).
|
|
28
|
-
|
|
29
|
-
- **`on`**** (Symbol)**: The trigger for automatic emission. Must be `:success` or `:failure`.
|
|
30
|
-
|
|
31
|
-
- **`with`**** (Symbol, Optional)**: The name of an instance method on the service that will build the event payload. This method receives the service's `Servus::Support::Response` object as its only argument.
|
|
32
|
-
|
|
33
|
-
If the `with:` option is omitted, the payload will be `result.data` for a success event or `{ error: result.error }` for a failure event.
|
|
34
|
-
|
|
35
|
-
#### **Example: Service Declaration**
|
|
36
|
-
|
|
37
|
-
```ruby
|
|
38
|
-
# app/services/referrals/create_referral/service.rb
|
|
39
|
-
class Referrals::CreateReferral::Service < Servus::Base
|
|
40
|
-
# On success, emit :referral_created, building the payload with the
|
|
41
|
-
# :referral_payload instance method.
|
|
42
|
-
emits :referral_created, on: :success, with: :referral_payload
|
|
43
|
-
|
|
44
|
-
# On failure, emit :referral_failed with a default error payload.
|
|
45
|
-
emits :referral_failed, on: :failure
|
|
46
|
-
|
|
47
|
-
# On error, emit :referral_error with a default error payload
|
|
48
|
-
emits :referral_error, on: :error
|
|
49
|
-
|
|
50
|
-
def initialize(referee_id:)
|
|
51
|
-
@referee_id = referee_id
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def call
|
|
55
|
-
# ... business logic ...
|
|
56
|
-
|
|
57
|
-
# The :referral_created event is automatically emitted here upon success.
|
|
58
|
-
success({ referral: @referral, referee: @referee, referrer: @referrer })
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
private
|
|
62
|
-
|
|
63
|
-
# This method is called by the event system to build the payload.
|
|
64
|
-
def referral_payload(result)
|
|
65
|
-
{
|
|
66
|
-
referral_id: result.data[:referral].id,
|
|
67
|
-
referee_id: result.data[:referee].id,
|
|
68
|
-
referrer_id: result.data[:referrer].id,
|
|
69
|
-
created_at: Time.current
|
|
70
|
-
}
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
### 2.2. The Mapper: `Servus::EventHandler`
|
|
76
|
-
|
|
77
|
-
This is a new, lightweight class that lives in the `app/events` directory. Each handler subscribes to a single event and maps it to one or more service invocations.
|
|
78
|
-
|
|
79
|
-
#### **API: ****`handles(event_name)`**
|
|
80
|
-
|
|
81
|
-
- **`event_name`**** (Symbol)**: The unique name of the event this class handles.
|
|
82
|
-
|
|
83
|
-
#### **API: ****`invoke(service_class, options = {}, &block)`**
|
|
84
|
-
|
|
85
|
-
- **`service_class`**** (Class)**: The `Servus::Base` subclass to be invoked.
|
|
86
|
-
|
|
87
|
-
- **`options`**** (Hash, Optional)**: A hash of options for the invocation.
|
|
88
|
-
- `:async` (Boolean): If `true`, invokes the service using `.call_async`. Defaults to `false`.
|
|
89
|
-
- `:queue` (Symbol): The queue name to use for async jobs.
|
|
90
|
-
- `:if` (Proc): A lambda that receives the payload and must return `true` for the invocation to proceed.
|
|
91
|
-
- `:unless` (Proc): A lambda that receives the payload and must return `false` for the invocation to proceed.
|
|
92
|
-
|
|
93
|
-
- **`&block`**** (Block)**: A block that receives the event payload and **must** return a hash of keyword arguments for the target service's `initialize` method.
|
|
94
|
-
|
|
95
|
-
#### **Example: Event Handler**
|
|
96
|
-
|
|
97
|
-
```ruby
|
|
98
|
-
# app/events/referral_created_handler.rb
|
|
99
|
-
class ReferralCreatedHandler < Servus::EventHandler
|
|
100
|
-
# Subscribe to the :referral_created event.
|
|
101
|
-
handles :referral_created
|
|
102
|
-
|
|
103
|
-
# Define an invocation for the Rewards::GrantReferralRewards::Service.
|
|
104
|
-
invoke Rewards::GrantReferralRewards::Service, async: true do |payload|
|
|
105
|
-
# Map the event payload to the service's required arguments.
|
|
106
|
-
{ user_id: payload[:referrer_id] }
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
# Define another invocation for the Referrals::ActivityNotifier::Service.
|
|
110
|
-
invoke Referrals::ActivityNotifier::Service, async: true, queue: :notifications do |payload|
|
|
111
|
-
{ referral_id: payload[:referral_id] }
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
### 2.3. Automatic Registration
|
|
117
|
-
|
|
118
|
-
All classes inheriting from `Servus::EventHandler` within the `app/events` directory will be automatically discovered and registered by the gem at boot time. No manual configuration is required.
|
|
119
|
-
|
|
120
|
-
---
|
|
121
|
-
|
|
122
|
-
## 3. Directory Structure
|
|
123
|
-
|
|
124
|
-
The introduction of `EventHandler` classes establishes a new conventional directory:
|
|
125
|
-
|
|
126
|
-
```
|
|
127
|
-
app/
|
|
128
|
-
├── events/ # New directory for event handlers
|
|
129
|
-
│ ├── referral_created_handler.rb
|
|
130
|
-
│ └── user_graduated_handler.rb
|
|
131
|
-
├── services/
|
|
132
|
-
│ ├── referrals/
|
|
133
|
-
│ │ └── create_referral/
|
|
134
|
-
│ │ └── service.rb # Emitter
|
|
135
|
-
│ └── rewards/
|
|
136
|
-
│ └── grant_referral_rewards/
|
|
137
|
-
│ └── service.rb # Consumer
|
|
138
|
-
└── ...
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
---
|
|
142
|
-
|
|
143
|
-
## 4. Generators
|
|
144
|
-
|
|
145
|
-
A Rails generator will be provided to facilitate the creation of `EventHandler` classes.
|
|
146
|
-
|
|
147
|
-
#### **Command**
|
|
148
|
-
|
|
149
|
-
```bash
|
|
150
|
-
$ rails g servus:event_handler referral_created
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
#### **Output**
|
|
154
|
-
|
|
155
|
-
This command will generate two files:
|
|
156
|
-
|
|
157
|
-
1. `app/events/referral_created_handler.rb`
|
|
158
|
-
|
|
159
|
-
1. `spec/events/referral_created_handler_spec.rb`
|
|
160
|
-
|
|
161
|
-
#### **Generated Handler Template**
|
|
162
|
-
|
|
163
|
-
```ruby
|
|
164
|
-
# app/events/referral_created_handler.rb
|
|
165
|
-
class ReferralCreatedHandler < Servus::EventHandler
|
|
166
|
-
handles :referral_created
|
|
167
|
-
|
|
168
|
-
# TODO: Add service invocations using the `invoke` DSL.
|
|
169
|
-
#
|
|
170
|
-
# Example:
|
|
171
|
-
# invoke SomeService, async: true do |payload|
|
|
172
|
-
# { argument_name: payload[:some_key] }
|
|
173
|
-
# end
|
|
174
|
-
end
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
---
|
|
178
|
-
|
|
179
|
-
## 5. Testing Strategy
|
|
180
|
-
|
|
181
|
-
The separation of concerns enables focused and decoupled testing.
|
|
182
|
-
|
|
183
|
-
### 5.1. Testing Event Emission
|
|
184
|
-
|
|
185
|
-
When testing a service, you should only assert that the correct event was emitted with the expected payload. A test helper will be provided for this.
|
|
186
|
-
|
|
187
|
-
```ruby
|
|
188
|
-
# spec/services/referrals/create_referral/service_spec.rb
|
|
189
|
-
RSpec.describe Referrals::CreateReferral::Service do
|
|
190
|
-
include Servus::Events::TestHelpers
|
|
191
|
-
|
|
192
|
-
it 'emits a :referral_created event on success' do
|
|
193
|
-
# Assert that the block will cause the specified event to be emitted.
|
|
194
|
-
expect_event(:referral_created)
|
|
195
|
-
.with_payload(hash_including(:referral_id, :referee_id, :referrer_id))
|
|
196
|
-
.when { described_class.call(referee_id: referee.id) }
|
|
197
|
-
end
|
|
198
|
-
end
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
### 5.2. Testing an Event Handler
|
|
202
|
-
|
|
203
|
-
When testing a handler, you should provide a sample payload and assert that the correct services are invoked with the correctly mapped arguments.
|
|
204
|
-
|
|
205
|
-
```ruby
|
|
206
|
-
# spec/events/referral_created_handler_spec.rb
|
|
207
|
-
RSpec.describe ReferralCreatedHandler do
|
|
208
|
-
let(:payload) do
|
|
209
|
-
{
|
|
210
|
-
referral_id: 'referral-123',
|
|
211
|
-
referrer_id: 'user-456',
|
|
212
|
-
referee_id: 'user-789',
|
|
213
|
-
created_at: Time.current
|
|
214
|
-
}
|
|
215
|
-
end
|
|
216
|
-
|
|
217
|
-
it 'invokes the GrantReferralRewards service with the correct user ID' do
|
|
218
|
-
expect(Rewards::GrantReferralRewards::Service)
|
|
219
|
-
.to receive(:call_async)
|
|
220
|
-
.with(user_id: 'user-456')
|
|
221
|
-
|
|
222
|
-
# Trigger the handler with the test payload.
|
|
223
|
-
described_class.handle(payload)
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
it 'invokes the ActivityNotifier service with the correct referral ID' do
|
|
227
|
-
expect(Referrals::ActivityNotifier::Service)
|
|
228
|
-
.to receive(:call_async)
|
|
229
|
-
.with(referral_id: 'referral-123', queue: :notifications)
|
|
230
|
-
|
|
231
|
-
described_class.handle(payload)
|
|
232
|
-
end
|
|
233
|
-
end
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
---
|
|
237
|
-
|
|
238
|
-
## 6. Implementation Plan
|
|
239
|
-
|
|
240
|
-
This section provides a detailed, phase-by-phase breakdown of tasks required to implement the event bus feature. Each phase builds upon the previous one, and tasks are organized by logical implementation order.
|
|
241
|
-
|
|
242
|
-
### Phase 1: Core Event Infrastructure
|
|
243
|
-
|
|
244
|
-
**Goal**: Establish the foundational event emission capability in `Servus::Base`.
|
|
245
|
-
|
|
246
|
-
- [ ] **Create Event Bus/Registry** (`lib/servus/events/bus.rb`)
|
|
247
|
-
- Create `Servus::Events::Bus` singleton class
|
|
248
|
-
- Implement event registration: `Bus.register_handler(event_name, handler_class)`
|
|
249
|
-
- Implement event emission: `Bus.emit(event_name, payload)`
|
|
250
|
-
- Store handlers in a thread-safe Hash: `@handlers = Concurrent::Hash.new { |h, k| h[k] = [] }`
|
|
251
|
-
- Add method to dispatch event to all registered handlers
|
|
252
|
-
- **Files**: `lib/servus/events/bus.rb`, `spec/servus/events/bus_spec.rb`
|
|
253
|
-
|
|
254
|
-
- [ ] **Add `emits` DSL to Servus::Base** (`lib/servus/base.rb`)
|
|
255
|
-
- Create class-level `emits(event_name, on:, with: nil)` method
|
|
256
|
-
- Store event declarations in class instance variable: `@event_emissions ||= []`
|
|
257
|
-
- Validate `on:` parameter is one of: `:success`, `:failure`, `:error`
|
|
258
|
-
- Store event config as: `{ event_name:, trigger:, payload_builder: }`
|
|
259
|
-
- Add accessor method: `def self.event_emissions; @event_emissions || []; end`
|
|
260
|
-
- **Files**: `lib/servus/base.rb:30-50`
|
|
261
|
-
|
|
262
|
-
- [ ] **Implement Automatic Event Emission** (`lib/servus/base.rb`)
|
|
263
|
-
- In `#call` method, after executing user's `#call` (around line 120):
|
|
264
|
-
- After success: trigger events where `on: :success`
|
|
265
|
-
- After failure: trigger events where `on: :failure`
|
|
266
|
-
- In rescue blocks: trigger events where `on: :error`
|
|
267
|
-
- Create private method `#emit_events_for(trigger_type, result)`
|
|
268
|
-
- **Files**: `lib/servus/base.rb:120-140`
|
|
269
|
-
|
|
270
|
-
- [ ] **Implement Payload Builder Logic** (`lib/servus/base.rb`)
|
|
271
|
-
- Create private method `#build_event_payload(event_config, result)`
|
|
272
|
-
- If `with:` option present: call instance method with `result` as argument
|
|
273
|
-
- If `with:` absent and success: return `result.data`
|
|
274
|
-
- If `with:` absent and failure/error: return `{ error: result.error }`
|
|
275
|
-
- Handle case where custom payload builder returns nil (log warning, use default)
|
|
276
|
-
- **Files**: `lib/servus/base.rb:250-270`
|
|
277
|
-
|
|
278
|
-
- [ ] **Write Comprehensive Specs**
|
|
279
|
-
- Test `emits` DSL declaration and storage
|
|
280
|
-
- Test automatic emission on success/failure/error
|
|
281
|
-
- Test custom payload builders via `with:` option
|
|
282
|
-
- Test default payloads when `with:` omitted
|
|
283
|
-
- Test multiple event declarations on same service
|
|
284
|
-
- Test events inherited by subclasses
|
|
285
|
-
- **Files**: `spec/servus/base_spec.rb:450-600` (new section)
|
|
286
|
-
|
|
287
|
-
### Phase 2: EventHandler Base Class
|
|
288
|
-
|
|
289
|
-
**Goal**: Create the `Servus::EventHandler` class with the `handles` and `invoke` DSL.
|
|
290
|
-
|
|
291
|
-
- [ ] **Create EventHandler Base Class** (`lib/servus/event_handler.rb`)
|
|
292
|
-
- Create `Servus::EventHandler` class
|
|
293
|
-
- Add class instance variable: `@event_name` for event subscription
|
|
294
|
-
- Add class instance variable: `@invocations = []` for service mappings
|
|
295
|
-
- Add reader: `def self.event_name; @event_name; end`
|
|
296
|
-
- Add reader: `def self.invocations; @invocations || []; end`
|
|
297
|
-
- **Files**: `lib/servus/event_handler.rb`, `spec/servus/event_handler_spec.rb`
|
|
298
|
-
|
|
299
|
-
- [ ] **Implement `handles` DSL Method** (`lib/servus/event_handler.rb`)
|
|
300
|
-
- Create class method: `def self.handles(event_name)`
|
|
301
|
-
- Store event name: `@event_name = event_name`
|
|
302
|
-
- Automatically register with Bus: `Servus::Events::Bus.register_handler(event_name, self)`
|
|
303
|
-
- Raise error if `handles` called multiple times in same class
|
|
304
|
-
- **Files**: `lib/servus/event_handler.rb:20-30`
|
|
305
|
-
|
|
306
|
-
- [ ] **Implement `invoke` DSL Method** (`lib/servus/event_handler.rb`)
|
|
307
|
-
- Create class method: `def self.invoke(service_class, options = {}, &block)`
|
|
308
|
-
- Validate `service_class` is a subclass of `Servus::Base`
|
|
309
|
-
- Validate `options` keys are valid: `:async`, `:queue`, `:if`, `:unless`
|
|
310
|
-
- Require block to be present (raise error if missing)
|
|
311
|
-
- Store invocation config: `@invocations << { service_class:, options:, mapper: block }`
|
|
312
|
-
- **Files**: `lib/servus/event_handler.rb:40-60`
|
|
313
|
-
|
|
314
|
-
- [ ] **Implement Event Handling Dispatcher** (`lib/servus/event_handler.rb`)
|
|
315
|
-
- Create class method: `def self.handle(payload)`
|
|
316
|
-
- Iterate over `@invocations`
|
|
317
|
-
- For each invocation:
|
|
318
|
-
- Check `:if` condition (skip if returns false)
|
|
319
|
-
- Check `:unless` condition (skip if returns true)
|
|
320
|
-
- Call mapper block with payload to get service kwargs
|
|
321
|
-
- Invoke service: `service_class.call(**kwargs)` or `.call_async(**kwargs.merge(queue: options[:queue]))`
|
|
322
|
-
- Return array of results from all invocations
|
|
323
|
-
- **Files**: `lib/servus/event_handler.rb:70-95`
|
|
324
|
-
|
|
325
|
-
- [ ] **Handle Async Options** (`lib/servus/event_handler.rb`)
|
|
326
|
-
- When `async: true`, use `service_class.call_async(**kwargs)`
|
|
327
|
-
- Pass `:queue` option to `call_async` if present
|
|
328
|
-
- Ensure async calls work with existing `Servus::Extensions::Async` module
|
|
329
|
-
- **Files**: `lib/servus/event_handler.rb:85-90`
|
|
330
|
-
|
|
331
|
-
- [ ] **Implement Conditional Logic** (`lib/servus/event_handler.rb`)
|
|
332
|
-
- Create private method: `def self.should_invoke?(payload, options)`
|
|
333
|
-
- Check `:if` proc: `return false if options[:if] && !options[:if].call(payload)`
|
|
334
|
-
- Check `:unless` proc: `return false if options[:unless] && options[:unless].call(payload)`
|
|
335
|
-
- Return true if all conditions pass
|
|
336
|
-
- **Files**: `lib/servus/event_handler.rb:100-110`
|
|
337
|
-
|
|
338
|
-
- [ ] **Write Comprehensive Specs**
|
|
339
|
-
- Test `handles` DSL declaration and registration
|
|
340
|
-
- Test `invoke` DSL with various options
|
|
341
|
-
- Test `.handle(payload)` dispatches to services correctly
|
|
342
|
-
- Test conditional execution (`:if`, `:unless`)
|
|
343
|
-
- Test sync vs async invocation
|
|
344
|
-
- Test queue routing for async jobs
|
|
345
|
-
- Test multiple invocations in single handler
|
|
346
|
-
- Test payload mapping via block
|
|
347
|
-
- **Files**: `spec/servus/event_handler_spec.rb`
|
|
348
|
-
|
|
349
|
-
### Phase 3: Automatic Handler Discovery
|
|
350
|
-
|
|
351
|
-
**Goal**: Auto-discover and register all EventHandler classes in `app/events/` at Rails boot.
|
|
352
|
-
|
|
353
|
-
- [ ] **Create Railtie for Initialization** (`lib/servus/railtie.rb`)
|
|
354
|
-
- Update existing railtie or create if doesn't exist
|
|
355
|
-
- Add initializer: `initializer 'servus.discover_event_handlers', after: :load_config_initializers`
|
|
356
|
-
- In initializer, call `Servus::Events::Loader.discover_handlers`
|
|
357
|
-
- **Files**: `lib/servus/railtie.rb:20-30`
|
|
358
|
-
|
|
359
|
-
- [ ] **Create Handler Discovery Loader** (`lib/servus/events/loader.rb`)
|
|
360
|
-
- Create `Servus::Events::Loader` module
|
|
361
|
-
- Method: `def self.discover_handlers`
|
|
362
|
-
- Scan `app/events/**/*_handler.rb` using `Dir.glob`
|
|
363
|
-
- Require each file: `require_dependency(file_path)`
|
|
364
|
-
- Return count of discovered handlers for logging
|
|
365
|
-
- **Files**: `lib/servus/events/loader.rb`, `spec/servus/events/loader_spec.rb`
|
|
366
|
-
|
|
367
|
-
- [ ] **Add Handler Conflict Detection** (`lib/servus/events/bus.rb`)
|
|
368
|
-
- In `Bus.register_handler`, detect if event already has handler
|
|
369
|
-
- Raise `Servus::Events::DuplicateHandlerError` if duplicate detected
|
|
370
|
-
- Include both handler class names in error message
|
|
371
|
-
- Add config option to allow multiple handlers (default: false)
|
|
372
|
-
- **Files**: `lib/servus/events/bus.rb:25-35`
|
|
373
|
-
|
|
374
|
-
- [ ] **Create Custom Errors** (`lib/servus/events/errors.rb`)
|
|
375
|
-
- Create `Servus::Events::DuplicateHandlerError < StandardError`
|
|
376
|
-
- Create `Servus::Events::UnregisteredEventError < StandardError`
|
|
377
|
-
- **Files**: `lib/servus/events/errors.rb`
|
|
378
|
-
|
|
379
|
-
- [ ] **Add Development Mode Reloading** (`lib/servus/railtie.rb`)
|
|
380
|
-
- Clear handler registry on code reload: `to_prepare` hook
|
|
381
|
-
- Call `Servus::Events::Bus.clear` before re-discovering
|
|
382
|
-
- Ensure handlers re-register properly in development
|
|
383
|
-
- **Files**: `lib/servus/railtie.rb:35-40`
|
|
384
|
-
|
|
385
|
-
- [ ] **Write Comprehensive Specs**
|
|
386
|
-
- Test handler discovery in dummy Rails app
|
|
387
|
-
- Test duplicate handler detection raises error
|
|
388
|
-
- Test handler reloading in development mode
|
|
389
|
-
- Test nested handler files are discovered
|
|
390
|
-
- Test handlers are properly registered with Bus
|
|
391
|
-
- **Files**: `spec/servus/events/loader_spec.rb`, `spec/integration/handler_discovery_spec.rb`
|
|
392
|
-
|
|
393
|
-
### Phase 4: Test Helpers
|
|
394
|
-
|
|
395
|
-
**Goal**: Provide intuitive test helpers for asserting event emissions and testing handlers.
|
|
396
|
-
|
|
397
|
-
- [ ] **Create Test Helpers Module** (`lib/servus/events/test_helpers.rb`)
|
|
398
|
-
- Create `Servus::Events::TestHelpers` module
|
|
399
|
-
- Add RSpec-specific helpers
|
|
400
|
-
- Include event capture/inspection utilities
|
|
401
|
-
- **Files**: `lib/servus/events/test_helpers.rb`, `spec/servus/events/test_helpers_spec.rb`
|
|
402
|
-
|
|
403
|
-
- [ ] **Implement `expect_event` Matcher** (`lib/servus/events/test_helpers.rb`)
|
|
404
|
-
- Create chainable matcher: `expect_event(event_name)`
|
|
405
|
-
- Implement `.with_payload(expected_payload)` chain
|
|
406
|
-
- Implement `.when { block }` chain that executes code
|
|
407
|
-
- Capture events emitted during block execution
|
|
408
|
-
- Assert event was emitted with matching payload
|
|
409
|
-
- Use RSpec's `hash_including` for partial payload matching
|
|
410
|
-
- **Files**: `lib/servus/events/test_helpers.rb:10-60`
|
|
411
|
-
|
|
412
|
-
- [ ] **Create Event Capture Mechanism** (`lib/servus/events/test_helpers.rb`)
|
|
413
|
-
- Create thread-local event store: `@captured_events = []`
|
|
414
|
-
- Hook into `Bus.emit` to capture events during tests
|
|
415
|
-
- Method: `def capture_events(&block)` that returns array of emitted events
|
|
416
|
-
- Auto-clear captured events between test runs
|
|
417
|
-
- **Files**: `lib/servus/events/test_helpers.rb:70-90`
|
|
418
|
-
|
|
419
|
-
- [ ] **Add Handler Testing Utilities** (`lib/servus/events/test_helpers.rb`)
|
|
420
|
-
- Helper method: `trigger_event(event_name, payload)` for directly testing handlers
|
|
421
|
-
- Method to assert handler invoked specific service: `expect_handler_to_invoke(service_class)`
|
|
422
|
-
- Method to build sample payloads: `sample_payload_for(event_name)`
|
|
423
|
-
- **Files**: `lib/servus/events/test_helpers.rb:100-130`
|
|
424
|
-
|
|
425
|
-
- [ ] **Create RSpec Configuration** (`lib/servus/events/test_helpers.rb`)
|
|
426
|
-
- Add RSpec config to auto-include TestHelpers in event specs
|
|
427
|
-
- Add config to auto-clear event registry between tests
|
|
428
|
-
- Add matcher aliases for readability
|
|
429
|
-
- **Files**: `lib/servus/events/test_helpers.rb:140-160`
|
|
430
|
-
|
|
431
|
-
- [ ] **Write Comprehensive Specs and Examples**
|
|
432
|
-
- Test `expect_event` matcher with various payload matchers
|
|
433
|
-
- Test `.when` block execution and event capture
|
|
434
|
-
- Test negative cases (event not emitted, wrong payload)
|
|
435
|
-
- Test handler testing utilities
|
|
436
|
-
- Create example specs showing usage patterns
|
|
437
|
-
- **Files**: `spec/servus/events/test_helpers_spec.rb`, `spec/examples/event_testing_spec.rb`
|
|
438
|
-
|
|
439
|
-
### Phase 5: Generator
|
|
440
|
-
|
|
441
|
-
**Goal**: Provide Rails generator for quickly scaffolding new EventHandler classes and specs.
|
|
442
|
-
|
|
443
|
-
- [ ] **Create Generator Class** (`lib/generators/servus/event_handler/event_handler_generator.rb`)
|
|
444
|
-
- Inherit from `Rails::Generators::NamedBase`
|
|
445
|
-
- Set source root: `source_root File.expand_path('templates', __dir__)`
|
|
446
|
-
- Define generator description and usage
|
|
447
|
-
- **Files**: `lib/generators/servus/event_handler/event_handler_generator.rb`
|
|
448
|
-
|
|
449
|
-
- [ ] **Implement File Generation Logic** (`lib/generators/servus/event_handler/event_handler_generator.rb`)
|
|
450
|
-
- Method: `def create_handler_file`
|
|
451
|
-
- Generate file at: `app/events/#{file_name}_handler.rb`
|
|
452
|
-
- Use ERB template with proper class name and event name
|
|
453
|
-
- Method: `def create_spec_file`
|
|
454
|
-
- Generate file at: `spec/events/#{file_name}_handler_spec.rb`
|
|
455
|
-
- **Files**: `lib/generators/servus/event_handler/event_handler_generator.rb:15-30`
|
|
456
|
-
|
|
457
|
-
- [ ] **Create Handler Template** (`lib/generators/servus/event_handler/templates/handler.rb.tt`)
|
|
458
|
-
- ERB template with `<%= class_name %>Handler < Servus::EventHandler`
|
|
459
|
-
- Include `handles :<%= event_name %>`
|
|
460
|
-
- Include TODO comment with example invoke usage
|
|
461
|
-
- **Files**: `lib/generators/servus/event_handler/templates/handler.rb.tt`
|
|
462
|
-
|
|
463
|
-
- [ ] **Create Spec Template** (`lib/generators/servus/event_handler/templates/handler_spec.rb.tt`)
|
|
464
|
-
- ERB template for RSpec test file
|
|
465
|
-
- Include sample payload `let` block
|
|
466
|
-
- Include example test for service invocation
|
|
467
|
-
- Include pending test for additional invocations
|
|
468
|
-
- **Files**: `lib/generators/servus/event_handler/templates/handler_spec.rb.tt`
|
|
469
|
-
|
|
470
|
-
- [ ] **Add Naming Conventions** (`lib/generators/servus/event_handler/event_handler_generator.rb`)
|
|
471
|
-
- Convert snake_case event names to proper class names
|
|
472
|
-
- Example: `referral_created` → `ReferralCreatedHandler`
|
|
473
|
-
- Handle multi-word event names correctly
|
|
474
|
-
- Add validation for event name format (only alphanumeric and underscores)
|
|
475
|
-
- **Files**: `lib/generators/servus/event_handler/event_handler_generator.rb:40-55`
|
|
476
|
-
|
|
477
|
-
- [ ] **Write Generator Specs** (`spec/generators/servus/event_handler_generator_spec.rb`)
|
|
478
|
-
- Test generator creates handler file in correct location
|
|
479
|
-
- Test generator creates spec file in correct location
|
|
480
|
-
- Test generated files have correct content/structure
|
|
481
|
-
- Test naming conventions work correctly
|
|
482
|
-
- Test generator with various event name formats
|
|
483
|
-
- Use `Rails::Generators::TestCase` for generator testing
|
|
484
|
-
- **Files**: `spec/generators/servus/event_handler_generator_spec.rb`
|
|
485
|
-
|
|
486
|
-
### Phase 6: Documentation & Polish
|
|
487
|
-
|
|
488
|
-
**Goal**: Document the event bus feature and prepare for release.
|
|
489
|
-
|
|
490
|
-
- [ ] **Move Spec to Feature Docs** (`docs/features/5_event_bus.md`)
|
|
491
|
-
- Copy content from `docs/current_focus.md` to `docs/features/5_event_bus.md`
|
|
492
|
-
- Remove "Implementation Plan" section (internal only)
|
|
493
|
-
- Polish language to be present tense ("The event bus provides...")
|
|
494
|
-
- Add introduction paragraph linking to related features (async execution)
|
|
495
|
-
- **Files**: `docs/features/5_event_bus.md`
|
|
496
|
-
|
|
497
|
-
- [ ] **Update Current Focus** (`docs/current_focus.md`)
|
|
498
|
-
- Clear or archive current content
|
|
499
|
-
- Add new focus area (could be generator updates from IDEAS.md)
|
|
500
|
-
- Or mark as "Event bus implementation complete, awaiting next focus"
|
|
501
|
-
- **Files**: `docs/current_focus.md`
|
|
502
|
-
|
|
503
|
-
- [ ] **Update README** (`READme.md`)
|
|
504
|
-
- Add "Event Bus" section under features list
|
|
505
|
-
- Add quick example showing emitter → handler → consumer flow
|
|
506
|
-
- Add link to full documentation: `docs/features/5_event_bus.md`
|
|
507
|
-
- Keep example concise (10-15 lines)
|
|
508
|
-
- **Files**: `READme.md:30-60`
|
|
509
|
-
|
|
510
|
-
- [ ] **Add YARD Documentation** (various files)
|
|
511
|
-
- Document `Servus::Base.emits` with @param and @example tags
|
|
512
|
-
- Document `Servus::EventHandler` class and DSL methods
|
|
513
|
-
- Document `Servus::Events::Bus` public methods
|
|
514
|
-
- Document test helpers module and matchers
|
|
515
|
-
- Generate updated YARD docs: `bundle exec yard doc`
|
|
516
|
-
- **Files**: `lib/servus/base.rb`, `lib/servus/event_handler.rb`, etc.
|
|
517
|
-
|
|
518
|
-
- [ ] **Update CHANGELOG** (`CHANGELOG.md`)
|
|
519
|
-
- Add new section: `## [0.2.0] - Unreleased`
|
|
520
|
-
- List new features:
|
|
521
|
-
- Event bus with `emits` DSL for services
|
|
522
|
-
- `Servus::EventHandler` for mapping events to service invocations
|
|
523
|
-
- Automatic handler discovery in `app/events/`
|
|
524
|
-
- Test helpers with `expect_event` matcher
|
|
525
|
-
- Generator: `rails g servus:event_handler`
|
|
526
|
-
- Note any breaking changes (hopefully none)
|
|
527
|
-
- **Files**: `CHANGELOG.md:1-20`
|
|
528
|
-
|
|
529
|
-
- [ ] **Create Migration Guide** (`docs/guides/3_adding_events.md`)
|
|
530
|
-
- Guide for adding events to existing services
|
|
531
|
-
- Walkthrough: identify business events → add `emits` → create handler → test
|
|
532
|
-
- Best practices: when to use events vs direct service calls
|
|
533
|
-
- Common patterns: notification events, audit events, workflow triggers
|
|
534
|
-
- Troubleshooting: handler not found, payload mapping issues
|
|
535
|
-
- **Files**: `docs/guides/3_adding_events.md`
|
|
536
|
-
|
|
537
|
-
- [ ] **Update Version Number** (`lib/servus/version.rb`)
|
|
538
|
-
- Bump version to `0.2.0` (minor version for new feature)
|
|
539
|
-
- Update version in `servus.gemspec` if needed
|
|
540
|
-
- **Files**: `lib/servus/version.rb:3`
|
|
541
|
-
|
|
542
|
-
---
|
|
543
|
-
|
|
544
|
-
### Implementation Notes
|
|
545
|
-
|
|
546
|
-
**Testing Strategy**:
|
|
547
|
-
- Write specs FIRST for each component (TDD approach)
|
|
548
|
-
- Use dummy Rails app in `spec/dummy` for integration tests
|
|
549
|
-
- Test thread safety for Bus registry (use concurrent gem)
|
|
550
|
-
|
|
551
|
-
**Performance Considerations**:
|
|
552
|
-
- Event emission should add < 1ms to service execution
|
|
553
|
-
- Handler lookup should be O(1) using hash-based registry
|
|
554
|
-
- Consider async-by-default for most event handlers to avoid blocking
|
|
555
|
-
|
|
556
|
-
**Backward Compatibility**:
|
|
557
|
-
- All event features are opt-in (no breaking changes)
|
|
558
|
-
- Services without `emits` declarations work exactly as before
|
|
559
|
-
- No changes to existing public APIs
|
|
560
|
-
|
|
561
|
-
**Dependencies**:
|
|
562
|
-
- May need `concurrent-ruby` gem for thread-safe Bus registry
|
|
563
|
-
- Async features already depend on ActiveJob (no new dependencies)
|
|
564
|
-
|
|
565
|
-
**Phasing Approach**:
|
|
566
|
-
- Phases 1-2 can be merged as single PR (core functionality)
|
|
567
|
-
- Phase 3 requires Rails integration testing (separate PR recommended)
|
|
568
|
-
- Phases 4-6 are polish/DX improvements (can be bundled or separate)
|
|
569
|
-
|