Skip to content

Commit

Permalink
Merge pull request #44 from traP-jp/feat/user-impl
Browse files Browse the repository at this point in the history
impl UserService
H1rono authored Jan 23, 2025
2 parents 12f7cd8 + c0f6ce3 commit 813f465
Showing 6 changed files with 217 additions and 2 deletions.
Empty file removed server/migrations/.gitkeep
Empty file.
8 changes: 8 additions & 0 deletions server/migrations/1_init_user.sql
Original file line number Diff line number Diff line change
@@ -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`)
);
25 changes: 23 additions & 2 deletions server/src/user.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
//! user.proto
pub mod error;
pub mod grpc;
mod r#impl;

use std::sync::Arc;

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);
@@ -67,6 +77,17 @@ pub trait ProvideUserService: Send + Sync + 'static {
let ctx = self.context();
self.user_service().create_user(ctx, req)
}
// TODO: build_server(this: Arc<Self>) -> UserServiceServer<...>
// get_userをgRPCのUserServiceで公開する
fn build_server(this: Arc<Self>) -> UserServiceServer<Self>
where
Self: Sized,
{
let service = grpc::ServiceImpl::new(this);
UserServiceServer::new(service)
}
}

#[derive(Debug, Clone, Copy)]
pub struct UserServiceImpl;

pub type UserServiceServer<State> =
schema::user::user_service_server::UserServiceServer<grpc::ServiceImpl<State>>;
18 changes: 18 additions & 0 deletions server/src/user/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Not found")]
NotFound,
#[error("Database error")]
Sqlx(#[from] sqlx::Error),
}

impl From<Error> for tonic::Status {
fn from(value: Error) -> Self {
match value {
Error::NotFound => tonic::Status::not_found("Not found"),
Error::Sqlx(_) => tonic::Status::internal("Database error"),
}
}
}

pub type Result<T, E = Error> = std::result::Result<T, E>;
78 changes: 78 additions & 0 deletions server/src/user/grpc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
use std::sync::Arc;

use schema::user as schema;

use crate::prelude::IntoStatus;

// MARK: type conversions

impl From<super::User> 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<State> {
state: Arc<State>,
}

impl<State> Clone for ServiceImpl<State>
where
State: super::ProvideUserService,
{
fn clone(&self) -> Self {
Self {
state: Arc::clone(&self.state),
}
}
}

impl<State> ServiceImpl<State>
where
State: super::ProvideUserService,
{
pub(super) fn new(state: Arc<State>) -> Self {
Self { state }
}
}

#[async_trait::async_trait]
impl<State> schema::user_service_server::UserService for ServiceImpl<State>
where
State: super::ProvideUserService,
{
async fn get_user(
&self,
request: tonic::Request<schema::GetUserRequest>,
) -> Result<tonic::Response<schema::GetUserResponse>, tonic::Status> {
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))
}
}
90 changes: 90 additions & 0 deletions server/src/user/impl.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
use crate::prelude::Timestamp;
use futures::{future::BoxFuture, FutureExt};
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, MySqlPool};
use uuid::Uuid;

impl<Context> super::UserService<Context> for super::UserServiceImpl
where
Context: AsRef<MySqlPool>,
{
type Error = super::Error;

fn get_user<'a>(
&'a self,
ctx: &'a Context,
req: super::GetUser,
) -> BoxFuture<'a, Result<super::User, Self::Error>> {
get_user(ctx.as_ref(), req).boxed()
}

fn create_user<'a>(
&'a self,
ctx: &'a Context,
req: super::CreateUser,
) -> BoxFuture<'a, Result<super::User, Self::Error>> {
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<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
}

impl From<UserRow> 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<super::User, super::Error> {
let super::GetUser {
id: super::UserId(id),
} = request;
let user: Option<UserRow> = 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<super::User, super::Error> {
let super::CreateUser { name, display_name } = request;
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?;
tracing::info!(id = %id, "Created a user");
let user = get_user(
pool,
super::GetUser {
id: super::UserId(id),
},
)
.await?;
Ok(user)
}

0 comments on commit 813f465

Please sign in to comment.