cmdx 1.19.0 → 1.21.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 (145) hide show
  1. checksums.yaml +4 -4
  2. data/.DS_Store +0 -0
  3. data/CHANGELOG.md +82 -16
  4. data/README.md +1 -1
  5. data/lib/cmdx/attribute.rb +82 -19
  6. data/lib/cmdx/attribute_registry.rb +79 -8
  7. data/lib/cmdx/attribute_value.rb +2 -2
  8. data/lib/cmdx/callback_registry.rb +60 -26
  9. data/lib/cmdx/chain.rb +34 -5
  10. data/lib/cmdx/coercion_registry.rb +42 -20
  11. data/lib/cmdx/coercions/array.rb +2 -2
  12. data/lib/cmdx/coercions/big_decimal.rb +1 -1
  13. data/lib/cmdx/coercions/boolean.rb +2 -2
  14. data/lib/cmdx/coercions/complex.rb +1 -1
  15. data/lib/cmdx/coercions/date.rb +1 -1
  16. data/lib/cmdx/coercions/date_time.rb +1 -1
  17. data/lib/cmdx/coercions/float.rb +1 -1
  18. data/lib/cmdx/coercions/hash.rb +1 -1
  19. data/lib/cmdx/coercions/integer.rb +1 -1
  20. data/lib/cmdx/coercions/rational.rb +1 -1
  21. data/lib/cmdx/coercions/string.rb +1 -1
  22. data/lib/cmdx/coercions/symbol.rb +1 -1
  23. data/lib/cmdx/coercions/time.rb +1 -1
  24. data/lib/cmdx/configuration.rb +38 -0
  25. data/lib/cmdx/context.rb +11 -8
  26. data/lib/cmdx/deprecator.rb +27 -14
  27. data/lib/cmdx/errors.rb +3 -4
  28. data/lib/cmdx/exception.rb +7 -0
  29. data/lib/cmdx/executor.rb +80 -53
  30. data/lib/cmdx/identifier.rb +4 -6
  31. data/lib/cmdx/locale.rb +32 -9
  32. data/lib/cmdx/middleware_registry.rb +43 -23
  33. data/lib/cmdx/middlewares/correlate.rb +4 -2
  34. data/lib/cmdx/middlewares/runtime.rb +18 -3
  35. data/lib/cmdx/middlewares/timeout.rb +11 -10
  36. data/lib/cmdx/parallelizer.rb +100 -0
  37. data/lib/cmdx/pipeline.rb +42 -23
  38. data/lib/cmdx/railtie.rb +1 -1
  39. data/lib/cmdx/result.rb +91 -19
  40. data/lib/cmdx/retry.rb +166 -0
  41. data/lib/cmdx/settings.rb +226 -0
  42. data/lib/cmdx/task.rb +62 -65
  43. data/lib/cmdx/utils/format.rb +17 -1
  44. data/lib/cmdx/utils/normalize.rb +52 -0
  45. data/lib/cmdx/utils/wrap.rb +38 -0
  46. data/lib/cmdx/validator_registry.rb +44 -19
  47. data/lib/cmdx/validators/absence.rb +1 -1
  48. data/lib/cmdx/validators/exclusion.rb +2 -2
  49. data/lib/cmdx/validators/format.rb +1 -1
  50. data/lib/cmdx/validators/inclusion.rb +2 -2
  51. data/lib/cmdx/validators/length.rb +1 -1
  52. data/lib/cmdx/validators/numeric.rb +1 -1
  53. data/lib/cmdx/validators/presence.rb +1 -1
  54. data/lib/cmdx/version.rb +1 -1
  55. data/lib/cmdx/workflow.rb +17 -0
  56. data/lib/cmdx.rb +12 -0
  57. data/lib/generators/cmdx/templates/install.rb +20 -5
  58. data/lib/locales/af.yml +2 -0
  59. data/lib/locales/ar.yml +2 -0
  60. data/lib/locales/az.yml +2 -0
  61. data/lib/locales/be.yml +2 -0
  62. data/lib/locales/bg.yml +2 -0
  63. data/lib/locales/bn.yml +2 -0
  64. data/lib/locales/bs.yml +2 -0
  65. data/lib/locales/ca.yml +2 -0
  66. data/lib/locales/cnr.yml +2 -0
  67. data/lib/locales/cs.yml +2 -0
  68. data/lib/locales/cy.yml +2 -0
  69. data/lib/locales/da.yml +2 -0
  70. data/lib/locales/de.yml +2 -0
  71. data/lib/locales/dz.yml +2 -0
  72. data/lib/locales/el.yml +2 -0
  73. data/lib/locales/en.yml +2 -0
  74. data/lib/locales/eo.yml +2 -0
  75. data/lib/locales/es.yml +2 -0
  76. data/lib/locales/et.yml +2 -0
  77. data/lib/locales/eu.yml +2 -0
  78. data/lib/locales/fa.yml +2 -0
  79. data/lib/locales/fi.yml +2 -0
  80. data/lib/locales/fr.yml +2 -0
  81. data/lib/locales/fy.yml +2 -0
  82. data/lib/locales/gd.yml +2 -0
  83. data/lib/locales/gl.yml +2 -0
  84. data/lib/locales/he.yml +2 -0
  85. data/lib/locales/hi.yml +2 -0
  86. data/lib/locales/hr.yml +2 -0
  87. data/lib/locales/hu.yml +2 -0
  88. data/lib/locales/hy.yml +2 -0
  89. data/lib/locales/id.yml +2 -0
  90. data/lib/locales/is.yml +2 -0
  91. data/lib/locales/it.yml +2 -0
  92. data/lib/locales/ja.yml +2 -0
  93. data/lib/locales/ka.yml +2 -0
  94. data/lib/locales/kk.yml +2 -0
  95. data/lib/locales/km.yml +2 -0
  96. data/lib/locales/kn.yml +2 -0
  97. data/lib/locales/ko.yml +2 -0
  98. data/lib/locales/lb.yml +2 -0
  99. data/lib/locales/lo.yml +2 -0
  100. data/lib/locales/lt.yml +2 -0
  101. data/lib/locales/lv.yml +2 -0
  102. data/lib/locales/mg.yml +2 -0
  103. data/lib/locales/mk.yml +2 -0
  104. data/lib/locales/ml.yml +2 -0
  105. data/lib/locales/mn.yml +2 -0
  106. data/lib/locales/mr-IN.yml +2 -0
  107. data/lib/locales/ms.yml +2 -0
  108. data/lib/locales/nb.yml +2 -0
  109. data/lib/locales/ne.yml +2 -0
  110. data/lib/locales/nl.yml +2 -0
  111. data/lib/locales/nn.yml +2 -0
  112. data/lib/locales/oc.yml +2 -0
  113. data/lib/locales/or.yml +2 -0
  114. data/lib/locales/pa.yml +2 -0
  115. data/lib/locales/pl.yml +2 -0
  116. data/lib/locales/pt.yml +2 -0
  117. data/lib/locales/rm.yml +2 -0
  118. data/lib/locales/ro.yml +2 -0
  119. data/lib/locales/ru.yml +2 -0
  120. data/lib/locales/sc.yml +2 -0
  121. data/lib/locales/sk.yml +2 -0
  122. data/lib/locales/sl.yml +2 -0
  123. data/lib/locales/sq.yml +2 -0
  124. data/lib/locales/sr.yml +2 -0
  125. data/lib/locales/st.yml +2 -0
  126. data/lib/locales/sv.yml +2 -0
  127. data/lib/locales/sw.yml +2 -0
  128. data/lib/locales/ta.yml +2 -0
  129. data/lib/locales/te.yml +2 -0
  130. data/lib/locales/th.yml +2 -0
  131. data/lib/locales/tl.yml +2 -0
  132. data/lib/locales/tr.yml +2 -0
  133. data/lib/locales/tt.yml +2 -0
  134. data/lib/locales/ug.yml +2 -0
  135. data/lib/locales/uk.yml +2 -0
  136. data/lib/locales/ur.yml +2 -0
  137. data/lib/locales/uz.yml +2 -0
  138. data/lib/locales/vi.yml +2 -0
  139. data/lib/locales/wo.yml +2 -0
  140. data/lib/locales/zh-CN.yml +2 -0
  141. data/lib/locales/zh-HK.yml +2 -0
  142. data/lib/locales/zh-TW.yml +2 -0
  143. data/lib/locales/zh-YUE.yml +2 -0
  144. data/mkdocs.yml +5 -1
  145. metadata +6 -15
@@ -5,19 +5,11 @@ module CMDx
5
5
  #
6
6
  # Provides a centralized way to register, deregister, and execute type coercions
7
7
  # for various data types including arrays, numbers, dates, and other primitives.
8
+ #
9
+ # Supports copy-on-write semantics: a duped registry shares the parent's
10
+ # data until a write operation triggers materialization.
8
11
  class CoercionRegistry
9
12
 
10
- # Returns the internal registry mapping coercion types to handler classes.
11
- #
12
- # @return [Hash{Symbol => Class}] Hash of coercion type names to coercion classes
13
- #
14
- # @example
15
- # registry.registry # => { integer: Coercions::Integer, boolean: Coercions::Boolean }
16
- #
17
- # @rbs @registry: Hash[Symbol, Class]
18
- attr_reader :registry
19
- alias to_h registry
20
-
21
13
  # Initialize a new coercion registry.
22
14
  #
23
15
  # @param registry [Hash{Symbol => Class}, nil] optional initial registry hash
@@ -44,17 +36,30 @@ module CMDx
44
36
  }
45
37
  end
46
38
 
47
- # Create a duplicate of this registry.
39
+ # Sets up copy-on-write state when duplicated via dup.
48
40
  #
49
- # @return [CoercionRegistry] a new instance with duplicated registry hash
41
+ # @param source [CoercionRegistry] The registry being duplicated
42
+ #
43
+ # @rbs (CoercionRegistry source) -> void
44
+ def initialize_dup(source)
45
+ @parent = source
46
+ @registry = nil
47
+ super
48
+ end
49
+
50
+ # Returns the internal registry mapping coercion types to handler classes.
51
+ # Delegates to the parent registry when not yet materialized.
52
+ #
53
+ # @return [Hash{Symbol => Class}] Hash of coercion type names to coercion classes
50
54
  #
51
55
  # @example
52
- # new_registry = registry.dup
56
+ # registry.registry # => { integer: Coercions::Integer, boolean: Coercions::Boolean }
53
57
  #
54
- # @rbs () -> CoercionRegistry
55
- def dup
56
- self.class.new(registry.dup)
58
+ # @rbs () -> Hash[Symbol, Class]
59
+ def registry
60
+ @registry || @parent.registry
57
61
  end
62
+ alias to_h registry
58
63
 
59
64
  # Register a new coercion handler for a type.
60
65
  #
@@ -69,7 +74,9 @@ module CMDx
69
74
  #
70
75
  # @rbs ((Symbol | String) name, Class coercion) -> self
71
76
  def register(name, coercion)
72
- registry[name.to_sym] = coercion
77
+ materialize!
78
+
79
+ @registry[name.to_sym] = coercion
73
80
  self
74
81
  end
75
82
 
@@ -85,7 +92,9 @@ module CMDx
85
92
  #
86
93
  # @rbs ((Symbol | String) name) -> self
87
94
  def deregister(name)
88
- registry.delete(name.to_sym)
95
+ materialize!
96
+
97
+ @registry.delete(name.to_sym)
89
98
  self
90
99
  end
91
100
 
@@ -106,11 +115,24 @@ module CMDx
106
115
  # result = registry.coerce(:boolean, task, "true", strict: true)
107
116
  #
108
117
  # @rbs (Symbol type, untyped task, untyped value, ?Hash[Symbol, untyped] options) -> untyped
109
- def coerce(type, task, value, options = {})
118
+ def coerce(type, task, value, options = EMPTY_HASH)
110
119
  raise TypeError, "unknown coercion type #{type.inspect}" unless registry.key?(type)
111
120
 
112
121
  Utils::Call.invoke(task, registry[type], value, options)
113
122
  end
114
123
 
124
+ private
125
+
126
+ # Copies the parent's registry data into this instance,
127
+ # severing the copy-on-write link.
128
+ #
129
+ # @rbs () -> void
130
+ def materialize!
131
+ return if @registry
132
+
133
+ @registry = @parent.registry.dup
134
+ @parent = nil
135
+ end
136
+
115
137
  end
116
138
  end
@@ -30,14 +30,14 @@ module CMDx
30
30
  # Array.call("[not json") # => raises CoercionError
31
31
  #
32
32
  # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Array[untyped]
33
- def call(value, options = {})
33
+ def call(value, options = EMPTY_HASH)
34
34
  if value.is_a?(::String) && (
35
35
  value.start_with?("[") ||
36
36
  value.strip == "null"
37
37
  )
38
38
  JSON.parse(value) || []
39
39
  else
40
- Array(value)
40
+ Utils::Wrap.array(value)
41
41
  end
42
42
  rescue JSON::ParserError
43
43
  type = Locale.t("cmdx.types.array")
@@ -31,7 +31,7 @@ module CMDx
31
31
  # BigDecimal.call(3.14159) # => #<BigDecimal:7f8b8c0d8e0f '0.314159E1',9(18)>
32
32
  #
33
33
  # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> BigDecimal
34
- def call(value, options = {})
34
+ def call(value, options = EMPTY_HASH)
35
35
  BigDecimal(value, options[:precision] || DEFAULT_PRECISION)
36
36
  rescue ArgumentError, TypeError
37
37
  type = Locale.t("cmdx.types.big_decimal")
@@ -43,9 +43,9 @@ module CMDx
43
43
  # Boolean.call("abc") # => raises CoercionError
44
44
  #
45
45
  # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> bool
46
- def call(value, options = {})
46
+ def call(value, options = EMPTY_HASH)
47
47
  case value.to_s
48
- when FALSEY, "" then false
48
+ when FALSEY, EMPTY_STRING then false
49
49
  when TRUTHY then true
50
50
  else
51
51
  type = Locale.t("cmdx.types.boolean")
@@ -29,7 +29,7 @@ module CMDx
29
29
  # Complex.call(Complex(1, 2)) # => (1+2i)
30
30
  #
31
31
  # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Complex
32
- def call(value, options = {})
32
+ def call(value, options = EMPTY_HASH)
33
33
  Complex(value)
34
34
  rescue ArgumentError, TypeError
35
35
  type = Locale.t("cmdx.types.complex")
@@ -32,7 +32,7 @@ module CMDx
32
32
  # Date.call(DateTime.new(2023, 12, 25)) # => #<Date: 2023-12-25>
33
33
  #
34
34
  # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Date
35
- def call(value, options = {})
35
+ def call(value, options = EMPTY_HASH)
36
36
  return value.to_date if value.respond_to?(:to_date)
37
37
  return ::Date.strptime(value, options[:strptime]) if options[:strptime]
38
38
 
@@ -32,7 +32,7 @@ module CMDx
32
32
  # DateTime.call(Time.new(2023, 12, 25)) # => #<DateTime: 2023-12-25T00:00:00+00:00>
33
33
  #
34
34
  # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> DateTime
35
- def call(value, options = {})
35
+ def call(value, options = EMPTY_HASH)
36
36
  return value.to_datetime if value.respond_to?(:to_datetime)
37
37
  return ::DateTime.strptime(value, options[:strptime]) if options[:strptime]
38
38
 
@@ -32,7 +32,7 @@ module CMDx
32
32
  # Float.call(Complex(5.0, 0)) # => 5.0
33
33
  #
34
34
  # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Float
35
- def call(value, options = {})
35
+ def call(value, options = EMPTY_HASH)
36
36
  Float(value)
37
37
  rescue ArgumentError, RangeError, TypeError
38
38
  type = Locale.t("cmdx.types.float")
@@ -33,7 +33,7 @@ module CMDx
33
33
  # Hash.call('{"key": "value"}') # => {"key" => "value"}
34
34
  #
35
35
  # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Hash[untyped, untyped]
36
- def call(value, options = {})
36
+ def call(value, options = EMPTY_HASH)
37
37
  if value.nil?
38
38
  {}
39
39
  elsif value.is_a?(::Hash)
@@ -35,7 +35,7 @@ module CMDx
35
35
  # Integer.call("abc") # => raises CoercionError
36
36
  #
37
37
  # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Integer
38
- def call(value, options = {})
38
+ def call(value, options = EMPTY_HASH)
39
39
  Integer(value)
40
40
  rescue ArgumentError, FloatDomainError, RangeError, TypeError
41
41
  type = Locale.t("cmdx.types.integer")
@@ -35,7 +35,7 @@ module CMDx
35
35
  # Rational.call(0) # => (0/1)
36
36
  #
37
37
  # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Rational
38
- def call(value, options = {})
38
+ def call(value, options = EMPTY_HASH)
39
39
  Rational(value)
40
40
  rescue ArgumentError, FloatDomainError, RangeError, TypeError, ZeroDivisionError
41
41
  type = Locale.t("cmdx.types.rational")
@@ -29,7 +29,7 @@ module CMDx
29
29
  # String.call(true) # => "true"
30
30
  #
31
31
  # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> String
32
- def call(value, options = {})
32
+ def call(value, options = EMPTY_HASH)
33
33
  String(value)
34
34
  end
35
35
 
@@ -28,7 +28,7 @@ module CMDx
28
28
  # Symbol.call(:existing) # => :existing
29
29
  #
30
30
  # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Symbol
31
- def call(value, options = {})
31
+ def call(value, options = EMPTY_HASH)
32
32
  value.to_sym
33
33
  rescue NoMethodError
34
34
  type = Locale.t("cmdx.types.symbol")
@@ -34,7 +34,7 @@ module CMDx
34
34
  # Time.call("12-25-2023", strptime: "%m-%d-%Y") # => Time object
35
35
  #
36
36
  # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Time
37
- def call(value, options = {})
37
+ def call(value, options = EMPTY_HASH)
38
38
  return value.to_time if value.respond_to?(:to_time)
39
39
  return ::Time.strptime(value, options[:strptime]) if options[:strptime]
40
40
 
@@ -123,6 +123,38 @@ module CMDx
123
123
  # @rbs @rollback_on: Array[String]
124
124
  attr_accessor :rollback_on
125
125
 
126
+ # Returns whether to include context data in hash representation output.
127
+ #
128
+ # @return [Boolean] true if context should be included (default: false)
129
+ #
130
+ # @example
131
+ # config.dump_context = true
132
+ #
133
+ # @rbs @dump_context: bool
134
+ attr_accessor :dump_context
135
+
136
+ # Returns whether to freeze task results after execution.
137
+ # Set to false in test environments to allow stubbing on result objects.
138
+ #
139
+ # @return [Boolean] true if results should be frozen (default: true)
140
+ #
141
+ # @example
142
+ # config.freeze_results = false
143
+ #
144
+ # @rbs @freeze_results: bool
145
+ attr_accessor :freeze_results
146
+
147
+ # Returns the default locale used for built-in translation lookups.
148
+ # Must match the basename of a YAML file in lib/locales/ (e.g. "en", "es", "ja").
149
+ #
150
+ # @return [String] The locale identifier (default: "en")
151
+ #
152
+ # @example
153
+ # config.default_locale = "es"
154
+ #
155
+ # @rbs @locale: String
156
+ attr_accessor :default_locale
157
+
126
158
  # Initializes a new Configuration instance with default values.
127
159
  #
128
160
  # Creates new registry instances for middlewares, callbacks, coercions, and
@@ -145,6 +177,9 @@ module CMDx
145
177
  @task_breakpoints = DEFAULT_BREAKPOINTS
146
178
  @workflow_breakpoints = DEFAULT_BREAKPOINTS
147
179
  @rollback_on = DEFAULT_ROLLPOINTS
180
+ @dump_context = false
181
+ @freeze_results = true
182
+ @default_locale = "en"
148
183
 
149
184
  @backtrace = false
150
185
  @backtrace_cleaner = nil
@@ -177,9 +212,12 @@ module CMDx
177
212
  task_breakpoints: @task_breakpoints,
178
213
  workflow_breakpoints: @workflow_breakpoints,
179
214
  rollback_on: @rollback_on,
215
+ freeze_results: @freeze_results,
216
+ default_locale: @default_locale,
180
217
  backtrace: @backtrace,
181
218
  backtrace_cleaner: @backtrace_cleaner,
182
219
  exception_handler: @exception_handler,
220
+ dump_context: @dump_context,
183
221
  logger: @logger
184
222
  }
185
223
  end
data/lib/cmdx/context.rb CHANGED
@@ -22,7 +22,7 @@ module CMDx
22
22
  attr_reader :table
23
23
  alias to_h table
24
24
 
25
- def_delegators :table, :each, :map
25
+ def_delegators :table, :keys, :values, :each, :each_key, :each_value, :map
26
26
 
27
27
  # Creates a new Context instance from the given arguments.
28
28
  #
@@ -160,8 +160,8 @@ module CMDx
160
160
  # context.to_h # => {name: "John", age: 30, city: "NYC"}
161
161
  #
162
162
  # @rbs (?untyped args) -> self
163
- def merge!(args = {})
164
- args.to_h.each { |key, value| self[key.to_sym] = value }
163
+ def merge!(args = EMPTY_HASH)
164
+ table.merge!(args.to_h.transform_keys(&:to_sym))
165
165
  self
166
166
  end
167
167
  alias merge merge!
@@ -214,7 +214,7 @@ module CMDx
214
214
  #
215
215
  # @rbs (untyped other) -> bool
216
216
  def eql?(other)
217
- other.is_a?(self.class) && (to_h == other.to_h)
217
+ other.is_a?(self.class) && (table == other.to_h)
218
218
  end
219
219
  alias == eql?
220
220
 
@@ -280,13 +280,15 @@ module CMDx
280
280
  #
281
281
  # @rbs (Symbol method_name, *untyped args, **untyped _kwargs) ?{ () -> untyped } -> untyped
282
282
  def method_missing(method_name, *args, **_kwargs, &)
283
- fetch(method_name) do
284
- str_name = method_name.to_s
285
- store(str_name.chop, args.first) if str_name.end_with?("=")
283
+ if method_name.end_with?("=")
284
+ store(method_name.name.chop, args.first)
285
+ else
286
+ table[method_name]
286
287
  end
287
288
  end
288
289
 
289
290
  # Checks if the object responds to a given method.
291
+ # Supports both getter access for existing keys and setter methods.
290
292
  #
291
293
  # @param method_name [Symbol] the method name to check
292
294
  # @param include_private [Boolean] whether to include private methods
@@ -296,11 +298,12 @@ module CMDx
296
298
  # @example
297
299
  # context = Context.new(name: "John")
298
300
  # context.respond_to?(:name) # => true
301
+ # context.respond_to?(:name=) # => true
299
302
  # context.respond_to?(:age) # => false
300
303
  #
301
304
  # @rbs (Symbol method_name, ?bool include_private) -> bool
302
305
  def respond_to_missing?(method_name, include_private = false)
303
- key?(method_name) || super
306
+ key?(method_name) || method_name.end_with?("=") || super
304
307
  end
305
308
 
306
309
  end
@@ -10,11 +10,23 @@ module CMDx
10
10
 
11
11
  extend self
12
12
 
13
+ # @rbs RAISE_REGEXP: Regexp
14
+ RAISE_REGEXP = /\Araise\z/
15
+ private_constant :RAISE_REGEXP
16
+
17
+ # @rbs LOG_REGEXP: Regexp
18
+ LOG_REGEXP = /\Alog\z/
19
+ private_constant :LOG_REGEXP
20
+
21
+ # @rbs WARN_REGEXP: Regexp
22
+ WARN_REGEXP = /\Awarn\z/
23
+ private_constant :WARN_REGEXP
24
+
13
25
  # @rbs EVAL: Proc
14
26
  EVAL = proc do |target, callable|
15
27
  case callable
16
- when /raise|log|warn/ then callable
17
28
  when NilClass, FalseClass, TrueClass then !!callable
29
+ when RAISE_REGEXP, LOG_REGEXP, WARN_REGEXP then callable
18
30
  when Symbol then target.send(callable)
19
31
  when Proc then target.instance_eval(&callable)
20
32
  else
@@ -28,14 +40,14 @@ module CMDx
28
40
  # Restricts task usage based on deprecation settings.
29
41
  #
30
42
  # @param task [Object] The task object to check for deprecation
31
- # @option task.class.settings[:deprecate] [Symbol, Proc, String, Boolean]
43
+ # @option task.class.settings.deprecate [Symbol, Proc, String, Boolean]
32
44
  # The deprecation configuration for the task
33
- # @option task.class.settings[:deprecate] :raise Raises DeprecationError
34
- # @option task.class.settings[:deprecate] :log Logs deprecation warning
35
- # @option task.class.settings[:deprecate] :warn Outputs warning to stderr
36
- # @option task.class.settings[:deprecate] true Raises DeprecationError
37
- # @option task.class.settings[:deprecate] false No action taken
38
- # @option task.class.settings[:deprecate] nil No action taken
45
+ # @option task.class.settings.deprecate :raise Raises DeprecationError
46
+ # @option task.class.settings.deprecate :log Logs deprecation warning
47
+ # @option task.class.settings.deprecate :warn Outputs warning to stderr
48
+ # @option task.class.settings.deprecate true Raises DeprecationError
49
+ # @option task.class.settings.deprecate false No action taken
50
+ # @option task.class.settings.deprecate nil No action taken
39
51
  #
40
52
  # @raise [DeprecationError] When deprecation type is :raise or true
41
53
  # @raise [RuntimeError] When deprecation type is unknown
@@ -49,13 +61,14 @@ module CMDx
49
61
  #
50
62
  # @rbs (Task task) -> void
51
63
  def restrict(task)
52
- type = EVAL.call(task, task.class.settings[:deprecate])
64
+ setting = task.class.settings.deprecate
65
+ return unless setting
53
66
 
54
- case type
55
- when NilClass, FalseClass # Do nothing
56
- when TrueClass, /raise/ then raise DeprecationError, "#{task.class.name} usage prohibited"
57
- when /log/ then task.logger.warn { "DEPRECATED: migrate to a replacement or discontinue use" }
58
- when /warn/ then warn("[#{task.class.name}] DEPRECATED: migrate to a replacement or discontinue use", category: :deprecated)
67
+ case type = EVAL.call(task, setting)
68
+ when NilClass, FalseClass then nil # Do nothing
69
+ when TrueClass, RAISE_REGEXP then raise DeprecationError, "#{task.class.name} usage prohibited"
70
+ when LOG_REGEXP then task.logger.warn { "DEPRECATED: migrate to a replacement or discontinue use" }
71
+ when WARN_REGEXP then warn("[#{task.class.name}] DEPRECATED: migrate to a replacement or discontinue use", category: :deprecated)
59
72
  else raise "unknown deprecation type #{type.inspect}"
60
73
  end
61
74
  end
data/lib/cmdx/errors.rb CHANGED
@@ -18,7 +18,7 @@ module CMDx
18
18
  # @rbs @messages: Hash[Symbol, Set[String]]
19
19
  attr_reader :messages
20
20
 
21
- def_delegators :messages, :empty?
21
+ def_delegators :messages, :any?, :clear, :empty?, :size
22
22
 
23
23
  # Initialize a new error collection.
24
24
  #
@@ -57,9 +57,8 @@ module CMDx
57
57
  #
58
58
  # @rbs (Symbol attribute) -> bool
59
59
  def for?(attribute)
60
- return false unless messages.key?(attribute)
61
-
62
- !messages[attribute].empty?
60
+ set = messages[attribute]
61
+ !set.nil? && !set.empty?
63
62
  end
64
63
 
65
64
  # Convert errors to a hash format with arrays of full messages.
@@ -29,6 +29,13 @@ module CMDx
29
29
  # of required functionality.
30
30
  UndefinedMethodError = Class.new(Error)
31
31
 
32
+ # Error raised when task execution exceeds the configured timeout limit.
33
+ #
34
+ # This error occurs when a task takes longer to execute than the specified
35
+ # time limit. Timeout errors are raised by Ruby's Timeout module and are
36
+ # caught by the middleware to properly fail the task with timeout information.
37
+ TimeoutError = Class.new(Interrupt)
38
+
32
39
  # Raised when attribute validation fails during task execution.
33
40
  #
34
41
  # This error occurs when a attribute value doesn't meet the validation criteria