servus 0.3.0 → 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 (132) 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 +46 -3
  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/guard.rb +7 -6
  11. data/lib/servus/guards/falsey_guard.rb +3 -3
  12. data/lib/servus/guards/presence_guard.rb +4 -4
  13. data/lib/servus/guards/state_guard.rb +4 -5
  14. data/lib/servus/guards/truthy_guard.rb +3 -3
  15. data/lib/servus/helpers/controller_helpers.rb +40 -0
  16. data/lib/servus/support/errors.rb +16 -0
  17. data/lib/servus/support/lockdown.rb +94 -0
  18. data/lib/servus/support/logger.rb +16 -0
  19. data/lib/servus/support/validator.rb +65 -34
  20. data/lib/servus/testing/example_builders.rb +52 -0
  21. data/lib/servus/testing/matchers.rb +99 -0
  22. data/lib/servus/version.rb +1 -1
  23. data/lib/servus.rb +1 -0
  24. metadata +7 -111
  25. data/.claude/commands/check-docs.md +0 -1
  26. data/.claude/commands/consistency-check.md +0 -1
  27. data/.claude/commands/fine-tooth-comb.md +0 -1
  28. data/.claude/commands/red-green-refactor.md +0 -5
  29. data/.claude/settings.json +0 -24
  30. data/.rspec +0 -3
  31. data/.rubocop.yml +0 -27
  32. data/.yardopts +0 -6
  33. data/CHANGELOG.md +0 -169
  34. data/CLAUDE.md +0 -10
  35. data/IDEAS.md +0 -5
  36. data/LICENSE.txt +0 -21
  37. data/READme.md +0 -856
  38. data/Rakefile +0 -45
  39. data/docs/core/1_overview.md +0 -81
  40. data/docs/core/2_architecture.md +0 -120
  41. data/docs/core/3_service_objects.md +0 -154
  42. data/docs/features/1_schema_validation.md +0 -161
  43. data/docs/features/2_error_handling.md +0 -129
  44. data/docs/features/3_async_execution.md +0 -81
  45. data/docs/features/4_logging.md +0 -64
  46. data/docs/features/5_event_bus.md +0 -244
  47. data/docs/features/6_guards.md +0 -356
  48. data/docs/features/7_lazy_resolvers.md +0 -238
  49. data/docs/features/guards_naming_convention.md +0 -540
  50. data/docs/guides/1_common_patterns.md +0 -90
  51. data/docs/guides/2_migration_guide.md +0 -225
  52. data/docs/integration/1_configuration.md +0 -154
  53. data/docs/integration/2_testing.md +0 -304
  54. data/docs/integration/3_rails_integration.md +0 -99
  55. data/docs/yard/Servus/Base.html +0 -1645
  56. data/docs/yard/Servus/Config.html +0 -582
  57. data/docs/yard/Servus/Extensions/Async/Call.html +0 -400
  58. data/docs/yard/Servus/Extensions/Async/Errors/AsyncError.html +0 -140
  59. data/docs/yard/Servus/Extensions/Async/Errors/JobEnqueueError.html +0 -154
  60. data/docs/yard/Servus/Extensions/Async/Errors/ServiceNotFoundError.html +0 -154
  61. data/docs/yard/Servus/Extensions/Async/Errors.html +0 -128
  62. data/docs/yard/Servus/Extensions/Async/Ext.html +0 -119
  63. data/docs/yard/Servus/Extensions/Async/Job.html +0 -310
  64. data/docs/yard/Servus/Extensions/Async.html +0 -141
  65. data/docs/yard/Servus/Extensions.html +0 -117
  66. data/docs/yard/Servus/Generators/ServiceGenerator.html +0 -261
  67. data/docs/yard/Servus/Generators.html +0 -115
  68. data/docs/yard/Servus/Helpers/ControllerHelpers.html +0 -457
  69. data/docs/yard/Servus/Helpers.html +0 -115
  70. data/docs/yard/Servus/Railtie.html +0 -134
  71. data/docs/yard/Servus/Support/Errors/AuthenticationError.html +0 -287
  72. data/docs/yard/Servus/Support/Errors/BadRequestError.html +0 -283
  73. data/docs/yard/Servus/Support/Errors/ForbiddenError.html +0 -284
  74. data/docs/yard/Servus/Support/Errors/InternalServerError.html +0 -283
  75. data/docs/yard/Servus/Support/Errors/NotFoundError.html +0 -284
  76. data/docs/yard/Servus/Support/Errors/ServiceError.html +0 -489
  77. data/docs/yard/Servus/Support/Errors/ServiceUnavailableError.html +0 -290
  78. data/docs/yard/Servus/Support/Errors/UnauthorizedError.html +0 -200
  79. data/docs/yard/Servus/Support/Errors/UnprocessableEntityError.html +0 -288
  80. data/docs/yard/Servus/Support/Errors/ValidationError.html +0 -200
  81. data/docs/yard/Servus/Support/Errors.html +0 -140
  82. data/docs/yard/Servus/Support/Logger.html +0 -856
  83. data/docs/yard/Servus/Support/Rescuer/BlockContext.html +0 -585
  84. data/docs/yard/Servus/Support/Rescuer/CallOverride.html +0 -257
  85. data/docs/yard/Servus/Support/Rescuer/ClassMethods.html +0 -343
  86. data/docs/yard/Servus/Support/Rescuer.html +0 -267
  87. data/docs/yard/Servus/Support/Response.html +0 -574
  88. data/docs/yard/Servus/Support/Validator.html +0 -1150
  89. data/docs/yard/Servus/Support.html +0 -119
  90. data/docs/yard/Servus/Testing/ExampleBuilders.html +0 -523
  91. data/docs/yard/Servus/Testing/ExampleExtractor.html +0 -578
  92. data/docs/yard/Servus/Testing.html +0 -142
  93. data/docs/yard/Servus.html +0 -343
  94. data/docs/yard/_index.html +0 -535
  95. data/docs/yard/class_list.html +0 -54
  96. data/docs/yard/css/common.css +0 -1
  97. data/docs/yard/css/full_list.css +0 -58
  98. data/docs/yard/css/style.css +0 -503
  99. data/docs/yard/file.1_common_patterns.html +0 -154
  100. data/docs/yard/file.1_configuration.html +0 -115
  101. data/docs/yard/file.1_overview.html +0 -142
  102. data/docs/yard/file.1_schema_validation.html +0 -188
  103. data/docs/yard/file.2_architecture.html +0 -157
  104. data/docs/yard/file.2_error_handling.html +0 -190
  105. data/docs/yard/file.2_migration_guide.html +0 -242
  106. data/docs/yard/file.2_testing.html +0 -227
  107. data/docs/yard/file.3_async_execution.html +0 -145
  108. data/docs/yard/file.3_rails_integration.html +0 -160
  109. data/docs/yard/file.3_service_objects.html +0 -191
  110. data/docs/yard/file.4_logging.html +0 -135
  111. data/docs/yard/file.ErrorHandling.html +0 -190
  112. data/docs/yard/file.READme.html +0 -674
  113. data/docs/yard/file.architecture.html +0 -157
  114. data/docs/yard/file.async_execution.html +0 -145
  115. data/docs/yard/file.common_patterns.html +0 -154
  116. data/docs/yard/file.configuration.html +0 -115
  117. data/docs/yard/file.error_handling.html +0 -190
  118. data/docs/yard/file.logging.html +0 -135
  119. data/docs/yard/file.migration_guide.html +0 -242
  120. data/docs/yard/file.overview.html +0 -142
  121. data/docs/yard/file.rails_integration.html +0 -160
  122. data/docs/yard/file.schema_validation.html +0 -188
  123. data/docs/yard/file.service_objects.html +0 -191
  124. data/docs/yard/file.testing.html +0 -227
  125. data/docs/yard/file_list.html +0 -119
  126. data/docs/yard/frames.html +0 -22
  127. data/docs/yard/index.html +0 -674
  128. data/docs/yard/js/app.js +0 -344
  129. data/docs/yard/js/full_list.js +0 -242
  130. data/docs/yard/js/jquery.js +0 -4
  131. data/docs/yard/method_list.html +0 -542
  132. data/docs/yard/top-level-namespace.html +0 -110
@@ -1,81 +0,0 @@
1
- # @title Features / 3. Async Execution
2
-
3
- # Async Execution
4
-
5
- Servus provides asynchronous execution via ActiveJob. Services run identically whether called sync or async - they're unaware of execution context.
6
-
7
- ## Usage
8
-
9
- Call `.call_async(**args)` instead of `.call(**args)` to execute in the background. The service is enqueued immediately and executed by a worker.
10
-
11
- ```ruby
12
- # Synchronous
13
- result = ProcessReport::Service.call(user_id: user.id, report_type: :monthly)
14
- result.data[:report] # Available immediately
15
-
16
- # Asynchronous
17
- ProcessReport::Service.call_async(user_id: user.id, report_type: :monthly)
18
- # Returns true if enqueued successfully
19
- # Result not available (service hasn't run yet)
20
- ```
21
-
22
- Services must accept JSON-serializable arguments for async execution (primitives, hashes, arrays, ActiveRecord objects via GlobalID). Complex objects like Procs won't work.
23
-
24
- ## Queue and Scheduling Options
25
-
26
- Pass ActiveJob options to control execution:
27
-
28
- ```ruby
29
- ProcessReport::Service.call_async(
30
- user_id: user.id,
31
- queue: :critical, # Specify queue
32
- priority: 10, # Higher priority
33
- wait: 5.minutes # Delay execution
34
- )
35
- ```
36
-
37
- ## Result Handling
38
-
39
- Async services can't return results to callers (the service hasn't executed yet). If you need results, implement persistence in the service:
40
-
41
- ```ruby
42
- class GenerateReport::Service < Servus::Base
43
- def call
44
- report_data = generate_report
45
-
46
- # Persist result
47
- Report.create!(
48
- user_id: @user_id,
49
- data: report_data,
50
- status: 'completed'
51
- )
52
-
53
- # Optionally notify user
54
- UserMailer.report_ready(@user_id).deliver_now
55
-
56
- success(data: report_data)
57
- end
58
- end
59
-
60
- # Controller creates placeholder, service updates it
61
- report = Report.create!(user_id: user.id, status: 'pending')
62
- GenerateReport::Service.call_async(user_id: user.id, report_id: report.id)
63
- ```
64
-
65
- ## Error Handling
66
-
67
- Failures (business logic) don't trigger retries - the job completes successfully but returns a failure Response.
68
-
69
- Exceptions (system errors) trigger ActiveJob retry logic. Use `rescue_from` to convert transient errors into exceptions:
70
-
71
- ```ruby
72
- class Service < Servus::Base
73
- rescue_from Net::HTTPError, Timeout::Error use: ServiceUnavailableError
74
- end
75
- ```
76
-
77
- ## When to Use Async
78
-
79
- **Good candidates**: Email sending, report generation, data imports, long-running API calls, cleanup tasks
80
-
81
- **Poor candidates**: Operations requiring immediate feedback, fast operations (<100ms), critical path operations where user waits for result
@@ -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
- ```