light-services 2.2.1 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/config/rubocop_linter_action.yml +4 -4
- data/.github/workflows/ci.yml +12 -12
- data/.gitignore +1 -0
- data/.rubocop.yml +83 -7
- data/CHANGELOG.md +38 -0
- data/CLAUDE.md +139 -0
- data/Gemfile +16 -11
- data/Gemfile.lock +53 -27
- data/README.md +84 -21
- data/docs/arguments.md +290 -0
- data/docs/best-practices.md +153 -0
- data/docs/callbacks.md +476 -0
- data/docs/concepts.md +80 -0
- data/docs/configuration.md +204 -0
- data/docs/context.md +128 -0
- data/docs/crud.md +525 -0
- data/docs/errors.md +280 -0
- data/docs/generators.md +250 -0
- data/docs/outputs.md +158 -0
- data/docs/pundit-authorization.md +320 -0
- data/docs/quickstart.md +134 -0
- data/docs/readme.md +101 -0
- data/docs/recipes.md +14 -0
- data/docs/rubocop.md +285 -0
- data/docs/ruby-lsp.md +133 -0
- data/docs/service-rendering.md +222 -0
- data/docs/steps.md +391 -0
- data/docs/summary.md +21 -0
- data/docs/testing.md +549 -0
- data/lib/generators/light_services/install/USAGE +15 -0
- data/lib/generators/light_services/install/install_generator.rb +41 -0
- data/lib/generators/light_services/install/templates/application_service.rb.tt +8 -0
- data/lib/generators/light_services/install/templates/application_service_spec.rb.tt +7 -0
- data/lib/generators/light_services/install/templates/initializer.rb.tt +30 -0
- data/lib/generators/light_services/service/USAGE +21 -0
- data/lib/generators/light_services/service/service_generator.rb +68 -0
- data/lib/generators/light_services/service/templates/service.rb.tt +48 -0
- data/lib/generators/light_services/service/templates/service_spec.rb.tt +40 -0
- data/lib/light/services/base.rb +134 -122
- data/lib/light/services/base_with_context.rb +23 -1
- data/lib/light/services/callbacks.rb +157 -0
- data/lib/light/services/collection.rb +145 -0
- data/lib/light/services/concerns/execution.rb +79 -0
- data/lib/light/services/concerns/parent_service.rb +34 -0
- data/lib/light/services/concerns/state_management.rb +30 -0
- data/lib/light/services/config.rb +82 -16
- data/lib/light/services/constants.rb +100 -0
- data/lib/light/services/dsl/arguments_dsl.rb +85 -0
- data/lib/light/services/dsl/outputs_dsl.rb +81 -0
- data/lib/light/services/dsl/steps_dsl.rb +205 -0
- data/lib/light/services/dsl/validation.rb +162 -0
- data/lib/light/services/exceptions.rb +25 -2
- data/lib/light/services/message.rb +28 -3
- data/lib/light/services/messages.rb +92 -32
- data/lib/light/services/rspec/matchers/define_argument.rb +174 -0
- data/lib/light/services/rspec/matchers/define_output.rb +147 -0
- data/lib/light/services/rspec/matchers/define_step.rb +225 -0
- data/lib/light/services/rspec/matchers/execute_step.rb +230 -0
- data/lib/light/services/rspec/matchers/have_error_on.rb +148 -0
- data/lib/light/services/rspec/matchers/have_warning_on.rb +148 -0
- data/lib/light/services/rspec/matchers/trigger_callback.rb +138 -0
- data/lib/light/services/rspec.rb +15 -0
- data/lib/light/services/rubocop/cop/light_services/argument_type_required.rb +52 -0
- data/lib/light/services/rubocop/cop/light_services/condition_method_exists.rb +173 -0
- data/lib/light/services/rubocop/cop/light_services/deprecated_methods.rb +113 -0
- data/lib/light/services/rubocop/cop/light_services/dsl_order.rb +176 -0
- data/lib/light/services/rubocop/cop/light_services/missing_private_keyword.rb +102 -0
- data/lib/light/services/rubocop/cop/light_services/no_direct_instantiation.rb +66 -0
- data/lib/light/services/rubocop/cop/light_services/output_type_required.rb +52 -0
- data/lib/light/services/rubocop/cop/light_services/step_method_exists.rb +109 -0
- data/lib/light/services/rubocop.rb +12 -0
- data/lib/light/services/settings/field.rb +114 -0
- data/lib/light/services/settings/step.rb +53 -20
- data/lib/light/services/utils.rb +38 -0
- data/lib/light/services/version.rb +1 -1
- data/lib/light/services.rb +2 -0
- data/lib/ruby_lsp/light_services/addon.rb +36 -0
- data/lib/ruby_lsp/light_services/definition.rb +132 -0
- data/lib/ruby_lsp/light_services/indexing_enhancement.rb +263 -0
- data/light-services.gemspec +6 -8
- metadata +68 -26
- data/lib/light/services/class_based_collection/base.rb +0 -86
- data/lib/light/services/class_based_collection/mount.rb +0 -33
- data/lib/light/services/collection/arguments.rb +0 -34
- data/lib/light/services/collection/base.rb +0 -59
- data/lib/light/services/collection/outputs.rb +0 -16
- data/lib/light/services/settings/argument.rb +0 -68
- data/lib/light/services/settings/output.rb +0 -34
|
@@ -0,0 +1,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
|
-
#
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
|
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
|