summaryrefslogtreecommitdiff
path: root/src/id.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/id.rs')
-rw-r--r--src/id.rs170
1 files changed, 137 insertions, 33 deletions
diff --git a/src/id.rs b/src/id.rs
index d79f600..8d71c5b 100644
--- a/src/id.rs
+++ b/src/id.rs
@@ -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()
}
}