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 base64::{engine::general_purpose, Engine as _};
17use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION};
18use reqwest::Client as HttpClient;
19
20use crate::config::{AuthConfig, Config};
21use crate::error::ApiError;
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, ApiError> {
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, ApiError> {
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).map_err(|_| {
89 ApiError::InvalidHeader("invalid basic auth header value".into())
90 })?,
91 );
92 }
93 AuthConfig::Bearer { token } => {
94 let value = format!("Bearer {token}");
95 headers.insert(
96 AUTHORIZATION,
97 HeaderValue::from_str(&value).map_err(|_| {
98 ApiError::InvalidHeader("invalid bearer auth header value".into())
99 })?,
100 );
101 }
102 AuthConfig::ApiKey { header, key } => {
103 let name = reqwest::header::HeaderName::from_bytes(header.as_bytes()).map_err(
104 |_| {
105 ApiError::InvalidHeader(
106 "invalid API_KEY_HEADER, must be a valid HTTP header name".into(),
107 )
108 },
109 )?;
110 headers.insert(
111 name,
112 HeaderValue::from_str(key).map_err(|_| {
113 ApiError::InvalidHeader("invalid API_KEY header value".into())
114 })?,
115 );
116 }
117 }
118 }
119
120 Ok(headers)
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::response::decode_json_response_body;
127 use serde_json::Value;
128
129 #[test]
130 fn decode_json_empty_and_whitespace_to_null() {
131 assert_eq!(decode_json_response_body(b""), Value::Null);
132 assert_eq!(decode_json_response_body(b" \n\t "), Value::Null);
133 }
134
135 #[test]
136 fn decode_json_object_roundtrip() {
137 let v = decode_json_response_body(br#"{"a":1}"#);
138 assert_eq!(v["a"], 1);
139 }
140
141 #[test]
142 fn decode_non_json_wrapped() {
143 let v = decode_json_response_body(b"plain text");
144 assert_eq!(v["_non_json_body"], "plain text");
145 }
146
147 #[test]
148 fn api_root_url_strips_trailing_api() {
149 assert_eq!(
150 super::api_root_url("http://localhost:8080/api"),
151 "http://localhost:8080"
152 );
153 assert_eq!(
154 super::api_root_url("http://localhost:8080/api/"),
155 "http://localhost:8080"
156 );
157 assert_eq!(
158 super::api_root_url("http://localhost:8080"),
159 "http://localhost:8080"
160 );
161 }
162
163 #[test]
164 fn openapi_spec_urls_try_primary_scheme_then_alt() {
165 let urls = super::openapi_spec_urls("http://example.test");
166 assert_eq!(urls[0], "http://example.test/openapi.json");
167 assert_eq!(urls[1], "http://example.test/api/openapi.json");
168 assert!(
169 urls.iter()
170 .any(|u| u == "https://example.test/openapi.json"),
171 "{urls:?}"
172 );
173 }
174}