Skip to main content

dioxus_mdx/components/openapi/
schema_viewer.rs

1//! Schema viewer component for displaying type definitions.
2
3use dioxus::prelude::*;
4use dioxus_free_icons::{Icon, icons::ld_icons::*};
5
6use crate::parser::SchemaDefinition;
7
8/// Props for SchemaViewer component.
9#[derive(Props, Clone, PartialEq)]
10pub struct SchemaViewerProps {
11    /// The schema to display.
12    pub schema: SchemaDefinition,
13    /// Nesting depth for indentation.
14    #[props(default = 0)]
15    pub depth: usize,
16    /// Whether this is initially expanded.
17    #[props(default = false)]
18    pub expanded: bool,
19    /// Property name (for nested properties).
20    #[props(default)]
21    pub name: Option<String>,
22    /// Whether this property is required.
23    #[props(default = false)]
24    pub required: bool,
25}
26
27/// Recursive schema viewer with expand/collapse for complex types.
28#[component]
29pub fn SchemaViewer(props: SchemaViewerProps) -> Element {
30    let mut is_expanded = use_signal(|| props.expanded || props.depth == 0);
31    let schema = &props.schema;
32
33    let is_complex = schema.is_complex();
34    let type_display = schema.display_type();
35
36    let indent_class = if props.depth > 0 {
37        "ml-4 border-l-2 border-base-300 pl-3"
38    } else {
39        ""
40    };
41
42    rsx! {
43        div { class: "py-1.5 {indent_class}",
44            // Header row with name, type, and expand button
45            div { class: "flex items-center gap-2 flex-wrap",
46                // Expand/collapse for complex types
47                if is_complex && !schema.properties.is_empty() {
48                    button {
49                        class: "p-0.5 hover:bg-base-300 rounded transition-colors",
50                        onclick: move |_| is_expanded.set(!is_expanded()),
51                        Icon {
52                            class: if is_expanded() { "size-4 text-base-content/50 transform rotate-90 transition-transform" } else { "size-4 text-base-content/50 transition-transform" },
53                            icon: LdChevronRight
54                        }
55                    }
56                }
57
58                // Property name
59                if let Some(name) = &props.name {
60                    code { class: "font-mono font-semibold text-primary text-sm",
61                        "{name}"
62                    }
63                }
64
65                // Type badge
66                span { class: "text-xs px-2 py-0.5 rounded-full bg-base-300 text-base-content/70",
67                    "{type_display}"
68                }
69
70                // Required indicator
71                if props.required {
72                    span { class: "text-xs px-2 py-0.5 rounded-full bg-error/20 text-error",
73                        "required"
74                    }
75                }
76
77                // Nullable indicator
78                if schema.nullable {
79                    span { class: "text-xs px-2 py-0.5 rounded-full bg-base-300 text-base-content/50",
80                        "nullable"
81                    }
82                }
83
84                // Format
85                if let Some(format) = &schema.format {
86                    span { class: "text-xs text-base-content/50",
87                        "({format})"
88                    }
89                }
90            }
91
92            // Description
93            if let Some(desc) = &schema.description {
94                p { class: "text-sm text-base-content/70 mt-1",
95                    "{desc}"
96                }
97            }
98
99            // Enum values
100            if !schema.enum_values.is_empty() {
101                div { class: "mt-1 flex items-center gap-2 flex-wrap",
102                    span { class: "text-xs text-base-content/50", "Enum:" }
103                    for value in &schema.enum_values {
104                        code { class: "text-xs px-1.5 py-0.5 rounded bg-base-300 font-mono",
105                            "{value}"
106                        }
107                    }
108                }
109            }
110
111            // Default value
112            if let Some(default) = &schema.default {
113                div { class: "mt-1",
114                    span { class: "text-xs text-base-content/50", "Default: " }
115                    code { class: "text-xs font-mono text-primary",
116                        "{default}"
117                    }
118                }
119            }
120
121            // Example value
122            if let Some(example) = &schema.example {
123                div { class: "mt-1",
124                    span { class: "text-xs text-base-content/50", "Example: " }
125                    code { class: "text-xs font-mono text-secondary",
126                        "{example}"
127                    }
128                }
129            }
130
131            // Nested properties for objects
132            if is_expanded() && !schema.properties.is_empty() {
133                div { class: "mt-2",
134                    for (name, prop_schema) in &schema.properties {
135                        SchemaViewer {
136                            key: "{name}",
137                            schema: prop_schema.clone(),
138                            depth: props.depth + 1,
139                            name: Some(name.clone()),
140                            required: schema.required.contains(name),
141                        }
142                    }
143                }
144            }
145
146            // Array items
147            if is_expanded() {
148                if let Some(items) = &schema.items {
149                    if items.is_complex() {
150                        div { class: "mt-2",
151                            span { class: "text-xs text-base-content/50 ml-4", "Array items:" }
152                            SchemaViewer {
153                                schema: (**items).clone(),
154                                depth: props.depth + 1,
155                            }
156                        }
157                    }
158                }
159            }
160
161            // OneOf/AnyOf/AllOf
162            if is_expanded() {
163                if !schema.one_of.is_empty() {
164                    div { class: "mt-2 ml-4",
165                        span { class: "text-xs text-base-content/50 font-semibold", "One of:" }
166                        for (i, variant) in schema.one_of.iter().enumerate() {
167                            SchemaViewer {
168                                key: "{i}",
169                                schema: variant.clone(),
170                                depth: props.depth + 1,
171                            }
172                        }
173                    }
174                }
175                if !schema.any_of.is_empty() {
176                    div { class: "mt-2 ml-4",
177                        span { class: "text-xs text-base-content/50 font-semibold", "Any of:" }
178                        for (i, variant) in schema.any_of.iter().enumerate() {
179                            SchemaViewer {
180                                key: "{i}",
181                                schema: variant.clone(),
182                                depth: props.depth + 1,
183                            }
184                        }
185                    }
186                }
187                if !schema.all_of.is_empty() {
188                    div { class: "mt-2 ml-4",
189                        span { class: "text-xs text-base-content/50 font-semibold", "All of:" }
190                        for (i, variant) in schema.all_of.iter().enumerate() {
191                            SchemaViewer {
192                                key: "{i}",
193                                schema: variant.clone(),
194                                depth: props.depth + 1,
195                            }
196                        }
197                    }
198                }
199            }
200        }
201    }
202}
203
204/// Props for SchemaTypeLabel component.
205#[derive(Props, Clone, PartialEq)]
206pub struct SchemaTypeLabelProps {
207    /// The schema to display type for.
208    pub schema: SchemaDefinition,
209}
210
211/// Simple type label without expand/collapse.
212#[component]
213pub fn SchemaTypeLabel(props: SchemaTypeLabelProps) -> Element {
214    let type_display = props.schema.display_type();
215
216    rsx! {
217        span { class: "text-xs px-2 py-0.5 rounded-full bg-base-300 text-base-content/70 font-mono",
218            "{type_display}"
219        }
220    }
221}