use nautilus_core::string::secret::SecretString;
pub const COINAPI_API_KEY: &str = "COINAPI_API_KEY";
pub fn resolve_api_key(api_key: Option<SecretString>) -> anyhow::Result<SecretString> {
let api_key = match api_key {
Some(api_key) => api_key,
None => std::env::var(COINAPI_API_KEY)
.map(SecretString::from)
.map_err(|_| {
anyhow::anyhow!("CoinAPI API key not configured; set api_key or {COINAPI_API_KEY}")
})?,
};
anyhow::ensure!(
is_valid_api_key(api_key.expose_secret()),
"CoinAPI API key must contain visible ASCII characters"
);
Ok(api_key)
}
#[must_use]
pub fn is_valid_api_key(key: &str) -> bool {
!key.is_empty() && key.bytes().all(|b| (33..=126).contains(&b))
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[rstest]
fn resolve_prefers_explicit_key() {
let api_key = resolve_api_key(Some(SecretString::from("explicit-key"))).unwrap();
assert_eq!(api_key.expose_secret(), "explicit-key");
}
#[rstest]
#[case("")]
#[case("has space")]
#[case("tab\tkey")]
#[case("key\u{e9}")]
fn resolve_rejects_invalid_explicit_key(#[case] api_key: &str) {
let error = resolve_api_key(Some(SecretString::from(api_key))).unwrap_err();
assert_eq!(
error.to_string(),
"CoinAPI API key must contain visible ASCII characters"
);
}
}