Skip to main content

chio_api_protect/
spec_discovery.rs

1//! OpenAPI spec auto-discovery and loading.
2
3use crate::error::ProtectError;
4
5/// Load an OpenAPI spec from a file path or URL.
6pub 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
11/// Try to discover the OpenAPI spec from the upstream server.
12///
13/// Probes well-known paths (`/openapi.json`, `/openapi.yaml`,
14/// `/swagger.json`, `/api-docs`) in order, returning the first
15/// non-empty successful response.
16pub 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}