use serde_json::Value;
use std::collections::HashMap;
pub struct OpenApiValidator {
v3_spec: Option<Value>,
v4_spec: Option<Value>,
#[allow(dead_code)]
strict_mode: bool,
#[allow(dead_code)]
allow_extra_fields: bool,
}
#[derive(Debug, Default)]
pub struct ValidationReport {
pub version: String,
pub compliant: Vec<String>,
pub warnings: HashMap<String, Vec<String>>,
pub errors: HashMap<String, Vec<String>>,
pub missing_implementations: Vec<String>,
pub extra_implementations: Vec<String>,
}
impl OpenApiValidator {
pub async fn new(
strict_mode: bool,
allow_extra_fields: bool,
) -> Result<Self, Box<dyn std::error::Error>> {
Ok(Self {
v3_spec: None, v4_spec: None,
strict_mode,
allow_extra_fields,
})
}
pub fn has_v3_spec(&self) -> bool {
self.v3_spec.is_some()
}
pub fn has_v4_spec(&self) -> bool {
self.v4_spec.is_some()
}
pub async fn load_v3_from_mcp(&mut self) -> Result<(), Box<dyn std::error::Error>> {
println!(" [OpenAPI] 从 MCP 加载 V3 规范...");
self.v3_spec = Some(serde_json::json!({}));
Ok(())
}
pub async fn load_v4_from_mcp(&mut self) -> Result<(), Box<dyn std::error::Error>> {
println!(" [OpenAPI] 从 MCP 加载 V4 规范...");
self.v4_spec = Some(serde_json::json!({}));
Ok(())
}
pub async fn load_v3_from_cache(&mut self) -> Result<(), Box<dyn std::error::Error>> {
let cache_path = "tests/openapi/v3_spec_cache.json";
match tokio::fs::read_to_string(cache_path).await {
Ok(content) => {
self.v3_spec = Some(serde_json::from_str(&content)?);
println!(" [OpenAPI] 从缓存加载 V3 规范");
Ok(())
}
Err(e) => {
println!(" [OpenAPI] V3 缓存不存在: {}", e);
Ok(())
}
}
}
pub async fn load_v4_from_cache(&mut self) -> Result<(), Box<dyn std::error::Error>> {
let cache_path = "tests/openapi/v4_spec_cache.json";
match tokio::fs::read_to_string(cache_path).await {
Ok(content) => {
self.v4_spec = Some(serde_json::from_str(&content)?);
println!(" [OpenAPI] 从缓存加载 V4 规范");
Ok(())
}
Err(e) => {
println!(" [OpenAPI] V4 缓存不存在: {}", e);
Ok(())
}
}
}
pub fn validate_v3(&self) -> ValidationReport {
let mut report = ValidationReport {
version: "V3".to_string(),
..Default::default()
};
if self.v3_spec.is_none() {
println!(" [OpenAPI] V3 规范未加载,跳过验证");
return report;
}
let spec_endpoints = self.extract_endpoints(self.v3_spec.as_ref().unwrap());
let impl_endpoints = self.get_v3_implemented_endpoints();
for endpoint in &spec_endpoints {
if impl_endpoints.contains(endpoint) {
report.compliant.push(endpoint.clone());
} else {
report.missing_implementations.push(endpoint.clone());
}
}
for endpoint in &impl_endpoints {
if !spec_endpoints.contains(endpoint) {
report.extra_implementations.push(endpoint.clone());
}
}
report
}
pub fn validate_v4(&self) -> ValidationReport {
let mut report = ValidationReport {
version: "V4".to_string(),
..Default::default()
};
if self.v4_spec.is_none() {
println!(" [OpenAPI] V4 规范未加载,跳过验证");
return report;
}
let spec_endpoints = self.extract_endpoints(self.v4_spec.as_ref().unwrap());
let impl_endpoints = self.get_v4_implemented_endpoints();
for endpoint in &spec_endpoints {
if impl_endpoints.contains(endpoint) {
report.compliant.push(endpoint.clone());
} else {
report.missing_implementations.push(endpoint.clone());
}
}
for endpoint in &impl_endpoints {
if !spec_endpoints.contains(endpoint) {
report.extra_implementations.push(endpoint.clone());
}
}
report
}
fn extract_endpoints(&self, spec: &Value) -> Vec<String> {
let mut endpoints = Vec::new();
if let Some(paths) = spec.get("paths").and_then(|p| p.as_object()) {
for (path, path_spec) in paths {
if let Some(methods) = path_spec.as_object() {
for (method, _) in methods {
if method != "$ref" {
endpoints.push(format!("{} {}", method.to_uppercase(), path));
}
}
}
}
}
endpoints
}
fn get_v3_implemented_endpoints(&self) -> Vec<String> {
vec![
"POST /user/session".to_string(),
"POST /user/2fa".to_string(),
"DELETE /user/session".to_string(),
"PUT /file/upload".to_string(),
"POST /file/upload/{sessionID}/{index}".to_string(),
"PUT /file/download/{id}".to_string(),
"POST /file/source".to_string(),
"GET /file/preview/{id}".to_string(),
"GET /file/thumb/{id}".to_string(),
"POST /file/create".to_string(),
"GET /directory{path}".to_string(),
"PUT /directory".to_string(),
"GET /object/property/{id}".to_string(),
"POST /object/rename".to_string(),
"PATCH /object".to_string(),
"DELETE /object".to_string(),
"POST /object/copy".to_string(),
"POST /share".to_string(),
"GET /site/config".to_string(),
"GET /site/ping".to_string(),
"GET /user/setting".to_string(),
"GET /user/storage".to_string(),
"GET /user/avatar/{id}/l".to_string(),
"GET /webdav/accounts".to_string(),
"POST /aria2/url".to_string(),
"GET /aria2/downloading".to_string(),
"GET /aria2/finished".to_string(),
"DELETE /aria2/task/{gid}".to_string(),
"GET /user/setting/tasks".to_string(),
]
}
fn get_v4_implemented_endpoints(&self) -> Vec<String> {
vec![
"GET /session/prepare".to_string(),
"POST /session/token".to_string(),
"POST /session/token/2fa".to_string(),
"POST /session/token/refresh".to_string(),
"DELETE /session/token".to_string(),
"PUT /session/openid".to_string(),
"POST /session/openid".to_string(),
"PUT /session/authn".to_string(),
"POST /session/authn".to_string(),
"GET /file".to_string(),
"DELETE /file/{uri}".to_string(),
"POST /file/create".to_string(),
"PUT /file/rename/{uri}".to_string(),
"POST /file/move".to_string(),
"POST /file/copy".to_string(),
"POST /file/url".to_string(),
"POST /file/restore".to_string(),
"GET /file/{uri}".to_string(),
"GET /file/stat/{uri}".to_string(),
"GET /file/thumb".to_string(),
"PUT /file/content".to_string(),
"PUT /file/upload".to_string(),
"POST /file/upload/{sessionId}/{index}".to_string(),
"DELETE /file/upload".to_string(),
"POST /file/permission".to_string(),
"DELETE /file/permission".to_string(),
"PATCH /file/metadata".to_string(),
"PATCH /file/policy".to_string(),
"PATCH /file/view".to_string(),
"GET /file/activities".to_string(),
"GET /file/archive".to_string(),
"PUT /file/viewerSession".to_string(),
"POST /user".to_string(),
"GET /user/capacity".to_string(),
"GET /user/search".to_string(),
"GET /user/info/{user_id}".to_string(),
"GET /user/avatar/{user_id}".to_string(),
"GET /user/shares/{user_id}".to_string(),
"GET /user/setting".to_string(),
"PATCH /user/setting".to_string(),
"GET /user/setting/policies".to_string(),
"GET /user/setting/nodes".to_string(),
"GET /user/setting/2fa".to_string(),
"PUT /user/setting/avatar".to_string(),
"PUT /user/authn".to_string(),
"POST /user/authn".to_string(),
"DELETE /user/authn".to_string(),
"PUT /share".to_string(),
"GET /share".to_string(),
"POST /share/{id}".to_string(),
"DELETE /share/{id}".to_string(),
"GET /share/info/{id}".to_string(),
"GET /devices/dav".to_string(),
"PUT /devices/dav".to_string(),
"PATCH /devices/dav/{id}".to_string(),
"DELETE /devices/dav/{id}".to_string(),
"POST /workflow/download".to_string(),
"PATCH /workflow/download/{task_id}".to_string(),
"DELETE /workflow/download/{task_id}".to_string(),
"GET /workflow".to_string(),
"GET /workflow/progress/{id}".to_string(),
"POST /workflow/archive".to_string(),
"POST /workflow/extract".to_string(),
"GET /site/ping".to_string(),
"GET /site/version".to_string(),
"GET /site/config/{section}".to_string(),
"POST /site/abuse".to_string(),
"GET /site/captcha".to_string(),
]
}
}
impl ValidationReport {
pub fn print(&self) {
println!(
"\n┌─ {} OpenAPI 验证报告 ───────────────────────────",
self.version
);
println!("│ 符合规范: {}", self.compliant.len());
println!("│ 警告: {}", self.warnings.len());
println!("│ 错误: {}", self.errors.len());
println!("│ 缺失实现: {}", self.missing_implementations.len());
println!("│ 额外实现: {}", self.extra_implementations.len());
if !self.missing_implementations.is_empty() {
println!("│");
println!("│ 缺失的端点实现:");
for endpoint in &self.missing_implementations {
println!("│ - {}", endpoint);
}
}
if !self.extra_implementations.is_empty() {
println!("│");
println!("│ 额外的端点实现:");
for endpoint in &self.extra_implementations {
println!("│ - {}", endpoint);
}
}
println!("└─────────────────────────────────────────────────");
}
pub fn has_errors(&self) -> bool {
!self.errors.is_empty() || !self.missing_implementations.is_empty()
}
#[allow(dead_code)]
pub fn has_warnings(&self) -> bool {
!self.warnings.is_empty() || !self.extra_implementations.is_empty()
}
}