Skip to main content

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