servus 0.1.3 → 0.1.5
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/.claude/commands/check-docs.md +1 -0
- data/.claude/commands/consistency-check.md +1 -0
- data/.claude/commands/fine-tooth-comb.md +1 -0
- data/.claude/commands/red-green-refactor.md +5 -0
- data/.claude/settings.json +15 -0
- data/.rubocop.yml +18 -2
- data/.yardopts +6 -0
- data/CHANGELOG.md +47 -0
- data/CLAUDE.md +10 -0
- data/IDEAS.md +5 -0
- data/READme.md +300 -47
- data/Rakefile +33 -0
- data/builds/servus-0.1.3.gem +0 -0
- data/builds/servus-0.1.4.gem +0 -0
- data/builds/servus-0.1.5.gem +0 -0
- data/docs/core/1_overview.md +77 -0
- data/docs/core/2_architecture.md +120 -0
- data/docs/core/3_service_objects.md +121 -0
- data/docs/current_focus.md +569 -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/features/5_event_bus.md +244 -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 +104 -0
- data/docs/integration/2_testing.md +287 -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/event_handler/event_handler_generator.rb +59 -0
- data/lib/generators/servus/event_handler/templates/handler.rb.erb +86 -0
- data/lib/generators/servus/event_handler/templates/handler_spec.rb.erb +48 -0
- data/lib/generators/servus/service/service_generator.rb +68 -1
- data/lib/generators/servus/service/templates/arguments.json.erb +19 -10
- data/lib/generators/servus/service/templates/result.json.erb +8 -2
- data/lib/generators/servus/service/templates/service.rb.erb +102 -5
- data/lib/generators/servus/service/templates/service_spec.rb.erb +67 -6
- data/lib/servus/base.rb +275 -58
- data/lib/servus/config.rb +83 -17
- data/lib/servus/event_handler.rb +275 -0
- data/lib/servus/events/bus.rb +137 -0
- data/lib/servus/events/emitter.rb +162 -0
- data/lib/servus/events/errors.rb +10 -0
- 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/railtie.rb +16 -0
- 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 +147 -19
- data/lib/servus/testing/example_builders.rb +133 -0
- data/lib/servus/testing/example_extractor.rb +309 -0
- data/lib/servus/testing/matchers.rb +88 -0
- data/lib/servus/testing.rb +19 -0
- data/lib/servus/version.rb +1 -1
- data/lib/servus.rb +6 -0
- metadata +135 -19
|
@@ -3,14 +3,34 @@
|
|
|
3
3
|
module Servus
|
|
4
4
|
module Extensions
|
|
5
5
|
module Async
|
|
6
|
+
# Error classes for asynchronous service execution.
|
|
7
|
+
#
|
|
8
|
+
# These errors are raised when async operations fail, such as job enqueueing
|
|
9
|
+
# failures or missing service classes during job execution.
|
|
6
10
|
module Errors
|
|
7
|
-
# Base error class for async
|
|
11
|
+
# Base error class for all async extension errors.
|
|
12
|
+
#
|
|
13
|
+
# All async-related errors inherit from this class for easy rescue handling.
|
|
8
14
|
class AsyncError < StandardError; end
|
|
9
15
|
|
|
10
|
-
#
|
|
16
|
+
# Raised when enqueueing a background job fails.
|
|
17
|
+
#
|
|
18
|
+
# This typically occurs due to connection issues with the job backend
|
|
19
|
+
# (Redis, database, etc.) or configuration problems.
|
|
20
|
+
#
|
|
21
|
+
# @example
|
|
22
|
+
# Services::SendEmail::Service.call_async(user_id: 123)
|
|
23
|
+
# # => Servus::Extensions::Async::Errors::JobEnqueueError: Failed to enqueue async job
|
|
11
24
|
class JobEnqueueError < AsyncError; end
|
|
12
25
|
|
|
13
|
-
#
|
|
26
|
+
# Raised when a service class name cannot be found.
|
|
27
|
+
#
|
|
28
|
+
# This occurs during job execution when the service class string
|
|
29
|
+
# cannot be constantized, usually due to typos or deleted classes.
|
|
30
|
+
#
|
|
31
|
+
# @example
|
|
32
|
+
# Job.perform_later(name: "NonExistent::Service", args: {})
|
|
33
|
+
# # => Servus::Extensions::Async::Errors::ServiceNotFoundError: Service class 'NonExistent::Service' not found
|
|
14
34
|
class ServiceNotFoundError < AsyncError; end
|
|
15
35
|
end
|
|
16
36
|
end
|
|
@@ -2,13 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
module Servus
|
|
4
4
|
module Extensions
|
|
5
|
-
#
|
|
5
|
+
# Asynchronous execution extensions for Servus services.
|
|
6
|
+
#
|
|
7
|
+
# This module provides the infrastructure for running services in background jobs
|
|
8
|
+
# via ActiveJob. When loaded, it extends {Servus::Base} with the {Call#call_async} method.
|
|
9
|
+
#
|
|
10
|
+
# @see Servus::Extensions::Async::Call
|
|
11
|
+
# @see Servus::Extensions::Async::Job
|
|
6
12
|
module Async
|
|
7
13
|
require 'servus/extensions/async/errors'
|
|
8
14
|
require 'servus/extensions/async/job'
|
|
9
15
|
require 'servus/extensions/async/call'
|
|
10
16
|
|
|
11
|
-
#
|
|
17
|
+
# Extension module for async functionality.
|
|
18
|
+
#
|
|
19
|
+
# @api private
|
|
12
20
|
module Ext; end
|
|
13
21
|
end
|
|
14
22
|
end
|
|
@@ -3,21 +3,32 @@
|
|
|
3
3
|
module Servus
|
|
4
4
|
module Extensions
|
|
5
5
|
module Async
|
|
6
|
-
#
|
|
6
|
+
# ActiveJob for executing Servus services asynchronously.
|
|
7
7
|
#
|
|
8
|
-
# This job
|
|
9
|
-
# It
|
|
10
|
-
#
|
|
8
|
+
# This job is used by {Call#call_async} to execute services in the background.
|
|
9
|
+
# It receives the service class name and arguments, instantiates the service,
|
|
10
|
+
# and executes it via {Servus::Base.call}.
|
|
11
11
|
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
12
|
+
# @example Enqueued by call_async
|
|
13
|
+
# Services::SendEmail::Service.call_async(user_id: 123)
|
|
14
|
+
# # Internally enqueues:
|
|
15
|
+
# # Job.perform_later(name: "Services::SendEmail::Service", args: { user_id: 123 })
|
|
14
16
|
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
# Errors during service execution are logged.
|
|
17
|
+
# @api private
|
|
18
18
|
class Job < ActiveJob::Base
|
|
19
19
|
queue_as :default
|
|
20
20
|
|
|
21
|
+
# Executes the service with the provided arguments.
|
|
22
|
+
#
|
|
23
|
+
# Dynamically loads the service class by name and calls it with the
|
|
24
|
+
# provided keyword arguments.
|
|
25
|
+
#
|
|
26
|
+
# @param name [String] fully-qualified service class name
|
|
27
|
+
# @param args [Hash] keyword arguments to pass to the service
|
|
28
|
+
# @return [Servus::Support::Response] the service execution result
|
|
29
|
+
# @raise [Servus::Extensions::Async::Errors::ServiceNotFoundError] if service class doesn't exist
|
|
30
|
+
#
|
|
31
|
+
# @api private
|
|
21
32
|
def perform(name:, args:)
|
|
22
33
|
constantize!(name).call(**args)
|
|
23
34
|
end
|
|
@@ -26,6 +37,16 @@ module Servus
|
|
|
26
37
|
|
|
27
38
|
attr_reader :klass
|
|
28
39
|
|
|
40
|
+
# Safely constantizes a class name string.
|
|
41
|
+
#
|
|
42
|
+
# Converts a string class name to its corresponding class constant,
|
|
43
|
+
# raising an error if the class doesn't exist.
|
|
44
|
+
#
|
|
45
|
+
# @param class_name [String] the service class name
|
|
46
|
+
# @return [Class] the service class
|
|
47
|
+
# @raise [Servus::Extensions::Async::Errors::ServiceNotFoundError] if class not found
|
|
48
|
+
#
|
|
49
|
+
# @api private
|
|
29
50
|
def constantize!(class_name)
|
|
30
51
|
"::#{class_name}".safe_constantize ||
|
|
31
52
|
(raise Errors::ServiceNotFoundError, "Service class '#{class_name}' not found.")
|
|
@@ -2,62 +2,98 @@
|
|
|
2
2
|
|
|
3
3
|
module Servus
|
|
4
4
|
module Helpers
|
|
5
|
-
#
|
|
5
|
+
# Rails controller helper methods for service integration.
|
|
6
|
+
#
|
|
7
|
+
# Provides convenient methods for calling services from controllers and
|
|
8
|
+
# handling their responses. Automatically included in ActionController::Base
|
|
9
|
+
# when Servus is loaded in a Rails application.
|
|
10
|
+
#
|
|
11
|
+
# @example Including in a controller
|
|
12
|
+
# class ApplicationController < ActionController::Base
|
|
13
|
+
# include Servus::Helpers::ControllerHelpers
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# @see #run_service
|
|
17
|
+
# @see #render_service_object_error
|
|
6
18
|
module ControllerHelpers
|
|
7
|
-
#
|
|
19
|
+
# Executes a service and handles success/failure automatically.
|
|
8
20
|
#
|
|
9
|
-
# This method
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
# automatically format and return an API error response.
|
|
21
|
+
# This method runs the service with the provided parameters. On success,
|
|
22
|
+
# it stores the result in @result for use in views. On failure, it
|
|
23
|
+
# automatically calls {#render_service_object_error} with the error details.
|
|
13
24
|
#
|
|
14
|
-
# @
|
|
15
|
-
#
|
|
16
|
-
# def index
|
|
17
|
-
# run_service MyService::Service, params
|
|
18
|
-
# end
|
|
19
|
-
# end
|
|
25
|
+
# The result is always stored in the @result instance variable, making it
|
|
26
|
+
# available in views for rendering successful responses.
|
|
20
27
|
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
28
|
+
# @param klass [Class] service class to execute (must inherit from {Servus::Base})
|
|
29
|
+
# @param params [Hash] keyword arguments to pass to the service
|
|
30
|
+
# @return [Servus::Support::Response, nil] the service result, or nil if error rendered
|
|
23
31
|
#
|
|
24
|
-
# @example
|
|
25
|
-
#
|
|
26
|
-
#
|
|
32
|
+
# @example Basic usage
|
|
33
|
+
# class UsersController < ApplicationController
|
|
34
|
+
# def create
|
|
35
|
+
# run_service Services::CreateUser::Service, user_params
|
|
36
|
+
# # If successful, @result is available for rendering
|
|
37
|
+
# # If failed, error response is automatically rendered
|
|
38
|
+
# end
|
|
27
39
|
# end
|
|
28
40
|
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
41
|
+
# @example Using @result in views
|
|
42
|
+
# # In your Jbuilder view (create.json.jbuilder)
|
|
43
|
+
# json.user do
|
|
44
|
+
# json.id @result.data[:user_id]
|
|
45
|
+
# json.email @result.data[:email]
|
|
46
|
+
# end
|
|
32
47
|
#
|
|
33
|
-
# @example
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
48
|
+
# @example Manual success handling
|
|
49
|
+
# class UsersController < ApplicationController
|
|
50
|
+
# def create
|
|
51
|
+
# run_service Services::CreateUser::Service, user_params
|
|
52
|
+
# return unless @result.success?
|
|
38
53
|
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
54
|
+
# # Custom success handling
|
|
55
|
+
# redirect_to user_path(@result.data[:user_id])
|
|
56
|
+
# end
|
|
42
57
|
# end
|
|
43
58
|
#
|
|
44
|
-
# @
|
|
45
|
-
# @
|
|
46
|
-
# @return [Servus::Support::Response] The result of the service
|
|
47
|
-
#
|
|
48
|
-
# @see Servus::Support::Errors::ServiceError
|
|
59
|
+
# @see #render_service_object_error
|
|
60
|
+
# @see Servus::Base.call
|
|
49
61
|
def run_service(klass, params)
|
|
50
62
|
@result = klass.call(**params)
|
|
51
63
|
render_service_object_error(@result.error.api_error) unless @result.success?
|
|
52
64
|
end
|
|
53
65
|
|
|
54
|
-
#
|
|
66
|
+
# Renders a service error as a JSON response.
|
|
55
67
|
#
|
|
56
|
-
# This method is
|
|
68
|
+
# This method is called automatically by {#run_service} when a service fails,
|
|
69
|
+
# but can also be called manually for custom error handling. It renders the
|
|
70
|
+
# error's api_error hash with the appropriate HTTP status code.
|
|
57
71
|
#
|
|
58
|
-
#
|
|
72
|
+
# Override this method in your controller to customize error response format.
|
|
73
|
+
#
|
|
74
|
+
# @param api_error [Hash] error hash with :code and :message keys from {Servus::Support::Errors::ServiceError#api_error}
|
|
75
|
+
# @return [void]
|
|
76
|
+
#
|
|
77
|
+
# @example Default behavior
|
|
78
|
+
# # Renders: { code: :not_found, message: "User not found" }
|
|
79
|
+
# # With status: 404
|
|
80
|
+
# render_service_object_error(result.error.api_error)
|
|
81
|
+
#
|
|
82
|
+
# @example Custom error rendering
|
|
83
|
+
# class ApplicationController < ActionController::Base
|
|
84
|
+
# def render_service_object_error(api_error)
|
|
85
|
+
# render json: {
|
|
86
|
+
# error: {
|
|
87
|
+
# type: api_error[:code],
|
|
88
|
+
# details: api_error[:message],
|
|
89
|
+
# timestamp: Time.current
|
|
90
|
+
# }
|
|
91
|
+
# }, status: api_error[:code]
|
|
92
|
+
# end
|
|
93
|
+
# end
|
|
59
94
|
#
|
|
60
|
-
# @see Servus::Support::Errors::ServiceError
|
|
95
|
+
# @see Servus::Support::Errors::ServiceError#api_error
|
|
96
|
+
# @see #run_service
|
|
61
97
|
def render_service_object_error(api_error)
|
|
62
98
|
render json: api_error, status: api_error[:code]
|
|
63
99
|
end
|
data/lib/servus/railtie.rb
CHANGED
|
@@ -18,5 +18,21 @@ module Servus
|
|
|
18
18
|
Servus::Base.extend Servus::Extensions::Async::Call
|
|
19
19
|
end
|
|
20
20
|
end
|
|
21
|
+
|
|
22
|
+
# Load event handlers and clear on reload
|
|
23
|
+
config.to_prepare do
|
|
24
|
+
Servus::Events::Bus.clear if Rails.env.development?
|
|
25
|
+
|
|
26
|
+
# Eager load all event handlers
|
|
27
|
+
events_path = Rails.root.join(Servus.config.events_dir)
|
|
28
|
+
Dir[File.join(events_path, '**/*_handler.rb')].each do |handler_file|
|
|
29
|
+
require_dependency handler_file
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# NOTE: Event validation is available but not run automatically due to load order issues.
|
|
34
|
+
# To validate handlers match emitted events, call manually:
|
|
35
|
+
# Servus::EventHandler.validate_all_handlers!
|
|
36
|
+
# Or create a rake task for CI validation.
|
|
21
37
|
end
|
|
22
38
|
end
|
|
@@ -2,34 +2,80 @@
|
|
|
2
2
|
|
|
3
3
|
module Servus
|
|
4
4
|
module Support
|
|
5
|
+
# Contains all error classes used by Servus services.
|
|
6
|
+
#
|
|
7
|
+
# All error classes inherit from {ServiceError} and provide both a human-readable
|
|
8
|
+
# message and an API-friendly error response via {ServiceError#api_error}.
|
|
9
|
+
#
|
|
10
|
+
# @see ServiceError
|
|
5
11
|
module Errors
|
|
6
|
-
# Base error class for
|
|
12
|
+
# Base error class for all Servus service errors.
|
|
7
13
|
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
14
|
+
# This class provides the foundation for all service-related errors, including:
|
|
15
|
+
# - Default error messages via DEFAULT_MESSAGE constant
|
|
16
|
+
# - API-friendly error responses via {#api_error}
|
|
17
|
+
# - Automatic message fallback to default if none provided
|
|
18
|
+
#
|
|
19
|
+
# @example Creating a custom error type
|
|
20
|
+
# class MyCustomError < Servus::Support::Errors::ServiceError
|
|
21
|
+
# DEFAULT_MESSAGE = 'Something went wrong'
|
|
22
|
+
#
|
|
23
|
+
# def api_error
|
|
24
|
+
# { code: :custom_error, message: message }
|
|
25
|
+
# end
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# @example Using with failure method
|
|
29
|
+
# def call
|
|
30
|
+
# return failure("User not found", type: Servus::Support::Errors::NotFoundError)
|
|
31
|
+
# # ...
|
|
32
|
+
# end
|
|
10
33
|
class ServiceError < StandardError
|
|
11
34
|
attr_reader :message
|
|
12
35
|
|
|
13
36
|
DEFAULT_MESSAGE = 'An error occurred'
|
|
14
37
|
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
# @
|
|
38
|
+
# Creates a new service error instance.
|
|
39
|
+
#
|
|
40
|
+
# @param message [String, nil] custom error message (uses DEFAULT_MESSAGE if nil)
|
|
41
|
+
# @return [ServiceError] the error instance
|
|
42
|
+
#
|
|
43
|
+
# @example With custom message
|
|
44
|
+
# error = ServiceError.new("Something went wrong")
|
|
45
|
+
# error.message # => "Something went wrong"
|
|
46
|
+
#
|
|
47
|
+
# @example With default message
|
|
48
|
+
# error = ServiceError.new
|
|
49
|
+
# error.message # => "An error occurred"
|
|
18
50
|
def initialize(message = nil)
|
|
19
51
|
@message = message || self.class::DEFAULT_MESSAGE
|
|
20
52
|
super("#{self.class}: #{message}")
|
|
21
53
|
end
|
|
22
54
|
|
|
23
|
-
#
|
|
24
|
-
#
|
|
55
|
+
# Returns an API-friendly error response.
|
|
56
|
+
#
|
|
57
|
+
# This method formats the error for API responses, providing both a
|
|
58
|
+
# symbolic code and the error message. Override in subclasses to customize
|
|
59
|
+
# the error code for specific HTTP status codes.
|
|
60
|
+
#
|
|
61
|
+
# @return [Hash] hash with :code and :message keys
|
|
62
|
+
#
|
|
63
|
+
# @example
|
|
64
|
+
# error = ServiceError.new("Failed to process")
|
|
65
|
+
# error.api_error # => { code: :bad_request, message: "Failed to process" }
|
|
25
66
|
def api_error
|
|
26
67
|
{ code: :bad_request, message: message }
|
|
27
68
|
end
|
|
28
69
|
end
|
|
29
70
|
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
71
|
+
# Represents a 400 Bad Request error.
|
|
72
|
+
#
|
|
73
|
+
# Use this error when the client sends malformed or invalid request data.
|
|
74
|
+
#
|
|
75
|
+
# @example
|
|
76
|
+
# def call
|
|
77
|
+
# return failure("Invalid JSON format", type: BadRequestError)
|
|
78
|
+
# end
|
|
33
79
|
class BadRequestError < ServiceError
|
|
34
80
|
DEFAULT_MESSAGE = 'Bad request'
|
|
35
81
|
|
|
@@ -40,29 +86,45 @@ module Servus
|
|
|
40
86
|
end
|
|
41
87
|
end
|
|
42
88
|
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
#
|
|
89
|
+
# Represents a 401 Unauthorized error for authentication failures.
|
|
90
|
+
#
|
|
91
|
+
# Use this error when authentication credentials are missing, invalid, or expired.
|
|
92
|
+
#
|
|
93
|
+
# @example
|
|
94
|
+
# def call
|
|
95
|
+
# return failure("Invalid API key", type: AuthenticationError) unless valid_api_key?
|
|
96
|
+
# end
|
|
46
97
|
class AuthenticationError < ServiceError
|
|
47
98
|
DEFAULT_MESSAGE = 'Authentication failed'
|
|
48
99
|
|
|
49
|
-
#
|
|
50
|
-
# @return [Hash] The error response
|
|
100
|
+
# @return [Hash] API error response with :unauthorized code
|
|
51
101
|
def api_error
|
|
52
102
|
{ code: :unauthorized, message: message }
|
|
53
103
|
end
|
|
54
104
|
end
|
|
55
105
|
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
#
|
|
106
|
+
# Represents a 401 Unauthorized error (alias for AuthenticationError).
|
|
107
|
+
#
|
|
108
|
+
# Use this error for authorization failures when credentials are valid but
|
|
109
|
+
# lack sufficient permissions.
|
|
110
|
+
#
|
|
111
|
+
# @example
|
|
112
|
+
# def call
|
|
113
|
+
# return failure("Access denied", type: UnauthorizedError) unless user.admin?
|
|
114
|
+
# end
|
|
59
115
|
class UnauthorizedError < AuthenticationError
|
|
60
116
|
DEFAULT_MESSAGE = 'Unauthorized'
|
|
61
117
|
end
|
|
62
118
|
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
#
|
|
119
|
+
# Represents a 403 Forbidden error.
|
|
120
|
+
#
|
|
121
|
+
# Use this error when the user is authenticated but not authorized to perform
|
|
122
|
+
# the requested action.
|
|
123
|
+
#
|
|
124
|
+
# @example
|
|
125
|
+
# def call
|
|
126
|
+
# return failure("Insufficient permissions", type: ForbiddenError) unless can_access?
|
|
127
|
+
# end
|
|
66
128
|
class ForbiddenError < ServiceError
|
|
67
129
|
DEFAULT_MESSAGE = 'Forbidden'
|
|
68
130
|
|
|
@@ -73,60 +135,88 @@ module Servus
|
|
|
73
135
|
end
|
|
74
136
|
end
|
|
75
137
|
|
|
76
|
-
#
|
|
77
|
-
#
|
|
78
|
-
#
|
|
138
|
+
# Represents a 404 Not Found error.
|
|
139
|
+
#
|
|
140
|
+
# Use this error when a requested resource cannot be found.
|
|
141
|
+
#
|
|
142
|
+
# @example
|
|
143
|
+
# def call
|
|
144
|
+
# user = User.find_by(id: @user_id)
|
|
145
|
+
# return failure("User not found", type: NotFoundError) unless user
|
|
146
|
+
# end
|
|
79
147
|
class NotFoundError < ServiceError
|
|
80
148
|
DEFAULT_MESSAGE = 'Not found'
|
|
81
149
|
|
|
82
|
-
#
|
|
83
|
-
# @return [Hash] The error response
|
|
150
|
+
# @return [Hash] API error response with :not_found code
|
|
84
151
|
def api_error
|
|
85
152
|
{ code: :not_found, message: message }
|
|
86
153
|
end
|
|
87
154
|
end
|
|
88
155
|
|
|
89
|
-
#
|
|
90
|
-
#
|
|
91
|
-
#
|
|
156
|
+
# Represents a 422 Unprocessable Entity error.
|
|
157
|
+
#
|
|
158
|
+
# Use this error when the request is well-formed but contains semantic errors
|
|
159
|
+
# that prevent processing (e.g., business logic violations).
|
|
160
|
+
#
|
|
161
|
+
# @example
|
|
162
|
+
# def call
|
|
163
|
+
# return failure("Order already shipped", type: UnprocessableEntityError) if @order.shipped?
|
|
164
|
+
# end
|
|
92
165
|
class UnprocessableEntityError < ServiceError
|
|
93
166
|
DEFAULT_MESSAGE = 'Unprocessable entity'
|
|
94
167
|
|
|
95
|
-
#
|
|
96
|
-
# @return [Hash] The error response
|
|
168
|
+
# @return [Hash] API error response with :unprocessable_entity code
|
|
97
169
|
def api_error
|
|
98
170
|
{ code: :unprocessable_entity, message: message }
|
|
99
171
|
end
|
|
100
172
|
end
|
|
101
173
|
|
|
102
|
-
#
|
|
103
|
-
#
|
|
104
|
-
#
|
|
174
|
+
# Represents validation failures (inherits 422 status).
|
|
175
|
+
#
|
|
176
|
+
# Automatically raised by the framework when schema validation fails.
|
|
177
|
+
# Can also be used for custom validation errors.
|
|
178
|
+
#
|
|
179
|
+
# @example
|
|
180
|
+
# def call
|
|
181
|
+
# return failure("Email format invalid", type: ValidationError) unless valid_email?
|
|
182
|
+
# end
|
|
105
183
|
class ValidationError < UnprocessableEntityError
|
|
106
184
|
DEFAULT_MESSAGE = 'Validation failed'
|
|
107
185
|
end
|
|
108
186
|
|
|
109
|
-
#
|
|
110
|
-
#
|
|
111
|
-
#
|
|
187
|
+
# Represents a 500 Internal Server Error.
|
|
188
|
+
#
|
|
189
|
+
# Use this error for unexpected server-side failures.
|
|
190
|
+
#
|
|
191
|
+
# @example
|
|
192
|
+
# def call
|
|
193
|
+
# return failure("Database connection lost", type: InternalServerError) if db_down?
|
|
194
|
+
# end
|
|
112
195
|
class InternalServerError < ServiceError
|
|
113
196
|
DEFAULT_MESSAGE = 'Internal server error'
|
|
114
197
|
|
|
115
|
-
#
|
|
116
|
-
# @return [Hash] The error response
|
|
198
|
+
# @return [Hash] API error response with :internal_server_error code
|
|
117
199
|
def api_error
|
|
118
200
|
{ code: :internal_server_error, message: message }
|
|
119
201
|
end
|
|
120
202
|
end
|
|
121
203
|
|
|
122
|
-
#
|
|
123
|
-
#
|
|
124
|
-
#
|
|
204
|
+
# Represents a 503 Service Unavailable error.
|
|
205
|
+
#
|
|
206
|
+
# Use this error when a service dependency is temporarily unavailable.
|
|
207
|
+
#
|
|
208
|
+
# @example Using with rescue_from
|
|
209
|
+
# class MyService < Servus::Base
|
|
210
|
+
# rescue_from Net::HTTPError, use: ServiceUnavailableError
|
|
211
|
+
#
|
|
212
|
+
# def call
|
|
213
|
+
# make_external_api_call
|
|
214
|
+
# end
|
|
215
|
+
# end
|
|
125
216
|
class ServiceUnavailableError < ServiceError
|
|
126
217
|
DEFAULT_MESSAGE = 'Service unavailable'
|
|
127
218
|
|
|
128
|
-
#
|
|
129
|
-
# @return [Hash] The error response
|
|
219
|
+
# @return [Hash] API error response with :service_unavailable code
|
|
130
220
|
def api_error
|
|
131
221
|
{ code: :service_unavailable, message: message }
|
|
132
222
|
end
|