cmdx 0.5.0 → 1.0.1
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/.DS_Store +0 -0
- data/.cursor/rules/cursor-instructions.mdc +6 -0
- data/.rubocop.yml +19 -1
- data/.ruby-version +1 -1
- data/CHANGELOG.md +95 -28
- data/README.md +73 -25
- data/docs/ai_prompts.md +319 -0
- data/docs/basics/call.md +234 -14
- data/docs/basics/chain.md +280 -0
- data/docs/basics/context.md +241 -33
- data/docs/basics/setup.md +85 -12
- data/docs/callbacks.md +283 -0
- data/docs/configuration.md +155 -30
- data/docs/getting_started.md +145 -22
- data/docs/internationalization.md +148 -0
- data/docs/interruptions/exceptions.md +198 -11
- data/docs/interruptions/faults.md +196 -44
- data/docs/interruptions/halt.md +188 -35
- data/docs/logging.md +204 -53
- data/docs/middlewares.md +745 -0
- data/docs/outcomes/result.md +305 -10
- data/docs/outcomes/states.md +212 -31
- data/docs/outcomes/statuses.md +284 -30
- data/docs/parameters/coercions.md +411 -29
- data/docs/parameters/defaults.md +258 -25
- data/docs/parameters/definitions.md +247 -72
- data/docs/parameters/namespacing.md +259 -27
- data/docs/parameters/validations.md +173 -168
- data/docs/testing.md +560 -0
- data/docs/tips_and_tricks.md +103 -42
- data/docs/workflows.md +329 -0
- data/lib/cmdx/.DS_Store +0 -0
- data/lib/cmdx/callback.rb +69 -0
- data/lib/cmdx/callback_registry.rb +106 -0
- data/lib/cmdx/chain.rb +190 -0
- data/lib/cmdx/chain_inspector.rb +149 -0
- data/lib/cmdx/chain_serializer.rb +175 -0
- data/lib/cmdx/coercions/array.rb +37 -0
- data/lib/cmdx/coercions/big_decimal.rb +33 -0
- data/lib/cmdx/coercions/boolean.rb +41 -1
- data/lib/cmdx/coercions/complex.rb +31 -0
- data/lib/cmdx/coercions/date.rb +39 -0
- data/lib/cmdx/coercions/date_time.rb +39 -0
- data/lib/cmdx/coercions/float.rb +31 -0
- data/lib/cmdx/coercions/hash.rb +42 -0
- data/lib/cmdx/coercions/integer.rb +32 -0
- data/lib/cmdx/coercions/rational.rb +31 -0
- data/lib/cmdx/coercions/string.rb +31 -0
- data/lib/cmdx/coercions/time.rb +39 -0
- data/lib/cmdx/coercions/virtual.rb +31 -0
- data/lib/cmdx/configuration.rb +217 -9
- data/lib/cmdx/context.rb +173 -2
- data/lib/cmdx/core_ext/hash.rb +72 -0
- data/lib/cmdx/core_ext/module.rb +94 -0
- data/lib/cmdx/core_ext/object.rb +105 -0
- data/lib/cmdx/correlator.rb +217 -0
- data/lib/cmdx/error.rb +210 -8
- data/lib/cmdx/errors.rb +256 -1
- data/lib/cmdx/fault.rb +177 -2
- data/lib/cmdx/faults.rb +158 -2
- data/lib/cmdx/immutator.rb +121 -2
- data/lib/cmdx/lazy_struct.rb +261 -18
- data/lib/cmdx/log_formatters/json.rb +46 -0
- data/lib/cmdx/log_formatters/key_value.rb +46 -0
- data/lib/cmdx/log_formatters/line.rb +54 -0
- data/lib/cmdx/log_formatters/logstash.rb +64 -0
- data/lib/cmdx/log_formatters/pretty_json.rb +57 -0
- data/lib/cmdx/log_formatters/pretty_key_value.rb +51 -0
- data/lib/cmdx/log_formatters/pretty_line.rb +60 -0
- data/lib/cmdx/log_formatters/raw.rb +54 -0
- data/lib/cmdx/logger.rb +85 -0
- data/lib/cmdx/logger_ansi.rb +93 -7
- data/lib/cmdx/logger_serializer.rb +116 -0
- data/lib/cmdx/middleware.rb +74 -0
- data/lib/cmdx/middleware_registry.rb +106 -0
- data/lib/cmdx/middlewares/correlate.rb +266 -0
- data/lib/cmdx/middlewares/timeout.rb +232 -0
- data/lib/cmdx/parameter.rb +228 -1
- data/lib/cmdx/parameter_inspector.rb +61 -0
- data/lib/cmdx/parameter_registry.rb +125 -0
- data/lib/cmdx/parameter_serializer.rb +83 -0
- data/lib/cmdx/parameter_validator.rb +62 -0
- data/lib/cmdx/parameter_value.rb +109 -1
- data/lib/cmdx/parameters_inspector.rb +59 -0
- data/lib/cmdx/parameters_serializer.rb +102 -0
- data/lib/cmdx/railtie.rb +123 -3
- data/lib/cmdx/result.rb +367 -25
- data/lib/cmdx/result_ansi.rb +105 -9
- data/lib/cmdx/result_inspector.rb +76 -0
- data/lib/cmdx/result_logger.rb +90 -3
- data/lib/cmdx/result_serializer.rb +137 -0
- data/lib/cmdx/rspec/result_matchers.rb +917 -0
- data/lib/cmdx/rspec/task_matchers.rb +570 -0
- data/lib/cmdx/task.rb +405 -37
- data/lib/cmdx/task_serializer.rb +74 -2
- data/lib/cmdx/utils/ansi_color.rb +95 -0
- data/lib/cmdx/utils/log_timestamp.rb +48 -0
- data/lib/cmdx/utils/monotonic_runtime.rb +71 -4
- data/lib/cmdx/utils/name_affix.rb +78 -0
- data/lib/cmdx/validators/custom.rb +82 -0
- data/lib/cmdx/validators/exclusion.rb +94 -0
- data/lib/cmdx/validators/format.rb +102 -8
- data/lib/cmdx/validators/inclusion.rb +104 -0
- data/lib/cmdx/validators/length.rb +128 -0
- data/lib/cmdx/validators/numeric.rb +128 -0
- data/lib/cmdx/validators/presence.rb +93 -7
- data/lib/cmdx/version.rb +7 -1
- data/lib/cmdx/workflow.rb +394 -0
- data/lib/cmdx.rb +25 -64
- data/lib/generators/cmdx/install_generator.rb +37 -1
- data/lib/generators/cmdx/task_generator.rb +69 -1
- data/lib/generators/cmdx/templates/install.rb +43 -15
- data/lib/generators/cmdx/workflow_generator.rb +109 -0
- data/lib/locales/ar.yml +36 -0
- data/lib/locales/cs.yml +36 -0
- data/lib/locales/da.yml +36 -0
- data/lib/locales/de.yml +36 -0
- data/lib/locales/el.yml +36 -0
- data/lib/locales/en.yml +20 -20
- data/lib/locales/es.yml +20 -20
- data/lib/locales/fi.yml +36 -0
- data/lib/locales/fr.yml +36 -0
- data/lib/locales/he.yml +36 -0
- data/lib/locales/hi.yml +36 -0
- data/lib/locales/it.yml +36 -0
- data/lib/locales/ja.yml +36 -0
- data/lib/locales/ko.yml +36 -0
- data/lib/locales/nl.yml +36 -0
- data/lib/locales/no.yml +36 -0
- data/lib/locales/pl.yml +36 -0
- data/lib/locales/pt.yml +36 -0
- data/lib/locales/ru.yml +36 -0
- data/lib/locales/sv.yml +36 -0
- data/lib/locales/th.yml +36 -0
- data/lib/locales/tr.yml +36 -0
- data/lib/locales/vi.yml +36 -0
- data/lib/locales/zh.yml +36 -0
- metadata +77 -15
- data/docs/basics/run.md +0 -34
- data/docs/batch.md +0 -53
- data/docs/example.md +0 -82
- data/docs/hooks.md +0 -62
- data/lib/cmdx/batch.rb +0 -43
- data/lib/cmdx/parameters.rb +0 -35
- data/lib/cmdx/run.rb +0 -39
- data/lib/cmdx/run_inspector.rb +0 -26
- data/lib/cmdx/run_serializer.rb +0 -20
- data/lib/cmdx/task_hook.rb +0 -18
- data/lib/generators/cmdx/batch_generator.rb +0 -30
- /data/lib/generators/cmdx/templates/{batch.rb.tt → workflow.rb.tt} +0 -0
data/lib/cmdx/configuration.rb
CHANGED
@@ -2,24 +2,232 @@
|
|
2
2
|
|
3
3
|
module CMDx
|
4
4
|
|
5
|
+
##
|
6
|
+
# Provides global configuration management for CMDx framework settings.
|
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
|
84
|
+
class Configuration
|
85
|
+
|
86
|
+
# Default configuration values
|
87
|
+
DEFAULT_HALT = "failed"
|
88
|
+
|
89
|
+
# Configuration attributes
|
90
|
+
attr_accessor :logger, :middlewares, :callbacks, :task_halt, :workflow_halt
|
91
|
+
|
92
|
+
##
|
93
|
+
# Initializes a new configuration with default values.
|
94
|
+
#
|
95
|
+
# @example
|
96
|
+
# config = CMDx::Configuration.new
|
97
|
+
def initialize
|
98
|
+
@logger = ::Logger.new($stdout, formatter: CMDx::LogFormatters::Line.new)
|
99
|
+
@middlewares = MiddlewareRegistry.new
|
100
|
+
@callbacks = CallbackRegistry.new
|
101
|
+
@task_halt = DEFAULT_HALT
|
102
|
+
@workflow_halt = DEFAULT_HALT
|
103
|
+
end
|
104
|
+
|
105
|
+
##
|
106
|
+
# Returns a hash representation of the configuration.
|
107
|
+
# Used internally by the framework for configuration merging.
|
108
|
+
#
|
109
|
+
# @return [Hash] configuration attributes as a hash
|
110
|
+
# @example
|
111
|
+
# config = CMDx.configuration
|
112
|
+
# config.to_h #=> { logger: ..., task_halt: "failed", ... }
|
113
|
+
def to_h
|
114
|
+
{
|
115
|
+
logger: @logger,
|
116
|
+
middlewares: @middlewares,
|
117
|
+
callbacks: @callbacks,
|
118
|
+
task_halt: @task_halt,
|
119
|
+
workflow_halt: @workflow_halt
|
120
|
+
}
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
|
5
125
|
module_function
|
6
126
|
|
127
|
+
##
|
128
|
+
# Returns the current global configuration instance.
|
129
|
+
# Creates a new configuration with default values if none exists.
|
130
|
+
#
|
131
|
+
# The configuration is stored as a module-level variable and persists
|
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
|
142
|
+
# config = CMDx.configuration
|
143
|
+
# config.logger.class #=> Logger
|
7
144
|
def configuration
|
8
|
-
@configuration
|
145
|
+
return @configuration if @configuration
|
146
|
+
|
147
|
+
@configuration ||= Configuration.new
|
9
148
|
end
|
10
149
|
|
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
|
161
|
+
#
|
162
|
+
# @example Basic configuration
|
163
|
+
# CMDx.configure do |config|
|
164
|
+
# config.task_halt = ["failed", "skipped"]
|
165
|
+
# end
|
166
|
+
#
|
167
|
+
# @example Complex configuration with conditionals
|
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
|
176
|
+
#
|
177
|
+
|
178
|
+
# end
|
179
|
+
#
|
180
|
+
# @example Formatter configuration
|
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
|
11
193
|
def configure
|
12
|
-
|
194
|
+
raise ArgumentError, "block required" unless block_given?
|
195
|
+
|
196
|
+
config = configuration
|
197
|
+
yield(config)
|
198
|
+
config
|
13
199
|
end
|
14
200
|
|
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.
|
205
|
+
#
|
206
|
+
# @return [Configuration] the newly created configuration with default values
|
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
|
214
|
+
# CMDx.reset_configuration!
|
215
|
+
# CMDx.configuration.task_halt #=> "failed"
|
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?
|
226
|
+
#
|
227
|
+
# @note This method is primarily useful for testing or when you need
|
228
|
+
# to return to a known default state.
|
15
229
|
def reset_configuration!
|
16
|
-
@configuration =
|
17
|
-
logger: ::Logger.new($stdout, formatter: CMDx::LogFormatters::Line.new),
|
18
|
-
task_halt: CMDx::Result::FAILED,
|
19
|
-
task_timeout: nil,
|
20
|
-
batch_halt: CMDx::Result::FAILED,
|
21
|
-
batch_timeout: nil
|
22
|
-
)
|
230
|
+
@configuration = Configuration.new
|
23
231
|
end
|
24
232
|
|
25
233
|
end
|
data/lib/cmdx/context.rb
CHANGED
@@ -1,10 +1,181 @@
|
|
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.
|
8
|
+
#
|
9
|
+
# Context objects act as the data container for task execution, holding input parameters,
|
10
|
+
# intermediate results, and any data that needs to be shared between tasks. They support
|
11
|
+
# dynamic attribute assignment and provide a convenient API for data manipulation throughout
|
12
|
+
# the task execution lifecycle.
|
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
|
4
137
|
class Context < LazyStruct
|
5
138
|
|
6
|
-
|
7
|
-
|
139
|
+
##
|
140
|
+
# Builds a Context instance from the given input, with intelligent handling
|
141
|
+
# of existing Context objects to avoid unnecessary object creation.
|
142
|
+
#
|
143
|
+
# This factory method provides optimized Context creation by:
|
144
|
+
# - Returning existing Context objects if they're unfrozen (reusable)
|
145
|
+
# - Creating new Context objects for frozen contexts (immutable)
|
146
|
+
# - Converting hash-like objects into new Context instances
|
147
|
+
#
|
148
|
+
# @param context [Hash, Context, #to_h] input data for context creation
|
149
|
+
# @return [Context] a Context instance ready for task execution
|
150
|
+
#
|
151
|
+
# @example Creating context from hash
|
152
|
+
# context = Context.build(name: "John", age: 30)
|
153
|
+
# context.name #=> "John"
|
154
|
+
# context.age #=> 30
|
155
|
+
#
|
156
|
+
# @example Reusing unfrozen context
|
157
|
+
# original = Context.build(data: "test")
|
158
|
+
# reused = Context.build(original)
|
159
|
+
# original.object_id == reused.object_id #=> true
|
160
|
+
#
|
161
|
+
# @example Creating new context from frozen context
|
162
|
+
# original = Context.build(data: "test")
|
163
|
+
# original.freeze
|
164
|
+
# new_context = Context.build(original)
|
165
|
+
# original.object_id == new_context.object_id #=> false
|
166
|
+
#
|
167
|
+
# @example Converting ActionController::Parameters
|
168
|
+
# # In Rails controllers
|
169
|
+
# params = ActionController::Parameters.new(user: { name: "John" })
|
170
|
+
# context = Context.build(params.permit(:user))
|
171
|
+
# context.user #=> { name: "John" }
|
172
|
+
#
|
173
|
+
# @example Task execution with built context
|
174
|
+
# # CMDx automatically uses Context.build for task parameters
|
175
|
+
# result = ProcessOrderTask.call(order_id: 123, priority: "high")
|
176
|
+
# # Equivalent to:
|
177
|
+
# # context = Context.build(order_id: 123, priority: "high")
|
178
|
+
# # ProcessOrderTask.new(context).call
|
8
179
|
def self.build(context = {})
|
9
180
|
return context if context.is_a?(self) && !context.frozen?
|
10
181
|
|
data/lib/cmdx/core_ext/hash.rb
CHANGED
@@ -2,8 +2,50 @@
|
|
2
2
|
|
3
3
|
module CMDx
|
4
4
|
module CoreExt
|
5
|
+
# Extensions to Hash that provide CMDx-specific key access methods.
|
6
|
+
#
|
7
|
+
# HashExtensions adds flexible key access that works with both
|
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
26
|
module HashExtensions
|
6
27
|
|
28
|
+
# Fetch a value with automatic symbol/string key conversion.
|
29
|
+
#
|
30
|
+
# This method provides flexible key access by trying both the original
|
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.
|
34
|
+
#
|
35
|
+
# @param key [Symbol, String, Object] key to fetch
|
36
|
+
# @return [Object] value for the key or its converted equivalent
|
37
|
+
#
|
38
|
+
# @example Symbol to string conversion
|
39
|
+
# hash = {"name" => "John"}
|
40
|
+
# hash.__cmdx_fetch(:name) # => "John" (tries :name, then "name")
|
41
|
+
#
|
42
|
+
# @example String to symbol conversion
|
43
|
+
# hash = {name: "John"}
|
44
|
+
# hash.__cmdx_fetch("name") # => "John" (tries "name", then :name)
|
45
|
+
#
|
46
|
+
# @example Direct key access
|
47
|
+
# hash = {id: 123}
|
48
|
+
# hash.__cmdx_fetch(:id) # => 123 (direct match)
|
7
49
|
def __cmdx_fetch(key)
|
8
50
|
case key
|
9
51
|
when Symbol then fetch(key) { self[key.to_s] }
|
@@ -12,6 +54,20 @@ module CMDx
|
|
12
54
|
end
|
13
55
|
end
|
14
56
|
|
57
|
+
# Check if a key exists with automatic symbol/string conversion.
|
58
|
+
#
|
59
|
+
# This method checks for key existence by trying both the original
|
60
|
+
# key and its converted form. Returns true if either variant exists.
|
61
|
+
#
|
62
|
+
# @param key [Symbol, String, Object] key to check
|
63
|
+
# @return [Boolean] true if key exists in either format
|
64
|
+
#
|
65
|
+
# @example Symbol/string checking
|
66
|
+
# hash = {name: "John", "age" => 30}
|
67
|
+
# hash.__cmdx_key?(:name) # => true
|
68
|
+
# hash.__cmdx_key?("name") # => true (checks :name fallback)
|
69
|
+
# hash.__cmdx_key?(:age) # => true (checks "age" fallback)
|
70
|
+
# hash.__cmdx_key?(:missing) # => false
|
15
71
|
def __cmdx_key?(key)
|
16
72
|
key?(key) || key?(
|
17
73
|
case key
|
@@ -23,6 +79,21 @@ module CMDx
|
|
23
79
|
false
|
24
80
|
end
|
25
81
|
|
82
|
+
# Check if hash responds to a method or contains a key.
|
83
|
+
#
|
84
|
+
# This method extends respond_to? behavior to also check if the
|
85
|
+
# hash contains a key that matches the method name. This enables
|
86
|
+
# hash keys to be treated as virtual methods.
|
87
|
+
#
|
88
|
+
# @param key [Symbol, String] method name to check
|
89
|
+
# @param include_private [Boolean] whether to include private methods
|
90
|
+
# @return [Boolean] true if responds to method or contains key
|
91
|
+
#
|
92
|
+
# @example Method response checking
|
93
|
+
# hash = {name: "John"}
|
94
|
+
# hash.__cmdx_respond_to?(:name) # => true (has key :name)
|
95
|
+
# hash.__cmdx_respond_to?(:keys) # => true (real Hash method)
|
96
|
+
# hash.__cmdx_respond_to?(:missing) # => false
|
26
97
|
def __cmdx_respond_to?(key, include_private = false)
|
27
98
|
respond_to?(key.to_sym, include_private) || __cmdx_key?(key)
|
28
99
|
rescue NoMethodError
|
@@ -33,4 +104,5 @@ module CMDx
|
|
33
104
|
end
|
34
105
|
end
|
35
106
|
|
107
|
+
# Extend all hashes with CMDx utility methods
|
36
108
|
Hash.include(CMDx::CoreExt::HashExtensions)
|
data/lib/cmdx/core_ext/module.rb
CHANGED
@@ -2,8 +2,64 @@
|
|
2
2
|
|
3
3
|
module CMDx
|
4
4
|
module CoreExt
|
5
|
+
# Extensions to Module that provide CMDx-specific metaprogramming capabilities.
|
6
|
+
#
|
7
|
+
# ModuleExtensions adds method delegation and attribute setting functionality
|
8
|
+
# used throughout the CMDx framework. These methods enable declarative
|
9
|
+
# programming patterns and automatic method generation.
|
10
|
+
#
|
11
|
+
# @example Method delegation
|
12
|
+
# class Task
|
13
|
+
# __cmdx_attr_delegator :name, :email, to: :user
|
14
|
+
# __cmdx_attr_delegator :save, to: :record, private: true
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# @example Attribute settings
|
18
|
+
# class Task
|
19
|
+
# __cmdx_attr_setting :default_options, default: -> { {} }
|
20
|
+
# __cmdx_attr_setting :configuration, default: {}
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# @see Task Tasks that use module extensions for delegation
|
24
|
+
# @see Parameter Parameters that use attribute settings
|
5
25
|
module ModuleExtensions
|
6
26
|
|
27
|
+
# Create delegator methods that forward calls to another object.
|
28
|
+
#
|
29
|
+
# This method generates instance methods that delegate to methods on
|
30
|
+
# another object. It supports method visibility controls and optional
|
31
|
+
# missing method handling.
|
32
|
+
#
|
33
|
+
# @param methods [Array<Symbol>] method names to delegate
|
34
|
+
# @param options [Hash] delegation options
|
35
|
+
# @option options [Symbol] :to target object method name (required)
|
36
|
+
# @option options [Boolean] :allow_missing whether to allow missing methods
|
37
|
+
# @option options [Boolean] :private make delegated methods private
|
38
|
+
# @option options [Boolean] :protected make delegated methods protected
|
39
|
+
# @return [void]
|
40
|
+
#
|
41
|
+
# @example Basic delegation
|
42
|
+
# class User
|
43
|
+
# __cmdx_attr_delegator :first_name, :last_name, to: :profile
|
44
|
+
# # Creates: def first_name; profile.first_name; end
|
45
|
+
# end
|
46
|
+
#
|
47
|
+
# @example Private delegation
|
48
|
+
# class Task
|
49
|
+
# __cmdx_attr_delegator :validate, to: :validator, private: true
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
# @example Class delegation
|
53
|
+
# class Task
|
54
|
+
# __cmdx_attr_delegator :configuration, to: :class
|
55
|
+
# end
|
56
|
+
#
|
57
|
+
# @example With missing method handling
|
58
|
+
# class Task
|
59
|
+
# __cmdx_attr_delegator :optional_method, to: :service, allow_missing: true
|
60
|
+
# end
|
61
|
+
#
|
62
|
+
# @raise [NoMethodError] if target object doesn't respond to method and allow_missing is false
|
7
63
|
def __cmdx_attr_delegator(*methods, **options)
|
8
64
|
methods.each do |method|
|
9
65
|
method_name = Utils::NameAffix.call(method, options.fetch(:to), options)
|
@@ -27,6 +83,43 @@ module CMDx
|
|
27
83
|
end
|
28
84
|
end
|
29
85
|
|
86
|
+
# Create class-level attribute accessor with lazy evaluation and inheritance.
|
87
|
+
#
|
88
|
+
# This method generates a class method that provides lazy-loaded attribute
|
89
|
+
# access with inheritance support. Values are cached and can be initialized
|
90
|
+
# with default values or procs.
|
91
|
+
#
|
92
|
+
# @param method [Symbol] name of the attribute method
|
93
|
+
# @param options [Hash] attribute options
|
94
|
+
# @option options [Object, Proc] :default default value or proc to generate value
|
95
|
+
# @return [void]
|
96
|
+
#
|
97
|
+
# @example Simple attribute setting
|
98
|
+
# class Task
|
99
|
+
# __cmdx_attr_setting :timeout, default: 30
|
100
|
+
# end
|
101
|
+
# # Task.timeout => 30
|
102
|
+
#
|
103
|
+
# @example Dynamic default with proc
|
104
|
+
# class Task
|
105
|
+
# __cmdx_attr_setting :timestamp, default: -> { Time.now }
|
106
|
+
# end
|
107
|
+
# # Task.timestamp => current time (evaluated lazily)
|
108
|
+
#
|
109
|
+
# @example Inherited settings
|
110
|
+
# class BaseTask
|
111
|
+
# __cmdx_attr_setting :options, default: {retry: 3}
|
112
|
+
# end
|
113
|
+
#
|
114
|
+
# class ProcessTask < BaseTask
|
115
|
+
# end
|
116
|
+
# # ProcessTask.options => {retry: 3} (inherited from BaseTask)
|
117
|
+
#
|
118
|
+
# @example Hash settings (automatically duplicated)
|
119
|
+
# class Task
|
120
|
+
# __cmdx_attr_setting :config, default: {}
|
121
|
+
# end
|
122
|
+
# # Each class gets its own copy of the hash
|
30
123
|
def __cmdx_attr_setting(method, **options)
|
31
124
|
define_singleton_method(method) do
|
32
125
|
@cmd_facets ||= {}
|
@@ -45,4 +138,5 @@ module CMDx
|
|
45
138
|
end
|
46
139
|
end
|
47
140
|
|
141
|
+
# Extend all modules with CMDx utility methods
|
48
142
|
Module.include(CMDx::CoreExt::ModuleExtensions)
|