diff options
Diffstat (limited to 'src/id.rs')
| -rw-r--r-- | src/id.rs | 170 |
1 files changed, 137 insertions, 33 deletions
@@ -1,4 +1,7 @@ use rand::{seq::SliceRandom, thread_rng}; +use serde::{Deserializer, Serializer}; +use sqlx::encode::IsNull; +use sqlx::{Database, Decode, Encode, Type}; use std::fmt; // Make IDs that: @@ -23,48 +26,149 @@ const ALPHABET: [char; 23] = [ // 68 bits per ID. const ID_SIZE: usize = 15; -// Intended to be wrapped in a newtype that provides both type-based separation -// from other identifier types, and a unique prefix to allow the intended type -// of an ID to be determined by eyeball when debugging. +// Intended to be wrapped in a type aliases that specalizes Id on a type that +// implements Prefix, both so that IDs are type-wise distinct within the server +// and so that IDs are readily distinguishable from one another outside of it. // // By convention, the prefix should be UPPERCASE - note that the alphabet for // this is entirely lowercase. -#[derive( - Clone, - Debug, - Hash, - Eq, - Ord, - PartialEq, - PartialOrd, - sqlx::Type, - serde::Deserialize, - serde::Serialize, -)] -#[sqlx(transparent)] -#[serde(transparent)] -pub struct Id(String); - -impl fmt::Display for Id { +// +// To build a new ID type, create a "marker" type that implements this trait, plus the following list of derives: +// +// ``` +// #[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)] +// pub struct Person; +// +// impl crate::id::Prefix for Person { +// fn prefix(&self) -> &str { +// "P" // try not to conflict with other prefixes +// } +// } +// ``` +// +// Then provide a type alias of the form +// +// ``` +// pub type Id = crate::id::Id<Person>; +// ``` +// +// The `Id` type will provide a `generate()` method that can generate new random IDs, and the +// resulting type alias can be serialized to and deserialized from strings, and stored in string +// compatible database columns. +#[derive(Clone, Debug, Hash, Eq, Ord, PartialEq, PartialOrd)] +pub struct Id<T>(String, T); + +impl<T> fmt::Display for Id<T> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0.fmt(f) } } -impl Id { - pub fn generate<T>(prefix: &str) -> T +pub trait Prefix { + fn prefix(&self) -> &str; +} + +impl<T> Id<T> +where + T: Prefix + Default, +{ + pub fn generate() -> Self { + let instance = T::default(); + let prefix = instance.prefix(); + + let random_part = (0..ID_SIZE) + .filter_map(|_| ALPHABET.choose(&mut thread_rng())) + .copied(); + + let id = prefix.chars().chain(random_part).collect(); + + Self(id, instance) + } +} + +impl<T> serde::ser::Serialize for Id<T> { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + let Self(id, _) = self; + id.serialize(serializer) + } +} + +impl<'de, T> serde::de::Deserialize<'de> for Id<T> +where + T: Default, +{ + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where - T: From<Self>, + D: Deserializer<'de>, { - let mut rng = thread_rng(); - let id = prefix - .chars() - .chain( - (0..ID_SIZE) - .filter_map(|_| ALPHABET.choose(&mut rng)) /* usize -> &char */ - .copied(), /* &char -> char */ - ) - .collect(); - T::from(Self(id)) + let instance = T::default(); + let id = String::deserialize(deserializer)?; + + Ok(Self(id, instance)) + } +} + +// Type is manually implemented so that we can implement Decode to do +// recover the type token on read. Implementation is otherwise based on +// `#[derive(sqlx::Type)]` with the `#[sqlx(transparent)]` attribute. +impl<DB, T> Type<DB> for Id<T> +where + DB: Database, + String: Type<DB>, +{ + fn type_info() -> <DB as Database>::TypeInfo { + <String as Type<DB>>::type_info() + } + + fn compatible(ty: &<DB as Database>::TypeInfo) -> bool { + <String as Type<DB>>::compatible(ty) + } +} + +impl<'r, DB, T> Decode<'r, DB> for Id<T> +where + DB: Database, + String: Decode<'r, DB>, + T: Default, +{ + fn decode(value: <DB as Database>::ValueRef<'r>) -> Result<Self, sqlx::error::BoxDynError> { + let instance = T::default(); + let id = String::decode(value)?; + Ok(Self(id, instance)) + } +} + +impl<'q, DB, T> Encode<'q, DB> for Id<T> +where + DB: Database, + String: Encode<'q, DB>, +{ + fn encode( + self, + buf: &mut <DB as Database>::ArgumentBuffer<'q>, + ) -> Result<IsNull, sqlx::error::BoxDynError> { + let Self(id, _) = self; + id.encode(buf) + } + + fn encode_by_ref( + &self, + buf: &mut <DB as Database>::ArgumentBuffer<'q>, + ) -> Result<IsNull, sqlx::error::BoxDynError> { + let Self(id, _) = self; + id.encode_by_ref(buf) + } + + fn produces(&self) -> Option<<DB as Database>::TypeInfo> { + let Self(id, _) = self; + id.produces() + } + + fn size_hint(&self) -> usize { + let Self(id, _) = self; + id.size_hint() } } |
