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,no_run
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};
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// ---------------------------------------------------------------------------
33// Inspect — structured object inspection
34// ---------------------------------------------------------------------------
35
36/// A renderable that displays structured inspection of a value.
37///
38/// Shows the type name, debug/value representation, and optional attribute
39/// table with types and documentation.
40#[derive(Debug, Clone)]
41pub struct Inspect {
42    /// The debug/value representation of the object.
43    value_repr: String,
44    /// The type name to display.
45    title: Option<String>,
46    /// Show full help-style output (more detail).
47    help: bool,
48    /// Show callable/method-like attributes.
49    methods: bool,
50    /// Show doc/comment strings.
51    docs: bool,
52    /// Show private attributes (prefixed with `_`).
53    private: bool,
54    /// Show dunder attributes (prefixed with `__`).
55    dunder: bool,
56    /// Sort attributes alphabetically.
57    sort: bool,
58    /// Show all attributes regardless of prefix.
59    all: bool,
60    /// Pretty-print values.
61    value: bool,
62    /// Attribute definitions: (name, type_str, value_str, doc_string).
63    attrs: Vec<(String, String, String, Option<String>)>,
64    /// Method definitions: (name, signature, doc_string).
65    method_list: Vec<(String, String, Option<String>)>,
66    /// Doc/summary text for the object itself.
67    doc_text: Option<String>,
68}
69
70impl Inspect {
71    /// Create a new `Inspect` for a value that implements [`std::fmt::Debug`].
72    ///
73    /// The debug representation is captured immediately.
74    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    /// Create a new `Inspect` from a string value representation.
93    #[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    /// Builder: set a custom title (overrides type name).
113    pub fn title(mut self, title: impl Into<String>) -> Self {
114        self.title = Some(title.into());
115        self
116    }
117
118    /// Builder: show full detail / help mode.
119    pub fn help(mut self, value: bool) -> Self {
120        self.help = value;
121        self
122    }
123
124    /// Builder: include method-like entries.
125    pub fn methods(mut self, value: bool) -> Self {
126        self.methods = value;
127        self
128    }
129
130    /// Builder: show documentation text.
131    pub fn docs(mut self, value: bool) -> Self {
132        self.docs = value;
133        self
134    }
135
136    /// Builder: show private attributes (`_` prefix).
137    pub fn private(mut self, value: bool) -> Self {
138        self.private = value;
139        self
140    }
141
142    /// Builder: show dunder attributes (`__` prefix).
143    pub fn dunder(mut self, value: bool) -> Self {
144        self.dunder = value;
145        self
146    }
147
148    /// Builder: sort attributes alphabetically.
149    pub fn sort(mut self, value: bool) -> Self {
150        self.sort = value;
151        self
152    }
153
154    /// Builder: show all attributes.
155    pub fn all(mut self, value: bool) -> Self {
156        self.all = value;
157        self
158    }
159
160    /// Builder: enable value pretty-printing.
161    pub fn value(mut self, value: bool) -> Self {
162        self.value = value;
163        self
164    }
165
166    /// Add an attribute to the inspection output.
167    ///
168    /// Parameters:
169    /// - `name`: the attribute name
170    /// - `type_name`: the type of the attribute (e.g. `"String"`, `"i32"`)
171    /// - `value`: the debug/display representation of the value
172    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    /// Add an attribute with documentation.
184    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    /// Add a method to the inspection output.
201    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    /// Add a method with documentation.
207    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    /// Set the object-level documentation text.
219    pub fn doc_text(mut self, doc: impl Into<String>) -> Self {
220        self.doc_text = Some(doc.into());
221        self
222    }
223
224    /// Build the attribute table from a list of `(name, type, value)` tuples.
225    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    /// Get the effective title for this inspection.
231    fn effective_title(&self) -> String {
232        self.title.clone().unwrap_or_else(|| "Object".to_string())
233    }
234
235    /// Determine if an attribute name should be visible based on filter settings.
236    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        // -- Build the styled title --
258        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        // -- Value representation --
264        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        // -- Doc text --
279        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        // -- Attributes table --
293        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        // -- Methods table --
360        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        // -- Collect all rendered lines --
399        let mut all_lines: Vec<Vec<Segment>> = Vec::new();
400
401        // Add intro segments
402        if !segments.is_empty() {
403            all_lines.push(segments);
404        }
405
406        // Render child tables
407        for item in &items {
408            let result = item.render(options);
409            all_lines.extend(result.lines);
410        }
411
412        // -- Wrap in a Panel --
413        // Build the panel content from the collected segments
414        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
432// ---------------------------------------------------------------------------
433// Convenience constructors
434// ---------------------------------------------------------------------------
435
436/// Create an `Inspect` for a value that implements `Debug`.
437///
438/// This is the Rust equivalent of Python Rich's `inspect()` function.
439pub fn inspect(value: &dyn std::fmt::Debug) -> Inspect {
440    Inspect::new(value)
441}
442
443/// Create an `Inspect` from a string value with a custom title.
444pub 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        // Verify it renders without panicking (sorting is internal)
545        let ansi = result.to_ansi();
546        assert!(!ansi.is_empty());
547    }
548}