use std::fmt;
use std::str::FromStr;
use bitcoin::base64::engine::general_purpose;
use bitcoin::base64::Engine as _;
use serde::{de, Deserialize, Deserializer, Serialize};
use thiserror::Error;
use uuid::Uuid;
#[derive(Debug, Error)]
pub enum QuoteIdError {
#[error("invalid UUID: {0}")]
Uuid(#[from] uuid::Error),
#[error("invalid base64")]
Base64,
#[error("neither a valid UUID nor a valid base64 string")]
InvalidQuoteId,
}
#[derive(Serialize, Debug, Clone, PartialEq, PartialOrd, Ord, Eq, Hash)]
#[serde(untagged)]
pub enum QuoteId {
BASE64(String),
UUID(Uuid),
}
impl QuoteId {
pub fn new_uuid() -> Self {
Self::UUID(Uuid::new_v4())
}
}
impl From<Uuid> for QuoteId {
fn from(uuid: Uuid) -> Self {
Self::UUID(uuid)
}
}
impl fmt::Display for QuoteId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
QuoteId::BASE64(s) => write!(f, "{s}"),
QuoteId::UUID(u) => write!(f, "{}", u.hyphenated()),
}
}
}
impl FromStr for QuoteId {
type Err = QuoteIdError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Ok(u) = Uuid::parse_str(s) {
return Ok(QuoteId::UUID(u));
}
match general_purpose::URL_SAFE.decode(s) {
Ok(_bytes) => Ok(QuoteId::BASE64(s.to_string())),
Err(_) => Err(QuoteIdError::InvalidQuoteId),
}
}
}
impl<'de> Deserialize<'de> for QuoteId {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
if let Ok(u) = Uuid::parse_str(&s) {
return Ok(QuoteId::UUID(u));
}
if general_purpose::URL_SAFE.decode(&s).is_ok() {
return Ok(QuoteId::BASE64(s));
}
Err(de::Error::custom(format!(
"QuoteId must be either a UUID (e.g. {}) or a valid base64 string; got: {}",
Uuid::nil(),
s
)))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_quote_id_display_uuid() {
let uuid = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
let quote_id = QuoteId::UUID(uuid);
let displayed = quote_id.to_string();
assert_eq!(displayed, "550e8400-e29b-41d4-a716-446655440000");
assert!(!displayed.is_empty());
let parsed: QuoteId = displayed.parse().unwrap();
assert_eq!(quote_id, parsed);
}
#[test]
fn test_quote_id_display_base64() {
let base64_str = "SGVsbG8gV29ybGQh"; let base64_id = QuoteId::BASE64(base64_str.to_string());
let displayed = base64_id.to_string();
assert_eq!(displayed, base64_str);
assert!(!displayed.is_empty());
let parsed: QuoteId = displayed.parse().unwrap();
assert_eq!(base64_id, parsed);
}
}