use serde::{Deserialize, Deserializer, Serialize, Serializer};
macro_rules! newtype_string {
($(#[$attr:meta])* $vis:vis $name:ident) => {
$(#[$attr])*
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
$vis struct $name(String);
impl $name {
pub fn new<T: Into<String>>(value: T) -> Self {
Self(value.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_inner(self) -> String {
self.0
}
}
impl std::fmt::Display for $name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<String> for $name {
fn from(s: String) -> Self {
Self::new(s)
}
}
impl From<&str> for $name {
fn from(s: &str) -> Self {
Self::new(s)
}
}
impl AsRef<str> for $name {
fn as_ref(&self) -> &str {
&self.0
}
}
impl std::ops::Deref for $name {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
};
}
macro_rules! newtype_id {
($(#[$attr:meta])* $vis:vis $name:ident, $prefix:expr) => {
$(#[$attr])*
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
$vis struct $name(String);
impl $name {
pub const PREFIX: &'static str = $prefix;
pub fn new<T: Into<String>>(value: T) -> Result<Self, InvalidIdError> {
let s = value.into();
if !s.starts_with(Self::PREFIX) {
return Err(InvalidIdError {
type_name: stringify!($name),
expected_prefix: Self::PREFIX,
actual_value: s,
});
}
Ok(Self(s))
}
pub(crate) fn new_unchecked<T: Into<String>>(value: T) -> Self {
Self(value.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_inner(self) -> String {
self.0
}
}
impl std::fmt::Display for $name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl AsRef<str> for $name {
fn as_ref(&self) -> &str {
&self.0
}
}
impl std::ops::Deref for $name {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Serialize for $name {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.0)
}
}
impl<'de> Deserialize<'de> for $name {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Ok(Self::new_unchecked(s))
}
}
};
}
#[derive(Debug, Clone, thiserror::Error)]
#[error("Invalid {type_name}: expected prefix '{expected_prefix}', got '{actual_value}'")]
pub struct InvalidIdError {
pub type_name: &'static str,
pub expected_prefix: &'static str,
pub actual_value: String,
}
#[derive(Debug, Clone, thiserror::Error)]
#[error("Invalid email address: '{0}'")]
pub struct InvalidEmailError(pub String);
newtype_string!(
pub UserToken
);
newtype_string!(
pub AppToken
);
newtype_string!(
pub AppSecret
);
newtype_string!(
pub ClientSecret
);
newtype_string!(
pub AuthCode
);
newtype_string!(
pub RedirectUri
);
newtype_id!(
pub AccountId,
"acc_"
);
newtype_id!(
pub TransactionId,
"trans_"
);
newtype_id!(
pub UserId,
"user_"
);
newtype_id!(
pub TransferId,
"transfer_"
);
newtype_id!(
pub PaymentId,
"payment_"
);
newtype_id!(
pub ConnectionId,
"conn_"
);
newtype_id!(
pub CategoryId,
"cat_"
);
newtype_id!(
pub MerchantId,
"_merchant"
);
newtype_id!(
pub AuthorizationId,
"auth_"
);
newtype_string!(
pub Cursor
);
#[cfg(test)]
#[allow(
clippy::unwrap_used,
reason = "Tests need to unwrap to verify correctness"
)]
mod tests {
use super::*;
#[test]
fn test_account_id_validation() {
AccountId::new("acc_123456").unwrap();
AccountId::new("trans_123456").unwrap_err();
AccountId::new("123456").unwrap_err();
}
#[test]
fn test_transaction_id_validation() {
TransactionId::new("trans_abcdef123").unwrap();
TransactionId::new("acc_123456").unwrap_err();
}
#[test]
fn test_newtype_conversions() {
let token = UserToken::new("test_token");
assert_eq!(token.as_str(), "test_token");
assert_eq!(&*token, "test_token"); assert_eq!(token.to_string(), "test_token");
let token2: UserToken = "another_token".into();
assert_eq!(token2.as_str(), "another_token");
}
}