use anyhow::{Context, Result};
use indexmap::{IndexMap, IndexSet};
use log::{debug, warn};
use std::collections::{HashMap, HashSet};
use std::fs::File;
use std::io::{BufReader, Read, Seek, SeekFrom};
use std::path::Path;
use crate::models::{ApiDocumentation, Endpoint, OpenApiSpec, Parameter, Response, Service};
use crate::utils::{
extract_security_schemes, extract_servers, resolve_parameter_ref, resolve_response_ref,
};
pub fn parse_openapi<P: AsRef<Path>>(path: P) -> Result<ApiDocumentation> {
let path_ref = path.as_ref();
let file = File::open(path_ref).context("Failed to open OpenAPI file")?;
let mut reader = BufReader::new(file);
match serde_json::from_reader(&mut reader) as Result<OpenApiSpec, _> {
Ok(spec) => {
validate_openapi(&spec, path_ref)?;
let services = extract_services(&spec);
debug!("Extracted {} services", services.len());
let servers = extract_servers(&spec);
debug!("Extracted {} server URLs", servers.len());
let security_schemes = extract_security_schemes(&spec);
debug!("Extracted {} security schemes", security_schemes.len());
let endpoints = extract_endpoints(&spec, &services);
debug!("Extracted {} endpoints", endpoints.len());
Ok(ApiDocumentation {
title: spec.info.title,
version: spec.info.version,
description: spec.info.description,
services,
endpoints,
servers,
security_schemes,
})
}
Err(err) => {
reader.seek(SeekFrom::Start(0))?;
let mut content = String::new();
reader.read_to_string(&mut content)?;
match serde_json::from_str::<serde_json::Value>(&content) {
Ok(json) => {
if !json.is_object() {
return Err(anyhow::anyhow!("Root element is not a JSON object"));
}
let obj = json.as_object().unwrap();
if !obj.contains_key("swagger") && !obj.contains_key("openapi") {
return Err(anyhow::anyhow!(
"Missing 'swagger' or 'openapi' field - not a valid OpenAPI specification"
));
}
if !obj.contains_key("paths") {
return Err(anyhow::anyhow!(
"Missing 'paths' field - not a valid OpenAPI specification"
));
}
if !obj.contains_key("info") {
return Err(anyhow::anyhow!(
"Missing 'info' field - not a valid OpenAPI specification"
));
}
Err(anyhow::anyhow!(
"Invalid OpenAPI specification structure: {}",
err
))
}
Err(_) => {
Err(anyhow::anyhow!("File is not valid JSON: {}", err))
}
}
}
}
}
fn validate_openapi(spec: &OpenApiSpec, path: &Path) -> Result<()> {
if let Some(version) = &spec.spec_version {
debug!("OpenAPI specification version: {}", version);
} else {
warn!(
"OpenAPI specification version not found in {}, continuing anyway",
path.display()
);
}
if spec.info.title.is_empty() {
warn!("OpenAPI specification is missing a title");
}
if spec.info.version.is_empty() {
warn!("OpenAPI specification is missing a version");
}
if spec.paths.is_empty() {
warn!("OpenAPI specification has no paths defined");
}
Ok(())
}
fn extract_services(spec: &OpenApiSpec) -> Vec<Service> {
let mut services = Vec::new();
if let Some(tags) = &spec.tags {
for tag in tags {
services.push(Service {
name: tag.name.clone(),
description: tag.description.clone(),
});
}
}
if services.is_empty() {
let mut service_names = IndexSet::new();
for (_, path_item) in &spec.paths {
for op in [
&path_item.get,
&path_item.post,
&path_item.put,
&path_item.delete,
&path_item.options,
&path_item.head,
&path_item.patch,
&path_item.trace,
]
.into_iter()
.flatten()
{
if let Some(tags) = &op.tags {
for tag in tags {
service_names.insert(tag.clone());
}
}
}
}
if service_names.is_empty() {
service_names.insert("API".to_string());
}
for name in service_names {
services.push(Service {
name,
description: None,
});
}
}
services
}
fn extract_endpoints(spec: &OpenApiSpec, services: &[Service]) -> Vec<Endpoint> {
let mut endpoints = Vec::new();
let service_map: HashSet<String> = services.iter().map(|s| s.name.clone()).collect();
for (path, path_item) in &spec.paths {
let operations = [
("get", &path_item.get),
("post", &path_item.post),
("put", &path_item.put),
("delete", &path_item.delete),
("options", &path_item.options),
("head", &path_item.head),
("patch", &path_item.patch),
("trace", &path_item.trace),
];
let path_parameters = path_item
.parameters
.as_ref()
.map(|params| {
params
.iter()
.filter_map(|p| resolve_parameter_ref(spec, p))
.collect::<Vec<_>>()
})
.unwrap_or_default();
for (method, operation_opt) in operations {
if let Some(operation) = operation_opt {
let service_tags = if let Some(tags) = &operation.tags {
let filtered_tags: Vec<String> = tags
.iter()
.filter(|tag| service_map.contains(*tag))
.cloned()
.collect();
if filtered_tags.is_empty() {
if !service_map.is_empty() {
vec![services[0].name.clone()]
} else {
vec!["API".to_string()]
}
} else {
filtered_tags
}
} else {
if !service_map.is_empty() {
vec![services[0].name.clone()]
} else {
vec!["API".to_string()]
}
};
let mut parameters = path_parameters.clone();
if let Some(op_params) = &operation.parameters {
for param in op_params {
if let Some(resolved_param) = resolve_parameter_ref(spec, param) {
parameters.push(resolved_param);
}
}
}
if let Some(req_body) = &operation.request_body {
if let Some((_, media_type)) = req_body.content.first() {
parameters.push(Parameter {
name: "requestBody".to_string(),
description: req_body.description.clone(),
parameter_in: "body".to_string(),
required: Some(req_body.required.unwrap_or(false)),
schema: media_type.schema.clone(),
extensions: HashMap::new(),
});
}
}
let resolved_responses: IndexMap<String, Response> = operation
.responses
.iter()
.map(|(status_code, response)| {
let resolved = resolve_response_ref(spec, response)
.unwrap_or_else(|| response.clone());
(status_code.clone(), resolved)
})
.collect();
endpoints.push(Endpoint {
path: path.clone(),
method: method.to_uppercase(),
services: service_tags,
summary: operation.summary.clone(),
description: operation.description.clone(),
operation_id: operation.operation_id.clone(),
parameters,
responses: resolved_responses,
deprecated: operation.deprecated.unwrap_or(false),
});
}
}
}
endpoints
}