1use crate::api::response::AnyJson;
27use crate::api::{ApiError, ApiResponse};
28use crate::errors::catalog::{ErrorCategory, ErrorCode};
29use schemars::Schema;
30use schemars::generate::SchemaSettings;
31use serde::{Deserialize, Serialize};
32
33#[must_use]
38pub fn generate_api_response_schema() -> Schema {
39 SchemaSettings::draft07()
40 .into_generator()
41 .into_root_schema_for::<ApiResponse<AnyJson>>()
42}
43
44#[must_use]
46pub fn generate_api_error_schema() -> Schema {
47 SchemaSettings::draft07()
48 .into_generator()
49 .into_root_schema_for::<ApiError>()
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct ErrorCodeEntry {
55 pub code: String,
57 pub number: u16,
59 pub category: ErrorCategory,
61 pub message: String,
63 pub remediation: Vec<String>,
65 #[serde(skip_serializing_if = "Option::is_none")]
67 pub doc_url: Option<String>,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct ErrorCategoryEntry {
73 pub id: String,
75 pub name: String,
77 pub description: String,
79 pub code_range: String,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct ErrorCatalog {
86 pub schema_version: String,
88 pub api_version: String,
90 pub categories: Vec<ErrorCategoryEntry>,
92 pub errors: Vec<ErrorCodeEntry>,
94}
95
96#[must_use]
98pub fn generate_error_catalog() -> ErrorCatalog {
99 let categories = vec![
100 ErrorCategoryEntry {
101 id: "config".to_string(),
102 name: ErrorCategory::Config.name().to_string(),
103 description: ErrorCategory::Config.description().to_string(),
104 code_range: "001-099".to_string(),
105 },
106 ErrorCategoryEntry {
107 id: "network".to_string(),
108 name: ErrorCategory::Network.name().to_string(),
109 description: ErrorCategory::Network.description().to_string(),
110 code_range: "100-199".to_string(),
111 },
112 ErrorCategoryEntry {
113 id: "worker".to_string(),
114 name: ErrorCategory::Worker.name().to_string(),
115 description: ErrorCategory::Worker.description().to_string(),
116 code_range: "200-299".to_string(),
117 },
118 ErrorCategoryEntry {
119 id: "build".to_string(),
120 name: ErrorCategory::Build.name().to_string(),
121 description: ErrorCategory::Build.description().to_string(),
122 code_range: "300-399".to_string(),
123 },
124 ErrorCategoryEntry {
125 id: "transfer".to_string(),
126 name: ErrorCategory::Transfer.name().to_string(),
127 description: ErrorCategory::Transfer.description().to_string(),
128 code_range: "400-499".to_string(),
129 },
130 ErrorCategoryEntry {
131 id: "internal".to_string(),
132 name: ErrorCategory::Internal.name().to_string(),
133 description: ErrorCategory::Internal.description().to_string(),
134 code_range: "500-599".to_string(),
135 },
136 ];
137
138 let errors: Vec<ErrorCodeEntry> = ErrorCode::all()
139 .iter()
140 .map(|code| ErrorCodeEntry {
141 code: code.code_string(),
142 number: code.code_number(),
143 category: code.category(),
144 message: code.message().to_string(),
145 remediation: code
146 .remediation()
147 .iter()
148 .map(|s| (*s).to_string())
149 .collect(),
150 doc_url: code.doc_url().map(String::from),
151 })
152 .collect();
153
154 ErrorCatalog {
155 schema_version: "1.0".to_string(),
156 api_version: crate::api::API_VERSION.to_string(),
157 categories,
158 errors,
159 }
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct SchemaExportResult {
165 pub files_generated: usize,
167 pub files: Vec<String>,
169 pub output_dir: String,
171}
172
173pub fn export_schemas(output_dir: &std::path::Path) -> std::io::Result<SchemaExportResult> {
187 use std::fs;
188
189 fs::create_dir_all(output_dir)?;
191
192 let mut files = Vec::new();
193
194 let api_response_schema = generate_api_response_schema();
196 let api_response_path = output_dir.join("api-response.schema.json");
197 fs::write(
198 &api_response_path,
199 serde_json::to_string_pretty(&api_response_schema)?,
200 )?;
201 files.push(api_response_path.display().to_string());
202
203 let api_error_schema = generate_api_error_schema();
205 let api_error_path = output_dir.join("api-error.schema.json");
206 fs::write(
207 &api_error_path,
208 serde_json::to_string_pretty(&api_error_schema)?,
209 )?;
210 files.push(api_error_path.display().to_string());
211
212 let error_catalog = generate_error_catalog();
214 let error_codes_path = output_dir.join("error-codes.json");
215 fs::write(
216 &error_codes_path,
217 serde_json::to_string_pretty(&error_catalog)?,
218 )?;
219 files.push(error_codes_path.display().to_string());
220
221 Ok(SchemaExportResult {
222 files_generated: files.len(),
223 files,
224 output_dir: output_dir.display().to_string(),
225 })
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231
232 #[test]
233 fn test_exported_contract_schemas_use_draft_7() {
234 let schemas = [
235 generate_api_response_schema(),
236 generate_api_error_schema(),
237 crate::proof_intent::proof_intent_schema(),
238 crate::bypass_record::bypass_record_schema(),
239 crate::worker_facts::worker_facts_schema(),
240 crate::repo_updater_contract::repo_updater_request_schema(),
241 crate::repo_updater_contract::repo_updater_response_schema(),
242 crate::repo_updater_contract::repo_updater_envelope_schema(),
243 crate::e2e::process_triage::process_triage_request_schema(),
244 crate::e2e::process_triage::process_triage_response_schema(),
245 ];
246 let values = schemas
247 .into_iter()
248 .map(|schema| serde_json::to_value(schema).unwrap())
249 .chain([
250 crate::redaction::RedactionPolicy::schema_json(),
251 crate::remediation_config::RemediationConfig::schema_json(),
252 ]);
253 for schema in values {
254 assert_eq!(schema["$schema"], "http://json-schema.org/draft-07/schema#");
255 assert!(schema.get("$defs").is_none());
256 }
257 }
258
259 #[test]
260 fn test_generate_api_response_schema() {
261 let schema = generate_api_response_schema();
262 let json = serde_json::to_string(&schema).unwrap();
263
264 assert!(json.contains("api_version"));
266 assert!(json.contains("success"));
267 assert!(json.contains("timestamp"));
268 }
269
270 #[test]
271 fn test_generate_api_error_schema() {
272 let schema = generate_api_error_schema();
273 let json = serde_json::to_string(&schema).unwrap();
274
275 assert!(json.contains("code"));
277 assert!(json.contains("category"));
278 assert!(json.contains("message"));
279 assert!(json.contains("remediation"));
280 }
281
282 #[test]
283 fn test_generate_error_catalog() {
284 let catalog = generate_error_catalog();
285
286 assert_eq!(catalog.schema_version, "1.0");
288 assert_eq!(catalog.api_version, crate::api::API_VERSION);
289 assert_eq!(catalog.categories.len(), 6);
290
291 assert_eq!(catalog.errors.len(), ErrorCode::all().len());
293
294 let first = &catalog.errors[0];
296 assert_eq!(first.code, "RCH-E001");
297 assert_eq!(first.number, 1);
298 assert_eq!(first.category, ErrorCategory::Config);
299 assert!(!first.remediation.is_empty());
300 }
301
302 #[test]
303 fn test_error_catalog_serialization() {
304 let catalog = generate_error_catalog();
305 let json = serde_json::to_string_pretty(&catalog).unwrap();
306
307 assert!(json.contains("\"schema_version\""));
309 assert!(json.contains("\"categories\""));
310 assert!(json.contains("\"errors\""));
311 assert!(json.contains("RCH-E001"));
312 assert!(json.contains("RCH-E100"));
313 assert!(json.contains("RCH-E500"));
314 }
315
316 #[test]
317 fn test_category_entries() {
318 let catalog = generate_error_catalog();
319
320 let config_cat = catalog
322 .categories
323 .iter()
324 .find(|c| c.id == "config")
325 .unwrap();
326 assert_eq!(config_cat.code_range, "001-099");
327
328 let network_cat = catalog
329 .categories
330 .iter()
331 .find(|c| c.id == "network")
332 .unwrap();
333 assert_eq!(network_cat.code_range, "100-199");
334
335 let internal_cat = catalog
336 .categories
337 .iter()
338 .find(|c| c.id == "internal")
339 .unwrap();
340 assert_eq!(internal_cat.code_range, "500-599");
341 }
342
343 #[test]
344 fn test_export_schemas_to_temp_dir() {
345 let temp_dir = std::env::temp_dir().join("rch-schema-test");
346 let _ = std::fs::remove_dir_all(&temp_dir); let result = export_schemas(&temp_dir).unwrap();
349
350 assert_eq!(result.files_generated, 3);
351 assert!(result.files.iter().any(|f| f.contains("api-response")));
352 assert!(result.files.iter().any(|f| f.contains("api-error")));
353 assert!(result.files.iter().any(|f| f.contains("error-codes")));
354
355 for file in &result.files {
357 let content = std::fs::read_to_string(file).unwrap();
358 let _: serde_json::Value = serde_json::from_str(&content).unwrap();
359 }
360
361 let _ = std::fs::remove_dir_all(&temp_dir);
363 }
364}