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 rendering is intentionally simple for now.
108        // When rich_rust is enabled, this can be upgraded to use panels/trees.
109        self.render_plain(output);
110    }
111
112    fn plain_lines(&self) -> Vec<String> {
113        let mut lines = Vec::new();
114        let total_layers = self.middlewares.len() + 1;
115        lines.push(format!("Middleware Stack ({total_layers} layers):"));
116
117        for mw in &self.middlewares {
118            let sc = if mw.can_short_circuit {
119                " [short-circuit]"
120            } else {
121                ""
122            };
123            lines.push(format!("  {}. {}{}", mw.order, mw.name, sc));
124
125            if mw.type_name != mw.name {
126                lines.push(format!("     type: {}", mw.type_name));
127            }
128
129            if self.show_config {
130                if let Some(config) = &mw.config_summary {
131                    lines.push(format!("     config: {config}"));
132                }
133            }
134        }
135
136        lines.push(format!("  {total_layers}. [Handler]"));
137
138        if self.show_flow && !self.middlewares.is_empty() {
139            let request_flow: Vec<String> = (1..=total_layers).map(|n| n.to_string()).collect();
140            let response_flow: Vec<String> =
141                (1..=total_layers).rev().map(|n| n.to_string()).collect();
142            lines.push(String::new());
143            lines.push(format!("Request flow: {}", request_flow.join(" -> ")));
144            lines.push(format!("Response flow: {}", response_flow.join(" -> ")));
145        }
146
147        lines
148    }
149
150    /// Return a plain text representation.
151    #[must_use]
152    pub fn as_plain_text(&self) -> String {
153        self.plain_lines().join("\n")
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160    use crate::testing::{assert_contains, assert_no_ansi, capture};
161
162    #[test]
163    fn test_middleware_info_new() {
164        let mw = MiddlewareInfo::new("RequestLogger", 1);
165        assert_eq!(mw.name, "RequestLogger");
166        assert_eq!(mw.order, 1);
167        assert!(!mw.can_short_circuit);
168        assert!(mw.config_summary.is_none());
169    }
170
171    #[test]
172    fn test_middleware_info_with_config() {
173        let mw = MiddlewareInfo::new("Cors", 2).with_config("origins=*");
174        assert_eq!(mw.config_summary, Some("origins=*".to_string()));
175    }
176
177    #[test]
178    fn test_middleware_info_short_circuits() {
179        let mw = MiddlewareInfo::new("Auth", 3).short_circuits();
180        assert!(mw.can_short_circuit);
181    }
182
183    #[test]
184    fn test_stack_display_multiple_middlewares() {
185        let middlewares = vec![
186            MiddlewareInfo::new("Logger", 1),
187            MiddlewareInfo::new("Cors", 2).with_config("origins=*"),
188            MiddlewareInfo::new("Auth", 3).short_circuits(),
189        ];
190        let display = MiddlewareStackDisplay::new(middlewares);
191
192        let captured = capture(OutputMode::Plain, || {
193            let output = RichOutput::plain();
194            display.render(&output);
195        });
196
197        assert_contains(&captured, "4 layers");
198        assert_contains(&captured, "1. Logger");
199        assert_contains(&captured, "2. Cors");
200        assert_contains(&captured, "3. Auth");
201        assert_contains(&captured, "[short-circuit]");
202        assert_contains(&captured, "[Handler]");
203    }
204
205    #[test]
206    fn test_stack_display_response_flow() {
207        let middlewares = vec![MiddlewareInfo::new("A", 1), MiddlewareInfo::new("B", 2)];
208        let display = MiddlewareStackDisplay::new(middlewares);
209
210        let captured = capture(OutputMode::Plain, || {
211            let output = RichOutput::plain();
212            display.render(&output);
213        });
214
215        assert_contains(&captured, "Request flow:");
216        assert_contains(&captured, "1 -> 2 -> 3");
217        assert_contains(&captured, "Response flow:");
218        assert_contains(&captured, "3 -> 2 -> 1");
219    }
220
221    #[test]
222    fn test_stack_display_hide_config() {
223        let middlewares = vec![MiddlewareInfo::new("Logger", 1).with_config("should-not-appear")];
224        let display = MiddlewareStackDisplay::new(middlewares).hide_config();
225
226        let captured = capture(OutputMode::Plain, || {
227            let output = RichOutput::plain();
228            display.render(&output);
229        });
230
231        assert!(!captured.contains("should-not-appear"));
232    }
233
234    #[test]
235    fn test_stack_display_hide_flow() {
236        let middlewares = vec![MiddlewareInfo::new("A", 1)];
237        let display = MiddlewareStackDisplay::new(middlewares).hide_flow();
238
239        let captured = capture(OutputMode::Plain, || {
240            let output = RichOutput::plain();
241            display.render(&output);
242        });
243
244        assert!(!captured.contains("Response flow"));
245    }
246
247    #[test]
248    fn test_stack_display_no_ansi() {
249        let middlewares = vec![MiddlewareInfo::new("Test", 1)];
250        let display = MiddlewareStackDisplay::new(middlewares);
251
252        let captured = capture(OutputMode::Plain, || {
253            let output = RichOutput::plain();
254            display.render(&output);
255        });
256
257        assert_no_ansi(&captured);
258    }
259
260    #[test]
261    fn test_stack_display_as_plain_text() {
262        let middlewares = vec![
263            MiddlewareInfo::new("Logger", 1),
264            MiddlewareInfo::new("Auth", 2).short_circuits(),
265        ];
266        let display = MiddlewareStackDisplay::new(middlewares);
267        let text = display.as_plain_text();
268
269        assert!(text.contains("3 layers"));
270        assert!(text.contains("Logger"));
271        assert!(text.contains("[short-circuit]"));
272    }
273
274    #[test]
275    fn test_large_middleware_stack() {
276        let middlewares: Vec<MiddlewareInfo> = (1..=10)
277            .map(|i| MiddlewareInfo::new(&format!("Middleware{i}"), i))
278            .collect();
279        let display = MiddlewareStackDisplay::new(middlewares);
280
281        let captured = capture(OutputMode::Plain, || {
282            let output = RichOutput::plain();
283            display.render(&output);
284        });
285
286        assert_contains(&captured, "11 layers");
287        assert_contains(&captured, "Middleware10");
288    }
289
290    #[test]
291    fn test_middleware_with_special_chars() {
292        let mw = MiddlewareInfo::new("Custom<T>", 1).with_config("key=\"value\"");
293        let display = MiddlewareStackDisplay::new(vec![mw]);
294        let text = display.as_plain_text();
295
296        assert!(text.contains("Custom<T>"));
297        assert!(text.contains("key=\"value\""));
298    }
299}