servus 0.1.3 → 0.1.5
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/commands/check-docs.md +1 -0
- data/.claude/commands/consistency-check.md +1 -0
- data/.claude/commands/fine-tooth-comb.md +1 -0
- data/.claude/commands/red-green-refactor.md +5 -0
- data/.claude/settings.json +15 -0
- data/.rubocop.yml +18 -2
- data/.yardopts +6 -0
- data/CHANGELOG.md +47 -0
- data/CLAUDE.md +10 -0
- data/IDEAS.md +5 -0
- data/READme.md +300 -47
- data/Rakefile +33 -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/core/1_overview.md +77 -0
- data/docs/core/2_architecture.md +120 -0
- data/docs/core/3_service_objects.md +121 -0
- data/docs/current_focus.md +569 -0
- data/docs/features/1_schema_validation.md +119 -0
- data/docs/features/2_error_handling.md +121 -0
- data/docs/features/3_async_execution.md +81 -0
- data/docs/features/4_logging.md +64 -0
- data/docs/features/5_event_bus.md +244 -0
- data/docs/guides/1_common_patterns.md +90 -0
- data/docs/guides/2_migration_guide.md +175 -0
- data/docs/integration/1_configuration.md +104 -0
- data/docs/integration/2_testing.md +287 -0
- data/docs/integration/3_rails_integration.md +99 -0
- data/docs/yard/Servus/Base.html +1645 -0
- data/docs/yard/Servus/Config.html +582 -0
- data/docs/yard/Servus/Extensions/Async/Call.html +400 -0
- data/docs/yard/Servus/Extensions/Async/Errors/AsyncError.html +140 -0
- data/docs/yard/Servus/Extensions/Async/Errors/JobEnqueueError.html +154 -0
- data/docs/yard/Servus/Extensions/Async/Errors/ServiceNotFoundError.html +154 -0
- data/docs/yard/Servus/Extensions/Async/Errors.html +128 -0
- data/docs/yard/Servus/Extensions/Async/Ext.html +119 -0
- data/docs/yard/Servus/Extensions/Async/Job.html +310 -0
- data/docs/yard/Servus/Extensions/Async.html +141 -0
- data/docs/yard/Servus/Extensions.html +117 -0
- data/docs/yard/Servus/Generators/ServiceGenerator.html +261 -0
- data/docs/yard/Servus/Generators.html +115 -0
- data/docs/yard/Servus/Helpers/ControllerHelpers.html +457 -0
- data/docs/yard/Servus/Helpers.html +115 -0
- data/docs/yard/Servus/Railtie.html +134 -0
- data/docs/yard/Servus/Support/Errors/AuthenticationError.html +287 -0
- data/docs/yard/Servus/Support/Errors/BadRequestError.html +283 -0
- data/docs/yard/Servus/Support/Errors/ForbiddenError.html +284 -0
- data/docs/yard/Servus/Support/Errors/InternalServerError.html +283 -0
- data/docs/yard/Servus/Support/Errors/NotFoundError.html +284 -0
- data/docs/yard/Servus/Support/Errors/ServiceError.html +489 -0
- data/docs/yard/Servus/Support/Errors/ServiceUnavailableError.html +290 -0
- data/docs/yard/Servus/Support/Errors/UnauthorizedError.html +200 -0
- data/docs/yard/Servus/Support/Errors/UnprocessableEntityError.html +288 -0
- data/docs/yard/Servus/Support/Errors/ValidationError.html +200 -0
- data/docs/yard/Servus/Support/Errors.html +140 -0
- data/docs/yard/Servus/Support/Logger.html +856 -0
- data/docs/yard/Servus/Support/Rescuer/BlockContext.html +585 -0
- data/docs/yard/Servus/Support/Rescuer/CallOverride.html +257 -0
- data/docs/yard/Servus/Support/Rescuer/ClassMethods.html +343 -0
- data/docs/yard/Servus/Support/Rescuer.html +267 -0
- data/docs/yard/Servus/Support/Response.html +574 -0
- data/docs/yard/Servus/Support/Validator.html +1150 -0
- data/docs/yard/Servus/Support.html +119 -0
- data/docs/yard/Servus/Testing/ExampleBuilders.html +523 -0
- data/docs/yard/Servus/Testing/ExampleExtractor.html +578 -0
- data/docs/yard/Servus/Testing.html +142 -0
- data/docs/yard/Servus.html +343 -0
- data/docs/yard/_index.html +535 -0
- data/docs/yard/class_list.html +54 -0
- data/docs/yard/css/common.css +1 -0
- data/docs/yard/css/full_list.css +58 -0
- data/docs/yard/css/style.css +503 -0
- data/docs/yard/file.1_common_patterns.html +154 -0
- data/docs/yard/file.1_configuration.html +115 -0
- data/docs/yard/file.1_overview.html +142 -0
- data/docs/yard/file.1_schema_validation.html +188 -0
- data/docs/yard/file.2_architecture.html +157 -0
- data/docs/yard/file.2_error_handling.html +190 -0
- data/docs/yard/file.2_migration_guide.html +242 -0
- data/docs/yard/file.2_testing.html +227 -0
- data/docs/yard/file.3_async_execution.html +145 -0
- data/docs/yard/file.3_rails_integration.html +160 -0
- data/docs/yard/file.3_service_objects.html +191 -0
- data/docs/yard/file.4_logging.html +135 -0
- data/docs/yard/file.ErrorHandling.html +190 -0
- data/docs/yard/file.READme.html +674 -0
- data/docs/yard/file.architecture.html +157 -0
- data/docs/yard/file.async_execution.html +145 -0
- data/docs/yard/file.common_patterns.html +154 -0
- data/docs/yard/file.configuration.html +115 -0
- data/docs/yard/file.error_handling.html +190 -0
- data/docs/yard/file.logging.html +135 -0
- data/docs/yard/file.migration_guide.html +242 -0
- data/docs/yard/file.overview.html +142 -0
- data/docs/yard/file.rails_integration.html +160 -0
- data/docs/yard/file.schema_validation.html +188 -0
- data/docs/yard/file.service_objects.html +191 -0
- data/docs/yard/file.testing.html +227 -0
- data/docs/yard/file_list.html +119 -0
- data/docs/yard/frames.html +22 -0
- data/docs/yard/index.html +674 -0
- data/docs/yard/js/app.js +344 -0
- data/docs/yard/js/full_list.js +242 -0
- data/docs/yard/js/jquery.js +4 -0
- data/docs/yard/method_list.html +542 -0
- data/docs/yard/top-level-namespace.html +110 -0
- data/lib/generators/servus/event_handler/event_handler_generator.rb +59 -0
- data/lib/generators/servus/event_handler/templates/handler.rb.erb +86 -0
- data/lib/generators/servus/event_handler/templates/handler_spec.rb.erb +48 -0
- data/lib/generators/servus/service/service_generator.rb +68 -1
- data/lib/generators/servus/service/templates/arguments.json.erb +19 -10
- data/lib/generators/servus/service/templates/result.json.erb +8 -2
- data/lib/generators/servus/service/templates/service.rb.erb +102 -5
- data/lib/generators/servus/service/templates/service_spec.rb.erb +67 -6
- data/lib/servus/base.rb +275 -58
- data/lib/servus/config.rb +83 -17
- data/lib/servus/event_handler.rb +275 -0
- data/lib/servus/events/bus.rb +137 -0
- data/lib/servus/events/emitter.rb +162 -0
- data/lib/servus/events/errors.rb +10 -0
- data/lib/servus/extensions/async/call.rb +50 -18
- data/lib/servus/extensions/async/errors.rb +23 -3
- data/lib/servus/extensions/async/ext.rb +10 -2
- data/lib/servus/extensions/async/job.rb +30 -9
- data/lib/servus/helpers/controller_helpers.rb +73 -37
- data/lib/servus/railtie.rb +16 -0
- data/lib/servus/support/errors.rb +135 -45
- data/lib/servus/support/rescuer.rb +189 -36
- data/lib/servus/support/response.rb +49 -7
- data/lib/servus/support/validator.rb +147 -19
- data/lib/servus/testing/example_builders.rb +133 -0
- data/lib/servus/testing/example_extractor.rb +309 -0
- data/lib/servus/testing/matchers.rb +88 -0
- data/lib/servus/testing.rb +19 -0
- data/lib/servus/version.rb +1 -1
- data/lib/servus.rb +6 -0
- metadata +135 -19
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f1094a06fd6195a99ef33f849a6ea30e6ec1a539eeb7ef73e7c27d1d4ac411bf
|
|
4
|
+
data.tar.gz: f6891749189216f6391396d79a29fd695fc3da5ff7647691318a90929781a90a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 02e57566af4b419281991b2b0a361010dd7eff9f7d061c226e32153292d60c060e58781099eee1113d8eab678859164a09be26b1ad790dea47ba0e7f2bfe4275
|
|
7
|
+
data.tar.gz: 2cfd4e9ae6f71bb051b61f2ea7b4315fef58289ffaf610f21669d6f6c98cd635479502a22af3819aa0a41eb3aed6b8de9972e74e8311a7e619fa09cf82c66ff9
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Scan the documentation in docs/ then check the current git changeset. If the changeset has modifications that might affect the documentation, then review the related documentation and make any necessary updates.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Are the latest changes consistent with the way that similar functionality has been implemented in the rest of the codebase? #$ARGUMENTS
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
We're going to go over #$ARGUMENTS with a fine-tooth comb. I want detailed explanations and no edits unless I ask for them. Start by identifying the first thing that looks wrong and explain why.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
During this session we will be practicing TDD in the red-green-refactor cycle. The first step in the cycle is to write a test for the smallest possible unit of functionality. Once the test is written, run the test and it should fail. This is the red phase. The next step is to write the code to make the test pass. This is the green phase. Once the test passes, the code is refactored to be more readable and maintainable. This is the refactor phase.
|
|
2
|
+
|
|
3
|
+
If we get into a situation where more than one test fails, we will focus on the test that is most important to fix first. During that fix phase, we will only run the test that is failing until it passes.
|
|
4
|
+
|
|
5
|
+
While we're in this mode, you will not use your TODO tool to track tasks. You will rely on me (the human user) to tell you what the next step is.
|
data/.rubocop.yml
CHANGED
|
@@ -1,11 +1,27 @@
|
|
|
1
1
|
AllCops:
|
|
2
|
+
NewCops: enable
|
|
3
|
+
TargetRubyVersion: 3.3
|
|
2
4
|
Include:
|
|
3
5
|
- 'lib/**/*.rb'
|
|
4
6
|
- 'spec/**/*.rb'
|
|
7
|
+
Exclude:
|
|
8
|
+
- 'spec/dummy/**/*'
|
|
9
|
+
- 'vendor/bundle/**/*'
|
|
10
|
+
|
|
11
|
+
Lint/ConstantDefinitionInBlock:
|
|
12
|
+
Enabled: false
|
|
13
|
+
|
|
14
|
+
Lint/ConstantReassignment:
|
|
15
|
+
Exclude:
|
|
16
|
+
- 'spec/**/*'
|
|
17
|
+
|
|
18
|
+
Lint/MissingSuper:
|
|
19
|
+
Exclude:
|
|
20
|
+
- 'spec/**/*'
|
|
5
21
|
|
|
6
22
|
Metrics/BlockLength:
|
|
7
23
|
Exclude:
|
|
8
24
|
- 'spec/**/*'
|
|
9
25
|
|
|
10
|
-
|
|
11
|
-
Enabled: false
|
|
26
|
+
Naming/PredicateMethod:
|
|
27
|
+
Enabled: false
|
data/.yardopts
ADDED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,52 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.1.5] - 2025-12-03
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Event Bus Architecture**: Introduced event-driven architecture for decoupling service logic from side effects
|
|
8
|
+
- `Servus::EventHandler` base class for creating event handlers that subscribe to events and invoke services
|
|
9
|
+
- `emits` DSL on `Servus::Base` for declaring events that fire on `:success`, `:failure`, or `:error!`
|
|
10
|
+
- `Servus::Events::Bus` for routing events to handlers via ActiveSupport::Notifications
|
|
11
|
+
- Rails generator: `rails g servus:event_handler event_name` creates handler and spec files
|
|
12
|
+
- Event handlers auto-load from `app/events/` directory in Rails applications
|
|
13
|
+
|
|
14
|
+
- **Event Payload Validation**: JSON Schema validation for event payloads
|
|
15
|
+
- `schema payload: {...}` DSL on EventHandler for declaring payload schemas
|
|
16
|
+
- Validation occurs when events are emitted via `EventHandler.emit(payload)`
|
|
17
|
+
|
|
18
|
+
- **Event Testing Matchers**: RSpec matchers for testing event emission
|
|
19
|
+
- `emit_event(:event_name)` matcher to assert events are emitted
|
|
20
|
+
- `emit_event(:event_name).with(payload)` for payload assertions
|
|
21
|
+
- `call_service(ServiceClass).with(args)` matcher for handler testing
|
|
22
|
+
- `call_service(ServiceClass).async` for async invocation testing
|
|
23
|
+
|
|
24
|
+
- **Configuration Options**: New and updated configuration settings
|
|
25
|
+
- `config.schemas_dir` - Directory for JSON schema files (default: `app/schemas`)
|
|
26
|
+
- `config.services_dir` - Directory for service files (default: `app/services`)
|
|
27
|
+
- `config.events_dir` - Directory for event handlers (default: `app/events`)
|
|
28
|
+
- `config.strict_event_validation` - Validate handlers subscribe to emitted events (default: `true`)
|
|
29
|
+
- `Servus::EventHandler.validate_all_handlers!` for CI validation of handler-event mappings
|
|
30
|
+
|
|
31
|
+
- **Generator Improvements**: Enhanced service and event handler generators
|
|
32
|
+
- Service templates now include comprehensive YARD documentation
|
|
33
|
+
- Service spec templates include example test patterns
|
|
34
|
+
- JSON schema templates include proper structure with `$schema` reference
|
|
35
|
+
- Event handler templates include full documentation and examples
|
|
36
|
+
- `--no-docs` flag to skip documentation comments in generated files
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- Updated execution flow to include event emission after result validation
|
|
41
|
+
- Enhanced Railtie to auto-load event handlers and clear the event bus on reload in development
|
|
42
|
+
|
|
43
|
+
## [0.1.4] - 2025-11-21
|
|
44
|
+
- Added: Test helpers (`servus_arguments_example` and `servus_result_example`) to extract example values from schemas for testing
|
|
45
|
+
- Added: YARD documentation configuration with README homepage and markdown file support
|
|
46
|
+
- Added: Added `schema` DSL method for cleaner schema definition. Supports `schema arguments: {...}, result: {...}` syntax. Fully backwards compatible with existing `ARGUMENTS_SCHEMA` and `RESULT_SCHEMA` constants.
|
|
47
|
+
- Added: Added support from blocks on `rescue_from` to override default failure handler.
|
|
48
|
+
- Fixed: YARD link resolution warnings in documentation
|
|
49
|
+
|
|
3
50
|
## [0.1.3] - 2025-10-10
|
|
4
51
|
- Added: Added `call_async` method to `Servus::Base` to enqueue a job for calling the service asynchronously
|
|
5
52
|
- Added: Added `Async::Job` to handle async enqueing with support for ActiveJob set options
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Before starting a session, always review the latest docs in the following order:
|
|
2
|
+
|
|
3
|
+
1. `/docs/core/**/*.md`
|
|
4
|
+
2. `/docs/features/**/*.md`
|
|
5
|
+
3. `/docs/guides/**/*.md`
|
|
6
|
+
4. `/docs/integration/**/*.md`
|
|
7
|
+
|
|
8
|
+
Focus on writing code consistent with the rest of the project. Use existing files as references for conventions and style.
|
|
9
|
+
|
|
10
|
+
Ensure new code always encludes world class YARD documentation. If documentation looks out of date or incomplete, suggest a relevant edit.
|
data/IDEAS.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
1. Make sure Async Jobs don't retry when the job class cant be constantized.
|
|
2
|
+
|
|
3
|
+
2. Improve error handling with an error registry that can be referenced by codes as opposed to fully qualified class names.
|
|
4
|
+
|
|
5
|
+
3. Update generators to not make schema files and instead add schemas to schema: method in generators.
|
data/READme.md
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
## Servus Gem
|
|
2
2
|
|
|
3
|
+
|
|
3
4
|
Servus is a gem for creating and managing service objects. It includes:
|
|
4
5
|
|
|
5
6
|
- A base class for service objects
|
|
@@ -7,8 +8,9 @@ Servus is a gem for creating and managing service objects. It includes:
|
|
|
7
8
|
- Support for schema validation
|
|
8
9
|
- Support for error handling
|
|
9
10
|
- Support for logging
|
|
11
|
+
- Event-driven architecture with EventHandlers
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
👉🏽 [View the docs](https://zarpay.github.io/servus/)
|
|
12
14
|
|
|
13
15
|
## Generators
|
|
14
16
|
|
|
@@ -119,10 +121,6 @@ end
|
|
|
119
121
|
|
|
120
122
|
```
|
|
121
123
|
|
|
122
|
-
Here’s a section you can add to your README for the new `.call_async` feature, matching the style of your existing `## Inheritance` section:
|
|
123
|
-
|
|
124
|
-
---
|
|
125
|
-
|
|
126
124
|
## **Asynchronous Execution**
|
|
127
125
|
|
|
128
126
|
You can asynchronously execute any service class that inherits from `Servus::Base` using `.call_async`. This uses `ActiveJob` under the hood and supports standard job options (`wait`, `queue`, `priority`, etc.). Only available in environments where `ActiveJob` is loaded (e.g., Rails apps)
|
|
@@ -171,6 +169,7 @@ Always use the class method `call` instead of manual instantiation. The `call
|
|
|
171
169
|
2. Calls the instance-level `call` method
|
|
172
170
|
3. Handles schema validation of inputs and outputs
|
|
173
171
|
4. Handles logging of inputs and results
|
|
172
|
+
5. Automatically benchmarks execution time for performance monitoring
|
|
174
173
|
|
|
175
174
|
```ruby
|
|
176
175
|
# Good ✅
|
|
@@ -246,25 +245,25 @@ class SomeServiceObject::Service < Servus::Base
|
|
|
246
245
|
def call
|
|
247
246
|
# Return default ServiceError with custom message
|
|
248
247
|
failure("That didn't work for some reason")
|
|
249
|
-
#=> Response(false, nil,
|
|
248
|
+
#=> Response(false, nil, Servus::Support::Errors::ServiceError("That didn't work for some reason"))
|
|
250
249
|
#
|
|
251
250
|
# OR
|
|
252
251
|
#
|
|
253
252
|
# Specify ServiceError type with custom message
|
|
254
253
|
failure("Custom message", type: Servus::Support::Errors::NotFoundError)
|
|
255
|
-
#=> Response(false, nil,
|
|
254
|
+
#=> Response(false, nil, Servus::Support::Errors::NotFoundError("Custom message"))
|
|
256
255
|
#
|
|
257
256
|
# OR
|
|
258
257
|
#
|
|
259
258
|
# Specify ServiceError type with default message
|
|
260
259
|
failure(type: Servus::Support::Errors::NotFoundError)
|
|
261
|
-
#=> Response(false, nil,
|
|
260
|
+
#=> Response(false, nil, Servus::Support::Errors::NotFoundError("Not found"))
|
|
262
261
|
#
|
|
263
262
|
# OR
|
|
264
263
|
#
|
|
265
264
|
# Accept all defaults
|
|
266
265
|
failure
|
|
267
|
-
#=> Response(false, nil,
|
|
266
|
+
#=> Response(false, nil, Servus::Support::Errors::ServiceError("An error occurred"))
|
|
268
267
|
end
|
|
269
268
|
end
|
|
270
269
|
|
|
@@ -329,6 +328,28 @@ The `rescue_from` method will rescue from the specified errors and use the speci
|
|
|
329
328
|
the custom error. It helps eliminate the need to manually rescue many errors and create failure responses within the call method of
|
|
330
329
|
a service object.
|
|
331
330
|
|
|
331
|
+
You can also provide a block for custom error handling:
|
|
332
|
+
|
|
333
|
+
```ruby
|
|
334
|
+
class SomeServiceObject::Service < Servus::Base
|
|
335
|
+
# Custom error handling with a block
|
|
336
|
+
rescue_from ActiveRecord::RecordInvalid do |exception|
|
|
337
|
+
failure("Validation failed: #{exception.message}", type: ValidationError)
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
rescue_from Net::HTTPError do |exception|
|
|
341
|
+
# Can even return success to recover from errors
|
|
342
|
+
success(recovered: true, error_message: exception.message)
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def call
|
|
346
|
+
# Service logic
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
The block receives the exception and has access to `success` and `failure` methods for creating the response.
|
|
352
|
+
|
|
332
353
|
## Controller Helpers
|
|
333
354
|
|
|
334
355
|
Service objects can be called from controllers using the `run_service` and `render_service_object_error` helpers.
|
|
@@ -437,43 +458,59 @@ Example `result.json`:
|
|
|
437
458
|
|
|
438
459
|
### 2. Inline Schema Validation
|
|
439
460
|
|
|
440
|
-
|
|
461
|
+
Schemas can be declared directly within the service class using the `schema` DSL method:
|
|
441
462
|
|
|
442
463
|
```ruby
|
|
443
464
|
class Services::ProcessPayment::Service < Servus::Base
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
465
|
+
schema(
|
|
466
|
+
arguments: {
|
|
467
|
+
type: "object",
|
|
468
|
+
required: ["user_id", "amount", "payment_method"],
|
|
469
|
+
properties: {
|
|
470
|
+
user_id: { type: "integer" },
|
|
471
|
+
amount: {
|
|
472
|
+
type: "integer",
|
|
473
|
+
minimum: 1
|
|
474
|
+
},
|
|
475
|
+
payment_method: {
|
|
476
|
+
type: "string",
|
|
477
|
+
enum: ["credit_card", "paypal", "bank_transfer"]
|
|
478
|
+
},
|
|
479
|
+
currency: {
|
|
480
|
+
type: "string",
|
|
481
|
+
default: "USD"
|
|
482
|
+
}
|
|
483
|
+
},
|
|
484
|
+
additionalProperties: false
|
|
485
|
+
},
|
|
486
|
+
result: {
|
|
487
|
+
type: "object",
|
|
488
|
+
required: ["transaction_id", "status"],
|
|
489
|
+
properties: {
|
|
490
|
+
transaction_id: { type: "string" },
|
|
491
|
+
status: {
|
|
492
|
+
type: "string",
|
|
493
|
+
enum: ["approved", "pending", "declined"]
|
|
494
|
+
},
|
|
495
|
+
receipt_url: { type: "string" }
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
def initialize(user_id:, amount:, payment_method:, currency: 'USD')
|
|
501
|
+
@user_id = user_id
|
|
502
|
+
@amount = amount
|
|
503
|
+
@payment_method = payment_method
|
|
504
|
+
@currency = currency
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
def call
|
|
508
|
+
# Service logic...
|
|
509
|
+
success({
|
|
510
|
+
transaction_id: "txn_1",
|
|
511
|
+
status: "approved"
|
|
512
|
+
})
|
|
513
|
+
end
|
|
477
514
|
end
|
|
478
515
|
```
|
|
479
516
|
|
|
@@ -485,9 +522,10 @@ These schemas use JSON Schema format to enforce type safety and input/output con
|
|
|
485
522
|
|
|
486
523
|
The validation system follows this precedence:
|
|
487
524
|
|
|
488
|
-
1.
|
|
489
|
-
2.
|
|
490
|
-
3.
|
|
525
|
+
1. Schemas defined via `schema` DSL method (recommended)
|
|
526
|
+
2. Inline schema constants (`ARGUMENTS_SCHEMA` or `RESULT_SCHEMA`) - legacy support
|
|
527
|
+
3. JSON files in schema_root directory - legacy support
|
|
528
|
+
4. Returns nil if no schema is found (validation is opt-in)
|
|
491
529
|
|
|
492
530
|
### Schema Caching
|
|
493
531
|
|
|
@@ -495,4 +533,219 @@ Both file-based and inline schemas are automatically cached:
|
|
|
495
533
|
|
|
496
534
|
- First validation request loads and caches the schema
|
|
497
535
|
- Subsequent validations use the cached version
|
|
498
|
-
- Cache can be cleared using `
|
|
536
|
+
- Cache can be cleared using `Servus::Support::Validator.clear_cache!`
|
|
537
|
+
|
|
538
|
+
## **Logging**
|
|
539
|
+
|
|
540
|
+
Servus automatically logs service execution details, making it easy to track and debug service calls.
|
|
541
|
+
|
|
542
|
+
### Automatic Logging
|
|
543
|
+
|
|
544
|
+
Every service call automatically logs:
|
|
545
|
+
|
|
546
|
+
- **Service invocation** with input arguments
|
|
547
|
+
- **Success results** with execution duration
|
|
548
|
+
- **Failure results** with error details and duration
|
|
549
|
+
- **Validation errors** for schema violations
|
|
550
|
+
- **Uncaught exceptions** with error messages
|
|
551
|
+
|
|
552
|
+
### Logger Configuration
|
|
553
|
+
|
|
554
|
+
The logger automatically adapts to your environment:
|
|
555
|
+
|
|
556
|
+
- **Rails applications**: Uses `Rails.logger`
|
|
557
|
+
- **Non-Rails applications**: Uses stdout logger
|
|
558
|
+
|
|
559
|
+
### Log Output Examples
|
|
560
|
+
|
|
561
|
+
```ruby
|
|
562
|
+
# Success
|
|
563
|
+
INFO -- : Calling Services::ProcessPayment::Service with args: {:user_id=>123, :amount=>50}
|
|
564
|
+
INFO -- : Services::ProcessPayment::Service succeeded in 0.245s
|
|
565
|
+
|
|
566
|
+
# Failure
|
|
567
|
+
INFO -- : Calling Services::ProcessPayment::Service with args: {:user_id=>123, :amount=>50}
|
|
568
|
+
WARN -- : Services::ProcessPayment::Service failed in 0.156s with error: Insufficient funds
|
|
569
|
+
|
|
570
|
+
# Validation Error
|
|
571
|
+
ERROR -- : Services::ProcessPayment::Service validation error: The property '#/amount' value -10 was less than minimum value 1
|
|
572
|
+
|
|
573
|
+
# Exception
|
|
574
|
+
ERROR -- : Services::ProcessPayment::Service uncaught exception: NoMethodError - undefined method 'charge' for nil:NilClass
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
All logging happens transparently when using the class-level `.call` method. This is one of the reasons why direct instantiation (bypassing `.call`) is discouraged.
|
|
578
|
+
|
|
579
|
+
## **Configuration**
|
|
580
|
+
|
|
581
|
+
Servus can be configured to customize behavior for your application needs.
|
|
582
|
+
|
|
583
|
+
### Schema Root Directory
|
|
584
|
+
|
|
585
|
+
By default, Servus looks for schema files in `app/schemas/services/`. You can customize this location:
|
|
586
|
+
|
|
587
|
+
```ruby
|
|
588
|
+
# config/initializers/servus.rb
|
|
589
|
+
Servus.configure do |config|
|
|
590
|
+
config.schema_root = Rails.root.join('lib/schemas')
|
|
591
|
+
end
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
### Default Behavior
|
|
595
|
+
|
|
596
|
+
Without explicit configuration:
|
|
597
|
+
|
|
598
|
+
- **Rails applications**: Schema root defaults to `Rails.root/app/schemas/services`
|
|
599
|
+
- **Non-Rails applications**: Schema root defaults to `./app/schemas/services` relative to the gem installation
|
|
600
|
+
|
|
601
|
+
The configuration is accessed through the singleton `Servus.config` instance and can be modified using `Servus.configure`.
|
|
602
|
+
|
|
603
|
+
## **Event Bus**
|
|
604
|
+
|
|
605
|
+
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.
|
|
606
|
+
|
|
607
|
+
### Emitting Events from Services
|
|
608
|
+
|
|
609
|
+
Services can declare events that are emitted on success or failure:
|
|
610
|
+
|
|
611
|
+
```ruby
|
|
612
|
+
class CreateUser::Service < Servus::Base
|
|
613
|
+
emits :user_created, on: :success
|
|
614
|
+
emits :user_creation_failed, on: :failure
|
|
615
|
+
|
|
616
|
+
def initialize(email:, name:)
|
|
617
|
+
@email = email
|
|
618
|
+
@name = name
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
def call
|
|
622
|
+
user = User.create!(email: @email, name: @name)
|
|
623
|
+
success(user: user)
|
|
624
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
625
|
+
failure(e.message)
|
|
626
|
+
end
|
|
627
|
+
end
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
Custom payloads can be provided via blocks or method references:
|
|
631
|
+
|
|
632
|
+
```ruby
|
|
633
|
+
emits :user_created, on: :success do |result|
|
|
634
|
+
{ user_id: result.data[:user].id, email: result.data[:user].email }
|
|
635
|
+
end
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
### Event Handlers
|
|
639
|
+
|
|
640
|
+
EventHandlers subscribe to events and invoke services in response. They live in `app/events/`:
|
|
641
|
+
|
|
642
|
+
```ruby
|
|
643
|
+
# app/events/user_created_handler.rb
|
|
644
|
+
class UserCreatedHandler < Servus::EventHandler
|
|
645
|
+
handles :user_created
|
|
646
|
+
|
|
647
|
+
invoke SendWelcomeEmail::Service, async: true do |payload|
|
|
648
|
+
{ user_id: payload[:user_id], email: payload[:email] }
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
invoke TrackAnalytics::Service, async: true do |payload|
|
|
652
|
+
{ event: 'user_created', user_id: payload[:user_id] }
|
|
653
|
+
end
|
|
654
|
+
end
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
### Generate Event Handler
|
|
658
|
+
|
|
659
|
+
```bash
|
|
660
|
+
$ rails g servus:event_handler user_created
|
|
661
|
+
=> create app/events/user_created_handler.rb
|
|
662
|
+
create spec/events/user_created_handler_spec.rb
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
### Invocation Options
|
|
666
|
+
|
|
667
|
+
```ruby
|
|
668
|
+
# Synchronous (default)
|
|
669
|
+
invoke NotifyAdmin::Service do |payload|
|
|
670
|
+
{ message: "New user: #{payload[:email]}" }
|
|
671
|
+
end
|
|
672
|
+
|
|
673
|
+
# Async via ActiveJob
|
|
674
|
+
invoke SendEmail::Service, async: true do |payload|
|
|
675
|
+
{ user_id: payload[:user_id] }
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
# Async with specific queue
|
|
679
|
+
invoke SendEmail::Service, async: true, queue: :mailers do |payload|
|
|
680
|
+
{ user_id: payload[:user_id] }
|
|
681
|
+
end
|
|
682
|
+
|
|
683
|
+
# Conditional invocation
|
|
684
|
+
invoke GrantRewards::Service, if: ->(p) { p[:premium] } do |payload|
|
|
685
|
+
{ user_id: payload[:user_id] }
|
|
686
|
+
end
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
### Emitting Events Directly
|
|
690
|
+
|
|
691
|
+
EventHandlers provide an `emit` class method for emitting events from controllers, jobs, or other code:
|
|
692
|
+
|
|
693
|
+
```ruby
|
|
694
|
+
class UsersController < ApplicationController
|
|
695
|
+
def create
|
|
696
|
+
user = User.create!(user_params)
|
|
697
|
+
UserCreatedHandler.emit({ user_id: user.id, email: user.email })
|
|
698
|
+
redirect_to user
|
|
699
|
+
end
|
|
700
|
+
end
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
### Payload Schema Validation
|
|
704
|
+
|
|
705
|
+
Define JSON schemas to validate event payloads:
|
|
706
|
+
|
|
707
|
+
```ruby
|
|
708
|
+
class UserCreatedHandler < Servus::EventHandler
|
|
709
|
+
handles :user_created
|
|
710
|
+
|
|
711
|
+
schema payload: {
|
|
712
|
+
type: 'object',
|
|
713
|
+
required: ['user_id', 'email'],
|
|
714
|
+
properties: {
|
|
715
|
+
user_id: { type: 'integer' },
|
|
716
|
+
email: { type: 'string', format: 'email' }
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
invoke SendWelcomeEmail::Service, async: true do |payload|
|
|
721
|
+
{ user_id: payload[:user_id], email: payload[:email] }
|
|
722
|
+
end
|
|
723
|
+
end
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
### Testing Events
|
|
727
|
+
|
|
728
|
+
Servus provides RSpec matchers for testing events:
|
|
729
|
+
|
|
730
|
+
```ruby
|
|
731
|
+
# Test that a service emits an event
|
|
732
|
+
it 'emits user_created event' do
|
|
733
|
+
expect {
|
|
734
|
+
CreateUser::Service.call(email: 'test@example.com', name: 'Test')
|
|
735
|
+
}.to emit_event(:user_created)
|
|
736
|
+
end
|
|
737
|
+
|
|
738
|
+
# Test payload content
|
|
739
|
+
it 'emits event with expected payload' do
|
|
740
|
+
expect {
|
|
741
|
+
CreateUser::Service.call(email: 'test@example.com', name: 'Test')
|
|
742
|
+
}.to emit_event(:user_created).with(hash_including(email: 'test@example.com'))
|
|
743
|
+
end
|
|
744
|
+
|
|
745
|
+
# Test handler invokes service
|
|
746
|
+
it 'invokes SendWelcomeEmail' do
|
|
747
|
+
expect {
|
|
748
|
+
UserCreatedHandler.handle(payload)
|
|
749
|
+
}.to call_service(SendWelcomeEmail::Service).with(user_id: 123)
|
|
750
|
+
end
|
|
751
|
+
```
|
data/Rakefile
CHANGED
|
@@ -1,12 +1,45 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'fileutils'
|
|
3
4
|
require 'bundler/gem_tasks'
|
|
4
5
|
require 'rspec/core/rake_task'
|
|
5
6
|
|
|
7
|
+
# Constants
|
|
8
|
+
GEM_NAME = 'servus'
|
|
9
|
+
BUILDS_DIR = 'builds'
|
|
10
|
+
|
|
6
11
|
RSpec::Core::RakeTask.new(:spec)
|
|
7
12
|
|
|
8
13
|
require 'rubocop/rake_task'
|
|
9
14
|
|
|
10
15
|
RuboCop::RakeTask.new
|
|
11
16
|
|
|
17
|
+
# Build gem
|
|
18
|
+
task :build do
|
|
19
|
+
FileUtils.mkdir_p(BUILDS_DIR)
|
|
20
|
+
|
|
21
|
+
# Build gem in current directory
|
|
22
|
+
sh "gem build #{GEM_NAME}.gemspec"
|
|
23
|
+
|
|
24
|
+
# Move to builds directory
|
|
25
|
+
gem_file = Dir["#{GEM_NAME}-*.gem"].first
|
|
26
|
+
if gem_file
|
|
27
|
+
FileUtils.mv(gem_file, BUILDS_DIR)
|
|
28
|
+
puts "Moved #{gem_file} to #{BUILDS_DIR}/"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Install gem locally
|
|
33
|
+
task install: :build do
|
|
34
|
+
gem_file = Dir["#{BUILDS_DIR}/#{GEM_NAME}-*.gem"].max_by { |f| File.mtime(f) }
|
|
35
|
+
sh "gem install #{gem_file}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Publish gem to RubyGems.org
|
|
39
|
+
task publish: :build do
|
|
40
|
+
gem_file = Dir["#{BUILDS_DIR}/#{GEM_NAME}-*.gem"].max_by { |f| File.mtime(f) }
|
|
41
|
+
puts "Publishing #{gem_file} to RubyGems.org..."
|
|
42
|
+
sh "gem push #{gem_file}"
|
|
43
|
+
end
|
|
44
|
+
|
|
12
45
|
task default: %i[spec rubocop]
|
|
Binary file
|
|
Binary file
|
|
Binary file
|