1use crate::api::response::AnyJson;
27use crate::api::{ApiError, ApiResponse};
28use crate::errors::catalog::{ErrorCategory, ErrorCode};
29use schemars::schema::RootSchema;
30use schemars::schema_for;
31use serde::{Deserialize, Serialize};
32
33#[must_use]
38pub fn generate_api_response_schema() -> RootSchema {
39 schema_for!(ApiResponse<AnyJson>)
40}
41
42#[must_use]
44pub fn generate_api_error_schema() -> RootSchema {
45 schema_for!(ApiError)
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct ErrorCodeEntry {
51 pub code: String,
53 pub number: u16,
55 pub category: ErrorCategory,
57 pub message: String,
59 pub remediation: Vec<String>,
61 #[serde(skip_serializing_if = "Option::is_none")]
63 pub doc_url: Option<String>,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct ErrorCategoryEntry {
69 pub id: String,
71 pub name: String,
73 pub description: String,
75 pub code_range: String,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct ErrorCatalog {
82 pub schema_version: String,
84 pub api_version: String,
86 pub categories: Vec<ErrorCategoryEntry>,
88 pub errors: Vec<ErrorCodeEntry>,
90}
91
92#[must_use]
94pub fn generate_error_catalog() -> ErrorCatalog {
95 let categories = vec![
96 ErrorCategoryEntry {
97 id: "config".to_string(),
98 name: ErrorCategory::Config.name().to_string(),
99 description: ErrorCategory::Config.description().to_string(),
100 code_range: "001-099".to_string(),
101 },
102 ErrorCategoryEntry {
103 id: "network".to_string(),
104 name: ErrorCategory::Network.name().to_string(),
105 description: ErrorCategory::Network.description().to_string(),
106 code_range: "100-199".to_string(),
107 },
108 ErrorCategoryEntry {
109 id: "worker".to_string(),
110 name: ErrorCategory::Worker.name().to_string(),
111 description: ErrorCategory::Worker.description().to_string(),
112 code_range: "200-299".to_string(),
113 },
114 ErrorCategoryEntry {
115 id: "build".to_string(),
116 name: ErrorCategory::Build.name().to_string(),
117 description: ErrorCategory::Build.description().to_string(),
118 code_range: "300-399".to_string(),
119 },
120 ErrorCategoryEntry {
121 id: "transfer".to_string(),
122 name: ErrorCategory::Transfer.name().to_string(),
123 description: ErrorCategory::Transfer.description().to_string(),
124 code_range: "400-499".to_string(),
125 },
126 ErrorCategoryEntry {
127 id: "internal".to_string(),
128 name: ErrorCategory::Internal.name().to_string(),
129 description: ErrorCategory::Internal.description().to_string(),
130 code_range: "500-599".to_string(),
131 },
132 ];
133
134 let errors: Vec<ErrorCodeEntry> = ErrorCode::all()
135 .iter()
136 .map(|code| ErrorCodeEntry {
137 code: code.code_string(),
138 number: code.code_number(),
139 category: code.category(),
140 message: code.message().to_string(),
141 remediation: code
142 .remediation()
143 .iter()
144 .map(|s| (*s).to_string())
145 .collect(),
146 doc_url: code.doc_url().map(String::from),
147 })
148 .collect();
149
150 ErrorCatalog {
151 schema_version: "1.0".to_string(),
152 api_version: crate::api::API_VERSION.to_string(),
153 categories,
154 errors,
155 }
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct SchemaExportResult {
161 pub files_generated: usize,
163 pub files: Vec<String>,
165 pub output_dir: String,
167}
168
169pub fn export_schemas(output_dir: &std::path::Path) -> std::io::Result<SchemaExportResult> {
183 use std::fs;
184
185 fs::create_dir_all(output_dir)?;
187
188 let mut files = Vec::new();
189
190 let api_response_schema = generate_api_response_schema();
192 let api_response_path = output_dir.join("api-response.schema.json");
193 fs::write(
194 &api_response_path,
195 serde_json::to_string_pretty(&api_response_schema)?,
196 )?;
197 files.push(api_response_path.display().to_string());
198
199 let api_error_schema = generate_api_error_schema();
201 let api_error_path = output_dir.join("api-error.schema.json");
202 fs::write(
203 &api_error_path,
204 serde_json::to_string_pretty(&api_error_schema)?,
205 )?;
206 files.push(api_error_path.display().to_string());
207
208 let error_catalog = generate_error_catalog();
210 let error_codes_path = output_dir.join("error-codes.json");
211 fs::write(
212 &error_codes_path,
213 serde_json::to_string_pretty(&error_catalog)?,
214 )?;
215 files.push(error_codes_path.display().to_string());
216
217 Ok(SchemaExportResult {
218 files_generated: files.len(),
219 files,
220 output_dir: output_dir.display().to_string(),
221 })
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227
228 #[test]
229 fn test_generate_api_response_schema() {
230 let schema = generate_api_response_schema();
231 let json = serde_json::to_string(&schema).unwrap();
232
233 assert!(json.contains("api_version"));
235 assert!(json.contains("success"));
236 assert!(json.contains("timestamp"));
237 }
238
239 #[test]
240 fn test_generate_api_error_schema() {
241 let schema = generate_api_error_schema();
242 let json = serde_json::to_string(&schema).unwrap();
243
244 assert!(json.contains("code"));
246 assert!(json.contains("category"));
247 assert!(json.contains("message"));
248 assert!(json.contains("remediation"));
249 }
250
251 #[test]
252 fn test_generate_error_catalog() {
253 let catalog = generate_error_catalog();
254
255 assert_eq!(catalog.schema_version, "1.0");
257 assert_eq!(catalog.api_version, crate::api::API_VERSION);
258 assert_eq!(catalog.categories.len(), 6);
259
260 assert_eq!(catalog.errors.len(), ErrorCode::all().len());
262
263 let first = &catalog.errors[0];
265 assert_eq!(first.code, "RCH-E001");
266 assert_eq!(first.number, 1);
267 assert_eq!(first.category, ErrorCategory::Config);
268 assert!(!first.remediation.is_empty());
269 }
270
271 #[test]
272 fn test_error_catalog_serialization() {
273 let catalog = generate_error_catalog();
274 let json = serde_json::to_string_pretty(&catalog).unwrap();
275
276 assert!(json.contains("\"schema_version\""));
278 assert!(json.contains("\"categories\""));
279 assert!(json.contains("\"errors\""));
280 assert!(json.contains("RCH-E001"));
281 assert!(json.contains("RCH-E100"));
282 assert!(json.contains("RCH-E500"));
283 }
284
285 #[test]
286 fn test_category_entries() {
287 let catalog = generate_error_catalog();
288
289 let config_cat = catalog
291 .categories
292 .iter()
293 .find(|c| c.id == "config")
294 .unwrap();
295 assert_eq!(config_cat.code_range, "001-099");
296
297 let network_cat = catalog
298 .categories
299 .iter()
300 .find(|c| c.id == "network")
301 .unwrap();
302 assert_eq!(network_cat.code_range, "100-199");
303
304 let internal_cat = catalog
305 .categories
306 .iter()
307 .find(|c| c.id == "internal")
308 .unwrap();
309 assert_eq!(internal_cat.code_range, "500-599");
310 }
311
312 #[test]
313 fn test_export_schemas_to_temp_dir() {
314 let temp_dir = std::env::temp_dir().join("rch-schema-test");
315 let _ = std::fs::remove_dir_all(&temp_dir); let result = export_schemas(&temp_dir).unwrap();
318
319 assert_eq!(result.files_generated, 3);
320 assert!(result.files.iter().any(|f| f.contains("api-response")));
321 assert!(result.files.iter().any(|f| f.contains("api-error")));
322 assert!(result.files.iter().any(|f| f.contains("error-codes")));
323
324 for file in &result.files {
326 let content = std::fs::read_to_string(file).unwrap();
327 let _: serde_json::Value = serde_json::from_str(&content).unwrap();
328 }
329
330 let _ = std::fs::remove_dir_all(&temp_dir);
332 }
333}