Skip to content

Commit

Permalink
Extract command mod
Browse files Browse the repository at this point in the history
  • Loading branch information
bouzuya committed Jan 12, 2024
1 parent 78d5b92 commit ad04c8d
Show file tree
Hide file tree
Showing 7 changed files with 263 additions and 47 deletions.
99 changes: 97 additions & 2 deletions backend/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ edition = "2021"

[dependencies]
anyhow = "1.0.78"
async-trait = "0.1.77"
axum = "0.7.3"
expo_push_notification_client = "0.1.0"
serde = { version = "1.0.193", features = ["derive"] }
Expand All @@ -14,5 +15,6 @@ tokio = { version = "1.35.1", features = ["full"] }
expo_push_notification_client = { git = "https://github.com/katayama8000/expo-push-notification-client-rust", branch = "main" }

[dev-dependencies]
mockall = "0.12.1"
serde_json = "1.0.111"
tower = { version = "0.4.13", features = ["util"] }
24 changes: 24 additions & 0 deletions backend/src/command.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use std::{collections::HashSet, sync::Arc};

use async_trait::async_trait;
use tokio::sync::Mutex;

use self::create_expo_push_token::CanCreateExpoPushToken;

pub mod create_expo_push_token;

#[derive(Clone, Debug, Default)]
pub struct App {
// TODO: hide
pub expo_push_tokens: Arc<Mutex<HashSet<String>>>,
}

#[async_trait]
impl CanCreateExpoPushToken for App {
async fn create_expo_push_token(
&self,
input: create_expo_push_token::Input,
) -> Result<create_expo_push_token::Output, create_expo_push_token::Error> {
create_expo_push_token::handle(self, input).await
}
}
23 changes: 23 additions & 0 deletions backend/src/command/create_expo_push_token.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use async_trait::async_trait;

use super::App;

#[async_trait]
#[cfg_attr(test, mockall::automock)]
pub trait CanCreateExpoPushToken {
async fn create_expo_push_token(&self, input: Input) -> Result<Output, Error>;
}

pub struct Input {
pub expo_push_token: String,
}

pub struct Output;
pub struct Error;

pub async fn handle(state: &App, Input { expo_push_token }: Input) -> Result<Output, Error> {
let mut expo_push_tokens = state.expo_push_tokens.lock().await;
// TODO: already exists
expo_push_tokens.insert(expo_push_token);
Ok(Output)
}
11 changes: 1 addition & 10 deletions backend/src/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,9 @@ mod create_notifications;
mod delete_expo_push_token;
mod get_root;

#[cfg(test)]
pub(crate) use self::create_expo_push_token::CreateExpoPushTokenRequestBody;

use std::{collections::HashSet, sync::Arc};
use crate::command::App;

use axum::Router;
use tokio::sync::Mutex;

#[derive(Clone, Debug, Default)]
pub struct App {
expo_push_tokens: Arc<Mutex<HashSet<String>>>,
}

pub fn route(app: App) -> Router {
Router::new()
Expand Down
122 changes: 110 additions & 12 deletions backend/src/handler/create_expo_push_token.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,120 @@
use axum::{extract::State, http::StatusCode, routing, Json, Router};
use axum::{
extract::State,
http::StatusCode,
response::{IntoResponse, Response},
routing, Json, Router,
};

use super::App;
use crate::command::create_expo_push_token::{CanCreateExpoPushToken, Input, Output};

pub fn route() -> Router<App> {
Router::new().route("/expo_push_tokens", routing::post(handler))
pub fn route<T>() -> Router<T>
where
T: Clone + CanCreateExpoPushToken + Send + Sync + 'static,
{
Router::new().route("/expo_push_tokens", routing::post(handler::<T>))
}

#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct CreateExpoPushTokenRequestBody {
pub expo_push_token: String,
}

async fn handler(
State(App { expo_push_tokens }): State<App>,
Json(CreateExpoPushTokenRequestBody { expo_push_token }): Json<CreateExpoPushTokenRequestBody>,
) -> StatusCode {
let mut expo_push_tokens = expo_push_tokens.lock().await;
// TODO: already exists
expo_push_tokens.insert(expo_push_token);
StatusCode::CREATED
impl TryFrom<CreateExpoPushTokenRequestBody> for Input {
type Error = StatusCode;

fn try_from(
CreateExpoPushTokenRequestBody { expo_push_token }: CreateExpoPushTokenRequestBody,
) -> Result<Self, Self::Error> {
Ok(Self { expo_push_token })
}
}

#[derive(Debug, serde::Deserialize, serde::Serialize)]
struct CreateExpoPushTokenResponseBody;

impl From<Output> for CreateExpoPushTokenResponseBody {
fn from(_: Output) -> Self {
Self
}
}

impl IntoResponse for CreateExpoPushTokenResponseBody {
fn into_response(self) -> Response {
StatusCode::CREATED.into_response()
}
}

async fn handler<T>(
State(app): State<T>,
Json(body): Json<CreateExpoPushTokenRequestBody>,
) -> Result<CreateExpoPushTokenResponseBody, StatusCode>
where
T: CanCreateExpoPushToken,
{
let input = Input::try_from(body)?;
let output = app.create_expo_push_token(input).await.map_err(|_| {
// TODO
StatusCode::INTERNAL_SERVER_ERROR
})?;
Ok(CreateExpoPushTokenResponseBody::from(output))
}

#[cfg(test)]
mod tests {
use std::sync::Arc;

use anyhow::Context;
use async_trait::async_trait;
use axum::{
body::Body,
http::{header::CONTENT_TYPE, Request},
};
use tower::ServiceExt as _;

use crate::{
command::create_expo_push_token::{self, MockCanCreateExpoPushToken},
tests::ResponseExt as _,
};

use super::*;

#[tokio::test]
async fn test_create_expo_push_token() -> anyhow::Result<()> {
let mut app = MockApp::default();
Arc::get_mut(&mut app.mock_create_expo_push_token)
.context("Arc::get_mut")?
.expect_create_expo_push_token()
.return_once(|_| Box::pin(async { Ok(Output) }));
let router = route().with_state(app);
let request = Request::builder()
.method("POST")
.uri("/expo_push_tokens")
.header(CONTENT_TYPE, "application/json")
.body(Body::from(serde_json::to_string(
&CreateExpoPushTokenRequestBody {
expo_push_token: "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]".to_string(),
},
)?))?;
let response = router.oneshot(request).await?;
assert_eq!(response.status(), StatusCode::CREATED);
assert_eq!(response.into_body_string().await?, "");
Ok(())
}

#[derive(Clone, Default)]
struct MockApp {
mock_create_expo_push_token: Arc<MockCanCreateExpoPushToken>,
}

#[async_trait]
impl CanCreateExpoPushToken for MockApp {
async fn create_expo_push_token(
&self,
input: create_expo_push_token::Input,
) -> Result<create_expo_push_token::Output, create_expo_push_token::Error> {
self.mock_create_expo_push_token
.create_expo_push_token(input)
.await
}
}
}

0 comments on commit ad04c8d

Please sign in to comment.