cmdx 1.1.2 → 1.5.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 (192) hide show
  1. checksums.yaml +4 -4
  2. data/.DS_Store +0 -0
  3. data/.cursor/prompts/docs.md +4 -1
  4. data/.cursor/prompts/llms.md +20 -0
  5. data/.cursor/prompts/rspec.md +4 -1
  6. data/.cursor/prompts/yardoc.md +3 -2
  7. data/.cursor/rules/cursor-instructions.mdc +56 -1
  8. data/.irbrc +6 -0
  9. data/.rubocop.yml +29 -18
  10. data/CHANGELOG.md +5 -133
  11. data/LLM.md +3317 -0
  12. data/README.md +68 -44
  13. data/docs/attributes/coercions.md +162 -0
  14. data/docs/attributes/defaults.md +90 -0
  15. data/docs/attributes/definitions.md +281 -0
  16. data/docs/attributes/naming.md +78 -0
  17. data/docs/attributes/validations.md +309 -0
  18. data/docs/basics/chain.md +56 -249
  19. data/docs/basics/context.md +56 -289
  20. data/docs/basics/execution.md +114 -0
  21. data/docs/basics/setup.md +37 -334
  22. data/docs/callbacks.md +89 -467
  23. data/docs/deprecation.md +91 -174
  24. data/docs/getting_started.md +212 -202
  25. data/docs/internationalization.md +11 -647
  26. data/docs/interruptions/exceptions.md +23 -198
  27. data/docs/interruptions/faults.md +71 -151
  28. data/docs/interruptions/halt.md +109 -186
  29. data/docs/logging.md +44 -256
  30. data/docs/middlewares.md +113 -426
  31. data/docs/outcomes/result.md +81 -228
  32. data/docs/outcomes/states.md +33 -221
  33. data/docs/outcomes/statuses.md +21 -311
  34. data/docs/tips_and_tricks.md +120 -70
  35. data/docs/workflows.md +99 -283
  36. data/lib/cmdx/.DS_Store +0 -0
  37. data/lib/cmdx/attribute.rb +229 -0
  38. data/lib/cmdx/attribute_registry.rb +94 -0
  39. data/lib/cmdx/attribute_value.rb +193 -0
  40. data/lib/cmdx/callback_registry.rb +69 -77
  41. data/lib/cmdx/chain.rb +56 -73
  42. data/lib/cmdx/coercion_registry.rb +52 -68
  43. data/lib/cmdx/coercions/array.rb +19 -18
  44. data/lib/cmdx/coercions/big_decimal.rb +20 -24
  45. data/lib/cmdx/coercions/boolean.rb +26 -25
  46. data/lib/cmdx/coercions/complex.rb +21 -22
  47. data/lib/cmdx/coercions/date.rb +25 -23
  48. data/lib/cmdx/coercions/date_time.rb +24 -25
  49. data/lib/cmdx/coercions/float.rb +25 -22
  50. data/lib/cmdx/coercions/hash.rb +31 -32
  51. data/lib/cmdx/coercions/integer.rb +30 -24
  52. data/lib/cmdx/coercions/rational.rb +29 -24
  53. data/lib/cmdx/coercions/string.rb +19 -22
  54. data/lib/cmdx/coercions/symbol.rb +37 -0
  55. data/lib/cmdx/coercions/time.rb +26 -25
  56. data/lib/cmdx/configuration.rb +49 -108
  57. data/lib/cmdx/context.rb +222 -44
  58. data/lib/cmdx/deprecator.rb +61 -0
  59. data/lib/cmdx/errors.rb +42 -252
  60. data/lib/cmdx/exceptions.rb +39 -0
  61. data/lib/cmdx/faults.rb +78 -39
  62. data/lib/cmdx/freezer.rb +51 -0
  63. data/lib/cmdx/identifier.rb +30 -0
  64. data/lib/cmdx/locale.rb +52 -0
  65. data/lib/cmdx/log_formatters/json.rb +21 -22
  66. data/lib/cmdx/log_formatters/key_value.rb +20 -22
  67. data/lib/cmdx/log_formatters/line.rb +15 -22
  68. data/lib/cmdx/log_formatters/logstash.rb +22 -23
  69. data/lib/cmdx/log_formatters/raw.rb +16 -22
  70. data/lib/cmdx/middleware_registry.rb +70 -74
  71. data/lib/cmdx/middlewares/correlate.rb +90 -54
  72. data/lib/cmdx/middlewares/runtime.rb +58 -0
  73. data/lib/cmdx/middlewares/timeout.rb +48 -68
  74. data/lib/cmdx/railtie.rb +12 -45
  75. data/lib/cmdx/result.rb +229 -314
  76. data/lib/cmdx/task.rb +194 -366
  77. data/lib/cmdx/utils/call.rb +49 -0
  78. data/lib/cmdx/utils/condition.rb +71 -0
  79. data/lib/cmdx/utils/format.rb +61 -0
  80. data/lib/cmdx/validator_registry.rb +63 -72
  81. data/lib/cmdx/validators/exclusion.rb +38 -67
  82. data/lib/cmdx/validators/format.rb +48 -49
  83. data/lib/cmdx/validators/inclusion.rb +43 -74
  84. data/lib/cmdx/validators/length.rb +91 -154
  85. data/lib/cmdx/validators/numeric.rb +87 -162
  86. data/lib/cmdx/validators/presence.rb +37 -50
  87. data/lib/cmdx/version.rb +1 -1
  88. data/lib/cmdx/worker.rb +178 -0
  89. data/lib/cmdx/workflow.rb +85 -81
  90. data/lib/cmdx.rb +19 -13
  91. data/lib/generators/cmdx/install_generator.rb +14 -13
  92. data/lib/generators/cmdx/task_generator.rb +25 -50
  93. data/lib/generators/cmdx/templates/install.rb +11 -46
  94. data/lib/generators/cmdx/templates/task.rb.tt +3 -2
  95. data/lib/locales/en.yml +18 -4
  96. data/src/cmdx-logo.png +0 -0
  97. metadata +32 -116
  98. data/docs/ai_prompts.md +0 -393
  99. data/docs/basics/call.md +0 -317
  100. data/docs/configuration.md +0 -344
  101. data/docs/parameters/coercions.md +0 -396
  102. data/docs/parameters/defaults.md +0 -335
  103. data/docs/parameters/definitions.md +0 -446
  104. data/docs/parameters/namespacing.md +0 -378
  105. data/docs/parameters/validations.md +0 -405
  106. data/docs/testing.md +0 -553
  107. data/lib/cmdx/callback.rb +0 -53
  108. data/lib/cmdx/chain_inspector.rb +0 -56
  109. data/lib/cmdx/chain_serializer.rb +0 -63
  110. data/lib/cmdx/coercion.rb +0 -57
  111. data/lib/cmdx/coercions/virtual.rb +0 -29
  112. data/lib/cmdx/core_ext/hash.rb +0 -83
  113. data/lib/cmdx/core_ext/module.rb +0 -98
  114. data/lib/cmdx/core_ext/object.rb +0 -125
  115. data/lib/cmdx/correlator.rb +0 -122
  116. data/lib/cmdx/error.rb +0 -67
  117. data/lib/cmdx/fault.rb +0 -140
  118. data/lib/cmdx/immutator.rb +0 -52
  119. data/lib/cmdx/lazy_struct.rb +0 -246
  120. data/lib/cmdx/log_formatters/pretty_json.rb +0 -40
  121. data/lib/cmdx/log_formatters/pretty_key_value.rb +0 -38
  122. data/lib/cmdx/log_formatters/pretty_line.rb +0 -41
  123. data/lib/cmdx/logger.rb +0 -49
  124. data/lib/cmdx/logger_ansi.rb +0 -68
  125. data/lib/cmdx/logger_serializer.rb +0 -116
  126. data/lib/cmdx/middleware.rb +0 -70
  127. data/lib/cmdx/parameter.rb +0 -312
  128. data/lib/cmdx/parameter_evaluator.rb +0 -231
  129. data/lib/cmdx/parameter_inspector.rb +0 -66
  130. data/lib/cmdx/parameter_registry.rb +0 -106
  131. data/lib/cmdx/parameter_serializer.rb +0 -59
  132. data/lib/cmdx/result_ansi.rb +0 -71
  133. data/lib/cmdx/result_inspector.rb +0 -71
  134. data/lib/cmdx/result_logger.rb +0 -59
  135. data/lib/cmdx/result_serializer.rb +0 -104
  136. data/lib/cmdx/rspec/matchers.rb +0 -28
  137. data/lib/cmdx/rspec/result_matchers/be_executed.rb +0 -42
  138. data/lib/cmdx/rspec/result_matchers/be_failed_task.rb +0 -94
  139. data/lib/cmdx/rspec/result_matchers/be_skipped_task.rb +0 -94
  140. data/lib/cmdx/rspec/result_matchers/be_state_matchers.rb +0 -59
  141. data/lib/cmdx/rspec/result_matchers/be_status_matchers.rb +0 -57
  142. data/lib/cmdx/rspec/result_matchers/be_successful_task.rb +0 -87
  143. data/lib/cmdx/rspec/result_matchers/have_bad_outcome.rb +0 -51
  144. data/lib/cmdx/rspec/result_matchers/have_caused_failure.rb +0 -58
  145. data/lib/cmdx/rspec/result_matchers/have_chain_index.rb +0 -59
  146. data/lib/cmdx/rspec/result_matchers/have_context.rb +0 -86
  147. data/lib/cmdx/rspec/result_matchers/have_empty_metadata.rb +0 -54
  148. data/lib/cmdx/rspec/result_matchers/have_good_outcome.rb +0 -52
  149. data/lib/cmdx/rspec/result_matchers/have_metadata.rb +0 -114
  150. data/lib/cmdx/rspec/result_matchers/have_preserved_context.rb +0 -66
  151. data/lib/cmdx/rspec/result_matchers/have_received_thrown_failure.rb +0 -64
  152. data/lib/cmdx/rspec/result_matchers/have_runtime.rb +0 -78
  153. data/lib/cmdx/rspec/result_matchers/have_thrown_failure.rb +0 -76
  154. data/lib/cmdx/rspec/task_matchers/be_well_formed_task.rb +0 -62
  155. data/lib/cmdx/rspec/task_matchers/have_callback.rb +0 -85
  156. data/lib/cmdx/rspec/task_matchers/have_cmd_setting.rb +0 -68
  157. data/lib/cmdx/rspec/task_matchers/have_executed_callbacks.rb +0 -92
  158. data/lib/cmdx/rspec/task_matchers/have_middleware.rb +0 -46
  159. data/lib/cmdx/rspec/task_matchers/have_parameter.rb +0 -181
  160. data/lib/cmdx/task_deprecator.rb +0 -58
  161. data/lib/cmdx/task_processor.rb +0 -246
  162. data/lib/cmdx/task_serializer.rb +0 -57
  163. data/lib/cmdx/utils/ansi_color.rb +0 -73
  164. data/lib/cmdx/utils/log_timestamp.rb +0 -36
  165. data/lib/cmdx/utils/monotonic_runtime.rb +0 -34
  166. data/lib/cmdx/utils/name_affix.rb +0 -52
  167. data/lib/cmdx/validator.rb +0 -57
  168. data/lib/generators/cmdx/templates/workflow.rb.tt +0 -7
  169. data/lib/generators/cmdx/workflow_generator.rb +0 -84
  170. data/lib/locales/ar.yml +0 -35
  171. data/lib/locales/cs.yml +0 -35
  172. data/lib/locales/da.yml +0 -35
  173. data/lib/locales/de.yml +0 -35
  174. data/lib/locales/el.yml +0 -35
  175. data/lib/locales/es.yml +0 -35
  176. data/lib/locales/fi.yml +0 -35
  177. data/lib/locales/fr.yml +0 -35
  178. data/lib/locales/he.yml +0 -35
  179. data/lib/locales/hi.yml +0 -35
  180. data/lib/locales/it.yml +0 -35
  181. data/lib/locales/ja.yml +0 -35
  182. data/lib/locales/ko.yml +0 -35
  183. data/lib/locales/nl.yml +0 -35
  184. data/lib/locales/no.yml +0 -35
  185. data/lib/locales/pl.yml +0 -35
  186. data/lib/locales/pt.yml +0 -35
  187. data/lib/locales/ru.yml +0 -35
  188. data/lib/locales/sv.yml +0 -35
  189. data/lib/locales/th.yml +0 -35
  190. data/lib/locales/tr.yml +0 -35
  191. data/lib/locales/vi.yml +0 -35
  192. data/lib/locales/zh.yml +0 -35
@@ -2,141 +2,94 @@
2
2
 
3
3
  module CMDx
4
4
 
5
- # Global configuration class for CMDx framework settings.
6
- #
7
- # Manages logging, middleware, callbacks, coercions, validators, and halt conditions
8
- # for the entire CMDx framework. The Configuration class provides centralized control
9
- # over framework behavior including task execution flow, error handling, and component
10
- # registration. All settings configured here become defaults for tasks and workflows
11
- # unless explicitly overridden at the task or workflow level.
12
- #
13
- # The configuration system supports both global and per-task customization, allowing
14
- # fine-grained control over framework behavior while maintaining sensible defaults.
5
+ # Configuration class that manages global settings for CMDx including middlewares,
6
+ # callbacks, coercions, validators, breakpoints, and logging.
15
7
  class Configuration
16
8
 
17
- DEFAULT_HALT = "failed"
18
-
19
- # @return [Logger] Logger instance for task execution logging
20
- attr_accessor :logger
21
-
22
- # @return [MiddlewareRegistry] Global middleware registry applied to all tasks
23
- attr_accessor :middlewares
24
-
25
- # @return [CallbackRegistry] Global callback registry applied to all tasks
26
- attr_accessor :callbacks
9
+ DEFAULT_BREAKPOINTS = %w[failed].freeze
27
10
 
28
- # @return [CoercionRegistry] Global coercion registry for custom parameter types
29
- attr_accessor :coercions
11
+ attr_accessor :middlewares, :callbacks, :coercions, :validators,
12
+ :task_breakpoints, :workflow_breakpoints, :logger
30
13
 
31
- # @return [ValidatorRegistry] Global validator registry for custom parameter validation
32
- attr_accessor :validators
33
-
34
- # @return [String, Array<String>] Result statuses that cause `call!` to raise faults
35
- attr_accessor :task_halt
36
-
37
- # @return [String, Array<String>] Result statuses that halt workflow execution
38
- attr_accessor :workflow_halt
39
-
40
- # Creates a new configuration instance with default settings.
14
+ # Initializes a new Configuration instance with default values.
41
15
  #
42
- # Initializes all configuration attributes with sensible defaults including
43
- # a stdout logger with line formatting, empty registries for extensibility
44
- # components, and default halt conditions for both tasks and workflows.
16
+ # Creates new registry instances for middlewares, callbacks, coercions, and
17
+ # validators. Sets default breakpoints and configures a basic logger.
45
18
  #
46
- # @return [Configuration] a new configuration instance with default settings
19
+ # @return [Configuration] a new Configuration instance
47
20
  #
48
- # @example Create a new configuration
21
+ # @example
49
22
  # config = Configuration.new
50
- # config.logger.class #=> Logger
51
- # config.task_halt #=> "failed"
23
+ # config.middlewares.class # => MiddlewareRegistry
24
+ # config.task_breakpoints # => ["failed"]
52
25
  def initialize
53
- @logger = ::Logger.new($stdout, formatter: CMDx::LogFormatters::Line.new)
54
- @middlewares = MiddlewareRegistry.new
55
- @callbacks = CallbackRegistry.new
56
- @coercions = CoercionRegistry.new
57
- @validators = ValidatorRegistry.new
58
- @task_halt = DEFAULT_HALT
59
- @workflow_halt = DEFAULT_HALT
26
+ @middlewares = MiddlewareRegistry.new
27
+ @callbacks = CallbackRegistry.new
28
+ @coercions = CoercionRegistry.new
29
+ @validators = ValidatorRegistry.new
30
+
31
+ @task_breakpoints = DEFAULT_BREAKPOINTS
32
+ @workflow_breakpoints = DEFAULT_BREAKPOINTS
33
+
34
+ @logger = Logger.new(
35
+ $stdout,
36
+ progname: "cmdx",
37
+ formatter: LogFormatters::Line.new,
38
+ level: Logger::INFO
39
+ )
60
40
  end
61
41
 
62
42
  # Converts the configuration to a hash representation.
63
43
  #
64
- # Creates a hash containing all configuration attributes for serialization,
65
- # inspection, or transfer between processes. The hash includes all registries
66
- # and settings in their current state.
67
- #
68
- # @return [Hash] hash representation of the configuration
69
- # @option return [Logger] :logger the configured logger instance
70
- # @option return [MiddlewareRegistry] :middlewares the middleware registry
71
- # @option return [CallbackRegistry] :callbacks the callback registry
72
- # @option return [CoercionRegistry] :coercions the coercion registry
73
- # @option return [ValidatorRegistry] :validators the validator registry
74
- # @option return [String, Array<String>] :task_halt the task halt configuration
75
- # @option return [String, Array<String>] :workflow_halt the workflow halt configuration
44
+ # @return [Hash<Symbol, Object>] hash containing all configuration values
76
45
  #
77
- # @example Convert configuration to hash
46
+ # @example
78
47
  # config = Configuration.new
79
- # hash = config.to_h
80
- # hash[:logger].class #=> Logger
81
- # hash[:task_halt] #=> "failed"
48
+ # config.to_h
49
+ # # => { middlewares: #<MiddlewareRegistry>, callbacks: #<CallbackRegistry>, ... }
82
50
  def to_h
83
51
  {
84
- logger: @logger,
85
52
  middlewares: @middlewares,
86
53
  callbacks: @callbacks,
87
54
  coercions: @coercions,
88
55
  validators: @validators,
89
- task_halt: @task_halt,
90
- workflow_halt: @workflow_halt
56
+ task_breakpoints: @task_breakpoints,
57
+ workflow_breakpoints: @workflow_breakpoints,
58
+ logger: @logger
91
59
  }
92
60
  end
93
61
 
94
62
  end
95
63
 
96
- module_function
64
+ extend self
97
65
 
98
- # Returns the current global configuration instance.
66
+ # Returns the global configuration instance, creating it if it doesn't exist.
99
67
  #
100
- # Provides access to the singleton configuration instance used by the entire
101
- # CMDx framework. Creates a new configuration with default settings if none
102
- # exists. This method is thread-safe and ensures only one configuration
103
- # instance exists per process.
68
+ # @return [Configuration] the global configuration instance
104
69
  #
105
- # @return [Configuration] the current global configuration instance
106
- #
107
- # @example Access global configuration
70
+ # @example
108
71
  # config = CMDx.configuration
109
- # config.logger.level = Logger::DEBUG
110
- # config.task_halt = ["failed", "skipped"]
72
+ # config.middlewares # => #<MiddlewareRegistry>
111
73
  def configuration
112
74
  return @configuration if @configuration
113
75
 
114
76
  @configuration ||= Configuration.new
115
77
  end
116
78
 
117
- # Configures the global CMDx settings using a block.
79
+ # Configures CMDx using a block that receives the configuration instance.
118
80
  #
119
- # Yields the current configuration instance to the provided block for
120
- # modification. This is the recommended way to configure CMDx as it
121
- # provides a clean DSL-like interface for setting up the framework.
81
+ # @param block [Proc] the configuration block
122
82
  #
123
- # @param block [Proc] configuration block that receives the configuration instance
83
+ # @yield [Configuration] the configuration instance to configure
124
84
  #
125
85
  # @return [Configuration] the configured configuration instance
126
86
  #
127
- # @raise [ArgumentError] if no block is provided
128
- #
129
- # @example Configure CMDx settings
130
- # CMDx.configure do |config|
131
- # config.logger.level = Logger::INFO
132
- # config.task_halt = ["failed", "skipped"]
133
- # config.middlewares.register(CMDx::Middlewares::Timeout.new(seconds: 30))
134
- # end
87
+ # @raise [ArgumentError] when no block is provided
135
88
  #
136
- # @example Configure with custom logger
89
+ # @example
137
90
  # CMDx.configure do |config|
138
- # config.logger = Rails.logger
139
- # config.logger.formatter = CMDx::LogFormatters::JSON.new
91
+ # config.task_breakpoints = ["failed", "skipped"]
92
+ # config.logger.level = Logger::DEBUG
140
93
  # end
141
94
  def configure
142
95
  raise ArgumentError, "block required" unless block_given?
@@ -146,25 +99,13 @@ module CMDx
146
99
  config
147
100
  end
148
101
 
149
- # Resets the global configuration to default settings.
102
+ # Resets the global configuration to a new instance with default values.
150
103
  #
151
- # Creates a new configuration instance with default settings, discarding
152
- # any existing configuration. This is useful for testing scenarios or
153
- # when you need to start with a clean configuration state.
154
- #
155
- # @return [Configuration] a new configuration instance with default settings
156
- #
157
- # @example Reset to defaults
158
- # CMDx.configure { |c| c.task_halt = ["failed", "skipped"] }
159
- # CMDx.configuration.task_halt #=> ["failed", "skipped"]
104
+ # @return [Configuration] the new configuration instance
160
105
  #
106
+ # @example
161
107
  # CMDx.reset_configuration!
162
- # CMDx.configuration.task_halt #=> "failed"
163
- #
164
- # @example Use in test setup
165
- # RSpec.configure do |config|
166
- # config.before(:each) { CMDx.reset_configuration! }
167
- # end
108
+ # # Configuration is now reset to defaults
168
109
  def reset_configuration!
169
110
  @configuration = Configuration.new
170
111
  end
data/lib/cmdx/context.rb CHANGED
@@ -1,53 +1,231 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CMDx
4
- # Execution context container for task parameter storage and access.
4
+ # A hash-like context object that provides a flexible way to store and access
5
+ # key-value pairs during task execution. Keys are automatically converted to
6
+ # symbols for consistency.
5
7
  #
6
- # Context provides normalized parameter storage for task execution, inheriting
7
- # from LazyStruct to provide flexible attribute access patterns. It serves as
8
- # the primary interface for storing and retrieving execution parameters, allowing
9
- # both hash-style and method-style attribute access with automatic key normalization.
10
- # Context instances are used throughout task execution to maintain parameter state
11
- # and provide consistent data access across the task lifecycle.
12
- class Context < LazyStruct
13
-
14
- # Creates or returns a Context instance from the provided input.
15
- #
16
- # This factory method normalizes various input types into a proper Context instance,
17
- # ensuring consistent context handling throughout the framework. If the input is
18
- # already a Context instance and not frozen, it returns the input unchanged to
19
- # avoid unnecessary object creation. Otherwise, it creates a new Context instance
20
- # with the provided data.
21
- #
22
- # @param context [Hash, Context, Object] input data to convert to Context
23
- # @option context [Object] any any attribute keys and values for context initialization
24
- #
25
- # @return [Context] a Context instance containing the provided data
26
- #
27
- # @example Create context from hash
28
- # context = Context.build(user_id: 123, action: "process")
29
- # context.user_id #=> 123
30
- # context.action #=> "process"
31
- #
32
- # @example Return existing unfrozen context
33
- # existing = Context.new(status: "active")
34
- # result = Context.build(existing)
35
- # result.equal?(existing) #=> true
36
- #
37
- # @example Create new context from frozen context
38
- # frozen_context = Context.new(data: "test").freeze
39
- # new_context = Context.build(frozen_context)
40
- # new_context.equal?(frozen_context) #=> false
41
- # new_context.data #=> "test"
42
- #
43
- # @example Create context from empty input
44
- # context = Context.build
45
- # context.class #=> CMDx::Context
46
- # context.to_h #=> {}
8
+ # The Context class extends Forwardable to delegate common hash methods and
9
+ # provides additional convenience methods for working with context data.
10
+ class Context
11
+
12
+ extend Forwardable
13
+
14
+ attr_reader :table
15
+ alias to_h table
16
+
17
+ def_delegators :table, :each, :map
18
+
19
+ # Creates a new Context instance from the given arguments.
20
+ #
21
+ # @param args [Hash, Object] arguments to initialize the context with
22
+ # @option args [Object] :key the key-value pairs to store in the context
23
+ #
24
+ # @return [Context] a new Context instance
25
+ #
26
+ # @raise [ArgumentError] when args doesn't respond to `to_h` or `to_hash`
27
+ #
28
+ # @example
29
+ # context = Context.new(name: "John", age: 30)
30
+ # context[:name] # => "John"
31
+ def initialize(args = {})
32
+ @table =
33
+ if args.respond_to?(:to_hash)
34
+ args.to_hash
35
+ elsif args.respond_to?(:to_h)
36
+ args.to_h
37
+ else
38
+ raise ArgumentError, "must respond to `to_h` or `to_hash`"
39
+ end.transform_keys(&:to_sym)
40
+ end
41
+
42
+ # Builds a Context instance, reusing existing unfrozen contexts when possible.
43
+ #
44
+ # @param context [Context, Object] the context to build from
45
+ # @option context [Object] :key the key-value pairs to store in the context
46
+ #
47
+ # @return [Context] a Context instance, either new or reused
48
+ #
49
+ # @example
50
+ # existing = Context.new(name: "John")
51
+ # built = Context.build(existing) # reuses existing context
52
+ # built.object_id == existing.object_id # => true
47
53
  def self.build(context = {})
48
- return context if context.is_a?(self) && !context.frozen?
54
+ if context.is_a?(self) && !context.frozen?
55
+ context
56
+ elsif context.respond_to?(:context)
57
+ build(context.context)
58
+ else
59
+ new(context)
60
+ end
61
+ end
62
+
63
+ # Retrieves a value from the context by key.
64
+ #
65
+ # @param key [String, Symbol] the key to retrieve
66
+ #
67
+ # @return [Object, nil] the value associated with the key, or nil if not found
68
+ #
69
+ # @example
70
+ # context = Context.new(name: "John")
71
+ # context[:name] # => "John"
72
+ # context["name"] # => "John" (automatically converted to symbol)
73
+ def [](key)
74
+ table[key.to_sym]
75
+ end
76
+
77
+ # Stores a key-value pair in the context.
78
+ #
79
+ # @param key [String, Symbol] the key to store
80
+ # @param value [Object] the value to store
81
+ #
82
+ # @return [Object] the stored value
83
+ #
84
+ # @example
85
+ # context = Context.new
86
+ # context.store(:name, "John")
87
+ # context[:name] # => "John"
88
+ def store(key, value)
89
+ table[key.to_sym] = value
90
+ end
91
+ alias []= store
92
+
93
+ # Fetches a value from the context by key, with optional default value.
94
+ #
95
+ # @param key [String, Symbol] the key to fetch
96
+ # @param default [Object] the default value if key is not found
97
+ #
98
+ # @yield [key] a block to compute the default value
99
+ #
100
+ # @return [Object] the value associated with the key, or the default/default block result
101
+ #
102
+ # @example
103
+ # context = Context.new(name: "John")
104
+ # context.fetch(:name) # => "John"
105
+ # context.fetch(:age, 25) # => 25
106
+ # context.fetch(:city) { |key| "Unknown #{key}" } # => "Unknown city"
107
+ def fetch(key, ...)
108
+ table.fetch(key.to_sym, ...)
109
+ end
110
+
111
+ # Merges the given arguments into the current context, modifying it in place.
112
+ #
113
+ # @param args [Hash, Object] arguments to merge into the context
114
+ # @option args [Object] :key the key-value pairs to merge
115
+ #
116
+ # @return [Context] self for method chaining
117
+ #
118
+ # @example
119
+ # context = Context.new(name: "John")
120
+ # context.merge!(age: 30, city: "NYC")
121
+ # context.to_h # => {name: "John", age: 30, city: "NYC"}
122
+ def merge!(args = {})
123
+ args.to_h.each { |key, value| self[key.to_sym] = value }
124
+ self
125
+ end
49
126
 
50
- new(context)
127
+ # Deletes a key-value pair from the context.
128
+ #
129
+ # @param key [String, Symbol] the key to delete
130
+ #
131
+ # @yield [key] a block to handle the case when key is not found
132
+ #
133
+ # @return [Object, nil] the deleted value, or the block result if key not found
134
+ #
135
+ # @example
136
+ # context = Context.new(name: "John", age: 30)
137
+ # context.delete!(:age) # => 30
138
+ # context.delete!(:city) { |key| "Key #{key} not found" } # => "Key city not found"
139
+ def delete!(key, &)
140
+ table.delete(key.to_sym, &)
141
+ end
142
+
143
+ # Compares this context with another object for equality.
144
+ #
145
+ # @param other [Object] the object to compare with
146
+ #
147
+ # @return [Boolean] true if other is a Context with the same data
148
+ #
149
+ # @example
150
+ # context1 = Context.new(name: "John")
151
+ # context2 = Context.new(name: "John")
152
+ # context1 == context2 # => true
153
+ def eql?(other)
154
+ other.is_a?(self.class) && (to_h == other.to_h)
155
+ end
156
+ alias == eql?
157
+
158
+ # Checks if the context contains a specific key.
159
+ #
160
+ # @param key [String, Symbol] the key to check
161
+ #
162
+ # @return [Boolean] true if the key exists in the context
163
+ #
164
+ # @example
165
+ # context = Context.new(name: "John")
166
+ # context.key?(:name) # => true
167
+ # context.key?(:age) # => false
168
+ def key?(key)
169
+ table.key?(key.to_sym)
170
+ end
171
+
172
+ # Digs into nested structures using the given keys.
173
+ #
174
+ # @param key [String, Symbol] the first key to dig with
175
+ # @param keys [Array<String, Symbol>] additional keys for deeper digging
176
+ #
177
+ # @return [Object, nil] the value found by digging, or nil if not found
178
+ #
179
+ # @example
180
+ # context = Context.new(user: {profile: {name: "John"}})
181
+ # context.dig(:user, :profile, :name) # => "John"
182
+ # context.dig(:user, :profile, :age) # => nil
183
+ def dig(key, *keys)
184
+ table.dig(key.to_sym, *keys)
185
+ end
186
+
187
+ # Converts the context to a string representation.
188
+ #
189
+ # @return [String] a formatted string representation of the context data
190
+ #
191
+ # @example
192
+ # context = Context.new(name: "John", age: 30)
193
+ # context.to_s # => "name: John, age: 30"
194
+ def to_s
195
+ Utils::Format.to_str(to_h)
196
+ end
197
+
198
+ private
199
+
200
+ # Handles method calls that don't match defined methods.
201
+ # Supports assignment-style calls (e.g., `name=`) and key access.
202
+ #
203
+ # @param method_name [Symbol] the method name that was called
204
+ # @param args [Array<Object>] arguments passed to the method
205
+ # @param _kwargs [Hash] keyword arguments (unused)
206
+ #
207
+ # @yield [Object] optional block
208
+ #
209
+ # @return [Object] the result of the method call
210
+ def method_missing(method_name, *args, **_kwargs, &)
211
+ fetch(method_name) do
212
+ store(method_name[0..-2], args.first) if method_name.end_with?("=")
213
+ end
214
+ end
215
+
216
+ # Checks if the object responds to a given method.
217
+ #
218
+ # @param method_name [Symbol] the method name to check
219
+ # @param include_private [Boolean] whether to include private methods
220
+ #
221
+ # @return [Boolean] true if the method can be called
222
+ #
223
+ # @example
224
+ # context = Context.new(name: "John")
225
+ # context.respond_to?(:name) # => true
226
+ # context.respond_to?(:age) # => false
227
+ def respond_to_missing?(method_name, include_private = false)
228
+ key?(method_name) || super
51
229
  end
52
230
 
53
231
  end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMDx
4
+ # Handles deprecation warnings and restrictions for tasks.
5
+ #
6
+ # The Deprecator module provides functionality to restrict usage of deprecated
7
+ # tasks based on configuration settings. It supports different deprecation
8
+ # behaviors including warnings, logging, and errors.
9
+ module Deprecator
10
+
11
+ extend self
12
+
13
+ EVAL = proc do |target, callable|
14
+ case callable
15
+ when /raise|log|warn/ then callable
16
+ when NilClass, FalseClass, TrueClass then !!callable
17
+ when Symbol then target.send(callable)
18
+ when Proc then target.instance_eval(&callable)
19
+ else
20
+ raise "cannot evaluate #{callable.inspect}" unless callable.respond_to?(:call)
21
+
22
+ callable.call(target)
23
+ end
24
+ end.freeze
25
+ private_constant :EVAL
26
+
27
+ # Restricts task usage based on deprecation settings.
28
+ #
29
+ # @param task [Object] The task object to check for deprecation
30
+ # @option task.class.settings[:deprecate] [Symbol, Proc, String, Boolean]
31
+ # The deprecation configuration for the task
32
+ # @option task.class.settings[:deprecate] :raise Raises DeprecationError
33
+ # @option task.class.settings[:deprecate] :log Logs deprecation warning
34
+ # @option task.class.settings[:deprecate] :warn Outputs warning to stderr
35
+ # @option task.class.settings[:deprecate] true Raises DeprecationError
36
+ # @option task.class.settings[:deprecate] false No action taken
37
+ # @option task.class.settings[:deprecate] nil No action taken
38
+ #
39
+ # @raise [DeprecationError] When deprecation type is :raise or true
40
+ # @raise [RuntimeError] When deprecation type is unknown
41
+ #
42
+ # @example
43
+ # class MyTask
44
+ # settings(deprecate: :warn)
45
+ # end
46
+ #
47
+ # MyTask.new # => [MyTask] DEPRECATED: migrate to replacement or discontinue use
48
+ def restrict(task)
49
+ type = EVAL.call(task, task.class.settings[:deprecate])
50
+
51
+ case type
52
+ when NilClass, FalseClass # Do nothing
53
+ when TrueClass, /raise/ then raise DeprecationError, "#{task.class.name} usage prohibited"
54
+ when /log/ then task.logger.warn { "DEPRECATED: migrate to replacement or discontinue use" }
55
+ when /warn/ then warn("[#{task.class.name}] DEPRECATED: migrate to replacement or discontinue use", category: :deprecated)
56
+ else raise "unknown deprecation type #{type.inspect}"
57
+ end
58
+ end
59
+
60
+ end
61
+ end