romm_cli/tui/
openapi_sync.rs1use anyhow::{anyhow, Result};
5use serde_json::Value;
6use std::path::Path;
7
8use crate::client::RommClient;
9use crate::tui::openapi::EndpointRegistry;
10
11const 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
30fn heartbeat_rom_version(v: &Value) -> Option<String> {
31 v.get("SYSTEM")?.get("VERSION")?.as_str().map(String::from)
32}
33
34pub async fn sync_openapi_registry(
39 client: &RommClient,
40 cache_path: &Path,
41) -> Result<(EndpointRegistry, Option<String>)> {
42 let fetch_result = client.fetch_openapi_json().await;
43
44 let openapi_body = match fetch_result {
45 Ok(body) => {
46 let remote_ver = parse_openapi_info_version(&body);
47 let local_ver = std::fs::read_to_string(cache_path)
48 .ok()
49 .as_deref()
50 .and_then(parse_openapi_info_version);
51
52 let needs_write =
53 !cache_path.is_file() || local_ver.as_deref() != remote_ver.as_deref();
54
55 if needs_write {
56 if let Some(parent) = cache_path.parent() {
57 std::fs::create_dir_all(parent)
58 .map_err(|e| anyhow!("create OpenAPI cache dir: {e}"))?;
59 }
60 std::fs::write(cache_path, &body)
61 .map_err(|e| anyhow!("write OpenAPI cache {}: {e}", cache_path.display()))?;
62 tracing::info!(
63 "OpenAPI cache {} (version {:?})",
64 cache_path.display(),
65 remote_ver
66 );
67 }
68 body
69 }
70 Err(e) => {
71 if let Some(body) = openapi_from_cwd() {
72 tracing::warn!(
73 "Using ./openapi.json (could not fetch from server: {:#})",
74 e
75 );
76 body
77 } else if let Ok(cached) = std::fs::read_to_string(cache_path) {
78 tracing::warn!(
79 "Using cached OpenAPI at {} (server unreachable: {})",
80 cache_path.display(),
81 e
82 );
83 cached
84 } else {
85 tracing::warn!(
86 "Using bundled OpenAPI spec (server unreachable: {:#}). \
87 API browser paths match the build-time snapshot; connect to refresh from your server.",
88 e
89 );
90 EMBEDDED_OPENAPI_JSON.to_string()
91 }
92 }
93 };
94
95 let registry = EndpointRegistry::from_openapi_json(&openapi_body)
96 .map_err(|e| anyhow!("invalid OpenAPI document: {e}"))?;
97
98 let server_version = client
99 .request_json("GET", "/api/heartbeat", &[], None)
100 .await
101 .ok()
102 .as_ref()
103 .and_then(heartbeat_rom_version);
104
105 Ok((registry, server_version))
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 #[test]
113 fn parses_info_version() {
114 let j = r#"{"openapi":"3.0.0","info":{"version":"1.2.3"},"paths":{}}"#;
115 assert_eq!(parse_openapi_info_version(j), Some("1.2.3".to_string()));
116 }
117
118 #[test]
119 fn embedded_openapi_json_parses() {
120 super::EndpointRegistry::from_openapi_json(EMBEDDED_OPENAPI_JSON)
121 .expect("bundled openapi.json");
122 }
123}