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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 36f4d4b15ff9038d9c039f6efb0fe5027c6326811334dc3e478a906b31286d5d
4
- data.tar.gz: 786b002fe1937d5204088fdb982156bc1fb1ce7bc1c20f2ecd4c737e23e1acd5
3
+ metadata.gz: 69e20c0a6d4d7b3651839114f930d1befbc49701c15f9489f6a180e9384698c9
4
+ data.tar.gz: 1267963f2b5957d73fc4fb39654df87104932fc831786541db48ba478fa53d4f
5
5
  SHA512:
6
- metadata.gz: 04ce3e7a6cdd2371c4da93d98123ea640736856638dfa5c2a60d3c4b4c7ae1109bd597bf577cec892461677a906e0955ee782c20532901b3808eca80c6ccbb6a
7
- data.tar.gz: 9c07175986bd05753df2b29dbc70f854cbab12745183d6e8937cc89dd46e5e819df588ac0ef88ea2db8298cb6e470e798a8e3361cc8765194ddee1244c7a2d1c
6
+ metadata.gz: 9ebcf136c2ce120b4489c4cf403c9a4b850ae83156429816acdc15776140b21a3d420cbe14fb24def19fde9c07fa7ef2fdac3cc2b62b0d2f6004a8dbd0898e8d
7
+ data.tar.gz: b367bd7c82bf0f5e4606b0bf8cbee2c777171018a370478d3a9ef07a5dc7d24ea6e240c78bdaf34c5a0cd19491101ea88c865b15e9bfa589e3dad76672ad4868
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,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 "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
@@ -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,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
- Rails.root.join("app/schemas/services")
12
- else
13
- File.expand_path("../../../app/schemas/services", __dir__)
14
- end
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
@@ -1,8 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # lib/servus/railtie.rb
4
- require "rails/railtie"
4
+ require 'rails/railtie'
5
5
 
6
6
  module Servus
7
- class Railtie < Rails::Railtie; end
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 = "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.1'
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.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