3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Webアプリ構築カレンダーAdvent Calendar 2023

Day 22

【Day 22】ブログ一覧を取得し、表示する - Rust Axum

Last updated at Posted at 2023-12-21

はじめに

スライド23.PNG


2023年アドベントカレンダー22日目です。

本日は昨日に引き続き、「ユーザーはトップページでブログタイトルの一覧を見ることができる」を進めていきます。

image.png

昨日はアーキテクチャの設計周りをしてきたので、本日は実装を進めていきます。

Axum

Axum は Rust プログラミング言語で使用されるウェブフレームワークです。

今回はこちらを使ってAPIを作成していこうと思います。

Hello World

まずは依存を追加します。

Cargo.toml
[dependencies]
axum = "0.7.1"
tokio = { version = "1.33.0", features = ["full"] }

公式に従い、まずはGET: http://localhost:3000/で、Hello Worldだけ出せるようにしていきます。

src/main.rsを簡単に実装してみます。

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に設定します。

Cargo.toml
[workspace]
members = ["blog"]

[dependencies]
.
.
.

blog = { path = "./blog" }

実装

ちなみにE2Eテストに関してはDay 19で既に書いています。

昨日設計したようにクリーンアーキテクチャで実装を進めていきます。

アーキテクチャ図

Domain層

こちらのStructを作成していきます。

Blog.idやBlog.titleといった各プロパティはバリューオブジェクトにしました。

blog/src/domain/blog.rs
#[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層をモジュラーモノリスにしつつ整理しました。

  1. Mainモジュール内のmain.rsで同モジュール内のRest層のbuildを実行します。
  2. そのbuildは、Blogモジュール内のRest層に依存しています

ディレクトリ構造で表すと下のような感じですね

├── blog
|   ├── Cargo.toml
|   ├── rest
|   |   └── get_blogs.rs
|   └── lib.rs
└── src
    ├── main.rs
    └── rest
        └── mod.rs (build)

これをもとに中身を実装を行いました。

実装(長いので閉まっちゃおうねぇ)
src/main.rs
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();
}
src/rest/mod.rs
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))
}
blog/lib.rs
pub mod rest {
    pub mod get_blogs;
}
blog/src/rest/get_blogs.rs
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層のこの部分を作っていきます

実装していくぅ!

本記事では表現がしにくいですが、TDDで開発はすすめていきます。
image.png

実装(長いときは閉まっちゃおうねぇ)
blog/src/port/blog.rs
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>>;
}
blog/src/usecase/get_blogs_usecase.rs
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を簡単にモックすることができるものです。

blog/src/gateway/blog_gateway.rs
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層

blog/src/driver/blog_driver.rs
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を忘れずに呼び出します。

他はそのまま

blog/src/rest/get_blogs.rs
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
3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?