diff options
| author | Owen Jacobson <owen@grimoire.ca> | 2025-08-23 15:27:21 -0400 |
|---|---|---|
| committer | Owen Jacobson <owen@grimoire.ca> | 2025-08-24 04:50:49 -0400 |
| commit | 97e4c1d25f6ee17959adc23cacd8361dcd42e519 (patch) | |
| tree | e78aebac605570f1f7ca77e7d676be17f2d633c2 | |
| parent | 9f4aa1c2a1cd830b9fd90a384b9c4e5923597e18 (diff) | |
Add conversions between String and Id<T>.
There's already an implicit conversion (via serialization), it's just awkward to use. However, we now need those conversions more directly.
| -rw-r--r-- | src/id.rs | 89 |
1 files changed, 65 insertions, 24 deletions
@@ -1,8 +1,9 @@ +use std::fmt; + 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: // @@ -58,12 +59,10 @@ const ID_SIZE: usize = 15; #[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) - } -} - +// Implementations of prefix provide type tokens for IDs where a new value can be generated, with a +// known prefix. Most implementations should also implement Default and be zero-sized types without +// state, as many of the operations on Ids will discard or re-create type tokens (using +// `Default::default()`) when converting between an ID type and some external form. pub trait Prefix { fn prefix(&self) -> &str; } @@ -86,16 +85,57 @@ where } } +impl<T> Id<T> { + pub fn as_str(&self) -> &str { + let Self(value, _) = self; + value + } + + fn inner(&self) -> &String { + let Self(value, _) = self; + value + } +} + +// Any ID can be converted to a string. Its type token is discarded during the conversion. +impl<T> From<Id<T>> for String { + fn from(value: Id<T>) -> Self { + let Id(value, _) = value; + value + } +} + +// If the type token implements `Default`, then the corresponding ID type can be converted from a +// string. A new type token will be generated using `T::default()`. +impl<T> From<String> for Id<T> +where + T: Default, +{ + fn from(value: String) -> Self { + Self(value, T::default()) + } +} + +// IDs are printable, exactly as their contained string is printable. +impl<T> fmt::Display for Id<T> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.inner().fmt(f) + } +} + +// IDs are serializable, exactly as their contained string is serializable. The type token is +// discarded during serialization. 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) + self.inner().serialize(serializer) } } +// If the type token implements `Default`, then the corresponding ID type is deserializable as a +// String. A new type token will be generated using `T::default()` during deserialization. impl<'de, T> serde::de::Deserialize<'de> for Id<T> where T: Default, @@ -104,16 +144,18 @@ where where D: Deserializer<'de>, { - let instance = T::default(); let id = String::deserialize(deserializer)?; - Ok(Self(id, instance)) + Ok(id.into()) } } -// 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. +// An ID is an sqlx database type for any database where String is a database type. In practice, +// that's all of them. +// +// This is largely cribbed from the implementation generated by `#[sqlx::transparent]`, but we +// implement it by hand here in order to handle the fact that an ID has two contained values, +// rather than one. impl<DB, T> Type<DB> for Id<T> where DB: Database, @@ -128,6 +170,8 @@ where } } +// If the type token implements Default, then the corresponding ID type can be decoded as a String +// from a database record. A new type token will be generated using `T::default()` during decoding. impl<'r, DB, T> Decode<'r, DB> for Id<T> where DB: Database, @@ -135,12 +179,13 @@ where 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)) + Ok(id.into()) } } +// An ID is encodeable to a database value whenever String is encodeable. The type token is +// discarded during encoding. impl<'q, DB, T> Encode<'q, DB> for Id<T> where DB: Database, @@ -150,25 +195,21 @@ where self, buf: &mut <DB as Database>::ArgumentBuffer<'q>, ) -> Result<IsNull, sqlx::error::BoxDynError> { - let Self(id, _) = self; - id.encode(buf) + self.inner().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) + self.inner().encode_by_ref(buf) } fn produces(&self) -> Option<<DB as Database>::TypeInfo> { - let Self(id, _) = self; - id.produces() + self.inner().produces() } fn size_hint(&self) -> usize { - let Self(id, _) = self; - id.size_hint() + self.inner().size_hint() } } |
