
如何快速實現REST API集成以優化業務流程
cargo --version
如果未安裝 Rust,請按照 Rust 官方網站上的說明進行操作。
讓我們從使用 cargo 來搭建新項目的基架開始:
cargo new rust-web-server-tutorial
然后,打開并添加以下依賴項:
[dependencies]
actix-web = "4"
chrono = "0.4.37"
futures-util = "0.3.30"
mongodb = "2.8.2"
serde = "1.0.197"
我們已經討論了選擇 Actix 和 MongoDB 的原因。接下來,讓我們看看其他依賴項的作用:
chrono
crate 來將字符串解析為日期格式。futures-util
crate 將幫助我們處理 MongoDB 的 Cursor
結構,它實現了 Stream
trait,并在我們獲取多個文檔時派上用場。serde
,這是一個非常流行的 crate,用于序列化和反序列化 Rust 數據結構。它允許我們將 Rust 結構轉換為可以通過網絡發送的格式(如 JSON),并將來自網絡的數據轉換為我們的 Rust 數據結構。現在,讓我們在入口點文件 main.rs
中添加一些新的模板代碼,以便 main
函數能夠返回一個 Actix HTTP 服務器,并將其綁定到 http://localhost:5001
。
use actix_web::{get, App, HttpResponse, HttpServer, Responder};
#[get("/")]
async fn hello() -> impl Responder {
HttpResponse::Ok().body("Hello Medium!")
}
#[actix_web::main]
async fn main() -> std::io::Result() {
HttpServer::new(|| App::new().service(hello))
.bind(("localhost", 5001))?
.run()
.await
}
在這里,您將看到我們正在利用一個路由來運行 HTTP 服務器。我們借助 Actix 提供的宏來定義路由的方法及其對應的路徑。在本例中,我們使用了 get
方法和根路徑 "/"
。
最后,讓我們來運行這個項目。在開發 Rust 項目時,我通常喜歡使用以下命令:
cargo watch -c -w src -x run
以下是配置標志的作用:
-c
或 --clear
標志用于在每次更改之間清除屏幕,以保持輸出的整潔。-w src
或 --watch src
告訴 cargo watch
只關注 src
目錄中的文件變化。-x run
表示 watch
命令僅在檢測到文件更改時運行 cargo run
。現在,如果您訪問 http://localhost:5001
,應該能看到文本 “Hello Medium!”。
如果您是 Rust 的新手,可能會發現滿足編譯器要求需要比使用大多數其他語言更多的時間。但請放心,這通常會帶來更加穩健的代碼!
在開發過程中,如果您想運行某個特定的函數,可以將其導入到 main.rs
中,并在 main
函數中調用它。為了方便調試,我已經在所有的數據結構中實現了 Debug
trait,因此您可以使用 dbg!
宏來查看大多數變量的內容。
您可以使用 cURL、Postman、Insomnia 等工具來測試 API 終端節點。在本文末尾,我提供了一些示例 cURL 命令供您參考。
我們將把我們的計劃分為三個主要領域:
models
:用于定義在數據庫中使用的數據結構以及處理 HTTP 請求和響應的數據結構。routes
:在這里,我們定義 API 的端點、方法和路徑,并負責將請求的 JSON 數據轉換為我們的數據結構。services
:此部分包含初始化數據庫的代碼以及與數據庫交互的邏輯。這些服務可以從我們的路由中調用。按照這樣的規劃,我們的文件系統結構應如下所示。
rust-web-server-tutorial/
└── src/
├── main.rs
├── models/
│ ├── booking_model.rs
│ ├── dog_model.rs
│ ├── mod.rs
│ └── owner_model.rs
├── routes/
│ ├── booking_route.rs
│ ├── dog_route.rs
│ ├── mod.rs
│ └── owner_route.rs
└── services/
├── db.rs
└── mod.rs
要快速設置,請在項目根目錄中運行以下 shell 命令:
cd src && \
mkdir models routes services && \
touch models/booking_model.rs models/dog_model.rs models/mod.rs models/owner_model.rs && \
touch routes/booking_route.rs routes/dog_route.rs routes/mod.rs routes/owner_route.rs && \
touch services/db.rs services/mod.rs && \
cd ..
首先,我們需要明確數據庫中所需數據的結構,以及端點請求正文中將支持的內容格式。
讓我們從 Booking
模型開始設計。
// booking_model.rs
use mongodb::bson::{oid::ObjectId, DateTime};
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize)]
pub struct Booking {
pub _id: ObjectId,
pub owner: ObjectId,
pub start_time: DateTime,
pub duration_in_minutes: u8,
pub cancelled: bool,
}
這表示我們在數據庫中希望存儲的內容有一定的格式。但是,對于最終用戶來說,我們期望他們通過HTTP請求正文以不同的方式提交數據。具體來說,用戶在創建新預訂時,我們不希望他們自行指定 _id
字段。此外,為了提升用戶體驗,我們希望簡化數據類型,允許最終用戶以字符串形式提交 start_time
,并可以直接指定 owner
。
為了達到這一目的,我們將創建一個?BookingRequest
?結構體,并編寫一個從?BookingRequest
?轉換到?Booking
?的邏輯,這一邏輯將通過實現?TryFrom<BookingRequest>
?trait 來完成。
// booking_model.rs
use std::{convert::TryFrom, time::SystemTime};
use chrono::Utc;
use mongodb::bson::{oid::ObjectId, DateTime};
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize)]
pub struct Booking {
pub _id: ObjectId,
pub owner: ObjectId,
pub start_time: DateTime,
pub duration_in_minutes: u8,
pub cancelled: bool,
}
#[derive(Debug, Deserialize)]
pub struct BookingRequest {
pub owner: String,
pub start_time: String,
pub duration_in_minutes: u8,
}
impl TryFromBookingRequest for Booking {
type Error = Boxdyn std::error::Error;
fn try_from(item: BookingRequest) -> ResultSelf, Self::Error {
let chrono_datetime: SystemTime = chrono::DateTime::parse_from_rfc3339(&item.start_time)
.map_err(|err| format!("Failed to parse start_time: {}", err))?
.with_timezone(&Utc)
.into();
Ok(Self {
_id: ObjectId::new(),
owner: ObjectId::parse_str(&item.owner).expect("Failed to parse owner"),
start_time: DateTime::from(chrono_datetime),
duration_in_minutes: item.duration_in_minutes,
cancelled: false,
})
}
}
確實,選擇使用 TryFrom
而不是 From
是明智的,因為處理來自第三方的數據時,轉換過程總是存在失敗的可能性,而 From
轉換要求總是能夠成功。
關于 Box<dyn std::error::Error>
,這是一個值得關注的錯誤類型。它是一個 trait 對象,能夠存儲任何實現了 std::error::Error
trait 的類型。這種錯誤類型非常通用,能夠表示多種不同類型的錯誤。在函數可能以多種方式失敗,而我們又不想手動處理每個可能的失敗情況時,它就顯得特別有用。
我們的工作還遠未結束!
展望未來,我們明白需要構建一個端點,用于返回包含所有相關主人和狗信息的預訂詳情。我們可以將這個新的數據結構命名為?FullBooking
,并將其添加到我們的文件中。添加完這個新結構后,我們的整體設計將如下所示:
// booking_model.rs
use std::{convert::TryFrom, time::SystemTime};
use super::{dog_model::Dog, owner_model::Owner};
use chrono::Utc;
use mongodb::bson::{oid::ObjectId, DateTime};
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize)]
pub struct Booking {
pub _id: ObjectId,
pub owner: ObjectId,
pub start_time: DateTime,
pub duration_in_minutes: u8,
pub cancelled: bool,
}
#[derive(Debug, Deserialize)]
pub struct BookingRequest {
pub owner: String,
pub start_time: String,
pub duration_in_minutes: u8,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct FullBooking {
pub _id: ObjectId,
pub owner: Owner,
pub dogs: VecDog,
pub start_time: DateTime,
pub duration_in_minutes: u8,
pub cancelled: bool,
}
impl TryFromBookingRequest for Booking {
type Error = Boxdyn std::error::Error;
fn try_from(item: BookingRequest) -> ResultSelf, Self::Error {
let chrono_datetime: SystemTime = chrono::DateTime::parse_from_rfc3339(&item.start_time)
.map_err(|err| format!("Failed to parse start_time: {}", err))?
.with_timezone(&Utc)
.into();
Ok(Self {
_id: ObjectId::new(),
owner: ObjectId::parse_str(&item.owner).expect("Failed to parse owner"),
start_time: DateTime::from(chrono_datetime),
duration_in_minutes: item.duration_in_minutes,
cancelled: false,
})
}
}
既然我們已經了解了模式的設計思路,那么接下來就可以實現其他更簡單的模型了。
接下來,讓我們定義Owner
模型。
考慮到我們的客戶可能并不總是愿意提供電子郵件地址,因此我們將電子郵件字段包裝在?Option
?類型中,以表示該字段是可選的。然而,為了與客戶保持聯系,我們通常需要知道他們的住址和電話號碼,因此這些字段是必需的。
此外,每個所有者都會有一個唯一的 ObjectId
,在處理請求時,我們還需要考慮如何將從請求中接收到的類型轉換為數據庫中的類型。
基于以上考慮,我們可以設計出如下的 Owner
數據結構。
// owner_model.rs
use mongodb::bson::oid::ObjectId;
use serde::{Deserialize, Serialize};
use std::convert::TryFrom;
#[derive(Debug, Serialize, Deserialize)]
pub struct Owner {
pub _id: ObjectId,
pub name: String,
pub email: OptionString,
pub phone: String,
pub address: String,
}
#[derive(Debug, Deserialize)]
pub struct OwnerRequest {
pub name: String,
pub email: OptionString,
pub phone: String,
pub address: String,
}
impl TryFromOwnerRequest for Owner {
type Error = Boxdyn std::error::Error;
fn try_from(item: OwnerRequest) -> ResultSelf, Self::Error {
Ok(Self {
_id: ObjectId::new(),
name: item.name,
email: item.email,
phone: item.phone,
address: item.address,
})
}
}
最后,我們可以實現我們的Dog
模型。
// dog_model.rs
use mongodb::bson::oid::ObjectId;
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize)]
pub struct Dog {
pub _id: ObjectId,
pub owner: ObjectId,
pub name: OptionString,
pub age: Optionu8,
pub breed: OptionString,
}
#[derive(Debug, Deserialize)]
pub struct DogRequest {
pub owner: String,
pub name: OptionString,
pub age: Optionu8,
pub breed: OptionString,
}
impl TryFromDogRequest for Dog {
type Error = Boxdyn std::error::Error;
fn try_from(item: DogRequest) -> ResultSelf, Self::Error {
Ok(Self {
_id: ObjectId::new(),
owner: ObjectId::parse_str(&item.owner).expect("Failed to parse owner"),
name: item.name,
age: item.age,
breed: item.breed,
})
}
}
不要忘記在mod.rs
中公開這些模型,這將把我們的models
文件夾變成一個模塊,我們可以導入到其他地方。
// mod.rs
pub mod booking_model;
pub mod dog_model;
pub mod owner_model;
確保將mod models
添加到main.rs
文件的頂部,以便其他目錄可以訪問此模塊。
為了集中處理數據庫連接、集合操作以及相關的數據庫方法,我們將在?services
?文件夾下的?db.rs
?文件中進行這些服務的定義。隨著項目的不斷發展,我們可能會考慮將這個文件拆分成多個更小的文件,但就目前而言,使用單個文件作為起點會更為簡便。
首先,讓我們為out Database
添加一個結構體。
// db.rs
use crate::models::booking_model::{Booking, FullBooking};
use crate::models::dog_model::Dog;
use crate::models::owner_model::Owner;
pub struct Database {
booking: CollectionBooking,
dog: CollectionDog,
owner: CollectionOwner,
}
接下來,我們將實現一個?init
?方法,該方法會嘗試使用?MONGO_URI
?環境變量來連接到數據庫。如果該環境變量可用,則使用它;如果不可用,則回退到使用本地的連接字符串來建立連接。
// db.rs
use std::env;
use mongodb::{Client, Collection};
use crate::models::booking_model::Booking;
use crate::models::dog_model::Dog;
use crate::models::owner_model::Owner;
pub struct Database {
booking: CollectionBooking,
dog: CollectionDog,
owner: CollectionOwner,
}
impl Database {
pub async fn init() -> Self {
let uri = match env::var("MONGO_URI") {
Ok(v) => v.to_string(),
Err(_) => "mongodb://localhost:27017/?directConnection=true".to_string(),
};
let client = Client::with_uri_str(uri).await.unwrap();
let db = client.database("dog_walking");
let booking: CollectionBooking = db.collection("booking");
let dog: CollectionDog = db.collection("dog");
let owner: CollectionOwner = db.collection("owner");
Database {
booking,
dog,
owner,
}
}
}
為了操作我們的數據庫,我們將依賴于 mongodb
crate 提供的功能。
針對 owner
、dog
和 booking
這三種不同的實體,我們需要分別實現三種不同的方法來進行數據庫操作。
// db.rs
impl Database {
// other functions
pub async fn create_owner(&self, owner: Owner) -> ResultInsertOneResult, Error {
let result = self
.owner
.insert_one(owner, None)
.await
.ok()
.expect("Error creating owner");
Ok(result)
}
pub async fn create_dog(&self, dog: Dog) -> ResultInsertOneResult, Error {
let result = self
.dog
.insert_one(dog, None)
.await
.ok()
.expect("Error creating dog");
Ok(result)
}
pub async fn create_booking(&self, booking: Booking) -> ResultInsertOneResult, Error {
let result = self
.booking
.insert_one(booking, None)
.await
.ok()
.expect("Error creating booking");
Ok(result)
}
}
我們的取消方法也很簡單。我們將使用參數booking_id
,并簡單地將cancelled
的值更新為false
。
// db.rs
impl Database {
// other functions
pub async fn cancel_booking(&self, booking_id: &str) -> ResultUpdateResult, Error {
let result = self
.booking
.update_one(
doc! {
"_id": ObjectId::from_str(booking_id).expect("Failed to parse booking_id")
},
doc! {
"$set": doc! {
"cancelled": true
}
},
None,
)
.await
.ok()
.expect("Error cancelling booking");
Ok(result)
}
}
我們的最后一項功能是獲取包含相關主人和狗信息的完整預訂數據,這一操作相對復雜,因為我們需要利用 MongoDB 的聚合功能來執行相關的查找操作。幸運的是,所有這些復雜的處理都可以在單個數據庫查詢中完成。
需要注意的是,本文并非 MongoDB 的教程,因此我不會在此深入講解聚合操作的細節。但簡而言之,如果我們在篩選預訂時考慮了時間因素(例如,只查看未來的預訂)并且這些預訂未被取消,那么我們就可以通過聚合查詢來找到與特定預訂相關聯的主人(owner)和狗(dog)信息。
// db.rs
impl Database {
// other functions
pub async fn get_bookings(&self) -> ResultVec<FullBooking, Error> {
let now: SystemTime = Utc::now().into();
let mut results = self
.booking
.aggregate(
vec![
doc! {
"$match": {
"cancelled": false,
"start_time": {
"$gte": DateTime::from_system_time(now)
}
}
},
doc! {
"$lookup": doc! {
"from": "owner",
"localField": "owner",
"foreignField": "_id",
"as": "owner"
}
},
doc! {
"$unwind": doc! {
"path": "$owner"
}
},
doc! {
"$lookup": doc! {
"from": "dog",
"localField": "owner._id",
"foreignField": "owner",
"as": "dogs"
}
},
],
None,
)
.await
.ok()
.expect("Error getting bookings");
let mut bookings: VecFullBooking = Vec::new();
while let Some(result) = results.next().await {
match result {
Ok(doc) => {
let booking: FullBooking =
from_document(doc).expect("Error converting document to FullBooking");
bookings.push(booking);
}
Err(err) => panic!("Error getting booking: {}", err),
}
}
Ok(bookings)
}
}
聚合查詢返回的結果類型是 Cursor<Document>
。為了將這些文檔轉換為我們期望的返回類型 Vec<FullBooking>
,我們需要借助 futures_util::stream::StreamExt
提供的 next
方法。
結合之前提到的所有數據庫操作方法,我們的 db.rs
文件現在將包含以下內容的大致框架:
// db.rs
use std::{env, str::FromStr, time::SystemTime};
use chrono::Utc;
use futures_util::stream::StreamExt;
use mongodb::{
bson::{doc, extjson::de::Error, from_document, oid::ObjectId, DateTime},
results::{InsertOneResult, UpdateResult},
Client, Collection,
};
use crate::models::booking_model::{Booking, FullBooking};
use crate::models::dog_model::Dog;
use crate::models::owner_model::Owner;
pub struct Database {
booking: CollectionBooking,
dog: CollectionDog,
owner: CollectionOwner,
}
impl Database {
pub async fn init() -> Self {
let uri = match env::var("MONGO_URI") {
Ok(v) => v.to_string(),
Err(_) => "mongodb://localhost:27017/?directConnection=true".to_string(),
};
let client = Client::with_uri_str(uri).await.unwrap();
let db = client.database("dog_walking");
let booking: CollectionBooking = db.collection("booking");
let dog: CollectionDog = db.collection("dog");
let owner: CollectionOwner = db.collection("owner");
Database {
booking,
dog,
owner,
}
}
pub async fn create_owner(&self, owner: Owner) -> ResultInsertOneResult, Error {
let result = self
.owner
.insert_one(owner, None)
.await
.ok()
.expect("Error creating owner");
Ok(result)
}
pub async fn create_dog(&self, dog: Dog) -> ResultInsertOneResult, Error {
let result = self
.dog
.insert_one(dog, None)
.await
.ok()
.expect("Error creating dog");
Ok(result)
}
pub async fn create_booking(&self, booking: Booking) -> ResultInsertOneResult, Error {
let result = self
.booking
.insert_one(booking, None)
.await
.ok()
.expect("Error creating booking");
Ok(result)
}
pub async fn get_bookings(&self) -> ResultVec<FullBooking, Error> {
let now: SystemTime = Utc::now().into();
let mut results = self
.booking
.aggregate(
vec![
doc! {
"$match": {
"cancelled": false,
"start_time": {
"$gte": DateTime::from_system_time(now)
}
}
},
doc! {
"$lookup": doc! {
"from": "owner",
"localField": "owner",
"foreignField": "_id",
"as": "owner"
}
},
doc! {
"$unwind": doc! {
"path": "$owner"
}
},
doc! {
"$lookup": doc! {
"from": "dog",
"localField": "owner._id",
"foreignField": "owner",
"as": "dogs"
}
},
],
None,
)
.await
.ok()
.expect("Error getting bookings");
let mut bookings: VecFullBooking = Vec::new();
while let Some(result) = results.next().await {
match result {
Ok(doc) => {
let booking: FullBooking =
from_document(doc).expect("Error converting document to FullBooking");
bookings.push(booking);
}
Err(err) => panic!("Error getting booking: {}", err),
}
}
Ok(bookings)
}
pub async fn cancel_booking(&self, booking_id: &str) -> ResultUpdateResult, Error {
let result = self
.booking
.update_one(
doc! {
"_id": ObjectId::from_str(booking_id).expect("Failed to parse booking_id")
},
doc! {
"$set": doc! {
"cancelled": true
}
},
None,
)
.await
.ok()
.expect("Error cancelling booking");
Ok(result)
}
}
為了更好地組織我們的代碼,我們需要一個小的 mod.rs
文件來公開我們的服務模塊。
// mod.rs
pub mod db;
至此,我們已經完成了與數據庫交互所需的大部分功能開發。接下來,就是定義HTTP路由的時刻了!
現在,我們已經做好了編寫終端節點的準備!
到目前這個階段,其實我們已經完成了最艱巨的工作。對于每一個路由,我們只需在 Database
服務中調用相應的函數即可。利用Actix框架提供的宏,我們可以非常便捷地為每個路由指定所需的方法和路徑。這樣,當客戶端發送請求時,就能夠觸發相應的業務邏輯,并與數據庫進行交互,最終返回期望的結果。
在定義Dog路由時,我們不僅要指定路由的處理方法和路徑,還需要將客戶端通過JSON請求發送的字段數據克隆到我們的Dog請求結構體中。
// dog_route.rs
use crate::{
models::dog_model::{Dog, DogRequest},
services::db::Database,
};
use actix_web::{
post,
web::{Data, Json},
HttpResponse,
};
#[post("/dog")]
pub async fn create_dog(db: DataDatabase, request: JsonDogRequest) -> HttpResponse {
match db
.create_dog(
Dog::try_from(DogRequest {
owner: request.owner.clone(),
name: request.name.clone(),
age: request.age.clone(),
breed: request.breed.clone(),
})
.expect("Error converting DogRequest to Dog."),
)
.await
{
Ok(booking) => HttpResponse::Ok().json(booking),
Err(err) => HttpResponse::InternalServerError().body(err.to_string()),
}
}
對于 owner 路由,我們將遵循相同的模式。
// owner_route.rs
use crate::{
models::owner_model::{Owner, OwnerRequest},
services::db::Database,
};
use actix_web::{
post,
web::{Data, Json},
HttpResponse,
};
#[post("/owner")]
pub async fn create_owner(db: DataDatabase, request: JsonOwnerRequest) -> HttpResponse {
match db
.create_owner(
Owner::try_from(OwnerRequest {
name: request.name.clone(),
email: request.email.clone(),
phone: request.phone.clone(),
address: request.address.clone(),
})
.expect("Error converting OwnerRequest to Owner."),
)
.await
{
Ok(booking) => HttpResponse::Ok().json(booking),
Err(err) => HttpResponse::InternalServerError().body(err.to_string()),
}
}
預訂功能相較于其他功能會更為復雜,因為它涉及到三個不同的端點。然而,對于create
端點來說,其實現模式與上述的Dog路由是相似的。
// booking_route.rs
use crate::{
models::booking_model::{Booking, BookingRequest},
services::db::Database,
};
use actix_web::{
get, post, put,
web::{Data, Json, Path},
HttpResponse,
};
#[post("/booking")]
pub async fn create_booking(db: DataDatabase, request: JsonBookingRequest) -> HttpResponse {
match db
.create_booking(
Booking::try_from(BookingRequest {
owner: request.owner.clone(),
start_time: request.start_time.clone(),
duration_in_minutes: request.duration_in_minutes.clone(),
})
.expect("Error converting BookingRequest to Booking."),
)
.await
{
Ok(booking) => HttpResponse::Ok().json(booking),
Err(err) => HttpResponse::InternalServerError().body(err.to_string()),
}
}
fetch端點甚至更簡單,因為我們沒有要解析的JSON主體。
// booking_route.rs
// existing code
#[get("/bookings")]
pub async fn get_bookings(db: DataDatabase) -> HttpResponse {
match db.get_bookings().await {
Ok(bookings) => HttpResponse::Ok().json(bookings),
Err(err) => HttpResponse::InternalServerError().body(err.to_string()),
}
}
最后,我們需要更新路線以取消預訂。我們將使用add一個動態的id
到路徑中,在我們調用db.cancel_booking
函數之前需要提取它。
// booking_route.rs
// existing code
#[put("/booking/{id}/cancel")]
pub async fn cancel_booking(db: DataDatabase, path: Path(String,)) -> HttpResponse {
let id = path.into_inner().0;
match db.cancel_booking(id.as_str()).await {
Ok(result) => HttpResponse::Ok().json(result),
Err(err) => HttpResponse::InternalServerError().body(err.to_string()),
}
}
總之,該文件應如下所示:
// booking_route.rs
use crate::{
models::booking_model::{Booking, BookingRequest},
services::db::Database,
};
use actix_web::{
get, post, put,
web::{Data, Json, Path},
HttpResponse,
};
#[get("/bookings")]
pub async fn get_bookings(db: DataDatabase) -> HttpResponse {
match db.get_bookings().await {
Ok(bookings) => HttpResponse::Ok().json(bookings),
Err(err) => HttpResponse::InternalServerError().body(err.to_string()),
}
}
#[put("/booking/{id}/cancel")]
pub async fn cancel_booking(db: DataDatabase, path: Path(String,)) -> HttpResponse {
let id = path.into_inner().0;
match db.cancel_booking(id.as_str()).await {
Ok(result) => HttpResponse::Ok().json(result),
Err(err) => HttpResponse::InternalServerError().body(err.to_string()),
}
}
#[post("/booking")]
pub async fn create_booking(db: DataDatabase, request: JsonBookingRequest) -> HttpResponse {
match db
.create_booking(
Booking::try_from(BookingRequest {
owner: request.owner.clone(),
start_time: request.start_time.clone(),
duration_in_minutes: request.duration_in_minutes.clone(),
})
.expect("Error converting BookingRequest to Booking."),
)
.await
{
Ok(booking) => HttpResponse::Ok().json(booking),
Err(err) => HttpResponse::InternalServerError().body(err.to_string()),
}
}
確保將我們的每個路由文件添加到mod.rs
。
// mod.rs
pub mod booking_route;
pub mod dog_route;
pub mod owner_route;
在完成所有路由的定義和實現后,我們可以將它們整合到HTTP服務器上,并進行測試。回到main.rs
文件,我們需要為每個路由添加一個service
調用,以便將它們注冊到Actix的HTTP服務器中。
// main.rs
mod models;
mod routes;
mod services;
use actix_web::{get, web::Data, App, HttpResponse, HttpServer, Responder};
use routes::{
booking_route::{cancel_booking, create_booking, get_bookings},
dog_route::create_dog,
owner_route::create_owner,
};
use services::db::Database;
#[get("/")]
async fn hello() -> impl Responder {
HttpResponse::Ok().body("Hello Medium!")
}
#[actix_web::main]
async fn main() -> std::io::Result() {
let db = Database::init().await;
let db_data = Data::new(db);
HttpServer::new(move || {
App::new()
.app_data(db_data.clone())
.service(hello)
.service(create_owner)
.service(create_dog)
.service(create_booking)
.service(get_bookings)
.service(cancel_booking)
})
.bind(("127.0.0.1", 5001))?
.run()
.await
}
我們已成功達成預設目標!但如何驗證其實際效果呢?幸運的是,我們可以借助Postman或cURL等工具來輕松測試各個端點。以下是一些cURL命令示例,幫助你快速上手測試:
## POST /owner
curl --location '[http://localhost:5001/owner](http://localhost:5001/owner)' \
--header 'Content-Type: application/json' \
--data-raw '{
"name": "Joe Bloggs",
"email": "joe.bloggs@example.org",
"phone": "+44800001066",
"address": "123 Main St"
}'
## POST /dog
curl --location '[http://localhost:5001/dog](http://localhost:5001/dog)' \
--header 'Content-Type: application/json' \
--data '{
"owner": "66080390d0e4f489a8e0bbd0",
"name": "Chuffey",
"age": 7,
"breed": "Miniature Schnauzer"
}'
## POST /booking
curl --location '[http://localhost:5001/booking](http://localhost:5001/booking)' \
--header 'Content-Type: application/json' \
--data '{
"owner": "66080390d0e4f489a8e0bbd0",
"start_time": "2024-04-30T10:00:00.000Z",
"duration_in_minutes": 30
}'
## GET /bookings
curl --location '[http://localhost:5001/bookings](http://localhost:5001/bookings)'
## PUT /booking/{id}/cancel
curl --location --request PUT '[http://localhost:5001/booking/66080390d0e4f489a8e0bbd0/cancel](http://localhost:5001/booking/66080390d0e4f489a8e0bbd0/cancel)'
項目圓滿結束啦!
希望本文能為你帶來實質性的幫助。在Rust中構建API服務器的方法靈活多樣,這只是其中一種解決方案!
原文鏈接:https://www.bretcameron.com/blog/how-to-build-an-api-server-with-rust