Skip to main content

dioxus_mdx/components/openapi/
spec_viewer.rs

1//! Main OpenAPI specification viewer component.
2
3use std::collections::BTreeMap;
4
5use dioxus::prelude::*;
6use dioxus_free_icons::{Icon, icons::ld_icons::*};
7
8use crate::parser::{ApiOperation, ApiTag, OpenApiSpec, SchemaDefinition};
9
10use super::schema_viewer::SchemaViewer;
11use super::tag_group::{TagGroup, UngroupedEndpoints};
12
13/// Props for OpenApiViewer component.
14#[derive(Props, Clone, PartialEq)]
15pub struct OpenApiViewerProps {
16    /// The parsed OpenAPI specification.
17    pub spec: OpenApiSpec,
18    /// Optional filter to show only specific tags.
19    #[props(default)]
20    pub tags: Option<Vec<String>>,
21    /// Whether to show schema definitions section.
22    #[props(default = true)]
23    pub show_schemas: bool,
24}
25
26/// Main OpenAPI specification viewer.
27#[component]
28pub fn OpenApiViewer(props: OpenApiViewerProps) -> Element {
29    let spec = &props.spec;
30
31    // Group operations by tag
32    let (grouped_ops, ungrouped_ops) = group_operations_by_tag(&spec.operations, &spec.tags);
33
34    // Filter tags if specified
35    let filtered_groups: Vec<_> = if let Some(filter_tags) = &props.tags {
36        grouped_ops
37            .into_iter()
38            .filter(|(tag, _)| {
39                filter_tags
40                    .iter()
41                    .any(|t| t.eq_ignore_ascii_case(&tag.name))
42            })
43            .collect()
44    } else {
45        grouped_ops
46    };
47
48    rsx! {
49        div { class: "openapi-viewer",
50            // API Info header
51            ApiInfoHeader { info: spec.info.clone(), servers: spec.servers.clone() }
52
53            // Endpoints grouped by tag
54            div { class: "mt-6",
55                for (tag, ops) in filtered_groups {
56                    TagGroup {
57                        key: "{tag.name}",
58                        tag: tag.clone(),
59                        operations: ops,
60                    }
61                }
62
63                // Ungrouped endpoints (only show if no tag filter)
64                if props.tags.is_none() {
65                    UngroupedEndpoints { operations: ungrouped_ops }
66                }
67            }
68
69            // Schema definitions
70            if props.show_schemas && !spec.schemas.is_empty() {
71                SchemaDefinitions { schemas: spec.schemas.clone() }
72            }
73        }
74    }
75}
76
77/// Group operations by their tags.
78fn group_operations_by_tag(
79    operations: &[ApiOperation],
80    tags: &[ApiTag],
81) -> (Vec<(ApiTag, Vec<ApiOperation>)>, Vec<ApiOperation>) {
82    let mut grouped: BTreeMap<String, Vec<ApiOperation>> = BTreeMap::new();
83    let mut ungrouped = Vec::new();
84
85    for op in operations {
86        if op.tags.is_empty() {
87            ungrouped.push(op.clone());
88        } else {
89            for tag_name in &op.tags {
90                grouped
91                    .entry(tag_name.clone())
92                    .or_default()
93                    .push(op.clone());
94            }
95        }
96    }
97
98    // Convert to vec with tag metadata, preserving tag order from spec
99    let mut result = Vec::new();
100    for tag in tags {
101        if let Some(ops) = grouped.remove(&tag.name) {
102            result.push((tag.clone(), ops));
103        }
104    }
105
106    // Add any remaining tags that weren't in the spec's tag list
107    for (tag_name, ops) in grouped {
108        result.push((
109            ApiTag {
110                name: tag_name,
111                description: None,
112            },
113            ops,
114        ));
115    }
116
117    (result, ungrouped)
118}
119
120/// Props for ApiInfoHeader component.
121#[derive(Props, Clone, PartialEq)]
122pub struct ApiInfoHeaderProps {
123    /// API info metadata.
124    pub info: crate::parser::ApiInfo,
125    /// Server URLs.
126    pub servers: Vec<crate::parser::ApiServer>,
127}
128
129/// API information header with title, version, and servers.
130#[component]
131pub fn ApiInfoHeader(props: ApiInfoHeaderProps) -> Element {
132    let info = &props.info;
133
134    rsx! {
135        div { class: "border-b border-base-300 pb-4 mb-4",
136            // Title and version
137            div { class: "flex items-center gap-3 flex-wrap",
138                h2 { class: "text-2xl font-bold text-base-content",
139                    "{info.title}"
140                }
141                span { class: "badge badge-primary badge-outline",
142                    "v{info.version}"
143                }
144            }
145
146            // Description
147            if let Some(desc) = &info.description {
148                p { class: "mt-2 text-base-content/70",
149                    "{desc}"
150                }
151            }
152
153            // Servers
154            if !props.servers.is_empty() {
155                div { class: "mt-4",
156                    span { class: "text-sm font-semibold text-base-content/60 flex items-center gap-2",
157                        Icon { class: "size-4", icon: LdServer }
158                        "Servers"
159                    }
160                    div { class: "mt-2 space-y-1",
161                        for server in &props.servers {
162                            div { class: "flex items-center gap-2",
163                                code { class: "text-sm font-mono text-primary bg-base-200 px-2 py-1 rounded",
164                                    "{server.url}"
165                                }
166                                if let Some(desc) = &server.description {
167                                    span { class: "text-sm text-base-content/50",
168                                        "- {desc}"
169                                    }
170                                }
171                            }
172                        }
173                    }
174                }
175            }
176        }
177    }
178}
179
180/// Props for SchemaDefinitions component.
181#[derive(Props, Clone, PartialEq)]
182pub struct SchemaDefinitionsProps {
183    /// Schema definitions by name.
184    pub schemas: BTreeMap<String, SchemaDefinition>,
185}
186
187/// Schema definitions section.
188#[component]
189pub fn SchemaDefinitions(props: SchemaDefinitionsProps) -> Element {
190    let mut is_expanded = use_signal(|| false);
191
192    rsx! {
193        div { class: "mt-8 border-t border-base-300 pt-4",
194            // Header
195            button {
196                class: "w-full flex items-center gap-2 py-2 text-left",
197                onclick: move |_| is_expanded.set(!is_expanded()),
198
199                Icon {
200                    class: if is_expanded() { "size-5 text-base-content/50 transform rotate-90 transition-transform" } else { "size-5 text-base-content/50 transition-transform" },
201                    icon: LdChevronRight
202                }
203
204                h3 { class: "text-lg font-semibold text-base-content flex items-center gap-2",
205                    Icon { class: "size-5", icon: LdBraces }
206                    "Schema Definitions"
207                }
208
209                span { class: "badge badge-ghost badge-sm",
210                    "{props.schemas.len()}"
211                }
212            }
213
214            // Schema list
215            if is_expanded() {
216                div { class: "mt-4 space-y-4",
217                    for (name, schema) in &props.schemas {
218                        div { class: "border border-base-300 rounded-lg overflow-hidden",
219                            // Schema name header
220                            div { class: "px-4 py-2 bg-base-200 border-b border-base-300",
221                                code { class: "font-mono font-semibold text-primary",
222                                    "{name}"
223                                }
224                            }
225                            // Schema content
226                            div { class: "p-4",
227                                SchemaViewer {
228                                    schema: schema.clone(),
229                                    expanded: true,
230                                }
231                            }
232                        }
233                    }
234                }
235            }
236        }
237    }
238}