はじめに
2023年アドベントカレンダー22日目です。
本日は昨日に引き続き、「ユーザーはトップページでブログタイトルの一覧を見ることができる」を進めていきます。
昨日はアーキテクチャの設計周りをしてきたので、本日は実装を進めていきます。
Axum
Axum は Rust プログラミング言語で使用されるウェブフレームワークです。
今回はこちらを使ってAPIを作成していこうと思います。
Hello World
まずは依存を追加します。
[dependencies]
axum = "0.7.1"
tokio = { version = "1.33.0", features = ["full"] }
公式に従い、まずはGET: http://localhost:3000/
で、Hello Worldだけ出せるようにしていきます。
src/main.rs
を簡単に実装してみます。
use axum::{
routing::get,
Router,
};
#[tokio::main]
async fn main() {
// build our application with a single route
let app = Router::new().route("/", get(|| async { "Hello, World!" }));
// run our app with hyper, listening globally on port 3000
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
実行してみます。
cargo run
$ curl http://localhost:3000
Hello, World!
期待通りのレスポンスが返ってきました。
モジュラーモノリス
まずはblogディレクトリを作成します。
├── blog
├── Cargo.lock
├── Cargo.toml
├── src
こんな感じで。
次にCargo.tomlに設定します。
[workspace]
members = ["blog"]
[dependencies]
.
.
.
blog = { path = "./blog" }
実装
ちなみにE2Eテストに関してはDay 19で既に書いています。
昨日設計したようにクリーンアーキテクチャで実装を進めていきます。
アーキテクチャ図
Domain層
こちらのStructを作成していきます。
Blog.idやBlog.title
といった各プロパティはバリューオブジェクトにしました。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BlogId(pub String);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BlogTitle(pub String);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BlogAuthor(pub String);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BlogBody(pub String);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BlogCreatedAt(pub String);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Blog {
pub id: BlogId,
pub title: BlogTitle,
pub author: BlogAuthor,
pub body: BlogBody,
pub created_at: BlogCreatedAt,
}
Rest層
上記のRest層をモジュラーモノリスにしつつ整理しました。
- Mainモジュール内の
main.rs
で同モジュール内のRest層のbuild
を実行します。 - その
build
は、Blogモジュール内のRest層に依存しています
ディレクトリ構造で表すと下のような感じですね
├── blog
| ├── Cargo.toml
| ├── rest
| | └── get_blogs.rs
| └── lib.rs
└── src
├── main.rs
└── rest
└── mod.rs (build)
これをもとに中身を実装を行いました。
実装(長いので閉まっちゃおうねぇ)
mod rest;
use rest::build;
#[tokio::main]
async fn main() {
let app = build();
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
use axum::{routing::get, Router};
use blog::rest::{get_blog_by_id, get_blogs};
pub fn build() -> Router {
Router::new().route("/blogs", get(get_blogs::run))
}
pub mod rest {
pub mod get_blogs;
}
use axum::Json;
use serde::Serialize;
#[derive(Serialize, PartialEq, Eq, Debug)]
#[serde(rename_all = "camelCase")]
pub struct BlogJson {
id: String,
title: String,
author: String,
body: String,
created_at: String,
}
#[derive(Serialize, PartialEq, Eq, Debug)]
#[serde(rename_all = "camelCase")]
pub struct BlogsJson {
blogs: Vec<BlogJson>,
}
impl From<Blog> for BlogJson {
fn from(blog: Blog) -> Self {
Self {
id: blog.id.0,
title: blog.title.0,
author: blog.author.0,
body: blog.body.0,
created_at: blog.created_at.0,
}
}
}
pub async fn run() -> Json<BlogsJson> {
todo!()
}
最後のtodo!()の部分で、Usecaseを呼び出す予定です。
Usecase層
次はUsecase層のこの部分を作っていきます
実装していくぅ!
実装(長いときは閉まっちゃおうねぇ)
use crate::domain::blog::Blog;
use axum::async_trait;
use eyre::Result;
use mockall::automock;
#[automock]
#[async_trait]
pub trait BlogPort {
async fn get(&self) -> Result<Vec<Blog>>;
}
use crate::{domain::blog::Blog, port::blog::BlogPort};
use eyre::Result;
pub async fn run(blog_port: impl BlogPort) -> Result<Vec<Blog>> {
blog_port.get().await
}
#[cfg(test)]
mod tests {
use crate::{
domain::blog::{BlogAuthor, BlogBody, BlogCreatedAt, BlogId, BlogTitle},
port::blog::MockBlogPort,
};
use eyre::Ok;
use super::*;
#[tokio::test]
async fn success() {
let expected = vec![Blog {
id: BlogId("id".to_string()),
title: BlogTitle("title".to_string()),
author: BlogAuthor("author".to_string()),
body: BlogBody("body".to_string()),
created_at: BlogCreatedAt("2023-11-11".to_string()),
}];
let expected_clone = expected.clone();
let mut mock_port = MockBlogPort::new();
mock_port
.expect_get()
.returning(move || Ok(expected_clone.clone()));
let actual = run(mock_port).await.unwrap();
assert_eq!(expected, actual)
}
}
Gateway層
先ほど作成したBlogPortを実装したクラスを作成します
BlogDriverのget関数をmockしたいので、mryというライブラリを使用します👍
こちらは私が所属するUzabase社の先輩が作成したライブラリで、structや、trait、functionを簡単にモックすることができるものです。
use axum::async_trait;
use eyre::Result;
use crate::{
domain::blog::{Blog, BlogAuthor, BlogBody, BlogCreatedAt, BlogId, BlogTitle},
driver::blog_driver,
port::blog::BlogPort,
};
pub struct BlogGateway;
#[async_trait]
impl BlogPort for BlogGateway {
async fn get(&self) -> Result<Vec<Blog>> {
let blog_json = blog_driver::get().await.unwrap();
Ok(blog_json
.blogs
.into_iter()
.map(|blog| Blog {
id: BlogId(blog.id.into()),
title: BlogTitle(blog.title.into()),
author: BlogAuthor(blog.author.into()),
body: BlogBody(blog.body.into()),
created_at: BlogCreatedAt(blog.created_at.into()),
})
.collect())
}
}
#[cfg(test)]
mod tests {
use eyre::Ok;
use super::*;
use crate::{
domain::blog::{BlogAuthor, BlogBody, BlogCreatedAt, BlogTitle},
driver::blog_driver::{get, mock_get, BlogJson, BlogsJson},
};
#[tokio::test]
#[mry::lock(get)]
async fn get_blogs_success() {
let expected = vec![Blog {
id: BlogId("id".to_string()),
title: BlogTitle("title".to_string()),
author: BlogAuthor("author".to_string()),
body: BlogBody("body".to_string()),
created_at: BlogCreatedAt("2023-11-11".to_string()),
}];
mock_get().returns_with(|| {
Ok(BlogsJson {
blogs: vec![BlogJson {
id: "id".to_string(),
title: "title".to_string(),
author: "author".to_string(),
body: "body".to_string(),
created_at: "2023-11-11".to_string(),
}],
})
});
let actual = BlogGateway.get().await.unwrap();
assert_eq!(expected, actual)
}
}
Driver層
use crate::entity::blog::Entity as BlogEntity;
use eyre::Result as EyreResult;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct BlogJson {
pub id: String,
pub title: String,
pub author: String,
pub body: String,
pub created_at: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct BlogsJson {
pub blogs: Vec<BlogJson>,
}
#[mry::mry]
pub async fn get() -> EyreResult<BlogsJson> {
todo!()
}
いったんDriver層はTodoにしておきます。
明日SeaORMを使ってDBと接続します。
Rest part 2
最後にRest層に戻り、usecaseを忘れずに呼び出します。
他はそのまま
pub async fn run() -> Json<BlogsJson> {
let blogs = get_blogs_usecase::run(BlogGateway).await.unwrap();
Json(BlogsJson {
blogs: blogs.into_iter().map(|blog| blog.into()).collect(),
})
}
これでDriver層のDB接続部分以外の実装が完了です。
明日はSeaORMを使って、DBからデータを取得できるようなDriverの実装を進めます。
補足:モジュラーモノリスの単体テストの回し方
workspaceのオプションを追加して実行します。
cargo test --workspace