好容易終於撐到第十天了,十在中文裡有全或滿的意思,比如十全十美
、十全大補
、十分可愛
,希望大家不會滿到吸收不了 XDD。
直接新增一個tic_tac_toe.rs
檔案:
// web/src/main.rs
mod tic_tac_toe;
然後先看一下warp的todo範例來寫api,這次我們先從get著手:
/// The 4 TODOs filters combined.
pub fn todos(
db: Db,
) -> impl Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone {
todos_list(db.clone())
.or(todos_create(db.clone()))
.or(todos_update(db.clone()))
.or(todos_delete(db))
}
/// GET /todos?offset=3&limit=5
pub fn todos_list(
db: Db,
) -> impl Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone {
warp::path!("todos")
.and(warp::get())
.and(warp::query::<ListOptions>())
.and(with_db(db))
.and_then(handlers::list_todos)
}
看起來它的DB
是使用參數注入,把每個api chain在一起,fn回傳的impl Filter應該是要讓fn可以一直chain下去(就像鐵軌可以一直往下接),再對照一下db的初始化過程:
// main
let db = models::blank_db(); // 初始化db物件
let api = filters::todos(db); // 注入db至API使用
// mod models
pub fn blank_db() -> Db {
Arc::new(Mutex::new(Vec::new())) // 有沒有似曾相似的感覺
}
不過以我們目前的案例而言,db(in-memory)是放在service裡,所以我們把它的注入的對象db置換成我們的service,仿著著寫出主體框架,看看編譯器又要教我們什麼:
// web/src/tic_tac_toe.rs
use std::convert::Infallible;
use warp::Filter;
use service::tic_tac_toe::TicTacToeService;
pub fn router_games(
service: impl TicTacToeService // 把範例db改成我們的service
) -> impl Filter<Extract=(impl warp::Reply, ), Error=warp::Rejection> + Clone {
games_get(service.clone())
// .or(games_create(db.clone())) // 先註解,保留等等擴充
// .or(games_update(db.clone()))
// .or(games_delete(db))
}
/// GET /tic_tac_toe/:id
pub fn games_get(
service: impl TicTacToeService
) -> impl Filter<Extract=(impl warp::Reply, ), Error=warp::Rejection> + Clone {
// 這邊想用path傳id,是參考todo範例的delete怎麼傳path參數
warp::path!("tic_tac_toe" / usize) // 在路由裡把 id 放path
.and(warp::get()) // 設為 get 方法
.and(warp::any().map(move || service.clone())) // 仿範例with_db
.and_then(handle_games_get)
}
pub async fn handle_games_get(
id: usize,
service: impl TicTacToeService,
) -> Result<impl warp::Reply, Infallible> {
let game = service.get(id).unwrap(); // 先用unwrap讓程式通過
Ok(warp::reply::json(&game)) // 等等再來處理error
}
impl trait
是指可傳遞有實作該trait的具體類別第一個問題是我們的Service需要實現Clone
,一種是加到參數的限制條件,一種是加到我們原先的trait
裡。以這個情境我把Clone
補在原先的trait裡,這樣之後需要引用到時,就不用再參數逐項列示了:
@@ service/src/tic_tac_toe.rs @@
+pub trait TicTacToeService : Clone {
-pub trait TicTacToeService {
fn new_game(&self) -> Result<(usize, Game), Error>;
fn get(&self, id: usize) -> Result<Game, Error>;
這裡雖然看起來很像TicTacToeService
繼承了Clone
的trait,但其實不是。因為rust裡沒有繼承。這裡定義的Clone
叫Supertrait,只是加了一個限制,定義我們的Service trait如果要被實作,那麼實作的類別也要一起實作Clone
,直接看實例:
編譯器提醒我們,因為我們在trait TicTacToeService
加上Clone
這個supertrait,而我們先前只有實作CRUD,沒有實作Clone
,所以要補上這個限制條件。可以自己寫也可以用內建的derive clone巨集幫我們實作:
@@ service/src/tic_tac_toe.rs @@
+#[derive(Clone)]
struct InMemoryTicTacToeService {
games: Arc<Mutex<HashMap<usize, Game>>>,
加完後又是其他error:
這裡又說我們的game
沒有實作 Serialize
,所以不能轉json,在rust裡序列化及反序列化基本上使用的是serde這個套件,取Serialize及Deserialize字首為命名,立馬加入package:
如果是效能的追求者,也可以選擇rkyv來進行。
@@ Cargo.toml @@
rand = { version = "0.8" }
+serde = { version = "1.0", features = ["derive"] }
thiserror = { version = "1.0" }
@@ core/Cargo.toml @@
[dependencies]
rand = { worksapce = true }
+serde = { workspace = true }
thiserror = { worksapce = true }
@@ core/src/tic_tac_toe.rs @@
+use serde::Serialize;
...
/// 井字遊戲的符號。
+#[derive(Copy, Clone, Debug, PartialEq, Serialize)]
-#[derive(Copy, Clone, Debug, PartialEq)]
pub enum Symbol {
O,
...
/// 井字遊戲棋局
+#[derive(Debug, Clone, Serialize)]
-#[derive(Debug, Clone)]
pub struct Game {
接下來出現的問題有點小棘手,我這裡先放結論,把過程的放在補充資料:
這裡說因為我們用到了move
,所以被move
的東西要是可Send
的,通常Send
又會搭配Sync
一起,在「可延展的並行與 Sync 及 Send 特徵」有介紹。我們直接回到Serivce的trait加上:
@@ service/src/tic_tac_toe.rs @@
+pub trait TicTacToeService: Clone + Send + Sync {
-pub trait TicTacToeService: Clone {
fn new_game(&self) -> Result<(usize, Game), Error>;
改完上面的東西後,編譯是通過了,不過我們的功能還沒加到路由裡,到main裡面補上:
// web/src/main.rs
use service::tic_tac_toe::TicTacToeService;
// 略
let game_service = service::tic_tac_toe::InMemoryTicTacToeService::new();
game_service.new_game().unwrap(); // 裡面是空的,先造一筆資料方便測試
let api_games = tic_tac_toe::router_games(game_service);
let routes = hello
.or(api_games)
記得要把service裡的相關東西改pub:
@@ service/src/tic_tac_toe.rs @@
#[derive(Clone)]
+pub struct InMemoryTicTacToeService {
-struct InMemoryTicTacToeService {
games: Arc<Mutex<HashMap<usize, Game>>>,
}
impl InMemoryTicTacToeService {
+ pub fn new() -> Self {
- fn new() -> Self {
Self {
games: Arc::new(Mutex::new(HashMap::new())),
好容易我們把rust的抱怨都解完了,接著開啟網頁http://localhost:3030/tic_tac_toe/1試一下api內容:
調整一下剛剛打API的參數,把 id 改成 2,看看會發生什麼事情:
看一下後端的log:
因為我們剛剛沒處理錯誤,所以在log裡可以看到出現了panic
恐慌。rust裡的恐慌panic一般是指發生了無法復原的錯誤,這裡的panic是unwrap
發出來的,我們知道unwrap
是去幫我們解開Ok
的資料結果,但這裡的結果是Err
,所以無法解開Ok
,故而直接報錯並中斷。我們先著手進行錯誤處理的修正吧,看一下warp裡是使用Rejection來回傳錯類別給API呼叫者:
// warp/examples/rejections.rs
let routes = div_with_header.or(div_with_body)
.recover(handle_rejection);
fn div_by() -> impl Filter<Extract = (NonZeroU16,), Error = Rejection> + Copy {
warp::header::<u16>("div-by").and_then(|n: u16| async move {
if let Some(denom) = NonZeroU16::new(n) {
Ok(denom)
} else {
Err(reject::custom(DivideByZero))
}
})
}
#[derive(Serialize)]
struct ErrorMessage {
code: u16,
message: String,
}
async fn handle_rejection(err: Rejection) -> Result<impl Reply, Infallible> {
// 略
let json = warp::reply::json(&ErrorMessage {
code: code.as_u16(),
message: message.into(),
});
Ok(warp::reply::with_status(json, code))
}
以上截取部分內容,看起來是在handle function裡丟出Error,這個Error是Rejection認得的(Error = Rejection
),我們仿照這個方式先寫個自己的錯誤訊息type:
// web/src/main.rs
mod error;
// web/src/error.rs
use std::convert::Infallible;
use std::error::Error;
use warp::{Rejection, Reply};
use warp::http::StatusCode;
pub enum AppError { // 我們在這定義web專案可能會遇到的錯誤
UserFriendly(String, String), // 回傳訊息給予前端User使用
BadRequest(String), // 錯誤的要求
NotFound(String), // 找不到資源
Unauthorized, // 未經授權的操作
InternalServerError, // 其他未歸類錯誤
}
struct AppErrorMessage { // 非 2XX 回應的Body
message: String, // 錯誤的訊息內容
details: Option<String>, // 有關錯誤的細節資料(如果有的話)
}
pub async fn handle_rejection(err: Rejection) -> Result<impl Reply, Infallible> {
let code; // http 回應代碼
let message; // http 回應訊息
let mut details = None; // http 回應細息
if let Some(AppError::UserFriendly(msg, detail)) = err.find() {
code = StatusCode::BAD_REQUEST; // HTTP回應代碼
message = msg.as_str(); // 所有message賦值都同類型,
details = Some(detail.to_string()); // 所以我們統一轉成 &str
// 略... 各種mapping,詳見原始碼
} else {
tracing::error!("unhandled rejection: {:?}", err); // 輸出至log
code = StatusCode::INTERNAL_SERVER_ERROR;
message = "UNHANDLED_REJECTION"; // <= 字串值本身也是&str類別
}
let json = warp::reply::json(&AppErrorMessage {
message: message.into(),
details: details.into(),
});
Ok(warp::reply::with_status(json, code))
}
大略照著寫一下,然後就差不多要開始解任務了:
這個我們知道,AppErrorMessage
要序列化成Json要加Serialize
的derive
,記得Cargo也要加引用:
@@ web/Cargo.toml @@
dotenvy = { workspace = true }
+serde = { workspace = true }
tokio = { workspace = true }
@@ web/src/error.rs @@
+#[derive(serde::Serialize)]
struct AppErrorMessage {
編譯過了,繼續小步向前:
@@ web/src/main.rs @@
let routes = hello
.or(api_games)
+ .recover(error::handle_rejection)
.with(warp::trace::request())
沒有問題,繼續試著把handle裡的unwrap改為拋出error,如下:
@@ web/src/tic_tac_toe.rs @@
pub async fn handle_games_get(id: usize, service: impl TicTacToeService) -> Result<impl warp::Reply, Infallible> {
+ let game = service.get(id)?;
- let game = service.get(id).unwrap();
Ok(warp::reply::json(&game))
rust說,這個方法不接受我們回傳的錯誤,比對一下剛剛的範例和games_get
的Error type使用的是warp::Rejection
,我們把這個方法的錯誤類別改Rejection
試試。
+use warp::{Filter, Rejection};
-use warp::Filter;
...
pub async fn handle_games_get(
id: usize,
service: impl TicTacToeService,
+) -> Result<impl warp::Reply, Rejection> {
-) -> Result<impl warp::Reply, Infallible> {
let game = service.get(id)?;
Ok(warp::reply::json(&game))
第一個warning提示我們有未使用的use
,把它移掉就好。再來是要實作(impl) Rejct
這個trait給service裡的Error
(這裡拋出來的)。記得套用trait的規則嗎,至少其中一個要是在這個crate內定義的,在這裡這兩個都是外部crate引入的,所以我們無法在這裡實作,我們就先把service裡的Error
轉換為剛剛設定的AppError
把,還記得From
trait嗎,我們在error裡寫From,然後這次透過map_err
轉換錯誤:
@@ web/src/tic_tac_toe.rs @@
-use std::convert::Infallible;
use warp::Filter;
use service::tic_tac_toe::TicTacToeService;
+use crate::error::AppError;
...
+let game = service.get(id).map_err(AppError::from)?;
-let game = service.get(id)?;
@@ web/src/error.rs @@
+impl From<service::tic_tac_toe::Error> for AppError {
+ fn from(value: service::tic_tac_toe::Error) -> Self {
+ match value {
+ service::tic_tac_toe::Error::GameRules(message)
+ => AppError::UserFriendly("違反遊戲規則".into(), message),
+ service::tic_tac_toe::Error::GameOver
+ => AppError::BadRequest("遊戲已結束".into()),
+ service::tic_tac_toe::Error::NotFound
+ => AppError::NotFound("遊戲不存在".into()),
+ service::tic_tac_toe::Error::Unknown
+ => AppError::InternalServerError,
+ }
+ }
+}
map_err
裡面也可以傳遞fn
,轉換完的Error就可以往外拋了(喂!不是那個外拋啦)
這裡又要實作一個trait,好在AppError
是我們在這寫的,可以實作:
@@ web/src/error.rs @@
+impl warp::reject::Reject for AppError {}
依說明補上derive:
+#[derive(Debug)]
pub enum AppError {
UserFriendly(String, String),
好了我們來試一下API,這裡我用VScode的Thunder client演示API的測試:
root的路由我們沒設定,所以出現了剛剛handle_rejection
裡最後一項未處理的錯誤:500
UNHANDLED_REJECTION
,接著試一下我們的API:
打一個正確的,如上圖正確顯示200 OK,及正確的json body。
再打一個錯誤的id,果然出現了我們剛剛包的錯誤訊息,如果我們想顯示更詳細的資訊的話,可以客製Error enum的Variant,加上想要的資料進行傳遞與顯示,
還記得剛剛在實作AppError
的from時,因為Error會撞名所以我們要拉很長的namespace路徑嗎?這時候可以用type簡化寫法:
// web/src/error.rs
type GameSrvError = service::tic_tac_toe::Error; // 定義給下面用的type。
impl From<GameSrvError> for AppError {
fn from(value: GameSrvError) -> Self {
match value {
GameSrvError::GameRules(message)
=> AppError::UserFriendly("違反遊戲規則".into(), message),
GameSrvError::GameOver
=> AppError::BadRequest("遊戲已結束".into()),
GameSrvError::NotFound => AppError::NotFound("遊戲不存在".into()),
GameSrvError::Unknown => AppError::InternalServerError,
}
}
Create方法,我在測試的時候無意間Copilot幫忙寫了另一種寫法,把獨立的方法改放至閉包裡,我在這邊列出來給大家參考:
// web/src/tic_tac_toe.rs
/// POST /tic_tac_toe/
pub fn games_create(
service: impl TicTacToeService
) -> impl Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone {
warp::path!("tic_tac_toe" )
.and(warp::post())
.and_then(move || {
let service = service.clone();
async move {
let games = service.new_game().map_err(AppError::from)?;
Ok::<_, warp::Rejection>(warp::reply::json(&games))
}
})
}
@@ web/src/tic_tac_toe.rs @@
games_get(service.clone())
+ .or(games_create(service.clone()))
- // .or(games_create(db.clone()))
上面連續POST兩筆,序號有正確遞增(id: 2 -> 3),用GET看看是否有正確讀出:
寫法OK,繼續補完剩下的API:
pub fn router_games(
service: impl TicTacToeService
) -> impl Filter<Extract = (impl warp::Reply,), Error = Rejection> + Clone {
games_get(service.clone())
.or(games_create(service.clone()))
.or(games_play(service.clone()))
.or(games_delete(service))
}
/// PUT /tic_tac_toe/:id/:num
pub fn games_play(
service: impl TicTacToeService
) -> impl Filter<Extract=(impl warp::Reply, ), Error=Rejection> + Clone {
warp::path!("tic_tac_toe" / usize / usize)
.and(warp::put())
.and_then(move |id, num| {
let service = service.clone();
async move {
let games = service.play(id, num).map_err(AppError::from)?;
Ok::<_, Rejection>(warp::reply::json(&games))
}
})
}
/// DELETE /tic_tac_toe/:id
pub fn games_delete(
service: impl TicTacToeService
) -> impl Filter<Extract=(impl warp::Reply, ), Error=Rejection> + Clone {
warp::path!("tic_tac_toe" / usize)
.and(warp::delete())
.and_then(move |id| {
let service = service.clone();
async move {
let games = service.delete(id).map_err(AppError::from)?;
Ok::<_, Rejection>(warp::reply::json(&games))
}
})
}
測一下,看起來都正常作動了,但其間我還有測到一個問題,就是我們輸入遊戲的步數並沒有檢核:
錯誤是在core裡發生panic
了,所以沒有依我們回傳error給予前端的路徑走:
我們調整一下,到核心層(domain layer)調整play
方法,順便加上一個新的錯誤型態:
@@ core/src/tic_tac_toe.rs @@
/// 井字遊戲的錯誤類型。
#[derive(Debug, thiserror::Error)]
pub enum Error {
+ #[error("錯誤的格號,請填1至9間的數字!")]
+ WrongStep,
...
pub fn play(&mut self, num: usize) -> Result<(), Error> {
+ if num < 1 || num > 9 {
+ return Err(Error::WrongStep);
+ }
這裡加好就可以了,看一下結果是正確的:
不過剛剛的num <1 || num > 9
好像有先前遇過,可以用range的寫法,不太記得了,直接請clippy幫我們回憶一下:
既然都請出clippy了,我們順便修一下其他的部分:
軟體的修改成本越早越小,可以看一下測試左移的概念。
這邊提到我們應該考慮對InMemoryTicTacToeService
實作Default
trait,咦可是我們不是已經有給new
了嗎,先不要自以為是覺得很多餘跳過它,看一下這邊的help說什麼:
這裡有說為什麼不好,因為user(應該是指其他引用到我們程式的開發人員)會預期有一個無參數的建構式,因為new是我們自己建立的,到時候可能會改成有參數的版本,所以clippy會建議提供一個Default
trait的,給定一個無參數的預設版本,因為rust並沒有支援function多載,也就是說你不能像其他語言一樣:
public void Hello();
public void Hello(string name);
public void Hello(int id);
那我們在這依照建議加一下Default
trait的實作吧:
@@ service/src/tic_tac_toe.rs @@
+impl Default for InMemoryTicTacToeService {
+ fn default() -> Self {
+ Self {
+ games: Arc::new(Mutex::new(HashMap::new())),
+ }
+ }
+}
接下來又說 這裡的let game
取得的是(unit value),請我們拿掉:
Why is this bad?
A unit value cannot usefully be used anywhere. So binding one is kind of pointless.
unit value就是指()
這個0元素的tuple(?),因為?
會把error外拋,所以在這邊的games
只會拿到()
,這樣指派好像沒什麼意義。我們直接拿掉就好了。
然後又發現了一個小bug (?),剛剛在delete的api太開心地把play copy下來改,忘了delete應該沒有回傳值,所以我們改掉:
+service.delete(id).map_err(AppError::from)?;
-let games = service.delete(id).map_err(AppError::from)?;
+Ok::<_, Rejection>(warp::reply())
-Ok::<_, Rejection>(warp::reply::json(&games))
測試結果回傳200 OK,好像哪裡怪怪的,沒內容應該要是204 No Content,再回去看一下warp的範例可以直接回傳如下:
@@ web/src/tic_tac_toe.rs @@
+use warp::http::StatusCode;
...
+Ok::<_, Rejection>(StatusCode::NO_CONTENT)
-Ok::<_, Rejection>(warp::reply())
最後我們回傳錯誤訊息的地方details並沒有型別轉換,不需要用到into()
,
複習一下,into是from的同義函數,我們實作 A From B 可以自動得到 B into A。
拿掉後:
這邊又說同名可以省略,記得之前第3篇提到rust的物件shorthand嗎,這邊rust可以使用這個寫法改寫如下:
@@ web/src/error.rs @@
let json = warp::reply::json(&AppErrorMessage {
message: message.into(),
+ details,
- details: details.into(),
});
最後我們補一下測試完成REST API的章節吧,打了前兩行,副駕幫我生成後續的案例:
記得自己補
run.sh
或run.ps1
,或是喜歡自己打命令也可以 XDD
// web/src/tic_tac_toe.rs
#[cfg(test)]
mod tests {
use super::*;
use warp::http::StatusCode;
use warp::test::request;
use service::tic_tac_toe::InMemoryTicTacToeService;
#[tokio::test]
async fn test_games_get() {
let service = InMemoryTicTacToeService::new();
let (id, _) = service.new_game().unwrap();
let api = games_get(service);
let res = request()
.method("GET")
.path(&format!("/tic_tac_toe/{}", id))
.reply(&api)
.await;
assert_eq!(res.status(), StatusCode::OK);
}
#[tokio::test]
async fn test_games_create() {
let service = InMemoryTicTacToeService::new();
let api = games_create(service);
let res = request()
.method("POST")
.path("/tic_tac_toe")
.reply(&api)
.await;
assert_eq!(res.status(), StatusCode::OK);
}
#[tokio::test]
async fn test_games_play() {
let service = InMemoryTicTacToeService::new();
let (id, _) = service.new_game().unwrap();
let api = games_play(service);
let res = request()
.method("PUT")
.path(&format!("/tic_tac_toe/{}/{}", id, 1))
.reply(&api)
.await;
assert_eq!(res.status(), StatusCode::OK);
}
#[tokio::test]
async fn test_games_delete() {
let service = InMemoryTicTacToeService::new();
let (id, _) = service.new_game().unwrap();
let api = games_delete(service);
let res = request()
.method("DELETE")
.path(&format!("/tic_tac_toe/{}", id))
.reply(&api)
.await;
assert_eq!(res.status(), StatusCode::NO_CONTENT);
}
}
但是卻出現了很奇怪的問題:
這裡的test要使用tokio是因為我們的測試fn是async的,所以請tokio來協助我們跑,就像之前的#[tokio::main]
一樣,不過我們tokio引用的feature已經是full了,應該不會再缺東西了,他說無法在core
裡找到prelude
,prelude在rust裡有預載入模組的意思,這裡找不到,花了一點時間也google不到相關的錯誤訊息,啊,忽然想到,該不會是我們一開始命名的專案core跟它打架了吧!囧。
rust裡要怎麼處理同名crate,可以看Cargo Book的說明:
@@ web/Cargo.toml @@
+my-core = { path = "../core", package = "core" }
-core = { path = "../core" }
我們把前面rename成my-core,(在web裡要引入的命名空間),在補上我們在"../core"這裡要引入的package名字是core
,如此即可,只是在web裡如果要使用到我們的core,namespace的路徑就都要改成my_core
:
let game = my_core::tic_tac_toe::Game::default();
記得把
-
改_
上面幾項happy path都通過了,我們試一下不happy的路徑會怎樣:再提供往下測試的一些方式:
// web/src/tic_tac_toe.rs
mod tests {
use crate::error::handle_rejection;
// ... 略
#[tokio::test]
async fn test_games_delete_not_found() {
let service = InMemoryTicTacToeService::new();
let api = games_delete(service)
.recover(handle_rejection); // 記得加上error handling
let res = request() // 沒加handle_rejection的話,
.method("DELETE") // 這裡的回應會走預設的500
.path(&format!("/tic_tac_toe/{}", 12))
.reply(&api)
.await;
assert_eq!(res.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn test_games_play_non_empty() {
let service = InMemoryTicTacToeService::new();
let (id, _) = service.new_game().unwrap();
let api = games_play(service.clone()).recover(handle_rejection);
service.play(id, 1).unwrap(); // 模擬已下第一格
let res = request()
.method("PUT")
.path(&format!("/tic_tac_toe/{}/{}", id, 1)) // 格號非空應報錯
.reply(&api)
.await;
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
}
}
如果想要測試response的body內容,則需要反序列化(deserialize)json檔,我們需要serde_json來幫我們處理:
@@ Cargo.toml @@
serde = { version = "1.0", features = ["derive"] }
+serde_json = { version = "1.0"}
thiserror = { version = "1.0" }
@@ web/Cargo.toml @@
serde = { workspace = true }
+serde_json = { workspace = true }
tokio = { workspace = true }
@@ core/src/tic_tac_toe.rs @@
+use serde::{Deserialize, Serialize};
-use serde::Serialize;
...
+#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
-#[derive(Copy, Clone, Debug, PartialEq, Serialize)]
pub enum Symbol {
...
+#[derive(Debug, Clone, Serialize, Deserialize)]
-#[derive(Debug, Clone, Serialize)]
pub struct Game {
// web/src/tic_tac_toe.rs
mod test {
/// ...略
use my_core::tic_tac_toe::Game;
async fn test_games_get() {
// ...略
let body = res.into_body(); // 取得body的Bytes
// 使用serde_json把字串反序列化為物件
let game: Game = serde_json::from_slice(&body).unwrap();
assert_eq!(game.is_over, false);
assert_eq!(game.winner, None);
let is_empty = game.cells.iter().all(|x| *x == None);
assert_eq!(is_empty, true);
}
}
測試本身也是一門藝術,我們先點到這邊,本篇我們完成了REST API,以及測試,下一篇可以開始進入前端了。
雖然clippy對我們的的代碼品質很有幫助,不過有時候有些情境可能要先空著,比如dead_code,或有人很堅持要不想改成建議的方式,是可以單獨設定關掉提示:在clippy的訊息會說是什麼原因造成,照著找那個項目加上allow的屬性就可以,比如剛剛的數字range,我們看原提示:
最下面有個 #[warn(clippy::manual_range_contains)]
表示這個規則檔被掃出來,我們在play方法上加以下:
+#[allow(clippy::manual_range_contains)]
pub fn play(&mut self, num: usize) -> Result<(), Error> {
就可以讓clippy安靜了,另外如果想要做整個專案的設定,可以參考Clippy的設定檔文件。
我覺得在學習中,常常會遇到不同的錯誤訊息,接著花很多時間處理,這裡順這個機會示範一下如何去找錯誤的方式,提供大家不同的思路。大家不要害怕程式噴出的錯誤訊息,往往我們的經驗就是從解bug中獲得的(?)。
我們看rust跑出來的結果很奇怪,而且很嚇人(?)
我們先看紅字的部分,And<And<And<...
隱約可以猜出因為我們呼叫了很多次and,所以泛型trait的type也一直train在一起,在這邊至少我們可以推知我們傳的handle_games_get需要符合參數傳遞的要求。
再看下面一堆白字第一段最後面有寫到第21:30
是第21行第30個字,對照我們的程式碼是在:
.and(warp::any().map(move || service.clone())) // <= 第30字是 move開始
但知道這樣好像沒什麼幫助(?),再回到畫面中間藍字說不符合 Filter或FilterBase要求,上面有提到warp原始檔的位置,就再去看一下warp的原始檔說了什麼:
#[derive(Clone, Copy, Debug)]
pub struct And<T, U> {
pub(super) first: T,
pub(super) second: U,
}
impl<T, U> FilterBase for And<T, U>
where
T: Filter,
T::Extract: Send,
U: Filter + Clone + Send,
<T::Extract as Tuple>::HList: Combine<<U::Extract as Tuple>::HList> + Send,
CombinedTuples<T::Extract, U::Extract>: Send,
U::Error: CombineRejection<T::Error>,
{
type Extract = CombinedTuples<T::Extract, U::Extract>;
type Error = <U::Error as CombineRejection<T::Error>>::One;
type Future = AndFuture<T, U>;
fn filter(&self, _: Internal) -> Self::Future {
AndFuture {
state: State::First(self.first.filter(Internal), self.second.clone()),
}
}
}
可以看出 And<T, U> 的U類別,需要套用 Filter + Clone + Send,而我們參數裡的service只有clone並沒有send,所以把我們要傳遞的service加上 Send + Sync 試試
// service/src/tic_tac_toe.rs
pub trait TicTacToeService: Clone + Send + Sync {
或者是一開始把回到warp的todo範例,不要自作聰明想節省行數,就亂結合fn (?):
pub fn games_get(
service: impl TicTacToeService
) -> impl Filter<Extract=(impl warp::Reply, ), Error=warp::Rejection> + Clone {
warp::path!("tic_tac_toe" / usize)
.and(warp::get())
.with_service(service) // <= 把move改這行
.and_then(handle_games_get)
}
// 把範例的with_db改為with_service
fn with_service(service: impl TicTacToeService) -> impl Filter<Extract=(impl TicTacToeService, ), Error=std::convert::Infallible> + Clone {
warp::any().map(move || service.clone())
}
這時候編譯器的提示就會清楚很多了:
本系列專案源始碼放置於 https://github.com/kenstt/demo-app