cmdx 1.0.0 → 1.1.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 (169) hide show
  1. checksums.yaml +4 -4
  2. data/.cursor/prompts/rspec.md +20 -0
  3. data/.cursor/prompts/yardoc.md +8 -0
  4. data/.rubocop.yml +5 -0
  5. data/CHANGELOG.md +101 -49
  6. data/README.md +2 -1
  7. data/docs/ai_prompts.md +10 -0
  8. data/docs/basics/call.md +11 -2
  9. data/docs/basics/chain.md +10 -1
  10. data/docs/basics/context.md +9 -0
  11. data/docs/basics/setup.md +9 -0
  12. data/docs/callbacks.md +14 -37
  13. data/docs/configuration.md +68 -27
  14. data/docs/getting_started.md +11 -0
  15. data/docs/internationalization.md +148 -0
  16. data/docs/interruptions/exceptions.md +10 -1
  17. data/docs/interruptions/faults.md +11 -2
  18. data/docs/interruptions/halt.md +9 -0
  19. data/docs/logging.md +14 -4
  20. data/docs/middlewares.md +53 -43
  21. data/docs/outcomes/result.md +9 -0
  22. data/docs/outcomes/states.md +9 -0
  23. data/docs/outcomes/statuses.md +9 -0
  24. data/docs/parameters/coercions.md +58 -38
  25. data/docs/parameters/defaults.md +10 -1
  26. data/docs/parameters/definitions.md +9 -0
  27. data/docs/parameters/namespacing.md +9 -0
  28. data/docs/parameters/validations.md +8 -67
  29. data/docs/testing.md +22 -13
  30. data/docs/tips_and_tricks.md +9 -0
  31. data/docs/workflows.md +14 -4
  32. data/lib/cmdx/.DS_Store +0 -0
  33. data/lib/cmdx/callback.rb +36 -56
  34. data/lib/cmdx/callback_registry.rb +82 -73
  35. data/lib/cmdx/chain.rb +65 -122
  36. data/lib/cmdx/chain_inspector.rb +22 -115
  37. data/lib/cmdx/chain_serializer.rb +17 -148
  38. data/lib/cmdx/coercion.rb +49 -0
  39. data/lib/cmdx/coercion_registry.rb +94 -0
  40. data/lib/cmdx/coercions/array.rb +18 -36
  41. data/lib/cmdx/coercions/big_decimal.rb +21 -33
  42. data/lib/cmdx/coercions/boolean.rb +21 -40
  43. data/lib/cmdx/coercions/complex.rb +18 -31
  44. data/lib/cmdx/coercions/date.rb +20 -39
  45. data/lib/cmdx/coercions/date_time.rb +22 -39
  46. data/lib/cmdx/coercions/float.rb +19 -32
  47. data/lib/cmdx/coercions/hash.rb +22 -41
  48. data/lib/cmdx/coercions/integer.rb +20 -33
  49. data/lib/cmdx/coercions/rational.rb +20 -32
  50. data/lib/cmdx/coercions/string.rb +23 -31
  51. data/lib/cmdx/coercions/time.rb +24 -40
  52. data/lib/cmdx/coercions/virtual.rb +14 -31
  53. data/lib/cmdx/configuration.rb +57 -171
  54. data/lib/cmdx/context.rb +22 -165
  55. data/lib/cmdx/core_ext/hash.rb +42 -67
  56. data/lib/cmdx/core_ext/module.rb +35 -79
  57. data/lib/cmdx/core_ext/object.rb +63 -98
  58. data/lib/cmdx/correlator.rb +40 -156
  59. data/lib/cmdx/error.rb +37 -202
  60. data/lib/cmdx/errors.rb +165 -202
  61. data/lib/cmdx/fault.rb +55 -158
  62. data/lib/cmdx/faults.rb +26 -137
  63. data/lib/cmdx/immutator.rb +22 -109
  64. data/lib/cmdx/lazy_struct.rb +103 -187
  65. data/lib/cmdx/log_formatters/json.rb +14 -40
  66. data/lib/cmdx/log_formatters/key_value.rb +14 -40
  67. data/lib/cmdx/log_formatters/line.rb +14 -48
  68. data/lib/cmdx/log_formatters/logstash.rb +14 -57
  69. data/lib/cmdx/log_formatters/pretty_json.rb +14 -50
  70. data/lib/cmdx/log_formatters/pretty_key_value.rb +13 -46
  71. data/lib/cmdx/log_formatters/pretty_line.rb +16 -54
  72. data/lib/cmdx/log_formatters/raw.rb +19 -49
  73. data/lib/cmdx/logger.rb +20 -82
  74. data/lib/cmdx/logger_ansi.rb +18 -75
  75. data/lib/cmdx/logger_serializer.rb +24 -114
  76. data/lib/cmdx/middleware.rb +38 -60
  77. data/lib/cmdx/middleware_registry.rb +81 -77
  78. data/lib/cmdx/middlewares/correlate.rb +41 -226
  79. data/lib/cmdx/middlewares/timeout.rb +46 -185
  80. data/lib/cmdx/parameter.rb +120 -198
  81. data/lib/cmdx/parameter_evaluator.rb +231 -0
  82. data/lib/cmdx/parameter_inspector.rb +25 -56
  83. data/lib/cmdx/parameter_registry.rb +59 -84
  84. data/lib/cmdx/parameter_serializer.rb +23 -74
  85. data/lib/cmdx/railtie.rb +24 -107
  86. data/lib/cmdx/result.rb +254 -260
  87. data/lib/cmdx/result_ansi.rb +19 -85
  88. data/lib/cmdx/result_inspector.rb +27 -68
  89. data/lib/cmdx/result_logger.rb +18 -81
  90. data/lib/cmdx/result_serializer.rb +28 -132
  91. data/lib/cmdx/rspec/matchers.rb +28 -0
  92. data/lib/cmdx/rspec/result_matchers/be_executed.rb +42 -0
  93. data/lib/cmdx/rspec/result_matchers/be_failed_task.rb +94 -0
  94. data/lib/cmdx/rspec/result_matchers/be_skipped_task.rb +94 -0
  95. data/lib/cmdx/rspec/result_matchers/be_state_matchers.rb +59 -0
  96. data/lib/cmdx/rspec/result_matchers/be_status_matchers.rb +57 -0
  97. data/lib/cmdx/rspec/result_matchers/be_successful_task.rb +87 -0
  98. data/lib/cmdx/rspec/result_matchers/have_bad_outcome.rb +51 -0
  99. data/lib/cmdx/rspec/result_matchers/have_caused_failure.rb +58 -0
  100. data/lib/cmdx/rspec/result_matchers/have_chain_index.rb +59 -0
  101. data/lib/cmdx/rspec/result_matchers/have_context.rb +86 -0
  102. data/lib/cmdx/rspec/result_matchers/have_empty_metadata.rb +54 -0
  103. data/lib/cmdx/rspec/result_matchers/have_good_outcome.rb +52 -0
  104. data/lib/cmdx/rspec/result_matchers/have_metadata.rb +114 -0
  105. data/lib/cmdx/rspec/result_matchers/have_preserved_context.rb +66 -0
  106. data/lib/cmdx/rspec/result_matchers/have_received_thrown_failure.rb +64 -0
  107. data/lib/cmdx/rspec/result_matchers/have_runtime.rb +78 -0
  108. data/lib/cmdx/rspec/result_matchers/have_thrown_failure.rb +76 -0
  109. data/lib/cmdx/rspec/task_matchers/be_well_formed_task.rb +62 -0
  110. data/lib/cmdx/rspec/task_matchers/have_callback.rb +85 -0
  111. data/lib/cmdx/rspec/task_matchers/have_cmd_setting.rb +68 -0
  112. data/lib/cmdx/rspec/task_matchers/have_executed_callbacks.rb +92 -0
  113. data/lib/cmdx/rspec/task_matchers/have_middleware.rb +46 -0
  114. data/lib/cmdx/rspec/task_matchers/have_parameter.rb +181 -0
  115. data/lib/cmdx/task.rb +213 -425
  116. data/lib/cmdx/task_deprecator.rb +55 -0
  117. data/lib/cmdx/task_processor.rb +245 -0
  118. data/lib/cmdx/task_serializer.rb +22 -70
  119. data/lib/cmdx/utils/ansi_color.rb +13 -89
  120. data/lib/cmdx/utils/log_timestamp.rb +13 -42
  121. data/lib/cmdx/utils/monotonic_runtime.rb +13 -63
  122. data/lib/cmdx/utils/name_affix.rb +21 -71
  123. data/lib/cmdx/validator.rb +48 -0
  124. data/lib/cmdx/validator_registry.rb +86 -0
  125. data/lib/cmdx/validators/exclusion.rb +55 -94
  126. data/lib/cmdx/validators/format.rb +31 -85
  127. data/lib/cmdx/validators/inclusion.rb +65 -110
  128. data/lib/cmdx/validators/length.rb +117 -133
  129. data/lib/cmdx/validators/numeric.rb +123 -130
  130. data/lib/cmdx/validators/presence.rb +38 -79
  131. data/lib/cmdx/version.rb +1 -7
  132. data/lib/cmdx/workflow.rb +46 -339
  133. data/lib/cmdx.rb +1 -1
  134. data/lib/generators/cmdx/install_generator.rb +14 -31
  135. data/lib/generators/cmdx/task_generator.rb +39 -55
  136. data/lib/generators/cmdx/templates/install.rb +61 -11
  137. data/lib/generators/cmdx/workflow_generator.rb +41 -66
  138. data/lib/locales/ar.yml +35 -0
  139. data/lib/locales/cs.yml +35 -0
  140. data/lib/locales/da.yml +35 -0
  141. data/lib/locales/de.yml +35 -0
  142. data/lib/locales/el.yml +35 -0
  143. data/lib/locales/en.yml +19 -20
  144. data/lib/locales/es.yml +19 -20
  145. data/lib/locales/fi.yml +35 -0
  146. data/lib/locales/fr.yml +35 -0
  147. data/lib/locales/he.yml +35 -0
  148. data/lib/locales/hi.yml +35 -0
  149. data/lib/locales/it.yml +35 -0
  150. data/lib/locales/ja.yml +35 -0
  151. data/lib/locales/ko.yml +35 -0
  152. data/lib/locales/nl.yml +35 -0
  153. data/lib/locales/no.yml +35 -0
  154. data/lib/locales/pl.yml +35 -0
  155. data/lib/locales/pt.yml +35 -0
  156. data/lib/locales/ru.yml +35 -0
  157. data/lib/locales/sv.yml +35 -0
  158. data/lib/locales/th.yml +35 -0
  159. data/lib/locales/tr.yml +35 -0
  160. data/lib/locales/vi.yml +35 -0
  161. data/lib/locales/zh.yml +35 -0
  162. metadata +57 -8
  163. data/lib/cmdx/parameter_validator.rb +0 -81
  164. data/lib/cmdx/parameter_value.rb +0 -244
  165. data/lib/cmdx/parameters_inspector.rb +0 -72
  166. data/lib/cmdx/parameters_serializer.rb +0 -115
  167. data/lib/cmdx/rspec/result_matchers.rb +0 -917
  168. data/lib/cmdx/rspec/task_matchers.rb +0 -570
  169. data/lib/cmdx/validators/custom.rb +0 -102
@@ -4,6 +4,7 @@ Parameter values can be validated using built-in validators or custom validation
4
4
 
5
5
  ## Table of Contents
6
6
 
7
+ - [TLDR](#tldr)
7
8
  - [Common Options](#common-options)
8
9
  - [Presence](#presence)
9
10
  - [Format](#format)
@@ -11,9 +12,14 @@ Parameter values can be validated using built-in validators or custom validation
11
12
  - [Inclusion](#inclusion)
12
13
  - [Length](#length)
13
14
  - [Numeric](#numeric)
14
- - [Custom](#custom)
15
15
  - [Validation Results](#validation-results)
16
- - [Internationalization (i18n)](#internationalization-i18n)
16
+
17
+ ## TLDR
18
+
19
+ - **Built-in validators** - `presence`, `format`, `inclusion`, `exclusion`, `length`, `numeric`
20
+ - **Common options** - All support `:allow_nil`, `:if`, `:unless`, `:message`
21
+ - **Usage** - Add to parameter definitions: `required :email, presence: true, format: { with: /@/ }`
22
+ - **Conditional** - Use `:if` and `:unless` for conditional validation
17
23
 
18
24
  ## Common Options
19
25
 
@@ -239,43 +245,6 @@ end
239
245
  | `:is_message` | "must be %{is}" |
240
246
  | `:is_not_message` | "must not be %{is_not}" |
241
247
 
242
- ## Custom
243
-
244
- Validates using custom logic. Accepts any callable object (class, proc, lambda) implementing a `call` method that returns truthy for valid values.
245
-
246
- ```ruby
247
- class EmailDomainValidator
248
- def self.call(value, options)
249
- allowed_domains = options.dig(:custom, :allowed_domains) || ['example.com']
250
- domain = value.split('@').last
251
- allowed_domains.include?(domain)
252
- end
253
- end
254
-
255
- class CreateAccountTask < CMDx::Task
256
- required :work_email, custom: {
257
- validator: EmailDomainValidator,
258
- allowed_domains: ['company.com', 'partner.org'],
259
- message: "must be from an approved domain"
260
- }
261
-
262
- required :age, custom: {
263
- validator: ->(value, options) { value.between?(18, 120) },
264
- message: "must be a valid age"
265
- }
266
-
267
- def call
268
- create_user_account
269
- end
270
- end
271
- ```
272
-
273
- **Options:**
274
-
275
- | Option | Description |
276
- | ------------ | ----------- |
277
- | `:validator` | Callable object returning true/false. Receives value and options as parameters |
278
-
279
248
  ## Validation Results
280
249
 
281
250
  When validation fails, tasks enter a failed state with detailed error information:
@@ -303,34 +272,6 @@ result.metadata[:messages][:email] #=> ["format is invalid"]
303
272
  result.metadata[:messages][:username] #=> ["cannot be empty"]
304
273
  ```
305
274
 
306
- ## Internationalization (i18n)
307
-
308
- All validators support internationalization through Rails i18n. Customize error messages in your locale files:
309
-
310
- ```yaml
311
- # config/locales/en.yml
312
- en:
313
- cmdx:
314
- validators:
315
- presence: "is required"
316
- format: "has invalid format"
317
- inclusion:
318
- of: "must be one of: %{values}"
319
- in: "must be within %{min} and %{max}"
320
- exclusion:
321
- of: "must not be one of: %{values}"
322
- in: "must not be within %{min} and %{max}"
323
- length:
324
- within: "must be between %{min} and %{max} characters"
325
- min: "must be at least %{min} characters"
326
- max: "must be at most %{max} characters"
327
- numeric:
328
- within: "must be between %{min} and %{max}"
329
- min: "must be at least %{min}"
330
- max: "must be at most %{max}"
331
- custom: "is invalid"
332
- ```
333
-
334
275
  ---
335
276
 
336
277
  - **Prev:** [Parameters - Coercions](coercions.md)
data/docs/testing.md CHANGED
@@ -4,6 +4,7 @@ CMDx provides a comprehensive suite of custom RSpec matchers designed for expres
4
4
 
5
5
  ## Table of Contents
6
6
 
7
+ - [TLDR](#tldr)
7
8
  - [External Project Setup](#external-project-setup)
8
9
  - [Matcher Organization](#matcher-organization)
9
10
  - [Result Matchers](#result-matchers)
@@ -20,23 +21,31 @@ CMDx provides a comprehensive suite of custom RSpec matchers designed for expres
20
21
  - [Composable Testing](#composable-testing)
21
22
  - [Best Practices](#best-practices)
22
23
 
23
- ## Using RSpec matchers
24
+ ## TLDR
25
+
26
+ - **Custom matchers** - 40+ specialized RSpec matchers for testing CMDx tasks and results
27
+ - **Setup** - Require `cmdx/rspec/matchers`
28
+ - **Result matchers** - `be_successful_task`, `be_failed_task`, `be_skipped_task` with chainable metadata
29
+ - **Task matchers** - Parameter validation, lifecycle, exception handling, and configuration testing
30
+ - **Composable** - Chain matchers for complex validation scenarios
31
+ - **YARD documented** - Complete documentation with examples for all matchers
32
+
33
+ ## External Project Setup
24
34
 
25
35
  To use CMDx's custom matchers in an external RSpec-based project update your `spec/spec_helper.rb` or `spec/rails_helper.rb`:
26
36
 
27
37
  ```ruby
28
- require "cmdx/rspec/result_matchers"
29
- require "cmdx/rspec/task_matchers"
38
+ require "cmdx/rspec/matchers"
30
39
  ```
31
40
 
32
41
  ## Matcher Organization
33
42
 
34
43
  CMDx matchers are organized into two primary files with comprehensive YARD documentation:
35
44
 
36
- | File | Purpose | Matcher Count |
37
- |------|---------|---------------|
38
- | `result_matchers.rb` | Task execution outcomes and side effects | 25+ matchers |
39
- | `task_matchers.rb` | Task behavior, validation, and lifecycle | 15+ matchers |
45
+ | Purpose | Matcher Count |
46
+ |---------|---------------|
47
+ | Task execution outcomes and side effects | 15+ matchers |
48
+ | Task behavior, validation, and lifecycle | 5+ matchers |
40
49
 
41
50
  All matchers include:
42
51
  - Complete parameter descriptions
@@ -427,15 +436,15 @@ expect(SimpleTask).not_to have_middleware(ComplexMiddleware)
427
436
 
428
437
  ```ruby
429
438
  # Test setting presence
430
- expect(ConfiguredTask).to have_task_setting(:timeout)
431
- expect(CustomTask).to have_task_setting(:priority)
439
+ expect(ConfiguredTask).to have_cmd_setting(:timeout)
440
+ expect(CustomTask).to have_cmd_setting(:priority)
432
441
 
433
442
  # Test setting with specific value
434
- expect(TimedTask).to have_task_setting(:timeout, 30)
435
- expect(PriorityTask).to have_task_setting(:priority, "high")
443
+ expect(TimedTask).to have_cmd_setting(:timeout, 30)
444
+ expect(PriorityTask).to have_cmd_setting(:priority, "high")
436
445
 
437
446
  # Negated usage
438
- expect(SimpleTask).not_to have_task_setting(:complex_setting)
447
+ expect(SimpleTask).not_to have_cmd_setting(:complex_setting)
439
448
  ```
440
449
 
441
450
  ## Composable Testing
@@ -546,5 +555,5 @@ end
546
555
 
547
556
  ---
548
557
 
549
- - **Prev:** [Logging](logging.md)
558
+ - **Prev:** [Internationalization (i18n)](internationalization.md)
550
559
  - **Next:** [AI Prompts](ai_prompts.md)
@@ -4,6 +4,7 @@ This guide covers advanced patterns and optimization techniques for getting the
4
4
 
5
5
  ## Table of Contents
6
6
 
7
+ - [TLDR](#tldr)
7
8
  - [Project Organization](#project-organization)
8
9
  - [Directory Structure](#directory-structure)
9
10
  - [Naming Conventions](#naming-conventions)
@@ -12,6 +13,14 @@ This guide covers advanced patterns and optimization techniques for getting the
12
13
  - [Monitoring and Observability](#monitoring-and-observability)
13
14
  - [ActiveRecord Query Tagging](#activerecord-query-tagging)
14
15
 
16
+ ## TLDR
17
+
18
+ - **Organization** - Group commands by domain in `/app/commands` with descriptive subdirectories
19
+ - **Naming** - Tasks use "Verb + Noun + Task", workflows use "Noun + Verb + Workflow"
20
+ - **Parameter optimization** - Use `with_options` to reduce duplication in parameter definitions
21
+ - **Monitoring** - Enable ActiveRecord query tagging for better debugging and observability
22
+ - **Base classes** - Create `ApplicationTask` and `ApplicationWorkflow` for shared configuration
23
+
15
24
  ## Project Organization
16
25
 
17
26
  ### Directory Structure
data/docs/workflows.md CHANGED
@@ -6,6 +6,7 @@ Workflows inherit from Task, gaining all task capabilities including callbacks,
6
6
 
7
7
  ## Table of Contents
8
8
 
9
+ - [TLDR](#tldr)
9
10
  - [Basic Usage](#basic-usage)
10
11
  - [Task Declaration](#task-declaration)
11
12
  - [Context Propagation](#context-propagation)
@@ -21,6 +22,15 @@ Workflows inherit from Task, gaining all task capabilities including callbacks,
21
22
  - [Task Settings Integration](#task-settings-integration)
22
23
  - [Generator](#generator)
23
24
 
25
+ ## TLDR
26
+
27
+ - **Purpose** - Orchestrate sequential execution of multiple tasks in linear pipeline
28
+ - **Declaration** - Use `process` method to declare tasks in execution order
29
+ - **Context sharing** - Context object shared across all tasks for data pipeline
30
+ - **Conditional execution** - Support `:if` and `:unless` options for conditional tasks
31
+ - **Halt behavior** - Configurable stopping on failed/skipped results (default: halt on failed only)
32
+ - **No call method** - Workflows automatically provide execution logic, don't define `call`
33
+
24
34
  ## Basic Usage
25
35
 
26
36
  > [!WARNING]
@@ -143,12 +153,12 @@ end
143
153
 
144
154
  ### Class-Level Configuration
145
155
 
146
- Configure halt behavior for the entire workflow using `task_settings!`:
156
+ Configure halt behavior for the entire workflow using `cmd_settings!`:
147
157
 
148
158
  ```ruby
149
159
  class CriticalDataProcessingWorkflow < CMDx::Workflow
150
160
  # Halt on both failed and skipped results
151
- task_settings!(workflow_halt: [CMDx::Result::FAILED, CMDx::Result::SKIPPED])
161
+ cmd_settings!(workflow_halt: [CMDx::Result::FAILED, CMDx::Result::SKIPPED])
152
162
 
153
163
  process LoadCriticalDataTask
154
164
  process ValidateCriticalDataTask
@@ -156,7 +166,7 @@ end
156
166
 
157
167
  class OptionalDataProcessingWorkflow < CMDx::Workflow
158
168
  # Never halt, always continue
159
- task_settings!(workflow_halt: [])
169
+ cmd_settings!(workflow_halt: [])
160
170
 
161
171
  process TryLoadDataTask
162
172
  process TryValidateDataTask
@@ -264,7 +274,7 @@ Workflows support all task settings and can be configured like regular tasks:
264
274
  ```ruby
265
275
  class PaymentProcessingWorkflow < CMDx::Workflow
266
276
  # Configure workflow-specific settings
267
- task_settings!(
277
+ cmd_settings!(
268
278
  workflow_halt: [CMDx::Result::FAILED],
269
279
  log_level: :debug,
270
280
  tags: [:critical, :payment]
data/lib/cmdx/.DS_Store CHANGED
Binary file
data/lib/cmdx/callback.rb CHANGED
@@ -1,67 +1,47 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CMDx
4
- ##
5
- # Base class for CMDx callbacks that provides lifecycle execution points.
4
+ # Base class for implementing callback functionality in task execution.
6
5
  #
7
- # Callback components can wrap or observe task execution at specific lifecycle
8
- # points like before validation, on success, after execution, etc.
9
- # Each callback must implement the `call` method which receives the
10
- # task instance and callback context.
11
- #
12
- # @example Basic callback implementation
13
- # class LoggingCallback < CMDx::Callback
14
- # def call(task, callback_type)
15
- # puts "Executing #{callback_type} callback for #{task.class.name}"
16
- # task.logger.info("Callback executed: #{callback_type}")
17
- # end
18
- # end
19
- #
20
- # @example Callback with initialization parameters
21
- # class NotificationCallback < CMDx::Callback
22
- # def initialize(channels)
23
- # @channels = channels
24
- # end
25
- #
26
- # def call(task, callback_type)
27
- # return unless callback_type == :on_success
28
- #
29
- # @channels.each do |channel|
30
- # NotificationService.send(channel, "Task #{task.class.name} completed")
31
- # end
32
- # end
33
- # end
34
- #
35
- # @example Conditional callback execution
36
- # class ErrorReportingCallback < CMDx::Callback
37
- # def call(task, callback_type)
38
- # return unless callback_type == :on_failure
39
- # return unless task.result.failed?
40
- #
41
- # ErrorReporter.notify(
42
- # task.errors.full_messages.join(", "),
43
- # context: task.context.to_h
44
- # )
45
- # end
46
- # end
47
- #
48
- # @see CallbackRegistry Callback management
49
- # @see Task Callback integration
50
- # @since 1.0.0
6
+ # Callbacks are executed at specific points during task lifecycle to
7
+ # provide hooks for custom behavior, logging, validation, or cleanup.
8
+ # All callback implementations must inherit from this class and implement
9
+ # the abstract call method.
51
10
  class Callback
52
11
 
53
- ##
54
- # Executes the callback logic.
12
+ # Executes a callback by creating a new instance and calling it.
13
+ #
14
+ # @param task [Task] the task instance executing the callback
15
+ # @param type [Symbol] the callback type identifier
16
+ #
17
+ # @return [Object] the result of the callback execution
18
+ #
19
+ # @raise [UndefinedCallError] when the callback subclass doesn't implement call
20
+ #
21
+ # @example Execute a callback on a task
22
+ # MyCallback.call(task, :before)
23
+ def self.call(task, type)
24
+ new.call(task, type)
25
+ end
26
+
27
+ # Abstract method that must be implemented by callback subclasses.
28
+ #
29
+ # This method contains the actual callback logic to be executed.
30
+ # Subclasses must override this method to provide their specific
31
+ # callback implementation.
32
+ #
33
+ # @param _task [Task] the task instance executing the callback
34
+ # @param _type [Symbol] the callback type identifier
35
+ #
36
+ # @return [Object] the result of the callback execution
55
37
  #
56
- # This method must be implemented by subclasses to define the callback
57
- # behavior. The method receives the task instance and the callback type
58
- # being executed.
38
+ # @raise [UndefinedCallError] always raised in the base class
59
39
  #
60
- # @param task [Task] the task instance being executed
61
- # @param callback_type [Symbol] the type of callback being executed
62
- # @return [void]
63
- # @abstract Subclasses must implement this method
64
- def call(_task, _callback_type)
40
+ # @example Implement in a subclass
41
+ # def call(task, type)
42
+ # puts "Executing #{type} callback for #{task.class.name}"
43
+ # end
44
+ def call(_task, _type)
65
45
  raise UndefinedCallError, "call method not defined in #{self.class.name}"
66
46
  end
67
47
 
@@ -1,106 +1,115 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CMDx
4
- ##
5
- # The CallbackRegistry collection provides a lifecycle callback system that executes
6
- # registered callbacks at specific points during task execution. Callbacks can be
7
- # conditionally executed based on task state and support both method references
8
- # and callable objects.
4
+ # Registry for managing callback definitions and execution within tasks.
9
5
  #
10
- # The CallbackRegistry collection extends Hash to provide specialized functionality for
11
- # managing collections of callback definitions within CMDx tasks. It handles
12
- # callback registration, conditional execution, and inspection.
13
- #
14
- # @example Basic callback usage
15
- # callback_registry = CallbackRegistry.new
16
- # callback_registry.register(:before_validation, :check_permissions)
17
- # callback_registry.register(:on_success, :log_success, if: :important?)
18
- # callback_registry.register(:on_failure, proc { alert_admin }, unless: :test_env?)
19
- #
20
- # callback_registry.call(task, :before_validation)
21
- #
22
- # @example Hash-like operations
23
- # callback_registry[:before_validation] = [[:check_permissions, {}]]
24
- # callback_registry.keys # => [:before_validation]
25
- # callback_registry.empty? # => false
26
- # callback_registry.each { |callback_name, callbacks| puts "#{callback_name}: #{callbacks}" }
27
- #
28
- # @see Callback Base callback execution class
29
- # @see Task Task lifecycle callbacks
30
- # @since 1.0.0
31
- class CallbackRegistry < Hash
6
+ # This registry handles the registration and execution of callbacks at various
7
+ # points in the task lifecycle, including validation, execution, and outcome
8
+ # handling phases.
9
+ class CallbackRegistry
10
+
11
+ TYPES = [
12
+ :before_validation,
13
+ :after_validation,
14
+ :before_execution,
15
+ :after_execution,
16
+ :on_executed,
17
+ :on_good,
18
+ :on_bad,
19
+ *Result::STATUSES.map { |s| :"on_#{s}" },
20
+ *Result::STATES.map { |s| :"on_#{s}" }
21
+ ].freeze
32
22
 
33
- ##
34
- # Initializes a new CallbackRegistry.
23
+ # The internal hash storing callback definitions.
35
24
  #
36
- # @param registry [CallbackRegistry, Hash, nil] Optional registry to copy from
25
+ # @return [Hash] hash containing callback type keys and callback definition arrays
26
+ attr_reader :registry
27
+
28
+ # Initializes a new callback registry.
37
29
  #
38
- # @example Initialize empty registry
39
- # registry = CallbackRegistry.new
30
+ # @param registry [Hash] initial registry hash with callback definitions
40
31
  #
41
- # @example Initialize with existing registry
42
- # global_callbacks = CallbackRegistry.new
43
- # task_callbacks = CallbackRegistry.new(global_callbacks)
44
- def initialize(registry = nil)
45
- super()
46
-
47
- registry&.each do |callback_type, callback_definitions|
48
- self[callback_type] = callback_definitions.dup
49
- end
32
+ # @return [CallbackRegistry] a new callback registry instance
33
+ #
34
+ # @example Creating an empty registry
35
+ # CallbackRegistry.new
36
+ #
37
+ # @example Creating a registry with initial callbacks
38
+ # CallbackRegistry.new(before_execution: [[:my_callback, {}]])
39
+ def initialize(registry = {})
40
+ @registry = registry.to_h
50
41
  end
51
42
 
52
- # Registers a callback for the given callback type.
43
+ # Registers one or more callbacks for a specific type.
53
44
  #
54
- # @param callback [Symbol] The callback type (e.g., :before_validation, :on_success)
55
- # @param callables [Array<Symbol, Proc, #call>] Methods or callables to execute
56
- # @param options [Hash] Conditions for callback execution
57
- # @option options [Symbol, Proc, #call] :if condition that must be truthy
58
- # @option options [Symbol, Proc, #call] :unless condition that must be falsy
59
- # @param block [Proc] Block to execute as part of the callback
60
- # @return [CallbackRegistry] self for method chaining
45
+ # @param type [Symbol] the callback type to register
46
+ # @param callables [Array<Object>] callable objects to register
47
+ # @param options [Hash] options for conditional callback execution
48
+ # @param block [Proc] optional block to register as a callback
61
49
  #
62
- # @example Register method callback
63
- # registry.register(:before_validation, :check_permissions)
50
+ # @return [CallbackRegistry] returns self for method chaining
64
51
  #
65
- # @example Register conditional callback
66
- # registry.register(:on_failure, :alert_admin, if: :critical?)
52
+ # @example Registering a symbol callback
53
+ # registry.register(:before_execution, :setup_database)
67
54
  #
68
- # @example Register proc callback
69
- # registry.register(:on_success, proc { log_completion })
70
- def register(callback, *callables, **options, &block)
55
+ # @example Registering a Proc callback
56
+ # registry.register(:on_good, ->(task) { puts "Task completed: #{task.name}" })
57
+ #
58
+ # @example Registering a Callback class
59
+ # registry.register(:after_validation, NotificationCallback)
60
+ #
61
+ # @example Registering multiple callbacks with options
62
+ # registry.register(:on_good, :send_notification, :log_success, if: -> { Rails.env.production? })
63
+ #
64
+ # @example Registering a block callback
65
+ # registry.register(:after_validation) { |task| puts "Validation complete" }
66
+ def register(type, *callables, **options, &block)
71
67
  callables << block if block_given?
72
- (self[callback] ||= []).push([callables, options]).uniq!
68
+ (registry[type] ||= []).push([callables, options]).uniq!
73
69
  self
74
70
  end
75
71
 
76
- # Executes all callbacks registered for a specific callback type on the given task.
77
- # Each callback is evaluated for its conditions (if/unless) before execution.
72
+ # Executes all registered callbacks for a specific type.
73
+ #
74
+ # @param task [Task] the task instance to execute callbacks on
75
+ # @param type [Symbol] the callback type to execute
78
76
  #
79
- # @param task [Task] The task instance to execute callbacks on
80
- # @param callback [Symbol] The callback type to execute (e.g., :before_validation, :on_success)
81
77
  # @return [void]
82
78
  #
83
- # @example Execute callbacks
79
+ # @raise [UnknownCallbackError] when the callback type is not recognized
80
+ #
81
+ # @example Executing before_validation callbacks
84
82
  # registry.call(task, :before_validation)
85
83
  #
86
- # @example Execute conditional callbacks
87
- # # Only executes if task.critical? returns true
88
- # registry.call(task, :on_failure) # where registry has on_failure :alert, if: :critical?
89
- def call(task, callback)
90
- return unless key?(callback)
84
+ # @example Executing outcome callbacks
85
+ # registry.call(task, :on_good)
86
+ def call(task, type)
87
+ raise UnknownCallbackError, "unknown callback #{type}" unless TYPES.include?(type)
91
88
 
92
- Array(self[callback]).each do |callables, options|
93
- next unless task.__cmdx_eval(options)
89
+ Array(registry[type]).each do |callables, options|
90
+ next unless task.cmdx_eval(options)
94
91
 
95
- Array(callables).each do |c|
96
- if c.is_a?(Callback)
97
- c.call(task, callback)
92
+ Array(callables).each do |callable|
93
+ case callable
94
+ when Symbol, String, Proc
95
+ task.cmdx_try(callable)
98
96
  else
99
- task.__cmdx_try(c)
97
+ callable.call(task)
100
98
  end
101
99
  end
102
100
  end
103
101
  end
104
102
 
103
+ # Returns a hash representation of the registry.
104
+ #
105
+ # @return [Hash] a deep copy of the registry hash
106
+ #
107
+ # @example Getting registry contents
108
+ # registry.to_h
109
+ # # => { before_execution: [[:setup, {}]], on_good: [[:notify, { if: -> { true } }]] }
110
+ def to_h
111
+ registry.transform_values(&:dup)
112
+ end
113
+
105
114
  end
106
115
  end