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.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/.github/config/rubocop_linter_action.yml +4 -4
  3. data/.github/workflows/ci.yml +12 -12
  4. data/.gitignore +1 -0
  5. data/.rubocop.yml +77 -7
  6. data/CHANGELOG.md +23 -0
  7. data/CLAUDE.md +139 -0
  8. data/Gemfile +16 -11
  9. data/Gemfile.lock +53 -27
  10. data/README.md +76 -13
  11. data/docs/arguments.md +267 -0
  12. data/docs/best-practices.md +153 -0
  13. data/docs/callbacks.md +476 -0
  14. data/docs/concepts.md +80 -0
  15. data/docs/configuration.md +168 -0
  16. data/docs/context.md +128 -0
  17. data/docs/crud.md +525 -0
  18. data/docs/errors.md +250 -0
  19. data/docs/generators.md +250 -0
  20. data/docs/outputs.md +135 -0
  21. data/docs/pundit-authorization.md +320 -0
  22. data/docs/quickstart.md +134 -0
  23. data/docs/readme.md +100 -0
  24. data/docs/recipes.md +14 -0
  25. data/docs/service-rendering.md +222 -0
  26. data/docs/steps.md +337 -0
  27. data/docs/summary.md +19 -0
  28. data/docs/testing.md +549 -0
  29. data/lib/generators/light_services/install/USAGE +15 -0
  30. data/lib/generators/light_services/install/install_generator.rb +41 -0
  31. data/lib/generators/light_services/install/templates/application_service.rb.tt +8 -0
  32. data/lib/generators/light_services/install/templates/application_service_spec.rb.tt +7 -0
  33. data/lib/generators/light_services/install/templates/initializer.rb.tt +30 -0
  34. data/lib/generators/light_services/service/USAGE +21 -0
  35. data/lib/generators/light_services/service/service_generator.rb +68 -0
  36. data/lib/generators/light_services/service/templates/service.rb.tt +48 -0
  37. data/lib/generators/light_services/service/templates/service_spec.rb.tt +40 -0
  38. data/lib/light/services/base.rb +23 -113
  39. data/lib/light/services/callbacks.rb +103 -0
  40. data/lib/light/services/collection.rb +97 -0
  41. data/lib/light/services/concerns/execution.rb +76 -0
  42. data/lib/light/services/concerns/parent_service.rb +34 -0
  43. data/lib/light/services/concerns/state_management.rb +30 -0
  44. data/lib/light/services/config.rb +4 -18
  45. data/lib/light/services/constants.rb +97 -0
  46. data/lib/light/services/dsl/arguments_dsl.rb +84 -0
  47. data/lib/light/services/dsl/outputs_dsl.rb +80 -0
  48. data/lib/light/services/dsl/steps_dsl.rb +205 -0
  49. data/lib/light/services/dsl/validation.rb +132 -0
  50. data/lib/light/services/exceptions.rb +7 -2
  51. data/lib/light/services/messages.rb +19 -31
  52. data/lib/light/services/rspec/matchers/define_argument.rb +174 -0
  53. data/lib/light/services/rspec/matchers/define_output.rb +147 -0
  54. data/lib/light/services/rspec/matchers/define_step.rb +225 -0
  55. data/lib/light/services/rspec/matchers/execute_step.rb +230 -0
  56. data/lib/light/services/rspec/matchers/have_error_on.rb +148 -0
  57. data/lib/light/services/rspec/matchers/have_warning_on.rb +148 -0
  58. data/lib/light/services/rspec/matchers/trigger_callback.rb +138 -0
  59. data/lib/light/services/rspec.rb +15 -0
  60. data/lib/light/services/settings/field.rb +86 -0
  61. data/lib/light/services/settings/step.rb +31 -16
  62. data/lib/light/services/utils.rb +38 -0
  63. data/lib/light/services/version.rb +1 -1
  64. data/lib/light/services.rb +2 -0
  65. data/light-services.gemspec +6 -8
  66. metadata +54 -26
  67. data/lib/light/services/class_based_collection/base.rb +0 -86
  68. data/lib/light/services/class_based_collection/mount.rb +0 -33
  69. data/lib/light/services/collection/arguments.rb +0 -34
  70. data/lib/light/services/collection/base.rb +0 -59
  71. data/lib/light/services/collection/outputs.rb +0 -16
  72. data/lib/light/services/settings/argument.rb +0 -68
  73. 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
- # Getters / Setters
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 do |key, value|
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, get(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 NoStepError < Error; end
8
- class TwoConditions < Error; end
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