cmdx 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. checksums.yaml +4 -4
  2. data/.cursor/prompts/rspec.md +20 -0
  3. data/.cursor/prompts/yardoc.md +8 -0
  4. data/.rubocop.yml +5 -0
  5. data/CHANGELOG.md +101 -49
  6. data/README.md +2 -1
  7. data/docs/ai_prompts.md +10 -0
  8. data/docs/basics/call.md +11 -2
  9. data/docs/basics/chain.md +10 -1
  10. data/docs/basics/context.md +9 -0
  11. data/docs/basics/setup.md +9 -0
  12. data/docs/callbacks.md +14 -37
  13. data/docs/configuration.md +68 -27
  14. data/docs/getting_started.md +11 -0
  15. data/docs/internationalization.md +148 -0
  16. data/docs/interruptions/exceptions.md +10 -1
  17. data/docs/interruptions/faults.md +11 -2
  18. data/docs/interruptions/halt.md +9 -0
  19. data/docs/logging.md +14 -4
  20. data/docs/middlewares.md +53 -43
  21. data/docs/outcomes/result.md +9 -0
  22. data/docs/outcomes/states.md +9 -0
  23. data/docs/outcomes/statuses.md +9 -0
  24. data/docs/parameters/coercions.md +58 -38
  25. data/docs/parameters/defaults.md +10 -1
  26. data/docs/parameters/definitions.md +9 -0
  27. data/docs/parameters/namespacing.md +9 -0
  28. data/docs/parameters/validations.md +8 -67
  29. data/docs/testing.md +22 -13
  30. data/docs/tips_and_tricks.md +9 -0
  31. data/docs/workflows.md +14 -4
  32. data/lib/cmdx/.DS_Store +0 -0
  33. data/lib/cmdx/callback.rb +36 -56
  34. data/lib/cmdx/callback_registry.rb +82 -73
  35. data/lib/cmdx/chain.rb +65 -122
  36. data/lib/cmdx/chain_inspector.rb +22 -115
  37. data/lib/cmdx/chain_serializer.rb +17 -148
  38. data/lib/cmdx/coercion.rb +49 -0
  39. data/lib/cmdx/coercion_registry.rb +94 -0
  40. data/lib/cmdx/coercions/array.rb +18 -36
  41. data/lib/cmdx/coercions/big_decimal.rb +21 -33
  42. data/lib/cmdx/coercions/boolean.rb +21 -40
  43. data/lib/cmdx/coercions/complex.rb +18 -31
  44. data/lib/cmdx/coercions/date.rb +20 -39
  45. data/lib/cmdx/coercions/date_time.rb +22 -39
  46. data/lib/cmdx/coercions/float.rb +19 -32
  47. data/lib/cmdx/coercions/hash.rb +22 -41
  48. data/lib/cmdx/coercions/integer.rb +20 -33
  49. data/lib/cmdx/coercions/rational.rb +20 -32
  50. data/lib/cmdx/coercions/string.rb +23 -31
  51. data/lib/cmdx/coercions/time.rb +24 -40
  52. data/lib/cmdx/coercions/virtual.rb +14 -31
  53. data/lib/cmdx/configuration.rb +57 -171
  54. data/lib/cmdx/context.rb +22 -165
  55. data/lib/cmdx/core_ext/hash.rb +42 -67
  56. data/lib/cmdx/core_ext/module.rb +35 -79
  57. data/lib/cmdx/core_ext/object.rb +63 -98
  58. data/lib/cmdx/correlator.rb +40 -156
  59. data/lib/cmdx/error.rb +37 -202
  60. data/lib/cmdx/errors.rb +165 -202
  61. data/lib/cmdx/fault.rb +55 -158
  62. data/lib/cmdx/faults.rb +26 -137
  63. data/lib/cmdx/immutator.rb +22 -109
  64. data/lib/cmdx/lazy_struct.rb +103 -187
  65. data/lib/cmdx/log_formatters/json.rb +14 -40
  66. data/lib/cmdx/log_formatters/key_value.rb +14 -40
  67. data/lib/cmdx/log_formatters/line.rb +14 -48
  68. data/lib/cmdx/log_formatters/logstash.rb +14 -57
  69. data/lib/cmdx/log_formatters/pretty_json.rb +14 -50
  70. data/lib/cmdx/log_formatters/pretty_key_value.rb +13 -46
  71. data/lib/cmdx/log_formatters/pretty_line.rb +16 -54
  72. data/lib/cmdx/log_formatters/raw.rb +19 -49
  73. data/lib/cmdx/logger.rb +20 -82
  74. data/lib/cmdx/logger_ansi.rb +18 -75
  75. data/lib/cmdx/logger_serializer.rb +24 -114
  76. data/lib/cmdx/middleware.rb +38 -60
  77. data/lib/cmdx/middleware_registry.rb +81 -77
  78. data/lib/cmdx/middlewares/correlate.rb +41 -226
  79. data/lib/cmdx/middlewares/timeout.rb +46 -185
  80. data/lib/cmdx/parameter.rb +120 -198
  81. data/lib/cmdx/parameter_evaluator.rb +231 -0
  82. data/lib/cmdx/parameter_inspector.rb +25 -56
  83. data/lib/cmdx/parameter_registry.rb +59 -84
  84. data/lib/cmdx/parameter_serializer.rb +23 -74
  85. data/lib/cmdx/railtie.rb +24 -107
  86. data/lib/cmdx/result.rb +254 -260
  87. data/lib/cmdx/result_ansi.rb +19 -85
  88. data/lib/cmdx/result_inspector.rb +27 -68
  89. data/lib/cmdx/result_logger.rb +18 -81
  90. data/lib/cmdx/result_serializer.rb +28 -132
  91. data/lib/cmdx/rspec/matchers.rb +28 -0
  92. data/lib/cmdx/rspec/result_matchers/be_executed.rb +42 -0
  93. data/lib/cmdx/rspec/result_matchers/be_failed_task.rb +94 -0
  94. data/lib/cmdx/rspec/result_matchers/be_skipped_task.rb +94 -0
  95. data/lib/cmdx/rspec/result_matchers/be_state_matchers.rb +59 -0
  96. data/lib/cmdx/rspec/result_matchers/be_status_matchers.rb +57 -0
  97. data/lib/cmdx/rspec/result_matchers/be_successful_task.rb +87 -0
  98. data/lib/cmdx/rspec/result_matchers/have_bad_outcome.rb +51 -0
  99. data/lib/cmdx/rspec/result_matchers/have_caused_failure.rb +58 -0
  100. data/lib/cmdx/rspec/result_matchers/have_chain_index.rb +59 -0
  101. data/lib/cmdx/rspec/result_matchers/have_context.rb +86 -0
  102. data/lib/cmdx/rspec/result_matchers/have_empty_metadata.rb +54 -0
  103. data/lib/cmdx/rspec/result_matchers/have_good_outcome.rb +52 -0
  104. data/lib/cmdx/rspec/result_matchers/have_metadata.rb +114 -0
  105. data/lib/cmdx/rspec/result_matchers/have_preserved_context.rb +66 -0
  106. data/lib/cmdx/rspec/result_matchers/have_received_thrown_failure.rb +64 -0
  107. data/lib/cmdx/rspec/result_matchers/have_runtime.rb +78 -0
  108. data/lib/cmdx/rspec/result_matchers/have_thrown_failure.rb +76 -0
  109. data/lib/cmdx/rspec/task_matchers/be_well_formed_task.rb +62 -0
  110. data/lib/cmdx/rspec/task_matchers/have_callback.rb +85 -0
  111. data/lib/cmdx/rspec/task_matchers/have_cmd_setting.rb +68 -0
  112. data/lib/cmdx/rspec/task_matchers/have_executed_callbacks.rb +92 -0
  113. data/lib/cmdx/rspec/task_matchers/have_middleware.rb +46 -0
  114. data/lib/cmdx/rspec/task_matchers/have_parameter.rb +181 -0
  115. data/lib/cmdx/task.rb +213 -425
  116. data/lib/cmdx/task_deprecator.rb +55 -0
  117. data/lib/cmdx/task_processor.rb +245 -0
  118. data/lib/cmdx/task_serializer.rb +22 -70
  119. data/lib/cmdx/utils/ansi_color.rb +13 -89
  120. data/lib/cmdx/utils/log_timestamp.rb +13 -42
  121. data/lib/cmdx/utils/monotonic_runtime.rb +13 -63
  122. data/lib/cmdx/utils/name_affix.rb +21 -71
  123. data/lib/cmdx/validator.rb +48 -0
  124. data/lib/cmdx/validator_registry.rb +86 -0
  125. data/lib/cmdx/validators/exclusion.rb +55 -94
  126. data/lib/cmdx/validators/format.rb +31 -85
  127. data/lib/cmdx/validators/inclusion.rb +65 -110
  128. data/lib/cmdx/validators/length.rb +117 -133
  129. data/lib/cmdx/validators/numeric.rb +123 -130
  130. data/lib/cmdx/validators/presence.rb +38 -79
  131. data/lib/cmdx/version.rb +1 -7
  132. data/lib/cmdx/workflow.rb +46 -339
  133. data/lib/cmdx.rb +1 -1
  134. data/lib/generators/cmdx/install_generator.rb +14 -31
  135. data/lib/generators/cmdx/task_generator.rb +39 -55
  136. data/lib/generators/cmdx/templates/install.rb +61 -11
  137. data/lib/generators/cmdx/workflow_generator.rb +41 -66
  138. data/lib/locales/ar.yml +35 -0
  139. data/lib/locales/cs.yml +35 -0
  140. data/lib/locales/da.yml +35 -0
  141. data/lib/locales/de.yml +35 -0
  142. data/lib/locales/el.yml +35 -0
  143. data/lib/locales/en.yml +19 -20
  144. data/lib/locales/es.yml +19 -20
  145. data/lib/locales/fi.yml +35 -0
  146. data/lib/locales/fr.yml +35 -0
  147. data/lib/locales/he.yml +35 -0
  148. data/lib/locales/hi.yml +35 -0
  149. data/lib/locales/it.yml +35 -0
  150. data/lib/locales/ja.yml +35 -0
  151. data/lib/locales/ko.yml +35 -0
  152. data/lib/locales/nl.yml +35 -0
  153. data/lib/locales/no.yml +35 -0
  154. data/lib/locales/pl.yml +35 -0
  155. data/lib/locales/pt.yml +35 -0
  156. data/lib/locales/ru.yml +35 -0
  157. data/lib/locales/sv.yml +35 -0
  158. data/lib/locales/th.yml +35 -0
  159. data/lib/locales/tr.yml +35 -0
  160. data/lib/locales/vi.yml +35 -0
  161. data/lib/locales/zh.yml +35 -0
  162. metadata +57 -8
  163. data/lib/cmdx/parameter_validator.rb +0 -81
  164. data/lib/cmdx/parameter_value.rb +0 -244
  165. data/lib/cmdx/parameters_inspector.rb +0 -72
  166. data/lib/cmdx/parameters_serializer.rb +0 -115
  167. data/lib/cmdx/rspec/result_matchers.rb +0 -917
  168. data/lib/cmdx/rspec/task_matchers.rb +0 -570
  169. data/lib/cmdx/validators/custom.rb +0 -102
@@ -1,41 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CMDx
4
- # ANSI color formatting module for logger severity levels.
4
+ # ANSI color formatting utilities for log severity levels.
5
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
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 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.
21
+ # Applies ANSI color formatting to a log message based on its severity level.
54
22
  #
55
- # @param s [String] The severity string to colorize
56
- # @return [String] The string with ANSI color codes applied
23
+ # @param s [String] the log message string to format
57
24
  #
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)
25
+ # @return [String] the formatted message with ANSI color codes applied
64
26
  #
65
- # @example Unknown severity level
66
- # LoggerAnsi.call("CUSTOM") # => "\e[1;39mCUSTOM\e[0m" (default color bold)
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 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"
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 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
36
+ # Determines the appropriate color for a log message based on its severity level.
86
37
  #
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
38
+ # @param s [String] the log message string to analyze
93
39
  #
94
- # @example Unknown severity level
95
- # color("CUSTOM") # => :default
96
- # color("TRACE") # => :default
40
+ # @return [Symbol] the color symbol corresponding to the severity level
97
41
  #
98
- # @example Case sensitivity
99
- # color("debug") # => :default (lowercase 'd' not mapped)
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
- # @note This method only considers the first character of the input string
103
- # @see SEVERITY_COLORS The mapping hash used for color determination
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
- # Logger message serialization module for structured log output.
4
+ # Serializes log messages for structured logging output.
5
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
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
- # Serializes a log message into a structured hash format.
18
+ # Converts a log message into a structured hash format.
63
19
  #
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.
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 [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
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
- # @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
- # # }
31
+ # @return [Hash] A structured hash representation of the log message with origin set to "CMDx"
88
32
  #
89
- # @example Result object serialization
33
+ # @example Serializing a Result object with colorization
90
34
  # 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", ... }
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 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
- # # }
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.respond_to?(:to_h) ? message.to_h : {}
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"
@@ -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 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
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
- # Executes the middleware logic.
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
- # 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.
38
+ # @raise [UndefinedCallError] always raised in the base class
64
39
  #
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
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
- # 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
+ # 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
- # Adds middleware to the registry.
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 Add middleware class
51
- # registry.use(LoggingMiddleware, log_level: :info)
40
+ # @example Register a middleware class
41
+ # registry.register(TimeoutMiddleware, 30)
52
42
  #
53
- # @example Add middleware instance
54
- # registry.use(LoggingMiddleware.new(log_level: :info))
43
+ # @example Register a middleware with keyword arguments
44
+ # registry.register(LoggingMiddleware, level: :info)
55
45
  #
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]
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 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
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
- return yield(task) if empty?
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 chain.
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
- # Creates a nested chain of callables where each middleware wraps the next,
86
- # with the provided block as the innermost callable.
95
+ # @return [Proc] the complete middleware chain as a callable proc
87
96
  #
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)|
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
- 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
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