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.
Files changed (169) hide show
  1. checksums.yaml +4 -4
  2. data/.cursor/prompts/rspec.md +20 -0
  3. data/.cursor/prompts/yardoc.md +8 -0
  4. data/.rubocop.yml +5 -0
  5. data/CHANGELOG.md +101 -49
  6. data/README.md +2 -1
  7. data/docs/ai_prompts.md +10 -0
  8. data/docs/basics/call.md +11 -2
  9. data/docs/basics/chain.md +10 -1
  10. data/docs/basics/context.md +9 -0
  11. data/docs/basics/setup.md +9 -0
  12. data/docs/callbacks.md +14 -37
  13. data/docs/configuration.md +68 -27
  14. data/docs/getting_started.md +11 -0
  15. data/docs/internationalization.md +148 -0
  16. data/docs/interruptions/exceptions.md +10 -1
  17. data/docs/interruptions/faults.md +11 -2
  18. data/docs/interruptions/halt.md +9 -0
  19. data/docs/logging.md +14 -4
  20. data/docs/middlewares.md +53 -43
  21. data/docs/outcomes/result.md +9 -0
  22. data/docs/outcomes/states.md +9 -0
  23. data/docs/outcomes/statuses.md +9 -0
  24. data/docs/parameters/coercions.md +58 -38
  25. data/docs/parameters/defaults.md +10 -1
  26. data/docs/parameters/definitions.md +9 -0
  27. data/docs/parameters/namespacing.md +9 -0
  28. data/docs/parameters/validations.md +8 -67
  29. data/docs/testing.md +22 -13
  30. data/docs/tips_and_tricks.md +9 -0
  31. data/docs/workflows.md +14 -4
  32. data/lib/cmdx/.DS_Store +0 -0
  33. data/lib/cmdx/callback.rb +36 -56
  34. data/lib/cmdx/callback_registry.rb +82 -73
  35. data/lib/cmdx/chain.rb +65 -122
  36. data/lib/cmdx/chain_inspector.rb +22 -115
  37. data/lib/cmdx/chain_serializer.rb +17 -148
  38. data/lib/cmdx/coercion.rb +49 -0
  39. data/lib/cmdx/coercion_registry.rb +94 -0
  40. data/lib/cmdx/coercions/array.rb +18 -36
  41. data/lib/cmdx/coercions/big_decimal.rb +21 -33
  42. data/lib/cmdx/coercions/boolean.rb +21 -40
  43. data/lib/cmdx/coercions/complex.rb +18 -31
  44. data/lib/cmdx/coercions/date.rb +20 -39
  45. data/lib/cmdx/coercions/date_time.rb +22 -39
  46. data/lib/cmdx/coercions/float.rb +19 -32
  47. data/lib/cmdx/coercions/hash.rb +22 -41
  48. data/lib/cmdx/coercions/integer.rb +20 -33
  49. data/lib/cmdx/coercions/rational.rb +20 -32
  50. data/lib/cmdx/coercions/string.rb +23 -31
  51. data/lib/cmdx/coercions/time.rb +24 -40
  52. data/lib/cmdx/coercions/virtual.rb +14 -31
  53. data/lib/cmdx/configuration.rb +57 -171
  54. data/lib/cmdx/context.rb +22 -165
  55. data/lib/cmdx/core_ext/hash.rb +42 -67
  56. data/lib/cmdx/core_ext/module.rb +35 -79
  57. data/lib/cmdx/core_ext/object.rb +63 -98
  58. data/lib/cmdx/correlator.rb +40 -156
  59. data/lib/cmdx/error.rb +37 -202
  60. data/lib/cmdx/errors.rb +165 -202
  61. data/lib/cmdx/fault.rb +55 -158
  62. data/lib/cmdx/faults.rb +26 -137
  63. data/lib/cmdx/immutator.rb +22 -109
  64. data/lib/cmdx/lazy_struct.rb +103 -187
  65. data/lib/cmdx/log_formatters/json.rb +14 -40
  66. data/lib/cmdx/log_formatters/key_value.rb +14 -40
  67. data/lib/cmdx/log_formatters/line.rb +14 -48
  68. data/lib/cmdx/log_formatters/logstash.rb +14 -57
  69. data/lib/cmdx/log_formatters/pretty_json.rb +14 -50
  70. data/lib/cmdx/log_formatters/pretty_key_value.rb +13 -46
  71. data/lib/cmdx/log_formatters/pretty_line.rb +16 -54
  72. data/lib/cmdx/log_formatters/raw.rb +19 -49
  73. data/lib/cmdx/logger.rb +20 -82
  74. data/lib/cmdx/logger_ansi.rb +18 -75
  75. data/lib/cmdx/logger_serializer.rb +24 -114
  76. data/lib/cmdx/middleware.rb +38 -60
  77. data/lib/cmdx/middleware_registry.rb +81 -77
  78. data/lib/cmdx/middlewares/correlate.rb +41 -226
  79. data/lib/cmdx/middlewares/timeout.rb +46 -185
  80. data/lib/cmdx/parameter.rb +120 -198
  81. data/lib/cmdx/parameter_evaluator.rb +231 -0
  82. data/lib/cmdx/parameter_inspector.rb +25 -56
  83. data/lib/cmdx/parameter_registry.rb +59 -84
  84. data/lib/cmdx/parameter_serializer.rb +23 -74
  85. data/lib/cmdx/railtie.rb +24 -107
  86. data/lib/cmdx/result.rb +254 -260
  87. data/lib/cmdx/result_ansi.rb +19 -85
  88. data/lib/cmdx/result_inspector.rb +27 -68
  89. data/lib/cmdx/result_logger.rb +18 -81
  90. data/lib/cmdx/result_serializer.rb +28 -132
  91. data/lib/cmdx/rspec/matchers.rb +28 -0
  92. data/lib/cmdx/rspec/result_matchers/be_executed.rb +42 -0
  93. data/lib/cmdx/rspec/result_matchers/be_failed_task.rb +94 -0
  94. data/lib/cmdx/rspec/result_matchers/be_skipped_task.rb +94 -0
  95. data/lib/cmdx/rspec/result_matchers/be_state_matchers.rb +59 -0
  96. data/lib/cmdx/rspec/result_matchers/be_status_matchers.rb +57 -0
  97. data/lib/cmdx/rspec/result_matchers/be_successful_task.rb +87 -0
  98. data/lib/cmdx/rspec/result_matchers/have_bad_outcome.rb +51 -0
  99. data/lib/cmdx/rspec/result_matchers/have_caused_failure.rb +58 -0
  100. data/lib/cmdx/rspec/result_matchers/have_chain_index.rb +59 -0
  101. data/lib/cmdx/rspec/result_matchers/have_context.rb +86 -0
  102. data/lib/cmdx/rspec/result_matchers/have_empty_metadata.rb +54 -0
  103. data/lib/cmdx/rspec/result_matchers/have_good_outcome.rb +52 -0
  104. data/lib/cmdx/rspec/result_matchers/have_metadata.rb +114 -0
  105. data/lib/cmdx/rspec/result_matchers/have_preserved_context.rb +66 -0
  106. data/lib/cmdx/rspec/result_matchers/have_received_thrown_failure.rb +64 -0
  107. data/lib/cmdx/rspec/result_matchers/have_runtime.rb +78 -0
  108. data/lib/cmdx/rspec/result_matchers/have_thrown_failure.rb +76 -0
  109. data/lib/cmdx/rspec/task_matchers/be_well_formed_task.rb +62 -0
  110. data/lib/cmdx/rspec/task_matchers/have_callback.rb +85 -0
  111. data/lib/cmdx/rspec/task_matchers/have_cmd_setting.rb +68 -0
  112. data/lib/cmdx/rspec/task_matchers/have_executed_callbacks.rb +92 -0
  113. data/lib/cmdx/rspec/task_matchers/have_middleware.rb +46 -0
  114. data/lib/cmdx/rspec/task_matchers/have_parameter.rb +181 -0
  115. data/lib/cmdx/task.rb +213 -425
  116. data/lib/cmdx/task_deprecator.rb +55 -0
  117. data/lib/cmdx/task_processor.rb +245 -0
  118. data/lib/cmdx/task_serializer.rb +22 -70
  119. data/lib/cmdx/utils/ansi_color.rb +13 -89
  120. data/lib/cmdx/utils/log_timestamp.rb +13 -42
  121. data/lib/cmdx/utils/monotonic_runtime.rb +13 -63
  122. data/lib/cmdx/utils/name_affix.rb +21 -71
  123. data/lib/cmdx/validator.rb +48 -0
  124. data/lib/cmdx/validator_registry.rb +86 -0
  125. data/lib/cmdx/validators/exclusion.rb +55 -94
  126. data/lib/cmdx/validators/format.rb +31 -85
  127. data/lib/cmdx/validators/inclusion.rb +65 -110
  128. data/lib/cmdx/validators/length.rb +117 -133
  129. data/lib/cmdx/validators/numeric.rb +123 -130
  130. data/lib/cmdx/validators/presence.rb +38 -79
  131. data/lib/cmdx/version.rb +1 -7
  132. data/lib/cmdx/workflow.rb +46 -339
  133. data/lib/cmdx.rb +1 -1
  134. data/lib/generators/cmdx/install_generator.rb +14 -31
  135. data/lib/generators/cmdx/task_generator.rb +39 -55
  136. data/lib/generators/cmdx/templates/install.rb +61 -11
  137. data/lib/generators/cmdx/workflow_generator.rb +41 -66
  138. data/lib/locales/ar.yml +35 -0
  139. data/lib/locales/cs.yml +35 -0
  140. data/lib/locales/da.yml +35 -0
  141. data/lib/locales/de.yml +35 -0
  142. data/lib/locales/el.yml +35 -0
  143. data/lib/locales/en.yml +19 -20
  144. data/lib/locales/es.yml +19 -20
  145. data/lib/locales/fi.yml +35 -0
  146. data/lib/locales/fr.yml +35 -0
  147. data/lib/locales/he.yml +35 -0
  148. data/lib/locales/hi.yml +35 -0
  149. data/lib/locales/it.yml +35 -0
  150. data/lib/locales/ja.yml +35 -0
  151. data/lib/locales/ko.yml +35 -0
  152. data/lib/locales/nl.yml +35 -0
  153. data/lib/locales/no.yml +35 -0
  154. data/lib/locales/pl.yml +35 -0
  155. data/lib/locales/pt.yml +35 -0
  156. data/lib/locales/ru.yml +35 -0
  157. data/lib/locales/sv.yml +35 -0
  158. data/lib/locales/th.yml +35 -0
  159. data/lib/locales/tr.yml +35 -0
  160. data/lib/locales/vi.yml +35 -0
  161. data/lib/locales/zh.yml +35 -0
  162. metadata +57 -8
  163. data/lib/cmdx/parameter_validator.rb +0 -81
  164. data/lib/cmdx/parameter_value.rb +0 -244
  165. data/lib/cmdx/parameters_inspector.rb +0 -72
  166. data/lib/cmdx/parameters_serializer.rb +0 -115
  167. data/lib/cmdx/rspec/result_matchers.rb +0 -917
  168. data/lib/cmdx/rspec/task_matchers.rb +0 -570
  169. data/lib/cmdx/validators/custom.rb +0 -102
@@ -2,119 +2,65 @@
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
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
- # Configuration attributes
90
- attr_accessor :logger, :middlewares, :callbacks, :task_halt, :workflow_halt
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
- # Initializes a new configuration with default values.
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 = ::Logger.new($stdout, formatter: CMDx::LogFormatters::Line.new)
99
- @middlewares = MiddlewareRegistry.new
100
- @callbacks = CallbackRegistry.new
101
- @task_halt = DEFAULT_HALT
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.configuration
112
- # config.to_h #=> { logger: ..., task_halt: "failed", ... }
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
- # Returns the current global configuration instance.
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
- # 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
76
+ # @example
142
77
  # config = CMDx.configuration
143
- # config.logger.class #=> 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 Basic configuration
89
+ # @example
163
90
  # CMDx.configure do |config|
164
- # config.task_halt = ["failed", "skipped"]
91
+ # config.task_halt = ["failed", "error"]
92
+ # config.logger.level = Logger::DEBUG
165
93
  # end
166
94
  #
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
95
+ # @yield [Configuration] The configuration instance
176
96
  #
177
-
178
- # end
97
+ # @return [Configuration] The configured instance
179
98
  #
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
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
- # @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
110
+ # @example
214
111
  # 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?
112
+ # CMDx.configuration.task_halt # => "failed"
226
113
  #
227
- # @note This method is primarily useful for testing or when you need
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 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
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 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
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, #to_h] input data for context creation
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
- # @example Creating context from hash
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
- # @example Reusing unfrozen context
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 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
24
+ # @example Build context from hash
25
+ # Context.build(name: "John", age: 30)
26
+ # # => #<CMDx::Context :name="John" :age=30>
166
27
  #
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" }
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 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
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
 
@@ -2,51 +2,28 @@
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
+ # 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
- # Fetch a value with automatic symbol/string key conversion.
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
- # 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.
13
+ # @param key [Symbol, String, Object] the key to fetch from the hash
34
14
  #
35
- # @param key [Symbol, String, Object] key to fetch
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 Symbol to string conversion
39
- # hash = {"name" => "John"}
40
- # hash.__cmdx_fetch(:name) # => "John" (tries :name, then "name")
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 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)
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
- # Check if a key exists with automatic symbol/string conversion.
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
- # This method checks for key existence by trying both the original
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
- # @param key [Symbol, String, Object] key to check
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 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
71
- def __cmdx_key?(key)
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
- # Check if hash responds to a method or contains a key.
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
- # 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.
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
- # @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
65
+ # @return [Boolean] true if the hash responds to the method or contains the key
91
66
  #
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
97
- def __cmdx_respond_to?(key, include_private = false)
98
- respond_to?(key.to_sym, include_private) || __cmdx_key?(key)
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
- __cmdx_key?(key)
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)