servus 0.2.1 → 0.4.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 (138) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/servus/event_handler/event_handler_generator.rb +1 -1
  3. data/lib/generators/servus/guard/guard_generator.rb +1 -1
  4. data/lib/generators/servus/guard/templates/guard.rb.erb +5 -3
  5. data/lib/generators/servus/service/service_generator.rb +1 -1
  6. data/lib/servus/base.rb +67 -9
  7. data/lib/servus/config.rb +71 -3
  8. data/lib/servus/events/bus.rb +29 -0
  9. data/lib/servus/events/emitter.rb +15 -0
  10. data/lib/servus/extensions/lazily/call.rb +82 -0
  11. data/lib/servus/extensions/lazily/errors.rb +37 -0
  12. data/lib/servus/extensions/lazily/ext.rb +23 -0
  13. data/lib/servus/extensions/lazily/resolver.rb +32 -0
  14. data/lib/servus/guard.rb +7 -6
  15. data/lib/servus/guards/falsey_guard.rb +3 -3
  16. data/lib/servus/guards/presence_guard.rb +4 -4
  17. data/lib/servus/guards/state_guard.rb +4 -5
  18. data/lib/servus/guards/truthy_guard.rb +3 -3
  19. data/lib/servus/helpers/controller_helpers.rb +40 -0
  20. data/lib/servus/railtie.rb +7 -1
  21. data/lib/servus/support/data_object.rb +80 -0
  22. data/lib/servus/support/errors.rb +16 -0
  23. data/lib/servus/support/lockdown.rb +94 -0
  24. data/lib/servus/support/logger.rb +16 -0
  25. data/lib/servus/support/response.rb +12 -1
  26. data/lib/servus/support/validator.rb +79 -34
  27. data/lib/servus/testing/example_builders.rb +74 -0
  28. data/lib/servus/testing/matchers.rb +99 -0
  29. data/lib/servus/version.rb +1 -1
  30. data/lib/servus.rb +2 -0
  31. metadata +16 -114
  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 -122
  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 -77
  47. data/docs/core/2_architecture.md +0 -120
  48. data/docs/core/3_service_objects.md +0 -121
  49. data/docs/features/1_schema_validation.md +0 -119
  50. data/docs/features/2_error_handling.md +0 -121
  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/guards_naming_convention.md +0 -540
  56. data/docs/guides/1_common_patterns.md +0 -90
  57. data/docs/guides/2_migration_guide.md +0 -175
  58. data/docs/integration/1_configuration.md +0 -154
  59. data/docs/integration/2_testing.md +0 -287
  60. data/docs/integration/3_rails_integration.md +0 -99
  61. data/docs/yard/Servus/Base.html +0 -1645
  62. data/docs/yard/Servus/Config.html +0 -582
  63. data/docs/yard/Servus/Extensions/Async/Call.html +0 -400
  64. data/docs/yard/Servus/Extensions/Async/Errors/AsyncError.html +0 -140
  65. data/docs/yard/Servus/Extensions/Async/Errors/JobEnqueueError.html +0 -154
  66. data/docs/yard/Servus/Extensions/Async/Errors/ServiceNotFoundError.html +0 -154
  67. data/docs/yard/Servus/Extensions/Async/Errors.html +0 -128
  68. data/docs/yard/Servus/Extensions/Async/Ext.html +0 -119
  69. data/docs/yard/Servus/Extensions/Async/Job.html +0 -310
  70. data/docs/yard/Servus/Extensions/Async.html +0 -141
  71. data/docs/yard/Servus/Extensions.html +0 -117
  72. data/docs/yard/Servus/Generators/ServiceGenerator.html +0 -261
  73. data/docs/yard/Servus/Generators.html +0 -115
  74. data/docs/yard/Servus/Helpers/ControllerHelpers.html +0 -457
  75. data/docs/yard/Servus/Helpers.html +0 -115
  76. data/docs/yard/Servus/Railtie.html +0 -134
  77. data/docs/yard/Servus/Support/Errors/AuthenticationError.html +0 -287
  78. data/docs/yard/Servus/Support/Errors/BadRequestError.html +0 -283
  79. data/docs/yard/Servus/Support/Errors/ForbiddenError.html +0 -284
  80. data/docs/yard/Servus/Support/Errors/InternalServerError.html +0 -283
  81. data/docs/yard/Servus/Support/Errors/NotFoundError.html +0 -284
  82. data/docs/yard/Servus/Support/Errors/ServiceError.html +0 -489
  83. data/docs/yard/Servus/Support/Errors/ServiceUnavailableError.html +0 -290
  84. data/docs/yard/Servus/Support/Errors/UnauthorizedError.html +0 -200
  85. data/docs/yard/Servus/Support/Errors/UnprocessableEntityError.html +0 -288
  86. data/docs/yard/Servus/Support/Errors/ValidationError.html +0 -200
  87. data/docs/yard/Servus/Support/Errors.html +0 -140
  88. data/docs/yard/Servus/Support/Logger.html +0 -856
  89. data/docs/yard/Servus/Support/Rescuer/BlockContext.html +0 -585
  90. data/docs/yard/Servus/Support/Rescuer/CallOverride.html +0 -257
  91. data/docs/yard/Servus/Support/Rescuer/ClassMethods.html +0 -343
  92. data/docs/yard/Servus/Support/Rescuer.html +0 -267
  93. data/docs/yard/Servus/Support/Response.html +0 -574
  94. data/docs/yard/Servus/Support/Validator.html +0 -1150
  95. data/docs/yard/Servus/Support.html +0 -119
  96. data/docs/yard/Servus/Testing/ExampleBuilders.html +0 -523
  97. data/docs/yard/Servus/Testing/ExampleExtractor.html +0 -578
  98. data/docs/yard/Servus/Testing.html +0 -142
  99. data/docs/yard/Servus.html +0 -343
  100. data/docs/yard/_index.html +0 -535
  101. data/docs/yard/class_list.html +0 -54
  102. data/docs/yard/css/common.css +0 -1
  103. data/docs/yard/css/full_list.css +0 -58
  104. data/docs/yard/css/style.css +0 -503
  105. data/docs/yard/file.1_common_patterns.html +0 -154
  106. data/docs/yard/file.1_configuration.html +0 -115
  107. data/docs/yard/file.1_overview.html +0 -142
  108. data/docs/yard/file.1_schema_validation.html +0 -188
  109. data/docs/yard/file.2_architecture.html +0 -157
  110. data/docs/yard/file.2_error_handling.html +0 -190
  111. data/docs/yard/file.2_migration_guide.html +0 -242
  112. data/docs/yard/file.2_testing.html +0 -227
  113. data/docs/yard/file.3_async_execution.html +0 -145
  114. data/docs/yard/file.3_rails_integration.html +0 -160
  115. data/docs/yard/file.3_service_objects.html +0 -191
  116. data/docs/yard/file.4_logging.html +0 -135
  117. data/docs/yard/file.ErrorHandling.html +0 -190
  118. data/docs/yard/file.READme.html +0 -674
  119. data/docs/yard/file.architecture.html +0 -157
  120. data/docs/yard/file.async_execution.html +0 -145
  121. data/docs/yard/file.common_patterns.html +0 -154
  122. data/docs/yard/file.configuration.html +0 -115
  123. data/docs/yard/file.error_handling.html +0 -190
  124. data/docs/yard/file.logging.html +0 -135
  125. data/docs/yard/file.migration_guide.html +0 -242
  126. data/docs/yard/file.overview.html +0 -142
  127. data/docs/yard/file.rails_integration.html +0 -160
  128. data/docs/yard/file.schema_validation.html +0 -188
  129. data/docs/yard/file.service_objects.html +0 -191
  130. data/docs/yard/file.testing.html +0 -227
  131. data/docs/yard/file_list.html +0 -119
  132. data/docs/yard/frames.html +0 -22
  133. data/docs/yard/index.html +0 -674
  134. data/docs/yard/js/app.js +0 -344
  135. data/docs/yard/js/full_list.js +0 -242
  136. data/docs/yard/js/jquery.js +0 -4
  137. data/docs/yard/method_list.html +0 -542
  138. data/docs/yard/top-level-namespace.html +0 -110
@@ -1,64 +0,0 @@
1
- # @title Features / 4. Logging
2
-
3
- # Logging
4
-
5
- Servus automatically logs all service executions with timing information. No instrumentation code needed in services.
6
-
7
- ## What Gets Logged
8
-
9
- **Service calls** (DEBUG): Service class name and arguments
10
- ```
11
- [Servus] Users::Create::Service called with {:email=>"user@example.com", :name=>"John"}
12
- ```
13
-
14
- **Successful completions** (INFO): Service class name and duration
15
- ```
16
- [Servus] Users::Create::Service completed successfully in 0.0243s
17
- ```
18
-
19
- **Failures** (WARN): Service class name, error type, message, and duration
20
- ```
21
- [Servus] Users::Create::Service failed with NotFoundError: User not found (0.0125s)
22
- ```
23
-
24
- **Exceptions** (ERROR): Service class name, exception type, message, and duration
25
- ```
26
- [Servus] Users::Create::Service raised ArgumentError: Missing required field (0.0089s)
27
- ```
28
-
29
- ## Log Levels
30
-
31
- Servus uses Rails.logger and respects application log level configuration:
32
-
33
- - **DEBUG**: Shows arguments (use in development, hide in production to avoid logging sensitive data)
34
- - **INFO**: Shows completions (normal operations)
35
- - **WARN**: Shows business failures
36
- - **ERROR**: Shows system exceptions
37
-
38
- Set production log level to INFO to hide argument logging:
39
-
40
- ```ruby
41
- # config/environments/production.rb
42
- config.log_level = :info
43
- ```
44
-
45
- ## Sensitive Data
46
-
47
- Arguments are logged at DEBUG level. In production, either:
48
- 1. Set log level to INFO (recommended)
49
- 2. Use Rails parameter filtering: `config.filter_parameters += [:password, :ssn, :credit_card]`
50
- 3. Pass IDs instead of full objects: `Service.call(user_id: 1)` not `Service.call(user: user_object)`
51
-
52
- ## Integration with Logging Tools
53
-
54
- The `[Servus]` prefix makes service logs easy to grep and filter:
55
-
56
- ```bash
57
- # Find all service calls
58
- grep "\[Servus\]" production.log
59
-
60
- # Find slow services
61
- grep "completed" production.log | grep "Servus" | awk '{print $NF}' | sort -n
62
- ```
63
-
64
- Servus logs work with structured logging tools (Lograge, Datadog, Splunk) without modification.
@@ -1,244 +0,0 @@
1
- # @title Features / 5. Event Bus
2
-
3
- # Event Bus
4
-
5
- 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.
6
-
7
- ## Overview
8
-
9
- The Event Bus provides:
10
- - **Emitters**: Services declare events they emit on success/failure
11
- - **EventHandlers**: Subscribe to events and invoke services in response
12
- - **Event Bus**: Routes events to registered handlers via ActiveSupport::Notifications
13
- - **Payload Validation**: Optional JSON Schema validation for event payloads
14
-
15
- ## Service Event Emission
16
-
17
- Services can emit events when they succeed or fail using the `emits` DSL:
18
-
19
- ```ruby
20
- class CreateUser::Service < Servus::Base
21
- emits :user_created, on: :success
22
- emits :user_creation_failed, on: :failure
23
-
24
- def initialize(email:, name:)
25
- @email = email
26
- @name = name
27
- end
28
-
29
- def call
30
- user = User.create!(email: @email, name: @name)
31
- success(user: user)
32
- rescue ActiveRecord::RecordInvalid => e
33
- failure(e.message)
34
- end
35
- end
36
- ```
37
-
38
- ### Custom Payloads
39
-
40
- By default, success events receive `result.data` and failure events receive `result.error`. Customize with a block or method:
41
-
42
- ```ruby
43
- class CreateUser::Service < Servus::Base
44
- # Block-based payload
45
- emits :user_created, on: :success do |result|
46
- { user_id: result.data[:user].id, email: result.data[:user].email }
47
- end
48
-
49
- # Method-based payload
50
- emits :user_stats_updated, on: :success, with: :stats_payload
51
-
52
- private
53
-
54
- def stats_payload(result)
55
- { user_count: User.count, latest_user_id: result.data[:user].id }
56
- end
57
- end
58
- ```
59
-
60
- ### Trigger Types
61
-
62
- - `:success` - Fires when service returns `success(...)`
63
- - `:failure` - Fires when service returns `failure(...)`
64
- - `:error!` - Fires when service calls `error!(...)` (before exception is raised)
65
-
66
- ## Event Handlers
67
-
68
- EventHandlers live in `app/events/` and subscribe to events using a declarative DSL:
69
-
70
- ```ruby
71
- # app/events/user_created_handler.rb
72
- class UserCreatedHandler < Servus::EventHandler
73
- handles :user_created
74
-
75
- invoke SendWelcomeEmail::Service, async: true do |payload|
76
- { user_id: payload[:user_id], email: payload[:email] }
77
- end
78
-
79
- invoke TrackAnalytics::Service, async: true do |payload|
80
- { event: 'user_created', user_id: payload[:user_id] }
81
- end
82
- end
83
- ```
84
-
85
- ### Generator
86
-
87
- Generate handlers with the Rails generator:
88
-
89
- ```bash
90
- rails g servus:event_handler user_created
91
- # Creates:
92
- # app/events/user_created_handler.rb
93
- # spec/events/user_created_handler_spec.rb
94
- ```
95
-
96
- ### Invocation Options
97
-
98
- ```ruby
99
- class UserCreatedHandler < Servus::EventHandler
100
- handles :user_created
101
-
102
- # Synchronous invocation (default)
103
- invoke NotifyAdmin::Service do |payload|
104
- { message: "New user: #{payload[:email]}" }
105
- end
106
-
107
- # Async via ActiveJob
108
- invoke SendWelcomeEmail::Service, async: true do |payload|
109
- { user_id: payload[:user_id] }
110
- end
111
-
112
- # Async with specific queue
113
- invoke SendWelcomeEmail::Service, async: true, queue: :mailers do |payload|
114
- { user_id: payload[:user_id] }
115
- end
116
-
117
- # Conditional invocation
118
- invoke GrantPremiumRewards::Service, if: ->(p) { p[:premium] } do |payload|
119
- { user_id: payload[:user_id] }
120
- end
121
-
122
- invoke SkipForPremium::Service, unless: ->(p) { p[:premium] } do |payload|
123
- { user_id: payload[:user_id] }
124
- end
125
- end
126
- ```
127
-
128
- ## Emitting Events Directly
129
-
130
- EventHandlers provide an `emit` class method for emitting events from controllers, jobs, or other code without a service:
131
-
132
- ```ruby
133
- class UsersController < ApplicationController
134
- def create
135
- user = User.create!(user_params)
136
- UserCreatedHandler.emit({ user_id: user.id, email: user.email })
137
- redirect_to user
138
- end
139
- end
140
- ```
141
-
142
- This is useful when the event source isn't a Servus service.
143
-
144
- ## Payload Schema Validation
145
-
146
- Define JSON schemas to validate event payloads:
147
-
148
- ```ruby
149
- class UserCreatedHandler < Servus::EventHandler
150
- handles :user_created
151
-
152
- schema payload: {
153
- type: 'object',
154
- required: ['user_id', 'email'],
155
- properties: {
156
- user_id: { type: 'integer' },
157
- email: { type: 'string', format: 'email' }
158
- }
159
- }
160
-
161
- invoke SendWelcomeEmail::Service, async: true do |payload|
162
- { user_id: payload[:user_id], email: payload[:email] }
163
- end
164
- end
165
- ```
166
-
167
- When `emit` is called, the payload is validated against the schema before the event is dispatched.
168
-
169
- ## Handler Validation
170
-
171
- Enable strict validation to catch handlers subscribing to non-existent events:
172
-
173
- ```ruby
174
- # config/initializers/servus.rb
175
- Servus.configure do |config|
176
- config.strict_event_validation = true # Default: true
177
- end
178
-
179
- # Then manually validate (typically in a rake task or CI)
180
- Servus::EventHandler.validate_all_handlers!
181
- ```
182
-
183
- This helps catch typos and orphaned handlers during development and CI.
184
-
185
- ## Best Practices
186
-
187
- ### Single Event Per Service
188
-
189
- Services should emit one event per trigger representing their core concern:
190
-
191
- ```ruby
192
- # Good - one event, handler coordinates reactions
193
- class CreateUser::Service < Servus::Base
194
- emits :user_created, on: :success
195
- end
196
-
197
- class UserCreatedHandler < Servus::EventHandler
198
- handles :user_created
199
-
200
- invoke SendWelcomeEmail::Service, async: true
201
- invoke TrackAnalytics::Service, async: true
202
- invoke NotifySlack::Service, async: true
203
- end
204
-
205
- # Avoid - service doing too much coordination
206
- class CreateUser::Service < Servus::Base
207
- emits :send_welcome_email, on: :success
208
- emits :track_user_analytics, on: :success
209
- emits :notify_slack, on: :success
210
- end
211
- ```
212
-
213
- ### Naming Conventions
214
-
215
- - Events: Past tense describing what happened (`user_created`, `payment_processed`)
216
- - Handlers: Event name + "Handler" suffix (`UserCreatedHandler`)
217
-
218
- ### Handler Location
219
-
220
- Handlers live in `app/events/` and are auto-loaded by the Railtie:
221
-
222
- ```
223
- app/events/
224
- ├── user_created_handler.rb
225
- ├── payment_processed_handler.rb
226
- └── order_completed_handler.rb
227
- ```
228
-
229
- ## Instrumentation
230
-
231
- Events are instrumented via ActiveSupport::Notifications and appear in Rails logs:
232
-
233
- ```
234
- servus.events.user_created (1.2ms) {:user_id=>123, :email=>"user@example.com"}
235
- ```
236
-
237
- Subscribe to events programmatically:
238
-
239
- ```ruby
240
- ActiveSupport::Notifications.subscribe(/^servus\.events\./) do |name, *args|
241
- event_name = name.sub('servus.events.', '')
242
- Rails.logger.info "Event emitted: #{event_name}"
243
- end
244
- ```
@@ -1,356 +0,0 @@
1
- # @title Features / 6. Guards
2
-
3
- # Guards
4
-
5
- Guards are reusable validation rules that halt service execution when conditions aren't met. They provide a declarative way to enforce preconditions with rich, API-friendly error responses.
6
-
7
- ## Why Guards?
8
-
9
- Instead of scattering validation logic throughout services:
10
-
11
- ```ruby
12
- # Without guards - repetitive and verbose
13
- def call
14
- return failure("User required", type: ValidationError) unless user
15
- return failure("User must be active", type: ValidationError) unless user.active?
16
- # ... business logic ...
17
- end
18
- ```
19
-
20
- Use guards for clean, declarative validation:
21
-
22
- ```ruby
23
- # With guards - clear and reusable
24
- def call
25
- enforce_presence!(user: user)
26
- enforce_truthy!(on: user, check: :active)
27
- # ... business logic ...
28
- end
29
- ```
30
-
31
- ## Built-in Guards
32
-
33
- Servus includes four guards by default:
34
-
35
- ### PresenceGuard
36
-
37
- Validates that all values are present (not nil or empty):
38
-
39
- ```ruby
40
- # Single value
41
- enforce_presence!(user: user)
42
-
43
- # Multiple values - all must be present
44
- enforce_presence!(user: user, account: account, device: device)
45
-
46
- # Works with strings, arrays, hashes
47
- enforce_presence!(email: email) # fails if nil or ""
48
- enforce_presence!(items: cart.items) # fails if nil or []
49
- enforce_presence!(data: response.body) # fails if nil or {}
50
- ```
51
-
52
- Error: `"user must be present (got nil)"` or `"email must be present (got \"\")"`
53
-
54
- ### TruthyGuard
55
-
56
- Validates that attribute(s) on an object are truthy:
57
-
58
- ```ruby
59
- # Single attribute
60
- enforce_truthy!(on: user, check: :active)
61
-
62
- # Multiple attributes - all must be truthy
63
- enforce_truthy!(on: user, check: [:active, :verified, :confirmed])
64
-
65
- # Conditional check
66
- if check_truthy?(on: subscription, check: :valid?)
67
- process_subscription
68
- end
69
- ```
70
-
71
- Error: `"User.active must be truthy (got false)"`
72
-
73
- ### FalseyGuard
74
-
75
- Validates that attribute(s) on an object are falsey:
76
-
77
- ```ruby
78
- # Single attribute - user must not be banned
79
- enforce_falsey!(on: user, check: :banned)
80
-
81
- # Multiple attributes - all must be falsey
82
- enforce_falsey!(on: post, check: [:deleted, :hidden, :flagged])
83
-
84
- # Conditional check
85
- if check_falsey?(on: user, check: :suspended)
86
- allow_action
87
- end
88
- ```
89
-
90
- Error: `"User.banned must be falsey (got true)"`
91
-
92
- ### StateGuard
93
-
94
- Validates that an attribute matches an expected value or one of several allowed values:
95
-
96
- ```ruby
97
- # Single expected value
98
- enforce_state!(on: order, check: :status, is: :pending)
99
-
100
- # Multiple allowed values - any match passes
101
- enforce_state!(on: account, check: :status, is: [:active, :trial])
102
-
103
- # Conditional check
104
- if check_state?(on: order, check: :status, is: :shipped)
105
- send_tracking_email
106
- end
107
- ```
108
-
109
- Errors:
110
- - Single value: `"Order.status must be pending (got shipped)"`
111
- - Multiple values: `"Account.status must be one of active, trial (got suspended)"`
112
-
113
- ## Guard Methods
114
-
115
- Each guard defines two methods on `Servus::Guards`:
116
-
117
- - **Bang method (`!`)** - Throws on failure, halts execution
118
- - **Predicate method (`?`)** - Returns boolean, continues execution
119
-
120
- ```ruby
121
- # Bang method - use for preconditions that must pass
122
- enforce_presence!(user: user) # throws :guard_failure if nil
123
-
124
- # Predicate method - use for conditional logic
125
- if check_truthy?(on: account, check: :premium)
126
- apply_premium_discount
127
- else
128
- apply_standard_rate
129
- end
130
- ```
131
-
132
- ## Creating Custom Guards
133
-
134
- Define guards by inheriting from `Servus::Guard`:
135
-
136
- ```ruby
137
- # app/guards/sufficient_balance_guard.rb
138
- class SufficientBalanceGuard < Servus::Guard
139
- http_status 422
140
- error_code 'insufficient_balance'
141
-
142
- message 'Insufficient balance: need %<required>s, have %<available>s' do
143
- message_data
144
- end
145
-
146
- def test(account:, amount:)
147
- account.balance >= amount
148
- end
149
-
150
- private
151
-
152
- def message_data
153
- {
154
- required: kwargs[:amount],
155
- available: kwargs[:account].balance
156
- }
157
- end
158
- end
159
- ```
160
-
161
- This automatically defines `enforce_sufficient_balance!` and `check_sufficient_balance?` methods.
162
-
163
- ### Guard DSL
164
-
165
- **`http_status`** - HTTP status code for API responses (default: 422)
166
-
167
- ```ruby
168
- http_status 422 # Unprocessable Entity
169
- http_status 403 # Forbidden
170
- http_status 400 # Bad Request
171
- ```
172
-
173
- **`error_code`** - Machine-readable error code for API clients
174
-
175
- ```ruby
176
- error_code 'insufficient_balance'
177
- error_code 'daily_limit_exceeded'
178
- error_code 'account_locked'
179
- ```
180
-
181
- **`message`** - Human-readable error message with optional interpolation
182
-
183
- ```ruby
184
- # Static message
185
- message 'Amount must be positive'
186
-
187
- # With interpolation (uses Ruby's % formatting)
188
- message 'Balance: %<current>s, Required: %<required>s' do
189
- { current: account.balance, required: amount }
190
- end
191
- ```
192
-
193
- The message block has access to all kwargs passed to the guard via `kwargs`.
194
-
195
- **`test`** - The validation logic (must return boolean)
196
-
197
- ```ruby
198
- def test(account:, amount:)
199
- account.balance >= amount
200
- end
201
- ```
202
-
203
- ## Message Templates
204
-
205
- Guards support multiple message template formats:
206
-
207
- ### String with Interpolation
208
-
209
- ```ruby
210
- message 'Insufficient balance: need %<required>s, have %<available>s' do
211
- { required: amount, available: account.balance }
212
- end
213
- ```
214
-
215
- ### I18n Symbol
216
-
217
- ```ruby
218
- message :insufficient_balance
219
- # Looks up: I18n.t('guards.insufficient_balance')
220
- # Falls back to: "Insufficient balance" (humanized)
221
- ```
222
-
223
- ### Inline Translations
224
-
225
- ```ruby
226
- message(
227
- en: 'Insufficient balance',
228
- es: 'Saldo insuficiente',
229
- fr: 'Solde insuffisant'
230
- )
231
- ```
232
-
233
- ### Dynamic Proc
234
-
235
- ```ruby
236
- message -> { "Limit exceeded for #{limit_type} transfers" }
237
- ```
238
-
239
- ## Error Handling
240
-
241
- When a bang guard fails, it throws `:guard_failure` with a `GuardError`. Services automatically catch this and return a failure response:
242
-
243
- ```ruby
244
- class TransferService < Servus::Base
245
- def call
246
- enforce_state!(on: from_account, check: :status, is: :active)
247
- # If guard fails, execution stops here
248
- # Service returns: Response(success: false, error: GuardError)
249
-
250
- transfer_funds
251
- success(transfer: transfer)
252
- end
253
- end
254
- ```
255
-
256
- The `GuardError` includes all metadata:
257
-
258
- ```ruby
259
- error = guard.error
260
- error.message # "Account.status must be active (got suspended)"
261
- error.code # "invalid_state"
262
- error.http_status # 422
263
- ```
264
-
265
- ## Naming Convention
266
-
267
- Guard class names are converted to method names by stripping the `Guard` suffix and converting to snake_case:
268
-
269
- | Class Name | Bang Method | Predicate Method |
270
- |------------|-------------|------------------|
271
- | `SufficientBalanceGuard` | `enforce_sufficient_balance!` | `check_sufficient_balance?` |
272
- | `ValidAmountGuard` | `enforce_valid_amount!` | `check_valid_amount?` |
273
- | `AuthorizedGuard` | `enforce_authorized!` | `check_authorized?` |
274
-
275
- The built-in guards follow this pattern: `TruthyGuard` -> `enforce_truthy!` / `check_truthy?`.
276
-
277
- ## Rails Auto-Loading
278
-
279
- In Rails, guards in `app/guards/` are automatically loaded. Files must follow the `*_guard.rb` naming convention:
280
-
281
- ```
282
- app/guards/
283
- ├── sufficient_balance_guard.rb
284
- ├── valid_amount_guard.rb
285
- └── authorized_guard.rb
286
- ```
287
-
288
- ## Configuration
289
-
290
- Disable built-in guards if you want to define your own (you can have both):
291
-
292
- ```ruby
293
- Servus.configure do |config|
294
- config.include_default_guards = false # Default: true
295
- config.guards_dir = 'app/guards' # Default: 'app/guards'
296
- end
297
- ```
298
-
299
- ## Testing Guards
300
-
301
- Test guards in isolation:
302
-
303
- ```ruby
304
- RSpec.describe Servus::Guards::TruthyGuard do
305
- let(:user_class) do
306
- Struct.new(:active, :verified, keyword_init: true) do
307
- def self.name
308
- 'User'
309
- end
310
- end
311
- end
312
-
313
- describe '#test' do
314
- it 'passes when attribute is truthy' do
315
- user = user_class.new(active: true)
316
- guard = described_class.new(on: user, check: :active)
317
- expect(guard.test(on: user, check: :active)).to be true
318
- end
319
-
320
- it 'fails when attribute is falsey' do
321
- user = user_class.new(active: false)
322
- guard = described_class.new(on: user, check: :active)
323
- expect(guard.test(on: user, check: :active)).to be false
324
- end
325
- end
326
-
327
- describe '#error' do
328
- it 'returns GuardError with correct metadata' do
329
- user = user_class.new(active: false)
330
- guard = described_class.new(on: user, check: :active)
331
- error = guard.error
332
-
333
- expect(error.code).to eq('must_be_truthy')
334
- expect(error.message).to include('User', 'active', 'false')
335
- expect(error.http_status).to eq(422)
336
- end
337
- end
338
- end
339
- ```
340
-
341
- Test guards in service integration:
342
-
343
- ```ruby
344
- RSpec.describe TransferService do
345
- it 'fails when account is not active' do
346
- result = described_class.call(
347
- from_account: suspended_account,
348
- to_account: recipient,
349
- amount: 100
350
- )
351
-
352
- expect(result).to be_failure
353
- expect(result.error.code).to eq('invalid_state')
354
- end
355
- end
356
- ```