servus 0.1.3 → 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 (139) 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/.yardopts +6 -0
  9. data/CHANGELOG.md +47 -0
  10. data/CLAUDE.md +10 -0
  11. data/IDEAS.md +5 -0
  12. data/READme.md +300 -47
  13. data/Rakefile +33 -0
  14. data/builds/servus-0.1.3.gem +0 -0
  15. data/builds/servus-0.1.4.gem +0 -0
  16. data/builds/servus-0.1.5.gem +0 -0
  17. data/docs/core/1_overview.md +77 -0
  18. data/docs/core/2_architecture.md +120 -0
  19. data/docs/core/3_service_objects.md +121 -0
  20. data/docs/current_focus.md +569 -0
  21. data/docs/features/1_schema_validation.md +119 -0
  22. data/docs/features/2_error_handling.md +121 -0
  23. data/docs/features/3_async_execution.md +81 -0
  24. data/docs/features/4_logging.md +64 -0
  25. data/docs/features/5_event_bus.md +244 -0
  26. data/docs/guides/1_common_patterns.md +90 -0
  27. data/docs/guides/2_migration_guide.md +175 -0
  28. data/docs/integration/1_configuration.md +104 -0
  29. data/docs/integration/2_testing.md +287 -0
  30. data/docs/integration/3_rails_integration.md +99 -0
  31. data/docs/yard/Servus/Base.html +1645 -0
  32. data/docs/yard/Servus/Config.html +582 -0
  33. data/docs/yard/Servus/Extensions/Async/Call.html +400 -0
  34. data/docs/yard/Servus/Extensions/Async/Errors/AsyncError.html +140 -0
  35. data/docs/yard/Servus/Extensions/Async/Errors/JobEnqueueError.html +154 -0
  36. data/docs/yard/Servus/Extensions/Async/Errors/ServiceNotFoundError.html +154 -0
  37. data/docs/yard/Servus/Extensions/Async/Errors.html +128 -0
  38. data/docs/yard/Servus/Extensions/Async/Ext.html +119 -0
  39. data/docs/yard/Servus/Extensions/Async/Job.html +310 -0
  40. data/docs/yard/Servus/Extensions/Async.html +141 -0
  41. data/docs/yard/Servus/Extensions.html +117 -0
  42. data/docs/yard/Servus/Generators/ServiceGenerator.html +261 -0
  43. data/docs/yard/Servus/Generators.html +115 -0
  44. data/docs/yard/Servus/Helpers/ControllerHelpers.html +457 -0
  45. data/docs/yard/Servus/Helpers.html +115 -0
  46. data/docs/yard/Servus/Railtie.html +134 -0
  47. data/docs/yard/Servus/Support/Errors/AuthenticationError.html +287 -0
  48. data/docs/yard/Servus/Support/Errors/BadRequestError.html +283 -0
  49. data/docs/yard/Servus/Support/Errors/ForbiddenError.html +284 -0
  50. data/docs/yard/Servus/Support/Errors/InternalServerError.html +283 -0
  51. data/docs/yard/Servus/Support/Errors/NotFoundError.html +284 -0
  52. data/docs/yard/Servus/Support/Errors/ServiceError.html +489 -0
  53. data/docs/yard/Servus/Support/Errors/ServiceUnavailableError.html +290 -0
  54. data/docs/yard/Servus/Support/Errors/UnauthorizedError.html +200 -0
  55. data/docs/yard/Servus/Support/Errors/UnprocessableEntityError.html +288 -0
  56. data/docs/yard/Servus/Support/Errors/ValidationError.html +200 -0
  57. data/docs/yard/Servus/Support/Errors.html +140 -0
  58. data/docs/yard/Servus/Support/Logger.html +856 -0
  59. data/docs/yard/Servus/Support/Rescuer/BlockContext.html +585 -0
  60. data/docs/yard/Servus/Support/Rescuer/CallOverride.html +257 -0
  61. data/docs/yard/Servus/Support/Rescuer/ClassMethods.html +343 -0
  62. data/docs/yard/Servus/Support/Rescuer.html +267 -0
  63. data/docs/yard/Servus/Support/Response.html +574 -0
  64. data/docs/yard/Servus/Support/Validator.html +1150 -0
  65. data/docs/yard/Servus/Support.html +119 -0
  66. data/docs/yard/Servus/Testing/ExampleBuilders.html +523 -0
  67. data/docs/yard/Servus/Testing/ExampleExtractor.html +578 -0
  68. data/docs/yard/Servus/Testing.html +142 -0
  69. data/docs/yard/Servus.html +343 -0
  70. data/docs/yard/_index.html +535 -0
  71. data/docs/yard/class_list.html +54 -0
  72. data/docs/yard/css/common.css +1 -0
  73. data/docs/yard/css/full_list.css +58 -0
  74. data/docs/yard/css/style.css +503 -0
  75. data/docs/yard/file.1_common_patterns.html +154 -0
  76. data/docs/yard/file.1_configuration.html +115 -0
  77. data/docs/yard/file.1_overview.html +142 -0
  78. data/docs/yard/file.1_schema_validation.html +188 -0
  79. data/docs/yard/file.2_architecture.html +157 -0
  80. data/docs/yard/file.2_error_handling.html +190 -0
  81. data/docs/yard/file.2_migration_guide.html +242 -0
  82. data/docs/yard/file.2_testing.html +227 -0
  83. data/docs/yard/file.3_async_execution.html +145 -0
  84. data/docs/yard/file.3_rails_integration.html +160 -0
  85. data/docs/yard/file.3_service_objects.html +191 -0
  86. data/docs/yard/file.4_logging.html +135 -0
  87. data/docs/yard/file.ErrorHandling.html +190 -0
  88. data/docs/yard/file.READme.html +674 -0
  89. data/docs/yard/file.architecture.html +157 -0
  90. data/docs/yard/file.async_execution.html +145 -0
  91. data/docs/yard/file.common_patterns.html +154 -0
  92. data/docs/yard/file.configuration.html +115 -0
  93. data/docs/yard/file.error_handling.html +190 -0
  94. data/docs/yard/file.logging.html +135 -0
  95. data/docs/yard/file.migration_guide.html +242 -0
  96. data/docs/yard/file.overview.html +142 -0
  97. data/docs/yard/file.rails_integration.html +160 -0
  98. data/docs/yard/file.schema_validation.html +188 -0
  99. data/docs/yard/file.service_objects.html +191 -0
  100. data/docs/yard/file.testing.html +227 -0
  101. data/docs/yard/file_list.html +119 -0
  102. data/docs/yard/frames.html +22 -0
  103. data/docs/yard/index.html +674 -0
  104. data/docs/yard/js/app.js +344 -0
  105. data/docs/yard/js/full_list.js +242 -0
  106. data/docs/yard/js/jquery.js +4 -0
  107. data/docs/yard/method_list.html +542 -0
  108. data/docs/yard/top-level-namespace.html +110 -0
  109. data/lib/generators/servus/event_handler/event_handler_generator.rb +59 -0
  110. data/lib/generators/servus/event_handler/templates/handler.rb.erb +86 -0
  111. data/lib/generators/servus/event_handler/templates/handler_spec.rb.erb +48 -0
  112. data/lib/generators/servus/service/service_generator.rb +68 -1
  113. data/lib/generators/servus/service/templates/arguments.json.erb +19 -10
  114. data/lib/generators/servus/service/templates/result.json.erb +8 -2
  115. data/lib/generators/servus/service/templates/service.rb.erb +102 -5
  116. data/lib/generators/servus/service/templates/service_spec.rb.erb +67 -6
  117. data/lib/servus/base.rb +275 -58
  118. data/lib/servus/config.rb +83 -17
  119. data/lib/servus/event_handler.rb +275 -0
  120. data/lib/servus/events/bus.rb +137 -0
  121. data/lib/servus/events/emitter.rb +162 -0
  122. data/lib/servus/events/errors.rb +10 -0
  123. data/lib/servus/extensions/async/call.rb +50 -18
  124. data/lib/servus/extensions/async/errors.rb +23 -3
  125. data/lib/servus/extensions/async/ext.rb +10 -2
  126. data/lib/servus/extensions/async/job.rb +30 -9
  127. data/lib/servus/helpers/controller_helpers.rb +73 -37
  128. data/lib/servus/railtie.rb +16 -0
  129. data/lib/servus/support/errors.rb +135 -45
  130. data/lib/servus/support/rescuer.rb +189 -36
  131. data/lib/servus/support/response.rb +49 -7
  132. data/lib/servus/support/validator.rb +147 -19
  133. data/lib/servus/testing/example_builders.rb +133 -0
  134. data/lib/servus/testing/example_extractor.rb +309 -0
  135. data/lib/servus/testing/matchers.rb +88 -0
  136. data/lib/servus/testing.rb +19 -0
  137. data/lib/servus/version.rb +1 -1
  138. data/lib/servus.rb +6 -0
  139. metadata +135 -19
@@ -0,0 +1,175 @@
1
+ # @title Guides / 2. Migration Guide
2
+
3
+ # Migration Guide
4
+
5
+ Strategies for adopting Servus in existing Rails applications.
6
+
7
+ ## Incremental Adoption
8
+
9
+ Servus coexists with existing code - no need to rewrite your entire application. Start with one complex use case, validate the pattern works for your team, then expand gradually.
10
+
11
+ ## Extracting from Fat Controllers
12
+
13
+ Identify controller actions with complex business logic and extract to services:
14
+
15
+ **Before**:
16
+ ```ruby
17
+ class OrdersController < ApplicationController
18
+ def create
19
+ # 50 lines of business logic
20
+ # Multiple model operations
21
+ # External API calls
22
+ # Email sending
23
+ end
24
+ end
25
+ ```
26
+
27
+ **After**:
28
+ ```ruby
29
+ class OrdersController < ApplicationController
30
+ def create
31
+ result = Orders::Create::Service.call(order_params)
32
+ if result.success?
33
+ render json: { order: result.data[:order] }, status: :created
34
+ else
35
+ render json: { error: result.error.api_error }, status: :unprocessable_entity
36
+ end
37
+ end
38
+ end
39
+
40
+ # Or use the helper
41
+ class OrdersController < ApplicationController
42
+ include Servus::Helpers::ControllerHelpers
43
+
44
+ def create
45
+ run_service(Orders::Create::Service, order_params) do |result|
46
+ render json: { order: result.data[:order] }, status: :created
47
+ end
48
+ end
49
+ end
50
+ ```
51
+
52
+ ## Extracting from Fat Models
53
+
54
+ Move orchestration logic from models to services. Keep data-related methods in models:
55
+
56
+ **Before**:
57
+ ```ruby
58
+ class Order < ApplicationRecord
59
+ def complete_purchase
60
+ charge_payment
61
+ update_inventory
62
+ send_confirmation_email
63
+ create_invoice
64
+ end
65
+ end
66
+ ```
67
+
68
+ **After**:
69
+ ```ruby
70
+ class Order < ApplicationRecord
71
+ # Model focuses on data
72
+ validates :total, presence: true
73
+ belongs_to :user
74
+ end
75
+
76
+ class Orders::CompletePurchase::Service < Servus::Base
77
+ # Service handles orchestration
78
+ def initialize(order_id:)
79
+ @order_id = order_id
80
+ end
81
+
82
+ def call
83
+ order = Order.find(@order_id)
84
+ Payments::Charge::Service.call(order_id: order.id)
85
+ Inventory::Update::Service.call(order_id: order.id)
86
+ Mailers::SendConfirmation::Service.call(order_id: order.id)
87
+ success(order: order)
88
+ end
89
+ end
90
+ ```
91
+
92
+ ## Replacing Callbacks
93
+
94
+ Extract callback logic to explicit service calls:
95
+
96
+ **Before**:
97
+ ```ruby
98
+ class User < ApplicationRecord
99
+ after_create :send_welcome_email
100
+ after_create :create_default_account
101
+ after_update :notify_changes, if: :email_changed?
102
+ end
103
+ ```
104
+
105
+ **After**:
106
+ ```ruby
107
+ class User < ApplicationRecord
108
+ # Minimal or no callbacks
109
+ end
110
+
111
+ class Users::Create::Service < Servus::Base
112
+ def call
113
+ user = User.create!(params)
114
+ send_welcome_email(user)
115
+ create_default_account(user)
116
+ success(user: user)
117
+ end
118
+ end
119
+ ```
120
+
121
+ ## Migrating Background Jobs
122
+
123
+ Extract job logic to services, call via `.call_async`:
124
+
125
+ **Before**:
126
+ ```ruby
127
+ class ProcessOrderJob < ApplicationJob
128
+ def perform(order_id)
129
+ # 50 lines of business logic
130
+ end
131
+ end
132
+
133
+ ProcessOrderJob.perform_later(order.id)
134
+ ```
135
+
136
+ **After**:
137
+ ```ruby
138
+ class Orders::Process::Service < Servus::Base
139
+ def initialize(order_id:)
140
+ @order_id = order_id
141
+ end
142
+
143
+ def call
144
+ # Business logic
145
+ end
146
+ end
147
+
148
+ Orders::Process::Service.call_async(order_id: order.id)
149
+ ```
150
+
151
+ Now the service can be called synchronously (from console, tests) or asynchronously (from controllers, jobs).
152
+
153
+ ## Testing During Migration
154
+
155
+ Keep existing tests working while adding service tests:
156
+
157
+ ```ruby
158
+ # Keep existing controller test
159
+ describe OrdersController do
160
+ it "creates order" do
161
+ post :create, params: params
162
+ expect(response).to be_successful
163
+ end
164
+ end
165
+
166
+ # Add service test
167
+ describe Orders::Create::Service do
168
+ it "creates order" do
169
+ result = described_class.call(params)
170
+ expect(result.success?).to be true
171
+ end
172
+ end
173
+ ```
174
+
175
+ Remove legacy tests after service tests prove comprehensive.
@@ -0,0 +1,104 @@
1
+ # @title Integration / 1. Configuration
2
+
3
+ # Configuration
4
+
5
+ Servus works without configuration. Optional settings exist for customizing directories and event validation.
6
+
7
+ ## Directory Configuration
8
+
9
+ Configure where Servus looks for schemas, services, and event handlers:
10
+
11
+ ```ruby
12
+ # config/initializers/servus.rb
13
+ Servus.configure do |config|
14
+ # Default: 'app/schemas'
15
+ config.schemas_dir = 'app/schemas'
16
+
17
+ # Default: 'app/services'
18
+ config.services_dir = 'app/services'
19
+
20
+ # Default: 'app/events'
21
+ config.events_dir = 'app/events'
22
+ end
23
+ ```
24
+
25
+ These affect legacy file-based schemas and handler auto-loading. Schemas defined via the `schema` DSL method do not use files.
26
+
27
+ ## Schema Cache
28
+
29
+ Schemas are cached after first load for performance. Clear the cache during development when schemas change:
30
+
31
+ ```ruby
32
+ Servus::Support::Validator.clear_cache!
33
+ ```
34
+
35
+ In production, schemas are deployed with code - no need to clear cache.
36
+
37
+ ## Log Level
38
+
39
+ Servus uses `Rails.logger` (or stdout in non-Rails apps). Control logging via Rails configuration:
40
+
41
+ ```ruby
42
+ # config/environments/production.rb
43
+ config.log_level = :info # Hides DEBUG argument logs
44
+ ```
45
+
46
+ ## ActiveJob Configuration
47
+
48
+ Async execution uses ActiveJob. Configure your adapter:
49
+
50
+ ```ruby
51
+ # config/application.rb
52
+ config.active_job.queue_adapter = :sidekiq
53
+ config.active_job.default_queue_name = :default
54
+ ```
55
+
56
+ Servus respects ActiveJob queue configuration - no Servus-specific setup needed.
57
+
58
+ ## Event Bus Configuration
59
+
60
+ ### Strict Event Validation
61
+
62
+ Enable strict validation to catch handlers subscribing to events that aren't emitted by any service:
63
+
64
+ ```ruby
65
+ # config/initializers/servus.rb
66
+ Servus.configure do |config|
67
+ # Default: true
68
+ config.strict_event_validation = true
69
+ end
70
+ ```
71
+
72
+ When enabled, you can validate handlers at boot or in CI:
73
+
74
+ ```ruby
75
+ # In a rake task or initializer
76
+ Servus::EventHandler.validate_all_handlers!
77
+ ```
78
+
79
+ This raises `Servus::Events::OrphanedHandlerError` if any handler subscribes to a non-existent event.
80
+
81
+ ### Handler Auto-Loading
82
+
83
+ In Rails, handlers in `app/events/` are automatically loaded. The Railtie:
84
+ - Clears the event bus on reload in development
85
+ - Eager-loads all `*_handler.rb` files from `config.events_dir`
86
+
87
+ ```
88
+ app/events/
89
+ ├── user_created_handler.rb
90
+ ├── payment_processed_handler.rb
91
+ └── order_completed_handler.rb
92
+ ```
93
+
94
+ ### Event Instrumentation
95
+
96
+ Events are instrumented via ActiveSupport::Notifications with the prefix `servus.events.`:
97
+
98
+ ```ruby
99
+ # Subscribe to all Servus events
100
+ ActiveSupport::Notifications.subscribe(/^servus\.events\./) do |name, *args|
101
+ event_name = name.sub('servus.events.', '')
102
+ Rails.logger.info "Event: #{event_name}"
103
+ end
104
+ ```
@@ -0,0 +1,287 @@
1
+ # @title Integration / 2. Testing
2
+
3
+ # Testing
4
+
5
+ Services are designed for easy testing with explicit inputs (arguments) and outputs (Response objects). No special test infrastructure needed.
6
+
7
+ ## Schema Example Helpers
8
+
9
+ Servus provides test helpers that extract `example` values from your JSON schemas, making it easy to generate test fixtures without maintaining separate factories.
10
+
11
+ ### Setup
12
+
13
+ Include the helpers in your test suite:
14
+
15
+ ```ruby
16
+ # spec/spec_helper.rb
17
+ require 'servus/testing'
18
+
19
+ RSpec.configure do |config|
20
+ config.include Servus::Testing::ExampleBuilders
21
+ end
22
+ ```
23
+
24
+ ### Using Schema Examples
25
+
26
+ Add `example` or `examples` keywords to your schemas:
27
+
28
+ ```ruby
29
+ class ProcessPayment::Service < Servus::Base
30
+ schema(
31
+ arguments: {
32
+ type: "object",
33
+ properties: {
34
+ user_id: { type: "integer", example: 123 },
35
+ amount: { type: "number", example: 100.0 },
36
+ currency: { type: "string", examples: ["USD", "EUR", "GBP"] }
37
+ }
38
+ },
39
+ result: {
40
+ type: "object",
41
+ properties: {
42
+ transaction_id: { type: "string", example: "txn_abc123" },
43
+ status: { type: "string", example: "approved" }
44
+ }
45
+ }
46
+ )
47
+ end
48
+ ```
49
+
50
+ Then use the helpers in your tests:
51
+
52
+ ```ruby
53
+ RSpec.describe ProcessPayment::Service do
54
+ it "processes payment successfully" do
55
+ # Extract examples from schema and override specific values
56
+ args = servus_arguments_example(ProcessPayment::Service, amount: 50.0)
57
+ # => { user_id: 123, amount: 50.0, currency: "USD" }
58
+
59
+ result = ProcessPayment::Service.call(**args)
60
+
61
+ expect(result).to be_success
62
+ expect(result.data.keys).to match_array(
63
+ servus_result_example(ProcessPayment::Service).data.keys
64
+ )
65
+ end
66
+
67
+ it "handles different currencies" do
68
+ %w[USD EUR GBP].each do |currency|
69
+ result = ProcessPayment::Service.call(
70
+ **servus_arguments_example(ProcessPayment::Service, currency: currency)
71
+ )
72
+ expect(result).to be_success
73
+ end
74
+ end
75
+ end
76
+ ```
77
+
78
+ ### Deep Merging
79
+
80
+ Overrides are deep-merged with schema examples, allowing you to override nested values:
81
+
82
+ ```ruby
83
+ # Schema has nested structure
84
+ args = servus_arguments_example(
85
+ CreateUser::Service,
86
+ user: { profile: { age: 35 } }
87
+ )
88
+ # => { user: { id: 1, profile: { name: 'Alice', age: 35 } } }
89
+ ```
90
+
91
+ ### Available Helpers
92
+
93
+ - `servus_arguments_example(ServiceClass, **overrides)` - Returns hash of argument examples
94
+ - `servus_result_example(ServiceClass, **overrides)` - Returns Response object with result examples
95
+
96
+ ## Basic Testing Pattern
97
+
98
+ ```ruby
99
+ RSpec.describe ProcessPayment::Service do
100
+ describe ".call" do
101
+ let(:user) { create(:user, balance: 1000) }
102
+
103
+ subject(:result) { described_class.call(user_id: user.id, amount: amount) }
104
+
105
+ context "with sufficient balance" do
106
+ let(:amount) { 50 }
107
+
108
+ it "processes payment" do
109
+ expect(result.success?).to be true
110
+ expect(result.data[:new_balance]).to eq(950)
111
+ expect(result.reload.balance).to eq(950)
112
+ end
113
+ end
114
+
115
+ context "with insufficient balance" do
116
+ let(:amount) { 2000 }
117
+
118
+ it "returns failure" do
119
+ expect(result.success?).to be false
120
+ expect(result.error.message).to eq("Insufficient funds")
121
+ expect(result.error).to be_a(Servus::Support::Errors::ServiceError)
122
+ end
123
+ end
124
+ end
125
+ end
126
+ ```
127
+
128
+ ## Testing Service Composition
129
+
130
+ When testing services that call other services, mock the child services:
131
+
132
+ ```ruby
133
+ describe Users::CreateWithAccount::Service do
134
+ # Make local or global helpers to clean up tests
135
+ def servus_success_result(data)
136
+ Servus::Support::Response.new(success: true, data: data, error: nil)
137
+ end
138
+
139
+ it "calls both create services" do
140
+ # Mock child services
141
+ allow(Users::Create::Service).to receive(:call).and_return(servus_success_result{ user: user })
142
+ allow(Accounts::Create::Service).to receive(:call).and_return(servus_success_result{ account: account })
143
+
144
+ result = described_class.call(email: "test@example.com", plan: "premium")
145
+
146
+ expect(Users::Create::Service).to have_received(:call)
147
+ expect(Accounts::Create::Service).to have_received(:call)
148
+
149
+ expect(result.success?).to be true
150
+ end
151
+ end
152
+ ```
153
+
154
+ ## Testing Schema Validation
155
+
156
+ Don't test that valid arguments pass validation - that's testing the framework. Do test that your schema catches invalid inputs:
157
+
158
+ ```ruby
159
+ it "validates required fields" do
160
+ expect {
161
+ Service.call(invalid: "params")
162
+ }.to raise_error(Servus::Support::Errors::ValidationError, /required/)
163
+ end
164
+ ```
165
+
166
+ ## Testing Event Emission
167
+
168
+ Servus provides RSpec matchers for testing that services emit events.
169
+
170
+ ### Setup
171
+
172
+ Include the matchers in your test suite:
173
+
174
+ ```ruby
175
+ # spec/spec_helper.rb
176
+ require 'servus/testing'
177
+ ```
178
+
179
+ ### emit_event Matcher
180
+
181
+ Assert that a block emits an event:
182
+
183
+ ```ruby
184
+ RSpec.describe CreateUser::Service do
185
+ it 'emits user_created event on success' do
186
+ expect {
187
+ described_class.call(email: 'test@example.com', name: 'Test')
188
+ }.to emit_event(:user_created)
189
+ end
190
+
191
+ it 'emits event with expected payload' do
192
+ expect {
193
+ described_class.call(email: 'test@example.com', name: 'Test')
194
+ }.to emit_event(:user_created).with(hash_including(email: 'test@example.com'))
195
+ end
196
+
197
+ # Using handler class instead of symbol
198
+ it 'emits to UserCreatedHandler' do
199
+ expect {
200
+ described_class.call(email: 'test@example.com', name: 'Test')
201
+ }.to emit_event(UserCreatedHandler)
202
+ end
203
+ end
204
+ ```
205
+
206
+ ### call_service Matcher
207
+
208
+ Assert that a handler invokes a service:
209
+
210
+ ```ruby
211
+ RSpec.describe UserCreatedHandler do
212
+ let(:payload) { { user_id: 123, email: 'test@example.com' } }
213
+
214
+ it 'invokes SendWelcomeEmail::Service' do
215
+ expect {
216
+ described_class.handle(payload)
217
+ }.to call_service(SendWelcomeEmail::Service).with(user_id: 123)
218
+ end
219
+
220
+ it 'invokes service asynchronously' do
221
+ expect {
222
+ described_class.handle(payload)
223
+ }.to call_service(SendWelcomeEmail::Service).async
224
+ end
225
+ end
226
+ ```
227
+
228
+ ### Testing EventHandler Directly
229
+
230
+ Test handlers by calling their `handle` class method:
231
+
232
+ ```ruby
233
+ RSpec.describe UserCreatedHandler do
234
+ let(:payload) { { user_id: 123, email: 'test@example.com' } }
235
+
236
+ before do
237
+ allow(SendWelcomeEmail::Service).to receive(:call_async)
238
+ allow(TrackAnalytics::Service).to receive(:call_async)
239
+ end
240
+
241
+ it 'invokes SendWelcomeEmail with mapped arguments' do
242
+ described_class.handle(payload)
243
+
244
+ expect(SendWelcomeEmail::Service)
245
+ .to have_received(:call_async)
246
+ .with(user_id: 123, email: 'test@example.com')
247
+ end
248
+
249
+ it 'invokes TrackAnalytics with event data' do
250
+ described_class.handle(payload)
251
+
252
+ expect(TrackAnalytics::Service)
253
+ .to have_received(:call_async)
254
+ .with(event: 'user_created', user_id: 123)
255
+ end
256
+
257
+ context 'with conditional invocation' do
258
+ it 'skips premium rewards for non-premium users' do
259
+ allow(GrantPremiumRewards::Service).to receive(:call)
260
+
261
+ described_class.handle(payload.merge(premium: false))
262
+
263
+ expect(GrantPremiumRewards::Service).not_to have_received(:call)
264
+ end
265
+ end
266
+ end
267
+ ```
268
+
269
+ ### Testing Event Emission from Handler
270
+
271
+ Test the `emit` class method:
272
+
273
+ ```ruby
274
+ RSpec.describe UserCreatedHandler do
275
+ it 'emits the user_created event' do
276
+ expect {
277
+ described_class.emit({ user_id: 123, email: 'test@example.com' })
278
+ }.to emit_event(:user_created)
279
+ end
280
+
281
+ it 'validates payload against schema' do
282
+ expect {
283
+ described_class.emit({ invalid: 'payload' })
284
+ }.to raise_error(Servus::Support::Errors::ValidationError)
285
+ end
286
+ end
287
+ ```
@@ -0,0 +1,99 @@
1
+ # @title Integration / 3. Rails Integration
2
+
3
+ # Rails Integration
4
+
5
+ Servus core works in any Ruby application. Rails-specific features (async, controller helpers, generators) are optional additions that integrate with Rails conventions.
6
+
7
+ ## Controller Integration
8
+
9
+ Use the `run_service` helper to call services from controllers with automatic error handling:
10
+
11
+ ```ruby
12
+ class UsersController < ApplicationController
13
+ include Servus::Helpers::ControllerHelpers
14
+
15
+ def create
16
+ run_service(Users::Create::Service, user_params)
17
+ end
18
+
19
+ # Failures automatically render JSON:
20
+ # { "error": { "code": "validation_error", "message": "..." } }
21
+ # with appropriate HTTP status code
22
+ #
23
+ # Success will go to view and service result will be available on @result
24
+ end
25
+ ```
26
+
27
+ Without the helper, handle responses manually:
28
+
29
+ ```ruby
30
+ def create
31
+ result = Users::Create::Service.call(user_params)
32
+ if result.success?
33
+ render json: { user: result.data[:user] }, status: :created
34
+ else
35
+ render json: { error: result.error.api_error }, status: error_status(result.error)
36
+ end
37
+ end
38
+ ```
39
+
40
+ ## Generator
41
+
42
+ Generate services with specs and schema files:
43
+
44
+ ```bash
45
+ rails generate servus:service process_payment
46
+
47
+ # Creates:
48
+ # app/services/process_payment/service.rb
49
+ # spec/services/process_payment/service_spec.rb
50
+ # app/schemas/services/process_payment/arguments.json
51
+ # app/schemas/services/process_payment/result.json
52
+ ```
53
+
54
+ Schema files are optional - delete them if you don't need validation.
55
+
56
+ ## Autoloading
57
+
58
+ Servus follows Rails autoloading conventions. Services in `app/services/` are automatically loaded by Rails:
59
+
60
+ ```ruby
61
+ # app/services/users/create/service.rb
62
+ module Users
63
+ module Create
64
+ class Service < Servus::Base
65
+ # ...
66
+ end
67
+ end
68
+ end
69
+
70
+ # Rails autoloads this as Users::Create::Service
71
+ ```
72
+
73
+ ## Configuration
74
+
75
+ Configure Servus in an initializer if needed:
76
+
77
+ ```ruby
78
+ # config/initializers/servus.rb
79
+ Servus.configure do |config|
80
+ config.schema_root = Rails.root.join('config/schemas')
81
+ end
82
+ ```
83
+
84
+ Most applications don't need any configuration.
85
+
86
+ ## Background Jobs
87
+
88
+ Async execution requires ActiveJob setup. Configure your adapter:
89
+
90
+ ```ruby
91
+ # config/application.rb
92
+ config.active_job.queue_adapter = :sidekiq
93
+ ```
94
+
95
+ Then use `.call_async`:
96
+
97
+ ```ruby
98
+ Users::SendWelcomeEmail::Service.call_async(user_id: user.id)
99
+ ```