mod download;
mod openapi;
mod request;
mod response;
mod tasks;
mod upload;
pub use openapi::{api_root_url, openapi_spec_urls, resolve_openapi_root};
use base64::{engine::general_purpose, Engine as _};
use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION};
use reqwest::Client as HttpClient;
use crate::config::{AuthConfig, Config};
use crate::error::ApiError;
#[derive(Debug, Clone, Default)]
pub struct SaveUploadOptions<'a> {
pub emulator: Option<&'a str>,
pub slot: Option<&'a str>,
pub device_id: Option<&'a str>,
pub session_id: Option<u64>,
pub overwrite: bool,
}
#[derive(Clone)]
pub struct RommClient {
pub(crate) http: HttpClient,
pub(crate) base_url: String,
pub(crate) auth: Option<AuthConfig>,
pub(crate) verbose: bool,
}
pub(crate) fn http_user_agent() -> String {
match std::env::var("ROMM_USER_AGENT") {
Ok(s) if !s.trim().is_empty() => s,
_ => format!(
"Mozilla/5.0 (compatible; romm-cli/{}; +https://github.com/patricksmill/romm-cli)",
env!("CARGO_PKG_VERSION")
),
}
}
impl RommClient {
pub fn new(config: &Config, verbose: bool) -> Result<Self, ApiError> {
let http = HttpClient::builder()
.user_agent(http_user_agent())
.build()?;
Ok(Self {
http,
base_url: config.base_url.clone(),
auth: config.auth.clone(),
verbose,
})
}
pub fn verbose(&self) -> bool {
self.verbose
}
pub(crate) fn build_headers(&self) -> Result<HeaderMap, ApiError> {
let mut headers = HeaderMap::new();
if let Some(auth) = &self.auth {
match auth {
AuthConfig::Basic { username, password } => {
let creds = format!("{username}:{password}");
let encoded = general_purpose::STANDARD.encode(creds.as_bytes());
let value = format!("Basic {encoded}");
headers.insert(
AUTHORIZATION,
HeaderValue::from_str(&value).map_err(|_| {
ApiError::InvalidHeader("invalid basic auth header value".into())
})?,
);
}
AuthConfig::Bearer { token } => {
let value = format!("Bearer {token}");
headers.insert(
AUTHORIZATION,
HeaderValue::from_str(&value).map_err(|_| {
ApiError::InvalidHeader("invalid bearer auth header value".into())
})?,
);
}
AuthConfig::ApiKey { header, key } => {
let name = reqwest::header::HeaderName::from_bytes(header.as_bytes()).map_err(
|_| {
ApiError::InvalidHeader(
"invalid API_KEY_HEADER, must be a valid HTTP header name".into(),
)
},
)?;
headers.insert(
name,
HeaderValue::from_str(key).map_err(|_| {
ApiError::InvalidHeader("invalid API_KEY header value".into())
})?,
);
}
}
}
Ok(headers)
}
}
#[cfg(test)]
mod tests {
use super::response::decode_json_response_body;
use serde_json::Value;
#[test]
fn decode_json_empty_and_whitespace_to_null() {
assert_eq!(decode_json_response_body(b""), Value::Null);
assert_eq!(decode_json_response_body(b" \n\t "), Value::Null);
}
#[test]
fn decode_json_object_roundtrip() {
let v = decode_json_response_body(br#"{"a":1}"#);
assert_eq!(v["a"], 1);
}
#[test]
fn decode_non_json_wrapped() {
let v = decode_json_response_body(b"plain text");
assert_eq!(v["_non_json_body"], "plain text");
}
#[test]
fn api_root_url_strips_trailing_api() {
assert_eq!(
super::api_root_url("http://localhost:8080/api"),
"http://localhost:8080"
);
assert_eq!(
super::api_root_url("http://localhost:8080/api/"),
"http://localhost:8080"
);
assert_eq!(
super::api_root_url("http://localhost:8080"),
"http://localhost:8080"
);
}
#[test]
fn openapi_spec_urls_try_primary_scheme_then_alt() {
let urls = super::openapi_spec_urls("http://example.test");
assert_eq!(urls[0], "http://example.test/openapi.json");
assert_eq!(urls[1], "http://example.test/api/openapi.json");
assert!(
urls.iter()
.any(|u| u == "https://example.test/openapi.json"),
"{urls:?}"
);
}
}