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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/commands/check-docs.md +1 -0
  3. data/.claude/commands/consistency-check.md +1 -0
  4. data/.claude/commands/fine-tooth-comb.md +1 -0
  5. data/.claude/commands/red-green-refactor.md +5 -0
  6. data/.claude/settings.json +15 -0
  7. data/.rubocop.yml +18 -2
  8. data/CHANGELOG.md +40 -0
  9. data/CLAUDE.md +10 -0
  10. data/IDEAS.md +1 -1
  11. data/READme.md +153 -5
  12. data/builds/servus-0.1.4.gem +0 -0
  13. data/builds/servus-0.1.5.gem +0 -0
  14. data/docs/core/2_architecture.md +32 -4
  15. data/docs/current_focus.md +569 -0
  16. data/docs/features/5_event_bus.md +244 -0
  17. data/docs/integration/1_configuration.md +60 -7
  18. data/docs/integration/2_testing.md +123 -0
  19. data/lib/generators/servus/event_handler/event_handler_generator.rb +59 -0
  20. data/lib/generators/servus/event_handler/templates/handler.rb.erb +86 -0
  21. data/lib/generators/servus/event_handler/templates/handler_spec.rb.erb +48 -0
  22. data/lib/generators/servus/service/service_generator.rb +4 -0
  23. data/lib/generators/servus/service/templates/arguments.json.erb +19 -10
  24. data/lib/generators/servus/service/templates/result.json.erb +8 -2
  25. data/lib/generators/servus/service/templates/service.rb.erb +101 -4
  26. data/lib/generators/servus/service/templates/service_spec.rb.erb +67 -6
  27. data/lib/servus/base.rb +21 -5
  28. data/lib/servus/config.rb +34 -14
  29. data/lib/servus/event_handler.rb +275 -0
  30. data/lib/servus/events/bus.rb +137 -0
  31. data/lib/servus/events/emitter.rb +162 -0
  32. data/lib/servus/events/errors.rb +10 -0
  33. data/lib/servus/railtie.rb +16 -0
  34. data/lib/servus/support/validator.rb +27 -0
  35. data/lib/servus/testing/matchers.rb +88 -0
  36. data/lib/servus/testing.rb +2 -0
  37. data/lib/servus/version.rb +1 -1
  38. data/lib/servus.rb +6 -0
  39. metadata +19 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: af1900cb767ad43568bf7e2c8c14a98bebc479a9b3ec0cc5d76d5ed3c13c5289
4
- data.tar.gz: d75379a7b744f9ae6d4938c9f21601af02dbedfe7befbd3b748410aff8b6e7d9
3
+ metadata.gz: f1094a06fd6195a99ef33f849a6ea30e6ec1a539eeb7ef73e7c27d1d4ac411bf
4
+ data.tar.gz: f6891749189216f6391396d79a29fd695fc3da5ff7647691318a90929781a90a
5
5
  SHA512:
6
- metadata.gz: 011756b6fc695253bca1cedd31c4f3be14426305ed1d698f1e8963f62734a7ceb19dab0f77c75892b6aef326ce04d2ce6d07b22f97c9622131988ff4c24b9989
7
- data.tar.gz: 04d948bd35038d768f0786ec253c771c50abc4e8d95d85b39bb2a573b411ee5e4486daec6b460548562e1a87fc5b7b4b3a47ddf2a27105d3ecb3cedd6f09c779
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.
@@ -0,0 +1,15 @@
1
+ {
2
+ "hooks": {
3
+ "PostToolUse": [
4
+ {
5
+ "matcher": "Write|Edit",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "bundle exec rubocop -A"
10
+ }
11
+ ]
12
+ }
13
+ ]
14
+ }
15
+ }
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
- Lint/ConstantDefinitionInBlock:
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. Mock result objects from schema defaults
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
+ ```
Binary file
Binary file
@@ -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 Benchmark
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