use std::{marker::PhantomData, str::FromStr};
use base64::{
display::Base64Display,
engine::{general_purpose::URL_SAFE_NO_PAD, GeneralPurpose},
Engine,
};
use schemars::JsonSchema;
use sqlx::{postgres::PgTypeInfo, Database};
use thiserror::Error;
use uuid::Uuid;
#[macro_export]
macro_rules! make_object_id {
($typ:ident, $prefix:ident) => {
mod $prefix {
#[derive(Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Hash)]
pub struct $typ;
impl $crate::object_id::ObjectIdPrefix for $typ {
fn prefix() -> &'static str {
stringify!($prefix)
}
}
}
pub type $typ = $crate::object_id::ObjectId<$prefix::$typ>;
};
}
#[derive(Debug, Error)]
pub enum ObjectIdError {
#[error("Invalid ID prefix, expected {0}")]
InvalidPrefix(&'static str),
#[error("Failed to decode object ID")]
DecodeFailure,
}
pub trait ObjectIdPrefix:
Clone + Copy + Eq + PartialEq + PartialOrd + Ord + std::hash::Hash
{
fn prefix() -> &'static str;
}
#[derive(Hash, PartialOrd, Ord, Eq)]
pub struct ObjectId<PREFIX: ObjectIdPrefix>(pub Uuid, PhantomData<PREFIX>);
impl<PREFIX: ObjectIdPrefix> JsonSchema for ObjectId<PREFIX> {
fn schema_name() -> String {
format!("ObjectId<{}>", PREFIX::prefix())
}
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
String::json_schema(gen)
}
fn is_referenceable() -> bool {
false
}
fn schema_id() -> std::borrow::Cow<'static, str> {
format!(concat!(module_path!(), "::ObjectId<{}>"), PREFIX::prefix()).into()
}
}
impl<PREFIX: ObjectIdPrefix> Clone for ObjectId<PREFIX> {
fn clone(&self) -> Self {
Self(self.0, PhantomData)
}
}
impl<PREFIX: ObjectIdPrefix> Copy for ObjectId<PREFIX> {}
impl<PREFIX: ObjectIdPrefix> ObjectId<PREFIX> {
pub fn new() -> Self {
Self(uuid::Uuid::now_v7(), PhantomData)
}
pub const fn from_uuid(u: Uuid) -> Self {
Self(u, PhantomData)
}
pub const fn into_inner(self) -> Uuid {
self.0
}
pub const fn as_uuid(&self) -> &Uuid {
&self.0
}
pub const fn nil() -> Self {
Self(Uuid::nil(), PhantomData)
}
pub fn display_without_prefix(&self) -> Base64Display<GeneralPurpose> {
base64::display::Base64Display::new(self.0.as_bytes(), &URL_SAFE_NO_PAD)
}
}
impl<PREFIX: ObjectIdPrefix> Default for ObjectId<PREFIX> {
fn default() -> Self {
Self::new()
}
}
impl<PREFIX: ObjectIdPrefix> PartialEq for ObjectId<PREFIX> {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
impl<PREFIX: ObjectIdPrefix> PartialEq<Uuid> for ObjectId<PREFIX> {
fn eq(&self, other: &Uuid) -> bool {
&self.0 == other
}
}
impl<PREFIX: ObjectIdPrefix> AsRef<Uuid> for ObjectId<PREFIX> {
fn as_ref(&self) -> &Uuid {
&self.0
}
}
impl<PREFIX: ObjectIdPrefix> From<Uuid> for ObjectId<PREFIX> {
fn from(u: Uuid) -> Self {
Self(u, PhantomData)
}
}
impl<PREFIX: ObjectIdPrefix> From<ObjectId<PREFIX>> for Uuid {
fn from(data: ObjectId<PREFIX>) -> Self {
data.0
}
}
impl<PREFIX: ObjectIdPrefix> std::fmt::Debug for ObjectId<PREFIX> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("ObjectId")
.field(&self.to_string())
.field(&self.0)
.finish()
}
}
impl<PREFIX: ObjectIdPrefix> std::fmt::Display for ObjectId<PREFIX> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(PREFIX::prefix())?;
self.display_without_prefix().fmt(f)
}
}
fn decode_suffix(s: &str) -> Result<Uuid, ObjectIdError> {
let bytes = URL_SAFE_NO_PAD
.decode(s)
.map_err(|_| ObjectIdError::DecodeFailure)?;
Uuid::from_slice(&bytes).map_err(|_| ObjectIdError::DecodeFailure)
}
impl<PREFIX: ObjectIdPrefix> FromStr for ObjectId<PREFIX> {
type Err = ObjectIdError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let expected_prefix = PREFIX::prefix();
if !s.starts_with(expected_prefix) {
return Err(ObjectIdError::InvalidPrefix(expected_prefix));
}
decode_suffix(&s[expected_prefix.len()..]).map(Self::from_uuid)
}
}
impl<PREFIX: ObjectIdPrefix> serde::Serialize for ObjectId<PREFIX> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let s = self.to_string();
serializer.serialize_str(&s)
}
}
struct ObjectIdVisitor<PREFIX: ObjectIdPrefix>(PhantomData<PREFIX>);
impl<PREFIX: ObjectIdPrefix> Default for ObjectIdVisitor<PREFIX> {
fn default() -> Self {
Self(Default::default())
}
}
impl<'de, PREFIX: ObjectIdPrefix> serde::de::Visitor<'de> for ObjectIdVisitor<PREFIX> {
type Value = ObjectId<PREFIX>;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("an object ID starting with ")?;
formatter.write_str(PREFIX::prefix())
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
match Self::Value::from_str(v) {
Ok(id) => Ok(id),
Err(e) => {
Uuid::from_str(v)
.map(ObjectId::<PREFIX>::from_uuid)
.map_err(|_| e)
}
}
.map_err(|_| E::invalid_value(serde::de::Unexpected::Str(v), &self))
}
}
impl<'de, PREFIX: ObjectIdPrefix> serde::Deserialize<'de> for ObjectId<PREFIX> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_str(ObjectIdVisitor::default())
}
}
impl<PREFIX: ObjectIdPrefix> sqlx::Type<sqlx::Postgres> for ObjectId<PREFIX> {
fn type_info() -> <sqlx::Postgres as Database>::TypeInfo {
<sqlx::types::Uuid as sqlx::Type<sqlx::Postgres>>::type_info()
}
}
impl<PREFIX: ObjectIdPrefix> sqlx::postgres::PgHasArrayType for ObjectId<PREFIX> {
fn array_type_info() -> PgTypeInfo {
<sqlx::types::Uuid as sqlx::postgres::PgHasArrayType>::array_type_info()
}
}
impl<'q, PREFIX: ObjectIdPrefix> sqlx::Encode<'q, sqlx::Postgres> for ObjectId<PREFIX> {
fn encode_by_ref(
&self,
buf: &mut <sqlx::Postgres as sqlx::Database>::ArgumentBuffer<'q>,
) -> Result<sqlx::encode::IsNull, Box<dyn std::error::Error + Send + Sync>> {
<sqlx::types::Uuid as sqlx::Encode<'_, sqlx::Postgres>>::encode_by_ref(&self.0, buf)
}
}
impl<'r, PREFIX: ObjectIdPrefix> sqlx::Decode<'r, sqlx::Postgres> for ObjectId<PREFIX> {
fn decode(
value: <sqlx::Postgres as sqlx::Database>::ValueRef<'r>,
) -> Result<Self, sqlx::error::BoxDynError> {
let u = <sqlx::types::Uuid as sqlx::Decode<'r, sqlx::Postgres>>::decode(value)?;
Ok(Self(u, PhantomData))
}
}
#[cfg(test)]
mod tests {
use axum::{extract::Path, response::IntoResponse, Router};
use super::*;
make_object_id!(TeamId, tm);
#[test]
fn to_from_str() {
let id = TeamId::new();
let s = id.to_string();
let id2 = TeamId::from_str(&s).unwrap();
assert_eq!(id, id2, "ID converts to string and back");
}
#[test]
fn serde() {
let id = TeamId::new();
let json_str = serde_json::to_string(&id).unwrap();
let id2: TeamId = serde_json::from_str(&json_str).unwrap();
drop(json_str);
assert_eq!(id, id2, "Value serializes and deserializes to itself");
}
#[test]
fn can_use_in_axum_path() {
async fn get_id(Path(_id): Path<TeamId>) -> impl IntoResponse {
"ok"
}
let _ = Router::<()>::new().route("/:id", axum::routing::get(get_id));
}
}