1mod download;
8mod openapi;
9mod request;
10mod response;
11mod tasks;
12mod upload;
13
14pub use openapi::{api_root_url, openapi_spec_urls, resolve_openapi_root};
15
16use anyhow::{anyhow, Result};
17use base64::{engine::general_purpose, Engine as _};
18use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION};
19use reqwest::Client as HttpClient;
20
21use crate::config::{AuthConfig, Config};
22
23#[derive(Debug, Clone, Default)]
25pub struct SaveUploadOptions<'a> {
26 pub emulator: Option<&'a str>,
27 pub slot: Option<&'a str>,
28 pub device_id: Option<&'a str>,
29 pub session_id: Option<u64>,
30 pub overwrite: bool,
31}
32
33#[derive(Clone)]
38pub struct RommClient {
39 pub(crate) http: HttpClient,
40 pub(crate) base_url: String,
41 pub(crate) auth: Option<AuthConfig>,
42 pub(crate) verbose: bool,
43}
44
45pub(crate) fn http_user_agent() -> String {
48 match std::env::var("ROMM_USER_AGENT") {
49 Ok(s) if !s.trim().is_empty() => s,
50 _ => format!(
51 "Mozilla/5.0 (compatible; romm-cli/{}; +https://github.com/patricksmill/romm-cli)",
52 env!("CARGO_PKG_VERSION")
53 ),
54 }
55}
56
57impl RommClient {
58 pub fn new(config: &Config, verbose: bool) -> Result<Self> {
60 let http = HttpClient::builder()
61 .user_agent(http_user_agent())
62 .build()?;
63 Ok(Self {
64 http,
65 base_url: config.base_url.clone(),
66 auth: config.auth.clone(),
67 verbose,
68 })
69 }
70
71 pub fn verbose(&self) -> bool {
73 self.verbose
74 }
75
76 pub(crate) fn build_headers(&self) -> Result<HeaderMap> {
78 let mut headers = HeaderMap::new();
79
80 if let Some(auth) = &self.auth {
81 match auth {
82 AuthConfig::Basic { username, password } => {
83 let creds = format!("{username}:{password}");
84 let encoded = general_purpose::STANDARD.encode(creds.as_bytes());
85 let value = format!("Basic {encoded}");
86 headers.insert(
87 AUTHORIZATION,
88 HeaderValue::from_str(&value)
89 .map_err(|_| anyhow!("invalid basic auth header value"))?,
90 );
91 }
92 AuthConfig::Bearer { token } => {
93 let value = format!("Bearer {token}");
94 headers.insert(
95 AUTHORIZATION,
96 HeaderValue::from_str(&value)
97 .map_err(|_| anyhow!("invalid bearer auth header value"))?,
98 );
99 }
100 AuthConfig::ApiKey { header, key } => {
101 let name = reqwest::header::HeaderName::from_bytes(header.as_bytes()).map_err(
102 |_| anyhow!("invalid API_KEY_HEADER, must be a valid HTTP header name"),
103 )?;
104 headers.insert(
105 name,
106 HeaderValue::from_str(key)
107 .map_err(|_| anyhow!("invalid API_KEY header value"))?,
108 );
109 }
110 }
111 }
112
113 Ok(headers)
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use super::response::decode_json_response_body;
120 use serde_json::Value;
121
122 #[test]
123 fn decode_json_empty_and_whitespace_to_null() {
124 assert_eq!(decode_json_response_body(b""), Value::Null);
125 assert_eq!(decode_json_response_body(b" \n\t "), Value::Null);
126 }
127
128 #[test]
129 fn decode_json_object_roundtrip() {
130 let v = decode_json_response_body(br#"{"a":1}"#);
131 assert_eq!(v["a"], 1);
132 }
133
134 #[test]
135 fn decode_non_json_wrapped() {
136 let v = decode_json_response_body(b"plain text");
137 assert_eq!(v["_non_json_body"], "plain text");
138 }
139
140 #[test]
141 fn api_root_url_strips_trailing_api() {
142 assert_eq!(
143 super::api_root_url("http://localhost:8080/api"),
144 "http://localhost:8080"
145 );
146 assert_eq!(
147 super::api_root_url("http://localhost:8080/api/"),
148 "http://localhost:8080"
149 );
150 assert_eq!(
151 super::api_root_url("http://localhost:8080"),
152 "http://localhost:8080"
153 );
154 }
155
156 #[test]
157 fn openapi_spec_urls_try_primary_scheme_then_alt() {
158 let urls = super::openapi_spec_urls("http://example.test");
159 assert_eq!(urls[0], "http://example.test/openapi.json");
160 assert_eq!(urls[1], "http://example.test/api/openapi.json");
161 assert!(
162 urls.iter()
163 .any(|u| u == "https://example.test/openapi.json"),
164 "{urls:?}"
165 );
166 }
167}