romm_cli/client/
openapi.rs1use anyhow::{anyhow, Result};
2use std::time::Instant;
3
4use crate::config::normalize_romm_origin;
5
6use super::response::{
7 read_error_response_text, romm_api_error_truncated, version_from_heartbeat_json,
8};
9use super::RommClient;
10
11pub fn api_root_url(base_url: &str) -> String {
13 normalize_romm_origin(base_url)
14}
15
16fn alternate_http_scheme_root(root: &str) -> Option<String> {
17 root.strip_prefix("http://")
18 .map(|rest| format!("https://{}", rest))
19 .or_else(|| {
20 root.strip_prefix("https://")
21 .map(|rest| format!("http://{}", rest))
22 })
23}
24
25pub fn resolve_openapi_root(api_base_url: &str) -> String {
27 if let Ok(s) = std::env::var("ROMM_OPENAPI_BASE_URL") {
28 let t = s.trim();
29 if !t.is_empty() {
30 return normalize_romm_origin(t);
31 }
32 }
33 normalize_romm_origin(api_base_url)
34}
35
36pub fn openapi_spec_urls(api_root: &str) -> Vec<String> {
38 let root = api_root.trim_end_matches('/').to_string();
39 let mut roots = vec![root.clone()];
40 if let Some(alt) = alternate_http_scheme_root(&root) {
41 if alt != root {
42 roots.push(alt);
43 }
44 }
45
46 let mut urls = Vec::new();
47 for r in roots {
48 let b = r.trim_end_matches('/');
49 urls.push(format!("{b}/openapi.json"));
50 urls.push(format!("{b}/api/openapi.json"));
51 }
52 urls
53}
54
55impl RommClient {
56 pub async fn rom_server_version_from_heartbeat(&self) -> Option<String> {
58 let v = self
59 .request_json_unauthenticated("GET", "/api/heartbeat", &[], None)
60 .await
61 .ok()?;
62 version_from_heartbeat_json(&v)
63 }
64
65 pub async fn fetch_openapi_json(&self) -> Result<String> {
67 let root = resolve_openapi_root(&self.base_url);
68 let urls = openapi_spec_urls(&root);
69 let mut failures = Vec::new();
70 for url in &urls {
71 match self.fetch_openapi_json_once(url).await {
72 Ok(body) => return Ok(body),
73 Err(e) => failures.push(format!("{url}: {e:#}")),
74 }
75 }
76 Err(anyhow!(
77 "could not download OpenAPI ({} attempt(s)): {}",
78 failures.len(),
79 failures.join(" | ")
80 ))
81 }
82
83 async fn fetch_openapi_json_once(&self, url: &str) -> Result<String> {
84 let headers = self.build_headers()?;
85
86 let t0 = Instant::now();
87 let resp = self
88 .http
89 .get(url)
90 .headers(headers)
91 .send()
92 .await
93 .map_err(|e| anyhow!("request failed: {e}"))?;
94
95 let status = resp.status();
96 if self.verbose {
97 tracing::info!(
98 "[romm-cli] GET {} -> {} ({}ms)",
99 url,
100 status.as_u16(),
101 t0.elapsed().as_millis()
102 );
103 }
104 if !status.is_success() {
105 let body = read_error_response_text(resp).await;
106 return Err(romm_api_error_truncated(status, &body, 500));
107 }
108
109 resp.text()
110 .await
111 .map_err(|e| anyhow!("read OpenAPI body: {e}"))
112 }
113}