rescript_openapi/
parser.rs1use anyhow::{Context, Result};
9use openapiv3::OpenAPI;
10use std::path::Path;
11
12pub 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 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#[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
61pub fn validate(spec: &OpenAPI) -> Vec<Diagnostic> {
63 let mut diagnostics = Vec::new();
64
65 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 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}