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);
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 #[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}