servitium 1.2.20

Sign up to get free protection for your applications and to get access to all the features.
@@ -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