use anyhow::{Context, Result};
use openapiv3::OpenAPI;
use std::path::Path;
pub fn parse_spec(path: &Path) -> Result<OpenAPI> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("Failed to read OpenAPI spec from {:?}", path))?;
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
match ext {
"json" => serde_json::from_str(&content)
.with_context(|| "Failed to parse OpenAPI spec as JSON"),
"yaml" | "yml" => serde_yaml::from_str(&content)
.with_context(|| "Failed to parse OpenAPI spec as YAML"),
_ => {
serde_json::from_str(&content)
.or_else(|_| serde_yaml::from_str(&content))
.with_context(|| "Failed to parse OpenAPI spec (tried JSON and YAML)")
}
}
}
#[derive(Debug)]
pub struct Diagnostic {
pub severity: Severity,
pub message: String,
pub path: Option<String>,
}
#[derive(Debug)]
pub enum Severity {
Error,
Warning,
}
impl std::fmt::Display for Diagnostic {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let prefix = match self.severity {
Severity::Error => "error",
Severity::Warning => "warning",
};
if let Some(path) = &self.path {
write!(f, "{}: {} (at {})", prefix, self.message, path)
} else {
write!(f, "{}: {}", prefix, self.message)
}
}
}
pub fn validate(spec: &OpenAPI) -> Vec<Diagnostic> {
let mut diagnostics = Vec::new();
for (path, item) in spec.paths.iter() {
if let openapiv3::ReferenceOr::Item(path_item) = item {
for (method, op) in path_item.iter() {
if op.operation_id.is_none() {
diagnostics.push(Diagnostic {
severity: Severity::Warning,
message: format!(
"Missing operationId for {} {} - will generate from path",
method, path
),
path: Some(format!("paths.{}.{}", path, method)),
});
}
}
}
}
if let Some(components) = &spec.components {
for (name, schema) in &components.schemas {
if let openapiv3::ReferenceOr::Item(schema) = schema {
check_schema_compatibility(name, schema, &mut diagnostics);
}
}
}
diagnostics
}
fn check_schema_compatibility(
name: &str,
schema: &openapiv3::Schema,
diagnostics: &mut Vec<Diagnostic>,
) {
match &schema.schema_kind {
openapiv3::SchemaKind::OneOf { .. } => {
diagnostics.push(Diagnostic {
severity: Severity::Warning,
message: format!(
"Schema '{}' uses oneOf - will generate as variant type",
name
),
path: Some(format!("components.schemas.{}", name)),
});
}
openapiv3::SchemaKind::AnyOf { .. } => {
diagnostics.push(Diagnostic {
severity: Severity::Warning,
message: format!(
"Schema '{}' uses anyOf - support is experimental",
name
),
path: Some(format!("components.schemas.{}", name)),
});
}
_ => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_json_spec() {
let spec_json = r#"{
"openapi": "3.0.0",
"info": { "title": "Test", "version": "1.0.0" },
"paths": {}
}"#;
let temp = tempfile::NamedTempFile::with_suffix(".json").unwrap();
std::fs::write(temp.path(), spec_json).unwrap();
let spec = parse_spec(temp.path()).unwrap();
assert_eq!(spec.info.title, "Test");
}
}