servus 0.0.1 → 0.1.1
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/.rubocop.yml +11 -0
- data/CHANGELOG.md +6 -0
- data/READme.md +96 -0
- data/Rakefile +3 -3
- data/builds/servus-0.0.1.gem +0 -0
- data/lib/generators/servus/service/service_generator.rb +11 -11
- data/lib/servus/base.rb +27 -9
- data/lib/servus/config.rb +6 -4
- data/lib/servus/helpers/controller_helpers.rb +66 -0
- data/lib/servus/railtie.rb +9 -2
- data/lib/servus/support/errors.rb +11 -10
- data/lib/servus/support/logger.rb +2 -1
- data/lib/servus/support/rescuer.rb +79 -0
- data/lib/servus/support/response.rb +14 -1
- data/lib/servus/support/validator.rb +36 -18
- data/lib/servus/version.rb +1 -1
- data/lib/servus.rb +16 -10
- metadata +33 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 69e20c0a6d4d7b3651839114f930d1befbc49701c15f9489f6a180e9384698c9
|
|
4
|
+
data.tar.gz: 1267963f2b5957d73fc4fb39654df87104932fc831786541db48ba478fa53d4f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9ebcf136c2ce120b4489c4cf403c9a4b850ae83156429816acdc15776140b21a3d420cbe14fb24def19fde9c07fa7ef2fdac3cc2b62b0d2f6004a8dbd0898e8d
|
|
7
|
+
data.tar.gz: b367bd7c82bf0f5e4606b0bf8cbee2c777171018a370478d3a9ef07a5dc7d24ea6e240c78bdaf34c5a0cd19491101ea88c865b15e9bfa589e3dad76672ad4868
|
data/.rubocop.yml
ADDED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.1.1] - 2025-08-20
|
|
4
|
+
|
|
5
|
+
- Added: Added `rescue_from` method to `Servus::Base` to rescue from standard errors and use custom error types.
|
|
6
|
+
- Added: Added `run_service` and `render_service_object_error` helpers to `Servus::Helpers::ControllerHelpers`.
|
|
7
|
+
- Fixed: All rubocop warnings.
|
|
8
|
+
|
|
3
9
|
## [0.1.0] - 2025-04-28
|
|
4
10
|
|
|
5
11
|
- Initial release
|
data/READme.md
CHANGED
|
@@ -261,6 +261,102 @@ class SomeController < AppController
|
|
|
261
261
|
end
|
|
262
262
|
```
|
|
263
263
|
|
|
264
|
+
### `rescue_from` for service errors
|
|
265
|
+
|
|
266
|
+
Services can configure default error handling using the `rescue_from` method.
|
|
267
|
+
|
|
268
|
+
```ruby
|
|
269
|
+
class SomeServiceObject::Service < Servus::Base
|
|
270
|
+
class SomethingBroke < StandardError; end
|
|
271
|
+
class SomethingGlitched < StandardError; end
|
|
272
|
+
|
|
273
|
+
# Rescue from standard errors and use custom error
|
|
274
|
+
rescue_from
|
|
275
|
+
SomethingBroke,
|
|
276
|
+
SomethingGlitched,
|
|
277
|
+
use: Servus::Support::Errors::ServiceUnavailableError # this is optional
|
|
278
|
+
|
|
279
|
+
def call
|
|
280
|
+
do_something
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
private
|
|
284
|
+
|
|
285
|
+
def do_something
|
|
286
|
+
make_and_api_call
|
|
287
|
+
rescue Net::HTTPError => e
|
|
288
|
+
raise SomethingGlitched, "Whoaaaa, something went wrong! #{e.message}"
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
```sh
|
|
295
|
+
result = SomeServiceObject::Service.call
|
|
296
|
+
# Failure response
|
|
297
|
+
result.error.class
|
|
298
|
+
=> Servus::Support::Errors::ServiceUnavailableError
|
|
299
|
+
result.error.message
|
|
300
|
+
=> "[SomeServiceObject::Service::SomethingGlitched]: Whoaaaa, something went wrong! Net::HTTPError (503)"
|
|
301
|
+
result.error.api_error
|
|
302
|
+
=> { code: :service_unavailable, message: "[SomeServiceObject::Service::SomethingGlitched]: Whoaaaa, something went wrong! Net::HTTPError (503)" }
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
The `rescue_from` method will rescue from the specified errors and use the specified error type to create a failure response object with
|
|
306
|
+
the custom error. It helps eliminate the need to manually rescue many errors and create failure responses within the call method of
|
|
307
|
+
a service object.
|
|
308
|
+
|
|
309
|
+
## Controller Helpers
|
|
310
|
+
|
|
311
|
+
Service objects can be called from controllers using the `run_service` and `render_service_object_error` helpers.
|
|
312
|
+
|
|
313
|
+
### run_service
|
|
314
|
+
|
|
315
|
+
`run_service` calls the service object with the provided parameters and set's an instance variable `@result` to the
|
|
316
|
+
result of the service object if the result is successful. If the result is not successful, it will pass the result
|
|
317
|
+
to error to the `render_service_object_error` helper. This allows for easy error handling in the controller for
|
|
318
|
+
repetetive usecases.
|
|
319
|
+
|
|
320
|
+
```ruby
|
|
321
|
+
class SomeController < AppController
|
|
322
|
+
# Before
|
|
323
|
+
def controller_action
|
|
324
|
+
result = Services::SomeServiceObject::Service.call(my_params)
|
|
325
|
+
return if result.success?
|
|
326
|
+
render_service_object_error(result.error.api_error)
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# After
|
|
330
|
+
def controller_action_refactored
|
|
331
|
+
run_service Services::SomeServiceObject::Service, my_params
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### render_service_object_error
|
|
337
|
+
|
|
338
|
+
`render_service_object_error` renders the error of a service object. It expects a hash with a `message` key and a `code` key from
|
|
339
|
+
the api_error method of the service error. This is all setup by default for a JSON API response, thought the method can be
|
|
340
|
+
overridden if needed to handle different usecases.
|
|
341
|
+
|
|
342
|
+
```ruby
|
|
343
|
+
# Behind the scenes, render_service_object_error calls the following:
|
|
344
|
+
#
|
|
345
|
+
# error = result.error.api_error
|
|
346
|
+
# => { message: "Error message", code: 400 }
|
|
347
|
+
#
|
|
348
|
+
# render json: { message: error[:message], code: error[:code] }, status: error[:code]
|
|
349
|
+
|
|
350
|
+
class SomeController < AppController
|
|
351
|
+
def controller_action
|
|
352
|
+
result = Services::SomeServiceObject::Service.call(my_params)
|
|
353
|
+
return if result.success?
|
|
354
|
+
|
|
355
|
+
render_service_object_error(result.error.api_error)
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
```
|
|
359
|
+
|
|
264
360
|
## **Schema Validation**
|
|
265
361
|
|
|
266
362
|
Service objects support two methods for schema validation: JSON Schema files and inline schema declarations.
|
data/Rakefile
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
3
|
+
require 'bundler/gem_tasks'
|
|
4
|
+
require 'rspec/core/rake_task'
|
|
5
5
|
|
|
6
6
|
RSpec::Core::RakeTask.new(:spec)
|
|
7
7
|
|
|
8
|
-
require
|
|
8
|
+
require 'rubocop/rake_task'
|
|
9
9
|
|
|
10
10
|
RuboCop::RakeTask.new
|
|
11
11
|
|
|
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(
|
|
7
|
+
source_root File.expand_path('templates', __dir__)
|
|
8
8
|
|
|
9
|
-
argument :parameters, type: :array, default: [], banner:
|
|
9
|
+
argument :parameters, type: :array, default: [], banner: 'parameter'
|
|
10
10
|
|
|
11
11
|
def create_service_file
|
|
12
|
-
template
|
|
13
|
-
template
|
|
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
|
|
17
|
-
template
|
|
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?(
|
|
43
|
+
service_class_name.include?('::') ? service_class_name : "::#{service_class_name}"
|
|
44
44
|
end
|
|
45
45
|
|
|
46
46
|
def parameter_list
|
|
47
|
-
return
|
|
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
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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,6 +1,8 @@
|
|
|
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
|
|
@@ -8,10 +10,10 @@ module Servus
|
|
|
8
10
|
def initialize
|
|
9
11
|
# Default to Rails.root if available, otherwise use current working directory
|
|
10
12
|
@schema_root = if defined?(Rails)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
Rails.root.join('app/schemas/services')
|
|
14
|
+
else
|
|
15
|
+
File.expand_path('../../../app/schemas/services', __dir__)
|
|
16
|
+
end
|
|
15
17
|
end
|
|
16
18
|
|
|
17
19
|
# Returns the path for a specific service's schema
|
|
@@ -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
|
data/lib/servus/railtie.rb
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# lib/servus/railtie.rb
|
|
4
|
-
require
|
|
4
|
+
require 'rails/railtie'
|
|
5
5
|
|
|
6
6
|
module Servus
|
|
7
|
-
|
|
7
|
+
# Railtie for Rails integration
|
|
8
|
+
class Railtie < Rails::Railtie
|
|
9
|
+
initializer 'servus.controller_helpers' do
|
|
10
|
+
ActiveSupport.on_load(:action_controller) do
|
|
11
|
+
include Servus::Helpers::ControllerHelpers
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
8
15
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
126
|
+
DEFAULT_MESSAGE = 'Service unavailable'
|
|
126
127
|
|
|
127
128
|
# 503 error response
|
|
128
129
|
# @return [Hash] The error response
|
|
@@ -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
|
-
|
|
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,
|
|
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,
|
|
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
|
|
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)
|
|
56
|
-
|
|
57
|
-
|
|
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
|
data/lib/servus/version.rb
CHANGED
data/lib/servus.rb
CHANGED
|
@@ -1,24 +1,30 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Globals
|
|
4
|
-
require
|
|
5
|
-
require
|
|
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
|
|
16
|
+
require_relative 'servus/railtie' if defined?(Rails::Railtie)
|
|
12
17
|
|
|
13
18
|
# Config
|
|
14
|
-
require_relative
|
|
19
|
+
require_relative 'servus/config'
|
|
15
20
|
|
|
16
21
|
# Support
|
|
17
|
-
require_relative
|
|
18
|
-
require_relative
|
|
19
|
-
require_relative
|
|
20
|
-
require_relative
|
|
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
|
|
24
|
-
require_relative
|
|
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.
|
|
4
|
+
version: 0.1.1
|
|
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,12 @@ 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
|
|
52
82
|
- lib/generators/servus/service/service_generator.rb
|
|
53
83
|
- lib/generators/servus/service/templates/arguments.json.erb
|
|
54
84
|
- lib/generators/servus/service/templates/result.json.erb
|
|
@@ -57,9 +87,11 @@ files:
|
|
|
57
87
|
- lib/servus.rb
|
|
58
88
|
- lib/servus/base.rb
|
|
59
89
|
- lib/servus/config.rb
|
|
90
|
+
- lib/servus/helpers/controller_helpers.rb
|
|
60
91
|
- lib/servus/railtie.rb
|
|
61
92
|
- lib/servus/support/errors.rb
|
|
62
93
|
- lib/servus/support/logger.rb
|
|
94
|
+
- lib/servus/support/rescuer.rb
|
|
63
95
|
- lib/servus/support/response.rb
|
|
64
96
|
- lib/servus/support/validator.rb
|
|
65
97
|
- lib/servus/version.rb
|