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.
Files changed (116) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +6 -0
  3. data/CHANGELOG.md +7 -0
  4. data/IDEAS.md +5 -0
  5. data/READme.md +147 -42
  6. data/Rakefile +33 -0
  7. data/builds/servus-0.1.3.gem +0 -0
  8. data/builds/servus-0.1.4.gem +0 -0
  9. data/docs/core/1_overview.md +77 -0
  10. data/docs/core/2_architecture.md +92 -0
  11. data/docs/core/3_service_objects.md +121 -0
  12. data/docs/features/1_schema_validation.md +119 -0
  13. data/docs/features/2_error_handling.md +121 -0
  14. data/docs/features/3_async_execution.md +81 -0
  15. data/docs/features/4_logging.md +64 -0
  16. data/docs/guides/1_common_patterns.md +90 -0
  17. data/docs/guides/2_migration_guide.md +175 -0
  18. data/docs/integration/1_configuration.md +51 -0
  19. data/docs/integration/2_testing.md +164 -0
  20. data/docs/integration/3_rails_integration.md +99 -0
  21. data/docs/yard/Servus/Base.html +1645 -0
  22. data/docs/yard/Servus/Config.html +582 -0
  23. data/docs/yard/Servus/Extensions/Async/Call.html +400 -0
  24. data/docs/yard/Servus/Extensions/Async/Errors/AsyncError.html +140 -0
  25. data/docs/yard/Servus/Extensions/Async/Errors/JobEnqueueError.html +154 -0
  26. data/docs/yard/Servus/Extensions/Async/Errors/ServiceNotFoundError.html +154 -0
  27. data/docs/yard/Servus/Extensions/Async/Errors.html +128 -0
  28. data/docs/yard/Servus/Extensions/Async/Ext.html +119 -0
  29. data/docs/yard/Servus/Extensions/Async/Job.html +310 -0
  30. data/docs/yard/Servus/Extensions/Async.html +141 -0
  31. data/docs/yard/Servus/Extensions.html +117 -0
  32. data/docs/yard/Servus/Generators/ServiceGenerator.html +261 -0
  33. data/docs/yard/Servus/Generators.html +115 -0
  34. data/docs/yard/Servus/Helpers/ControllerHelpers.html +457 -0
  35. data/docs/yard/Servus/Helpers.html +115 -0
  36. data/docs/yard/Servus/Railtie.html +134 -0
  37. data/docs/yard/Servus/Support/Errors/AuthenticationError.html +287 -0
  38. data/docs/yard/Servus/Support/Errors/BadRequestError.html +283 -0
  39. data/docs/yard/Servus/Support/Errors/ForbiddenError.html +284 -0
  40. data/docs/yard/Servus/Support/Errors/InternalServerError.html +283 -0
  41. data/docs/yard/Servus/Support/Errors/NotFoundError.html +284 -0
  42. data/docs/yard/Servus/Support/Errors/ServiceError.html +489 -0
  43. data/docs/yard/Servus/Support/Errors/ServiceUnavailableError.html +290 -0
  44. data/docs/yard/Servus/Support/Errors/UnauthorizedError.html +200 -0
  45. data/docs/yard/Servus/Support/Errors/UnprocessableEntityError.html +288 -0
  46. data/docs/yard/Servus/Support/Errors/ValidationError.html +200 -0
  47. data/docs/yard/Servus/Support/Errors.html +140 -0
  48. data/docs/yard/Servus/Support/Logger.html +856 -0
  49. data/docs/yard/Servus/Support/Rescuer/BlockContext.html +585 -0
  50. data/docs/yard/Servus/Support/Rescuer/CallOverride.html +257 -0
  51. data/docs/yard/Servus/Support/Rescuer/ClassMethods.html +343 -0
  52. data/docs/yard/Servus/Support/Rescuer.html +267 -0
  53. data/docs/yard/Servus/Support/Response.html +574 -0
  54. data/docs/yard/Servus/Support/Validator.html +1150 -0
  55. data/docs/yard/Servus/Support.html +119 -0
  56. data/docs/yard/Servus/Testing/ExampleBuilders.html +523 -0
  57. data/docs/yard/Servus/Testing/ExampleExtractor.html +578 -0
  58. data/docs/yard/Servus/Testing.html +142 -0
  59. data/docs/yard/Servus.html +343 -0
  60. data/docs/yard/_index.html +535 -0
  61. data/docs/yard/class_list.html +54 -0
  62. data/docs/yard/css/common.css +1 -0
  63. data/docs/yard/css/full_list.css +58 -0
  64. data/docs/yard/css/style.css +503 -0
  65. data/docs/yard/file.1_common_patterns.html +154 -0
  66. data/docs/yard/file.1_configuration.html +115 -0
  67. data/docs/yard/file.1_overview.html +142 -0
  68. data/docs/yard/file.1_schema_validation.html +188 -0
  69. data/docs/yard/file.2_architecture.html +157 -0
  70. data/docs/yard/file.2_error_handling.html +190 -0
  71. data/docs/yard/file.2_migration_guide.html +242 -0
  72. data/docs/yard/file.2_testing.html +227 -0
  73. data/docs/yard/file.3_async_execution.html +145 -0
  74. data/docs/yard/file.3_rails_integration.html +160 -0
  75. data/docs/yard/file.3_service_objects.html +191 -0
  76. data/docs/yard/file.4_logging.html +135 -0
  77. data/docs/yard/file.ErrorHandling.html +190 -0
  78. data/docs/yard/file.READme.html +674 -0
  79. data/docs/yard/file.architecture.html +157 -0
  80. data/docs/yard/file.async_execution.html +145 -0
  81. data/docs/yard/file.common_patterns.html +154 -0
  82. data/docs/yard/file.configuration.html +115 -0
  83. data/docs/yard/file.error_handling.html +190 -0
  84. data/docs/yard/file.logging.html +135 -0
  85. data/docs/yard/file.migration_guide.html +242 -0
  86. data/docs/yard/file.overview.html +142 -0
  87. data/docs/yard/file.rails_integration.html +160 -0
  88. data/docs/yard/file.schema_validation.html +188 -0
  89. data/docs/yard/file.service_objects.html +191 -0
  90. data/docs/yard/file.testing.html +227 -0
  91. data/docs/yard/file_list.html +119 -0
  92. data/docs/yard/frames.html +22 -0
  93. data/docs/yard/index.html +674 -0
  94. data/docs/yard/js/app.js +344 -0
  95. data/docs/yard/js/full_list.js +242 -0
  96. data/docs/yard/js/jquery.js +4 -0
  97. data/docs/yard/method_list.html +542 -0
  98. data/docs/yard/top-level-namespace.html +110 -0
  99. data/lib/generators/servus/service/service_generator.rb +64 -1
  100. data/lib/generators/servus/service/templates/service.rb.erb +1 -1
  101. data/lib/servus/base.rb +258 -57
  102. data/lib/servus/config.rb +58 -12
  103. data/lib/servus/extensions/async/call.rb +50 -18
  104. data/lib/servus/extensions/async/errors.rb +23 -3
  105. data/lib/servus/extensions/async/ext.rb +10 -2
  106. data/lib/servus/extensions/async/job.rb +30 -9
  107. data/lib/servus/helpers/controller_helpers.rb +73 -37
  108. data/lib/servus/support/errors.rb +135 -45
  109. data/lib/servus/support/rescuer.rb +189 -36
  110. data/lib/servus/support/response.rb +49 -7
  111. data/lib/servus/support/validator.rb +120 -19
  112. data/lib/servus/testing/example_builders.rb +133 -0
  113. data/lib/servus/testing/example_extractor.rb +309 -0
  114. data/lib/servus/testing.rb +17 -0
  115. data/lib/servus/version.rb +1 -1
  116. metadata +117 -19
@@ -2,12 +2,33 @@
2
2
 
3
3
  module Servus
4
4
  module Generators
5
- # Servus Generator
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
 
@@ -1,5 +1,5 @@
1
1
  module <%= class_name %>
2
- class Service < Servus::ApplicationService
2
+ class Service < Servus::Base
3
3
  def initialize<%= parameter_list %>
4
4
  <%= initialize_params %>
5
5
  end
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 services
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
- # Calls the service and returns a response
15
- #
16
- # @param args [Hash] The arguments to pass to the service
17
- # @return [Servus::Support::Response] The response
18
- # @raise [StandardError] If an exception is raised
19
- # @raise [Servus::Support::Errors::ValidationError] If result is invalid
20
- # @raise [Servus::Support::Errors::ValidationError] If arguments are invalid
21
- def self.call(**args)
22
- before_call(args)
23
- result = benchmark(**args) { new(**args).call }
24
- after_call(result)
25
-
26
- result
27
- rescue ValidationError => e
28
- Logger.log_validation_error(self, e)
29
- raise e
30
- rescue StandardError => e
31
- Logger.log_exception(self, e)
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
- # @param data [Object] The data to return
38
- # @return [Servus::Support::Response] The success response
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
- # Returns a failure response
82
+ # Creates a failure response with an error.
44
83
  #
45
- # @param message [String] The error message
46
- # @param type [Class] The error type
47
- # @return [Servus::Support::Response] The failure response
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
- # Raises an error and logs it
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] The error message
56
- # @param type [Class] The error type
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
- # Runs call setup before call
64
- #
65
- # @param args [Hash] The arguments to pass to the service
66
- # @return [Object] The result of the call
67
- def self.before_call(args)
68
- Logger.log_call(self, args)
69
- Validator.validate_arguments!(self, args)
70
- end
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
- # Runs after call
73
- #
74
- # @param args [Hash] The arguments to pass to the service
75
- # @return [Object] The result of the call
76
- def self.after_call(args)
77
- Validator.validate_result!(self, args)
78
- end
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
- # Benchmarks the call
81
- #
82
- # @param args [Hash] The arguments to pass to the service
83
- # @return [Object] The result of the call
84
- def self.benchmark(**_args)
85
- start_time = Time.now.utc
86
- result = yield
87
- duration = Time.now.utc - start_time
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
- Logger.log_result(self, result, duration)
289
+ Logger.log_result(self, result, duration)
90
290
 
91
- result
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 class for Servus
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 schemas are loaded from, can be set by the user
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 for a specific service's schema
34
+ # Returns the full path to a service's schema file.
16
35
  #
17
- # @param service_namespace [String] the namespace of the service
18
- # @param type [String] the type of the schema (e.g., "arguments", "result")
19
- # @return [String] the path for the service's schema type
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 for a specific service
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
- # @param service_namespace [String] the namespace of the service
27
- # @return [String] the directory for the service's schemas
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
- # Sets the schema root directory
64
+ # Determines the application root path.
35
65
  #
36
- # @param path [String] the new schema root directory
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
- # Singleton config instance
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
- # Calls the service asynchronously using AsyncCallerJob.
6
+ # Provides asynchronous service execution via ActiveJob.
7
7
  #
8
- # Supports all standard ActiveJob scheduling and routing options:
9
- # - wait: <ActiveSupport::Duration> (e.g., 5.minutes)
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
- # @param args [Hash] The arguments to pass to the service and job options.
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)