servus 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/servus/event/event_generator.rb +54 -0
  3. data/lib/generators/servus/event/templates/event.rb.erb +44 -0
  4. data/lib/generators/servus/event/templates/event_spec.rb.erb +20 -0
  5. data/lib/generators/servus/guard/guard_generator.rb +1 -1
  6. data/lib/generators/servus/guard/templates/guard.rb.erb +5 -3
  7. data/lib/generators/servus/service/service_generator.rb +1 -1
  8. data/lib/servus/base.rb +46 -3
  9. data/lib/servus/config.rb +85 -12
  10. data/lib/servus/event.rb +235 -0
  11. data/lib/servus/events/bus.rb +111 -72
  12. data/lib/servus/events/class_router.rb +40 -0
  13. data/lib/servus/events/emitter.rb +21 -6
  14. data/lib/servus/events/invocation.rb +94 -0
  15. data/lib/servus/events/router.rb +44 -0
  16. data/lib/servus/guard.rb +7 -6
  17. data/lib/servus/guards/falsey_guard.rb +3 -3
  18. data/lib/servus/guards/presence_guard.rb +4 -4
  19. data/lib/servus/guards/state_guard.rb +4 -5
  20. data/lib/servus/guards/truthy_guard.rb +3 -3
  21. data/lib/servus/helpers/controller_helpers.rb +40 -0
  22. data/lib/servus/railtie.rb +10 -8
  23. data/lib/servus/support/errors.rb +16 -0
  24. data/lib/servus/support/lockdown.rb +94 -0
  25. data/lib/servus/support/logger.rb +18 -0
  26. data/lib/servus/support/validator.rb +70 -40
  27. data/lib/servus/testing/example_builders.rb +52 -0
  28. data/lib/servus/testing/matchers.rb +103 -4
  29. data/lib/servus/version.rb +1 -1
  30. data/lib/servus.rb +7 -2
  31. metadata +14 -116
  32. data/.claude/commands/check-docs.md +0 -1
  33. data/.claude/commands/consistency-check.md +0 -1
  34. data/.claude/commands/fine-tooth-comb.md +0 -1
  35. data/.claude/commands/red-green-refactor.md +0 -5
  36. data/.claude/settings.json +0 -24
  37. data/.rspec +0 -3
  38. data/.rubocop.yml +0 -27
  39. data/.yardopts +0 -6
  40. data/CHANGELOG.md +0 -169
  41. data/CLAUDE.md +0 -10
  42. data/IDEAS.md +0 -5
  43. data/LICENSE.txt +0 -21
  44. data/READme.md +0 -856
  45. data/Rakefile +0 -45
  46. data/docs/core/1_overview.md +0 -81
  47. data/docs/core/2_architecture.md +0 -120
  48. data/docs/core/3_service_objects.md +0 -154
  49. data/docs/features/1_schema_validation.md +0 -161
  50. data/docs/features/2_error_handling.md +0 -129
  51. data/docs/features/3_async_execution.md +0 -81
  52. data/docs/features/4_logging.md +0 -64
  53. data/docs/features/5_event_bus.md +0 -244
  54. data/docs/features/6_guards.md +0 -356
  55. data/docs/features/7_lazy_resolvers.md +0 -238
  56. data/docs/features/guards_naming_convention.md +0 -540
  57. data/docs/guides/1_common_patterns.md +0 -90
  58. data/docs/guides/2_migration_guide.md +0 -225
  59. data/docs/integration/1_configuration.md +0 -154
  60. data/docs/integration/2_testing.md +0 -304
  61. data/docs/integration/3_rails_integration.md +0 -99
  62. data/docs/yard/Servus/Base.html +0 -1645
  63. data/docs/yard/Servus/Config.html +0 -582
  64. data/docs/yard/Servus/Extensions/Async/Call.html +0 -400
  65. data/docs/yard/Servus/Extensions/Async/Errors/AsyncError.html +0 -140
  66. data/docs/yard/Servus/Extensions/Async/Errors/JobEnqueueError.html +0 -154
  67. data/docs/yard/Servus/Extensions/Async/Errors/ServiceNotFoundError.html +0 -154
  68. data/docs/yard/Servus/Extensions/Async/Errors.html +0 -128
  69. data/docs/yard/Servus/Extensions/Async/Ext.html +0 -119
  70. data/docs/yard/Servus/Extensions/Async/Job.html +0 -310
  71. data/docs/yard/Servus/Extensions/Async.html +0 -141
  72. data/docs/yard/Servus/Extensions.html +0 -117
  73. data/docs/yard/Servus/Generators/ServiceGenerator.html +0 -261
  74. data/docs/yard/Servus/Generators.html +0 -115
  75. data/docs/yard/Servus/Helpers/ControllerHelpers.html +0 -457
  76. data/docs/yard/Servus/Helpers.html +0 -115
  77. data/docs/yard/Servus/Railtie.html +0 -134
  78. data/docs/yard/Servus/Support/Errors/AuthenticationError.html +0 -287
  79. data/docs/yard/Servus/Support/Errors/BadRequestError.html +0 -283
  80. data/docs/yard/Servus/Support/Errors/ForbiddenError.html +0 -284
  81. data/docs/yard/Servus/Support/Errors/InternalServerError.html +0 -283
  82. data/docs/yard/Servus/Support/Errors/NotFoundError.html +0 -284
  83. data/docs/yard/Servus/Support/Errors/ServiceError.html +0 -489
  84. data/docs/yard/Servus/Support/Errors/ServiceUnavailableError.html +0 -290
  85. data/docs/yard/Servus/Support/Errors/UnauthorizedError.html +0 -200
  86. data/docs/yard/Servus/Support/Errors/UnprocessableEntityError.html +0 -288
  87. data/docs/yard/Servus/Support/Errors/ValidationError.html +0 -200
  88. data/docs/yard/Servus/Support/Errors.html +0 -140
  89. data/docs/yard/Servus/Support/Logger.html +0 -856
  90. data/docs/yard/Servus/Support/Rescuer/BlockContext.html +0 -585
  91. data/docs/yard/Servus/Support/Rescuer/CallOverride.html +0 -257
  92. data/docs/yard/Servus/Support/Rescuer/ClassMethods.html +0 -343
  93. data/docs/yard/Servus/Support/Rescuer.html +0 -267
  94. data/docs/yard/Servus/Support/Response.html +0 -574
  95. data/docs/yard/Servus/Support/Validator.html +0 -1150
  96. data/docs/yard/Servus/Support.html +0 -119
  97. data/docs/yard/Servus/Testing/ExampleBuilders.html +0 -523
  98. data/docs/yard/Servus/Testing/ExampleExtractor.html +0 -578
  99. data/docs/yard/Servus/Testing.html +0 -142
  100. data/docs/yard/Servus.html +0 -343
  101. data/docs/yard/_index.html +0 -535
  102. data/docs/yard/class_list.html +0 -54
  103. data/docs/yard/css/common.css +0 -1
  104. data/docs/yard/css/full_list.css +0 -58
  105. data/docs/yard/css/style.css +0 -503
  106. data/docs/yard/file.1_common_patterns.html +0 -154
  107. data/docs/yard/file.1_configuration.html +0 -115
  108. data/docs/yard/file.1_overview.html +0 -142
  109. data/docs/yard/file.1_schema_validation.html +0 -188
  110. data/docs/yard/file.2_architecture.html +0 -157
  111. data/docs/yard/file.2_error_handling.html +0 -190
  112. data/docs/yard/file.2_migration_guide.html +0 -242
  113. data/docs/yard/file.2_testing.html +0 -227
  114. data/docs/yard/file.3_async_execution.html +0 -145
  115. data/docs/yard/file.3_rails_integration.html +0 -160
  116. data/docs/yard/file.3_service_objects.html +0 -191
  117. data/docs/yard/file.4_logging.html +0 -135
  118. data/docs/yard/file.ErrorHandling.html +0 -190
  119. data/docs/yard/file.READme.html +0 -674
  120. data/docs/yard/file.architecture.html +0 -157
  121. data/docs/yard/file.async_execution.html +0 -145
  122. data/docs/yard/file.common_patterns.html +0 -154
  123. data/docs/yard/file.configuration.html +0 -115
  124. data/docs/yard/file.error_handling.html +0 -190
  125. data/docs/yard/file.logging.html +0 -135
  126. data/docs/yard/file.migration_guide.html +0 -242
  127. data/docs/yard/file.overview.html +0 -142
  128. data/docs/yard/file.rails_integration.html +0 -160
  129. data/docs/yard/file.schema_validation.html +0 -188
  130. data/docs/yard/file.service_objects.html +0 -191
  131. data/docs/yard/file.testing.html +0 -227
  132. data/docs/yard/file_list.html +0 -119
  133. data/docs/yard/frames.html +0 -22
  134. data/docs/yard/index.html +0 -674
  135. data/docs/yard/js/app.js +0 -344
  136. data/docs/yard/js/full_list.js +0 -242
  137. data/docs/yard/js/jquery.js +0 -4
  138. data/docs/yard/method_list.html +0 -542
  139. data/docs/yard/top-level-namespace.html +0 -110
  140. data/lib/generators/servus/event_handler/event_handler_generator.rb +0 -59
  141. data/lib/generators/servus/event_handler/templates/handler.rb.erb +0 -86
  142. data/lib/generators/servus/event_handler/templates/handler_spec.rb.erb +0 -48
  143. data/lib/servus/event_handler.rb +0 -290
  144. data/lib/servus/events/errors.rb +0 -10
@@ -1,225 +0,0 @@
1
- # @title Guides / 2. Migration Guide
2
-
3
- # Migration Guide
4
-
5
- Strategies for adopting Servus in existing Rails applications, and version-specific migration notes.
6
-
7
- ## Migrating to 0.3.0
8
-
9
- ### Breaking: `result.data` is no longer guaranteed nil on failure
10
-
11
- In 0.2.x, `failure()` always set `result.data` to `nil`. Some code relied on this to distinguish success from failure:
12
-
13
- ```ruby
14
- # ❌ Unsafe in 0.3.0 — result.data can be non-nil on failure
15
- if result.data
16
- # handle success
17
- else
18
- # handle failure
19
- end
20
- ```
21
-
22
- In 0.3.0, `failure()` accepts an optional `data:` kwarg, so failure responses can carry structured data. Use `result.success?` or `result.failure?` instead:
23
-
24
- ```ruby
25
- # ✅ Correct — always use success? or failure?
26
- if result.success?
27
- render json: result.data, status: :ok
28
- else
29
- render json: { error: result.error.api_error, details: result.data }, status: result.error.http_status
30
- end
31
- ```
32
-
33
- **How to find affected code**: Search your codebase for patterns like `if result.data`, `result.data.present?`, or `result.data.nil?` used as success/failure checks. Replace them with `result.success?` or `result.failure?`.
34
-
35
- ### Removed: `Response#with_data`
36
-
37
- `with_data` was removed in favor of the `data:` kwarg on `failure()`:
38
-
39
- ```ruby
40
- # ❌ 0.2.x — no longer available
41
- failure("Declined").tap { |r| r.with_data(reason: "insufficient_funds") }
42
-
43
- # ✅ 0.3.0
44
- failure("Declined", data: { reason: "insufficient_funds" })
45
- ```
46
-
47
- ### New: DataObject wrapping
48
-
49
- Response data is now wrapped in a `DataObject` when it is a Hash. This is backwards compatible — `result.data[:key]` still works. You can also use accessor-style access:
50
-
51
- ```ruby
52
- result.data[:user] # still works
53
- result.data.user # also works (new)
54
- result.data.user.email # nested access (new)
55
- ```
56
-
57
- ## Incremental Adoption
58
-
59
- 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.
60
-
61
- ## Extracting from Fat Controllers
62
-
63
- Identify controller actions with complex business logic and extract to services:
64
-
65
- **Before**:
66
- ```ruby
67
- class OrdersController < ApplicationController
68
- def create
69
- # 50 lines of business logic
70
- # Multiple model operations
71
- # External API calls
72
- # Email sending
73
- end
74
- end
75
- ```
76
-
77
- **After**:
78
- ```ruby
79
- class OrdersController < ApplicationController
80
- def create
81
- result = Orders::Create::Service.call(order_params)
82
- if result.success?
83
- render json: { order: result.data[:order] }, status: :created
84
- else
85
- render json: { error: result.error.api_error }, status: :unprocessable_entity
86
- end
87
- end
88
- end
89
-
90
- # Or use the helper
91
- class OrdersController < ApplicationController
92
- include Servus::Helpers::ControllerHelpers
93
-
94
- def create
95
- run_service(Orders::Create::Service, order_params) do |result|
96
- render json: { order: result.data[:order] }, status: :created
97
- end
98
- end
99
- end
100
- ```
101
-
102
- ## Extracting from Fat Models
103
-
104
- Move orchestration logic from models to services. Keep data-related methods in models:
105
-
106
- **Before**:
107
- ```ruby
108
- class Order < ApplicationRecord
109
- def complete_purchase
110
- charge_payment
111
- update_inventory
112
- send_confirmation_email
113
- create_invoice
114
- end
115
- end
116
- ```
117
-
118
- **After**:
119
- ```ruby
120
- class Order < ApplicationRecord
121
- # Model focuses on data
122
- validates :total, presence: true
123
- belongs_to :user
124
- end
125
-
126
- class Orders::CompletePurchase::Service < Servus::Base
127
- # Service handles orchestration
128
- def initialize(order_id:)
129
- @order_id = order_id
130
- end
131
-
132
- def call
133
- order = Order.find(@order_id)
134
- Payments::Charge::Service.call(order_id: order.id)
135
- Inventory::Update::Service.call(order_id: order.id)
136
- Mailers::SendConfirmation::Service.call(order_id: order.id)
137
- success(order: order)
138
- end
139
- end
140
- ```
141
-
142
- ## Replacing Callbacks
143
-
144
- Extract callback logic to explicit service calls:
145
-
146
- **Before**:
147
- ```ruby
148
- class User < ApplicationRecord
149
- after_create :send_welcome_email
150
- after_create :create_default_account
151
- after_update :notify_changes, if: :email_changed?
152
- end
153
- ```
154
-
155
- **After**:
156
- ```ruby
157
- class User < ApplicationRecord
158
- # Minimal or no callbacks
159
- end
160
-
161
- class Users::Create::Service < Servus::Base
162
- def call
163
- user = User.create!(params)
164
- send_welcome_email(user)
165
- create_default_account(user)
166
- success(user: user)
167
- end
168
- end
169
- ```
170
-
171
- ## Migrating Background Jobs
172
-
173
- Extract job logic to services, call via `.call_async`:
174
-
175
- **Before**:
176
- ```ruby
177
- class ProcessOrderJob < ApplicationJob
178
- def perform(order_id)
179
- # 50 lines of business logic
180
- end
181
- end
182
-
183
- ProcessOrderJob.perform_later(order.id)
184
- ```
185
-
186
- **After**:
187
- ```ruby
188
- class Orders::Process::Service < Servus::Base
189
- def initialize(order_id:)
190
- @order_id = order_id
191
- end
192
-
193
- def call
194
- # Business logic
195
- end
196
- end
197
-
198
- Orders::Process::Service.call_async(order_id: order.id)
199
- ```
200
-
201
- Now the service can be called synchronously (from console, tests) or asynchronously (from controllers, jobs).
202
-
203
- ## Testing During Migration
204
-
205
- Keep existing tests working while adding service tests:
206
-
207
- ```ruby
208
- # Keep existing controller test
209
- describe OrdersController do
210
- it "creates order" do
211
- post :create, params: params
212
- expect(response).to be_successful
213
- end
214
- end
215
-
216
- # Add service test
217
- describe Orders::Create::Service do
218
- it "creates order" do
219
- result = described_class.call(params)
220
- expect(result.success?).to be true
221
- end
222
- end
223
- ```
224
-
225
- Remove legacy tests after service tests prove comprehensive.
@@ -1,154 +0,0 @@
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, event handlers, and guards:
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
-
23
- # Default: 'app/guards'
24
- config.guards_dir = 'app/guards'
25
- end
26
- ```
27
-
28
- These affect legacy file-based schemas, handler auto-loading, and guard auto-loading. Schemas defined via the `schema` DSL method do not use files.
29
-
30
- ## Schema Cache
31
-
32
- Schemas are cached after first load for performance. Clear the cache during development when schemas change:
33
-
34
- ```ruby
35
- Servus::Support::Validator.clear_cache!
36
- ```
37
-
38
- In production, schemas are deployed with code - no need to clear cache.
39
-
40
- ## Log Level
41
-
42
- Servus uses `Rails.logger` (or stdout in non-Rails apps). Control logging via Rails configuration:
43
-
44
- ```ruby
45
- # config/environments/production.rb
46
- config.log_level = :info # Hides DEBUG argument logs
47
- ```
48
-
49
- ## ActiveJob Configuration
50
-
51
- Async execution uses ActiveJob. Configure your adapter:
52
-
53
- ```ruby
54
- # config/application.rb
55
- config.active_job.queue_adapter = :sidekiq
56
- config.active_job.default_queue_name = :default
57
- ```
58
-
59
- Servus respects ActiveJob queue configuration - no Servus-specific setup needed.
60
-
61
- ## Guards Configuration
62
-
63
- ### Default Guards
64
-
65
- Servus includes built-in guards (`PresenceGuard`, `TruthyGuard`, `FalseyGuard`, `StateGuard`) that are loaded by default. Disable them if you want to define your own:
66
-
67
- ```ruby
68
- # config/initializers/servus.rb
69
- Servus.configure do |config|
70
- # Default: true
71
- config.include_default_guards = false
72
- end
73
- ```
74
-
75
- ### Guard Auto-Loading
76
-
77
- In Rails, custom guards in `app/guards/` are automatically loaded. The Railtie eager-loads all `*_guard.rb` files from `config.guards_dir`:
78
-
79
- ```
80
- app/guards/
81
- ├── sufficient_balance_guard.rb
82
- ├── valid_amount_guard.rb
83
- └── authorized_guard.rb
84
- ```
85
-
86
- Guards define methods on `Servus::Guards` when inherited from `Servus::Guard`. The `Guard` suffix is stripped from the method name:
87
-
88
- ```ruby
89
- # app/guards/sufficient_balance_guard.rb
90
- class SufficientBalanceGuard < Servus::Guard
91
- http_status 422
92
- error_code 'insufficient_balance'
93
-
94
- message 'Insufficient balance: need %<required>s, have %<available>s' do
95
- { required: amount, available: account.balance }
96
- end
97
-
98
- def test(account:, amount:)
99
- account.balance >= amount
100
- end
101
- end
102
-
103
- # Usage in services:
104
- # enforce_sufficient_balance!(account: account, amount: 100) # throws on failure
105
- # check_sufficient_balance?(account: account, amount: 100) # returns boolean
106
- ```
107
-
108
- ## Event Bus Configuration
109
-
110
- ### Strict Event Validation
111
-
112
- Enable strict validation to catch handlers subscribing to events that aren't emitted by any service:
113
-
114
- ```ruby
115
- # config/initializers/servus.rb
116
- Servus.configure do |config|
117
- # Default: true
118
- config.strict_event_validation = true
119
- end
120
- ```
121
-
122
- When enabled, you can validate handlers at boot or in CI:
123
-
124
- ```ruby
125
- # In a rake task or initializer
126
- Servus::EventHandler.validate_all_handlers!
127
- ```
128
-
129
- This raises `Servus::Events::OrphanedHandlerError` if any handler subscribes to a non-existent event.
130
-
131
- ### Handler Auto-Loading
132
-
133
- In Rails, handlers in `app/events/` are automatically loaded. The Railtie:
134
- - Clears the event bus on reload in development
135
- - Eager-loads all `*_handler.rb` files from `config.events_dir`
136
-
137
- ```
138
- app/events/
139
- ├── user_created_handler.rb
140
- ├── payment_processed_handler.rb
141
- └── order_completed_handler.rb
142
- ```
143
-
144
- ### Event Instrumentation
145
-
146
- Events are instrumented via ActiveSupport::Notifications with the prefix `servus.events.`:
147
-
148
- ```ruby
149
- # Subscribe to all Servus events
150
- ActiveSupport::Notifications.subscribe(/^servus\.events\./) do |name, *args|
151
- event_name = name.sub('servus.events.', '')
152
- Rails.logger.info "Event: #{event_name}"
153
- end
154
- ```
@@ -1,304 +0,0 @@
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
- failure: {
47
- type: "object",
48
- properties: {
49
- reason: { type: "string", example: "card_declined" },
50
- decline_code: { type: "string", example: "insufficient_funds" }
51
- }
52
- }
53
- )
54
- end
55
- ```
56
-
57
- Then use the helpers in your tests:
58
-
59
- ```ruby
60
- RSpec.describe ProcessPayment::Service do
61
- it "processes payment successfully" do
62
- # Extract examples from schema and override specific values
63
- args = servus_arguments_example(ProcessPayment::Service, amount: 50.0)
64
- # => { user_id: 123, amount: 50.0, currency: "USD" }
65
-
66
- result = ProcessPayment::Service.call(**args)
67
-
68
- expect(result).to be_success
69
- expect(result.data.keys).to match_array(
70
- servus_result_example(ProcessPayment::Service).data.keys
71
- )
72
- end
73
-
74
- it "returns structured failure data on decline" do
75
- args = servus_arguments_example(ProcessPayment::Service, amount: 999_999)
76
- result = ProcessPayment::Service.call(**args)
77
-
78
- expected = servus_failure_example(ProcessPayment::Service)
79
- expect(result).to be_failure
80
- expect(result.data.keys).to match_array(expected.data.keys)
81
- end
82
-
83
- it "handles different currencies" do
84
- %w[USD EUR GBP].each do |currency|
85
- result = ProcessPayment::Service.call(
86
- **servus_arguments_example(ProcessPayment::Service, currency: currency)
87
- )
88
- expect(result).to be_success
89
- end
90
- end
91
- end
92
- ```
93
-
94
- ### Deep Merging
95
-
96
- Overrides are deep-merged with schema examples, allowing you to override nested values:
97
-
98
- ```ruby
99
- # Schema has nested structure
100
- args = servus_arguments_example(
101
- CreateUser::Service,
102
- user: { profile: { age: 35 } }
103
- )
104
- # => { user: { id: 1, profile: { name: 'Alice', age: 35 } } }
105
- ```
106
-
107
- ### Available Helpers
108
-
109
- - `servus_arguments_example(ServiceClass, **overrides)` - Returns hash of argument examples
110
- - `servus_result_example(ServiceClass, **overrides)` - Returns successful Response with result examples
111
- - `servus_failure_example(ServiceClass, **overrides)` - Returns failure Response with failure schema examples
112
-
113
- ## Basic Testing Pattern
114
-
115
- ```ruby
116
- RSpec.describe ProcessPayment::Service do
117
- describe ".call" do
118
- let(:user) { create(:user, balance: 1000) }
119
-
120
- subject(:result) { described_class.call(user_id: user.id, amount: amount) }
121
-
122
- context "with sufficient balance" do
123
- let(:amount) { 50 }
124
-
125
- it "processes payment" do
126
- expect(result.success?).to be true
127
- expect(result.data[:new_balance]).to eq(950)
128
- expect(result.reload.balance).to eq(950)
129
- end
130
- end
131
-
132
- context "with insufficient balance" do
133
- let(:amount) { 2000 }
134
-
135
- it "returns failure" do
136
- expect(result.success?).to be false
137
- expect(result.error.message).to eq("Insufficient funds")
138
- expect(result.error).to be_a(Servus::Support::Errors::ServiceError)
139
- end
140
- end
141
- end
142
- end
143
- ```
144
-
145
- ## Testing Service Composition
146
-
147
- When testing services that call other services, mock the child services:
148
-
149
- ```ruby
150
- describe Users::CreateWithAccount::Service do
151
- # Make local or global helpers to clean up tests
152
- def servus_success_result(data)
153
- Servus::Support::Response.new(success: true, data: data, error: nil)
154
- end
155
-
156
- it "calls both create services" do
157
- # Mock child services
158
- allow(Users::Create::Service).to receive(:call).and_return(servus_success_result{ user: user })
159
- allow(Accounts::Create::Service).to receive(:call).and_return(servus_success_result{ account: account })
160
-
161
- result = described_class.call(email: "test@example.com", plan: "premium")
162
-
163
- expect(Users::Create::Service).to have_received(:call)
164
- expect(Accounts::Create::Service).to have_received(:call)
165
-
166
- expect(result.success?).to be true
167
- end
168
- end
169
- ```
170
-
171
- ## Testing Schema Validation
172
-
173
- Don't test that valid arguments pass validation - that's testing the framework. Do test that your schema catches invalid inputs:
174
-
175
- ```ruby
176
- it "validates required fields" do
177
- expect {
178
- Service.call(invalid: "params")
179
- }.to raise_error(Servus::Support::Errors::ValidationError, /required/)
180
- end
181
- ```
182
-
183
- ## Testing Event Emission
184
-
185
- Servus provides RSpec matchers for testing that services emit events.
186
-
187
- ### Setup
188
-
189
- Include the matchers in your test suite:
190
-
191
- ```ruby
192
- # spec/spec_helper.rb
193
- require 'servus/testing'
194
- ```
195
-
196
- ### emit_event Matcher
197
-
198
- Assert that a block emits an event:
199
-
200
- ```ruby
201
- RSpec.describe CreateUser::Service do
202
- it 'emits user_created event on success' do
203
- expect {
204
- described_class.call(email: 'test@example.com', name: 'Test')
205
- }.to emit_event(:user_created)
206
- end
207
-
208
- it 'emits event with expected payload' do
209
- expect {
210
- described_class.call(email: 'test@example.com', name: 'Test')
211
- }.to emit_event(:user_created).with(hash_including(email: 'test@example.com'))
212
- end
213
-
214
- # Using handler class instead of symbol
215
- it 'emits to UserCreatedHandler' do
216
- expect {
217
- described_class.call(email: 'test@example.com', name: 'Test')
218
- }.to emit_event(UserCreatedHandler)
219
- end
220
- end
221
- ```
222
-
223
- ### call_service Matcher
224
-
225
- Assert that a handler invokes a service:
226
-
227
- ```ruby
228
- RSpec.describe UserCreatedHandler do
229
- let(:payload) { { user_id: 123, email: 'test@example.com' } }
230
-
231
- it 'invokes SendWelcomeEmail::Service' do
232
- expect {
233
- described_class.handle(payload)
234
- }.to call_service(SendWelcomeEmail::Service).with(user_id: 123)
235
- end
236
-
237
- it 'invokes service asynchronously' do
238
- expect {
239
- described_class.handle(payload)
240
- }.to call_service(SendWelcomeEmail::Service).async
241
- end
242
- end
243
- ```
244
-
245
- ### Testing EventHandler Directly
246
-
247
- Test handlers by calling their `handle` class method:
248
-
249
- ```ruby
250
- RSpec.describe UserCreatedHandler do
251
- let(:payload) { { user_id: 123, email: 'test@example.com' } }
252
-
253
- before do
254
- allow(SendWelcomeEmail::Service).to receive(:call_async)
255
- allow(TrackAnalytics::Service).to receive(:call_async)
256
- end
257
-
258
- it 'invokes SendWelcomeEmail with mapped arguments' do
259
- described_class.handle(payload)
260
-
261
- expect(SendWelcomeEmail::Service)
262
- .to have_received(:call_async)
263
- .with(user_id: 123, email: 'test@example.com')
264
- end
265
-
266
- it 'invokes TrackAnalytics with event data' do
267
- described_class.handle(payload)
268
-
269
- expect(TrackAnalytics::Service)
270
- .to have_received(:call_async)
271
- .with(event: 'user_created', user_id: 123)
272
- end
273
-
274
- context 'with conditional invocation' do
275
- it 'skips premium rewards for non-premium users' do
276
- allow(GrantPremiumRewards::Service).to receive(:call)
277
-
278
- described_class.handle(payload.merge(premium: false))
279
-
280
- expect(GrantPremiumRewards::Service).not_to have_received(:call)
281
- end
282
- end
283
- end
284
- ```
285
-
286
- ### Testing Event Emission from Handler
287
-
288
- Test the `emit` class method:
289
-
290
- ```ruby
291
- RSpec.describe UserCreatedHandler do
292
- it 'emits the user_created event' do
293
- expect {
294
- described_class.emit({ user_id: 123, email: 'test@example.com' })
295
- }.to emit_event(:user_created)
296
- end
297
-
298
- it 'validates payload against schema' do
299
- expect {
300
- described_class.emit({ invalid: 'payload' })
301
- }.to raise_error(Servus::Support::Errors::ValidationError)
302
- end
303
- end
304
- ```