servus 0.1.5 → 0.2.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.
- checksums.yaml +4 -4
- data/.claude/settings.json +9 -0
- data/CHANGELOG.md +45 -1
- data/READme.md +120 -15
- data/docs/features/6_guards.md +356 -0
- data/docs/features/guards_naming_convention.md +540 -0
- data/docs/integration/1_configuration.md +52 -2
- data/lib/generators/servus/guard/guard_generator.rb +75 -0
- data/lib/generators/servus/guard/templates/guard.rb.erb +69 -0
- data/lib/generators/servus/guard/templates/guard_spec.rb.erb +65 -0
- data/lib/servus/base.rb +9 -1
- data/lib/servus/config.rb +17 -2
- data/lib/servus/events/emitter.rb +3 -3
- data/lib/servus/guard.rb +289 -0
- data/lib/servus/guards/falsey_guard.rb +59 -0
- data/lib/servus/guards/presence_guard.rb +80 -0
- data/lib/servus/guards/state_guard.rb +62 -0
- data/lib/servus/guards/truthy_guard.rb +61 -0
- data/lib/servus/guards.rb +48 -0
- data/lib/servus/helpers/controller_helpers.rb +20 -48
- data/lib/servus/railtie.rb +11 -3
- data/lib/servus/support/errors.rb +69 -140
- data/lib/servus/support/message_resolver.rb +166 -0
- data/lib/servus/version.rb +1 -1
- data/lib/servus.rb +5 -0
- metadata +13 -8
- data/builds/servus-0.0.1.gem +0 -0
- data/builds/servus-0.1.1.gem +0 -0
- data/builds/servus-0.1.2.gem +0 -0
- data/builds/servus-0.1.3.gem +0 -0
- data/builds/servus-0.1.4.gem +0 -0
- data/builds/servus-0.1.5.gem +0 -0
- data/docs/current_focus.md +0 -569
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
# Servus Guards: Final Naming Convention
|
|
2
|
+
|
|
3
|
+
## The Pattern
|
|
4
|
+
|
|
5
|
+
### Class Naming: `<Condition>Guard`
|
|
6
|
+
|
|
7
|
+
**Rule:** Guard class names should describe **what is being checked**, NOT the action.
|
|
8
|
+
|
|
9
|
+
- ✅ **DO** use nouns, adjectives, or states
|
|
10
|
+
- ❌ **DON'T** use action verbs like "Enforce", "Check", "Verify", "Require", "Assert"
|
|
11
|
+
|
|
12
|
+
### Generated Methods
|
|
13
|
+
|
|
14
|
+
The framework automatically generates two methods:
|
|
15
|
+
|
|
16
|
+
1. **Bang method:** `enforce_<condition>!` - Enforces the rule or throws
|
|
17
|
+
2. **Predicate method:** `check_<condition>?` - Checks if the rule is met
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## ✅ Good Examples
|
|
22
|
+
|
|
23
|
+
### Example 1: Balance Check
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
# ✅ GOOD - Describes the condition
|
|
27
|
+
class SufficientBalanceGuard < Servus::Guard
|
|
28
|
+
message 'Insufficient balance: need %<required>s, have %<available>s' do
|
|
29
|
+
{ required: amount, available: account.balance }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def test(account:, amount:)
|
|
33
|
+
account.balance >= amount
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Generates:
|
|
38
|
+
enforce_sufficient_balance!(account: account, amount: amount)
|
|
39
|
+
check_sufficient_balance?(account: account, amount: amount)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**Why it's good:** "SufficientBalance" describes the state/condition being checked.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
### Example 2: Presence Check
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
# ✅ GOOD - Describes the condition
|
|
50
|
+
class PresenceGuard < Servus::Guard
|
|
51
|
+
message '%<keys>s must be present' do
|
|
52
|
+
{ keys: kwargs.keys.join(', ') }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def test(**values)
|
|
56
|
+
values.values.all?(&:present?)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Generates:
|
|
61
|
+
enforce_presence!(user: user, account: account)
|
|
62
|
+
check_presence?(user: user, account: account)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Alternative naming:**
|
|
66
|
+
```ruby
|
|
67
|
+
# Also good
|
|
68
|
+
class NotNilGuard < Servus::Guard
|
|
69
|
+
# ...
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Generates:
|
|
73
|
+
enforce_not_nil!(user: user)
|
|
74
|
+
check_not_nil?(user: user)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
### Example 3: Authorization
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
# ✅ GOOD - Describes the requirement
|
|
83
|
+
class AdminRoleGuard < Servus::Guard
|
|
84
|
+
message 'User must have admin role' do
|
|
85
|
+
{}
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def test(user:)
|
|
89
|
+
user.admin?
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Generates:
|
|
94
|
+
enforce_admin_role!(user: user)
|
|
95
|
+
check_admin_role?(user: user)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
### Example 4: Product Feature
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
# ✅ GOOD - Describes the enabled state
|
|
104
|
+
class EnabledProductGuard < Servus::Guard
|
|
105
|
+
message 'Product %<product_name>s is not enabled' do
|
|
106
|
+
{ product_name: product.name }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def test(product:)
|
|
110
|
+
product.enabled?
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Generates:
|
|
115
|
+
enforce_enabled_product!(product: product)
|
|
116
|
+
check_enabled_product?(product: product)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
### Example 5: Age Requirement
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
# ✅ GOOD - Describes the requirement
|
|
125
|
+
class MinimumAgeGuard < Servus::Guard
|
|
126
|
+
message 'Must be at least %<minimum>s years old' do
|
|
127
|
+
{ minimum: 18 }
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def test(date_of_birth:)
|
|
131
|
+
age = ((Time.zone.now - date_of_birth.to_time) / 1.year.seconds).floor
|
|
132
|
+
age >= 18
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Generates:
|
|
137
|
+
enforce_minimum_age!(date_of_birth: user.date_of_birth)
|
|
138
|
+
check_minimum_age?(date_of_birth: user.date_of_birth)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
### Example 6: Rate Limiting
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
# ✅ GOOD - Describes the limit state
|
|
147
|
+
class DailyLimitRemainingGuard < Servus::Guard
|
|
148
|
+
message 'Daily limit exceeded: %<used>s/%<limit>s' do
|
|
149
|
+
{
|
|
150
|
+
used: user.daily_api_calls,
|
|
151
|
+
limit: user.daily_api_limit
|
|
152
|
+
}
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def test(user:)
|
|
156
|
+
user.daily_api_calls < user.daily_api_limit
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Generates:
|
|
161
|
+
enforce_daily_limit_remaining!(user: user)
|
|
162
|
+
check_daily_limit_remaining?(user: user)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
### Example 7: Ownership
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
# ✅ GOOD - Describes the relationship
|
|
171
|
+
class OwnershipGuard < Servus::Guard
|
|
172
|
+
message 'User does not own this resource' do
|
|
173
|
+
{}
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def test(user:, resource:)
|
|
177
|
+
resource.user_id == user.id
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Generates:
|
|
182
|
+
enforce_ownership!(user: user, resource: account)
|
|
183
|
+
check_ownership?(user: user, resource: account)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## ❌ Bad Examples (DON'T DO THIS)
|
|
189
|
+
|
|
190
|
+
### Example 1: Using "Enforce" in Class Name
|
|
191
|
+
|
|
192
|
+
```ruby
|
|
193
|
+
# ❌ BAD - Uses action verb
|
|
194
|
+
class EnforceSufficientBalanceGuard < Servus::Guard
|
|
195
|
+
# ...
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Generates (redundant!):
|
|
199
|
+
enforce_enforce_sufficient_balance!(...) # ❌ Redundant!
|
|
200
|
+
check_enforce_sufficient_balance?(...) # ❌ Doesn't make sense!
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
**Why it's bad:** The action verb "Enforce" is already added by the framework.
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
### Example 2: Using "Check" in Class Name
|
|
208
|
+
|
|
209
|
+
```ruby
|
|
210
|
+
# ❌ BAD - Uses action verb
|
|
211
|
+
class CheckPresenceGuard < Servus::Guard
|
|
212
|
+
# ...
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Generates (redundant!):
|
|
216
|
+
enforce_check_presence!(...) # ❌ Weird!
|
|
217
|
+
check_check_presence?(...) # ❌ Redundant!
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
**Why it's bad:** The action verb "Check" is already added by the framework.
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
### Example 3: Using "Require" in Class Name
|
|
225
|
+
|
|
226
|
+
```ruby
|
|
227
|
+
# ❌ BAD - Uses action verb
|
|
228
|
+
class RequireAdminRoleGuard < Servus::Guard
|
|
229
|
+
# ...
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Generates (awkward!):
|
|
233
|
+
enforce_require_admin_role!(...) # ❌ Double action verbs!
|
|
234
|
+
check_require_admin_role?(...) # ❌ Confusing!
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**Why it's bad:** "Require" is an action verb that conflicts with the framework's verbs.
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
### Example 4: Using "Verify" in Class Name
|
|
242
|
+
|
|
243
|
+
```ruby
|
|
244
|
+
# ❌ BAD - Uses action verb
|
|
245
|
+
class VerifyOwnershipGuard < Servus::Guard
|
|
246
|
+
# ...
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Generates (awkward!):
|
|
250
|
+
enforce_verify_ownership!(...) # ❌ Double action verbs!
|
|
251
|
+
check_verify_ownership?(...) # ❌ Confusing!
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
**Why it's bad:** "Verify" is an action verb that conflicts with the framework.
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
### Example 5: Using "Validate" in Class Name
|
|
259
|
+
|
|
260
|
+
```ruby
|
|
261
|
+
# ❌ BAD - Uses action verb
|
|
262
|
+
class ValidateEmailGuard < Servus::Guard
|
|
263
|
+
# ...
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Generates (awkward!):
|
|
267
|
+
enforce_validate_email!(...) # ❌ Double action verbs!
|
|
268
|
+
check_validate_email?(...) # ❌ Confusing!
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
**Why it's bad:** "Validate" is an action verb.
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## 📝 Naming Guidelines
|
|
276
|
+
|
|
277
|
+
### DO: Use Descriptive Conditions
|
|
278
|
+
|
|
279
|
+
**Pattern:** `<Adjective><Noun>Guard` or `<Noun>Guard`
|
|
280
|
+
|
|
281
|
+
Examples:
|
|
282
|
+
- `SufficientBalanceGuard` - adjective + noun
|
|
283
|
+
- `PresenceGuard` - noun
|
|
284
|
+
- `AdminRoleGuard` - noun
|
|
285
|
+
- `EnabledProductGuard` - adjective + noun
|
|
286
|
+
- `MinimumAgeGuard` - adjective + noun
|
|
287
|
+
- `ActiveDeviceGuard` - adjective + noun
|
|
288
|
+
- `ValidEmailGuard` - adjective + noun
|
|
289
|
+
- `PositiveAmountGuard` - adjective + noun
|
|
290
|
+
- `UniqueEmailGuard` - adjective + noun
|
|
291
|
+
|
|
292
|
+
### DON'T: Use Action Verbs
|
|
293
|
+
|
|
294
|
+
**Avoid these prefixes:**
|
|
295
|
+
- ❌ `Enforce...Guard`
|
|
296
|
+
- ❌ `Check...Guard`
|
|
297
|
+
- ❌ `Verify...Guard`
|
|
298
|
+
- ❌ `Require...Guard`
|
|
299
|
+
- ❌ `Assert...Guard`
|
|
300
|
+
- ❌ `Validate...Guard`
|
|
301
|
+
- ❌ `Ensure...Guard`
|
|
302
|
+
- ❌ `Demand...Guard`
|
|
303
|
+
- ❌ `Test...Guard`
|
|
304
|
+
|
|
305
|
+
**Why:** The framework automatically adds `enforce_` and `check_` prefixes to the generated methods.
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
## 🎯 Naming Tips
|
|
310
|
+
|
|
311
|
+
### Tip 1: Think About the Condition, Not the Action
|
|
312
|
+
|
|
313
|
+
**Ask yourself:** "What state or condition am I checking?"
|
|
314
|
+
|
|
315
|
+
- ✅ "Is the balance sufficient?" → `SufficientBalanceGuard`
|
|
316
|
+
- ✅ "Is the user present?" → `PresenceGuard`
|
|
317
|
+
- ✅ "Does the user have admin role?" → `AdminRoleGuard`
|
|
318
|
+
- ✅ "Is the product enabled?" → `EnabledProductGuard`
|
|
319
|
+
|
|
320
|
+
**Don't ask:** "What action am I taking?"
|
|
321
|
+
|
|
322
|
+
- ❌ "I'm enforcing balance" → `EnforceBalanceGuard` (wrong!)
|
|
323
|
+
- ❌ "I'm checking presence" → `CheckPresenceGuard` (wrong!)
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
### Tip 2: Use Adjectives for States
|
|
328
|
+
|
|
329
|
+
When checking if something is in a certain state, use an adjective:
|
|
330
|
+
|
|
331
|
+
- `ActiveDeviceGuard` - device is active
|
|
332
|
+
- `ValidEmailGuard` - email is valid
|
|
333
|
+
- `PositiveAmountGuard` - amount is positive
|
|
334
|
+
- `UniqueEmailGuard` - email is unique
|
|
335
|
+
- `EnabledProductGuard` - product is enabled
|
|
336
|
+
|
|
337
|
+
---
|
|
338
|
+
|
|
339
|
+
### Tip 3: Use Nouns for Existence/Presence
|
|
340
|
+
|
|
341
|
+
When checking if something exists or is present:
|
|
342
|
+
|
|
343
|
+
- `PresenceGuard` - checks presence
|
|
344
|
+
- `OwnershipGuard` - checks ownership
|
|
345
|
+
- `AdminRoleGuard` - checks for admin role
|
|
346
|
+
- `PermissionGuard` - checks for permission
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
### Tip 4: Describe Requirements Positively
|
|
351
|
+
|
|
352
|
+
Prefer positive descriptions over negative:
|
|
353
|
+
|
|
354
|
+
- ✅ `SufficientBalanceGuard` (positive)
|
|
355
|
+
- ⚠️ `InsufficientBalanceGuard` (negative - works but less clear)
|
|
356
|
+
|
|
357
|
+
- ✅ `ActiveDeviceGuard` (positive)
|
|
358
|
+
- ⚠️ `InactiveDeviceGuard` (negative)
|
|
359
|
+
|
|
360
|
+
- ✅ `ValidEmailGuard` (positive)
|
|
361
|
+
- ⚠️ `InvalidEmailGuard` (negative)
|
|
362
|
+
|
|
363
|
+
**Exception:** Sometimes negative is clearer:
|
|
364
|
+
|
|
365
|
+
- `NotNilGuard` - clear and concise
|
|
366
|
+
- `NotEmptyGuard` - clear what it checks
|
|
367
|
+
|
|
368
|
+
---
|
|
369
|
+
|
|
370
|
+
## 📚 Complete Examples Library
|
|
371
|
+
|
|
372
|
+
### Simple Validations
|
|
373
|
+
|
|
374
|
+
```ruby
|
|
375
|
+
class PresenceGuard < Servus::Guard
|
|
376
|
+
# enforce_presence! / check_presence?
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
class NotNilGuard < Servus::Guard
|
|
380
|
+
# enforce_not_nil! / check_not_nil?
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
class PositiveAmountGuard < Servus::Guard
|
|
384
|
+
# enforce_positive_amount! / check_positive_amount?
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
class ValidEmailGuard < Servus::Guard
|
|
388
|
+
# enforce_valid_email! / check_valid_email?
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
class UniqueEmailGuard < Servus::Guard
|
|
392
|
+
# enforce_unique_email! / check_unique_email?
|
|
393
|
+
end
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
### Business Rules
|
|
397
|
+
|
|
398
|
+
```ruby
|
|
399
|
+
class SufficientBalanceGuard < Servus::Guard
|
|
400
|
+
# enforce_sufficient_balance! / check_sufficient_balance?
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
class DailyLimitRemainingGuard < Servus::Guard
|
|
404
|
+
# enforce_daily_limit_remaining! / check_daily_limit_remaining?
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
class MinimumPurchaseAmountGuard < Servus::Guard
|
|
408
|
+
# enforce_minimum_purchase_amount! / check_minimum_purchase_amount?
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
class WithinTransferLimitGuard < Servus::Guard
|
|
412
|
+
# enforce_within_transfer_limit! / check_within_transfer_limit?
|
|
413
|
+
end
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
### Authorization
|
|
417
|
+
|
|
418
|
+
```ruby
|
|
419
|
+
class AdminRoleGuard < Servus::Guard
|
|
420
|
+
# enforce_admin_role! / check_admin_role?
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
class OwnershipGuard < Servus::Guard
|
|
424
|
+
# enforce_ownership! / check_ownership?
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
class PermissionGuard < Servus::Guard
|
|
428
|
+
# enforce_permission! / check_permission?
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
class ActiveMembershipGuard < Servus::Guard
|
|
432
|
+
# enforce_active_membership! / check_active_membership?
|
|
433
|
+
end
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
### Resource States
|
|
437
|
+
|
|
438
|
+
```ruby
|
|
439
|
+
class ActiveDeviceGuard < Servus::Guard
|
|
440
|
+
# enforce_active_device! / check_active_device?
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
class EnabledProductGuard < Servus::Guard
|
|
444
|
+
# enforce_enabled_product! / check_enabled_product?
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
class AvailableInventoryGuard < Servus::Guard
|
|
448
|
+
# enforce_available_inventory! / check_available_inventory?
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
class OpenAccountGuard < Servus::Guard
|
|
452
|
+
# enforce_open_account! / check_open_account?
|
|
453
|
+
end
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
### Compliance
|
|
457
|
+
|
|
458
|
+
```ruby
|
|
459
|
+
class MinimumAgeGuard < Servus::Guard
|
|
460
|
+
# enforce_minimum_age! / check_minimum_age?
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
class CompletedKYCGuard < Servus::Guard
|
|
464
|
+
# enforce_completed_kyc! / check_completed_kyc?
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
class AcceptedTermsGuard < Servus::Guard
|
|
468
|
+
# enforce_accepted_terms! / check_accepted_terms?
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
class VerifiedEmailGuard < Servus::Guard
|
|
472
|
+
# enforce_verified_email! / check_verified_email?
|
|
473
|
+
end
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
---
|
|
477
|
+
|
|
478
|
+
## 🔄 Migration Guide
|
|
479
|
+
|
|
480
|
+
If you have existing guards with action verbs, here's how to rename them:
|
|
481
|
+
|
|
482
|
+
### Before (with action verbs)
|
|
483
|
+
```ruby
|
|
484
|
+
class EnforceSufficientBalanceGuard < Servus::Guard
|
|
485
|
+
# ...
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
# Usage:
|
|
489
|
+
enforce_enforce_sufficient_balance!(...) # Redundant!
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
### After (condition only)
|
|
493
|
+
```ruby
|
|
494
|
+
class SufficientBalanceGuard < Servus::Guard
|
|
495
|
+
# ...
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
# Usage:
|
|
499
|
+
enforce_sufficient_balance!(...) # Clean!
|
|
500
|
+
check_sufficient_balance?(...) # Clear!
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
### Rename Mapping
|
|
504
|
+
|
|
505
|
+
| Old Name (❌) | New Name (✅) |
|
|
506
|
+
|--------------|--------------|
|
|
507
|
+
| `EnforceSufficientBalanceGuard` | `SufficientBalanceGuard` |
|
|
508
|
+
| `RequirePresenceGuard` | `PresenceGuard` |
|
|
509
|
+
| `CheckAdminRoleGuard` | `AdminRoleGuard` |
|
|
510
|
+
| `VerifyOwnershipGuard` | `OwnershipGuard` |
|
|
511
|
+
| `AssertPositiveGuard` | `PositiveAmountGuard` |
|
|
512
|
+
| `EnsureValidEmailGuard` | `ValidEmailGuard` |
|
|
513
|
+
| `DemandActiveDeviceGuard` | `ActiveDeviceGuard` |
|
|
514
|
+
|
|
515
|
+
---
|
|
516
|
+
|
|
517
|
+
## ✅ Summary
|
|
518
|
+
|
|
519
|
+
**The Golden Rule:**
|
|
520
|
+
|
|
521
|
+
> Guard class names describe **WHAT** is being checked, not **HOW** it's being checked.
|
|
522
|
+
|
|
523
|
+
**Pattern:**
|
|
524
|
+
```ruby
|
|
525
|
+
class <Condition>Guard < Servus::Guard
|
|
526
|
+
# Describes the condition/state/requirement
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
# Framework generates:
|
|
530
|
+
enforce_<condition>! # Action verb added by framework
|
|
531
|
+
check_<condition>? # Action verb added by framework
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
**Examples:**
|
|
535
|
+
- `SufficientBalanceGuard` → `enforce_sufficient_balance!` / `check_sufficient_balance?`
|
|
536
|
+
- `PresenceGuard` → `enforce_presence!` / `check_presence?`
|
|
537
|
+
- `AdminRoleGuard` → `enforce_admin_role!` / `check_admin_role?`
|
|
538
|
+
- `EnabledProductGuard` → `enforce_enabled_product!` / `check_enabled_product?`
|
|
539
|
+
|
|
540
|
+
**Remember:** Let the framework add the action verbs. Your job is to describe the condition! 🎯
|
|
@@ -6,7 +6,7 @@ Servus works without configuration. Optional settings exist for customizing dire
|
|
|
6
6
|
|
|
7
7
|
## Directory Configuration
|
|
8
8
|
|
|
9
|
-
Configure where Servus looks for schemas, services,
|
|
9
|
+
Configure where Servus looks for schemas, services, event handlers, and guards:
|
|
10
10
|
|
|
11
11
|
```ruby
|
|
12
12
|
# config/initializers/servus.rb
|
|
@@ -19,10 +19,13 @@ Servus.configure do |config|
|
|
|
19
19
|
|
|
20
20
|
# Default: 'app/events'
|
|
21
21
|
config.events_dir = 'app/events'
|
|
22
|
+
|
|
23
|
+
# Default: 'app/guards'
|
|
24
|
+
config.guards_dir = 'app/guards'
|
|
22
25
|
end
|
|
23
26
|
```
|
|
24
27
|
|
|
25
|
-
These affect legacy file-based schemas and
|
|
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.
|
|
26
29
|
|
|
27
30
|
## Schema Cache
|
|
28
31
|
|
|
@@ -55,6 +58,53 @@ config.active_job.default_queue_name = :default
|
|
|
55
58
|
|
|
56
59
|
Servus respects ActiveJob queue configuration - no Servus-specific setup needed.
|
|
57
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
|
+
|
|
58
108
|
## Event Bus Configuration
|
|
59
109
|
|
|
60
110
|
### Strict Event Validation
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Servus
|
|
4
|
+
module Generators
|
|
5
|
+
# Rails generator for creating Servus guards.
|
|
6
|
+
#
|
|
7
|
+
# Generates a guard class and spec file.
|
|
8
|
+
#
|
|
9
|
+
# @example Generate a guard
|
|
10
|
+
# rails g servus:guard sufficient_balance
|
|
11
|
+
#
|
|
12
|
+
# @example Generated files
|
|
13
|
+
# app/guards/sufficient_balance_guard.rb
|
|
14
|
+
# spec/guards/sufficient_balance_guard_spec.rb
|
|
15
|
+
#
|
|
16
|
+
# @see https://guides.rubyonrails.org/generators.html
|
|
17
|
+
class GuardGenerator < Rails::Generators::NamedBase
|
|
18
|
+
source_root File.expand_path('templates', __dir__)
|
|
19
|
+
|
|
20
|
+
class_option :no_docs, type: :boolean,
|
|
21
|
+
default: false,
|
|
22
|
+
desc: 'Skip documentation comments in generated files'
|
|
23
|
+
|
|
24
|
+
# Creates the guard and spec files.
|
|
25
|
+
#
|
|
26
|
+
# @return [void]
|
|
27
|
+
def create_guard_file
|
|
28
|
+
template 'guard.rb.erb', guard_path
|
|
29
|
+
template 'guard_spec.rb.erb', guard_spec_path
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
# Returns the path for the guard file.
|
|
35
|
+
#
|
|
36
|
+
# @return [String] guard file path
|
|
37
|
+
# @api private
|
|
38
|
+
def guard_path
|
|
39
|
+
File.join(Servus.config.guards_dir, "#{file_name}_guard.rb")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Returns the path for the guard spec file.
|
|
43
|
+
#
|
|
44
|
+
# @return [String] spec file path
|
|
45
|
+
# @api private
|
|
46
|
+
def guard_spec_path
|
|
47
|
+
File.join('spec', Servus.config.guards_dir, "#{file_name}_guard_spec.rb")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Returns the guard class name.
|
|
51
|
+
#
|
|
52
|
+
# @return [String] guard class name
|
|
53
|
+
# @api private
|
|
54
|
+
def guard_class_name
|
|
55
|
+
"#{class_name}Guard"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Returns the enforce method name.
|
|
59
|
+
#
|
|
60
|
+
# @return [String] enforce method name
|
|
61
|
+
# @api private
|
|
62
|
+
def enforce_method_name
|
|
63
|
+
"enforce_#{file_name}!"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Returns the check method name.
|
|
67
|
+
#
|
|
68
|
+
# @return [String] check method name
|
|
69
|
+
# @api private
|
|
70
|
+
def check_method_name
|
|
71
|
+
"check_#{file_name}?"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|