servus 0.1.2 → 0.1.4
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/.yardopts +6 -0
- data/CHANGELOG.md +8 -1
- data/IDEAS.md +5 -0
- data/READme.md +147 -42
- data/Rakefile +33 -0
- data/builds/servus-0.1.2.gem +0 -0
- data/builds/servus-0.1.3.gem +0 -0
- data/builds/servus-0.1.4.gem +0 -0
- data/docs/core/1_overview.md +77 -0
- data/docs/core/2_architecture.md +92 -0
- data/docs/core/3_service_objects.md +121 -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/guides/1_common_patterns.md +90 -0
- data/docs/guides/2_migration_guide.md +175 -0
- data/docs/integration/1_configuration.md +51 -0
- data/docs/integration/2_testing.md +164 -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/service/service_generator.rb +64 -1
- data/lib/generators/servus/service/templates/service.rb.erb +1 -1
- data/lib/servus/base.rb +258 -57
- data/lib/servus/config.rb +58 -12
- 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 +32 -11
- data/lib/servus/helpers/controller_helpers.rb +73 -37
- 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 +120 -19
- data/lib/servus/testing/example_builders.rb +133 -0
- data/lib/servus/testing/example_extractor.rb +309 -0
- data/lib/servus/testing.rb +17 -0
- data/lib/servus/version.rb +1 -1
- metadata +118 -19
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: af1900cb767ad43568bf7e2c8c14a98bebc479a9b3ec0cc5d76d5ed3c13c5289
|
|
4
|
+
data.tar.gz: d75379a7b744f9ae6d4938c9f21601af02dbedfe7befbd3b748410aff8b6e7d9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 011756b6fc695253bca1cedd31c4f3be14426305ed1d698f1e8963f62734a7ceb19dab0f77c75892b6aef326ce04d2ce6d07b22f97c9622131988ff4c24b9989
|
|
7
|
+
data.tar.gz: 04d948bd35038d768f0786ec253c771c50abc4e8d95d85b39bb2a573b411ee5e4486daec6b460548562e1a87fc5b7b4b3a47ddf2a27105d3ecb3cedd6f09c779
|
data/.yardopts
ADDED
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
-
## [0.1.
|
|
3
|
+
## [0.1.4] - 2025-11-21
|
|
4
|
+
- Added: Test helpers (`servus_arguments_example` and `servus_result_example`) to extract example values from schemas for testing
|
|
5
|
+
- Added: YARD documentation configuration with README homepage and markdown file support
|
|
6
|
+
- 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.
|
|
7
|
+
- Added: Added support from blocks on `rescue_from` to override default failure handler.
|
|
8
|
+
- Fixed: YARD link resolution warnings in documentation
|
|
9
|
+
|
|
10
|
+
## [0.1.3] - 2025-10-10
|
|
4
11
|
- Added: Added `call_async` method to `Servus::Base` to enqueue a job for calling the service asynchronously
|
|
5
12
|
- Added: Added `Async::Job` to handle async enqueing with support for ActiveJob set options
|
|
6
13
|
|
data/IDEAS.md
ADDED
data/READme.md
CHANGED
|
@@ -171,6 +171,7 @@ Always use the class method `call` instead of manual instantiation. The `call
|
|
|
171
171
|
2. Calls the instance-level `call` method
|
|
172
172
|
3. Handles schema validation of inputs and outputs
|
|
173
173
|
4. Handles logging of inputs and results
|
|
174
|
+
5. Automatically benchmarks execution time for performance monitoring
|
|
174
175
|
|
|
175
176
|
```ruby
|
|
176
177
|
# Good ✅
|
|
@@ -246,25 +247,25 @@ class SomeServiceObject::Service < Servus::Base
|
|
|
246
247
|
def call
|
|
247
248
|
# Return default ServiceError with custom message
|
|
248
249
|
failure("That didn't work for some reason")
|
|
249
|
-
#=> Response(false, nil,
|
|
250
|
+
#=> Response(false, nil, Servus::Support::Errors::ServiceError("That didn't work for some reason"))
|
|
250
251
|
#
|
|
251
252
|
# OR
|
|
252
253
|
#
|
|
253
254
|
# Specify ServiceError type with custom message
|
|
254
255
|
failure("Custom message", type: Servus::Support::Errors::NotFoundError)
|
|
255
|
-
#=> Response(false, nil,
|
|
256
|
+
#=> Response(false, nil, Servus::Support::Errors::NotFoundError("Custom message"))
|
|
256
257
|
#
|
|
257
258
|
# OR
|
|
258
259
|
#
|
|
259
260
|
# Specify ServiceError type with default message
|
|
260
261
|
failure(type: Servus::Support::Errors::NotFoundError)
|
|
261
|
-
#=> Response(false, nil,
|
|
262
|
+
#=> Response(false, nil, Servus::Support::Errors::NotFoundError("Not found"))
|
|
262
263
|
#
|
|
263
264
|
# OR
|
|
264
265
|
#
|
|
265
266
|
# Accept all defaults
|
|
266
267
|
failure
|
|
267
|
-
#=> Response(false, nil,
|
|
268
|
+
#=> Response(false, nil, Servus::Support::Errors::ServiceError("An error occurred"))
|
|
268
269
|
end
|
|
269
270
|
end
|
|
270
271
|
|
|
@@ -329,6 +330,28 @@ The `rescue_from` method will rescue from the specified errors and use the speci
|
|
|
329
330
|
the custom error. It helps eliminate the need to manually rescue many errors and create failure responses within the call method of
|
|
330
331
|
a service object.
|
|
331
332
|
|
|
333
|
+
You can also provide a block for custom error handling:
|
|
334
|
+
|
|
335
|
+
```ruby
|
|
336
|
+
class SomeServiceObject::Service < Servus::Base
|
|
337
|
+
# Custom error handling with a block
|
|
338
|
+
rescue_from ActiveRecord::RecordInvalid do |exception|
|
|
339
|
+
failure("Validation failed: #{exception.message}", type: ValidationError)
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
rescue_from Net::HTTPError do |exception|
|
|
343
|
+
# Can even return success to recover from errors
|
|
344
|
+
success(recovered: true, error_message: exception.message)
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def call
|
|
348
|
+
# Service logic
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
The block receives the exception and has access to `success` and `failure` methods for creating the response.
|
|
354
|
+
|
|
332
355
|
## Controller Helpers
|
|
333
356
|
|
|
334
357
|
Service objects can be called from controllers using the `run_service` and `render_service_object_error` helpers.
|
|
@@ -437,43 +460,59 @@ Example `result.json`:
|
|
|
437
460
|
|
|
438
461
|
### 2. Inline Schema Validation
|
|
439
462
|
|
|
440
|
-
|
|
463
|
+
Schemas can be declared directly within the service class using the `schema` DSL method:
|
|
441
464
|
|
|
442
465
|
```ruby
|
|
443
466
|
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
|
-
|
|
467
|
+
schema(
|
|
468
|
+
arguments: {
|
|
469
|
+
type: "object",
|
|
470
|
+
required: ["user_id", "amount", "payment_method"],
|
|
471
|
+
properties: {
|
|
472
|
+
user_id: { type: "integer" },
|
|
473
|
+
amount: {
|
|
474
|
+
type: "integer",
|
|
475
|
+
minimum: 1
|
|
476
|
+
},
|
|
477
|
+
payment_method: {
|
|
478
|
+
type: "string",
|
|
479
|
+
enum: ["credit_card", "paypal", "bank_transfer"]
|
|
480
|
+
},
|
|
481
|
+
currency: {
|
|
482
|
+
type: "string",
|
|
483
|
+
default: "USD"
|
|
484
|
+
}
|
|
485
|
+
},
|
|
486
|
+
additionalProperties: false
|
|
487
|
+
},
|
|
488
|
+
result: {
|
|
489
|
+
type: "object",
|
|
490
|
+
required: ["transaction_id", "status"],
|
|
491
|
+
properties: {
|
|
492
|
+
transaction_id: { type: "string" },
|
|
493
|
+
status: {
|
|
494
|
+
type: "string",
|
|
495
|
+
enum: ["approved", "pending", "declined"]
|
|
496
|
+
},
|
|
497
|
+
receipt_url: { type: "string" }
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
def initialize(user_id:, amount:, payment_method:, currency: 'USD')
|
|
503
|
+
@user_id = user_id
|
|
504
|
+
@amount = amount
|
|
505
|
+
@payment_method = payment_method
|
|
506
|
+
@currency = currency
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
def call
|
|
510
|
+
# Service logic...
|
|
511
|
+
success({
|
|
512
|
+
transaction_id: "txn_1",
|
|
513
|
+
status: "approved"
|
|
514
|
+
})
|
|
515
|
+
end
|
|
477
516
|
end
|
|
478
517
|
```
|
|
479
518
|
|
|
@@ -485,9 +524,10 @@ These schemas use JSON Schema format to enforce type safety and input/output con
|
|
|
485
524
|
|
|
486
525
|
The validation system follows this precedence:
|
|
487
526
|
|
|
488
|
-
1.
|
|
489
|
-
2.
|
|
490
|
-
3.
|
|
527
|
+
1. Schemas defined via `schema` DSL method (recommended)
|
|
528
|
+
2. Inline schema constants (`ARGUMENTS_SCHEMA` or `RESULT_SCHEMA`) - legacy support
|
|
529
|
+
3. JSON files in schema_root directory - legacy support
|
|
530
|
+
4. Returns nil if no schema is found (validation is opt-in)
|
|
491
531
|
|
|
492
532
|
### Schema Caching
|
|
493
533
|
|
|
@@ -495,4 +535,69 @@ Both file-based and inline schemas are automatically cached:
|
|
|
495
535
|
|
|
496
536
|
- First validation request loads and caches the schema
|
|
497
537
|
- Subsequent validations use the cached version
|
|
498
|
-
- Cache can be cleared using `
|
|
538
|
+
- Cache can be cleared using `Servus::Support::Validator.clear_cache!`
|
|
539
|
+
|
|
540
|
+
## **Logging**
|
|
541
|
+
|
|
542
|
+
Servus automatically logs service execution details, making it easy to track and debug service calls.
|
|
543
|
+
|
|
544
|
+
### Automatic Logging
|
|
545
|
+
|
|
546
|
+
Every service call automatically logs:
|
|
547
|
+
|
|
548
|
+
- **Service invocation** with input arguments
|
|
549
|
+
- **Success results** with execution duration
|
|
550
|
+
- **Failure results** with error details and duration
|
|
551
|
+
- **Validation errors** for schema violations
|
|
552
|
+
- **Uncaught exceptions** with error messages
|
|
553
|
+
|
|
554
|
+
### Logger Configuration
|
|
555
|
+
|
|
556
|
+
The logger automatically adapts to your environment:
|
|
557
|
+
|
|
558
|
+
- **Rails applications**: Uses `Rails.logger`
|
|
559
|
+
- **Non-Rails applications**: Uses stdout logger
|
|
560
|
+
|
|
561
|
+
### Log Output Examples
|
|
562
|
+
|
|
563
|
+
```ruby
|
|
564
|
+
# Success
|
|
565
|
+
INFO -- : Calling Services::ProcessPayment::Service with args: {:user_id=>123, :amount=>50}
|
|
566
|
+
INFO -- : Services::ProcessPayment::Service succeeded in 0.245s
|
|
567
|
+
|
|
568
|
+
# Failure
|
|
569
|
+
INFO -- : Calling Services::ProcessPayment::Service with args: {:user_id=>123, :amount=>50}
|
|
570
|
+
WARN -- : Services::ProcessPayment::Service failed in 0.156s with error: Insufficient funds
|
|
571
|
+
|
|
572
|
+
# Validation Error
|
|
573
|
+
ERROR -- : Services::ProcessPayment::Service validation error: The property '#/amount' value -10 was less than minimum value 1
|
|
574
|
+
|
|
575
|
+
# Exception
|
|
576
|
+
ERROR -- : Services::ProcessPayment::Service uncaught exception: NoMethodError - undefined method 'charge' for nil:NilClass
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
All logging happens transparently when using the class-level `.call` method. This is one of the reasons why direct instantiation (bypassing `.call`) is discouraged.
|
|
580
|
+
|
|
581
|
+
## **Configuration**
|
|
582
|
+
|
|
583
|
+
Servus can be configured to customize behavior for your application needs.
|
|
584
|
+
|
|
585
|
+
### Schema Root Directory
|
|
586
|
+
|
|
587
|
+
By default, Servus looks for schema files in `app/schemas/services/`. You can customize this location:
|
|
588
|
+
|
|
589
|
+
```ruby
|
|
590
|
+
# config/initializers/servus.rb
|
|
591
|
+
Servus.configure do |config|
|
|
592
|
+
config.schema_root = Rails.root.join('lib/schemas')
|
|
593
|
+
end
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
### Default Behavior
|
|
597
|
+
|
|
598
|
+
Without explicit configuration:
|
|
599
|
+
|
|
600
|
+
- **Rails applications**: Schema root defaults to `Rails.root/app/schemas/services`
|
|
601
|
+
- **Non-Rails applications**: Schema root defaults to `./app/schemas/services` relative to the gem installation
|
|
602
|
+
|
|
603
|
+
The configuration is accessed through the singleton `Servus.config` instance and can be modified using `Servus.configure`.
|
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
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# @title Core / 1. Overview
|
|
2
|
+
|
|
3
|
+
# Servus Overview
|
|
4
|
+
|
|
5
|
+
Servus is a lightweight framework for implementing service objects in Ruby applications. It extracts business logic from controllers and models into testable, single-purpose classes with built-in validation, error handling, and logging.
|
|
6
|
+
|
|
7
|
+
## Core Concepts
|
|
8
|
+
|
|
9
|
+
### The Service Pattern
|
|
10
|
+
|
|
11
|
+
Services encapsulate one business operation. Each service inherits from `Servus::Base`, implements `initialize` and `call`, and returns a `Response` object indicating success or failure.
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
class ProcessPayment::Service < Servus::Base
|
|
15
|
+
def initialize(user_id:, amount:)
|
|
16
|
+
@user_id = user_id
|
|
17
|
+
@amount = amount
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def call
|
|
21
|
+
user = User.find(@user_id)
|
|
22
|
+
return failure("Insufficient funds") unless user.balance >= @amount
|
|
23
|
+
|
|
24
|
+
user.update!(balance: user.balance - @amount)
|
|
25
|
+
success(user: user, new_balance: user.balance)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Usage
|
|
30
|
+
result = ProcessPayment::Service.call(user_id: 1, amount: 50)
|
|
31
|
+
result.success? # => true
|
|
32
|
+
result.data # => { user: #<User>, new_balance: 950 }
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Response Objects
|
|
36
|
+
|
|
37
|
+
Services return `Response` objects instead of raising exceptions for business failures. This makes success and failure paths explicit and enables service composition without exception handling.
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
result = SomeService.call(params)
|
|
41
|
+
if result.success?
|
|
42
|
+
result.data # Hash or object returned by success()
|
|
43
|
+
else
|
|
44
|
+
result.error # ServiceError instance
|
|
45
|
+
result.error.message
|
|
46
|
+
result.error.api_error # { code: :symbol, message: "string" }
|
|
47
|
+
end
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Optional Schema Validation
|
|
51
|
+
|
|
52
|
+
Services can define JSON schemas for arguments and results. Validation happens automatically before/after execution but is entirely optional.
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
class Service < Servus::Base
|
|
56
|
+
schema(
|
|
57
|
+
arguments: {
|
|
58
|
+
type: "object",
|
|
59
|
+
required: ["user_id", "amount"],
|
|
60
|
+
properties: {
|
|
61
|
+
user_id: { type: "integer" },
|
|
62
|
+
amount: { type: "number", minimum: 0.01 }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## When to Use Servus
|
|
70
|
+
|
|
71
|
+
**Good fits**: Multi-step workflows, operations spanning multiple models, external API calls, background jobs, complex business logic.
|
|
72
|
+
|
|
73
|
+
**Poor fits**: Simple CRUD, single-model operations, operations tightly coupled to one model.
|
|
74
|
+
|
|
75
|
+
## Framework Integration
|
|
76
|
+
|
|
77
|
+
Servus core works in any Ruby application. Rails-specific features (async via ActiveJob, controller helpers, generators) are optional additions. Services work without any configuration - just inherit from `Servus::Base` and implement your logic.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# @title Core / 2. Architecture
|
|
2
|
+
|
|
3
|
+
# Architecture
|
|
4
|
+
|
|
5
|
+
Servus wraps service execution with automatic validation, logging, and error handling. When you call `Service.call(**args)`, the framework orchestrates these concerns transparently.
|
|
6
|
+
|
|
7
|
+
## Execution Flow
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
Arguments → Validation → Service#call → Result Validation → Logging → Response
|
|
11
|
+
↓ ↓ ↓
|
|
12
|
+
ValidationError ValidationError Benchmark
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The framework intercepts the `.call` class method to inject cross-cutting concerns before and after your business logic runs. Your `call` instance method contains only business logic - validation, logging, and timing happen automatically.
|
|
16
|
+
|
|
17
|
+
## Core Components
|
|
18
|
+
|
|
19
|
+
**Servus::Base** (`lib/servus/base.rb`): Foundation class providing `.call()` orchestration and response helpers (`success`, `failure`, `error!`)
|
|
20
|
+
|
|
21
|
+
**Support::Response** (`lib/servus/support/response.rb`): Immutable result object with `success?`, `data`, and `error` attributes
|
|
22
|
+
|
|
23
|
+
**Support::Validator** (`lib/servus/support/validator.rb`): JSON Schema validation for arguments (before execution) and results (after execution). Schemas are cached after first load.
|
|
24
|
+
|
|
25
|
+
**Support::Logger** (`lib/servus/support/logger.rb`): Automatic logging at DEBUG (calls with args), INFO (success), WARN (failures), ERROR (exceptions)
|
|
26
|
+
|
|
27
|
+
**Support::Rescuer** (`lib/servus/support/rescuer.rb`): Declarative exception handling via `rescue_from` class method
|
|
28
|
+
|
|
29
|
+
**Support::Errors** (`lib/servus/support/errors.rb`): HTTP-aligned error hierarchy (ServiceError, NotFoundError, ValidationError, etc.)
|
|
30
|
+
|
|
31
|
+
## Extension Points
|
|
32
|
+
|
|
33
|
+
### Schema Validation
|
|
34
|
+
|
|
35
|
+
Use the `schema` DSL method to define JSON Schema validation for arguments and results:
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
class Service < Servus::Base
|
|
39
|
+
schema(
|
|
40
|
+
arguments: { type: "object", required: ["user_id"] },
|
|
41
|
+
result: { type: "object", required: ["user"] }
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Declarative Error Handling
|
|
47
|
+
|
|
48
|
+
Use `rescue_from` to convert exceptions into failures. Provide a custom error type or use a block for custom handling.
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
class Service < Servus::Base
|
|
52
|
+
# Default error type
|
|
53
|
+
rescue_from Net::HTTPError, Timeout::Error, use: ServiceUnavailableError
|
|
54
|
+
|
|
55
|
+
# Custom handling with block
|
|
56
|
+
rescue_from ActiveRecord::RecordInvalid do |exception|
|
|
57
|
+
failure("Validation failed: #{exception.message}", type: ValidationError)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Support Classes
|
|
63
|
+
|
|
64
|
+
Create helper classes in `app/services/service_name/support/*.rb`. These are namespaced to your service.
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
app/services/process_payment/
|
|
68
|
+
├── service.rb
|
|
69
|
+
└── support/
|
|
70
|
+
├── payment_gateway.rb
|
|
71
|
+
└── receipt_formatter.rb
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Async Execution
|
|
75
|
+
|
|
76
|
+
`Service.call_async(**args)` enqueues execution via ActiveJob. The service runs identically whether called sync or async.
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
ProcessPayment::Service.call_async(
|
|
80
|
+
user_id: 1,
|
|
81
|
+
amount: 50,
|
|
82
|
+
queue: :critical,
|
|
83
|
+
wait: 5.minutes
|
|
84
|
+
)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Performance
|
|
88
|
+
|
|
89
|
+
- Schema loading: Cached per class after first use
|
|
90
|
+
- Validation overhead: ~1-5ms when schemas defined
|
|
91
|
+
- Logging overhead: ~0.1ms per call
|
|
92
|
+
- Total framework overhead: < 10ms per service call
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# @title Core / 3. Service Objects
|
|
2
|
+
|
|
3
|
+
# Service Objects
|
|
4
|
+
|
|
5
|
+
Service objects encapsulate one business operation into a testable, reusable class. They sit between controllers and models, handling orchestration logic that doesn't belong in either.
|
|
6
|
+
|
|
7
|
+
## The Pattern
|
|
8
|
+
|
|
9
|
+
Services implement two methods: `initialize` (sets up dependencies) and `call` (executes business logic). All services return a `Response` object indicating success or failure.
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
module Users
|
|
13
|
+
module Create
|
|
14
|
+
class Service < Servus::Base
|
|
15
|
+
def initialize(email:, name:)
|
|
16
|
+
@email = email
|
|
17
|
+
@name = name
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def call
|
|
21
|
+
return failure("Email taken") if User.exists?(email: @email)
|
|
22
|
+
|
|
23
|
+
user = User.create!(email: @email, name: @name)
|
|
24
|
+
send_welcome_email(user)
|
|
25
|
+
|
|
26
|
+
success(user: user)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Usage
|
|
33
|
+
result = Users::Create::Service.call(email: "user@example.com", name: "John")
|
|
34
|
+
result.success? # => true
|
|
35
|
+
result.data[:user] # => #<User>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Service Composition
|
|
39
|
+
|
|
40
|
+
Services can call other services. Use the returned Response to decide whether to continue or propagate the failure.
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
def call
|
|
44
|
+
user_result = Users::Create::Service.call(user_params)
|
|
45
|
+
return user_result unless user_result.success? # propogates result failure
|
|
46
|
+
|
|
47
|
+
account_result = Accounts::Create::Service.call(
|
|
48
|
+
user: user_result.data[:user],
|
|
49
|
+
plan: @plan
|
|
50
|
+
)
|
|
51
|
+
return account_result unless account_result.success? # propogates result failure
|
|
52
|
+
|
|
53
|
+
success(
|
|
54
|
+
user: user_result.data[:user],
|
|
55
|
+
account: account_result.data[:account]
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## When to Extract to Services
|
|
61
|
+
|
|
62
|
+
**Extract when**:
|
|
63
|
+
- Logic spans multiple models
|
|
64
|
+
- Complex conditional branching
|
|
65
|
+
- External API calls
|
|
66
|
+
- Background processing needed
|
|
67
|
+
- Testing requires extensive setup
|
|
68
|
+
|
|
69
|
+
**Don't extract when**:
|
|
70
|
+
- Simple CRUD operations
|
|
71
|
+
- Single-model updates
|
|
72
|
+
- Logic naturally belongs in model
|
|
73
|
+
|
|
74
|
+
## Directory Structure
|
|
75
|
+
|
|
76
|
+
Each service lives in its own namespace to avoid naming collisions and allow for support classes.
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
app/services/
|
|
80
|
+
├── users/
|
|
81
|
+
│ └── create/
|
|
82
|
+
│ ├── service.rb
|
|
83
|
+
│ └── support/
|
|
84
|
+
│ └── welcome_email.rb
|
|
85
|
+
└── orders/
|
|
86
|
+
└── process/
|
|
87
|
+
├── service.rb
|
|
88
|
+
└── support/
|
|
89
|
+
├── payment_gateway.rb
|
|
90
|
+
└── inventory_updater.rb
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Support classes are private to their service - they should never be used by other services.
|
|
94
|
+
|
|
95
|
+
## Testing
|
|
96
|
+
|
|
97
|
+
Services are designed for easy testing with explicit inputs and outputs.
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
RSpec.describe Users::Create::Service do
|
|
101
|
+
describe ".call" do
|
|
102
|
+
context "with valid params" do
|
|
103
|
+
it "creates user" do
|
|
104
|
+
result = described_class.call(email: "test@example.com", name: "Test")
|
|
105
|
+
expect(result.success?).to be true
|
|
106
|
+
expect(result.data[:user]).to be_persisted
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
context "with duplicate email" do
|
|
111
|
+
before { create(:user, email: "test@example.com") }
|
|
112
|
+
|
|
113
|
+
it "returns failure" do
|
|
114
|
+
result = described_class.call(email: "test@example.com", name: "Test")
|
|
115
|
+
expect(result.success?).to be false
|
|
116
|
+
expect(result.error.message).to eq("Email taken")
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
```
|