使用 Rust 實作「短網址產生器」應用程式

建立專案

建立專案。

1
2
cargo new conifer
cd conifer

修改 Cargo.toml 檔,安裝依賴套件。

1
2
3
4
5
6
[dependencies]
diesel = { version = "2.0", features = ["postgres"] }
dotenvy = "0.15"
nanoid = "0.4"
rocket = { version = "0.5.0-rc.2", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }

實作

新增 .env 檔。

1
DATABASE_URL=postgres://postgres:[email protected]/records

使用 diesel 指令初始化,以建立 records 資料庫和預設的遷移資料表。

1
diesel setup

建立 records 資料表。

1
diesel migration generate create_records

修改 migrations/2022-10-12-133835_create_records/up.sql 檔。

1
2
3
4
CREATE TABLE records (
id VARCHAR(10) PRIMARY KEY,
url TEXT NOT NULL
);

修改 migrations/2022-10-12-133835_create_records/down.sql 檔。

1
DROP TABLE records;

執行遷移。

1
diesel migration run

建立 model.rs 檔。

1
2
3
4
5
6
7
8
9
use crate::schema::records;
use diesel::prelude::*;
use rocket::serde::{Deserialize, Serialize};

#[derive(Queryable, Insertable, Serialize, Deserialize)]
pub struct Record {
pub id: String,
pub url: String,
}

建立 repository.rs 檔。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
use crate::model::Record;
use crate::schema::records::dsl::{id, records};
use diesel::pg::PgConnection;
use diesel::prelude::*;
use diesel::result::Error;
use dotenvy::dotenv;
use nanoid::nanoid;
use std::env;

pub fn connect() -> PgConnection {
dotenv().ok();
let database_url = env::var("DATABASE_URL").unwrap();
PgConnection::establish(&database_url)
.unwrap_or_else(|_| panic!("Error connecting to {}", database_url))
}

pub fn get_records(conn: &mut PgConnection) -> Result<Option<Vec<Record>>, Error> {
records.get_results::<Record>(conn).optional()
}

pub fn get_record(conn: &mut PgConnection, _id: &str) -> Result<Option<Record>, Error> {
records
.filter(id.eq(_id))
.limit(1)
.get_result::<Record>(conn)
.optional()
}

pub fn store_record(conn: &mut PgConnection, _url: &str) -> Result<Option<Record>, Error> {
use crate::schema::records;
let record = Record {
id: nanoid!(10),
url: String::from(_url),
};

diesel::insert_into(records::table)
.values(&record)
.get_result::<Record>(conn)
.optional()
}

建立 main.rs 檔。

1
2
3
4
5
6
7
#[macro_use]
extern crate rocket;

#[launch]
fn rocket() -> _ {
conifer::rocket()
}

建立 lib.rs 檔。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#[macro_use]
extern crate rocket;
use crate::handler::{get_record, get_records, redirect, store_record};

mod handler;
mod model;
mod repository;
mod request;
mod response;
mod schema;

#[launch]
pub fn rocket() -> _ {
rocket::build()
.mount("/", routes![redirect])
.mount("/api", routes![get_records])
.mount("/api", routes![store_record])
.mount("/api", routes![get_record])
}

建立 handler.rs 檔。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
use crate::{
repository,
request::ReqStoreRecord,
response::{RespGetRecord, RespGetRecords, RespStoreRecord},
};
use rocket::{http::Status, response::Redirect, serde::json::Json};

#[get("/<id>")]
pub fn redirect(id: String) -> Result<Redirect, Status> {
let conn = &mut repository::connect();
let record = repository::get_record(conn, &id);
match record {
Ok(r) => match r {
Some(r) => Ok(Redirect::to(r.url)),
None => Err(Status::NotFound),
},
Err(e) => {
print!("{}", e);
Err(Status::InternalServerError)
}
}
}

#[get("/records")]
pub fn get_records() -> Result<Json<RespGetRecords>, Status> {
let conn = &mut repository::connect();
let records = repository::get_records(conn);
match records {
Ok(r) => match r {
Some(r) => Ok(Json(RespGetRecords { data: r })),
None => Err(Status::NotFound),
},
Err(e) => {
print!("{}", e);
Err(Status::InternalServerError)
}
}
}

#[post("/records", format = "json", data = "<req>")]
pub fn store_record(req: Json<ReqStoreRecord>) -> Result<Json<RespStoreRecord>, Status> {
let conn = &mut repository::connect();
let record = repository::store_record(conn, &req.url);
match record {
Ok(r) => match r {
Some(r) => Ok(Json(RespStoreRecord { data: r })),
None => Err(Status::NotFound),
},
Err(e) => {
print!("{}", e);
Err(Status::InternalServerError)
}
}
}

#[get("/records/<id>")]
pub fn get_record(id: &str) -> Result<Json<RespGetRecord>, Status> {
let conn = &mut repository::connect();
let record = repository::get_record(conn, id);
match record {
Ok(r) => match r {
Some(r) => Ok(Json(RespGetRecord { data: r })),
None => Err(Status::NotFound),
},
Err(e) => {
print!("{}", e);
Err(Status::InternalServerError)
}
}
}

建立 request.rs 檔。

1
2
3
4
5
6
use serde::Deserialize;

#[derive(Deserialize)]
pub struct ReqStoreRecord {
pub url: String,
}

建立 response.rs 檔。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
use crate::model::Record;
use diesel::Queryable;
use rocket::serde::Serialize;

#[derive(Queryable, Serialize)]
pub struct RespGetRecords {
pub data: Vec<Record>,
}

#[derive(Queryable, Serialize)]
pub struct RespStoreRecord {
pub data: Record,
}

#[derive(Queryable, Serialize)]
pub struct RespGetRecord {
pub data: Record,
}

啟動程式。

1
cargo run

參考資料