light-services 2.2.1 → 3.0.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.
- checksums.yaml +4 -4
- data/.github/config/rubocop_linter_action.yml +4 -4
- data/.github/workflows/ci.yml +12 -12
- data/.gitignore +1 -0
- data/.rubocop.yml +77 -7
- data/CHANGELOG.md +23 -0
- data/CLAUDE.md +139 -0
- data/Gemfile +16 -11
- data/Gemfile.lock +53 -27
- data/README.md +76 -13
- data/docs/arguments.md +267 -0
- data/docs/best-practices.md +153 -0
- data/docs/callbacks.md +476 -0
- data/docs/concepts.md +80 -0
- data/docs/configuration.md +168 -0
- data/docs/context.md +128 -0
- data/docs/crud.md +525 -0
- data/docs/errors.md +250 -0
- data/docs/generators.md +250 -0
- data/docs/outputs.md +135 -0
- data/docs/pundit-authorization.md +320 -0
- data/docs/quickstart.md +134 -0
- data/docs/readme.md +100 -0
- data/docs/recipes.md +14 -0
- data/docs/service-rendering.md +222 -0
- data/docs/steps.md +337 -0
- data/docs/summary.md +19 -0
- data/docs/testing.md +549 -0
- data/lib/generators/light_services/install/USAGE +15 -0
- data/lib/generators/light_services/install/install_generator.rb +41 -0
- data/lib/generators/light_services/install/templates/application_service.rb.tt +8 -0
- data/lib/generators/light_services/install/templates/application_service_spec.rb.tt +7 -0
- data/lib/generators/light_services/install/templates/initializer.rb.tt +30 -0
- data/lib/generators/light_services/service/USAGE +21 -0
- data/lib/generators/light_services/service/service_generator.rb +68 -0
- data/lib/generators/light_services/service/templates/service.rb.tt +48 -0
- data/lib/generators/light_services/service/templates/service_spec.rb.tt +40 -0
- data/lib/light/services/base.rb +23 -113
- data/lib/light/services/callbacks.rb +103 -0
- data/lib/light/services/collection.rb +97 -0
- data/lib/light/services/concerns/execution.rb +76 -0
- data/lib/light/services/concerns/parent_service.rb +34 -0
- data/lib/light/services/concerns/state_management.rb +30 -0
- data/lib/light/services/config.rb +4 -18
- data/lib/light/services/constants.rb +97 -0
- data/lib/light/services/dsl/arguments_dsl.rb +84 -0
- data/lib/light/services/dsl/outputs_dsl.rb +80 -0
- data/lib/light/services/dsl/steps_dsl.rb +205 -0
- data/lib/light/services/dsl/validation.rb +132 -0
- data/lib/light/services/exceptions.rb +7 -2
- data/lib/light/services/messages.rb +19 -31
- data/lib/light/services/rspec/matchers/define_argument.rb +174 -0
- data/lib/light/services/rspec/matchers/define_output.rb +147 -0
- data/lib/light/services/rspec/matchers/define_step.rb +225 -0
- data/lib/light/services/rspec/matchers/execute_step.rb +230 -0
- data/lib/light/services/rspec/matchers/have_error_on.rb +148 -0
- data/lib/light/services/rspec/matchers/have_warning_on.rb +148 -0
- data/lib/light/services/rspec/matchers/trigger_callback.rb +138 -0
- data/lib/light/services/rspec.rb +15 -0
- data/lib/light/services/settings/field.rb +86 -0
- data/lib/light/services/settings/step.rb +31 -16
- data/lib/light/services/utils.rb +38 -0
- data/lib/light/services/version.rb +1 -1
- data/lib/light/services.rb +2 -0
- data/light-services.gemspec +6 -8
- metadata +54 -26
- data/lib/light/services/class_based_collection/base.rb +0 -86
- data/lib/light/services/class_based_collection/mount.rb +0 -33
- data/lib/light/services/collection/arguments.rb +0 -34
- data/lib/light/services/collection/base.rb +0 -59
- data/lib/light/services/collection/outputs.rb +0 -16
- data/lib/light/services/settings/argument.rb +0 -68
- data/lib/light/services/settings/output.rb +0 -34
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/base"
|
|
4
|
+
|
|
5
|
+
module LightServices
|
|
6
|
+
module Generators
|
|
7
|
+
class ServiceGenerator < ::Rails::Generators::Base
|
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
|
9
|
+
|
|
10
|
+
argument :name, type: :string, required: true,
|
|
11
|
+
desc: "The name of the service (e.g., user/create or CreateUser)"
|
|
12
|
+
|
|
13
|
+
class_option :args, type: :array, default: [],
|
|
14
|
+
desc: "List of arguments for the service"
|
|
15
|
+
class_option :steps, type: :array, default: [],
|
|
16
|
+
desc: "List of steps for the service"
|
|
17
|
+
class_option :outputs, type: :array, default: [],
|
|
18
|
+
desc: "List of outputs for the service"
|
|
19
|
+
class_option :skip_spec, type: :boolean, default: false,
|
|
20
|
+
desc: "Skip creating the spec file"
|
|
21
|
+
class_option :parent, type: :string, default: "ApplicationService",
|
|
22
|
+
desc: "Parent class for the service"
|
|
23
|
+
|
|
24
|
+
desc "Creates a new service class"
|
|
25
|
+
|
|
26
|
+
def create_service_file
|
|
27
|
+
template "service.rb.tt", "app/services/#{file_path}.rb"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def create_spec_file
|
|
31
|
+
return if options[:skip_spec]
|
|
32
|
+
return unless rspec_installed?
|
|
33
|
+
|
|
34
|
+
template "service_spec.rb.tt", "spec/services/#{file_path}_spec.rb"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def file_path
|
|
40
|
+
name.underscore
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def class_name
|
|
44
|
+
name.camelize
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def parent_class
|
|
48
|
+
options[:parent]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def arguments
|
|
52
|
+
options[:args]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def steps
|
|
56
|
+
options[:steps]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def outputs
|
|
60
|
+
options[:outputs]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def rspec_installed?
|
|
64
|
+
File.directory?(File.join(destination_root, "spec"))
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class <%= class_name %> < <%= parent_class %>
|
|
4
|
+
<% if arguments.any? -%>
|
|
5
|
+
# Arguments
|
|
6
|
+
<% arguments.each do |arg| -%>
|
|
7
|
+
arg :<%= arg %>
|
|
8
|
+
<% end -%>
|
|
9
|
+
|
|
10
|
+
<% end -%>
|
|
11
|
+
<% if steps.any? -%>
|
|
12
|
+
# Steps
|
|
13
|
+
<% steps.each do |step| -%>
|
|
14
|
+
step :<%= step %>
|
|
15
|
+
<% end -%>
|
|
16
|
+
|
|
17
|
+
<% end -%>
|
|
18
|
+
<% if outputs.any? -%>
|
|
19
|
+
# Outputs
|
|
20
|
+
<% outputs.each do |output| -%>
|
|
21
|
+
output :<%= output %>
|
|
22
|
+
<% end -%>
|
|
23
|
+
|
|
24
|
+
<% end -%>
|
|
25
|
+
<% if steps.empty? -%>
|
|
26
|
+
# step :step_a
|
|
27
|
+
# step :step_b
|
|
28
|
+
|
|
29
|
+
<% end -%>
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
<% if steps.any? -%>
|
|
33
|
+
<% steps.each_with_index do |step, index| -%>
|
|
34
|
+
def <%= step %>
|
|
35
|
+
# TODO: Implement <%= step %>
|
|
36
|
+
end
|
|
37
|
+
<%= "\n" unless index == steps.length - 1 -%>
|
|
38
|
+
<% end -%>
|
|
39
|
+
<% else -%>
|
|
40
|
+
# def step_a
|
|
41
|
+
# # TODO: Implement service logic
|
|
42
|
+
# end
|
|
43
|
+
|
|
44
|
+
# def step_b
|
|
45
|
+
# # TODO: Implement service logic
|
|
46
|
+
# end
|
|
47
|
+
<% end -%>
|
|
48
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails_helper"
|
|
4
|
+
|
|
5
|
+
# TODO: Add test implementation
|
|
6
|
+
RSpec.describe <%= class_name %>, type: :service do
|
|
7
|
+
<% if arguments.any? -%>
|
|
8
|
+
describe "arguments" do
|
|
9
|
+
<% arguments.each do |arg| -%>
|
|
10
|
+
it { is_expected.to define_argument(:<%= arg %>) }
|
|
11
|
+
<% end -%>
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
<% end -%>
|
|
15
|
+
<% if steps.any? -%>
|
|
16
|
+
describe "steps" do
|
|
17
|
+
<% steps.each do |step| -%>
|
|
18
|
+
it { is_expected.to define_step(:<%= step %>) }
|
|
19
|
+
<% end -%>
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
<% end -%>
|
|
23
|
+
<% if outputs.any? -%>
|
|
24
|
+
describe "outputs" do
|
|
25
|
+
<% outputs.each do |output| -%>
|
|
26
|
+
it { is_expected.to define_output(:<%= output %>) }
|
|
27
|
+
<% end -%>
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
<% end -%>
|
|
31
|
+
describe "#run" do
|
|
32
|
+
subject(:service) { described_class.run(args) }
|
|
33
|
+
|
|
34
|
+
let(:args) { {} }
|
|
35
|
+
|
|
36
|
+
it "succeeds" do
|
|
37
|
+
expect(service).to be_success
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
data/lib/light/services/base.rb
CHANGED
|
@@ -1,38 +1,34 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
4
|
-
|
|
3
|
+
require "light/services/constants"
|
|
5
4
|
require "light/services/message"
|
|
6
5
|
require "light/services/messages"
|
|
7
6
|
require "light/services/base_with_context"
|
|
8
7
|
|
|
9
8
|
require "light/services/settings/step"
|
|
10
|
-
require "light/services/settings/
|
|
11
|
-
|
|
9
|
+
require "light/services/settings/field"
|
|
10
|
+
|
|
11
|
+
require "light/services/collection"
|
|
12
12
|
|
|
13
|
-
require "light/services/
|
|
14
|
-
require "light/services/
|
|
15
|
-
require "light/services/
|
|
13
|
+
require "light/services/dsl/arguments_dsl"
|
|
14
|
+
require "light/services/dsl/outputs_dsl"
|
|
15
|
+
require "light/services/dsl/steps_dsl"
|
|
16
16
|
|
|
17
|
-
require "light/services/
|
|
18
|
-
require "light/services/
|
|
17
|
+
require "light/services/concerns/execution"
|
|
18
|
+
require "light/services/concerns/state_management"
|
|
19
|
+
require "light/services/concerns/parent_service"
|
|
19
20
|
|
|
20
21
|
# Base class for all service objects
|
|
21
22
|
module Light
|
|
22
23
|
module Services
|
|
23
24
|
class Base
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
# Arguments
|
|
33
|
-
arg :verbose, default: false
|
|
34
|
-
arg :benchmark, default: false
|
|
35
|
-
arg :deepness, default: 0, context: true
|
|
25
|
+
include Callbacks
|
|
26
|
+
include Dsl::ArgumentsDsl
|
|
27
|
+
include Dsl::OutputsDsl
|
|
28
|
+
include Dsl::StepsDsl
|
|
29
|
+
include Concerns::Execution
|
|
30
|
+
include Concerns::StateManagement
|
|
31
|
+
include Concerns::ParentService
|
|
36
32
|
|
|
37
33
|
# Getters
|
|
38
34
|
attr_reader :outputs, :arguments, :errors, :warnings
|
|
@@ -41,8 +37,8 @@ module Light
|
|
|
41
37
|
@config = Light::Services.config.merge(self.class.class_config || {}).merge(config)
|
|
42
38
|
@parent_service = parent_service
|
|
43
39
|
|
|
44
|
-
@outputs = Collection::
|
|
45
|
-
@arguments = Collection::
|
|
40
|
+
@outputs = Collection::Base.new(self, CollectionTypes::OUTPUTS)
|
|
41
|
+
@arguments = Collection::Base.new(self, CollectionTypes::ARGUMENTS, args.dup)
|
|
46
42
|
|
|
47
43
|
@done = false
|
|
48
44
|
@launched_steps = []
|
|
@@ -77,20 +73,14 @@ module Light
|
|
|
77
73
|
|
|
78
74
|
def call
|
|
79
75
|
load_defaults_and_validate
|
|
80
|
-
log_header if benchmark? || verbose?
|
|
81
76
|
|
|
82
|
-
|
|
83
|
-
run_steps
|
|
84
|
-
run_steps_with_always
|
|
77
|
+
run_callbacks(:before_service_run, self)
|
|
85
78
|
|
|
86
|
-
|
|
87
|
-
|
|
79
|
+
run_callbacks(:around_service_run, self) do
|
|
80
|
+
execute_service
|
|
88
81
|
end
|
|
89
82
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
log "🟢 Finished #{self.class} in #{time}ms"
|
|
93
|
-
puts
|
|
83
|
+
run_service_result_callbacks
|
|
94
84
|
rescue StandardError => e
|
|
95
85
|
run_steps_with_always
|
|
96
86
|
raise e
|
|
@@ -118,86 +108,6 @@ module Light
|
|
|
118
108
|
BaseWithContext.new(self, service, config.dup)
|
|
119
109
|
end
|
|
120
110
|
end
|
|
121
|
-
|
|
122
|
-
# TODO: Add possibility to specify logger
|
|
123
|
-
def log(message)
|
|
124
|
-
puts "#{' ' * deepness}→ #{message}"
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
private
|
|
128
|
-
|
|
129
|
-
def initialize_errors
|
|
130
|
-
@errors = Messages.new(
|
|
131
|
-
break_on_add: @config[:break_on_error],
|
|
132
|
-
raise_on_add: @config[:raise_on_error],
|
|
133
|
-
rollback_on_add: @config[:use_transactions] && @config[:rollback_on_error]
|
|
134
|
-
)
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
def initialize_warnings
|
|
138
|
-
@warnings = Messages.new(
|
|
139
|
-
break_on_add: @config[:break_on_warning],
|
|
140
|
-
raise_on_add: @config[:raise_on_warning],
|
|
141
|
-
rollback_on_add: @config[:use_transactions] && @config[:rollback_on_warning]
|
|
142
|
-
)
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
def run_steps
|
|
146
|
-
within_transaction do
|
|
147
|
-
self.class.steps.each do |name, step|
|
|
148
|
-
@launched_steps << name if step.run(self, benchmark: benchmark)
|
|
149
|
-
|
|
150
|
-
break if @errors.break? || @warnings.break?
|
|
151
|
-
end
|
|
152
|
-
end
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
# Run steps with parameter `always` if they weren't launched because of errors/warnings
|
|
156
|
-
def run_steps_with_always
|
|
157
|
-
self.class.steps.each do |name, step|
|
|
158
|
-
next if !step.always || @launched_steps.include?(name)
|
|
159
|
-
|
|
160
|
-
@launched_steps << name if step.run(self)
|
|
161
|
-
end
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
def copy_warnings_to_parent_service
|
|
165
|
-
return if !@parent_service || !@config[:load_warnings]
|
|
166
|
-
|
|
167
|
-
@parent_service.warnings.copy_from(
|
|
168
|
-
@warnings,
|
|
169
|
-
break: @config[:self_break_on_warning],
|
|
170
|
-
rollback: @config[:self_rollback_on_warning]
|
|
171
|
-
)
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
def copy_errors_to_parent_service
|
|
175
|
-
return if !@parent_service || !@config[:load_errors]
|
|
176
|
-
|
|
177
|
-
@parent_service.errors.copy_from(
|
|
178
|
-
@errors,
|
|
179
|
-
break: @config[:self_break_on_error],
|
|
180
|
-
rollback: @config[:self_rollback_on_error]
|
|
181
|
-
)
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
def load_defaults_and_validate
|
|
185
|
-
@outputs.load_defaults
|
|
186
|
-
@arguments.load_defaults
|
|
187
|
-
@arguments.validate!
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
def log_header
|
|
191
|
-
log "🏎 Run service #{self.class}"
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
def within_transaction(&block)
|
|
195
|
-
if @config[:use_transactions] && defined?(ActiveRecord::Base)
|
|
196
|
-
ActiveRecord::Base.transaction(requires_new: true, &block)
|
|
197
|
-
else
|
|
198
|
-
yield
|
|
199
|
-
end
|
|
200
|
-
end
|
|
201
111
|
end
|
|
202
112
|
end
|
|
203
113
|
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Light
|
|
4
|
+
module Services
|
|
5
|
+
module Callbacks
|
|
6
|
+
EVENTS = [
|
|
7
|
+
:before_step_run,
|
|
8
|
+
:after_step_run,
|
|
9
|
+
:around_step_run,
|
|
10
|
+
:on_step_success,
|
|
11
|
+
:on_step_failure,
|
|
12
|
+
:on_step_crash,
|
|
13
|
+
:before_service_run,
|
|
14
|
+
:after_service_run,
|
|
15
|
+
:around_service_run,
|
|
16
|
+
:on_service_success,
|
|
17
|
+
:on_service_failure,
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
20
|
+
def self.included(base)
|
|
21
|
+
base.extend(ClassMethods)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
module ClassMethods
|
|
25
|
+
# Define DSL methods for each callback event
|
|
26
|
+
EVENTS.each do |event|
|
|
27
|
+
define_method(event) do |method_name = nil, &block|
|
|
28
|
+
callback = method_name || block
|
|
29
|
+
raise ArgumentError, "#{event} requires a method name (symbol) or a block" unless callback
|
|
30
|
+
|
|
31
|
+
unless callback.is_a?(Symbol) || callback.is_a?(Proc)
|
|
32
|
+
raise ArgumentError,
|
|
33
|
+
"#{event} callback must be a Symbol or Proc"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
callbacks_for(event) << callback
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Get all callbacks for a specific event (including inherited ones)
|
|
41
|
+
def callbacks_for(event)
|
|
42
|
+
@callbacks ||= {}
|
|
43
|
+
@callbacks[event] ||= []
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Get all callbacks including inherited ones
|
|
47
|
+
def all_callbacks_for(event)
|
|
48
|
+
if superclass.respond_to?(:all_callbacks_for)
|
|
49
|
+
inherited = superclass.all_callbacks_for(event)
|
|
50
|
+
else
|
|
51
|
+
inherited = []
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
inherited + callbacks_for(event)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Run callbacks for a given event
|
|
59
|
+
# For around callbacks, yields to the block
|
|
60
|
+
# For other callbacks, just executes them in order
|
|
61
|
+
def run_callbacks(event, *args, &block)
|
|
62
|
+
callbacks = self.class.all_callbacks_for(event)
|
|
63
|
+
|
|
64
|
+
if event.to_s.start_with?("around_")
|
|
65
|
+
run_around_callbacks(callbacks, args, &block)
|
|
66
|
+
else
|
|
67
|
+
run_simple_callbacks(callbacks, args)
|
|
68
|
+
yield if block_given?
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def run_simple_callbacks(callbacks, args)
|
|
75
|
+
callbacks.each do |callback|
|
|
76
|
+
execute_callback(callback, args)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def run_around_callbacks(callbacks, args, &block)
|
|
81
|
+
return yield if callbacks.empty?
|
|
82
|
+
|
|
83
|
+
# Build a chain of around callbacks
|
|
84
|
+
chain = callbacks.reverse.reduce(block) do |next_block, callback|
|
|
85
|
+
proc { execute_callback(callback, args, &next_block) }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
chain.call
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def execute_callback(callback, args, &block)
|
|
92
|
+
case callback
|
|
93
|
+
in Symbol
|
|
94
|
+
block_given? ? send(callback, *args, &block) : send(callback, *args)
|
|
95
|
+
in Proc
|
|
96
|
+
block_given? ? instance_exec(*args, block, &callback) : instance_exec(*args, &callback)
|
|
97
|
+
else
|
|
98
|
+
raise ArgumentError, "Callback must be a Symbol or Proc, got #{callback.class}"
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "constants"
|
|
4
|
+
|
|
5
|
+
# Collection to store arguments and outputs values
|
|
6
|
+
module Light
|
|
7
|
+
module Services
|
|
8
|
+
module Collection
|
|
9
|
+
class Base
|
|
10
|
+
extend Forwardable
|
|
11
|
+
|
|
12
|
+
def_delegators :@storage, :key?, :to_h
|
|
13
|
+
|
|
14
|
+
def initialize(instance, collection_type, storage = {})
|
|
15
|
+
validate_collection_type!(collection_type)
|
|
16
|
+
|
|
17
|
+
@instance = instance
|
|
18
|
+
@collection_type = collection_type
|
|
19
|
+
@storage = storage
|
|
20
|
+
|
|
21
|
+
return if storage.is_a?(Hash)
|
|
22
|
+
|
|
23
|
+
raise Light::Services::ArgTypeError, "#{instance.class} - #{collection_type} must be a Hash"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def set(key, value)
|
|
27
|
+
@storage[key] = value
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def get(key)
|
|
31
|
+
@storage[key]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def [](key)
|
|
35
|
+
get(key)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def []=(key, value)
|
|
39
|
+
set(key, value)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def load_defaults
|
|
43
|
+
settings_collection.each do |name, settings|
|
|
44
|
+
next if !settings.default_exists || key?(name)
|
|
45
|
+
|
|
46
|
+
if settings.default.is_a?(Proc)
|
|
47
|
+
set(name, @instance.instance_exec(&settings.default))
|
|
48
|
+
else
|
|
49
|
+
set(name, Utils.deep_dup(settings.default))
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def validate!
|
|
55
|
+
settings_collection.each do |name, field|
|
|
56
|
+
next if field.optional && (!key?(name) || get(name).nil?)
|
|
57
|
+
|
|
58
|
+
# validate_type! returns the (possibly coerced) value
|
|
59
|
+
coerced_value = field.validate_type!(get(name))
|
|
60
|
+
# Store the coerced value back (supports dry-types coercion)
|
|
61
|
+
set(name, coerced_value) if coerced_value != get(name)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Extend args with context values (only for arguments)
|
|
66
|
+
def extend_with_context(args)
|
|
67
|
+
return args unless @collection_type == CollectionTypes::ARGUMENTS
|
|
68
|
+
|
|
69
|
+
settings_collection.each do |name, field|
|
|
70
|
+
next if !field.context || args.key?(name) || !key?(name)
|
|
71
|
+
|
|
72
|
+
args[field.name] = get(name)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
args
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def validate_collection_type!(type)
|
|
81
|
+
return if CollectionTypes::ALL.include?(type)
|
|
82
|
+
|
|
83
|
+
raise ArgumentError,
|
|
84
|
+
"collection_type must be one of #{CollectionTypes::ALL.join(', ')}, got: #{type.inspect}"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def settings_collection
|
|
88
|
+
@instance.class.public_send(@collection_type)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Aliases for backwards compatibility
|
|
93
|
+
Arguments = Base
|
|
94
|
+
Outputs = Base
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Light
|
|
4
|
+
module Services
|
|
5
|
+
module Concerns
|
|
6
|
+
# Handles service execution logic including steps and validation
|
|
7
|
+
module Execution
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
# Execute the main service logic
|
|
11
|
+
def execute_service
|
|
12
|
+
self.class.validate_steps!
|
|
13
|
+
run_steps
|
|
14
|
+
run_steps_with_always
|
|
15
|
+
@outputs.validate! if success?
|
|
16
|
+
|
|
17
|
+
copy_warnings_to_parent_service
|
|
18
|
+
copy_errors_to_parent_service
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Run all service result callbacks based on success/failure
|
|
22
|
+
def run_service_result_callbacks
|
|
23
|
+
run_callbacks(:after_service_run, self)
|
|
24
|
+
|
|
25
|
+
if success?
|
|
26
|
+
run_callbacks(:on_service_success, self)
|
|
27
|
+
else
|
|
28
|
+
run_callbacks(:on_service_failure, self)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Run normal steps within transaction
|
|
33
|
+
def run_steps
|
|
34
|
+
within_transaction do
|
|
35
|
+
# Cache steps once for both normal and always execution
|
|
36
|
+
@cached_steps = self.class.steps
|
|
37
|
+
|
|
38
|
+
@cached_steps.each do |name, step|
|
|
39
|
+
@launched_steps << name if step.run(self)
|
|
40
|
+
|
|
41
|
+
break if @errors.break? || @warnings.break?
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Run steps with parameter `always` if they weren't launched because of errors/warnings
|
|
47
|
+
def run_steps_with_always
|
|
48
|
+
# Use cached steps from run_steps, or get them if run_steps wasn't called
|
|
49
|
+
steps_to_check = @cached_steps || self.class.steps
|
|
50
|
+
|
|
51
|
+
steps_to_check.each do |name, step|
|
|
52
|
+
next if !step.always || @launched_steps.include?(name)
|
|
53
|
+
|
|
54
|
+
@launched_steps << name if step.run(self)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Load defaults for outputs and arguments, then validate arguments
|
|
59
|
+
def load_defaults_and_validate
|
|
60
|
+
@outputs.load_defaults
|
|
61
|
+
@arguments.load_defaults
|
|
62
|
+
@arguments.validate!
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Execute block within transaction if configured
|
|
66
|
+
def within_transaction(&block)
|
|
67
|
+
if @config[:use_transactions] && defined?(ActiveRecord::Base)
|
|
68
|
+
ActiveRecord::Base.transaction(requires_new: true, &block)
|
|
69
|
+
else
|
|
70
|
+
yield
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Light
|
|
4
|
+
module Services
|
|
5
|
+
module Concerns
|
|
6
|
+
# Handles copying errors and warnings to parent services
|
|
7
|
+
module ParentService
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
# Copy warnings from this service to parent service
|
|
11
|
+
def copy_warnings_to_parent_service
|
|
12
|
+
return if !@parent_service || !@config[:load_warnings]
|
|
13
|
+
|
|
14
|
+
@parent_service.warnings.copy_from(
|
|
15
|
+
@warnings,
|
|
16
|
+
break: @config[:self_break_on_warning],
|
|
17
|
+
rollback: @config[:self_rollback_on_warning],
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Copy errors from this service to parent service
|
|
22
|
+
def copy_errors_to_parent_service
|
|
23
|
+
return if !@parent_service || !@config[:load_errors]
|
|
24
|
+
|
|
25
|
+
@parent_service.errors.copy_from(
|
|
26
|
+
@errors,
|
|
27
|
+
break: @config[:self_break_on_error],
|
|
28
|
+
rollback: @config[:self_rollback_on_error],
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Light
|
|
4
|
+
module Services
|
|
5
|
+
module Concerns
|
|
6
|
+
# Manages service state including errors and warnings initialization
|
|
7
|
+
module StateManagement
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
# Initialize errors collection with configuration
|
|
11
|
+
def initialize_errors
|
|
12
|
+
@errors = Messages.new(
|
|
13
|
+
break_on_add: @config[:break_on_error],
|
|
14
|
+
raise_on_add: @config[:raise_on_error],
|
|
15
|
+
rollback_on_add: @config[:use_transactions] && @config[:rollback_on_error],
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Initialize warnings collection with configuration
|
|
20
|
+
def initialize_warnings
|
|
21
|
+
@warnings = Messages.new(
|
|
22
|
+
break_on_add: @config[:break_on_warning],
|
|
23
|
+
raise_on_add: @config[:raise_on_warning],
|
|
24
|
+
rollback_on_add: @config[:use_transactions] && @config[:rollback_on_warning],
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|