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