cmdx 1.7.5 → 1.9.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 (150) hide show
  1. checksums.yaml +4 -4
  2. data/.DS_Store +0 -0
  3. data/.cursor/prompts/docs.md +3 -3
  4. data/.cursor/prompts/llms.md +1 -3
  5. data/.cursor/prompts/rspec.md +1 -1
  6. data/.irbrc +14 -2
  7. data/CHANGELOG.md +62 -29
  8. data/LLM.md +203 -78
  9. data/README.md +23 -85
  10. data/docs/.DS_Store +0 -0
  11. data/docs/assets/favicon.ico +0 -0
  12. data/docs/assets/favicon.svg +1 -0
  13. data/docs/attributes/coercions.md +19 -29
  14. data/docs/attributes/defaults.md +3 -16
  15. data/docs/attributes/definitions.md +29 -39
  16. data/docs/attributes/naming.md +3 -13
  17. data/docs/attributes/transformations.md +63 -0
  18. data/docs/attributes/validations.md +23 -40
  19. data/docs/basics/chain.md +14 -23
  20. data/docs/basics/context.md +13 -22
  21. data/docs/basics/execution.md +8 -26
  22. data/docs/basics/setup.md +8 -19
  23. data/docs/callbacks.md +19 -32
  24. data/docs/deprecation.md +8 -25
  25. data/docs/getting_started.md +101 -77
  26. data/docs/index.md +120 -0
  27. data/docs/internationalization.md +6 -18
  28. data/docs/interruptions/exceptions.md +10 -16
  29. data/docs/interruptions/faults.md +8 -25
  30. data/docs/interruptions/halt.md +31 -25
  31. data/docs/logging.md +7 -17
  32. data/docs/middlewares.md +13 -29
  33. data/docs/outcomes/result.md +21 -38
  34. data/docs/outcomes/states.md +8 -22
  35. data/docs/outcomes/statuses.md +10 -21
  36. data/docs/stylesheets/extra.css +42 -0
  37. data/docs/tips_and_tricks.md +7 -46
  38. data/docs/workflows.md +23 -38
  39. data/examples/active_record_query_tagging.md +46 -0
  40. data/examples/paper_trail_whatdunnit.md +39 -0
  41. data/lib/cmdx/attribute.rb +9 -2
  42. data/lib/cmdx/attribute_value.rb +31 -10
  43. data/lib/cmdx/callback_registry.rb +12 -2
  44. data/lib/cmdx/coercions/hash.rb +6 -1
  45. data/lib/cmdx/configuration.rb +10 -2
  46. data/lib/cmdx/deprecator.rb +3 -3
  47. data/lib/cmdx/errors.rb +1 -1
  48. data/lib/cmdx/executor.rb +97 -9
  49. data/lib/cmdx/log_formatters/logstash.rb +4 -4
  50. data/lib/cmdx/pipeline.rb +4 -4
  51. data/lib/cmdx/railtie.rb +9 -0
  52. data/lib/cmdx/result.rb +10 -1
  53. data/lib/cmdx/task.rb +12 -7
  54. data/lib/cmdx/version.rb +1 -1
  55. data/lib/cmdx.rb +1 -0
  56. data/lib/generators/cmdx/templates/install.rb +9 -0
  57. data/lib/locales/af.yml +2 -2
  58. data/lib/locales/ar.yml +2 -2
  59. data/lib/locales/az.yml +2 -2
  60. data/lib/locales/be.yml +2 -2
  61. data/lib/locales/bg.yml +2 -2
  62. data/lib/locales/bn.yml +2 -2
  63. data/lib/locales/bs.yml +2 -2
  64. data/lib/locales/ca.yml +2 -2
  65. data/lib/locales/cnr.yml +2 -2
  66. data/lib/locales/cs.yml +2 -2
  67. data/lib/locales/cy.yml +2 -2
  68. data/lib/locales/da.yml +2 -2
  69. data/lib/locales/de.yml +2 -2
  70. data/lib/locales/dz.yml +2 -2
  71. data/lib/locales/el.yml +2 -2
  72. data/lib/locales/en.yml +2 -2
  73. data/lib/locales/eo.yml +2 -2
  74. data/lib/locales/es.yml +2 -2
  75. data/lib/locales/et.yml +2 -2
  76. data/lib/locales/eu.yml +2 -2
  77. data/lib/locales/fa.yml +2 -2
  78. data/lib/locales/fi.yml +2 -2
  79. data/lib/locales/fr.yml +2 -2
  80. data/lib/locales/fy.yml +2 -2
  81. data/lib/locales/gd.yml +2 -2
  82. data/lib/locales/gl.yml +2 -2
  83. data/lib/locales/he.yml +2 -2
  84. data/lib/locales/hi.yml +2 -2
  85. data/lib/locales/hr.yml +2 -2
  86. data/lib/locales/hu.yml +2 -2
  87. data/lib/locales/hy.yml +2 -2
  88. data/lib/locales/id.yml +2 -2
  89. data/lib/locales/is.yml +2 -2
  90. data/lib/locales/it.yml +2 -2
  91. data/lib/locales/ja.yml +2 -2
  92. data/lib/locales/ka.yml +2 -2
  93. data/lib/locales/kk.yml +2 -2
  94. data/lib/locales/km.yml +2 -2
  95. data/lib/locales/kn.yml +2 -2
  96. data/lib/locales/ko.yml +2 -2
  97. data/lib/locales/lb.yml +2 -2
  98. data/lib/locales/lo.yml +2 -2
  99. data/lib/locales/lt.yml +2 -2
  100. data/lib/locales/lv.yml +2 -2
  101. data/lib/locales/mg.yml +2 -2
  102. data/lib/locales/mk.yml +2 -2
  103. data/lib/locales/ml.yml +2 -2
  104. data/lib/locales/mn.yml +2 -2
  105. data/lib/locales/mr-IN.yml +2 -2
  106. data/lib/locales/ms.yml +2 -2
  107. data/lib/locales/nb.yml +2 -2
  108. data/lib/locales/ne.yml +2 -2
  109. data/lib/locales/nl.yml +2 -2
  110. data/lib/locales/nn.yml +2 -2
  111. data/lib/locales/oc.yml +2 -2
  112. data/lib/locales/or.yml +2 -2
  113. data/lib/locales/pa.yml +2 -2
  114. data/lib/locales/pl.yml +2 -2
  115. data/lib/locales/pt.yml +2 -2
  116. data/lib/locales/rm.yml +2 -2
  117. data/lib/locales/ro.yml +2 -2
  118. data/lib/locales/ru.yml +2 -2
  119. data/lib/locales/sc.yml +2 -2
  120. data/lib/locales/sk.yml +2 -2
  121. data/lib/locales/sl.yml +2 -2
  122. data/lib/locales/sq.yml +2 -2
  123. data/lib/locales/sr.yml +2 -2
  124. data/lib/locales/st.yml +2 -2
  125. data/lib/locales/sv.yml +2 -2
  126. data/lib/locales/sw.yml +2 -2
  127. data/lib/locales/ta.yml +2 -2
  128. data/lib/locales/te.yml +2 -2
  129. data/lib/locales/th.yml +2 -2
  130. data/lib/locales/tl.yml +2 -2
  131. data/lib/locales/tr.yml +2 -2
  132. data/lib/locales/tt.yml +2 -2
  133. data/lib/locales/ug.yml +2 -2
  134. data/lib/locales/uk.yml +2 -2
  135. data/lib/locales/ur.yml +2 -2
  136. data/lib/locales/uz.yml +2 -2
  137. data/lib/locales/vi.yml +2 -2
  138. data/lib/locales/wo.yml +2 -2
  139. data/lib/locales/zh-CN.yml +2 -2
  140. data/lib/locales/zh-HK.yml +2 -2
  141. data/lib/locales/zh-TW.yml +2 -2
  142. data/lib/locales/zh-YUE.yml +2 -2
  143. data/mkdocs.yml +122 -0
  144. data/src/cmdx-dark-logo.png +0 -0
  145. data/src/cmdx-favicon.svg +1 -0
  146. data/src/cmdx-light-logo.png +0 -0
  147. data/src/cmdx-logo.svg +1 -0
  148. metadata +15 -4
  149. data/lib/cmdx/freezer.rb +0 -51
  150. data/src/cmdx-logo.png +0 -0
@@ -0,0 +1,39 @@
1
+ # Paper Trail Whatdunnit
2
+
3
+ Tag paper trail version records with which service made a change with a custom `whatdunnit` attribute.
4
+
5
+ <https://github.com/paper-trail-gem/paper_trail?tab=readme-ov-file#4c-storing-metadata>
6
+
7
+ ### Setup
8
+
9
+ ```ruby
10
+ # lib/cmdx_paper_trail_middleware.rb
11
+ class CmdxPaperTrailMiddleware
12
+ def self.call(task, **options, &)
13
+ # This makes sure to reset the whatdunnit value to the previous
14
+ # value for nested task calls
15
+
16
+ begin
17
+ PaperTrail.request.controller_info ||= {}
18
+ old_whatdunnit = PaperTrail.request.controller_info[:whatdunnit]
19
+ PaperTrail.request.controller_info[:whatdunnit] = task.class.name
20
+ yield
21
+ ensure
22
+ PaperTrail.request.controller_info[:whatdunnit] = old_whatdunnit
23
+ end
24
+ end
25
+ end
26
+ ```
27
+
28
+ ### Usage
29
+
30
+ ```ruby
31
+ class MyTask < CMDx::Task
32
+ register :middleware, CmdxPaperTrailMiddleware
33
+
34
+ def work
35
+ # Do work...
36
+ end
37
+
38
+ end
39
+ ```
@@ -66,7 +66,7 @@ module CMDx
66
66
  if names.none?
67
67
  raise ArgumentError, "no attributes given"
68
68
  elsif (names.size > 1) && options.key?(:as)
69
- raise ArgumentError, ":as option only supports one attribute per definition"
69
+ raise ArgumentError, "the :as option only supports one attribute per definition"
70
70
  end
71
71
 
72
72
  names.filter_map { |name| new(name, **options, &) }
@@ -212,7 +212,14 @@ module CMDx
212
212
  #
213
213
  # @raise [RuntimeError] When the method name is already defined on the task
214
214
  def define_and_verify
215
- raise "#{task.class.name}##{method_name} already defined" if task.respond_to?(method_name, true)
215
+ if task.respond_to?(method_name, true)
216
+ raise <<~MESSAGE
217
+ The method #{method_name.inspect} is already defined on the #{task.class.name} task.
218
+ This may be due conflicts with one of the task's user defined or internal methods/attributes.
219
+
220
+ Use :as, :prefix, and/or :suffix attribute options to avoid conflicts with existing methods.
221
+ MESSAGE
222
+ end
216
223
 
217
224
  attribute_value = AttributeValue.new(self)
218
225
  attribute_value.generate
@@ -51,9 +51,10 @@ module CMDx
51
51
  return if errors.for?(method_name)
52
52
 
53
53
  coerced_value = coerce_value(derived_value)
54
+ transformed_value = transform_value(coerced_value)
54
55
  return if errors.for?(method_name)
55
56
 
56
- attributes[method_name] = coerced_value
57
+ attributes[method_name] = transformed_value
57
58
  end
58
59
 
59
60
  # Validates the current attribute value against configured validators.
@@ -113,7 +114,7 @@ module CMDx
113
114
  #
114
115
  # @example
115
116
  # # Default can be symbol, proc, or direct value
116
- # default_value # => "default_value"
117
+ # -> { rand(100) } # => 23
117
118
  def default_value
118
119
  default = options[:default]
119
120
 
@@ -138,7 +139,7 @@ module CMDx
138
139
  #
139
140
  # @example
140
141
  # # Derives from hash key, method call, or proc execution
141
- # derive_value({user_id: 42}) # => 42
142
+ # context.user_id # => 42
142
143
  def derive_value(source_value)
143
144
  derived_value =
144
145
  case source_value
@@ -154,9 +155,29 @@ module CMDx
154
155
  nil
155
156
  end
156
157
 
158
+ # Transforms the derived value using the transform option.
159
+ #
160
+ # @param derived_value [Object] The value to transform
161
+ #
162
+ # @return [Object, nil] The transformed value or nil if transformation failed
163
+ #
164
+ # @example
165
+ # :downcase # => "hello"
166
+ def transform_value(derived_value)
167
+ transform = options[:transform]
168
+
169
+ if transform.is_a?(Symbol) && derived_value.respond_to?(transform, true)
170
+ derived_value.send(transform)
171
+ elsif transform.respond_to?(:call)
172
+ transform.call(derived_value)
173
+ else
174
+ derived_value
175
+ end
176
+ end
177
+
157
178
  # Coerces the derived value to the expected type(s) using the coercion registry.
158
179
  #
159
- # @param derived_value [Object] The value to coerce
180
+ # @param transformed_value [Object] The value to coerce
160
181
  #
161
182
  # @return [Object, nil] The coerced value or nil if coercion failed
162
183
  #
@@ -165,14 +186,14 @@ module CMDx
165
186
  # @example
166
187
  # # Coerces "42" to Integer, "true" to Boolean, etc.
167
188
  # coerce_value("42") # => 42
168
- def coerce_value(derived_value)
169
- return derived_value if attribute.types.empty?
189
+ def coerce_value(transformed_value)
190
+ return transformed_value if types.empty?
170
191
 
171
192
  registry = task.class.settings[:coercions]
172
- last_idx = attribute.types.size - 1
193
+ last_idx = types.size - 1
173
194
 
174
- attribute.types.find.with_index do |type, i|
175
- break registry.coerce(type, task, derived_value, options)
195
+ types.find.with_index do |type, i|
196
+ break registry.coerce(type, task, transformed_value, options)
176
197
  rescue CoercionError => e
177
198
  next if i != last_idx
178
199
 
@@ -180,7 +201,7 @@ module CMDx
180
201
  if last_idx.zero?
181
202
  e.message
182
203
  else
183
- tl = attribute.types.map { |t| Locale.t("cmdx.types.#{t}") }.join(", ")
204
+ tl = types.map { |t| Locale.t("cmdx.types.#{t}") }.join(", ")
184
205
  Locale.t("cmdx.coercions.into_any", types: tl)
185
206
  end
186
207
 
@@ -95,9 +95,19 @@ module CMDx
95
95
  raise TypeError, "unknown callback type #{type.inspect}" unless TYPES.include?(type)
96
96
 
97
97
  Array(registry[type]).each do |callables, options|
98
- next unless Utils::Condition.evaluate(task, options, task)
98
+ next unless Utils::Condition.evaluate(task, options)
99
99
 
100
- Array(callables).each { |callable| Utils::Call.invoke(task, callable, task) }
100
+ Array(callables).each do |callable|
101
+ if callable.is_a?(Symbol)
102
+ task.send(callable)
103
+ elsif callable.is_a?(Proc)
104
+ task.instance_exec(&callable)
105
+ elsif callable.respond_to?(:call)
106
+ callable.call(task)
107
+ else
108
+ raise "cannot invoke #{callable}"
109
+ end
110
+ end
101
111
  end
102
112
  end
103
113
 
@@ -5,6 +5,7 @@ module CMDx
5
5
  # Coerces various input types into Hash objects
6
6
  #
7
7
  # Supports conversion from:
8
+ # - Nil values (converted to empty Hash)
8
9
  # - Hash objects (returned as-is)
9
10
  # - Array objects (converted using Hash[*array])
10
11
  # - JSON strings starting with "{" (parsed into Hash)
@@ -30,12 +31,16 @@ module CMDx
30
31
  # @example Coerce from JSON string
31
32
  # Hash.call('{"key": "value"}') # => {"key" => "value"}
32
33
  def call(value, options = {})
33
- if value.is_a?(::Hash)
34
+ if value.nil?
35
+ {}
36
+ elsif value.is_a?(::Hash)
34
37
  value
35
38
  elsif value.is_a?(::Array)
36
39
  ::Hash[*value]
37
40
  elsif value.is_a?(::String) && value.start_with?("{")
38
41
  JSON.parse(value)
42
+ elsif value.respond_to?(:to_h)
43
+ value.to_h
39
44
  else
40
45
  raise_coercion_error!
41
46
  end
@@ -3,13 +3,14 @@
3
3
  module CMDx
4
4
 
5
5
  # Configuration class that manages global settings for CMDx including middlewares,
6
- # callbacks, coercions, validators, breakpoints, and logging.
6
+ # callbacks, coercions, validators, breakpoints, backtraces, and logging.
7
7
  class Configuration
8
8
 
9
9
  DEFAULT_BREAKPOINTS = %w[failed].freeze
10
10
 
11
11
  attr_accessor :middlewares, :callbacks, :coercions, :validators,
12
- :task_breakpoints, :workflow_breakpoints, :logger
12
+ :task_breakpoints, :workflow_breakpoints, :logger,
13
+ :backtrace, :backtrace_cleaner, :exception_handler
13
14
 
14
15
  # Initializes a new Configuration instance with default values.
15
16
  #
@@ -31,6 +32,10 @@ module CMDx
31
32
  @task_breakpoints = DEFAULT_BREAKPOINTS
32
33
  @workflow_breakpoints = DEFAULT_BREAKPOINTS
33
34
 
35
+ @backtrace = false
36
+ @backtrace_cleaner = nil
37
+ @exception_handler = nil
38
+
34
39
  @logger = Logger.new(
35
40
  $stdout,
36
41
  progname: "cmdx",
@@ -55,6 +60,9 @@ module CMDx
55
60
  validators: @validators,
56
61
  task_breakpoints: @task_breakpoints,
57
62
  workflow_breakpoints: @workflow_breakpoints,
63
+ backtrace: @backtrace,
64
+ backtrace_cleaner: @backtrace_cleaner,
65
+ exception_handler: @exception_handler,
58
66
  logger: @logger
59
67
  }
60
68
  end
@@ -44,15 +44,15 @@ module CMDx
44
44
  # settings(deprecate: :warn)
45
45
  # end
46
46
  #
47
- # MyTask.new # => [MyTask] DEPRECATED: migrate to replacement or discontinue use
47
+ # MyTask.new # => [MyTask] DEPRECATED: migrate to a replacement or discontinue use
48
48
  def restrict(task)
49
49
  type = EVAL.call(task, task.class.settings[:deprecate])
50
50
 
51
51
  case type
52
52
  when NilClass, FalseClass # Do nothing
53
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)
54
+ when /log/ then task.logger.warn { "DEPRECATED: migrate to a replacement or discontinue use" }
55
+ when /warn/ then warn("[#{task.class.name}] DEPRECATED: migrate to a replacement or discontinue use", category: :deprecated)
56
56
  else raise "unknown deprecation type #{type.inspect}"
57
57
  end
58
58
  end
data/lib/cmdx/errors.rb CHANGED
@@ -48,7 +48,7 @@ module CMDx
48
48
  !messages[attribute].empty?
49
49
  end
50
50
 
51
- # Convert errors to a hash format with arrays of fullmessages.
51
+ # Convert errors to a hash format with arrays of full messages.
52
52
  #
53
53
  # @return [Hash{Symbol => Array<String>}] Hash with attribute keys and message arrays
54
54
  #
data/lib/cmdx/executor.rb CHANGED
@@ -46,14 +46,16 @@ module CMDx
46
46
  # result = executor.execute
47
47
  def execute
48
48
  task.class.settings[:middlewares].call!(task) do
49
- pre_execution!
49
+ pre_execution! unless @pre_execution
50
50
  execution!
51
51
  rescue UndefinedMethodError => e
52
52
  raise(e) # No need to clear the Chain since exception is not being re-raised
53
53
  rescue Fault => e
54
54
  task.result.throw!(e.result, halt: false, cause: e)
55
55
  rescue StandardError => e
56
+ retry if retry_execution?(e)
56
57
  task.result.fail!("[#{e.class}] #{e.message}", halt: false, cause: e)
58
+ task.class.settings[:exception_handler]&.call(task, e)
57
59
  ensure
58
60
  task.result.executed!
59
61
  post_execution!
@@ -73,7 +75,7 @@ module CMDx
73
75
  # result = executor.execute!
74
76
  def execute!
75
77
  task.class.settings[:middlewares].call!(task) do
76
- pre_execution!
78
+ pre_execution! unless @pre_execution
77
79
  execution!
78
80
  rescue UndefinedMethodError => e
79
81
  raise_exception(e)
@@ -81,6 +83,7 @@ module CMDx
81
83
  task.result.throw!(e.result, halt: false, cause: e)
82
84
  halt_execution?(e) ? raise_exception(e) : post_execution!
83
85
  rescue StandardError => e
86
+ retry if retry_execution?(e)
84
87
  task.result.fail!("[#{e.class}] #{e.message}", halt: false, cause: e)
85
88
  raise_exception(e)
86
89
  else
@@ -108,6 +111,38 @@ module CMDx
108
111
  breakpoints.include?(exception.result.status)
109
112
  end
110
113
 
114
+ # Determines if execution should be retried based on retry configuration.
115
+ #
116
+ # @param exception [Exception] The exception that occurred
117
+ #
118
+ # @return [Boolean] Whether execution should be retried
119
+ #
120
+ # @example
121
+ # retry_execution?(standard_error)
122
+ def retry_execution?(exception)
123
+ available_retries = (task.class.settings[:retries] || 0).to_i
124
+ return false unless available_retries.positive?
125
+
126
+ current_retries = (task.result.metadata[:retries] ||= 0).to_i
127
+ remaining_retries = available_retries - current_retries
128
+ return false unless remaining_retries.positive?
129
+
130
+ exceptions = Array(task.class.settings[:retry_on] || StandardError)
131
+ return false unless exceptions.any? { |e| exception.class <= e }
132
+
133
+ task.result.metadata[:retries] += 1
134
+
135
+ task.logger.warn do
136
+ reason = "[#{exception.class}] #{exception.message}"
137
+ task.to_h.merge!(reason:, remaining_retries:)
138
+ end
139
+
140
+ jitter = task.class.settings[:retry_jitter].to_f * current_retries
141
+ sleep(jitter) if jitter.positive?
142
+
143
+ true
144
+ end
145
+
111
146
  # Raises an exception and clears the chain.
112
147
  #
113
148
  # @param exception [Exception] The exception to raise
@@ -118,6 +153,7 @@ module CMDx
118
153
  # raise_exception(standard_error)
119
154
  def raise_exception(exception)
120
155
  Chain.clear
156
+
121
157
  raise(exception)
122
158
  end
123
159
 
@@ -135,8 +171,15 @@ module CMDx
135
171
 
136
172
  private
137
173
 
174
+ # Lazy loaded repeator instance to handle retries.
175
+ def repeator
176
+ @repeator ||= Repeator.new(task)
177
+ end
178
+
138
179
  # Performs pre-execution tasks including validation and attribute verification.
139
180
  def pre_execution!
181
+ @pre_execution = true
182
+
140
183
  invoke_callbacks(:before_validation)
141
184
 
142
185
  task.class.settings[:attributes].define_and_verify(task)
@@ -144,8 +187,10 @@ module CMDx
144
187
 
145
188
  task.result.fail!(
146
189
  Locale.t("cmdx.faults.invalid"),
147
- full_message: task.errors.to_s,
148
- messages: task.errors.to_h
190
+ errors: {
191
+ full_message: task.errors.to_s,
192
+ messages: task.errors.to_h
193
+ }
149
194
  )
150
195
  end
151
196
 
@@ -169,13 +214,56 @@ module CMDx
169
214
 
170
215
  # Finalizes execution by freezing the task and logging results.
171
216
  def finalize_execution!
172
- task.logger.tap do |logger|
173
- logger.with_level(:info) do
174
- logger.info { task.result.to_h }
175
- end
217
+ log_execution!
218
+ log_backtrace! if task.class.settings[:backtrace]
219
+
220
+ freeze_execution!
221
+ clear_chain!
222
+ end
223
+
224
+ # Logs the execution result at the configured log level.
225
+ def log_execution!
226
+ task.logger.info { task.result.to_h }
227
+ end
228
+
229
+ # Logs the backtrace of the exception if the task failed.
230
+ def log_backtrace!
231
+ return unless task.result.failed?
232
+
233
+ exception = task.result.caused_failure.cause
234
+ return if exception.is_a?(Fault)
235
+
236
+ task.logger.error do
237
+ "[#{exception.class}] #{exception.message}\n" <<
238
+ if (cleaner = task.class.settings[:backtrace_cleaner])
239
+ cleaner.call(exception.backtrace).join("\n\t")
240
+ else
241
+ exception.full_message(highlight: false)
242
+ end
176
243
  end
244
+ end
245
+
246
+ # Freezes the task and its associated objects to prevent modifications.
247
+ def freeze_execution!
248
+ # Stubbing on frozen objects is not allowed in most test environments.
249
+ skip_freezing = ENV.fetch("SKIP_CMDX_FREEZING", false)
250
+ return if Coercions::Boolean.call(skip_freezing)
251
+
252
+ task.freeze
253
+ task.result.freeze
254
+
255
+ # Freezing the context and chain can only be done
256
+ # once the outer-most task has completed.
257
+ return unless task.result.index.zero?
177
258
 
178
- Freezer.immute(task)
259
+ task.context.freeze
260
+ task.chain.freeze
261
+ end
262
+
263
+ def clear_chain!
264
+ return unless task.result.index.zero?
265
+
266
+ Chain.clear
179
267
  end
180
268
 
181
269
  end
@@ -21,15 +21,15 @@ module CMDx
21
21
  #
22
22
  # @example Basic usage
23
23
  # logger_formatter.call("INFO", Time.now, "MyApp", "User logged in")
24
- # # => '{"@version":"1","@timestamp":"2024-01-15T10:30:45.123456Z","severity":"INFO","progname":"MyApp","pid":12345,"message":"User logged in"}\n'
24
+ # # => '{"severity":"INFO","progname":"MyApp","pid":12345,"message":"User logged in","@version":"1","@timestamp":"2024-01-15T10:30:45.123456Z"}\n'
25
25
  def call(severity, time, progname, message)
26
26
  hash = {
27
- "@version" => "1",
28
- "@timestamp" => time.utc.iso8601(6),
29
27
  severity:,
30
28
  progname:,
31
29
  pid: Process.pid,
32
- message: Utils::Format.to_log(message)
30
+ message: Utils::Format.to_log(message),
31
+ "@version" => "1",
32
+ "@timestamp" => time.utc.iso8601(6)
33
33
  }
34
34
 
35
35
  ::JSON.dump(hash) << "\n"
data/lib/cmdx/pipeline.rb CHANGED
@@ -42,7 +42,7 @@ module CMDx
42
42
  # pipeline.execute
43
43
  def execute
44
44
  workflow.class.pipeline.each do |group|
45
- next unless Utils::Condition.evaluate(workflow, group.options, workflow)
45
+ next unless Utils::Condition.evaluate(workflow, group.options)
46
46
 
47
47
  breakpoints = group.options[:breakpoints] ||
48
48
  workflow.class.settings[:breakpoints] ||
@@ -108,18 +108,18 @@ module CMDx
108
108
  # @example
109
109
  # execute_tasks_in_parallel(group, ["failed"])
110
110
  def execute_tasks_in_parallel(group, breakpoints)
111
- raise "install the `parallel` gem to use this feature" unless defined?(::Parallel)
111
+ raise "install the `parallel` gem to use this feature" unless defined?(Parallel)
112
112
 
113
113
  parallel_options = group.options.slice(:in_threads, :in_processes)
114
114
  throwable_result = nil
115
115
 
116
- ::Parallel.each(group.tasks, **parallel_options) do |task|
116
+ Parallel.each(group.tasks, **parallel_options) do |task|
117
117
  Chain.current = workflow.chain
118
118
 
119
119
  task_result = task.execute(workflow.context)
120
120
  next unless breakpoints.include?(task_result.status)
121
121
 
122
- raise ::Parallel::Break, throwable_result = task_result
122
+ raise Parallel::Break, throwable_result = task_result
123
123
  end
124
124
 
125
125
  return if throwable_result.nil?
data/lib/cmdx/railtie.rb CHANGED
@@ -32,5 +32,14 @@ module CMDx
32
32
  ::I18n.reload!
33
33
  end
34
34
 
35
+ # Configures the backtrace cleaner for CMDx in a Rails environment.
36
+ #
37
+ # Sets the backtrace cleaner to the Rails backtrace cleaner.
38
+ initializer("cmdx.backtrace_cleaner") do
39
+ CMDx.configuration.backtrace_cleaner = lambda do |backtrace|
40
+ Rails.backtrace_cleaner.clean(backtrace)
41
+ end
42
+ end
43
+
35
44
  end
36
45
  end
data/lib/cmdx/result.rb CHANGED
@@ -291,7 +291,16 @@ module CMDx
291
291
 
292
292
  # Strip the first two frames (this method and the delegator)
293
293
  frames = caller_locations(3..-1)
294
- fault.set_backtrace(frames.map(&:to_s)) unless frames.empty?
294
+
295
+ unless frames.empty?
296
+ frames = frames.map(&:to_s)
297
+
298
+ if (cleaner = task.class.settings[:backtrace_cleaner])
299
+ cleaner.call(frames)
300
+ end
301
+
302
+ fault.set_backtrace(frames)
303
+ end
295
304
 
296
305
  raise(fault)
297
306
  end
data/lib/cmdx/task.rb CHANGED
@@ -40,9 +40,6 @@ module CMDx
40
40
  class << self
41
41
 
42
42
  # @param options [Hash] Configuration options to merge with existing settings
43
- # @option options [AttributeRegistry] :attributes Registry for task attributes
44
- # @option options [Boolean] :deprecate Whether the task is deprecated
45
- # @option options [Array<Symbol>] :tags Tags associated with the task
46
43
  #
47
44
  # @return [Hash] The merged settings hash
48
45
  #
@@ -54,13 +51,21 @@ module CMDx
54
51
  @settings ||= begin
55
52
  hash =
56
53
  if superclass.respond_to?(:settings)
57
- superclass.settings
54
+ parent = superclass.settings
55
+ parent
56
+ .except(:backtrace_cleaner, :exception_handler, :logger, :deprecate)
57
+ .transform_values!(&:dup)
58
+ .merge!(
59
+ backtrace_cleaner: parent[:backtrace_cleaner] || CMDx.configuration.backtrace_cleaner,
60
+ exception_handler: parent[:exception_handler] || CMDx.configuration.exception_handler,
61
+ logger: parent[:logger] || CMDx.configuration.logger,
62
+ deprecate: parent[:deprecate]
63
+ )
58
64
  else
59
- CMDx.configuration.to_h.except(:logger)
60
- end.transform_values(&:dup)
65
+ CMDx.configuration.to_h
66
+ end
61
67
 
62
68
  hash[:attributes] ||= AttributeRegistry.new
63
- hash[:deprecate] ||= false
64
69
  hash[:tags] ||= []
65
70
 
66
71
  hash.merge!(options)
data/lib/cmdx/version.rb CHANGED
@@ -2,6 +2,6 @@
2
2
 
3
3
  module CMDx
4
4
 
5
- VERSION = "1.7.5"
5
+ VERSION = "1.9.0"
6
6
 
7
7
  end
data/lib/cmdx.rb CHANGED
@@ -5,6 +5,7 @@ require "date"
5
5
  require "forwardable"
6
6
  require "json"
7
7
  require "logger"
8
+ require "pathname"
8
9
  require "securerandom"
9
10
  require "set"
10
11
  require "time"
@@ -32,6 +32,15 @@ CMDx.configure do |config|
32
32
  level: Logger::INFO
33
33
  )
34
34
 
35
+ # Backtrace configuration - controls whether to log backtraces on faults and exceptions
36
+ # https://github.com/drexed/cmdx/blob/main/docs/getting_started.md#backtraces
37
+ # config.backtrace = false
38
+ # config.backtrace_cleaner = nil
39
+
40
+ # Exception handler configuration - called when non-fault exceptions are raised
41
+ # https://github.com/drexed/cmdx/blob/main/docs/getting_started.md#exception-handler
42
+ # config.exception_handler = nil
43
+
35
44
  # Additional global configurations - automatically applied to all tasks
36
45
  #
37
46
  # Middlewares - https://github.com/drexed/cmdx/blob/main/docs/middlewares.md
data/lib/locales/af.yml CHANGED
@@ -9,8 +9,8 @@ af:
9
9
  into_any: "kon nie na een van: %{types} omskep word nie"
10
10
  unknown: "onbekende %{type} omskep tipe"
11
11
  faults:
12
- invalid: "Ongeldige insette"
13
- unspecified: "Geen rede gegee nie"
12
+ invalid: "Ongeldig"
13
+ unspecified: "Ongespesifiseer"
14
14
  types:
15
15
  array: "skikking"
16
16
  big_decimal: "groot desimale"
data/lib/locales/ar.yml CHANGED
@@ -9,8 +9,8 @@ ar:
9
9
  into_any: "لا يمكن تحويله إلى واحد من: %{types}"
10
10
  unknown: "نوع تحويل %{type} غير معروف"
11
11
  faults:
12
- invalid: "مدخلات غير صالحة"
13
- unspecified: "لم يتم تقديم سبب"
12
+ invalid: "غير صالح"
13
+ unspecified: "غير محدد"
14
14
  types:
15
15
  array: "مصفوفة"
16
16
  big_decimal: "عدد عشري كبير"
data/lib/locales/az.yml CHANGED
@@ -9,8 +9,8 @@ az:
9
9
  into_any: "aşağıdakılardan birinə çevrilə bilmədi: %{types}"
10
10
  unknown: "naməlum %{type} çevrilmə tipi"
11
11
  faults:
12
- invalid: "Etibarsız girişlər"
13
- unspecified: "Səbəb göstərilməyib"
12
+ invalid: "Etibarsız"
13
+ unspecified: "Göstərilməyib"
14
14
  types:
15
15
  array: "massiv"
16
16
  big_decimal: "böyük onluq"
data/lib/locales/be.yml CHANGED
@@ -9,8 +9,8 @@ be:
9
9
  into_any: "не ўдалося пераўтварыць у адзін з: %{types}"
10
10
  unknown: "невядомы тып пераўтварэння %{type}"
11
11
  faults:
12
- invalid: "Няправільныя ўваходныя даныя"
13
- unspecified: "Прычына не паказана"
12
+ invalid: "Няправільныя"
13
+ unspecified: "Не паказана"
14
14
  types:
15
15
  array: "масіў"
16
16
  big_decimal: "вялікае дзесятковае лік"
data/lib/locales/bg.yml CHANGED
@@ -9,8 +9,8 @@ bg:
9
9
  into_any: "не може да бъде преобразуван в един от: %{types}"
10
10
  unknown: "неизвестен тип преобразуване %{type}"
11
11
  faults:
12
- invalid: "Невалидни входни данни"
13
- unspecified: "Не е посочена причина"
12
+ invalid: "Невалидни"
13
+ unspecified: "Не е посочена"
14
14
  types:
15
15
  array: "масив"
16
16
  big_decimal: "голямо десетично число"
data/lib/locales/bn.yml CHANGED
@@ -9,8 +9,8 @@ bn:
9
9
  into_any: "নিম্নলিখিতগুলির মধ্যে একটিতে রূপান্তর করা যায়নি: %{types}"
10
10
  unknown: "অজানা %{type} রূপান্তর প্রকার"
11
11
  faults:
12
- invalid: "অবৈধ ইনপুট"
13
- unspecified: "কোন কারণ দেওয়া হয়নি"
12
+ invalid: "অবৈধ"
13
+ unspecified: "অনির্দিষ্ট"
14
14
  types:
15
15
  array: "অ্যারে"
16
16
  big_decimal: "বড় দশমিক"