cmdx 0.4.0 → 1.0.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 (126) hide show
  1. checksums.yaml +4 -4
  2. data/.DS_Store +0 -0
  3. data/.cursor/rules/cursor-instructions.mdc +6 -0
  4. data/.rubocop.yml +16 -1
  5. data/.ruby-version +1 -1
  6. data/CHANGELOG.md +42 -1
  7. data/README.md +72 -25
  8. data/docs/ai_prompts.md +309 -0
  9. data/docs/basics/call.md +225 -14
  10. data/docs/basics/chain.md +271 -0
  11. data/docs/basics/context.md +232 -33
  12. data/docs/basics/setup.md +76 -12
  13. data/docs/callbacks.md +273 -0
  14. data/docs/configuration.md +158 -28
  15. data/docs/getting_started.md +134 -22
  16. data/docs/interruptions/exceptions.md +189 -11
  17. data/docs/interruptions/faults.md +187 -44
  18. data/docs/interruptions/halt.md +179 -35
  19. data/docs/logging.md +194 -53
  20. data/docs/middlewares.md +735 -0
  21. data/docs/outcomes/result.md +296 -10
  22. data/docs/outcomes/states.md +212 -19
  23. data/docs/outcomes/statuses.md +284 -18
  24. data/docs/parameters/coercions.md +402 -29
  25. data/docs/parameters/defaults.md +249 -25
  26. data/docs/parameters/definitions.md +238 -72
  27. data/docs/parameters/namespacing.md +250 -27
  28. data/docs/parameters/validations.md +193 -168
  29. data/docs/testing.md +550 -0
  30. data/docs/tips_and_tricks.md +95 -43
  31. data/docs/workflows.md +319 -0
  32. data/lib/cmdx/.DS_Store +0 -0
  33. data/lib/cmdx/callback.rb +69 -0
  34. data/lib/cmdx/callback_registry.rb +106 -0
  35. data/lib/cmdx/chain.rb +190 -0
  36. data/lib/cmdx/chain_inspector.rb +149 -0
  37. data/lib/cmdx/chain_serializer.rb +175 -0
  38. data/lib/cmdx/coercions/array.rb +37 -0
  39. data/lib/cmdx/coercions/big_decimal.rb +33 -0
  40. data/lib/cmdx/coercions/boolean.rb +41 -1
  41. data/lib/cmdx/coercions/complex.rb +31 -0
  42. data/lib/cmdx/coercions/date.rb +39 -0
  43. data/lib/cmdx/coercions/date_time.rb +39 -0
  44. data/lib/cmdx/coercions/float.rb +31 -0
  45. data/lib/cmdx/coercions/hash.rb +42 -0
  46. data/lib/cmdx/coercions/integer.rb +32 -0
  47. data/lib/cmdx/coercions/rational.rb +31 -0
  48. data/lib/cmdx/coercions/string.rb +31 -0
  49. data/lib/cmdx/coercions/time.rb +39 -0
  50. data/lib/cmdx/coercions/virtual.rb +31 -0
  51. data/lib/cmdx/configuration.rb +217 -9
  52. data/lib/cmdx/context.rb +173 -2
  53. data/lib/cmdx/core_ext/hash.rb +72 -0
  54. data/lib/cmdx/core_ext/module.rb +94 -0
  55. data/lib/cmdx/core_ext/object.rb +105 -0
  56. data/lib/cmdx/correlator.rb +217 -0
  57. data/lib/cmdx/error.rb +210 -8
  58. data/lib/cmdx/errors.rb +256 -1
  59. data/lib/cmdx/fault.rb +177 -2
  60. data/lib/cmdx/faults.rb +158 -2
  61. data/lib/cmdx/immutator.rb +121 -2
  62. data/lib/cmdx/lazy_struct.rb +261 -18
  63. data/lib/cmdx/log_formatters/json.rb +46 -0
  64. data/lib/cmdx/log_formatters/key_value.rb +46 -0
  65. data/lib/cmdx/log_formatters/line.rb +54 -0
  66. data/lib/cmdx/log_formatters/logstash.rb +64 -0
  67. data/lib/cmdx/log_formatters/pretty_json.rb +57 -0
  68. data/lib/cmdx/log_formatters/pretty_key_value.rb +51 -0
  69. data/lib/cmdx/log_formatters/pretty_line.rb +60 -0
  70. data/lib/cmdx/log_formatters/raw.rb +54 -0
  71. data/lib/cmdx/logger.rb +85 -0
  72. data/lib/cmdx/logger_ansi.rb +93 -7
  73. data/lib/cmdx/logger_serializer.rb +116 -0
  74. data/lib/cmdx/middleware.rb +74 -0
  75. data/lib/cmdx/middleware_registry.rb +106 -0
  76. data/lib/cmdx/middlewares/correlate.rb +266 -0
  77. data/lib/cmdx/middlewares/timeout.rb +232 -0
  78. data/lib/cmdx/parameter.rb +228 -1
  79. data/lib/cmdx/parameter_inspector.rb +61 -0
  80. data/lib/cmdx/parameter_registry.rb +125 -0
  81. data/lib/cmdx/parameter_serializer.rb +83 -0
  82. data/lib/cmdx/parameter_validator.rb +62 -0
  83. data/lib/cmdx/parameter_value.rb +109 -1
  84. data/lib/cmdx/parameters_inspector.rb +59 -0
  85. data/lib/cmdx/parameters_serializer.rb +102 -0
  86. data/lib/cmdx/railtie.rb +123 -3
  87. data/lib/cmdx/result.rb +399 -20
  88. data/lib/cmdx/result_ansi.rb +105 -9
  89. data/lib/cmdx/result_inspector.rb +76 -0
  90. data/lib/cmdx/result_logger.rb +90 -3
  91. data/lib/cmdx/result_serializer.rb +137 -0
  92. data/lib/cmdx/rspec/result_matchers.rb +917 -0
  93. data/lib/cmdx/rspec/task_matchers.rb +570 -0
  94. data/lib/cmdx/task.rb +409 -34
  95. data/lib/cmdx/task_serializer.rb +74 -2
  96. data/lib/cmdx/utils/ansi_color.rb +95 -0
  97. data/lib/cmdx/utils/log_timestamp.rb +48 -0
  98. data/lib/cmdx/utils/monotonic_runtime.rb +71 -4
  99. data/lib/cmdx/utils/name_affix.rb +78 -0
  100. data/lib/cmdx/validators/custom.rb +82 -0
  101. data/lib/cmdx/validators/exclusion.rb +94 -0
  102. data/lib/cmdx/validators/format.rb +102 -8
  103. data/lib/cmdx/validators/inclusion.rb +104 -0
  104. data/lib/cmdx/validators/length.rb +128 -0
  105. data/lib/cmdx/validators/numeric.rb +128 -0
  106. data/lib/cmdx/validators/presence.rb +93 -7
  107. data/lib/cmdx/version.rb +7 -1
  108. data/lib/cmdx/workflow.rb +394 -0
  109. data/lib/cmdx.rb +25 -64
  110. data/lib/generators/cmdx/install_generator.rb +37 -1
  111. data/lib/generators/cmdx/task_generator.rb +69 -1
  112. data/lib/generators/cmdx/templates/install.rb +8 -12
  113. data/lib/generators/cmdx/workflow_generator.rb +109 -0
  114. metadata +54 -15
  115. data/docs/basics/run.md +0 -34
  116. data/docs/batch.md +0 -53
  117. data/docs/example.md +0 -82
  118. data/docs/hooks.md +0 -59
  119. data/lib/cmdx/batch.rb +0 -43
  120. data/lib/cmdx/parameters.rb +0 -34
  121. data/lib/cmdx/run.rb +0 -38
  122. data/lib/cmdx/run_inspector.rb +0 -26
  123. data/lib/cmdx/run_serializer.rb +0 -16
  124. data/lib/cmdx/task_hook.rb +0 -18
  125. data/lib/generators/cmdx/batch_generator.rb +0 -30
  126. /data/lib/generators/cmdx/templates/{batch.rb.tt → workflow.rb.tt} +0 -0
@@ -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 || reset_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
- yield(configuration)
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 = LazyStruct.new(
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
- attr_reader :run
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
 
@@ -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)
@@ -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)