Skip to main content

fastapi_output/components/
middleware_stack.rs

1//! Middleware stack visualization component.
2//!
3//! Provides a structured view of middleware execution order and
4//! response flow, with plain and rich rendering modes.
5
6use crate::facade::RichOutput;
7use crate::mode::OutputMode;
8
9/// Information about a single middleware layer.
10#[derive(Debug, Clone)]
11pub struct MiddlewareInfo {
12    /// Display name of the middleware.
13    pub name: String,
14    /// Type name for debugging (optional).
15    pub type_name: String,
16    /// Execution order (1-based).
17    pub order: usize,
18    /// Whether middleware can short-circuit the request.
19    pub can_short_circuit: bool,
20    /// Optional configuration summary.
21    pub config_summary: Option<String>,
22}
23
24impl MiddlewareInfo {
25    /// Create a new middleware info entry.
26    #[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    /// Set a configuration summary.
38    #[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    /// Set a type name different from the display name.
45    #[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    /// Mark this middleware as short-circuiting.
52    #[must_use]
53    pub fn short_circuits(mut self) -> Self {
54        self.can_short_circuit = true;
55        self
56    }
57}
58
59/// Middleware stack display component.
60#[derive(Debug, Clone)]
61pub struct MiddlewareStackDisplay {
62    middlewares: Vec<MiddlewareInfo>,
63    show_config: bool,
64    show_flow: bool,
65}
66
67impl MiddlewareStackDisplay {
68    /// Create a new middleware stack display.
69    #[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    /// Hide configuration summaries.
79    #[must_use]
80    pub fn hide_config(mut self) -> Self {
81        self.show_config = false;
82        self
83    }
84
85    /// Hide response flow line.
86    #[must_use]
87    pub fn hide_flow(mut self) -> Self {
88        self.show_flow = false;
89        self
90    }
91
92    /// Render the middleware stack to the provided output.
93    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        // Rich mode currently shares the same deterministic text output as plain mode.
108        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    /// Return a plain text representation.
150    #[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}