cmdx 1.0.1 → 1.1.1

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 (170) hide show
  1. checksums.yaml +4 -4
  2. data/.cursor/prompts/docs.md +9 -0
  3. data/.cursor/prompts/rspec.md +21 -0
  4. data/.cursor/prompts/yardoc.md +13 -0
  5. data/.rubocop.yml +2 -0
  6. data/CHANGELOG.md +29 -3
  7. data/README.md +2 -1
  8. data/docs/ai_prompts.md +269 -195
  9. data/docs/basics/call.md +126 -60
  10. data/docs/basics/chain.md +190 -160
  11. data/docs/basics/context.md +242 -154
  12. data/docs/basics/setup.md +302 -32
  13. data/docs/callbacks.md +382 -119
  14. data/docs/configuration.md +211 -49
  15. data/docs/deprecation.md +245 -0
  16. data/docs/getting_started.md +161 -39
  17. data/docs/internationalization.md +590 -70
  18. data/docs/interruptions/exceptions.md +135 -118
  19. data/docs/interruptions/faults.md +152 -127
  20. data/docs/interruptions/halt.md +134 -80
  21. data/docs/logging.md +183 -120
  22. data/docs/middlewares.md +165 -392
  23. data/docs/outcomes/result.md +140 -112
  24. data/docs/outcomes/states.md +134 -99
  25. data/docs/outcomes/statuses.md +204 -146
  26. data/docs/parameters/coercions.md +251 -289
  27. data/docs/parameters/defaults.md +224 -169
  28. data/docs/parameters/definitions.md +289 -141
  29. data/docs/parameters/namespacing.md +250 -161
  30. data/docs/parameters/validations.md +247 -159
  31. data/docs/testing.md +196 -203
  32. data/docs/workflows.md +146 -101
  33. data/lib/cmdx/.DS_Store +0 -0
  34. data/lib/cmdx/callback.rb +39 -55
  35. data/lib/cmdx/callback_registry.rb +80 -73
  36. data/lib/cmdx/chain.rb +65 -122
  37. data/lib/cmdx/chain_inspector.rb +23 -116
  38. data/lib/cmdx/chain_serializer.rb +34 -146
  39. data/lib/cmdx/coercion.rb +57 -0
  40. data/lib/cmdx/coercion_registry.rb +113 -0
  41. data/lib/cmdx/coercions/array.rb +18 -36
  42. data/lib/cmdx/coercions/big_decimal.rb +21 -33
  43. data/lib/cmdx/coercions/boolean.rb +21 -40
  44. data/lib/cmdx/coercions/complex.rb +18 -31
  45. data/lib/cmdx/coercions/date.rb +20 -39
  46. data/lib/cmdx/coercions/date_time.rb +22 -39
  47. data/lib/cmdx/coercions/float.rb +19 -32
  48. data/lib/cmdx/coercions/hash.rb +22 -41
  49. data/lib/cmdx/coercions/integer.rb +20 -33
  50. data/lib/cmdx/coercions/rational.rb +20 -32
  51. data/lib/cmdx/coercions/string.rb +23 -31
  52. data/lib/cmdx/coercions/time.rb +24 -40
  53. data/lib/cmdx/coercions/virtual.rb +14 -31
  54. data/lib/cmdx/configuration.rb +101 -162
  55. data/lib/cmdx/context.rb +34 -166
  56. data/lib/cmdx/core_ext/hash.rb +42 -67
  57. data/lib/cmdx/core_ext/module.rb +35 -79
  58. data/lib/cmdx/core_ext/object.rb +63 -98
  59. data/lib/cmdx/correlator.rb +59 -154
  60. data/lib/cmdx/error.rb +37 -202
  61. data/lib/cmdx/errors.rb +153 -216
  62. data/lib/cmdx/fault.rb +68 -150
  63. data/lib/cmdx/faults.rb +26 -137
  64. data/lib/cmdx/immutator.rb +22 -110
  65. data/lib/cmdx/lazy_struct.rb +110 -186
  66. data/lib/cmdx/log_formatters/json.rb +14 -40
  67. data/lib/cmdx/log_formatters/key_value.rb +14 -40
  68. data/lib/cmdx/log_formatters/line.rb +14 -48
  69. data/lib/cmdx/log_formatters/logstash.rb +14 -57
  70. data/lib/cmdx/log_formatters/pretty_json.rb +14 -50
  71. data/lib/cmdx/log_formatters/pretty_key_value.rb +13 -46
  72. data/lib/cmdx/log_formatters/pretty_line.rb +16 -54
  73. data/lib/cmdx/log_formatters/raw.rb +19 -49
  74. data/lib/cmdx/logger.rb +22 -79
  75. data/lib/cmdx/logger_ansi.rb +31 -72
  76. data/lib/cmdx/logger_serializer.rb +74 -103
  77. data/lib/cmdx/middleware.rb +56 -60
  78. data/lib/cmdx/middleware_registry.rb +82 -77
  79. data/lib/cmdx/middlewares/correlate.rb +41 -226
  80. data/lib/cmdx/middlewares/timeout.rb +46 -185
  81. data/lib/cmdx/parameter.rb +167 -183
  82. data/lib/cmdx/parameter_evaluator.rb +231 -0
  83. data/lib/cmdx/parameter_inspector.rb +37 -55
  84. data/lib/cmdx/parameter_registry.rb +65 -84
  85. data/lib/cmdx/parameter_serializer.rb +32 -76
  86. data/lib/cmdx/railtie.rb +24 -107
  87. data/lib/cmdx/result.rb +254 -259
  88. data/lib/cmdx/result_ansi.rb +28 -80
  89. data/lib/cmdx/result_inspector.rb +34 -70
  90. data/lib/cmdx/result_logger.rb +23 -77
  91. data/lib/cmdx/result_serializer.rb +59 -125
  92. data/lib/cmdx/rspec/matchers.rb +28 -0
  93. data/lib/cmdx/rspec/result_matchers/be_executed.rb +42 -0
  94. data/lib/cmdx/rspec/result_matchers/be_failed_task.rb +94 -0
  95. data/lib/cmdx/rspec/result_matchers/be_skipped_task.rb +94 -0
  96. data/lib/cmdx/rspec/result_matchers/be_state_matchers.rb +59 -0
  97. data/lib/cmdx/rspec/result_matchers/be_status_matchers.rb +57 -0
  98. data/lib/cmdx/rspec/result_matchers/be_successful_task.rb +87 -0
  99. data/lib/cmdx/rspec/result_matchers/have_bad_outcome.rb +51 -0
  100. data/lib/cmdx/rspec/result_matchers/have_caused_failure.rb +58 -0
  101. data/lib/cmdx/rspec/result_matchers/have_chain_index.rb +59 -0
  102. data/lib/cmdx/rspec/result_matchers/have_context.rb +86 -0
  103. data/lib/cmdx/rspec/result_matchers/have_empty_metadata.rb +54 -0
  104. data/lib/cmdx/rspec/result_matchers/have_good_outcome.rb +52 -0
  105. data/lib/cmdx/rspec/result_matchers/have_metadata.rb +114 -0
  106. data/lib/cmdx/rspec/result_matchers/have_preserved_context.rb +66 -0
  107. data/lib/cmdx/rspec/result_matchers/have_received_thrown_failure.rb +64 -0
  108. data/lib/cmdx/rspec/result_matchers/have_runtime.rb +78 -0
  109. data/lib/cmdx/rspec/result_matchers/have_thrown_failure.rb +76 -0
  110. data/lib/cmdx/rspec/task_matchers/be_well_formed_task.rb +62 -0
  111. data/lib/cmdx/rspec/task_matchers/have_callback.rb +85 -0
  112. data/lib/cmdx/rspec/task_matchers/have_cmd_setting.rb +68 -0
  113. data/lib/cmdx/rspec/task_matchers/have_executed_callbacks.rb +92 -0
  114. data/lib/cmdx/rspec/task_matchers/have_middleware.rb +46 -0
  115. data/lib/cmdx/rspec/task_matchers/have_parameter.rb +181 -0
  116. data/lib/cmdx/task.rb +336 -427
  117. data/lib/cmdx/task_deprecator.rb +52 -0
  118. data/lib/cmdx/task_processor.rb +246 -0
  119. data/lib/cmdx/task_serializer.rb +34 -69
  120. data/lib/cmdx/utils/ansi_color.rb +13 -89
  121. data/lib/cmdx/utils/log_timestamp.rb +13 -42
  122. data/lib/cmdx/utils/monotonic_runtime.rb +11 -63
  123. data/lib/cmdx/utils/name_affix.rb +21 -71
  124. data/lib/cmdx/validator.rb +57 -0
  125. data/lib/cmdx/validator_registry.rb +108 -0
  126. data/lib/cmdx/validators/exclusion.rb +55 -94
  127. data/lib/cmdx/validators/format.rb +31 -85
  128. data/lib/cmdx/validators/inclusion.rb +65 -110
  129. data/lib/cmdx/validators/length.rb +117 -133
  130. data/lib/cmdx/validators/numeric.rb +123 -130
  131. data/lib/cmdx/validators/presence.rb +38 -79
  132. data/lib/cmdx/version.rb +1 -7
  133. data/lib/cmdx/workflow.rb +58 -330
  134. data/lib/cmdx.rb +1 -1
  135. data/lib/generators/cmdx/install_generator.rb +14 -31
  136. data/lib/generators/cmdx/task_generator.rb +39 -55
  137. data/lib/generators/cmdx/templates/install.rb +24 -6
  138. data/lib/generators/cmdx/workflow_generator.rb +41 -66
  139. data/lib/locales/ar.yml +0 -1
  140. data/lib/locales/cs.yml +0 -1
  141. data/lib/locales/da.yml +0 -1
  142. data/lib/locales/de.yml +0 -1
  143. data/lib/locales/el.yml +0 -1
  144. data/lib/locales/en.yml +0 -1
  145. data/lib/locales/es.yml +0 -1
  146. data/lib/locales/fi.yml +0 -1
  147. data/lib/locales/fr.yml +0 -1
  148. data/lib/locales/he.yml +0 -1
  149. data/lib/locales/hi.yml +0 -1
  150. data/lib/locales/it.yml +0 -1
  151. data/lib/locales/ja.yml +0 -1
  152. data/lib/locales/ko.yml +0 -1
  153. data/lib/locales/nl.yml +0 -1
  154. data/lib/locales/no.yml +0 -1
  155. data/lib/locales/pl.yml +0 -1
  156. data/lib/locales/pt.yml +0 -1
  157. data/lib/locales/ru.yml +0 -1
  158. data/lib/locales/sv.yml +0 -1
  159. data/lib/locales/th.yml +0 -1
  160. data/lib/locales/tr.yml +0 -1
  161. data/lib/locales/vi.yml +0 -1
  162. data/lib/locales/zh.yml +0 -1
  163. metadata +36 -8
  164. data/lib/cmdx/parameter_validator.rb +0 -81
  165. data/lib/cmdx/parameter_value.rb +0 -244
  166. data/lib/cmdx/parameters_inspector.rb +0 -72
  167. data/lib/cmdx/parameters_serializer.rb +0 -115
  168. data/lib/cmdx/rspec/result_matchers.rb +0 -917
  169. data/lib/cmdx/rspec/task_matchers.rb +0 -570
  170. data/lib/cmdx/validators/custom.rb +0 -102
data/docs/basics/setup.md CHANGED
@@ -1,9 +1,6 @@
1
1
  # Basics - Setup
2
2
 
3
- A task represents a unit of work to execute. Tasks are the core building blocks
4
- of CMDx, encapsulating business logic within a structured, reusable object. While
5
- CMDx offers extensive features like parameter validation, callbacks, and state tracking,
6
- only a `call` method is required to create a functional task.
3
+ A task represents a unit of work to execute. Tasks are the core building blocks of CMDx, encapsulating business logic within a structured, reusable object. While CMDx offers extensive features like parameter validation, callbacks, and state tracking, only a `call` method is required to create a functional task.
7
4
 
8
5
  ## Table of Contents
9
6
 
@@ -13,57 +10,197 @@ only a `call` method is required to create a functional task.
13
10
  - [Inheritance and Application Tasks](#inheritance-and-application-tasks)
14
11
  - [Generator](#generator)
15
12
  - [Task Lifecycle](#task-lifecycle)
13
+ - [Error Handling](#error-handling)
16
14
 
17
15
  ## TLDR
18
16
 
19
- - **Tasks** - Ruby classes that inherit from `CMDx::Task` with a `call` method
20
- - **Structure** - Only `call` method required, everything else is optional
21
- - **Execution** - Run with `MyTask.call(params)` to get a `CMDx::Result`
22
- - **Generator** - Use `rails g cmdx:task MyTask` to create task templates
23
- - **Single-use** - Tasks are frozen after execution, create new instances for each run
17
+ ```ruby
18
+ # Minimal task - only call method required
19
+ class ProcessOrderTask < CMDx::Task
20
+ def call
21
+ context.result = "Order processed"
22
+ end
23
+ end
24
+
25
+ # Execute and access results
26
+ result = ProcessOrderTask.call(order_id: 123)
27
+ result.success? # → true
28
+ result.context.result # → "Order processed"
29
+
30
+ # With parameters and validation
31
+ class UpdateUserTask < CMDx::Task
32
+ required :user_id, type: :integer
33
+ required :email, type: :string
34
+
35
+ def call
36
+ user = User.find(context.user_id)
37
+ user.update!(email: context.email)
38
+ end
39
+ end
40
+
41
+ # Generator for quick scaffolding
42
+ rails g cmdx:task ProcessPayment # Creates structured template
43
+ ```
24
44
 
25
45
  ## Basic Task Structure
26
46
 
47
+ > [!NOTE]
48
+ > Tasks are Ruby classes that inherit from `CMDx::Task`. Only the `call` method is required - all other features are optional and can be added as needed.
49
+
50
+ ### Minimal Task
51
+
27
52
  ```ruby
28
53
  class ProcessUserOrderTask < CMDx::Task
29
-
30
54
  def call
31
55
  # Your business logic here
32
56
  context.order = Order.find(context.order_id)
33
57
  context.order.process!
34
58
  end
59
+ end
60
+ ```
61
+
62
+ ### Complete Task Structure
63
+
64
+ ```ruby
65
+ class ProcessPaymentTask < CMDx::Task
66
+ # Parameter definitions (optional)
67
+ required :amount, type: :float
68
+ required :user_id, type: :integer
69
+ optional :currency, type: :string, default: "USD"
70
+
71
+ # Callbacks (optional)
72
+ before_call :validate_user
73
+ after_call :send_notification
74
+
75
+ def call
76
+ # Core business logic
77
+ user = User.find(context.user_id)
78
+ payment = Payment.create!(
79
+ user: user,
80
+ amount: context.amount,
81
+ currency: context.currency
82
+ )
83
+
84
+ context.payment = payment
85
+ context.success_message = "Payment processed successfully"
86
+ end
35
87
 
36
88
  private
37
89
 
38
- # Support methods and business logic
90
+ def validate_user
91
+ # Validation logic
92
+ end
39
93
 
94
+ def send_notification
95
+ # Notification logic
96
+ end
40
97
  end
41
98
  ```
42
99
 
43
100
  ## Task Execution
44
101
 
102
+ > [!IMPORTANT]
103
+ > Tasks return a `CMDx::Result` object that contains execution state, context data, and metadata. Always check the result status before accessing context data.
104
+
105
+ ### Basic Execution
106
+
45
107
  ```ruby
46
108
  # Execute a task
47
109
  result = ProcessUserOrderTask.call(order_id: 123)
48
110
 
49
- # Access the result
50
- result.success? #=> true
51
- result.context.order #=> <Order id: 123>
111
+ # Check execution status
112
+ result.success? # true/false
113
+ result.failed? # true/false
114
+
115
+ # Access context data
116
+ result.context.order # → <Order id: 123>
117
+
118
+ # Access execution metadata
119
+ result.status # → :success, :failure, etc.
120
+ result.state # → :executed, :skipped, etc.
121
+ result.runtime # → 0.1234 (seconds)
122
+ ```
123
+
124
+ ### Handling Different Outcomes
125
+
126
+ ```ruby
127
+ result = ProcessPaymentTask.call(
128
+ amount: 99.99,
129
+ user_id: 12345,
130
+ currency: "EUR"
131
+ )
132
+
133
+ case result.status
134
+ when :success
135
+ payment = result.context.payment
136
+ puts result.context.success_message
137
+ when :failure
138
+ puts "Payment failed: #{result.metadata[:reason]}"
139
+ when :halt
140
+ puts "Payment halted: #{result.metadata[:reason]}"
141
+ end
52
142
  ```
53
143
 
54
144
  ## Inheritance and Application Tasks
55
145
 
56
- In Rails applications, tasks typically inherit from an `ApplicationTask` base class:
146
+ > [!TIP]
147
+ > In Rails applications, create an `ApplicationTask` base class to share common configuration, middleware, and functionality across all your tasks.
148
+
149
+ ### Application Base Class
57
150
 
58
151
  ```ruby
59
152
  # app/tasks/application_task.rb
60
153
  class ApplicationTask < CMDx::Task
61
- # Shared configuration and functionality
154
+ # Shared configuration
155
+ use :middleware, AuthenticateUserMiddleware
156
+ use :middleware, LogExecutionMiddleware
157
+
158
+ # Common callbacks
159
+ before_call :set_correlation_id
160
+ after_call :cleanup_temp_data
161
+
162
+ # Shared parameter definitions
163
+ optional :current_user, type: :virtual
164
+ optional :request_id, type: :string
165
+
166
+ private
167
+
168
+ def set_correlation_id
169
+ context.correlation_id ||= SecureRandom.uuid
170
+ end
171
+
172
+ def cleanup_temp_data
173
+ # Cleanup logic
174
+ end
62
175
  end
176
+ ```
177
+
178
+ ### Task Implementation
63
179
 
180
+ ```ruby
64
181
  # app/tasks/process_user_order_task.rb
65
182
  class ProcessUserOrderTask < ApplicationTask
183
+ required :order_id, type: :integer
184
+ required :payment_method, type: :string
185
+
66
186
  def call
187
+ # Inherits all ApplicationTask functionality
188
+ order = Order.find(context.order_id)
189
+
190
+ # Business logic specific to this task
191
+ process_order(order)
192
+ charge_payment(order, context.payment_method)
193
+
194
+ context.order = order
195
+ end
196
+
197
+ private
198
+
199
+ def process_order(order)
200
+ # Implementation
201
+ end
202
+
203
+ def charge_payment(order, method)
67
204
  # Implementation
68
205
  end
69
206
  end
@@ -71,33 +208,166 @@ end
71
208
 
72
209
  ## Generator
73
210
 
74
- Rails applications can use the built-in generator to create task templates:
211
+ > [!NOTE]
212
+ > Rails applications can use the built-in generator to create consistent task templates with proper structure and naming conventions.
213
+
214
+ ### Basic Task Generation
75
215
 
76
216
  ```bash
217
+ # Generate a basic task
77
218
  rails g cmdx:task ProcessUserOrder
78
219
  ```
79
220
 
80
- This creates `app/tasks/process_user_order_task.rb` with:
81
- - Proper inheritance from `ApplicationTask` (if available) or `CMDx::Task`
82
- - Basic structure with parameter definitions
83
- - Template implementation
221
+ This creates `app/tasks/process_user_order_task.rb`:
84
222
 
85
- > [!TIP]
86
- > Use the generator to maintain consistent task structure and naming conventions across your application.
223
+ ```ruby
224
+ class ProcessUserOrderTask < ApplicationTask
225
+ # Define required parameters
226
+ # required :param_name, type: :string
87
227
 
88
- ## Task Lifecycle
228
+ # Define optional parameters
229
+ # optional :param_name, type: :string, default: "default_value"
230
+
231
+ def call
232
+ # Implement your task logic here
233
+ # Access parameters via context.param_name
234
+ end
235
+
236
+ private
89
237
 
90
- Every task follows a predictable lifecycle:
238
+ # Add private methods for supporting logic
239
+ end
240
+ ```
241
+
242
+ ### Advanced Generation Options
243
+
244
+ ```bash
245
+ # Generate with workflow
246
+ rails g cmdx:workflow ProcessOrder
247
+
248
+ # Generate with specific namespace
249
+ rails g cmdx:task Billing::ProcessPayment
250
+ ```
91
251
 
92
- 1. **Instantiation** - Task object created with context
93
- 2. **Validation** - Parameters validated against definitions
94
- 3. **Execution** - The `call` method runs business logic
95
- 4. **Completion** - Result finalized with state and status
96
- 5. **Freezing** - Task becomes immutable after execution
252
+ ## Task Lifecycle
97
253
 
98
254
  > [!IMPORTANT]
99
- > Tasks are single-use objects. Once executed, they are frozen and cannot
100
- > be called again. Create a new task instance for each execution.
255
+ > Understanding the task lifecycle is crucial for proper error handling and debugging. Tasks follow a predictable execution pattern with specific states and status transitions.
256
+
257
+ ### Lifecycle Stages
258
+
259
+ | Stage | Description | State | Possible Statuses |
260
+ |-------|-------------|--------|-------------------|
261
+ | **Instantiation** | Task object created with context | `:initialized` | `:pending` |
262
+ | **Pre-validation** | Before callbacks and middleware run | `:executing` | `:pending` |
263
+ | **Validation** | Parameters validated against definitions | `:executing` | `:pending`, `:failure` |
264
+ | **Execution** | The `call` method runs business logic | `:executing` | `:pending`, `:halt` |
265
+ | **Post-execution** | After callbacks run | `:executing` | `:success`, `:failure` |
266
+ | **Completion** | Result finalized with final state | `:executed` | `:success`, `:failure` |
267
+ | **Freezing** | Task becomes immutable | `:executed` | Final status |
268
+
269
+ ### Lifecycle Example
270
+
271
+ ```ruby
272
+ class ExampleTask < CMDx::Task
273
+ required :data, type: :string
274
+
275
+ before_call :log_start
276
+ after_call :log_completion
277
+
278
+ def call
279
+ # Main logic
280
+ context.processed_data = context.data.upcase
281
+ end
282
+
283
+ private
284
+
285
+ def log_start
286
+ puts "Task starting with data: #{context.data}"
287
+ end
288
+
289
+ def log_completion
290
+ puts "Task completed: #{context.processed_data}"
291
+ end
292
+ end
293
+
294
+ # Execution trace
295
+ result = ExampleTask.call(data: "hello")
296
+ # Output:
297
+ # Task starting with data: hello
298
+ # Task completed: HELLO
299
+
300
+ result.state # → :executed
301
+ result.status # → :success
302
+ ```
303
+
304
+ > [!WARNING]
305
+ > Tasks are single-use objects. Once executed, they are frozen and cannot be called again. Attempting to call a frozen task will raise an error.
306
+
307
+ ```ruby
308
+ task = ProcessOrderTask.new(order_id: 123)
309
+ result1 = task.call # ✓ Works
310
+ result2 = task.call # ✗ Raises FrozenError
311
+
312
+ # Create new instances for each execution
313
+ result1 = ProcessOrderTask.call(order_id: 123)
314
+ result2 = ProcessOrderTask.call(order_id: 456) # ✓ Works
315
+ ```
316
+
317
+ ## Error Handling
318
+
319
+ > [!NOTE]
320
+ > CMDx provides comprehensive error handling with detailed metadata about failures, including parameter validation errors, execution exceptions, and halt conditions.
321
+
322
+ ### Parameter Validation Errors
323
+
324
+ ```ruby
325
+ class ProcessOrderTask < CMDx::Task
326
+ required :order_id, type: :integer
327
+ required :amount, type: :float
328
+
329
+ def call
330
+ # Task logic
331
+ end
332
+ end
333
+
334
+ # Invalid parameters
335
+ result = ProcessOrderTask.call(
336
+ order_id: "not-a-number",
337
+ amount: "invalid"
338
+ )
339
+
340
+ result.failed? # → true
341
+ result.status # → :failure
342
+ result.metadata
343
+ # {
344
+ # reason: "order_id could not coerce into an integer. amount could not coerce into a float.",
345
+ # messages: {
346
+ # order_id: ["could not coerce into an integer"],
347
+ # amount: ["could not coerce into a float"]
348
+ # }
349
+ # }
350
+ ```
351
+
352
+ ### Runtime Exceptions
353
+
354
+ ```ruby
355
+ class ProcessOrderTask < CMDx::Task
356
+ required :order_id, type: :integer
357
+
358
+ def call
359
+ order = Order.find(context.order_id) # May raise ActiveRecord::RecordNotFound
360
+ order.process!
361
+ end
362
+ end
363
+
364
+ # Order not found
365
+ result = ProcessOrderTask.call(order_id: 99999)
366
+
367
+ result.failed? # → true
368
+ result.status # → :failure
369
+ result.metadata[:reason] # → "ActiveRecord::RecordNotFound: Couldn't find Order..."
370
+ ```
101
371
 
102
372
  ---
103
373