]> piware.de Git - learn-rust.git/commitdiff
concepts: rustfmt master
authorMartin Pitt <martin@piware.de>
Fri, 6 Jan 2023 14:47:19 +0000 (15:47 +0100)
committerMartin Pitt <martin@piware.de>
Fri, 6 Jan 2023 14:49:16 +0000 (15:49 +0100)
30 files changed:
actix-server/Cargo.toml [new file with mode: 0644]
actix-server/src/main.rs [new file with mode: 0644]
async-http/404.html [new file with mode: 0644]
async-http/Cargo.toml [new file with mode: 0644]
async-http/index.html [new file with mode: 0644]
async-http/src/main.rs [new file with mode: 0644]
axum-server/Cargo.toml [new file with mode: 0644]
axum-server/src/main.rs [new file with mode: 0644]
concepts/src/lib.rs
concepts/src/main.rs
gtk3-hello-world/Cargo.toml [new file with mode: 0644]
gtk3-hello-world/src/main.rs [new file with mode: 0644]
gtk4-hello-world/Cargo.toml [new file with mode: 0644]
gtk4-hello-world/src/main.rs [new file with mode: 0644]
hyper-server/Cargo.toml [new file with mode: 0644]
hyper-server/src/main.rs [new file with mode: 0644]
serde/src/main.rs
static/README.md [new file with mode: 0644]
static/dir1/optzip.txt [new file with mode: 0644]
static/dir1/optzip.txt.gz [new file with mode: 0644]
static/onlycompressed.txt.gz [new file with mode: 0644]
static/plain.txt [new file with mode: 0644]
tokio-tutorial-jbarszczewski/Cargo.toml [new file with mode: 0644]
tokio-tutorial-jbarszczewski/src/main.rs [new file with mode: 0644]
tokio-tutorial-mini-redis/Cargo.toml [new file with mode: 0644]
tokio-tutorial-mini-redis/examples/hello-redis.rs [new file with mode: 0644]
tokio-tutorial-mini-redis/src/bin/client.rs [new file with mode: 0644]
tokio-tutorial-mini-redis/src/bin/server.rs [new file with mode: 0644]
warp-server/Cargo.toml [new file with mode: 0644]
warp-server/src/main.rs [new file with mode: 0644]

diff --git a/actix-server/Cargo.toml b/actix-server/Cargo.toml
new file mode 100644 (file)
index 0000000..4208b99
--- /dev/null
@@ -0,0 +1,18 @@
+[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 = "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
new file mode 100644 (file)
index 0000000..44ed45c
--- /dev/null
@@ -0,0 +1,241 @@
+use std::path::Path;
+
+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<String>, req: HttpRequest) -> Result<String> {
+    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))
+    }
+}
+
+#[get("/file/{path:.*}")]
+async fn static_file(params: web::Path<String>, req: HttpRequest) -> Result<impl Responder> {
+    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())
+}
+
+struct WsEcho;
+
+impl Actor for WsEcho {
+    type Context = ws::WebsocketContext<Self>;
+}
+
+impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for WsEcho {
+    fn handle(&mut self, msg: Result<ws::Message, ws::ProtocolError>, 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<HttpResponse, Error> {
+    ws::start(WsEcho {}, &req, stream)
+}
+
+struct WsRev;
+
+impl Actor for WsRev {
+    type Context = ws::WebsocketContext<Self>;
+}
+
+impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for WsRev {
+    fn handle(&mut self, msg: Result<ws::Message, ws::ProtocolError>, 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::<String>();
+                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<HttpResponse, Error> {
+    ws::start(WsRev {}, &req, stream)
+}
+
+// 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))?
+        .run()
+        .await
+}
+
+#[cfg(test)]
+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;
+
+    use super::{hello, static_file, ws_echo, ws_rev};
+
+    #[actix_web::test]
+    async fn test_hello() {
+        let app = test::init_service(get_app!()).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!"));
+    }
+
+    #[actix_web::test]
+    async fn test_static_dir() {
+        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;
+        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);
+    }
+
+    #[actix_web::test]
+    async fn test_static_file() {
+        let app = test::init_service(get_app!()).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);
+    }
+
+    #[actix_web::test]
+    async fn test_ws_echo() {
+        let mut srv = actix_test::start(|| get_app!());
+        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])));
+    }
+
+    #[actix_web::test]
+    async fn test_ws_rev() {
+        let mut srv = actix_test::start(|| get_app!());
+        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])));
+    }
+}
diff --git a/async-http/404.html b/async-http/404.html
new file mode 100644 (file)
index 0000000..d59a923
--- /dev/null
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <title>Hello!</title>
+  </head>
+  <body>
+    <h1>Oops!</h1>
+    <p>I don't know about this resource.</p>
+  </body>
+</html>
diff --git a/async-http/Cargo.toml b/async-http/Cargo.toml
new file mode 100644 (file)
index 0000000..30b4930
--- /dev/null
@@ -0,0 +1,15 @@
+[package]
+name = "async-http"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+futures = "0.3"
+log = "0.4"
+env_logger = "0.9"
+
+[dependencies.async-std]
+version = "1.6"
+features = ["attributes"]
diff --git a/async-http/index.html b/async-http/index.html
new file mode 100644 (file)
index 0000000..fe442d6
--- /dev/null
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <title>Hello!</title>
+  </head>
+  <body>
+    <h1>Hello!</h1>
+    <p>Hi from Rust</p>
+  </body>
+</html>
diff --git a/async-http/src/main.rs b/async-http/src/main.rs
new file mode 100644 (file)
index 0000000..1a8dddb
--- /dev/null
@@ -0,0 +1,115 @@
+use std::fs;
+use std::time::Duration;
+
+use async_std::prelude::*;
+use async_std::io::{ Read, Write };
+use async_std::net::{ TcpListener };
+use async_std::task;
+use futures::stream::StreamExt;
+
+#[async_std::main]
+async fn main() {
+    env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
+
+    // Listen for incoming TCP connections on localhost port 7878
+    let listener = TcpListener::bind("127.0.0.1:7878").await.unwrap();
+
+    listener.incoming().for_each_concurrent(/* limit */ None, |tcpstream| async move {
+        let tcpstream = tcpstream.unwrap();
+        task::spawn(handle_connection(tcpstream));
+    }).await;
+}
+
+async fn handle_connection(mut stream: impl Read + Write + Unpin) {
+    // Read the first 1024 bytes of data from the stream
+    let mut buffer = [0; 1024];
+    assert!(stream.read(&mut buffer).await.unwrap() > 0);
+
+    // Respond with greetings or a 404,
+    // depending on the data in the request
+    let (status_line, filename) = if buffer.starts_with(b"GET / HTTP/1.1\r\n") {
+        ("HTTP/1.1 200 OK", "index.html")
+    } else if buffer.starts_with(b"GET /sleep HTTP/1.1\r\n") {
+        task::sleep(Duration::from_secs(5)).await;
+        // sync version, to demonstrate concurrent async vs. parallel threads
+        // std::thread::sleep(Duration::from_secs(5));
+        ("HTTP/1.1 201 Sleep", "index.html")
+    } else {
+        ("HTTP/1.1 404 NOT FOUND", "404.html")
+    };
+    let contents = fs::read_to_string(filename).unwrap();
+    log::info!("GET {} {}", filename, status_line);
+
+    // Write response back to the stream,
+    // and flush the stream to ensure the response is sent back to the client
+    let response = format!("{status_line}\r\n\r\n{contents}");
+    stream.write_all(response.as_bytes()).await.unwrap();
+    stream.flush().await.unwrap();
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    use std::cmp;
+    use std::pin::Pin;
+
+    use futures::io::Error;
+    use futures::task::{Context, Poll};
+
+    struct MockTcpStream {
+        read_data: Vec<u8>,
+        write_data: Vec<u8>,
+    }
+
+    impl Read for MockTcpStream {
+        fn poll_read(
+            self: Pin<&mut Self>,
+            _: &mut Context,
+            buf: &mut [u8],
+        ) -> Poll<Result<usize, Error>> {
+            let size: usize = cmp::min(self.read_data.len(), buf.len());
+            buf[..size].copy_from_slice(&self.read_data[..size]);
+            Poll::Ready(Ok(size))
+        }
+    }
+
+    impl Write for MockTcpStream {
+        fn poll_write(
+            mut self: Pin<&mut Self>,
+            _: &mut Context,
+            buf: &[u8],
+        ) -> Poll<Result<usize, Error>> {
+            self.write_data = Vec::from(buf);
+
+            Poll::Ready(Ok(buf.len()))
+        }
+
+        fn poll_flush(self: Pin<&mut Self>, _: &mut Context) -> Poll<Result<(), Error>> {
+            Poll::Ready(Ok(()))
+        }
+
+        fn poll_close(self: Pin<&mut Self>, _: &mut Context) -> Poll<Result<(), Error>> {
+            Poll::Ready(Ok(()))
+        }
+    }
+
+    impl Unpin for MockTcpStream {}
+
+    #[async_std::test]
+    async fn test_handle_connection() {
+        let input_bytes = b"GET / HTTP/1.1\r\n";
+        let mut contents = vec![0u8; 1024];
+        contents[..input_bytes.len()].clone_from_slice(input_bytes);
+        let mut stream = MockTcpStream {
+            read_data: contents,
+            write_data: Vec::new(),
+        };
+
+        handle_connection(&mut stream).await;
+
+        let expected_contents = fs::read_to_string("index.html").unwrap();
+        let expected_response = format!("HTTP/1.1 200 OK\r\n\r\n{}", expected_contents);
+        assert!(stream.write_data.starts_with(expected_response.as_bytes()));
+    }
+}
diff --git a/axum-server/Cargo.toml b/axum-server/Cargo.toml
new file mode 100644 (file)
index 0000000..9033d18
--- /dev/null
@@ -0,0 +1,15 @@
+[package]
+name = "axum-server"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[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"] }
+tracing = "0.1"
+tracing-subscriber = "0.3"
diff --git a/axum-server/src/main.rs b/axum-server/src/main.rs
new file mode 100644 (file)
index 0000000..fa44ac1
--- /dev/null
@@ -0,0 +1,145 @@
+use std::io;
+
+use axum::{
+    routing::{get, get_service},
+    extract::{Path, TypedHeader, ws},
+    http::{StatusCode},
+    response,
+    Router};
+
+async fn hello(Path(name): Path<String>, user_agent: Option<TypedHeader<axum::headers::UserAgent>>) -> impl response::IntoResponse {
+    if let Some(TypedHeader(user_agent)) = user_agent {
+        (StatusCode::OK, format!("Hello {} from {}", name, user_agent))
+    } else {
+        (StatusCode::OK, format!("Hello {}", name))
+    }
+}
+
+async fn ws_echo(mut socket: ws::WebSocket) {
+    while let Some(msg) = socket.recv().await {
+        if let Ok(msg) = msg {
+            tracing::debug!("websocket got message: {:?}", msg);
+
+            let reply = match msg  {
+                ws::Message::Text(t) => ws::Message::Text(t),
+                ws::Message::Binary(b) => ws::Message::Binary(b),
+                // axum handles Ping/Pong by itself
+                ws::Message::Ping(_) => { continue },
+                ws::Message::Pong(_) => { continue },
+                ws::Message::Close(_) => { break }
+            };
+
+            if socket.send(reply).await
+                .is_err() {
+                    tracing::info!("websocket client disconnected");
+                    break;
+                }
+        }
+        else {
+            tracing::info!("websocket client disconnected");
+            break;
+        }
+    }
+}
+
+fn app() -> Router {
+    Router::new()
+        .route("/hello/:name", get(hello))
+        .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))
+                   })
+        )
+        .route("/ws-echo", get(|ws: ws::WebSocketUpgrade| async {ws.on_upgrade(ws_echo)}))
+        .layer(
+            tower::ServiceBuilder::new()
+                .layer(
+                    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())
+        .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;
+    }
+
+    #[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);
+    }
+}
index 5f1feea9404c8116e1cbb1106b825e5a6d52c677..f2b1c100c043a0f567771536c36df6fa32ffd5c7 100644 (file)
@@ -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<String, std::io::Error> {
     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<T, A, V> {
-        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<dyn State> {
-        Box::new(PendingReview {acks: 0})
+        Box::new(PendingReview { acks: 0 })
     }
 
     fn approve(&mut self) -> Box<dyn State> {
@@ -176,14 +178,16 @@ struct PendingReview {
 
 impl State for PendingReview {
     fn request_review(&self) -> Box<dyn State> {
-        Box::new(Self {acks: self.acks})
+        Box::new(Self { acks: self.acks })
     }
 
     fn approve(&mut self) -> Box<dyn State> {
         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,
+        }
     }
 }
index 4b6df6447be9930e5773202f24601cd817722eb8..be26bdb6f2f8bb4212291f5aeb857dcf6bd4e6a8 100644 (file)
@@ -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());
 }
 
diff --git a/gtk3-hello-world/Cargo.toml b/gtk3-hello-world/Cargo.toml
new file mode 100644 (file)
index 0000000..0d0f585
--- /dev/null
@@ -0,0 +1,9 @@
+[package]
+name = "gtk3-hello-world"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+gtk = "0.15"
diff --git a/gtk3-hello-world/src/main.rs b/gtk3-hello-world/src/main.rs
new file mode 100644 (file)
index 0000000..a44bc62
--- /dev/null
@@ -0,0 +1,41 @@
+use gtk::prelude::*;
+use gtk::{
+    Application, ApplicationWindow,
+    Button,
+    Widget,
+};
+
+fn build_ui(app: &Application) {
+    let button = Button::builder()
+        .label("Click me!")
+        .margin_top(12)
+        .margin_bottom(12)
+        .margin_start(12)
+        .margin_end(12)
+        .build();
+
+    button.connect_clicked(move |button| {
+        button.set_label("Hello world!");
+    });
+
+    let button_w: &Widget = button.upcast_ref::<Widget>();
+
+    println!("button visible: {}", button_w.is_visible());
+
+    let window = ApplicationWindow::builder()
+        .application(app)
+        .title("Hello GTK")
+        .child(&button)
+        .build();
+
+    window.show_all();
+}
+
+fn main() {
+    let app = Application::builder()
+        .application_id("ork.gtk-rs.example")
+        .build();
+
+    app.connect_activate(build_ui);
+    app.run();
+}
diff --git a/gtk4-hello-world/Cargo.toml b/gtk4-hello-world/Cargo.toml
new file mode 100644 (file)
index 0000000..8a60626
--- /dev/null
@@ -0,0 +1,9 @@
+[package]
+name = "gtk4-hello-world"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+gtk = { version = "0.4", package = "gtk4" }
diff --git a/gtk4-hello-world/src/main.rs b/gtk4-hello-world/src/main.rs
new file mode 100644 (file)
index 0000000..06d096c
--- /dev/null
@@ -0,0 +1,36 @@
+use gtk::prelude::*;
+use gtk::{
+    Application, ApplicationWindow,
+    Button
+};
+
+fn build_ui(app: &Application) {
+    let button = Button::builder()
+        .label("Click me!")
+        .margin_top(12)
+        .margin_bottom(12)
+        .margin_start(12)
+        .margin_end(12)
+        .build();
+
+    button.connect_clicked(|button| {
+        button.set_label("Hello world!");
+    });
+
+    let window = ApplicationWindow::builder()
+        .application(app)
+        .title("Hello GTK")
+        .child(&button)
+        .build();
+
+    window.present();
+}
+
+fn main() {
+    let app = Application::builder()
+        .application_id("ork.gtk-rs.example")
+        .build();
+
+    app.connect_activate(build_ui);
+    app.run();
+}
diff --git a/hyper-server/Cargo.toml b/hyper-server/Cargo.toml
new file mode 100644 (file)
index 0000000..0d9bdfe
--- /dev/null
@@ -0,0 +1,11 @@
+[package]
+name = "hyper-server"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+futures = "0.3"
+hyper = { version = "0.14", features = ["server", "runtime", "http1"] }
+tokio = { version = "1", features = ["full"] }
diff --git a/hyper-server/src/main.rs b/hyper-server/src/main.rs
new file mode 100644 (file)
index 0000000..8199471
--- /dev/null
@@ -0,0 +1,42 @@
+use std::convert::Infallible;
+use std::net::SocketAddr;
+
+use hyper::service::{make_service_fn, service_fn};
+use hyper::{Body, Request, Response, Server, Method, StatusCode};
+
+async fn service(req: Request<Body>) -> Result<Response<Body>, Infallible> {
+    let mut response = Response::new(Body::empty());
+
+    match (req.method(), req.uri().path()) {
+        (&Method::GET, "/") => {
+            *response.body_mut() = Body::from("Try POSTing data to /echo");
+        },
+        (&Method::POST, "/echo") => {
+             *response.body_mut() = req.into_body();
+        },
+        _ => {
+            *response.status_mut() = StatusCode::NOT_FOUND;
+        }
+    }
+
+    Ok(response)
+}
+
+#[tokio::main]
+async fn main() {
+    // We'll bind to 127.0.0.1:3000
+    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
+    let make_svc = make_service_fn(|_conn| async {
+        Ok::<_, Infallible>(service_fn(service))
+    });
+    let server = Server::bind(&addr).serve(make_svc);
+    let graceful = server.with_graceful_shutdown(async {
+        tokio::signal::ctrl_c()
+        .await
+        .expect("failed to install CTRL+C signal handler");
+    });
+
+    if let Err(e) = graceful.await {
+        eprintln!("server error: {}", e);
+    }
+}
index fc88d23c418c5872db2d7df475506038a1644949..cdc7c81259b5aa4b177fbf2cb8197d7b78b30fc6 100644 (file)
@@ -44,9 +44,9 @@ fn load_contacts() -> Result<Contacts, Box<dyn Error>> {
 }
 
 fn main() -> Result<(), Box<dyn Error>> {
-    env_logger::init();
+    env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
     create_contacts()?;
     let contacts = load_contacts()?;
-    println!("deserialized: {:?}", &contacts);
+    log::info!("deserialized: {:?}", &contacts);
     Ok(())
 }
diff --git a/static/README.md b/static/README.md
new file mode 100644 (file)
index 0000000..41f2f35
--- /dev/null
@@ -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 (file)
index 0000000..cc31dba
--- /dev/null
@@ -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 (file)
index 0000000..5742cd6
Binary files /dev/null and b/static/dir1/optzip.txt.gz differ
diff --git a/static/onlycompressed.txt.gz b/static/onlycompressed.txt.gz
new file mode 100644 (file)
index 0000000..8542d28
Binary files /dev/null and b/static/onlycompressed.txt.gz differ
diff --git a/static/plain.txt b/static/plain.txt
new file mode 100644 (file)
index 0000000..9300012
--- /dev/null
@@ -0,0 +1 @@
+Hello world! This is uncompressed text.
diff --git a/tokio-tutorial-jbarszczewski/Cargo.toml b/tokio-tutorial-jbarszczewski/Cargo.toml
new file mode 100644 (file)
index 0000000..86cffc6
--- /dev/null
@@ -0,0 +1,9 @@
+[package]
+name = "tokio-tutorial-jbarszczewski"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+tokio = {version = "1.21", features = ["full"]}
diff --git a/tokio-tutorial-jbarszczewski/src/main.rs b/tokio-tutorial-jbarszczewski/src/main.rs
new file mode 100644 (file)
index 0000000..f998acc
--- /dev/null
@@ -0,0 +1,58 @@
+use std::str;
+use std::sync::{ Arc, Mutex };
+
+use tokio::io::{ AsyncReadExt, AsyncWriteExt };
+use tokio::net::{ TcpListener, TcpStream };
+
+#[tokio::main]
+async fn main() {
+    let balance = Arc::new(Mutex::new(0.0f64));
+    let listener = TcpListener::bind("127.0.0.1:8181").await.unwrap();
+
+    loop {
+        let (stream, _) = listener.accept().await.unwrap();
+        let balance_ref = balance.clone();
+        tokio::spawn(async move { handle_connection(stream, balance_ref).await });
+    }
+}
+
+async fn handle_connection(mut stream: TcpStream, balance: Arc<Mutex<f64>>) {
+    // Read the first 16 characters from the incoming stream
+    let mut buffer = [0; 16];
+    assert!(stream.read(&mut buffer).await.unwrap() >= 16);
+    // First 4 characters are used to detect HTTP method
+    let method_type = match str::from_utf8(&buffer[0..4]) {
+        Ok(v) => v,
+        Err(e) => panic!("Invalid UTF-8 sequence: {}", e),
+    };
+
+    let contents = match method_type {
+        "GET " => {
+            format!("{{\"balance\": {}}}", balance.lock().unwrap())
+        }
+        "POST" => {
+            // Take characters after 'POST /' until whitespace is detected.
+            let input: String = buffer[6..16]
+                .iter()
+                .take_while(|x| **x != 32u8)
+                .map(|x| *x as char)
+                .collect();
+            let balance_update = input.parse::<f64>().unwrap();
+            println!("got POST request to update by {}", balance_update);
+            let mut locked_balance = balance.lock().unwrap();
+            *locked_balance += balance_update;
+            format!("{{\"balance\": {}}}", locked_balance)
+        }
+        _ => {
+            panic!("Invalid HTTP method!")
+        }
+    };
+
+    let response = format!(
+        "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}",
+        contents.len(),
+        contents
+    );
+    assert!(stream.write(response.as_bytes()).await.unwrap() > 0);
+    stream.flush().await.unwrap();
+}
diff --git a/tokio-tutorial-mini-redis/Cargo.toml b/tokio-tutorial-mini-redis/Cargo.toml
new file mode 100644 (file)
index 0000000..b6caa1b
--- /dev/null
@@ -0,0 +1,13 @@
+[package]
+name = "tokio-tutorial-mini-redis"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+bytes = "1"
+env_logger = "0.9"
+log = "0.4"
+mini-redis = "0.4"
+tokio = { version = "1", features = ["full"] }
diff --git a/tokio-tutorial-mini-redis/examples/hello-redis.rs b/tokio-tutorial-mini-redis/examples/hello-redis.rs
new file mode 100644 (file)
index 0000000..a498655
--- /dev/null
@@ -0,0 +1,17 @@
+use mini_redis::{client, Result};
+
+#[tokio::main]
+async fn main() -> Result<()> {
+    // Open a connection to the mini-redis address.
+    let mut client = client::connect("127.0.0.1:6379").await?;
+
+    // Set the key "hello" with value "world"
+    client.set("hello", "world".into()).await?;
+
+    // Get key "hello"
+    let result = client.get("hello").await?;
+
+    println!("got value from the server; result={:?}", result);
+
+    Ok(())
+}
diff --git a/tokio-tutorial-mini-redis/src/bin/client.rs b/tokio-tutorial-mini-redis/src/bin/client.rs
new file mode 100644 (file)
index 0000000..184170e
--- /dev/null
@@ -0,0 +1,57 @@
+use bytes::Bytes;
+use mini_redis::client;
+use tokio::sync::{ mpsc, oneshot };
+
+/// Provided by the requester and used by the manager task to send
+/// the command response back to the requester.
+type Responder<T> = oneshot::Sender<mini_redis::Result<T>>;
+
+#[derive(Debug)]
+enum Command {
+    Get {
+        key: String,
+        resp: Responder<Option<Bytes>>,
+    },
+    Set {
+        key: String,
+        val: Bytes,
+        resp: Responder<()>,
+    }
+}
+
+#[tokio::main]
+async fn main() {
+    let (manager_tx, mut manager_rx) = mpsc::channel(32);
+
+    let manager = tokio::spawn(async move {
+        // Establish a connection to the server
+        let mut client = client::connect("127.0.0.1:6379").await.unwrap();
+
+        while let Some(cmd) = manager_rx.recv().await {
+            match cmd {
+                Command::Get { key, resp } => { resp.send(client.get(&key).await).unwrap(); }
+                Command::Set { key, val, resp } => { resp.send(client.set(&key, val).await).unwrap(); }
+            }
+        }
+    });
+
+    let manager_tx2 = manager_tx.clone();
+    // Spawn two tasks, one gets a key, the other sets a key
+    let t1 = tokio::spawn(async move {
+        let (resp_tx, resp_rx) = oneshot::channel();
+        manager_tx.send(Command::Get { key: "hello".to_string(), resp: resp_tx }).await.unwrap();
+        let res = resp_rx.await;
+        println!("t1: got {:?}", res);
+    });
+
+    let t2 = tokio::spawn(async move {
+        let (resp_tx, resp_rx) = oneshot::channel();
+        manager_tx2.send(Command::Set { key: "hello".to_string(), val: "bar".into(), resp: resp_tx }).await.unwrap();
+        let res = resp_rx.await.unwrap();
+        println!("t2: got {:?}", res);
+    });
+
+    t1.await.unwrap();
+    t2.await.unwrap();
+    manager.await.unwrap();
+}
diff --git a/tokio-tutorial-mini-redis/src/bin/server.rs b/tokio-tutorial-mini-redis/src/bin/server.rs
new file mode 100644 (file)
index 0000000..962c923
--- /dev/null
@@ -0,0 +1,65 @@
+use std::collections::HashMap;
+use std::error::Error;
+use std::sync::{Arc, Mutex};
+
+use bytes::Bytes;
+use mini_redis::{Connection, Frame};
+use mini_redis::Command::{self, Get, Set};
+use tokio::net::{TcpListener, TcpStream};
+
+type Db = Arc<Mutex<HashMap<String, Bytes>>>;
+
+const LISTEN: &str = "127.0.0.1:6379";
+
+#[tokio::main]
+async fn main() {
+    env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("debug")).init();
+
+    let listener = TcpListener::bind(LISTEN).await.unwrap();
+    log::info!("Listening on {}", LISTEN);
+    let db: Db = Arc::new(Mutex::new(HashMap::new()));
+
+    loop {
+        match listener.accept().await {
+            Ok((socket, addr)) => {
+                log::debug!("got connection from {:?}", addr);
+                let db_i = db.clone();
+                tokio::spawn(async move {
+                    if let Err(e) = process(socket, db_i).await {
+                        log::warn!("failed: {:?}", e);
+                    }
+                });
+            },
+            Err(e) => log::warn!("Failed to accept connection: {}", e),
+        };
+    }
+}
+
+async fn process(socket: TcpStream, db: Db) -> Result<(), Box<dyn Error + Send + Sync>> {
+    let mut connection = Connection::new(socket);
+
+    while let Some(frame) = connection.read_frame().await? {
+        let response = match Command::from_frame(frame)? {
+            Set(cmd) => {
+                // The value is stored as `Vec<u8>`
+                db.lock().unwrap().insert(cmd.key().to_string(), cmd.value().clone());
+                log::debug!("Set {} → {:?}", &cmd.key(), &cmd.value());
+                Frame::Simple("OK".to_string())
+            }
+            Get(cmd) => {
+                if let Some(value) = db.lock().unwrap().get(cmd.key()) {
+                    log::debug!("Get {} → {:?}", &cmd.key(), &value);
+                    Frame::Bulk(value.clone())
+                } else {
+                    log::debug!("Get {} unknown key", &cmd.key());
+                    Frame::Null
+                }
+            }
+            cmd => panic!("unimplemented {:?}", cmd),
+        };
+
+        // Write the response to the client
+        connection.write_frame(&response).await?;
+    }
+    Ok(())
+}
diff --git a/warp-server/Cargo.toml b/warp-server/Cargo.toml
new file mode 100644 (file)
index 0000000..3f804c5
--- /dev/null
@@ -0,0 +1,13 @@
+[package]
+name = "warp-server"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+futures-util = { version = "0.3", default-features = false, features = ["sink"] }
+tokio = { version = "1", features = ["full"] }
+env_logger = "0.9"
+log = "0.4"
+warp = "0.3"
diff --git a/warp-server/src/main.rs b/warp-server/src/main.rs
new file mode 100644 (file)
index 0000000..06b289a
--- /dev/null
@@ -0,0 +1,179 @@
+use futures_util::{FutureExt, StreamExt, SinkExt};
+use warp::Filter;
+
+// GET /hello/warp => 200 OK with body "Hello, warp!"
+async fn hello(name: String, agent: String) -> Result<impl warp::Reply, warp::Rejection> {
+    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-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);
+
+            if msg.is_close() {
+                break;
+            }
+            if msg.is_text() {
+                let text = msg.to_str().unwrap();
+                let rev = text.chars().rev().collect::<String>();
+                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");
+    });
+}
+
+pub fn api() -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
+    let hello = warp::path!("hello" / String)
+        .and(warp::header::<String>("user-agent"))
+        .and_then(hello);
+
+    let ws_echo = warp::path("ws-echo")
+        .and(warp::ws())
+        .map(|ws: warp::ws::Ws| { ws.on_upgrade(ws_echo_connected) });
+
+    let ws_rev = warp::path("ws-rev")
+        .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"))
+}
+
+#[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!");
+    }
+
+    #[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<u8> = 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<u8> = 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();
+    }
+
+    #[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);
+    }
+}