Skip to main content

メインプログラムをライブラリにする

このまま書いていくと、どんどん src/main.rs が長くなり、メンテナンスが困難になってきます。そこで、今のうちにより小さな単位に分割しておくことにします。

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

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

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

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

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

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

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

ロジックをライブラリクレートに移動する#

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

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

などの恩恵があります。

では、早速 src/lib.rs を作成しましょう。

src/lib.rs
#![feature(proc_macro_hygiene, decl_macro)]
#[macro_use] extern crate rocket;extern crate rocket_contrib;extern crate chrono;#[macro_use] extern crate serde;
use rocket::request::Form;use rocket_contrib::templates::Template;use rocket_contrib::templates::handlebars::{Context, Handlebars, Helper, HelperResult, Output, RenderContext};use chrono::{DateTime, Local, TimeZone};
#[derive(Serialize)]struct Tweet {    account_name: String,    message: String,    posted_at: DateTime<Local>}
#[derive(FromForm)]struct NewTweet {    message: String}
#[get("/")]fn index() -> Template {    let context = vec![        Tweet {            account_name: "はなこ".into(),            message: "2回目のツイート".into(),            posted_at: Local.ymd(2020, 2, 20).and_hms(0, 0, 0)        },        Tweet {            account_name: "はなこ".into(),            message: "はじめてのツイート".into(),            posted_at: Local.ymd(2021, 2, 18).and_hms(0, 0, 0)        }    ];    Template::render("index", &context)}
#[post("/", data = "<form>")]fn post_tweet(form: Form<NewTweet>) -> Template {    let context = vec![        Tweet {            account_name: "はなこ".into(),            message: form.message.clone(),            posted_at: Local.ymd(2020, 2, 20).and_hms(0, 0, 0)        }    ];    Template::render("index", &context)}
fn datetimeformat(    h: &Helper,    _: &Handlebars,    _: &Context,    _: &mut RenderContext,    out: &mut dyn Output,) -> HelperResult {    let datetime = h.param(0).and_then(|dt| {        dt.value().as_str().and_then(|dt| DateTime::parse_from_rfc3339(dt).ok())    });    let format = h.param(1).and_then(|x| x.value().as_str());    if let (Some(datetime), Some(format)) = (datetime, format) {        let _ = out.write(datetime.format(format).to_string().as_str());    }    Ok(())}
pub fn run() { // --(1)    rocket::ignite()        .mount("/", routes![index])        .mount("/tweets", routes![post_tweet])        .attach(Template::custom(|e| {            e.handlebars.register_helper("datetimeformat", Box::new(datetimeformat));        }))        .launch();}

実は、いままで src/main.rs に書いていた内容をほとんどコピーしただけです。注意点としては、(1)fnpub fn に変更します。

次に src/main.rs も変更します。

src/main.rs
extern crate rustwi; // --(1)
fn main() {    rustwi::run();}

なんと、5行になってしまいました。 (1) は、他のクレートをインポートして利用するという宣言です。今回は同じパッケージのライブラリクレートを利用するため、パッケージ名を指定します。

これでも、今まで通り動作させることができているはずです。

モジュールをファイルに分割する#

このままではまだメンテナンス性が悪いため、ライブラリクレートを、さらにモジュールと呼ばれる単位に分割してみます。

現在の実装状況を下記の役割に整理してみます。

コントローラーユーザーのリクエストを受けてサービスを起動し、レスポンスを返す
サービスユースケースを実装する
エンティティビジネスモデルを実装する
フォームリクエストデータを実装する

まずは、 src/lib.rs から「ユーザーのリクエストを受けてサービスを起動し、レスポンスを返す」部分を切り出して controllers モジュールを作成してみましょう。 ファイルはsrc/controllers.rs となります。

src/controllers.rs
use rocket::request::Form;use rocket_contrib::templates::Template;use chrono::{DateTime, Local, TimeZone};
#[derive(Serialize)]pub struct Tweet {    pub account_name: String,    pub message: String,    pub posted_at: DateTime<Local>}
#[derive(FromForm)]pub struct NewTweet {    pub message: String}
#[get("/")]pub fn index() -> Template {    let context = vec![        Tweet {            account_name: "はなこ".into(),            message: "2回目のツイート".into(),            posted_at: Local.ymd(2020, 2, 20).and_hms(0, 0, 0)        },        Tweet {            account_name: "はなこ".into(),            message: "はじめてのツイート".into(),            posted_at: Local.ymd(2021, 2, 18).and_hms(0, 0, 0)        }    ];    Template::render("index", &context)}
#[post("/", data = "<form>")]pub fn post_tweet(form: Form<NewTweet>) -> Template {    let context = vec![        Tweet {            account_name: "はなこ".into(),            message: form.message.clone(),            posted_at: Local.ymd(2020, 2, 20).and_hms(0, 0, 0)        }    ];    Template::render("index", &context)}
caution
  • 構造体、構造体のフィールド、関数に pub をつけ忘れないようにしてください
  • ここで使うライブラリクレートがあったとしても、ここではなく src/lib.rs にて extern crate 宣言をします

これで controllers モジュールに処理を移行したので、もとの src/lib.rs からは記載を削除します。

src/lib.rs
  #![feature(proc_macro_hygiene, decl_macro)]  + mod controllers; // --(1)    #[macro_use] extern crate rocket;  extern crate rocket_contrib;  extern crate chrono;  #[macro_use] extern crate serde;    use rocket::request::Form;  use rocket_contrib::templates::Template;  use rocket_contrib::templates::handlebars::{Context, Handlebars, Helper, HelperResult, Output, RenderContext};  use chrono::{DateTime, Local, TimeZone};- - #[derive(Serialize)]- struct Tweet {-     account_name: String,-     message: String,-     posted_at: DateTime<Local>- }- - #[derive(FromForm)]- struct NewTweet {-     message: String- }- - #[get("/")]- fn index() -> Template {-     let context = vec![-         Tweet {-             account_name: "はなこ".into(),-             message: "2回目のツイート".into(),-             posted_at: Local.ymd(2020, 2, 20).and_hms(0, 0, 0)-         },-         Tweet {-             account_name: "はなこ".into(),-             message: "はじめてのツイート".into(),-             posted_at: Local.ymd(2021, 2, 18).and_hms(0, 0, 0)-         }-     ];-     Template::render("index", &context)- }- - #[post("/", data = "<form>")]- fn post_tweet(form: Form<NewTweet>) -> Template {-     let context = vec![-         Tweet {-             account_name: "はなこ".into(),-             message: form.message.clone(),-             posted_at: Local.ymd(2020, 2, 20).and_hms(0, 0, 0)-         }-     ];-     Template::render("index", &context)- }    fn datetimeformat(      h: &Helper,      _: &Handlebars,      _: &Context,      _: &mut RenderContext,      out: &mut dyn Output,  ) -> HelperResult {      let datetime = h.param(0).and_then(|dt| {          dt.value().as_str().and_then(|dt| DateTime::parse_from_rfc3339(dt).ok())      });      let format = h.param(1).and_then(|x| x.value().as_str());      if let (Some(datetime), Some(format)) = (datetime, format) {          let _ = out.write(datetime.format(format).to_string().as_str());      }      Ok(())  }    pub fn run() {      rocket::ignite()-         .mount("/", routes![index])-         .mount("/tweets", routes![post_tweet])+         .mount("/", routes![controllers::index])+         .mount("/tweets", routes![controllers::post_tweet])          .attach(Template::custom(|e| {              e.handlebars.register_helper("datetimeformat", Box::new(datetimeformat));          }))          .launch();  }

(1)controller モジュールが別ファイル( src/controllers.rs )に存在していることを示唆します。ファイル名は一致している必要があります。

info
mod controllers;

ではなく、

mod controllers {  ...}

とすることで、直接モジュールを定義することもできます。

さらに、 controllers モジュールからユースケースを抜き出して service モジュールにしてみます。

src/services.rs
use chrono::{DateTime, Local, TimeZone};
#[derive(Serialize)]pub struct Tweet {    pub account_name: String,    pub message: String,    pub posted_at: DateTime<Local>}
pub fn list_tweets() -> Vec<Tweet> {    vec![        Tweet {            account_name: "はなこ".into(),            message: "2回目のツイート".into(),            posted_at: Local.ymd(2020, 2, 20).and_hms(0, 0, 0)        },        Tweet {            account_name: "はなこ".into(),            message: "はじめてのツイート".into(),            posted_at: Local.ymd(2021, 2, 18).and_hms(0, 0, 0)        }    ]}
pub fn post_tweet(tweet: crate::controllers::NewTweet) -> Tweet {    Tweet {        account_name: "はなこ".into(),        message: tweet.message.clone(),        posted_at: Local.ymd(2020, 2, 20).and_hms(0, 0, 0)    }}

controllers モジュールはリクエストデータの前処理とレスポンス用のHTMLを作成する役割にフォーカスします。

src/controllers.rs
  use rocket::request::Form;  use rocket_contrib::templates::Template;- use chrono::{DateTime, Local, TimeZone};
+ use crate::services; // --(1)
- #[derive(Serialize)]- pub struct Tweet {-     pub account_name: String,-     pub message: String,-     pub posted_at: DateTime<Local>- }
  #[derive(FromForm)]  pub struct NewTweet {      pub message: String  }
  #[get("/")]  pub fn index() -> Template {-     let context = vec![-         Tweet {-             account_name: "はなこ".into(),-             message: "2回目のツイート".into(),-             posted_at: Local.ymd(2020, 2, 20).and_hms(0, 0, 0)-         },-         Tweet {-             account_name: "はなこ".into(),-             message: "はじめてのツイート".into(),-             posted_at: Local.ymd(2021, 2, 18).and_hms(0, 0, 0)-         }-     ];+     let context = services::list_tweets();      Template::render("index", &context)  }
  #[post("/", data = "<form>")]  pub fn post_tweet(form: Form<NewTweet>) -> Template {-     let context = vec![-         Tweet {-             account_name: "はなこ".into(),-             message: form.message.clone(),-             posted_at: Local.ymd(2020, 2, 20).and_hms(0, 0, 0)-         }-     ];+     let tweet = services::post_tweet(form.0);+     let context = vec![tweet];      Template::render("index", &context)  }

同じ階層にある別のモジュールを使用するために、 (1)crate を使用しています。これは、クレートルートファイルを示しており、そこから services を辿っています。

src/lib.rs では、 services モジュールも存在することを宣言します。

src/lib.rs
  #![feature(proc_macro_hygiene, decl_macro)]    mod controllers;+ mod services;    #[macro_use] extern crate rocket;  extern crate rocket_contrib;
...(省略)...

モジュールをディレクトリで管理する#

controllers モジュールにある indexpost_tweet はパス階層が異なるため、これもファイルに分割してみましょう。 そのために、 controllers ディレクトリを作成し、各々のコントローラを作成します。

src/controllers/index.rs
use rocket_contrib::templates::Template;use crate::services;
#[get("/")]pub fn index() -> Template {    let context = services::list_tweets();    Template::render("index", &context)}
src/controllers/tweets.rs
use rocket_contrib::templates::Template;use rocket::request::Form;use crate::services;use crate::controllers;
#[post("/", data = "<form>")]pub fn post_tweet(form: Form<controllers::NewTweet>) -> Template {    let tweet = services::post_tweet(form.0);    let context = vec![tweet];    Template::render("index", &context)}

controllers モジュールも修正します。

src/controllers.rs
- use rocket::request::Form;- use rocket_contrib::templates::Template;- - use crate::services;+ pub mod index; // --(1)+ pub mod tweets; // --(1)
  #[derive(FromForm)]  pub struct NewTweet {      pub message: String  }- - #[get("/")]- pub fn index() -> Template {-   let context = services::list_tweets();-   Template::render("index", &context)- }-- #[post("/", data = "<form>")]- pub fn post_tweet(form: Form<NewTweet>) -> Template {-   let tweet = services::post_tweet(form.0);-   let context = vec![tweet];-   Template::render("index", &context)- }

(1) でモジュールが存在することを宣言しつつ、さらに外部にこのモジュールを公開しています。

階層が1段階深くなったので、 src/lib.rs も修正します。

src/lib.rs
...(省略)...
  pub fn run() {      rocket::ignite()-         .mount("/", routes![controllers::index])-         .mount("/tweets", routes![controllers::post_tweet])+         .mount("/", routes![controllers::index::index])+         .mount("/tweets", routes![controllers::tweets::post_tweet])          .attach(Template::custom(|e| {              e.handlebars.register_helper("datetimeformat", Box::new(datetimeformat));          }))          .launch();  }

エンティティとフォームの作成#

このページの最後のタスクとして、エンティティとフォームもモジュールにしてみましょう。

src/entities.rs
use chrono::{DateTime, Local};
#[derive(Serialize)]pub struct Tweet {    pub account_name: String,    pub message: String,    pub posted_at: DateTime<Local>}
src/forms.rs
#[derive(FromForm)]pub struct NewTweet {    pub message: String}
src/controllers/tweets.rs
  use rocket_contrib::templates::Template;  use rocket::request::Form;  use crate::services;- use crate::controllers;+ use crate::forms;    #[post("/", data = "<form>")]- pub fn post_tweet(form: Form<controllers::NewTweet>) -> Template {+ pub fn post_tweet(form: Form<forms::NewTweet>) -> Template {      let tweet = services::post_tweet(form.0);      let context = vec![tweet];      Template::render("index", &context)  }
src/controllers.rs
  pub mod index;  pub mod tweets;-- #[derive(FromForm)]- pub struct NewTweet {-     pub message: String- }
src/services.rs
- use chrono::{DateTime, Local, TimeZone};+ use chrono::{Local, TimeZone};+ use crate::entities;+ use crate::forms;-- #[derive(Serialize)]- pub struct Tweet {-     pub account_name: String,-     pub message: String,-     pub posted_at: DateTime<Local>- }  - pub fn list_tweets() -> Vec<Tweet> {+ pub fn list_tweets() -> Vec<entities::Tweet> {      vec![-         Tweet {+         entities::Tweet {              account_name: "はなこ".into(),              message: "2回目のツイート".into(),              posted_at: Local.ymd(2020, 2, 20).and_hms(0, 0, 0)          },-         Tweet {+         entities::Tweet {              account_name: "はなこ".into(),              message: "はじめてのツイート".into(),              posted_at: Local.ymd(2021, 2, 18).and_hms(0, 0, 0)          }      ]  }  - pub fn post_tweet(tweet: crate::controllers::NewTweet) -> Tweet {+ pub fn post_tweet(tweet: forms::NewTweet) -> entities::Tweet {-     Tweet {+     entities::Tweet {          account_name: "はなこ".into(),          message: tweet.message.clone(),          posted_at: Local.ymd(2020, 2, 20).and_hms(0, 0, 0)      }  }
src/lib.rs
  #![feature(proc_macro_hygiene, decl_macro)]    mod controllers;  mod services;+ mod entities;+ mod forms;    #[macro_use] extern crate rocket;...(省略)...

次の章では、データを保存するための準備をします。