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