cmdx 1.0.1 → 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.
- checksums.yaml +4 -4
- data/.cursor/prompts/rspec.md +20 -0
- data/.cursor/prompts/yardoc.md +8 -0
- data/.rubocop.yml +2 -0
- data/CHANGELOG.md +17 -2
- data/README.md +1 -1
- data/docs/basics/call.md +2 -2
- data/docs/basics/chain.md +1 -1
- data/docs/callbacks.md +3 -36
- data/docs/configuration.md +58 -12
- data/docs/interruptions/exceptions.md +1 -1
- data/docs/interruptions/faults.md +2 -2
- data/docs/logging.md +4 -4
- data/docs/middlewares.md +43 -43
- data/docs/parameters/coercions.md +49 -38
- data/docs/parameters/defaults.md +1 -1
- data/docs/parameters/validations.md +0 -39
- data/docs/testing.md +11 -12
- data/docs/workflows.md +4 -4
- data/lib/cmdx/.DS_Store +0 -0
- data/lib/cmdx/callback.rb +36 -56
- data/lib/cmdx/callback_registry.rb +82 -73
- data/lib/cmdx/chain.rb +65 -122
- data/lib/cmdx/chain_inspector.rb +22 -115
- data/lib/cmdx/chain_serializer.rb +17 -148
- data/lib/cmdx/coercion.rb +49 -0
- data/lib/cmdx/coercion_registry.rb +94 -0
- data/lib/cmdx/coercions/array.rb +18 -36
- data/lib/cmdx/coercions/big_decimal.rb +21 -33
- data/lib/cmdx/coercions/boolean.rb +21 -40
- data/lib/cmdx/coercions/complex.rb +18 -31
- data/lib/cmdx/coercions/date.rb +20 -39
- data/lib/cmdx/coercions/date_time.rb +22 -39
- data/lib/cmdx/coercions/float.rb +19 -32
- data/lib/cmdx/coercions/hash.rb +22 -41
- data/lib/cmdx/coercions/integer.rb +20 -33
- data/lib/cmdx/coercions/rational.rb +20 -32
- data/lib/cmdx/coercions/string.rb +23 -31
- data/lib/cmdx/coercions/time.rb +24 -40
- data/lib/cmdx/coercions/virtual.rb +14 -31
- data/lib/cmdx/configuration.rb +57 -171
- data/lib/cmdx/context.rb +22 -165
- data/lib/cmdx/core_ext/hash.rb +42 -67
- data/lib/cmdx/core_ext/module.rb +35 -79
- data/lib/cmdx/core_ext/object.rb +63 -98
- data/lib/cmdx/correlator.rb +40 -156
- data/lib/cmdx/error.rb +37 -202
- data/lib/cmdx/errors.rb +165 -202
- data/lib/cmdx/fault.rb +55 -158
- data/lib/cmdx/faults.rb +26 -137
- data/lib/cmdx/immutator.rb +22 -109
- data/lib/cmdx/lazy_struct.rb +103 -187
- data/lib/cmdx/log_formatters/json.rb +14 -40
- data/lib/cmdx/log_formatters/key_value.rb +14 -40
- data/lib/cmdx/log_formatters/line.rb +14 -48
- data/lib/cmdx/log_formatters/logstash.rb +14 -57
- data/lib/cmdx/log_formatters/pretty_json.rb +14 -50
- data/lib/cmdx/log_formatters/pretty_key_value.rb +13 -46
- data/lib/cmdx/log_formatters/pretty_line.rb +16 -54
- data/lib/cmdx/log_formatters/raw.rb +19 -49
- data/lib/cmdx/logger.rb +20 -82
- data/lib/cmdx/logger_ansi.rb +18 -75
- data/lib/cmdx/logger_serializer.rb +24 -114
- data/lib/cmdx/middleware.rb +38 -60
- data/lib/cmdx/middleware_registry.rb +81 -77
- data/lib/cmdx/middlewares/correlate.rb +41 -226
- data/lib/cmdx/middlewares/timeout.rb +46 -185
- data/lib/cmdx/parameter.rb +120 -198
- data/lib/cmdx/parameter_evaluator.rb +231 -0
- data/lib/cmdx/parameter_inspector.rb +25 -56
- data/lib/cmdx/parameter_registry.rb +59 -84
- data/lib/cmdx/parameter_serializer.rb +23 -74
- data/lib/cmdx/railtie.rb +24 -107
- data/lib/cmdx/result.rb +254 -260
- data/lib/cmdx/result_ansi.rb +19 -85
- data/lib/cmdx/result_inspector.rb +27 -68
- data/lib/cmdx/result_logger.rb +18 -81
- data/lib/cmdx/result_serializer.rb +28 -132
- data/lib/cmdx/rspec/matchers.rb +28 -0
- data/lib/cmdx/rspec/result_matchers/be_executed.rb +42 -0
- data/lib/cmdx/rspec/result_matchers/be_failed_task.rb +94 -0
- data/lib/cmdx/rspec/result_matchers/be_skipped_task.rb +94 -0
- data/lib/cmdx/rspec/result_matchers/be_state_matchers.rb +59 -0
- data/lib/cmdx/rspec/result_matchers/be_status_matchers.rb +57 -0
- data/lib/cmdx/rspec/result_matchers/be_successful_task.rb +87 -0
- data/lib/cmdx/rspec/result_matchers/have_bad_outcome.rb +51 -0
- data/lib/cmdx/rspec/result_matchers/have_caused_failure.rb +58 -0
- data/lib/cmdx/rspec/result_matchers/have_chain_index.rb +59 -0
- data/lib/cmdx/rspec/result_matchers/have_context.rb +86 -0
- data/lib/cmdx/rspec/result_matchers/have_empty_metadata.rb +54 -0
- data/lib/cmdx/rspec/result_matchers/have_good_outcome.rb +52 -0
- data/lib/cmdx/rspec/result_matchers/have_metadata.rb +114 -0
- data/lib/cmdx/rspec/result_matchers/have_preserved_context.rb +66 -0
- data/lib/cmdx/rspec/result_matchers/have_received_thrown_failure.rb +64 -0
- data/lib/cmdx/rspec/result_matchers/have_runtime.rb +78 -0
- data/lib/cmdx/rspec/result_matchers/have_thrown_failure.rb +76 -0
- data/lib/cmdx/rspec/task_matchers/be_well_formed_task.rb +62 -0
- data/lib/cmdx/rspec/task_matchers/have_callback.rb +85 -0
- data/lib/cmdx/rspec/task_matchers/have_cmd_setting.rb +68 -0
- data/lib/cmdx/rspec/task_matchers/have_executed_callbacks.rb +92 -0
- data/lib/cmdx/rspec/task_matchers/have_middleware.rb +46 -0
- data/lib/cmdx/rspec/task_matchers/have_parameter.rb +181 -0
- data/lib/cmdx/task.rb +213 -425
- data/lib/cmdx/task_deprecator.rb +55 -0
- data/lib/cmdx/task_processor.rb +245 -0
- data/lib/cmdx/task_serializer.rb +22 -70
- data/lib/cmdx/utils/ansi_color.rb +13 -89
- data/lib/cmdx/utils/log_timestamp.rb +13 -42
- data/lib/cmdx/utils/monotonic_runtime.rb +13 -63
- data/lib/cmdx/utils/name_affix.rb +21 -71
- data/lib/cmdx/validator.rb +48 -0
- data/lib/cmdx/validator_registry.rb +86 -0
- data/lib/cmdx/validators/exclusion.rb +55 -94
- data/lib/cmdx/validators/format.rb +31 -85
- data/lib/cmdx/validators/inclusion.rb +65 -110
- data/lib/cmdx/validators/length.rb +117 -133
- data/lib/cmdx/validators/numeric.rb +123 -130
- data/lib/cmdx/validators/presence.rb +38 -79
- data/lib/cmdx/version.rb +1 -7
- data/lib/cmdx/workflow.rb +46 -339
- data/lib/cmdx.rb +1 -1
- data/lib/generators/cmdx/install_generator.rb +14 -31
- data/lib/generators/cmdx/task_generator.rb +39 -55
- data/lib/generators/cmdx/templates/install.rb +24 -6
- data/lib/generators/cmdx/workflow_generator.rb +41 -66
- data/lib/locales/ar.yml +0 -1
- data/lib/locales/cs.yml +0 -1
- data/lib/locales/da.yml +0 -1
- data/lib/locales/de.yml +0 -1
- data/lib/locales/el.yml +0 -1
- data/lib/locales/en.yml +0 -1
- data/lib/locales/es.yml +0 -1
- data/lib/locales/fi.yml +0 -1
- data/lib/locales/fr.yml +0 -1
- data/lib/locales/he.yml +0 -1
- data/lib/locales/hi.yml +0 -1
- data/lib/locales/it.yml +0 -1
- data/lib/locales/ja.yml +0 -1
- data/lib/locales/ko.yml +0 -1
- data/lib/locales/nl.yml +0 -1
- data/lib/locales/no.yml +0 -1
- data/lib/locales/pl.yml +0 -1
- data/lib/locales/pt.yml +0 -1
- data/lib/locales/ru.yml +0 -1
- data/lib/locales/sv.yml +0 -1
- data/lib/locales/th.yml +0 -1
- data/lib/locales/tr.yml +0 -1
- data/lib/locales/vi.yml +0 -1
- data/lib/locales/zh.yml +0 -1
- metadata +34 -8
- data/lib/cmdx/parameter_validator.rb +0 -81
- data/lib/cmdx/parameter_value.rb +0 -244
- data/lib/cmdx/parameters_inspector.rb +0 -72
- data/lib/cmdx/parameters_serializer.rb +0 -115
- data/lib/cmdx/rspec/result_matchers.rb +0 -917
- data/lib/cmdx/rspec/task_matchers.rb +0 -570
- data/lib/cmdx/validators/custom.rb +0 -102
data/lib/cmdx/logger_ansi.rb
CHANGED
@@ -1,41 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module CMDx
|
4
|
-
# ANSI color formatting
|
4
|
+
# ANSI color formatting utilities for log severity levels.
|
5
5
|
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
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
|
6
|
+
# This module provides functionality to apply ANSI color codes to log messages
|
7
|
+
# based on their severity level. It maps standard log severity indicators to
|
8
|
+
# appropriate colors for enhanced readability in terminal output.
|
33
9
|
module LoggerAnsi
|
34
10
|
|
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.
|
39
11
|
SEVERITY_COLORS = {
|
40
12
|
"D" => :blue, # DEBUG
|
41
13
|
"I" => :green, # INFO
|
@@ -46,61 +18,32 @@ module CMDx
|
|
46
18
|
|
47
19
|
module_function
|
48
20
|
|
49
|
-
# Applies ANSI color formatting to a severity
|
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.
|
21
|
+
# Applies ANSI color formatting to a log message based on its severity level.
|
54
22
|
#
|
55
|
-
# @param s [String]
|
56
|
-
# @return [String] The string with ANSI color codes applied
|
23
|
+
# @param s [String] the log message string to format
|
57
24
|
#
|
58
|
-
# @
|
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)
|
25
|
+
# @return [String] the formatted message with ANSI color codes applied
|
64
26
|
#
|
65
|
-
# @example
|
66
|
-
# LoggerAnsi.call("
|
27
|
+
# @example Format a debug message
|
28
|
+
# CMDx::LoggerAnsi.call("DEBUG: Starting process") #=> "\e[1;34;49mDEBUG: Starting process\e[0m"
|
67
29
|
#
|
68
|
-
# @example
|
69
|
-
#
|
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"
|
30
|
+
# @example Format an error message
|
31
|
+
# CMDx::LoggerAnsi.call("ERROR: Connection failed") #=> "\e[1;31;49mERROR: Connection failed\e[0m"
|
74
32
|
def call(s)
|
75
33
|
Utils::AnsiColor.call(s, color: color(s), mode: :bold)
|
76
34
|
end
|
77
35
|
|
78
|
-
# Determines the appropriate color for a severity
|
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
|
36
|
+
# Determines the appropriate color for a log message based on its severity level.
|
86
37
|
#
|
87
|
-
# @
|
88
|
-
# color("DEBUG") # => :blue
|
89
|
-
# color("INFO") # => :green
|
90
|
-
# color("WARN") # => :yellow
|
91
|
-
# color("ERROR") # => :red
|
92
|
-
# color("FATAL") # => :magenta
|
38
|
+
# @param s [String] the log message string to analyze
|
93
39
|
#
|
94
|
-
# @
|
95
|
-
# color("CUSTOM") # => :default
|
96
|
-
# color("TRACE") # => :default
|
40
|
+
# @return [Symbol] the color symbol corresponding to the severity level
|
97
41
|
#
|
98
|
-
# @example
|
99
|
-
# color("
|
100
|
-
# color("Debug") # => :blue (uppercase 'D' is mapped)
|
42
|
+
# @example Get color for debug message
|
43
|
+
# CMDx::LoggerAnsi.color("DEBUG: message") #=> :blue
|
101
44
|
#
|
102
|
-
# @
|
103
|
-
#
|
45
|
+
# @example Get color for unknown severity
|
46
|
+
# CMDx::LoggerAnsi.color("UNKNOWN: message") #=> :default
|
104
47
|
def color(s)
|
105
48
|
SEVERITY_COLORS[s[0]] || :default
|
106
49
|
end
|
@@ -1,140 +1,50 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module CMDx
|
4
|
-
#
|
4
|
+
# Serializes log messages for structured logging output.
|
5
5
|
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
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
|
6
|
+
# This module provides functionality to convert log messages into a structured
|
7
|
+
# hash format suitable for various logging formatters. It handles special
|
8
|
+
# processing for Result objects, including optional ANSI colorization of
|
9
|
+
# specific keys and merging of task serialization data.
|
50
10
|
module LoggerSerializer
|
51
11
|
|
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.
|
56
12
|
COLORED_KEYS = %i[
|
57
13
|
state status outcome
|
58
14
|
].freeze
|
59
15
|
|
60
16
|
module_function
|
61
17
|
|
62
|
-
#
|
18
|
+
# Converts a log message into a structured hash format.
|
63
19
|
#
|
64
|
-
#
|
65
|
-
#
|
66
|
-
#
|
20
|
+
# Processes the message based on its type - if it's a Result object,
|
21
|
+
# optionally colorizes specific keys. For non-Result messages, merges
|
22
|
+
# task serialization data and the original message.
|
67
23
|
#
|
68
|
-
# @param _severity [
|
69
|
-
# @param _time [Time]
|
70
|
-
# @param task [CMDx::Task] The task instance
|
71
|
-
# @param message [Object] The message to
|
72
|
-
# @param options [Hash]
|
73
|
-
# @option options [Boolean] :ansi_colorize
|
74
|
-
# @return [Hash] Structured hash representation of the log message
|
24
|
+
# @param _severity [String] The log severity level (unused but kept for compatibility)
|
25
|
+
# @param _time [Time] The log timestamp (unused but kept for compatibility)
|
26
|
+
# @param task [CMDx::Task] The task instance associated with the log message
|
27
|
+
# @param message [Object] The message to be serialized (can be a Result or any object)
|
28
|
+
# @param options [Hash] Additional options for serialization
|
29
|
+
# @option options [Boolean] :ansi_colorize Whether to apply ANSI colorization to Result objects
|
75
30
|
#
|
76
|
-
# @
|
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
|
-
# # }
|
31
|
+
# @return [Hash] A structured hash representation of the log message with origin set to "CMDx"
|
88
32
|
#
|
89
|
-
# @example Result object
|
33
|
+
# @example Serializing a Result object with colorization
|
90
34
|
# result = CMDx::Result.new(task)
|
91
|
-
# result
|
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", ... }
|
35
|
+
# LoggerSerializer.call("info", Time.now, task, result, ansi_colorize: true)
|
36
|
+
# # => { state: "\e[32msuccess\e[0m", status: "complete", origin: "CMDx", ... }
|
113
37
|
#
|
114
|
-
# @example
|
115
|
-
#
|
116
|
-
#
|
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
|
-
# # }
|
38
|
+
# @example Serializing a plain message
|
39
|
+
# LoggerSerializer.call("info", Time.now, task, "Processing user data")
|
40
|
+
# # => { index: 1, chain_id: "abc123", type: "Task", message: "Processing user data", origin: "CMDx", ... }
|
128
41
|
def call(_severity, _time, task, message, **options)
|
129
|
-
m = message.
|
42
|
+
m = message.is_a?(Result) ? message.to_h : {}
|
130
43
|
|
131
44
|
if options.delete(:ansi_colorize) && message.is_a?(Result)
|
132
45
|
COLORED_KEYS.each { |k| m[k] = ResultAnsi.call(m[k]) if m.key?(k) }
|
133
46
|
elsif !message.is_a?(Result)
|
134
|
-
m.merge!(
|
135
|
-
TaskSerializer.call(task),
|
136
|
-
message: message
|
137
|
-
)
|
47
|
+
m.merge!(TaskSerializer.call(task), message: message)
|
138
48
|
end
|
139
49
|
|
140
50
|
m[:origin] ||= "CMDx"
|
data/lib/cmdx/middleware.rb
CHANGED
@@ -1,71 +1,49 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module CMDx
|
4
|
-
|
5
|
-
# Base class for CMDx middleware that follows Rack-style interface.
|
4
|
+
# Base class for implementing middleware functionality in task execution.
|
6
5
|
#
|
7
|
-
# Middleware
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
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
|
6
|
+
# Middleware provides a way to wrap task execution with custom behavior
|
7
|
+
# such as logging, timing, authentication, or other cross-cutting concerns.
|
8
|
+
# All middleware implementations must inherit from this class and implement
|
9
|
+
# the abstract call method.
|
56
10
|
class Middleware
|
57
11
|
|
58
|
-
|
59
|
-
#
|
12
|
+
# Executes middleware by creating a new instance and calling it.
|
13
|
+
#
|
14
|
+
# @param task [Task] the task instance being wrapped by the middleware
|
15
|
+
# @param callable [Proc] the callable object to execute within the middleware
|
16
|
+
#
|
17
|
+
# @return [Object] the result of the middleware execution
|
18
|
+
#
|
19
|
+
# @raise [UndefinedCallError] when the middleware subclass doesn't implement call
|
20
|
+
#
|
21
|
+
# @example Execute middleware on a task
|
22
|
+
# MyMiddleware.call(task, -> { task.process })
|
23
|
+
def self.call(task, callable)
|
24
|
+
new.call(task, callable)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Abstract method that must be implemented by middleware subclasses.
|
28
|
+
#
|
29
|
+
# This method contains the actual middleware logic to be executed.
|
30
|
+
# Subclasses must override this method to provide their specific
|
31
|
+
# middleware implementation that wraps the callable execution.
|
32
|
+
#
|
33
|
+
# @param _task [Task] the task instance being wrapped by the middleware
|
34
|
+
# @param _callable [Proc] the callable object to execute within the middleware
|
35
|
+
#
|
36
|
+
# @return [Object] the result of the middleware execution
|
60
37
|
#
|
61
|
-
#
|
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.
|
38
|
+
# @raise [UndefinedCallError] always raised in the base class
|
64
39
|
#
|
65
|
-
# @
|
66
|
-
#
|
67
|
-
#
|
68
|
-
#
|
40
|
+
# @example Implement in a subclass
|
41
|
+
# def call(task, callable)
|
42
|
+
# puts "Before #{task.class.name} execution"
|
43
|
+
# result = callable.call
|
44
|
+
# puts "After #{task.class.name} execution"
|
45
|
+
# result
|
46
|
+
# end
|
69
47
|
def call(_task, _callable)
|
70
48
|
raise UndefinedCallError, "call method not defined in #{self.class.name}"
|
71
49
|
end
|
@@ -1,103 +1,107 @@
|
|
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
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
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
|
+
# The internal hash storing middleware definitions and their configurations.
|
12
|
+
#
|
13
|
+
# @return [Hash] hash containing middleware classes/objects and their configurations
|
14
|
+
attr_reader :registry
|
15
|
+
|
16
|
+
# Initializes a new middleware registry.
|
17
|
+
#
|
18
|
+
# @param registry [Hash] optional hash of initial middleware configurations
|
19
|
+
#
|
20
|
+
# @return [MiddlewareRegistry] a new middleware registry instance
|
21
|
+
#
|
22
|
+
# @example Creating an empty registry
|
23
|
+
# MiddlewareRegistry.new
|
24
|
+
#
|
25
|
+
# @example Creating a registry with initial middleware
|
26
|
+
# MiddlewareRegistry.new(TimeoutMiddleware => [[], {timeout: 30}, nil])
|
27
|
+
def initialize(registry = {})
|
28
|
+
@registry = registry.to_h
|
29
|
+
end
|
42
30
|
|
43
|
-
#
|
31
|
+
# Registers a middleware with the registry.
|
32
|
+
#
|
33
|
+
# @param middleware [Class, Object] the middleware class or instance to register
|
34
|
+
# @param args [Array] positional arguments to pass to middleware initialization
|
35
|
+
# @param kwargs [Hash] keyword arguments to pass to middleware initialization
|
36
|
+
# @param block [Proc] optional block to pass to middleware initialization
|
44
37
|
#
|
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
38
|
# @return [MiddlewareRegistry] self for method chaining
|
49
39
|
#
|
50
|
-
# @example
|
51
|
-
# registry.
|
40
|
+
# @example Register a middleware class
|
41
|
+
# registry.register(TimeoutMiddleware, 30)
|
52
42
|
#
|
53
|
-
# @example
|
54
|
-
# registry.
|
43
|
+
# @example Register a middleware with keyword arguments
|
44
|
+
# registry.register(LoggingMiddleware, level: :info)
|
55
45
|
#
|
56
|
-
# @example
|
57
|
-
# registry.
|
58
|
-
def
|
59
|
-
|
46
|
+
# @example Register a middleware with a block
|
47
|
+
# registry.register(CustomMiddleware) { |task| puts "Processing #{task.id}" }
|
48
|
+
def register(middleware, *args, **kwargs, &block)
|
49
|
+
registry[middleware] = [args, kwargs, block]
|
60
50
|
self
|
61
51
|
end
|
62
52
|
|
63
|
-
# Executes
|
64
|
-
#
|
65
|
-
# @param task [Task]
|
66
|
-
# @
|
67
|
-
#
|
68
|
-
# @return [Object]
|
69
|
-
#
|
70
|
-
# @
|
71
|
-
#
|
72
|
-
#
|
73
|
-
#
|
74
|
-
#
|
53
|
+
# Executes all registered middleware around the provided task.
|
54
|
+
#
|
55
|
+
# @param task [Task] the task instance to execute middleware around
|
56
|
+
# @param block [Proc] the block to execute after all middleware processing
|
57
|
+
#
|
58
|
+
# @return [Object] the result of the middleware chain execution
|
59
|
+
#
|
60
|
+
# @raise [ArgumentError] if no block is provided
|
61
|
+
#
|
62
|
+
# @example Execute middleware around a task
|
63
|
+
# registry.call(task) { |task| task.process }
|
64
|
+
#
|
65
|
+
# @example Execute with early return if no middleware
|
66
|
+
# registry.call(task) { |task| puts "No middleware to execute" }
|
75
67
|
def call(task, &)
|
76
|
-
|
68
|
+
raise ArgumentError, "block required" unless block_given?
|
69
|
+
|
70
|
+
return yield(task) if registry.empty?
|
77
71
|
|
78
72
|
build_chain(&).call(task)
|
79
73
|
end
|
80
74
|
|
75
|
+
# Returns a hash representation of the registry.
|
76
|
+
#
|
77
|
+
# @return [Hash] deep copy of registry with duplicated configuration arrays
|
78
|
+
#
|
79
|
+
# @example Getting registry hash
|
80
|
+
# registry.to_h
|
81
|
+
# # => { TimeoutMiddleware => [[30], {}, nil] }
|
82
|
+
def to_h
|
83
|
+
registry.transform_values do |config|
|
84
|
+
args, kwargs, block = config
|
85
|
+
[args.dup, kwargs.dup, block]
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
81
89
|
private
|
82
90
|
|
83
|
-
# Builds the middleware call
|
91
|
+
# Builds the middleware execution chain by wrapping middleware around the call block.
|
92
|
+
#
|
93
|
+
# @param call_block [Proc] the final block to execute after all middleware
|
84
94
|
#
|
85
|
-
#
|
86
|
-
# with the provided block as the innermost callable.
|
95
|
+
# @return [Proc] the complete middleware chain as a callable proc
|
87
96
|
#
|
88
|
-
# @
|
89
|
-
#
|
90
|
-
def build_chain(&
|
91
|
-
|
97
|
+
# @example Building a middleware chain (internal use)
|
98
|
+
# build_chain { |task| task.process }
|
99
|
+
def build_chain(&call_block)
|
100
|
+
registry.reverse_each.reduce(call_block) do |next_callable, (middleware, config)|
|
92
101
|
proc do |task|
|
93
|
-
|
94
|
-
|
95
|
-
|
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
|
102
|
+
args, kwargs, block = config
|
103
|
+
instance = middleware.respond_to?(:new) ? middleware.new(*args, **kwargs, &block) : middleware
|
104
|
+
instance.call(task, next_callable)
|
101
105
|
end
|
102
106
|
end
|
103
107
|
end
|