use super::errors::CoreError;
use anyhow::{Context, Result};
use openapiv3::{OpenAPI, Operation};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::fs;
use std::path::Path;
pub fn load_openapi_spec(path: &Path) -> Result<OpenAPI, CoreError> {
if !path.exists() {
let parent = path.parent().unwrap_or(Path::new("."));
let available = fs::read_dir(parent)
.map(|entries| {
entries
.filter_map(|e| e.ok())
.filter_map(|e| e.file_name().to_str().map(String::from))
.filter(|name| {
name.ends_with(".yaml") || name.ends_with(".yml") || name.ends_with(".json")
})
.collect()
})
.unwrap_or_default();
return Err(CoreError::SpecNotFound {
path: path.to_path_buf(),
available,
});
}
let content = fs::read_to_string(path).map_err(|e| CoreError::SpecParseFailed {
reason: format!("Cannot read file: {}", e),
})?;
let spec = if path.extension().and_then(|s| s.to_str()) == Some("json") {
serde_json::from_str(&content).map_err(|e| CoreError::SpecParseFailed {
reason: format!("Invalid JSON: {}", e),
})?
} else {
serde_yaml::from_str(&content).map_err(|e| CoreError::SpecParseFailed {
reason: format!("Invalid YAML: {}", e),
})?
};
Ok(spec)
}
pub fn find_operation<'a>(
spec: &'a OpenAPI,
operation_id: &str,
) -> Result<&'a Operation, CoreError> {
for (_path, path_item) in &spec.paths.paths {
let path_item = match path_item {
openapiv3::ReferenceOr::Item(item) => item,
_ => continue,
};
let operations = [
(&path_item.get, "GET"),
(&path_item.post, "POST"),
(&path_item.put, "PUT"),
(&path_item.delete, "DELETE"),
(&path_item.patch, "PATCH"),
];
for (op, _method) in operations {
let Some(operation) = op else { continue };
if operation.operation_id.as_deref() == Some(operation_id) {
return Ok(operation);
}
}
}
let available = list_operations(spec);
Err(CoreError::OperationNotFound {
operation: operation_id.to_string(),
available,
})
}
pub fn list_operations(spec: &OpenAPI) -> Vec<String> {
let mut operations = Vec::new();
for (_path, path_item) in &spec.paths.paths {
let path_item = match path_item {
openapiv3::ReferenceOr::Item(item) => item,
_ => continue,
};
let ops = [
&path_item.get,
&path_item.post,
&path_item.put,
&path_item.delete,
&path_item.patch,
];
for op in ops.into_iter().flatten() {
if let Some(id) = &op.operation_id {
operations.push(id.clone());
}
}
}
operations
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_load_valid_openapi_spec() {
let path = PathBuf::from("examples/petstore.yaml");
let result = load_openapi_spec(&path);
assert!(result.is_ok());
let spec = result.unwrap();
assert_eq!(spec.info.title, "Pet Store API");
assert_eq!(spec.info.version, "1.0.0");
}
#[test]
fn test_load_nonexistent_file() {
let path = PathBuf::from("nonexistent.yaml");
let result = load_openapi_spec(&path);
assert!(result.is_err());
match result.unwrap_err() {
CoreError::SpecNotFound { .. } => (),
_ => panic!("Expected SpecNotFound error"),
}
}
#[test]
fn test_find_operation_exists() {
let path = PathBuf::from("examples/petstore.yaml");
let spec = load_openapi_spec(&path).unwrap();
let result = find_operation(&spec, "getPetById");
assert!(result.is_ok());
let operation = result.unwrap();
assert_eq!(operation.operation_id.as_deref(), Some("getPetById"));
}
#[test]
fn test_find_operation_not_exists() {
let path = PathBuf::from("examples/petstore.yaml");
let spec = load_openapi_spec(&path).unwrap();
let result = find_operation(&spec, "nonexistentOperation");
assert!(result.is_err());
match result.unwrap_err() {
CoreError::OperationNotFound { available, .. } => {
assert!(available.contains(&"getPetById".to_string()));
}
_ => panic!("Expected OperationNotFound error"),
}
}
#[test]
fn test_list_operations() {
let path = PathBuf::from("examples/petstore.yaml");
let spec = load_openapi_spec(&path).unwrap();
let operations = list_operations(&spec);
assert!(operations.contains(&"getPetById".to_string()));
assert!(operations.contains(&"addPet".to_string()));
assert!(operations.contains(&"findPetsByStatus".to_string()));
}
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct UnifiedSpec {
pub info: ApiInfo,
pub servers: Vec<Server>,
pub paths: HashMap<String, PathItem>,
pub security_schemes: HashMap<String, UnifiedSecurityScheme>,
pub security: Option<Vec<HashMap<String, Vec<String>>>>,
}
#[derive(Debug, Clone)]
pub struct ApiInfo {
#[allow(dead_code)]
pub title: String,
#[allow(dead_code)]
pub version: String,
#[allow(dead_code)]
pub description: Option<String>,
}
#[derive(Debug, Clone)]
pub struct Server {
pub url: String,
#[allow(dead_code)]
pub description: Option<String>,
}
#[derive(Debug, Clone)]
pub struct PathItem {
pub operations: HashMap<String, UnifiedOperation>,
}
#[derive(Debug, Clone)]
pub struct UnifiedOperation {
pub operation_id: Option<String>,
#[allow(dead_code)]
pub summary: Option<String>,
#[allow(dead_code)]
pub description: Option<String>,
pub security: Option<Vec<HashMap<String, Vec<String>>>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UnifiedSecurityScheme {
#[serde(rename = "type")]
pub scheme_type: String,
pub description: Option<String>,
pub name: Option<String>,
#[serde(rename = "in")]
pub location: Option<String>,
pub scheme: Option<String>,
pub bearer_format: Option<String>,
pub flows: Option<Value>,
pub openid_connect_url: Option<String>,
pub flow: Option<String>,
pub authorization_url: Option<String>,
pub token_url: Option<String>,
pub refresh_url: Option<String>,
pub scopes: Option<HashMap<String, String>>,
}
impl UnifiedSpec {
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
let content =
std::fs::read_to_string(path.as_ref()).context("Failed to read specification file")?;
let value: Value = if path
.as_ref()
.extension()
.and_then(|s| s.to_str())
.map(|s| s.ends_with("json"))
.unwrap_or(false)
{
serde_json::from_str(&content)?
} else {
serde_yaml::from_str(&content)?
};
Self::from_value(value)
}
pub fn from_value(value: Value) -> Result<Self> {
let is_openapi_3 = value.get("openapi").is_some();
let info = Self::parse_info(&value)?;
let servers = Self::parse_servers(&value, is_openapi_3)?;
let paths = Self::parse_paths(&value)?;
let security_schemes = Self::parse_security_schemes(&value, is_openapi_3)?;
let security = Self::parse_security(&value)?;
Ok(Self {
info,
servers,
paths,
security_schemes,
security,
})
}
fn parse_info(value: &Value) -> Result<ApiInfo> {
let info = value.get("info").context("Missing 'info' field")?;
Ok(ApiInfo {
title: info
.get("title")
.and_then(|v| v.as_str())
.unwrap_or("API")
.to_string(),
version: info
.get("version")
.and_then(|v| v.as_str())
.unwrap_or("1.0.0")
.to_string(),
description: info
.get("description")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
})
}
fn parse_servers(value: &Value, is_openapi_3: bool) -> Result<Vec<Server>> {
if is_openapi_3 {
if let Some(servers) = value.get("servers").and_then(|v| v.as_array()) {
return Ok(servers
.iter()
.filter_map(|s| {
s.get("url").and_then(|u| u.as_str()).map(|url| Server {
url: url.to_string(),
description: s
.get("description")
.and_then(|d| d.as_str())
.map(|s| s.to_string()),
})
})
.collect());
}
} else {
let mut url = String::new();
if let Some(schemes) = value.get("schemes").and_then(|v| v.as_array()) {
if let Some(scheme) = schemes.get(0).and_then(|s| s.as_str()) {
url.push_str(scheme);
url.push_str("://");
}
} else {
url.push_str("https://");
}
if let Some(host) = value.get("host").and_then(|v| v.as_str()) {
url.push_str(host);
} else {
url.push_str("localhost");
}
if let Some(base_path) = value.get("basePath").and_then(|v| v.as_str()) {
if !base_path.starts_with('/') {
url.push('/');
}
url.push_str(base_path);
}
return Ok(vec![Server {
url,
description: None,
}]);
}
Ok(vec![Server {
url: "http://localhost".to_string(),
description: None,
}])
}
fn parse_paths(value: &Value) -> Result<HashMap<String, PathItem>> {
let mut paths = HashMap::new();
if let Some(paths_obj) = value.get("paths").and_then(|v| v.as_object()) {
for (path, path_value) in paths_obj {
let mut operations = HashMap::new();
if let Some(path_obj) = path_value.as_object() {
for (method, op_value) in path_obj {
if ["get", "post", "put", "delete", "patch", "head", "options"]
.contains(&method.as_str())
{
if let Ok(operation) = Self::parse_operation(op_value) {
operations.insert(method.clone(), operation);
}
}
}
}
if !operations.is_empty() {
paths.insert(path.clone(), PathItem { operations });
}
}
}
Ok(paths)
}
fn parse_operation(value: &Value) -> Result<UnifiedOperation> {
Ok(UnifiedOperation {
operation_id: value
.get("operationId")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
summary: value
.get("summary")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
description: value
.get("description")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
security: Self::parse_security(value)?,
})
}
fn parse_security(value: &Value) -> Result<Option<Vec<HashMap<String, Vec<String>>>>> {
if let Some(security) = value.get("security").and_then(|v| v.as_array()) {
let mut result = Vec::new();
for item in security {
if let Some(obj) = item.as_object() {
let mut requirement = HashMap::new();
for (scheme, scopes) in obj {
let scope_vec = if let Some(arr) = scopes.as_array() {
arr.iter()
.filter_map(|s| s.as_str().map(|s| s.to_string()))
.collect()
} else {
Vec::new()
};
requirement.insert(scheme.clone(), scope_vec);
}
result.push(requirement);
}
}
if !result.is_empty() {
return Ok(Some(result));
}
}
Ok(None)
}
fn parse_security_schemes(
value: &Value,
is_openapi_3: bool,
) -> Result<HashMap<String, UnifiedSecurityScheme>> {
let mut schemes = HashMap::new();
let security_defs = if is_openapi_3 {
value
.get("components")
.and_then(|c| c.get("securitySchemes"))
} else {
value.get("securityDefinitions")
};
if let Some(defs) = security_defs.and_then(|v| v.as_object()) {
for (name, scheme_value) in defs {
if let Ok(scheme) =
serde_json::from_value::<UnifiedSecurityScheme>(scheme_value.clone())
{
schemes.insert(name.clone(), scheme);
}
}
}
Ok(schemes)
}
}