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.
Files changed (89) 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 +83 -7
  6. data/CHANGELOG.md +38 -0
  7. data/CLAUDE.md +139 -0
  8. data/Gemfile +16 -11
  9. data/Gemfile.lock +53 -27
  10. data/README.md +84 -21
  11. data/docs/arguments.md +290 -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 +204 -0
  16. data/docs/context.md +128 -0
  17. data/docs/crud.md +525 -0
  18. data/docs/errors.md +280 -0
  19. data/docs/generators.md +250 -0
  20. data/docs/outputs.md +158 -0
  21. data/docs/pundit-authorization.md +320 -0
  22. data/docs/quickstart.md +134 -0
  23. data/docs/readme.md +101 -0
  24. data/docs/recipes.md +14 -0
  25. data/docs/rubocop.md +285 -0
  26. data/docs/ruby-lsp.md +133 -0
  27. data/docs/service-rendering.md +222 -0
  28. data/docs/steps.md +391 -0
  29. data/docs/summary.md +21 -0
  30. data/docs/testing.md +549 -0
  31. data/lib/generators/light_services/install/USAGE +15 -0
  32. data/lib/generators/light_services/install/install_generator.rb +41 -0
  33. data/lib/generators/light_services/install/templates/application_service.rb.tt +8 -0
  34. data/lib/generators/light_services/install/templates/application_service_spec.rb.tt +7 -0
  35. data/lib/generators/light_services/install/templates/initializer.rb.tt +30 -0
  36. data/lib/generators/light_services/service/USAGE +21 -0
  37. data/lib/generators/light_services/service/service_generator.rb +68 -0
  38. data/lib/generators/light_services/service/templates/service.rb.tt +48 -0
  39. data/lib/generators/light_services/service/templates/service_spec.rb.tt +40 -0
  40. data/lib/light/services/base.rb +134 -122
  41. data/lib/light/services/base_with_context.rb +23 -1
  42. data/lib/light/services/callbacks.rb +157 -0
  43. data/lib/light/services/collection.rb +145 -0
  44. data/lib/light/services/concerns/execution.rb +79 -0
  45. data/lib/light/services/concerns/parent_service.rb +34 -0
  46. data/lib/light/services/concerns/state_management.rb +30 -0
  47. data/lib/light/services/config.rb +82 -16
  48. data/lib/light/services/constants.rb +100 -0
  49. data/lib/light/services/dsl/arguments_dsl.rb +85 -0
  50. data/lib/light/services/dsl/outputs_dsl.rb +81 -0
  51. data/lib/light/services/dsl/steps_dsl.rb +205 -0
  52. data/lib/light/services/dsl/validation.rb +162 -0
  53. data/lib/light/services/exceptions.rb +25 -2
  54. data/lib/light/services/message.rb +28 -3
  55. data/lib/light/services/messages.rb +92 -32
  56. data/lib/light/services/rspec/matchers/define_argument.rb +174 -0
  57. data/lib/light/services/rspec/matchers/define_output.rb +147 -0
  58. data/lib/light/services/rspec/matchers/define_step.rb +225 -0
  59. data/lib/light/services/rspec/matchers/execute_step.rb +230 -0
  60. data/lib/light/services/rspec/matchers/have_error_on.rb +148 -0
  61. data/lib/light/services/rspec/matchers/have_warning_on.rb +148 -0
  62. data/lib/light/services/rspec/matchers/trigger_callback.rb +138 -0
  63. data/lib/light/services/rspec.rb +15 -0
  64. data/lib/light/services/rubocop/cop/light_services/argument_type_required.rb +52 -0
  65. data/lib/light/services/rubocop/cop/light_services/condition_method_exists.rb +173 -0
  66. data/lib/light/services/rubocop/cop/light_services/deprecated_methods.rb +113 -0
  67. data/lib/light/services/rubocop/cop/light_services/dsl_order.rb +176 -0
  68. data/lib/light/services/rubocop/cop/light_services/missing_private_keyword.rb +102 -0
  69. data/lib/light/services/rubocop/cop/light_services/no_direct_instantiation.rb +66 -0
  70. data/lib/light/services/rubocop/cop/light_services/output_type_required.rb +52 -0
  71. data/lib/light/services/rubocop/cop/light_services/step_method_exists.rb +109 -0
  72. data/lib/light/services/rubocop.rb +12 -0
  73. data/lib/light/services/settings/field.rb +114 -0
  74. data/lib/light/services/settings/step.rb +53 -20
  75. data/lib/light/services/utils.rb +38 -0
  76. data/lib/light/services/version.rb +1 -1
  77. data/lib/light/services.rb +2 -0
  78. data/lib/ruby_lsp/light_services/addon.rb +36 -0
  79. data/lib/ruby_lsp/light_services/definition.rb +132 -0
  80. data/lib/ruby_lsp/light_services/indexing_enhancement.rb +263 -0
  81. data/light-services.gemspec +6 -8
  82. metadata +68 -26
  83. data/lib/light/services/class_based_collection/base.rb +0 -86
  84. data/lib/light/services/class_based_collection/mount.rb +0 -33
  85. data/lib/light/services/collection/arguments.rb +0 -34
  86. data/lib/light/services/collection/base.rb +0 -59
  87. data/lib/light/services/collection/outputs.rb +0 -16
  88. data/lib/light/services/settings/argument.rb +0 -68
  89. data/lib/light/services/settings/output.rb +0 -34
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "constants"
4
+
5
+ module Light
6
+ module Services
7
+ # Collection module for storing argument and output values.
8
+ module Collection
9
+ # Storage for service arguments or outputs with type validation.
10
+ #
11
+ # @example Accessing values
12
+ # service.arguments[:name] # => "John"
13
+ # service.outputs[:user] # => #<User id: 1>
14
+ class Base
15
+ extend Forwardable
16
+
17
+ # @!method key?(key)
18
+ # Check if a key exists in the collection.
19
+ # @param key [Symbol] the key to check
20
+ # @return [Boolean] true if key exists
21
+
22
+ # @!method to_h
23
+ # Convert collection to a hash.
24
+ # @return [Hash] the stored values
25
+ def_delegators :@storage, :key?, :to_h
26
+
27
+ # Initialize a new collection.
28
+ #
29
+ # @param instance [Base] the service instance
30
+ # @param collection_type [String] "arguments" or "outputs"
31
+ # @param storage [Hash] initial values
32
+ # @raise [ArgTypeError] if storage is not a Hash
33
+ def initialize(instance, collection_type, storage = {})
34
+ validate_collection_type!(collection_type)
35
+
36
+ @instance = instance
37
+ @collection_type = collection_type
38
+ @storage = storage
39
+
40
+ return if storage.is_a?(Hash)
41
+
42
+ raise Light::Services::ArgTypeError, "#{instance.class} - #{collection_type} must be a Hash"
43
+ end
44
+
45
+ # Set a value in the collection.
46
+ #
47
+ # @param key [Symbol] the key to set
48
+ # @param value [Object] the value to store
49
+ # @return [Object] the stored value
50
+ def set(key, value)
51
+ @storage[key] = value
52
+ end
53
+
54
+ # Get a value from the collection.
55
+ #
56
+ # @param key [Symbol] the key to retrieve
57
+ # @return [Object, nil] the stored value or nil
58
+ def get(key)
59
+ @storage[key]
60
+ end
61
+
62
+ # Get a value using bracket notation.
63
+ #
64
+ # @param key [Symbol] the key to retrieve
65
+ # @return [Object, nil] the stored value or nil
66
+ def [](key)
67
+ get(key)
68
+ end
69
+
70
+ # Set a value using bracket notation.
71
+ #
72
+ # @param key [Symbol] the key to set
73
+ # @param value [Object] the value to store
74
+ # @return [Object] the stored value
75
+ def []=(key, value)
76
+ set(key, value)
77
+ end
78
+
79
+ # Load default values for fields that haven't been set.
80
+ #
81
+ # @return [void]
82
+ def load_defaults
83
+ settings_collection.each do |name, settings|
84
+ next if !settings.default_exists || key?(name)
85
+
86
+ if settings.default.is_a?(Proc)
87
+ set(name, @instance.instance_exec(&settings.default))
88
+ else
89
+ set(name, Utils.deep_dup(settings.default))
90
+ end
91
+ end
92
+ end
93
+
94
+ # Validate all values against their type definitions.
95
+ #
96
+ # @return [void]
97
+ # @raise [ArgTypeError] if a value fails type validation
98
+ def validate!
99
+ settings_collection.each do |name, field|
100
+ next if field.optional && (!key?(name) || get(name).nil?)
101
+
102
+ # validate_type! returns the (possibly coerced) value
103
+ coerced_value = field.validate_type!(get(name))
104
+ # Store the coerced value back (supports dry-types coercion)
105
+ set(name, coerced_value) if coerced_value != get(name)
106
+ end
107
+ end
108
+
109
+ # Extend arguments hash with context values from this collection.
110
+ # Only applies to arguments collections.
111
+ #
112
+ # @param args [Hash] arguments hash to extend
113
+ # @return [Hash] the extended arguments hash
114
+ def extend_with_context(args)
115
+ return args unless @collection_type == CollectionTypes::ARGUMENTS
116
+
117
+ settings_collection.each do |name, field|
118
+ next if !field.context || args.key?(name) || !key?(name)
119
+
120
+ args[field.name] = get(name)
121
+ end
122
+
123
+ args
124
+ end
125
+
126
+ private
127
+
128
+ def validate_collection_type!(type)
129
+ return if CollectionTypes::ALL.include?(type)
130
+
131
+ raise ArgumentError,
132
+ "collection_type must be one of #{CollectionTypes::ALL.join(', ')}, got: #{type.inspect}"
133
+ end
134
+
135
+ def settings_collection
136
+ @instance.class.public_send(@collection_type)
137
+ end
138
+ end
139
+
140
+ # Aliases for backwards compatibility
141
+ Arguments = Base
142
+ Outputs = Base
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Light
4
+ module Services
5
+ module Concerns
6
+ # Handles service execution logic including steps and validation
7
+ module Execution
8
+ private
9
+
10
+ # Execute the main service logic
11
+ def execute_service
12
+ self.class.validate_steps!
13
+ run_steps
14
+ run_steps_with_always
15
+ @outputs.validate! if success?
16
+
17
+ copy_warnings_to_parent_service
18
+ copy_errors_to_parent_service
19
+ end
20
+
21
+ # Run all service result callbacks based on success/failure
22
+ def run_service_result_callbacks
23
+ run_callbacks(:after_service_run, self)
24
+
25
+ if success?
26
+ run_callbacks(:on_service_success, self)
27
+ else
28
+ run_callbacks(:on_service_failure, self)
29
+ end
30
+ end
31
+
32
+ # Run normal steps within transaction
33
+ def run_steps
34
+ within_transaction do
35
+ # Cache steps once for both normal and always execution
36
+ @cached_steps = self.class.steps
37
+
38
+ @cached_steps.each do |name, step|
39
+ @launched_steps << name if step.run(self)
40
+
41
+ break if @errors.break? || @warnings.break?
42
+ end
43
+ rescue Light::Services::StopExecution
44
+ # Gracefully handle stop_immediately! inside transaction to prevent rollback
45
+ @stopped = true
46
+ end
47
+ end
48
+
49
+ # Run steps with parameter `always` if they weren't launched because of errors/warnings
50
+ def run_steps_with_always
51
+ # Use cached steps from run_steps, or get them if run_steps wasn't called
52
+ steps_to_check = @cached_steps || self.class.steps
53
+
54
+ steps_to_check.each do |name, step|
55
+ next if !step.always || @launched_steps.include?(name)
56
+
57
+ @launched_steps << name if step.run(self)
58
+ end
59
+ end
60
+
61
+ # Load defaults for outputs and arguments, then validate arguments
62
+ def load_defaults_and_validate
63
+ @outputs.load_defaults
64
+ @arguments.load_defaults
65
+ @arguments.validate!
66
+ end
67
+
68
+ # Execute block within transaction if configured
69
+ def within_transaction(&block)
70
+ if @config[:use_transactions] && defined?(ActiveRecord::Base)
71
+ ActiveRecord::Base.transaction(requires_new: true, &block)
72
+ else
73
+ yield
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Light
4
+ module Services
5
+ module Concerns
6
+ # Handles copying errors and warnings to parent services
7
+ module ParentService
8
+ private
9
+
10
+ # Copy warnings from this service to parent service
11
+ def copy_warnings_to_parent_service
12
+ return if !@parent_service || !@config[:load_warnings]
13
+
14
+ @parent_service.warnings.copy_from(
15
+ @warnings,
16
+ break: @config[:self_break_on_warning],
17
+ rollback: @config[:self_rollback_on_warning],
18
+ )
19
+ end
20
+
21
+ # Copy errors from this service to parent service
22
+ def copy_errors_to_parent_service
23
+ return if !@parent_service || !@config[:load_errors]
24
+
25
+ @parent_service.errors.copy_from(
26
+ @errors,
27
+ break: @config[:self_break_on_error],
28
+ rollback: @config[:self_rollback_on_error],
29
+ )
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Light
4
+ module Services
5
+ module Concerns
6
+ # Manages service state including errors and warnings initialization
7
+ module StateManagement
8
+ private
9
+
10
+ # Initialize errors collection with configuration
11
+ def initialize_errors
12
+ @errors = Messages.new(
13
+ break_on_add: @config[:break_on_error],
14
+ raise_on_add: @config[:raise_on_error],
15
+ rollback_on_add: @config[:use_transactions] && @config[:rollback_on_error],
16
+ )
17
+ end
18
+
19
+ # Initialize warnings collection with configuration
20
+ def initialize_warnings
21
+ @warnings = Messages.new(
22
+ break_on_add: @config[:break_on_warning],
23
+ raise_on_add: @config[:raise_on_warning],
24
+ rollback_on_add: @config[:use_transactions] && @config[:rollback_on_warning],
25
+ )
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -3,18 +3,73 @@
3
3
  module Light
4
4
  module Services
5
5
  class << self
6
+ # Configure Light::Services with a block.
7
+ #
8
+ # @yield [Config] the configuration object
9
+ # @return [void]
10
+ #
11
+ # @example
12
+ # Light::Services.configure do |config|
13
+ # config.require_type = true
14
+ # config.use_transactions = false
15
+ # end
6
16
  def configure
7
17
  yield config
8
18
  end
9
19
 
20
+ # Get the global configuration object.
21
+ #
22
+ # @return [Config] the configuration instance
10
23
  def config
11
24
  @config ||= Config.new
12
25
  end
13
26
  end
14
27
 
28
+ # Configuration class for Light::Services global settings.
29
+ #
30
+ # @example Accessing configuration
31
+ # Light::Services.config.require_type # => true
32
+ #
33
+ # @example Modifying configuration
34
+ # Light::Services.config.use_transactions = false
15
35
  class Config
16
- # Constants
36
+ # @return [Boolean] whether arguments and outputs must have a type specified
37
+ attr_reader :require_type
38
+
39
+ # @return [Boolean] whether to wrap service execution in a database transaction
40
+ attr_reader :use_transactions
41
+
42
+ # @return [Boolean] whether to copy errors to parent service in chain
43
+ attr_reader :load_errors
44
+
45
+ # @return [Boolean] whether to stop executing steps when an error is added
46
+ attr_reader :break_on_error
47
+
48
+ # @return [Boolean] whether to raise Light::Services::Error when service fails
49
+ attr_reader :raise_on_error
50
+
51
+ # @return [Boolean] whether to rollback the transaction when an error is added
52
+ attr_reader :rollback_on_error
53
+
54
+ # @return [Boolean] whether to copy warnings to parent service in chain
55
+ attr_reader :load_warnings
56
+
57
+ # @return [Boolean] whether to stop executing steps when a warning is added
58
+ attr_reader :break_on_warning
59
+
60
+ # @return [Boolean] whether to raise Light::Services::Error when service has warnings
61
+ attr_reader :raise_on_warning
62
+
63
+ # @return [Boolean] whether to rollback the transaction when a warning is added
64
+ attr_reader :rollback_on_warning
65
+
66
+ # @return [Hash{String => String}] custom type mappings for Ruby LSP addon.
67
+ # Maps dry-types or custom types to Ruby types for hover/completion.
68
+ # @example { "Types::UUID" => "String", "CustomTypes::Money" => "BigDecimal" }
69
+ attr_reader :ruby_lsp_type_mappings
70
+
17
71
  DEFAULTS = {
72
+ require_type: true,
18
73
  use_transactions: true,
19
74
 
20
75
  load_errors: true,
@@ -25,36 +80,47 @@ module Light
25
80
  load_warnings: true,
26
81
  break_on_warning: false,
27
82
  raise_on_warning: false,
28
- rollback_on_warning: false
83
+ rollback_on_warning: false,
84
+
85
+ ruby_lsp_type_mappings: {}.freeze,
29
86
  }.freeze
30
87
 
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
88
+ DEFAULTS.each_key do |name|
89
+ define_method(:"#{name}=") do |value|
90
+ instance_variable_set(:"@#{name}", value)
91
+ @to_h = nil # Invalidate memoized hash
92
+ end
93
+ end
35
94
 
95
+ # Initialize configuration with default values.
36
96
  def initialize
37
97
  reset_to_defaults!
38
98
  end
39
99
 
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
-
100
+ # Reset all configuration options to their default values.
101
+ #
102
+ # @return [void]
48
103
  def reset_to_defaults!
49
104
  DEFAULTS.each do |key, value|
50
- set(key, value)
105
+ instance_variable_set(:"@#{key}", value)
51
106
  end
107
+
108
+ @to_h = nil # Invalidate memoized hash
52
109
  end
53
110
 
111
+ # Convert configuration to a hash.
112
+ #
113
+ # @return [Hash{Symbol => Object}] all configuration options as a hash
54
114
  def to_h
55
- DEFAULTS.keys.to_h { |key| [key, get(key)] }
115
+ @to_h ||= DEFAULTS.keys.to_h do |key|
116
+ [key, public_send(key)]
117
+ end
56
118
  end
57
119
 
120
+ # Merge configuration with additional options.
121
+ #
122
+ # @param config [Hash] options to merge
123
+ # @return [Hash] merged configuration hash
58
124
  def merge(config)
59
125
  to_h.merge(config)
60
126
  end
@@ -0,0 +1,100 @@
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
+ :stop!,
35
+ :stopped?,
36
+ :stop_immediately!,
37
+ :done!,
38
+ :done?,
39
+ :call,
40
+ :run_callbacks,
41
+ ].freeze
42
+
43
+ # Class methods that could conflict
44
+ CLASS_METHODS = [
45
+ :config,
46
+ :run,
47
+ :run!,
48
+ :with,
49
+ :arg,
50
+ :remove_arg,
51
+ :output,
52
+ :remove_output,
53
+ :step,
54
+ :remove_step,
55
+ :steps,
56
+ :outputs,
57
+ :arguments,
58
+ ].freeze
59
+
60
+ # Callback method names
61
+ CALLBACK_METHODS = [
62
+ :before_step_run,
63
+ :after_step_run,
64
+ :around_step_run,
65
+ :on_step_success,
66
+ :on_step_failure,
67
+ :on_step_crash,
68
+ :before_service_run,
69
+ :after_service_run,
70
+ :around_service_run,
71
+ :on_service_success,
72
+ :on_service_failure,
73
+ ].freeze
74
+
75
+ # Ruby reserved words and common Object methods
76
+ RUBY_RESERVED = [
77
+ :initialize,
78
+ :class,
79
+ :object_id,
80
+ :send,
81
+ :__send__,
82
+ :public_send,
83
+ :respond_to?,
84
+ :method,
85
+ :methods,
86
+ :instance_variable_get,
87
+ :instance_variable_set,
88
+ :instance_variables,
89
+ :extend,
90
+ :include,
91
+ :new,
92
+ :allocate,
93
+ :superclass,
94
+ ].freeze
95
+
96
+ # All reserved names combined (used for validation)
97
+ ALL = (BASE_METHODS + CALLBACK_METHODS + RUBY_RESERVED).uniq.freeze
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,85 @@
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
+ Validation.validate_type_required!(name, :argument, self, opts)
45
+
46
+ own_arguments[name] = Settings::Field.new(name, self, opts.merge(field_type: FieldTypes::ARGUMENT))
47
+ @arguments = nil # Clear memoized arguments since we're modifying them
48
+ end
49
+
50
+ # Remove an argument from the service
51
+ #
52
+ # @param name [Symbol] the argument name to remove
53
+ def remove_arg(name)
54
+ own_arguments.delete(name)
55
+ @arguments = nil # Clear memoized arguments since we're modifying them
56
+ end
57
+
58
+ # Get all arguments including inherited ones
59
+ #
60
+ # @return [Hash] all arguments defined for this service
61
+ def arguments
62
+ @arguments ||= build_arguments
63
+ end
64
+
65
+ # Get only arguments defined in this class
66
+ #
67
+ # @return [Hash] arguments defined in this class only
68
+ def own_arguments
69
+ @own_arguments ||= {}
70
+ end
71
+
72
+ private
73
+
74
+ # Build arguments by merging inherited arguments with own arguments
75
+ #
76
+ # @return [Hash] merged arguments
77
+ def build_arguments
78
+ inherited = superclass.respond_to?(:arguments) ? superclass.arguments.dup : {}
79
+ inherited.merge(own_arguments)
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,81 @@
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
+ Validation.validate_type_required!(name, :output, self, opts)
41
+
42
+ own_outputs[name] = Settings::Field.new(name, self, opts.merge(field_type: FieldTypes::OUTPUT))
43
+ @outputs = nil # Clear memoized outputs since we're modifying them
44
+ end
45
+
46
+ # Remove an output from the service
47
+ #
48
+ # @param name [Symbol] the output name to remove
49
+ def remove_output(name)
50
+ own_outputs.delete(name)
51
+ @outputs = nil # Clear memoized outputs since we're modifying them
52
+ end
53
+
54
+ # Get all outputs including inherited ones
55
+ #
56
+ # @return [Hash] all outputs defined for this service
57
+ def outputs
58
+ @outputs ||= build_outputs
59
+ end
60
+
61
+ # Get only outputs defined in this class
62
+ #
63
+ # @return [Hash] outputs defined in this class only
64
+ def own_outputs
65
+ @own_outputs ||= {}
66
+ end
67
+
68
+ private
69
+
70
+ # Build outputs by merging inherited outputs with own outputs
71
+ #
72
+ # @return [Hash] merged outputs
73
+ def build_outputs
74
+ inherited = superclass.respond_to?(:outputs) ? superclass.outputs.dup : {}
75
+ inherited.merge(own_outputs)
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end