servus 0.0.1 → 0.1.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 36f4d4b15ff9038d9c039f6efb0fe5027c6326811334dc3e478a906b31286d5d
4
- data.tar.gz: 786b002fe1937d5204088fdb982156bc1fb1ce7bc1c20f2ecd4c737e23e1acd5
3
+ metadata.gz: 0bddb5c486e4996dff5183d6a9bd57ef086739d7007a12125479d320464a0018
4
+ data.tar.gz: 2d948b6202d0888664a75766cf4e9a3c607b4f819e7d9293ac0d0ff8f42c6c32
5
5
  SHA512:
6
- metadata.gz: 04ce3e7a6cdd2371c4da93d98123ea640736856638dfa5c2a60d3c4b4c7ae1109bd597bf577cec892461677a906e0955ee782c20532901b3808eca80c6ccbb6a
7
- data.tar.gz: 9c07175986bd05753df2b29dbc70f854cbab12745183d6e8937cc89dd46e5e819df588ac0ef88ea2db8298cb6e470e798a8e3361cc8765194ddee1244c7a2d1c
6
+ metadata.gz: 18a6ffd8d72e5a452bda53b5d91a621d0b963872c08d72b8ee4a533160babfde16ed9690a1f9a0b2c5aa1a4c330f3e6209b9ada2ffdba3635bad9cf0bd351d8e
7
+ data.tar.gz: 59b87da3ae33ec0a586275892632b6fe84810b44d75759d9c6c2733fdb9224093d1ed9740467438908eb4a5640b41b6468f1c8381c65cc07adf843e23979fb80
data/.rubocop.yml ADDED
@@ -0,0 +1,11 @@
1
+ AllCops:
2
+ Include:
3
+ - 'lib/**/*.rb'
4
+ - 'spec/**/*.rb'
5
+
6
+ Metrics/BlockLength:
7
+ Exclude:
8
+ - 'spec/**/*'
9
+
10
+ Lint/ConstantDefinitionInBlock:
11
+ Enabled: false
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.1.2] - 2025-10-10
4
+ - Added: Added `call_async` method to `Servus::Base` to enqueue a job for calling the service asynchronously
5
+ - Added: Added `Async::Job` to handle async enqueing with support for ActiveJob set options
6
+
7
+ ## [0.1.1] - 2025-08-20
8
+
9
+ - Added: Added `rescue_from` method to `Servus::Base` to rescue from standard errors and use custom error types.
10
+ - Added: Added `run_service` and `render_service_object_error` helpers to `Servus::Helpers::ControllerHelpers`.
11
+ - Fixed: All rubocop warnings.
12
+
3
13
  ## [0.1.0] - 2025-04-28
4
14
 
5
15
  - Initial release
data/READme.md CHANGED
@@ -119,6 +119,29 @@ end
119
119
 
120
120
  ```
121
121
 
122
+ Here’s a section you can add to your README for the new `.call_async` feature, matching the style of your existing `## Inheritance` section:
123
+
124
+ ---
125
+
126
+ ## **Asynchronous Execution**
127
+
128
+ You can asynchronously execute any service class that inherits from `Servus::Base` using `.call_async`. This uses `ActiveJob` under the hood and supports standard job options (`wait`, `queue`, `priority`, etc.). Only available in environments where `ActiveJob` is loaded (e.g., Rails apps)
129
+
130
+ ```ruby
131
+ # Good ✅
132
+ Services::NotifyUser::Service.call_async(
133
+ user_id: current_user.id,
134
+ wait: 5.minutes,
135
+ queue: :low_priority,
136
+ job_options: { tags: ['notifications'] }
137
+ )
138
+
139
+ # Bad ❌
140
+ Services::NotifyUser::Support::MessageBuilder.call_async(
141
+ # Invalid: support classes don't inherit from Servus::Base
142
+ )
143
+ ```
144
+
122
145
  ## **Inheritance**
123
146
 
124
147
  - Every main service class (`service.rb`) must inherit from `Servus::Base`
@@ -249,18 +272,114 @@ end
249
272
  class SomeController < AppController
250
273
  def controller_action
251
274
  result = SomeServiceObject::Service.call(arg: 1)
252
-
275
+
253
276
  return if result.success?
254
-
277
+
255
278
  # If you just want the error message
256
279
  bad_request(result.error.message)
257
-
280
+
258
281
  # If you want the API error
259
282
  service_object_error(result.error.api_error)
260
283
  end
261
284
  end
262
285
  ```
263
286
 
287
+ ### `rescue_from` for service errors
288
+
289
+ Services can configure default error handling using the `rescue_from` method.
290
+
291
+ ```ruby
292
+ class SomeServiceObject::Service < Servus::Base
293
+ class SomethingBroke < StandardError; end
294
+ class SomethingGlitched < StandardError; end
295
+
296
+ # Rescue from standard errors and use custom error
297
+ rescue_from
298
+ SomethingBroke,
299
+ SomethingGlitched,
300
+ use: Servus::Support::Errors::ServiceUnavailableError # this is optional
301
+
302
+ def call
303
+ do_something
304
+ end
305
+
306
+ private
307
+
308
+ def do_something
309
+ make_and_api_call
310
+ rescue Net::HTTPError => e
311
+ raise SomethingGlitched, "Whoaaaa, something went wrong! #{e.message}"
312
+ end
313
+ end
314
+ end
315
+ ```
316
+
317
+ ```sh
318
+ result = SomeServiceObject::Service.call
319
+ # Failure response
320
+ result.error.class
321
+ => Servus::Support::Errors::ServiceUnavailableError
322
+ result.error.message
323
+ => "[SomeServiceObject::Service::SomethingGlitched]: Whoaaaa, something went wrong! Net::HTTPError (503)"
324
+ result.error.api_error
325
+ => { code: :service_unavailable, message: "[SomeServiceObject::Service::SomethingGlitched]: Whoaaaa, something went wrong! Net::HTTPError (503)" }
326
+ ```
327
+
328
+ The `rescue_from` method will rescue from the specified errors and use the specified error type to create a failure response object with
329
+ the custom error. It helps eliminate the need to manually rescue many errors and create failure responses within the call method of
330
+ a service object.
331
+
332
+ ## Controller Helpers
333
+
334
+ Service objects can be called from controllers using the `run_service` and `render_service_object_error` helpers.
335
+
336
+ ### run_service
337
+
338
+ `run_service` calls the service object with the provided parameters and set's an instance variable `@result` to the
339
+ result of the service object if the result is successful. If the result is not successful, it will pass the result
340
+ to error to the `render_service_object_error` helper. This allows for easy error handling in the controller for
341
+ repetetive usecases.
342
+
343
+ ```ruby
344
+ class SomeController < AppController
345
+ # Before
346
+ def controller_action
347
+ result = Services::SomeServiceObject::Service.call(my_params)
348
+ return if result.success?
349
+ render_service_object_error(result.error.api_error)
350
+ end
351
+
352
+ # After
353
+ def controller_action_refactored
354
+ run_service Services::SomeServiceObject::Service, my_params
355
+ end
356
+ end
357
+ ```
358
+
359
+ ### render_service_object_error
360
+
361
+ `render_service_object_error` renders the error of a service object. It expects a hash with a `message` key and a `code` key from
362
+ the api_error method of the service error. This is all setup by default for a JSON API response, thought the method can be
363
+ overridden if needed to handle different usecases.
364
+
365
+ ```ruby
366
+ # Behind the scenes, render_service_object_error calls the following:
367
+ #
368
+ # error = result.error.api_error
369
+ # => { message: "Error message", code: 400 }
370
+ #
371
+ # render json: { message: error[:message], code: error[:code] }, status: error[:code]
372
+
373
+ class SomeController < AppController
374
+ def controller_action
375
+ result = Services::SomeServiceObject::Service.call(my_params)
376
+ return if result.success?
377
+
378
+ render_service_object_error(result.error.api_error)
379
+ end
380
+ end
381
+ ```
382
+
264
383
  ## **Schema Validation**
265
384
 
266
385
  Service objects support two methods for schema validation: JSON Schema files and inline schema declarations.
@@ -376,4 +495,4 @@ Both file-based and inline schemas are automatically cached:
376
495
 
377
496
  - First validation request loads and caches the schema
378
497
  - Subsequent validations use the cached version
379
- - Cache can be cleared using `SchemaValidation.clear_cache!`
498
+ - Cache can be cleared using `SchemaValidation.clear_cache!`
data/Rakefile CHANGED
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "bundler/gem_tasks"
4
- require "rspec/core/rake_task"
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
5
 
6
6
  RSpec::Core::RakeTask.new(:spec)
7
7
 
8
- require "rubocop/rake_task"
8
+ require 'rubocop/rake_task'
9
9
 
10
10
  RuboCop::RakeTask.new
11
11
 
Binary file
Binary file
@@ -4,17 +4,17 @@ module Servus
4
4
  module Generators
5
5
  # Servus Generator
6
6
  class ServiceGenerator < Rails::Generators::NamedBase
7
- source_root File.expand_path("templates", __dir__)
7
+ source_root File.expand_path('templates', __dir__)
8
8
 
9
- argument :parameters, type: :array, default: [], banner: "parameter"
9
+ argument :parameters, type: :array, default: [], banner: 'parameter'
10
10
 
11
11
  def create_service_file
12
- template "service.rb.erb", service_path
13
- template "service_spec.rb.erb", service_path_spec
12
+ template 'service.rb.erb', service_path
13
+ template 'service_spec.rb.erb', service_path_spec
14
14
 
15
15
  # Template json schemas
16
- template "result.json.erb", service_result_schema_path
17
- template "arguments.json.erb", service_arguments_shecma_path
16
+ template 'result.json.erb', service_result_schema_path
17
+ template 'arguments.json.erb', service_arguments_shecma_path
18
18
  end
19
19
 
20
20
  private
@@ -40,13 +40,13 @@ module Servus
40
40
  end
41
41
 
42
42
  def service_full_class_name
43
- service_class_name.include?("::") ? service_class_name : "::#{service_class_name}"
43
+ service_class_name.include?('::') ? service_class_name : "::#{service_class_name}"
44
44
  end
45
45
 
46
46
  def parameter_list
47
- return "" if parameters.empty?
47
+ return '' if parameters.empty?
48
48
 
49
- "(#{parameters.map { |param| "#{param}:" }.join(", ")})"
49
+ "(#{parameters.map { |param| "#{param}:" }.join(', ')})"
50
50
  end
51
51
 
52
52
  def initialize_params
@@ -54,9 +54,9 @@ module Servus
54
54
  end
55
55
 
56
56
  def attr_readers
57
- return "" if parameters.empty?
57
+ return '' if parameters.empty?
58
58
 
59
- "attr_reader #{parameters.map { |param| ":#{param}" }.join(", ")}"
59
+ "attr_reader #{parameters.map { |param| ":#{param}" }.join(', ')}"
60
60
  end
61
61
  end
62
62
  end
data/lib/servus/base.rb CHANGED
@@ -1,8 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Servus
4
+ # Base class for all services
4
5
  class Base
5
6
  include Servus::Support::Errors
7
+ include Servus::Support::Rescuer
6
8
 
7
9
  # Support class aliases
8
10
  Logger = Servus::Support::Logger
@@ -10,21 +12,16 @@ module Servus
10
12
  Validator = Servus::Support::Validator
11
13
 
12
14
  # Calls the service and returns a response
15
+ #
13
16
  # @param args [Hash] The arguments to pass to the service
14
17
  # @return [Servus::Support::Response] The response
15
18
  # @raise [StandardError] If an exception is raised
16
19
  # @raise [Servus::Support::Errors::ValidationError] If result is invalid
17
20
  # @raise [Servus::Support::Errors::ValidationError] If arguments are invalid
18
21
  def self.call(**args)
19
- Logger.log_call(self, args)
20
-
21
- Validator.validate_arguments!(self, args)
22
-
23
- result = benchmark(**args) do
24
- new(**args).call
25
- end
26
-
27
- Validator.validate_result!(self, result)
22
+ before_call(args)
23
+ result = benchmark(**args) { new(**args).call }
24
+ after_call(result)
28
25
 
29
26
  result
30
27
  rescue ValidationError => e
@@ -36,6 +33,7 @@ module Servus
36
33
  end
37
34
 
38
35
  # Returns a success response
36
+ #
39
37
  # @param data [Object] The data to return
40
38
  # @return [Servus::Support::Response] The success response
41
39
  def success(data)
@@ -43,6 +41,7 @@ module Servus
43
41
  end
44
42
 
45
43
  # Returns a failure response
44
+ #
46
45
  # @param message [String] The error message
47
46
  # @param type [Class] The error type
48
47
  # @return [Servus::Support::Response] The failure response
@@ -52,6 +51,7 @@ module Servus
52
51
  end
53
52
 
54
53
  # Raises an error and logs it
54
+ #
55
55
  # @param message [String] The error message
56
56
  # @param type [Class] The error type
57
57
  # @return [void]
@@ -60,7 +60,25 @@ module Servus
60
60
  raise type, message
61
61
  end
62
62
 
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
71
+
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
79
+
63
80
  # Benchmarks the call
81
+ #
64
82
  # @param args [Hash] The arguments to pass to the service
65
83
  # @return [Object] The result of the call
66
84
  def self.benchmark(**_args)
data/lib/servus/config.rb CHANGED
@@ -1,17 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Servus namespace
3
4
  module Servus
5
+ # Configuration class for Servus
4
6
  class Config
5
7
  # The directory where schemas are loaded from, can be set by the user
6
8
  attr_reader :schema_root
7
9
 
8
10
  def initialize
9
11
  # Default to Rails.root if available, otherwise use current working directory
10
- @schema_root = if defined?(Rails)
11
- Rails.root.join("app/schemas/services")
12
- else
13
- File.expand_path("../../../app/schemas/services", __dir__)
14
- end
12
+ @schema_root = File.join(root_path, 'app/schemas/services')
15
13
  end
16
14
 
17
15
  # Returns the path for a specific service's schema
@@ -30,6 +28,19 @@ module Servus
30
28
  def schema_dir_for(service_namespace)
31
29
  File.join(schema_root.to_s, service_namespace)
32
30
  end
31
+
32
+ private
33
+
34
+ # Sets the schema root directory
35
+ #
36
+ # @param path [String] the new schema root directory
37
+ def root_path
38
+ if defined?(Rails) && Rails.respond_to?(:root)
39
+ Rails.root
40
+ else
41
+ File.expand_path('../../..', __dir__)
42
+ end
43
+ end
33
44
  end
34
45
 
35
46
  # Singleton config instance
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servus
4
+ module Extensions
5
+ module Async
6
+ # Calls the service asynchronously using AsyncCallerJob.
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
+ # )
24
+ #
25
+ module Call
26
+ # @param args [Hash] The arguments to pass to the service and job options.
27
+ # @return [void]
28
+ def call_async(**args)
29
+ # Extract ActiveJob configuration options
30
+ job_options = args.slice(:wait, :wait_until, :queue, :priority)
31
+ job_options.merge!(args.delete(:job_options) || {}) # merge custom job options
32
+
33
+ # Remove special keys that shouldn't be passed to the service
34
+ args.except!(:wait, :wait_until, :queue, :priority, :job_options)
35
+
36
+ # Build job with optional delay, scheduling, or queue settings
37
+ job = job_options.any? ? Job.set(**job_options.compact) : Job
38
+
39
+ # Enqueue the job asynchronously
40
+ job.perform_later(name: name, args: args)
41
+ rescue StandardError => e
42
+ raise Errors::JobEnqueueError, "Failed to enqueue async job for #{self}: #{e.message}"
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servus
4
+ module Extensions
5
+ module Async
6
+ module Errors
7
+ # Base error class for async extensions
8
+ class AsyncError < StandardError; end
9
+
10
+ # Error raised when the job fails to enqueue
11
+ class JobEnqueueError < AsyncError; end
12
+
13
+ # Error raised when the service class cannot be found
14
+ class ServiceNotFoundError < AsyncError; end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servus
4
+ module Extensions
5
+ # Async extensions for Servus
6
+ module Async
7
+ require 'servus/extensions/async/errors'
8
+ require 'servus/extensions/async/job'
9
+ require 'servus/extensions/async/call'
10
+
11
+ # Module providing async extensions for Servus
12
+ module Ext; end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servus
4
+ module Extensions
5
+ module Async
6
+ # Job to run a service class with given arguments.
7
+ #
8
+ # This job will be migrated to Servus once it's stable as a .call_async method.
9
+ # It takes the fully-qualified class name of the service as a string and any keyword arguments
10
+ # required by the service's .call method.
11
+ #
12
+ # Example usage:
13
+ # RunServiceJob.perform_later('SomeModule::SomeService', arg1: value1, arg2: value2)
14
+ #
15
+ # This will invoke SomeModule::SomeService.call(arg1: value1, arg2: value2) in a background job.
16
+ #
17
+ # Errors during service execution are logged.
18
+ class Job < ActiveJob::Base
19
+ queue_as :default
20
+
21
+ def perform(name:, args:)
22
+ constantize!(name).call(**args)
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :klass
28
+
29
+ def constantize!(class_name)
30
+ class_name.safe_constantize || (raise Errors::ServiceNotFoundError,
31
+ "Service class '#{class_name}' not found.")
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servus
4
+ module Helpers
5
+ # Controller helpers
6
+ module ControllerHelpers
7
+ # Run a service object and return the result
8
+ #
9
+ # This method is a helper method for controllers to run a service object and return the result.
10
+ # Servus errors (Servus::Support::Errors::*) all impliment an api_error method that returns a hash with
11
+ # a code and message. The service_object_error method and any custom implimentation, can be used to
12
+ # automatically format and return an API error response.
13
+ #
14
+ # @example:
15
+ # class TestController < ApplicationController
16
+ # def index
17
+ # run_service MyService::Service, params
18
+ # end
19
+ # end
20
+ #
21
+ # The result of the service is stored in the instance variable @result, which can be used
22
+ # in views to template a response.
23
+ #
24
+ # @example:
25
+ # json.data do
26
+ # json.some_key @result.data[:some_key]
27
+ # end
28
+ #
29
+ # When investigating the servus error classes, you can see the api_error method implimentation
30
+ # for each error type. Below is an example implementation of the service_object_error method, which
31
+ # could be overwritten to meet a specific applications needs.
32
+ #
33
+ # @example:
34
+ # # Example implementation of api_error on Servus::Support::Errors::ServiceError
35
+ # # def api_error
36
+ # # { code: :bad_request, message: message }
37
+ # # end
38
+ #
39
+ # Example implementation of service_object_error
40
+ # def service_object_error(api_error)
41
+ # render json: api_error, status: api_error[:code]
42
+ # end
43
+ #
44
+ # @param klass [Class] The service class
45
+ # @param params [Hash] The parameters to pass to the service
46
+ # @return [Servus::Support::Response] The result of the service
47
+ #
48
+ # @see Servus::Support::Errors::ServiceError
49
+ def run_service(klass, params)
50
+ @result = klass.call(**params)
51
+ render_service_object_error(@result.error.api_error) unless @result.success?
52
+ end
53
+
54
+ # Service object error renderer
55
+ #
56
+ # This method is a helper method for controllers to render service object errors.
57
+ #
58
+ # @param api_error [Hash] The API error response
59
+ #
60
+ # @see Servus::Support::Errors::ServiceError
61
+ def render_service_object_error(api_error)
62
+ render json: api_error, status: api_error[:code]
63
+ end
64
+ end
65
+ end
66
+ end
@@ -1,8 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # lib/servus/railtie.rb
4
- require "rails/railtie"
3
+ require 'rails/railtie'
5
4
 
6
5
  module Servus
7
- class Railtie < Rails::Railtie; end
6
+ # Railtie for Rails integration
7
+ class Railtie < Rails::Railtie
8
+ initializer 'servus.controller_helpers' do
9
+ ActiveSupport.on_load(:action_controller) do
10
+ include Servus::Helpers::ControllerHelpers
11
+ end
12
+ end
13
+
14
+ initializer 'servus.job_async' do
15
+ ActiveSupport.on_load(:active_job) do
16
+ require 'servus/extensions/async/ext'
17
+ # Extend the base service with the async call method
18
+ Servus::Base.extend Servus::Extensions::Async::Call
19
+ end
20
+ end
21
+ end
8
22
  end
@@ -4,12 +4,13 @@ module Servus
4
4
  module Support
5
5
  module Errors
6
6
  # Base error class for application services
7
+ #
7
8
  # @param message [String] The error message
8
9
  # @return [ServiceError] The error instance
9
10
  class ServiceError < StandardError
10
11
  attr_reader :message
11
12
 
12
- DEFAULT_MESSAGE = "An error occurred"
13
+ DEFAULT_MESSAGE = 'An error occurred'
13
14
 
14
15
  # Initializes a new error instance
15
16
  # @param message [String] The error message
@@ -30,7 +31,7 @@ module Servus
30
31
  # @param message [String] The error message
31
32
  # @return [BadRequestError] The error instance
32
33
  class BadRequestError < ServiceError
33
- DEFAULT_MESSAGE = "Bad request"
34
+ DEFAULT_MESSAGE = 'Bad request'
34
35
 
35
36
  # 400 error response
36
37
  # @return [Hash] The error response
@@ -43,7 +44,7 @@ module Servus
43
44
  # @param message [String] The error message
44
45
  # @return [AuthenticationError] The error instance
45
46
  class AuthenticationError < ServiceError
46
- DEFAULT_MESSAGE = "Authentication failed"
47
+ DEFAULT_MESSAGE = 'Authentication failed'
47
48
 
48
49
  # 401 error response
49
50
  # @return [Hash] The error response
@@ -56,14 +57,14 @@ module Servus
56
57
  # @param message [String] The error message
57
58
  # @return [UnauthorizedError] The error instance
58
59
  class UnauthorizedError < AuthenticationError
59
- DEFAULT_MESSAGE = "Unauthorized"
60
+ DEFAULT_MESSAGE = 'Unauthorized'
60
61
  end
61
62
 
62
63
  # Error class for forbidden errors
63
64
  # @param message [String] The error message
64
65
  # @return [ForbiddenError] The error instance
65
66
  class ForbiddenError < ServiceError
66
- DEFAULT_MESSAGE = "Forbidden"
67
+ DEFAULT_MESSAGE = 'Forbidden'
67
68
 
68
69
  # 403 error response
69
70
  # @return [Hash] The error response
@@ -76,7 +77,7 @@ module Servus
76
77
  # @param message [String] The error message
77
78
  # @return [NotFoundError] The error instance
78
79
  class NotFoundError < ServiceError
79
- DEFAULT_MESSAGE = "Not found"
80
+ DEFAULT_MESSAGE = 'Not found'
80
81
 
81
82
  # 404 error response
82
83
  # @return [Hash] The error response
@@ -89,7 +90,7 @@ module Servus
89
90
  # @param message [String] The error message
90
91
  # @return [UnprocessableEntityError] The error instance
91
92
  class UnprocessableEntityError < ServiceError
92
- DEFAULT_MESSAGE = "Unprocessable entity"
93
+ DEFAULT_MESSAGE = 'Unprocessable entity'
93
94
 
94
95
  # 422 error response
95
96
  # @return [Hash] The error response
@@ -102,14 +103,14 @@ module Servus
102
103
  # @param message [String] The error message
103
104
  # @return [ValidationError] The error instance
104
105
  class ValidationError < UnprocessableEntityError
105
- DEFAULT_MESSAGE = "Validation failed"
106
+ DEFAULT_MESSAGE = 'Validation failed'
106
107
  end
107
108
 
108
109
  # Error class for internal server errors
109
110
  # @param message [String] The error message
110
111
  # @return [InternalServerError] The error instance
111
112
  class InternalServerError < ServiceError
112
- DEFAULT_MESSAGE = "Internal server error"
113
+ DEFAULT_MESSAGE = 'Internal server error'
113
114
 
114
115
  # 500 error response
115
116
  # @return [Hash] The error response
@@ -122,7 +123,7 @@ module Servus
122
123
  # @param message [String] The error message
123
124
  # @return [ServiceUnavailableError] The error instance
124
125
  class ServiceUnavailableError < ServiceError
125
- DEFAULT_MESSAGE = "Service unavailable"
126
+ DEFAULT_MESSAGE = 'Service unavailable'
126
127
 
127
128
  # 503 error response
128
129
  # @return [Hash] The error response
@@ -1,9 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "logger"
3
+ require 'logger'
4
4
 
5
5
  module Servus
6
6
  module Support
7
+ # Logger class for logging service calls and results
7
8
  class Logger
8
9
  # Returns the logger instance depending on the environment
9
10
  #
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servus
4
+ module Support
5
+ # Module that rescues the call method from errors
6
+ module Rescuer
7
+ # Includes the rescuer module into the base class
8
+ #
9
+ # @param base [Class] The base class to include the rescuer module into
10
+ def self.included(base)
11
+ base.class_attribute :rescuable_errors, default: []
12
+ base.class_attribute :rescuable_error_type, default: nil
13
+ base.singleton_class.prepend(CallOverride)
14
+ base.extend(ClassMethods)
15
+ end
16
+
17
+ # Class methods for rescue_from
18
+ module ClassMethods
19
+ # Rescues the call method from errors
20
+ #
21
+ # By configuring error classes in the rescue_from method, the call method will rescue from those errors
22
+ # and return a failure response with a ServiceError and formatted error message. This prevents the need to
23
+ # to have excessive rescue blocks in the call method.
24
+ #
25
+ # @example:
26
+ # class TestService < Servus::Base
27
+ # rescue_from SomeError, type: Servus::Support::Errors::ServiceError
28
+ # end
29
+ #
30
+ # @param [Error] errors One or more errors to rescue from (variadic)
31
+ # @param [Error] use The error to be used (optional, defaults to Servus::Support::Errors::ServiceError)
32
+ def rescue_from(*errors, use: Servus::Support::Errors::ServiceError)
33
+ self.rescuable_errors = errors
34
+ self.rescuable_error_type = use
35
+ end
36
+ end
37
+
38
+ # Module that overrides the call method to rescue from errors
39
+ module CallOverride
40
+ # Overrides the call method to rescue from errors
41
+ #
42
+ # @param args [Hash] The arguments passed to the call method
43
+ # @return [Servus::Support::Response] The result of the call method
44
+ def call(**args)
45
+ if rescuable_errors.any?
46
+ begin
47
+ super
48
+ rescue *rescuable_errors => e
49
+ handle_failure(e, rescuable_error_type)
50
+ end
51
+ else
52
+ super
53
+ end
54
+ end
55
+
56
+ # Returns a failure response with a ServiceError and formatted error message
57
+ #
58
+ # The `failure` method is an instance method of the base class, so it can't be called from this module which
59
+ # is rescuing the call method.
60
+ #
61
+ # @param [Error] error The error to be used
62
+ # @param [Class] type The error type
63
+ # @return [Servus::Support::Response] The failure response
64
+ def handle_failure(error, type)
65
+ error = type.new(template_error_message(error))
66
+ Response.new(false, nil, error)
67
+ end
68
+
69
+ # Templates the error message
70
+ #
71
+ # @param [Error] error The error to be used
72
+ # @return [String] The formatted error message
73
+ def template_error_message(error)
74
+ "[#{error.class}]: #{error.message}"
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -2,15 +2,28 @@
2
2
 
3
3
  module Servus
4
4
  module Support
5
+ # Response class for service results
5
6
  class Response
6
- attr_reader :data, :error
7
+ # [Object] The data returned by the service
8
+ attr_reader :data
7
9
 
10
+ # [Servus::Support::Errors::ServiceError] The error returned by the service
11
+ attr_reader :error
12
+
13
+ # Initializes a new response
14
+ #
15
+ # @param success [Boolean] Whether the response was successful
16
+ # @param data [Object] The data returned by the service
17
+ # @param error [Servus::Support::Errors::ServiceError] The error returned by the service
8
18
  def initialize(success, data, error)
9
19
  @success = success
10
20
  @data = data
11
21
  @error = error
12
22
  end
13
23
 
24
+ # Returns whether the response was successful
25
+ #
26
+ # @return [Boolean] Whether the response was successful
14
27
  def success?
15
28
  @success
16
29
  end
@@ -2,20 +2,21 @@
2
2
 
3
3
  module Servus
4
4
  module Support
5
+ # Validates arguments and results
5
6
  class Validator
6
7
  # Class-level schema cache
7
8
  @schema_cache = {}
8
9
 
9
10
  # Validate service arguments against schema
10
11
  def self.validate_arguments!(service_class, args)
11
- schema = load_schema(service_class, "arguments")
12
+ schema = load_schema(service_class, 'arguments')
12
13
  return true unless schema # Skip validation if no schema exists
13
14
 
14
15
  serialized_result = args.as_json
15
16
  validation_errors = JSON::Validator.fully_validate(schema, serialized_result)
16
17
 
17
18
  if validation_errors.any?
18
- error_message = "Invalid arguments for #{service_class.name}: #{validation_errors.join(", ")}"
19
+ error_message = "Invalid arguments for #{service_class.name}: #{validation_errors.join(', ')}"
19
20
  raise Servus::Base::ValidationError, error_message
20
21
  end
21
22
 
@@ -26,14 +27,14 @@ module Servus
26
27
  def self.validate_result!(service_class, result)
27
28
  return result unless result.success?
28
29
 
29
- schema = load_schema(service_class, "result")
30
+ schema = load_schema(service_class, 'result')
30
31
  return result unless schema # Skip validation if no schema exists
31
32
 
32
33
  serialized_result = result.data.as_json
33
34
  validation_errors = JSON::Validator.fully_validate(schema, serialized_result)
34
35
 
35
36
  if validation_errors.any?
36
- error_message = "Invalid result structure from #{service_class.name}: #{validation_errors.join(", ")}"
37
+ error_message = "Invalid result structure from #{service_class.name}: #{validation_errors.join(', ')}"
37
38
  raise Servus::Base::ValidationError, error_message
38
39
  end
39
40
 
@@ -43,27 +44,18 @@ module Servus
43
44
  # Load schema from file with caching
44
45
  def self.load_schema(service_class, type)
45
46
  # Get service path based on class name (e.g., "process_payment" from "Servus::ProcessPayment::Service")
46
- service_namespace = service_class.name.split("::")[..-2].map do |s|
47
- s.gsub(/([a-z])([A-Z])/, '\1_\2').downcase
48
- end.join("/")
47
+ service_namespace = parse_service_namespace(service_class)
49
48
  schema_path = Servus.config.schema_path_for(service_namespace, type)
50
49
 
51
50
  # Return from cache if available
52
51
  return @schema_cache[schema_path] if @schema_cache.key?(schema_path)
53
52
 
54
53
  inline_schema_constant_name = "#{service_class}::#{type.upcase}_SCHEMA"
55
- inline_schema_constant = Object.const_defined?(inline_schema_constant_name) ? Object.const_get(inline_schema_constant_name) : nil
56
-
57
- if inline_schema_constant
58
- @schema_cache[schema_path] =
59
- inline_schema_constant.respond_to?(:deep_stringify_keys) ? inline_schema_constant.deep_stringify_keys : inline_schema_constant
60
- elsif File.exist?(schema_path)
61
- @schema_cache[schema_path] = JSON.parse(File.read(schema_path))
62
- else
63
- # Cache nil result to avoid checking file system again
64
- @schema_cache[schema_path] = nil
65
- end
54
+ inline_schema_constant = if Object.const_defined?(inline_schema_constant_name)
55
+ Object.const_get(inline_schema_constant_name)
56
+ end
66
57
 
58
+ @schema_cache[schema_path] = fetch_schema_from_sources(inline_schema_constant, schema_path)
67
59
  @schema_cache[schema_path]
68
60
  end
69
61
 
@@ -76,6 +68,32 @@ module Servus
76
68
  def self.cache
77
69
  @schema_cache
78
70
  end
71
+
72
+ # Fetches the schema from the sources
73
+ #
74
+ # This method checks if the schema is defined as an inline constant or if it exists as a file. The
75
+ # schema is then symbolized and returned. If the schema is not found, nil is returned.
76
+ #
77
+ # @param inline_schema_constant [Hash, String] the inline schema constant to process
78
+ # @param schema_path [String] the path to the schema file
79
+ # @return [Hash] the processed inline schema constant
80
+ def self.fetch_schema_from_sources(inline_schema_constant, schema_path)
81
+ if inline_schema_constant
82
+ inline_schema_constant.with_indifferent_access
83
+ elsif File.exist?(schema_path)
84
+ JSON.load_file(schema_path).with_indifferent_access
85
+ end
86
+ end
87
+
88
+ # Parses the service namespace from the service class name
89
+ #
90
+ # @param service_class [Class] the service class to parse
91
+ # @return [String] the service namespace
92
+ def self.parse_service_namespace(service_class)
93
+ service_class.name.split('::')[..-2].map do |s|
94
+ s.gsub(/([a-z])([A-Z])/, '\1_\2').downcase
95
+ end.join('/')
96
+ end
79
97
  end
80
98
  end
81
99
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Servus
4
- VERSION = "0.0.1"
4
+ VERSION = '0.1.2'
5
5
  end
data/lib/servus.rb CHANGED
@@ -1,24 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Globals
4
- require "json-schema"
5
- require "active_model_serializers"
4
+ require 'json-schema'
5
+ require 'active_support'
6
+ require 'active_support/core_ext/class/attribute'
7
+ require 'active_model_serializers'
6
8
 
7
9
  # Servus namespace
8
10
  module Servus; end
9
11
 
12
+ # Helpers
13
+ require_relative 'servus/helpers/controller_helpers'
14
+
10
15
  # Railtie
11
- require_relative "servus/railtie" if defined?(Rails::Railtie)
16
+ require_relative 'servus/railtie' if defined?(Rails::Railtie)
12
17
 
13
18
  # Config
14
- require_relative "servus/config"
19
+ require_relative 'servus/config'
15
20
 
16
21
  # Support
17
- require_relative "servus/support/logger"
18
- require_relative "servus/support/response"
19
- require_relative "servus/support/validator"
20
- require_relative "servus/support/errors"
22
+ require_relative 'servus/support/logger'
23
+ require_relative 'servus/support/response'
24
+ require_relative 'servus/support/validator'
25
+ require_relative 'servus/support/errors'
26
+ require_relative 'servus/support/rescuer'
21
27
 
22
28
  # Core
23
- require_relative "servus/version"
24
- require_relative "servus/base"
29
+ require_relative 'servus/version'
30
+ require_relative 'servus/base'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: servus
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sebastian Scholl
@@ -23,6 +23,20 @@ dependencies:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
25
  version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: activesupport
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
26
40
  - !ruby/object:Gem::Dependency
27
41
  name: json-schema
28
42
  requirement: !ruby/object:Gem::Requirement
@@ -37,6 +51,20 @@ dependencies:
37
51
  - - ">="
38
52
  - !ruby/object:Gem::Version
39
53
  version: '0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: actionpack
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
40
68
  description: A gem for managing service objects.
41
69
  email:
42
70
  - sebscholl@gmail.com
@@ -45,10 +73,13 @@ extensions: []
45
73
  extra_rdoc_files: []
46
74
  files:
47
75
  - ".rspec"
76
+ - ".rubocop.yml"
48
77
  - CHANGELOG.md
49
78
  - LICENSE.txt
50
79
  - READme.md
51
80
  - Rakefile
81
+ - builds/servus-0.0.1.gem
82
+ - builds/servus-0.1.1.gem
52
83
  - lib/generators/servus/service/service_generator.rb
53
84
  - lib/generators/servus/service/templates/arguments.json.erb
54
85
  - lib/generators/servus/service/templates/result.json.erb
@@ -57,9 +88,15 @@ files:
57
88
  - lib/servus.rb
58
89
  - lib/servus/base.rb
59
90
  - lib/servus/config.rb
91
+ - lib/servus/extensions/async/call.rb
92
+ - lib/servus/extensions/async/errors.rb
93
+ - lib/servus/extensions/async/ext.rb
94
+ - lib/servus/extensions/async/job.rb
95
+ - lib/servus/helpers/controller_helpers.rb
60
96
  - lib/servus/railtie.rb
61
97
  - lib/servus/support/errors.rb
62
98
  - lib/servus/support/logger.rb
99
+ - lib/servus/support/rescuer.rb
63
100
  - lib/servus/support/response.rb
64
101
  - lib/servus/support/validator.rb
65
102
  - lib/servus/version.rb