diff --git a/.github/workflows/CI.yml b/.github/workflows/ci.yml similarity index 85% rename from .github/workflows/CI.yml rename to .github/workflows/ci.yml index 644bf7a6..ea3ac5e1 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,6 @@ env: CARGO_NET_RETRY: 10 CARGO_TERM_COLOR: always RUST_BACKTRACE: 1 - RUSTFLAGS: -Dwarnings RUSTUP_MAX_RETRIES: 10 jobs: @@ -29,7 +28,6 @@ jobs: steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@nightly - - uses: Swatinem/rust-cache@v2 - run: cargo test --workspace --verbose coverage: @@ -39,7 +37,6 @@ jobs: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - uses: taiki-e/install-action@cargo-llvm-cov - - uses: Swatinem/rust-cache@v2 - name: Generate code coverage run: cargo llvm-cov --all-features --workspace --lcov --output-path lcov.info - name: Upload coverage to Codecov @@ -54,8 +51,7 @@ jobs: steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@clippy - - uses: Swatinem/rust-cache@v2 - - run: cargo clippy --tests -- -Dclippy::all -Dclippy::pedantic + - run: cargo clippy --tests fmt: name: Fmt @@ -63,16 +59,15 @@ jobs: steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 - run: cargo fmt --all -- --check docs: name: Doc runs-on: ubuntu-latest env: + RUSTFLAGS: --cfg hyper_unstable_tracing RUSTDOCFLAGS: -Dwarnings --cfg docsrs steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@nightly - - uses: Swatinem/rust-cache@v2 - run: cargo doc --workspace --all-features --no-deps diff --git a/Cargo.toml b/Cargo.toml index 507d8de3..a3c094f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ members = [ "examples/unix-socket", "examples/static-files/embed", "examples/static-files/serve", + "examples/static-files/include-dir", "examples/limits", "examples/forms/form", "examples/forms/multipart", @@ -28,23 +29,25 @@ members = [ "examples/templates/*", "examples/tracing", "examples/graceful-shutdown", + "examples/databases/*", ] resolver = "2" [workspace.package] -version = "0.5.0-rc.2" +version = "0.5.1" authors = ["Fangdun Tsai "] edition = "2021" homepage = "https://viz.rs" documentation = "https://docs.rs/viz" repository = "https://github.com/viz-rs/viz" license = "MIT" +rust-version = "1.63" # follows `tokio` and `hyper` [workspace.dependencies] -viz = { version = "0.5.0-rc.2", path = "viz" } -viz-core = { version = "0.5.0-rc.2", path = "viz-core" } -viz-router = { version = "0.5.0-rc.2", path = "viz-router" } -viz-handlers = { version = "0.5.0-rc.2", path = "viz-handlers", default-features = false } +viz = { version = "0.5.1", path = "viz" } +viz-core = { version = "0.5.1", path = "viz-core" } +viz-router = { version = "0.5.1", path = "viz-router" } +viz-handlers = { version = "0.5.1", path = "viz-handlers", default-features = false } viz-macros = { version = "0.1", path = "viz-macros" } viz-test = { version = "0.1", path = "viz-test" } @@ -61,8 +64,7 @@ thiserror = "1.0" path-tree = "0.7" # http -# TODO: wait headers-v1.0 -headers = { git = "https://github.com/hyperium/headers.git", rev = "4400aa9" } +headers = "0.4" http = "1" http-body = "1" http-body-util = "0.1" @@ -70,8 +72,8 @@ hyper = { version = "1", features = ["server"] } hyper-util = { version = "0.1", features = ["server-auto", "tokio"] } futures-util = "0.3" -tokio = { version = "1.33", features = ["net"] } -tokio-tungstenite = "0.20" +tokio = { version = "1.35", features = ["net"] } +tokio-tungstenite = "0.21" tokio-stream = "0.1" tokio-util = "0.7" rustls-pemfile = "1.0" @@ -110,3 +112,19 @@ split-debuginfo = "unpacked" [profile.dev.package."*"] opt-level = 3 debug = false + +[workspace.lints.rust] +unsafe_code = "forbid" +rust_2018_idioms = "warn" +single_use_lifetimes = "warn" +non_ascii_idents = "warn" +unreachable_pub = "warn" +missing_debug_implementations = "warn" +missing_docs = "warn" + +[workspace.lints.clippy] +all = "deny" +pedantic = "deny" +module_name_repetitions = { level = "allow", priority = 1 } +too_many_lines = { level = "allow", priority = 1 } +type_complexity = { level = "allow", priority = 1 } diff --git a/clippy.toml b/clippy.toml index ad0ea4e7..938388da 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1 +1,2 @@ -msrv = "1.64" +# Clippy configuration +# https://doc.rust-lang.org/nightly/clippy/lint_configuration.html diff --git a/examples/README.md b/examples/README.md index e6395a1c..2eddf67f 100644 --- a/examples/README.md +++ b/examples/README.md @@ -32,7 +32,7 @@ Here you can find a lot of small crabs 🦀. ## Usage -### Run it +### Run it in `viz` directory ```console $ cargo run --bin hello-world -- --nocapture diff --git a/examples/databases/README.md b/examples/databases/README.md new file mode 100644 index 00000000..bfd2fe0c --- /dev/null +++ b/examples/databases/README.md @@ -0,0 +1,4 @@ +# Databases + +## Examples +- [sea-orm](./sea-orm/README.md) \ No newline at end of file diff --git a/examples/databases/sea-orm/Cargo.toml b/examples/databases/sea-orm/Cargo.toml new file mode 100644 index 00000000..2f4199b7 --- /dev/null +++ b/examples/databases/sea-orm/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "sea-orm-example" +version = "0.1.0" +edition.workspace = true +publish = false + +[dependencies] +viz = { workspace = true, features = ["serve"] } +serde.workspace = true + +tokio = { workspace = true, features = [ "rt-multi-thread", "macros" ] } +sea-orm = { version = "0.12", features = ["runtime-tokio-rustls", "sqlx-sqlite"] } + +[lints] +workspace = true diff --git a/examples/databases/sea-orm/README.md b/examples/databases/sea-orm/README.md new file mode 100644 index 00000000..275a3dbd --- /dev/null +++ b/examples/databases/sea-orm/README.md @@ -0,0 +1,25 @@ +# Viz SeaOrm example + +UI inspired by: https://github.com/HapticX/happyx/blob/master/examples/todo/README.md + +## USAGE +sqlite use `in-memory`` mode,every time run the app, content reset! +```base +carog run +``` + +## FUNCTION IMPL + +- [x] list +- [x] create +- [x] update +- [ ] delete + +## SCREENSHOT + +![SeaOrm Demo](./sea-orm-demo.gif) + +## FAQ +- libsqlite3 error: you need install libsqlite3 for your system + +- sea-orm doc: https://www.sea-ql.org/sea-orm-tutorial/ch01-00-build-backend-getting-started.html diff --git a/examples/databases/sea-orm/public/index.html b/examples/databases/sea-orm/public/index.html new file mode 100644 index 00000000..9a20b0fc --- /dev/null +++ b/examples/databases/sea-orm/public/index.html @@ -0,0 +1,154 @@ + + + + + + + + Viz SeaOrm Todo Demo + + + +
+
+
+ + +
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/examples/databases/sea-orm/sea-orm-demo.gif b/examples/databases/sea-orm/sea-orm-demo.gif new file mode 100644 index 00000000..2cce8334 Binary files /dev/null and b/examples/databases/sea-orm/sea-orm-demo.gif differ diff --git a/examples/databases/sea-orm/src/api.rs b/examples/databases/sea-orm/src/api.rs new file mode 100644 index 00000000..f3e806c1 --- /dev/null +++ b/examples/databases/sea-orm/src/api.rs @@ -0,0 +1,78 @@ +//! web api mod + +use crate::entities::todo::{ActiveModel, Entity as todoEntity, Model as Todo}; + +use sea_orm::{ + ActiveModelTrait, ActiveValue::NotSet, DatabaseConnection, EntityTrait, Set, TryIntoModel, +}; +use viz::{ + types::{Json, Params, State}, + IntoResponse, Request, RequestExt, Response, ResponseExt, Result, +}; + +/// list todos +/// # Errors +/// - `viz::Error` +pub async fn list(mut req: Request) -> Result { + let State(db) = req.extract::>().await?; + let todos = todoEntity::find() + .all(&db) + .await + .map_err(|err| err.to_string().into_error())?; + Ok(Response::json(todos)?) +} + +/// create todos +/// # Errors +/// - `viz::Error` +pub async fn create(mut req: Request) -> Result { + let (State(db), Json(todo)) = req + .extract::<(State, Json)>() + .await?; + + let mut todo_am: ActiveModel = todo.into(); + todo_am.id = NotSet; + let result = todo_am + .insert(&db) + .await + .map_err(|err| err.to_string().into_error())?; + let todo_new: Todo = result + .try_into_model() + .map_err(|err| err.to_string().into_error())?; + Ok(Response::json(todo_new)?) +} + +/// update todos +/// PUT /todos/:id +/// # Errors +/// - `viz::Error` +pub async fn update(mut req: Request) -> Result { + let (State(db), Params(id), Json(todo)) = req + .extract::<(State, Params, Json)>() + .await?; + let mut todo_am: ActiveModel = todo.clone().into(); + todo_am.id = Set(id); + todo_am.completed = Set(todo.completed); + let model = todo_am + .update(&db) + .await + .map_err(|err| err.to_string().into_error())?; + + Ok(Response::json(model)?) +} + +/// delete todos +/// DELETE /todos/:id +/// # Errors +/// - `viz::Error` +pub async fn delete(mut req: Request) -> Result { + let (State(db), Params(id)) = req + .extract::<(State, Params)>() + .await?; + let delete_result = todoEntity::delete_by_id(id) + .exec(&db) + .await + .map_err(|err| err.to_string().into_error())?; + let rows_affected = delete_result.rows_affected; + Ok(Response::json(rows_affected)?) +} diff --git a/examples/databases/sea-orm/src/db.rs b/examples/databases/sea-orm/src/db.rs new file mode 100644 index 00000000..d8ac6418 --- /dev/null +++ b/examples/databases/sea-orm/src/db.rs @@ -0,0 +1,54 @@ +//! db module , init sqlite db + +use crate::entities::todo::Entity; +use sea_orm::{ + sea_query::{ColumnDef, SqliteQueryBuilder, Table, TableCreateStatement}, + ConnectionTrait, Database, DatabaseConnection, DbBackend, Schema, +}; + +/// +/// # Errors +/// - `DbErr` +pub async fn init_db() -> Result> { + let db = Database::connect("sqlite::memory:").await?; + setup_schema(&db).await; + Ok(db) +} + +/// setup sqlite schema +async fn setup_schema(db: &DatabaseConnection) { + // Setup Schema helper + let schema = Schema::new(DbBackend::Sqlite); + + // Derive from Entity + let stmt: TableCreateStatement = schema.create_table_from_entity(Entity); + + // Or setup manually + assert_eq!( + stmt.build(SqliteQueryBuilder), + Table::create() + .table(Entity) + .col( + ColumnDef::new(::Column::Id) + .primary_key() + .auto_increment() + .integer() + .not_null() + ) + .col( + ColumnDef::new(::Column::Text) + .text() + .not_null() + ) + .col( + ColumnDef::new(::Column::Completed) + .boolean() + .not_null() + ) + //... + .build(SqliteQueryBuilder) + ); + + // Execute create table statement + let _ = db.execute(db.get_database_backend().build(&stmt)).await; +} diff --git a/examples/databases/sea-orm/src/entities/mod.rs b/examples/databases/sea-orm/src/entities/mod.rs new file mode 100644 index 00000000..a8a4793b --- /dev/null +++ b/examples/databases/sea-orm/src/entities/mod.rs @@ -0,0 +1,2 @@ +//! sea-orm entities mod +pub mod todo; diff --git a/examples/databases/sea-orm/src/entities/todo.rs b/examples/databases/sea-orm/src/entities/todo.rs new file mode 100644 index 00000000..80a531d2 --- /dev/null +++ b/examples/databases/sea-orm/src/entities/todo.rs @@ -0,0 +1,23 @@ +//! todo model + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +/// +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Deserialize, Serialize)] +#[sea_orm(table_name = "todos")] +pub struct Model { + /// + #[sea_orm(primary_key)] + #[serde[skip_deserializing]] + pub id: i32, + /// + pub text: String, + /// + pub completed: bool, +} +/// +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/examples/databases/sea-orm/src/lib.rs b/examples/databases/sea-orm/src/lib.rs new file mode 100644 index 00000000..d0254bce --- /dev/null +++ b/examples/databases/sea-orm/src/lib.rs @@ -0,0 +1,6 @@ +#![deny(warnings)] +//! `SeaOrm` example for Viz framework. + +pub mod api; +pub mod db; +pub mod entities; diff --git a/examples/databases/sea-orm/src/main.rs b/examples/databases/sea-orm/src/main.rs new file mode 100644 index 00000000..ba968495 --- /dev/null +++ b/examples/databases/sea-orm/src/main.rs @@ -0,0 +1,34 @@ +#![deny(warnings)] +#![allow(clippy::unused_async)] + +//! `SeaOrm` example for Viz framework. +use sea_orm_example::{api, db::init_db}; +use std::{env, net::SocketAddr, sync::Arc}; +use tokio::net::TcpListener; +use viz::{handlers::serve, middleware, serve, types::State, Result, Router, Tree}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); + let listener = TcpListener::bind(addr).await?; + + let db = init_db().await?; + + println!("listening on http://{addr}"); + let dir = env::current_dir().unwrap(); + let app = Router::new() + .get("/", serve::File::new(dir.join("public/index.html"))) + .get("/todos", api::list) + .post("/todos", api::create) + .put("/todos/:id", api::update) + .delete("/todos/:id", api::delete) + .with(State::new(db.clone())) + .with(middleware::limits::Config::new()); + let tree = Arc::new(Tree::from(app)); + + loop { + let (stream, addr) = listener.accept().await?; + let tree = tree.clone(); + tokio::task::spawn(serve(stream, tree, Some(addr))); + } +} diff --git a/examples/routing/openapi/Cargo.toml b/examples/routing/openapi/Cargo.toml index c2daeaf6..7e8ba191 100644 --- a/examples/routing/openapi/Cargo.toml +++ b/examples/routing/openapi/Cargo.toml @@ -13,4 +13,4 @@ serde_json.workspace = true tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } utoipa = "4" -utoipa-swagger-ui = "4" +utoipa-swagger-ui = "5" diff --git a/examples/static-files/embed/Cargo.toml b/examples/static-files/embed/Cargo.toml index 73f24174..a5445f17 100644 --- a/examples/static-files/embed/Cargo.toml +++ b/examples/static-files/embed/Cargo.toml @@ -4,8 +4,19 @@ version = "0.1.0" edition.workspace = true publish = false +[features] +tracing = [ + "dep:hyper", + "dep:tracing", + "dep:tracing-subscriber" +] + [dependencies] viz = { workspace = true, features = ["embed"] } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } rust-embed.workspace = true + +hyper = { workspace = true, features = ["tracing"], optional = true } +tracing = { workspace = true, optional = true } +tracing-subscriber = { workspace = true, features = ["env-filter"], optional = true } diff --git a/examples/static-files/embed/README.md b/examples/static-files/embed/README.md new file mode 100644 index 00000000..338af464 --- /dev/null +++ b/examples/static-files/embed/README.md @@ -0,0 +1,5 @@ +## embed + +```console +RUSTFLAGS="--cfg hyper_unstable_tracing" RUST_LOG='trace,hyper=trace' cargo run --bin embed --features tracing +``` diff --git a/examples/static-files/embed/src/main.rs b/examples/static-files/embed/src/main.rs index 5e880e5d..8c7cdb58 100644 --- a/examples/static-files/embed/src/main.rs +++ b/examples/static-files/embed/src/main.rs @@ -10,6 +10,9 @@ struct Asset; #[tokio::main] async fn main() -> Result<()> { + #[cfg(feature = "tracing")] + tracing_subscriber::fmt::init(); + let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); let listener = TcpListener::bind(addr).await?; println!("listening on http://{addr}"); diff --git a/examples/static-files/include-dir/Cargo.toml b/examples/static-files/include-dir/Cargo.toml new file mode 100644 index 00000000..8080e100 --- /dev/null +++ b/examples/static-files/include-dir/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "include-dir" +version = "0.1.0" +edition.workspace = true +publish = false + +[dependencies] +viz.workspace = true +tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } + +include_dir = "0.7" +http-body-util = "0.1" diff --git a/examples/static-files/include-dir/html/assets/style.css b/examples/static-files/include-dir/html/assets/style.css new file mode 100644 index 00000000..5b652ae9 --- /dev/null +++ b/examples/static-files/include-dir/html/assets/style.css @@ -0,0 +1,3 @@ +h2 { + color: red; +} diff --git a/examples/static-files/include-dir/html/index.html b/examples/static-files/include-dir/html/index.html new file mode 100644 index 00000000..679e4dfe --- /dev/null +++ b/examples/static-files/include-dir/html/index.html @@ -0,0 +1,13 @@ + + + + + + Document + + + +

hello word!

+

hello word!

+ + diff --git a/examples/static-files/include-dir/src/main.rs b/examples/static-files/include-dir/src/main.rs new file mode 100644 index 00000000..64218e76 --- /dev/null +++ b/examples/static-files/include-dir/src/main.rs @@ -0,0 +1,55 @@ +#![deny(warnings)] +#![allow(clippy::unused_async)] + +use http_body_util::Full; +use include_dir::{include_dir, Dir}; +use std::{net::SocketAddr, sync::Arc}; +use tokio::net::TcpListener; +use viz::{ + serve, IntoResponse, Request, RequestExt, Response, ResponseExt, Result, Router, StatusCode, + Tree, +}; + +const ASSETS: Dir = include_dir!("examples/static-files/include-dir/html"); // frontend dir + +pub async fn index(_: Request) -> Result { + let file_content = ASSETS + .get_file("index.html") + .unwrap() + .contents_utf8() + .unwrap(); + Ok(Response::html(file_content)) +} + +pub async fn assets(req: Request) -> Result { + let path = req.path().trim_start_matches('/'); + if path.contains("..") { + return Ok(StatusCode::FORBIDDEN.into_response()); + } + Ok(match ASSETS.get_file(path) { + Some(v) => { + let data = v.contents(); + Response::builder().body(Full::from(data).into())? + } + None => StatusCode::NOT_FOUND.into_response(), + }) +} + +#[tokio::main] +async fn main() -> Result<()> { + let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); + let listener = TcpListener::bind(addr).await?; + println!("listening on http://{addr}"); + + let app = Router::new() + .any("/*", |_| async { Ok(StatusCode::NOT_FOUND) }) + .get("/", index) + .get("/*", assets); + let tree = Arc::new(Tree::from(app)); + + loop { + let (stream, addr) = listener.accept().await?; + let tree = tree.clone(); + tokio::task::spawn(serve(stream, tree, Some(addr))); + } +} diff --git a/examples/static-files/serve/src/main.rs b/examples/static-files/serve/src/main.rs index b35ea63a..eac6aa3f 100644 --- a/examples/static-files/serve/src/main.rs +++ b/examples/static-files/serve/src/main.rs @@ -15,12 +15,16 @@ async fn main() -> Result<()> { let listener = TcpListener::bind(addr).await?; println!("listening on http://{addr}"); + // in `viz` directory let dir = env::current_dir().unwrap(); let app = Router::new() .get("/", index) .get("/cargo.toml", serve::File::new(dir.join("Cargo.toml"))) - .get("/examples/*", serve::Dir::new(dir).listing()) + .get( + "/examples/*", + serve::Dir::new(dir.join("examples")).listing(), + ) .any("/*", |_| async { Ok(Response::text("Welcome!")) }); let tree = Arc::new(Tree::from(app)); diff --git a/examples/static-routes/Cargo.toml b/examples/static-routes/Cargo.toml index 3c13eb22..cc9fb714 100644 --- a/examples/static-routes/Cargo.toml +++ b/examples/static-routes/Cargo.toml @@ -9,4 +9,4 @@ viz.workspace = true hyper.workspace = true tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } -once_cell = "1.17" +once_cell = "1.19" diff --git a/examples/templates/markup/Cargo.toml b/examples/templates/markup/Cargo.toml index ee497407..58245d05 100644 --- a/examples/templates/markup/Cargo.toml +++ b/examples/templates/markup/Cargo.toml @@ -9,5 +9,5 @@ viz.workspace = true tokio = { workspace = true, features = [ "rt-multi-thread", "macros" ] } -markup = "0.14" +markup = "0.15" v_htmlescape = "0.15" diff --git a/examples/templates/tera/Cargo.toml b/examples/templates/tera/Cargo.toml index b02cf390..13b740a7 100644 --- a/examples/templates/tera/Cargo.toml +++ b/examples/templates/tera/Cargo.toml @@ -10,4 +10,4 @@ viz.workspace = true serde.workspace = true tokio = { workspace = true, features = [ "rt-multi-thread", "macros" ] } tera = "1.18" -once_cell = "1.17" +once_cell = "1.19" diff --git a/examples/websocket-chat/src/main.rs b/examples/websocket-chat/src/main.rs index b64f6e3c..01ec3798 100644 --- a/examples/websocket-chat/src/main.rs +++ b/examples/websocket-chat/src/main.rs @@ -6,7 +6,7 @@ use std::{net::SocketAddr, sync::Arc}; use tokio::net::TcpListener; use tokio::sync::broadcast::{channel, Sender}; use viz::{ - get, serve, + get, serve_with_upgrades, types::{Message, Params, State, WebSocket}, HandlerExt, IntoHandler, IntoResponse, Request, RequestExt, Response, ResponseExt, Result, Router, Tree, @@ -67,7 +67,7 @@ async fn main() -> Result<()> { let (stream, addr) = listener.accept().await?; let tree = tree.clone(); tokio::task::spawn(async move { - if let Err(err) = serve(stream, tree, Some(addr)).await { + if let Err(err) = serve_with_upgrades(stream, tree, Some(addr)).await { eprintln!("Error while serving HTTP connection: {err}"); } }); diff --git a/viz-core/Cargo.toml b/viz-core/Cargo.toml index 45694cd0..4964d33b 100644 --- a/viz-core/Cargo.toml +++ b/viz-core/Cargo.toml @@ -6,6 +6,7 @@ license.workspace = true authors.workspace = true homepage.workspace = true repository.workspace = true +rust-version.workspace = true readme = "README.md" documentation = "https://docs.rs/viz-core" description = "The core traits and types in for Viz" @@ -98,10 +99,11 @@ tokio-stream = { workspace = true, optional = true } tokio-util = { workspace = true, optional = true } [dev-dependencies] -viz = { workspace = true, features = ["session"] } -viz-test.workspace = true tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] + +[lints] +workspace = true diff --git a/viz-core/src/lib.rs b/viz-core/src/lib.rs index 39aa6a4f..ac3c91b9 100644 --- a/viz-core/src/lib.rs +++ b/viz-core/src/lib.rs @@ -4,14 +4,6 @@ #![doc(html_logo_url = "https://viz.rs/logo.svg")] #![doc(html_favicon_url = "https://viz.rs/logo.svg")] -#![allow(clippy::module_name_repetitions)] -#![forbid(unsafe_code)] -#![warn( - missing_debug_implementations, - missing_docs, - rust_2018_idioms, - unreachable_pub -)] #![doc(test( no_crate_inject, attr( diff --git a/viz-core/src/middleware/cors.rs b/viz-core/src/middleware/cors.rs index 57791ea9..2790330a 100644 --- a/viz-core/src/middleware/cors.rs +++ b/viz-core/src/middleware/cors.rs @@ -24,7 +24,6 @@ pub struct Config { allow_headers: HashSet, allow_origins: HashSet, expose_headers: HashSet, - #[allow(clippy::type_complexity)] origin_verify: Option bool + Send + Sync>>, } @@ -118,7 +117,6 @@ impl Config { } /// A function to verify the origin. If the function returns false, the request will be rejected. - #[allow(clippy::type_complexity)] #[must_use] pub fn origin_verify( mut self, @@ -210,9 +208,8 @@ where type Output = Result; async fn call(&self, req: Request) -> Self::Output { - let origin = match req.header(ORIGIN).filter(is_not_empty) { - Some(origin) => origin, - None => return self.h.call(req).await.map(IntoResponse::into_response), + let Some(origin) = req.header(ORIGIN).filter(is_not_empty) else { + return self.h.call(req).await.map(IntoResponse::into_response); }; if !self.config.allow_origins.contains(&origin) diff --git a/viz-core/src/middleware/csrf.rs b/viz-core/src/middleware/csrf.rs index 55152276..40584707 100644 --- a/viz-core/src/middleware/csrf.rs +++ b/viz-core/src/middleware/csrf.rs @@ -97,7 +97,7 @@ where Some(raw_token) => base64::engine::general_purpose::URL_SAFE_NO_PAD .decode(raw_token) .ok() - .filter(is_64) + .filter(|b| b.len() == 64) .map(unmask::<32>) .map(Option::Some) .ok_or_else(|| { @@ -252,10 +252,13 @@ pub fn generate(secret: &[u8], otp: Vec) -> Vec { /// Verifys Token with a secret #[must_use] pub fn verify(secret: &[u8], raw_token: String) -> bool { - if let Ok(token) = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(raw_token) { - return is_64(&token) && secret == unmask::<32>(token); - } - false + base64::engine::general_purpose::URL_SAFE_NO_PAD + .decode(raw_token) + .ok() + .filter(|b| b.len() == 64) + .map(unmask::<32>) + .filter(|t| t == secret) + .is_some() } /// Retures masked token @@ -282,10 +285,6 @@ fn unmask(mut token: Vec) -> Vec { secret } -fn is_64(buf: &Vec) -> bool { - buf.len() == 64 -} - #[cfg(test)] mod tests { use super::*; diff --git a/viz-core/src/middleware/otel/tracing.rs b/viz-core/src/middleware/otel/tracing.rs index 82043035..d60af86c 100644 --- a/viz-core/src/middleware/otel/tracing.rs +++ b/viz-core/src/middleware/otel/tracing.rs @@ -141,7 +141,7 @@ impl<'a> RequestHeaderCarrier<'a> { } } -impl<'a> Extractor for RequestHeaderCarrier<'a> { +impl Extractor for RequestHeaderCarrier<'_> { fn get(&self, key: &str) -> Option<&str> { self.headers.get(key).and_then(|v| v.to_str().ok()) } diff --git a/viz-core/src/types/websocket.rs b/viz-core/src/types/websocket.rs index f72d6d7c..8169b086 100644 --- a/viz-core/src/types/websocket.rs +++ b/viz-core/src/types/websocket.rs @@ -76,9 +76,8 @@ impl WebSocket { let on_upgrade = self.on_upgrade.take().expect("missing OnUpgrade"); tokio::task::spawn(async move { - let upgraded = match on_upgrade.await { - Ok(upgraded) => upgraded, - Err(_) => return, + let Ok(upgraded) = on_upgrade.await else { + return; }; let socket = diff --git a/viz-core/tests/handler.rs b/viz-core/tests/handler.rs index b404dbfd..ca582ca5 100644 --- a/viz-core/tests/handler.rs +++ b/viz-core/tests/handler.rs @@ -1,7 +1,6 @@ #![allow(dead_code)] #![allow(clippy::unused_async)] #![allow(clippy::similar_names)] -#![allow(clippy::too_many_lines)] #![allow(clippy::wildcard_imports)] use http_body_util::Full; diff --git a/viz-core/tests/request.rs b/viz-core/tests/request.rs index eb46bc95..e1f74c65 100644 --- a/viz-core/tests/request.rs +++ b/viz-core/tests/request.rs @@ -1,6 +1,4 @@ -use std::collections::{BTreeMap, HashMap}; - -use headers::{authorization::Bearer, Authorization, ContentType, HeaderValue}; +use headers::{ContentType, HeaderValue}; use http::uri::Scheme; use serde::{Deserialize, Serialize}; use viz_core::{ @@ -8,14 +6,10 @@ use viz_core::{ // header::{AUTHORIZATION, CONTENT_TYPE, COOKIE, SET_COOKIE}, // StatusCode, header::CONTENT_TYPE, - types::{self, PayloadError}, - Error, + types::PayloadError, IncomingBody, - IntoResponse, Request, RequestExt, - Response, - ResponseExt, Result, }; @@ -70,285 +64,3 @@ fn request_ext() -> Result<()> { Ok(()) } - -#[allow(clippy::too_many_lines)] -#[tokio::test] -async fn request_body() -> Result<()> { - use futures_util::stream::TryStreamExt; - use viz::{ - middleware::{cookie, limits}, - Router, - }; - use viz_test::http::{ - header::{AUTHORIZATION, COOKIE}, - StatusCode, - }; - use viz_test::TestServer; - - let router = Router::new() - .get("/:id", |req: Request| async move { - let id = req.param::("id")?; - Ok(id) - }) - .get("/:username/:repo", |req: Request| async move { - let (username, repo): (String, String) = req.params()?; - Ok(format!("{username}/{repo}")) - }) - .get("/extract-token", |mut req: Request| async move { - let header: types::Header> = req.extract().await?; - Ok(header.into_inner().token().to_string()) - }) - .post("/extract-body", |mut req: Request| async move { - let form: types::Form> = req.extract().await?; - Ok(Response::json(form.into_inner())) - }) - .get("/cookies", |req: Request| async move { - let cookies = req.cookies()?; - let jar = cookies - .jar() - .lock() - .map_err(|e| Error::Responder(e.to_string().into_response()))?; - Ok(jar.iter().count().to_string()) - }) - .get("/cookie", |req: Request| async move { - Ok(req.cookie("viz").unwrap().value().to_string()) - }) - .with(cookie::Config::default()) - .post("/bytes", |mut req: Request| async move { - let data = req.bytes().await?; - Ok(data) - }) - .post("/bytes-with-limit", |mut req: Request| async move { - let data = req.bytes_with(None, 4).await?; - Ok(data) - }) - .post("/bytes-used", |mut req: Request| async move { - req.bytes().await?; - let data = req.bytes().await?; - Ok(data) - }) - .post("/text", |mut req: Request| async move { - let data = req.text().await?; - Ok(Response::text(data)) - }) - .post("/json", |mut req: Request| async move { - let data = req.json::().await?; - Ok(Response::json(data)) - }) - .post("/form", |mut req: Request| async move { - let data = req.form::>().await?; - Ok(Response::json(data)) - }) - .post("/multipart", |mut req: Request| async move { - let mut multipart = req.multipart().await?; - let mut data = HashMap::new(); - - while let Some(mut field) = multipart.try_next().await? { - let buf = field.bytes().await?.to_vec(); - data.insert(field.name, String::from_utf8(buf).map_err(Error::normal)?); - } - - Ok(Response::json(data)) - }) - .with(limits::Config::new().limits(types::Limits::new())); - - let client = TestServer::new(router).await?; - - let resp = client.get("/7").send().await.map_err(Error::normal)?; - assert_eq!(resp.text().await.map_err(Error::normal)?, "7"); - - let resp = client - .get("/viz-rs/viz") - .send() - .await - .map_err(Error::normal)?; - assert_eq!(resp.text().await.map_err(Error::normal)?, "viz-rs/viz"); - - let resp = client - .get("/extract-token") - .header(AUTHORIZATION, "Bearer viz.rs") - .send() - .await - .map_err(Error::normal)?; - assert_eq!(resp.text().await.map_err(Error::normal)?, "viz.rs"); - - let mut form = BTreeMap::new(); - form.insert("password", "rs"); - form.insert("username", "viz"); - let resp = client - .post("/extract-body") - .form(&form) - .send() - .await - .map_err(Error::normal)?; - assert_eq!( - resp.text().await.map_err(Error::normal)?, - r#"{"password":"rs","username":"viz"}"# - ); - - let resp = client - .get("/cookie") - .header(COOKIE, "viz=crate") - .send() - .await - .map_err(Error::normal)?; - assert_eq!(resp.text().await.map_err(Error::normal)?, "crate"); - - let resp = client - .get("/cookies") - .header(COOKIE, "auth=true;dark=false") - .send() - .await - .map_err(Error::normal)?; - assert_eq!(resp.text().await.map_err(Error::normal)?, "2"); - - let resp = client - .post("/bytes") - .body("bytes") - .send() - .await - .map_err(Error::normal)?; - assert_eq!(resp.text().await.map_err(Error::normal)?, "bytes"); - - let resp = client - .post("/bytes-with-limit") - .body("rust") - .send() - .await - .map_err(Error::normal)?; - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.text().await.map_err(Error::normal)?, "rust"); - - let resp = client - .post("/bytes-with-limit") - .body("crate") - .send() - .await - .map_err(Error::normal)?; - assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE); - assert_eq!( - resp.text().await.map_err(Error::normal)?, - "payload is too large" - ); - - let resp = client - .post("/bytes-used") - .body("used") - .send() - .await - .map_err(Error::normal)?; - assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); - assert_eq!( - resp.text().await.map_err(Error::normal)?, - "payload has been used" - ); - - let resp = client - .post("/text") - .body("text") - .send() - .await - .map_err(Error::normal)?; - assert_eq!(resp.text().await.map_err(Error::normal)?, "text"); - - let resp = client - .post("/json") - .json(&Page { p: 1 }) - .send() - .await - .map_err(Error::normal)?; - assert_eq!( - resp.json::().await.map_err(Error::normal)?, - Page { p: 1 } - ); - - let mut form = HashMap::new(); - form.insert("username", "viz"); - form.insert("password", "rs"); - let resp = client - .post("/form") - .form(&form) - .send() - .await - .map_err(Error::normal)?; - assert_eq!( - resp.json::>() - .await - .map_err(Error::normal)?, - { - let mut form = HashMap::new(); - form.insert("username".to_string(), "viz".to_string()); - form.insert("password".to_string(), "rs".to_string()); - form - } - ); - - let form = viz_test::multipart::Form::new() - .text("key3", "3") - .text("key4", "4"); - let resp = client - .post("/multipart") - .multipart(form) - .send() - .await - .map_err(Error::normal)?; - assert_eq!( - resp.json::>() - .await - .map_err(Error::normal)?, - { - let mut form = HashMap::new(); - form.insert("key3".to_string(), "3".to_string()); - form.insert("key4".to_string(), "4".to_string()); - form - } - ); - - Ok(()) -} - -#[tokio::test] -async fn request_session() -> Result<()> { - use viz::{ - middleware::{cookie, helper::CookieOptions, session}, - Router, - }; - use viz_test::http::header::{COOKIE, SET_COOKIE}; - use viz_test::{nano_id, sessions, TestServer}; - - let router = Router::new() - .post("/session/set", |req: Request| async move { - let counter = req.session().get::("counter")?.unwrap_or_default() + 1; - req.session().set("counter", counter)?; - Ok(counter.to_string()) - }) - .with(session::Config::new( - session::Store::new( - sessions::MemoryStorage::new(), - nano_id::base64::<32>, - |sid: &str| sid.len() == 32, - ), - CookieOptions::default(), - )) - .with(cookie::Config::default()); - - let client = TestServer::new(router).await?; - - let resp = client - .post("/session/set") - .send() - .await - .map_err(Error::normal)?; - let cookie = resp.headers().get(SET_COOKIE).cloned().unwrap(); - assert_eq!(resp.text().await.map_err(Error::normal)?, "1"); - - let resp = client - .post("/session/set") - .header(COOKIE, cookie) - .send() - .await - .map_err(Error::normal)?; - assert_eq!(resp.text().await.map_err(Error::normal)?, "2"); - - Ok(()) -} diff --git a/viz-core/tests/response.rs b/viz-core/tests/response.rs index 763b8c25..b3bbcb05 100644 --- a/viz-core/tests/response.rs +++ b/viz-core/tests/response.rs @@ -123,30 +123,3 @@ async fn response_ext() -> Result<()> { fn response_ext_panic() { Response::redirect_with_status("/oauth", StatusCode::OK); } - -#[tokio::test] -async fn response_ext_with_server() -> Result<()> { - use viz::{Request, Router}; - use viz_test::TestServer; - - let router = Router::new() - .get("/", |_: Request| async move { Ok("") }) - .post("/", |_: Request| async move { - Ok(Response::with( - Full::new("".into()), - mime::TEXT_XML.as_ref(), - )) - }); - - let client = TestServer::new(router).await?; - - let resp = client.get("/").send().await.map_err(Error::normal)?; - assert_eq!(resp.content_length(), Some(0)); - assert_eq!(resp.text().await.map_err(Error::normal)?, ""); - - let resp = client.post("/").send().await.map_err(Error::normal)?; - assert_eq!(resp.content_length(), Some(6)); - assert_eq!(resp.text().await.map_err(Error::normal)?, ""); - - Ok(()) -} diff --git a/viz-handlers/Cargo.toml b/viz-handlers/Cargo.toml index 7713b577..d34c6aa9 100644 --- a/viz-handlers/Cargo.toml +++ b/viz-handlers/Cargo.toml @@ -6,6 +6,7 @@ license.workspace = true authors.workspace = true homepage.workspace = true repository.workspace = true +rust-version.workspace = true readme = "README.md" documentation = "https://docs.rs/viz-handlers" description = "The handlers for Viz" @@ -66,3 +67,6 @@ tokio = { workspace = true, features = ["rt", "macros"] } [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] + +[lints] +workspace = true diff --git a/viz-handlers/src/lib.rs b/viz-handlers/src/lib.rs index 7ec4742a..eb8843d8 100644 --- a/viz-handlers/src/lib.rs +++ b/viz-handlers/src/lib.rs @@ -2,13 +2,6 @@ #![doc(html_logo_url = "https://viz.rs/logo.svg")] #![doc(html_favicon_url = "https://viz.rs/logo.svg")] -#![forbid(unsafe_code)] -#![warn( - missing_debug_implementations, - missing_docs, - rust_2018_idioms, - unreachable_pub -)] #![doc(test( no_crate_inject, attr( diff --git a/viz-handlers/src/serve.rs b/viz-handlers/src/serve.rs index 887d2286..09280c90 100644 --- a/viz-handlers/src/serve.rs +++ b/viz-handlers/src/serve.rs @@ -196,9 +196,10 @@ fn serve(path: &Path, headers: &HeaderMap) -> Result { last_modified.replace(LastModified::from(modified)); } + // See https://github.com/hyperium/headers/pull/155 if let Some((start, end)) = headers .typed_get::() - .and_then(|range| range.iter().next()) + .and_then(|range| range.satisfiable_ranges(100).next()) { let start = match start { Bound::Included(n) => n, diff --git a/viz-handlers/src/serve/directory.rs b/viz-handlers/src/serve/directory.rs index a43ec367..196cbc9b 100644 --- a/viz-handlers/src/serve/directory.rs +++ b/viz-handlers/src/serve/directory.rs @@ -6,7 +6,7 @@ use std::{ ffi::OsStr, fmt::{Display, Formatter, Result}, fs::read_dir, - path::PathBuf, + path::{Path, PathBuf}, str::FromStr, string::ToString, }; @@ -24,7 +24,7 @@ impl Directory { pub(crate) fn new( base: &str, prev: bool, - root: &std::path::Path, + root: &Path, unlisted: &Option>, ) -> Option { let mut entries = read_dir(root).ok()?; @@ -66,7 +66,11 @@ impl Directory { 0, ( parent.join("").to_str()?.strip_prefix('/')?.to_string(), - parent.file_name().and_then(OsStr::to_str)?.to_string(), + parent + .file_name() + .and_then(OsStr::to_str) + .unwrap_or("") + .to_string(), false, None, "..".to_string(), diff --git a/viz-macros/Cargo.toml b/viz-macros/Cargo.toml index 127838e0..5e2676c5 100644 --- a/viz-macros/Cargo.toml +++ b/viz-macros/Cargo.toml @@ -6,6 +6,7 @@ license.workspace = true authors.workspace = true homepage.workspace = true repository.workspace = true +rust-version.workspace = true readme = "README.md" documentation = "https://docs.rs/viz-macros" description = "The proc macros for Viz" @@ -28,3 +29,6 @@ tokio = { workspace = true, features = ["rt", "macros"] } [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] + +[lints] +workspace = true diff --git a/viz-macros/src/lib.rs b/viz-macros/src/lib.rs index 8e4a4f8e..d23e8dbe 100644 --- a/viz-macros/src/lib.rs +++ b/viz-macros/src/lib.rs @@ -27,13 +27,6 @@ #![doc(html_logo_url = "https://viz.rs/logo.svg")] #![doc(html_favicon_url = "https://viz.rs/logo.svg")] -#![forbid(unsafe_code)] -#![warn( - missing_debug_implementations, - missing_docs, - rust_2018_idioms, - unreachable_pub -)] #![doc(test( no_crate_inject, attr( diff --git a/viz-router/Cargo.toml b/viz-router/Cargo.toml index abddc76f..e4e1a73a 100644 --- a/viz-router/Cargo.toml +++ b/viz-router/Cargo.toml @@ -6,6 +6,7 @@ license.workspace = true authors.workspace = true homepage.workspace = true repository.workspace = true +rust-version.workspace = true readme = "README.md" documentation = "https://docs.rs/viz-router" description = "The router for Viz" @@ -30,3 +31,6 @@ serde_derive.workspace = true hyper.workspace = true http-body-util.workspace = true tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } + +[lints] +workspace = true diff --git a/viz-router/src/lib.rs b/viz-router/src/lib.rs index a9d17d56..f339ff62 100644 --- a/viz-router/src/lib.rs +++ b/viz-router/src/lib.rs @@ -2,13 +2,6 @@ #![doc(html_logo_url = "https://viz.rs/logo.svg")] #![doc(html_favicon_url = "https://viz.rs/logo.svg")] -#![forbid(unsafe_code)] -#![warn( - missing_debug_implementations, - missing_docs, - rust_2018_idioms, - unreachable_pub -)] #![doc(test( no_crate_inject, attr( diff --git a/viz-router/src/resources.rs b/viz-router/src/resources.rs index 21106f70..bd3ca803 100644 --- a/viz-router/src/resources.rs +++ b/viz-router/src/resources.rs @@ -254,7 +254,6 @@ impl IntoIterator for Resources { #[cfg(test)] #[allow(clippy::unused_async)] -#[allow(clippy::too_many_lines)] mod tests { use super::Kind; use crate::{get, Resources}; diff --git a/viz-router/src/route.rs b/viz-router/src/route.rs index 8d8d6d23..d9142ca2 100644 --- a/viz-router/src/route.rs +++ b/viz-router/src/route.rs @@ -212,7 +212,6 @@ impl fmt::Debug for Route { #[cfg(test)] #[allow(dead_code)] #[allow(clippy::unused_async)] -#[allow(clippy::too_many_lines)] mod tests { use super::Route; use http_body_util::BodyExt; diff --git a/viz-router/src/router.rs b/viz-router/src/router.rs index 2312032f..b98c1c05 100644 --- a/viz-router/src/router.rs +++ b/viz-router/src/router.rs @@ -182,7 +182,6 @@ impl Router { #[cfg(test)] #[allow(clippy::unused_async)] -#[allow(clippy::too_many_lines)] mod tests { use http_body_util::{BodyExt, Full}; use std::sync::Arc; diff --git a/viz-test/Cargo.toml b/viz-test/Cargo.toml index a46ea389..e364b578 100644 --- a/viz-test/Cargo.toml +++ b/viz-test/Cargo.toml @@ -6,16 +6,24 @@ license.workspace = true authors.workspace = true homepage.workspace = true repository.workspace = true +rust-version.workspace = true readme = "README.md" documentation = "https://docs.rs/viz-test" description = "The core traits and types in for Viz" keywords = ["async", "framework", "http", "service", "web"] categories = ["asynchronous", "network-programming", "web-programming"] -publish = false [dependencies] viz.workspace = true +bytes.workspace = true +futures-util.workspace = true +headers.workspace = true +http-body.workspace = true +http-body-util.workspace = true +hyper.workspace = true +mime.workspace = true +serde.workspace = true sessions = { version = "0.5", features = ["memory"] } nano-id = "0.3" diff --git a/viz-test/README.md b/viz-test/README.md new file mode 100644 index 00000000..71709cdb --- /dev/null +++ b/viz-test/README.md @@ -0,0 +1,30 @@ +

+ +

+ +

+ Viz Test +

+ +
+

Viz Test

+
+ +
+ + + Safety! + + + Docs.rs docs + + + Crates.io version + + + Download +
diff --git a/viz-core/tests/body.rs b/viz-test/tests/body.rs similarity index 98% rename from viz-core/tests/body.rs rename to viz-test/tests/body.rs index e35d9c0d..ffed2441 100644 --- a/viz-core/tests/body.rs +++ b/viz-test/tests/body.rs @@ -1,11 +1,10 @@ use http_body_util::{combinators::BoxBody, BodyExt, Full}; -use viz_core::{Bytes, Error, IncomingBody, OutgoingBody, Request, RequestExt, Result}; +use viz::{Bytes, Error, IncomingBody, OutgoingBody, Request, RequestExt, Result}; #[tokio::test] async fn incoming_body() -> Result<()> { use bytes::Buf; - use viz::Router; - use viz_core::Body; + use viz::{Body, Router}; use viz_test::TestServer; let mut empty = IncomingBody::Empty; @@ -135,7 +134,7 @@ async fn incoming_stream() -> Result<()> { #[tokio::test] async fn outgoing_body() -> Result<()> { - use viz_core::Body; + use viz::Body; let mut empty = OutgoingBody::::Empty; assert!(empty.is_end_stream()); diff --git a/viz-core/tests/type_payload.rs b/viz-test/tests/payload.rs similarity index 97% rename from viz-core/tests/type_payload.rs rename to viz-test/tests/payload.rs index b8db5383..2161abd4 100644 --- a/viz-core/tests/type_payload.rs +++ b/viz-test/tests/payload.rs @@ -1,5 +1,5 @@ use std::collections::HashMap; -use viz_core::{types, Error, Request, RequestExt, Response, ResponseExt, Result}; +use viz::{types, Error, Request, RequestExt, Response, ResponseExt, Result}; #[tokio::test] async fn payload() -> Result<()> { diff --git a/viz-test/tests/request.rs b/viz-test/tests/request.rs new file mode 100644 index 00000000..4c80d193 --- /dev/null +++ b/viz-test/tests/request.rs @@ -0,0 +1,303 @@ +use std::collections::{BTreeMap, HashMap}; + +use headers::{authorization::Bearer, Authorization}; +use serde::{Deserialize, Serialize}; +use viz::{ + // TODO: reqwest and hyper haven't used the same version of `http`. + // header::{AUTHORIZATION, CONTENT_TYPE, COOKIE, SET_COOKIE}, + // StatusCode, + types::{self}, + Error, + IntoResponse, + Request, + RequestExt, + Response, + ResponseExt, + Result, +}; + +#[derive(Debug, Deserialize, Serialize, PartialEq)] +struct Page { + p: u8, +} + +#[tokio::test] +async fn request_body() -> Result<()> { + use futures_util::stream::TryStreamExt; + use viz::{ + middleware::{cookie, limits}, + Router, + }; + use viz_test::http::{ + header::{AUTHORIZATION, COOKIE}, + StatusCode, + }; + use viz_test::TestServer; + + let router = Router::new() + .get("/:id", |req: Request| async move { + let id = req.param::("id")?; + Ok(id) + }) + .get("/:username/:repo", |req: Request| async move { + let (username, repo): (String, String) = req.params()?; + Ok(format!("{username}/{repo}")) + }) + .get("/extract-token", |mut req: Request| async move { + let header: types::Header> = req.extract().await?; + Ok(header.into_inner().token().to_string()) + }) + .post("/extract-body", |mut req: Request| async move { + let form: types::Form> = req.extract().await?; + Ok(Response::json(form.into_inner())) + }) + .get("/cookies", |req: Request| async move { + let cookies = req.cookies()?; + let jar = cookies + .jar() + .lock() + .map_err(|e| Error::Responder(e.to_string().into_response()))?; + Ok(jar.iter().count().to_string()) + }) + .get("/cookie", |req: Request| async move { + Ok(req.cookie("viz").unwrap().value().to_string()) + }) + .with(cookie::Config::default()) + .post("/bytes", |mut req: Request| async move { + let data = req.bytes().await?; + Ok(data) + }) + .post("/bytes-with-limit", |mut req: Request| async move { + let data = req.bytes_with(None, 4).await?; + Ok(data) + }) + .post("/bytes-used", |mut req: Request| async move { + req.bytes().await?; + let data = req.bytes().await?; + Ok(data) + }) + .post("/text", |mut req: Request| async move { + let data = req.text().await?; + Ok(Response::text(data)) + }) + .post("/json", |mut req: Request| async move { + let data = req.json::().await?; + Ok(Response::json(data)) + }) + .post("/form", |mut req: Request| async move { + let data = req.form::>().await?; + Ok(Response::json(data)) + }) + .post("/multipart", |mut req: Request| async move { + let mut multipart = req.multipart().await?; + let mut data = HashMap::new(); + + while let Some(mut field) = multipart.try_next().await? { + let buf = field.bytes().await?.to_vec(); + data.insert(field.name, String::from_utf8(buf).map_err(Error::normal)?); + } + + Ok(Response::json(data)) + }) + .with(limits::Config::new().limits(types::Limits::new())); + + let client = TestServer::new(router).await?; + + let resp = client.get("/7").send().await.map_err(Error::normal)?; + assert_eq!(resp.text().await.map_err(Error::normal)?, "7"); + + let resp = client + .get("/viz-rs/viz") + .send() + .await + .map_err(Error::normal)?; + assert_eq!(resp.text().await.map_err(Error::normal)?, "viz-rs/viz"); + + let resp = client + .get("/extract-token") + .header(AUTHORIZATION, "Bearer viz.rs") + .send() + .await + .map_err(Error::normal)?; + assert_eq!(resp.text().await.map_err(Error::normal)?, "viz.rs"); + + let mut form = BTreeMap::new(); + form.insert("password", "rs"); + form.insert("username", "viz"); + let resp = client + .post("/extract-body") + .form(&form) + .send() + .await + .map_err(Error::normal)?; + assert_eq!( + resp.text().await.map_err(Error::normal)?, + r#"{"password":"rs","username":"viz"}"# + ); + + let resp = client + .get("/cookie") + .header(COOKIE, "viz=crate") + .send() + .await + .map_err(Error::normal)?; + assert_eq!(resp.text().await.map_err(Error::normal)?, "crate"); + + let resp = client + .get("/cookies") + .header(COOKIE, "auth=true;dark=false") + .send() + .await + .map_err(Error::normal)?; + assert_eq!(resp.text().await.map_err(Error::normal)?, "2"); + + let resp = client + .post("/bytes") + .body("bytes") + .send() + .await + .map_err(Error::normal)?; + assert_eq!(resp.text().await.map_err(Error::normal)?, "bytes"); + + let resp = client + .post("/bytes-with-limit") + .body("rust") + .send() + .await + .map_err(Error::normal)?; + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!(resp.text().await.map_err(Error::normal)?, "rust"); + + let resp = client + .post("/bytes-with-limit") + .body("crate") + .send() + .await + .map_err(Error::normal)?; + assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE); + assert_eq!( + resp.text().await.map_err(Error::normal)?, + "payload is too large" + ); + + let resp = client + .post("/bytes-used") + .body("used") + .send() + .await + .map_err(Error::normal)?; + assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); + assert_eq!( + resp.text().await.map_err(Error::normal)?, + "payload has been used" + ); + + let resp = client + .post("/text") + .body("text") + .send() + .await + .map_err(Error::normal)?; + assert_eq!(resp.text().await.map_err(Error::normal)?, "text"); + + let resp = client + .post("/json") + .json(&Page { p: 1 }) + .send() + .await + .map_err(Error::normal)?; + assert_eq!( + resp.json::().await.map_err(Error::normal)?, + Page { p: 1 } + ); + + let mut form = HashMap::new(); + form.insert("username", "viz"); + form.insert("password", "rs"); + let resp = client + .post("/form") + .form(&form) + .send() + .await + .map_err(Error::normal)?; + assert_eq!( + resp.json::>() + .await + .map_err(Error::normal)?, + { + let mut form = HashMap::new(); + form.insert("username".to_string(), "viz".to_string()); + form.insert("password".to_string(), "rs".to_string()); + form + } + ); + + let form = viz_test::multipart::Form::new() + .text("key3", "3") + .text("key4", "4"); + let resp = client + .post("/multipart") + .multipart(form) + .send() + .await + .map_err(Error::normal)?; + assert_eq!( + resp.json::>() + .await + .map_err(Error::normal)?, + { + let mut form = HashMap::new(); + form.insert("key3".to_string(), "3".to_string()); + form.insert("key4".to_string(), "4".to_string()); + form + } + ); + + Ok(()) +} + +#[tokio::test] +async fn request_session() -> Result<()> { + use viz::{ + middleware::{cookie, helper::CookieOptions, session}, + Router, + }; + use viz_test::http::header::{COOKIE, SET_COOKIE}; + use viz_test::{nano_id, sessions, TestServer}; + + let router = Router::new() + .post("/session/set", |req: Request| async move { + let counter = req.session().get::("counter")?.unwrap_or_default() + 1; + req.session().set("counter", counter)?; + Ok(counter.to_string()) + }) + .with(session::Config::new( + session::Store::new( + sessions::MemoryStorage::new(), + nano_id::base64::<32>, + |sid: &str| sid.len() == 32, + ), + CookieOptions::default(), + )) + .with(cookie::Config::default()); + + let client = TestServer::new(router).await?; + + let resp = client + .post("/session/set") + .send() + .await + .map_err(Error::normal)?; + let cookie = resp.headers().get(SET_COOKIE).cloned().unwrap(); + assert_eq!(resp.text().await.map_err(Error::normal)?, "1"); + + let resp = client + .post("/session/set") + .header(COOKIE, cookie) + .send() + .await + .map_err(Error::normal)?; + assert_eq!(resp.text().await.map_err(Error::normal)?, "2"); + + Ok(()) +} diff --git a/viz-test/tests/response.rs b/viz-test/tests/response.rs new file mode 100644 index 00000000..0a67b9e8 --- /dev/null +++ b/viz-test/tests/response.rs @@ -0,0 +1,152 @@ +use futures_util::{stream, Stream, StreamExt}; +use headers::{ContentDisposition, ContentType, HeaderMapExt}; +use http_body_util::{BodyExt, Full}; +use hyper::body::Body; +use serde::{Deserialize, Serialize}; +use viz::{ + header::{CONTENT_DISPOSITION, CONTENT_LOCATION, LOCATION}, + Error, OutgoingBody, Response, ResponseExt, Result, StatusCode, +}; + +#[derive(Debug, Deserialize, Serialize, PartialEq)] +struct Page { + p: u8, +} + +#[tokio::test] +async fn response_ext() -> Result<()> { + let resp = Response::with(Full::new("".into()), mime::TEXT_XML.as_ref()); + assert!(resp.ok()); + assert!(resp.content_length().is_none()); + let content_type = resp.headers().typed_get::(); + assert_eq!( + Into::::into(content_type.unwrap()), + mime::TEXT_XML + ); + + let body: OutgoingBody = resp.into_body(); + assert_eq!(Body::size_hint(&body).exact(), Some(b"".len() as u64)); + assert_eq!( + BodyExt::collect(body).await.unwrap().to_bytes().to_vec(), + b"" + ); + + let mut resp = Response::text(""); + *resp.status_mut() = StatusCode::NOT_FOUND; + assert!(!resp.ok()); + let content_type = resp.headers().typed_get::(); + assert_eq!( + Into::::into(content_type.unwrap()), + mime::TEXT_PLAIN_UTF_8 + ); + let mut body: OutgoingBody = resp.into_body(); + assert_eq!(Body::size_hint(&body).exact(), Some(0)); + assert!(body.frame().await.is_none()); + assert!(body.is_end_stream()); + + let resp = Response::html(""); + assert!(resp.ok()); + let content_type = resp.headers().typed_get::(); + assert_eq!( + Into::::into(content_type.unwrap()), + mime::TEXT_HTML_UTF_8 + ); + let mut body: OutgoingBody = resp.into_body(); + assert_eq!(Body::size_hint(&body).exact(), Some(7)); + assert_eq!( + body.frame().await.unwrap().unwrap().into_data().unwrap(), + "" + ); + assert!(body.is_end_stream()); + + let resp = Response::json(Page { p: 255 })?; + assert!(resp.ok()); + let content_type = resp.headers().typed_get::(); + assert_eq!( + Into::::into(content_type.unwrap()), + mime::APPLICATION_JSON + ); + + let resp = Response::stream(stream::repeat("viz").take(2).map(Result::<_, Error>::Ok)); + assert!(resp.ok()); + let body: OutgoingBody = resp.into_body(); + assert_eq!(Stream::size_hint(&body), (0, None)); + let (item, stream) = body.into_future().await; + assert_eq!(item.unwrap().unwrap().to_vec(), b"viz"); + let (item, stream) = stream.into_future().await; + assert_eq!(item.unwrap().unwrap().to_vec(), b"viz"); + let (item, _) = stream.into_future().await; + assert!(item.is_none()); + + let resp = Response::attachment("inline"); + let content_disposition = resp.headers().typed_get::().unwrap(); + assert!(content_disposition.is_inline()); + + let resp = Response::attachment("attachment"); + let content_disposition = resp.headers().typed_get::().unwrap(); + assert!(content_disposition.is_attachment()); + + let resp = Response::attachment(r#"attachment; filename="filename.jpg""#); + let content_disposition = resp.headers().get(CONTENT_DISPOSITION).unwrap(); + assert_eq!( + content_disposition, + r#"attachment; filename="filename.jpg""# + ); + + let resp = Response::location("/login"); + let location = resp.headers().get(CONTENT_LOCATION).unwrap(); + assert_eq!(location, "/login"); + + let resp = Response::redirect("/oauth"); + let location = resp.headers().get(LOCATION).unwrap(); + assert_eq!(location, "/oauth"); + + let resp = Response::redirect_with_status("/oauth", StatusCode::TEMPORARY_REDIRECT); + let location = resp.headers().get(LOCATION).unwrap(); + assert_eq!(location, "/oauth"); + assert_eq!(resp.status(), 307); + + let resp = Response::see_other("/oauth"); + assert_eq!(resp.status(), 303); + + let resp = Response::temporary("/oauth"); + assert_eq!(resp.status(), 307); + + let resp = Response::permanent("/oauth"); + assert_eq!(resp.status(), 308); + + Ok(()) +} + +#[test] +#[should_panic(expected = "not a redirection status code")] +fn response_ext_panic() { + Response::redirect_with_status("/oauth", StatusCode::OK); +} + +#[tokio::test] +async fn response_ext_with_server() -> Result<()> { + use viz::{Request, Router}; + use viz_test::TestServer; + + let router = Router::new() + .get("/", |_: Request| async move { Ok("") }) + .post("/", |_: Request| async move { + Ok(Response::with( + Full::new("".into()), + mime::TEXT_XML.as_ref(), + )) + }); + + let client = TestServer::new(router).await?; + + let resp = client.get("/").send().await.map_err(Error::normal)?; + assert_eq!(resp.content_length(), Some(0)); + assert_eq!(resp.text().await.map_err(Error::normal)?, ""); + + let resp = client.post("/").send().await.map_err(Error::normal)?; + assert_eq!(resp.content_length(), Some(6)); + assert_eq!(resp.text().await.map_err(Error::normal)?, ""); + + Ok(()) +} diff --git a/viz/Cargo.toml b/viz/Cargo.toml index 0421a38f..e1aaf52b 100644 --- a/viz/Cargo.toml +++ b/viz/Cargo.toml @@ -87,4 +87,5 @@ tokio = { workspace = true, features = ["macros", "rt", "rt-multi-thread"] } all-features = true rustdoc-args = ["--cfg", "docsrs"] -[package.metadata.playground] +[lints] +workspace = true diff --git a/viz/README.md b/viz/README.md index e697804e..7c29ea59 100644 --- a/viz/README.md +++ b/viz/README.md @@ -37,10 +37,6 @@ alt="Discord"> -> **Note**: viz's [main](https://github.com/viz-rs/viz) branch is -> currently preparing breaking changes. For the most recently *released* code, -> look to the [0.4.x branch](https://github.com/viz-rs/viz/tree/0.4.x). - ## Features - **Safety** `#![forbid(unsafe_code)]` diff --git a/viz/src/lib.rs b/viz/src/lib.rs index 2e7d912d..3ec2b174 100644 --- a/viz/src/lib.rs +++ b/viz/src/lib.rs @@ -514,13 +514,6 @@ #![doc(html_logo_url = "https://viz.rs/logo.svg")] #![doc(html_favicon_url = "https://viz.rs/logo.svg")] -#![forbid(unsafe_code)] -#![warn( - missing_debug_implementations, - missing_docs, - rust_2018_idioms, - unreachable_pub -)] #![doc(test( no_crate_inject, attr( @@ -538,7 +531,7 @@ pub use responder::Responder; #[cfg(any(feature = "http1", feature = "http2"))] mod serve; #[cfg(any(feature = "http1", feature = "http2"))] -pub use serve::serve; +pub use serve::{serve, serve_with_upgrades}; /// TLS pub mod tls; diff --git a/viz/src/serve.rs b/viz/src/serve.rs index 99f655e7..0d9af2d3 100644 --- a/viz/src/serve.rs +++ b/viz/src/serve.rs @@ -14,6 +14,25 @@ use crate::Responder; /// /// Will return `Err` if the connection does not be served. pub async fn serve(stream: I, tree: Arc, addr: Option) -> Result<()> +where + I: AsyncRead + AsyncWrite + Unpin + Send + 'static, +{ + Builder::new(TokioExecutor::new()) + .serve_connection(Io::new(stream), Responder::new(tree, addr)) + .await + .map_err(Into::into) +} + +/// Serve the connections with upgrades. +/// +/// # Errors +/// +/// Will return `Err` if the connection does not be served. +pub async fn serve_with_upgrades( + stream: I, + tree: Arc, + addr: Option, +) -> Result<()> where I: AsyncRead + AsyncWrite + Unpin + Send + 'static, { diff --git a/viz/src/tls.rs b/viz/src/tls.rs index 10479993..7907ad36 100644 --- a/viz/src/tls.rs +++ b/viz/src/tls.rs @@ -1,5 +1,3 @@ -#![allow(clippy::module_name_repetitions)] - mod listener; pub use listener::Listener;