fastapi_output/components/
middleware_stack.rs1use crate::facade::RichOutput;
7use crate::mode::OutputMode;
8
9#[derive(Debug, Clone)]
11pub struct MiddlewareInfo {
12 pub name: String,
14 pub type_name: String,
16 pub order: usize,
18 pub can_short_circuit: bool,
20 pub config_summary: Option<String>,
22}
23
24impl MiddlewareInfo {
25 #[must_use]
27 pub fn new(name: &str, order: usize) -> Self {
28 Self {
29 name: name.to_string(),
30 type_name: name.to_string(),
31 order,
32 can_short_circuit: false,
33 config_summary: None,
34 }
35 }
36
37 #[must_use]
39 pub fn with_config(mut self, config: &str) -> Self {
40 self.config_summary = Some(config.to_string());
41 self
42 }
43
44 #[must_use]
46 pub fn with_type_name(mut self, type_name: &str) -> Self {
47 self.type_name = type_name.to_string();
48 self
49 }
50
51 #[must_use]
53 pub fn short_circuits(mut self) -> Self {
54 self.can_short_circuit = true;
55 self
56 }
57}
58
59#[derive(Debug, Clone)]
61pub struct MiddlewareStackDisplay {
62 middlewares: Vec<MiddlewareInfo>,
63 show_config: bool,
64 show_flow: bool,
65}
66
67impl MiddlewareStackDisplay {
68 #[must_use]
70 pub fn new(middlewares: Vec<MiddlewareInfo>) -> Self {
71 Self {
72 middlewares,
73 show_config: true,
74 show_flow: true,
75 }
76 }
77
78 #[must_use]
80 pub fn hide_config(mut self) -> Self {
81 self.show_config = false;
82 self
83 }
84
85 #[must_use]
87 pub fn hide_flow(mut self) -> Self {
88 self.show_flow = false;
89 self
90 }
91
92 pub fn render(&self, output: &RichOutput) {
94 match output.mode() {
95 OutputMode::Rich => self.render_rich(output),
96 OutputMode::Plain | OutputMode::Minimal => self.render_plain(output),
97 }
98 }
99
100 fn render_plain(&self, output: &RichOutput) {
101 for line in self.plain_lines() {
102 output.print(&line);
103 }
104 }
105
106 fn render_rich(&self, output: &RichOutput) {
107 self.render_plain(output);
109 }
110
111 fn plain_lines(&self) -> Vec<String> {
112 let mut lines = Vec::new();
113 let total_layers = self.middlewares.len() + 1;
114 lines.push(format!("Middleware Stack ({total_layers} layers):"));
115
116 for mw in &self.middlewares {
117 let sc = if mw.can_short_circuit {
118 " [short-circuit]"
119 } else {
120 ""
121 };
122 lines.push(format!(" {}. {}{}", mw.order, mw.name, sc));
123
124 if mw.type_name != mw.name {
125 lines.push(format!(" type: {}", mw.type_name));
126 }
127
128 if self.show_config {
129 if let Some(config) = &mw.config_summary {
130 lines.push(format!(" config: {config}"));
131 }
132 }
133 }
134
135 lines.push(format!(" {total_layers}. [Handler]"));
136
137 if self.show_flow && !self.middlewares.is_empty() {
138 let request_flow: Vec<String> = (1..=total_layers).map(|n| n.to_string()).collect();
139 let response_flow: Vec<String> =
140 (1..=total_layers).rev().map(|n| n.to_string()).collect();
141 lines.push(String::new());
142 lines.push(format!("Request flow: {}", request_flow.join(" -> ")));
143 lines.push(format!("Response flow: {}", response_flow.join(" -> ")));
144 }
145
146 lines
147 }
148
149 #[must_use]
151 pub fn as_plain_text(&self) -> String {
152 self.plain_lines().join("\n")
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159 use crate::testing::{assert_contains, assert_no_ansi, capture};
160
161 #[test]
162 fn test_middleware_info_new() {
163 let mw = MiddlewareInfo::new("RequestLogger", 1);
164 assert_eq!(mw.name, "RequestLogger");
165 assert_eq!(mw.order, 1);
166 assert!(!mw.can_short_circuit);
167 assert!(mw.config_summary.is_none());
168 }
169
170 #[test]
171 fn test_middleware_info_with_config() {
172 let mw = MiddlewareInfo::new("Cors", 2).with_config("origins=*");
173 assert_eq!(mw.config_summary, Some("origins=*".to_string()));
174 }
175
176 #[test]
177 fn test_middleware_info_short_circuits() {
178 let mw = MiddlewareInfo::new("Auth", 3).short_circuits();
179 assert!(mw.can_short_circuit);
180 }
181
182 #[test]
183 fn test_stack_display_multiple_middlewares() {
184 let middlewares = vec![
185 MiddlewareInfo::new("Logger", 1),
186 MiddlewareInfo::new("Cors", 2).with_config("origins=*"),
187 MiddlewareInfo::new("Auth", 3).short_circuits(),
188 ];
189 let display = MiddlewareStackDisplay::new(middlewares);
190
191 let captured = capture(OutputMode::Plain, || {
192 let output = RichOutput::plain();
193 display.render(&output);
194 });
195
196 assert_contains(&captured, "4 layers");
197 assert_contains(&captured, "1. Logger");
198 assert_contains(&captured, "2. Cors");
199 assert_contains(&captured, "3. Auth");
200 assert_contains(&captured, "[short-circuit]");
201 assert_contains(&captured, "[Handler]");
202 }
203
204 #[test]
205 fn test_stack_display_response_flow() {
206 let middlewares = vec![MiddlewareInfo::new("A", 1), MiddlewareInfo::new("B", 2)];
207 let display = MiddlewareStackDisplay::new(middlewares);
208
209 let captured = capture(OutputMode::Plain, || {
210 let output = RichOutput::plain();
211 display.render(&output);
212 });
213
214 assert_contains(&captured, "Request flow:");
215 assert_contains(&captured, "1 -> 2 -> 3");
216 assert_contains(&captured, "Response flow:");
217 assert_contains(&captured, "3 -> 2 -> 1");
218 }
219
220 #[test]
221 fn test_stack_display_hide_config() {
222 let middlewares = vec![MiddlewareInfo::new("Logger", 1).with_config("should-not-appear")];
223 let display = MiddlewareStackDisplay::new(middlewares).hide_config();
224
225 let captured = capture(OutputMode::Plain, || {
226 let output = RichOutput::plain();
227 display.render(&output);
228 });
229
230 assert!(!captured.contains("should-not-appear"));
231 }
232
233 #[test]
234 fn test_stack_display_hide_flow() {
235 let middlewares = vec![MiddlewareInfo::new("A", 1)];
236 let display = MiddlewareStackDisplay::new(middlewares).hide_flow();
237
238 let captured = capture(OutputMode::Plain, || {
239 let output = RichOutput::plain();
240 display.render(&output);
241 });
242
243 assert!(!captured.contains("Response flow"));
244 }
245
246 #[test]
247 fn test_stack_display_no_ansi() {
248 let middlewares = vec![MiddlewareInfo::new("Test", 1)];
249 let display = MiddlewareStackDisplay::new(middlewares);
250
251 let captured = capture(OutputMode::Plain, || {
252 let output = RichOutput::plain();
253 display.render(&output);
254 });
255
256 assert_no_ansi(&captured);
257 }
258
259 #[test]
260 fn test_stack_display_as_plain_text() {
261 let middlewares = vec![
262 MiddlewareInfo::new("Logger", 1),
263 MiddlewareInfo::new("Auth", 2).short_circuits(),
264 ];
265 let display = MiddlewareStackDisplay::new(middlewares);
266 let text = display.as_plain_text();
267
268 assert!(text.contains("3 layers"));
269 assert!(text.contains("Logger"));
270 assert!(text.contains("[short-circuit]"));
271 }
272
273 #[test]
274 fn test_large_middleware_stack() {
275 let middlewares: Vec<MiddlewareInfo> = (1..=10)
276 .map(|i| MiddlewareInfo::new(&format!("Middleware{i}"), i))
277 .collect();
278 let display = MiddlewareStackDisplay::new(middlewares);
279
280 let captured = capture(OutputMode::Plain, || {
281 let output = RichOutput::plain();
282 display.render(&output);
283 });
284
285 assert_contains(&captured, "11 layers");
286 assert_contains(&captured, "Middleware10");
287 }
288
289 #[test]
290 fn test_middleware_with_special_chars() {
291 let mw = MiddlewareInfo::new("Custom<T>", 1).with_config("key=\"value\"");
292 let display = MiddlewareStackDisplay::new(vec![mw]);
293 let text = display.as_plain_text();
294
295 assert!(text.contains("Custom<T>"));
296 assert!(text.contains("key=\"value\""));
297 }
298}