prefixed_tsid/
tsid.rs

1use crate::resources::{IDUnknown, TSIDResource};
2use anyhow::anyhow;
3use std::{
4    fmt::{Debug, Display},
5    marker::PhantomData,
6    ops::Deref,
7};
8use tsid::{create_tsid, TSID};
9
10/// Represents a prefixed, type-safe, resource-specific ID in your database.
11/// The resource is defined by `Resource`: when deserializing, that resource must be matched when
12/// reading the prefix. When serializing, that resource will be used to create the prefix.
13///
14/// Internally, this stores a `TSID`, which is actually a `u64`. In your database, you should
15/// probably store this `u64` instead of the base32-encoded prefixed string. The numbers are
16/// time-ordered so you can sort your database with great performance.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
19#[cfg_attr(feature = "ts-rs", ts(type = "string", concrete(Resource = IDUnknown)))]
20#[cfg_attr(
21    feature = "diesel",
22    derive(diesel::expression::AsExpression, diesel::deserialize::FromSqlRow)
23)]
24#[cfg_attr(feature = "diesel", diesel(sql_type = diesel::sql_types::BigInt))]
25pub struct TSIDDatabaseID<Resource: TSIDResource> {
26    pub(crate) id: TSID,
27    resource: PhantomData<Resource>,
28}
29
30impl<Resource: TSIDResource> TSIDDatabaseID<Resource> {
31    /// Parse a number (e.g. from your database) into a TSID. As long as it fits into a `u64`, it
32    /// will be a valid ID, so this can't error.
33    pub fn from_raw_number(number: u64) -> Self {
34        Self {
35            id: TSID::from(number),
36            resource: PhantomData,
37        }
38    }
39
40    pub fn from_integer(number: i64) -> Self {
41        Self {
42            id: TSID::from(number as u64),
43            resource: PhantomData,
44        }
45    }
46
47    /// Creates a new, random TSID.
48    pub fn random() -> Self {
49        Self {
50            id: create_tsid(),
51            resource: PhantomData,
52        }
53    }
54
55    /// Returns the `u64` value of the TSID stored internally. Use this to get a value you can
56    /// store in your database.
57    pub fn to_raw_number(&self) -> u64 {
58        self.id.number()
59    }
60
61    pub fn into_unknown(&self) -> TSIDDatabaseID<IDUnknown> {
62        TSIDDatabaseID::<IDUnknown> {
63            id: self.id,
64            resource: PhantomData,
65        }
66    }
67
68    /// Attempts to parse a string into a `TSIDDatabaseID` matching the prefix of the specified
69    /// resource. If the string does not contain a prefix, or it contains the wrong one, an error
70    /// will be returned instead.
71    ///
72    /// If the resource does not require a prefix, any string is accepted, as long as it is a valid
73    /// base32-encoded TSID.
74    pub fn from_str(v: &str) -> Result<TSIDDatabaseID<Resource>, anyhow::Error> {
75        let tsid_only = if let Some(prefix) = Resource::prefix() {
76            v.strip_prefix(&format!("{}_", prefix))
77                .ok_or(anyhow!("missing prefix {}_", prefix))?
78        } else {
79            v
80        };
81
82        let tsid = TSID::try_from(tsid_only).map_err(|_| anyhow!("invalid tsid"))?;
83        Ok(TSIDDatabaseID::<Resource>::from(tsid))
84    }
85}
86
87impl<Resource: TSIDResource> Display for TSIDDatabaseID<Resource> {
88    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89        if let Some(prefix) = Resource::prefix() {
90            write!(f, "{}_{}", prefix, self.id.to_string())
91        } else {
92            write!(f, "{}", self.id.to_string())
93        }
94    }
95}
96
97impl<Resource: TSIDResource> Deref for TSIDDatabaseID<Resource> {
98    type Target = TSID;
99    fn deref(&self) -> &Self::Target {
100        &self.id
101    }
102}
103
104impl<Resource: TSIDResource> From<TSID> for TSIDDatabaseID<Resource> {
105    fn from(value: TSID) -> Self {
106        Self {
107            id: value,
108            resource: PhantomData,
109        }
110    }
111}