Skip to main content

rusty_rich/
inspect.rs

1//! Object introspection — equivalent to Rich's `_inspect.py`.
2//!
3//! Provides the [`Inspect`] renderable for displaying structured information
4//! about Rust values: their type name, debug representation, attributes (via
5//! serde or manual key-value pairs), and documentation.
6//!
7//! Since Rust lacks Python's runtime introspection, this module works with
8//! the [`std::fmt::Debug`] trait, [`serde::Serialize`], and manual attribute
9//! maps to produce Rich-styled inspection output.
10//!
11//! # Quick Example
12//!
13//! ```rust,ignore
14//! use rusty_rich::inspect::Inspect;
15//!
16//! let obj = vec![1, 2, 3];
17//! let insp = Inspect::new(&obj)
18//!     .title("my_vec")
19//!     .methods(true)
20//!     .docs(true);
21//! ```
22
23use 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// ---------------------------------------------------------------------------
32// Inspect — structured object inspection
33// ---------------------------------------------------------------------------
34
35/// A renderable that displays structured inspection of a value.
36///
37/// Shows the type name, debug/value representation, and optional attribute
38/// table with types and documentation.
39#[derive(Debug, Clone)]
40pub struct Inspect {
41    /// The debug/value representation of the object.
42    value_repr: String,
43    /// The type name to display.
44    title: Option<String>,
45    /// Show full help-style output (more detail).
46    help: bool,
47    /// Show callable/method-like attributes.
48    methods: bool,
49    /// Show doc/comment strings.
50    docs: bool,
51    /// Show private attributes (prefixed with `_`).
52    private: bool,
53    /// Show dunder attributes (prefixed with `__`).
54    dunder: bool,
55    /// Sort attributes alphabetically.
56    sort: bool,
57    /// Show all attributes regardless of prefix.
58    all: bool,
59    /// Pretty-print values.
60    value: bool,
61    /// Attribute definitions: (name, type_str, value_str, doc_string).
62    attrs: Vec<(String, String, String, Option<String>)>,
63    /// Method definitions: (name, signature, doc_string).
64    method_list: Vec<(String, String, Option<String>)>,
65    /// Doc/summary text for the object itself.
66    doc_text: Option<String>,
67}
68
69impl Inspect {
70    /// Create a new `Inspect` for a value that implements [`std::fmt::Debug`].
71    ///
72    /// The debug representation is captured immediately.
73    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    /// Create a new `Inspect` from a string value representation.
92    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    /// Builder: set a custom title (overrides type name).
111    pub fn title(mut self, title: impl Into<String>) -> Self {
112        self.title = Some(title.into());
113        self
114    }
115
116    /// Builder: show full detail / help mode.
117    pub fn help(mut self, value: bool) -> Self {
118        self.help = value;
119        self
120    }
121
122    /// Builder: include method-like entries.
123    pub fn methods(mut self, value: bool) -> Self {
124        self.methods = value;
125        self
126    }
127
128    /// Builder: show documentation text.
129    pub fn docs(mut self, value: bool) -> Self {
130        self.docs = value;
131        self
132    }
133
134    /// Builder: show private attributes (`_` prefix).
135    pub fn private(mut self, value: bool) -> Self {
136        self.private = value;
137        self
138    }
139
140    /// Builder: show dunder attributes (`__` prefix).
141    pub fn dunder(mut self, value: bool) -> Self {
142        self.dunder = value;
143        self
144    }
145
146    /// Builder: sort attributes alphabetically.
147    pub fn sort(mut self, value: bool) -> Self {
148        self.sort = value;
149        self
150    }
151
152    /// Builder: show all attributes.
153    pub fn all(mut self, value: bool) -> Self {
154        self.all = value;
155        self
156    }
157
158    /// Builder: enable value pretty-printing.
159    pub fn value(mut self, value: bool) -> Self {
160        self.value = value;
161        self
162    }
163
164    /// Add an attribute to the inspection output.
165    ///
166    /// Parameters:
167    /// - `name`: the attribute name
168    /// - `type_name`: the type of the attribute (e.g. `"String"`, `"i32"`)
169    /// - `value`: the debug/display representation of the value
170    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    /// Add an attribute with documentation.
181    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    /// Add a method to the inspection output.
193    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    /// Add a method with documentation.
203    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    /// Set the object-level documentation text.
214    pub fn doc_text(mut self, doc: impl Into<String>) -> Self {
215        self.doc_text = Some(doc.into());
216        self
217    }
218
219    /// Build the attribute table from a list of `(name, type, value)` tuples.
220    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    /// Get the effective title for this inspection.
229    fn effective_title(&self) -> String {
230        self.title.clone().unwrap_or_else(|| "Object".to_string())
231    }
232
233    /// Determine if an attribute name should be visible based on filter settings.
234    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        // -- Build the styled title --
255        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        // -- Value representation --
261        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        // -- Doc text --
269        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        // -- Attributes table --
286        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        // -- Methods table --
348        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        // -- Collect all rendered lines --
387        let mut all_lines: Vec<Vec<Segment>> = Vec::new();
388
389        // Add intro segments
390        if !segments.is_empty() {
391            all_lines.push(segments);
392        }
393
394        // Render child tables
395        for item in &items {
396            let result = item.render(options);
397            all_lines.extend(result.lines);
398        }
399
400        // -- Wrap in a Panel --
401        // Build the panel content from the collected segments
402        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
421// ---------------------------------------------------------------------------
422// Convenience constructors
423// ---------------------------------------------------------------------------
424
425/// Create an `Inspect` for a value that implements `Debug`.
426///
427/// This is the Rust equivalent of Python Rich's `inspect()` function.
428pub fn inspect(value: &dyn std::fmt::Debug) -> Inspect {
429    Inspect::new(value)
430}
431
432/// Create an `Inspect` from a string value with a custom title.
433pub 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        // Verify it renders without panicking (sorting is internal)
534        let ansi = result.to_ansi();
535        assert!(!ansi.is_empty());
536    }
537}