light-services 2.2.1 → 3.1.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 +83 -7
- data/CHANGELOG.md +38 -0
- data/CLAUDE.md +139 -0
- data/Gemfile +16 -11
- data/Gemfile.lock +53 -27
- data/README.md +84 -21
- data/docs/arguments.md +290 -0
- data/docs/best-practices.md +153 -0
- data/docs/callbacks.md +476 -0
- data/docs/concepts.md +80 -0
- data/docs/configuration.md +204 -0
- data/docs/context.md +128 -0
- data/docs/crud.md +525 -0
- data/docs/errors.md +280 -0
- data/docs/generators.md +250 -0
- data/docs/outputs.md +158 -0
- data/docs/pundit-authorization.md +320 -0
- data/docs/quickstart.md +134 -0
- data/docs/readme.md +101 -0
- data/docs/recipes.md +14 -0
- data/docs/rubocop.md +285 -0
- data/docs/ruby-lsp.md +133 -0
- data/docs/service-rendering.md +222 -0
- data/docs/steps.md +391 -0
- data/docs/summary.md +21 -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 +134 -122
- data/lib/light/services/base_with_context.rb +23 -1
- data/lib/light/services/callbacks.rb +157 -0
- data/lib/light/services/collection.rb +145 -0
- data/lib/light/services/concerns/execution.rb +79 -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 +82 -16
- data/lib/light/services/constants.rb +100 -0
- data/lib/light/services/dsl/arguments_dsl.rb +85 -0
- data/lib/light/services/dsl/outputs_dsl.rb +81 -0
- data/lib/light/services/dsl/steps_dsl.rb +205 -0
- data/lib/light/services/dsl/validation.rb +162 -0
- data/lib/light/services/exceptions.rb +25 -2
- data/lib/light/services/message.rb +28 -3
- data/lib/light/services/messages.rb +92 -32
- 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/rubocop/cop/light_services/argument_type_required.rb +52 -0
- data/lib/light/services/rubocop/cop/light_services/condition_method_exists.rb +173 -0
- data/lib/light/services/rubocop/cop/light_services/deprecated_methods.rb +113 -0
- data/lib/light/services/rubocop/cop/light_services/dsl_order.rb +176 -0
- data/lib/light/services/rubocop/cop/light_services/missing_private_keyword.rb +102 -0
- data/lib/light/services/rubocop/cop/light_services/no_direct_instantiation.rb +66 -0
- data/lib/light/services/rubocop/cop/light_services/output_type_required.rb +52 -0
- data/lib/light/services/rubocop/cop/light_services/step_method_exists.rb +109 -0
- data/lib/light/services/rubocop.rb +12 -0
- data/lib/light/services/settings/field.rb +114 -0
- data/lib/light/services/settings/step.rb +53 -20
- data/lib/light/services/utils.rb +38 -0
- data/lib/light/services/version.rb +1 -1
- data/lib/light/services.rb +2 -0
- data/lib/ruby_lsp/light_services/addon.rb +36 -0
- data/lib/ruby_lsp/light_services/definition.rb +132 -0
- data/lib/ruby_lsp/light_services/indexing_enhancement.rb +263 -0
- data/light-services.gemspec +6 -8
- metadata +68 -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,116 +1,208 @@
|
|
|
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
|
|
24
|
+
# Base class for building service objects with arguments, outputs, and steps.
|
|
25
|
+
#
|
|
26
|
+
# @example Basic service
|
|
27
|
+
# class CreateUser < Light::Services::Base
|
|
28
|
+
# arg :name, type: String
|
|
29
|
+
# arg :email, type: String
|
|
30
|
+
#
|
|
31
|
+
# output :user, type: User
|
|
32
|
+
#
|
|
33
|
+
# step :create_user
|
|
34
|
+
#
|
|
35
|
+
# private
|
|
36
|
+
#
|
|
37
|
+
# def create_user
|
|
38
|
+
# self.user = User.create!(name: name, email: email)
|
|
39
|
+
# end
|
|
40
|
+
# end
|
|
41
|
+
#
|
|
42
|
+
# result = CreateUser.run(name: "John", email: "john@example.com")
|
|
43
|
+
# result.success? # => true
|
|
44
|
+
# result.user # => #<User id: 1, name: "John">
|
|
23
45
|
class Base
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
#
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
46
|
+
include Callbacks
|
|
47
|
+
include Dsl::ArgumentsDsl
|
|
48
|
+
include Dsl::OutputsDsl
|
|
49
|
+
include Dsl::StepsDsl
|
|
50
|
+
include Concerns::Execution
|
|
51
|
+
include Concerns::StateManagement
|
|
52
|
+
include Concerns::ParentService
|
|
53
|
+
|
|
54
|
+
# @return [Collection::Base] collection of output values
|
|
55
|
+
attr_reader :outputs
|
|
56
|
+
|
|
57
|
+
# @return [Collection::Base] collection of argument values
|
|
58
|
+
attr_reader :arguments
|
|
59
|
+
|
|
60
|
+
# @return [Messages] collection of error messages
|
|
61
|
+
attr_reader :errors
|
|
62
|
+
|
|
63
|
+
# @return [Messages] collection of warning messages
|
|
64
|
+
attr_reader :warnings
|
|
65
|
+
|
|
66
|
+
# Initialize a new service instance.
|
|
67
|
+
#
|
|
68
|
+
# @param args [Hash] arguments to pass to the service
|
|
69
|
+
# @param config [Hash] runtime configuration overrides
|
|
70
|
+
# @param parent_service [Base, nil] parent service for nested calls
|
|
40
71
|
def initialize(args = {}, config = {}, parent_service = nil)
|
|
41
72
|
@config = Light::Services.config.merge(self.class.class_config || {}).merge(config)
|
|
42
73
|
@parent_service = parent_service
|
|
43
74
|
|
|
44
|
-
@outputs = Collection::
|
|
45
|
-
@arguments = Collection::
|
|
75
|
+
@outputs = Collection::Base.new(self, CollectionTypes::OUTPUTS)
|
|
76
|
+
@arguments = Collection::Base.new(self, CollectionTypes::ARGUMENTS, args.dup)
|
|
46
77
|
|
|
47
|
-
@
|
|
78
|
+
@stopped = false
|
|
48
79
|
@launched_steps = []
|
|
49
80
|
|
|
50
81
|
initialize_errors
|
|
51
82
|
initialize_warnings
|
|
52
83
|
end
|
|
53
84
|
|
|
85
|
+
# Check if the service completed without errors.
|
|
86
|
+
#
|
|
87
|
+
# @return [Boolean] true if no errors were added
|
|
54
88
|
def success?
|
|
55
89
|
!errors?
|
|
56
90
|
end
|
|
57
91
|
|
|
92
|
+
# Check if the service completed with errors.
|
|
93
|
+
#
|
|
94
|
+
# @return [Boolean] true if any errors were added
|
|
58
95
|
def failed?
|
|
59
96
|
errors?
|
|
60
97
|
end
|
|
61
98
|
|
|
99
|
+
# Check if the service has any errors.
|
|
100
|
+
#
|
|
101
|
+
# @return [Boolean] true if errors collection is not empty
|
|
62
102
|
def errors?
|
|
63
103
|
@errors.any?
|
|
64
104
|
end
|
|
65
105
|
|
|
106
|
+
# Check if the service has any warnings.
|
|
107
|
+
#
|
|
108
|
+
# @return [Boolean] true if warnings collection is not empty
|
|
66
109
|
def warnings?
|
|
67
110
|
@warnings.any?
|
|
68
111
|
end
|
|
69
112
|
|
|
70
|
-
|
|
71
|
-
|
|
113
|
+
# Stop executing remaining steps after the current step completes.
|
|
114
|
+
#
|
|
115
|
+
# @return [Boolean] true
|
|
116
|
+
def stop!
|
|
117
|
+
@stopped = true
|
|
118
|
+
end
|
|
119
|
+
alias done! stop!
|
|
120
|
+
|
|
121
|
+
# Check if the service has been stopped.
|
|
122
|
+
#
|
|
123
|
+
# @return [Boolean] true if stop! was called
|
|
124
|
+
def stopped?
|
|
125
|
+
@stopped
|
|
72
126
|
end
|
|
127
|
+
alias done? stopped?
|
|
73
128
|
|
|
74
|
-
|
|
75
|
-
|
|
129
|
+
# Stop execution immediately, skipping any remaining code in the current step.
|
|
130
|
+
#
|
|
131
|
+
# @raise [StopExecution] always raises to halt execution
|
|
132
|
+
# @return [void]
|
|
133
|
+
def stop_immediately!
|
|
134
|
+
@stopped = true
|
|
135
|
+
raise Light::Services::StopExecution
|
|
76
136
|
end
|
|
77
137
|
|
|
138
|
+
# Execute the service steps.
|
|
139
|
+
#
|
|
140
|
+
# @return [void]
|
|
141
|
+
# @raise [StandardError] re-raises any exception after running always steps
|
|
78
142
|
def call
|
|
79
143
|
load_defaults_and_validate
|
|
80
|
-
log_header if benchmark? || verbose?
|
|
81
144
|
|
|
82
|
-
|
|
83
|
-
run_steps
|
|
84
|
-
run_steps_with_always
|
|
145
|
+
run_callbacks(:before_service_run, self)
|
|
85
146
|
|
|
86
|
-
|
|
87
|
-
|
|
147
|
+
run_callbacks(:around_service_run, self) do
|
|
148
|
+
execute_service
|
|
88
149
|
end
|
|
89
150
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
log "🟢 Finished #{self.class} in #{time}ms"
|
|
93
|
-
puts
|
|
151
|
+
run_service_result_callbacks
|
|
94
152
|
rescue StandardError => e
|
|
95
153
|
run_steps_with_always
|
|
96
154
|
raise e
|
|
97
155
|
end
|
|
98
156
|
|
|
99
157
|
class << self
|
|
158
|
+
# @return [Hash, nil] class-level configuration options
|
|
100
159
|
attr_accessor :class_config
|
|
101
160
|
|
|
161
|
+
# Set class-level configuration for this service.
|
|
162
|
+
#
|
|
163
|
+
# @param config [Hash] configuration options
|
|
164
|
+
# @return [Hash] the configuration hash
|
|
102
165
|
def config(config = {})
|
|
103
166
|
self.class_config = config
|
|
104
167
|
end
|
|
105
168
|
|
|
169
|
+
# Run the service and return the result.
|
|
170
|
+
#
|
|
171
|
+
# @param args [Hash] arguments to pass to the service
|
|
172
|
+
# @param config [Hash] runtime configuration overrides
|
|
173
|
+
# @return [Base] the executed service instance
|
|
174
|
+
#
|
|
175
|
+
# @example
|
|
176
|
+
# result = MyService.run(name: "test")
|
|
177
|
+
# result.success? # => true
|
|
106
178
|
def run(args = {}, config = {})
|
|
107
179
|
new(args, config).tap(&:call)
|
|
108
180
|
end
|
|
109
181
|
|
|
182
|
+
# Run the service and raise an error if it fails.
|
|
183
|
+
#
|
|
184
|
+
# @param args [Hash] arguments to pass to the service
|
|
185
|
+
# @param config [Hash] runtime configuration overrides
|
|
186
|
+
# @return [Base] the executed service instance
|
|
187
|
+
# @raise [Error] if the service fails
|
|
188
|
+
#
|
|
189
|
+
# @example
|
|
190
|
+
# MyService.run!(name: "test") # raises if service fails
|
|
110
191
|
def run!(args = {}, config = {})
|
|
111
192
|
run(args, config.merge(raise_on_error: true))
|
|
112
193
|
end
|
|
113
194
|
|
|
195
|
+
# Create a context for running the service with a parent service or config.
|
|
196
|
+
#
|
|
197
|
+
# @param service_or_config [Base, Hash] parent service or configuration hash
|
|
198
|
+
# @param config [Hash] configuration hash (when first param is a service)
|
|
199
|
+
# @return [BaseWithContext] context wrapper for running the service
|
|
200
|
+
#
|
|
201
|
+
# @example With parent service
|
|
202
|
+
# ChildService.with(self).run(data: value)
|
|
203
|
+
#
|
|
204
|
+
# @example With configuration
|
|
205
|
+
# MyService.with(use_transactions: false).run(name: "test")
|
|
114
206
|
def with(service_or_config = {}, config = {})
|
|
115
207
|
service = service_or_config.is_a?(Hash) ? nil : service_or_config
|
|
116
208
|
config = service_or_config unless service
|
|
@@ -118,86 +210,6 @@ module Light
|
|
|
118
210
|
BaseWithContext.new(self, service, config.dup)
|
|
119
211
|
end
|
|
120
212
|
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
213
|
end
|
|
202
214
|
end
|
|
203
215
|
end
|
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# This class allows running a service object with context (parent class and custom config)
|
|
4
3
|
module Light
|
|
5
4
|
module Services
|
|
5
|
+
# Wrapper for running a service with a parent context or custom configuration.
|
|
6
|
+
# Created via {Base.with} method.
|
|
7
|
+
#
|
|
8
|
+
# @example Running with parent service context
|
|
9
|
+
# ChildService.with(self).run(data: value)
|
|
10
|
+
#
|
|
11
|
+
# @example Running with custom configuration
|
|
12
|
+
# MyService.with(use_transactions: false).run(name: "test")
|
|
6
13
|
class BaseWithContext
|
|
14
|
+
# Initialize a new context wrapper.
|
|
15
|
+
#
|
|
16
|
+
# @param service_class [Class] the service class to run
|
|
17
|
+
# @param parent_service [Base, nil] parent service for error/warning propagation
|
|
18
|
+
# @param config [Hash] configuration overrides
|
|
19
|
+
# @raise [ArgTypeError] if parent_service is not a Base subclass
|
|
7
20
|
def initialize(service_class, parent_service, config)
|
|
8
21
|
@service_class = service_class
|
|
9
22
|
@config = config
|
|
@@ -14,10 +27,19 @@ module Light
|
|
|
14
27
|
raise Light::Services::ArgTypeError, "#{parent_service.class} - must be a subclass of Light::Services::Base"
|
|
15
28
|
end
|
|
16
29
|
|
|
30
|
+
# Run the service with the configured context.
|
|
31
|
+
#
|
|
32
|
+
# @param args [Hash] arguments to pass to the service
|
|
33
|
+
# @return [Base] the executed service instance
|
|
17
34
|
def run(args = {})
|
|
18
35
|
@service_class.new(extend_arguments(args), @config, @parent_service).tap(&:call)
|
|
19
36
|
end
|
|
20
37
|
|
|
38
|
+
# Run the service and raise an error if it fails.
|
|
39
|
+
#
|
|
40
|
+
# @param args [Hash] arguments to pass to the service
|
|
41
|
+
# @return [Base] the executed service instance
|
|
42
|
+
# @raise [Error] if the service fails
|
|
21
43
|
def run!(args = {})
|
|
22
44
|
@config[:raise_on_error] = true
|
|
23
45
|
run(args)
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Light
|
|
4
|
+
module Services
|
|
5
|
+
# Provides callback hooks for service and step lifecycle events.
|
|
6
|
+
#
|
|
7
|
+
# @example Service-level callbacks
|
|
8
|
+
# class MyService < Light::Services::Base
|
|
9
|
+
# before_service_run :log_start
|
|
10
|
+
# after_service_run { |service| Rails.logger.info("Done!") }
|
|
11
|
+
# on_service_success :send_notification
|
|
12
|
+
# on_service_failure :log_error
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# @example Step-level callbacks
|
|
16
|
+
# class MyService < Light::Services::Base
|
|
17
|
+
# before_step_run :log_step_start
|
|
18
|
+
# after_step_run { |service, step_name| puts "Finished #{step_name}" }
|
|
19
|
+
# on_step_failure :handle_step_error
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# @example Around callbacks
|
|
23
|
+
# class MyService < Light::Services::Base
|
|
24
|
+
# around_service_run :with_timing
|
|
25
|
+
#
|
|
26
|
+
# private
|
|
27
|
+
#
|
|
28
|
+
# def with_timing(service)
|
|
29
|
+
# start = Time.now
|
|
30
|
+
# yield
|
|
31
|
+
# puts "Took #{Time.now - start}s"
|
|
32
|
+
# end
|
|
33
|
+
# end
|
|
34
|
+
module Callbacks
|
|
35
|
+
# Available callback events.
|
|
36
|
+
# @return [Array<Symbol>] list of callback event names
|
|
37
|
+
EVENTS = [
|
|
38
|
+
:before_step_run,
|
|
39
|
+
:after_step_run,
|
|
40
|
+
:around_step_run,
|
|
41
|
+
:on_step_success,
|
|
42
|
+
:on_step_failure,
|
|
43
|
+
:on_step_crash,
|
|
44
|
+
:before_service_run,
|
|
45
|
+
:after_service_run,
|
|
46
|
+
:around_service_run,
|
|
47
|
+
:on_service_success,
|
|
48
|
+
:on_service_failure,
|
|
49
|
+
].freeze
|
|
50
|
+
|
|
51
|
+
def self.included(base)
|
|
52
|
+
base.extend(ClassMethods)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Class methods for registering callbacks.
|
|
56
|
+
#
|
|
57
|
+
# Each callback event has a corresponding class method:
|
|
58
|
+
# - {before_step_run} - before each step executes
|
|
59
|
+
# - {after_step_run} - after each step executes
|
|
60
|
+
# - {around_step_run} - wraps step execution (must yield)
|
|
61
|
+
# - {on_step_success} - when a step completes without adding errors
|
|
62
|
+
# - {on_step_failure} - when a step adds errors
|
|
63
|
+
# - {on_step_crash} - when a step raises an exception
|
|
64
|
+
# - {before_service_run} - before the service starts
|
|
65
|
+
# - {after_service_run} - after the service completes
|
|
66
|
+
# - {around_service_run} - wraps service execution (must yield)
|
|
67
|
+
# - {on_service_success} - when service completes without errors
|
|
68
|
+
# - {on_service_failure} - when service completes with errors
|
|
69
|
+
module ClassMethods
|
|
70
|
+
# Define DSL methods for each callback event
|
|
71
|
+
EVENTS.each do |event|
|
|
72
|
+
define_method(event) do |method_name = nil, &block|
|
|
73
|
+
callback = method_name || block
|
|
74
|
+
raise ArgumentError, "#{event} requires a method name (symbol) or a block" unless callback
|
|
75
|
+
|
|
76
|
+
unless callback.is_a?(Symbol) || callback.is_a?(Proc)
|
|
77
|
+
raise ArgumentError,
|
|
78
|
+
"#{event} callback must be a Symbol or Proc"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
callbacks_for(event) << callback
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Get callbacks defined in this class for a specific event.
|
|
86
|
+
#
|
|
87
|
+
# @param event [Symbol] the callback event name
|
|
88
|
+
# @return [Array<Symbol, Proc>] callbacks for this event
|
|
89
|
+
def callbacks_for(event)
|
|
90
|
+
@callbacks ||= {}
|
|
91
|
+
@callbacks[event] ||= []
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Get all callbacks for an event including inherited ones.
|
|
95
|
+
#
|
|
96
|
+
# @param event [Symbol] the callback event name
|
|
97
|
+
# @return [Array<Symbol, Proc>] all callbacks for this event
|
|
98
|
+
def all_callbacks_for(event)
|
|
99
|
+
if superclass.respond_to?(:all_callbacks_for)
|
|
100
|
+
inherited = superclass.all_callbacks_for(event)
|
|
101
|
+
else
|
|
102
|
+
inherited = []
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
inherited + callbacks_for(event)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Run all callbacks for a given event.
|
|
110
|
+
#
|
|
111
|
+
# @param event [Symbol] the callback event name
|
|
112
|
+
# @param args [Array] arguments to pass to callbacks
|
|
113
|
+
# @yield for around callbacks, the block to wrap
|
|
114
|
+
# @return [void]
|
|
115
|
+
def run_callbacks(event, *args, &block)
|
|
116
|
+
callbacks = self.class.all_callbacks_for(event)
|
|
117
|
+
|
|
118
|
+
if event.to_s.start_with?("around_")
|
|
119
|
+
run_around_callbacks(callbacks, args, &block)
|
|
120
|
+
else
|
|
121
|
+
run_simple_callbacks(callbacks, args)
|
|
122
|
+
yield if block_given?
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
private
|
|
127
|
+
|
|
128
|
+
def run_simple_callbacks(callbacks, args)
|
|
129
|
+
callbacks.each do |callback|
|
|
130
|
+
execute_callback(callback, args)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def run_around_callbacks(callbacks, args, &block)
|
|
135
|
+
return yield if callbacks.empty?
|
|
136
|
+
|
|
137
|
+
# Build a chain of around callbacks
|
|
138
|
+
chain = callbacks.reverse.reduce(block) do |next_block, callback|
|
|
139
|
+
proc { execute_callback(callback, args, &next_block) }
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
chain.call
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def execute_callback(callback, args, &block)
|
|
146
|
+
case callback
|
|
147
|
+
in Symbol
|
|
148
|
+
block_given? ? send(callback, *args, &block) : send(callback, *args)
|
|
149
|
+
in Proc
|
|
150
|
+
block_given? ? instance_exec(*args, block, &callback) : instance_exec(*args, &callback)
|
|
151
|
+
else
|
|
152
|
+
raise ArgumentError, "Callback must be a Symbol or Proc, got #{callback.class}"
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|