Skip to main content

skiff_cli/openapi/
mod.rs

1//! OpenAPI `$ref` resolution, command extraction, and HTTP execution.
2//!
3//! Specs load from file or URL (URL responses cached under `$SKIFF_CACHE_DIR`).
4//! Remote fetches are not SSRF-sandboxed — only pass trusted URLs.
5
6mod execute;
7mod extract;
8mod refs;
9
10pub use execute::{execute_openapi, resolve_base_url};
11pub use extract::extract_openapi_commands;
12pub use refs::resolve_refs;
13
14use serde_json::Value;
15use std::fs;
16use std::path::Path;
17
18use crate::cache::{cache_key_for, load_cached, save_cache};
19use crate::error::{Error, Result};
20use crate::paths::DEFAULT_CACHE_TTL;
21
22/// Load an OpenAPI spec from a local JSON/YAML file.
23pub fn load_openapi_spec_file(path: &Path) -> Result<Value> {
24    let raw = fs::read_to_string(path)?;
25    parse_spec(&raw)
26}
27
28pub fn parse_spec(raw: &str) -> Result<Value> {
29    let spec: Value = match serde_json::from_str(raw) {
30        Ok(v) => v,
31        Err(_) => serde_yaml::from_str(raw)?,
32    };
33    if !spec.is_object() || spec.get("paths").is_none() {
34        return Err(Error::runtime("spec must contain 'paths'"));
35    }
36    Ok(resolve_refs(spec))
37}
38
39/// Load from URL with caching (uses blocking reqwest for simplicity in sync API).
40pub fn load_openapi_spec_url(
41    url: &str,
42    auth_headers: &[(String, String)],
43    cache_key: Option<&str>,
44    ttl: u64,
45    refresh: bool,
46) -> Result<Value> {
47    let key = cache_key.map(str::to_string).unwrap_or_else(|| {
48        cache_key_for(&serde_json::json!({
49            "source": url,
50            "auth_headers": auth_headers,
51        }))
52    });
53    if !refresh {
54        if let Some(cached) = load_cached(&key, ttl)? {
55            return Ok(cached);
56        }
57    }
58
59    let client = reqwest::blocking::Client::builder()
60        .timeout(std::time::Duration::from_secs(30))
61        .build()
62        .map_err(|e| Error::runtime(e.to_string()))?;
63    let mut req = client.get(url);
64    for (k, v) in auth_headers {
65        req = req.header(k, v);
66    }
67    let resp = req.send().map_err(|e| Error::runtime(e.to_string()))?;
68    if !resp.status().is_success() {
69        return Err(Error::runtime(format!(
70            "Error {}: {}",
71            resp.status().as_u16(),
72            resp.text().unwrap_or_default()
73        )));
74    }
75    let raw = resp.text().map_err(|e| Error::runtime(e.to_string()))?;
76    let spec = parse_spec(&raw)?;
77    save_cache(&key, &spec)?;
78    Ok(spec)
79}
80
81pub fn load_openapi_spec(
82    source: &str,
83    auth_headers: &[(String, String)],
84    cache_key: Option<&str>,
85    ttl: Option<u64>,
86    refresh: bool,
87) -> Result<Value> {
88    let ttl = ttl.unwrap_or(DEFAULT_CACHE_TTL);
89    if source.starts_with("http://") || source.starts_with("https://") {
90        load_openapi_spec_url(source, auth_headers, cache_key, ttl, refresh)
91    } else {
92        load_openapi_spec_file(Path::new(source))
93    }
94}