Skip to main content

romm_cli/tui/
openapi_sync.rs

1//! Load the RomM OpenAPI spec for the API browser: prefer the live server, fall back to cache,
2//! then a bundled copy shipped in the binary so the TUI always starts without manual `openapi.json`.
3
4use anyhow::{anyhow, Result};
5use serde_json::Value;
6use std::path::Path;
7
8use crate::client::RommClient;
9use crate::tui::openapi::EndpointRegistry;
10
11/// OpenAPI document baked into the binary (same as `openapi.json` in the crate root at build time).
12const EMBEDDED_OPENAPI_JSON: &str =
13    include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/openapi.json"));
14
15fn openapi_from_cwd() -> Option<String> {
16    let dir = std::env::current_dir().ok()?;
17    let p = dir.join("openapi.json");
18    if p.is_file() {
19        std::fs::read_to_string(p).ok()
20    } else {
21        None
22    }
23}
24
25pub fn parse_openapi_info_version(json: &str) -> Option<String> {
26    let v: Value = serde_json::from_str(json).ok()?;
27    v.get("info")?.get("version")?.as_str().map(String::from)
28}
29
30/// Resolve OpenAPI JSON: try the server first (updates disk cache when the spec changes), then
31/// `./openapi.json`, then the user cache file, then the embedded bundle.
32///
33/// Also calls `GET /api/heartbeat` for the RomM server version shown in Settings.
34pub async fn sync_openapi_registry(
35    client: &RommClient,
36    cache_path: &Path,
37) -> Result<(EndpointRegistry, Option<String>)> {
38    let fetch_result = client.fetch_openapi_json().await;
39
40    let openapi_body = match fetch_result {
41        Ok(body) => {
42            let remote_ver = parse_openapi_info_version(&body);
43            let local_ver = std::fs::read_to_string(cache_path)
44                .ok()
45                .as_deref()
46                .and_then(parse_openapi_info_version);
47
48            let needs_write =
49                !cache_path.is_file() || local_ver.as_deref() != remote_ver.as_deref();
50
51            if needs_write {
52                if let Some(parent) = cache_path.parent() {
53                    std::fs::create_dir_all(parent)
54                        .map_err(|e| anyhow!("create OpenAPI cache dir: {e}"))?;
55                }
56                std::fs::write(cache_path, &body)
57                    .map_err(|e| anyhow!("write OpenAPI cache {}: {e}", cache_path.display()))?;
58                tracing::info!(
59                    "OpenAPI cache {} (version {:?})",
60                    cache_path.display(),
61                    remote_ver
62                );
63            }
64            body
65        }
66        Err(e) => {
67            if let Some(body) = openapi_from_cwd() {
68                tracing::warn!(
69                    "Using ./openapi.json (could not fetch from server: {:#})",
70                    e
71                );
72                body
73            } else if let Ok(cached) = std::fs::read_to_string(cache_path) {
74                tracing::warn!(
75                    "Using cached OpenAPI at {} (server unreachable: {})",
76                    cache_path.display(),
77                    e
78                );
79                cached
80            } else {
81                tracing::warn!(
82                    "Using bundled OpenAPI spec (server unreachable: {:#}). \
83                     API browser paths match the build-time snapshot; connect to refresh from your server.",
84                    e
85                );
86                EMBEDDED_OPENAPI_JSON.to_string()
87            }
88        }
89    };
90
91    let registry = EndpointRegistry::from_openapi_json(&openapi_body)
92        .map_err(|e| anyhow!("invalid OpenAPI document: {e}"))?;
93
94    let server_version = client.rom_server_version_from_heartbeat().await;
95
96    Ok((registry, server_version))
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn parses_info_version() {
105        let j = r#"{"openapi":"3.0.0","info":{"version":"1.2.3"},"paths":{}}"#;
106        assert_eq!(parse_openapi_info_version(j), Some("1.2.3".to_string()));
107    }
108
109    #[test]
110    fn embedded_openapi_json_parses() {
111        super::EndpointRegistry::from_openapi_json(EMBEDDED_OPENAPI_JSON)
112            .expect("bundled openapi.json");
113    }
114}