operandi 5.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 +7 -0
- data/.cursor/rules/services/RULE.md +269 -0
- data/.cursor/rules/services-rspec/RULE.md +354 -0
- data/.github/dependabot.yml +11 -0
- data/.github/workflows/ci.yml +77 -0
- data/.gitignore +25 -0
- data/.rspec +3 -0
- data/.rubocop.yml +134 -0
- data/.ruby-version +1 -0
- data/.vscode/cspell.json +18 -0
- data/.vscode/project-words.txt +12 -0
- data/AGENTS.md +139 -0
- data/CHANGELOG.md +111 -0
- data/CLAUDE.md +139 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +28 -0
- data/Gemfile.lock +149 -0
- data/LICENSE.txt +21 -0
- data/README.md +172 -0
- data/Rakefile +8 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/config/default.yml +57 -0
- data/docs/README.md +105 -0
- data/docs/SUMMARY.md +31 -0
- data/docs/arguments.md +275 -0
- data/docs/best-practices.md +153 -0
- data/docs/callbacks.md +475 -0
- data/docs/concepts.md +79 -0
- data/docs/configuration.md +218 -0
- data/docs/context.md +128 -0
- data/docs/crud.md +525 -0
- data/docs/errors.md +331 -0
- data/docs/generators.md +250 -0
- data/docs/outputs.md +150 -0
- data/docs/pundit-authorization.md +320 -0
- data/docs/quickstart.md +133 -0
- data/docs/recipes.md +14 -0
- data/docs/rubocop.md +430 -0
- data/docs/ruby-lsp.md +121 -0
- data/docs/service-rendering.md +222 -0
- data/docs/sorbet-runtime.md +283 -0
- data/docs/steps.md +438 -0
- data/docs/tapioca.md +188 -0
- data/docs/testing.md +548 -0
- data/lib/generators/operandi/install/USAGE +15 -0
- data/lib/generators/operandi/install/install_generator.rb +45 -0
- data/lib/generators/operandi/install/templates/application_service.rb.tt +8 -0
- data/lib/generators/operandi/install/templates/application_service_spec.rb.tt +7 -0
- data/lib/generators/operandi/install/templates/initializer.rb.tt +30 -0
- data/lib/generators/operandi/service/USAGE +21 -0
- data/lib/generators/operandi/service/service_generator.rb +80 -0
- data/lib/generators/operandi/service/templates/service.rb.tt +48 -0
- data/lib/generators/operandi/service/templates/service_spec.rb.tt +40 -0
- data/lib/operandi/base.rb +230 -0
- data/lib/operandi/base_with_context.rb +57 -0
- data/lib/operandi/callbacks.rb +353 -0
- data/lib/operandi/collection.rb +166 -0
- data/lib/operandi/concerns/execution.rb +80 -0
- data/lib/operandi/concerns/parent_service.rb +32 -0
- data/lib/operandi/concerns/state_management.rb +34 -0
- data/lib/operandi/config.rb +142 -0
- data/lib/operandi/constants.rb +96 -0
- data/lib/operandi/dsl/arguments_dsl.rb +83 -0
- data/lib/operandi/dsl/outputs_dsl.rb +79 -0
- data/lib/operandi/dsl/steps_dsl.rb +206 -0
- data/lib/operandi/dsl/validation.rb +171 -0
- data/lib/operandi/exceptions.rb +66 -0
- data/lib/operandi/message.rb +52 -0
- data/lib/operandi/messages.rb +185 -0
- data/lib/operandi/rspec/matchers/define_argument.rb +172 -0
- data/lib/operandi/rspec/matchers/define_output.rb +145 -0
- data/lib/operandi/rspec/matchers/define_step.rb +223 -0
- data/lib/operandi/rspec/matchers/execute_step.rb +228 -0
- data/lib/operandi/rspec/matchers/have_error_on.rb +144 -0
- data/lib/operandi/rspec/matchers/have_warning_on.rb +146 -0
- data/lib/operandi/rspec/matchers/trigger_callback.rb +136 -0
- data/lib/operandi/rspec.rb +15 -0
- data/lib/operandi/rubocop/cop/operandi/argument_type_required.rb +52 -0
- data/lib/operandi/rubocop/cop/operandi/condition_method_exists.rb +173 -0
- data/lib/operandi/rubocop/cop/operandi/deprecated_accessors.rb +113 -0
- data/lib/operandi/rubocop/cop/operandi/deprecated_methods.rb +113 -0
- data/lib/operandi/rubocop/cop/operandi/dsl_order.rb +181 -0
- data/lib/operandi/rubocop/cop/operandi/missing_private_keyword.rb +102 -0
- data/lib/operandi/rubocop/cop/operandi/no_direct_instantiation.rb +66 -0
- data/lib/operandi/rubocop/cop/operandi/no_hash_argument.rb +101 -0
- data/lib/operandi/rubocop/cop/operandi/output_type_required.rb +52 -0
- data/lib/operandi/rubocop/cop/operandi/prefer_fail_method.rb +112 -0
- data/lib/operandi/rubocop/cop/operandi/prefer_optional_over_default_nil.rb +124 -0
- data/lib/operandi/rubocop/cop/operandi/redundant_optional.rb +103 -0
- data/lib/operandi/rubocop/cop/operandi/reserved_name.rb +56 -0
- data/lib/operandi/rubocop/cop/operandi/step_method_exists.rb +134 -0
- data/lib/operandi/rubocop.rb +22 -0
- data/lib/operandi/settings/field.rb +147 -0
- data/lib/operandi/settings/step.rb +105 -0
- data/lib/operandi/utils.rb +36 -0
- data/lib/operandi/version.rb +5 -0
- data/lib/operandi.rb +11 -0
- data/lib/ruby_lsp/operandi/addon.rb +37 -0
- data/lib/ruby_lsp/operandi/definition.rb +134 -0
- data/lib/ruby_lsp/operandi/indexing_enhancement.rb +224 -0
- data/lib/tapioca/dsl/compilers/operandi.rb +378 -0
- data/operandi.gemspec +33 -0
- data/rbi/operandi.rbi +197 -0
- data/sorbet/cache/data.mdb +0 -0
- data/sorbet/cache/lock.mdb +0 -0
- metadata +151 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/base"
|
|
4
|
+
|
|
5
|
+
module Operandi
|
|
6
|
+
module Generators
|
|
7
|
+
class ServiceGenerator < ::Rails::Generators::Base
|
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
|
9
|
+
|
|
10
|
+
argument :name,
|
|
11
|
+
type: :string,
|
|
12
|
+
required: true,
|
|
13
|
+
desc: "The name of the service (e.g., user/create or CreateUser)"
|
|
14
|
+
|
|
15
|
+
class_option :args,
|
|
16
|
+
type: :array,
|
|
17
|
+
default: [],
|
|
18
|
+
desc: "List of arguments for the service"
|
|
19
|
+
class_option :steps,
|
|
20
|
+
type: :array,
|
|
21
|
+
default: [],
|
|
22
|
+
desc: "List of steps for the service"
|
|
23
|
+
class_option :outputs,
|
|
24
|
+
type: :array,
|
|
25
|
+
default: [],
|
|
26
|
+
desc: "List of outputs for the service"
|
|
27
|
+
class_option :skip_spec,
|
|
28
|
+
type: :boolean,
|
|
29
|
+
default: false,
|
|
30
|
+
desc: "Skip creating the spec file"
|
|
31
|
+
class_option :parent,
|
|
32
|
+
type: :string,
|
|
33
|
+
default: "ApplicationService",
|
|
34
|
+
desc: "Parent class for the service"
|
|
35
|
+
|
|
36
|
+
desc "Creates a new service class"
|
|
37
|
+
|
|
38
|
+
def create_service_file
|
|
39
|
+
template "service.rb.tt", "app/services/#{file_path}.rb"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def create_spec_file
|
|
43
|
+
return if options[:skip_spec]
|
|
44
|
+
return unless rspec_installed?
|
|
45
|
+
|
|
46
|
+
template "service_spec.rb.tt", "spec/services/#{file_path}_spec.rb"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def file_path
|
|
52
|
+
name.underscore
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def class_name
|
|
56
|
+
name.camelize
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def parent_class
|
|
60
|
+
options[:parent]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def arguments
|
|
64
|
+
options[:args]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def steps
|
|
68
|
+
options[:steps]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def outputs
|
|
72
|
+
options[:outputs]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def rspec_installed?
|
|
76
|
+
File.directory?(File.join(destination_root, "spec"))
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
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_successful
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "operandi/constants"
|
|
4
|
+
require "operandi/message"
|
|
5
|
+
require "operandi/messages"
|
|
6
|
+
require "operandi/base_with_context"
|
|
7
|
+
|
|
8
|
+
require "operandi/settings/step"
|
|
9
|
+
require "operandi/settings/field"
|
|
10
|
+
|
|
11
|
+
require "operandi/collection"
|
|
12
|
+
|
|
13
|
+
require "operandi/dsl/arguments_dsl"
|
|
14
|
+
require "operandi/dsl/outputs_dsl"
|
|
15
|
+
require "operandi/dsl/steps_dsl"
|
|
16
|
+
|
|
17
|
+
require "operandi/concerns/execution"
|
|
18
|
+
require "operandi/concerns/state_management"
|
|
19
|
+
require "operandi/concerns/parent_service"
|
|
20
|
+
|
|
21
|
+
# Base class for all service objects
|
|
22
|
+
module Operandi
|
|
23
|
+
# Base class for building service objects with arguments, outputs, and steps.
|
|
24
|
+
#
|
|
25
|
+
# @example Basic service
|
|
26
|
+
# class CreateUser < Operandi::Base
|
|
27
|
+
# arg :name, type: String
|
|
28
|
+
# arg :email, type: String
|
|
29
|
+
#
|
|
30
|
+
# output :user, type: User
|
|
31
|
+
#
|
|
32
|
+
# step :create_user
|
|
33
|
+
#
|
|
34
|
+
# private
|
|
35
|
+
#
|
|
36
|
+
# def create_user
|
|
37
|
+
# self.user = User.create!(name: name, email: email)
|
|
38
|
+
# end
|
|
39
|
+
# end
|
|
40
|
+
#
|
|
41
|
+
# result = CreateUser.run(name: "John", email: "john@example.com")
|
|
42
|
+
# result.success? # => true
|
|
43
|
+
# result.user # => #<User id: 1, name: "John">
|
|
44
|
+
class Base
|
|
45
|
+
extend CallbackDsl
|
|
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 argument values
|
|
55
|
+
attr_reader :arg
|
|
56
|
+
|
|
57
|
+
# @return [Collection::Base] collection of output values
|
|
58
|
+
attr_reader :output
|
|
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
|
|
71
|
+
def initialize(args = {}, config = {}, parent_service = nil)
|
|
72
|
+
@config = Operandi.config.merge(self.class.class_config || {}).merge(config)
|
|
73
|
+
@parent_service = parent_service
|
|
74
|
+
|
|
75
|
+
@output = Collection::Base.new(self, CollectionTypes::OUTPUTS)
|
|
76
|
+
@arg = Collection::Base.new(self, CollectionTypes::ARGUMENTS, args.dup)
|
|
77
|
+
|
|
78
|
+
@stopped = false
|
|
79
|
+
@launched_steps = []
|
|
80
|
+
|
|
81
|
+
initialize_errors
|
|
82
|
+
initialize_warnings
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Check if the service completed without errors.
|
|
86
|
+
#
|
|
87
|
+
# @return [Boolean] true if no errors were added
|
|
88
|
+
def success?
|
|
89
|
+
!errors?
|
|
90
|
+
end
|
|
91
|
+
alias successful? success?
|
|
92
|
+
|
|
93
|
+
# Check if the service completed with errors.
|
|
94
|
+
#
|
|
95
|
+
# @return [Boolean] true if any errors were added
|
|
96
|
+
def failed?
|
|
97
|
+
errors?
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Check if the service has any errors.
|
|
101
|
+
#
|
|
102
|
+
# @return [Boolean] true if errors collection is not empty
|
|
103
|
+
def errors?
|
|
104
|
+
@errors.any?
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Check if the service has any warnings.
|
|
108
|
+
#
|
|
109
|
+
# @return [Boolean] true if warnings collection is not empty
|
|
110
|
+
def warnings?
|
|
111
|
+
@warnings.any?
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Stop executing remaining steps after the current step completes.
|
|
115
|
+
#
|
|
116
|
+
# @return [Boolean] true
|
|
117
|
+
def stop!
|
|
118
|
+
@stopped = true
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Check if the service has been stopped.
|
|
122
|
+
#
|
|
123
|
+
# @return [Boolean] true if stop! was called
|
|
124
|
+
def stopped?
|
|
125
|
+
@stopped
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Stop execution immediately, skipping any remaining code in the current step.
|
|
129
|
+
#
|
|
130
|
+
# @raise [StopExecution] always raises to halt execution
|
|
131
|
+
# @return [void]
|
|
132
|
+
def stop_immediately!
|
|
133
|
+
@stopped = true
|
|
134
|
+
raise Operandi::StopExecution
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Add an error to the :base key.
|
|
138
|
+
#
|
|
139
|
+
# @param message [String] the error message
|
|
140
|
+
# @return [void]
|
|
141
|
+
def fail!(message)
|
|
142
|
+
errors.add(:base, message)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Add an error and stop execution immediately, causing transaction rollback.
|
|
146
|
+
# Steps marked with `always: true` will still run after this method is called.
|
|
147
|
+
#
|
|
148
|
+
# @param message [String] the error message
|
|
149
|
+
# @raise [FailExecution] always raises to halt execution and rollback
|
|
150
|
+
# @return [void]
|
|
151
|
+
def fail_immediately!(message)
|
|
152
|
+
errors.add(:base, message, rollback: false)
|
|
153
|
+
raise Operandi::FailExecution
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Execute the service steps.
|
|
157
|
+
#
|
|
158
|
+
# @return [void]
|
|
159
|
+
# @raise [StandardError] re-raises any exception after running always steps
|
|
160
|
+
def call
|
|
161
|
+
load_defaults_and_validate
|
|
162
|
+
|
|
163
|
+
run_callbacks(:before_service_run, self)
|
|
164
|
+
|
|
165
|
+
run_callbacks(:around_service_run, self) do
|
|
166
|
+
execute_service
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
run_service_result_callbacks
|
|
170
|
+
rescue StandardError => e
|
|
171
|
+
run_steps_with_always
|
|
172
|
+
raise e
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
class << self
|
|
176
|
+
# @return [Hash, nil] class-level configuration options
|
|
177
|
+
attr_accessor :class_config
|
|
178
|
+
|
|
179
|
+
# Set class-level configuration for this service.
|
|
180
|
+
#
|
|
181
|
+
# @param config [Hash] configuration options
|
|
182
|
+
# @return [Hash] the configuration hash
|
|
183
|
+
def config(config = {})
|
|
184
|
+
self.class_config = config
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Run the service and return the result.
|
|
188
|
+
#
|
|
189
|
+
# @param kwargs [Hash] keyword arguments matching service arguments
|
|
190
|
+
# @return [Base] the executed service instance
|
|
191
|
+
#
|
|
192
|
+
# @example
|
|
193
|
+
# result = MyService.run(name: "test")
|
|
194
|
+
# result.success? # => true
|
|
195
|
+
def run(**kwargs)
|
|
196
|
+
new(kwargs).tap(&:call)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Run the service and raise an error if it fails.
|
|
200
|
+
#
|
|
201
|
+
# @param kwargs [Hash] keyword arguments matching service arguments
|
|
202
|
+
# @return [Base] the executed service instance
|
|
203
|
+
# @raise [RuntimeError] if the service fails
|
|
204
|
+
#
|
|
205
|
+
# @example
|
|
206
|
+
# MyService.run!(name: "test") # raises if service fails
|
|
207
|
+
def run!(**kwargs)
|
|
208
|
+
new(kwargs, { raise_on_error: true }).tap(&:call)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Create a context for running the service with a parent service or config.
|
|
212
|
+
#
|
|
213
|
+
# @param service_or_config [Base, Hash] parent service or configuration hash
|
|
214
|
+
# @param config [Hash] configuration hash (when first param is a service)
|
|
215
|
+
# @return [BaseWithContext] context wrapper for running the service
|
|
216
|
+
#
|
|
217
|
+
# @example With parent service
|
|
218
|
+
# ChildService.with(self).run(data: value)
|
|
219
|
+
#
|
|
220
|
+
# @example With configuration
|
|
221
|
+
# MyService.with(use_transactions: false).run(name: "test")
|
|
222
|
+
def with(service_or_config, config = {})
|
|
223
|
+
service = service_or_config.is_a?(Hash) ? nil : service_or_config
|
|
224
|
+
config = service_or_config unless service
|
|
225
|
+
|
|
226
|
+
BaseWithContext.new(self, service, config.dup)
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Operandi
|
|
4
|
+
# Wrapper for running a service with a parent context or custom configuration.
|
|
5
|
+
# Created via {Base.with} method.
|
|
6
|
+
#
|
|
7
|
+
# @example Running with parent service context
|
|
8
|
+
# ChildService.with(self).run(data: value)
|
|
9
|
+
#
|
|
10
|
+
# @example Running with custom configuration
|
|
11
|
+
# MyService.with(use_transactions: false).run(name: "test")
|
|
12
|
+
class BaseWithContext
|
|
13
|
+
# Initialize a new context wrapper.
|
|
14
|
+
#
|
|
15
|
+
# @param service_class [Class] the service class to run
|
|
16
|
+
# @param parent_service [Base, nil] parent service for error/warning propagation
|
|
17
|
+
# @param config [Hash] configuration overrides
|
|
18
|
+
# @raise [ArgTypeError] if parent_service is not a Base subclass
|
|
19
|
+
def initialize(service_class, parent_service, config)
|
|
20
|
+
@service_class = service_class
|
|
21
|
+
@config = config
|
|
22
|
+
@parent_service = parent_service
|
|
23
|
+
|
|
24
|
+
return if parent_service.nil? || parent_service.is_a?(Operandi::Base)
|
|
25
|
+
|
|
26
|
+
message = "#{parent_service.class} - must be a subclass of Operandi::Base"
|
|
27
|
+
raise Operandi::ArgTypeError.new(message, service_class: @service_class)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Run the service with the configured context.
|
|
31
|
+
#
|
|
32
|
+
# @param kwargs [Hash] keyword arguments matching service arguments
|
|
33
|
+
# @return [Base] the executed service instance
|
|
34
|
+
def run(**kwargs)
|
|
35
|
+
@service_class.new(extend_arguments(kwargs), @config, @parent_service).tap(&:call)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Run the service and raise an error if it fails.
|
|
39
|
+
#
|
|
40
|
+
# @param kwargs [Hash] keyword arguments matching service arguments
|
|
41
|
+
# @return [Base] the executed service instance
|
|
42
|
+
# @raise [RuntimeError] if the service fails
|
|
43
|
+
def run!(**kwargs)
|
|
44
|
+
@config[:raise_on_error] = true
|
|
45
|
+
run(**kwargs)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def extend_arguments(args)
|
|
51
|
+
args = @parent_service.arg.dup.extend_with_context(args) if @parent_service
|
|
52
|
+
args[:deepness] += 1 if args[:deepness]
|
|
53
|
+
|
|
54
|
+
args
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|