servus 0.1.4 → 0.1.5
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/commands/check-docs.md +1 -0
- data/.claude/commands/consistency-check.md +1 -0
- data/.claude/commands/fine-tooth-comb.md +1 -0
- data/.claude/commands/red-green-refactor.md +5 -0
- data/.claude/settings.json +15 -0
- data/.rubocop.yml +18 -2
- data/CHANGELOG.md +40 -0
- data/CLAUDE.md +10 -0
- data/IDEAS.md +1 -1
- data/READme.md +153 -5
- data/builds/servus-0.1.4.gem +0 -0
- data/builds/servus-0.1.5.gem +0 -0
- data/docs/core/2_architecture.md +32 -4
- data/docs/current_focus.md +569 -0
- data/docs/features/5_event_bus.md +244 -0
- data/docs/integration/1_configuration.md +60 -7
- data/docs/integration/2_testing.md +123 -0
- data/lib/generators/servus/event_handler/event_handler_generator.rb +59 -0
- data/lib/generators/servus/event_handler/templates/handler.rb.erb +86 -0
- data/lib/generators/servus/event_handler/templates/handler_spec.rb.erb +48 -0
- data/lib/generators/servus/service/service_generator.rb +4 -0
- data/lib/generators/servus/service/templates/arguments.json.erb +19 -10
- data/lib/generators/servus/service/templates/result.json.erb +8 -2
- data/lib/generators/servus/service/templates/service.rb.erb +101 -4
- data/lib/generators/servus/service/templates/service_spec.rb.erb +67 -6
- data/lib/servus/base.rb +21 -5
- data/lib/servus/config.rb +34 -14
- data/lib/servus/event_handler.rb +275 -0
- data/lib/servus/events/bus.rb +137 -0
- data/lib/servus/events/emitter.rb +162 -0
- data/lib/servus/events/errors.rb +10 -0
- data/lib/servus/railtie.rb +16 -0
- data/lib/servus/support/validator.rb +27 -0
- data/lib/servus/testing/matchers.rb +88 -0
- data/lib/servus/testing.rb +2 -0
- data/lib/servus/version.rb +1 -1
- data/lib/servus.rb +6 -0
- metadata +19 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f1094a06fd6195a99ef33f849a6ea30e6ec1a539eeb7ef73e7c27d1d4ac411bf
|
|
4
|
+
data.tar.gz: f6891749189216f6391396d79a29fd695fc3da5ff7647691318a90929781a90a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 02e57566af4b419281991b2b0a361010dd7eff9f7d061c226e32153292d60c060e58781099eee1113d8eab678859164a09be26b1ad790dea47ba0e7f2bfe4275
|
|
7
|
+
data.tar.gz: 2cfd4e9ae6f71bb051b61f2ea7b4315fef58289ffaf610f21669d6f6c98cd635479502a22af3819aa0a41eb3aed6b8de9972e74e8311a7e619fa09cf82c66ff9
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Scan the documentation in docs/ then check the current git changeset. If the changeset has modifications that might affect the documentation, then review the related documentation and make any necessary updates.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Are the latest changes consistent with the way that similar functionality has been implemented in the rest of the codebase? #$ARGUMENTS
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
We're going to go over #$ARGUMENTS with a fine-tooth comb. I want detailed explanations and no edits unless I ask for them. Start by identifying the first thing that looks wrong and explain why.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
During this session we will be practicing TDD in the red-green-refactor cycle. The first step in the cycle is to write a test for the smallest possible unit of functionality. Once the test is written, run the test and it should fail. This is the red phase. The next step is to write the code to make the test pass. This is the green phase. Once the test passes, the code is refactored to be more readable and maintainable. This is the refactor phase.
|
|
2
|
+
|
|
3
|
+
If we get into a situation where more than one test fails, we will focus on the test that is most important to fix first. During that fix phase, we will only run the test that is failing until it passes.
|
|
4
|
+
|
|
5
|
+
While we're in this mode, you will not use your TODO tool to track tasks. You will rely on me (the human user) to tell you what the next step is.
|
data/.rubocop.yml
CHANGED
|
@@ -1,11 +1,27 @@
|
|
|
1
1
|
AllCops:
|
|
2
|
+
NewCops: enable
|
|
3
|
+
TargetRubyVersion: 3.3
|
|
2
4
|
Include:
|
|
3
5
|
- 'lib/**/*.rb'
|
|
4
6
|
- 'spec/**/*.rb'
|
|
7
|
+
Exclude:
|
|
8
|
+
- 'spec/dummy/**/*'
|
|
9
|
+
- 'vendor/bundle/**/*'
|
|
10
|
+
|
|
11
|
+
Lint/ConstantDefinitionInBlock:
|
|
12
|
+
Enabled: false
|
|
13
|
+
|
|
14
|
+
Lint/ConstantReassignment:
|
|
15
|
+
Exclude:
|
|
16
|
+
- 'spec/**/*'
|
|
17
|
+
|
|
18
|
+
Lint/MissingSuper:
|
|
19
|
+
Exclude:
|
|
20
|
+
- 'spec/**/*'
|
|
5
21
|
|
|
6
22
|
Metrics/BlockLength:
|
|
7
23
|
Exclude:
|
|
8
24
|
- 'spec/**/*'
|
|
9
25
|
|
|
10
|
-
|
|
11
|
-
Enabled: false
|
|
26
|
+
Naming/PredicateMethod:
|
|
27
|
+
Enabled: false
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.1.5] - 2025-12-03
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Event Bus Architecture**: Introduced event-driven architecture for decoupling service logic from side effects
|
|
8
|
+
- `Servus::EventHandler` base class for creating event handlers that subscribe to events and invoke services
|
|
9
|
+
- `emits` DSL on `Servus::Base` for declaring events that fire on `:success`, `:failure`, or `:error!`
|
|
10
|
+
- `Servus::Events::Bus` for routing events to handlers via ActiveSupport::Notifications
|
|
11
|
+
- Rails generator: `rails g servus:event_handler event_name` creates handler and spec files
|
|
12
|
+
- Event handlers auto-load from `app/events/` directory in Rails applications
|
|
13
|
+
|
|
14
|
+
- **Event Payload Validation**: JSON Schema validation for event payloads
|
|
15
|
+
- `schema payload: {...}` DSL on EventHandler for declaring payload schemas
|
|
16
|
+
- Validation occurs when events are emitted via `EventHandler.emit(payload)`
|
|
17
|
+
|
|
18
|
+
- **Event Testing Matchers**: RSpec matchers for testing event emission
|
|
19
|
+
- `emit_event(:event_name)` matcher to assert events are emitted
|
|
20
|
+
- `emit_event(:event_name).with(payload)` for payload assertions
|
|
21
|
+
- `call_service(ServiceClass).with(args)` matcher for handler testing
|
|
22
|
+
- `call_service(ServiceClass).async` for async invocation testing
|
|
23
|
+
|
|
24
|
+
- **Configuration Options**: New and updated configuration settings
|
|
25
|
+
- `config.schemas_dir` - Directory for JSON schema files (default: `app/schemas`)
|
|
26
|
+
- `config.services_dir` - Directory for service files (default: `app/services`)
|
|
27
|
+
- `config.events_dir` - Directory for event handlers (default: `app/events`)
|
|
28
|
+
- `config.strict_event_validation` - Validate handlers subscribe to emitted events (default: `true`)
|
|
29
|
+
- `Servus::EventHandler.validate_all_handlers!` for CI validation of handler-event mappings
|
|
30
|
+
|
|
31
|
+
- **Generator Improvements**: Enhanced service and event handler generators
|
|
32
|
+
- Service templates now include comprehensive YARD documentation
|
|
33
|
+
- Service spec templates include example test patterns
|
|
34
|
+
- JSON schema templates include proper structure with `$schema` reference
|
|
35
|
+
- Event handler templates include full documentation and examples
|
|
36
|
+
- `--no-docs` flag to skip documentation comments in generated files
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- Updated execution flow to include event emission after result validation
|
|
41
|
+
- Enhanced Railtie to auto-load event handlers and clear the event bus on reload in development
|
|
42
|
+
|
|
3
43
|
## [0.1.4] - 2025-11-21
|
|
4
44
|
- Added: Test helpers (`servus_arguments_example` and `servus_result_example`) to extract example values from schemas for testing
|
|
5
45
|
- Added: YARD documentation configuration with README homepage and markdown file support
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Before starting a session, always review the latest docs in the following order:
|
|
2
|
+
|
|
3
|
+
1. `/docs/core/**/*.md`
|
|
4
|
+
2. `/docs/features/**/*.md`
|
|
5
|
+
3. `/docs/guides/**/*.md`
|
|
6
|
+
4. `/docs/integration/**/*.md`
|
|
7
|
+
|
|
8
|
+
Focus on writing code consistent with the rest of the project. Use existing files as references for conventions and style.
|
|
9
|
+
|
|
10
|
+
Ensure new code always encludes world class YARD documentation. If documentation looks out of date or incomplete, suggest a relevant edit.
|
data/IDEAS.md
CHANGED
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
|
|
3
3
|
2. Improve error handling with an error registry that can be referenced by codes as opposed to fully qualified class names.
|
|
4
4
|
|
|
5
|
-
3.
|
|
5
|
+
3. Update generators to not make schema files and instead add schemas to schema: method in generators.
|
data/READme.md
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
## Servus Gem
|
|
2
2
|
|
|
3
|
+
|
|
3
4
|
Servus is a gem for creating and managing service objects. It includes:
|
|
4
5
|
|
|
5
6
|
- A base class for service objects
|
|
@@ -7,8 +8,9 @@ Servus is a gem for creating and managing service objects. It includes:
|
|
|
7
8
|
- Support for schema validation
|
|
8
9
|
- Support for error handling
|
|
9
10
|
- Support for logging
|
|
11
|
+
- Event-driven architecture with EventHandlers
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
👉🏽 [View the docs](https://zarpay.github.io/servus/)
|
|
12
14
|
|
|
13
15
|
## Generators
|
|
14
16
|
|
|
@@ -119,10 +121,6 @@ end
|
|
|
119
121
|
|
|
120
122
|
```
|
|
121
123
|
|
|
122
|
-
Here’s a section you can add to your README for the new `.call_async` feature, matching the style of your existing `## Inheritance` section:
|
|
123
|
-
|
|
124
|
-
---
|
|
125
|
-
|
|
126
124
|
## **Asynchronous Execution**
|
|
127
125
|
|
|
128
126
|
You can asynchronously execute any service class that inherits from `Servus::Base` using `.call_async`. This uses `ActiveJob` under the hood and supports standard job options (`wait`, `queue`, `priority`, etc.). Only available in environments where `ActiveJob` is loaded (e.g., Rails apps)
|
|
@@ -601,3 +599,153 @@ Without explicit configuration:
|
|
|
601
599
|
- **Non-Rails applications**: Schema root defaults to `./app/schemas/services` relative to the gem installation
|
|
602
600
|
|
|
603
601
|
The configuration is accessed through the singleton `Servus.config` instance and can be modified using `Servus.configure`.
|
|
602
|
+
|
|
603
|
+
## **Event Bus**
|
|
604
|
+
|
|
605
|
+
Servus includes an event-driven architecture for decoupling service logic from side effects. Services emit events, and EventHandlers subscribe to them and invoke downstream services.
|
|
606
|
+
|
|
607
|
+
### Emitting Events from Services
|
|
608
|
+
|
|
609
|
+
Services can declare events that are emitted on success or failure:
|
|
610
|
+
|
|
611
|
+
```ruby
|
|
612
|
+
class CreateUser::Service < Servus::Base
|
|
613
|
+
emits :user_created, on: :success
|
|
614
|
+
emits :user_creation_failed, on: :failure
|
|
615
|
+
|
|
616
|
+
def initialize(email:, name:)
|
|
617
|
+
@email = email
|
|
618
|
+
@name = name
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
def call
|
|
622
|
+
user = User.create!(email: @email, name: @name)
|
|
623
|
+
success(user: user)
|
|
624
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
625
|
+
failure(e.message)
|
|
626
|
+
end
|
|
627
|
+
end
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
Custom payloads can be provided via blocks or method references:
|
|
631
|
+
|
|
632
|
+
```ruby
|
|
633
|
+
emits :user_created, on: :success do |result|
|
|
634
|
+
{ user_id: result.data[:user].id, email: result.data[:user].email }
|
|
635
|
+
end
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
### Event Handlers
|
|
639
|
+
|
|
640
|
+
EventHandlers subscribe to events and invoke services in response. They live in `app/events/`:
|
|
641
|
+
|
|
642
|
+
```ruby
|
|
643
|
+
# app/events/user_created_handler.rb
|
|
644
|
+
class UserCreatedHandler < Servus::EventHandler
|
|
645
|
+
handles :user_created
|
|
646
|
+
|
|
647
|
+
invoke SendWelcomeEmail::Service, async: true do |payload|
|
|
648
|
+
{ user_id: payload[:user_id], email: payload[:email] }
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
invoke TrackAnalytics::Service, async: true do |payload|
|
|
652
|
+
{ event: 'user_created', user_id: payload[:user_id] }
|
|
653
|
+
end
|
|
654
|
+
end
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
### Generate Event Handler
|
|
658
|
+
|
|
659
|
+
```bash
|
|
660
|
+
$ rails g servus:event_handler user_created
|
|
661
|
+
=> create app/events/user_created_handler.rb
|
|
662
|
+
create spec/events/user_created_handler_spec.rb
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
### Invocation Options
|
|
666
|
+
|
|
667
|
+
```ruby
|
|
668
|
+
# Synchronous (default)
|
|
669
|
+
invoke NotifyAdmin::Service do |payload|
|
|
670
|
+
{ message: "New user: #{payload[:email]}" }
|
|
671
|
+
end
|
|
672
|
+
|
|
673
|
+
# Async via ActiveJob
|
|
674
|
+
invoke SendEmail::Service, async: true do |payload|
|
|
675
|
+
{ user_id: payload[:user_id] }
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
# Async with specific queue
|
|
679
|
+
invoke SendEmail::Service, async: true, queue: :mailers do |payload|
|
|
680
|
+
{ user_id: payload[:user_id] }
|
|
681
|
+
end
|
|
682
|
+
|
|
683
|
+
# Conditional invocation
|
|
684
|
+
invoke GrantRewards::Service, if: ->(p) { p[:premium] } do |payload|
|
|
685
|
+
{ user_id: payload[:user_id] }
|
|
686
|
+
end
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
### Emitting Events Directly
|
|
690
|
+
|
|
691
|
+
EventHandlers provide an `emit` class method for emitting events from controllers, jobs, or other code:
|
|
692
|
+
|
|
693
|
+
```ruby
|
|
694
|
+
class UsersController < ApplicationController
|
|
695
|
+
def create
|
|
696
|
+
user = User.create!(user_params)
|
|
697
|
+
UserCreatedHandler.emit({ user_id: user.id, email: user.email })
|
|
698
|
+
redirect_to user
|
|
699
|
+
end
|
|
700
|
+
end
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
### Payload Schema Validation
|
|
704
|
+
|
|
705
|
+
Define JSON schemas to validate event payloads:
|
|
706
|
+
|
|
707
|
+
```ruby
|
|
708
|
+
class UserCreatedHandler < Servus::EventHandler
|
|
709
|
+
handles :user_created
|
|
710
|
+
|
|
711
|
+
schema payload: {
|
|
712
|
+
type: 'object',
|
|
713
|
+
required: ['user_id', 'email'],
|
|
714
|
+
properties: {
|
|
715
|
+
user_id: { type: 'integer' },
|
|
716
|
+
email: { type: 'string', format: 'email' }
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
invoke SendWelcomeEmail::Service, async: true do |payload|
|
|
721
|
+
{ user_id: payload[:user_id], email: payload[:email] }
|
|
722
|
+
end
|
|
723
|
+
end
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
### Testing Events
|
|
727
|
+
|
|
728
|
+
Servus provides RSpec matchers for testing events:
|
|
729
|
+
|
|
730
|
+
```ruby
|
|
731
|
+
# Test that a service emits an event
|
|
732
|
+
it 'emits user_created event' do
|
|
733
|
+
expect {
|
|
734
|
+
CreateUser::Service.call(email: 'test@example.com', name: 'Test')
|
|
735
|
+
}.to emit_event(:user_created)
|
|
736
|
+
end
|
|
737
|
+
|
|
738
|
+
# Test payload content
|
|
739
|
+
it 'emits event with expected payload' do
|
|
740
|
+
expect {
|
|
741
|
+
CreateUser::Service.call(email: 'test@example.com', name: 'Test')
|
|
742
|
+
}.to emit_event(:user_created).with(hash_including(email: 'test@example.com'))
|
|
743
|
+
end
|
|
744
|
+
|
|
745
|
+
# Test handler invokes service
|
|
746
|
+
it 'invokes SendWelcomeEmail' do
|
|
747
|
+
expect {
|
|
748
|
+
UserCreatedHandler.handle(payload)
|
|
749
|
+
}.to call_service(SendWelcomeEmail::Service).with(user_id: 123)
|
|
750
|
+
end
|
|
751
|
+
```
|
data/builds/servus-0.1.4.gem
CHANGED
|
Binary file
|
|
Binary file
|
data/docs/core/2_architecture.md
CHANGED
|
@@ -7,12 +7,12 @@ Servus wraps service execution with automatic validation, logging, and error han
|
|
|
7
7
|
## Execution Flow
|
|
8
8
|
|
|
9
9
|
```
|
|
10
|
-
Arguments → Validation → Service#call → Result Validation → Logging → Response
|
|
11
|
-
↓ ↓
|
|
12
|
-
ValidationError ValidationError
|
|
10
|
+
Arguments → Validation → Service#call → Result Validation → Event Emission → Logging → Response
|
|
11
|
+
↓ ↓ ↓ ↓
|
|
12
|
+
ValidationError ValidationError EventHandlers Benchmark
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
The framework intercepts the `.call` class method to inject cross-cutting concerns before and after your business logic runs. Your `call` instance method contains only business logic - validation, logging, and timing happen automatically.
|
|
15
|
+
The framework intercepts the `.call` class method to inject cross-cutting concerns before and after your business logic runs. Your `call` instance method contains only business logic - validation, logging, event emission, and timing happen automatically.
|
|
16
16
|
|
|
17
17
|
## Core Components
|
|
18
18
|
|
|
@@ -28,6 +28,12 @@ The framework intercepts the `.call` class method to inject cross-cutting concer
|
|
|
28
28
|
|
|
29
29
|
**Support::Errors** (`lib/servus/support/errors.rb`): HTTP-aligned error hierarchy (ServiceError, NotFoundError, ValidationError, etc.)
|
|
30
30
|
|
|
31
|
+
**Events::Emitter** (`lib/servus/events/emitter.rb`): DSL for declaring events that services emit on success/failure
|
|
32
|
+
|
|
33
|
+
**Events::Bus** (`lib/servus/events/bus.rb`): Central event router using ActiveSupport::Notifications for thread-safe dispatch
|
|
34
|
+
|
|
35
|
+
**EventHandler** (`lib/servus/event_handler.rb`): Base class for handlers that subscribe to events and invoke services
|
|
36
|
+
|
|
31
37
|
## Extension Points
|
|
32
38
|
|
|
33
39
|
### Schema Validation
|
|
@@ -84,6 +90,28 @@ ProcessPayment::Service.call_async(
|
|
|
84
90
|
)
|
|
85
91
|
```
|
|
86
92
|
|
|
93
|
+
## Event-Driven Architecture
|
|
94
|
+
|
|
95
|
+
Services can emit events that trigger downstream handlers. This decouples services from their side effects.
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
# Service emits events
|
|
99
|
+
class CreateUser::Service < Servus::Base
|
|
100
|
+
emits :user_created, on: :success
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Handler reacts to events
|
|
104
|
+
class UserCreatedHandler < Servus::EventHandler
|
|
105
|
+
handles :user_created
|
|
106
|
+
|
|
107
|
+
invoke SendWelcomeEmail::Service, async: true do |payload|
|
|
108
|
+
{ user_id: payload[:user_id] }
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
See {file:docs/features/5_event_bus.md Event Bus} for full documentation.
|
|
114
|
+
|
|
87
115
|
## Performance
|
|
88
116
|
|
|
89
117
|
- Schema loading: Cached per class after first use
|