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
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# @title Integration / 2. Testing
|
|
2
|
+
|
|
3
|
+
# Testing
|
|
4
|
+
|
|
5
|
+
Services are designed for easy testing with explicit inputs (arguments) and outputs (Response objects). No special test infrastructure needed.
|
|
6
|
+
|
|
7
|
+
## Schema Example Helpers
|
|
8
|
+
|
|
9
|
+
Servus provides test helpers that extract `example` values from your JSON schemas, making it easy to generate test fixtures without maintaining separate factories.
|
|
10
|
+
|
|
11
|
+
### Setup
|
|
12
|
+
|
|
13
|
+
Include the helpers in your test suite:
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
# spec/spec_helper.rb
|
|
17
|
+
require 'servus/testing'
|
|
18
|
+
|
|
19
|
+
RSpec.configure do |config|
|
|
20
|
+
config.include Servus::Testing::ExampleBuilders
|
|
21
|
+
end
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Using Schema Examples
|
|
25
|
+
|
|
26
|
+
Add `example` or `examples` keywords to your schemas:
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
class ProcessPayment::Service < Servus::Base
|
|
30
|
+
schema(
|
|
31
|
+
arguments: {
|
|
32
|
+
type: "object",
|
|
33
|
+
properties: {
|
|
34
|
+
user_id: { type: "integer", example: 123 },
|
|
35
|
+
amount: { type: "number", example: 100.0 },
|
|
36
|
+
currency: { type: "string", examples: ["USD", "EUR", "GBP"] }
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
result: {
|
|
40
|
+
type: "object",
|
|
41
|
+
properties: {
|
|
42
|
+
transaction_id: { type: "string", example: "txn_abc123" },
|
|
43
|
+
status: { type: "string", example: "approved" }
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Then use the helpers in your tests:
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
RSpec.describe ProcessPayment::Service do
|
|
54
|
+
it "processes payment successfully" do
|
|
55
|
+
# Extract examples from schema and override specific values
|
|
56
|
+
args = servus_arguments_example(ProcessPayment::Service, amount: 50.0)
|
|
57
|
+
# => { user_id: 123, amount: 50.0, currency: "USD" }
|
|
58
|
+
|
|
59
|
+
result = ProcessPayment::Service.call(**args)
|
|
60
|
+
|
|
61
|
+
expect(result).to be_success
|
|
62
|
+
expect(result.data.keys).to match_array(
|
|
63
|
+
servus_result_example(ProcessPayment::Service).data.keys
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it "handles different currencies" do
|
|
68
|
+
%w[USD EUR GBP].each do |currency|
|
|
69
|
+
result = ProcessPayment::Service.call(
|
|
70
|
+
**servus_arguments_example(ProcessPayment::Service, currency: currency)
|
|
71
|
+
)
|
|
72
|
+
expect(result).to be_success
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Deep Merging
|
|
79
|
+
|
|
80
|
+
Overrides are deep-merged with schema examples, allowing you to override nested values:
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
# Schema has nested structure
|
|
84
|
+
args = servus_arguments_example(
|
|
85
|
+
CreateUser::Service,
|
|
86
|
+
user: { profile: { age: 35 } }
|
|
87
|
+
)
|
|
88
|
+
# => { user: { id: 1, profile: { name: 'Alice', age: 35 } } }
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Available Helpers
|
|
92
|
+
|
|
93
|
+
- `servus_arguments_example(ServiceClass, **overrides)` - Returns hash of argument examples
|
|
94
|
+
- `servus_result_example(ServiceClass, **overrides)` - Returns Response object with result examples
|
|
95
|
+
|
|
96
|
+
## Basic Testing Pattern
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
RSpec.describe ProcessPayment::Service do
|
|
100
|
+
describe ".call" do
|
|
101
|
+
let(:user) { create(:user, balance: 1000) }
|
|
102
|
+
|
|
103
|
+
subject(:result) { described_class.call(user_id: user.id, amount: amount) }
|
|
104
|
+
|
|
105
|
+
context "with sufficient balance" do
|
|
106
|
+
let(:amount) { 50 }
|
|
107
|
+
|
|
108
|
+
it "processes payment" do
|
|
109
|
+
expect(result.success?).to be true
|
|
110
|
+
expect(result.data[:new_balance]).to eq(950)
|
|
111
|
+
expect(result.reload.balance).to eq(950)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
context "with insufficient balance" do
|
|
116
|
+
let(:amount) { 2000 }
|
|
117
|
+
|
|
118
|
+
it "returns failure" do
|
|
119
|
+
expect(result.success?).to be false
|
|
120
|
+
expect(result.error.message).to eq("Insufficient funds")
|
|
121
|
+
expect(result.error).to be_a(Servus::Support::Errors::ServiceError)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Testing Service Composition
|
|
129
|
+
|
|
130
|
+
When testing services that call other services, mock the child services:
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
describe Users::CreateWithAccount::Service do
|
|
134
|
+
# Make local or global helpers to clean up tests
|
|
135
|
+
def servus_success_result(data)
|
|
136
|
+
Servus::Support::Response.new(success: true, data: data, error: nil)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
it "calls both create services" do
|
|
140
|
+
# Mock child services
|
|
141
|
+
allow(Users::Create::Service).to receive(:call).and_return(servus_success_result{ user: user })
|
|
142
|
+
allow(Accounts::Create::Service).to receive(:call).and_return(servus_success_result{ account: account })
|
|
143
|
+
|
|
144
|
+
result = described_class.call(email: "test@example.com", plan: "premium")
|
|
145
|
+
|
|
146
|
+
expect(Users::Create::Service).to have_received(:call)
|
|
147
|
+
expect(Accounts::Create::Service).to have_received(:call)
|
|
148
|
+
|
|
149
|
+
expect(result.success?).to be true
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Testing Schema Validation
|
|
155
|
+
|
|
156
|
+
Don't test that valid arguments pass validation - that's testing the framework. Do test that your schema catches invalid inputs:
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
it "validates required fields" do
|
|
160
|
+
expect {
|
|
161
|
+
Service.call(invalid: "params")
|
|
162
|
+
}.to raise_error(Servus::Support::Errors::ValidationError, /required/)
|
|
163
|
+
end
|
|
164
|
+
```
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# @title Integration / 3. Rails Integration
|
|
2
|
+
|
|
3
|
+
# Rails Integration
|
|
4
|
+
|
|
5
|
+
Servus core works in any Ruby application. Rails-specific features (async, controller helpers, generators) are optional additions that integrate with Rails conventions.
|
|
6
|
+
|
|
7
|
+
## Controller Integration
|
|
8
|
+
|
|
9
|
+
Use the `run_service` helper to call services from controllers with automatic error handling:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
class UsersController < ApplicationController
|
|
13
|
+
include Servus::Helpers::ControllerHelpers
|
|
14
|
+
|
|
15
|
+
def create
|
|
16
|
+
run_service(Users::Create::Service, user_params)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Failures automatically render JSON:
|
|
20
|
+
# { "error": { "code": "validation_error", "message": "..." } }
|
|
21
|
+
# with appropriate HTTP status code
|
|
22
|
+
#
|
|
23
|
+
# Success will go to view and service result will be available on @result
|
|
24
|
+
end
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Without the helper, handle responses manually:
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
def create
|
|
31
|
+
result = Users::Create::Service.call(user_params)
|
|
32
|
+
if result.success?
|
|
33
|
+
render json: { user: result.data[:user] }, status: :created
|
|
34
|
+
else
|
|
35
|
+
render json: { error: result.error.api_error }, status: error_status(result.error)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Generator
|
|
41
|
+
|
|
42
|
+
Generate services with specs and schema files:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
rails generate servus:service process_payment
|
|
46
|
+
|
|
47
|
+
# Creates:
|
|
48
|
+
# app/services/process_payment/service.rb
|
|
49
|
+
# spec/services/process_payment/service_spec.rb
|
|
50
|
+
# app/schemas/services/process_payment/arguments.json
|
|
51
|
+
# app/schemas/services/process_payment/result.json
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Schema files are optional - delete them if you don't need validation.
|
|
55
|
+
|
|
56
|
+
## Autoloading
|
|
57
|
+
|
|
58
|
+
Servus follows Rails autoloading conventions. Services in `app/services/` are automatically loaded by Rails:
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
# app/services/users/create/service.rb
|
|
62
|
+
module Users
|
|
63
|
+
module Create
|
|
64
|
+
class Service < Servus::Base
|
|
65
|
+
# ...
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Rails autoloads this as Users::Create::Service
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Configuration
|
|
74
|
+
|
|
75
|
+
Configure Servus in an initializer if needed:
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
# config/initializers/servus.rb
|
|
79
|
+
Servus.configure do |config|
|
|
80
|
+
config.schema_root = Rails.root.join('config/schemas')
|
|
81
|
+
end
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Most applications don't need any configuration.
|
|
85
|
+
|
|
86
|
+
## Background Jobs
|
|
87
|
+
|
|
88
|
+
Async execution requires ActiveJob setup. Configure your adapter:
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
# config/application.rb
|
|
92
|
+
config.active_job.queue_adapter = :sidekiq
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Then use `.call_async`:
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
Users::SendWelcomeEmail::Service.call_async(user_id: user.id)
|
|
99
|
+
```
|