servus 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/servus/event_handler/event_handler_generator.rb +1 -1
  3. data/lib/generators/servus/guard/guard_generator.rb +1 -1
  4. data/lib/generators/servus/guard/templates/guard.rb.erb +5 -3
  5. data/lib/generators/servus/service/service_generator.rb +1 -1
  6. data/lib/servus/base.rb +67 -9
  7. data/lib/servus/config.rb +71 -3
  8. data/lib/servus/events/bus.rb +29 -0
  9. data/lib/servus/events/emitter.rb +15 -0
  10. data/lib/servus/extensions/lazily/call.rb +82 -0
  11. data/lib/servus/extensions/lazily/errors.rb +37 -0
  12. data/lib/servus/extensions/lazily/ext.rb +23 -0
  13. data/lib/servus/extensions/lazily/resolver.rb +32 -0
  14. data/lib/servus/guard.rb +7 -6
  15. data/lib/servus/guards/falsey_guard.rb +3 -3
  16. data/lib/servus/guards/presence_guard.rb +4 -4
  17. data/lib/servus/guards/state_guard.rb +4 -5
  18. data/lib/servus/guards/truthy_guard.rb +3 -3
  19. data/lib/servus/helpers/controller_helpers.rb +40 -0
  20. data/lib/servus/railtie.rb +7 -1
  21. data/lib/servus/support/data_object.rb +80 -0
  22. data/lib/servus/support/errors.rb +16 -0
  23. data/lib/servus/support/lockdown.rb +94 -0
  24. data/lib/servus/support/logger.rb +16 -0
  25. data/lib/servus/support/response.rb +12 -1
  26. data/lib/servus/support/validator.rb +79 -34
  27. data/lib/servus/testing/example_builders.rb +74 -0
  28. data/lib/servus/testing/matchers.rb +99 -0
  29. data/lib/servus/version.rb +1 -1
  30. data/lib/servus.rb +2 -0
  31. metadata +16 -114
  32. data/.claude/commands/check-docs.md +0 -1
  33. data/.claude/commands/consistency-check.md +0 -1
  34. data/.claude/commands/fine-tooth-comb.md +0 -1
  35. data/.claude/commands/red-green-refactor.md +0 -5
  36. data/.claude/settings.json +0 -24
  37. data/.rspec +0 -3
  38. data/.rubocop.yml +0 -27
  39. data/.yardopts +0 -6
  40. data/CHANGELOG.md +0 -122
  41. data/CLAUDE.md +0 -10
  42. data/IDEAS.md +0 -5
  43. data/LICENSE.txt +0 -21
  44. data/READme.md +0 -856
  45. data/Rakefile +0 -45
  46. data/docs/core/1_overview.md +0 -77
  47. data/docs/core/2_architecture.md +0 -120
  48. data/docs/core/3_service_objects.md +0 -121
  49. data/docs/features/1_schema_validation.md +0 -119
  50. data/docs/features/2_error_handling.md +0 -121
  51. data/docs/features/3_async_execution.md +0 -81
  52. data/docs/features/4_logging.md +0 -64
  53. data/docs/features/5_event_bus.md +0 -244
  54. data/docs/features/6_guards.md +0 -356
  55. data/docs/features/guards_naming_convention.md +0 -540
  56. data/docs/guides/1_common_patterns.md +0 -90
  57. data/docs/guides/2_migration_guide.md +0 -175
  58. data/docs/integration/1_configuration.md +0 -154
  59. data/docs/integration/2_testing.md +0 -287
  60. data/docs/integration/3_rails_integration.md +0 -99
  61. data/docs/yard/Servus/Base.html +0 -1645
  62. data/docs/yard/Servus/Config.html +0 -582
  63. data/docs/yard/Servus/Extensions/Async/Call.html +0 -400
  64. data/docs/yard/Servus/Extensions/Async/Errors/AsyncError.html +0 -140
  65. data/docs/yard/Servus/Extensions/Async/Errors/JobEnqueueError.html +0 -154
  66. data/docs/yard/Servus/Extensions/Async/Errors/ServiceNotFoundError.html +0 -154
  67. data/docs/yard/Servus/Extensions/Async/Errors.html +0 -128
  68. data/docs/yard/Servus/Extensions/Async/Ext.html +0 -119
  69. data/docs/yard/Servus/Extensions/Async/Job.html +0 -310
  70. data/docs/yard/Servus/Extensions/Async.html +0 -141
  71. data/docs/yard/Servus/Extensions.html +0 -117
  72. data/docs/yard/Servus/Generators/ServiceGenerator.html +0 -261
  73. data/docs/yard/Servus/Generators.html +0 -115
  74. data/docs/yard/Servus/Helpers/ControllerHelpers.html +0 -457
  75. data/docs/yard/Servus/Helpers.html +0 -115
  76. data/docs/yard/Servus/Railtie.html +0 -134
  77. data/docs/yard/Servus/Support/Errors/AuthenticationError.html +0 -287
  78. data/docs/yard/Servus/Support/Errors/BadRequestError.html +0 -283
  79. data/docs/yard/Servus/Support/Errors/ForbiddenError.html +0 -284
  80. data/docs/yard/Servus/Support/Errors/InternalServerError.html +0 -283
  81. data/docs/yard/Servus/Support/Errors/NotFoundError.html +0 -284
  82. data/docs/yard/Servus/Support/Errors/ServiceError.html +0 -489
  83. data/docs/yard/Servus/Support/Errors/ServiceUnavailableError.html +0 -290
  84. data/docs/yard/Servus/Support/Errors/UnauthorizedError.html +0 -200
  85. data/docs/yard/Servus/Support/Errors/UnprocessableEntityError.html +0 -288
  86. data/docs/yard/Servus/Support/Errors/ValidationError.html +0 -200
  87. data/docs/yard/Servus/Support/Errors.html +0 -140
  88. data/docs/yard/Servus/Support/Logger.html +0 -856
  89. data/docs/yard/Servus/Support/Rescuer/BlockContext.html +0 -585
  90. data/docs/yard/Servus/Support/Rescuer/CallOverride.html +0 -257
  91. data/docs/yard/Servus/Support/Rescuer/ClassMethods.html +0 -343
  92. data/docs/yard/Servus/Support/Rescuer.html +0 -267
  93. data/docs/yard/Servus/Support/Response.html +0 -574
  94. data/docs/yard/Servus/Support/Validator.html +0 -1150
  95. data/docs/yard/Servus/Support.html +0 -119
  96. data/docs/yard/Servus/Testing/ExampleBuilders.html +0 -523
  97. data/docs/yard/Servus/Testing/ExampleExtractor.html +0 -578
  98. data/docs/yard/Servus/Testing.html +0 -142
  99. data/docs/yard/Servus.html +0 -343
  100. data/docs/yard/_index.html +0 -535
  101. data/docs/yard/class_list.html +0 -54
  102. data/docs/yard/css/common.css +0 -1
  103. data/docs/yard/css/full_list.css +0 -58
  104. data/docs/yard/css/style.css +0 -503
  105. data/docs/yard/file.1_common_patterns.html +0 -154
  106. data/docs/yard/file.1_configuration.html +0 -115
  107. data/docs/yard/file.1_overview.html +0 -142
  108. data/docs/yard/file.1_schema_validation.html +0 -188
  109. data/docs/yard/file.2_architecture.html +0 -157
  110. data/docs/yard/file.2_error_handling.html +0 -190
  111. data/docs/yard/file.2_migration_guide.html +0 -242
  112. data/docs/yard/file.2_testing.html +0 -227
  113. data/docs/yard/file.3_async_execution.html +0 -145
  114. data/docs/yard/file.3_rails_integration.html +0 -160
  115. data/docs/yard/file.3_service_objects.html +0 -191
  116. data/docs/yard/file.4_logging.html +0 -135
  117. data/docs/yard/file.ErrorHandling.html +0 -190
  118. data/docs/yard/file.READme.html +0 -674
  119. data/docs/yard/file.architecture.html +0 -157
  120. data/docs/yard/file.async_execution.html +0 -145
  121. data/docs/yard/file.common_patterns.html +0 -154
  122. data/docs/yard/file.configuration.html +0 -115
  123. data/docs/yard/file.error_handling.html +0 -190
  124. data/docs/yard/file.logging.html +0 -135
  125. data/docs/yard/file.migration_guide.html +0 -242
  126. data/docs/yard/file.overview.html +0 -142
  127. data/docs/yard/file.rails_integration.html +0 -160
  128. data/docs/yard/file.schema_validation.html +0 -188
  129. data/docs/yard/file.service_objects.html +0 -191
  130. data/docs/yard/file.testing.html +0 -227
  131. data/docs/yard/file_list.html +0 -119
  132. data/docs/yard/frames.html +0 -22
  133. data/docs/yard/index.html +0 -674
  134. data/docs/yard/js/app.js +0 -344
  135. data/docs/yard/js/full_list.js +0 -242
  136. data/docs/yard/js/jquery.js +0 -4
  137. data/docs/yard/method_list.html +0 -542
  138. data/docs/yard/top-level-namespace.html +0 -110
data/Rakefile DELETED
@@ -1,45 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'fileutils'
4
- require 'bundler/gem_tasks'
5
- require 'rspec/core/rake_task'
6
-
7
- # Constants
8
- GEM_NAME = 'servus'
9
- BUILDS_DIR = 'builds'
10
-
11
- RSpec::Core::RakeTask.new(:spec)
12
-
13
- require 'rubocop/rake_task'
14
-
15
- RuboCop::RakeTask.new
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
-
45
- task default: %i[spec rubocop]
@@ -1,77 +0,0 @@
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.
@@ -1,120 +0,0 @@
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 → Event Emission → Logging → Response
11
- ↓ ↓ ↓ ↓
12
- ValidationError ValidationError EventHandlers 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, event emission, 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
- **Events::Emitter** (`lib/servus/events/emitter.rb`): DSL for declaring events that services emit on success/failure
32
-
33
- **Events::Bus** (`lib/servus/events/bus.rb`): Central event router using ActiveSupport::Notifications for thread-safe dispatch
34
-
35
- **EventHandler** (`lib/servus/event_handler.rb`): Base class for handlers that subscribe to events and invoke services
36
-
37
- ## Extension Points
38
-
39
- ### Schema Validation
40
-
41
- Use the `schema` DSL method to define JSON Schema validation for arguments and results:
42
-
43
- ```ruby
44
- class Service < Servus::Base
45
- schema(
46
- arguments: { type: "object", required: ["user_id"] },
47
- result: { type: "object", required: ["user"] }
48
- )
49
- end
50
- ```
51
-
52
- ### Declarative Error Handling
53
-
54
- Use `rescue_from` to convert exceptions into failures. Provide a custom error type or use a block for custom handling.
55
-
56
- ```ruby
57
- class Service < Servus::Base
58
- # Default error type
59
- rescue_from Net::HTTPError, Timeout::Error, use: ServiceUnavailableError
60
-
61
- # Custom handling with block
62
- rescue_from ActiveRecord::RecordInvalid do |exception|
63
- failure("Validation failed: #{exception.message}", type: ValidationError)
64
- end
65
- end
66
- ```
67
-
68
- ### Support Classes
69
-
70
- Create helper classes in `app/services/service_name/support/*.rb`. These are namespaced to your service.
71
-
72
- ```
73
- app/services/process_payment/
74
- ├── service.rb
75
- └── support/
76
- ├── payment_gateway.rb
77
- └── receipt_formatter.rb
78
- ```
79
-
80
- ## Async Execution
81
-
82
- `Service.call_async(**args)` enqueues execution via ActiveJob. The service runs identically whether called sync or async.
83
-
84
- ```ruby
85
- ProcessPayment::Service.call_async(
86
- user_id: 1,
87
- amount: 50,
88
- queue: :critical,
89
- wait: 5.minutes
90
- )
91
- ```
92
-
93
- ## Event-Driven Architecture
94
-
95
- Services can emit events that trigger downstream handlers. This decouples services from their side effects.
96
-
97
- ```ruby
98
- # Service emits events
99
- class CreateUser::Service < Servus::Base
100
- emits :user_created, on: :success
101
- end
102
-
103
- # Handler reacts to events
104
- class UserCreatedHandler < Servus::EventHandler
105
- handles :user_created
106
-
107
- invoke SendWelcomeEmail::Service, async: true do |payload|
108
- { user_id: payload[:user_id] }
109
- end
110
- end
111
- ```
112
-
113
- See {file:docs/features/5_event_bus.md Event Bus} for full documentation.
114
-
115
- ## Performance
116
-
117
- - Schema loading: Cached per class after first use
118
- - Validation overhead: ~1-5ms when schemas defined
119
- - Logging overhead: ~0.1ms per call
120
- - Total framework overhead: < 10ms per service call
@@ -1,121 +0,0 @@
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
- ```
@@ -1,119 +0,0 @@
1
- # @title Features / 1. Schema Validation
2
-
3
- # Schema Validation
4
-
5
- Servus provides optional JSON Schema validation for service arguments and results. Validation is opt-in - services work fine without schemas.
6
-
7
- ## How It Works
8
-
9
- Define schemas using the `schema` DSL method (recommended) or as constants. The framework validates arguments before execution and results after execution. Invalid data raises `ValidationError`.
10
-
11
- ### Preferred: Schema DSL Method
12
-
13
- ```ruby
14
- class ProcessPayment::Service < Servus::Base
15
- schema(
16
- arguments: {
17
- type: "object",
18
- required: ["user_id", "amount"],
19
- properties: {
20
- user_id: { type: "integer", example: 123 },
21
- amount: { type: "number", minimum: 0.01, example: 100.0 }
22
- }
23
- },
24
- result: {
25
- type: "object",
26
- required: ["transaction_id", "new_balance"],
27
- properties: {
28
- transaction_id: { type: "string", example: "txn_abc123" },
29
- new_balance: { type: "number", example: 950.0 }
30
- }
31
- }
32
- )
33
- end
34
- ```
35
-
36
- **Pro tip:** Add `example` or `examples` keywords to your schemas. These values can be automatically extracted in tests using `servus_arguments_example()` and `servus_result_example()` helpers. See the [Testing documentation](../integration/testing.md#schema-example-helpers) for details.
37
-
38
- You can define just one schema if needed:
39
-
40
- ```ruby
41
- class SendEmail::Service < Servus::Base
42
- schema arguments: {
43
- type: "object",
44
- required: ["email", "subject"],
45
- properties: {
46
- email: { type: "string", format: "email" },
47
- subject: { type: "string" }
48
- }
49
- }
50
- end
51
- ```
52
-
53
- ### Alternative: Inline Constants
54
-
55
- Constants are still supported for backwards compatibility:
56
-
57
- ```ruby
58
- class ProcessPayment::Service < Servus::Base
59
- ARGUMENTS_SCHEMA = {
60
- type: "object",
61
- required: ["user_id", "amount"],
62
- properties: {
63
- user_id: { type: "integer" },
64
- amount: { type: "number", minimum: 0.01 }
65
- }
66
- }.freeze
67
-
68
- RESULT_SCHEMA = {
69
- type: "object",
70
- required: ["transaction_id", "new_balance"],
71
- properties: {
72
- transaction_id: { type: "string" },
73
- new_balance: { type: "number" }
74
- }
75
- }.freeze
76
- end
77
- ```
78
-
79
- ## File-Based Schemas
80
-
81
- For complex schemas, use JSON files instead of inline definitions. Create files at:
82
- - `app/schemas/services/service_name/arguments.json`
83
- - `app/schemas/services/service_name/result.json`
84
-
85
- ### Schema Lookup Precedence
86
-
87
- Servus checks for schemas in this order:
88
- 1. **schema DSL method** (if defined)
89
- 2. **Inline constants** (ARGUMENTS_SCHEMA, RESULT_SCHEMA)
90
- 3. **JSON files** (in schema_root directory)
91
-
92
- Schemas are cached after first load for performance.
93
-
94
- ## Three Layers of Validation
95
-
96
- **Schema Validation** (Servus): Type safety and structure at service boundaries
97
-
98
- **Business Rules** (Service Logic): Domain-specific constraints during execution
99
-
100
- **Model Validation** (ActiveRecord): Database constraints before persistence
101
-
102
- Each layer has a different purpose - don't duplicate validation across layers.
103
-
104
- ## Configuration
105
-
106
- Change the schema file location if needed:
107
-
108
- ```ruby
109
- # config/initializers/servus.rb
110
- Servus.configure do |config|
111
- config.schema_root = Rails.root.join('config/schemas')
112
- end
113
- ```
114
-
115
- Clear the schema cache during development when schemas change:
116
-
117
- ```ruby
118
- Servus::Support::Validator.clear_cache!
119
- ```
@@ -1,121 +0,0 @@
1
- # @title Features / 2. Error Handling
2
-
3
- # Error Handling
4
-
5
- Servus distinguishes between expected business failures (return failure) and unexpected system errors (raise exceptions). This separation makes error handling predictable and explicit.
6
-
7
- ## Failures vs Exceptions
8
-
9
- **Use `failure()`** for expected business conditions:
10
- - User not found
11
- - Insufficient balance
12
- - Invalid state transition
13
-
14
- **Use `error!()` or raise** for unexpected system errors:
15
- - Database connection failure
16
- - Nil reference error
17
- - External API timeout
18
-
19
- Failures return a Response object so callers can handle them. Exceptions halt execution and bubble up.
20
-
21
- ```ruby
22
- def call
23
- user = User.find_by(id: user_id)
24
- return failure("User not found", type: NotFoundError) unless user
25
- return failure("Insufficient funds") unless user.balance >= amount
26
-
27
- user.update!(balance: user.balance - amount) # Raises on system error
28
- success(user: user)
29
- end
30
- ```
31
-
32
- ## Error Classes
33
-
34
- All error classes inherit from `ServiceError` and map to HTTP status codes. Use them for API-friendly errors.
35
-
36
- ```ruby
37
- # Built-in errors
38
- NotFoundError # 404
39
- BadRequestError # 400
40
- UnauthorizedError # 401
41
- ForbiddenError # 403
42
- ValidationError # 422
43
- InternalServerError # 500
44
- ServiceUnavailableError # 503
45
-
46
- # Usage
47
- failure("Resource not found", type: NotFoundError)
48
- error!("Database corrupted", type: InternalServerError) # Raises exception
49
- ```
50
-
51
- Each error has an `api_error` method returning `{ code: :symbol, message: "string" }` for JSON APIs.
52
-
53
- ## Declarative Exception Handling
54
-
55
- Use `rescue_from` to convert specific exceptions into failures. Original exception details are preserved in error messages.
56
-
57
- ```ruby
58
- class CallExternalApi::Service < Servus::Base
59
- rescue_from Net::HTTPError, Timeout::Error use: ServiceUnavailableError
60
- rescue_from JSON::ParserError, use: BadRequestError
61
-
62
- def call
63
- response = http_client.get(url) # May raise
64
- data = JSON.parse(response.body) # May raise
65
- success(data: data)
66
- end
67
- end
68
-
69
- # If Net::HTTPError is raised, service returns:
70
- # Response(success: false, error: ServiceUnavailableError("[Net::HTTPError]: original message"))
71
- ```
72
-
73
- The `rescue_from` pattern keeps business logic clean while ensuring consistent error handling across services.
74
-
75
- ### Custom Error Handling with Blocks
76
-
77
- For more control over error handling, provide a block to `rescue_from`. The block receives the exception and can return either success or failure:
78
-
79
- ```ruby
80
- class ProcessPayment::Service < Servus::Base
81
- # Custom failure with error details
82
- rescue_from ActiveRecord::RecordInvalid do |exception|
83
- failure("Payment failed: #{exception.record.errors.full_messages.join(', ')}",
84
- type: ValidationError)
85
- end
86
-
87
- # Recover from certain errors with success
88
- rescue_from Stripe::CardError do |exception|
89
- if exception.code == 'card_declined'
90
- failure("Card was declined", type: BadRequestError)
91
- else
92
- # Log and continue for other card errors
93
- Rails.logger.warn("Stripe error: #{exception.message}")
94
- success(recovered: true, fallback_used: true)
95
- end
96
- end
97
-
98
- def call
99
- # Service logic that may raise exceptions
100
- end
101
- end
102
- ```
103
-
104
- The block has access to `success(data)` and `failure(message, type:)` methods. This allows conditional error handling and even recovering from exceptions.
105
-
106
- ## Custom Errors
107
-
108
- Create domain-specific errors by inheriting from `ServiceError`:
109
-
110
- ```ruby
111
- class InsufficientFundsError < Servus::Support::Errors::ServiceError
112
- DEFAULT_MESSAGE = "Insufficient funds"
113
-
114
- def api_error
115
- { code: :insufficient_funds, message: message }
116
- end
117
- end
118
-
119
- # Usage
120
- failure("Account balance too low", type: InsufficientFundsError)
121
- ```
@@ -1,81 +0,0 @@
1
- # @title Features / 3. Async Execution
2
-
3
- # Async Execution
4
-
5
- Servus provides asynchronous execution via ActiveJob. Services run identically whether called sync or async - they're unaware of execution context.
6
-
7
- ## Usage
8
-
9
- Call `.call_async(**args)` instead of `.call(**args)` to execute in the background. The service is enqueued immediately and executed by a worker.
10
-
11
- ```ruby
12
- # Synchronous
13
- result = ProcessReport::Service.call(user_id: user.id, report_type: :monthly)
14
- result.data[:report] # Available immediately
15
-
16
- # Asynchronous
17
- ProcessReport::Service.call_async(user_id: user.id, report_type: :monthly)
18
- # Returns true if enqueued successfully
19
- # Result not available (service hasn't run yet)
20
- ```
21
-
22
- Services must accept JSON-serializable arguments for async execution (primitives, hashes, arrays, ActiveRecord objects via GlobalID). Complex objects like Procs won't work.
23
-
24
- ## Queue and Scheduling Options
25
-
26
- Pass ActiveJob options to control execution:
27
-
28
- ```ruby
29
- ProcessReport::Service.call_async(
30
- user_id: user.id,
31
- queue: :critical, # Specify queue
32
- priority: 10, # Higher priority
33
- wait: 5.minutes # Delay execution
34
- )
35
- ```
36
-
37
- ## Result Handling
38
-
39
- Async services can't return results to callers (the service hasn't executed yet). If you need results, implement persistence in the service:
40
-
41
- ```ruby
42
- class GenerateReport::Service < Servus::Base
43
- def call
44
- report_data = generate_report
45
-
46
- # Persist result
47
- Report.create!(
48
- user_id: @user_id,
49
- data: report_data,
50
- status: 'completed'
51
- )
52
-
53
- # Optionally notify user
54
- UserMailer.report_ready(@user_id).deliver_now
55
-
56
- success(data: report_data)
57
- end
58
- end
59
-
60
- # Controller creates placeholder, service updates it
61
- report = Report.create!(user_id: user.id, status: 'pending')
62
- GenerateReport::Service.call_async(user_id: user.id, report_id: report.id)
63
- ```
64
-
65
- ## Error Handling
66
-
67
- Failures (business logic) don't trigger retries - the job completes successfully but returns a failure Response.
68
-
69
- Exceptions (system errors) trigger ActiveJob retry logic. Use `rescue_from` to convert transient errors into exceptions:
70
-
71
- ```ruby
72
- class Service < Servus::Base
73
- rescue_from Net::HTTPError, Timeout::Error use: ServiceUnavailableError
74
- end
75
- ```
76
-
77
- ## When to Use Async
78
-
79
- **Good candidates**: Email sending, report generation, data imports, long-running API calls, cleanup tasks
80
-
81
- **Poor candidates**: Operations requiring immediate feedback, fast operations (<100ms), critical path operations where user waits for result