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.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.travis.yml +6 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +59 -0
- data/Rakefile +18 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/lib/generators/servitium/USAGE +9 -0
- data/lib/generators/servitium/service_generator.rb +17 -0
- data/lib/generators/servitium/templates/context.rb +13 -0
- data/lib/generators/servitium/templates/service.rb +13 -0
- data/lib/generators/servitium/templates/service_test.rb +11 -0
- data/lib/servitium/capture_exceptions_mixin.rb +26 -0
- data/lib/servitium/context.rb +65 -0
- data/lib/servitium/context_failure.rb +12 -0
- data/lib/servitium/context_model.rb +93 -0
- data/lib/servitium/error.rb +6 -0
- data/lib/servitium/i18n.rb +21 -0
- data/lib/servitium/rails/railtie.rb +11 -0
- data/lib/servitium/rails.rb +4 -0
- data/lib/servitium/scoped_attributes.rb +32 -0
- data/lib/servitium/service.rb +270 -0
- data/lib/servitium/service_job.rb +21 -0
- data/lib/servitium/sub_contexts.rb +111 -0
- data/lib/servitium/transactional_mixin.rb +26 -0
- data/lib/servitium/version.rb +5 -0
- data/lib/servitium.rb +20 -0
- data/lib/tasks/servitium_tasks.rake +18 -0
- data/servitium.gemspec +45 -0
- metadata +286 -0
@@ -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
|
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
|