ช่วงนี้จะวน ๆ อยู่กับ Rust ค่อนข้างเยอะหน่อย ซึ่งโจทย์ในวันนี้ก็จะเป็นทดลองสร้าง REST API ขึ้นมา โดยใช้ Rust ในการสร้าง Service
Service ที่จะทำการสร้างในวันนี้จะเป็น REST API ในรูปแบบที่ไม่ได้ซับซ้อนอะไร เอาแค่เพื่อให้เข้าใจในการทำงาน ว่าโค้ดแต่ละส่วนเราจะ implement ยังไงได้บ้าง
Diagram ของสิ่งที่เราจะสร้างกันในวันนี้
จะเป็น Service โดยคอนเซปก็ประมาณเก็บข้อมูล Stock โดยเราจะเริ่มต้นกันแค่
GET
กับPOST
กันก่อน
Project setup
ก่อนจะไปกันต่อ สิ่งที่เราต้องเตรียมให้พร้อมสำหรับการทำงานทั้งหมดนี้ก็จะเป็น
- Rust แน่ ๆ ล่ะ จะขาดสิ่งนี้ไปได้ยังไงครับบบบ 5555 - ซึ่งถ้าใครยังไม่ได้ install กัน ก็ไปตามกันได้ที่ official doc ได้เลย - https://www.rust-lang.org/tools/install - MongoDB - Postman สำหรับการทดสอบ API endpoint
Rust dependencies
ในครั้งนี้ผมเลือก Axum ในการสร้าง Web service https://docs.rs/axum/latest/axum/ ซึ่งก็ยังมีตัวเลือกอื่น ๆ ที่ยังมีอยู่อีก เช่น Actix, Rocket ส่วนนี้ก็แล้วแต่เลือกได้เลยนะครับ
Cargo.toml
[package] name = "inventory-service" version = "0.1.0" edition = "2021" [dependencies] axum = "0.8.4" tokio = { version = "1", features = ["full"] } serde = { version = "1", features = ["derive"] } serde_json = "1" tower = "0.5.2" dotenv = "0.15.0" mongodb = "3.2.3" futures = "0.3.31" tower-http = { version = "0.3", features = ["trace"] } bson = "2.14.0"
เริ่ม
ออกสตาร์ทกันง่ายที่สุด ด้วยการเริ่มจากการสร้าง
fn main()
ขึ้นมาก่อน ซึ่งสิ่งที่เราจะทำกันก่อน ก็จะเป็น database connectorDatabase connect
use dotenv::dotenv; use mongodb::{ bson::doc, Client, Collection, bson::oid::ObjectId, }; . . . #[tokio::main] async fn main() { dotenv().ok(); let mongo_uri = env::var("INVENTORY_DB_URL").expect("INVENTORY_DB_URL must be set"); println!("MongoDB URI: {}", mongo_uri); let client = Client::with_uri_str(&mongo_uri) .await .expect("Failed to connect to MongoDB"); let db = client.database("test_service"); let collection = db.collection::<InventoryItem>("inventory"); . . . }
เปิดง่าย ๆ กันก่อนกับการ Connect database โดยผมได้ใช้ .env file ในครั้งนี้ด้วย ซึ่ง
INVENTORY_DB_URL
ก็จะเป็นตัวแปรที่สร้างไว้ใน .env file เรียบร้อยแล้วเราก็จะ connect ไปที่ database และ collection ที่เราต้องการ ซึ่ง collection ในตัวอย่างนี้ คือ
inventory
ซึ่งมาถึงตรงนี้ เราก็จะได้ database collection มาอยู่ในตัวแปร collection ในบรรทัดนี้ไปเรียบร้อยแล้ว
let collection = db.collection::<InventoryItem>("inventory");
ทีนี้อาจจะสงสัยกันว่า แล้ว Structure ของ InventoryItem เราจะกำหนดได้ยังไงล่ะ ?
. . . use mongodb::{ bson::doc, Client, Collection, bson::oid::ObjectId, }; #[derive(Debug, Serialize, Deserialize)] struct InventoryItem { #[serde(rename = "_id")] _id: ObjectId, name: String, quantity: i32, } . . . # fn main() . let collection = db.collection::<InventoryItem>("inventory"); . . .
เราก็จะได้โครงสร้างที่จะใช้ใน collection
API routing
เมื่อ connect db ได้เรียบร้อยแล้ว ถัดมาเราจะมาเริ่มทำ routing เพื่อให้ API สามารถยิงทดสอบใน Postman ได้
. . use axum::{ extract::{Json, State}, response::IntoResponse, routing::{get, post}, Router, }; . . # fn main() . let app = Router::new() .route("/inventory", get(check_stock)) .route("/inventory/addItems", post(add_items)) .with_state(state); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); axum::serve(listener, app).await.unwrap(); . . .
ผมทำการ routing มา 2 จุด
/inventory
รับ method GET/inventory/addItems
รับ method POSTหลังจากนั้นก็เปิด Listener มาที่ port :3000 ทีนี้อาจจะสงสัยว่า แล้ว
check_stock
กับadd_items
จะเขียนยังไงต่อล่ะ ?ไปกันต่อ
. async fn check_stock() { println!("Checking stock..."); } async fn add_items() { println!("Adding items..."); } .
ก็เขียนเพิ่มขึ้นมาเป็นฟังก์ชันใหม่ แล้วเราก็จะพบปัญหาเพิ่มขึ้นมาว่า อ้าว … แล้วถ้าอยากจะส่งตัวแปรจากฟังก์ชัน main ไปที่ฟังก์ชันอื่น ??? เราจะทำยังไงได้บ้างล่ะ ???
Axum คิดเรื่องนี้มาให้เราเรียบร้อยแล้วครับ ถ้าผมต้องการส่งตัวแปร collection ที่ connect เรียบร้อยแล้วไปที่ function ย่อย Axum มีสิ่งที่เรียกว่า State อยู่ครับ! ที่จะใช้สำหรับการจัดการเรื่องนี้ ref: https://docs.rs/axum/latest/axum/#using-the-state-extractor
Handling params by State
use std::{ sync::Arc, env, }; use axum::{ extract::{Json, State}, response::IntoResponse, routing::{get, post}, Router, }; use mongodb::{ bson::doc, Client, Collection, bson::oid::ObjectId, }; . . #[derive(Clone)] struct AppState { collection: Collection<InventoryItem>, } # fn main() . let collection = db.collection::<InventoryItem>("inventory"); let state = Arc::new(AppState { collection }); . . .
ผมทำการสร้าง AppState แล้วก็ส่งไปให้ fn อื่น ๆ ใช้ได้ ซึ่งสิ่งที่ผมส่งไปก็คือตัวแปร
collection
โดยจะส่งไปให้ fn อื่น ด้วยวิธีการ. . async fn check_stock( State(state): State<Arc<AppState>> ) { println!("Checking stock..."); } async fn add_items( State(state): State<Arc<AppState>> ) { println!("Adding items..."); } . .
Fetch data
ทีนี้มาเจาะใน แต่ละฟังก์ชันกันบ้าง ถ้าสำหรับการ Check_stock ผมก็จะใช้เป็นการ fetch ง่าย ๆ และต้องการให้ return เป็น Json ด้วย ผมก็จะเรียกใช้เป็นประมาณนี้
async fn check_stock( State(state): State<Arc<AppState>> ) -> impl IntoResponse { println!("Checking stock..."); let filter = doc! { "name": "Widget" }; let mut cursor = state .collection .find(filter) .await .expect("Failed to execute find."); let mut items = Vec::new(); while let Some(doc) = cursor .try_next() .await .expect("Error while iterating") { let item = InventoryItemResponse { _id: doc._id.to_hex(), name: doc.name, quantity: doc.quantity, }; items.push(item); } Json(items) }
Add data
async fn add_items( State(state): State<Arc<AppState>>, Json(payload): Json<InventoryItem>, ) -> impl IntoResponse { println!("Adding items..."); state .collection .insert_one(&payload) .await .expect("Failed to insert document"); Json(payload) }
Complete file
REST API ตัวอย่างในครั้งนี้ก็จะได้เป็นตามนี้ครับ
use dotenv::dotenv; use futures::stream::TryStreamExt; use std::{ sync::Arc, env, }; use serde::{ Deserialize, Serialize }; use axum::{ extract::{Json, State}, response::IntoResponse, routing::{get, post}, Router, }; use mongodb::{ bson::doc, Client, Collection, bson::oid::ObjectId, }; #[derive(Debug, Serialize, Deserialize)] struct InventoryItem { #[serde(rename = "_id")] _id: ObjectId, name: String, quantity: i32, } #[derive(Debug, Serialize)] struct InventoryItemResponse { _id: String, name: String, quantity: i32, } #[derive(Clone)] struct AppState { collection: Collection<InventoryItem>, } #[tokio::main] async fn main() { dotenv().ok(); let mongo_uri = env::var("INVENTORY_DB_URL").expect("INVENTORY_DB_URL must be set"); println!("MongoDB URI: {}", mongo_uri); let client = Client::with_uri_str(&mongo_uri) .await .expect("Failed to connect to MongoDB"); let db = client.database("test_service"); println!("Connected to MongoDB database: {}", db.name()); println!("MongoDB collections: {:?}", db.list_collection_names().await.unwrap()); let collection = db.collection::<InventoryItem>("inventory"); let state = Arc::new(AppState { collection }); let app = Router::new() .route("/inventory", get(check_stock)) .route("/inventory/addItems", post(add_items)) .with_state(state); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); axum::serve(listener, app).await.unwrap(); } async fn check_stock( State(state): State<Arc<AppState>> ) -> impl IntoResponse { println!("Checking stock..."); let filter = doc! { "name": "Widget" }; let mut cursor = state .collection .find(filter) .await .expect("Failed to execute find."); let mut items = Vec::new(); while let Some(doc) = cursor .try_next() .await .expect("Error while iterating") { let item = InventoryItemResponse { _id: doc._id.to_hex(), name: doc.name, quantity: doc.quantity, }; items.push(item); } Json(items) } async fn add_items( State(state): State<Arc<AppState>>, Json(payload): Json<InventoryItem>, ) -> impl IntoResponse { println!("Adding items..."); state .collection .insert_one(&payload) .await .expect("Failed to insert document"); Json(payload) }
ก็เป็นการ experiment กับการทดลองเขียน REST API ด้วย Rust กัน ให้พอเห็นภาพในการสร้าง Service โดยใช้ RUST + Axum
>_JV
