From 6d7b20b8f00be8b9cc86ad835d64bceca8a8b628 Mon Sep 17 00:00:00 2001 From: Martin Pitt Date: Fri, 9 Dec 2022 08:47:09 +0100 Subject: [PATCH 01/16] warp-server: Move handlers and filters into submodules Exposing the filters as functions makes them accessible to unit testing. --- warp-server/src/main.rs | 147 ++++++++++++++++++++++------------------ 1 file changed, 80 insertions(+), 67 deletions(-) diff --git a/warp-server/src/main.rs b/warp-server/src/main.rs index 0664684..b23f2af 100644 --- a/warp-server/src/main.rs +++ b/warp-server/src/main.rs @@ -1,85 +1,98 @@ -use futures_util::{FutureExt, StreamExt, SinkExt}; -use warp::Filter; +mod handlers { + use futures_util::{FutureExt, StreamExt, SinkExt}; -// GET /hello/warp => 200 OK with body "Hello, warp!" -async fn hello(name: String, agent: String) -> Result { - Ok(format!("Hello, {} from {}!", name, agent)) -} + // GET /hello/warp => 200 OK with body "Hello, warp!" + pub async fn hello(name: String, agent: String) -> Result { + Ok(format!("Hello, {} from {}!", name, agent)) + } -// websocat ws://127.0.0.1:3030/ws-echo -async fn ws_echo_connected(websocket: warp::ws::WebSocket) { - // echo all messages back - let (tx, rx) = websocket.split(); - rx.forward(tx).map(|result| { - if let Err(e) = result { - log::warn!("websocket error: {:?}", e); - } - }).await; -} + // websocat ws://127.0.0.1:3030/ws-echo + pub async fn ws_echo_connected(websocket: warp::ws::WebSocket) { + // echo all messages back + let (tx, rx) = websocket.split(); + rx.forward(tx).map(|result| { + if let Err(e) = result { + log::warn!("websocket error: {:?}", e); + } + }).await; + } -// websocat ws://127.0.0.1:3030/ws-rev -async fn ws_rev_connected(websocket: warp::ws::WebSocket) { - // echo all messages back - tokio::task::spawn(async { - let (mut tx, mut rx) = websocket.split(); - while let Some(message) = rx.next().await { - let msg = match message { - Ok(msg) => msg, - Err(e) => { - log::error!("websocket error: {}", e); - break; - } - }; - log::info!("ws_rev_connected: got message: {:?}", msg); + // websocat ws://127.0.0.1:3030/ws-rev + pub async fn ws_rev_connected(websocket: warp::ws::WebSocket) { + // echo all messages back + tokio::task::spawn(async { + let (mut tx, mut rx) = websocket.split(); + while let Some(message) = rx.next().await { + let msg = match message { + Ok(msg) => msg, + Err(e) => { + log::error!("websocket error: {}", e); + break; + } + }; + log::info!("ws_rev_connected: got message: {:?}", msg); - if msg.is_close() { - break; - } - if msg.is_text() { - let text = msg.to_str().unwrap(); - let rev = text.chars().rev().collect::(); - if let Err(e) = tx.send(warp::ws::Message::text(rev)).await { - // disconnected - log::info!("peer disconnected: {}", e); + if msg.is_close() { break; } - } - if msg.is_binary() { - let mut rev = msg.into_bytes(); - rev.reverse(); - if let Err(e) = tx.send(warp::ws::Message::binary(rev)).await { - // disconnected - log::info!("peer disconnected: {}", e); - break; + if msg.is_text() { + let text = msg.to_str().unwrap(); + let rev = text.chars().rev().collect::(); + if let Err(e) = tx.send(warp::ws::Message::text(rev)).await { + // disconnected + log::info!("peer disconnected: {}", e); + break; + } + } + if msg.is_binary() { + let mut rev = msg.into_bytes(); + rev.reverse(); + if let Err(e) = tx.send(warp::ws::Message::binary(rev)).await { + // disconnected + log::info!("peer disconnected: {}", e); + break; + } } } - } - log::info!("ws_rev_connected ended"); - }); + log::info!("ws_rev_connected ended"); + }); + } } -#[tokio::main] -async fn main() { - env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); +mod filters { + use warp::Filter; + use super::handlers; - let hello = warp::path!("hello" / String) - .and(warp::header::("user-agent")) - .and_then(hello); + pub fn hello() -> impl Filter + Clone { + warp::path!("hello" / String) + .and(warp::header::("user-agent")) + .and_then(handlers::hello) + } - let ws_echo = warp::path("ws-echo") - .and(warp::ws()) - .map(|ws: warp::ws::Ws| { ws.on_upgrade(ws_echo_connected) }); + pub fn ws_echo() -> impl Filter + Clone { + warp::path("ws-echo") + .and(warp::ws()) + .map(|ws: warp::ws::Ws| { ws.on_upgrade(handlers::ws_echo_connected) }) + } - let ws_rev = warp::path("ws-rev") - .and(warp::ws()) - .map(|ws: warp::ws::Ws| { ws.on_upgrade(ws_rev_connected) }); + pub fn ws_rev() -> impl Filter + Clone { + warp::path("ws-rev") + .and(warp::ws()) + .map(|ws: warp::ws::Ws| { ws.on_upgrade(handlers::ws_rev_connected) }) + } - let api = hello - .or(ws_echo) - .or(ws_rev) - .with(warp::log("warp-server")); + pub fn api() -> impl Filter + Clone { + hello() + .or(ws_echo()) + .or(ws_rev()) + .with(warp::log("warp-server")) + } +} - warp::serve(api) +#[tokio::main] +async fn main() { + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); + warp::serve(filters::api()) .run(([127, 0, 0, 1], 3030)) .await; } -- 2.39.2 From 354db3bfde77c75c9d53a76b7f3bc09de3243e00 Mon Sep 17 00:00:00 2001 From: Martin Pitt Date: Fri, 9 Dec 2022 09:02:25 +0100 Subject: [PATCH 02/16] Revert "warp-server: Move handlers and filters into submodules" We don't need this after all, exposing the API filter is enough. This reverts commit 6d7b20b8f00be8b9cc86ad835d64bceca8a8b628. --- warp-server/src/main.rs | 147 ++++++++++++++++++---------------------- 1 file changed, 67 insertions(+), 80 deletions(-) diff --git a/warp-server/src/main.rs b/warp-server/src/main.rs index b23f2af..0664684 100644 --- a/warp-server/src/main.rs +++ b/warp-server/src/main.rs @@ -1,98 +1,85 @@ -mod handlers { - use futures_util::{FutureExt, StreamExt, SinkExt}; +use futures_util::{FutureExt, StreamExt, SinkExt}; +use warp::Filter; - // GET /hello/warp => 200 OK with body "Hello, warp!" - pub async fn hello(name: String, agent: String) -> Result { - Ok(format!("Hello, {} from {}!", name, agent)) - } - - // websocat ws://127.0.0.1:3030/ws-echo - pub async fn ws_echo_connected(websocket: warp::ws::WebSocket) { - // echo all messages back - let (tx, rx) = websocket.split(); - rx.forward(tx).map(|result| { - if let Err(e) = result { - log::warn!("websocket error: {:?}", e); - } - }).await; - } +// GET /hello/warp => 200 OK with body "Hello, warp!" +async fn hello(name: String, agent: String) -> Result { + Ok(format!("Hello, {} from {}!", name, agent)) +} - // websocat ws://127.0.0.1:3030/ws-rev - pub async fn ws_rev_connected(websocket: warp::ws::WebSocket) { - // echo all messages back - tokio::task::spawn(async { - let (mut tx, mut rx) = websocket.split(); - while let Some(message) = rx.next().await { - let msg = match message { - Ok(msg) => msg, - Err(e) => { - log::error!("websocket error: {}", e); - break; - } - }; - log::info!("ws_rev_connected: got message: {:?}", msg); +// websocat ws://127.0.0.1:3030/ws-echo +async fn ws_echo_connected(websocket: warp::ws::WebSocket) { + // echo all messages back + let (tx, rx) = websocket.split(); + rx.forward(tx).map(|result| { + if let Err(e) = result { + log::warn!("websocket error: {:?}", e); + } + }).await; +} - if msg.is_close() { +// websocat ws://127.0.0.1:3030/ws-rev +async fn ws_rev_connected(websocket: warp::ws::WebSocket) { + // echo all messages back + tokio::task::spawn(async { + let (mut tx, mut rx) = websocket.split(); + while let Some(message) = rx.next().await { + let msg = match message { + Ok(msg) => msg, + Err(e) => { + log::error!("websocket error: {}", e); break; } - if msg.is_text() { - let text = msg.to_str().unwrap(); - let rev = text.chars().rev().collect::(); - if let Err(e) = tx.send(warp::ws::Message::text(rev)).await { - // disconnected - log::info!("peer disconnected: {}", e); - break; - } + }; + log::info!("ws_rev_connected: got message: {:?}", msg); + + if msg.is_close() { + break; + } + if msg.is_text() { + let text = msg.to_str().unwrap(); + let rev = text.chars().rev().collect::(); + if let Err(e) = tx.send(warp::ws::Message::text(rev)).await { + // disconnected + log::info!("peer disconnected: {}", e); + break; } - if msg.is_binary() { - let mut rev = msg.into_bytes(); - rev.reverse(); - if let Err(e) = tx.send(warp::ws::Message::binary(rev)).await { - // disconnected - log::info!("peer disconnected: {}", e); - break; - } + } + if msg.is_binary() { + let mut rev = msg.into_bytes(); + rev.reverse(); + if let Err(e) = tx.send(warp::ws::Message::binary(rev)).await { + // disconnected + log::info!("peer disconnected: {}", e); + break; } } - log::info!("ws_rev_connected ended"); - }); - } + } + log::info!("ws_rev_connected ended"); + }); } -mod filters { - use warp::Filter; - use super::handlers; +#[tokio::main] +async fn main() { + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); - pub fn hello() -> impl Filter + Clone { - warp::path!("hello" / String) - .and(warp::header::("user-agent")) - .and_then(handlers::hello) - } + let hello = warp::path!("hello" / String) + .and(warp::header::("user-agent")) + .and_then(hello); - pub fn ws_echo() -> impl Filter + Clone { - warp::path("ws-echo") - .and(warp::ws()) - .map(|ws: warp::ws::Ws| { ws.on_upgrade(handlers::ws_echo_connected) }) - } + let ws_echo = warp::path("ws-echo") + .and(warp::ws()) + .map(|ws: warp::ws::Ws| { ws.on_upgrade(ws_echo_connected) }); - pub fn ws_rev() -> impl Filter + Clone { - warp::path("ws-rev") - .and(warp::ws()) - .map(|ws: warp::ws::Ws| { ws.on_upgrade(handlers::ws_rev_connected) }) - } + let ws_rev = warp::path("ws-rev") + .and(warp::ws()) + .map(|ws: warp::ws::Ws| { ws.on_upgrade(ws_rev_connected) }); - pub fn api() -> impl Filter + Clone { - hello() - .or(ws_echo()) - .or(ws_rev()) - .with(warp::log("warp-server")) - } -} + let api = hello + .or(ws_echo) + .or(ws_rev) + .with(warp::log("warp-server")); -#[tokio::main] -async fn main() { - env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); - warp::serve(filters::api()) + warp::serve(api) .run(([127, 0, 0, 1], 3030)) .await; } -- 2.39.2 From be3212fb7c5a4b7a9f595f51f821c79cbda9df3a Mon Sep 17 00:00:00 2001 From: Martin Pitt Date: Fri, 9 Dec 2022 09:00:12 +0100 Subject: [PATCH 03/16] warp-server: Add unit test for /hello route Split out the API into a separate function, to make it accessible for unit tests. --- warp-server/src/main.rs | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/warp-server/src/main.rs b/warp-server/src/main.rs index 0664684..7aa6f97 100644 --- a/warp-server/src/main.rs +++ b/warp-server/src/main.rs @@ -58,10 +58,7 @@ async fn ws_rev_connected(websocket: warp::ws::WebSocket) { }); } -#[tokio::main] -async fn main() { - env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); - +pub fn api() -> impl Filter + Clone { let hello = warp::path!("hello" / String) .and(warp::header::("user-agent")) .and_then(hello); @@ -74,12 +71,31 @@ async fn main() { .and(warp::ws()) .map(|ws: warp::ws::Ws| { ws.on_upgrade(ws_rev_connected) }); - let api = hello + hello .or(ws_echo) .or(ws_rev) - .with(warp::log("warp-server")); + .with(warp::log("warp-server")) +} - warp::serve(api) +#[tokio::main] +async fn main() { + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); + + warp::serve(api()) .run(([127, 0, 0, 1], 3030)) .await; } + +#[cfg(test)] +mod tests { + #[tokio::test] + async fn test_hello() { + let res = warp::test::request() + .path("/hello/rust") + .header("user-agent", "TestBrowser 0.1") + .reply(&super::api()) + .await; + assert_eq!(res.status(), 200); + assert_eq!(res.body(), "Hello, rust from TestBrowser 0.1!"); + } +} -- 2.39.2 From f89086d8e40c377921576e04038d7a08680f7907 Mon Sep 17 00:00:00 2001 From: Martin Pitt Date: Fri, 9 Dec 2022 09:59:39 +0100 Subject: [PATCH 04/16] warp-server: Add unit tests for websocket routes --- warp-server/src/main.rs | 48 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/warp-server/src/main.rs b/warp-server/src/main.rs index 7aa6f97..fe36ca7 100644 --- a/warp-server/src/main.rs +++ b/warp-server/src/main.rs @@ -98,4 +98,52 @@ mod tests { assert_eq!(res.status(), 200); assert_eq!(res.body(), "Hello, rust from TestBrowser 0.1!"); } + + #[tokio::test] + async fn test_ws_echo() { + let mut client = warp::test::ws() + .path("/ws-echo") + .handshake(super::api()) + .await + .expect("handshake failed"); + + // text + client.send_text("Hello").await; + let reply = client.recv().await.unwrap(); + assert_eq!(reply.to_str().unwrap(), "Hello"); + + // binary + let msg: Vec = vec![42, 99]; + client.send(warp::ws::Message::binary(msg.clone())).await; + let reply = client.recv().await.unwrap(); + assert_eq!(reply.as_bytes(), &msg); + + //close + client.send(warp::ws::Message::close()).await; + client.recv_closed().await.unwrap(); + } + + #[tokio::test] + async fn test_ws_rev() { + let mut client = warp::test::ws() + .path("/ws-rev") + .handshake(super::api()) + .await + .expect("handshake failed"); + + // text + client.send_text("Hello\n").await; + let reply = client.recv().await.unwrap(); + assert_eq!(reply.to_str().unwrap(), "\nolleH"); + + // binary + let msg: Vec = vec![42, 99]; + client.send(warp::ws::Message::binary(msg)).await; + let reply = client.recv().await.unwrap(); + assert_eq!(reply.as_bytes(), vec![99, 42]); + + //close + client.send(warp::ws::Message::close()).await; + client.recv_closed().await.unwrap(); + } } -- 2.39.2 From 25dad5d64048c91e315daf4199753c9cab3a1496 Mon Sep 17 00:00:00 2001 From: Martin Pitt Date: Fri, 9 Dec 2022 10:25:18 +0100 Subject: [PATCH 05/16] warp-server: Add route for serving static directory Add a static/ file tree for testing. --- static/README.md | 1 + static/dir1/optzip.txt | 2 ++ static/dir1/optzip.txt.gz | Bin 0 -> 63 bytes static/onlycompressed.txt.gz | Bin 0 -> 66 bytes static/plain.txt | 1 + warp-server/src/main.rs | 30 ++++++++++++++++++++++++++++++ 6 files changed, 34 insertions(+) create mode 100644 static/README.md create mode 100644 static/dir1/optzip.txt create mode 100644 static/dir1/optzip.txt.gz create mode 100644 static/onlycompressed.txt.gz create mode 100644 static/plain.txt diff --git a/static/README.md b/static/README.md new file mode 100644 index 0000000..41f2f35 --- /dev/null +++ b/static/README.md @@ -0,0 +1 @@ +This directory contains static files for serving through HTTP frameworks. diff --git a/static/dir1/optzip.txt b/static/dir1/optzip.txt new file mode 100644 index 0000000..cc31dba --- /dev/null +++ b/static/dir1/optzip.txt @@ -0,0 +1,2 @@ +This file is available uncompressed or compressed +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA diff --git a/static/dir1/optzip.txt.gz b/static/dir1/optzip.txt.gz new file mode 100644 index 0000000000000000000000000000000000000000..5742cd618a6813ecb5f72453f1dbf9b2c4e6eee8 GIT binary patch literal 63 zcmb2|=3oE;CT8xFCv?I*PM`4%Ww_Lt+Bws*)6>B(Gp?vE&oMBourRC3scs@u)A6Ss PN)MPA;#qVwlY!a*h@lj% literal 0 HcmV?d00001 diff --git a/static/onlycompressed.txt.gz b/static/onlycompressed.txt.gz new file mode 100644 index 0000000000000000000000000000000000000000..8542d289a839292e2d4e2dac2ab643b48814f38b GIT binary patch literal 66 zcmb2|=HTe~Gbx#YIX^F_GC4oDpeVJtI5kDDq@sl3^ZC;&Lp*f6&z$k}IprDZp%dn@ W{*=yxq9p impl Filter .and(warp::ws()) .map(|ws: warp::ws::Ws| { ws.on_upgrade(ws_rev_connected) }); + let static_dir = warp::path("dir") + .and(warp::fs::dir("../static")); + hello .or(ws_echo) .or(ws_rev) + .or(static_dir) .with(warp::log("warp-server")) } @@ -146,4 +150,30 @@ mod tests { client.send(warp::ws::Message::close()).await; client.recv_closed().await.unwrap(); } + + #[tokio::test] + async fn test_static_dir() { + let res = warp::test::request() + .path("/dir/plain.txt") + .reply(&super::api()) + .await; + assert_eq!(res.status(), 200); + assert_eq!(res.body(), "Hello world! This is uncompressed text.\n"); + + // subdir + let res = warp::test::request() + .path("/dir/dir1/optzip.txt") + .reply(&super::api()) + .await; + assert_eq!(res.status(), 200); + assert_eq!(res.body(), "This file is available uncompressed or compressed\n\ + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n"); + + // fs::dir does not support transparent decompression + let res = warp::test::request() + .path("/dir/onlycompressed.txt") + .reply(&super::api()) + .await; + assert_eq!(res.status(), 404); + } } -- 2.39.2 From 8fdc8d06c05d3cd957b17755609db90d1123df31 Mon Sep 17 00:00:00 2001 From: Martin Pitt Date: Fri, 9 Dec 2022 12:05:11 +0100 Subject: [PATCH 06/16] axum-server: Sync directory route and port with warp-server --- axum-server/src/main.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/axum-server/src/main.rs b/axum-server/src/main.rs index 02b4700..2a2eaaa 100644 --- a/axum-server/src/main.rs +++ b/axum-server/src/main.rs @@ -47,8 +47,8 @@ async fn main() { tracing_subscriber::fmt::init(); let app = Router::new() .route("/hello/:name", get(hello)) - .nest("/static", - get_service(tower_http::services::ServeDir::new(".").precompressed_gzip()) + .nest("/dir", + get_service(tower_http::services::ServeDir::new("../static").precompressed_gzip()) .handle_error(|e: io::Error| async move { (StatusCode::INTERNAL_SERVER_ERROR, format!("Unhandled internal error: {}", e)) }) @@ -62,7 +62,7 @@ async fn main() { ) ); - let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 3000)); + let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 3030)); tracing::info!("listening on {}", addr); axum::Server::bind(&addr) .serve(app.into_make_service()) -- 2.39.2 From 9f1890257c2ebae6b98314fd1eedcec8ad1b942a Mon Sep 17 00:00:00 2001 From: Martin Pitt Date: Fri, 9 Dec 2022 13:33:45 +0100 Subject: [PATCH 07/16] axum-server: Add unit test for /hello route Similar to what commit be3212fb7c5a did for warp-server. Split out the whole app router into a separate app() function, to make it accessible to unit testing. --- axum-server/Cargo.toml | 1 + axum-server/src/main.rs | 50 ++++++++++++++++++++++++++++++++++++----- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/axum-server/Cargo.toml b/axum-server/Cargo.toml index 0444b39..9033d18 100644 --- a/axum-server/Cargo.toml +++ b/axum-server/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" [dependencies] axum = { version = "0.5", features = ["ws", "headers"] } +hyper = { version = "0.14" } tokio = { version = "1", features = ["full"] } tower = "0.4" tower-http = { version = "0.3", features = ["trace", "fs"] } diff --git a/axum-server/src/main.rs b/axum-server/src/main.rs index 2a2eaaa..7bb1e94 100644 --- a/axum-server/src/main.rs +++ b/axum-server/src/main.rs @@ -42,10 +42,8 @@ async fn ws_echo(mut socket: ws::WebSocket) { } } -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - let app = Router::new() +fn app() -> Router { + Router::new() .route("/hello/:name", get(hello)) .nest("/dir", get_service(tower_http::services::ServeDir::new("../static").precompressed_gzip()) @@ -60,12 +58,52 @@ async fn main() { tower_http::trace::TraceLayer::new_for_http() .make_span_with(tower_http::trace::DefaultMakeSpan::default().include_headers(true)), ) - ); + ) +} + +#[tokio::main] +async fn main() { + tracing_subscriber::fmt::init(); let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 3030)); tracing::info!("listening on {}", addr); axum::Server::bind(&addr) - .serve(app.into_make_service()) + .serve(app().into_make_service()) .await .unwrap(); } + +#[cfg(test)] +mod tests { + use axum::{ + http::{Request, StatusCode}, + response::Response, + body::Body + }; + use tower::ServiceExt; // for `oneshot` + + async fn assert_res_ok_body(res: Response, expected_body: &[u8]) { + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(hyper::body::to_bytes(res.into_body()).await.unwrap(), expected_body); + } + + #[tokio::test] + async fn test_hello() { + // no user-agent + let res = super::app() + .oneshot(Request::builder().uri("/hello/rust").body(Body::empty()).unwrap()) + .await + .unwrap(); + assert_res_ok_body(res, b"Hello rust").await; + + // with user-agent + let res = super::app() + .oneshot(Request::builder() + .uri("/hello/rust") + .header("user-agent", "TestBrowser 0.1") + .body(Body::empty()).unwrap()) + .await + .unwrap(); + assert_res_ok_body(res, b"Hello rust from TestBrowser 0.1").await; + } +} -- 2.39.2 From 71ce3fbf93eac5646e759b5bbfe5c9766d63bad9 Mon Sep 17 00:00:00 2001 From: Martin Pitt Date: Fri, 9 Dec 2022 14:12:38 +0100 Subject: [PATCH 08/16] axum-server: Add unit tests for static directory route --- axum-server/src/main.rs | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/axum-server/src/main.rs b/axum-server/src/main.rs index 7bb1e94..fa44ac1 100644 --- a/axum-server/src/main.rs +++ b/axum-server/src/main.rs @@ -106,4 +106,40 @@ mod tests { .unwrap(); assert_res_ok_body(res, b"Hello rust from TestBrowser 0.1").await; } + + #[tokio::test] + async fn test_static_dir() { + let res = super::app() + .oneshot(Request::builder().uri("/dir/plain.txt").body(Body::empty()).unwrap()) + .await + .unwrap(); + assert_res_ok_body(res, b"Hello world! This is uncompressed text.\n").await; + + // transparent .gz lookup, without gzip transfer encoding + let res = super::app() + .oneshot(Request::builder() + .uri("/dir/dir1/optzip.txt") + .header("accept-encoding", "deflate") + .body(Body::empty()).unwrap()) + .await + .unwrap(); + assert_eq!(res.status(), StatusCode::OK); + // that returns the uncompressed file + assert_res_ok_body(res, b"This file is available uncompressed or compressed\n\ + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n").await; + + // transparent .gz lookup, with gzip transfer encoding + let res = super::app() + .oneshot(Request::builder() + .uri("/dir/dir1/optzip.txt") + .header("accept-encoding", "deflate, gzip") + .body(Body::empty()).unwrap()) + .await + .unwrap(); + assert_eq!(res.status(), StatusCode::OK); + let res_bytes: &[u8] = &hyper::body::to_bytes(res.into_body()).await.unwrap(); + // that returns the compressed file + assert_eq!(res_bytes.len(), 63); // file size of ../static/dir1/optzip.txt.gz + assert_eq!(res_bytes[0], 31); + } } -- 2.39.2 From 577248e40bb772480fb036fe2deafc1ac2a11354 Mon Sep 17 00:00:00 2001 From: Martin Pitt Date: Fri, 9 Dec 2022 16:15:21 +0100 Subject: [PATCH 09/16] actix-server: Initial version with /hello/name route See https://actix.rs/ --- actix-server/Cargo.toml | 10 +++++++ actix-server/src/main.rs | 57 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 actix-server/Cargo.toml create mode 100644 actix-server/src/main.rs diff --git a/actix-server/Cargo.toml b/actix-server/Cargo.toml new file mode 100644 index 0000000..237dc2e --- /dev/null +++ b/actix-server/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "actix-server" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +actix-web = "4" +env_logger = "0.9" diff --git a/actix-server/src/main.rs b/actix-server/src/main.rs new file mode 100644 index 0000000..946eb36 --- /dev/null +++ b/actix-server/src/main.rs @@ -0,0 +1,57 @@ +use actix_web::{get, web, App, HttpRequest, HttpServer, Result}; +use actix_web::http::header; +use actix_web::middleware::Logger; + +#[get("/hello/{name}")] +async fn hello(params: web::Path, req: HttpRequest) -> Result { + let name = params.into_inner(); + + match req.headers().get(header::USER_AGENT) { + Some(agent) => Ok(format!("Hello {} from {}!", name, agent.to_str().unwrap())), + None => Ok(format!("Hello {}!", name)) + } +} + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + env_logger::init_from_env(env_logger::Env::default().default_filter_or("info")); + + HttpServer::new(|| { + App::new() + .service(hello) + .wrap(Logger::default()) + }) + .bind(("127.0.0.1", 3030))? + .run() + .await +} + +#[cfg(test)] +mod tests { + use actix_web::{App, body, test, web}; + use actix_web::http::header; + + use super::{hello}; + + #[actix_web::test] + async fn test_hello() { + let app = test::init_service(App::new().service(hello)).await; + + // no user-agent + let req = test::TestRequest::get().uri("/hello/rust").to_request(); + let res = test::call_service(&app, req).await; + assert!(res.status().is_success()); + assert_eq!(body::to_bytes(res.into_body()).await.unwrap(), + web::Bytes::from_static(b"Hello rust!")); + + // with user-agent + let req = test::TestRequest::get() + .uri("/hello/rust") + .insert_header((header::USER_AGENT, "TestBrowser 0.1")) + .to_request(); + let res = test::call_service(&app, req).await; + assert!(res.status().is_success()); + assert_eq!(body::to_bytes(res.into_body()).await.unwrap(), + web::Bytes::from_static(b"Hello rust from TestBrowser 0.1!")); + } +} -- 2.39.2 From 9bcb2f272943c88dd3cafe1f5679fcd3a20a89a9 Mon Sep 17 00:00:00 2001 From: Martin Pitt Date: Fri, 9 Dec 2022 21:58:53 +0100 Subject: [PATCH 10/16] actix-server: Add static directory route --- actix-server/Cargo.toml | 1 + actix-server/src/main.rs | 29 ++++++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/actix-server/Cargo.toml b/actix-server/Cargo.toml index 237dc2e..fa315f9 100644 --- a/actix-server/Cargo.toml +++ b/actix-server/Cargo.toml @@ -7,4 +7,5 @@ edition = "2021" [dependencies] actix-web = "4" +actix-files = "0.6" env_logger = "0.9" diff --git a/actix-server/src/main.rs b/actix-server/src/main.rs index 946eb36..ae2b223 100644 --- a/actix-server/src/main.rs +++ b/actix-server/src/main.rs @@ -19,6 +19,7 @@ async fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new() .service(hello) + .service(actix_files::Files::new("/dir", "../static")) .wrap(Logger::default()) }) .bind(("127.0.0.1", 3030))? @@ -29,12 +30,13 @@ async fn main() -> std::io::Result<()> { #[cfg(test)] mod tests { use actix_web::{App, body, test, web}; - use actix_web::http::header; + use actix_web::http::{header, StatusCode}; use super::{hello}; #[actix_web::test] async fn test_hello() { + // FIXME: duplicating the .service() call from main() here is super ugly, but it's hard to move that into a fn let app = test::init_service(App::new().service(hello)).await; // no user-agent @@ -54,4 +56,29 @@ mod tests { assert_eq!(body::to_bytes(res.into_body()).await.unwrap(), web::Bytes::from_static(b"Hello rust from TestBrowser 0.1!")); } + + #[actix_web::test] + async fn test_static_dir() { + // FIXME: duplicating the .service() call from main() here is super ugly, but it's hard to move that into a fn + let app = test::init_service(App::new().service(actix_files::Files::new("/dir", "../static"))).await; + + let req = test::TestRequest::get().uri("/dir/plain.txt").to_request(); + let res = test::call_service(&app, req).await; + assert!(res.status().is_success()); + assert_eq!(body::to_bytes(res.into_body()).await.unwrap(), + web::Bytes::from_static(b"Hello world! This is uncompressed text.\n")); + + // subdir + let req = test::TestRequest::get().uri("/dir/dir1/optzip.txt").to_request(); + let res = test::call_service(&app, req).await; + assert!(res.status().is_success()); + assert_eq!(body::to_bytes(res.into_body()).await.unwrap(), + web::Bytes::from_static(b"This file is available uncompressed or compressed\n\ + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n")); + + // does not support transparent decompression + let req = test::TestRequest::get().uri("/dir/onlycompressed.txt").to_request(); + let res = test::call_service(&app, req).await; + assert_eq!(res.status(), StatusCode::NOT_FOUND); + } } -- 2.39.2 From f5c4a6a87fc589dec2f23bee0196beb07d427ae7 Mon Sep 17 00:00:00 2001 From: Martin Pitt Date: Sat, 10 Dec 2022 11:59:12 +0100 Subject: [PATCH 11/16] actix-server: Add static file route with gz support This shows how to serve dynamically computed file paths/types, with gzip transport encoding support. --- actix-server/Cargo.toml | 1 + actix-server/src/main.rs | 57 +++++++++++++++++++++++++++++++++++++--- 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/actix-server/Cargo.toml b/actix-server/Cargo.toml index fa315f9..803f53e 100644 --- a/actix-server/Cargo.toml +++ b/actix-server/Cargo.toml @@ -9,3 +9,4 @@ edition = "2021" actix-web = "4" actix-files = "0.6" env_logger = "0.9" +log = "0.4" diff --git a/actix-server/src/main.rs b/actix-server/src/main.rs index ae2b223..c73e7ad 100644 --- a/actix-server/src/main.rs +++ b/actix-server/src/main.rs @@ -1,6 +1,9 @@ -use actix_web::{get, web, App, HttpRequest, HttpServer, Result}; +use std::path::Path; + +use actix_web::{get, web, App, HttpRequest, HttpServer, Responder, Result}; use actix_web::http::header; use actix_web::middleware::Logger; +use actix_files::{Files, NamedFile}; #[get("/hello/{name}")] async fn hello(params: web::Path, req: HttpRequest) -> Result { @@ -12,6 +15,28 @@ async fn hello(params: web::Path, req: HttpRequest) -> Result { } } +#[get("/file/{path:.*}")] +async fn static_file(params: web::Path, req: HttpRequest) -> Result { + let request_path = params.into_inner(); + let disk_path = "../static/".to_string() + &request_path; + + // if the client accepts gzip encoding, try that first + if let Some(accept_encoding) = req.headers().get(header::ACCEPT_ENCODING) { + if accept_encoding.to_str().unwrap().contains("gzip") { + let path_gz = disk_path.clone() + ".gz"; + if Path::new(&path_gz).is_file() { + log::debug!("client accepts gzip encoding, sending pre-compressed file {}", &path_gz); + return Ok(NamedFile::open_async(path_gz).await? + .customize() + .insert_header(header::ContentEncoding::Gzip)); + } + } + } + + // uncompressed file + Ok(NamedFile::open_async(disk_path).await?.customize()) +} + #[actix_web::main] async fn main() -> std::io::Result<()> { env_logger::init_from_env(env_logger::Env::default().default_filter_or("info")); @@ -19,7 +44,8 @@ async fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new() .service(hello) - .service(actix_files::Files::new("/dir", "../static")) + .service(static_file) + .service(Files::new("/dir", "../static")) .wrap(Logger::default()) }) .bind(("127.0.0.1", 3030))? @@ -32,7 +58,7 @@ mod tests { use actix_web::{App, body, test, web}; use actix_web::http::{header, StatusCode}; - use super::{hello}; + use super::{hello, static_file}; #[actix_web::test] async fn test_hello() { @@ -81,4 +107,29 @@ mod tests { let res = test::call_service(&app, req).await; assert_eq!(res.status(), StatusCode::NOT_FOUND); } + + #[actix_web::test] + async fn test_static_file() { + // FIXME: duplicating the .service() call from main() here is super ugly, but it's hard to move that into a fn + let app = test::init_service(App::new().service(static_file)).await; + + // uncompressed + let req = test::TestRequest::get().uri("/file/dir1/optzip.txt").to_request(); + let res = test::call_service(&app, req).await; + assert!(res.status().is_success()); + assert_eq!(body::to_bytes(res.into_body()).await.unwrap(), + web::Bytes::from_static(b"This file is available uncompressed or compressed\n\ + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n")); + + // gzipped + let req = test::TestRequest::get() + .uri("/file/dir1/optzip.txt") + .insert_header((header::ACCEPT_ENCODING, "deflate, gzip")) + .to_request(); + let res = test::call_service(&app, req).await; + assert!(res.status().is_success()); + let res_bytes = body::to_bytes(res.into_body()).await.unwrap(); + assert_eq!(res_bytes.len(), 63); // file size of ../static/dir1/optzip.txt.gz + assert_eq!(res_bytes[0], 31); + } } -- 2.39.2 From 7580ac5646156d35fd83b65f7d0bd959a896fb6c Mon Sep 17 00:00:00 2001 From: Martin Pitt Date: Sat, 10 Dec 2022 12:34:07 +0100 Subject: [PATCH 12/16] actix-server: Make hello route work with HEAD This is a little bit tricky: https://github.com/actix/actix-web/issues/2702 --- actix-server/src/main.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/actix-server/src/main.rs b/actix-server/src/main.rs index c73e7ad..4a43ef5 100644 --- a/actix-server/src/main.rs +++ b/actix-server/src/main.rs @@ -1,11 +1,11 @@ use std::path::Path; -use actix_web::{get, web, App, HttpRequest, HttpServer, Responder, Result}; +use actix_web::{get, route, web, App, HttpRequest, HttpServer, Responder, Result}; use actix_web::http::header; use actix_web::middleware::Logger; use actix_files::{Files, NamedFile}; -#[get("/hello/{name}")] +#[route("/hello/{name}", method="GET", method="HEAD")] async fn hello(params: web::Path, req: HttpRequest) -> Result { let name = params.into_inner(); -- 2.39.2 From 883c4d01f6138070672c08118e2be9facdcd2e15 Mon Sep 17 00:00:00 2001 From: Martin Pitt Date: Sat, 10 Dec 2022 12:50:51 +0100 Subject: [PATCH 13/16] actix-server: Add echo websocket route --- actix-server/Cargo.toml | 6 +++++ actix-server/src/main.rs | 55 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/actix-server/Cargo.toml b/actix-server/Cargo.toml index 803f53e..4208b99 100644 --- a/actix-server/Cargo.toml +++ b/actix-server/Cargo.toml @@ -6,7 +6,13 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +actix = "0.13" actix-web = "4" +actix-web-actors = "4.1" actix-files = "0.6" env_logger = "0.9" log = "0.4" + +[dev-dependencies] +actix-test = "0.1" +futures-util = "0.3" diff --git a/actix-server/src/main.rs b/actix-server/src/main.rs index 4a43ef5..58756de 100644 --- a/actix-server/src/main.rs +++ b/actix-server/src/main.rs @@ -1,9 +1,11 @@ use std::path::Path; -use actix_web::{get, route, web, App, HttpRequest, HttpServer, Responder, Result}; +use actix::{Actor, ActorContext, StreamHandler}; +use actix_web::{get, route, web, App, Error, HttpRequest, HttpResponse, HttpServer, Responder, Result}; use actix_web::http::header; use actix_web::middleware::Logger; use actix_files::{Files, NamedFile}; +use actix_web_actors::ws; #[route("/hello/{name}", method="GET", method="HEAD")] async fn hello(params: web::Path, req: HttpRequest) -> Result { @@ -37,6 +39,33 @@ async fn static_file(params: web::Path, req: HttpRequest) -> Result; +} + +impl StreamHandler> for WsEcho { + fn handle(&mut self, msg: Result, ctx: &mut Self::Context) { + log::info!("WsEcho got message {:?}", msg); + match msg { + Ok(ws::Message::Ping(msg)) => ctx.pong(&msg), + Ok(ws::Message::Text(text)) => ctx.text(text), + Ok(ws::Message::Binary(bin)) => ctx.binary(bin), + Ok(ws::Message::Close(reason)) => { + ctx.close(reason); + ctx.stop(); + }, + _ => ctx.stop(), + } + } +} + +#[get("/ws-echo")] +async fn ws_echo(req: HttpRequest, stream: web::Payload) -> Result { + ws::start(WsEcho {}, &req, stream) +} + #[actix_web::main] async fn main() -> std::io::Result<()> { env_logger::init_from_env(env_logger::Env::default().default_filter_or("info")); @@ -46,6 +75,7 @@ async fn main() -> std::io::Result<()> { .service(hello) .service(static_file) .service(Files::new("/dir", "../static")) + .service(ws_echo) .wrap(Logger::default()) }) .bind(("127.0.0.1", 3030))? @@ -57,8 +87,12 @@ async fn main() -> std::io::Result<()> { mod tests { use actix_web::{App, body, test, web}; use actix_web::http::{header, StatusCode}; + use actix_web_actors::ws; - use super::{hello, static_file}; + use futures_util::sink::SinkExt; + use futures_util::StreamExt; + + use super::{hello, static_file, ws_echo}; #[actix_web::test] async fn test_hello() { @@ -132,4 +166,21 @@ mod tests { assert_eq!(res_bytes.len(), 63); // file size of ../static/dir1/optzip.txt.gz assert_eq!(res_bytes[0], 31); } + + #[actix_web::test] + async fn test_ws_echo() { + // FIXME: duplicating the .service() call from main() here is super ugly, but it's hard to move that into a fn + let mut srv = actix_test::start(|| App::new().service(ws_echo)); + let mut client = srv.ws_at("/ws-echo").await.unwrap(); + + // text echo + client.send(ws::Message::Text("hello".into())).await.unwrap(); + let received = client.next().await.unwrap().unwrap(); + assert_eq!(received, ws::Frame::Text("hello".into())); + + // binary echo + client.send(ws::Message::Binary(web::Bytes::from_static(&[42, 99]))).await.unwrap(); + let received = client.next().await.unwrap().unwrap(); + assert_eq!(received, ws::Frame::Binary(web::Bytes::from_static(&[42, 99]))); + } } -- 2.39.2 From d1e80e86e989dc0a425429dbac3b5252bfc232c4 Mon Sep 17 00:00:00 2001 From: Martin Pitt Date: Mon, 12 Dec 2022 08:35:58 +0100 Subject: [PATCH 14/16] actix-server: Add "reverse input" websocket route --- actix-server/src/main.rs | 54 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/actix-server/src/main.rs b/actix-server/src/main.rs index 58756de..af63efd 100644 --- a/actix-server/src/main.rs +++ b/actix-server/src/main.rs @@ -66,6 +66,40 @@ async fn ws_echo(req: HttpRequest, stream: web::Payload) -> Result; +} + +impl StreamHandler> for WsRev { + fn handle(&mut self, msg: Result, ctx: &mut Self::Context) { + log::info!("WsRev got message {:?}", msg); + match msg { + Ok(ws::Message::Ping(msg)) => ctx.pong(&msg), + Ok(ws::Message::Text(text)) => { + let rev = text.chars().rev().collect::(); + ctx.text(rev); + }, + Ok(ws::Message::Binary(bin)) => { + let mut rev = bin.to_vec(); + rev.reverse(); + ctx.binary(rev); + } + Ok(ws::Message::Close(reason)) => { + ctx.close(reason); + ctx.stop(); + }, + _ => ctx.stop(), + } + } +} + +#[get("/ws-rev")] +async fn ws_rev(req: HttpRequest, stream: web::Payload) -> Result { + ws::start(WsRev {}, &req, stream) +} + #[actix_web::main] async fn main() -> std::io::Result<()> { env_logger::init_from_env(env_logger::Env::default().default_filter_or("info")); @@ -76,6 +110,7 @@ async fn main() -> std::io::Result<()> { .service(static_file) .service(Files::new("/dir", "../static")) .service(ws_echo) + .service(ws_rev) .wrap(Logger::default()) }) .bind(("127.0.0.1", 3030))? @@ -92,7 +127,7 @@ mod tests { use futures_util::sink::SinkExt; use futures_util::StreamExt; - use super::{hello, static_file, ws_echo}; + use super::{hello, static_file, ws_echo, ws_rev}; #[actix_web::test] async fn test_hello() { @@ -183,4 +218,21 @@ mod tests { let received = client.next().await.unwrap().unwrap(); assert_eq!(received, ws::Frame::Binary(web::Bytes::from_static(&[42, 99]))); } + + #[actix_web::test] + async fn test_ws_rev() { + // FIXME: duplicating the .service() call from main() here is super ugly, but it's hard to move that into a fn + let mut srv = actix_test::start(|| App::new().service(ws_rev)); + let mut client = srv.ws_at("/ws-rev").await.unwrap(); + + // text reversed + client.send(ws::Message::Text("hello".into())).await.unwrap(); + let received = client.next().await.unwrap().unwrap(); + assert_eq!(received, ws::Frame::Text("olleh".into())); + + // binary reversed + client.send(ws::Message::Binary(web::Bytes::from_static(&[42, 99]))).await.unwrap(); + let received = client.next().await.unwrap().unwrap(); + assert_eq!(received, ws::Frame::Binary(web::Bytes::from_static(&[99, 42]))); + } } -- 2.39.2 From 121e3e9e83ec2bba3f0ebd22bdb1128c83fd5321 Mon Sep 17 00:00:00 2001 From: Martin Pitt Date: Tue, 13 Dec 2022 14:29:05 +0100 Subject: [PATCH 15/16] actix-server: Factorize App creation This is too hard as a proper function, as App is too template-y. Use a macro instead. Thanks to Aravinth Manivannan for the suggestion! --- actix-server/src/main.rs | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/actix-server/src/main.rs b/actix-server/src/main.rs index af63efd..44ed45c 100644 --- a/actix-server/src/main.rs +++ b/actix-server/src/main.rs @@ -100,17 +100,24 @@ async fn ws_rev(req: HttpRequest, stream: web::Payload) -> Result std::io::Result<()> { - env_logger::init_from_env(env_logger::Env::default().default_filter_or("info")); - - HttpServer::new(|| { +// App is a template soup, too hard as a proper function +macro_rules! get_app { + () => { App::new() .service(hello) .service(static_file) .service(Files::new("/dir", "../static")) .service(ws_echo) .service(ws_rev) + } +} + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + env_logger::init_from_env(env_logger::Env::default().default_filter_or("info")); + + HttpServer::new(|| { + get_app!() .wrap(Logger::default()) }) .bind(("127.0.0.1", 3030))? @@ -123,6 +130,7 @@ mod tests { use actix_web::{App, body, test, web}; use actix_web::http::{header, StatusCode}; use actix_web_actors::ws; + use actix_files::Files; use futures_util::sink::SinkExt; use futures_util::StreamExt; @@ -131,8 +139,7 @@ mod tests { #[actix_web::test] async fn test_hello() { - // FIXME: duplicating the .service() call from main() here is super ugly, but it's hard to move that into a fn - let app = test::init_service(App::new().service(hello)).await; + let app = test::init_service(get_app!()).await; // no user-agent let req = test::TestRequest::get().uri("/hello/rust").to_request(); @@ -154,8 +161,7 @@ mod tests { #[actix_web::test] async fn test_static_dir() { - // FIXME: duplicating the .service() call from main() here is super ugly, but it's hard to move that into a fn - let app = test::init_service(App::new().service(actix_files::Files::new("/dir", "../static"))).await; + let app = test::init_service(get_app!()).await; let req = test::TestRequest::get().uri("/dir/plain.txt").to_request(); let res = test::call_service(&app, req).await; @@ -179,8 +185,7 @@ mod tests { #[actix_web::test] async fn test_static_file() { - // FIXME: duplicating the .service() call from main() here is super ugly, but it's hard to move that into a fn - let app = test::init_service(App::new().service(static_file)).await; + let app = test::init_service(get_app!()).await; // uncompressed let req = test::TestRequest::get().uri("/file/dir1/optzip.txt").to_request(); @@ -204,8 +209,7 @@ mod tests { #[actix_web::test] async fn test_ws_echo() { - // FIXME: duplicating the .service() call from main() here is super ugly, but it's hard to move that into a fn - let mut srv = actix_test::start(|| App::new().service(ws_echo)); + let mut srv = actix_test::start(|| get_app!()); let mut client = srv.ws_at("/ws-echo").await.unwrap(); // text echo @@ -221,8 +225,7 @@ mod tests { #[actix_web::test] async fn test_ws_rev() { - // FIXME: duplicating the .service() call from main() here is super ugly, but it's hard to move that into a fn - let mut srv = actix_test::start(|| App::new().service(ws_rev)); + let mut srv = actix_test::start(|| get_app!()); let mut client = srv.ws_at("/ws-rev").await.unwrap(); // text reversed -- 2.39.2 From 1354104c119bdcf6e8050c1a26a53bc77dbb85a6 Mon Sep 17 00:00:00 2001 From: Martin Pitt Date: Fri, 6 Jan 2023 15:47:19 +0100 Subject: [PATCH 16/16] concepts: rustfmt --- concepts/src/lib.rs | 36 ++++++++++++++-------- concepts/src/main.rs | 71 +++++++++++++++++++++++++++++++------------- 2 files changed, 75 insertions(+), 32 deletions(-) diff --git a/concepts/src/lib.rs b/concepts/src/lib.rs index 5f1feea..f2b1c10 100644 --- a/concepts/src/lib.rs +++ b/concepts/src/lib.rs @@ -1,11 +1,10 @@ +use std::collections::HashMap; use std::fs::File; use std::io::prelude::*; -use std::collections::HashMap; pub fn read_file(path: &str) -> Result { let mut s = String::new(); - File::open(path)? - .read_to_string(&mut s)?; + File::open(path)?.read_to_string(&mut s)?; Ok(s) } @@ -71,7 +70,10 @@ where V: Copy, { pub fn new(calc: T) -> Cacher { - Cacher { calc, values: HashMap::new() } + Cacher { + calc, + values: HashMap::new(), + } } pub fn value(&mut self, arg: A) -> V { @@ -87,7 +89,7 @@ where } pub struct Counter5 { - count: u32 + count: u32, } impl Counter5 { @@ -157,7 +159,7 @@ trait State { struct Draft {} impl State for Draft { fn request_review(&self) -> Box { - Box::new(PendingReview {acks: 0}) + Box::new(PendingReview { acks: 0 }) } fn approve(&mut self) -> Box { @@ -176,14 +178,16 @@ struct PendingReview { impl State for PendingReview { fn request_review(&self) -> Box { - Box::new(Self {acks: self.acks}) + Box::new(Self { acks: self.acks }) } fn approve(&mut self) -> Box { if self.acks >= 1 { Box::new(Published {}) } else { - Box::new(Self {acks: self.acks + 1}) + Box::new(Self { + acks: self.acks + 1, + }) } } @@ -218,7 +222,9 @@ pub struct TPost { impl TPost { pub fn new() -> TPostDraft { - TPostDraft {content: String::new()} + TPostDraft { + content: String::new(), + } } pub fn content(&self) -> &str { @@ -236,7 +242,9 @@ impl TPostDraft { } pub fn request_review(self) -> TPostReview { - TPostReview {content: self.content} + TPostReview { + content: self.content, + } } } @@ -246,10 +254,14 @@ pub struct TPostReview { impl TPostReview { pub fn approve(self) -> TPost { - TPost {content: self.content} + TPost { + content: self.content, + } } pub fn reject(self) -> TPostDraft { - TPostDraft {content: self.content} + TPostDraft { + content: self.content, + } } } diff --git a/concepts/src/main.rs b/concepts/src/main.rs index 4b6df64..be26bdb 100644 --- a/concepts/src/main.rs +++ b/concepts/src/main.rs @@ -1,10 +1,10 @@ -mod word_utils; mod lib; +mod word_utils; use std::collections::HashMap; -use std::io::{prelude::*, ErrorKind}; use std::fs::{self, File}; -use std::{thread, time, sync}; +use std::io::{prelude::*, ErrorKind}; +use std::{sync, thread, time}; use lib::*; use word_utils::{first_word, second_word}; @@ -15,7 +15,10 @@ fn test_strings() { println!("second word: '{}'", second_word(&s).unwrap()); let s2 = "hello dude blah"; - println!("second word of single: '{}'", second_word(s2).unwrap_or("(none)")); + println!( + "second word of single: '{}'", + second_word(s2).unwrap_or("(none)") + ); match second_word(s2) { Some(w) => println!("match: second word of '{}' exists: {}", s2, w), @@ -66,18 +69,25 @@ fn test_hashmaps() { println!("scores after doubling: {:?}", scores); // double scores of immutable hashmap (rebuild it) - let collect_scores: HashMap<_, _> = collect_scores.into_iter() + let collect_scores: HashMap<_, _> = collect_scores + .into_iter() .map(|(k, v)| (k, 2 * v)) .collect(); - println!("collect_scores after rebuilding with doubling: {:?}", collect_scores); + println!( + "collect_scores after rebuilding with doubling: {:?}", + collect_scores + ); } fn test_files() { if let Ok(mut f) = File::open("Cargo.toml") { let mut contents = String::new(); match f.read_to_string(&mut contents) { - Ok(len) => println!("successfully opened Cargo.toml: {:?}, contents {} bytes:\n{}\n----------", f, len, contents), - Err(e) => panic!("could not read file: {:?}", e) + Ok(len) => println!( + "successfully opened Cargo.toml: {:?}, contents {} bytes:\n{}\n----------", + f, len, contents + ), + Err(e) => panic!("could not read file: {:?}", e), } } else { println!("could not open Cargo.toml"); @@ -102,13 +112,13 @@ fn test_files() { // using the '?' operator match read_file("Cargo.toml") { Ok(s) => println!("Cargo.toml contents:\n{}\n-------------", s), - Err(e) => println!("Could not open Cargo.toml: {:?}", e) + Err(e) => println!("Could not open Cargo.toml: {:?}", e), } // using std API match fs::read_to_string("Cargo.toml") { Ok(s) => println!("Cargo.toml contents:\n{}\n-------------", s), - Err(e) => println!("Could not open Cargo.toml: {:?}", e) + Err(e) => println!("Could not open Cargo.toml: {:?}", e), } } @@ -125,7 +135,10 @@ fn test_generics() { println!("str_list: {:?}", str_list); let string_list = vec!["aaaa".to_string(), "xxxxx".to_string(), "ffff".to_string()]; - println!("largest string (with cloning): {}", largest_clone(&string_list)); + println!( + "largest string (with cloning): {}", + largest_clone(&string_list) + ); println!("largest string (with ref): {}", largest_ref(&string_list)); println!("string_list: {:?}", string_list); @@ -144,18 +157,36 @@ fn test_closures() { 2 * x }); - println!("1st int call for value 1: {}", expensive_int_result.value(1)); - println!("2nd int call for value 1: {}", expensive_int_result.value(1)); - println!("1st int call for value 2: {}", expensive_int_result.value(2)); + println!( + "1st int call for value 1: {}", + expensive_int_result.value(1) + ); + println!( + "2nd int call for value 1: {}", + expensive_int_result.value(1) + ); + println!( + "1st int call for value 2: {}", + expensive_int_result.value(2) + ); let mut expensive_str_result = Cacher::new(|x: &str| { println!("calculating expensive str result for {}", x); x.len() }); - println!("1st int call for value abc: {}", expensive_str_result.value("abc")); - println!("2nd int call for value abc: {}", expensive_str_result.value("abc")); - println!("1st int call for value defg: {}", expensive_str_result.value("defg")); + println!( + "1st int call for value abc: {}", + expensive_str_result.value("abc") + ); + println!( + "2nd int call for value abc: {}", + expensive_str_result.value("abc") + ); + println!( + "1st int call for value defg: {}", + expensive_str_result.value("defg") + ); } fn test_iterators() { @@ -262,13 +293,13 @@ fn test_dyn_traits() { post.request_review(); assert_eq!("", post.content()); - post.approve(); // first + post.approve(); // first assert_eq!("", post.content()); - post.approve(); // second + post.approve(); // second assert_eq!(text, post.content()); - post.reject(); // no-op + post.reject(); // no-op assert_eq!(text, post.content()); } -- 2.39.2