From d734b929b3447f5060c8c6663fdfa4556d68e6c5 Mon Sep 17 00:00:00 2001 From: ras0q Date: Thu, 23 Jan 2025 21:03:14 +0900 Subject: [PATCH 1/7] impl UserService --- server/src/speaker_phone.rs | 2 +- server/src/user.rs | 19 +++++++++++++++++ server/src/user/error.rs | 16 ++++++++++++++ server/src/user/grpc.rs | 42 +++++++++++++++++++++++++++++++++++++ server/src/user/impl.rs | 28 +++++++++++++++++++++++++ 5 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 server/src/user/error.rs create mode 100644 server/src/user/grpc.rs create mode 100644 server/src/user/impl.rs diff --git a/server/src/speaker_phone.rs b/server/src/speaker_phone.rs index cc77fb56..4ec17c67 100644 --- a/server/src/speaker_phone.rs +++ b/server/src/speaker_phone.rs @@ -7,7 +7,7 @@ use crate::prelude::{IntoStatus, Timestamp}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)] #[serde(transparent)] -pub struct SpeakerPhoneId(pub uuid::Uuid); + pub struct SpeakerPhoneId(pub uuid::Uuid); #[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] #[serde(transparent)] diff --git a/server/src/user.rs b/server/src/user.rs index e62f32ce..46416bae 100644 --- a/server/src/user.rs +++ b/server/src/user.rs @@ -1,8 +1,16 @@ +//! user.proto + +pub mod error; +pub mod grpc; +mod r#impl; + use futures::future::BoxFuture; use serde::{Deserialize, Serialize}; use crate::prelude::{IntoStatus, Timestamp}; +pub use error::Error; + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)] #[serde(transparent)] pub struct UserId(pub uuid::Uuid); @@ -17,6 +25,11 @@ pub struct User { pub updated_at: Timestamp, } +pub trait UserStore: Send + Sync + 'static { + fn find(&self, id: UserId) -> Option; + fn create(&self, name: String, display_name: String) -> User; +} + #[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] pub struct GetUser { pub id: UserId, @@ -70,3 +83,9 @@ pub trait ProvideUserService: Send + Sync + 'static { // TODO: build_server(this: Arc) -> UserServiceServer<...> // get_userをgRPCのUserServiceで公開する } + +#[derive(Debug, Clone, Copy)] +pub struct UserServiceImpl; + +pub type UserServiceServer = + schema::user::user_service_server::UserServiceServer>; diff --git a/server/src/user/error.rs b/server/src/user/error.rs new file mode 100644 index 00000000..0aa5f39b --- /dev/null +++ b/server/src/user/error.rs @@ -0,0 +1,16 @@ +/// infallible +#[derive(Debug, Clone, thiserror::Error)] +pub enum Error { + #[error("not found")] + NotFound, +} + +impl From for tonic::Status { + fn from(value: Error) -> Self { + match value { + Error::NotFound => tonic::Status::not_found("not found"), + } + } +} + +pub type Result = std::result::Result; diff --git a/server/src/user/grpc.rs b/server/src/user/grpc.rs new file mode 100644 index 00000000..e77ca622 --- /dev/null +++ b/server/src/user/grpc.rs @@ -0,0 +1,42 @@ +use std::sync::Arc; + +use schema::user as schema; + +// MARK: ServiceImpl + +pub struct ServiceImpl { + state: Arc, +} + +impl Clone for ServiceImpl +where + State: super::ProvideUserService, +{ + fn clone(&self) -> Self { + Self { + state: Arc::clone(&self.state), + } + } +} + +impl ServiceImpl +where + State: super::ProvideUserService, +{ + pub(super) fn new(state: Arc) -> Self { + Self { state } + } +} + +#[async_trait::async_trait] +impl schema::user_service_server::UserService for ServiceImpl +where + State: super::ProvideUserService, +{ + async fn get_user( + &self, + request: tonic::Request, + ) -> Result, tonic::Status> { + unimplemented!() + } +} diff --git a/server/src/user/impl.rs b/server/src/user/impl.rs new file mode 100644 index 00000000..97165c3a --- /dev/null +++ b/server/src/user/impl.rs @@ -0,0 +1,28 @@ +use futures::{future, FutureExt}; + +impl super::UserService for super::UserServiceImpl +where + Context: super::UserStore, +{ + type Error = super::Error; + + fn get_user<'a>( + &'a self, + ctx: &'a Context, + req: super::GetUser, + ) -> future::BoxFuture<'a, Result> { + let user = ctx.find(req.id); + let fut = future::ready(user.ok_or(super::Error::NotFound)); + fut.boxed() + } + + fn create_user<'a>( + &'a self, + ctx: &'a Context, + req: super::CreateUser, + ) -> future::BoxFuture<'a, Result> { + let user = ctx.create(req.name, req.display_name); + let fut = future::ready(Ok(user)); + fut.boxed() + } +} From 9b76493af3247eb4a543cdf0f98866d375f2d017 Mon Sep 17 00:00:00 2001 From: ras0q Date: Thu, 23 Jan 2025 21:13:48 +0900 Subject: [PATCH 2/7] add build_server() --- server/src/speaker_phone.rs | 2 +- server/src/user.rs | 11 +++++++++-- server/src/user/grpc.rs | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/server/src/speaker_phone.rs b/server/src/speaker_phone.rs index 4ec17c67..cc77fb56 100644 --- a/server/src/speaker_phone.rs +++ b/server/src/speaker_phone.rs @@ -7,7 +7,7 @@ use crate::prelude::{IntoStatus, Timestamp}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)] #[serde(transparent)] - pub struct SpeakerPhoneId(pub uuid::Uuid); +pub struct SpeakerPhoneId(pub uuid::Uuid); #[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] #[serde(transparent)] diff --git a/server/src/user.rs b/server/src/user.rs index 46416bae..2cedfb45 100644 --- a/server/src/user.rs +++ b/server/src/user.rs @@ -4,6 +4,8 @@ pub mod error; pub mod grpc; mod r#impl; +use std::sync::Arc; + use futures::future::BoxFuture; use serde::{Deserialize, Serialize}; @@ -80,8 +82,13 @@ pub trait ProvideUserService: Send + Sync + 'static { let ctx = self.context(); self.user_service().create_user(ctx, req) } - // TODO: build_server(this: Arc) -> UserServiceServer<...> - // get_userをgRPCのUserServiceで公開する + fn build_server(this: Arc) -> UserServiceServer + where + Self: Sized, + { + let service = grpc::ServiceImpl::new(this); + UserServiceServer::new(service) + } } #[derive(Debug, Clone, Copy)] diff --git a/server/src/user/grpc.rs b/server/src/user/grpc.rs index e77ca622..8fc3eadc 100644 --- a/server/src/user/grpc.rs +++ b/server/src/user/grpc.rs @@ -35,7 +35,7 @@ where { async fn get_user( &self, - request: tonic::Request, + _request: tonic::Request, ) -> Result, tonic::Status> { unimplemented!() } From b16003b29a1490322341ebe8745c9c06bfbef16b Mon Sep 17 00:00:00 2001 From: ras0q Date: Thu, 23 Jan 2025 22:15:57 +0900 Subject: [PATCH 3/7] DB operations --- server/migrations/.gitkeep | 0 server/migrations/1_init_user.sql | 8 ++++ server/src/user.rs | 5 --- server/src/user/error.rs | 9 ++-- server/src/user/impl.rs | 69 ++++++++++++++++++++++++++----- 5 files changed, 73 insertions(+), 18 deletions(-) delete mode 100644 server/migrations/.gitkeep create mode 100644 server/migrations/1_init_user.sql diff --git a/server/migrations/.gitkeep b/server/migrations/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/server/migrations/1_init_user.sql b/server/migrations/1_init_user.sql new file mode 100644 index 00000000..714fe378 --- /dev/null +++ b/server/migrations/1_init_user.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS `users` ( + `id` BINARY(16) NOT NULL, + `name` VARCHAR(255) NOT NULL, + `display_name` VARCHAR(255) NOT NULL, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +); diff --git a/server/src/user.rs b/server/src/user.rs index 2cedfb45..c7726850 100644 --- a/server/src/user.rs +++ b/server/src/user.rs @@ -27,11 +27,6 @@ pub struct User { pub updated_at: Timestamp, } -pub trait UserStore: Send + Sync + 'static { - fn find(&self, id: UserId) -> Option; - fn create(&self, name: String, display_name: String) -> User; -} - #[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] pub struct GetUser { pub id: UserId, diff --git a/server/src/user/error.rs b/server/src/user/error.rs index 0aa5f39b..27ceee2e 100644 --- a/server/src/user/error.rs +++ b/server/src/user/error.rs @@ -1,14 +1,17 @@ /// infallible -#[derive(Debug, Clone, thiserror::Error)] +#[derive(Debug, thiserror::Error)] pub enum Error { - #[error("not found")] + #[error("Not found")] NotFound, + #[error("Database error")] + Sqlx(#[from] sqlx::Error), } impl From for tonic::Status { fn from(value: Error) -> Self { match value { - Error::NotFound => tonic::Status::not_found("not found"), + Error::NotFound => tonic::Status::not_found("Not found"), + Error::Sqlx(_) => tonic::Status::internal("Database error"), } } } diff --git a/server/src/user/impl.rs b/server/src/user/impl.rs index 97165c3a..79e26e33 100644 --- a/server/src/user/impl.rs +++ b/server/src/user/impl.rs @@ -1,8 +1,12 @@ -use futures::{future, FutureExt}; +use crate::prelude::Timestamp; +use futures::{future::BoxFuture, FutureExt}; +use serde::{Deserialize, Serialize}; +use sqlx::{FromRow, MySqlPool}; +use uuid::Uuid; impl super::UserService for super::UserServiceImpl where - Context: super::UserStore, + Context: AsRef, { type Error = super::Error; @@ -10,19 +14,64 @@ where &'a self, ctx: &'a Context, req: super::GetUser, - ) -> future::BoxFuture<'a, Result> { - let user = ctx.find(req.id); - let fut = future::ready(user.ok_or(super::Error::NotFound)); - fut.boxed() + ) -> BoxFuture<'a, Result> { + get_user(ctx.as_ref(), req).boxed() } fn create_user<'a>( &'a self, ctx: &'a Context, req: super::CreateUser, - ) -> future::BoxFuture<'a, Result> { - let user = ctx.create(req.name, req.display_name); - let fut = future::ready(Ok(user)); - fut.boxed() + ) -> BoxFuture<'a, Result> { + create_user(ctx.as_ref(), req).boxed() } } + +// MARK: DB operations + +#[derive(Debug, Clone, Hash, Deserialize, Serialize, FromRow)] +struct UserRow { + pub id: Uuid, + pub name: String, + pub display_name: String, + pub created_at: chrono::DateTime, + pub updated_at: chrono::DateTime, +} + +impl From for super::User { + fn from(value: UserRow) -> Self { + Self { + id: super::UserId(value.id), + name: value.name, + display_name: value.display_name, + updated_at: Timestamp(value.updated_at), + created_at: Timestamp(value.created_at), + } + } +} + +async fn get_user(pool: &MySqlPool, request: super::GetUser) -> Result { + let super::GetUser { + id: super::UserId(id), + } = request; + let user: Option = sqlx::query_as(r#"SELECT * FROM `users` WHERE `id` = ?"#) + .bind(id) + .fetch_optional(pool) + .await?; + user.map_or(Err(super::Error::NotFound), |user| Ok(user.into())) +} + +async fn create_user( + pool: &MySqlPool, + request: super::CreateUser, +) -> Result { + let super::CreateUser { name, display_name } = request; + let user: UserRow = + sqlx::query_as(r#"INSERT INTO `users` (`name`, `display_name`) VALUES (?, ?)"#) + .bind(name) + .bind(display_name) + .fetch_one(pool) + .await + .map_err(super::Error::from)?; + Ok(user.into()) +} From 438d3ef89428a05244d29c7aa33cf5b295095fd9 Mon Sep 17 00:00:00 2001 From: ras0q Date: Thu, 23 Jan 2025 22:29:31 +0900 Subject: [PATCH 4/7] impl grpc conversion --- server/src/user/grpc.rs | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/server/src/user/grpc.rs b/server/src/user/grpc.rs index 8fc3eadc..b7cfd936 100644 --- a/server/src/user/grpc.rs +++ b/server/src/user/grpc.rs @@ -2,6 +2,28 @@ use std::sync::Arc; use schema::user as schema; +use crate::prelude::IntoStatus; + +// MARK: type conversions + +impl From for schema::User { + fn from(value: super::User) -> Self { + let super::User { + id, + name, + display_name, + created_at, + updated_at: _, + } = value; + Self { + id: id.0.to_string(), + name, + display_name, + created_at: Some(created_at.into()), + } + } +} + // MARK: ServiceImpl pub struct ServiceImpl { @@ -35,8 +57,22 @@ where { async fn get_user( &self, - _request: tonic::Request, + request: tonic::Request, ) -> Result, tonic::Status> { - unimplemented!() + let (_, _, schema::GetUserRequest { id }) = request.into_parts(); + let req = super::GetUser { + id: super::UserId( + uuid::Uuid::parse_str(&id) + .map_err(|_| tonic::Status::invalid_argument("Invalid UUID"))?, + ), + }; + let user = self + .state + .get_user(req) + .await + .map_err(IntoStatus::into_status)? + .into(); + let res = schema::GetUserResponse { user: Some(user) }; + Ok(tonic::Response::new(res)) } } From b91cab3167c4b60e6ee885ddd70d41560be3ec78 Mon Sep 17 00:00:00 2001 From: ras0q Date: Thu, 23 Jan 2025 22:39:02 +0900 Subject: [PATCH 5/7] remove unused comment --- server/src/user/error.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/server/src/user/error.rs b/server/src/user/error.rs index 27ceee2e..2b1e5529 100644 --- a/server/src/user/error.rs +++ b/server/src/user/error.rs @@ -1,4 +1,3 @@ -/// infallible #[derive(Debug, thiserror::Error)] pub enum Error { #[error("Not found")] From 9e33b4311ca352873c3a6843b1d1a14f8ec4cbc7 Mon Sep 17 00:00:00 2001 From: ras0q Date: Thu, 23 Jan 2025 22:39:21 +0900 Subject: [PATCH 6/7] fixup! DB operations --- server/src/user/impl.rs | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/server/src/user/impl.rs b/server/src/user/impl.rs index 79e26e33..b2c4b254 100644 --- a/server/src/user/impl.rs +++ b/server/src/user/impl.rs @@ -66,12 +66,25 @@ async fn create_user( request: super::CreateUser, ) -> Result { let super::CreateUser { name, display_name } = request; - let user: UserRow = - sqlx::query_as(r#"INSERT INTO `users` (`name`, `display_name`) VALUES (?, ?)"#) - .bind(name) - .bind(display_name) - .fetch_one(pool) - .await - .map_err(super::Error::from)?; - Ok(user.into()) + let id = Uuid::now_v7(); + sqlx::query( + r#" + INSERT INTO `users` (`id`, `name`, `display_name`) + VALUES (?, ?, ?) + "#, + ) + .bind(id) + .bind(name) + .bind(display_name) + .execute(pool) + .await?; + + let user = get_user( + pool, + super::GetUser { + id: super::UserId(id), + }, + ) + .await?; + Ok(user) } From c0f6ce3a4d36aa048d214c251a86108c78e24c94 Mon Sep 17 00:00:00 2001 From: Kira Kawai <66677201+ras0q@users.noreply.github.com> Date: Thu, 23 Jan 2025 22:56:13 +0900 Subject: [PATCH 7/7] add trace Co-authored-by: H1rono_K <54711422+H1rono@users.noreply.github.com> --- server/src/user/impl.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/user/impl.rs b/server/src/user/impl.rs index b2c4b254..da730676 100644 --- a/server/src/user/impl.rs +++ b/server/src/user/impl.rs @@ -78,7 +78,7 @@ async fn create_user( .bind(display_name) .execute(pool) .await?; - + tracing::info!(id = %id, "Created a user"); let user = get_user( pool, super::GetUser {