servus 0.1.3 → 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 +7 -0
- data/IDEAS.md +5 -0
- data/READme.md +147 -42
- data/Rakefile +33 -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 +30 -9
- 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 +117 -19
|
@@ -2,12 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
module Servus
|
|
4
4
|
module Generators
|
|
5
|
-
# Servus
|
|
5
|
+
# Rails generator for creating Servus service objects.
|
|
6
|
+
#
|
|
7
|
+
# Generates a complete service structure including:
|
|
8
|
+
# - Service class file
|
|
9
|
+
# - RSpec test file
|
|
10
|
+
# - JSON schema files for arguments and results
|
|
11
|
+
#
|
|
12
|
+
# @example Generate a service
|
|
13
|
+
# rails g servus:service namespace/do_something_helpful user amount
|
|
14
|
+
#
|
|
15
|
+
# @example Generated files
|
|
16
|
+
# app/services/namespace/do_something_helpful/service.rb
|
|
17
|
+
# spec/services/namespace/do_something_helpful/service_spec.rb
|
|
18
|
+
# app/schemas/services/namespace/do_something_helpful/arguments.json
|
|
19
|
+
# app/schemas/services/namespace/do_something_helpful/result.json
|
|
20
|
+
#
|
|
21
|
+
# @see https://guides.rubyonrails.org/generators.html
|
|
6
22
|
class ServiceGenerator < Rails::Generators::NamedBase
|
|
7
23
|
source_root File.expand_path('templates', __dir__)
|
|
8
24
|
|
|
9
25
|
argument :parameters, type: :array, default: [], banner: 'parameter'
|
|
10
26
|
|
|
27
|
+
# Creates all service-related files.
|
|
28
|
+
#
|
|
29
|
+
# Generates the service class, spec file, and schema files from templates.
|
|
30
|
+
#
|
|
31
|
+
# @return [void]
|
|
11
32
|
def create_service_file
|
|
12
33
|
template 'service.rb.erb', service_path
|
|
13
34
|
template 'service_spec.rb.erb', service_path_spec
|
|
@@ -19,40 +40,82 @@ module Servus
|
|
|
19
40
|
|
|
20
41
|
private
|
|
21
42
|
|
|
43
|
+
# Returns the path for the service file.
|
|
44
|
+
#
|
|
45
|
+
# @return [String] service file path
|
|
46
|
+
# @api private
|
|
22
47
|
def service_path
|
|
23
48
|
"app/services/#{file_path}/service.rb"
|
|
24
49
|
end
|
|
25
50
|
|
|
51
|
+
# Returns the path for the service spec file.
|
|
52
|
+
#
|
|
53
|
+
# @return [String] spec file path
|
|
54
|
+
# @api private
|
|
26
55
|
def service_path_spec
|
|
27
56
|
"spec/services/#{file_path}/service_spec.rb"
|
|
28
57
|
end
|
|
29
58
|
|
|
59
|
+
# Returns the path for the result schema file.
|
|
60
|
+
#
|
|
61
|
+
# @return [String] result schema path
|
|
62
|
+
# @api private
|
|
30
63
|
def service_result_schema_path
|
|
31
64
|
"app/schemas/services/#{file_path}/result.json"
|
|
32
65
|
end
|
|
33
66
|
|
|
67
|
+
# Returns the path for the arguments schema file.
|
|
68
|
+
#
|
|
69
|
+
# @return [String] arguments schema path
|
|
70
|
+
# @api private
|
|
34
71
|
def service_arguments_shecma_path
|
|
35
72
|
"app/schemas/services/#{file_path}/arguments.json"
|
|
36
73
|
end
|
|
37
74
|
|
|
75
|
+
# Returns the service class name with ::Service appended.
|
|
76
|
+
#
|
|
77
|
+
# @return [String] service class name
|
|
78
|
+
# @api private
|
|
38
79
|
def service_class_name
|
|
39
80
|
"#{class_name}::Service"
|
|
40
81
|
end
|
|
41
82
|
|
|
83
|
+
# Returns the fully-qualified service class name.
|
|
84
|
+
#
|
|
85
|
+
# @return [String] fully-qualified class name
|
|
86
|
+
# @api private
|
|
42
87
|
def service_full_class_name
|
|
43
88
|
service_class_name.include?('::') ? service_class_name : "::#{service_class_name}"
|
|
44
89
|
end
|
|
45
90
|
|
|
91
|
+
# Generates the parameter list for the initialize method.
|
|
92
|
+
#
|
|
93
|
+
# @return [String] parameter list with keyword syntax
|
|
94
|
+
# @example
|
|
95
|
+
# parameter_list # => "(user:, amount:)"
|
|
96
|
+
# @api private
|
|
46
97
|
def parameter_list
|
|
47
98
|
return '' if parameters.empty?
|
|
48
99
|
|
|
49
100
|
"(#{parameters.map { |param| "#{param}:" }.join(', ')})"
|
|
50
101
|
end
|
|
51
102
|
|
|
103
|
+
# Generates instance variable assignments for initialize method.
|
|
104
|
+
#
|
|
105
|
+
# @return [String] multi-line instance variable assignments
|
|
106
|
+
# @example
|
|
107
|
+
# initialize_params # => "@user = user\n @amount = amount"
|
|
108
|
+
# @api private
|
|
52
109
|
def initialize_params
|
|
53
110
|
parameters.map { |param| "@#{param} = #{param}" }.join("\n ")
|
|
54
111
|
end
|
|
55
112
|
|
|
113
|
+
# Generates attr_reader declarations for parameters.
|
|
114
|
+
#
|
|
115
|
+
# @return [String] attr_reader declaration or empty string
|
|
116
|
+
# @example
|
|
117
|
+
# attr_readers # => "attr_reader :user, :amount"
|
|
118
|
+
# @api private
|
|
56
119
|
def attr_readers
|
|
57
120
|
return '' if parameters.empty?
|
|
58
121
|
|
data/lib/servus/base.rb
CHANGED
|
@@ -1,7 +1,50 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Servus
|
|
4
|
-
# Base class for all
|
|
4
|
+
# Base class for all service objects in the Servus framework.
|
|
5
|
+
#
|
|
6
|
+
# This class provides the foundational functionality for implementing the Service Object pattern,
|
|
7
|
+
# including automatic validation, logging, benchmarking, and error handling.
|
|
8
|
+
#
|
|
9
|
+
# @abstract Subclass and implement initialize and call methods to create a service
|
|
10
|
+
#
|
|
11
|
+
# @example Creating a basic service
|
|
12
|
+
# class Services::ProcessPayment::Service < Servus::Base
|
|
13
|
+
# def initialize(user:, amount:, payment_method:)
|
|
14
|
+
# @user = user
|
|
15
|
+
# @amount = amount
|
|
16
|
+
# @payment_method = payment_method
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# def call
|
|
20
|
+
# return failure("Invalid amount") if @amount <= 0
|
|
21
|
+
#
|
|
22
|
+
# transaction = charge_payment
|
|
23
|
+
# success({ transaction_id: transaction.id })
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
# private
|
|
27
|
+
#
|
|
28
|
+
# def charge_payment
|
|
29
|
+
# # Payment processing logic
|
|
30
|
+
# end
|
|
31
|
+
# end
|
|
32
|
+
#
|
|
33
|
+
# @example Using a service
|
|
34
|
+
# result = Services::ProcessPayment::Service.call(
|
|
35
|
+
# user: current_user,
|
|
36
|
+
# amount: 100,
|
|
37
|
+
# payment_method: "credit_card"
|
|
38
|
+
# )
|
|
39
|
+
#
|
|
40
|
+
# if result.success?
|
|
41
|
+
# puts "Transaction ID: #{result.data[:transaction_id]}"
|
|
42
|
+
# else
|
|
43
|
+
# puts "Error: #{result.error.message}"
|
|
44
|
+
# end
|
|
45
|
+
#
|
|
46
|
+
# @see Servus::Support::Response
|
|
47
|
+
# @see Servus::Support::Errors
|
|
5
48
|
class Base
|
|
6
49
|
include Servus::Support::Errors
|
|
7
50
|
include Servus::Support::Rescuer
|
|
@@ -11,84 +54,242 @@ module Servus
|
|
|
11
54
|
Response = Servus::Support::Response
|
|
12
55
|
Validator = Servus::Support::Validator
|
|
13
56
|
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
# @
|
|
20
|
-
# @
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
raise e
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
# Returns a success response
|
|
57
|
+
# Creates a successful response with the provided data.
|
|
58
|
+
#
|
|
59
|
+
# Use this method to return successful results from your service's call method.
|
|
60
|
+
# The data will be validated against the RESULT_SCHEMA if one is defined.
|
|
61
|
+
#
|
|
62
|
+
# @param data [Object] the data to return in the response (typically a Hash)
|
|
63
|
+
# @return [Servus::Support::Response] response with success: true and the provided data
|
|
64
|
+
#
|
|
65
|
+
# @example Returning simple data
|
|
66
|
+
# def call
|
|
67
|
+
# success({ user_id: 123, status: "active" })
|
|
68
|
+
# end
|
|
69
|
+
#
|
|
70
|
+
# @example Returning nil for operations without data
|
|
71
|
+
# def call
|
|
72
|
+
# perform_action
|
|
73
|
+
# success(nil)
|
|
74
|
+
# end
|
|
36
75
|
#
|
|
37
|
-
# @
|
|
38
|
-
# @
|
|
76
|
+
# @see #failure
|
|
77
|
+
# @see Servus::Support::Response
|
|
39
78
|
def success(data)
|
|
40
79
|
Response.new(true, data, nil)
|
|
41
80
|
end
|
|
42
81
|
|
|
43
|
-
#
|
|
82
|
+
# Creates a failure response with an error.
|
|
44
83
|
#
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
#
|
|
84
|
+
# Use this method to return failure results from your service's call method.
|
|
85
|
+
# The failure is logged automatically and returns a response containing the error.
|
|
86
|
+
#
|
|
87
|
+
# @param message [String, nil] custom error message (uses error type's default if nil)
|
|
88
|
+
# @param type [Class] error class to instantiate (must inherit from ServiceError)
|
|
89
|
+
# @return [Servus::Support::Response] response with success: false and the error
|
|
90
|
+
#
|
|
91
|
+
# @example Using default error type with custom message
|
|
92
|
+
# def call
|
|
93
|
+
# return failure("User not found") unless user_exists?
|
|
94
|
+
# # ...
|
|
95
|
+
# end
|
|
96
|
+
#
|
|
97
|
+
# @example Using custom error type
|
|
98
|
+
# def call
|
|
99
|
+
# return failure("Invalid payment", type: Servus::Support::Errors::BadRequestError)
|
|
100
|
+
# # ...
|
|
101
|
+
# end
|
|
102
|
+
#
|
|
103
|
+
# @example Using error type's default message
|
|
104
|
+
# def call
|
|
105
|
+
# return failure(type: Servus::Support::Errors::NotFoundError)
|
|
106
|
+
# # Uses "Not found" as the message
|
|
107
|
+
# end
|
|
108
|
+
#
|
|
109
|
+
# @see #success
|
|
110
|
+
# @see #error!
|
|
111
|
+
# @see Servus::Support::Errors
|
|
48
112
|
def failure(message = nil, type: Servus::Support::Errors::ServiceError)
|
|
49
113
|
error = type.new(message)
|
|
50
114
|
Response.new(false, nil, error)
|
|
51
115
|
end
|
|
52
116
|
|
|
53
|
-
#
|
|
117
|
+
# Logs an error and raises an exception, halting service execution.
|
|
118
|
+
#
|
|
119
|
+
# Use this method when you need to immediately halt execution with an exception
|
|
120
|
+
# rather than returning a failure response. The error is automatically logged before
|
|
121
|
+
# the exception is raised.
|
|
54
122
|
#
|
|
55
|
-
# @param message [String]
|
|
56
|
-
# @param type [Class]
|
|
123
|
+
# @param message [String, nil] error message for the exception (uses default if nil)
|
|
124
|
+
# @param type [Class] error class to raise (must inherit from ServiceError)
|
|
57
125
|
# @return [void]
|
|
126
|
+
# @raise [Servus::Support::Errors::ServiceError] the specified error type
|
|
127
|
+
#
|
|
128
|
+
# @example Raising an error with custom message
|
|
129
|
+
# def call
|
|
130
|
+
# error!("Critical system failure") if system_down?
|
|
131
|
+
# end
|
|
132
|
+
#
|
|
133
|
+
# @example Raising with specific error type
|
|
134
|
+
# def call
|
|
135
|
+
# error!("Unauthorized access", type: Servus::Support::Errors::UnauthorizedError)
|
|
136
|
+
# end
|
|
137
|
+
#
|
|
138
|
+
# @note Prefer {#failure} for expected error conditions. Use this for exceptional cases.
|
|
139
|
+
# @see #failure
|
|
58
140
|
def error!(message = nil, type: Servus::Support::Errors::ServiceError)
|
|
59
141
|
Logger.log_exception(self.class, type.new(message))
|
|
60
142
|
raise type, message
|
|
61
143
|
end
|
|
62
144
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
145
|
+
class << self
|
|
146
|
+
# Executes the service with automatic validation, logging, and benchmarking.
|
|
147
|
+
#
|
|
148
|
+
# This is the primary entry point for executing services. It handles the complete
|
|
149
|
+
# service lifecycle including:
|
|
150
|
+
# - Input argument validation against schema
|
|
151
|
+
# - Service instantiation
|
|
152
|
+
# - Execution timing/benchmarking
|
|
153
|
+
# - Result validation against schema
|
|
154
|
+
# - Automatic logging of calls, results, and errors
|
|
155
|
+
#
|
|
156
|
+
# @param args [Hash] keyword arguments passed to the service's initialize method
|
|
157
|
+
# @return [Servus::Support::Response] response object with success status and data or error
|
|
158
|
+
#
|
|
159
|
+
# @raise [Servus::Support::Errors::ValidationError] if input arguments fail schema validation
|
|
160
|
+
# @raise [Servus::Support::Errors::ValidationError] if result data fails schema validation
|
|
161
|
+
# @raise [StandardError] if an uncaught exception occurs during execution
|
|
162
|
+
#
|
|
163
|
+
# @example Successful execution
|
|
164
|
+
# result = MyService.call(user_id: 123, amount: 50)
|
|
165
|
+
# result.success? # => true
|
|
166
|
+
# result.data # => { transaction_id: "abc123" }
|
|
167
|
+
#
|
|
168
|
+
# @example Failed execution
|
|
169
|
+
# result = MyService.call(user_id: 123, amount: -10)
|
|
170
|
+
# result.success? # => false
|
|
171
|
+
# result.error.message # => "Amount must be positive"
|
|
172
|
+
#
|
|
173
|
+
# @see #initialize
|
|
174
|
+
# @see #call
|
|
175
|
+
def call(**args)
|
|
176
|
+
before_call(args)
|
|
177
|
+
result = benchmark(**args) { new(**args).call }
|
|
178
|
+
after_call(result)
|
|
71
179
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
180
|
+
result
|
|
181
|
+
rescue Servus::Support::Errors::ValidationError => e
|
|
182
|
+
Logger.log_validation_error(self, e)
|
|
183
|
+
raise e
|
|
184
|
+
rescue StandardError => e
|
|
185
|
+
Logger.log_exception(self, e)
|
|
186
|
+
raise e
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Defines schema validation rules for the service's arguments and/or result.
|
|
190
|
+
#
|
|
191
|
+
# This method provides a clean DSL for specifying JSON schemas that will be used
|
|
192
|
+
# to validate service inputs and outputs. Schemas defined via this method take
|
|
193
|
+
# precedence over ARGUMENTS_SCHEMA and RESULT_SCHEMA constants. The next major
|
|
194
|
+
# version will deprecate those constants in favor of this DSL.
|
|
195
|
+
#
|
|
196
|
+
# @param arguments [Hash, nil] JSON schema for validating service arguments
|
|
197
|
+
# @param result [Hash, nil] JSON schema for validating service result data
|
|
198
|
+
# @return [void]
|
|
199
|
+
#
|
|
200
|
+
# @example Defining both arguments and result schemas
|
|
201
|
+
# class ProcessPayment::Service < Servus::Base
|
|
202
|
+
# schema(
|
|
203
|
+
# arguments: {
|
|
204
|
+
# type: 'object',
|
|
205
|
+
# required: ['user_id', 'amount'],
|
|
206
|
+
# properties: {
|
|
207
|
+
# user_id: { type: 'integer' },
|
|
208
|
+
# amount: { type: 'number', minimum: 0.01 }
|
|
209
|
+
# }
|
|
210
|
+
# },
|
|
211
|
+
# result: {
|
|
212
|
+
# type: 'object',
|
|
213
|
+
# required: ['transaction_id'],
|
|
214
|
+
# properties: {
|
|
215
|
+
# transaction_id: { type: 'string' }
|
|
216
|
+
# }
|
|
217
|
+
# }
|
|
218
|
+
# )
|
|
219
|
+
# end
|
|
220
|
+
#
|
|
221
|
+
# @example Defining only arguments schema
|
|
222
|
+
# class SendEmail::Service < Servus::Base
|
|
223
|
+
# schema arguments: { type: 'object', required: ['email', 'subject'] }
|
|
224
|
+
# end
|
|
225
|
+
#
|
|
226
|
+
# @see Servus::Support::Validator
|
|
227
|
+
def schema(arguments: nil, result: nil)
|
|
228
|
+
@arguments_schema = arguments.with_indifferent_access if arguments
|
|
229
|
+
@result_schema = result.with_indifferent_access if result
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Returns the arguments schema defined via the schema DSL method.
|
|
233
|
+
#
|
|
234
|
+
# @return [Hash, nil] the arguments schema or nil if not defined
|
|
235
|
+
# @api private
|
|
236
|
+
attr_reader :arguments_schema
|
|
237
|
+
|
|
238
|
+
# Returns the result schema defined via the schema DSL method.
|
|
239
|
+
#
|
|
240
|
+
# @return [Hash, nil] the result schema or nil if not defined
|
|
241
|
+
# @api private
|
|
242
|
+
attr_reader :result_schema
|
|
243
|
+
|
|
244
|
+
# Executes pre-call hooks including logging and argument validation.
|
|
245
|
+
#
|
|
246
|
+
# This method is automatically called before service execution and handles:
|
|
247
|
+
# - Logging the service call with arguments
|
|
248
|
+
# - Validating arguments against ARGUMENTS_SCHEMA (if defined)
|
|
249
|
+
#
|
|
250
|
+
# @param args [Hash] keyword arguments being passed to the service
|
|
251
|
+
# @return [void]
|
|
252
|
+
# @raise [Servus::Support::Errors::ValidationError] if arguments fail validation
|
|
253
|
+
#
|
|
254
|
+
# @api private
|
|
255
|
+
def before_call(args)
|
|
256
|
+
Logger.log_call(self, args)
|
|
257
|
+
Validator.validate_arguments!(self, args)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Executes post-call hooks including result validation.
|
|
261
|
+
#
|
|
262
|
+
# This method is automatically called after service execution completes and handles:
|
|
263
|
+
# - Validating the result data against RESULT_SCHEMA (if defined)
|
|
264
|
+
#
|
|
265
|
+
# @param result [Servus::Support::Response] the response returned from the service
|
|
266
|
+
# @return [void]
|
|
267
|
+
# @raise [Servus::Support::Errors::ValidationError] if result data fails validation
|
|
268
|
+
#
|
|
269
|
+
# @api private
|
|
270
|
+
def after_call(result)
|
|
271
|
+
Validator.validate_result!(self, result)
|
|
272
|
+
end
|
|
79
273
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
result
|
|
87
|
-
|
|
274
|
+
# Measures service execution time and logs the result.
|
|
275
|
+
#
|
|
276
|
+
# This method wraps the service execution to capture timing metrics.
|
|
277
|
+
# The duration is logged along with the success/failure status of the service.
|
|
278
|
+
#
|
|
279
|
+
# @param _args [Hash] keyword arguments (unused, kept for method signature compatibility)
|
|
280
|
+
# @yieldreturn [Servus::Support::Response] the result from executing the service
|
|
281
|
+
# @return [Servus::Support::Response] the service execution result
|
|
282
|
+
#
|
|
283
|
+
# @api private
|
|
284
|
+
def benchmark(**_args)
|
|
285
|
+
start_time = Time.now.utc
|
|
286
|
+
result = yield
|
|
287
|
+
duration = Time.now.utc - start_time
|
|
88
288
|
|
|
89
|
-
|
|
289
|
+
Logger.log_result(self, result, duration)
|
|
90
290
|
|
|
91
|
-
|
|
291
|
+
result
|
|
292
|
+
end
|
|
92
293
|
end
|
|
93
294
|
end
|
|
94
295
|
end
|
data/lib/servus/config.rb
CHANGED
|
@@ -2,38 +2,69 @@
|
|
|
2
2
|
|
|
3
3
|
# Servus namespace
|
|
4
4
|
module Servus
|
|
5
|
-
# Configuration
|
|
5
|
+
# Configuration settings for the Servus gem.
|
|
6
|
+
#
|
|
7
|
+
# Manages global configuration options including schema file locations.
|
|
8
|
+
# Access the configuration via {Servus.config} or modify via {Servus.configure}.
|
|
9
|
+
#
|
|
10
|
+
# @example Customizing schema location
|
|
11
|
+
# Servus.configure do |config|
|
|
12
|
+
# config.schema_root = Rails.root.join('lib/schemas')
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# @see Servus.config
|
|
16
|
+
# @see Servus.configure
|
|
6
17
|
class Config
|
|
7
|
-
# The directory where
|
|
18
|
+
# The root directory where schema files are located.
|
|
19
|
+
#
|
|
20
|
+
# Defaults to `Rails.root/app/schemas/services` in Rails applications,
|
|
21
|
+
# or a relative path from the gem installation otherwise.
|
|
22
|
+
#
|
|
23
|
+
# @return [String] the schema root directory path
|
|
8
24
|
attr_reader :schema_root
|
|
9
25
|
|
|
26
|
+
# Initializes a new configuration with default values.
|
|
27
|
+
#
|
|
28
|
+
# @api private
|
|
10
29
|
def initialize
|
|
11
30
|
# Default to Rails.root if available, otherwise use current working directory
|
|
12
31
|
@schema_root = File.join(root_path, 'app/schemas/services')
|
|
13
32
|
end
|
|
14
33
|
|
|
15
|
-
# Returns the path
|
|
34
|
+
# Returns the full path to a service's schema file.
|
|
16
35
|
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
36
|
+
# Constructs the path by combining {#schema_root} with the service namespace
|
|
37
|
+
# and schema type.
|
|
38
|
+
#
|
|
39
|
+
# @param service_namespace [String] underscored service namespace (e.g., "process_payment")
|
|
40
|
+
# @param type [String] schema type ("arguments" or "result")
|
|
41
|
+
# @return [String] full path to the schema JSON file
|
|
42
|
+
#
|
|
43
|
+
# @example
|
|
44
|
+
# config.schema_path_for("process_payment", "arguments")
|
|
45
|
+
# # => "/app/app/schemas/services/process_payment/arguments.json"
|
|
20
46
|
def schema_path_for(service_namespace, type)
|
|
21
47
|
File.join(schema_root.to_s, service_namespace, "#{type}.json")
|
|
22
48
|
end
|
|
23
49
|
|
|
24
|
-
# Returns the directory
|
|
50
|
+
# Returns the directory containing a service's schema files.
|
|
51
|
+
#
|
|
52
|
+
# @param service_namespace [String] underscored service namespace
|
|
53
|
+
# @return [String] directory path for the service's schemas
|
|
25
54
|
#
|
|
26
|
-
# @
|
|
27
|
-
#
|
|
55
|
+
# @example
|
|
56
|
+
# config.schema_dir_for("process_payment")
|
|
57
|
+
# # => "/app/app/schemas/services/process_payment"
|
|
28
58
|
def schema_dir_for(service_namespace)
|
|
29
59
|
File.join(schema_root.to_s, service_namespace)
|
|
30
60
|
end
|
|
31
61
|
|
|
32
62
|
private
|
|
33
63
|
|
|
34
|
-
#
|
|
64
|
+
# Determines the application root path.
|
|
35
65
|
#
|
|
36
|
-
# @
|
|
66
|
+
# @return [String] Rails.root in Rails apps, or gem's root directory otherwise
|
|
67
|
+
# @api private
|
|
37
68
|
def root_path
|
|
38
69
|
if defined?(Rails) && Rails.respond_to?(:root)
|
|
39
70
|
Rails.root
|
|
@@ -43,11 +74,26 @@ module Servus
|
|
|
43
74
|
end
|
|
44
75
|
end
|
|
45
76
|
|
|
46
|
-
#
|
|
77
|
+
# Returns the singleton configuration instance.
|
|
78
|
+
#
|
|
79
|
+
# @return [Servus::Config] the global configuration object
|
|
80
|
+
#
|
|
81
|
+
# @example
|
|
82
|
+
# Servus.config.schema_root
|
|
83
|
+
# # => "/app/app/schemas/services"
|
|
47
84
|
def self.config
|
|
48
85
|
@config ||= Config.new
|
|
49
86
|
end
|
|
50
87
|
|
|
88
|
+
# Yields the configuration for modification.
|
|
89
|
+
#
|
|
90
|
+
# @yieldparam config [Servus::Config] the configuration object to modify
|
|
91
|
+
# @return [void]
|
|
92
|
+
#
|
|
93
|
+
# @example
|
|
94
|
+
# Servus.configure do |config|
|
|
95
|
+
# config.schema_root = Rails.root.join('custom/schemas')
|
|
96
|
+
# end
|
|
51
97
|
def self.configure
|
|
52
98
|
yield(config)
|
|
53
99
|
end
|
|
@@ -3,28 +3,60 @@
|
|
|
3
3
|
module Servus
|
|
4
4
|
module Extensions
|
|
5
5
|
module Async
|
|
6
|
-
#
|
|
6
|
+
# Provides asynchronous service execution via ActiveJob.
|
|
7
7
|
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# - wait_until: <Time> (e.g., 2.hours.from_now)
|
|
11
|
-
# - queue: <Symbol/String> (e.g., :critical, 'low_priority')
|
|
12
|
-
# - priority: <Integer> (depends on adapter support)
|
|
13
|
-
# - retry: <Boolean> (custom control for job retry)
|
|
14
|
-
# - job_options: <Hash> (extra options, merged in)
|
|
15
|
-
#
|
|
16
|
-
# Example:
|
|
17
|
-
# call_async(
|
|
18
|
-
# wait: 10.minutes,
|
|
19
|
-
# queue: :low_priority,
|
|
20
|
-
# priority: 20,
|
|
21
|
-
# job_options: { tags: ['user_graduation'] },
|
|
22
|
-
# user_id: current_user.id
|
|
23
|
-
# )
|
|
8
|
+
# This module extends {Servus::Base} with the {#call_async} method, enabling
|
|
9
|
+
# services to be executed in background jobs. Requires ActiveJob to be loaded.
|
|
24
10
|
#
|
|
11
|
+
# @see Call#call_async
|
|
25
12
|
module Call
|
|
26
|
-
#
|
|
13
|
+
# Enqueues the service for asynchronous execution via ActiveJob.
|
|
14
|
+
#
|
|
15
|
+
# This method schedules the service to run in a background job, supporting
|
|
16
|
+
# all standard ActiveJob options for scheduling, queue routing, and priority.
|
|
17
|
+
#
|
|
18
|
+
# Service arguments are passed as keyword arguments alongside job configuration.
|
|
19
|
+
# Job-specific options are extracted and the remaining arguments are passed
|
|
20
|
+
# to the service's initialize method.
|
|
21
|
+
#
|
|
22
|
+
# @param args [Hash] combined service arguments and job configuration options
|
|
23
|
+
# @option args [ActiveSupport::Duration] :wait delay before execution (e.g., 5.minutes)
|
|
24
|
+
# @option args [Time] :wait_until specific time to execute (e.g., 2.hours.from_now)
|
|
25
|
+
# @option args [Symbol, String] :queue queue name (e.g., :low_priority)
|
|
26
|
+
# @option args [Integer] :priority job priority (adapter-dependent)
|
|
27
|
+
# @option args [Hash] :job_options additional ActiveJob options
|
|
28
|
+
#
|
|
27
29
|
# @return [void]
|
|
30
|
+
# @raise [Servus::Extensions::Async::Errors::JobEnqueueError] if job enqueueing fails
|
|
31
|
+
#
|
|
32
|
+
# @example Basic async execution
|
|
33
|
+
# Services::SendEmail::Service.call_async(
|
|
34
|
+
# user_id: 123,
|
|
35
|
+
# template: :welcome
|
|
36
|
+
# )
|
|
37
|
+
#
|
|
38
|
+
# @example With delay
|
|
39
|
+
# Services::SendReminder::Service.call_async(
|
|
40
|
+
# wait: 1.day,
|
|
41
|
+
# user_id: 123
|
|
42
|
+
# )
|
|
43
|
+
#
|
|
44
|
+
# @example With queue and priority
|
|
45
|
+
# Services::ProcessPayment::Service.call_async(
|
|
46
|
+
# queue: :critical,
|
|
47
|
+
# priority: 10,
|
|
48
|
+
# order_id: 456
|
|
49
|
+
# )
|
|
50
|
+
#
|
|
51
|
+
# @example With custom job options
|
|
52
|
+
# Services::GenerateReport::Service.call_async(
|
|
53
|
+
# wait_until: Date.tomorrow.beginning_of_day,
|
|
54
|
+
# job_options: { tags: ['reports', 'daily'] },
|
|
55
|
+
# report_type: :sales
|
|
56
|
+
# )
|
|
57
|
+
#
|
|
58
|
+
# @note Only available when ActiveJob is loaded (typically in Rails applications)
|
|
59
|
+
# @see Servus::Base.call
|
|
28
60
|
def call_async(**args)
|
|
29
61
|
# Extract ActiveJob configuration options
|
|
30
62
|
job_options = args.slice(:wait, :wait_until, :queue, :priority)
|