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.
Files changed (139) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/commands/check-docs.md +1 -0
  3. data/.claude/commands/consistency-check.md +1 -0
  4. data/.claude/commands/fine-tooth-comb.md +1 -0
  5. data/.claude/commands/red-green-refactor.md +5 -0
  6. data/.claude/settings.json +15 -0
  7. data/.rubocop.yml +18 -2
  8. data/.yardopts +6 -0
  9. data/CHANGELOG.md +47 -0
  10. data/CLAUDE.md +10 -0
  11. data/IDEAS.md +5 -0
  12. data/READme.md +300 -47
  13. data/Rakefile +33 -0
  14. data/builds/servus-0.1.3.gem +0 -0
  15. data/builds/servus-0.1.4.gem +0 -0
  16. data/builds/servus-0.1.5.gem +0 -0
  17. data/docs/core/1_overview.md +77 -0
  18. data/docs/core/2_architecture.md +120 -0
  19. data/docs/core/3_service_objects.md +121 -0
  20. data/docs/current_focus.md +569 -0
  21. data/docs/features/1_schema_validation.md +119 -0
  22. data/docs/features/2_error_handling.md +121 -0
  23. data/docs/features/3_async_execution.md +81 -0
  24. data/docs/features/4_logging.md +64 -0
  25. data/docs/features/5_event_bus.md +244 -0
  26. data/docs/guides/1_common_patterns.md +90 -0
  27. data/docs/guides/2_migration_guide.md +175 -0
  28. data/docs/integration/1_configuration.md +104 -0
  29. data/docs/integration/2_testing.md +287 -0
  30. data/docs/integration/3_rails_integration.md +99 -0
  31. data/docs/yard/Servus/Base.html +1645 -0
  32. data/docs/yard/Servus/Config.html +582 -0
  33. data/docs/yard/Servus/Extensions/Async/Call.html +400 -0
  34. data/docs/yard/Servus/Extensions/Async/Errors/AsyncError.html +140 -0
  35. data/docs/yard/Servus/Extensions/Async/Errors/JobEnqueueError.html +154 -0
  36. data/docs/yard/Servus/Extensions/Async/Errors/ServiceNotFoundError.html +154 -0
  37. data/docs/yard/Servus/Extensions/Async/Errors.html +128 -0
  38. data/docs/yard/Servus/Extensions/Async/Ext.html +119 -0
  39. data/docs/yard/Servus/Extensions/Async/Job.html +310 -0
  40. data/docs/yard/Servus/Extensions/Async.html +141 -0
  41. data/docs/yard/Servus/Extensions.html +117 -0
  42. data/docs/yard/Servus/Generators/ServiceGenerator.html +261 -0
  43. data/docs/yard/Servus/Generators.html +115 -0
  44. data/docs/yard/Servus/Helpers/ControllerHelpers.html +457 -0
  45. data/docs/yard/Servus/Helpers.html +115 -0
  46. data/docs/yard/Servus/Railtie.html +134 -0
  47. data/docs/yard/Servus/Support/Errors/AuthenticationError.html +287 -0
  48. data/docs/yard/Servus/Support/Errors/BadRequestError.html +283 -0
  49. data/docs/yard/Servus/Support/Errors/ForbiddenError.html +284 -0
  50. data/docs/yard/Servus/Support/Errors/InternalServerError.html +283 -0
  51. data/docs/yard/Servus/Support/Errors/NotFoundError.html +284 -0
  52. data/docs/yard/Servus/Support/Errors/ServiceError.html +489 -0
  53. data/docs/yard/Servus/Support/Errors/ServiceUnavailableError.html +290 -0
  54. data/docs/yard/Servus/Support/Errors/UnauthorizedError.html +200 -0
  55. data/docs/yard/Servus/Support/Errors/UnprocessableEntityError.html +288 -0
  56. data/docs/yard/Servus/Support/Errors/ValidationError.html +200 -0
  57. data/docs/yard/Servus/Support/Errors.html +140 -0
  58. data/docs/yard/Servus/Support/Logger.html +856 -0
  59. data/docs/yard/Servus/Support/Rescuer/BlockContext.html +585 -0
  60. data/docs/yard/Servus/Support/Rescuer/CallOverride.html +257 -0
  61. data/docs/yard/Servus/Support/Rescuer/ClassMethods.html +343 -0
  62. data/docs/yard/Servus/Support/Rescuer.html +267 -0
  63. data/docs/yard/Servus/Support/Response.html +574 -0
  64. data/docs/yard/Servus/Support/Validator.html +1150 -0
  65. data/docs/yard/Servus/Support.html +119 -0
  66. data/docs/yard/Servus/Testing/ExampleBuilders.html +523 -0
  67. data/docs/yard/Servus/Testing/ExampleExtractor.html +578 -0
  68. data/docs/yard/Servus/Testing.html +142 -0
  69. data/docs/yard/Servus.html +343 -0
  70. data/docs/yard/_index.html +535 -0
  71. data/docs/yard/class_list.html +54 -0
  72. data/docs/yard/css/common.css +1 -0
  73. data/docs/yard/css/full_list.css +58 -0
  74. data/docs/yard/css/style.css +503 -0
  75. data/docs/yard/file.1_common_patterns.html +154 -0
  76. data/docs/yard/file.1_configuration.html +115 -0
  77. data/docs/yard/file.1_overview.html +142 -0
  78. data/docs/yard/file.1_schema_validation.html +188 -0
  79. data/docs/yard/file.2_architecture.html +157 -0
  80. data/docs/yard/file.2_error_handling.html +190 -0
  81. data/docs/yard/file.2_migration_guide.html +242 -0
  82. data/docs/yard/file.2_testing.html +227 -0
  83. data/docs/yard/file.3_async_execution.html +145 -0
  84. data/docs/yard/file.3_rails_integration.html +160 -0
  85. data/docs/yard/file.3_service_objects.html +191 -0
  86. data/docs/yard/file.4_logging.html +135 -0
  87. data/docs/yard/file.ErrorHandling.html +190 -0
  88. data/docs/yard/file.READme.html +674 -0
  89. data/docs/yard/file.architecture.html +157 -0
  90. data/docs/yard/file.async_execution.html +145 -0
  91. data/docs/yard/file.common_patterns.html +154 -0
  92. data/docs/yard/file.configuration.html +115 -0
  93. data/docs/yard/file.error_handling.html +190 -0
  94. data/docs/yard/file.logging.html +135 -0
  95. data/docs/yard/file.migration_guide.html +242 -0
  96. data/docs/yard/file.overview.html +142 -0
  97. data/docs/yard/file.rails_integration.html +160 -0
  98. data/docs/yard/file.schema_validation.html +188 -0
  99. data/docs/yard/file.service_objects.html +191 -0
  100. data/docs/yard/file.testing.html +227 -0
  101. data/docs/yard/file_list.html +119 -0
  102. data/docs/yard/frames.html +22 -0
  103. data/docs/yard/index.html +674 -0
  104. data/docs/yard/js/app.js +344 -0
  105. data/docs/yard/js/full_list.js +242 -0
  106. data/docs/yard/js/jquery.js +4 -0
  107. data/docs/yard/method_list.html +542 -0
  108. data/docs/yard/top-level-namespace.html +110 -0
  109. data/lib/generators/servus/event_handler/event_handler_generator.rb +59 -0
  110. data/lib/generators/servus/event_handler/templates/handler.rb.erb +86 -0
  111. data/lib/generators/servus/event_handler/templates/handler_spec.rb.erb +48 -0
  112. data/lib/generators/servus/service/service_generator.rb +68 -1
  113. data/lib/generators/servus/service/templates/arguments.json.erb +19 -10
  114. data/lib/generators/servus/service/templates/result.json.erb +8 -2
  115. data/lib/generators/servus/service/templates/service.rb.erb +102 -5
  116. data/lib/generators/servus/service/templates/service_spec.rb.erb +67 -6
  117. data/lib/servus/base.rb +275 -58
  118. data/lib/servus/config.rb +83 -17
  119. data/lib/servus/event_handler.rb +275 -0
  120. data/lib/servus/events/bus.rb +137 -0
  121. data/lib/servus/events/emitter.rb +162 -0
  122. data/lib/servus/events/errors.rb +10 -0
  123. data/lib/servus/extensions/async/call.rb +50 -18
  124. data/lib/servus/extensions/async/errors.rb +23 -3
  125. data/lib/servus/extensions/async/ext.rb +10 -2
  126. data/lib/servus/extensions/async/job.rb +30 -9
  127. data/lib/servus/helpers/controller_helpers.rb +73 -37
  128. data/lib/servus/railtie.rb +16 -0
  129. data/lib/servus/support/errors.rb +135 -45
  130. data/lib/servus/support/rescuer.rb +189 -36
  131. data/lib/servus/support/response.rb +49 -7
  132. data/lib/servus/support/validator.rb +147 -19
  133. data/lib/servus/testing/example_builders.rb +133 -0
  134. data/lib/servus/testing/example_extractor.rb +309 -0
  135. data/lib/servus/testing/matchers.rb +88 -0
  136. data/lib/servus/testing.rb +19 -0
  137. data/lib/servus/version.rb +1 -1
  138. data/lib/servus.rb +6 -0
  139. metadata +135 -19
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1d4674f76ed62ceba7d4bc333e075142debb42060e2e42dbb8e4fdbf807fce9b
4
- data.tar.gz: 2bf1707e65e97a088d8a891e1a846ceee50897b50ffbe550cbe81e747a308664
3
+ metadata.gz: f1094a06fd6195a99ef33f849a6ea30e6ec1a539eeb7ef73e7c27d1d4ac411bf
4
+ data.tar.gz: f6891749189216f6391396d79a29fd695fc3da5ff7647691318a90929781a90a
5
5
  SHA512:
6
- metadata.gz: dfd0f3bf8f32a44b394179ff5c5bfa52b6891aaab54388563ea521c1523cc5b9ea60258525e894f984e1997713832d234abb06785c84c0dbfd86544f866fb587
7
- data.tar.gz: abb0f1a3b4291d76157e4ac6d1858616f9f4bf569cde3da4c4874bfd2f5d66bc9ac77617ef96cbc2a7eb26aa79349e8eb8bdf8723771e42e49fb9981c142332a
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.
@@ -0,0 +1,15 @@
1
+ {
2
+ "hooks": {
3
+ "PostToolUse": [
4
+ {
5
+ "matcher": "Write|Edit",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "bundle exec rubocop -A"
10
+ }
11
+ ]
12
+ }
13
+ ]
14
+ }
15
+ }
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
- Lint/ConstantDefinitionInBlock:
11
- Enabled: false
26
+ Naming/PredicateMethod:
27
+ Enabled: false
data/.yardopts ADDED
@@ -0,0 +1,6 @@
1
+ --output-dir ./docs/yard
2
+ --readme READme.md
3
+ --files docs/**/*.md
4
+ --markup markdown
5
+ --markup-provider redcarpet
6
+ --title "Servus | Service Object Framework"
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, ApplicationService::Support::Errors::ServiceError("That didn't work for some reason"))
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, ApplicationService::Support::Errors::NotFoundError("Custom message"))
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, ApplicationService::Support::Errors::NotFoundError("Record not found"))
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, ApplicationService::Support::Errors::ServiceError("An error occurred"))
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
- Alternatively, schemas can be declared directly within the service class using `ARGUMENTS_SCHEMA` and `RESULT_SCHEMA` constants.
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
- ARGUMENTS_SCHEMA = {
445
- type: "object",
446
- required: ["user_id", "amount", "payment_method"],
447
- properties: {
448
- user_id: { type: "integer" },
449
- amount: {
450
- type: "integer",
451
- minimum: 1
452
- },
453
- payment_method: {
454
- type: "string",
455
- enum: ["credit_card", "paypal", "bank_transfer"]
456
- },
457
- currency: {
458
- type: "string",
459
- default: "USD"
460
- }
461
- },
462
- additionalProperties: false
463
- }
464
-
465
- RESULT_SCHEMA = {
466
- type: "object",
467
- required: ["transaction_id", "status"],
468
- properties: {
469
- transaction_id: { type: "string" },
470
- status: {
471
- type: "string",
472
- enum: ["approved", "pending", "declined"]
473
- },
474
- receipt_url: { type: "string" }
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. Checks for inline schema constants (`ARGUMENTS_SCHEMA` or `RESULT_SCHEMA`)
489
- 2. Falls back to JSON files if no inline schema is found
490
- 3. Returns nil if neither exists
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 `SchemaValidation.clear_cache!`
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