今天要對程式碼做大量的調整,所以會看到很多重複的程式碼請見諒。
到目前為止的程式碼都放在main.rs
裡面:
use axum::{http::StatusCode, routing::get, Form, Router};
use serde::Deserialize;
#[tokio::main]
async fn main() {
// build our application with a single route
let app = Router::new().route("/", get(|| async { "Hello, World!" }));
// run it with hyper on localhost:3000
axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
}
pub async fn health_check() -> StatusCode {
StatusCode::OK
}
#[derive(Deserialize)]
pub struct NewSubscriber {
/// Subscriber Email
pub email: Option<String>,
/// Subscriber Name
pub name: Option<String>,
}
pub async fn subscribe(Form(data): Form<NewSubscriber>) -> StatusCode {
if data.email.is_some() && data.name.is_some() {
StatusCode::OK
} else {
StatusCode::BAD_REQUEST
}
}
這樣的程式碼有點雜亂無章,而且可以想見在增加更多功能後,整個專案會變得難以管理,不過在昨天我們已經準備好了整合測試,就可以在測試保護的前提下對專案的結構做整理。
首先把Router
的部份抽成build
方法,啟動Server
的部份抽成run
方法,這時候main裡面只剩下:
//// main.rs
#[tokio::main]
async fn main() {
let app = build();
run(app).await;
}
這一步的目的是為了把運行的入口點與程式碼的核心邏輯拆開來。
接下來建立lib.rs
並把所有的東西都移進去:
//// lib.rs
use axum::{Form, Router};
use axum::routing::get;
use serde::Deserialize;
use axum::http::StatusCode;
pub async fn run(app: Router) {
axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
}
pub fn build() -> Router {
let app = Router::new().route("/", get(|| async { "Hello, World!" }));
app
}
pub async fn health_check() -> StatusCode {
StatusCode::OK
}
#[derive(Deserialize)]
pub struct NewSubscriber {
/// Subscriber Email
pub email: Option<String>,
/// Subscriber Name
pub name: Option<String>,
}
pub async fn subscribe(Form(data): Form<NewSubscriber>) -> StatusCode {
if data.email.is_some() && data.name.is_some() {
StatusCode::OK
} else {
StatusCode::BAD_REQUEST
}
}
這時候下達cargo build
指令,突然發現編譯不過:
❯ cargo run
Compiling zero_to_production v0.1.0 (/home/marvinhsu/Documents/test/zero_to_production)
error[E0425]: cannot find function `build` in this scope
--> src/main.rs:5:15
|
5 | let app = build();
| ^^^^^ not found in this scope
|
help: consider importing this function
|
3 + use zero_to_production::build;
|
error[E0425]: cannot find function `run` in this scope
--> src/main.rs:6:5
|
6 | run(app).await;
| ^^^ not found in this scope
|
help: consider importing this function
|
3 + use zero_to_production::run;
|
For more information about this error, try `rustc --explain E0425`.
error: could not compile `zero_to_production` (bin "zero_to_production") due to 2 previous errors
rust優秀的編譯器告訴你,原來少加了方法的參考,所以再調整一下main.rs
//// main.rs
use zero_to_production::{build, run};
#[tokio::main]
async fn main() {
let app = build();
run(app).await;
}
到這邊先暫停一下,我們來了解一下發生什麼事。
目前的檔案結構如下:
.
├── Cargo.lock
├── Cargo.toml
└── src
├── lib.rs
└── main.rs
我們的zero_to_production專案就是一個套件(Package),套件的名稱被定義在cargo.toml
中。crate是rust編譯的最小單位,原則上一個檔案就會是一個crate,一個套件中可以多個執行檔crate(main.rs),但只能有一個函式庫crate(lib.rs),在編譯的時候cargo默認會由執行檔crate開始,細節可以藉由cargo.toml
調整(文件)。
目前我們把程式碼一股腦塞在lib.rs
中,接下來要進一步模組化,這部份就是依照團隊或個人的偏好。首先我想先把handler拆出來,所以建立一個資料夾並在裡面建立health_check.rs
與subscription.rs
,並把handler函數各自放入,另外也把NewSubscriber
這個結構放到subscription.rs
,這時候編譯當然不會過。
在我們要使用模組來組織我們的程式碼,rust會尋找mod.rs
或是把資料夾作為一個模組。首先在handler資料夾中加入mod.rs
//// mod.rs
mod health_check;
mod subscribe;
pub use health_check::*;
pub use subscribe::*;
在這邊我就組織了handler模組,接下來在lib.rs
一開始添加參考就可以順利編譯了
//// lib.rs
mod handler;
use handler::*;
稍微講解一下mod health_check
就是把health_check放到handler模組下面,
pub use health_check::*
則是把health_check.rs公開的東西暴露出模組,模組還有其他種撰寫方式,只是這個方法可以自行決定模組的公開程度(我這邊使用*只是因為懶而已,實際上應該要仔細思考模組要暴露的部份)。至於lib.rs
中只用了use handler::*
是因為目前的程式碼都在lib內,實在不需要再對外公開。
目前我們的檔案結構如下:
.
├── Cargo.lock
├── Cargo.toml
└── src
├── handler
│ ├── health_check.rs
│ ├── mod.rs
│ └── subscribe.rs
├── lib.rs
└── main.rs
進行到這裡可以跑一下整合測試確保一切正常,明天仍然會繼續重構,我希望可以融入最近讀到模組設計的觀念。