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.
Files changed (117) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +6 -0
  3. data/CHANGELOG.md +8 -1
  4. data/IDEAS.md +5 -0
  5. data/READme.md +147 -42
  6. data/Rakefile +33 -0
  7. data/builds/servus-0.1.2.gem +0 -0
  8. data/builds/servus-0.1.3.gem +0 -0
  9. data/builds/servus-0.1.4.gem +0 -0
  10. data/docs/core/1_overview.md +77 -0
  11. data/docs/core/2_architecture.md +92 -0
  12. data/docs/core/3_service_objects.md +121 -0
  13. data/docs/features/1_schema_validation.md +119 -0
  14. data/docs/features/2_error_handling.md +121 -0
  15. data/docs/features/3_async_execution.md +81 -0
  16. data/docs/features/4_logging.md +64 -0
  17. data/docs/guides/1_common_patterns.md +90 -0
  18. data/docs/guides/2_migration_guide.md +175 -0
  19. data/docs/integration/1_configuration.md +51 -0
  20. data/docs/integration/2_testing.md +164 -0
  21. data/docs/integration/3_rails_integration.md +99 -0
  22. data/docs/yard/Servus/Base.html +1645 -0
  23. data/docs/yard/Servus/Config.html +582 -0
  24. data/docs/yard/Servus/Extensions/Async/Call.html +400 -0
  25. data/docs/yard/Servus/Extensions/Async/Errors/AsyncError.html +140 -0
  26. data/docs/yard/Servus/Extensions/Async/Errors/JobEnqueueError.html +154 -0
  27. data/docs/yard/Servus/Extensions/Async/Errors/ServiceNotFoundError.html +154 -0
  28. data/docs/yard/Servus/Extensions/Async/Errors.html +128 -0
  29. data/docs/yard/Servus/Extensions/Async/Ext.html +119 -0
  30. data/docs/yard/Servus/Extensions/Async/Job.html +310 -0
  31. data/docs/yard/Servus/Extensions/Async.html +141 -0
  32. data/docs/yard/Servus/Extensions.html +117 -0
  33. data/docs/yard/Servus/Generators/ServiceGenerator.html +261 -0
  34. data/docs/yard/Servus/Generators.html +115 -0
  35. data/docs/yard/Servus/Helpers/ControllerHelpers.html +457 -0
  36. data/docs/yard/Servus/Helpers.html +115 -0
  37. data/docs/yard/Servus/Railtie.html +134 -0
  38. data/docs/yard/Servus/Support/Errors/AuthenticationError.html +287 -0
  39. data/docs/yard/Servus/Support/Errors/BadRequestError.html +283 -0
  40. data/docs/yard/Servus/Support/Errors/ForbiddenError.html +284 -0
  41. data/docs/yard/Servus/Support/Errors/InternalServerError.html +283 -0
  42. data/docs/yard/Servus/Support/Errors/NotFoundError.html +284 -0
  43. data/docs/yard/Servus/Support/Errors/ServiceError.html +489 -0
  44. data/docs/yard/Servus/Support/Errors/ServiceUnavailableError.html +290 -0
  45. data/docs/yard/Servus/Support/Errors/UnauthorizedError.html +200 -0
  46. data/docs/yard/Servus/Support/Errors/UnprocessableEntityError.html +288 -0
  47. data/docs/yard/Servus/Support/Errors/ValidationError.html +200 -0
  48. data/docs/yard/Servus/Support/Errors.html +140 -0
  49. data/docs/yard/Servus/Support/Logger.html +856 -0
  50. data/docs/yard/Servus/Support/Rescuer/BlockContext.html +585 -0
  51. data/docs/yard/Servus/Support/Rescuer/CallOverride.html +257 -0
  52. data/docs/yard/Servus/Support/Rescuer/ClassMethods.html +343 -0
  53. data/docs/yard/Servus/Support/Rescuer.html +267 -0
  54. data/docs/yard/Servus/Support/Response.html +574 -0
  55. data/docs/yard/Servus/Support/Validator.html +1150 -0
  56. data/docs/yard/Servus/Support.html +119 -0
  57. data/docs/yard/Servus/Testing/ExampleBuilders.html +523 -0
  58. data/docs/yard/Servus/Testing/ExampleExtractor.html +578 -0
  59. data/docs/yard/Servus/Testing.html +142 -0
  60. data/docs/yard/Servus.html +343 -0
  61. data/docs/yard/_index.html +535 -0
  62. data/docs/yard/class_list.html +54 -0
  63. data/docs/yard/css/common.css +1 -0
  64. data/docs/yard/css/full_list.css +58 -0
  65. data/docs/yard/css/style.css +503 -0
  66. data/docs/yard/file.1_common_patterns.html +154 -0
  67. data/docs/yard/file.1_configuration.html +115 -0
  68. data/docs/yard/file.1_overview.html +142 -0
  69. data/docs/yard/file.1_schema_validation.html +188 -0
  70. data/docs/yard/file.2_architecture.html +157 -0
  71. data/docs/yard/file.2_error_handling.html +190 -0
  72. data/docs/yard/file.2_migration_guide.html +242 -0
  73. data/docs/yard/file.2_testing.html +227 -0
  74. data/docs/yard/file.3_async_execution.html +145 -0
  75. data/docs/yard/file.3_rails_integration.html +160 -0
  76. data/docs/yard/file.3_service_objects.html +191 -0
  77. data/docs/yard/file.4_logging.html +135 -0
  78. data/docs/yard/file.ErrorHandling.html +190 -0
  79. data/docs/yard/file.READme.html +674 -0
  80. data/docs/yard/file.architecture.html +157 -0
  81. data/docs/yard/file.async_execution.html +145 -0
  82. data/docs/yard/file.common_patterns.html +154 -0
  83. data/docs/yard/file.configuration.html +115 -0
  84. data/docs/yard/file.error_handling.html +190 -0
  85. data/docs/yard/file.logging.html +135 -0
  86. data/docs/yard/file.migration_guide.html +242 -0
  87. data/docs/yard/file.overview.html +142 -0
  88. data/docs/yard/file.rails_integration.html +160 -0
  89. data/docs/yard/file.schema_validation.html +188 -0
  90. data/docs/yard/file.service_objects.html +191 -0
  91. data/docs/yard/file.testing.html +227 -0
  92. data/docs/yard/file_list.html +119 -0
  93. data/docs/yard/frames.html +22 -0
  94. data/docs/yard/index.html +674 -0
  95. data/docs/yard/js/app.js +344 -0
  96. data/docs/yard/js/full_list.js +242 -0
  97. data/docs/yard/js/jquery.js +4 -0
  98. data/docs/yard/method_list.html +542 -0
  99. data/docs/yard/top-level-namespace.html +110 -0
  100. data/lib/generators/servus/service/service_generator.rb +64 -1
  101. data/lib/generators/servus/service/templates/service.rb.erb +1 -1
  102. data/lib/servus/base.rb +258 -57
  103. data/lib/servus/config.rb +58 -12
  104. data/lib/servus/extensions/async/call.rb +50 -18
  105. data/lib/servus/extensions/async/errors.rb +23 -3
  106. data/lib/servus/extensions/async/ext.rb +10 -2
  107. data/lib/servus/extensions/async/job.rb +32 -11
  108. data/lib/servus/helpers/controller_helpers.rb +73 -37
  109. data/lib/servus/support/errors.rb +135 -45
  110. data/lib/servus/support/rescuer.rb +189 -36
  111. data/lib/servus/support/response.rb +49 -7
  112. data/lib/servus/support/validator.rb +120 -19
  113. data/lib/servus/testing/example_builders.rb +133 -0
  114. data/lib/servus/testing/example_extractor.rb +309 -0
  115. data/lib/servus/testing.rb +17 -0
  116. data/lib/servus/version.rb +1 -1
  117. metadata +118 -19
@@ -0,0 +1,119 @@
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
+ ```
@@ -0,0 +1,121 @@
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
+ ```
@@ -0,0 +1,81 @@
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
@@ -0,0 +1,64 @@
1
+ # @title Features / 4. Logging
2
+
3
+ # Logging
4
+
5
+ Servus automatically logs all service executions with timing information. No instrumentation code needed in services.
6
+
7
+ ## What Gets Logged
8
+
9
+ **Service calls** (DEBUG): Service class name and arguments
10
+ ```
11
+ [Servus] Users::Create::Service called with {:email=>"user@example.com", :name=>"John"}
12
+ ```
13
+
14
+ **Successful completions** (INFO): Service class name and duration
15
+ ```
16
+ [Servus] Users::Create::Service completed successfully in 0.0243s
17
+ ```
18
+
19
+ **Failures** (WARN): Service class name, error type, message, and duration
20
+ ```
21
+ [Servus] Users::Create::Service failed with NotFoundError: User not found (0.0125s)
22
+ ```
23
+
24
+ **Exceptions** (ERROR): Service class name, exception type, message, and duration
25
+ ```
26
+ [Servus] Users::Create::Service raised ArgumentError: Missing required field (0.0089s)
27
+ ```
28
+
29
+ ## Log Levels
30
+
31
+ Servus uses Rails.logger and respects application log level configuration:
32
+
33
+ - **DEBUG**: Shows arguments (use in development, hide in production to avoid logging sensitive data)
34
+ - **INFO**: Shows completions (normal operations)
35
+ - **WARN**: Shows business failures
36
+ - **ERROR**: Shows system exceptions
37
+
38
+ Set production log level to INFO to hide argument logging:
39
+
40
+ ```ruby
41
+ # config/environments/production.rb
42
+ config.log_level = :info
43
+ ```
44
+
45
+ ## Sensitive Data
46
+
47
+ Arguments are logged at DEBUG level. In production, either:
48
+ 1. Set log level to INFO (recommended)
49
+ 2. Use Rails parameter filtering: `config.filter_parameters += [:password, :ssn, :credit_card]`
50
+ 3. Pass IDs instead of full objects: `Service.call(user_id: 1)` not `Service.call(user: user_object)`
51
+
52
+ ## Integration with Logging Tools
53
+
54
+ The `[Servus]` prefix makes service logs easy to grep and filter:
55
+
56
+ ```bash
57
+ # Find all service calls
58
+ grep "\[Servus\]" production.log
59
+
60
+ # Find slow services
61
+ grep "completed" production.log | grep "Servus" | awk '{print $NF}' | sort -n
62
+ ```
63
+
64
+ Servus logs work with structured logging tools (Lograge, Datadog, Splunk) without modification.
@@ -0,0 +1,90 @@
1
+ # @title Guides / 1. Common Patterns
2
+
3
+ # Common Patterns
4
+
5
+ Common architectural patterns for using Servus effectively.
6
+
7
+ ## Parent-Child Services
8
+
9
+ When one service orchestrates multiple sub-operations, decide on transaction boundaries and error propagation.
10
+
11
+ ```ruby
12
+ class Orders::Checkout::Service < Servus::Base
13
+ def call
14
+ ActiveRecord::Base.transaction do
15
+ # Create order
16
+ order_result = Orders::Create::Service.call(order_params)
17
+ return order_result unless order_result.success?
18
+
19
+ # Charge payment
20
+ payment_result = Payments::Charge::Service.call(
21
+ user_id: @user_id,
22
+ amount: order_result.data[:order].total
23
+ )
24
+ return payment_result unless payment_result.success?
25
+
26
+ # Update inventory
27
+ inventory_result = Inventory::Reserve::Service.call(
28
+ order_id: order_result.data[:order].id
29
+ )
30
+ return inventory_result unless inventory_result.success?
31
+
32
+ success(order: order_result.data[:order])
33
+ end
34
+ end
35
+ end
36
+ ```
37
+
38
+ **Use parent transaction when**: All children must succeed or all roll back (atomic operation)
39
+
40
+ **Use child transactions when**: Children can succeed independently (partial success acceptable)
41
+
42
+ ## Async with Result Persistence
43
+
44
+ Store async results in database for later retrieval:
45
+
46
+ ```ruby
47
+ # Controller creates placeholder
48
+ report = Report.create!(user_id: user.id, status: 'pending')
49
+ GenerateReport::Service.call_async(report_id: report.id)
50
+
51
+ # Service updates record
52
+ class GenerateReport::Service < Servus::Base
53
+ def call
54
+ report = Report.find(@report_id)
55
+ data = generate_report_data
56
+
57
+ report.update!(data: data, status: 'completed')
58
+ success(report: report)
59
+ end
60
+ end
61
+ ```
62
+
63
+ ## Idempotent Services
64
+
65
+ Use database constraints to make services idempotent:
66
+
67
+ ```ruby
68
+ class Users::Create::Service < Servus::Base
69
+ def call
70
+ # Unique constraint on email prevents duplicates
71
+ user = User.create!(email: @email, name: @name)
72
+ success(user: user)
73
+ rescue ActiveRecord::RecordNotUnique
74
+ user = User.find_by!(email: @email)
75
+ success(user: user) # Return existing user, not error
76
+ end
77
+ end
78
+ ```
79
+
80
+ Or check for existing resources explicitly:
81
+
82
+ ```ruby
83
+ def call
84
+ existing = User.find_by(email: @email)
85
+ return success(user: existing) if existing
86
+
87
+ user = User.create!(email: @email, name: @name)
88
+ success(user: user)
89
+ end
90
+ ```
@@ -0,0 +1,175 @@
1
+ # @title Guides / 2. Migration Guide
2
+
3
+ # Migration Guide
4
+
5
+ Strategies for adopting Servus in existing Rails applications.
6
+
7
+ ## Incremental Adoption
8
+
9
+ Servus coexists with existing code - no need to rewrite your entire application. Start with one complex use case, validate the pattern works for your team, then expand gradually.
10
+
11
+ ## Extracting from Fat Controllers
12
+
13
+ Identify controller actions with complex business logic and extract to services:
14
+
15
+ **Before**:
16
+ ```ruby
17
+ class OrdersController < ApplicationController
18
+ def create
19
+ # 50 lines of business logic
20
+ # Multiple model operations
21
+ # External API calls
22
+ # Email sending
23
+ end
24
+ end
25
+ ```
26
+
27
+ **After**:
28
+ ```ruby
29
+ class OrdersController < ApplicationController
30
+ def create
31
+ result = Orders::Create::Service.call(order_params)
32
+ if result.success?
33
+ render json: { order: result.data[:order] }, status: :created
34
+ else
35
+ render json: { error: result.error.api_error }, status: :unprocessable_entity
36
+ end
37
+ end
38
+ end
39
+
40
+ # Or use the helper
41
+ class OrdersController < ApplicationController
42
+ include Servus::Helpers::ControllerHelpers
43
+
44
+ def create
45
+ run_service(Orders::Create::Service, order_params) do |result|
46
+ render json: { order: result.data[:order] }, status: :created
47
+ end
48
+ end
49
+ end
50
+ ```
51
+
52
+ ## Extracting from Fat Models
53
+
54
+ Move orchestration logic from models to services. Keep data-related methods in models:
55
+
56
+ **Before**:
57
+ ```ruby
58
+ class Order < ApplicationRecord
59
+ def complete_purchase
60
+ charge_payment
61
+ update_inventory
62
+ send_confirmation_email
63
+ create_invoice
64
+ end
65
+ end
66
+ ```
67
+
68
+ **After**:
69
+ ```ruby
70
+ class Order < ApplicationRecord
71
+ # Model focuses on data
72
+ validates :total, presence: true
73
+ belongs_to :user
74
+ end
75
+
76
+ class Orders::CompletePurchase::Service < Servus::Base
77
+ # Service handles orchestration
78
+ def initialize(order_id:)
79
+ @order_id = order_id
80
+ end
81
+
82
+ def call
83
+ order = Order.find(@order_id)
84
+ Payments::Charge::Service.call(order_id: order.id)
85
+ Inventory::Update::Service.call(order_id: order.id)
86
+ Mailers::SendConfirmation::Service.call(order_id: order.id)
87
+ success(order: order)
88
+ end
89
+ end
90
+ ```
91
+
92
+ ## Replacing Callbacks
93
+
94
+ Extract callback logic to explicit service calls:
95
+
96
+ **Before**:
97
+ ```ruby
98
+ class User < ApplicationRecord
99
+ after_create :send_welcome_email
100
+ after_create :create_default_account
101
+ after_update :notify_changes, if: :email_changed?
102
+ end
103
+ ```
104
+
105
+ **After**:
106
+ ```ruby
107
+ class User < ApplicationRecord
108
+ # Minimal or no callbacks
109
+ end
110
+
111
+ class Users::Create::Service < Servus::Base
112
+ def call
113
+ user = User.create!(params)
114
+ send_welcome_email(user)
115
+ create_default_account(user)
116
+ success(user: user)
117
+ end
118
+ end
119
+ ```
120
+
121
+ ## Migrating Background Jobs
122
+
123
+ Extract job logic to services, call via `.call_async`:
124
+
125
+ **Before**:
126
+ ```ruby
127
+ class ProcessOrderJob < ApplicationJob
128
+ def perform(order_id)
129
+ # 50 lines of business logic
130
+ end
131
+ end
132
+
133
+ ProcessOrderJob.perform_later(order.id)
134
+ ```
135
+
136
+ **After**:
137
+ ```ruby
138
+ class Orders::Process::Service < Servus::Base
139
+ def initialize(order_id:)
140
+ @order_id = order_id
141
+ end
142
+
143
+ def call
144
+ # Business logic
145
+ end
146
+ end
147
+
148
+ Orders::Process::Service.call_async(order_id: order.id)
149
+ ```
150
+
151
+ Now the service can be called synchronously (from console, tests) or asynchronously (from controllers, jobs).
152
+
153
+ ## Testing During Migration
154
+
155
+ Keep existing tests working while adding service tests:
156
+
157
+ ```ruby
158
+ # Keep existing controller test
159
+ describe OrdersController do
160
+ it "creates order" do
161
+ post :create, params: params
162
+ expect(response).to be_successful
163
+ end
164
+ end
165
+
166
+ # Add service test
167
+ describe Orders::Create::Service do
168
+ it "creates order" do
169
+ result = described_class.call(params)
170
+ expect(result.success?).to be true
171
+ end
172
+ end
173
+ ```
174
+
175
+ Remove legacy tests after service tests prove comprehensive.
@@ -0,0 +1,51 @@
1
+ # @title Integration / 1. Configuration
2
+
3
+ # Configuration
4
+
5
+ Servus works without configuration. One optional setting exists for schema file location.
6
+
7
+ ## Schema Root
8
+
9
+ By default, Servus looks for schema JSON files in `Rails.root/app/schemas/services` (or `./app/schemas/services` in non-Rails apps).
10
+
11
+ Change the location if needed:
12
+
13
+ ```ruby
14
+ # config/initializers/servus.rb
15
+ Servus.configure do |config|
16
+ config.schema_root = Rails.root.join('config/schemas')
17
+ end
18
+ ```
19
+
20
+ This affects legacy file-based schemas only - schemas defined via the `schema` DSL method do not use files.
21
+
22
+ ## Schema Cache
23
+
24
+ Schemas are cached after first load for performance. Clear the cache during development when schemas change:
25
+
26
+ ```ruby
27
+ Servus::Support::Validator.clear_cache!
28
+ ```
29
+
30
+ In production, schemas are deployed with code - no need to clear cache.
31
+
32
+ ## Log Level
33
+
34
+ Servus uses `Rails.logger` (or stdout in non-Rails apps). Control logging via Rails configuration:
35
+
36
+ ```ruby
37
+ # config/environments/production.rb
38
+ config.log_level = :info # Hides DEBUG argument logs
39
+ ```
40
+
41
+ ## ActiveJob Configuration
42
+
43
+ Async execution uses ActiveJob. Configure your adapter:
44
+
45
+ ```ruby
46
+ # config/application.rb
47
+ config.active_job.queue_adapter = :sidekiq
48
+ config.active_job.default_queue_name = :default
49
+ ```
50
+
51
+ Servus respects ActiveJob queue configuration - no Servus-specific setup needed.