use serde::{Deserialize, Serialize};
use std::fmt;
use uuid::Uuid;
macro_rules! define_id {
(
$(#[$meta:meta])*
$name:ident
) => {
$(#[$meta])*
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
#[must_use]
pub struct $name(Uuid);
impl $name {
#[inline]
pub fn new() -> Self {
Self(Uuid::new_v4())
}
#[inline]
pub const fn nil() -> Self {
Self(Uuid::nil())
}
#[inline]
pub const fn from_uuid(id: Uuid) -> Self {
Self(id)
}
#[inline]
pub const fn as_uuid(&self) -> &Uuid {
&self.0
}
#[inline]
pub const fn into_uuid(self) -> Uuid {
self.0
}
#[inline]
pub const fn is_nil(&self) -> bool {
self.0.is_nil()
}
}
impl Default for $name {
#[inline]
fn default() -> Self {
Self::new()
}
}
impl fmt::Debug for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}({})", stringify!($name), self.0)
}
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl std::str::FromStr for $name {
type Err = uuid::Error;
#[inline]
fn from_str(s: &str) -> Result<Self, Self::Err> {
Uuid::parse_str(s).map(Self)
}
}
impl From<Uuid> for $name {
#[inline]
fn from(id: Uuid) -> Self {
Self(id)
}
}
impl From<$name> for Uuid {
#[inline]
fn from(id: $name) -> Self {
id.0
}
}
impl AsRef<Uuid> for $name {
#[inline]
fn as_ref(&self) -> &Uuid {
&self.0
}
}
#[cfg(feature = "sqlx-postgres")]
impl sqlx::Type<sqlx::Postgres> for $name {
fn type_info() -> sqlx::postgres::PgTypeInfo {
<Uuid as sqlx::Type<sqlx::Postgres>>::type_info()
}
}
#[cfg(feature = "sqlx-postgres")]
impl sqlx::postgres::PgHasArrayType for $name {
fn array_type_info() -> sqlx::postgres::PgTypeInfo {
<Uuid as sqlx::postgres::PgHasArrayType>::array_type_info()
}
}
#[cfg(feature = "sqlx-postgres")]
impl<'q> sqlx::Encode<'q, sqlx::Postgres> for $name {
fn encode_by_ref(
&self,
buf: &mut sqlx::postgres::PgArgumentBuffer,
) -> Result<sqlx::encode::IsNull, sqlx::error::BoxDynError> {
<Uuid as sqlx::Encode<sqlx::Postgres>>::encode_by_ref(&self.0, buf)
}
}
#[cfg(feature = "sqlx-postgres")]
impl<'r> sqlx::Decode<'r, sqlx::Postgres> for $name {
fn decode(value: sqlx::postgres::PgValueRef<'r>)
-> Result<Self, sqlx::error::BoxDynError> {
<Uuid as sqlx::Decode<'r, sqlx::Postgres>>::decode(value).map(Self)
}
}
#[cfg(any(test, feature = "arbitrary"))]
impl proptest::arbitrary::Arbitrary for $name {
type Parameters = ();
type Strategy = proptest::strategy::MapInto<
proptest::arbitrary::StrategyFor<[u8; 16]>,
Self,
>;
fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
use proptest::strategy::Strategy;
proptest::arbitrary::any::<[u8; 16]>().prop_map_into()
}
}
#[cfg(any(test, feature = "arbitrary"))]
impl From<[u8; 16]> for $name {
fn from(bytes: [u8; 16]) -> Self {
Self(Uuid::from_bytes(bytes))
}
}
#[cfg(feature = "rusqlite")]
impl rusqlite::types::ToSql for $name {
#[inline]
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
Ok(rusqlite::types::ToSqlOutput::Owned(
rusqlite::types::Value::Text(self.0.to_string()),
))
}
}
#[cfg(feature = "rusqlite")]
impl rusqlite::types::FromSql for $name {
#[inline]
fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult<Self> {
let text = value.as_str()?;
Uuid::parse_str(text)
.map(Self)
.map_err(|e| rusqlite::types::FromSqlError::Other(Box::new(e)))
}
}
};
}
define_id! {
OrderId
}
define_id! {
CustomerId
}
define_id! {
ProductId
}
define_id! {
InvoiceId
}
define_id! {
ShipmentId
}
define_id! {
ReturnId
}
define_id! {
WarehouseId
}
define_id! {
PaymentId
}
define_id! {
InventoryItemId
}
define_id! {
SubscriptionId
}
define_id! {
CartId
}
define_id! {
FulfillmentId
}
define_id! {
OrderItemId
}
define_id! {
PurchaseOrderId
}
define_id! {
PromotionId
}
define_id! {
WarrantyId
}
define_id! {
CreditId
}
define_id! {
AgentId
}
define_id! {
GiftCardId
}
define_id! {
StoreCreditId
}
define_id! {
SegmentId
}
define_id! {
ShippingZoneId
}
define_id! {
ShippingMethodId
}
define_id! {
ReviewId
}
define_id! {
WishlistId
}
define_id! {
LoyaltyProgramId
}
define_id! {
RewardId
}
define_id! {
GiftCardTransactionId
}
define_id! {
StoreCreditTransactionId
}
define_id! {
LoyaltyTransactionId
}
define_id! {
FraudRuleId
}
define_id! {
SearchConfigId
}
define_id! {
LoyaltyAccountId
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn id_types_are_distinct() {
let order_id = OrderId::new();
let customer_id = CustomerId::new();
let _: Uuid = order_id.into();
let _: Uuid = customer_id.into();
}
#[test]
fn roundtrip_display_parse() {
let id = OrderId::new();
let s = id.to_string();
let parsed: OrderId = s.parse().unwrap();
assert_eq!(id, parsed);
}
#[test]
fn serde_roundtrip() {
let id = ProductId::new();
let json = serde_json::to_string(&id).unwrap();
let parsed: ProductId = serde_json::from_str(&json).unwrap();
assert_eq!(id, parsed);
let uuid_json = serde_json::to_string(id.as_uuid()).unwrap();
assert_eq!(json, uuid_json);
}
#[test]
fn nil_id() {
let id = OrderId::nil();
assert!(id.is_nil());
assert_eq!(id.to_string(), "00000000-0000-0000-0000-000000000000");
}
#[test]
fn debug_includes_type_name() {
let id = CustomerId::nil();
let debug = format!("{:?}", id);
assert!(debug.starts_with("CustomerId("));
}
#[test]
fn from_uuid_roundtrip() {
let uuid = Uuid::new_v4();
let order_id = OrderId::from(uuid);
assert_eq!(Uuid::from(order_id), uuid);
}
mod proptests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn order_id_display_parse_roundtrip(id: OrderId) {
let s = id.to_string();
let parsed: OrderId = s.parse().unwrap();
prop_assert_eq!(id, parsed);
}
#[test]
fn customer_id_display_parse_roundtrip(id: CustomerId) {
let s = id.to_string();
let parsed: CustomerId = s.parse().unwrap();
prop_assert_eq!(id, parsed);
}
#[test]
fn product_id_serde_roundtrip(id: ProductId) {
let json = serde_json::to_string(&id).unwrap();
let parsed: ProductId = serde_json::from_str(&json).unwrap();
prop_assert_eq!(id, parsed);
}
}
}
}