chio_api_protect/
spec_discovery.rs1use crate::error::ProtectError;
4
5pub fn load_spec_from_file(path: &str) -> Result<String, ProtectError> {
7 std::fs::read_to_string(path)
8 .map_err(|e| ProtectError::SpecLoad(format!("cannot read {path}: {e}")))
9}
10
11pub async fn discover_spec(upstream: &str) -> Result<String, ProtectError> {
17 let client = reqwest::Client::new();
18 let well_known_paths = [
19 "/openapi.json",
20 "/openapi.yaml",
21 "/swagger.json",
22 "/api-docs",
23 ];
24
25 for path in &well_known_paths {
26 let url = format!("{}{}", upstream.trim_end_matches('/'), path);
27 match client.get(&url).send().await {
28 Ok(resp) if resp.status().is_success() => match resp.text().await {
29 Ok(body) if !body.is_empty() => return Ok(body),
30 _ => continue,
31 },
32 _ => continue,
33 }
34 }
35
36 Err(ProtectError::SpecLoad(
37 "could not auto-discover OpenAPI spec from upstream; use --spec to provide one".to_string(),
38 ))
39}
40
41#[cfg(test)]
42mod tests {
43 use super::*;
44 use std::time::{SystemTime, UNIX_EPOCH};
45
46 #[test]
47 fn load_spec_from_existing_file() -> Result<(), Box<dyn std::error::Error>> {
48 let suffix = SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos();
49 let dir = std::env::temp_dir().join(format!("chio-api-protect-test-{suffix}"));
50 std::fs::create_dir_all(&dir)?;
51 let path = dir.join("spec.json");
52 std::fs::write(&path, r#"{"openapi":"3.1.0"}"#)?;
53 let spec = load_spec_from_file(&path.to_string_lossy())?;
54 assert!(spec.contains("3.1.0"));
55 let _ = std::fs::remove_dir_all(&dir);
56 Ok(())
57 }
58
59 #[test]
60 fn load_spec_from_missing_file_fails() {
61 let result = load_spec_from_file("/nonexistent/path/openapi.json");
62 assert!(result.is_err());
63 }
64}