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
@@ -1,103 +1,108 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CMDx
4
- ##
5
- # The MiddlewareRegistry collection provides a Rack-style middleware chain that wraps
6
- # task execution with cross-cutting concerns like logging, authentication,
7
- # caching, and more. Middleware can short-circuit execution by returning
8
- # early without calling the next middleware in the chain.
4
+ # Registry for managing middleware definitions and execution within tasks.
9
5
  #
10
- # The MiddlewareRegistry collection extends Array to provide specialized functionality for
11
- # managing collections of middleware definitions within CMDx tasks. It handles
12
- # middleware execution coordination, chaining, and inspection.
13
- #
14
- # @example Basic middleware usage
15
- # middleware_registry = MiddlewareRegistry.new
16
- # middleware_registry.use(LoggingMiddleware)
17
- # middleware_registry.use(AuthenticationMiddleware, required_role: :admin)
18
- # middleware_registry.use(CachingMiddleware, ttl: 300)
19
- #
20
- # result = middleware_registry.call(task) do |t|
21
- # t.call
22
- # t.result
23
- # end
24
- #
25
- # @example Array-like operations
26
- # middleware_registry << [LoggingMiddleware, [], nil]
27
- # middleware_registry.size # => 1
28
- # middleware_registry.empty? # => false
29
- # middleware_registry.each { |middleware| puts middleware.inspect }
30
- #
31
- # @example Using proc middleware
32
- # middleware_registry.use(proc do |task, callable|
33
- # puts "Before task execution"
34
- # result = callable.call(task)
35
- # puts "After task execution"
36
- # result
37
- # end)
38
- #
39
- # @see Middleware Base middleware class
40
- # @since 1.0.0
41
- class MiddlewareRegistry < Array
6
+ # This registry handles the registration and execution of middleware that can
7
+ # wrap task execution, providing cross-cutting concerns like logging, timing,
8
+ # authentication, and error handling.
9
+ class MiddlewareRegistry
10
+
11
+ # @return [Hash] hash containing middleware classes/objects and their configurations
12
+ attr_reader :registry
42
13
 
43
- # Adds middleware to the registry.
14
+ # Initializes a new middleware registry.
15
+ #
16
+ # @param registry [Hash] optional hash of initial middleware configurations
17
+ #
18
+ # @return [MiddlewareRegistry] a new middleware registry instance
19
+ #
20
+ # @example Creating an empty registry
21
+ # MiddlewareRegistry.new
22
+ #
23
+ # @example Creating a registry with initial middleware
24
+ # MiddlewareRegistry.new(TimeoutMiddleware => [[], {timeout: 30}, nil])
25
+ def initialize(registry = {})
26
+ @registry = registry.to_h
27
+ end
28
+
29
+ # Registers a middleware with the registry.
30
+ #
31
+ # @param middleware [Class, Object] the middleware class or instance to register
32
+ # @param args [Array] positional arguments to pass to middleware initialization
33
+ # @param kwargs [Hash] keyword arguments to pass to middleware initialization
34
+ # @param block [Proc] optional block to pass to middleware initialization
44
35
  #
45
- # @param middleware [Class, Object, Proc] The middleware to add
46
- # @param args [Array] Arguments to pass to middleware constructor
47
- # @param block [Proc] Block to pass to middleware constructor
48
36
  # @return [MiddlewareRegistry] self for method chaining
49
37
  #
50
- # @example Add middleware class
51
- # registry.use(LoggingMiddleware, log_level: :info)
38
+ # @example Register a middleware class
39
+ # registry.register(TimeoutMiddleware, 30)
52
40
  #
53
- # @example Add middleware instance
54
- # registry.use(LoggingMiddleware.new(log_level: :info))
41
+ # @example Register a middleware with keyword arguments
42
+ # registry.register(LoggingMiddleware, level: :info)
55
43
  #
56
- # @example Add proc middleware
57
- # registry.use(proc { |task, callable| callable.call(task) })
58
- def use(middleware, *args, &block)
59
- self << [middleware, args, block]
44
+ # @example Register a middleware with a block
45
+ # registry.register(CustomMiddleware) { |task| puts "Processing #{task.id}" }
46
+ def register(middleware, *args, **kwargs, &block)
47
+ registry[middleware] = [args, kwargs, block]
60
48
  self
61
49
  end
62
50
 
63
- # Executes the middleware chain around the given block.
64
- #
65
- # @param task [Task] The task instance to pass through middleware
66
- # @yield [Task] The task instance for final execution
67
- # @yieldreturn [Object] The result of task execution
68
- # @return [Object] The result from the middleware chain
69
- #
70
- # @example Execute with middleware
71
- # result = registry.call(task) do |t|
72
- # t.call
73
- # t.result
74
- # end
51
+ # Executes all registered middleware around the provided task.
52
+ #
53
+ # @param task [Task] the task instance to execute middleware around
54
+ # @param block [Proc] the block to execute after all middleware processing
55
+ #
56
+ # @return [Object] the result of the middleware chain execution
57
+ #
58
+ # @raise [ArgumentError] if no block is provided
59
+ #
60
+ # @example Execute middleware around a task
61
+ # registry.call(task) { |task| task.process }
62
+ #
63
+ # @example Execute with early return if no middleware
64
+ # registry.call(task) { |task| puts "No middleware to execute" }
75
65
  def call(task, &)
76
- return yield(task) if empty?
66
+ raise ArgumentError, "block required" unless block_given?
67
+
68
+ return yield(task) if registry.empty?
77
69
 
78
70
  build_chain(&).call(task)
79
71
  end
80
72
 
73
+ # Returns a hash representation of the registry.
74
+ #
75
+ # @return [Hash] deep copy of registry with duplicated configuration arrays
76
+ # @option return [Array] args duplicated positional arguments array
77
+ # @option return [Hash] kwargs duplicated keyword arguments hash
78
+ # @option return [Proc, nil] block the original block reference (not duplicated)
79
+ #
80
+ # @example Getting registry hash
81
+ # registry.to_h
82
+ # #=> { TimeoutMiddleware => [[30], {}, nil] }
83
+ def to_h
84
+ registry.transform_values do |config|
85
+ args, kwargs, block = config
86
+ [args.dup, kwargs.dup, block]
87
+ end
88
+ end
89
+
81
90
  private
82
91
 
83
- # Builds the middleware call chain.
92
+ # Builds the middleware execution chain by wrapping middleware around the call block.
93
+ #
94
+ # @param call_block [Proc] the final block to execute after all middleware
84
95
  #
85
- # Creates a nested chain of callables where each middleware wraps the next,
86
- # with the provided block as the innermost callable.
96
+ # @return [Proc] the complete middleware chain as a callable proc
87
97
  #
88
- # @param block [Proc] The final block to execute
89
- # @return [Proc] The middleware chain as a callable
90
- def build_chain(&block)
91
- reverse.reduce(block) do |next_callable, (middleware, args, middleware_block)|
98
+ # @example Building a middleware chain (internal use)
99
+ # build_chain { |task| task.process }
100
+ def build_chain(&call_block)
101
+ registry.reverse_each.reduce(call_block) do |next_callable, (middleware, config)|
92
102
  proc do |task|
93
- if middleware.respond_to?(:call) && !middleware.respond_to?(:new)
94
- # Proc middleware
95
- middleware.call(task, next_callable)
96
- else
97
- # Class or instance middleware
98
- instance = middleware.respond_to?(:new) ? middleware.new(*args, &middleware_block) : middleware
99
- instance.call(task, next_callable)
100
- end
103
+ args, kwargs, block = config
104
+ instance = middleware.respond_to?(:new) ? middleware.new(*args, **kwargs, &block) : middleware
105
+ instance.call(task, next_callable)
101
106
  end
102
107
  end
103
108
  end
@@ -2,255 +2,70 @@
2
2
 
3
3
  module CMDx
4
4
  module Middlewares
5
- ##
6
- # Correlation middleware for ensuring consistent correlation ID context during task execution.
7
- #
8
- # The Correlate middleware establishes and maintains correlation ID context throughout
9
- # task execution, enabling seamless request tracking across task boundaries. It ensures
10
- # that all tasks within an execution chain share the same correlation identifier for
11
- # comprehensive traceability and debugging.
12
- #
13
- # ## Correlation ID Precedence
14
- #
15
- # The middleware determines the correlation ID using the following precedence:
16
- # 1. **Explicit correlation ID** - Value provided during middleware initialization
17
- # 2. **Current thread correlation** - Existing correlation from `CMDx::Correlator.id`
18
- # 3. **Chain identifier** - The task's chain ID if no thread correlation exists
19
- # 4. **Generated UUID** - New correlation ID if none of the above is available
20
- #
21
- # ## Conditional Execution
22
- #
23
- # The middleware supports conditional execution using `:if` and `:unless` options:
24
- # - `:if` - Only applies correlation when the condition evaluates to true
25
- # - `:unless` - Only applies correlation when the condition evaluates to false
26
- # - Conditions can be Procs, method symbols, or boolean values
27
- #
28
- # ## Thread Safety
29
- #
30
- # The middleware uses `CMDx::Correlator.use` to establish a correlation context
31
- # that is automatically restored after task execution, ensuring thread-local
32
- # isolation and proper cleanup even in case of exceptions.
33
- #
34
- # ## Integration with CMDx Framework
35
- #
36
- # - **Automatic activation**: Can be applied globally or per-task via `use` directive
37
- # - **Chain integration**: Works seamlessly with CMDx::Chain correlation inheritance
38
- # - **Nested tasks**: Maintains correlation context across nested task calls
39
- # - **Exception safety**: Restores correlation context even when tasks fail
40
- #
41
- # @example Basic task-specific middleware application
42
- # class ProcessOrderTask < CMDx::Task
43
- # use CMDx::Middlewares::Correlate
44
- #
45
- # def call
46
- # # Task execution maintains correlation context
47
- # SendEmailTask.call(context) # Inherits same correlation
48
- # end
49
- # end
50
- #
51
- # @example Middleware with explicit correlation ID
52
- # class ProcessOrderTask < CMDx::Task
53
- # use CMDx::Middlewares::Correlate, id: "order-processing-123"
54
- #
55
- # def call
56
- # # Always uses "order-processing-123" as correlation ID
57
- # context.correlation_used = CMDx::Correlator.id
58
- # end
59
- # end
60
- #
61
- # result = ProcessOrderTask.call(order_id: 123)
62
- # result.context.correlation_used # => "order-processing-123"
63
- #
64
- # @example Middleware with dynamic correlation ID using procs
65
- # class ProcessOrderTask < CMDx::Task
66
- # use CMDx::Middlewares::Correlate, id: -> { "order-#{order_id}-#{Time.now.to_i}" }
67
- #
68
- # def call
69
- # # Uses dynamically generated correlation ID
70
- # context.correlation_used = CMDx::Correlator.id
71
- # end
72
- # end
73
- #
74
- # result = ProcessOrderTask.call(order_id: 456)
75
- # result.context.correlation_used # => "order-456-1703123456"
76
- #
77
- # @example Middleware with method-based correlation ID
78
- # class ProcessOrderTask < CMDx::Task
79
- # use CMDx::Middlewares::Correlate, id: :generate_order_correlation
80
- #
81
- # def call
82
- # # Uses correlation ID from generate_order_correlation method
83
- # context.correlation_used = CMDx::Correlator.id
84
- # end
85
- #
86
- # private
87
- #
88
- # def generate_order_correlation
89
- # "order-#{order_id}-#{context.request_id}"
90
- # end
91
- # end
92
- #
93
- # @example Conditional correlation based on environment
94
- # class ProcessOrderTask < CMDx::Task
95
- # use CMDx::Middlewares::Correlate, unless: -> { Rails.env.test? }
96
- #
97
- # def call
98
- # # Correlation only applied in non-test environments
99
- # context.order = Order.find(order_id)
100
- # end
101
- # end
102
- #
103
- # @example Conditional correlation based on task state
104
- # class ProcessOrderTask < CMDx::Task
105
- # use CMDx::Middlewares::Correlate, if: :correlation_required?
106
- #
107
- # def call
108
- # # Correlation applied only when correlation_required? returns true
109
- # context.order = Order.find(order_id)
110
- # end
111
- #
112
- # private
113
- #
114
- # def correlation_required?
115
- # context.tracking_enabled == true
116
- # end
117
- # end
118
- #
119
- # @example Nested task correlation propagation
120
- # class ParentTask < CMDx::Task
121
- # use CMDx::Middlewares::Correlate
122
- #
123
- # def call
124
- # # Correlation established at parent level
125
- # ChildTask.call(context)
126
- # end
127
- # end
128
- #
129
- # class ChildTask < CMDx::Task
130
- # use CMDx::Middlewares::Correlate
131
- #
132
- # def call
133
- # # Inherits parent's correlation ID
134
- # context.child_correlation = CMDx::Correlator.id
135
- # end
136
- # end
137
- #
138
- # @example Exception handling with correlation restoration
139
- # class RiskyTask < CMDx::Task
140
- # use CMDx::Middlewares::Correlate
141
- #
142
- # def call
143
- # raise StandardError, "Task failed"
144
- # end
145
- # end
146
- #
147
- # CMDx::Correlator.id = "original-correlation"
148
- #
149
- # begin
150
- # RiskyTask.call
151
- # rescue StandardError
152
- # CMDx::Correlator.id # => "original-correlation" (properly restored)
153
- # end
154
- #
155
- # @see CMDx::Correlator Thread-safe correlation ID management
156
- # @see CMDx::Chain Chain execution context with correlation inheritance
157
- # @see CMDx::Middleware Base middleware class
158
- # @since 1.0.0
5
+ # Middleware that manages correlation IDs for task execution tracing.
6
+ # Automatically generates or uses provided correlation IDs to track task execution
7
+ # across complex workflows, enabling better debugging and monitoring.
159
8
  class Correlate < CMDx::Middleware
160
9
 
161
- # @return [String, nil] The explicit correlation ID to use
10
+ # @return [String, Symbol, Proc, nil] The explicit correlation ID to use, or callable that generates one
11
+ attr_reader :id
12
+
162
13
  # @return [Hash] The conditional options for correlation application
163
- attr_reader :id, :conditional
14
+ attr_reader :conditional
164
15
 
165
- ##
166
- # Initializes the Correlate middleware with optional configuration.
16
+ # Initializes the correlation middleware with optional configuration.
167
17
  #
168
18
  # @param options [Hash] configuration options for the middleware
169
- # @option options [String, Symbol, Proc] :id explicit correlation ID to use (takes precedence over all other sources)
170
- # @option options [Proc, Symbol, Boolean] :if condition that must be true for middleware to execute
171
- # @option options [Proc, Symbol, Boolean] :unless condition that must be false for middleware to execute
19
+ # @option options [String, Symbol, Proc] :id explicit correlation ID or callable to generate one
20
+ # @option options [Symbol, Proc] :if condition that must be truthy to apply correlation
21
+ # @option options [Symbol, Proc] :unless condition that must be falsy to apply correlation
22
+ #
23
+ # @return [Correlate] new instance of the middleware
24
+ #
25
+ # @example Register with a middleware instance
26
+ # use :middleware, CMDx::Middlewares::Correlate.new(id: "request-123")
172
27
  #
173
- # @example Basic initialization
174
- # middleware = CMDx::Middlewares::Correlate.new
28
+ # @example Register with explicit ID
29
+ # use :middleware, CMDx::Middlewares::Correlate, id: "request-123"
175
30
  #
176
- # @example With explicit correlation ID
177
- # middleware = CMDx::Middlewares::Correlate.new(id: "api-request-123")
31
+ # @example Register with dynamic ID generation
32
+ # use :middleware, CMDx::Middlewares::Correlate, id: -> { SecureRandom.uuid }
178
33
  #
179
- # @example With conditional execution
180
- # middleware = CMDx::Middlewares::Correlate.new(unless: -> { Rails.env.test? })
181
- # middleware = CMDx::Middlewares::Correlate.new(if: :correlation_enabled?)
34
+ # @example Register with conditions
35
+ # use :middleware, CMDx::Middlewares::Correlate, if: :production?, unless: :testing?
182
36
  def initialize(options = {})
183
37
  @id = options[:id]
184
38
  @conditional = options.slice(:if, :unless)
185
39
  end
186
40
 
187
- ##
188
- # Executes the task within a managed correlation context.
189
- #
190
- # First evaluates any conditional execution rules (`:if` or `:unless` options).
191
- # If conditions allow execution, establishes a correlation ID using the
192
- # precedence hierarchy and executes the task within that correlation context.
193
- # The correlation ID is automatically restored after task completion, ensuring
194
- # proper cleanup and thread isolation.
195
- #
196
- # The correlation ID determination follows this precedence:
197
- # 1. Explicit correlation ID (provided during middleware initialization)
198
- # - String/Symbol: Used as-is or called as method if task responds to it
199
- # - Proc/Lambda: Executed in task context for dynamic generation
200
- # 2. Current thread correlation (CMDx::Correlator.id)
201
- # 3. Task's chain ID (task.chain.id)
202
- # 4. Generated UUID (CMDx::Correlator.generate)
203
- #
204
- # @param task [CMDx::Task] the task instance to execute
205
- # @param callable [#call] the callable that executes the task
206
- # @return [CMDx::Result] the task execution result
207
- #
208
- # @example Basic middleware execution
209
- # middleware = CMDx::Middlewares::Correlate.new
210
- # task = ProcessOrderTask.new(order_id: 123)
211
- # callable = -> { task.call }
212
- #
213
- # result = middleware.call(task, callable)
214
- # # Task executed within correlation context
215
- #
216
- # @example Correlation ID precedence in action
217
- # # Scenario 1: Explicit string correlation ID takes precedence
218
- # middleware = CMDx::Middlewares::Correlate.new(id: "explicit-123")
219
- # middleware.call(task, callable) # Uses "explicit-123"
220
- #
221
- # # Scenario 2: Dynamic correlation ID using proc
222
- # middleware = CMDx::Middlewares::Correlate.new(id: -> { "dynamic-#{order_id}" })
223
- # middleware.call(task, callable) # Uses result of proc execution
41
+ # Executes the middleware, wrapping task execution with correlation context.
42
+ # Evaluates conditions, determines correlation ID, and executes the task within
43
+ # the correlation context for tracing purposes.
224
44
  #
225
- # # Scenario 3: Method-based correlation ID
226
- # middleware = CMDx::Middlewares::Correlate.new(id: :correlation_method)
227
- # middleware.call(task, callable) # Uses task.correlation_method if it exists
45
+ # @param task [CMDx::Task] the task being executed
46
+ # @param callable [Proc] the callable that executes the task
228
47
  #
229
- # # Scenario 4: Thread correlation when no explicit ID
230
- # CMDx::Correlator.id = "thread-correlation"
231
- # middleware = CMDx::Middlewares::Correlate.new
232
- # middleware.call(task, callable) # Uses "thread-correlation"
48
+ # @return [Object] the result of the task execution
233
49
  #
234
- # # Scenario 5: Chain ID when no explicit or thread correlation
235
- # CMDx::Correlator.clear
236
- # middleware.call(task, callable) # Uses task.chain.id
50
+ # @example Task using correlation middleware
51
+ # class ProcessOrderTask < CMDx::Task
52
+ # use :middleware, CMDx::Middlewares::Correlate, id: "trace-123"
237
53
  #
238
- # # Scenario 6: Generated UUID when no other correlation exists
239
- # CMDx::Correlator.clear
240
- # # Assuming task.chain.id is nil
241
- # middleware.call(task, callable) # Uses generated UUID
54
+ # def call
55
+ # # Task execution is automatically wrapped with correlation
56
+ # end
57
+ # end
242
58
  #
243
- # @example Conditional execution
244
- # # Middleware only executes in production
245
- # middleware = CMDx::Middlewares::Correlate.new(if: -> { Rails.env.production? })
246
- # result = middleware.call(task, callable)
247
- # # Correlation applied only in production environment
59
+ # @example Global configuration with conditional tracing
60
+ # CMDx.configure do |config|
61
+ # config.middlewares.register CMDx::Middlewares::Correlate, if: :should_trace?
62
+ # end
248
63
  def call(task, callable)
249
64
  # Check if correlation should be applied based on conditions
250
- return callable.call(task) unless task.__cmdx_eval(conditional)
65
+ return callable.call(task) unless task.cmdx_eval(conditional)
251
66
 
252
67
  # Get correlation ID using yield for dynamic generation
253
- correlation_id = task.__cmdx_yield(id) ||
68
+ correlation_id = task.cmdx_yield(id) ||
254
69
  CMDx::Correlator.id ||
255
70
  task.chain.id ||
256
71
  CMDx::Correlator.generate