Skip to main content

dioxus_mdx/parser/
openapi_types.rs

1//! Internal type definitions for parsed OpenAPI specifications.
2//!
3//! These types provide a simplified view of OpenAPI specs for rendering.
4
5use serde_json::json;
6use std::collections::BTreeMap;
7
8/// Parsed OpenAPI specification.
9#[derive(Debug, Clone, PartialEq)]
10pub struct OpenApiSpec {
11    /// API info (title, version, description).
12    pub info: ApiInfo,
13    /// Server URLs.
14    pub servers: Vec<ApiServer>,
15    /// API operations grouped by tag.
16    pub operations: Vec<ApiOperation>,
17    /// Unique tags in order of appearance.
18    pub tags: Vec<ApiTag>,
19    /// Reusable schema definitions.
20    pub schemas: BTreeMap<String, SchemaDefinition>,
21}
22
23/// API metadata.
24#[derive(Debug, Clone, PartialEq, Default)]
25pub struct ApiInfo {
26    /// API title.
27    pub title: String,
28    /// API version.
29    pub version: String,
30    /// API description.
31    pub description: Option<String>,
32}
33
34/// Server configuration.
35#[derive(Debug, Clone, PartialEq)]
36pub struct ApiServer {
37    /// Server URL.
38    pub url: String,
39    /// Server description.
40    pub description: Option<String>,
41}
42
43/// Tag metadata.
44#[derive(Debug, Clone, PartialEq)]
45pub struct ApiTag {
46    /// Tag name.
47    pub name: String,
48    /// Tag description.
49    pub description: Option<String>,
50}
51
52/// HTTP method.
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum HttpMethod {
55    Get,
56    Post,
57    Put,
58    Delete,
59    Patch,
60    Head,
61    Options,
62}
63
64impl HttpMethod {
65    /// Parse from string.
66    pub fn parse(s: &str) -> Option<Self> {
67        match s.to_lowercase().as_str() {
68            "get" => Some(Self::Get),
69            "post" => Some(Self::Post),
70            "put" => Some(Self::Put),
71            "delete" => Some(Self::Delete),
72            "patch" => Some(Self::Patch),
73            "head" => Some(Self::Head),
74            "options" => Some(Self::Options),
75            _ => None,
76        }
77    }
78
79    /// Convert to uppercase string.
80    pub fn as_str(&self) -> &'static str {
81        match self {
82            Self::Get => "GET",
83            Self::Post => "POST",
84            Self::Put => "PUT",
85            Self::Delete => "DELETE",
86            Self::Patch => "PATCH",
87            Self::Head => "HEAD",
88            Self::Options => "OPTIONS",
89        }
90    }
91
92    /// DaisyUI badge class for the method.
93    pub fn badge_class(&self) -> &'static str {
94        match self {
95            Self::Get => "badge-soft badge-success",
96            Self::Post => "badge-soft badge-primary",
97            Self::Put => "badge-soft badge-warning",
98            Self::Delete => "badge-soft badge-error",
99            Self::Patch => "badge-soft badge-info",
100            Self::Head => "badge-soft badge-ghost",
101            Self::Options => "badge-soft badge-ghost",
102        }
103    }
104
105    /// Tailwind background class for the method.
106    pub fn bg_class(&self) -> &'static str {
107        match self {
108            Self::Get => "bg-success/10 border-success/30 text-success",
109            Self::Post => "bg-primary/10 border-primary/30 text-primary",
110            Self::Put => "bg-warning/10 border-warning/30 text-warning",
111            Self::Delete => "bg-error/10 border-error/30 text-error",
112            Self::Patch => "bg-info/10 border-info/30 text-info",
113            Self::Head => "bg-base-300 border-base-content/20 text-base-content/70",
114            Self::Options => "bg-base-300 border-base-content/20 text-base-content/70",
115        }
116    }
117}
118
119/// API endpoint operation.
120#[derive(Debug, Clone, PartialEq)]
121pub struct ApiOperation {
122    /// Unique operation ID.
123    pub operation_id: Option<String>,
124    /// HTTP method.
125    pub method: HttpMethod,
126    /// URL path.
127    pub path: String,
128    /// Short summary.
129    pub summary: Option<String>,
130    /// Full description.
131    pub description: Option<String>,
132    /// Tags for grouping.
133    pub tags: Vec<String>,
134    /// Parameters (path, query, header).
135    pub parameters: Vec<ApiParameter>,
136    /// Request body.
137    pub request_body: Option<ApiRequestBody>,
138    /// Response definitions.
139    pub responses: Vec<ApiResponse>,
140    /// Whether the endpoint is deprecated.
141    pub deprecated: bool,
142}
143
144impl ApiOperation {
145    /// Generate a URL-friendly slug for this operation.
146    ///
147    /// Uses `operation_id` if present (camelCase → kebab-case), otherwise
148    /// falls back to `method-path` format.
149    pub fn slug(&self) -> String {
150        if let Some(op_id) = &self.operation_id {
151            slugify_operation_id(op_id)
152        } else {
153            // Fallback: method-path format
154            let path_slug = self
155                .path
156                .trim_matches('/')
157                .replace('/', "-")
158                .replace(['{', '}'], "");
159            format!("{}-{}", self.method.as_str().to_lowercase(), path_slug)
160        }
161    }
162
163    /// Generate a curl command for this endpoint.
164    pub fn generate_curl(&self, base_url: &str) -> String {
165        let mut parts = vec!["curl".to_string()];
166
167        // Method
168        if !matches!(self.method, HttpMethod::Get) {
169            parts.push(format!("-X {}", self.method.as_str()));
170        }
171
172        // Build URL with path params
173        let mut url = format!("{}{}", base_url.trim_end_matches('/'), self.path);
174        let mut query_parts = Vec::new();
175
176        for param in &self.parameters {
177            match param.location {
178                ParameterLocation::Path => {
179                    let placeholder = if let Some(schema) = &param.schema {
180                        let val = schema.generate_example_json(0);
181                        val.as_str()
182                            .map(|s| s.to_string())
183                            .unwrap_or_else(|| val.to_string())
184                    } else {
185                        format!("{{{}}}", param.name)
186                    };
187                    url = url.replace(&format!("{{{}}}", param.name), &placeholder);
188                }
189                ParameterLocation::Query => {
190                    if let Some(schema) = &param.schema {
191                        let val = schema.generate_example_json(0);
192                        let val_str = val
193                            .as_str()
194                            .map(|s| s.to_string())
195                            .unwrap_or_else(|| val.to_string());
196                        query_parts.push(format!("{}={}", param.name, val_str));
197                    }
198                }
199                _ => {}
200            }
201        }
202
203        if !query_parts.is_empty() {
204            url = format!("{}?{}", url, query_parts.join("&"));
205        }
206
207        parts.push(format!("\"{}\"", url));
208
209        // Content-Type header if there's a request body
210        if self.request_body.is_some() {
211            parts.push("-H \"Content-Type: application/json\"".to_string());
212        }
213
214        // Request body
215        if let Some(body) = &self.request_body {
216            for content in &body.content {
217                if content.media_type.contains("json") {
218                    if let Some(schema) = &content.schema {
219                        let example = schema.generate_example_json(0);
220                        if let Ok(pretty) = serde_json::to_string_pretty(&example) {
221                            parts.push(format!("-d '{}'", pretty));
222                        }
223                    }
224                    break;
225                }
226            }
227        }
228
229        parts.join(" \\\n  ")
230    }
231
232    /// Generate a response example from the first 2xx response.
233    ///
234    /// Returns `Some((status_code, pretty_json))` if a 2xx response with
235    /// content schema is found, `None` otherwise.
236    pub fn generate_response_example(&self) -> Option<(String, String)> {
237        for response in &self.responses {
238            if response.status_code.starts_with('2') {
239                for content in &response.content {
240                    if let Some(schema) = &content.schema {
241                        let example = schema.generate_example_json(0);
242                        if let Ok(pretty) = serde_json::to_string_pretty(&example) {
243                            return Some((response.status_code.clone(), pretty));
244                        }
245                    }
246                }
247            }
248        }
249        None
250    }
251}
252
253/// Convert a camelCase operation ID to kebab-case slug.
254fn slugify_operation_id(id: &str) -> String {
255    let mut result = String::new();
256    for (i, ch) in id.chars().enumerate() {
257        if ch.is_uppercase() && i > 0 {
258            result.push('-');
259        }
260        result.push(ch.to_lowercase().next().unwrap_or(ch));
261    }
262    result
263}
264
265/// Parameter location.
266#[derive(Debug, Clone, Copy, PartialEq, Eq)]
267pub enum ParameterLocation {
268    Path,
269    Query,
270    Header,
271    Cookie,
272}
273
274impl ParameterLocation {
275    /// Parse from string.
276    pub fn parse(s: &str) -> Option<Self> {
277        match s.to_lowercase().as_str() {
278            "path" => Some(Self::Path),
279            "query" => Some(Self::Query),
280            "header" => Some(Self::Header),
281            "cookie" => Some(Self::Cookie),
282            _ => None,
283        }
284    }
285
286    /// Convert to string.
287    pub fn as_str(&self) -> &'static str {
288        match self {
289            Self::Path => "path",
290            Self::Query => "query",
291            Self::Header => "header",
292            Self::Cookie => "cookie",
293        }
294    }
295
296    /// Badge class for the location.
297    pub fn badge_class(&self) -> &'static str {
298        match self {
299            Self::Path => "badge-primary",
300            Self::Query => "badge-info",
301            Self::Header => "badge-warning",
302            Self::Cookie => "badge-secondary",
303        }
304    }
305}
306
307/// API parameter.
308#[derive(Debug, Clone, PartialEq)]
309pub struct ApiParameter {
310    /// Parameter name.
311    pub name: String,
312    /// Parameter location.
313    pub location: ParameterLocation,
314    /// Parameter description.
315    pub description: Option<String>,
316    /// Whether the parameter is required.
317    pub required: bool,
318    /// Whether the parameter is deprecated.
319    pub deprecated: bool,
320    /// Parameter schema.
321    pub schema: Option<SchemaDefinition>,
322    /// Example value.
323    pub example: Option<String>,
324}
325
326/// Request body definition.
327#[derive(Debug, Clone, PartialEq)]
328pub struct ApiRequestBody {
329    /// Description.
330    pub description: Option<String>,
331    /// Whether the body is required.
332    pub required: bool,
333    /// Content by media type.
334    pub content: Vec<MediaTypeContent>,
335}
336
337/// Content for a specific media type.
338#[derive(Debug, Clone, PartialEq)]
339pub struct MediaTypeContent {
340    /// Media type (e.g., "application/json").
341    pub media_type: String,
342    /// Schema for the content.
343    pub schema: Option<SchemaDefinition>,
344    /// Example value.
345    pub example: Option<String>,
346}
347
348/// API response definition.
349#[derive(Debug, Clone, PartialEq)]
350pub struct ApiResponse {
351    /// HTTP status code or "default".
352    pub status_code: String,
353    /// Response description.
354    pub description: String,
355    /// Content by media type.
356    pub content: Vec<MediaTypeContent>,
357}
358
359impl ApiResponse {
360    /// Get badge class based on status code.
361    pub fn status_badge_class(&self) -> &'static str {
362        match self.status_code.chars().next() {
363            Some('2') => "badge-success",
364            Some('3') => "badge-info",
365            Some('4') => "badge-warning",
366            Some('5') => "badge-error",
367            _ => "badge-ghost",
368        }
369    }
370}
371
372/// Schema type.
373#[derive(Debug, Clone, PartialEq)]
374pub enum SchemaType {
375    String,
376    Number,
377    Integer,
378    Boolean,
379    Array,
380    Object,
381    Null,
382    Any,
383}
384
385impl SchemaType {
386    /// Convert to string.
387    pub fn as_str(&self) -> &'static str {
388        match self {
389            Self::String => "string",
390            Self::Number => "number",
391            Self::Integer => "integer",
392            Self::Boolean => "boolean",
393            Self::Array => "array",
394            Self::Object => "object",
395            Self::Null => "null",
396            Self::Any => "any",
397        }
398    }
399}
400
401/// Schema definition for a type.
402#[derive(Debug, Clone, PartialEq)]
403pub struct SchemaDefinition {
404    /// Schema type.
405    pub schema_type: SchemaType,
406    /// Format (e.g., "int64", "email", "date-time").
407    pub format: Option<String>,
408    /// Description.
409    pub description: Option<String>,
410    /// For arrays, the item schema.
411    pub items: Option<Box<SchemaDefinition>>,
412    /// For objects, property schemas.
413    pub properties: BTreeMap<String, SchemaDefinition>,
414    /// Required property names.
415    pub required: Vec<String>,
416    /// Reference name (for $ref).
417    pub ref_name: Option<String>,
418    /// Enum values.
419    pub enum_values: Vec<String>,
420    /// Example value.
421    pub example: Option<String>,
422    /// Default value.
423    pub default: Option<String>,
424    /// Nullable flag.
425    pub nullable: bool,
426    /// Additional properties schema (for objects).
427    pub additional_properties: Option<Box<SchemaDefinition>>,
428    /// OneOf schemas.
429    pub one_of: Vec<SchemaDefinition>,
430    /// AnyOf schemas.
431    pub any_of: Vec<SchemaDefinition>,
432    /// AllOf schemas.
433    pub all_of: Vec<SchemaDefinition>,
434}
435
436impl Default for SchemaDefinition {
437    fn default() -> Self {
438        Self {
439            schema_type: SchemaType::Any,
440            format: None,
441            description: None,
442            items: None,
443            properties: BTreeMap::new(),
444            required: Vec::new(),
445            ref_name: None,
446            enum_values: Vec::new(),
447            example: None,
448            default: None,
449            nullable: false,
450            additional_properties: None,
451            one_of: Vec::new(),
452            any_of: Vec::new(),
453            all_of: Vec::new(),
454        }
455    }
456}
457
458impl SchemaDefinition {
459    /// Get a display type string (e.g., "string", "array`<User>`", "object").
460    pub fn display_type(&self) -> String {
461        if let Some(ref_name) = &self.ref_name {
462            return ref_name.clone();
463        }
464
465        match &self.schema_type {
466            SchemaType::Array => {
467                if let Some(items) = &self.items {
468                    format!("array<{}>", items.display_type())
469                } else {
470                    "array".to_string()
471                }
472            }
473            SchemaType::Object if !self.properties.is_empty() => "object".to_string(),
474            other => {
475                let mut s = other.as_str().to_string();
476                if let Some(format) = &self.format {
477                    s.push_str(&format!(" ({format})"));
478                }
479                s
480            }
481        }
482    }
483
484    /// Check if this is a complex type (object or array with object items).
485    pub fn is_complex(&self) -> bool {
486        matches!(self.schema_type, SchemaType::Object | SchemaType::Array)
487            || !self.one_of.is_empty()
488            || !self.any_of.is_empty()
489            || !self.all_of.is_empty()
490    }
491
492    /// Generate example JSON for this schema.
493    ///
494    /// Uses explicit `example` if present, otherwise generates placeholder values by type.
495    /// `depth` prevents infinite recursion from circular refs (max 5).
496    pub fn generate_example_json(&self, depth: usize) -> serde_json::Value {
497        if depth > 5 {
498            return json!({});
499        }
500
501        // Use explicit example if available
502        if let Some(example) = &self.example {
503            if let Ok(val) = serde_json::from_str(example) {
504                return val;
505            }
506            return json!(example);
507        }
508
509        match &self.schema_type {
510            SchemaType::String => {
511                if !self.enum_values.is_empty() {
512                    return json!(self.enum_values[0]);
513                }
514                match self.format.as_deref() {
515                    Some("uuid") => json!("550e8400-e29b-41d4-a716-446655440000"),
516                    Some("date-time") => json!("2024-01-15T09:30:00Z"),
517                    Some("date") => json!("2024-01-15"),
518                    Some("uri") | Some("url") => json!("https://example.com"),
519                    Some("email") => json!("user@example.com"),
520                    _ => json!("string"),
521                }
522            }
523            SchemaType::Integer => {
524                if let Some(default) = &self.default
525                    && let Ok(n) = default.parse::<i64>()
526                {
527                    return json!(n);
528                }
529                json!(0)
530            }
531            SchemaType::Number => json!(0.0),
532            SchemaType::Boolean => json!(true),
533            SchemaType::Array => {
534                if let Some(items) = &self.items {
535                    json!([items.generate_example_json(depth + 1)])
536                } else {
537                    json!([])
538                }
539            }
540            SchemaType::Object => {
541                if self.properties.is_empty() {
542                    return json!({});
543                }
544                let mut map = serde_json::Map::new();
545                for (name, prop) in &self.properties {
546                    map.insert(name.clone(), prop.generate_example_json(depth + 1));
547                }
548                serde_json::Value::Object(map)
549            }
550            SchemaType::Null => json!(null),
551            SchemaType::Any => json!("any"),
552        }
553    }
554}