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