リファクタ

このページでは、ライブラリ化とモジュール化について学びます。

バイナリクレートとライブラリクレート

src/main.rs は特別なファイルであると以前説明しましたが、これは バイナリクレートクレートルートファイル と呼ばれています。

クレートとはパッケージ(他ユーザーや他パッケージに機能を提供する目的を持ったコード群)の中で、最も大きなプログラムです。下記2つの種類に分類できます。

  • バイナリクレート
    • ユーザーが実行できるプログラムで、1つのパッケージでいくつでも持てます。
  • ライブラリクレート
    • 自身のバイナリクレートや他のパッケージにロジックを提供するプログラムで、1つのパッケージで1つまでしか持てません。

クレートは複数のモジュールで構成することができます。そのため、クレートには予め起点となる特別なファイルが規定されており、これをクレートルートファイルと呼びます。

ライブラリクレートのクレートルートファイルは src/lib.rs となります。

なお、パッケージは1つ以上のクレートを持っている必要があります。

ロジックのライブラリクレート化

Rustではプログラムの実行に関する部分だけを src/main.rs に記述し、ロジックは src/lib.rs に記述する習慣があります。そうすることにより、

  • src/main.rs をプログラムの実行に関することだけに集中させることができ、コード量が小さくなる
  • テストコードが書けるようになる
    • src/main.rs はテストできない

などの恩恵があります。

では、早速 src/lib.rs を作成しましょう。といっても、ほとんどのコードを src/lib.rs に移動するだけです。

src/lib.rs
use askama::Template;
use axum::error_handling::HandleErrorExt;
use axum::extract::Form;
use axum::{
    body::{Bytes, Full},
    http::{Response, StatusCode},
    response::{Html, IntoResponse},
    routing::{get, post, service_method_routing},
    Router,
};
use serde::Deserialize;
use std::{convert::Infallible, net::SocketAddr};
use tower_http::services::ServeDir;

pub fn routes() {  // main.rsからアクセスできるようにするためにpubを付ける必要がある
    Router::new()
        .nest("/static", static_contents())
        .route("/", get(index))
        .route("/tweets/new", post(post_tweets))
}

async fn index() -> impl IntoResponse {
    HtmlTemplate(IndexView {
        tweets: vec![
            TweetView {
                name: "太郎".into(),
                message: "こんにちは!".into(),
                posted_at: "2020-01-01 12:34".into(),
            },
            TweetView {
                name: "次郎".into(),
                message: "こんにちは!こんにちは!!".into(),
                posted_at: "2020-01-02 12:34".into(),
            },
        ],
    })
}

async fn post_tweets(form: Form<TweetForm>) -> impl IntoResponse {
    HtmlTemplate(IndexView {
        tweets: vec![TweetView {
            name: form.name.clone(),
            message: form.message.clone(),
            posted_at: "2020-01-01 12:34".into(),
        }],
    })
}

pub fn static_contents() -> Router {
    Router::new().nest(
        "/",
        service_method_routing::get(ServeDir::new("static")).handle_error(
            |error: std::io::Error| {
                (
                    StatusCode::INTERNAL_SERVER_ERROR,
                    format!("Unhandled internal error: {}", error),
                )
            },
        ),
    )
}

#[derive(Template)]
#[template(path = "index.html")]
struct IndexView {
    tweets: Vec<TweetView>,
}

struct TweetView {
    name: String,
    message: String,
    posted_at: String,
}

#[derive(Deserialize)]
struct TweetForm {
    name: String,
    message: String,
}

struct HtmlTemplate<T>(T);

impl<T> IntoResponse for HtmlTemplate<T>
where
    T: Template,
{
    type Body = Full<Bytes>;
    type BodyError = Infallible;

    fn into_response(self) -> Response<Self::Body> {
        match self.0.render() {
            Ok(html) => Html(html).into_response(),
            Err(err) => Response::builder()
                .status(StatusCode::INTERNAL_SERVER_ERROR)
                .body(Full::from(format!(
                    "Failed to render template. Error: {}",
                    err
                )))
                .unwrap(),
        }
    }
}

コードを移動したので、 src/main.rs は、移動した関数の呼び出しのみになります。

src/main.rs
use std::net::SocketAddr;

#[tokio::main]
async fn main() {
    let routes = rustwi::routes();
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    println!("listening on {}", addr);
    axum::Server::bind(&addr)
        .serve(routes.into_make_service())
        .await
        .unwrap();
}

モジュール分割

しかし、 src/lib.rs に移動しただけでは今までとあまり変わりません。ここからさらに、いくつかのファイルに分割してみます。

手始めに、 HtmlTemplate をファイル化しましょう。

src/html_template.rs
use askama::Template;
use axum::body::{Bytes, Full};
use axum::http::{Response, StatusCode};
use axum::response::{Html, IntoResponse};
use std::convert::Infallible;

pub struct HtmlTemplate<T>(pub T);  // pubをつける

impl<T> IntoResponse for HtmlTemplate<T>
where
    T: Template,
{
    type Body = Full<Bytes>;
    type BodyError = Infallible;

    fn into_response(self) -> Response<Self::Body> {
        match self.0.render() {
            Ok(html) => Html(html).into_response(),
            Err(err) => Response::builder()
                .status(StatusCode::INTERNAL_SERVER_ERROR)
                .body(Full::from(format!(
                    "Failed to render template. Error: {}",
                    err
                )))
                .unwrap(),
        }
    }
}

Note: ファイル化した場合、他のファイルから利用する構造体や関数には pub キーワードが必要です。

src/lib.rs で先程ファイル化した内容を呼び出すためには src/lib.rsmod キーワードを使用します。これにより、先程作成したファイルがモジュールとして登録されます。

src/lib.rs
mod html_template;  // モジュール宣言する

use axum::extract::Form;
use axum::{
    routing::{get, post},
    Router,
};
use serde::Deserialize;
use std::net::SocketAddr;

use crate::html_template::HtmlTemplate;  // HtmlTemplateの使用を宣言する

pub fn routes() {  // main.rsからアクセスできるようにするためにpubを付ける必要がある
    Router::new()
        .nest("/static", static_contents())
        .route("/", get(index))
        .route("/tweets/new", post(post_tweets))
}

async fn index() -> impl IntoResponse {
    HtmlTemplate(IndexView {  // いままでどおりHtmlTemplateが使える
        tweets: vec![
            TweetView {
                name: "太郎".into(),
                message: "こんにちは!".into(),
                posted_at: "2020-01-01 12:34".into(),
            },
            TweetView {
                name: "次郎".into(),
                message: "こんにちは!こんにちは!!".into(),
                posted_at: "2020-01-02 12:34".into(),
            },
        ],
    })
}

async fn post_tweets(form: Form<TweetForm>) -> impl IntoResponse {
    HtmlTemplate(IndexView {
        tweets: vec![TweetView {
            name: form.name.clone(),
            message: form.message.clone(),
            posted_at: "2020-01-01 12:34".into(),
        }],
    })
}

fn static_contents() -> Router {
    Router::new().nest(
        "/",
        service_method_routing::get(ServeDir::new("static")).handle_error(
            |error: std::io::Error| {
                (
                    StatusCode::INTERNAL_SERVER_ERROR,
                    format!("Unhandled internal error: {}", error),
                )
            },
        ),
    )
}


#[derive(Template)]
#[template(path = "index.html")]
struct IndexView {
    tweets: Vec<TweetView>,
}

struct TweetView {
    name: String,
    message: String,
    posted_at: String,
}

#[derive(Deserialize)]
struct TweetForm {
    name: String,
    message: String,
}

このようにファイルとして切り出すことで、モジュールとして取り扱うことができます。

Note: ちなみに、ファイル化せず直接モジュールとして定義することもできます。

mod moduleA {
    pub fn funcA() {
        ...
    }
}

モジュールの階層管理

より大きな機能の場合、モジュールを階層化したい欲求がでてくるでしょう。Rustではモジュールを階層化することができます。

次に、 TweetForm をファイル化しましょう。フォームは今後いくつも作成する可能性があるため、 forms モジュール配下でそれぞれファイル化します。

src/forms/ ディレクトリを作成し、その中にファイルを作成します。

src/forms/tweet.rs
use serde::Deserialize;

#[derive(Deserialize)]
pub struct Tweet {  // formsモジュールに宣言しているため、名前からFormを削除した
    pub name: String,
    pub message: String,
}

このファイルをモジュールで扱うために src/forms.rs を作成します。

src/forms.rs
mod tweet;

pub use tweet::*;  // re-exports (pub useで「利用すること」自体を公開している)

Note: ディレクトリを作成した場所に、ディレクトリ名と同名でファイルを作成する必要があります。

3行目は re-exports と呼ばれるテクニックです。モジュールの階層は作成者が管理しやすいように構成するのが普通ですが、それが利用者にとっても都合がいいとは限りません。そのため、公開する内容を再構成する機能が提供されています。

これにより、Tweet は以下のいずれの方法でも利用可能となります。

let form1 = crate::forms::tweet::Tweet {name: "Bob", message: "Hello!"};
let form2 = crate::forms::Tweet {name: "Bob", message: "Hello!"};  // tweetの重複がなくなり記述がスッキリしている

あとは、先程と同じように src/lib.rsmod forms; を記載すれば利用可能となります。

役割ごとの階層化

下記のように役割を設定し、この単位でモジュール化してみます。

モジュール役割
コントローラーリクエストを受けてレスポンスを返す部分を実装する
エンティティビジネスモデルを実装する
フォームフォームデータのデータ構造を定義する
ビューレスポンスのデータ構造を定義する

src/ はこのように階層化できます。

src
├── entities
│   └── tweet.rs
├── entities.rs
├── forms
│   └── tweet.rs
├── forms.rs
├── lib.rs
├── main.rs
├── controllers
│   ├── root.rs
│   ├── static_contents.rs
│   └── tweets.rs
├── controllers.rs
├── views
│   ├── html_template.rs
│   └── index.rs
└── views.rs

Note: HtmlTemplateはsrc/viewsのディレクトリに移動しました。

残りのファイルについても掲載しておきます。

src/controllers.rs
mod root;
mod static_contents;
mod tweets;

use axum::Router;

pub fn routes() -> Router {
    Router::new()
        .nest("/", root::root())
        .nest("/static", static_contents::static_contents())
        .nest("/tweets", tweets::tweets())
}
src/controllers/root.rs
use crate::entities::Tweet;
use crate::views::Index;
use axum::response::IntoResponse;
use axum::routing;
use axum::Router;

pub fn root() -> Router {
    Router::new().route("/", routing::get(get))
}

async fn get() -> impl IntoResponse {
    Index::new(vec![
        Tweet::new(
            "太郎".into(),
            "こんにちは!".into(),
            "2020-01-01 12:34".into(),
        ),
        Tweet::new(
            "次郎".into(),
            "こんにちは!こんにちは!!".into(),
            "2020-01-02 12:34".into(),
        ),
    ])
    .into_html()
}
src/controllers/static_contents.rs
use axum::error_handling::HandleErrorExt;
use axum::http::StatusCode;
use axum::routing::service_method_routing;
use axum::Router;
use tower_http::services::ServeDir;

pub fn static_contents() -> Router {
    Router::new().nest(
        "/",
        service_method_routing::get(ServeDir::new("static")).handle_error(
            |error: std::io::Error| {
                (
                    StatusCode::INTERNAL_SERVER_ERROR,
                    format!("Unhandled internal error: {}", error),
                )
            },
        ),
    )
}
src/controllers/tweets.rs
use crate::entities::Tweet as TweetEntity;
use crate::forms::Tweet as TweetForm;
use crate::views::Index;
use axum::extract::Form;
use axum::response::IntoResponse;
use axum::routing;
use axum::Router;

pub fn tweets() -> Router {
    Router::new().route("/new", routing::post(post))
}

async fn post(form: Form<TweetForm>) -> impl IntoResponse {
    Index::new(vec![TweetEntity::new(
        form.name.clone(),
        form.message.clone(),
        "2020-01-01 12:34".into(),
    )])
    .into_html()
}
src/views.rs
mod index;

pub use index::*;
src/views/index.rs
use super::html_template::HtmlTemplate;
use crate::entities::Tweet;
use askama::Template;

#[derive(Template)]
#[template(path = "index.html")]
pub struct Index {
    pub tweets: Vec<Tweet>,
}

impl Index {
    pub fn new(tweets: Vec<Tweet>) -> Index {
        Index { tweets }
    }

    pub fn into_html(self) -> HtmlTemplate<Index> {
        HtmlTemplate(self)
    }
}
src/entities.rs
mod tweet;

pub use tweet::*;
src/entities/tweet.rs
pub struct Tweet {
    pub name: String,
    pub message: String,
    pub posted_at: String,
}

impl Tweet {
    pub fn new(name: String, message: String, posted_at: String) -> Tweet {
        Tweet {
            name,
            message,
            posted_at,
        }
    }
}
src/lib.rs
mod entities;
mod forms;
mod controllers;
mod views;

pub use services::routes;