Skip to main content

rescript_openapi/
parser.rs

1// SPDX-License-Identifier: PMPL-1.0-or-later
2// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell
3
4//! OpenAPI specification parser
5//!
6//! Handles parsing of OpenAPI 3.x specifications in JSON and YAML formats.
7
8use anyhow::{Context, Result};
9use openapiv3::OpenAPI;
10use std::path::Path;
11
12/// Parse an OpenAPI specification from a file
13pub fn parse_spec(path: &Path) -> Result<OpenAPI> {
14    let content = std::fs::read_to_string(path)
15        .with_context(|| format!("Failed to read OpenAPI spec from {:?}", path))?;
16
17    let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
18
19    match ext {
20        "json" => serde_json::from_str(&content)
21            .with_context(|| "Failed to parse OpenAPI spec as JSON"),
22        "yaml" | "yml" => serde_yaml::from_str(&content)
23            .with_context(|| "Failed to parse OpenAPI spec as YAML"),
24        _ => {
25            // Try JSON first, then YAML
26            serde_json::from_str(&content)
27                .or_else(|_| serde_yaml::from_str(&content))
28                .with_context(|| "Failed to parse OpenAPI spec (tried JSON and YAML)")
29        }
30    }
31}
32
33/// Diagnostic message for validation issues
34#[derive(Debug)]
35pub struct Diagnostic {
36    pub severity: Severity,
37    pub message: String,
38    pub path: Option<String>,
39}
40
41#[derive(Debug)]
42pub enum Severity {
43    Error,
44    Warning,
45}
46
47impl std::fmt::Display for Diagnostic {
48    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49        let prefix = match self.severity {
50            Severity::Error => "error",
51            Severity::Warning => "warning",
52        };
53        if let Some(path) = &self.path {
54            write!(f, "{}: {} (at {})", prefix, self.message, path)
55        } else {
56            write!(f, "{}: {}", prefix, self.message)
57        }
58    }
59}
60
61/// Validate an OpenAPI specification for ReScript codegen compatibility
62pub fn validate(spec: &OpenAPI) -> Vec<Diagnostic> {
63    let mut diagnostics = Vec::new();
64
65    // Check for operationId on all operations
66    for (path, item) in spec.paths.iter() {
67        if let openapiv3::ReferenceOr::Item(path_item) = item {
68            for (method, op) in path_item.iter() {
69                if op.operation_id.is_none() {
70                    diagnostics.push(Diagnostic {
71                        severity: Severity::Warning,
72                        message: format!(
73                            "Missing operationId for {} {} - will generate from path",
74                            method, path
75                        ),
76                        path: Some(format!("paths.{}.{}", path, method)),
77                    });
78                }
79            }
80        }
81    }
82
83    // Check for unsupported features
84    if let Some(components) = &spec.components {
85        for (name, schema) in &components.schemas {
86            if let openapiv3::ReferenceOr::Item(schema) = schema {
87                check_schema_compatibility(name, schema, &mut diagnostics);
88            }
89        }
90    }
91
92    diagnostics
93}
94
95fn check_schema_compatibility(
96    name: &str,
97    schema: &openapiv3::Schema,
98    diagnostics: &mut Vec<Diagnostic>,
99) {
100    match &schema.schema_kind {
101        openapiv3::SchemaKind::OneOf { .. } => {
102            diagnostics.push(Diagnostic {
103                severity: Severity::Warning,
104                message: format!(
105                    "Schema '{}' uses oneOf - will generate as variant type",
106                    name
107                ),
108                path: Some(format!("components.schemas.{}", name)),
109            });
110        }
111        openapiv3::SchemaKind::AnyOf { .. } => {
112            diagnostics.push(Diagnostic {
113                severity: Severity::Warning,
114                message: format!(
115                    "Schema '{}' uses anyOf - support is experimental",
116                    name
117                ),
118                path: Some(format!("components.schemas.{}", name)),
119            });
120        }
121        _ => {}
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    #[test]
130    fn test_parse_json_spec() {
131        let spec_json = r#"{
132            "openapi": "3.0.0",
133            "info": { "title": "Test", "version": "1.0.0" },
134            "paths": {}
135        }"#;
136
137        let temp = tempfile::NamedTempFile::with_suffix(".json").unwrap();
138        std::fs::write(temp.path(), spec_json).unwrap();
139
140        let spec = parse_spec(temp.path()).unwrap();
141        assert_eq!(spec.info.title, "Test");
142    }
143}