steroids 1.0.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +854 -0
  3. data/Rakefile +27 -0
  4. data/app/jobs/steroids/async_service_job.rb +10 -0
  5. data/app/serializers/steroids/error_serializer.rb +35 -0
  6. data/lib/resources/quotes.yml +114 -0
  7. data/lib/steroids/controllers/methods.rb +18 -0
  8. data/lib/{concerns/controller.rb → steroids/controllers/responders_helper.rb} +20 -21
  9. data/lib/steroids/controllers/serializers_helper.rb +15 -0
  10. data/lib/steroids/engine.rb +6 -0
  11. data/lib/steroids/errors/base.rb +57 -0
  12. data/lib/steroids/errors/context.rb +70 -0
  13. data/lib/steroids/errors/quotes.rb +29 -0
  14. data/lib/steroids/errors.rb +89 -0
  15. data/lib/steroids/extensions/array_extension.rb +25 -0
  16. data/lib/steroids/extensions/class_extension.rb +141 -0
  17. data/lib/steroids/extensions/hash_extension.rb +14 -0
  18. data/lib/steroids/extensions/method_extension.rb +63 -0
  19. data/lib/steroids/extensions/module_extension.rb +32 -0
  20. data/lib/steroids/extensions/object_extension.rb +122 -0
  21. data/lib/steroids/extensions/proc_extension.rb +9 -0
  22. data/lib/steroids/logger.rb +162 -0
  23. data/lib/steroids/railtie.rb +60 -0
  24. data/lib/steroids/serializers/base.rb +7 -0
  25. data/lib/{concerns/serializer.rb → steroids/serializers/methods.rb} +3 -3
  26. data/lib/steroids/services/base.rb +181 -0
  27. data/lib/steroids/support/magic_class.rb +17 -0
  28. data/lib/steroids/support/noticable_methods.rb +134 -0
  29. data/lib/steroids/support/servicable_methods.rb +34 -0
  30. data/lib/{base/type.rb → steroids/types/base.rb} +3 -3
  31. data/lib/{base/model.rb → steroids/types/serializable_type.rb} +2 -2
  32. data/lib/steroids/version.rb +4 -0
  33. data/lib/steroids.rb +12 -0
  34. metadata +75 -34
  35. data/lib/base/class.rb +0 -15
  36. data/lib/base/error.rb +0 -87
  37. data/lib/base/hash.rb +0 -49
  38. data/lib/base/list.rb +0 -51
  39. data/lib/base/service.rb +0 -104
  40. data/lib/concern.rb +0 -130
  41. data/lib/concerns/error.rb +0 -20
  42. data/lib/concerns/model.rb +0 -9
  43. data/lib/errors/bad_request_error.rb +0 -15
  44. data/lib/errors/conflict_error.rb +0 -15
  45. data/lib/errors/forbidden_error.rb +0 -15
  46. data/lib/errors/generic_error.rb +0 -14
  47. data/lib/errors/internal_server_error.rb +0 -15
  48. data/lib/errors/not_found_error.rb +0 -15
  49. data/lib/errors/not_implemented_error.rb +0 -15
  50. data/lib/errors/unauthorized_error.rb +0 -15
  51. data/lib/errors/unprocessable_entity_error.rb +0 -15
@@ -0,0 +1,181 @@
1
+ module Steroids
2
+ module Services
3
+ class Base < Steroids::Support::MagicClass
4
+ include Steroids::Support::ServicableMethods
5
+ include Steroids::Support::NoticableMethods
6
+
7
+ @@wrap_in_transaction = true
8
+ @@skip_callbacks = false
9
+
10
+ class AmbiguousProcessMethodError < Steroids::Errors::Base; end
11
+
12
+ class AsyncProcessArgumentError < Steroids::Errors::Base; end
13
+
14
+ class RuntimeError < Steroids::Errors::Base
15
+ self.default_message = "Runtime error"
16
+ end
17
+
18
+ # --------------------------------------------------------------------------------------------
19
+ # Core public interface
20
+ # --------------------------------------------------------------------------------------------
21
+
22
+ def call(*args, **options, &block)
23
+ return unless process_method.present?
24
+
25
+ @steroids_force = (!!options[:force]) || false
26
+ @steroids_skip_callbacks = (!!options[:skip_callbacks]) || @@skip_callbacks || false
27
+ if process_method.name == :async_process
28
+ schedule_process(*args, **options, &block)
29
+ else
30
+ exec_process(*args, **options, &block)
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ # --------------------------------------------------------------------------------------------
37
+ # Run process
38
+ # --------------------------------------------------------------------------------------------
39
+
40
+ def exec_process(*args, **options, &block)
41
+ outcome = process_wrapper do
42
+ run_before_callbacks(*args, **options) unless @steroids_skip_callbacks
43
+ process_method.call.tap do |outcome|
44
+ drop! if !block_given? && errors.any?
45
+ run_after_callbacks(outcome) unless @steroids_skip_callbacks
46
+ end
47
+ end
48
+ rescue StandardError => outcome
49
+ errors.add(outcome.message, outcome)
50
+ if respond_to?(:rescue!, true) || block_given?
51
+ Steroids::Logger.print(outcome)
52
+ send_apply(:rescue!, outcome)
53
+ else
54
+ raise outcome
55
+ end
56
+ ensure
57
+ ensure! if respond_to?(:ensure!, true)
58
+ block.apply(self, outcome, noticable: self.noticable) if block_given?
59
+ end
60
+
61
+ def schedule_process(*args, **options, &block)
62
+ async_exec = (!!options[:async]) || true
63
+ if self.respond_to?(:async_process, true)
64
+ if async_exec?(async_exec)
65
+ AsyncServiceJob.perform_later(
66
+ class_name: self.class.name,
67
+ params: @_steroids_serialized_init_options
68
+ )
69
+ else
70
+ exec_process(*args, **options, &block)
71
+ end
72
+ end
73
+ end
74
+
75
+ def process_method
76
+ self.class.validate_process_definition!
77
+ @process_method ||= (try_method(:process) || try_method(:async_process))
78
+ end
79
+
80
+ def async_exec?(async)
81
+ development_env = Rails.env.development? || Rails.env.test?
82
+ !!if async == true && (Sidekiq::ProcessSet.new.any? || !development_env)
83
+ !(development_env || Rails.const_defined?(:Console))
84
+ end
85
+ end
86
+
87
+ # --------------------------------------------------------------------------------------------
88
+ # Process wrapper
89
+ # --------------------------------------------------------------------------------------------
90
+
91
+ def process_wrapper(&block)
92
+ return block.call unless @@wrap_in_transaction
93
+
94
+ ActiveRecord::Base.transaction do
95
+ block.call
96
+ end
97
+ rescue RuntimeError => error
98
+ errors.add(error.message)
99
+ end
100
+
101
+ def run_before_callbacks(*args, **options)
102
+ if self.class.steroids_before_callbacks.is_a?(Array)
103
+ self.class.steroids_before_callbacks.each do |callback|
104
+ send_apply(callback, *args, **options)
105
+ end
106
+ end
107
+ send_apply(:before_process, *args, **options)
108
+ end
109
+
110
+ def run_after_callbacks(outcome)
111
+ send_apply(:after_process, outcome)
112
+ if self.class.steroids_after_callbacks.is_a?(Array)
113
+ self.class.steroids_after_callbacks.each do |callback|
114
+ send_apply(callback, outcome)
115
+ end
116
+ end
117
+ end
118
+
119
+ # --------------------------------------------------------------------------------------------
120
+ # Flow control
121
+ # --------------------------------------------------------------------------------------------
122
+
123
+ def drop!(message_or_nil = nil, message: nil)
124
+ unless @steroids_force
125
+ raise RuntimeError.new(
126
+ message: message_or_nil || message,
127
+ errors: errors,
128
+ log: true
129
+ )
130
+ end
131
+ end
132
+
133
+ class << self
134
+ def async?
135
+ self.private_instance_methods.include?(:async_process) || self.instance_methods.include?(:async_process)
136
+ end
137
+
138
+ def call(*args, **options, &block)
139
+ new(*args, **options).call(&block)
140
+ end
141
+
142
+ def new(*arguments, **options)
143
+ validate_process_definition!
144
+ instance = super
145
+ if self.async?
146
+ if arguments.empty? && options.serializable?
147
+ instance.instance_variable_set(:"@_steroids_serialized_init_options", options.deep_serialize)
148
+ else
149
+ raise AsyncProcessArgumentError.new("Async services require serializable options")
150
+ end
151
+ end
152
+ instance
153
+ end
154
+
155
+ def steroids_before_callbacks
156
+ @steroids_before_callbacks ||= []
157
+ end
158
+
159
+ def steroids_after_callbacks
160
+ @steroids_after_callbacks ||= []
161
+ end
162
+
163
+ def validate_process_definition!
164
+ if async? && (self.private_instance_methods.include?(:process) || self.instance_methods.include?(:process))
165
+ raise AmbiguousProcessMethodError.new("Can't define both `process` and `async_process`")
166
+ end
167
+ end
168
+
169
+ protected
170
+
171
+ def before_process(method)
172
+ steroids_before_callbacks << method
173
+ end
174
+
175
+ def after_process(method)
176
+ steroids_after_callbacks << method
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,17 @@
1
+ module Steroids
2
+ module Support
3
+ class MagicClass
4
+ include Steroids::Support::NoticableMethods
5
+
6
+ class << self
7
+ # TODO: Get rid of this.
8
+ # def inherited(subclass)
9
+ # instance_variables.each do |var|
10
+ # subclass_variable_value = instance_variable_get(var).dup
11
+ # subclass.instance_variable_set(var, subclass_variable_value)
12
+ # end
13
+ # end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,134 @@
1
+ module Steroids
2
+ module Support
3
+ module NoticableMethods
4
+ extend ActiveSupport::Concern
5
+ # TODO:
6
+ # do |service, error:,full_notice:| etc
7
+ # error_methods: full_notice success_notice, etc
8
+ # Service: auto log notice
9
+ # error_mothods -> noticable
10
+ # noticable.logger / noticable.log
11
+ # noticable.erros (i.e. delagate error to noticable Noticable.new(self))
12
+
13
+ # --------------------------------------------------------------------------------------------
14
+ # TODO
15
+ # To rename collection for notices -> notice -> single message
16
+ # Message alias notice
17
+ # notice can be either error (full message) or success_notice
18
+
19
+ # --------------------------------------------------------------------------------------------
20
+ # Noticable collection
21
+ # --------------------------------------------------------------------------------------------
22
+
23
+ class NoticableCollection
24
+ NOTICABLE_TYPES = %i[errors notices]
25
+
26
+ attr_reader :collection
27
+
28
+ delegate :any?, :map, :each, :to_a, to: :collection
29
+
30
+ def initialize(collection_type)
31
+ @collection_type = NOTICABLE_TYPES.cast(collection_type)
32
+ @collection = []
33
+ end
34
+
35
+ def add(message, exception = nil)
36
+ nil.tap do
37
+ @collection << {
38
+ message: message.typed!(String),
39
+ exception: exception
40
+ }
41
+ end
42
+ end
43
+
44
+ def merge(errors)
45
+ nil.tap do
46
+ errors.each do |error|
47
+ @collection << error
48
+ end
49
+ end
50
+ end
51
+
52
+ alias_method :<<, :add
53
+
54
+ def full_messages
55
+ if @collection.any?
56
+ @collection.map do |error|
57
+ error[:message]
58
+ end.join("\n").presence
59
+ end
60
+ end
61
+ end
62
+
63
+ # --------------------------------------------------------------------------------------------
64
+ # Noticable runtime class (attached to instance)
65
+ # --------------------------------------------------------------------------------------------
66
+
67
+ class NoticableRuntime
68
+ attr_reader :notices
69
+ attr_reader :errors
70
+
71
+ def initialize(concern = [], success_notice: nil)
72
+ @concern = concern
73
+ @success_notice = success_notice.presence || success_notice_placeholder
74
+ @errors = NoticableCollection.new(:errors)
75
+ @notices = NoticableCollection.new(:notices)
76
+ end
77
+
78
+ def full_messages
79
+ if self.errors?
80
+ @errors.full_messages
81
+ else
82
+ @notices.full_messages.presence || @success_notice
83
+ end
84
+ end
85
+
86
+ alias_method :notice, :full_messages
87
+ alias_method :message, :full_messages
88
+
89
+ def errors?
90
+ @errors.any?
91
+ end
92
+
93
+ def success?
94
+ !errors?
95
+ end
96
+
97
+ def merge(noticable)
98
+ @notices.merge(noticable.notices)
99
+ @errors.merge(noticable.errors)
100
+ end
101
+
102
+ private
103
+
104
+ def success_notice_placeholder
105
+ humanized_class_name = @concern.class.name.split("::").last.underscore.humanize
106
+ "#{humanized_class_name} succeeded"
107
+ end
108
+ end
109
+
110
+ # --------------------------------------------------------------------------------------------
111
+ # Instance methods
112
+ # --------------------------------------------------------------------------------------------
113
+
114
+ included do
115
+ def noticable
116
+ @steroids_noticable_runtime ||= NoticableRuntime.new(
117
+ self,
118
+ success_notice: self.class.steroids_noticable_notice
119
+ )
120
+ end
121
+
122
+ delegate :notice, :errors, :notices, :success?, :errors?, to: :noticable
123
+ end
124
+
125
+ class_methods do
126
+ attr_reader :steroids_noticable_notice
127
+
128
+ def success_notice(message)
129
+ @steroids_noticable_notice ||= message
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,34 @@
1
+ module Steroids
2
+ module Support
3
+ module ServicableMethods
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ def noticable_binding
8
+ Proc.new do |concern|
9
+ if self.respond_to?(:noticable) && concern.respond_to?(:noticable)
10
+ self.noticable.merge(concern.noticable)
11
+ end
12
+ end
13
+ end
14
+
15
+ def service_context_for(options)
16
+ respond_to?(:context) ? context.merge(options).symbolize_keys : options
17
+ end
18
+ end
19
+
20
+ class_methods do
21
+ def service(service_name, class_name:, **class_options)
22
+ define_method service_name do | *args, **options, &block |
23
+ service_options = service_context_for({ **class_options, **options })
24
+ service_block = block.present? ? block : noticable_binding
25
+ Object.const_get(class_name).new(
26
+ *args,
27
+ **service_options
28
+ ).call(**options, &service_block)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -1,6 +1,6 @@
1
1
  module Steroids
2
- module Base
3
- class Type < Steroids::Base::Model
2
+ module Types
3
+ class Base < Steroids::Types::SerializableType
4
4
  def import(_options, _object = {})
5
5
  raise Steroids::Errors::InternalServerError.new(
6
6
  message: name + ': Import not implemented'
@@ -65,7 +65,7 @@ module Steroids
65
65
  instance
66
66
  rescue Exception => e
67
67
  raise Steroids::Errors::InternalServerError.new(
68
- exception: e,
68
+ cause: e,
69
69
  message: 'Import failed'
70
70
  )
71
71
  end
@@ -1,6 +1,6 @@
1
1
  module Steroids
2
- module Base
3
- class Model < Steroids::Base::Class
2
+ module Types
3
+ class SerializableType < Steroids::Support::MagicClass
4
4
  include ActiveModel::Model
5
5
  include ActiveModel::Serialization
6
6
 
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ module Steroids
3
+ VERSION = "1.6.0".freeze
4
+ end
data/lib/steroids.rb ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+ require "steroids/railtie"
3
+ require "steroids/engine"
4
+
5
+ module Steroids
6
+ def self.loader
7
+ @loader ||= Loader.new
8
+ end
9
+
10
+ loader.zeitwerk.setup
11
+ loader.load_extensions!
12
+ end
metadata CHANGED
@@ -1,46 +1,87 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: steroids
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paul Reboh
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-10-20 00:00:00.000000000 Z
12
- dependencies: []
13
- description: Steroids provides helper for Service-oriented, API based, Rails apps.
14
- email: dev@bernstein.io
11
+ date: 2025-09-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rainbow
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '3.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '3.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rails
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '7'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '7'
41
+ description: Steroids - Rails helpers
42
+ email: paul@reboh.net
15
43
  executables: []
16
44
  extensions: []
17
45
  extra_rdoc_files: []
18
46
  files:
19
- - lib/base/class.rb
20
- - lib/base/error.rb
21
- - lib/base/hash.rb
22
- - lib/base/list.rb
23
- - lib/base/model.rb
24
- - lib/base/service.rb
25
- - lib/base/type.rb
26
- - lib/concern.rb
27
- - lib/concerns/controller.rb
28
- - lib/concerns/error.rb
29
- - lib/concerns/model.rb
30
- - lib/concerns/serializer.rb
31
- - lib/errors/bad_request_error.rb
32
- - lib/errors/conflict_error.rb
33
- - lib/errors/forbidden_error.rb
34
- - lib/errors/generic_error.rb
35
- - lib/errors/internal_server_error.rb
36
- - lib/errors/not_found_error.rb
37
- - lib/errors/not_implemented_error.rb
38
- - lib/errors/unauthorized_error.rb
39
- - lib/errors/unprocessable_entity_error.rb
40
- homepage: https://github.com/bernstein-io/steroids
41
- licenses: []
42
- metadata: {}
43
- post_install_message:
47
+ - README.md
48
+ - Rakefile
49
+ - app/jobs/steroids/async_service_job.rb
50
+ - app/serializers/steroids/error_serializer.rb
51
+ - lib/resources/quotes.yml
52
+ - lib/steroids.rb
53
+ - lib/steroids/controllers/methods.rb
54
+ - lib/steroids/controllers/responders_helper.rb
55
+ - lib/steroids/controllers/serializers_helper.rb
56
+ - lib/steroids/engine.rb
57
+ - lib/steroids/errors.rb
58
+ - lib/steroids/errors/base.rb
59
+ - lib/steroids/errors/context.rb
60
+ - lib/steroids/errors/quotes.rb
61
+ - lib/steroids/extensions/array_extension.rb
62
+ - lib/steroids/extensions/class_extension.rb
63
+ - lib/steroids/extensions/hash_extension.rb
64
+ - lib/steroids/extensions/method_extension.rb
65
+ - lib/steroids/extensions/module_extension.rb
66
+ - lib/steroids/extensions/object_extension.rb
67
+ - lib/steroids/extensions/proc_extension.rb
68
+ - lib/steroids/logger.rb
69
+ - lib/steroids/railtie.rb
70
+ - lib/steroids/serializers/base.rb
71
+ - lib/steroids/serializers/methods.rb
72
+ - lib/steroids/services/base.rb
73
+ - lib/steroids/support/magic_class.rb
74
+ - lib/steroids/support/noticable_methods.rb
75
+ - lib/steroids/support/servicable_methods.rb
76
+ - lib/steroids/types/base.rb
77
+ - lib/steroids/types/serializable_type.rb
78
+ - lib/steroids/version.rb
79
+ homepage: https://github.com/somelibs/steroids
80
+ licenses:
81
+ - MIT
82
+ metadata:
83
+ allowed_push_host: https://rubygems.org
84
+ post_install_message:
44
85
  rdoc_options: []
45
86
  require_paths:
46
87
  - lib
@@ -48,15 +89,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
48
89
  requirements:
49
90
  - - ">="
50
91
  - !ruby/object:Gem::Version
51
- version: '0'
92
+ version: 3.0.0
52
93
  required_rubygems_version: !ruby/object:Gem::Requirement
53
94
  requirements:
54
95
  - - ">="
55
96
  - !ruby/object:Gem::Version
56
97
  version: '0'
57
98
  requirements: []
58
- rubygems_version: 3.0.6
59
- signing_key:
99
+ rubygems_version: 3.3.26
100
+ signing_key:
60
101
  specification_version: 4
61
102
  summary: Steroids - Rails helpers
62
103
  test_files: []
data/lib/base/class.rb DELETED
@@ -1,15 +0,0 @@
1
- module Steroids
2
- module Base
3
- class Class
4
- include Steroids::Concerns::Error
5
- class << self
6
- def inherited(subclass)
7
- instance_variables.each do |var|
8
- subclass_variable_value = instance_variable_get(var).dup
9
- subclass.instance_variable_set(var, subclass_variable_value)
10
- end
11
- end
12
- end
13
- end
14
- end
15
- end
data/lib/base/error.rb DELETED
@@ -1,87 +0,0 @@
1
- module Steroids
2
- module Base
3
- class Error < StandardError
4
- include ActiveModel::Serialization
5
-
6
- @@DEFAULT_MESSAGE = 'An error has occurred while processing your request [Steroids::Base::Error]'
7
-
8
- attr_reader :id
9
- attr_reader :code
10
- attr_reader :status
11
- attr_reader :message
12
- attr_reader :reference
13
- attr_reader :data
14
- attr_reader :errors
15
- attr_reader :key
16
- attr_reader :proverb
17
- attr_reader :klass
18
-
19
- def initialize(status: false, message:, key: nil, errors: nil, data: nil, reference: nil, exception: nil)
20
- @id = SecureRandom.uuid
21
- @key = assert_key(key)
22
- @data = assert_data(data)
23
- @code = assert_code(status)
24
- @status = assert_status(status)
25
- @reference = assert_reference(reference)
26
- @message = assert_message(exception, message)
27
- @errors = assert_errors(exception, errors)
28
- @klass = assert_class(exception)
29
- @proverb = proverb
30
- super(@message)
31
- end
32
-
33
- protected
34
-
35
- def proverb
36
- begin
37
- proverbs = Rails.cache.fetch('core/error_proverbs', expires_in: 1.hour) do
38
- YAML.load_file(Rails.root.join('config/proverbs.yml'))
39
- end
40
- rescue StandardError => e
41
- Utilities::Logger.push(e)
42
- proverbs = ['One little bug...']
43
- end
44
- proverbs.sample
45
- end
46
-
47
- private
48
-
49
- def assert_class(exception)
50
- exception&.class
51
- end
52
-
53
- def assert_status(status)
54
- status ? Rack::Utils.status_code(status) : false
55
- end
56
-
57
- def assert_code(status)
58
- status ? status.to_s : 'error'
59
- end
60
-
61
- def assert_data(data)
62
- data
63
- end
64
-
65
- def assert_reference(reference)
66
- reference ? reference.to_s : 'http_error'
67
- end
68
-
69
- def assert_key(key)
70
- Array(key)
71
- end
72
-
73
- def assert_errors(exception, errors = [])
74
- exception_errors = Array(reflect_on_exception(exception, :message))
75
- (Array(errors) + exception_errors).compact
76
- end
77
-
78
- def assert_message(exception, message)
79
- message && message.to_s || @@DEFAULT_MESSAGE
80
- end
81
-
82
- def reflect_on_exception(exception, attribute)
83
- exception&.respond_to?(attribute) ? exception.message : nil
84
- end
85
- end
86
- end
87
- end