1use crate::color::Color;
24use crate::console::{ConsoleOptions, RenderResult, Renderable};
25use crate::panel::Panel;
26use crate::segment::Segment;
27use crate::style::Style;
28use crate::table::{Column, Table};
29use crate::highlighter::ReprHighlighter;
30
31#[derive(Debug, Clone)]
40pub struct Inspect {
41 value_repr: String,
43 title: Option<String>,
45 help: bool,
47 methods: bool,
49 docs: bool,
51 private: bool,
53 dunder: bool,
55 sort: bool,
57 all: bool,
59 value: bool,
61 attrs: Vec<(String, String, String, Option<String>)>,
63 method_list: Vec<(String, String, Option<String>)>,
65 doc_text: Option<String>,
67}
68
69impl Inspect {
70 pub fn new(value: &dyn std::fmt::Debug) -> Self {
74 Self {
75 value_repr: format!("{value:?}"),
76 title: None,
77 help: false,
78 methods: false,
79 docs: true,
80 private: false,
81 dunder: false,
82 sort: true,
83 all: false,
84 value: true,
85 attrs: Vec::new(),
86 method_list: Vec::new(),
87 doc_text: None,
88 }
89 }
90
91 pub fn from_str(value_repr: impl Into<String>) -> Self {
93 Self {
94 value_repr: value_repr.into(),
95 title: None,
96 help: false,
97 methods: false,
98 docs: true,
99 private: false,
100 dunder: false,
101 sort: true,
102 all: false,
103 value: true,
104 attrs: Vec::new(),
105 method_list: Vec::new(),
106 doc_text: None,
107 }
108 }
109
110 pub fn title(mut self, title: impl Into<String>) -> Self {
112 self.title = Some(title.into());
113 self
114 }
115
116 pub fn help(mut self, value: bool) -> Self {
118 self.help = value;
119 self
120 }
121
122 pub fn methods(mut self, value: bool) -> Self {
124 self.methods = value;
125 self
126 }
127
128 pub fn docs(mut self, value: bool) -> Self {
130 self.docs = value;
131 self
132 }
133
134 pub fn private(mut self, value: bool) -> Self {
136 self.private = value;
137 self
138 }
139
140 pub fn dunder(mut self, value: bool) -> Self {
142 self.dunder = value;
143 self
144 }
145
146 pub fn sort(mut self, value: bool) -> Self {
148 self.sort = value;
149 self
150 }
151
152 pub fn all(mut self, value: bool) -> Self {
154 self.all = value;
155 self
156 }
157
158 pub fn value(mut self, value: bool) -> Self {
160 self.value = value;
161 self
162 }
163
164 pub fn add_attr(
171 mut self,
172 name: impl Into<String>,
173 type_name: impl Into<String>,
174 value: impl Into<String>,
175 ) -> Self {
176 self.attrs.push((name.into(), type_name.into(), value.into(), None));
177 self
178 }
179
180 pub fn add_attr_doc(
182 mut self,
183 name: impl Into<String>,
184 type_name: impl Into<String>,
185 value: impl Into<String>,
186 doc: impl Into<String>,
187 ) -> Self {
188 self.attrs.push((name.into(), type_name.into(), value.into(), Some(doc.into())));
189 self
190 }
191
192 pub fn add_method(
194 mut self,
195 name: impl Into<String>,
196 signature: impl Into<String>,
197 ) -> Self {
198 self.method_list.push((name.into(), signature.into(), None));
199 self
200 }
201
202 pub fn add_method_doc(
204 mut self,
205 name: impl Into<String>,
206 signature: impl Into<String>,
207 doc: impl Into<String>,
208 ) -> Self {
209 self.method_list.push((name.into(), signature.into(), Some(doc.into())));
210 self
211 }
212
213 pub fn doc_text(mut self, doc: impl Into<String>) -> Self {
215 self.doc_text = Some(doc.into());
216 self
217 }
218
219 pub fn with_attrs(
221 mut self,
222 attrs: Vec<(String, String, String)>,
223 ) -> Self {
224 self.attrs = attrs.into_iter().map(|(n, t, v)| (n, t, v, None)).collect();
225 self
226 }
227
228 fn effective_title(&self) -> String {
230 self.title.clone().unwrap_or_else(|| "Object".to_string())
231 }
232
233 fn is_visible(&self, name: &str) -> bool {
235 if self.all {
236 return true;
237 }
238 if name.starts_with("__") && name.ends_with("__") {
239 return self.dunder;
240 }
241 if name.starts_with('_') {
242 return self.private;
243 }
244 true
245 }
246}
247
248impl Renderable for Inspect {
249 fn render(&self, options: &ConsoleOptions) -> RenderResult {
250 let highlighter = ReprHighlighter::new();
251 let mut segments: Vec<Segment> = Vec::new();
252 let mut items: Vec<Box<dyn Renderable>> = Vec::new();
253
254 let title_style = Style::new().bold(true);
256 let title_text = title_style.render(&self.effective_title());
257 segments.push(Segment::line());
258 segments.push(Segment::new(title_text));
259
260 if self.value {
262 let highlighted = highlighter.highlight_str(&self.value_repr);
263 let rendered = highlighted.render();
264 segments.push(Segment::new(rendered));
265 segments.push(Segment::line());
266 }
267
268 if self.docs {
270 if let Some(ref doc) = self.doc_text {
271 segments.push(Segment::line());
272 let doc_style = Style::new().italic(true).color(
273 Color::parse("bright_black").unwrap_or_else(|_| Color::default()),
274 );
275 for line in doc.lines() {
276 segments.push(Segment::styled(
277 format!(" {line}"),
278 doc_style.clone(),
279 ));
280 segments.push(Segment::line());
281 }
282 }
283 }
284
285 let visible_attrs: Vec<_> = self
287 .attrs
288 .iter()
289 .filter(|(name, _, _, _)| self.is_visible(name))
290 .collect();
291
292 if !visible_attrs.is_empty() {
293 let mut sorted_attrs: Vec<_> = visible_attrs.iter().collect();
294 if self.sort {
295 sorted_attrs.sort_by(|a, b| {
296 let a_name = a.0.trim_start_matches('_');
297 let b_name = b.0.trim_start_matches('_');
298 a_name.to_lowercase().cmp(&b_name.to_lowercase())
299 });
300 }
301
302 let mut table = Table::new();
303 table.show_header = true;
304 table.show_edge = false;
305 table.show_lines = false;
306 table.add_column(
307 Column::new("Attribute")
308 .header_style(Style::new().bold(true).color(
309 Color::parse("bright_cyan").unwrap_or_else(|_| Color::default()),
310 )),
311 );
312 table.add_column(
313 Column::new("Type")
314 .header_style(Style::new().bold(true).color(
315 Color::parse("bright_green").unwrap_or_else(|_| Color::default()),
316 )),
317 );
318 table.add_column(
319 Column::new("Value")
320 .header_style(Style::new().bold(true).color(
321 Color::parse("bright_yellow").unwrap_or_else(|_| Color::default()),
322 )),
323 );
324
325 for (name, type_name, value_repr, _doc) in &sorted_attrs {
326 let name_style = Style::new().color(
327 Color::parse("cyan").unwrap_or_else(|_| Color::default()),
328 );
329 let type_style = Style::new().color(
330 Color::parse("green").unwrap_or_else(|_| Color::default()),
331 ).italic(true);
332
333 let name_text = name_style.render(name);
334 let type_text = type_style.render(type_name);
335 let val_text = highlighter.highlight_str(value_repr).render();
336
337 table.add_row(vec![
338 crate::table::Cell::new(name_text),
339 crate::table::Cell::new(type_text),
340 crate::table::Cell::new(val_text),
341 ]);
342 }
343
344 items.push(Box::new(table));
345 }
346
347 if self.methods && !self.method_list.is_empty() {
349 let mut table = Table::new();
350 table.show_header = true;
351 table.show_edge = false;
352 table.show_lines = false;
353 table.add_column(
354 Column::new("Method").header_style(
355 Style::new().bold(true).color(
356 Color::parse("bright_magenta").unwrap_or_else(|_| Color::default()),
357 ),
358 ),
359 );
360 table.add_column(
361 Column::new("Signature").header_style(
362 Style::new().bold(true).color(
363 Color::parse("bright_blue").unwrap_or_else(|_| Color::default()),
364 ),
365 ),
366 );
367
368 for (name, sig, _doc) in &self.method_list {
369 let name_style = Style::new().bold(true).color(
370 Color::parse("magenta").unwrap_or_else(|_| Color::default()),
371 );
372 let sig_style = Style::new().italic(true);
373
374 let name_text = name_style.render(name);
375 let sig_text = sig_style.render(sig);
376
377 table.add_row(vec![
378 crate::table::Cell::new(name_text),
379 crate::table::Cell::new(sig_text),
380 ]);
381 }
382
383 items.push(Box::new(table));
384 }
385
386 let mut all_lines: Vec<Vec<Segment>> = Vec::new();
388
389 if !segments.is_empty() {
391 all_lines.push(segments);
392 }
393
394 for item in &items {
396 let result = item.render(options);
397 all_lines.extend(result.lines);
398 }
399
400 let panel_content = all_lines
403 .into_iter()
404 .flatten()
405 .map(|s| s.text)
406 .collect::<Vec<_>>()
407 .join("\n");
408
409 let panel = Panel::new(panel_content)
410 .title(self.effective_title())
411 .border_style(
412 Style::new().color(
413 Color::parse("bright_blue").unwrap_or_else(|_| Color::default()),
414 ),
415 );
416
417 panel.render(options)
418 }
419}
420
421pub fn inspect(value: &dyn std::fmt::Debug) -> Inspect {
429 Inspect::new(value)
430}
431
432pub fn inspect_str(title: impl Into<String>, value: impl Into<String>) -> Inspect {
434 Inspect::from_str(value).title(title)
435}
436
437#[cfg(test)]
438mod tests {
439 use super::*;
440
441 #[test]
442 fn test_inspect_new() {
443 let val = vec![1, 2, 3];
444 let insp = Inspect::new(&val);
445 assert!(insp.value_repr.contains("1"));
446 assert!(insp.value_repr.contains("2"));
447 assert!(insp.value_repr.contains("3"));
448 }
449
450 #[test]
451 fn test_inspect_title() {
452 let insp = Inspect::from_str("test_value").title("MyStruct");
453 assert_eq!(insp.effective_title(), "MyStruct");
454 }
455
456 #[test]
457 fn test_inspect_add_attr() {
458 let insp = Inspect::from_str("test")
459 .add_attr("name", "String", "\"hello\"")
460 .add_attr("count", "i32", "42");
461 assert_eq!(insp.attrs.len(), 2);
462 assert_eq!(insp.attrs[0].0, "name");
463 assert_eq!(insp.attrs[1].0, "count");
464 }
465
466 #[test]
467 fn test_inspect_add_method() {
468 let insp = Inspect::from_str("test")
469 .add_method("do_thing", "fn do_thing(&self, x: i32) -> bool");
470 assert_eq!(insp.method_list.len(), 1);
471 assert_eq!(insp.method_list[0].0, "do_thing");
472 }
473
474 #[test]
475 fn test_inspect_visibility() {
476 let insp = Inspect::from_str("test");
477 assert!(!insp.is_visible("_private"));
478 assert!(insp.is_visible("public"));
479
480 let insp2 = Inspect::from_str("test").private(true);
481 assert!(insp2.is_visible("_private"));
482 assert!(!insp2.is_visible("__dunder__"));
483
484 let insp3 = Inspect::from_str("test").dunder(true);
485 assert!(insp3.is_visible("__dunder__"));
486 }
487
488 #[test]
489 fn test_inspect_all() {
490 let insp = Inspect::from_str("test").all(true);
491 assert!(insp.is_visible("_private"));
492 assert!(insp.is_visible("__dunder__"));
493 assert!(insp.is_visible("public"));
494 }
495
496 #[test]
497 fn test_inspect_render() {
498 let insp = Inspect::from_str("hello world")
499 .title("TestObject")
500 .add_attr("field1", "String", "\"value1\"")
501 .add_attr("field2", "u64", "42");
502 let opts = ConsoleOptions::default();
503 let result = insp.render(&opts);
504 let ansi = result.to_ansi();
505 assert!(ansi.contains("TestObject"));
506 assert!(ansi.contains("field1"));
507 assert!(ansi.contains("field2"));
508 }
509
510 #[test]
511 fn test_inspect_with_methods() {
512 let insp = Inspect::from_str("obj")
513 .title("MyType")
514 .methods(true)
515 .add_method("run", "fn run(&mut self) -> Result<()>")
516 .add_method("stop", "fn stop(&self)");
517 let opts = ConsoleOptions::default();
518 let result = insp.render(&opts);
519 let ansi = result.to_ansi();
520 assert!(ansi.contains("MyType"));
521 assert!(ansi.contains("run"));
522 assert!(ansi.contains("stop"));
523 }
524
525 #[test]
526 fn test_inspect_sorting() {
527 let insp = Inspect::from_str("obj")
528 .add_attr("zebra", "i32", "1")
529 .add_attr("alpha", "i32", "2")
530 .add_attr("beta", "i32", "3");
531 let opts = ConsoleOptions::default();
532 let result = insp.render(&opts);
533 let ansi = result.to_ansi();
535 assert!(!ansi.is_empty());
536 }
537}