cmdx 0.5.0 → 1.0.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.
- checksums.yaml +4 -4
- data/.DS_Store +0 -0
- data/.cursor/rules/cursor-instructions.mdc +6 -0
- data/.rubocop.yml +19 -1
- data/.ruby-version +1 -1
- data/CHANGELOG.md +95 -28
- data/README.md +73 -25
- data/docs/ai_prompts.md +319 -0
- data/docs/basics/call.md +234 -14
- data/docs/basics/chain.md +280 -0
- data/docs/basics/context.md +241 -33
- data/docs/basics/setup.md +85 -12
- data/docs/callbacks.md +283 -0
- data/docs/configuration.md +155 -30
- data/docs/getting_started.md +145 -22
- data/docs/internationalization.md +148 -0
- data/docs/interruptions/exceptions.md +198 -11
- data/docs/interruptions/faults.md +196 -44
- data/docs/interruptions/halt.md +188 -35
- data/docs/logging.md +204 -53
- data/docs/middlewares.md +745 -0
- data/docs/outcomes/result.md +305 -10
- data/docs/outcomes/states.md +212 -31
- data/docs/outcomes/statuses.md +284 -30
- data/docs/parameters/coercions.md +411 -29
- data/docs/parameters/defaults.md +258 -25
- data/docs/parameters/definitions.md +247 -72
- data/docs/parameters/namespacing.md +259 -27
- data/docs/parameters/validations.md +173 -168
- data/docs/testing.md +560 -0
- data/docs/tips_and_tricks.md +103 -42
- data/docs/workflows.md +329 -0
- data/lib/cmdx/.DS_Store +0 -0
- data/lib/cmdx/callback.rb +69 -0
- data/lib/cmdx/callback_registry.rb +106 -0
- data/lib/cmdx/chain.rb +190 -0
- data/lib/cmdx/chain_inspector.rb +149 -0
- data/lib/cmdx/chain_serializer.rb +175 -0
- data/lib/cmdx/coercions/array.rb +37 -0
- data/lib/cmdx/coercions/big_decimal.rb +33 -0
- data/lib/cmdx/coercions/boolean.rb +41 -1
- data/lib/cmdx/coercions/complex.rb +31 -0
- data/lib/cmdx/coercions/date.rb +39 -0
- data/lib/cmdx/coercions/date_time.rb +39 -0
- data/lib/cmdx/coercions/float.rb +31 -0
- data/lib/cmdx/coercions/hash.rb +42 -0
- data/lib/cmdx/coercions/integer.rb +32 -0
- data/lib/cmdx/coercions/rational.rb +31 -0
- data/lib/cmdx/coercions/string.rb +31 -0
- data/lib/cmdx/coercions/time.rb +39 -0
- data/lib/cmdx/coercions/virtual.rb +31 -0
- data/lib/cmdx/configuration.rb +217 -9
- data/lib/cmdx/context.rb +173 -2
- data/lib/cmdx/core_ext/hash.rb +72 -0
- data/lib/cmdx/core_ext/module.rb +94 -0
- data/lib/cmdx/core_ext/object.rb +105 -0
- data/lib/cmdx/correlator.rb +217 -0
- data/lib/cmdx/error.rb +210 -8
- data/lib/cmdx/errors.rb +256 -1
- data/lib/cmdx/fault.rb +177 -2
- data/lib/cmdx/faults.rb +158 -2
- data/lib/cmdx/immutator.rb +121 -2
- data/lib/cmdx/lazy_struct.rb +261 -18
- data/lib/cmdx/log_formatters/json.rb +46 -0
- data/lib/cmdx/log_formatters/key_value.rb +46 -0
- data/lib/cmdx/log_formatters/line.rb +54 -0
- data/lib/cmdx/log_formatters/logstash.rb +64 -0
- data/lib/cmdx/log_formatters/pretty_json.rb +57 -0
- data/lib/cmdx/log_formatters/pretty_key_value.rb +51 -0
- data/lib/cmdx/log_formatters/pretty_line.rb +60 -0
- data/lib/cmdx/log_formatters/raw.rb +54 -0
- data/lib/cmdx/logger.rb +85 -0
- data/lib/cmdx/logger_ansi.rb +93 -7
- data/lib/cmdx/logger_serializer.rb +116 -0
- data/lib/cmdx/middleware.rb +74 -0
- data/lib/cmdx/middleware_registry.rb +106 -0
- data/lib/cmdx/middlewares/correlate.rb +266 -0
- data/lib/cmdx/middlewares/timeout.rb +232 -0
- data/lib/cmdx/parameter.rb +228 -1
- data/lib/cmdx/parameter_inspector.rb +61 -0
- data/lib/cmdx/parameter_registry.rb +125 -0
- data/lib/cmdx/parameter_serializer.rb +83 -0
- data/lib/cmdx/parameter_validator.rb +62 -0
- data/lib/cmdx/parameter_value.rb +109 -1
- data/lib/cmdx/parameters_inspector.rb +59 -0
- data/lib/cmdx/parameters_serializer.rb +102 -0
- data/lib/cmdx/railtie.rb +123 -3
- data/lib/cmdx/result.rb +367 -25
- data/lib/cmdx/result_ansi.rb +105 -9
- data/lib/cmdx/result_inspector.rb +76 -0
- data/lib/cmdx/result_logger.rb +90 -3
- data/lib/cmdx/result_serializer.rb +137 -0
- data/lib/cmdx/rspec/result_matchers.rb +917 -0
- data/lib/cmdx/rspec/task_matchers.rb +570 -0
- data/lib/cmdx/task.rb +405 -37
- data/lib/cmdx/task_serializer.rb +74 -2
- data/lib/cmdx/utils/ansi_color.rb +95 -0
- data/lib/cmdx/utils/log_timestamp.rb +48 -0
- data/lib/cmdx/utils/monotonic_runtime.rb +71 -4
- data/lib/cmdx/utils/name_affix.rb +78 -0
- data/lib/cmdx/validators/custom.rb +82 -0
- data/lib/cmdx/validators/exclusion.rb +94 -0
- data/lib/cmdx/validators/format.rb +102 -8
- data/lib/cmdx/validators/inclusion.rb +104 -0
- data/lib/cmdx/validators/length.rb +128 -0
- data/lib/cmdx/validators/numeric.rb +128 -0
- data/lib/cmdx/validators/presence.rb +93 -7
- data/lib/cmdx/version.rb +7 -1
- data/lib/cmdx/workflow.rb +394 -0
- data/lib/cmdx.rb +25 -64
- data/lib/generators/cmdx/install_generator.rb +37 -1
- data/lib/generators/cmdx/task_generator.rb +69 -1
- data/lib/generators/cmdx/templates/install.rb +43 -15
- data/lib/generators/cmdx/workflow_generator.rb +109 -0
- data/lib/locales/ar.yml +36 -0
- data/lib/locales/cs.yml +36 -0
- data/lib/locales/da.yml +36 -0
- data/lib/locales/de.yml +36 -0
- data/lib/locales/el.yml +36 -0
- data/lib/locales/en.yml +20 -20
- data/lib/locales/es.yml +20 -20
- data/lib/locales/fi.yml +36 -0
- data/lib/locales/fr.yml +36 -0
- data/lib/locales/he.yml +36 -0
- data/lib/locales/hi.yml +36 -0
- data/lib/locales/it.yml +36 -0
- data/lib/locales/ja.yml +36 -0
- data/lib/locales/ko.yml +36 -0
- data/lib/locales/nl.yml +36 -0
- data/lib/locales/no.yml +36 -0
- data/lib/locales/pl.yml +36 -0
- data/lib/locales/pt.yml +36 -0
- data/lib/locales/ru.yml +36 -0
- data/lib/locales/sv.yml +36 -0
- data/lib/locales/th.yml +36 -0
- data/lib/locales/tr.yml +36 -0
- data/lib/locales/vi.yml +36 -0
- data/lib/locales/zh.yml +36 -0
- metadata +77 -15
- data/docs/basics/run.md +0 -34
- data/docs/batch.md +0 -53
- data/docs/example.md +0 -82
- data/docs/hooks.md +0 -62
- data/lib/cmdx/batch.rb +0 -43
- data/lib/cmdx/parameters.rb +0 -35
- data/lib/cmdx/run.rb +0 -39
- data/lib/cmdx/run_inspector.rb +0 -26
- data/lib/cmdx/run_serializer.rb +0 -20
- data/lib/cmdx/task_hook.rb +0 -18
- data/lib/generators/cmdx/batch_generator.rb +0 -30
- /data/lib/generators/cmdx/templates/{batch.rb.tt → workflow.rb.tt} +0 -0
data/lib/cmdx/logger_ansi.rb
CHANGED
@@ -1,22 +1,108 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module CMDx
|
4
|
+
# ANSI color formatting module for logger severity levels.
|
5
|
+
#
|
6
|
+
# The LoggerAnsi module provides ANSI color formatting for log severity levels
|
7
|
+
# to enhance readability in terminal output. Each severity level is assigned
|
8
|
+
# a specific color and bold formatting to make log messages more visually
|
9
|
+
# distinguishable.
|
10
|
+
#
|
11
|
+
# @example Basic severity colorization
|
12
|
+
# LoggerAnsi.call("DEBUG message") # => Blue bold text
|
13
|
+
# LoggerAnsi.call("INFO message") # => Green bold text
|
14
|
+
# LoggerAnsi.call("WARN message") # => Yellow bold text
|
15
|
+
# LoggerAnsi.call("ERROR message") # => Red bold text
|
16
|
+
# LoggerAnsi.call("FATAL message") # => Magenta bold text
|
17
|
+
#
|
18
|
+
# @example Usage in log formatters
|
19
|
+
# class CustomFormatter
|
20
|
+
# def call(severity, time, progname, msg)
|
21
|
+
# colored_severity = LoggerAnsi.call(severity)
|
22
|
+
# "#{colored_severity} #{msg}"
|
23
|
+
# end
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# @example Integration with pretty formatters
|
27
|
+
# # Used internally by PrettyLine, PrettyJson, PrettyKeyValue formatters
|
28
|
+
# formatted_severity = LoggerAnsi.call("ERROR") # => Red bold "ERROR"
|
29
|
+
#
|
30
|
+
# @see CMDx::Utils::AnsiColor ANSI color utility functions
|
31
|
+
# @see CMDx::LogFormatters::PrettyLine Pretty line formatter with colors
|
32
|
+
# @see CMDx::LogFormatters::PrettyJson Pretty JSON formatter with colors
|
4
33
|
module LoggerAnsi
|
5
34
|
|
35
|
+
# Mapping of log severity levels to ANSI colors.
|
36
|
+
#
|
37
|
+
# Maps the first character of severity levels to their corresponding
|
38
|
+
# color codes for consistent visual representation across log output.
|
6
39
|
SEVERITY_COLORS = {
|
7
|
-
"D" => :blue,
|
8
|
-
"I" => :green,
|
9
|
-
"W" => :yellow,
|
10
|
-
"E" => :red,
|
11
|
-
"F" => :magenta
|
40
|
+
"D" => :blue, # DEBUG
|
41
|
+
"I" => :green, # INFO
|
42
|
+
"W" => :yellow, # WARN
|
43
|
+
"E" => :red, # ERROR
|
44
|
+
"F" => :magenta # FATAL
|
12
45
|
}.freeze
|
13
46
|
|
14
47
|
module_function
|
15
48
|
|
49
|
+
# Applies ANSI color formatting to a severity string.
|
50
|
+
#
|
51
|
+
# Formats the input string with appropriate ANSI color codes based on
|
52
|
+
# the first character of the string, which typically represents the
|
53
|
+
# log severity level. All formatted text is rendered in bold.
|
54
|
+
#
|
55
|
+
# @param s [String] The severity string to colorize
|
56
|
+
# @return [String] The string with ANSI color codes applied
|
57
|
+
#
|
58
|
+
# @example Colorizing different severity levels
|
59
|
+
# LoggerAnsi.call("DEBUG") # => "\e[1;34mDEBUG\e[0m" (blue bold)
|
60
|
+
# LoggerAnsi.call("INFO") # => "\e[1;32mINFO\e[0m" (green bold)
|
61
|
+
# LoggerAnsi.call("WARN") # => "\e[1;33mWARN\e[0m" (yellow bold)
|
62
|
+
# LoggerAnsi.call("ERROR") # => "\e[1;31mERROR\e[0m" (red bold)
|
63
|
+
# LoggerAnsi.call("FATAL") # => "\e[1;35mFATAL\e[0m" (magenta bold)
|
64
|
+
#
|
65
|
+
# @example Unknown severity level
|
66
|
+
# LoggerAnsi.call("CUSTOM") # => "\e[1;39mCUSTOM\e[0m" (default color bold)
|
67
|
+
#
|
68
|
+
# @example Full log message formatting
|
69
|
+
# severity = "ERROR"
|
70
|
+
# message = "Task failed with validation errors"
|
71
|
+
# colored_severity = LoggerAnsi.call(severity)
|
72
|
+
# log_line = "#{colored_severity}: #{message}"
|
73
|
+
# # => "\e[1;31mERROR\e[0m: Task failed with validation errors"
|
16
74
|
def call(s)
|
17
|
-
color
|
75
|
+
Utils::AnsiColor.call(s, color: color(s), mode: :bold)
|
76
|
+
end
|
18
77
|
|
19
|
-
|
78
|
+
# Determines the appropriate color for a severity string.
|
79
|
+
#
|
80
|
+
# Extracts the first character from the severity string and maps it to
|
81
|
+
# the corresponding color symbol using the SEVERITY_COLORS hash. If no
|
82
|
+
# mapping is found for the first character, returns the default color.
|
83
|
+
#
|
84
|
+
# @param s [String] The severity string to determine color for
|
85
|
+
# @return [Symbol] The color symbol for the severity level
|
86
|
+
#
|
87
|
+
# @example Mapping severity levels to colors
|
88
|
+
# color("DEBUG") # => :blue
|
89
|
+
# color("INFO") # => :green
|
90
|
+
# color("WARN") # => :yellow
|
91
|
+
# color("ERROR") # => :red
|
92
|
+
# color("FATAL") # => :magenta
|
93
|
+
#
|
94
|
+
# @example Unknown severity level
|
95
|
+
# color("CUSTOM") # => :default
|
96
|
+
# color("TRACE") # => :default
|
97
|
+
#
|
98
|
+
# @example Case sensitivity
|
99
|
+
# color("debug") # => :default (lowercase 'd' not mapped)
|
100
|
+
# color("Debug") # => :blue (uppercase 'D' is mapped)
|
101
|
+
#
|
102
|
+
# @note This method only considers the first character of the input string
|
103
|
+
# @see SEVERITY_COLORS The mapping hash used for color determination
|
104
|
+
def color(s)
|
105
|
+
SEVERITY_COLORS[s[0]] || :default
|
20
106
|
end
|
21
107
|
|
22
108
|
end
|
@@ -1,14 +1,130 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module CMDx
|
4
|
+
# Logger message serialization module for structured log output.
|
5
|
+
#
|
6
|
+
# The LoggerSerializer module provides functionality to serialize log messages
|
7
|
+
# into structured hash format suitable for various log formatters. It handles
|
8
|
+
# different message types including Result objects and plain messages, with
|
9
|
+
# optional ANSI colorization for terminal output.
|
10
|
+
#
|
11
|
+
# @example Basic message serialization
|
12
|
+
# task = ProcessOrderTask.new
|
13
|
+
# message = "Processing order 123"
|
14
|
+
#
|
15
|
+
# LoggerSerializer.call(:info, Time.now, task, message)
|
16
|
+
# # => {
|
17
|
+
# # origin: "CMDx",
|
18
|
+
# # index: 0,
|
19
|
+
# # chain_id: "...",
|
20
|
+
# # type: "Task",
|
21
|
+
# # class: "ProcessOrderTask",
|
22
|
+
# # id: "...",
|
23
|
+
# # tags: [],
|
24
|
+
# # message: "Processing order 123"
|
25
|
+
# # }
|
26
|
+
#
|
27
|
+
# @example Result object serialization
|
28
|
+
# result = task.result # CMDx::Result instance
|
29
|
+
#
|
30
|
+
# LoggerSerializer.call(:info, Time.now, task, result)
|
31
|
+
# # => {
|
32
|
+
# # origin: "CMDx",
|
33
|
+
# # state: "complete",
|
34
|
+
# # status: "success",
|
35
|
+
# # outcome: "success",
|
36
|
+
# # metadata: {},
|
37
|
+
# # runtime: 0.5,
|
38
|
+
# # index: 0,
|
39
|
+
# # chain_id: "...",
|
40
|
+
# # # ... other result data
|
41
|
+
# # }
|
42
|
+
#
|
43
|
+
# @example Colorized result serialization
|
44
|
+
# LoggerSerializer.call(:info, Time.now, task, result, ansi_colorize: true)
|
45
|
+
# # => Same as above but with ANSI color codes in state/status/outcome values
|
46
|
+
#
|
47
|
+
# @see CMDx::Result Result object structure and data
|
48
|
+
# @see CMDx::TaskSerializer Task serialization functionality
|
49
|
+
# @see CMDx::ResultAnsi Result ANSI colorization
|
4
50
|
module LoggerSerializer
|
5
51
|
|
52
|
+
# Keys that should be colorized when ANSI colorization is enabled.
|
53
|
+
#
|
54
|
+
# These keys represent result state information that benefits from
|
55
|
+
# color coding in terminal output for better visual distinction.
|
6
56
|
COLORED_KEYS = %i[
|
7
57
|
state status outcome
|
8
58
|
].freeze
|
9
59
|
|
10
60
|
module_function
|
11
61
|
|
62
|
+
# Serializes a log message into a structured hash format.
|
63
|
+
#
|
64
|
+
# Converts log messages into hash format suitable for structured logging.
|
65
|
+
# Handles both Result objects and plain messages differently, with optional
|
66
|
+
# ANSI colorization for terminal-friendly output.
|
67
|
+
#
|
68
|
+
# @param _severity [Symbol] Log severity level (not used in current implementation)
|
69
|
+
# @param _time [Time] Log timestamp (not used in current implementation)
|
70
|
+
# @param task [CMDx::Task] The task instance generating the log message
|
71
|
+
# @param message [Object] The message to serialize (Result object or other)
|
72
|
+
# @param options [Hash] Serialization options
|
73
|
+
# @option options [Boolean] :ansi_colorize (false) Whether to apply ANSI colors
|
74
|
+
# @return [Hash] Structured hash representation of the log message
|
75
|
+
#
|
76
|
+
# @example Plain message serialization
|
77
|
+
# LoggerSerializer.call(:info, Time.now, task, "Task started")
|
78
|
+
# # => {
|
79
|
+
# # origin: "CMDx",
|
80
|
+
# # index: 0,
|
81
|
+
# # chain_id: "018c2b95-b764-7615-a924-cc5b910ed1e5",
|
82
|
+
# # type: "Task",
|
83
|
+
# # class: "MyTask",
|
84
|
+
# # id: "018c2b95-b764-7615-a924-cc5b910ed1e5",
|
85
|
+
# # tags: [],
|
86
|
+
# # message: "Task started"
|
87
|
+
# # }
|
88
|
+
#
|
89
|
+
# @example Result object serialization
|
90
|
+
# result = CMDx::Result.new(task)
|
91
|
+
# result.complete!
|
92
|
+
#
|
93
|
+
# LoggerSerializer.call(:info, Time.now, task, result)
|
94
|
+
# # => {
|
95
|
+
# # origin: "CMDx",
|
96
|
+
# # state: "complete",
|
97
|
+
# # status: "success",
|
98
|
+
# # outcome: "success",
|
99
|
+
# # metadata: {},
|
100
|
+
# # runtime: 0.001,
|
101
|
+
# # index: 0,
|
102
|
+
# # chain_id: "018c2b95-b764-7615-a924-cc5b910ed1e5",
|
103
|
+
# # type: "Task",
|
104
|
+
# # class: "MyTask",
|
105
|
+
# # id: "018c2b95-b764-7615-a924-cc5b910ed1e5",
|
106
|
+
# # tags: []
|
107
|
+
# # }
|
108
|
+
#
|
109
|
+
# @example Colorized result serialization
|
110
|
+
# LoggerSerializer.call(:info, Time.now, task, result, ansi_colorize: true)
|
111
|
+
# # => Same as above but state/status/outcome values contain ANSI color codes
|
112
|
+
# # => { state: "\e[32mcomplete\e[0m", status: "\e[32msuccess\e[0m", ... }
|
113
|
+
#
|
114
|
+
# @example Hash-like message object
|
115
|
+
# custom_message = OpenStruct.new(action: "process", item_id: 123)
|
116
|
+
# LoggerSerializer.call(:debug, Time.now, task, custom_message)
|
117
|
+
# # => {
|
118
|
+
# # origin: "CMDx",
|
119
|
+
# # action: "process",
|
120
|
+
# # item_id: 123,
|
121
|
+
# # index: 0,
|
122
|
+
# # chain_id: "...",
|
123
|
+
# # type: "Task",
|
124
|
+
# # class: "MyTask",
|
125
|
+
# # id: "...",
|
126
|
+
# # tags: []
|
127
|
+
# # }
|
12
128
|
def call(_severity, _time, task, message, **options)
|
13
129
|
m = message.respond_to?(:to_h) ? message.to_h : {}
|
14
130
|
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CMDx
|
4
|
+
##
|
5
|
+
# Base class for CMDx middleware that follows Rack-style interface.
|
6
|
+
#
|
7
|
+
# Middleware components can wrap task execution to provide cross-cutting
|
8
|
+
# concerns like logging, authentication, caching, or error handling.
|
9
|
+
# Each middleware must implement the `call` method which receives the
|
10
|
+
# task instance and a callable that represents the next middleware
|
11
|
+
# in the chain.
|
12
|
+
#
|
13
|
+
# @example Basic middleware implementation
|
14
|
+
# class LoggingMiddleware < CMDx::Middleware
|
15
|
+
# def call(task, callable)
|
16
|
+
# puts "Before executing #{task.class.name}"
|
17
|
+
# result = callable.call(task)
|
18
|
+
# puts "After executing #{task.class.name}"
|
19
|
+
# result
|
20
|
+
# end
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# @example Middleware with initialization parameters
|
24
|
+
# class AuthenticationMiddleware < CMDx::Middleware
|
25
|
+
# def initialize(required_role)
|
26
|
+
# @required_role = required_role
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# def call(task, callable)
|
30
|
+
# unless task.context.user&.has_role?(@required_role)
|
31
|
+
# task.fail!(reason: "Insufficient permissions")
|
32
|
+
# return task.result
|
33
|
+
# end
|
34
|
+
# callable.call(task)
|
35
|
+
# end
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
# @example Short-circuiting middleware
|
39
|
+
# class CachingMiddleware < CMDx::Middleware
|
40
|
+
# def call(task, callable)
|
41
|
+
# cache_key = "#{task.class.name}:#{task.context.to_h.hash}"
|
42
|
+
#
|
43
|
+
# if cached_result = Rails.cache.read(cache_key)
|
44
|
+
# task.result.merge!(cached_result)
|
45
|
+
# return task.result
|
46
|
+
# end
|
47
|
+
#
|
48
|
+
# result = callable.call(task)
|
49
|
+
# Rails.cache.write(cache_key, result.to_h) if result.success?
|
50
|
+
# result
|
51
|
+
# end
|
52
|
+
# end
|
53
|
+
#
|
54
|
+
# @see MiddlewareRegistry management
|
55
|
+
# @see Task middleware integration
|
56
|
+
class Middleware
|
57
|
+
|
58
|
+
##
|
59
|
+
# Executes the middleware logic.
|
60
|
+
#
|
61
|
+
# This method must be implemented by subclasses to define the middleware
|
62
|
+
# behavior. The method receives the task instance and a callable that
|
63
|
+
# represents the next middleware in the chain or the final task execution.
|
64
|
+
#
|
65
|
+
# @param task [Task] the task instance being executed
|
66
|
+
# @param callable [#call] the next middleware or task execution callable
|
67
|
+
# @return [Result] the task execution result
|
68
|
+
# @abstract Subclasses must implement this method
|
69
|
+
def call(_task, _callable)
|
70
|
+
raise UndefinedCallError, "call method not defined in #{self.class.name}"
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
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.
|
9
|
+
#
|
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
|
42
|
+
|
43
|
+
# Adds middleware to the registry.
|
44
|
+
#
|
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
|
+
# @return [MiddlewareRegistry] self for method chaining
|
49
|
+
#
|
50
|
+
# @example Add middleware class
|
51
|
+
# registry.use(LoggingMiddleware, log_level: :info)
|
52
|
+
#
|
53
|
+
# @example Add middleware instance
|
54
|
+
# registry.use(LoggingMiddleware.new(log_level: :info))
|
55
|
+
#
|
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]
|
60
|
+
self
|
61
|
+
end
|
62
|
+
|
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
|
75
|
+
def call(task, &)
|
76
|
+
return yield(task) if empty?
|
77
|
+
|
78
|
+
build_chain(&).call(task)
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
# Builds the middleware call chain.
|
84
|
+
#
|
85
|
+
# Creates a nested chain of callables where each middleware wraps the next,
|
86
|
+
# with the provided block as the innermost callable.
|
87
|
+
#
|
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)|
|
92
|
+
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
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,266 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CMDx
|
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
|
159
|
+
class Correlate < CMDx::Middleware
|
160
|
+
|
161
|
+
# @return [String, nil] The explicit correlation ID to use
|
162
|
+
# @return [Hash] The conditional options for correlation application
|
163
|
+
attr_reader :id, :conditional
|
164
|
+
|
165
|
+
##
|
166
|
+
# Initializes the Correlate middleware with optional configuration.
|
167
|
+
#
|
168
|
+
# @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
|
172
|
+
#
|
173
|
+
# @example Basic initialization
|
174
|
+
# middleware = CMDx::Middlewares::Correlate.new
|
175
|
+
#
|
176
|
+
# @example With explicit correlation ID
|
177
|
+
# middleware = CMDx::Middlewares::Correlate.new(id: "api-request-123")
|
178
|
+
#
|
179
|
+
# @example With conditional execution
|
180
|
+
# middleware = CMDx::Middlewares::Correlate.new(unless: -> { Rails.env.test? })
|
181
|
+
# middleware = CMDx::Middlewares::Correlate.new(if: :correlation_enabled?)
|
182
|
+
def initialize(options = {})
|
183
|
+
@id = options[:id]
|
184
|
+
@conditional = options.slice(:if, :unless)
|
185
|
+
end
|
186
|
+
|
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
|
224
|
+
#
|
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
|
228
|
+
#
|
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"
|
233
|
+
#
|
234
|
+
# # Scenario 5: Chain ID when no explicit or thread correlation
|
235
|
+
# CMDx::Correlator.clear
|
236
|
+
# middleware.call(task, callable) # Uses task.chain.id
|
237
|
+
#
|
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
|
242
|
+
#
|
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
|
248
|
+
def call(task, callable)
|
249
|
+
# Check if correlation should be applied based on conditions
|
250
|
+
return callable.call(task) unless task.__cmdx_eval(conditional)
|
251
|
+
|
252
|
+
# Get correlation ID using yield for dynamic generation
|
253
|
+
correlation_id = task.__cmdx_yield(id) ||
|
254
|
+
CMDx::Correlator.id ||
|
255
|
+
task.chain.id ||
|
256
|
+
CMDx::Correlator.generate
|
257
|
+
|
258
|
+
# Execute task with correlation context
|
259
|
+
CMDx::Correlator.use(correlation_id) do
|
260
|
+
callable.call(task)
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|