use crate::api::response::AnyJson;
use crate::api::{ApiError, ApiResponse};
use crate::errors::catalog::{ErrorCategory, ErrorCode};
use schemars::schema::RootSchema;
use schemars::schema_for;
use serde::{Deserialize, Serialize};
#[must_use]
pub fn generate_api_response_schema() -> RootSchema {
schema_for!(ApiResponse<AnyJson>)
}
#[must_use]
pub fn generate_api_error_schema() -> RootSchema {
schema_for!(ApiError)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorCodeEntry {
pub code: String,
pub number: u16,
pub category: ErrorCategory,
pub message: String,
pub remediation: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub doc_url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorCategoryEntry {
pub id: String,
pub name: String,
pub description: String,
pub code_range: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorCatalog {
pub schema_version: String,
pub api_version: String,
pub categories: Vec<ErrorCategoryEntry>,
pub errors: Vec<ErrorCodeEntry>,
}
#[must_use]
pub fn generate_error_catalog() -> ErrorCatalog {
let categories = vec![
ErrorCategoryEntry {
id: "config".to_string(),
name: ErrorCategory::Config.name().to_string(),
description: ErrorCategory::Config.description().to_string(),
code_range: "001-099".to_string(),
},
ErrorCategoryEntry {
id: "network".to_string(),
name: ErrorCategory::Network.name().to_string(),
description: ErrorCategory::Network.description().to_string(),
code_range: "100-199".to_string(),
},
ErrorCategoryEntry {
id: "worker".to_string(),
name: ErrorCategory::Worker.name().to_string(),
description: ErrorCategory::Worker.description().to_string(),
code_range: "200-299".to_string(),
},
ErrorCategoryEntry {
id: "build".to_string(),
name: ErrorCategory::Build.name().to_string(),
description: ErrorCategory::Build.description().to_string(),
code_range: "300-399".to_string(),
},
ErrorCategoryEntry {
id: "transfer".to_string(),
name: ErrorCategory::Transfer.name().to_string(),
description: ErrorCategory::Transfer.description().to_string(),
code_range: "400-499".to_string(),
},
ErrorCategoryEntry {
id: "internal".to_string(),
name: ErrorCategory::Internal.name().to_string(),
description: ErrorCategory::Internal.description().to_string(),
code_range: "500-599".to_string(),
},
];
let errors: Vec<ErrorCodeEntry> = ErrorCode::all()
.iter()
.map(|code| ErrorCodeEntry {
code: code.code_string(),
number: code.code_number(),
category: code.category(),
message: code.message().to_string(),
remediation: code
.remediation()
.iter()
.map(|s| (*s).to_string())
.collect(),
doc_url: code.doc_url().map(String::from),
})
.collect();
ErrorCatalog {
schema_version: "1.0".to_string(),
api_version: crate::api::API_VERSION.to_string(),
categories,
errors,
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SchemaExportResult {
pub files_generated: usize,
pub files: Vec<String>,
pub output_dir: String,
}
pub fn export_schemas(output_dir: &std::path::Path) -> std::io::Result<SchemaExportResult> {
use std::fs;
fs::create_dir_all(output_dir)?;
let mut files = Vec::new();
let api_response_schema = generate_api_response_schema();
let api_response_path = output_dir.join("api-response.schema.json");
fs::write(
&api_response_path,
serde_json::to_string_pretty(&api_response_schema)?,
)?;
files.push(api_response_path.display().to_string());
let api_error_schema = generate_api_error_schema();
let api_error_path = output_dir.join("api-error.schema.json");
fs::write(
&api_error_path,
serde_json::to_string_pretty(&api_error_schema)?,
)?;
files.push(api_error_path.display().to_string());
let error_catalog = generate_error_catalog();
let error_codes_path = output_dir.join("error-codes.json");
fs::write(
&error_codes_path,
serde_json::to_string_pretty(&error_catalog)?,
)?;
files.push(error_codes_path.display().to_string());
Ok(SchemaExportResult {
files_generated: files.len(),
files,
output_dir: output_dir.display().to_string(),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_api_response_schema() {
let schema = generate_api_response_schema();
let json = serde_json::to_string(&schema).unwrap();
assert!(json.contains("api_version"));
assert!(json.contains("success"));
assert!(json.contains("timestamp"));
}
#[test]
fn test_generate_api_error_schema() {
let schema = generate_api_error_schema();
let json = serde_json::to_string(&schema).unwrap();
assert!(json.contains("code"));
assert!(json.contains("category"));
assert!(json.contains("message"));
assert!(json.contains("remediation"));
}
#[test]
fn test_generate_error_catalog() {
let catalog = generate_error_catalog();
assert_eq!(catalog.schema_version, "1.0");
assert_eq!(catalog.api_version, crate::api::API_VERSION);
assert_eq!(catalog.categories.len(), 6);
assert_eq!(catalog.errors.len(), ErrorCode::all().len());
let first = &catalog.errors[0];
assert_eq!(first.code, "RCH-E001");
assert_eq!(first.number, 1);
assert_eq!(first.category, ErrorCategory::Config);
assert!(!first.remediation.is_empty());
}
#[test]
fn test_error_catalog_serialization() {
let catalog = generate_error_catalog();
let json = serde_json::to_string_pretty(&catalog).unwrap();
assert!(json.contains("\"schema_version\""));
assert!(json.contains("\"categories\""));
assert!(json.contains("\"errors\""));
assert!(json.contains("RCH-E001"));
assert!(json.contains("RCH-E100"));
assert!(json.contains("RCH-E500"));
}
#[test]
fn test_category_entries() {
let catalog = generate_error_catalog();
let config_cat = catalog
.categories
.iter()
.find(|c| c.id == "config")
.unwrap();
assert_eq!(config_cat.code_range, "001-099");
let network_cat = catalog
.categories
.iter()
.find(|c| c.id == "network")
.unwrap();
assert_eq!(network_cat.code_range, "100-199");
let internal_cat = catalog
.categories
.iter()
.find(|c| c.id == "internal")
.unwrap();
assert_eq!(internal_cat.code_range, "500-599");
}
#[test]
fn test_export_schemas_to_temp_dir() {
let temp_dir = std::env::temp_dir().join("rch-schema-test");
let _ = std::fs::remove_dir_all(&temp_dir);
let result = export_schemas(&temp_dir).unwrap();
assert_eq!(result.files_generated, 3);
assert!(result.files.iter().any(|f| f.contains("api-response")));
assert!(result.files.iter().any(|f| f.contains("api-error")));
assert!(result.files.iter().any(|f| f.contains("error-codes")));
for file in &result.files {
let content = std::fs::read_to_string(file).unwrap();
let _: serde_json::Value = serde_json::from_str(&content).unwrap();
}
let _ = std::fs::remove_dir_all(&temp_dir);
}
}