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 prefix: Option<String>,
}
impl BoxliteRestOptions {
pub fn new(url: impl Into<String>) -> Self {
Self {
url: url.into(),
credential: None,
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 prefix = std::env::var(envs::BOXLITE_REST_PREFIX).ok();
Ok(Self {
url,
credential,
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_prefix(mut self, prefix: String) -> Self {
self.prefix = Some(prefix);
self
}
pub(crate) fn effective_prefix(&self) -> &str {
self.prefix.as_deref().unwrap_or("v1")
}
}
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("prefix", &self.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.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_prefix() {
let opts = BoxliteRestOptions::new("https://api.example.com").with_prefix("v2".into());
assert_eq!(opts.effective_prefix(), "v2");
}
#[test]
fn test_effective_prefix_default() {
let opts = BoxliteRestOptions::new("https://api.example.com");
assert_eq!(opts.effective_prefix(), "v1");
}
#[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"));
}
}