feat: implement type-safe ID system and enum-based listing architecture
- Add UserId and ListingId newtype wrappers with SQLx integration - Prevents mixing up different ID types at compile time - Custom Encode/Decode traits for seamless database operations - Zero-cost abstractions with full type safety - Refactor Listing to enum-based architecture - BasicAuction: Traditional time-based auction with bidding - MultiSlotAuction: Auction with multiple winners/slots - FixedPriceListing: Fixed price sale with no bidding - BlindAuction: Blind auction where seller chooses winner - ListingBase: Common fields shared by all listing types - ListingRow: Flat database row structure for SQLx compatibility - Update ListingType enum variants - Standard -> BasicAuction - MultiSlot -> MultiSlotAuction - FixedPrice -> FixedPriceListing - Blind -> BlindAuction - Add comprehensive conversion traits and helper methods - Maintain full database compatibility with existing schema - Remove unused helper methods and make base() private for cleaner API
This commit is contained in:
71
src/db/listing_id.rs
Normal file
71
src/db/listing_id.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
//! ListingId newtype for type-safe listing identification
|
||||
//!
|
||||
//! This newtype prevents accidentally mixing up listing IDs with other ID types
|
||||
//! while maintaining compatibility with the database layer through SQLx traits.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{
|
||||
encode::IsNull, error::BoxDynError, sqlite::SqliteTypeInfo, Decode, Encode, Sqlite, Type,
|
||||
};
|
||||
use std::fmt;
|
||||
|
||||
/// Type-safe wrapper for listing IDs
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
pub struct ListingId(i64);
|
||||
|
||||
impl ListingId {
|
||||
/// Create a new ListingId from an i64
|
||||
pub fn new(id: i64) -> Self {
|
||||
Self(id)
|
||||
}
|
||||
|
||||
/// Get the inner i64 value
|
||||
pub fn get(&self) -> i64 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i64> for ListingId {
|
||||
fn from(id: i64) -> Self {
|
||||
Self(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ListingId> for i64 {
|
||||
fn from(listing_id: ListingId) -> Self {
|
||||
listing_id.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ListingId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
// SQLx implementations for database compatibility
|
||||
impl Type<Sqlite> for ListingId {
|
||||
fn type_info() -> SqliteTypeInfo {
|
||||
<i64 as Type<Sqlite>>::type_info()
|
||||
}
|
||||
|
||||
fn compatible(ty: &SqliteTypeInfo) -> bool {
|
||||
<i64 as Type<Sqlite>>::compatible(ty)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'q> Encode<'q, Sqlite> for ListingId {
|
||||
fn encode_by_ref(
|
||||
&self,
|
||||
args: &mut Vec<sqlx::sqlite::SqliteArgumentValue<'q>>,
|
||||
) -> Result<IsNull, BoxDynError> {
|
||||
<i64 as Encode<'q, Sqlite>>::encode_by_ref(&self.0, args)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'r> Decode<'r, Sqlite> for ListingId {
|
||||
fn decode(value: sqlx::sqlite::SqliteValueRef<'r>) -> Result<Self, BoxDynError> {
|
||||
let id = <i64 as Decode<'r, Sqlite>>::decode(value)?;
|
||||
Ok(Self(id))
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
pub mod currency_type;
|
||||
pub mod listing_id;
|
||||
pub mod models;
|
||||
pub mod money_amount;
|
||||
pub mod user_id;
|
||||
pub mod users;
|
||||
|
||||
// Re-export all models for easy access
|
||||
pub use currency_type::*;
|
||||
pub use listing_id::*;
|
||||
pub use models::*;
|
||||
pub use money_amount::*;
|
||||
pub use user_id::*;
|
||||
pub use users::UserRepository;
|
||||
|
||||
@@ -1,48 +1,422 @@
|
||||
//! Listing models with type-safe enum-based structure
|
||||
//!
|
||||
//! This module provides a type-safe representation of different listing types:
|
||||
//! - `BasicAuction`: Traditional time-based auction with bidding
|
||||
//! - `MultiSlotAuction`: Auction with multiple winners/slots available
|
||||
//! - `FixedPriceListing`: Fixed price sale with no bidding
|
||||
//! - `BlindAuction`: Blind auction where seller chooses winner
|
||||
//!
|
||||
//! The main `Listing` enum ensures that only valid fields are accessible for each type.
|
||||
//! Database mapping is handled through `ListingRow` with conversion traits.
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::FromRow;
|
||||
|
||||
use super::listing_type::ListingType;
|
||||
use crate::db::money_amount::MoneyAmount;
|
||||
use crate::db::{listing_id::ListingId, money_amount::MoneyAmount, user_id::UserId};
|
||||
|
||||
/// Main listing/auction entity
|
||||
#[derive(Debug, Clone, FromRow, Serialize, Deserialize)]
|
||||
pub struct Listing {
|
||||
pub id: i64,
|
||||
pub seller_id: i64,
|
||||
/// Common fields shared by all listing types
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ListingBase {
|
||||
pub id: ListingId,
|
||||
pub seller_id: UserId,
|
||||
pub title: String,
|
||||
pub description: Option<String>,
|
||||
pub starts_at: DateTime<Utc>,
|
||||
pub ends_at: DateTime<Utc>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// Traditional time-based auction with bidding
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BasicAuction {
|
||||
#[serde(flatten)]
|
||||
pub base: ListingBase,
|
||||
pub starting_bid: Option<MoneyAmount>,
|
||||
pub buy_now_price: Option<MoneyAmount>,
|
||||
pub min_increment: MoneyAmount,
|
||||
pub anti_snipe_minutes: i32,
|
||||
}
|
||||
|
||||
/// Auction with multiple winners/slots available
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MultiSlotAuction {
|
||||
#[serde(flatten)]
|
||||
pub base: ListingBase,
|
||||
pub starting_bid: Option<MoneyAmount>,
|
||||
pub buy_now_price: Option<MoneyAmount>,
|
||||
pub min_increment: MoneyAmount,
|
||||
pub slots_available: i32,
|
||||
pub anti_snipe_minutes: i32,
|
||||
}
|
||||
|
||||
/// Fixed price sale with no bidding
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FixedPriceListing {
|
||||
#[serde(flatten)]
|
||||
pub base: ListingBase,
|
||||
pub buy_now_price: MoneyAmount,
|
||||
pub slots_available: i32,
|
||||
}
|
||||
|
||||
/// Blind auction where seller chooses winner
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BlindAuction {
|
||||
#[serde(flatten)]
|
||||
pub base: ListingBase,
|
||||
}
|
||||
|
||||
/// Main listing/auction entity enum
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "listing_type")]
|
||||
pub enum Listing {
|
||||
#[serde(rename = "basic_auction")]
|
||||
BasicAuction(BasicAuction),
|
||||
#[serde(rename = "multi_slot_auction")]
|
||||
MultiSlotAuction(MultiSlotAuction),
|
||||
#[serde(rename = "fixed_price_listing")]
|
||||
FixedPriceListing(FixedPriceListing),
|
||||
#[serde(rename = "blind_auction")]
|
||||
BlindAuction(BlindAuction),
|
||||
}
|
||||
|
||||
/// New listing data for insertion
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum NewListing {
|
||||
BasicAuction {
|
||||
seller_id: UserId,
|
||||
title: String,
|
||||
description: Option<String>,
|
||||
starting_bid: Option<MoneyAmount>,
|
||||
buy_now_price: Option<MoneyAmount>,
|
||||
min_increment: MoneyAmount,
|
||||
starts_at: DateTime<Utc>,
|
||||
ends_at: DateTime<Utc>,
|
||||
anti_snipe_minutes: i32,
|
||||
},
|
||||
MultiSlotAuction {
|
||||
seller_id: UserId,
|
||||
title: String,
|
||||
description: Option<String>,
|
||||
starting_bid: Option<MoneyAmount>,
|
||||
buy_now_price: Option<MoneyAmount>,
|
||||
min_increment: MoneyAmount,
|
||||
slots_available: i32,
|
||||
starts_at: DateTime<Utc>,
|
||||
ends_at: DateTime<Utc>,
|
||||
anti_snipe_minutes: i32,
|
||||
},
|
||||
FixedPriceListing {
|
||||
seller_id: UserId,
|
||||
title: String,
|
||||
description: Option<String>,
|
||||
buy_now_price: MoneyAmount,
|
||||
slots_available: i32,
|
||||
starts_at: DateTime<Utc>,
|
||||
ends_at: DateTime<Utc>,
|
||||
},
|
||||
BlindAuction {
|
||||
seller_id: UserId,
|
||||
title: String,
|
||||
description: Option<String>,
|
||||
starts_at: DateTime<Utc>,
|
||||
ends_at: DateTime<Utc>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Listing {
|
||||
/// Get the base information common to all listing types
|
||||
fn base(&self) -> &ListingBase {
|
||||
match self {
|
||||
Listing::BasicAuction(auction) => &auction.base,
|
||||
Listing::MultiSlotAuction(auction) => &auction.base,
|
||||
Listing::FixedPriceListing(listing) => &listing.base,
|
||||
Listing::BlindAuction(auction) => &auction.base,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the listing type as an enum value
|
||||
pub fn listing_type(&self) -> ListingType {
|
||||
match self {
|
||||
Listing::BasicAuction(_) => ListingType::BasicAuction,
|
||||
Listing::MultiSlotAuction(_) => ListingType::MultiSlotAuction,
|
||||
Listing::FixedPriceListing(_) => ListingType::FixedPriceListing,
|
||||
Listing::BlindAuction(_) => ListingType::BlindAuction,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the seller ID
|
||||
pub fn seller_id(&self) -> UserId {
|
||||
self.base().seller_id
|
||||
}
|
||||
|
||||
/// Get the listing title
|
||||
pub fn title(&self) -> &str {
|
||||
&self.base().title
|
||||
}
|
||||
|
||||
/// Get the listing description
|
||||
pub fn description(&self) -> Option<&str> {
|
||||
self.base().description.as_deref()
|
||||
}
|
||||
}
|
||||
|
||||
impl NewListing {
|
||||
/// Get the listing type for this new listing
|
||||
pub fn listing_type(&self) -> ListingType {
|
||||
match self {
|
||||
NewListing::BasicAuction { .. } => ListingType::BasicAuction,
|
||||
NewListing::MultiSlotAuction { .. } => ListingType::MultiSlotAuction,
|
||||
NewListing::FixedPriceListing { .. } => ListingType::FixedPriceListing,
|
||||
NewListing::BlindAuction { .. } => ListingType::BlindAuction,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the seller ID for this new listing
|
||||
pub fn seller_id(&self) -> UserId {
|
||||
match self {
|
||||
NewListing::BasicAuction { seller_id, .. } => *seller_id,
|
||||
NewListing::MultiSlotAuction { seller_id, .. } => *seller_id,
|
||||
NewListing::FixedPriceListing { seller_id, .. } => *seller_id,
|
||||
NewListing::BlindAuction { seller_id, .. } => *seller_id,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the title for this new listing
|
||||
pub fn title(&self) -> &str {
|
||||
match self {
|
||||
NewListing::BasicAuction { title, .. } => title,
|
||||
NewListing::MultiSlotAuction { title, .. } => title,
|
||||
NewListing::FixedPriceListing { title, .. } => title,
|
||||
NewListing::BlindAuction { title, .. } => title,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new basic auction listing
|
||||
pub fn new_basic_auction(
|
||||
seller_id: UserId,
|
||||
title: String,
|
||||
description: Option<String>,
|
||||
starting_bid: Option<MoneyAmount>,
|
||||
buy_now_price: Option<MoneyAmount>,
|
||||
min_increment: MoneyAmount,
|
||||
starts_at: DateTime<Utc>,
|
||||
ends_at: DateTime<Utc>,
|
||||
anti_snipe_minutes: i32,
|
||||
) -> Self {
|
||||
NewListing::BasicAuction {
|
||||
seller_id,
|
||||
title,
|
||||
description,
|
||||
starting_bid,
|
||||
buy_now_price,
|
||||
min_increment,
|
||||
starts_at,
|
||||
ends_at,
|
||||
anti_snipe_minutes,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new multi-slot auction listing
|
||||
pub fn new_multi_slot_auction(
|
||||
seller_id: UserId,
|
||||
title: String,
|
||||
description: Option<String>,
|
||||
starting_bid: Option<MoneyAmount>,
|
||||
buy_now_price: Option<MoneyAmount>,
|
||||
min_increment: MoneyAmount,
|
||||
slots_available: i32,
|
||||
starts_at: DateTime<Utc>,
|
||||
ends_at: DateTime<Utc>,
|
||||
anti_snipe_minutes: i32,
|
||||
) -> Self {
|
||||
NewListing::MultiSlotAuction {
|
||||
seller_id,
|
||||
title,
|
||||
description,
|
||||
starting_bid,
|
||||
buy_now_price,
|
||||
min_increment,
|
||||
slots_available,
|
||||
starts_at,
|
||||
ends_at,
|
||||
anti_snipe_minutes,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new fixed price listing
|
||||
pub fn new_fixed_price_listing(
|
||||
seller_id: UserId,
|
||||
title: String,
|
||||
description: Option<String>,
|
||||
buy_now_price: MoneyAmount,
|
||||
slots_available: i32,
|
||||
starts_at: DateTime<Utc>,
|
||||
ends_at: DateTime<Utc>,
|
||||
) -> Self {
|
||||
NewListing::FixedPriceListing {
|
||||
seller_id,
|
||||
title,
|
||||
description,
|
||||
buy_now_price,
|
||||
slots_available,
|
||||
starts_at,
|
||||
ends_at,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new blind auction listing
|
||||
pub fn new_blind_auction(
|
||||
seller_id: UserId,
|
||||
title: String,
|
||||
description: Option<String>,
|
||||
starts_at: DateTime<Utc>,
|
||||
ends_at: DateTime<Utc>,
|
||||
) -> Self {
|
||||
NewListing::BlindAuction {
|
||||
seller_id,
|
||||
title,
|
||||
description,
|
||||
starts_at,
|
||||
ends_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Flat database row structure for SQLx FromRow compatibility
|
||||
#[derive(Debug, Clone, FromRow)]
|
||||
pub struct ListingRow {
|
||||
pub id: ListingId,
|
||||
pub seller_id: UserId,
|
||||
pub listing_type: ListingType,
|
||||
pub title: String,
|
||||
pub description: Option<String>,
|
||||
|
||||
// Pricing fields
|
||||
// Pricing fields - nullable for types that don't use them
|
||||
pub starting_bid: Option<MoneyAmount>,
|
||||
pub buy_now_price: Option<MoneyAmount>,
|
||||
pub min_increment: MoneyAmount,
|
||||
pub min_increment: Option<MoneyAmount>,
|
||||
|
||||
// Multi-slot/fixed price
|
||||
pub slots_available: i32,
|
||||
pub slots_available: Option<i32>,
|
||||
|
||||
// Timing
|
||||
pub starts_at: DateTime<Utc>,
|
||||
pub ends_at: DateTime<Utc>,
|
||||
pub anti_snipe_minutes: i32,
|
||||
pub anti_snipe_minutes: Option<i32>,
|
||||
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// New listing data for insertion
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NewListing {
|
||||
pub seller_id: i64,
|
||||
pub listing_type: ListingType,
|
||||
pub title: String,
|
||||
pub description: Option<String>,
|
||||
pub starting_bid: Option<MoneyAmount>,
|
||||
pub buy_now_price: Option<MoneyAmount>,
|
||||
pub min_increment: MoneyAmount,
|
||||
pub slots_available: i32,
|
||||
pub starts_at: DateTime<Utc>,
|
||||
pub ends_at: DateTime<Utc>,
|
||||
pub anti_snipe_minutes: i32,
|
||||
impl From<ListingRow> for Listing {
|
||||
fn from(row: ListingRow) -> Self {
|
||||
let base = ListingBase {
|
||||
id: row.id,
|
||||
seller_id: row.seller_id,
|
||||
title: row.title,
|
||||
description: row.description,
|
||||
starts_at: row.starts_at,
|
||||
ends_at: row.ends_at,
|
||||
created_at: row.created_at,
|
||||
updated_at: row.updated_at,
|
||||
};
|
||||
|
||||
match row.listing_type {
|
||||
ListingType::BasicAuction => Listing::BasicAuction(BasicAuction {
|
||||
base,
|
||||
starting_bid: row.starting_bid,
|
||||
buy_now_price: row.buy_now_price,
|
||||
min_increment: row.min_increment.unwrap_or_else(MoneyAmount::zero),
|
||||
anti_snipe_minutes: row.anti_snipe_minutes.unwrap_or(5),
|
||||
}),
|
||||
ListingType::MultiSlotAuction => Listing::MultiSlotAuction(MultiSlotAuction {
|
||||
base,
|
||||
starting_bid: row.starting_bid,
|
||||
buy_now_price: row.buy_now_price,
|
||||
min_increment: row.min_increment.unwrap_or_else(MoneyAmount::zero),
|
||||
slots_available: row.slots_available.unwrap_or(1),
|
||||
anti_snipe_minutes: row.anti_snipe_minutes.unwrap_or(5),
|
||||
}),
|
||||
ListingType::FixedPriceListing => Listing::FixedPriceListing(FixedPriceListing {
|
||||
base,
|
||||
buy_now_price: row.buy_now_price.unwrap_or_else(MoneyAmount::zero),
|
||||
slots_available: row.slots_available.unwrap_or(1),
|
||||
}),
|
||||
ListingType::BlindAuction => Listing::BlindAuction(BlindAuction { base }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Listing> for ListingRow {
|
||||
fn from(listing: Listing) -> Self {
|
||||
match listing {
|
||||
Listing::BasicAuction(auction) => ListingRow {
|
||||
id: auction.base.id,
|
||||
seller_id: auction.base.seller_id,
|
||||
listing_type: ListingType::BasicAuction,
|
||||
title: auction.base.title,
|
||||
description: auction.base.description,
|
||||
starting_bid: auction.starting_bid,
|
||||
buy_now_price: auction.buy_now_price,
|
||||
min_increment: Some(auction.min_increment),
|
||||
slots_available: Some(1),
|
||||
starts_at: auction.base.starts_at,
|
||||
ends_at: auction.base.ends_at,
|
||||
anti_snipe_minutes: Some(auction.anti_snipe_minutes),
|
||||
created_at: auction.base.created_at,
|
||||
updated_at: auction.base.updated_at,
|
||||
},
|
||||
Listing::MultiSlotAuction(auction) => ListingRow {
|
||||
id: auction.base.id,
|
||||
seller_id: auction.base.seller_id,
|
||||
listing_type: ListingType::MultiSlotAuction,
|
||||
title: auction.base.title,
|
||||
description: auction.base.description,
|
||||
starting_bid: auction.starting_bid,
|
||||
buy_now_price: auction.buy_now_price,
|
||||
min_increment: Some(auction.min_increment),
|
||||
slots_available: Some(auction.slots_available),
|
||||
starts_at: auction.base.starts_at,
|
||||
ends_at: auction.base.ends_at,
|
||||
anti_snipe_minutes: Some(auction.anti_snipe_minutes),
|
||||
created_at: auction.base.created_at,
|
||||
updated_at: auction.base.updated_at,
|
||||
},
|
||||
Listing::FixedPriceListing(listing) => ListingRow {
|
||||
id: listing.base.id,
|
||||
seller_id: listing.base.seller_id,
|
||||
listing_type: ListingType::FixedPriceListing,
|
||||
title: listing.base.title,
|
||||
description: listing.base.description,
|
||||
starting_bid: None,
|
||||
buy_now_price: Some(listing.buy_now_price),
|
||||
min_increment: None,
|
||||
slots_available: Some(listing.slots_available),
|
||||
starts_at: listing.base.starts_at,
|
||||
ends_at: listing.base.ends_at,
|
||||
anti_snipe_minutes: None,
|
||||
created_at: listing.base.created_at,
|
||||
updated_at: listing.base.updated_at,
|
||||
},
|
||||
Listing::BlindAuction(auction) => ListingRow {
|
||||
id: auction.base.id,
|
||||
seller_id: auction.base.seller_id,
|
||||
listing_type: ListingType::BlindAuction,
|
||||
title: auction.base.title,
|
||||
description: auction.base.description,
|
||||
starting_bid: None,
|
||||
buy_now_price: None,
|
||||
min_increment: None,
|
||||
slots_available: Some(1),
|
||||
starts_at: auction.base.starts_at,
|
||||
ends_at: auction.base.ends_at,
|
||||
anti_snipe_minutes: None,
|
||||
created_at: auction.base.created_at,
|
||||
updated_at: auction.base.updated_at,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,14 @@ use serde::{Deserialize, Serialize};
|
||||
/// Types of listings supported by the platform
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::Type)]
|
||||
#[sqlx(type_name = "TEXT")]
|
||||
#[sqlx(rename_all = "lowercase")]
|
||||
#[sqlx(rename_all = "snake_case")]
|
||||
pub enum ListingType {
|
||||
/// Traditional time-based auction with bidding
|
||||
Standard,
|
||||
BasicAuction,
|
||||
/// Auction with multiple winners/slots available
|
||||
MultiSlot,
|
||||
MultiSlotAuction,
|
||||
/// Fixed price sale with no bidding
|
||||
FixedPrice,
|
||||
FixedPriceListing,
|
||||
/// Blind auction where seller chooses winner
|
||||
Blind,
|
||||
BlindAuction,
|
||||
}
|
||||
|
||||
71
src/db/user_id.rs
Normal file
71
src/db/user_id.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
//! UserId newtype for type-safe user identification
|
||||
//!
|
||||
//! This newtype prevents accidentally mixing up user IDs with other ID types
|
||||
//! while maintaining compatibility with the database layer through SQLx traits.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{
|
||||
encode::IsNull, error::BoxDynError, sqlite::SqliteTypeInfo, Decode, Encode, Sqlite, Type,
|
||||
};
|
||||
use std::fmt;
|
||||
|
||||
/// Type-safe wrapper for user IDs
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
pub struct UserId(i64);
|
||||
|
||||
impl UserId {
|
||||
/// Create a new UserId from an i64
|
||||
pub fn new(id: i64) -> Self {
|
||||
Self(id)
|
||||
}
|
||||
|
||||
/// Get the inner i64 value
|
||||
pub fn get(&self) -> i64 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i64> for UserId {
|
||||
fn from(id: i64) -> Self {
|
||||
Self(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UserId> for i64 {
|
||||
fn from(user_id: UserId) -> Self {
|
||||
user_id.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for UserId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
// SQLx implementations for database compatibility
|
||||
impl Type<Sqlite> for UserId {
|
||||
fn type_info() -> SqliteTypeInfo {
|
||||
<i64 as Type<Sqlite>>::type_info()
|
||||
}
|
||||
|
||||
fn compatible(ty: &SqliteTypeInfo) -> bool {
|
||||
<i64 as Type<Sqlite>>::compatible(ty)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'q> Encode<'q, Sqlite> for UserId {
|
||||
fn encode_by_ref(
|
||||
&self,
|
||||
args: &mut Vec<sqlx::sqlite::SqliteArgumentValue<'q>>,
|
||||
) -> Result<IsNull, BoxDynError> {
|
||||
<i64 as Encode<'q, Sqlite>>::encode_by_ref(&self.0, args)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'r> Decode<'r, Sqlite> for UserId {
|
||||
fn decode(value: sqlx::sqlite::SqliteValueRef<'r>) -> Result<Self, BoxDynError> {
|
||||
let id = <i64 as Decode<'r, Sqlite>>::decode(value)?;
|
||||
Ok(Self(id))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user