use std::fmt;
use serde::{Deserialize, Deserializer, Serialize};
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum ApiKeyError {
#[error("api key cannot be empty")]
Empty,
#[error("api key cannot have leading or trailing whitespace")]
Whitespace,
#[error("api key cannot contain control characters")]
ControlChar,
}
#[derive(Clone, PartialEq, Eq, Serialize)]
pub struct ApiKey(String);
impl fmt::Debug for ApiKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("ApiKey(<redacted>)")
}
}
impl ApiKey {
pub fn parse(value: impl Into<String>) -> Result<Self, ApiKeyError> {
let value = value.into();
if value.is_empty() {
return Err(ApiKeyError::Empty);
}
if value.trim() != value {
return Err(ApiKeyError::Whitespace);
}
if value.chars().any(char::is_control) {
return Err(ApiKeyError::ControlChar);
}
Ok(Self(value))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn into_string(self) -> String {
self.0
}
}
impl fmt::Display for ApiKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("<api-key:redacted>")
}
}
impl<'de> Deserialize<'de> for ApiKey {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Self::parse(s).map_err(serde::de::Error::custom)
}
}
impl From<ApiKey> for String {
fn from(value: ApiKey) -> Self {
value.0
}
}
impl TryFrom<String> for ApiKey {
type Error = ApiKeyError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::parse(value)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ModelId(pub String);
impl ModelId {
#[must_use]
pub fn new(value: impl Into<String>) -> Self {
Self(value.into())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn into_string(self) -> String {
self.0
}
}
impl fmt::Display for ModelId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_accepts_typical_key() {
let key = ApiKey::parse("sk-ant-abc123").unwrap();
assert_eq!(key.as_str(), "sk-ant-abc123");
}
#[test]
fn parse_rejects_empty() {
assert_eq!(ApiKey::parse(""), Err(ApiKeyError::Empty));
}
#[test]
fn parse_rejects_leading_whitespace() {
assert_eq!(ApiKey::parse(" sk-ant-abc"), Err(ApiKeyError::Whitespace));
}
#[test]
fn parse_rejects_trailing_whitespace() {
assert_eq!(ApiKey::parse("sk-ant-abc\n"), Err(ApiKeyError::Whitespace));
}
#[test]
fn parse_rejects_embedded_control_byte() {
assert_eq!(ApiKey::parse("sk-ant\0abc"), Err(ApiKeyError::ControlChar));
}
#[test]
fn display_redacts() {
let key = ApiKey::parse("super-secret").unwrap();
assert_eq!(format!("{key}"), "<api-key:redacted>");
}
#[test]
fn debug_redacts() {
let key = ApiKey::parse("super-secret").unwrap();
let rendered = format!("{key:?}");
assert_eq!(rendered, "ApiKey(<redacted>)");
assert!(!rendered.contains("super-secret"));
}
#[test]
fn deserialize_revalidates() {
let ok: ApiKey = serde_json::from_str("\"sk-ant-abc\"").unwrap();
assert_eq!(ok.as_str(), "sk-ant-abc");
let err = serde_json::from_str::<ApiKey>("\"\"").unwrap_err();
assert!(err.to_string().contains("empty"), "got: {err}");
}
}