use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PictureUrl(url::Url);
impl PictureUrl {
pub(super) fn parse(s: &str) -> Option<Self> {
s.parse().ok().map(Self)
}
#[must_use]
pub const fn as_url(&self) -> &url::Url {
&self.0
}
}
impl fmt::Display for PictureUrl {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.0.as_str())
}
}
impl Serialize for PictureUrl {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.0.as_str())
}
}
impl<'de> Deserialize<'de> for PictureUrl {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
s.parse::<url::Url>()
.map(PictureUrl)
.map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
#![expect(
clippy::unwrap_used,
reason = "tests do not need to meet production lint standards"
)]
use super::PictureUrl;
#[test]
fn oidc_claims_picture_returns_picture_url() {
let p = PictureUrl::parse("https://example.com/avatar.jpg").unwrap();
assert_eq!(p.as_url().as_str(), "https://example.com/avatar.jpg");
assert_eq!(p.to_string(), "https://example.com/avatar.jpg");
}
#[test]
fn oidc_claims_picture_invalid_url_returns_none() {
assert!(PictureUrl::parse("not-a-url").is_none());
}
}