【Rust】Bruno我來了,一起來打網路API…
前言
根據尼爾森的不正確調查,上一期文章的收視率高達了87%,因應廣大網友的要求,筆者只好再緊急加映一集…這集就進階一點,來做做網路API吧,把CSV文件當成簡單的資料來源,讀取文件後,轉成JSON格式輸出,達到隔空抓藥的效果…
作業環境
項目 | 版本 |
---|---|
macOS | Sequoia 15.5 |
Visual Studio Code | 1.100.3 |
Rust | 1.87.0 |
Bruno | 2.4.0 |
Rust套件
套件 | 版本 | 功能 |
---|---|---|
actix-web | 4.4.0 | 是一個高效能、非常靈活的 Rust 網頁框架。可以建立網頁伺服器、客戶端 |
actix-multipart | 0.6 | actix-web的多檔案下載功能套件 |
serde | 1.0 | 是Rust生態系統中最受歡迎的序列化框架之一,提供了高效能且易於使用的JSON處理功能 |
serde_json | 1.0 | 是用於處理JSON資料的Serde的一部分 |
colored | 3.0 | 為終端機輸出添加顏色 |
local-ip-address | 0.6.5 | 取得本地端的區網IP位置 |
csv | 1.3.1 | 快速處理可變動的CSV文件 |
futures | 0.3.31 | 非同步功能實現 - Future、Stream、Sink |
chrono | 0.4 | 處理日期 / 時間 / 時區的套件 |
安裝Rust套件
- 這裡先用指令建立一個名叫rust_csv_api的專案名稱,注意要用小寫喲…
cargo new rust_csv_api
- 然後更新Cargo.toml的內容,填入後面要用到的套件們…
[dependencies]
actix-web = "4.4.0"
actix-multipart = "0.6"
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0" }
csv = "1.3.1"
futures = "0.3.31"
chrono = { version = "0.4", features = ["serde"] }
colored = "3.0"
local-ip-address = "0.6.5"
- 之後在終端機執行以下的指令就會下載安裝了,你會看到會多出一個名叫target的資料夾,容量很大…
- 當然,要用到的兩筆CSV文件也就順便放在document的目錄之中了…
cargo check
- 當然,要清掉的話也是可以的…
cargo clean # 清理當前編譯產物
cargo clean --target debug # 清理當前編譯產物 for debug環境
cargo clean --target release # 清理當前編譯產物 for release環境
cargo clean -p <package> # 清理該指名套件
- 也可以用tree指令來看看到底裝了什麼?
cargo tree --duplicate
建立列印小工具 - ww_print
- 我們先來仿製一個類似WWPrint的列印巨集,來簡化打印位置資訊…
- 正所謂「工欲善其事,必先利其器」嘛…
- 在library/macros.rs寫入以下的內容,記得要在mod.rs加入該模組…
#[macro_export]
macro_rules! ww_print {
($message:expr) => {
println!("\n[{file} - {line}]\n{message}",
file = file!().green().bold(),
line = format!("line.{}", line!().to_string()).yellow().bold(),
message = $message
)
};
}
- 然後我們在
main.rs
中使用一下… - 記得一定要先裝
colored
套件,不然會有錯誤喲…
use colored::Colorize;
mod library;
fn main() {
ww_print!("Hello, world!");
}
- 然後執行一下以下的指令,有沒有看到有顏色的檔名跟行數啊,是不是很好用啊…
建立API
建立第一支API
- 首先先建立兩個檔案,一個叫config.rs,另一個叫model.rs…
- config.rs是放公用的常數設定檔…
pub const DEFAULT_PORT: u16 = 8080;
pub const LOCALHOST: &str = "127.0.0.1";
pub const CSV_FOLDER_PATH: &str = "document";
- model.rs是放參數模型,在rust常常使用模型來處理變數,反而不會直接去添加文字Key值來處理…
use serde::{Serialize};
#[derive(Serialize)]
pub struct Message {
pub message: String,
pub timestamp: i64,
pub date: String,
}
use std::io::{Error};
use colored::Colorize;
use actix_web::{get};
use actix_web::{App, HttpServer, Responder, web};
use chrono::{Utc};
use local_ip_address::{local_ip};
mod library;
mod model;
mod config;
use crate::model::{Message};
use crate::config::*;
#[get("/first")]
async fn get_first_action() -> impl Responder {
let now = Utc::now();
let response = Message {
message: "安安你好嗎?".to_string(),
timestamp: now.timestamp_millis(),
date: now.to_rfc3339(),
};
web::Json(response)
}
async fn register_service() -> Result<(), Error> {
let current_ip = local_ip().unwrap_or_else(|_| LOCALHOST.parse().unwrap());
ww_print!(format!(
"Starting server at {localhost}:{port}\nStarting server at {ip}:{port}",
ip = current_ip,
port = DEFAULT_PORT,
localhost = LOCALHOST
));
HttpServer::new(|| {
App::new().service(get_first_action)
})
.bind(format!("{}:{}", current_ip, DEFAULT_PORT))?
.bind(format!("{}:{}", LOCALHOST, DEFAULT_PORT))?
.run()
.await
}
#[actix_web::main]
async fn main() -> Result<(), Error> {
register_service().await
}
- 大家可以用http://127.0.0.1:8080試試看…
cargo run
- 在這邊要先說明的是Macro - 巨集的功能,就是
#[get("/first")]
跟#[actix_web::main]
這兩個巨集… - 這個功能在Rust很常用到,很像Swift的property wrapper功能,簡化了很多樣板代碼的撰寫…
- #[get("/first")]就是指使用GET執行get_first_action()這個函數…
- #[actix_web::main]是指讓main()在編譯時運行非同步程式碼…
- 另外就是閉包的問題 - Closure,居然是以
「||」
來表示的,其實就是下面這個樣子啦…
fn add_one_v1 (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
&str / String / &String
- 建議使用原則
- 函數參數優先使用 &str (字串切片 - stack / 固定大小,不可變)
- 需要修改時使用 String (所有權 - heap / 可變大小)
- 需要共享時使用 &String (引用 - stack / 固定大小,不可變)
看門狗?
- 有沒有想過,每一次改寫程式碼後,都要關閉服務器,再重新啟動呢?有沒有自動會重新啟動功能呢?
- 有…它就是cargo-watch
- 安裝好之後,執行下列的指令,它就會…
- -c: 每次重新執行前清除控制台
- -q: 使輸出更簡潔
- -d 2: 設置 2 秒的延遲,避免過於頻繁的重新編譯
- -x run: 執行 run 指令
cargo install cargo-watch
cargo watch -c -q -d 2 -x run
Rust沒有Defer怎麼辦?那就自己做一個吧…
- 相信寫過Swift的同學們,應該很常用
defer
的功能吧?一種延時呼叫的功能,至少我個人很滿常用的,但是…Rust沒有,所以要自己做一個… - Rust也有類似defer功能,可以使用
std::mem::drop
來實現… - 跟一般建立struct大同小異,主要是實現一個
Drop trait
,可以把它當成是一個swift的protocol
,就先這樣類比吧… - Drop trait的fn drop()的功能是當struct被
drop
時,再執行closure
,有點像deinit
的行為,可以讓我們在struct被drop時,執行一些清理工作,例如關閉檔案、釋放資源等。
pub struct Defer<F: FnMut()> {
closure: F,
}
impl<F: FnMut()> Defer<F> {
pub fn new(closure: F) -> Self {
Self { closure }
}
}
impl<F: FnMut()> Drop for Defer<F> {
fn drop(&mut self) {
(self.closure)();
}
}
- 這裡我們可以試試看結果有沒有跟我們要的功能一致?
- 要注意的是,一定要給一個變數記下來,不然就沒有變數可以drop,也就沒有執行Defer closure的機會了。
...
use crate::model::{Message, Defer};
...
#[get("/first")]
async fn get_first_action() -> impl Responder {
let _defer = Defer::new(|| {
ww_print!("執行Defer closure");
});
ww_print!("執行get_first_action");
...
}
美化一下API的輸出Log
- 為了讓API的輸出Log更易讀,我們可以使用
enum
的方法,順便加一些顏色 / 類型轉換的功能,方便辨識。 - 在
model.rs
再加一個名叫HttpMethod
的enum… - 有
&self
參數的是一般函數 (.方法),沒有的是static函數 (::方法)…
#[derive(Debug)]
#[allow(dead_code)]
pub enum HttpMethod {
Get,
Post,
}
impl HttpMethod {
pub fn as_str(&self) -> &str {
match self {
HttpMethod::Get => "Get",
HttpMethod::Post => "Post",
}
}
pub fn on_custom_color(&self) -> CustomColor {
match self {
HttpMethod::Get => CustomColor::new(24, 226, 18),
HttpMethod::Post => CustomColor::new(255, 250, 250),
}
}
}
- 細節就不多說了,主要是仿一些主流的Web框架的Log輸出,例如gin、flask等。
- 建立一個新的
library
,名叫utility.rs
,記錄一下小工具,程式碼
如下,記得要在mod.rs加入它喲… - 這裡唯一要說明的東西,因為status_code會比_defer內的closure的生命週期短,需要宣告成
Cell
,然後使用get
方法獲取值,也就是指標,當然也是要為了單線程安全
。
/// library/utility.rs
use std::cell::Cell;
use colored::Colorize;
use actix_web::http::StatusCode;
use actix_web::{HttpRequest};
use chrono::{Local};
use crate::model::{HttpMethod};
pub fn log_message(request: &HttpRequest, status_code: &Cell<StatusCode>, method: HttpMethod) -> String {
let connection_info = request.connection_info().clone();
let client_ip = connection_info.peer_addr().unwrap_or("unknown");
let request_url = request.uri().to_string();
let status = match status_code.get() {
code if (500..600).contains(&code.as_u16()) => status_code.get().to_string().red(),
StatusCode::OK => status_code.get().to_string().green(),
_ => status_code.get().to_string().yellow(),
};
let fix_method = format!("{:^5}", method.as_str());
format!(
"{ip} - [{date}] {method} {url}, {status}",
ip = client_ip.bold(),
date = Local::now().format("%d/%b/%Y %H:%M:%S"),
url = request_url.yellow(),
method = fix_method.on_custom_color(method.on_custom_color()),
status = status,
)
}
/// library/mod.rs
pub mod macros;
pub mod utility;
- 現在我們就來試看看吧,是不是很有feel啊?
mod library;
...
use crate::library::utility::log_message;
...
#[get("/first")]
async fn get_first_action(request: HttpRequest) -> impl Responder {
let status_code = Cell::new(StatusCode::OK);
let now = Utc::now();
let _defer = Defer::new(|| {
ww_print!(log_message(&request, &status_code, HttpMethod::Get));
});
let response = Message {
message: "安安你好嗎?".to_string(),
timestamp: now.timestamp_millis(),
date: now.to_rfc3339(),
};
web::Json(response)
}
取得Query變數
- 一般常常看到網址的尾端會加上
?id=123&name=William
的字樣,這就是要取得的Query變數,可以把這些數值傳給Server端。 - 接下來我們就來試做一個簡單的Demo,加強它的功能…
- 先在
model.rs
新增一個struct
,用來儲存Query變數
的值,就是含有id和name兩個欄位,是可填可不填的Option
欄位。
use serde::{Deserialize};
#[derive(Deserialize)]
pub struct Params {
pub id: Option<i32>,
pub name: Option<String>,
}
- 然後新增一個
/query
的API來取得Query變數
的值,程式碼跟/first
的差不多,只是多了一個處理query的值… - 這裡使用match來處理query的值,因為是
可選的
,所以會有四種可能… - 最後要記得註冊
get_query_action
喲…
...
use crate::model::{Message, Defer, HttpMethod, Params};
...
#[get("/query")]
async fn get_query_action(request: HttpRequest, query: web::Query<Params>) -> impl Responder {
let status_code = Cell::new(StatusCode::OK);
let now = Utc::now();
let _defer = Defer::new(|| {
ww_print!(log_message(&request, &status_code, HttpMethod::Get));
});
let message = match (&query.name, query.id) {
(Some(name), Some(id)) => format!("[Hello, {} (ID: {})!", name, id),
(Some(name), None) => format!("Hello, {}!", name),
(None, Some(id)) => format!("Hello, User {}!", id),
(None, None) => String::from("Hello, World!"),
};
let response = Message {
message: message.to_string(),
timestamp: now.timestamp_millis(),
date: now.to_rfc3339(),
};
web::Json(response)
}
...
async fn register_service() -> Result<(), Error> {
...
HttpServer::new(|| {
App::new()
.service(get_first_action)
.service(get_query_action)
})
.bind(format!("{}:{}", current_ip, DEFAULT_PORT))?
.bind(format!("{}:{}", LOCALHOST, DEFAULT_PORT))?
.run()
.await
}
- 最後,大家可以試試看
http://localhost:8080/query?id=987987&name=William
這個API喲…
取得Body的值
- 這裡主要是要取得Body的值,因為
GET
規定是沒有Body的,所以我們選用POST
來做示範… - 新增一個
/json
的API來取得Body
的值,程式碼跟/query
的差不多… - 這裡我們直接使用
JSON字串
來當輸出值,使用serde_json::from_str()
來把JSON字串轉成Value
,一個可輸出的值… - 這裡的
unwrap()
,其實就是Swift的!
,強制解出其值… - 最後還是要記得註冊
post_json_action
喲…
...
use serde_json::{Value};
...
#[post("/json")]
async fn post_json_action(request: HttpRequest, input: web::Json<Params>) -> impl Responder {
let status_code = Cell::new(StatusCode::OK);
let now = Utc::now();
let _defer = Defer::new(|| {
ww_print!(log_message(&request, &status_code, HttpMethod::Post));
});
let message = match (&input.name, input.id) {
(Some(name), Some(id)) => format!("Hello, {} (ID: {})!", name, id),
(Some(name), None) => format!("Hello, {}!", name),
(None, Some(id)) => format!("Hello, User {}!", id),
(None, None) => String::from("Hello, World!"),
};
let json = format!(
r#"{{
"message": "{}",
"timestamp": {},
"date": "{}"
}}"#,
message,
now.timestamp_millis(),
now.to_rfc3339()
);
let parsed: Value = serde_json::from_str(&json).unwrap();
web::Json(parsed).customize().with_status(status_code.get())
}
...
- 再來就請大家再測測看啦…
動態路由 - Dynamic Routing
- Body的值可以很容易的增加,那Query的值可以嗎?
- 可以的,
Router
也是可以設定成Path
的值,例如/user/{id}/{name}
,這樣就可以根據不同的id
/name
來取得不同的使用者資訊了。 - 比較要說明的是,我們直接使用
path: web::Path<(String, String)>
來接id
和name
,再用into_inner()
把值解開… - 如果沒有用到的值,可以使用
_
或_name
來命名,不然Rust會一直提示說這個值沒用到…
#[post("/user/{id}/{name}")]
async fn post_router_action(request: HttpRequest, path: web::Path<(String, String)>) -> impl Responder {
let status_code = Cell::new(StatusCode::OK);
let _defer = Defer::new(|| {
ww_print!(log_message(&request, &status_code, HttpMethod::Post));
});
let (id, _name) = path.into_inner();
let response = serde_json::json!({
"id": id,
"message": "更新成功",
"timestamp": Utc::now().timestamp_millis()
});
web::Json(parsed).customize().with_status(status_code.get())
}
- 有木有,
動態路由 - Dynamic Routing
是不是很方便啊? - 其實Rust也是有
tuple - 元組
的資料型態…
let user: (i32, &str, bool) = (42, "hello", true);
let (name, age, is_active) = user;
println!("第一個值: {}", user.0);
println!("第二個值: {}", user.1);
println!("第三個值: {}", user.2);
動態Query
...
use actix_web::{App, HttpServer, HttpRequest, HttpResponse, Responder, web};
use std::collections::HashMap;
...
#[post("/dynamic")]
async fn dynamic_query(request: HttpRequest, query: web::Query<HashMap<String, String>>) -> impl Responder {
let status_code = Cell::new(StatusCode::OK);
let _defer = Defer::new(|| {
ww_print!(log_message(&request, &status_code, HttpMethod::Post));
});
for (key, value) in query.iter() {
println!("{}: {}", key, value);
}
HttpResponse::Ok().json(query.into_inner())
}
建立讀取CSV檔的API
讀取CSV檔
- 接下來要進入正題了,我們先讀取
Linux.csv
文件… - 先建立的一個struct,名叫
CsvRecord
,用來儲存CSV檔單行的內容,欄位對應就在程式碼之中。 - 其中
#[derive(Serialize, Deserialize, Debug)]
的Serialize
跟Deserialize
可以當成是Swift的Codable = Decodable & Encodable
,用來解/編JSON的implementations
,而Debug
就是允許打印出結構內容的意思,Rust其實防得滿嚴格的。
#[derive(Serialize, Deserialize, Debug)]
pub struct CsvRecord {
pub name: String,
pub notes: String,
pub url: String,
pub example: String,
pub level: u8,
}
- 然後使用
csv
套件的功能來讀取CSV檔… - 程式碼應該也很好理解,主要是利用
泛型
來加強功能,只處理符合DeserializeOwned - 可以解的
跟Debug - 可列印
的trait
約束,其實就是規範共同的行為
…
...
use std::fmt::Debug;
use csv::Reader;
use serde::de::DeserializeOwned;
...
pub fn parse_csv_file<T>(file_path: String) -> Result<Vec<T>, Error> where T: DeserializeOwned + Debug {
let mut records: Vec<T> = Vec::new();
let opened_file = match File::open(file_path) {
Err(error) => return Err(error),
Ok(file) => file,
};
let mut reader = Reader::from_reader(opened_file);
for result in reader.deserialize() {
match result {
Err(error) => return Err(error.into()),
Ok(record) => {
ww_print!(format!("解析到記錄: {:?}", record));
records.push(record)
}
}
}
Ok(records)
}
- 我們先試看看讀不讀得出來CSV的文件檔,就建立一個
/csv
的API打看看… - 也可以把
filename
打錯,有做對應的錯誤處理,要更改status_code,要用.set(StatusCode::OK);
才行喲…
#[post("/csv")]
async fn csv_action(request: HttpRequest) -> impl Responder {
let status_code = Cell::new(StatusCode::NOT_FOUND);
let _defer = Defer::new(|| {
ww_print!(log_message(&request, &status_code, HttpMethod::Post));
});
let filename = "Linux.csv";
let file_path = format!("{}/{}", config::CSV_FOLDER_PATH, filename);
let records: Vec<CsvRecord> = match parse_csv_file(file_path) {
Err(error) => {`
return web::Json(serde_json::json!({ "error": error.to_string() }))
.customize()
.with_status(status_code.get());
}
Ok(records) => records,
};
status_code.set(StatusCode::OK);
web::Json(serde_json::json!({})).customize().with_status(status_code.get())
}
- 不知道大家有沒有注意到,為什麼有
return
呢?之前不都沒有寫嗎? - Rust常用的是
隱式回傳
的寫法,就是沒分號
的return,這是最後一行;而顯式回傳
的寫法,就是一般的寫法,是不是很有趣啊?差一個分號就差很大~了…
轉成JSON格式輸出
#[post("/csv")]
async fn csv_action(request: HttpRequest) -> impl Responder {
...
let response = serde_json::json!({ "result": records });
status_code.set(StatusCode::OK);
web::Json(serde_json::json!(response)).customize().with_status(status_code.get())
}
選擇CSV文件
- 現在,我們要利用變數來選擇CSV文件,並根據選擇的文件名稱讀取相應的CSV文件內容。
- 首先新增一個constant.rs檔案,並新增一個名叫
CsvFileType
的enum
,也把HttpMethod
移來這邊,相關的地方也要修正一下… - 這個enum的功能就是要將
Linux.csv
跟CrossPlatform.csv
這兩個檔名做分類,當然用1、2、3也是可以的… - 然後再加上一個
自定義錯誤
類型,因為有可能會轉錯嘛,然後再把std::fmt::Display
的trait特徵加到CustomError
中,實現fmt()
方法… - 這個
std::fmt::Display
可以把它類比成Swift的CustomStringConvertible
這個Protocol
,自訂print輸出文字… - 在print!()方法中的參數都要實作這個
fmt()
才能被印出來,以後應該會很常用到它,它都叫Display
了嘛,可以想象成print!("hello".fmt())
…
use colored::{CustomColor};
pub enum CustomError {
EmptyString,
ParseStringError,
InvalidType(String),
FileNotFound(String),
UnknownError,
}
impl std::fmt::Display for CustomError {
fn fmt(&self, format: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
CustomError::InvalidType(_type) => write!(format, "無效的檔案類型: {}", _type),
CustomError::EmptyString => write!(format, "檔案類型不能為空"),
CustomError::UnknownError => write!(format, "未知錯誤"),
CustomError::ParseStringError => write!(format, "字串解析錯誤"),
CustomError::FileNotFound(path) => write!(format, "檔案未找到: {}", path),
}
}
}
#[derive(Debug)]
#[allow(dead_code)]
pub enum HttpMethod {
Get,
Post,
}
...
#[derive(Debug)]
#[allow(dead_code)]
pub enum CsvFileType {
Linux,
CrossPlatform,
}
...
可選的CSV文件檔名
- 我們來改造一下
csv_action
函數,使其能夠處理不同的CSV檔案類型,例如Linux和CrossPlatform,並根據不同的檔案類型進行不同的處理… - API路徑加上一個可變Path參數
filename
,接下來就把filename
跟自定義錯誤印出來看看吧,這樣是不是更加清楚錯什麼了啊?
#[post("/csv/{filename}")]
async fn csv_action(request: HttpRequest, path: web::Path<String>) -> impl Responder {
...
let filename = path.into_inner();
let error = CustomError::ParseStringError;
ww_print!(format!("CSV檔名: {}", filename));
ww_print!(format!("自定義錯誤: {}", error));
- 再來我們希望將
filename
轉成CsvFileType
,除了防止打錯字之外,用enum
的話,可以用match (switch..case)
來處理,更加的方便… - 就是跟Swift的enum一樣,字串轉enum…
...
use std::str::FromStr;
...
use crate::constant::{HttpMethod, CsvFileType};
...
#[post("/csv/{filename}")]
async fn csv_action(request: HttpRequest, path: web::Path<String>) -> impl Responder {
...
let filename = path.into_inner();
// let file_type: CsvFileType = match filename.parse() {
let file_type: CsvFileType = match CsvFileType::from_str(filename.as_str()) {
Ok(_type) => _type,
Err(error) => {
ww_print!(format!("錯誤: {}. 預設為: Linux.csv", error));
CsvFileType::Linux
}
};
ww_print!(format!("轉換的類型: {:?}", file_type));
ww_print!(format!("CSV檔名: {:?}", file_type.as_str()));
...
}
- 執行之後會發現少了些什麼?是
CsvFileType::from_str()
,我加了from_str()
… - 這個是一個
std::str::FromStr
的trait的實現,就是一般會用來轉型的那個parse()
方法… - 除了是這樣用 -
<filename>.parse()
,也可以這樣用 -CsvFileType::from_str(<filename>)
…
use colored::{Colorize, CustomColor};
use crate::ww_print;
impl std::str::FromStr for CsvFileType {
type Err = CustomError;
fn from_str(str: &str) -> Result<Self, Self::Err> {
ww_print!(format!("輸入的文字為: {}", str));
match str.to_lowercase().as_str() {
"linux" => Ok(CsvFileType::Linux),
"crossplatform" => Ok(CsvFileType::CrossPlatform),
_ => Err(CustomError::InvalidType(str.to_string())),
}
}
}
- 我們把加強後的程式碼再來跑看看 -
http://127.0.0.1:8080/csv/windows
… - 是不是一模一樣,沒什麼變化啊?但是如果是用 -
http://127.0.0.1:8080/csv/CrossPlatform
呢?
#[post("/csv/{filename}")]
async fn csv_action(request: HttpRequest, path: web::Path<String>) -> impl Responder {
let status_code = Cell::new(StatusCode::NOT_FOUND);
let _defer = Defer::new(|| {
ww_print!(log_message(&request, &status_code, HttpMethod::Post));
});
let filename = path.into_inner();
// let file_type: CsvFileType = match filename.parse() {
let file_type: CsvFileType = match CsvFileType::from_str(filename.as_str()) {
Ok(_type) => _type,
Err(error) => {
ww_print!(format!("錯誤: {}. 預設為: Linux.csv", error));
CsvFileType::Linux
}
};
let file_path: String = format!("{}/{}", config::CSV_FOLDER_PATH, file_type.as_str());
let records: Vec<CsvRecord> = match parse_csv_file(file_path) {
Err(error) => {
return web::Json(serde_json::json!({ "error": error.to_string() }))
.customize()
.with_status(status_code.get());
}
Ok(records) => records,
};
let response = serde_json::json!({ "result": records });
status_code.set(StatusCode::OK);
web::Json(serde_json::json!(response)).customize().with_status(status_code.get())
}
反序列化欄位不一怎麼辦?
- 什麼?明明就有
CrossPlatform.csv
啊,為什麼會讀不出來呢? - 原來是因為欄位名稱不一致,沒有
example
這一列,導致反序列化失敗,那要怎麼辦呢?
{
"error": "CSV deserialize error: record 1 (line: 2, byte: 33): missing field `example`"
}
- 照一般的做法,應該會是
CsvFileType
的各類型,各去對一個struct
,然後利用泛型
去處理,但…serde
有一個很有趣的處理方式… - 我們先把多出來的欄位也加到
CsvRecord
上…
#[derive(Serialize, Deserialize, Debug)]
pub struct CsvRecord {
pub name: String,
pub notes: String,
pub url: String,
pub level: u8,
#[serde(skip_serializing_if = "Option::is_none")]
pub example: Option<String>,
#[serde(deserialize_with = "deserialize_platform")]
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub platform: Vec<String>,
}
- 其中
#[serde(skip_serializing_if = "Option::is_none")]
這個巨集,用來控制序列化時是否跳過None
值的欄位,跟Go的套件gin
有點像… - 也就是說,這一欄有就解出來,沒有也沒關係的意思啦,可以先單獨加上這行試試看,應該就可以沒問題了…
#[serde(skip_serializing_if = "Option::is_none")]
pub example: Option<String>,
- 先說
#[serde(skip_serializing_if = "Vec::is_empty")]
,就是它可有可無的意思… - 再來就是
#[serde(default)]
,用人話說,就是有預設值,是Vec的Default::default() - 空Array… - 這個
#[serde(deserialize_with = "deserialize_platform")]
就是說,要反序列化時,要使用deserialize_platform
函數來處理platform
欄位的反序列化… - 因為
platform
的值是長得像Windows,macOS,Linux,Android,iOS
這樣子的字串,所以要更進一步加工成Vec<String>
的樣子…
#[serde(deserialize_with = "deserialize_platform")]
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub platform: Vec<String>,
- 這個就是
deserialize_with
中要實作的函數,用來處理platform
欄位的反序列化…
use serde::de::Deserializer;
fn deserialize_platform<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error> where D: Deserializer<'de> {
let s: String = String::deserialize(deserializer)?;
Ok(s.split(',').map(|s| s.trim().to_string()).collect())
}
- 附上完整的
model.rs
的程式碼… - 這樣就完成了
platform
欄位的反序列化,我們再用bruno
再post
一次看看吧…
http://127.0.0.1:8080/csv/CrossPlatform
http://127.0.0.1:8080/csv/linux
use serde::{Serialize, Deserialize};
use serde::de::Deserializer;
#[derive(Serialize)]
pub struct Message {
pub message: String,
pub timestamp: i64,
pub date: String,
}
#[derive(Deserialize)]
pub struct Params {
pub id: Option<i32>,
pub name: Option<String>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct CsvRecord {
pub name: String,
pub notes: String,
pub url: String,
pub level: u8,
#[serde(skip_serializing_if = "Option::is_none")]
pub example: Option<String>,
#[serde(deserialize_with = "deserialize_platform")]
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub platform: Vec<String>,
}
fn deserialize_platform<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error> where D: Deserializer<'de> {
let str = String::deserialize(deserializer)?;
Ok(str.split(',').map(|str| str.trim().to_string()).collect())
}
pub struct Defer<F: FnMut()> {
closure: F,
}
impl<F: FnMut()> Defer<F> {
pub fn new(closure: F) -> Self {
Self { closure }
}
}
impl<F: FnMut()> Drop for Defer<F> {
fn drop(&mut self) {
(self.closure)();
}
}
整理程式碼
- 最後,我們把
main.rs
內的程式包裝成一個service
,就放在src/service/csv_service.rs
底下… - 把程式跟Web路徑分開,看起來比較乾淨,日後也好維護…
use std::cell::Cell;
use std::collections::HashMap;
use std::str::FromStr;
use actix_web::http::StatusCode;
use actix_web::{HttpRequest, HttpResponse, Responder, web};
use chrono::{Utc};
use colored::Colorize;
use serde_json::{Value};
use crate::library::utility::{log_message, parse_csv_file};
use crate::model::*;
use crate::config;
use crate::constant::{HttpMethod, CsvFileType};
use crate::ww_print;
pub fn get_first_action(request: HttpRequest) -> impl Responder {
let status_code = Cell::new(StatusCode::OK);
let now = Utc::now();
let _defer = Defer::new(|| {
ww_print!(log_message(&request, &status_code, HttpMethod::Get));
});
let response = Message {
message: "安安你好嗎?".to_string(),
timestamp: now.timestamp_millis(),
date: now.to_rfc3339(),
};
web::Json(response)
}
pub fn get_query_action(request: HttpRequest, query: web::Query<Params>) -> impl Responder {
let status_code = Cell::new(StatusCode::OK);
let now = Utc::now();
let _defer = Defer::new(|| {
ww_print!(log_message(&request, &status_code, HttpMethod::Get));
});
let message = match (&query.name, query.id) {
(Some(name), Some(id)) => format!("[Hello, {} (ID: {})!", name, id),
(Some(name), None) => format!("Hello, {}!", name),
(None, Some(id)) => format!("Hello, User {}!", id),
(None, None) => String::from("Hello, World!"),
};
let response = Message {
message: message.to_string(),
timestamp: now.timestamp_millis(),
date: now.to_rfc3339(),
};
web::Json(response)
}
pub fn post_json_action(request: HttpRequest, input: web::Json<Params>) -> impl Responder {
let status_code = Cell::new(StatusCode::OK);
let now = Utc::now();
let _defer = Defer::new(|| {
ww_print!(log_message(&request, &status_code, HttpMethod::Post));
});
let message = match (&input.name, input.id) {
(Some(name), Some(id)) => format!("Hello, {} (ID: {})!", name, id),
(Some(name), None) => format!("Hello, {}!", name),
(None, Some(id)) => format!("Hello, User {}!", id),
(None, None) => String::from("Hello, World!"),
};
let json = format!(
r#"{{
"message": "{}",
"timestamp": {},
"date": "{}"
}}"#,
message,
now.timestamp_millis(),
now.to_rfc3339()
);
let parsed: Value = serde_json::from_str(&json).unwrap();
web::Json(parsed).customize().with_status(status_code.get())
}
pub fn post_router_action(request: HttpRequest, path: web::Path<(String, String)>) -> impl Responder {
let status_code = Cell::new(StatusCode::OK);
let _defer = Defer::new(|| {
ww_print!(log_message(&request, &status_code, HttpMethod::Post));
});
let (id, _name) = path.into_inner();
let response = serde_json::json!({
"id": id,
"message": "更新成功",
"timestamp": Utc::now().timestamp_millis()
});
web::Json(response).customize().with_status(status_code.get())
}
pub fn dynamic_query(request: HttpRequest, query: web::Query<HashMap<String, String>>) -> impl Responder {
let status_code = Cell::new(StatusCode::OK);
let _defer = Defer::new(|| {
ww_print!(log_message(&request, &status_code, HttpMethod::Post));
});
for (key, value) in query.iter() {
println!("{}: {}", key, value);
}
HttpResponse::Ok().json(query.into_inner())
}
pub fn read_csv_action(request: HttpRequest, path: web::Path<String>) -> impl Responder {
let status_code = Cell::new(StatusCode::NOT_FOUND);
let _defer = Defer::new(|| {
ww_print!(log_message(&request, &status_code, HttpMethod::Post));
});
let filename = path.into_inner();
// let file_type: CsvFileType = match filename.parse() {
let file_type: CsvFileType = match CsvFileType::from_str(filename.as_str()) {
Ok(_type) => _type,
Err(error) => {
ww_print!(format!("錯誤: {}. 預設為: Linux.csv", error));
CsvFileType::Linux
}
};
let file_path: String = format!("{}/{}", config::CSV_FOLDER_PATH, file_type.as_str());
let records: Vec<CsvRecord> = match parse_csv_file(file_path) {
Err(error) => {
status_code.set(StatusCode::BAD_REQUEST);
return web::Json(serde_json::json!({ "error": error.to_string() })).customize().with_status(status_code.get());
}
Ok(records) => records,
};
let response = serde_json::json!({ "result": records });
status_code.set(StatusCode::OK);
web::Json(serde_json::json!(response)).customize().with_status(status_code.get())
}
- 最後附上
main.rs
的程式碼,完結撒花…
use std::io::{Error};
use std::collections::HashMap;
use colored::Colorize;
use actix_web::{get, post};
use actix_web::{App, HttpServer, HttpRequest, Responder, web};
use local_ip_address::{local_ip};
mod library;
mod model;
mod config;
mod constant;
mod service;
use crate::model::Params;
use crate::config::*;
use crate::{
service::csv_service as _service_,
};
#[get("/first")]
async fn get_first_action(request: HttpRequest) -> impl Responder {
_service_::get_first_action(request)
}
#[get("/query")]
async fn get_query_action(request: HttpRequest, query: web::Query<Params>) -> impl Responder {
_service_::get_query_action(request, query)
}
#[post("/json")]
async fn post_json_action(request: HttpRequest, input: web::Json<Params>) -> impl Responder {
_service_::post_json_action(request, input)
}
#[post("/user/{id}/{name}")]
async fn post_router_action(request: HttpRequest, path: web::Path<(String, String)>) -> impl Responder {
_service_::post_router_action(request, path)
}
#[post("/dynamic")]
async fn dynamic_query(request: HttpRequest, query: web::Query<HashMap<String, String>>) -> impl Responder {
_service_::dynamic_query(request, query)
}
#[post("/csv/{filename}")]
async fn csv_action(request: HttpRequest, path: web::Path<String>) -> impl Responder {
_service_::read_csv_action(request, path)
}
async fn register_service() -> Result<(), Error> {
let current_ip = local_ip().unwrap_or_else(|_| LOCALHOST.parse().unwrap());
ww_print!(format!(
"Starting server at {localhost}:{port}\nStarting server at {ip}:{port}",
ip = current_ip,
port = DEFAULT_PORT,
localhost = LOCALHOST
));
HttpServer::new(|| {
App::new()
.service(get_first_action)
.service(get_query_action)
.service(post_json_action)
.service(dynamic_query)
.service(post_router_action)
.service(csv_action)
})
.bind(format!("{}:{}", current_ip, DEFAULT_PORT))?
.bind(format!("{}:{}", LOCALHOST, DEFAULT_PORT))?
.run()
.await
}
#[actix_web::main]
async fn main() -> Result<(), Error> {
register_service().await
}
編譯二進制檔案
- 可以使用
cargo build
命令編譯二進制檔案,用cargo run
命令執行二進制檔案。 - 如果是編譯
debug
版的,檔案會在target/debug
目錄下。 - 如果是編譯
release
版的,檔案會在target/release
目錄下。
cargo build #編譯debug版
cargo run #執行debug版
cargo build --release #編譯release版
cargo run --release #執行release版
cargo build --target aarch64-apple-darwin --release #編譯aarch64-apple-darwin release版
cargo build --target x86_64-apple-darwin --release #編譯x86_64-apple-darwin release版
範例程式碼下載
後記
- 終於打完了,也感謝
Copilot
的幫忙,這篇應該筆者寫過最長的一篇吧?寫完之後,發現Rust還滿愛用巨集
的,也許是為了速度吧,在運算符號
上也為了分別,怕打錯字,也用了滿多其它語言沒有的,雖然說在Key的時候有點麻煩,但一眼就能看出是什麼意思。 - 沒想到Rust還真的是麻煩啊,如果用Golang / Java / Python / Node.js,開發速度一定是快得多的多了,不過做為一個下一代的C++,連Windows 11的核心也用它重寫了,雖然語法很麻煩,但就是…安全,連Google開始以Rust重寫Android裸機元件,進一步強化記憶體安全性,還有新酷音輸入法也用Rust重寫了,除了Linux群之外,不過要無償的工程師去維護兩套程式碼,也是滿說不過去的啦…