summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOwen Jacobson <owen@grimoire.ca>2025-07-24 23:18:06 -0400
committerOwen Jacobson <owen@grimoire.ca>2025-07-24 23:53:37 -0400
commitcae0d11fa25160b38a411d4edd8a0f3b5b06df8c (patch)
treeec90a0b7fbb5ccc9fda1fb8f2a81672d2587f9fc
parentaec3eaeebd37bce9ab4dad14e7e86ef0db8f0c2d (diff)
Define ID types as specializations, rather than newtypes.
This is based heavily on the work done for normalized strings, in `crate::normalize`. The key realization in that module is that the logic distinguishing one kind of thing (normalized strings in that case, IDs, in this case) can be packaged up as a type token, and that doing so may reduce the overall complexity. This implementation for ID also borrows heavily from the implementation for normalized strings. It's less flexible: an ID implemented this way can't expose _less_ of `crate::id::ID`'s interface, whereas newtype wrappers can, for example. However, our code doesn't use that flexiblity on purpose anywhere and we're relatively unlikely to change that. In return, the individual ID types require substantially less code - they do not, for example, need to re-implement `Display` for themselves. I very nearly made the trait `Prefix`: ```rust pub trait Prefix { const PREFIX: &str; } ``` however, I think having an effectively-constant method is less surprising overall.
-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"
}
}