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.
@@ -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, and event handlers:
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 handler auto-loading. Schemas defined via the `schema` DSL method do not use files.
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