summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/conversation/id.rs40
-rw-r--r--src/error.rs25
-rw-r--r--src/id.rs170
-rw-r--r--src/invite/id.rs26
-rw-r--r--src/message/id.rs29
-rw-r--r--src/token/id.rs29
-rw-r--r--src/user/id.rs25
7 files changed, 173 insertions, 171 deletions
diff --git a/src/conversation/id.rs b/src/conversation/id.rs
index 5f37a59..1c260c2 100644
--- a/src/conversation/id.rs
+++ b/src/conversation/id.rs
@@ -1,38 +1,10 @@
-use std::fmt;
+pub type Id = crate::id::Id<Conversation>;
-use crate::id::Id as BaseId;
+#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
+pub struct Conversation;
-// Stable identifier for a [Conversation]. Prefixed with `C`.
-#[derive(
- Clone,
- Debug,
- Eq,
- Hash,
- Ord,
- PartialEq,
- PartialOrd,
- sqlx::Type,
- serde::Deserialize,
- serde::Serialize,
-)]
-#[sqlx(transparent)]
-#[serde(transparent)]
-pub struct Id(BaseId);
-
-impl From<BaseId> for Id {
- fn from(id: BaseId) -> Self {
- Self(id)
- }
-}
-
-impl Id {
- pub fn generate() -> Self {
- BaseId::generate("C")
- }
-}
-
-impl fmt::Display for Id {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- self.0.fmt(f)
+impl crate::id::Prefix for Conversation {
+ fn prefix(&self) -> &'static str {
+ "C"
}
}
diff --git a/src/error.rs b/src/error.rs
index 7483f00..3c46097 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -5,8 +5,6 @@ use axum::{
response::{IntoResponse, Response},
};
-use crate::id::Id as BaseId;
-
// I'm making an effort to avoid `anyhow` here, as that crate is _enormously_
// complex (though very usable). We don't need to be overly careful about
// allocations on errors in this app, so this is fine for most "general
@@ -45,25 +43,14 @@ impl IntoResponse for Internal {
}
}
-// Transient identifier for an InternalError. Prefixed with `E`.
-#[derive(Debug)]
-pub struct Id(BaseId);
-
-impl From<BaseId> for Id {
- fn from(id: BaseId) -> Self {
- Self(id)
- }
-}
+pub type Id = crate::id::Id<InternalError>;
-impl Id {
- pub fn generate() -> Self {
- BaseId::generate("E")
- }
-}
+#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
+pub struct InternalError;
-impl fmt::Display for Id {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- self.0.fmt(f)
+impl crate::id::Prefix for InternalError {
+ fn prefix(&self) -> &'static str {
+ "E"
}
}
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()
}
}
diff --git a/src/invite/id.rs b/src/invite/id.rs
index bd53f1f..c37f3ac 100644
--- a/src/invite/id.rs
+++ b/src/invite/id.rs
@@ -1,24 +1,10 @@
-use crate::id::Id as BaseId;
+pub type Id = crate::id::Id<Invitation>;
-// Stable identifier for an invite Prefixed with `I`.
-#[derive(Clone, Debug, Eq, PartialEq, sqlx::Type, serde::Deserialize, serde::Serialize)]
-#[sqlx(transparent)]
-pub struct Id(BaseId);
+#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
+pub struct Invitation;
-impl From<BaseId> for Id {
- fn from(id: BaseId) -> Self {
- Self(id)
- }
-}
-
-impl Id {
- pub fn generate() -> Self {
- BaseId::generate("I")
- }
-}
-
-impl std::fmt::Display for Id {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- self.0.fmt(f)
+impl crate::id::Prefix for Invitation {
+ fn prefix(&self) -> &'static str {
+ "I"
}
}
diff --git a/src/message/id.rs b/src/message/id.rs
index 385b103..9161c78 100644
--- a/src/message/id.rs
+++ b/src/message/id.rs
@@ -1,27 +1,10 @@
-use std::fmt;
+pub type Id = crate::id::Id<Message>;
-use crate::id::Id as BaseId;
+#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
+pub struct Message;
-// Stable identifier for a [Message]. Prefixed with `M`.
-#[derive(Clone, Debug, Eq, Hash, PartialEq, sqlx::Type, serde::Deserialize, serde::Serialize)]
-#[sqlx(transparent)]
-#[serde(transparent)]
-pub struct Id(BaseId);
-
-impl From<BaseId> for Id {
- fn from(id: BaseId) -> Self {
- Self(id)
- }
-}
-
-impl Id {
- pub fn generate() -> Self {
- BaseId::generate("M")
- }
-}
-
-impl fmt::Display for Id {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- self.0.fmt(f)
+impl crate::id::Prefix for Message {
+ fn prefix(&self) -> &'static str {
+ "M"
}
}
diff --git a/src/token/id.rs b/src/token/id.rs
index 9ef063c..e978d59 100644
--- a/src/token/id.rs
+++ b/src/token/id.rs
@@ -1,27 +1,10 @@
-use std::fmt;
+pub type Id = crate::id::Id<Token>;
-use crate::id::Id as BaseId;
+#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
+pub struct Token;
-// Stable identifier for a token. Prefixed with `T`.
-#[derive(Clone, Debug, Eq, Hash, PartialEq, sqlx::Type, serde::Deserialize, serde::Serialize)]
-#[sqlx(transparent)]
-#[serde(transparent)]
-pub struct Id(BaseId);
-
-impl From<BaseId> for Id {
- fn from(id: BaseId) -> Self {
- Self(id)
- }
-}
-
-impl Id {
- pub fn generate() -> Self {
- BaseId::generate("T")
- }
-}
-
-impl fmt::Display for Id {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- self.0.fmt(f)
+impl crate::id::Prefix for Token {
+ fn prefix(&self) -> &'static str {
+ "T"
}
}
diff --git a/src/user/id.rs b/src/user/id.rs
index bc14c1f..3ad8d16 100644
--- a/src/user/id.rs
+++ b/src/user/id.rs
@@ -1,25 +1,12 @@
-use crate::id::Id as BaseId;
-
// Stable identifier for a User. Prefixed with `U`. Users created before March, 2025 may have an `L`
// prefix, instead.
-#[derive(Clone, Debug, Eq, PartialEq, sqlx::Type, serde::Serialize)]
-#[sqlx(transparent)]
-pub struct Id(BaseId);
-
-impl From<BaseId> for Id {
- fn from(id: BaseId) -> Self {
- Self(id)
- }
-}
+pub type Id = crate::id::Id<User>;
-impl Id {
- pub fn generate() -> Self {
- BaseId::generate("U")
- }
-}
+#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
+pub struct User;
-impl std::fmt::Display for Id {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- self.0.fmt(f)
+impl crate::id::Prefix for User {
+ fn prefix(&self) -> &'static str {
+ "U"
}
}