chat: add Id<T> type

This commit is contained in:
sam 2024-01-20 23:09:19 +01:00
parent e57bff00c2
commit ce543e7ee1
Signed by: sam
GPG key ID: B4EF20DDE721CAA1
8 changed files with 146 additions and 43 deletions

View file

@ -1,18 +1,18 @@
use eyre::Result; use eyre::Result;
use foxchat::FoxError; use foxchat::{FoxError, Id, id::{ChannelType, GuildType}};
use sqlx::PgExecutor; use sqlx::PgExecutor;
use ulid::Ulid; use ulid::Ulid;
pub struct Channel { pub struct Channel {
pub id: String, pub id: Id<ChannelType>,
pub guild_id: String, pub guild_id: Id<GuildType>,
pub name: String, pub name: String,
pub topic: Option<String>, pub topic: Option<String>,
} }
pub async fn create_channel( pub async fn create_channel(
executor: impl PgExecutor<'_>, executor: impl PgExecutor<'_>,
guild_id: &str, guild_id: &Id<GuildType>,
name: &str, name: &str,
topic: Option<String>, topic: Option<String>,
) -> Result<Channel> { ) -> Result<Channel> {
@ -20,7 +20,7 @@ pub async fn create_channel(
Channel, Channel,
"insert into channels (id, guild_id, name, topic) values ($1, $2, $3, $4) returning *", "insert into channels (id, guild_id, name, topic) values ($1, $2, $3, $4) returning *",
Ulid::new().to_string(), Ulid::new().to_string(),
guild_id, guild_id.0,
name, name,
topic topic
) )
@ -30,8 +30,8 @@ pub async fn create_channel(
Ok(channel) Ok(channel)
} }
pub async fn get_channel(executor: impl PgExecutor<'_>, channel_id: &str) -> Result<Channel, FoxError> { pub async fn get_channel(executor: impl PgExecutor<'_>, channel_id: &Id<ChannelType>) -> Result<Channel, FoxError> {
let channel = sqlx::query_as!(Channel, "select * from channels where id = $1", channel_id) let channel = sqlx::query_as!(Channel, "select * from channels where id = $1", channel_id.0)
.fetch_one(executor) .fetch_one(executor)
.await .await
.map_err(|e| match e { .map_err(|e| match e {

View file

@ -1,24 +1,24 @@
use eyre::Result; use eyre::Result;
use foxchat::FoxError; use foxchat::{FoxError, Id, id::{GuildType, UserType}};
use sqlx::PgExecutor; use sqlx::PgExecutor;
use ulid::Ulid; use ulid::Ulid;
pub struct Guild { pub struct Guild {
pub id: String, pub id: Id<GuildType>,
pub owner_id: String, pub owner_id: Id<UserType>,
pub name: String, pub name: String,
} }
pub async fn create_guild( pub async fn create_guild(
executor: impl PgExecutor<'_>, executor: impl PgExecutor<'_>,
owner_id: &str, owner_id: &Id<UserType>,
name: &str, name: &str,
) -> Result<Guild> { ) -> Result<Guild> {
let guild = sqlx::query_as!( let guild = sqlx::query_as!(
Guild, Guild,
"insert into guilds (id, owner_id, name) values ($1, $2, $3) returning *", "insert into guilds (id, owner_id, name) values ($1, $2, $3) returning *",
Ulid::new().to_string(), Ulid::new().to_string(),
owner_id, owner_id.0,
name name
) )
.fetch_one(executor) .fetch_one(executor)
@ -29,13 +29,13 @@ pub async fn create_guild(
pub async fn join_guild( pub async fn join_guild(
executor: impl PgExecutor<'_>, executor: impl PgExecutor<'_>,
guild_id: &str, guild_id: &Id<GuildType>,
user_id: &str, user_id: &Id<UserType>,
) -> Result<()> { ) -> Result<()> {
sqlx::query!( sqlx::query!(
"insert into guilds_users (guild_id, user_id) values ($1, $2) on conflict (guild_id, user_id) do nothing", "insert into guilds_users (guild_id, user_id) values ($1, $2) on conflict (guild_id, user_id) do nothing",
guild_id, guild_id.0,
user_id user_id.0
) )
.execute(executor) .execute(executor)
.await?; .await?;
@ -45,16 +45,14 @@ pub async fn join_guild(
pub async fn get_guild( pub async fn get_guild(
executor: impl PgExecutor<'_>, executor: impl PgExecutor<'_>,
guild_id: &str, guild_id: &Id<GuildType>,
user_id: &str, user_id: &Id<UserType>,
) -> Result<Guild, FoxError> { ) -> Result<Guild, FoxError> {
println!("guild id: {}, user id: {}", guild_id, user_id);
let guild = sqlx::query_as!( let guild = sqlx::query_as!(
Guild, Guild,
"select g.* from guilds_users u join guilds g on u.guild_id = g.id where u.guild_id = $1 and u.user_id = $2", "select g.* from guilds_users u join guilds g on u.guild_id = g.id where u.guild_id = $1 and u.user_id = $2",
guild_id, guild_id.0,
user_id user_id.0
) )
.fetch_one(executor) .fetch_one(executor)
.await .await

View file

@ -1,29 +1,29 @@
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use eyre::Result; use eyre::Result;
use foxchat::model::http::channel::CreateMessageParams; use foxchat::{Id, model::http::channel::CreateMessageParams, id::{ChannelType, UserType, MessageType}};
use sqlx::PgExecutor; use sqlx::PgExecutor;
use ulid::Ulid; use ulid::Ulid;
pub struct Message { pub struct Message {
pub id: String, pub id: Id<MessageType>,
pub channel_id: String, pub channel_id: Id<ChannelType>,
pub author_id: String, pub author_id: Id<UserType>,
pub updated_at: DateTime<Utc>, pub updated_at: DateTime<Utc>,
pub content: String, pub content: String,
} }
pub async fn create_message( pub async fn create_message(
executor: impl PgExecutor<'_>, executor: impl PgExecutor<'_>,
channel_id: &str, channel_id: &Id<ChannelType>,
user_id: &str, user_id: &Id<UserType>,
params: CreateMessageParams, params: CreateMessageParams,
) -> Result<Message> { ) -> Result<Message> {
let message = sqlx::query_as!( let message = sqlx::query_as!(
Message, Message,
"insert into messages (id, channel_id, author_id, content) values ($1, $2, $3, $4) returning *", "insert into messages (id, channel_id, author_id, content) values ($1, $2, $3, $4) returning *",
Ulid::new().to_string(), Ulid::new().to_string(),
channel_id, channel_id.0,
user_id, user_id.0,
params.content, params.content,
) )
.fetch_one(executor) .fetch_one(executor)

View file

@ -5,7 +5,7 @@ use eyre::Result;
use foxchat::{ use foxchat::{
http::ApiError, http::ApiError,
model::{http::channel::CreateMessageParams, Message, user::PartialUser}, model::{http::channel::CreateMessageParams, Message, user::PartialUser},
FoxError, ulid_timestamp, FoxError, ulid_timestamp, id::ChannelType, Id,
}; };
use crate::{ use crate::{
@ -17,8 +17,8 @@ use crate::{
pub async fn post_messages( pub async fn post_messages(
Extension(state): Extension<Arc<AppState>>, Extension(state): Extension<Arc<AppState>>,
Path(channel_id): Path<Id<ChannelType>>,
request: FoxRequestData, request: FoxRequestData,
Path(channel_id): Path<String>,
Json(params): Json<CreateMessageParams>, Json(params): Json<CreateMessageParams>,
) -> Result<Json<Message>, ApiError> { ) -> Result<Json<Message>, ApiError> {
let user_id = request.user_id.ok_or(FoxError::MissingUser)?; let user_id = request.user_id.ok_or(FoxError::MissingUser)?;
@ -35,14 +35,14 @@ pub async fn post_messages(
// TODO: dispatch message create event // TODO: dispatch message create event
Ok(Json(Message { Ok(Json(Message {
id: message.id.clone(), id: message.id.0.clone(),
channel_id: channel.id, channel_id: channel.id.0,
author: PartialUser { author: PartialUser {
id: user.id, id: user.id.0,
username: user.username, username: user.username,
instance: request.instance.domain, instance: request.instance.domain,
}, },
content: Some(message.content), content: Some(message.content),
created_at: ulid_timestamp(&message.id), created_at: ulid_timestamp(&message.id.0),
})) }))
} }

View file

@ -31,15 +31,15 @@ pub async fn post_guilds(
tx.commit().await?; tx.commit().await?;
Ok(Json(Guild { Ok(Json(Guild {
id: guild.id, id: guild.id.0,
name: guild.name, name: guild.name,
owner: PartialUser { owner: PartialUser {
id: user.id, id: user.id.0,
username: user.username, username: user.username,
instance: user.instance.domain, instance: user.instance.domain,
}, },
default_channel: PartialChannel { default_channel: PartialChannel {
id: channel.id, id: channel.id.0,
name: channel.name, name: channel.name,
} }
})) }))

View file

@ -1,7 +1,7 @@
use std::sync::Arc; use std::sync::Arc;
use eyre::Result; use eyre::Result;
use foxchat::{fed, model::User as HttpUser}; use foxchat::{fed, model::User as HttpUser, Id, id::UserType};
use ulid::Ulid; use ulid::Ulid;
use crate::app_state::AppState; use crate::app_state::AppState;
@ -9,7 +9,7 @@ use crate::app_state::AppState;
use super::identity_instance::IdentityInstance; use super::identity_instance::IdentityInstance;
pub struct User { pub struct User {
pub id: String, pub id: Id<UserType>,
pub instance_id: String, pub instance_id: String,
pub instance: IdentityInstance, pub instance: IdentityInstance,
pub remote_user_id: String, pub remote_user_id: String,
@ -32,7 +32,7 @@ impl User {
.await? .await?
{ {
return Ok(User { return Ok(User {
id: user.id, id: user.id.into(),
username: user.username, username: user.username,
instance_id: user.instance_id, instance_id: user.instance_id,
instance: instance.to_owned(), instance: instance.to_owned(),
@ -62,7 +62,7 @@ impl User {
.await?; .await?;
Ok(User { Ok(User {
id: user.id, id: user.id.into(),
username: user.username, username: user.username,
instance_id: user.instance_id, instance_id: user.instance_id,
instance: instance.to_owned(), instance: instance.to_owned(),

103
foxchat/src/id.rs Normal file
View file

@ -0,0 +1,103 @@
use std::marker::PhantomData;
use serde::{Deserialize, Serialize};
use sqlx::{
database::{HasArguments, HasValueRef},
encode::IsNull,
error::BoxDynError,
postgres::PgTypeInfo,
Decode, Encode, Postgres, Type,
};
pub struct Id<T>(pub String, PhantomData<T>)
where
T: IdType;
impl<T: IdType> Id<T> {
pub fn new(value: String) -> Self {
Self(value, PhantomData)
}
pub fn from_str(value: &str) -> Self {
value.into()
}
}
impl<T: IdType> From<&str> for Id<T> {
fn from(value: &str) -> Self {
Self::new(value.to_string())
}
}
impl<T: IdType> From<String> for Id<T> {
fn from(value: String) -> Self {
Self::new(value)
}
}
impl<T: IdType> From<Id<T>> for String {
fn from(value: Id<T>) -> Self {
value.0
}
}
pub trait IdType: private::Sealed {}
pub struct GuildType;
pub struct ChannelType;
pub struct MessageType;
pub struct UserType;
impl IdType for GuildType {}
impl IdType for ChannelType {}
impl IdType for MessageType {}
impl IdType for UserType {}
mod private {
use super::*;
pub trait Sealed {}
impl Sealed for GuildType {}
impl Sealed for ChannelType {}
impl Sealed for MessageType {}
impl Sealed for UserType {}
}
impl<T: IdType> Type<Postgres> for Id<T> {
fn type_info() -> PgTypeInfo {
<&str as Type<Postgres>>::type_info()
}
}
impl<'r, T: IdType> Decode<'r, Postgres> for Id<T> {
fn decode(value: <Postgres as HasValueRef<'r>>::ValueRef) -> Result<Self, BoxDynError> {
let value = <&str as Decode<Postgres>>::decode(value)?;
Ok(Id::<T>::new(value.parse()?))
}
}
impl<'q, T: IdType> Encode<'q, Postgres> for Id<T> {
fn encode_by_ref(&self, buf: &mut <Postgres as HasArguments<'q>>::ArgumentBuffer) -> IsNull {
<&str as Encode<Postgres>>::encode(&self.0, buf)
}
}
impl<T: IdType> Serialize for Id<T> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.0)
}
}
impl<'de, T: IdType> Deserialize<'de> for Id<T> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
Ok(Self::new(String::deserialize(deserializer)?))
}
}

View file

@ -3,9 +3,11 @@ pub mod fed;
pub mod http; pub mod http;
pub mod model; pub mod model;
pub mod s2s; pub mod s2s;
pub mod id;
pub use error::FoxError; pub use error::FoxError;
pub use fed::signature; pub use fed::signature;
pub use id::Id;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use ulid::Ulid; use ulid::Ulid;