diff --git a/Cargo.toml b/Cargo.toml index 86afce55ef..ebdc7fbadf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,16 +20,16 @@ repository = "https://github.com/poem-web/poem" rust-version = "1.75" [workspace.dependencies] -poem = { path = "poem", version = "3.0.2", default-features = false } -poem-derive = { path = "poem-derive", version = "3.0.2" } +poem = { path = "poem", version = "3.0.4", default-features = false } +poem-derive = { path = "poem-derive", version = "3.0.4" } poem-openapi-derive = { path = "poem-openapi-derive", version = "5.0.3" } -poem-grpc-build = { path = "poem-grpc-build", version = "0.4.0" } +poem-grpc-build = { path = "poem-grpc-build", version = "0.4.2" } proc-macro-crate = "3.0.0" proc-macro2 = "1.0.29" quote = "1.0.9" syn = { version = "2.0" } -tokio = "1.17.0" +tokio = "1.39.1" serde_json = "1.0.68" serde = { version = "1.0.130", features = ["derive"] } thiserror = "1.0.30" @@ -41,7 +41,7 @@ bytes = "1.1.0" futures-util = "0.3.17" tokio-stream = "0.1.8" serde_yaml = "0.9" -quick-xml = { version = "0.32.0", features = ["serialize"] } +quick-xml = { version = "0.36.1", features = ["serialize"] } base64 = "0.22.0" serde_urlencoded = "0.7.1" indexmap = "2.0.0" diff --git a/examples/poem/redis-session/Cargo.toml b/examples/poem/redis-session/Cargo.toml index a188a4440e..98ba74ce2e 100644 --- a/examples/poem/redis-session/Cargo.toml +++ b/examples/poem/redis-session/Cargo.toml @@ -8,4 +8,8 @@ publish.workspace = true poem = { workspace = true, features = ["redis-session"] } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } tracing-subscriber.workspace = true -redis = { version = "0.25.2", features = ["aio", "tokio-comp", "connection-manager"] } +redis = { version = "0.26.0", features = [ + "aio", + "tokio-comp", + "connection-manager", +] } diff --git a/poem-derive/Cargo.toml b/poem-derive/Cargo.toml index c9a2408eaa..3e713e854b 100644 --- a/poem-derive/Cargo.toml +++ b/poem-derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "poem-derive" -version = "3.0.2" +version = "3.0.4" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/poem-derive/src/lib.rs b/poem-derive/src/lib.rs index ed12849991..fb2a16644e 100644 --- a/poem-derive/src/lib.rs +++ b/poem-derive/src/lib.rs @@ -59,7 +59,7 @@ fn generate_handler(internal: bool, input: TokenStream) -> Result { }; let def_struct = if !item_fn.sig.generics.params.is_empty() { - let members = item_fn + let iter = item_fn .sig .generics .params @@ -70,13 +70,26 @@ fn generate_handler(internal: bool, input: TokenStream) -> Result { }) .enumerate() .map(|(idx, ty)| { - let ty_ident = &ty.ident; let ident = format_ident!("_mark{}", idx); - quote! { #ident: ::std::marker::PhantomData<#ty_ident> } + let ty_ident = &ty.ident; + (ident, ty_ident) }); + + let struct_members = iter.clone().map(|(ident, ty_ident)| { + quote! { #ident: ::std::marker::PhantomData<#ty_ident> } + }); + + let default_members = iter.clone().map(|(ident, _ty_ident)| { + quote! { #ident: ::std::marker::PhantomData } + }); + quote! { - #[derive(Default)] - #vis struct #ident #type_generics { #(#members),*} + #vis struct #ident #type_generics { #(#struct_members),*} + impl #type_generics ::std::default::Default for #ident #type_generics { + fn default() -> Self { + Self { #(#default_members),* } + } + } } } else { quote! { #vis struct #ident; } diff --git a/poem-grpc/Cargo.toml b/poem-grpc/Cargo.toml index 8b4675c996..0efd641ae7 100644 --- a/poem-grpc/Cargo.toml +++ b/poem-grpc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "poem-grpc" -version = "0.4.2" +version = "0.4.3" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/poem-lambda/Cargo.toml b/poem-lambda/Cargo.toml index 2ea3f62aeb..d9c227e1e1 100644 --- a/poem-lambda/Cargo.toml +++ b/poem-lambda/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "poem-lambda" -version = "5.0.0" +version = "5.0.1" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/poem-openapi/CHANGELOG.md b/poem-openapi/CHANGELOG.md index 94fd01bdfd..9244d0a61b 100644 --- a/poem-openapi/CHANGELOG.md +++ b/poem-openapi/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +# [5.0.3] 2024-07-27 + +- Added derivations for Type, ParseFromJSON and ToJSON for sqlx types [#833](https://github.com/poem-web/poem/pull/833) + # [5.0.1] 2024-05-18 - Add enum_items to discriminated union [#741](https://github.com/poem-web/poem/pull/741) diff --git a/poem-openapi/Cargo.toml b/poem-openapi/Cargo.toml index f0af31b95e..be20f802eb 100644 --- a/poem-openapi/Cargo.toml +++ b/poem-openapi/Cargo.toml @@ -59,7 +59,7 @@ hostname-validator = { version = "1.1.0", optional = true } chrono = { workspace = true, optional = true, default-features = false, features = [ "clock", ] } -time = { version = "0.3.9", optional = true, features = [ +time = { version = "0.3.36", optional = true, features = [ "parsing", "formatting", ] } @@ -72,6 +72,12 @@ ipnet = { version = "2.7.1", optional = true } prost-wkt-types = { version = "0.5.0", optional = true } geo-types = { version = "0.7.12", optional = true } geojson = { version = "0.24.1", features = ["geo-types"], optional = true } +sqlx = { version = "0.7.4", features = [ + "json", + "postgres", + "sqlite", + "mysql", +], optional = true } [dev-dependencies] tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } diff --git a/poem-openapi/src/types/external/mod.rs b/poem-openapi/src/types/external/mod.rs index b385f08fa5..d0c41bdad5 100644 --- a/poem-openapi/src/types/external/mod.rs +++ b/poem-openapi/src/types/external/mod.rs @@ -25,6 +25,8 @@ mod optional; mod prost_wkt_types; mod regex; mod slice; +#[cfg(feature = "sqlx")] +mod sqlx; mod string; #[cfg(feature = "time")] mod time; diff --git a/poem-openapi/src/types/external/sqlx.rs b/poem-openapi/src/types/external/sqlx.rs new file mode 100644 index 0000000000..ddcf51b8b1 --- /dev/null +++ b/poem-openapi/src/types/external/sqlx.rs @@ -0,0 +1,48 @@ +use std::borrow::Cow; + +use serde_json::Value; + +use crate::{ + registry::MetaSchemaRef, + types::{ParseError, ParseFromJSON, ParseResult, ToJSON, Type}, +}; + +impl Type for sqlx::types::Json { + const IS_REQUIRED: bool = Self::RawValueType::IS_REQUIRED; + + type RawValueType = T; + + type RawElementValueType = T::RawElementValueType; + + fn name() -> Cow<'static, str> { + Self::RawValueType::name() + } + + fn schema_ref() -> MetaSchemaRef { + Self::RawValueType::schema_ref() + } + + fn as_raw_value(&self) -> Option<&Self::RawValueType> { + Some(&self.0) + } + + fn raw_element_iter<'a>( + &'a self, + ) -> Box + 'a> { + self.0.raw_element_iter() + } +} + +impl ParseFromJSON for sqlx::types::Json { + fn parse_from_json(value: Option) -> ParseResult { + Self::RawValueType::parse_from_json(value) + .map(sqlx::types::Json) + .map_err(ParseError::propagate) + } +} + +impl ToJSON for sqlx::types::Json { + fn to_json(&self) -> Option { + self.0.to_json() + } +} diff --git a/poem-openapi/src/types/multipart/upload.rs b/poem-openapi/src/types/multipart/upload.rs index 3a4baa918f..2a3e5774fe 100644 --- a/poem-openapi/src/types/multipart/upload.rs +++ b/poem-openapi/src/types/multipart/upload.rs @@ -6,7 +6,7 @@ use std::{ use poem::web::Field as PoemField; use tokio::{ fs::File, - io::{AsyncRead, AsyncReadExt, Error as IoError, ErrorKind}, + io::{AsyncRead, AsyncReadExt, AsyncSeek, Error as IoError, ErrorKind}, }; use crate::{ @@ -74,7 +74,7 @@ impl Upload { } /// Consumes this body object to return a reader. - pub fn into_async_read(self) -> impl AsyncRead + Unpin + Send + 'static { + pub fn into_async_read(self) -> impl AsyncRead + AsyncSeek + Unpin + Send + 'static { self.file } diff --git a/poem-openapi/tests/multipart.rs b/poem-openapi/tests/multipart.rs index 5a1e9009cf..0b61756e8e 100644 --- a/poem-openapi/tests/multipart.rs +++ b/poem-openapi/tests/multipart.rs @@ -10,6 +10,7 @@ use poem_openapi::{ }, Enum, Multipart, Object, }; +use tokio::io::{AsyncReadExt, AsyncSeekExt}; fn create_multipart_payload(parts: &[(&str, Option<&str>, &[u8])]) -> Vec { let mut data = Vec::new(); @@ -237,6 +238,34 @@ async fn upload() { assert_eq!(a.file.content_type(), None); assert_eq!(a.file.size(), 3); assert_eq!(a.file.into_vec().await.unwrap(), vec![1, 2, 3]); + + let data = + create_multipart_payload(&[("name", None, b"abc"), ("file", Some("1.txt"), &[1, 2, 3])]); + let a = A::from_request( + &Request::builder() + .header("content-type", "multipart/form-data; boundary=X-BOUNDARY") + .finish(), + &mut RequestBody::new(data.into()), + ) + .await + .unwrap(); + assert_eq!(a.name, "abc".to_string()); + + assert_eq!(a.file.file_name(), Some("1.txt")); + assert_eq!(a.file.content_type(), None); + assert_eq!(a.file.size(), 3); + + let mut reader = a.file.into_async_read(); + let mut buffer = [0; 3]; + let n = reader.read_exact(&mut buffer[..]).await.unwrap(); + assert_eq!(n, 3); + assert_eq!(buffer[..n], vec![1, 2, 3]); + let n = reader.read(&mut buffer[..]).await.unwrap(); + assert_eq!(n, 0); // EOF + reader.seek(std::io::SeekFrom::Start(0)).await.unwrap(); + let n = reader.read_exact(&mut buffer[..]).await.unwrap(); + assert_eq!(n, 3); + assert_eq!(buffer[..n], vec![1, 2, 3]); } #[tokio::test] diff --git a/poem-openapi/tests/object.rs b/poem-openapi/tests/object.rs index 1aabe737e0..3945b07f3e 100644 --- a/poem-openapi/tests/object.rs +++ b/poem-openapi/tests/object.rs @@ -4,7 +4,6 @@ use poem_openapi::{ Enum, NewType, Object, OpenApi, }; use serde_json::json; -use time::OffsetDateTime; fn get_meta() -> MetaSchema { let mut registry = Registry::new(); @@ -390,8 +389,10 @@ fn read_only() { ); } +#[cfg(feature = "time")] #[test] fn read_only_with_default() { + use time::OffsetDateTime; fn default_offset_datetime() -> OffsetDateTime { OffsetDateTime::from_unix_timestamp(1694045893).unwrap() } diff --git a/poem/CHANGELOG.md b/poem/CHANGELOG.md index 9563ac645a..23b3c48619 100644 --- a/poem/CHANGELOG.md +++ b/poem/CHANGELOG.md @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +# [3.0.4] 2024-07-27 + +- Add manual Default implementation for `#[handler]` [#848](https://github.com/poem-web/poem/pull/848) +- feat: add `AsyncSeek trait` to `Upload::into_async_read` return type [#853](https://github.com/poem-web/poem/pull/853) +- Fix `EmbeddedFilesEndpoint` not working for `index.html` in subdirectories [#825](https://github.com/poem-web/poem/pull/825) +- chore: Bump `redis` to `0.26` [#856](https://github.com/poem-web/poem/pull/856) +- chore: bump `tokio-tungstenite`, `quick-xml`, `tokio`, `openssl` [#857](https://github.com/poem-web/poem/pull/857) + # [3.0.3] 2024-07-20 - bump `opentelemetry` from `0.23` to `0.24` diff --git a/poem/Cargo.toml b/poem/Cargo.toml index 7f4a799e89..7c07cc4fd4 100644 --- a/poem/Cargo.toml +++ b/poem/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "poem" -version = "3.0.3" +version = "3.0.4" authors.workspace = true edition.workspace = true license.workspace = true @@ -97,7 +97,7 @@ sync_wrapper = { version = "1.0.0", features = ["futures"] } # Non-feature optional dependencies multer = { version = "3.0.0", features = ["tokio"], optional = true } -tokio-tungstenite = { version = "0.21.0", optional = true } +tokio-tungstenite = { version = "0.23.1", optional = true } tokio-rustls = { workspace = true, optional = true } rustls-pemfile = { version = "2.0.0", optional = true } async-compression = { version = "0.4.0", optional = true, features = [ @@ -116,7 +116,7 @@ chrono = { workspace = true, optional = true, default-features = false, features time = { version = "0.3", optional = true } mime_guess = { version = "2.0.3", optional = true } rand = { version = "0.8.4", optional = true } -redis = { version = "0.25.2", optional = true, features = [ +redis = { version = "0.26.0", optional = true, features = [ "aio", "tokio-comp", "connection-manager", @@ -139,7 +139,7 @@ libtempfile = { package = "tempfile", version = "3.2.0", optional = true } priority-queue = { version = "2.0.2", optional = true } tokio-native-tls = { version = "0.3.0", optional = true } tokio-openssl = { version = "0.6.3", optional = true } -openssl = { version = "0.10.56", optional = true } +openssl = { version = "0.10.66", optional = true } base64 = { workspace = true, optional = true } libcsrf = { package = "csrf", version = "0.4.1", optional = true } httpdate = { version = "1.0.2", optional = true } @@ -168,7 +168,7 @@ uuid = { version = "1.8.0", optional = true, default-features = false, features ] } [target.'cfg(unix)'.dependencies] -nix = { version = "0.28.0", features = ["fs", "user"] } +nix = { version = "0.29.0", features = ["fs", "user"] } [dev-dependencies] async-stream = "0.3.2" diff --git a/poem/src/endpoint/embed.rs b/poem/src/endpoint/embed.rs index d07aa8544f..c7165cb63d 100644 --- a/poem/src/endpoint/embed.rs +++ b/poem/src/endpoint/embed.rs @@ -83,16 +83,29 @@ impl Endpoint for EmbeddedFilesEndpoint { type Output = Response; async fn call(&self, req: Request) -> Result { - let mut path = req - .uri() - .path() - .trim_start_matches('/') - .trim_end_matches('/') - .to_string(); - if path.is_empty() { - path = "index.html".to_string(); + let path = req.uri().path().trim_start_matches('/'); + let original_path = req.original_uri().path(); + let original_end_with_slash = original_path.ends_with('/'); + + use header::LOCATION; + + if path.is_empty() && !original_end_with_slash { + Ok(Response::builder() + .status(StatusCode::FOUND) + .header(LOCATION, format!("{}/", original_path)) + .finish()) + } else if original_end_with_slash { + let path = format!("{}index.html", path); + EmbeddedFileEndpoint::::new(&path).call(req).await + } else if E::get(path).is_some() { + EmbeddedFileEndpoint::::new(path).call(req).await + } else if E::get(&format!("{}/index.html", path)).is_some() { + Ok(Response::builder() + .status(StatusCode::FOUND) + .header(LOCATION, format!("{}/", original_path)) + .finish()) + } else { + EmbeddedFileEndpoint::::new(path).call(req).await } - let path = path.as_ref(); - EmbeddedFileEndpoint::::new(path).call(req).await } } diff --git a/poem/src/error.rs b/poem/src/error.rs index 143449ba3e..b55746208d 100644 --- a/poem/src/error.rs +++ b/poem/src/error.rs @@ -1176,11 +1176,11 @@ mod tests { assert_eq!(err.into_response().status(), StatusCode::BAD_GATEWAY); } - #[cfg(feature = "eyre6")] + #[cfg(feature = "eyre06")] #[test] - fn test_eyre6_error() { - let eyre6_err: eyre6::Error = IoError::new(ErrorKind::AlreadyExists, "aaa").into(); - let err: Error = Error::from((StatusCode::BAD_GATEWAY, eyre6_err)); + fn test_eyre06_error() { + let eyre06_err: eyre06::Error = IoError::new(ErrorKind::AlreadyExists, "aaa").into(); + let err: Error = Error::from((StatusCode::BAD_GATEWAY, eyre06_err)); assert!(err.is::()); assert_eq!( err.downcast_ref::().unwrap().kind(), diff --git a/poem/src/session/redis_storage.rs b/poem/src/session/redis_storage.rs index ab80962e7c..341cebf95b 100644 --- a/poem/src/session/redis_storage.rs +++ b/poem/src/session/redis_storage.rs @@ -52,7 +52,7 @@ impl SessionStorage for RedisStorage Some(expires) => Cmd::set_ex(session_id, value, expires.as_secs()), None => Cmd::set(session_id, value), }; - cmd.query_async(&mut self.connection.clone()) + cmd.query_async::<()>(&mut self.connection.clone()) .await .map_err(RedisSessionError::Redis)?; Ok(()) @@ -60,7 +60,7 @@ impl SessionStorage for RedisStorage async fn remove_session<'a>(&'a self, session_id: &'a str) -> Result<()> { Cmd::del(session_id) - .query_async(&mut self.connection.clone()) + .query_async::<()>(&mut self.connection.clone()) .await .map_err(RedisSessionError::Redis)?; Ok(()) diff --git a/poem/src/web/websocket/mod.rs b/poem/src/web/websocket/mod.rs index 1d208d9586..81a0efe6f3 100644 --- a/poem/src/web/websocket/mod.rs +++ b/poem/src/web/websocket/mod.rs @@ -92,7 +92,6 @@ mod tests { check(addr, "aaa", Some(&HeaderValue::from_static("aaa"))).await; check(addr, "bbb", Some(&HeaderValue::from_static("bbb"))).await; - check(addr, "ccc", None).await; handle.abort(); }