cmdx 1.0.0 → 1.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/.cursor/prompts/rspec.md +20 -0
- data/.cursor/prompts/yardoc.md +8 -0
- data/.rubocop.yml +5 -0
- data/CHANGELOG.md +101 -49
- data/README.md +2 -1
- data/docs/ai_prompts.md +10 -0
- data/docs/basics/call.md +11 -2
- data/docs/basics/chain.md +10 -1
- data/docs/basics/context.md +9 -0
- data/docs/basics/setup.md +9 -0
- data/docs/callbacks.md +14 -37
- data/docs/configuration.md +68 -27
- data/docs/getting_started.md +11 -0
- data/docs/internationalization.md +148 -0
- data/docs/interruptions/exceptions.md +10 -1
- data/docs/interruptions/faults.md +11 -2
- data/docs/interruptions/halt.md +9 -0
- data/docs/logging.md +14 -4
- data/docs/middlewares.md +53 -43
- data/docs/outcomes/result.md +9 -0
- data/docs/outcomes/states.md +9 -0
- data/docs/outcomes/statuses.md +9 -0
- data/docs/parameters/coercions.md +58 -38
- data/docs/parameters/defaults.md +10 -1
- data/docs/parameters/definitions.md +9 -0
- data/docs/parameters/namespacing.md +9 -0
- data/docs/parameters/validations.md +8 -67
- data/docs/testing.md +22 -13
- data/docs/tips_and_tricks.md +9 -0
- data/docs/workflows.md +14 -4
- data/lib/cmdx/.DS_Store +0 -0
- data/lib/cmdx/callback.rb +36 -56
- data/lib/cmdx/callback_registry.rb +82 -73
- data/lib/cmdx/chain.rb +65 -122
- data/lib/cmdx/chain_inspector.rb +22 -115
- data/lib/cmdx/chain_serializer.rb +17 -148
- data/lib/cmdx/coercion.rb +49 -0
- data/lib/cmdx/coercion_registry.rb +94 -0
- data/lib/cmdx/coercions/array.rb +18 -36
- data/lib/cmdx/coercions/big_decimal.rb +21 -33
- data/lib/cmdx/coercions/boolean.rb +21 -40
- data/lib/cmdx/coercions/complex.rb +18 -31
- data/lib/cmdx/coercions/date.rb +20 -39
- data/lib/cmdx/coercions/date_time.rb +22 -39
- data/lib/cmdx/coercions/float.rb +19 -32
- data/lib/cmdx/coercions/hash.rb +22 -41
- data/lib/cmdx/coercions/integer.rb +20 -33
- data/lib/cmdx/coercions/rational.rb +20 -32
- data/lib/cmdx/coercions/string.rb +23 -31
- data/lib/cmdx/coercions/time.rb +24 -40
- data/lib/cmdx/coercions/virtual.rb +14 -31
- data/lib/cmdx/configuration.rb +57 -171
- data/lib/cmdx/context.rb +22 -165
- data/lib/cmdx/core_ext/hash.rb +42 -67
- data/lib/cmdx/core_ext/module.rb +35 -79
- data/lib/cmdx/core_ext/object.rb +63 -98
- data/lib/cmdx/correlator.rb +40 -156
- data/lib/cmdx/error.rb +37 -202
- data/lib/cmdx/errors.rb +165 -202
- data/lib/cmdx/fault.rb +55 -158
- data/lib/cmdx/faults.rb +26 -137
- data/lib/cmdx/immutator.rb +22 -109
- data/lib/cmdx/lazy_struct.rb +103 -187
- data/lib/cmdx/log_formatters/json.rb +14 -40
- data/lib/cmdx/log_formatters/key_value.rb +14 -40
- data/lib/cmdx/log_formatters/line.rb +14 -48
- data/lib/cmdx/log_formatters/logstash.rb +14 -57
- data/lib/cmdx/log_formatters/pretty_json.rb +14 -50
- data/lib/cmdx/log_formatters/pretty_key_value.rb +13 -46
- data/lib/cmdx/log_formatters/pretty_line.rb +16 -54
- data/lib/cmdx/log_formatters/raw.rb +19 -49
- data/lib/cmdx/logger.rb +20 -82
- data/lib/cmdx/logger_ansi.rb +18 -75
- data/lib/cmdx/logger_serializer.rb +24 -114
- data/lib/cmdx/middleware.rb +38 -60
- data/lib/cmdx/middleware_registry.rb +81 -77
- data/lib/cmdx/middlewares/correlate.rb +41 -226
- data/lib/cmdx/middlewares/timeout.rb +46 -185
- data/lib/cmdx/parameter.rb +120 -198
- data/lib/cmdx/parameter_evaluator.rb +231 -0
- data/lib/cmdx/parameter_inspector.rb +25 -56
- data/lib/cmdx/parameter_registry.rb +59 -84
- data/lib/cmdx/parameter_serializer.rb +23 -74
- data/lib/cmdx/railtie.rb +24 -107
- data/lib/cmdx/result.rb +254 -260
- data/lib/cmdx/result_ansi.rb +19 -85
- data/lib/cmdx/result_inspector.rb +27 -68
- data/lib/cmdx/result_logger.rb +18 -81
- data/lib/cmdx/result_serializer.rb +28 -132
- data/lib/cmdx/rspec/matchers.rb +28 -0
- data/lib/cmdx/rspec/result_matchers/be_executed.rb +42 -0
- data/lib/cmdx/rspec/result_matchers/be_failed_task.rb +94 -0
- data/lib/cmdx/rspec/result_matchers/be_skipped_task.rb +94 -0
- data/lib/cmdx/rspec/result_matchers/be_state_matchers.rb +59 -0
- data/lib/cmdx/rspec/result_matchers/be_status_matchers.rb +57 -0
- data/lib/cmdx/rspec/result_matchers/be_successful_task.rb +87 -0
- data/lib/cmdx/rspec/result_matchers/have_bad_outcome.rb +51 -0
- data/lib/cmdx/rspec/result_matchers/have_caused_failure.rb +58 -0
- data/lib/cmdx/rspec/result_matchers/have_chain_index.rb +59 -0
- data/lib/cmdx/rspec/result_matchers/have_context.rb +86 -0
- data/lib/cmdx/rspec/result_matchers/have_empty_metadata.rb +54 -0
- data/lib/cmdx/rspec/result_matchers/have_good_outcome.rb +52 -0
- data/lib/cmdx/rspec/result_matchers/have_metadata.rb +114 -0
- data/lib/cmdx/rspec/result_matchers/have_preserved_context.rb +66 -0
- data/lib/cmdx/rspec/result_matchers/have_received_thrown_failure.rb +64 -0
- data/lib/cmdx/rspec/result_matchers/have_runtime.rb +78 -0
- data/lib/cmdx/rspec/result_matchers/have_thrown_failure.rb +76 -0
- data/lib/cmdx/rspec/task_matchers/be_well_formed_task.rb +62 -0
- data/lib/cmdx/rspec/task_matchers/have_callback.rb +85 -0
- data/lib/cmdx/rspec/task_matchers/have_cmd_setting.rb +68 -0
- data/lib/cmdx/rspec/task_matchers/have_executed_callbacks.rb +92 -0
- data/lib/cmdx/rspec/task_matchers/have_middleware.rb +46 -0
- data/lib/cmdx/rspec/task_matchers/have_parameter.rb +181 -0
- data/lib/cmdx/task.rb +213 -425
- data/lib/cmdx/task_deprecator.rb +55 -0
- data/lib/cmdx/task_processor.rb +245 -0
- data/lib/cmdx/task_serializer.rb +22 -70
- data/lib/cmdx/utils/ansi_color.rb +13 -89
- data/lib/cmdx/utils/log_timestamp.rb +13 -42
- data/lib/cmdx/utils/monotonic_runtime.rb +13 -63
- data/lib/cmdx/utils/name_affix.rb +21 -71
- data/lib/cmdx/validator.rb +48 -0
- data/lib/cmdx/validator_registry.rb +86 -0
- data/lib/cmdx/validators/exclusion.rb +55 -94
- data/lib/cmdx/validators/format.rb +31 -85
- data/lib/cmdx/validators/inclusion.rb +65 -110
- data/lib/cmdx/validators/length.rb +117 -133
- data/lib/cmdx/validators/numeric.rb +123 -130
- data/lib/cmdx/validators/presence.rb +38 -79
- data/lib/cmdx/version.rb +1 -7
- data/lib/cmdx/workflow.rb +46 -339
- data/lib/cmdx.rb +1 -1
- data/lib/generators/cmdx/install_generator.rb +14 -31
- data/lib/generators/cmdx/task_generator.rb +39 -55
- data/lib/generators/cmdx/templates/install.rb +61 -11
- data/lib/generators/cmdx/workflow_generator.rb +41 -66
- data/lib/locales/ar.yml +35 -0
- data/lib/locales/cs.yml +35 -0
- data/lib/locales/da.yml +35 -0
- data/lib/locales/de.yml +35 -0
- data/lib/locales/el.yml +35 -0
- data/lib/locales/en.yml +19 -20
- data/lib/locales/es.yml +19 -20
- data/lib/locales/fi.yml +35 -0
- data/lib/locales/fr.yml +35 -0
- data/lib/locales/he.yml +35 -0
- data/lib/locales/hi.yml +35 -0
- data/lib/locales/it.yml +35 -0
- data/lib/locales/ja.yml +35 -0
- data/lib/locales/ko.yml +35 -0
- data/lib/locales/nl.yml +35 -0
- data/lib/locales/no.yml +35 -0
- data/lib/locales/pl.yml +35 -0
- data/lib/locales/pt.yml +35 -0
- data/lib/locales/ru.yml +35 -0
- data/lib/locales/sv.yml +35 -0
- data/lib/locales/th.yml +35 -0
- data/lib/locales/tr.yml +35 -0
- data/lib/locales/vi.yml +35 -0
- data/lib/locales/zh.yml +35 -0
- metadata +57 -8
- data/lib/cmdx/parameter_validator.rb +0 -81
- data/lib/cmdx/parameter_value.rb +0 -244
- data/lib/cmdx/parameters_inspector.rb +0 -72
- data/lib/cmdx/parameters_serializer.rb +0 -115
- data/lib/cmdx/rspec/result_matchers.rb +0 -917
- data/lib/cmdx/rspec/task_matchers.rb +0 -570
- data/lib/cmdx/validators/custom.rb +0 -102
data/lib/cmdx/configuration.rb
CHANGED
@@ -2,119 +2,65 @@
|
|
2
2
|
|
3
3
|
module CMDx
|
4
4
|
|
5
|
-
|
6
|
-
#
|
7
|
-
# The configuration system allows customization of default behaviors for tasks,
|
8
|
-
# workflows, logging, and error handling across the entire application.
|
9
|
-
#
|
10
|
-
# Configuration settings are stored as instance variables with explicit accessors
|
11
|
-
# and can be modified through the configure block pattern. These settings serve
|
12
|
-
# as defaults that can be overridden at the task or workflow level when needed.
|
13
|
-
#
|
14
|
-
# ## Available Configuration Options
|
15
|
-
#
|
16
|
-
# - **logger**: Logger instance for task execution logging
|
17
|
-
# - **task_halt**: Result statuses that cause `call!` to raise faults
|
18
|
-
# - **workflow_halt**: Result statuses that halt workflow execution
|
19
|
-
# - **middlewares**: Global middleware registry applied to all tasks
|
20
|
-
# - **callbacks**: Global callback registry applied to all tasks
|
21
|
-
#
|
22
|
-
# ## Configuration Hierarchy
|
23
|
-
#
|
24
|
-
# CMDx follows a configuration hierarchy where settings can be overridden:
|
25
|
-
# 1. **Global Configuration**: Framework-wide defaults (this module)
|
26
|
-
# 2. **Task Settings**: Class-level overrides via `task_settings!`
|
27
|
-
# 3. **Runtime Parameters**: Instance-specific overrides during execution
|
28
|
-
#
|
29
|
-
# @example Basic configuration setup
|
30
|
-
# CMDx.configure do |config|
|
31
|
-
# config.logger = Logger.new($stdout)
|
32
|
-
# config.task_halt = ["failed"] # Only halt on failures
|
33
|
-
# config.middlewares.use CMDx::Middlewares::Timeout, 30
|
34
|
-
# end
|
35
|
-
#
|
36
|
-
# @example Rails initializer configuration
|
37
|
-
# # config/initializers/cmdx.rb
|
38
|
-
# CMDx.configure do |config|
|
39
|
-
# config.logger = Logger.new($stdout)
|
40
|
-
# config.task_halt = CMDx::Result::FAILED
|
41
|
-
# config.workflow_halt = [CMDx::Result::FAILED, CMDx::Result::SKIPPED]
|
42
|
-
#
|
43
|
-
# # Add global middlewares
|
44
|
-
# config.middlewares.use CMDx::Middlewares::Timeout, 30
|
45
|
-
# config.middlewares.use AuthenticationMiddleware if Rails.env.production?
|
46
|
-
#
|
47
|
-
# # Add global callbacks
|
48
|
-
# config.callbacks.register :before_execution, :log_task_start
|
49
|
-
# config.callbacks.register :on_success, NotificationCallback.new([:slack])
|
50
|
-
# config.callbacks.register :on_failure, :alert_admin, if: :production?
|
51
|
-
# end
|
52
|
-
#
|
53
|
-
# @example Custom logger configuration
|
54
|
-
# CMDx.configure do |config|
|
55
|
-
# config.logger = Logger.new(
|
56
|
-
# Rails.root.join('log', 'cmdx.log'),
|
57
|
-
# formatter: CMDx::LogFormatters::Json.new
|
58
|
-
# )
|
59
|
-
# end
|
60
|
-
#
|
61
|
-
# @example Environment-specific configuration
|
62
|
-
# CMDx.configure do |config|
|
63
|
-
# case Rails.env
|
64
|
-
# when 'development'
|
65
|
-
# config.logger = Logger.new($stdout, formatter: CMDx::LogFormatters::PrettyLine.new)
|
66
|
-
# when 'test'
|
67
|
-
# config.logger = Logger.new('/dev/null') # Silent logging
|
68
|
-
# when 'production'
|
69
|
-
# config.logger = Logger.new($stdout, formatter: CMDx::LogFormatters::Json.new)
|
70
|
-
# end
|
71
|
-
# end
|
72
|
-
#
|
73
|
-
# @see Task Task-level configuration overrides
|
74
|
-
# @see Workflow Workflow-level configuration overrides
|
75
|
-
# @see LogFormatters Available logging formatters
|
76
|
-
# @see Result Result statuses for halt configuration
|
77
|
-
# @since 1.0.0
|
78
|
-
|
79
|
-
##
|
80
|
-
# Configuration class that manages CMDx framework settings.
|
81
|
-
# Provides explicit attribute accessors for all configuration options.
|
82
|
-
#
|
83
|
-
# @since 1.0.0
|
5
|
+
# Global configuration class for CMDx framework settings.
|
6
|
+
# Manages logging, middleware, callbacks, coercions, validators, and halt conditions.
|
84
7
|
class Configuration
|
85
8
|
|
86
|
-
# Default configuration values
|
87
9
|
DEFAULT_HALT = "failed"
|
88
10
|
|
89
|
-
#
|
90
|
-
attr_accessor :logger
|
11
|
+
# @return [Logger] Logger instance for task execution logging
|
12
|
+
attr_accessor :logger
|
13
|
+
|
14
|
+
# @return [MiddlewareRegistry] Global middleware registry applied to all tasks
|
15
|
+
attr_accessor :middlewares
|
16
|
+
|
17
|
+
# @return [CallbackRegistry] Global callback registry applied to all tasks
|
18
|
+
attr_accessor :callbacks
|
19
|
+
|
20
|
+
# @return [CoercionRegistry] Global coercion registry for custom parameter types
|
21
|
+
attr_accessor :coercions
|
91
22
|
|
92
|
-
|
93
|
-
|
23
|
+
# @return [ValidatorRegistry] Global validator registry for custom parameter validation
|
24
|
+
attr_accessor :validators
|
25
|
+
|
26
|
+
# @return [String, Array<String>] Result statuses that cause `call!` to raise faults
|
27
|
+
attr_accessor :task_halt
|
28
|
+
|
29
|
+
# @return [String, Array<String>] Result statuses that halt workflow execution
|
30
|
+
attr_accessor :workflow_halt
|
31
|
+
|
32
|
+
# Initialize a new Configuration instance with default settings.
|
94
33
|
#
|
95
34
|
# @example
|
96
35
|
# config = CMDx::Configuration.new
|
36
|
+
# config.logger.level = Logger::DEBUG
|
37
|
+
#
|
38
|
+
# @return [Configuration] A new configuration instance
|
97
39
|
def initialize
|
98
|
-
@logger
|
99
|
-
@middlewares
|
100
|
-
@callbacks
|
101
|
-
@
|
40
|
+
@logger = ::Logger.new($stdout, formatter: CMDx::LogFormatters::Line.new)
|
41
|
+
@middlewares = MiddlewareRegistry.new
|
42
|
+
@callbacks = CallbackRegistry.new
|
43
|
+
@coercions = CoercionRegistry.new
|
44
|
+
@validators = ValidatorRegistry.new
|
45
|
+
@task_halt = DEFAULT_HALT
|
102
46
|
@workflow_halt = DEFAULT_HALT
|
103
47
|
end
|
104
48
|
|
105
|
-
|
106
|
-
# Returns a hash representation of the configuration.
|
107
|
-
# Used internally by the framework for configuration merging.
|
49
|
+
# Convert the configuration to a hash representation.
|
108
50
|
#
|
109
|
-
# @return [Hash] configuration attributes as a hash
|
110
51
|
# @example
|
111
|
-
# config = CMDx.
|
112
|
-
# config.to_h
|
52
|
+
# config = CMDx::Configuration.new
|
53
|
+
# hash = config.to_h
|
54
|
+
# puts hash[:task_halt] # => "failed"
|
55
|
+
#
|
56
|
+
# @return [Hash] Hash containing all configuration values
|
113
57
|
def to_h
|
114
58
|
{
|
115
59
|
logger: @logger,
|
116
60
|
middlewares: @middlewares,
|
117
61
|
callbacks: @callbacks,
|
62
|
+
coercions: @coercions,
|
63
|
+
validators: @validators,
|
118
64
|
task_halt: @task_halt,
|
119
65
|
workflow_halt: @workflow_halt
|
120
66
|
}
|
@@ -124,72 +70,33 @@ module CMDx
|
|
124
70
|
|
125
71
|
module_function
|
126
72
|
|
127
|
-
|
128
|
-
#
|
129
|
-
# Creates a new configuration with default values if none exists.
|
73
|
+
# Get the current global configuration instance.
|
74
|
+
# Creates a new configuration if none exists.
|
130
75
|
#
|
131
|
-
#
|
132
|
-
# throughout the application lifecycle. It uses lazy initialization,
|
133
|
-
# creating the configuration only when first accessed.
|
134
|
-
#
|
135
|
-
# @return [Configuration] the current configuration object
|
136
|
-
#
|
137
|
-
# @example Accessing configuration values
|
138
|
-
# CMDx.configuration.logger #=> <Logger instance>
|
139
|
-
# CMDx.configuration.task_halt #=> "failed"
|
140
|
-
#
|
141
|
-
# @example Checking configuration state
|
76
|
+
# @example
|
142
77
|
# config = CMDx.configuration
|
143
|
-
# config.logger.
|
78
|
+
# config.logger.level = Logger::INFO
|
79
|
+
#
|
80
|
+
# @return [Configuration] The global configuration instance
|
144
81
|
def configuration
|
145
82
|
return @configuration if @configuration
|
146
83
|
|
147
84
|
@configuration ||= Configuration.new
|
148
85
|
end
|
149
86
|
|
150
|
-
|
151
|
-
# Configures CMDx settings using a block-based DSL.
|
152
|
-
# This is the preferred method for setting up CMDx configuration
|
153
|
-
# as it provides a clean, readable syntax for configuration management.
|
154
|
-
#
|
155
|
-
# The configuration block yields the current configuration object,
|
156
|
-
# allowing you to set multiple options in a single, organized block.
|
157
|
-
#
|
158
|
-
# @yieldparam config [Configuration] the configuration object to modify
|
159
|
-
# @return [Configuration] the updated configuration object
|
160
|
-
# @raise [ArgumentError] if no block is provided
|
87
|
+
# Configure the global CMDx settings using a block.
|
161
88
|
#
|
162
|
-
# @example
|
89
|
+
# @example
|
163
90
|
# CMDx.configure do |config|
|
164
|
-
# config.task_halt = ["failed", "
|
91
|
+
# config.task_halt = ["failed", "error"]
|
92
|
+
# config.logger.level = Logger::DEBUG
|
165
93
|
# end
|
166
94
|
#
|
167
|
-
# @
|
168
|
-
# CMDx.configure do |config|
|
169
|
-
# config.logger = Rails.logger if defined?(Rails)
|
170
|
-
#
|
171
|
-
# config.task_halt = if Rails.env.production?
|
172
|
-
# "failed" # Only halt on failures in production
|
173
|
-
# else
|
174
|
-
# ["failed", "skipped"] # Halt on both in development
|
175
|
-
# end
|
95
|
+
# @yield [Configuration] The configuration instance
|
176
96
|
#
|
177
|
-
|
178
|
-
# end
|
97
|
+
# @return [Configuration] The configured instance
|
179
98
|
#
|
180
|
-
# @
|
181
|
-
# CMDx.configure do |config|
|
182
|
-
# config.logger = Logger.new($stdout).tap do |logger|
|
183
|
-
# logger.formatter = case ENV['LOG_FORMAT']
|
184
|
-
# when 'json'
|
185
|
-
# CMDx::LogFormatters::Json.new
|
186
|
-
# when 'pretty'
|
187
|
-
# CMDx::LogFormatters::PrettyLine.new
|
188
|
-
# else
|
189
|
-
# CMDx::LogFormatters::Line.new
|
190
|
-
# end
|
191
|
-
# end
|
192
|
-
# end
|
99
|
+
# @raise [ArgumentError] If no block is provided
|
193
100
|
def configure
|
194
101
|
raise ArgumentError, "block required" unless block_given?
|
195
102
|
|
@@ -198,34 +105,13 @@ module CMDx
|
|
198
105
|
config
|
199
106
|
end
|
200
107
|
|
201
|
-
|
202
|
-
# Resets the configuration to default values.
|
203
|
-
# This method creates a fresh configuration object with framework defaults,
|
204
|
-
# discarding any previously set custom values.
|
108
|
+
# Reset the global configuration to default values.
|
205
109
|
#
|
206
|
-
# @
|
207
|
-
#
|
208
|
-
# @example Resetting configuration
|
209
|
-
# # After custom configuration
|
210
|
-
# CMDx.configure { |c| c.task_halt = ["failed"] }
|
211
|
-
# CMDx.configuration.task_halt #=> ["failed"]
|
212
|
-
#
|
213
|
-
# # Reset to defaults
|
110
|
+
# @example
|
214
111
|
# CMDx.reset_configuration!
|
215
|
-
# CMDx.configuration.task_halt
|
216
|
-
#
|
217
|
-
# @example Testing with clean configuration
|
218
|
-
# # In test setup
|
219
|
-
# def setup
|
220
|
-
# CMDx.reset_configuration! # Start with clean defaults
|
221
|
-
# end
|
222
|
-
#
|
223
|
-
# @example Conditional reset
|
224
|
-
# # Reset configuration in development for experimentation
|
225
|
-
# CMDx.reset_configuration! if Rails.env.development?
|
112
|
+
# CMDx.configuration.task_halt # => "failed"
|
226
113
|
#
|
227
|
-
# @
|
228
|
-
# to return to a known default state.
|
114
|
+
# @return [Configuration] A new configuration instance with defaults
|
229
115
|
def reset_configuration!
|
230
116
|
@configuration = Configuration.new
|
231
117
|
end
|
data/lib/cmdx/context.rb
CHANGED
@@ -1,181 +1,38 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module CMDx
|
4
|
-
|
5
|
-
# Context provides a flexible parameter storage and data passing mechanism for CMDx tasks.
|
6
|
-
# It extends LazyStruct to offer dynamic attribute access with both hash-style and method-style
|
7
|
-
# syntax, serving as the primary interface for task input parameters and inter-task communication.
|
4
|
+
# Parameter and data context for task execution.
|
8
5
|
#
|
9
|
-
# Context
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
# ## Usage Patterns
|
16
|
-
#
|
17
|
-
# Context is typically used in three main scenarios:
|
18
|
-
# 1. **Parameter Input**: Passing initial data to tasks
|
19
|
-
# 2. **Data Storage**: Storing intermediate results during task execution
|
20
|
-
# 3. **Task Communication**: Sharing data between multiple tasks
|
21
|
-
#
|
22
|
-
# @example Basic parameter input
|
23
|
-
# class ProcessOrderTask < CMDx::Task
|
24
|
-
# required :order_id, type: :integer
|
25
|
-
# optional :notify_customer, type: :boolean, default: true
|
26
|
-
#
|
27
|
-
# def call
|
28
|
-
# context.order = Order.find(order_id)
|
29
|
-
# context.processed_at = Time.now
|
30
|
-
#
|
31
|
-
# if notify_customer
|
32
|
-
# context.notification_sent = send_notification
|
33
|
-
# end
|
34
|
-
# end
|
35
|
-
# end
|
36
|
-
#
|
37
|
-
# result = ProcessOrderTask.call(order_id: 123, notify_customer: false)
|
38
|
-
# result.context.order #=> <Order id: 123>
|
39
|
-
# result.context.processed_at #=> 2023-01-01 12:00:00 UTC
|
40
|
-
# result.context.notification_sent #=> nil
|
41
|
-
#
|
42
|
-
# @example Dynamic attribute assignment
|
43
|
-
# class DataProcessingTask < CMDx::Task
|
44
|
-
# required :input_data, type: :hash
|
45
|
-
#
|
46
|
-
# def call
|
47
|
-
# # Method-style assignment
|
48
|
-
# context.processed_data = transform(input_data)
|
49
|
-
# context.validation_errors = validate(context.processed_data)
|
50
|
-
#
|
51
|
-
# # Hash-style assignment
|
52
|
-
# context[:metadata] = { processed_at: Time.now }
|
53
|
-
# context["summary"] = generate_summary
|
54
|
-
#
|
55
|
-
# # Workflow assignment
|
56
|
-
# context.merge!(
|
57
|
-
# status: "complete",
|
58
|
-
# record_count: context.processed_data.size
|
59
|
-
# )
|
60
|
-
# end
|
61
|
-
# end
|
62
|
-
#
|
63
|
-
# @example Inter-task communication
|
64
|
-
# class OrderProcessingWorkflow < CMDx::Workflow
|
65
|
-
# def call
|
66
|
-
# # First task sets up context
|
67
|
-
# ValidateOrderTask.call(context)
|
68
|
-
#
|
69
|
-
# # Subsequent tasks use and modify context
|
70
|
-
# ProcessPaymentTask.call(context)
|
71
|
-
# UpdateInventoryTask.call(context)
|
72
|
-
# SendConfirmationTask.call(context)
|
73
|
-
# end
|
74
|
-
# end
|
75
|
-
#
|
76
|
-
# # Initial context with order data
|
77
|
-
# result = OrderProcessingWorkflow.call(
|
78
|
-
# order_id: 123,
|
79
|
-
# payment_method: "credit_card",
|
80
|
-
# customer_email: "customer@example.com"
|
81
|
-
# )
|
82
|
-
#
|
83
|
-
# # Context accumulates data from all tasks
|
84
|
-
# result.context.order #=> <Order> (from ValidateOrderTask)
|
85
|
-
# result.context.payment_result #=> <Payment> (from ProcessPaymentTask)
|
86
|
-
# result.context.inventory_updated #=> true (from UpdateInventoryTask)
|
87
|
-
# result.context.confirmation_sent #=> true (from SendConfirmationTask)
|
88
|
-
#
|
89
|
-
# @example Context passing between tasks
|
90
|
-
# class ProcessOrderTask < CMDx::Task
|
91
|
-
# required :order_id, type: :integer
|
92
|
-
#
|
93
|
-
# def call
|
94
|
-
# context.order = Order.find(order_id)
|
95
|
-
#
|
96
|
-
# # Pass context to subtasks
|
97
|
-
# payment_result = ProcessPaymentTask.call(context)
|
98
|
-
# email_result = SendEmailTask.call(context)
|
99
|
-
#
|
100
|
-
# # Results maintain context continuity
|
101
|
-
# context.payment_processed = payment_result.success?
|
102
|
-
# context.email_sent = email_result.success?
|
103
|
-
# end
|
104
|
-
# end
|
105
|
-
#
|
106
|
-
# # After execution, context contains accumulated data
|
107
|
-
# result = ProcessOrderTask.call(order_id: 123)
|
108
|
-
# result.context.order #=> <Order>
|
109
|
-
# result.context.payment_processed #=> true
|
110
|
-
# result.context.email_sent #=> true
|
111
|
-
#
|
112
|
-
# @example Context with nested data structures
|
113
|
-
# class AnalyticsTask < CMDx::Task
|
114
|
-
# required :user_id, type: :integer
|
115
|
-
#
|
116
|
-
# def call
|
117
|
-
# context.user = User.find(user_id)
|
118
|
-
# context.analytics = {
|
119
|
-
# page_views: calculate_page_views,
|
120
|
-
# session_duration: calculate_session_duration,
|
121
|
-
# conversion_rate: calculate_conversion_rate
|
122
|
-
# }
|
123
|
-
#
|
124
|
-
# # Access nested data
|
125
|
-
# context.dig(:analytics, :page_views) #=> 150
|
126
|
-
#
|
127
|
-
# # Add more nested data
|
128
|
-
# context.analytics[:last_login] = context.user.last_login
|
129
|
-
# end
|
130
|
-
# end
|
131
|
-
#
|
132
|
-
# @see LazyStruct Base class providing dynamic attribute functionality
|
133
|
-
# @see Task Task base class that uses Context for parameter storage
|
134
|
-
# @see Chain Chain execution context that Context belongs to
|
135
|
-
# @see Parameter Parameter definitions that populate Context
|
136
|
-
# @since 1.0.0
|
6
|
+
# Context provides flexible data storage and access patterns for task
|
7
|
+
# parameters and runtime data. Built on LazyStruct, it supports both
|
8
|
+
# hash-like and object-like access patterns with dynamic attribute
|
9
|
+
# assignment and automatic key normalization.
|
137
10
|
class Context < LazyStruct
|
138
11
|
|
139
|
-
|
140
|
-
# Builds a Context instance from the given input, with intelligent handling
|
141
|
-
# of existing Context objects to avoid unnecessary object creation.
|
12
|
+
# Creates or returns a context instance from the given input.
|
142
13
|
#
|
143
|
-
# This
|
144
|
-
#
|
145
|
-
#
|
146
|
-
# - Converting hash-like objects into new Context instances
|
14
|
+
# This method provides a safe way to build context instances, returning
|
15
|
+
# the input unchanged if it's already a Context instance and not frozen,
|
16
|
+
# otherwise creating a new Context instance with the provided data.
|
147
17
|
#
|
148
|
-
# @param context [Hash, Context,
|
149
|
-
# @return [Context] a Context instance ready for task execution
|
18
|
+
# @param context [Hash, Context, Object] input data to build context from
|
150
19
|
#
|
151
|
-
# @
|
152
|
-
# context = Context.build(name: "John", age: 30)
|
153
|
-
# context.name #=> "John"
|
154
|
-
# context.age #=> 30
|
20
|
+
# @return [Context] a Context instance containing the provided data
|
155
21
|
#
|
156
|
-
# @
|
157
|
-
# original = Context.build(data: "test")
|
158
|
-
# reused = Context.build(original)
|
159
|
-
# original.object_id == reused.object_id #=> true
|
22
|
+
# @raise [ArgumentError] if the input doesn't respond to to_h
|
160
23
|
#
|
161
|
-
# @example
|
162
|
-
#
|
163
|
-
#
|
164
|
-
# new_context = Context.build(original)
|
165
|
-
# original.object_id == new_context.object_id #=> false
|
24
|
+
# @example Build context from hash
|
25
|
+
# Context.build(name: "John", age: 30)
|
26
|
+
# # => #<CMDx::Context :name="John" :age=30>
|
166
27
|
#
|
167
|
-
# @example
|
168
|
-
#
|
169
|
-
#
|
170
|
-
# context
|
171
|
-
# context.user #=> { name: "John" }
|
28
|
+
# @example Build context from existing context
|
29
|
+
# existing = Context.build(user_id: 123)
|
30
|
+
# Context.build(existing)
|
31
|
+
# # => returns existing context unchanged
|
172
32
|
#
|
173
|
-
# @example
|
174
|
-
#
|
175
|
-
#
|
176
|
-
# # Equivalent to:
|
177
|
-
# # context = Context.build(order_id: 123, priority: "high")
|
178
|
-
# # ProcessOrderTask.new(context).call
|
33
|
+
# @example Build context from hash-like object
|
34
|
+
# Context.build(OpenStruct.new(status: "active"))
|
35
|
+
# # => #<CMDx::Context :status="active">
|
179
36
|
def self.build(context = {})
|
180
37
|
return context if context.is_a?(self) && !context.frozen?
|
181
38
|
|
data/lib/cmdx/core_ext/hash.rb
CHANGED
@@ -2,51 +2,28 @@
|
|
2
2
|
|
3
3
|
module CMDx
|
4
4
|
module CoreExt
|
5
|
-
# Extensions
|
6
|
-
#
|
7
|
-
#
|
8
|
-
# string and symbol keys interchangeably. These methods are prefixed
|
9
|
-
# with `__cmdx_` to avoid conflicts with existing Hash methods.
|
10
|
-
#
|
11
|
-
# @example Flexible key access
|
12
|
-
# hash = {name: "John", "age" => 30}
|
13
|
-
# hash.__cmdx_fetch(:name) # => "John" (symbol key)
|
14
|
-
# hash.__cmdx_fetch("name") # => "John" (tries symbol fallback)
|
15
|
-
# hash.__cmdx_fetch(:age) # => 30 (string fallback)
|
16
|
-
#
|
17
|
-
# @example Key checking
|
18
|
-
# hash.__cmdx_key?(:name) # => true (checks both symbol and string)
|
19
|
-
# hash.__cmdx_key?("age") # => true (checks both string and symbol)
|
20
|
-
#
|
21
|
-
# @example Method response checking
|
22
|
-
# hash.__cmdx_respond_to?(:name) # => true (considers key as method)
|
23
|
-
#
|
24
|
-
# @see Context Context objects that use hash extensions
|
25
|
-
# @see LazyStruct Structs that leverage hash-like behavior
|
5
|
+
# Extensions for Ruby's Hash class that provide flexible key access and querying.
|
6
|
+
# These extensions are automatically included in all hashes when CMDx is loaded, providing
|
7
|
+
# seamless symbol/string key interoperability and enhanced key existence checking.
|
26
8
|
module HashExtensions
|
27
9
|
|
28
|
-
#
|
10
|
+
# Fetches a value from the hash with flexible key matching.
|
11
|
+
# Tries the exact key first, then attempts symbol/string conversion if not found.
|
29
12
|
#
|
30
|
-
#
|
31
|
-
# key and its converted form (symbol to string or string to symbol).
|
32
|
-
# This is particularly useful for parameter hashes that might use
|
33
|
-
# either format.
|
13
|
+
# @param key [Symbol, String, Object] the key to fetch from the hash
|
34
14
|
#
|
35
|
-
# @
|
36
|
-
# @return [Object] value for the key or its converted equivalent
|
15
|
+
# @return [Object, nil] the value associated with the key, or nil if not found
|
37
16
|
#
|
38
|
-
# @example
|
39
|
-
# hash = {
|
40
|
-
# hash.
|
17
|
+
# @example Fetch with symbol key
|
18
|
+
# hash = { name: "John", "age" => 30 }
|
19
|
+
# hash.cmdx_fetch(:name) # => "John"
|
20
|
+
# hash.cmdx_fetch(:age) # => 30
|
41
21
|
#
|
42
|
-
# @example
|
43
|
-
# hash = {name: "John"}
|
44
|
-
# hash.
|
45
|
-
#
|
46
|
-
|
47
|
-
# hash = {id: 123}
|
48
|
-
# hash.__cmdx_fetch(:id) # => 123 (direct match)
|
49
|
-
def __cmdx_fetch(key)
|
22
|
+
# @example Fetch with string key
|
23
|
+
# hash = { name: "John", "age" => 30 }
|
24
|
+
# hash.cmdx_fetch("name") # => "John"
|
25
|
+
# hash.cmdx_fetch("age") # => 30
|
26
|
+
def cmdx_fetch(key)
|
50
27
|
case key
|
51
28
|
when Symbol then fetch(key) { self[key.to_s] }
|
52
29
|
when String then fetch(key) { self[key.to_sym] }
|
@@ -54,21 +31,21 @@ module CMDx
|
|
54
31
|
end
|
55
32
|
end
|
56
33
|
|
57
|
-
#
|
34
|
+
# Checks if a key exists in the hash with flexible key matching.
|
35
|
+
# Tries the exact key first, then attempts symbol/string conversion.
|
58
36
|
#
|
59
|
-
#
|
60
|
-
# key and its converted form. Returns true if either variant exists.
|
37
|
+
# @param key [Symbol, String, Object] the key to check for existence
|
61
38
|
#
|
62
|
-
# @
|
63
|
-
# @return [Boolean] true if key exists in either format
|
39
|
+
# @return [Boolean] true if the key exists (in any form), false otherwise
|
64
40
|
#
|
65
|
-
# @example
|
66
|
-
# hash = {name: "John", "age" => 30}
|
67
|
-
# hash.
|
68
|
-
# hash.
|
69
|
-
# hash.
|
70
|
-
# hash.
|
71
|
-
|
41
|
+
# @example Check key existence
|
42
|
+
# hash = { name: "John", "age" => 30 }
|
43
|
+
# hash.cmdx_key?(:name) # => true
|
44
|
+
# hash.cmdx_key?("name") # => true
|
45
|
+
# hash.cmdx_key?(:age) # => true
|
46
|
+
# hash.cmdx_key?("age") # => true
|
47
|
+
# hash.cmdx_key?(:missing) # => false
|
48
|
+
def cmdx_key?(key)
|
72
49
|
key?(key) || key?(
|
73
50
|
case key
|
74
51
|
when Symbol then key.to_s
|
@@ -79,30 +56,28 @@ module CMDx
|
|
79
56
|
false
|
80
57
|
end
|
81
58
|
|
82
|
-
#
|
59
|
+
# Checks if the hash responds to a method or contains a key.
|
60
|
+
# Combines method existence checking with flexible key existence checking.
|
83
61
|
#
|
84
|
-
#
|
85
|
-
#
|
86
|
-
# hash keys to be treated as virtual methods.
|
62
|
+
# @param key [Symbol, String] the method name or key to check
|
63
|
+
# @param include_private [Boolean] whether to include private methods in the check
|
87
64
|
#
|
88
|
-
# @
|
89
|
-
# @param include_private [Boolean] whether to include private methods
|
90
|
-
# @return [Boolean] true if responds to method or contains key
|
65
|
+
# @return [Boolean] true if the hash responds to the method or contains the key
|
91
66
|
#
|
92
|
-
# @example
|
93
|
-
# hash = {name: "John"}
|
94
|
-
# hash.
|
95
|
-
# hash.
|
96
|
-
# hash.
|
97
|
-
|
98
|
-
|
67
|
+
# @example Check method or key response
|
68
|
+
# hash = { name: "John", "age" => 30 }
|
69
|
+
# hash.cmdx_respond_to?(:keys) # => true (method exists)
|
70
|
+
# hash.cmdx_respond_to?(:name) # => true (key exists)
|
71
|
+
# hash.cmdx_respond_to?("age") # => true (key exists)
|
72
|
+
# hash.cmdx_respond_to?(:missing) # => false
|
73
|
+
def cmdx_respond_to?(key, include_private = false)
|
74
|
+
respond_to?(key.to_sym, include_private) || cmdx_key?(key)
|
99
75
|
rescue NoMethodError
|
100
|
-
|
76
|
+
cmdx_key?(key)
|
101
77
|
end
|
102
78
|
|
103
79
|
end
|
104
80
|
end
|
105
81
|
end
|
106
82
|
|
107
|
-
# Extend all hashes with CMDx utility methods
|
108
83
|
Hash.include(CMDx::CoreExt::HashExtensions)
|