use serde::{Deserialize, Serialize};
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Secret(pub String);
impl std::fmt::Debug for Secret {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Secret(\"****\")")
}
}
impl From<String> for Secret {
fn from(value: String) -> Self {
Self(value)
}
}
impl From<&str> for Secret {
fn from(value: &str) -> Self {
Self(value.to_string())
}
}
impl schemars::JsonSchema for Secret {
fn schema_name() -> std::borrow::Cow<'static, str> {
"Secret".into()
}
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
let mut schema = <String as schemars::JsonSchema>::json_schema(generator);
schema.insert("format".to_string(), "password".into());
schema
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(schemars::JsonSchema)]
struct HasSecret {
#[allow(unused)] api_key: Option<Secret>,
}
#[test]
fn secret_schema_is_marked_as_password_format() {
let schema = schemars::schema_for!(HasSecret);
let as_str = serde_json::to_string(&schema).unwrap();
assert!(
as_str.contains("\"password\""),
"schema should mark api_key as password format: {as_str}"
);
}
#[test]
fn secret_debug_never_leaks_the_value() {
let secret = Secret("super-sensitive-value".to_string());
let debug_str = format!("{secret:?}");
assert!(!debug_str.contains("super-sensitive-value"));
assert!(debug_str.contains("****"));
}
#[test]
fn secret_roundtrips_through_json_as_a_plain_string() {
let secret = Secret("s3cr3t".to_string());
let value = serde_json::to_value(&secret).unwrap();
assert_eq!(value, serde_json::json!("s3cr3t"));
let back: Secret = serde_json::from_value(value).unwrap();
assert_eq!(back, secret);
}
}