light-services 2.2 → 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 +5 -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 +24 -114
- data/lib/light/services/base_with_context.rb +2 -3
- 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
|
@@ -13,7 +13,6 @@ module Light
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
class Config
|
|
16
|
-
# Constants
|
|
17
16
|
DEFAULTS = {
|
|
18
17
|
use_transactions: true,
|
|
19
18
|
|
|
@@ -25,34 +24,21 @@ module Light
|
|
|
25
24
|
load_warnings: true,
|
|
26
25
|
break_on_warning: false,
|
|
27
26
|
raise_on_warning: false,
|
|
28
|
-
rollback_on_warning: false
|
|
27
|
+
rollback_on_warning: false,
|
|
29
28
|
}.freeze
|
|
30
29
|
|
|
31
|
-
|
|
32
|
-
attr_accessor :use_transactions,
|
|
33
|
-
:load_errors, :break_on_error, :raise_on_error, :rollback_on_error,
|
|
34
|
-
:load_warnings, :break_on_warning, :raise_on_warning, :rollback_on_warning
|
|
30
|
+
attr_accessor(*DEFAULTS.keys)
|
|
35
31
|
|
|
36
32
|
def initialize
|
|
37
33
|
reset_to_defaults!
|
|
38
34
|
end
|
|
39
35
|
|
|
40
|
-
def set(key, value)
|
|
41
|
-
instance_variable_set(:"@#{key}", value)
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def get(key)
|
|
45
|
-
instance_variable_get(:"@#{key}")
|
|
46
|
-
end
|
|
47
|
-
|
|
48
36
|
def reset_to_defaults!
|
|
49
|
-
DEFAULTS.each
|
|
50
|
-
set(key, value)
|
|
51
|
-
end
|
|
37
|
+
DEFAULTS.each { |key, value| public_send(:"#{key}=", value) }
|
|
52
38
|
end
|
|
53
39
|
|
|
54
40
|
def to_h
|
|
55
|
-
DEFAULTS.keys.to_h { |key| [key,
|
|
41
|
+
DEFAULTS.keys.to_h { |key| [key, public_send(key)] }
|
|
56
42
|
end
|
|
57
43
|
|
|
58
44
|
def merge(config)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Light
|
|
4
|
+
module Services
|
|
5
|
+
# Collection type constants
|
|
6
|
+
module CollectionTypes
|
|
7
|
+
ARGUMENTS = :arguments
|
|
8
|
+
OUTPUTS = :outputs
|
|
9
|
+
|
|
10
|
+
ALL = [ARGUMENTS, OUTPUTS].freeze
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Field type constants
|
|
14
|
+
module FieldTypes
|
|
15
|
+
ARGUMENT = :argument
|
|
16
|
+
OUTPUT = :output
|
|
17
|
+
|
|
18
|
+
ALL = [ARGUMENT, OUTPUT].freeze
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Reserved names that cannot be used for arguments, outputs, or steps
|
|
22
|
+
# These names would conflict with existing gem methods
|
|
23
|
+
module ReservedNames
|
|
24
|
+
# Instance methods from Base class and concerns
|
|
25
|
+
BASE_METHODS = [
|
|
26
|
+
:outputs,
|
|
27
|
+
:arguments,
|
|
28
|
+
:errors,
|
|
29
|
+
:warnings,
|
|
30
|
+
:success?,
|
|
31
|
+
:failed?,
|
|
32
|
+
:errors?,
|
|
33
|
+
:warnings?,
|
|
34
|
+
:done!,
|
|
35
|
+
:done?,
|
|
36
|
+
:call,
|
|
37
|
+
:run_callbacks,
|
|
38
|
+
].freeze
|
|
39
|
+
|
|
40
|
+
# Class methods that could conflict
|
|
41
|
+
CLASS_METHODS = [
|
|
42
|
+
:config,
|
|
43
|
+
:run,
|
|
44
|
+
:run!,
|
|
45
|
+
:with,
|
|
46
|
+
:arg,
|
|
47
|
+
:remove_arg,
|
|
48
|
+
:output,
|
|
49
|
+
:remove_output,
|
|
50
|
+
:step,
|
|
51
|
+
:remove_step,
|
|
52
|
+
:steps,
|
|
53
|
+
:outputs,
|
|
54
|
+
:arguments,
|
|
55
|
+
].freeze
|
|
56
|
+
|
|
57
|
+
# Callback method names
|
|
58
|
+
CALLBACK_METHODS = [
|
|
59
|
+
:before_step_run,
|
|
60
|
+
:after_step_run,
|
|
61
|
+
:around_step_run,
|
|
62
|
+
:on_step_success,
|
|
63
|
+
:on_step_failure,
|
|
64
|
+
:on_step_crash,
|
|
65
|
+
:before_service_run,
|
|
66
|
+
:after_service_run,
|
|
67
|
+
:around_service_run,
|
|
68
|
+
:on_service_success,
|
|
69
|
+
:on_service_failure,
|
|
70
|
+
].freeze
|
|
71
|
+
|
|
72
|
+
# Ruby reserved words and common Object methods
|
|
73
|
+
RUBY_RESERVED = [
|
|
74
|
+
:initialize,
|
|
75
|
+
:class,
|
|
76
|
+
:object_id,
|
|
77
|
+
:send,
|
|
78
|
+
:__send__,
|
|
79
|
+
:public_send,
|
|
80
|
+
:respond_to?,
|
|
81
|
+
:method,
|
|
82
|
+
:methods,
|
|
83
|
+
:instance_variable_get,
|
|
84
|
+
:instance_variable_set,
|
|
85
|
+
:instance_variables,
|
|
86
|
+
:extend,
|
|
87
|
+
:include,
|
|
88
|
+
:new,
|
|
89
|
+
:allocate,
|
|
90
|
+
:superclass,
|
|
91
|
+
].freeze
|
|
92
|
+
|
|
93
|
+
# All reserved names combined (used for validation)
|
|
94
|
+
ALL = (BASE_METHODS + CALLBACK_METHODS + RUBY_RESERVED).uniq.freeze
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../constants"
|
|
4
|
+
require_relative "validation"
|
|
5
|
+
|
|
6
|
+
module Light
|
|
7
|
+
module Services
|
|
8
|
+
module Dsl
|
|
9
|
+
# DSL for defining and managing service arguments
|
|
10
|
+
module ArgumentsDsl
|
|
11
|
+
def self.included(base)
|
|
12
|
+
base.extend(ClassMethods)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
module ClassMethods
|
|
16
|
+
# Define an argument for the service
|
|
17
|
+
#
|
|
18
|
+
# @param name [Symbol] the argument name
|
|
19
|
+
# @param opts [Hash] options for configuring the argument
|
|
20
|
+
# @option opts [Class, Array<Class>] :type Type(s) to validate against
|
|
21
|
+
# (e.g., String, Integer, [String, Symbol])
|
|
22
|
+
# @option opts [Boolean] :optional (false) Whether nil values are allowed
|
|
23
|
+
# @option opts [Object, Proc] :default Default value or proc to evaluate in instance context
|
|
24
|
+
# @option opts [Boolean] :context (false) Whether to pass this argument to child services
|
|
25
|
+
#
|
|
26
|
+
# @example Define a required string argument
|
|
27
|
+
# arg :name, type: String
|
|
28
|
+
#
|
|
29
|
+
# @example Define an optional argument with default
|
|
30
|
+
# arg :age, type: Integer, optional: true, default: 25
|
|
31
|
+
#
|
|
32
|
+
# @example Define an argument with multiple allowed types
|
|
33
|
+
# arg :id, type: [String, Integer]
|
|
34
|
+
#
|
|
35
|
+
# @example Define an argument with proc default
|
|
36
|
+
# arg :timestamp, type: Time, default: -> { Time.now }
|
|
37
|
+
#
|
|
38
|
+
# @example Define a context argument passed to child services
|
|
39
|
+
# arg :current_user, type: User, context: true
|
|
40
|
+
def arg(name, opts = {})
|
|
41
|
+
Validation.validate_symbol_name!(name, :argument, self)
|
|
42
|
+
Validation.validate_reserved_name!(name, :argument, self)
|
|
43
|
+
Validation.validate_name_conflicts!(name, :argument, self)
|
|
44
|
+
|
|
45
|
+
own_arguments[name] = Settings::Field.new(name, self, opts.merge(field_type: FieldTypes::ARGUMENT))
|
|
46
|
+
@arguments = nil # Clear memoized arguments since we're modifying them
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Remove an argument from the service
|
|
50
|
+
#
|
|
51
|
+
# @param name [Symbol] the argument name to remove
|
|
52
|
+
def remove_arg(name)
|
|
53
|
+
own_arguments.delete(name)
|
|
54
|
+
@arguments = nil # Clear memoized arguments since we're modifying them
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Get all arguments including inherited ones
|
|
58
|
+
#
|
|
59
|
+
# @return [Hash] all arguments defined for this service
|
|
60
|
+
def arguments
|
|
61
|
+
@arguments ||= build_arguments
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Get only arguments defined in this class
|
|
65
|
+
#
|
|
66
|
+
# @return [Hash] arguments defined in this class only
|
|
67
|
+
def own_arguments
|
|
68
|
+
@own_arguments ||= {}
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
# Build arguments by merging inherited arguments with own arguments
|
|
74
|
+
#
|
|
75
|
+
# @return [Hash] merged arguments
|
|
76
|
+
def build_arguments
|
|
77
|
+
inherited = superclass.respond_to?(:arguments) ? superclass.arguments.dup : {}
|
|
78
|
+
inherited.merge(own_arguments)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../constants"
|
|
4
|
+
require_relative "validation"
|
|
5
|
+
|
|
6
|
+
module Light
|
|
7
|
+
module Services
|
|
8
|
+
module Dsl
|
|
9
|
+
# DSL for defining and managing service outputs
|
|
10
|
+
module OutputsDsl
|
|
11
|
+
def self.included(base)
|
|
12
|
+
base.extend(ClassMethods)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
module ClassMethods
|
|
16
|
+
# Define an output for the service
|
|
17
|
+
#
|
|
18
|
+
# @param name [Symbol] the output name
|
|
19
|
+
# @param opts [Hash] options for configuring the output
|
|
20
|
+
# @option opts [Class, Array<Class>] :type Type(s) to validate against
|
|
21
|
+
# (e.g., Hash, String, [String, Symbol])
|
|
22
|
+
# @option opts [Boolean] :optional (false) Whether nil values are allowed
|
|
23
|
+
# @option opts [Object, Proc] :default Default value or proc to evaluate in instance context
|
|
24
|
+
#
|
|
25
|
+
# @example Define a required hash output
|
|
26
|
+
# output :result, type: Hash
|
|
27
|
+
#
|
|
28
|
+
# @example Define an optional output with default
|
|
29
|
+
# output :status, type: String, optional: true, default: "pending"
|
|
30
|
+
#
|
|
31
|
+
# @example Define an output with multiple allowed types
|
|
32
|
+
# output :data, type: [Hash, Array]
|
|
33
|
+
#
|
|
34
|
+
# @example Define an output with proc default
|
|
35
|
+
# output :metadata, type: Hash, default: -> { {} }
|
|
36
|
+
def output(name, opts = {})
|
|
37
|
+
Validation.validate_symbol_name!(name, :output, self)
|
|
38
|
+
Validation.validate_reserved_name!(name, :output, self)
|
|
39
|
+
Validation.validate_name_conflicts!(name, :output, self)
|
|
40
|
+
|
|
41
|
+
own_outputs[name] = Settings::Field.new(name, self, opts.merge(field_type: FieldTypes::OUTPUT))
|
|
42
|
+
@outputs = nil # Clear memoized outputs since we're modifying them
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Remove an output from the service
|
|
46
|
+
#
|
|
47
|
+
# @param name [Symbol] the output name to remove
|
|
48
|
+
def remove_output(name)
|
|
49
|
+
own_outputs.delete(name)
|
|
50
|
+
@outputs = nil # Clear memoized outputs since we're modifying them
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Get all outputs including inherited ones
|
|
54
|
+
#
|
|
55
|
+
# @return [Hash] all outputs defined for this service
|
|
56
|
+
def outputs
|
|
57
|
+
@outputs ||= build_outputs
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Get only outputs defined in this class
|
|
61
|
+
#
|
|
62
|
+
# @return [Hash] outputs defined in this class only
|
|
63
|
+
def own_outputs
|
|
64
|
+
@own_outputs ||= {}
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
# Build outputs by merging inherited outputs with own outputs
|
|
70
|
+
#
|
|
71
|
+
# @return [Hash] merged outputs
|
|
72
|
+
def build_outputs
|
|
73
|
+
inherited = superclass.respond_to?(:outputs) ? superclass.outputs.dup : {}
|
|
74
|
+
inherited.merge(own_outputs)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../constants"
|
|
4
|
+
require_relative "validation"
|
|
5
|
+
|
|
6
|
+
module Light
|
|
7
|
+
module Services
|
|
8
|
+
module Dsl
|
|
9
|
+
# DSL for defining and managing service steps
|
|
10
|
+
module StepsDsl
|
|
11
|
+
def self.included(base)
|
|
12
|
+
base.extend(ClassMethods)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
module ClassMethods
|
|
16
|
+
# Define a step for the service
|
|
17
|
+
#
|
|
18
|
+
# @param name [Symbol] the step name (must correspond to a private method)
|
|
19
|
+
# @param opts [Hash] options for configuring the step
|
|
20
|
+
# @option opts [Symbol, Proc] :if Condition to determine if step should run
|
|
21
|
+
# @option opts [Symbol, Proc] :unless Condition to skip step (returns truthy to skip)
|
|
22
|
+
# @option opts [Boolean] :always (false) Run step even after errors/warnings
|
|
23
|
+
# @option opts [Symbol] :before Insert this step before the specified step
|
|
24
|
+
# @option opts [Symbol] :after Insert this step after the specified step
|
|
25
|
+
#
|
|
26
|
+
# @example Define a simple step
|
|
27
|
+
# step :validate_input
|
|
28
|
+
#
|
|
29
|
+
# @example Define a conditional step
|
|
30
|
+
# step :send_notification, if: :should_notify?
|
|
31
|
+
# step :skip_validation, unless: :production?
|
|
32
|
+
#
|
|
33
|
+
# @example Define a step that always runs
|
|
34
|
+
# step :cleanup, always: true
|
|
35
|
+
#
|
|
36
|
+
# @example Define step ordering
|
|
37
|
+
# step :log_start, before: :validate_input
|
|
38
|
+
# step :log_end, after: :process_data
|
|
39
|
+
#
|
|
40
|
+
# @example Define a step with proc condition
|
|
41
|
+
# step :premium_feature, if: -> { user.premium? && feature_enabled? }
|
|
42
|
+
def step(name, opts = {}) # rubocop:disable Metrics/MethodLength
|
|
43
|
+
Validation.validate_symbol_name!(name, :step, self)
|
|
44
|
+
Validation.validate_reserved_name!(name, :step, self)
|
|
45
|
+
Validation.validate_name_conflicts!(name, :step, self)
|
|
46
|
+
validate_step_opts!(name, opts)
|
|
47
|
+
|
|
48
|
+
# Build current steps to check for duplicates and find insertion targets
|
|
49
|
+
current = steps
|
|
50
|
+
if current.key?(name)
|
|
51
|
+
raise Light::Services::Error,
|
|
52
|
+
"Step `#{name}` is already defined in service #{self}. Each step must have a unique name."
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
if (target = opts[:before] || opts[:after]) && !current.key?(target)
|
|
56
|
+
available = current.keys.join(", ")
|
|
57
|
+
raise Light::Services::Error,
|
|
58
|
+
"Cannot find target step `#{target}` in service #{self}. Available steps: [#{available}]"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
step_obj = Settings::Step.new(name, self, opts)
|
|
62
|
+
|
|
63
|
+
if opts[:before] || opts[:after]
|
|
64
|
+
step_operations << { action: :insert, name: name, step: step_obj, before: opts[:before],
|
|
65
|
+
after: opts[:after], }
|
|
66
|
+
else
|
|
67
|
+
step_operations << { action: :add, name: name, step: step_obj }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Clear memoized steps since we're modifying them
|
|
71
|
+
@steps = nil
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Remove a step from the service
|
|
75
|
+
#
|
|
76
|
+
# @param name [Symbol] the step name to remove
|
|
77
|
+
def remove_step(name)
|
|
78
|
+
step_operations << { action: :remove, name: name }
|
|
79
|
+
|
|
80
|
+
# Clear memoized steps since we're modifying them
|
|
81
|
+
@steps = nil
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Get all steps including inherited ones
|
|
85
|
+
#
|
|
86
|
+
# @return [Hash] all steps defined for this service
|
|
87
|
+
def steps
|
|
88
|
+
@steps ||= build_steps
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Get the list of step operations to be applied
|
|
92
|
+
#
|
|
93
|
+
# @return [Array] list of operations
|
|
94
|
+
def step_operations
|
|
95
|
+
@step_operations ||= []
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Validate that the service has steps defined
|
|
99
|
+
# Called before executing the service
|
|
100
|
+
#
|
|
101
|
+
# @raise [NoStepsError] if no steps are defined
|
|
102
|
+
def validate_steps!
|
|
103
|
+
return unless steps.empty?
|
|
104
|
+
|
|
105
|
+
raise Light::Services::NoStepsError,
|
|
106
|
+
"Service #{self} has no steps defined. Define at least one step or implement a `run` method."
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
# Validate step options to ensure they are valid
|
|
112
|
+
#
|
|
113
|
+
# @param name [Symbol] the step name
|
|
114
|
+
# @param opts [Hash] the step options
|
|
115
|
+
def validate_step_opts!(name, opts)
|
|
116
|
+
return unless opts[:before] && opts[:after]
|
|
117
|
+
|
|
118
|
+
raise Light::Services::Error, "You cannot specify `before` and `after` " \
|
|
119
|
+
"for step `#{name}` in service #{self} at the same time"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Build steps by applying operations to inherited steps
|
|
123
|
+
#
|
|
124
|
+
# @return [Hash] the final steps hash
|
|
125
|
+
def build_steps
|
|
126
|
+
# Start with inherited steps
|
|
127
|
+
result = inherit_steps
|
|
128
|
+
|
|
129
|
+
# Apply operations in order
|
|
130
|
+
step_operations.each { |op| apply_step_operation(result, op) }
|
|
131
|
+
|
|
132
|
+
# If no steps defined, check for `run` method as fallback
|
|
133
|
+
result[:run] = Settings::Step.new(:run, self, {}) if result.empty? && instance_method_defined?(:run)
|
|
134
|
+
|
|
135
|
+
result
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Check if an instance method is defined in this class or its ancestors
|
|
139
|
+
# (excluding Light::Services::Base and its modules)
|
|
140
|
+
#
|
|
141
|
+
# @param method_name [Symbol] the method name to check
|
|
142
|
+
# @return [Boolean] true if the method is defined
|
|
143
|
+
def instance_method_defined?(method_name)
|
|
144
|
+
# Check if method exists and is not from base service classes
|
|
145
|
+
return false unless method_defined?(method_name) || private_method_defined?(method_name)
|
|
146
|
+
|
|
147
|
+
# Get the method owner to ensure it's defined in user's service class
|
|
148
|
+
owner = instance_method(method_name).owner
|
|
149
|
+
|
|
150
|
+
# Method should be defined in a class that inherits from Base,
|
|
151
|
+
# not in Base itself or its included modules
|
|
152
|
+
!owner.to_s.start_with?("Light::Services")
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Inherit steps from parent class
|
|
156
|
+
#
|
|
157
|
+
# @return [Hash] inherited steps
|
|
158
|
+
def inherit_steps
|
|
159
|
+
superclass.respond_to?(:steps) ? superclass.steps.dup : {}
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Apply a single step operation to the steps hash
|
|
163
|
+
#
|
|
164
|
+
# @param steps [Hash] the steps hash
|
|
165
|
+
# @param operation [Hash] the operation to apply
|
|
166
|
+
def apply_step_operation(steps, operation)
|
|
167
|
+
case operation[:action]
|
|
168
|
+
when :add
|
|
169
|
+
steps[operation[:name]] = operation[:step]
|
|
170
|
+
when :remove
|
|
171
|
+
steps.delete(operation[:name])
|
|
172
|
+
when :insert
|
|
173
|
+
insert_step(steps, operation)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Insert a step before or after a target step
|
|
178
|
+
#
|
|
179
|
+
# @param steps [Hash] the steps hash
|
|
180
|
+
# @param operation [Hash] the insert operation details
|
|
181
|
+
def insert_step(steps, operation)
|
|
182
|
+
target = operation[:before] || operation[:after]
|
|
183
|
+
keys = steps.keys
|
|
184
|
+
index = keys.index(target)
|
|
185
|
+
return unless index
|
|
186
|
+
|
|
187
|
+
# More efficient insertion using ordered hash reconstruction
|
|
188
|
+
new_steps = {}
|
|
189
|
+
|
|
190
|
+
keys.each_with_index do |key, i|
|
|
191
|
+
# Insert before target
|
|
192
|
+
new_steps[operation[:name]] = operation[:step] if operation[:before] && i == index
|
|
193
|
+
new_steps[key] = steps[key]
|
|
194
|
+
|
|
195
|
+
# Insert after target
|
|
196
|
+
new_steps[operation[:name]] = operation[:step] if operation[:after] && i == index
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
steps.replace(new_steps)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../constants"
|
|
4
|
+
|
|
5
|
+
module Light
|
|
6
|
+
module Services
|
|
7
|
+
module Dsl
|
|
8
|
+
# Shared validation logic for DSL modules
|
|
9
|
+
module Validation
|
|
10
|
+
# Validate that the name is a symbol
|
|
11
|
+
#
|
|
12
|
+
# @param name [Object] the name to validate
|
|
13
|
+
# @param field_type [Symbol] the type of field (:argument, :output, :step)
|
|
14
|
+
# @param service_class [Class] the service class for error messages
|
|
15
|
+
def self.validate_symbol_name!(name, field_type, service_class)
|
|
16
|
+
return if name.is_a?(Symbol)
|
|
17
|
+
|
|
18
|
+
raise Light::Services::InvalidNameError,
|
|
19
|
+
"#{field_type.to_s.capitalize} name must be a Symbol, " \
|
|
20
|
+
"got #{name.class} (#{name.inspect}) in #{service_class}"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Validate that the name is not a reserved word
|
|
24
|
+
#
|
|
25
|
+
# @param name [Symbol] the name to validate
|
|
26
|
+
# @param field_type [Symbol] the type of field (:argument, :output, :step)
|
|
27
|
+
# @param service_class [Class] the service class for error messages
|
|
28
|
+
def self.validate_reserved_name!(name, field_type, service_class)
|
|
29
|
+
return unless ReservedNames::ALL.include?(name.to_sym)
|
|
30
|
+
|
|
31
|
+
raise Light::Services::ReservedNameError,
|
|
32
|
+
"Cannot use `#{name}` as #{field_type} name in #{service_class} - " \
|
|
33
|
+
"it is a reserved word that conflicts with gem methods"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Validate that the name doesn't conflict with other defined names
|
|
37
|
+
#
|
|
38
|
+
# @param name [Symbol] the name to validate
|
|
39
|
+
# @param field_type [Symbol] the type of field being defined (:argument, :output, :step)
|
|
40
|
+
# @param service_class [Class] the service class to check for conflicts
|
|
41
|
+
def self.validate_name_conflicts!(name, field_type, service_class)
|
|
42
|
+
name_sym = name.to_sym
|
|
43
|
+
|
|
44
|
+
case field_type
|
|
45
|
+
when :argument
|
|
46
|
+
validate_argument_conflicts!(name_sym, service_class)
|
|
47
|
+
when :output
|
|
48
|
+
validate_output_conflicts!(name_sym, service_class)
|
|
49
|
+
when :step
|
|
50
|
+
validate_step_conflicts!(name_sym, service_class)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Validate argument name doesn't conflict with outputs or steps
|
|
55
|
+
def self.validate_argument_conflicts!(name_sym, service_class)
|
|
56
|
+
# Check against existing outputs
|
|
57
|
+
if has_output?(name_sym, service_class)
|
|
58
|
+
raise Light::Services::ReservedNameError,
|
|
59
|
+
"Cannot use `#{name_sym}` as argument name in #{service_class} - " \
|
|
60
|
+
"it is already defined as an output"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Check against existing steps
|
|
64
|
+
if has_step?(name_sym, service_class)
|
|
65
|
+
raise Light::Services::ReservedNameError,
|
|
66
|
+
"Cannot use `#{name_sym}` as argument name in #{service_class} - " \
|
|
67
|
+
"it is already defined as a step"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Validate output name doesn't conflict with arguments or steps
|
|
72
|
+
def self.validate_output_conflicts!(name_sym, service_class)
|
|
73
|
+
# Check against existing arguments
|
|
74
|
+
if has_argument?(name_sym, service_class)
|
|
75
|
+
raise Light::Services::ReservedNameError,
|
|
76
|
+
"Cannot use `#{name_sym}` as output name in #{service_class} - " \
|
|
77
|
+
"it is already defined as an argument"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Check against existing steps
|
|
81
|
+
if has_step?(name_sym, service_class)
|
|
82
|
+
raise Light::Services::ReservedNameError,
|
|
83
|
+
"Cannot use `#{name_sym}` as output name in #{service_class} - " \
|
|
84
|
+
"it is already defined as a step"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Validate step name doesn't conflict with arguments or outputs
|
|
89
|
+
def self.validate_step_conflicts!(name_sym, service_class)
|
|
90
|
+
# Check against existing arguments
|
|
91
|
+
if has_argument?(name_sym, service_class)
|
|
92
|
+
raise Light::Services::ReservedNameError,
|
|
93
|
+
"Cannot use `#{name_sym}` as step name in #{service_class} - " \
|
|
94
|
+
"it is already defined as an argument"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Check against existing outputs
|
|
98
|
+
if has_output?(name_sym, service_class)
|
|
99
|
+
raise Light::Services::ReservedNameError,
|
|
100
|
+
"Cannot use `#{name_sym}` as step name in #{service_class} - " \
|
|
101
|
+
"it is already defined as an output"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Check if a name is already defined as an argument
|
|
106
|
+
def self.has_argument?(name_sym, service_class)
|
|
107
|
+
# Check own_arguments (current class)
|
|
108
|
+
(service_class.respond_to?(:own_arguments) && service_class.own_arguments.key?(name_sym)) ||
|
|
109
|
+
# Check inherited arguments
|
|
110
|
+
(service_class.superclass.respond_to?(:arguments) && service_class.superclass.arguments.key?(name_sym))
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Check if a name is already defined as an output
|
|
114
|
+
def self.has_output?(name_sym, service_class)
|
|
115
|
+
# Check own_outputs (current class)
|
|
116
|
+
(service_class.respond_to?(:own_outputs) && service_class.own_outputs.key?(name_sym)) ||
|
|
117
|
+
# Check inherited outputs
|
|
118
|
+
(service_class.superclass.respond_to?(:outputs) && service_class.superclass.outputs.key?(name_sym))
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Check if a name is already defined as a step
|
|
122
|
+
def self.has_step?(name_sym, service_class)
|
|
123
|
+
# Check step_operations (current class) for non-removed steps
|
|
124
|
+
(service_class.respond_to?(:step_operations) &&
|
|
125
|
+
service_class.step_operations.any? { |op| op[:name] == name_sym && op[:action] != :remove }) ||
|
|
126
|
+
# Check inherited steps
|
|
127
|
+
(service_class.superclass.respond_to?(:steps) && service_class.superclass.steps.key?(name_sym))
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -4,7 +4,12 @@ module Light
|
|
|
4
4
|
module Services
|
|
5
5
|
class Error < StandardError; end
|
|
6
6
|
class ArgTypeError < Error; end
|
|
7
|
-
class
|
|
8
|
-
class
|
|
7
|
+
class ReservedNameError < Error; end
|
|
8
|
+
class InvalidNameError < Error; end
|
|
9
|
+
class NoStepsError < Error; end
|
|
10
|
+
|
|
11
|
+
# Backwards compatibility aliases (deprecated)
|
|
12
|
+
NoStepError = Error
|
|
13
|
+
TwoConditions = Error
|
|
9
14
|
end
|
|
10
15
|
end
|