servitium 1.2.20

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.
@@ -0,0 +1,270 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'transactional_mixin'
4
+ require_relative 'capture_exceptions_mixin'
5
+
6
+ module Servitium
7
+ class Service
8
+ include ActiveSupport::Callbacks
9
+ include TransactionalMixin
10
+ include CaptureExceptionsMixin
11
+ include I18n
12
+
13
+ attr_reader :context, :raise_on_error
14
+
15
+ alias ctx context
16
+
17
+ define_callbacks :commit
18
+ define_callbacks :perform
19
+ define_callbacks :failure
20
+ define_callbacks :async_success
21
+ define_callbacks :async_failure
22
+ private_class_method :new
23
+
24
+ delegate :transactional, to: :class
25
+ delegate :capture_exceptions, to: :class
26
+ delegate :model_name, to: :context
27
+
28
+ def initialize(*args)
29
+ @raise_on_error = false
30
+ @command = args.first.is_a?(Symbol) ? args.shift : :perform
31
+ @context = context_class.new(*args)
32
+ super()
33
+ end
34
+
35
+ private
36
+
37
+ def call
38
+ if transactional && defined?(ActiveRecord::Base)
39
+ ActiveRecord::Base.transaction(requires_new: true) do
40
+ exec
41
+ # This will only rollback the changes of the service, SILENTLY, however the context will be failed? already.
42
+ # This is the most close to expected behaviour this can get.
43
+ raise ActiveRecord::Rollback if context.failed?
44
+ end
45
+
46
+ context
47
+ else
48
+ exec
49
+ end
50
+ end
51
+
52
+ def call!
53
+ @raise_on_error = true
54
+ call
55
+ end
56
+
57
+ def context_class
58
+ self.class.context_class || Servitium::Context
59
+ end
60
+
61
+ def exec
62
+ run_callbacks :perform do
63
+ send(@command)
64
+ rescue Servitium::ContextFailure => e
65
+ raise_if_needed(e)
66
+ rescue StandardError => e
67
+ # If capture exceptions is true, eat the exception and set the context errors.
68
+ raise unless capture_exceptions
69
+
70
+ begin
71
+ context.fail!(:base, e.message)
72
+ rescue Servitium::ContextFailure => e
73
+ # Eat this as well, we don't want to raise here, capture exceptions is true.
74
+ end
75
+ end
76
+ raise_if_needed
77
+ context
78
+ end
79
+
80
+ def raise_on_error?
81
+ @raise_on_error
82
+ end
83
+
84
+ def raise_if_needed(e = nil)
85
+ raise e if e.present? && e.context.object_id != context.object_id
86
+ return unless raise_on_error?
87
+
88
+ if e
89
+ raise e
90
+ elsif context.errors.present?
91
+ errors = context.errors.full_messages.join(', ')
92
+ log :error, "raising: #{errors}"
93
+ raise StandardError, errors
94
+ end
95
+ end
96
+
97
+ def log(level, message)
98
+ return unless defined? Rails.logger
99
+
100
+ Rails.logger.send level, "#{self.class.name}: #{message}"
101
+ end
102
+
103
+ def after_commit
104
+ run_callbacks :commit do
105
+ end
106
+ end
107
+
108
+ def failure
109
+ run_callbacks :failure do
110
+ end
111
+ end
112
+
113
+ def async_success
114
+ run_callbacks :async_success do
115
+ end
116
+ end
117
+
118
+ def async_failure
119
+ run_callbacks :async_failure do
120
+ end
121
+ end
122
+
123
+ class << self
124
+ # Main point of entry for services, will raise in case of errors
125
+ def perform!(*args)
126
+ inst = new(*args)
127
+
128
+ begin
129
+ inst.context.validate!(:in) if inst.context.class.inbound_scope_used
130
+ inst.context.validate!
131
+ inst.context.instance_variable_set(:@called, true)
132
+ inst.send(:call!)
133
+ inst.context.validate!(:out) if inst.context.errors.blank? && inst.context.class.inbound_scope_used
134
+ inst.send(:after_commit) unless inst.context.failed?
135
+ ensure
136
+ inst.send(:failure) if inst.context.failed?
137
+ end
138
+
139
+ inst.context
140
+ end
141
+
142
+ # Main point of entry for services
143
+ def perform(*args)
144
+ call(*args).context
145
+ end
146
+
147
+ # Call the service returning the service instance
148
+ def call(*args)
149
+ inst = new(*args)
150
+
151
+ valid_in = inst.context.valid?
152
+ valid_in &&= inst.context.valid?(:in) if inst.context.class.inbound_scope_used
153
+
154
+ if valid_in
155
+ inst.context.instance_variable_set(:@called, true)
156
+ inst.send(:call)
157
+ inst.context.valid?(:out) if inst.context.errors.blank? && inst.context.class.inbound_scope_used
158
+ inst.send(:after_commit) unless inst.context.failed?
159
+ end
160
+
161
+ inst.send(:failure) if inst.context.failed?
162
+
163
+ inst
164
+ end
165
+
166
+ # Perform this service async
167
+ def perform_later(*args)
168
+ inst = new(*args)
169
+
170
+ valid_in = inst.context.valid?
171
+ valid_in &&= inst.context.valid?(:in) if inst.context.class.inbound_scope_used
172
+
173
+ if valid_in
174
+ inst.context.instance_variable_set(:@called, true)
175
+ Servitium::ServiceJob.perform_later(name, inst.context.attributes_hash)
176
+ end
177
+
178
+ inst.context
179
+ end
180
+
181
+ def queue_name
182
+ 'default'
183
+ end
184
+
185
+ # Callbacks
186
+ def after_commit(*filters, &block)
187
+ set_callback(:commit, :after, *filters, &block)
188
+ end
189
+
190
+ def before_perform(*filters, &block)
191
+ set_callback(:perform, :before, *filters, &block)
192
+ end
193
+
194
+ def around_perform(*filters, &block)
195
+ set_callback(:perform, :around, *filters, &block)
196
+ end
197
+
198
+ def after_perform(*filters, &block)
199
+ set_callback(:perform, :after, *filters, &block)
200
+ end
201
+
202
+ def after_failure(*filters, &block)
203
+ set_callback(:failure, :after, *filters, &block)
204
+ end
205
+
206
+ def around_async_success(*filters, &block)
207
+ set_callback(:async_success, :around, *filters, &block)
208
+ end
209
+
210
+ def around_async_failure(*filters, &block)
211
+ set_callback(:async_failure, :around, *filters, &block)
212
+ end
213
+
214
+ def after_async_success(*filters, &block)
215
+ set_callback(:async_success, :after, *filters, &block)
216
+ end
217
+
218
+ def after_async_failure(*filters, &block)
219
+ set_callback(:async_failure, :after, *filters, &block)
220
+ end
221
+
222
+ def context_class
223
+ context_class_name.safe_constantize
224
+ end
225
+
226
+ def context_class_name
227
+ name.gsub('Service', 'Context')
228
+ end
229
+
230
+ def context_class!
231
+ return context_class if context_class
232
+
233
+ context_class_parts = context_class_name.split('::')
234
+ context_class_name_part = context_class_parts.pop
235
+ context_module_name = context_class_parts.join('::')
236
+ context_module = context_module_name.present? ? context_module_name.constantize : Object
237
+
238
+ context_module.const_set(context_class_name_part, Class.new(context_base_class_name.constantize))
239
+ context_class
240
+ end
241
+
242
+ # Get the base class for new contexts defined using context blocks
243
+ # Defaults to Servitium::Context
244
+ def context_base_class_name
245
+ @@_context_base_class_name ||= 'Servitium::Context'
246
+ end
247
+
248
+ # Override the base class for contexts defined using context blocks, you can use this to
249
+ # change the base class to your own ApplicationContext
250
+ def context_base_class_name=(base_class)
251
+ @@_context_base_class_name = base_class
252
+ end
253
+
254
+ def context(*args, &block)
255
+ return initialized_context(*args) unless block_given?
256
+
257
+ begin
258
+ context_class!.new
259
+ rescue StandardError
260
+ nil
261
+ end
262
+ context_class!.class_eval(&block)
263
+ end
264
+
265
+ def initialized_context(*args)
266
+ context_class.new(*args)
267
+ end
268
+ end
269
+ end
270
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servitium
4
+ class ServiceJob < ActiveJob::Base
5
+ queue_as { queue_name }
6
+
7
+ def perform(class_name, *args)
8
+ service = class_name.constantize.call(*args)
9
+
10
+ if service.context.success?
11
+ service.send(:async_success)
12
+ else
13
+ service.send(:async_failure)
14
+ end
15
+ end
16
+
17
+ def queue_name
18
+ arguments.first.constantize.queue_name
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servitium
4
+ module SubContexts
5
+ def sub_context(name)
6
+ if name.to_s.singularize == name.to_s
7
+ define_method("#{name}_attributes=") do |attributes|
8
+ klass = "#{self.class.name}::#{name.to_s.camelize}".safe_constantize
9
+
10
+ writer = "#{name}="
11
+ inst = klass.new(attributes)
12
+ inst.supercontext = self
13
+
14
+ send writer, inst if respond_to? writer
15
+
16
+ @subcontexts ||= {}
17
+ @subcontexts[name] = inst
18
+
19
+ inst
20
+ end
21
+
22
+ define_method("#{name}=") do |attributes|
23
+ klass = "#{self.class.name}::#{name.to_s.camelize}".safe_constantize
24
+ inst = if attributes.is_a? klass
25
+ attributes
26
+ else
27
+ inst = klass.new(attributes)
28
+ inst.supercontext = self
29
+ inst
30
+ end
31
+
32
+ instance_variable_set("@#{name}".to_sym, inst)
33
+
34
+ @subcontexts ||= {}
35
+ @subcontexts[name] = inst
36
+
37
+ inst
38
+ end
39
+ else
40
+ define_method("#{name}_attributes=") do |attributes|
41
+ klass = "#{self.class.name}::#{name.to_s.singularize.camelize}".safe_constantize
42
+
43
+ if attributes.is_a?(Hash) || attributes.is_a?(ActionController::Parameters)
44
+ keys = attributes.keys
45
+ attributes = (if keys.reject { |k| k == 'TEMPLATE' }.all? { |k| k.to_i.to_s == k }
46
+ attributes.reject do |k|
47
+ k == 'TEMPLATE'
48
+ end.values
49
+ end)
50
+ end
51
+
52
+ result = []
53
+ attributes.each do |params|
54
+ next if params['_destroy'] == '1' || params[:_destroy] == '1'
55
+
56
+ inst = klass.new(params)
57
+ inst.supercontext = self
58
+ result.push(inst)
59
+ end
60
+ writer = "#{name}="
61
+ send writer, result if respond_to? writer
62
+
63
+ @subcontexts ||= {}
64
+ @subcontexts[name] = result
65
+
66
+ result
67
+ end
68
+
69
+ define_method("#{name}=") do |attributes|
70
+ klass = "#{self.class.name}::#{name.to_s.singularize.camelize}".safe_constantize
71
+ result = if attributes.is_a?(Array) && attributes.all? { |a| a.instance_of?(klass) }
72
+ attributes
73
+ else
74
+ if attributes.is_a?(Hash) || attributes.is_a?(ActionController::Parameters)
75
+ attributes = (attributes.values if attributes.keys.all? { |k| k.to_i.to_s == k })
76
+ end
77
+
78
+ result = []
79
+ attributes.each do |params|
80
+ inst = klass.new(params)
81
+ inst.supercontext = self
82
+ result.push(inst)
83
+ end
84
+ result
85
+ end
86
+
87
+ instance_variable_set("@#{name}".to_sym, result)
88
+
89
+ @subcontexts ||= {}
90
+ @subcontexts[name] = result
91
+
92
+ result
93
+ end
94
+ end
95
+
96
+ if name.to_s.singularize == name.to_s
97
+ define_method(name.to_s) do
98
+ instance_variable_get("@#{name}".to_sym) if instance_variable_defined?("@#{name}".to_sym)
99
+ end
100
+ else
101
+ define_method(name.to_s) do
102
+ value = instance_variable_get("@#{name}".to_sym) if instance_variable_defined?("@#{name}".to_sym)
103
+ value ||= []
104
+ value
105
+ end
106
+ end
107
+
108
+ @attributes[name.to_s] = ActiveAttr::AttributeDefinition.new(name, {})
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servitium
4
+ module TransactionalMixin
5
+ class << self
6
+ def included(base)
7
+ base.extend ClassMethods
8
+ end
9
+ end
10
+
11
+ module ClassMethods
12
+ def transactional(value = nil)
13
+ @transactional = value if value
14
+ @transactional = nil unless defined?(@transactional)
15
+ if @transactional.nil?
16
+ @transactional = if superclass < Servitium::Service
17
+ superclass.transactional
18
+ else
19
+ false
20
+ end
21
+ end
22
+ @transactional
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servitium
4
+ VERSION = "1.2.20"
5
+ end
data/lib/servitium.rb ADDED
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_model'
4
+ require 'active_attr'
5
+ require 'active_support'
6
+ require 'action_controller'
7
+ require 'active_job'
8
+
9
+ require 'servitium/error'
10
+ require 'servitium/context_failure'
11
+ require 'servitium/i18n'
12
+ require 'servitium/sub_contexts'
13
+ require 'servitium/scoped_attributes'
14
+ require 'servitium/context_model'
15
+ require 'servitium/context'
16
+ require 'servitium/service_job'
17
+ require 'servitium/service'
18
+ require 'servitium/version'
19
+
20
+ require 'servitium/rails' if defined?(::Rails)
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require 'active_support/inflector'
5
+
6
+ namespace :servitium do
7
+ desc 'Convert localization keys'
8
+ task :convert_keys do
9
+ locs = YAML.load(File.read('./config/locales/en.yml'))
10
+ locs['en']['services'].each_key do |service|
11
+ locs['en'][service[0..-9].pluralize] = { 'service' => locs['en']['services'][service].dup }
12
+ end
13
+
14
+ File.open('./config/locales/en.yml', 'w') do |f|
15
+ f.write YAML.dump(locs)
16
+ end
17
+ end
18
+ end
data/servitium.gemspec ADDED
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/servitium/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'servitium'
7
+ spec.version = Servitium::VERSION
8
+ spec.authors = ['Tom de Grunt']
9
+ spec.email = ['tom@degrunt.nl']
10
+
11
+ spec.summary = 'Service objects'
12
+ spec.description = 'An implementation of the command pattern for Ruby'
13
+ spec.homepage = 'https://entropydecelerator.com/components/servitium'
14
+ spec.license = 'MIT'
15
+ spec.required_ruby_version = Gem::Requirement.new('>= 2.6.5')
16
+
17
+ spec.metadata['homepage_uri'] = spec.homepage
18
+ spec.metadata['source_code_uri'] = 'https://code.entropydecelerator.com/components/servitium'
19
+
20
+ # Specify which files should be added to the gem when it is released.
21
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
22
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
23
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
24
+ end
25
+ spec.bindir = 'exe'
26
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
27
+ spec.require_paths = ['lib']
28
+
29
+ spec.add_dependency 'active_attr', '>= 0.15'
30
+ spec.add_dependency 'activejob', '> 5.1'
31
+ spec.add_dependency 'activemodel', '> 5.1'
32
+ spec.add_dependency 'activerecord', '> 5.1'
33
+ spec.add_dependency 'activesupport', '> 5.1'
34
+ spec.add_dependency 'actionpack', '> 5.1'
35
+ spec.add_dependency 'i18n', '>= 0.7'
36
+
37
+ spec.add_development_dependency 'auxilium', '>= 3'
38
+ spec.add_development_dependency 'minitest', '~> 5.11'
39
+ spec.add_development_dependency 'minitest-reporters', '~> 1.1'
40
+ spec.add_development_dependency 'pry'
41
+ spec.add_development_dependency 'pry-rails', '~> 0.3'
42
+ spec.add_development_dependency 'rake', '~> 12.0'
43
+ spec.add_development_dependency 'rubocop', '~> 0.79'
44
+ spec.add_development_dependency 'sqlite3', '~> 1.4'
45
+ end