use std::fmt;
use std::sync::Arc;
use boxlite_shared::errors::{BoxliteError, BoxliteResult};
use super::credential::{ApiKeyCredential, Credential};
use crate::runtime::constants::envs;
#[derive(Clone)]
pub struct BoxliteRestOptions {
pub url: String,
pub credential: Option<Arc<dyn Credential>>,
pub path_prefix: Option<String>,
}
impl BoxliteRestOptions {
pub fn new(url: impl Into<String>) -> Self {
Self {
url: url.into(),
credential: None,
path_prefix: None,
}
}
pub fn from_env() -> BoxliteResult<Self> {
let url = std::env::var(envs::BOXLITE_REST_URL)
.map_err(|_| BoxliteError::Config("BOXLITE_REST_URL not set".into()))?;
let credential = std::env::var(envs::BOXLITE_API_KEY)
.ok()
.map(|key| Arc::new(ApiKeyCredential::new(key)) as Arc<dyn Credential>);
let path_prefix = std::env::var(envs::BOXLITE_REST_PATH_PREFIX).ok();
Ok(Self {
url,
credential,
path_prefix,
})
}
pub fn with_api_key(mut self, key: impl Into<String>) -> Self {
self.credential = Some(Arc::new(ApiKeyCredential::new(key)));
self
}
pub fn with_credential(mut self, credential: Arc<dyn Credential>) -> Self {
self.credential = Some(credential);
self
}
pub fn with_path_prefix(mut self, path_prefix: impl Into<String>) -> Self {
self.path_prefix = Some(path_prefix.into());
self
}
}
impl fmt::Debug for BoxliteRestOptions {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("BoxliteRestOptions")
.field("url", &self.url)
.field("credential", &self.credential)
.field("path_prefix", &self.path_prefix)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_new_minimal() {
let opts = BoxliteRestOptions::new("https://api.example.com");
assert_eq!(opts.url, "https://api.example.com");
assert!(opts.credential.is_none());
assert!(opts.path_prefix.is_none());
}
#[tokio::test]
async fn test_with_api_key() {
let opts = BoxliteRestOptions::new("https://api.example.com").with_api_key("blk_live_x");
let cred = opts.credential.expect("credential set");
let tok = cred.get_token().await.expect("get_token");
assert_eq!(tok.token, "blk_live_x");
assert!(tok.expires_at.is_none());
}
#[test]
fn test_with_path_prefix() {
let opts = BoxliteRestOptions::new("https://api.example.com").with_path_prefix("acme");
assert_eq!(opts.path_prefix.as_deref(), Some("acme"));
}
#[test]
fn test_path_prefix_unset_is_none() {
let opts = BoxliteRestOptions::new("https://api.example.com");
assert!(opts.path_prefix.is_none());
}
#[test]
fn test_debug_redacts_api_key() {
let opts =
BoxliteRestOptions::new("https://api.example.com").with_api_key("opaque-key-1234");
let dbg = format!("{:?}", opts);
assert!(
!dbg.contains("opaque-key-1234"),
"Debug output leaked api_key"
);
assert!(dbg.contains("REDACTED"));
}
}