diff --git a/Cargo.toml b/Cargo.toml index 0b8aea4d6b..4419a92b4a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ tokio = "1.39.1" serde_json = "1.0.68" sonic-rs = "0.3.5" serde = { version = "1.0.130", features = ["derive"] } -thiserror = "1.0.30" +thiserror = "2.0" regex = "1.5.5" mime = "0.3.16" tracing = "0.1.36" diff --git a/examples/grpc/helloworld_typename/Cargo.toml b/examples/grpc/helloworld_typename/Cargo.toml new file mode 100644 index 0000000000..31a2d49456 --- /dev/null +++ b/examples/grpc/helloworld_typename/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "example-grpc-helloworld-typename" +version.workspace = true +edition.workspace = true +publish.workspace = true + +[dependencies] +poem.workspace = true +poem-grpc.workspace = true +prost.workspace = true +tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } + +[build-dependencies] +poem-grpc-build.workspace = true diff --git a/examples/grpc/helloworld_typename/build.rs b/examples/grpc/helloworld_typename/build.rs new file mode 100644 index 0000000000..11facf0a09 --- /dev/null +++ b/examples/grpc/helloworld_typename/build.rs @@ -0,0 +1,9 @@ +use std::io::Result; + +use poem_grpc_build::Config; + +fn main() -> Result<()> { + Config::new() + .enable_type_names() + .compile(&["./proto/helloworld.proto"], &["./proto"]) +} diff --git a/examples/grpc/helloworld_typename/proto/helloworld.proto b/examples/grpc/helloworld_typename/proto/helloworld.proto new file mode 100644 index 0000000000..8de5d08ef4 --- /dev/null +++ b/examples/grpc/helloworld_typename/proto/helloworld.proto @@ -0,0 +1,37 @@ +// Copyright 2015 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +option java_multiple_files = true; +option java_package = "io.grpc.examples.helloworld"; +option java_outer_classname = "HelloWorldProto"; + +package helloworld; + +// The greeting service definition. +service Greeter { + // Sends a greeting + rpc SayHello (HelloRequest) returns (HelloReply) {} +} + +// The request message containing the user's name. +message HelloRequest { + string name = 1; +} + +// The response message containing the greetings +message HelloReply { + string message = 1; +} \ No newline at end of file diff --git a/examples/grpc/helloworld_typename/src/main.rs b/examples/grpc/helloworld_typename/src/main.rs new file mode 100644 index 0000000000..ef0ff77609 --- /dev/null +++ b/examples/grpc/helloworld_typename/src/main.rs @@ -0,0 +1,18 @@ +use prost::Name; + +poem_grpc::include_proto!("helloworld"); + +fn main() -> Result<(), std::io::Error> { + println!( + "HelloRequest has {} full name and {} type url", + HelloRequest::full_name(), + HelloRequest::type_url() + ); + println!( + "HelloReply has {} full name and {} type url", + HelloReply::full_name(), + HelloReply::type_url() + ); + + Ok(()) +} diff --git a/examples/poem/custom-error/Cargo.toml b/examples/poem/custom-error/Cargo.toml index 6ed5de8aeb..dfb1b71a2d 100644 --- a/examples/poem/custom-error/Cargo.toml +++ b/examples/poem/custom-error/Cargo.toml @@ -8,4 +8,4 @@ publish.workspace = true poem.workspace = true tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } tracing-subscriber.workspace = true -thiserror = "1.0.30" +thiserror = "2.0" diff --git a/poem-grpc-build/src/config.rs b/poem-grpc-build/src/config.rs index ab60cc0e91..505d113d74 100644 --- a/poem-grpc-build/src/config.rs +++ b/poem-grpc-build/src/config.rs @@ -286,6 +286,12 @@ impl Config { self } + /// Enable auto implementation of the `prost::Name` trait + pub fn enable_type_names(mut self) -> Self { + self.prost_config.enable_type_names(); + self + } + /// When set, the `FileDescriptorSet` generated by `protoc` is written to /// the provided filesystem path. pub fn file_descriptor_set_path(mut self, path: impl AsRef) -> Self { diff --git a/poem-grpc/CHANGELOG.md b/poem-grpc/CHANGELOG.md index 816bd41307..065201d5c5 100644 --- a/poem-grpc/CHANGELOG.md +++ b/poem-grpc/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). +# [0.5.3] 2024-01-04 + +- feat: Implement enable_type_name config method [#924](https://github.com/poem-web/poem/pull/924) + # [0.5.2] 2024-11-20 - Add `ClientConfigBuilder::http2_max_header_list_size` method to set the max size of received header frames. diff --git a/poem-grpc/Cargo.toml b/poem-grpc/Cargo.toml index 216acde75c..912d6995cf 100644 --- a/poem-grpc/Cargo.toml +++ b/poem-grpc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "poem-grpc" -version = "0.5.2" +version = "0.5.3" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/poem-grpc/src/metadata.rs b/poem-grpc/src/metadata.rs index 9ecb68f333..fe72114ecb 100644 --- a/poem-grpc/src/metadata.rs +++ b/poem-grpc/src/metadata.rs @@ -168,7 +168,7 @@ pub struct GetBinaryAll<'a> { iter: poem::http::header::ValueIter<'a, HeaderValue>, } -impl<'a> Iterator for GetBinaryAll<'a> { +impl Iterator for GetBinaryAll<'_> { type Item = Vec; fn next(&mut self) -> Option { diff --git a/poem-openapi-derive/src/common_args.rs b/poem-openapi-derive/src/common_args.rs index a469f9a897..7da2a8e97d 100644 --- a/poem-openapi-derive/src/common_args.rs +++ b/poem-openapi-derive/src/common_args.rs @@ -222,6 +222,76 @@ pub(crate) struct ExtraHeader { pub(crate) deprecated: bool, } +pub(crate) enum LitOrPath { + Lit(T), + Path(syn::Path), +} + +impl darling::FromMeta for LitOrPath +where + T: darling::FromMeta, +{ + fn from_nested_meta(item: &darling::ast::NestedMeta) -> darling::Result { + T::from_nested_meta(item) + .map(Self::Lit) + .or_else(|_| syn::Path::from_nested_meta(item).map(Self::Path)) + } + + fn from_meta(item: &syn::Meta) -> darling::Result { + T::from_meta(item) + .map(Self::Lit) + .or_else(|_| syn::Path::from_meta(item).map(Self::Path)) + } + + fn from_none() -> Option { + T::from_none() + .map(Self::Lit) + .or_else(|| syn::Path::from_none().map(Self::Path)) + } + + fn from_word() -> darling::Result { + T::from_word() + .map(Self::Lit) + .or_else(|_| syn::Path::from_word().map(Self::Path)) + } + + fn from_list(items: &[darling::ast::NestedMeta]) -> darling::Result { + T::from_list(items) + .map(Self::Lit) + .or_else(|_| syn::Path::from_list(items).map(Self::Path)) + } + + fn from_value(value: &Lit) -> darling::Result { + T::from_value(value) + .map(Self::Lit) + .or_else(|_| syn::Path::from_value(value).map(Self::Path)) + } + + fn from_expr(expr: &syn::Expr) -> darling::Result { + T::from_expr(expr) + .map(Self::Lit) + .or_else(|_| syn::Path::from_expr(expr).map(Self::Path)) + } + + fn from_char(value: char) -> darling::Result { + T::from_char(value) + .map(Self::Lit) + .or_else(|_| syn::Path::from_char(value).map(Self::Path)) + } + + fn from_string(value: &str) -> darling::Result { + T::from_string(value) + .map(Self::Lit) + .or_else(|_| syn::Path::from_string(value).map(Self::Path)) + } + + fn from_bool(value: bool) -> darling::Result { + T::from_bool(value) + .map(Self::Lit) + .or_else(|_| syn::Path::from_bool(value).map(Self::Path)) + } +} + #[derive(FromMeta)] pub(crate) struct CodeSample { pub(crate) lang: String, diff --git a/poem-openapi-derive/src/response.rs b/poem-openapi-derive/src/response.rs index 7be505897c..b40b553f77 100644 --- a/poem-openapi-derive/src/response.rs +++ b/poem-openapi-derive/src/response.rs @@ -8,7 +8,7 @@ use quote::quote; use syn::{Attribute, DeriveInput, Error, Generics, Path, Type}; use crate::{ - common_args::ExtraHeader, + common_args::{ExtraHeader, LitOrPath}, error::GeneratorResult, utils::{get_crate_name, get_description, optional_literal, optional_literal_string}, }; @@ -33,7 +33,7 @@ struct ResponseItem { fields: Fields, #[darling(default)] - status: Option, + status: Option>, #[darling(default)] content_type: Option, #[darling(default, multiple, rename = "header")] @@ -218,7 +218,7 @@ pub(crate) fn generate(args: DeriveInput) -> GeneratorResult { // #[oai(status = 200)] // Item(media) let media_ty = &values[0].ty; - let status = get_status(variant.ident.span(), variant.status)?; + let status = get_status(variant.ident.span(), &variant.status)?; let (update_response_content_type, update_meta_content_type) = update_content_type( &crate_name, variant.content_type.as_deref(), @@ -257,7 +257,7 @@ pub(crate) fn generate(args: DeriveInput) -> GeneratorResult { 0 => { // #[oai(status = 200)] // Item - let status = get_status(variant.ident.span(), variant.status)?; + let status = get_status(variant.ident.span(), &variant.status)?; let item = if !headers.is_empty() { quote!(#ident::#item_ident(#(#match_headers),*)) } else { @@ -362,16 +362,23 @@ pub(crate) fn generate(args: DeriveInput) -> GeneratorResult { Ok(expanded) } -fn get_status(span: Span, status: Option) -> GeneratorResult { - let status = status.ok_or_else(|| Error::new(span, "Missing status attribute"))?; - if !(100..1000).contains(&status) { - return Err(Error::new( - span, - "Invalid status code, it must be greater or equal to 100 and less than 1000.", - ) - .into()); +fn get_status(span: Span, status: &Option>) -> GeneratorResult { + let status = status + .as_ref() + .ok_or_else(|| Error::new(span, "Missing status attribute"))?; + match status { + LitOrPath::Lit(status) => { + if !(100..1000).contains(status) { + return Err(Error::new( + span, + "Invalid status code, it must be greater or equal to 100 and less than 1000.", + ) + .into()); + } + Ok(quote!(#status)) + } + LitOrPath::Path(ident) => Ok(quote!(#ident)), } - Ok(quote!(#status)) } fn parse_fields( diff --git a/poem-openapi-derive/src/union.rs b/poem-openapi-derive/src/union.rs index 635fed6ea8..567f171841 100644 --- a/poem-openapi-derive/src/union.rs +++ b/poem-openapi-derive/src/union.rs @@ -151,6 +151,7 @@ pub(crate) fn generate(args: DeriveInput) -> GeneratorResult { } let schema = #crate_name::registry::MetaSchema { + description: #description, all_of: ::std::vec![ #crate_name::registry::MetaSchemaRef::Inline(::std::boxed::Box::new(#crate_name::registry::MetaSchema { required: #required, diff --git a/poem-openapi/CHANGELOG.md b/poem-openapi/CHANGELOG.md index a4e586bfdd..6bf0f02faf 100644 --- a/poem-openapi/CHANGELOG.md +++ b/poem-openapi/CHANGELOG.md @@ -4,6 +4,13 @@ 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.1.5] 2025-01-04 + +- Add description to Union descriminator object schema [#921](https://github.com/poem-web/poem/pull/921) +- make Json from poem-openapi derive Default because Json from poem does [#938](https://github.com/poem-web/poem/pull/938) +- Pass `ParsePayload::IS_REQUIRED` to `T` instead of defaulting to `true` [#932](https://github.com/poem-web/poem/pull/932) +- allow path in status for ApiResponse [#937](https://github.com/poem-web/poem/pull/937) + #[5.1.4] 2024-11-25 - Assign the description to the request object in OpenAPI [#886](https://github.com/poem-web/poem/pull/886) diff --git a/poem-openapi/Cargo.toml b/poem-openapi/Cargo.toml index e7756ddbea..187ca227a2 100644 --- a/poem-openapi/Cargo.toml +++ b/poem-openapi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "poem-openapi" -version = "5.1.4" +version = "5.1.5" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/poem-openapi/src/payload/base64_payload.rs b/poem-openapi/src/payload/base64_payload.rs index 4ee10c3ff6..1fa489057d 100644 --- a/poem-openapi/src/payload/base64_payload.rs +++ b/poem-openapi/src/payload/base64_payload.rs @@ -93,7 +93,7 @@ impl Payload for Base64 { && (content_type.subtype() == "plain" || content_type .suffix() - .map_or(false, |v| v == "plain"))) + .is_some_and(|v| v == "plain"))) } fn schema_ref() -> MetaSchemaRef { diff --git a/poem-openapi/src/payload/binary.rs b/poem-openapi/src/payload/binary.rs index 1932d0de02..609c34ceff 100644 --- a/poem-openapi/src/payload/binary.rs +++ b/poem-openapi/src/payload/binary.rs @@ -92,7 +92,7 @@ impl Payload for Binary { && (content_type.subtype() == "octet-stream" || content_type .suffix() - .map_or(false, |v| v == "octet-stream"))) + .is_some_and(|v| v == "octet-stream"))) } fn schema_ref() -> MetaSchemaRef { diff --git a/poem-openapi/src/payload/form.rs b/poem-openapi/src/payload/form.rs index 2464989064..b06a43cbd3 100644 --- a/poem-openapi/src/payload/form.rs +++ b/poem-openapi/src/payload/form.rs @@ -36,7 +36,7 @@ impl Payload for Form { && (content_type.subtype() == "x-www-form-urlencoded" || content_type .suffix() - .map_or(false, |v| v == "x-www-form-urlencoded"))) + .is_some_and(|v| v == "x-www-form-urlencoded"))) } fn schema_ref() -> MetaSchemaRef { diff --git a/poem-openapi/src/payload/html.rs b/poem-openapi/src/payload/html.rs index ed32d9402e..d501917fe9 100644 --- a/poem-openapi/src/payload/html.rs +++ b/poem-openapi/src/payload/html.rs @@ -35,7 +35,7 @@ impl Payload for Html { && (content_type.subtype() == "html" || content_type .suffix() - .map_or(false, |v| v == "html"))) + .is_some_and(|v| v == "html"))) } fn schema_ref() -> MetaSchemaRef { diff --git a/poem-openapi/src/payload/json.rs b/poem-openapi/src/payload/json.rs index ce858a9d96..c46562818f 100644 --- a/poem-openapi/src/payload/json.rs +++ b/poem-openapi/src/payload/json.rs @@ -12,7 +12,7 @@ use crate::{ }; /// A JSON payload. -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq, Default)] pub struct Json(pub T); impl Deref for Json { @@ -37,7 +37,7 @@ impl Payload for Json { && (content_type.subtype() == "json" || content_type .suffix() - .map_or(false, |v| v == "json"))) + .is_some_and(|v| v == "json"))) } fn schema_ref() -> MetaSchemaRef { @@ -51,7 +51,7 @@ impl Payload for Json { } impl ParsePayload for Json { - const IS_REQUIRED: bool = true; + const IS_REQUIRED: bool = T::IS_REQUIRED; async fn from_request(request: &Request, body: &mut RequestBody) -> Result { let data = Vec::::from_request(request, body).await?; diff --git a/poem-openapi/src/payload/plain_text.rs b/poem-openapi/src/payload/plain_text.rs index 8adcc40337..62fa4998ec 100644 --- a/poem-openapi/src/payload/plain_text.rs +++ b/poem-openapi/src/payload/plain_text.rs @@ -35,7 +35,7 @@ impl Payload for PlainText { && (content_type.subtype() == "plain" || content_type .suffix() - .map_or(false, |v| v == "plain"))) + .is_some_and(|v| v == "plain"))) } fn schema_ref() -> MetaSchemaRef { diff --git a/poem-openapi/src/payload/xml.rs b/poem-openapi/src/payload/xml.rs index 36558e4470..2998d16a02 100644 --- a/poem-openapi/src/payload/xml.rs +++ b/poem-openapi/src/payload/xml.rs @@ -37,7 +37,7 @@ impl Payload for Xml { && (content_type.subtype() == "xml" || content_type .suffix() - .map_or(false, |v| v == "xml"))) + .is_some_and(|v| v == "xml"))) } fn schema_ref() -> MetaSchemaRef { diff --git a/poem-openapi/src/payload/yaml.rs b/poem-openapi/src/payload/yaml.rs index 24e4bd5d78..e7c587b1d2 100644 --- a/poem-openapi/src/payload/yaml.rs +++ b/poem-openapi/src/payload/yaml.rs @@ -37,7 +37,7 @@ impl Payload for Yaml { && (content_type.subtype() == "yaml" || content_type .suffix() - .map_or(false, |v| v == "yaml"))) + .is_some_and(|v| v == "yaml"))) } fn schema_ref() -> MetaSchemaRef { diff --git a/poem-openapi/src/registry/ser.rs b/poem-openapi/src/registry/ser.rs index ade9eb0591..0d394a6cc4 100644 --- a/poem-openapi/src/registry/ser.rs +++ b/poem-openapi/src/registry/ser.rs @@ -24,7 +24,7 @@ impl Serialize for MetaSchemaRef { struct PathMap<'a>(&'a [MetaApi], Option<&'a str>); -impl<'a> Serialize for PathMap<'a> { +impl Serialize for PathMap<'_> { fn serialize(&self, serializer: S) -> Result { let mut s = serializer.serialize_map(Some(self.0.len()))?; for api in self.0 { @@ -66,7 +66,7 @@ impl Serialize for MetaResponses { struct WebhookMap<'a>(&'a [MetaWebhook]); -impl<'a> Serialize for WebhookMap<'a> { +impl Serialize for WebhookMap<'_> { fn serialize(&self, serializer: S) -> Result { let mut s = serializer.serialize_map(Some(self.0.len()))?; for webhook in self.0 { @@ -86,7 +86,7 @@ pub(crate) struct Document<'a> { pub(crate) url_prefix: Option<&'a str>, } -impl<'a> Serialize for Document<'a> { +impl Serialize for Document<'_> { fn serialize(&self, serializer: S) -> Result { #[derive(Serialize)] #[serde(rename_all = "camelCase")] diff --git a/poem-openapi/src/types/external/string.rs b/poem-openapi/src/types/external/string.rs index 27c09b7b8a..31078de0ea 100644 --- a/poem-openapi/src/types/external/string.rs +++ b/poem-openapi/src/types/external/string.rs @@ -83,7 +83,7 @@ impl ToHeader for String { } } -impl<'a> Type for &'a str { +impl Type for &str { const IS_REQUIRED: bool = true; type RawValueType = Self; @@ -109,7 +109,7 @@ impl<'a> Type for &'a str { } } -impl<'a> ToJSON for &'a str { +impl ToJSON for &str { fn to_json(&self) -> Option { Some(Value::String(self.to_string())) } diff --git a/poem-openapi/src/types/string_types.rs b/poem-openapi/src/types/string_types.rs index 436d29dc39..9c12e974dc 100644 --- a/poem-openapi/src/types/string_types.rs +++ b/poem-openapi/src/types/string_types.rs @@ -126,7 +126,7 @@ impl_string_types!( #[cfg(feature = "hostname")] impl_string_types!( - /// A email address type. + /// A hostname type. #[cfg_attr(docsrs, doc(cfg(feature = "hostname")))] Hostname, "string", diff --git a/poem-openapi/tests/api.rs b/poem-openapi/tests/api.rs index c5b9e31a79..85c6e7c025 100644 --- a/poem-openapi/tests/api.rs +++ b/poem-openapi/tests/api.rs @@ -353,13 +353,15 @@ async fn payload_request() { #[tokio::test] async fn response() { + const ALREADY_EXISTS_CODE: u16 = 409; + #[derive(ApiResponse)] enum MyResponse { /// Ok #[oai(status = 200)] Ok, /// Already exists - #[oai(status = 409)] + #[oai(status = ALREADY_EXISTS_CODE)] AlreadyExists(Json), /// Default Default(StatusCode, PlainText), diff --git a/poem/CHANGELOG.md b/poem/CHANGELOG.md index 5d499451b9..80c3e07566 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.1.6] 2025-01-04 + +- fix: update otel semconv as well [#916](https://github.com/poem-web/poem/pull/916) +- chore: bump thiserror and tokio-tungstenite [#931](https://github.com/poem-web/poem/pull/931) +- Implement middleware for `&T: Middleware<_>` [#936](https://github.com/poem-web/poem/pull/936) +- Respect client cookie precedence [#943](https://github.com/poem-web/poem/pull/943) +- Set a Path on the CSRF cookie [#944](https://github.com/poem-web/poem/pull/944) + # [3.1.5] 2024-11-25 - Bump `opentelemetry` to `0.27.0` diff --git a/poem/Cargo.toml b/poem/Cargo.toml index 6c88f62059..ad863e5624 100644 --- a/poem/Cargo.toml +++ b/poem/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "poem" -version = "3.1.5" +version = "3.1.6" authors.workspace = true edition.workspace = true license.workspace = true @@ -99,7 +99,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.23.1", optional = true } +tokio-tungstenite = { version = "0.25", 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 = [ @@ -131,7 +131,9 @@ libcookie = { package = "cookie", version = "0.18", features = [ "secure", ], optional = true } opentelemetry-http = { version = "0.27.0", optional = true } -opentelemetry-semantic-conventions = { version = "0.16.0", optional = true } +opentelemetry-semantic-conventions = { version = "0.27.0", optional = true, features = [ + "semconv_experimental", +] } opentelemetry-prometheus = { version = "0.17.0", optional = true } libprometheus = { package = "prometheus", version = "0.13.0", optional = true } libopentelemetry = { package = "opentelemetry", version = "0.27.0", features = [ diff --git a/poem/src/endpoint/endpoint.rs b/poem/src/endpoint/endpoint.rs index e23aee7c6c..5a5613499b 100644 --- a/poem/src/endpoint/endpoint.rs +++ b/poem/src/endpoint/endpoint.rs @@ -93,7 +93,7 @@ where } } -/// The enum `EitherEndpoint` with variants `Left`` and `Right` is a general +/// The enum `EitherEndpoint` with variants `Left` and `Right` is a general /// purpose sum type with two cases. pub enum EitherEndpoint { /// A endpoint of type `A` diff --git a/poem/src/middleware/csrf.rs b/poem/src/middleware/csrf.rs index 103bdeab62..aef7a42ffa 100644 --- a/poem/src/middleware/csrf.rs +++ b/poem/src/middleware/csrf.rs @@ -1,4 +1,4 @@ -use std::{sync::Arc, time::Duration}; +use std::{borrow::Cow, sync::Arc, time::Duration}; use base64::{engine::general_purpose::STANDARD, Engine}; use libcsrf::{ @@ -83,6 +83,10 @@ pub struct Csrf { http_only: bool, same_site: Option, ttl: Duration, + // Using Arc> here because the path is most likely going + // to be a static str, and if it's not, we don't want to have to copy it + // into every endpoint. + path: Arc>, } impl Default for Csrf { @@ -94,6 +98,7 @@ impl Default for Csrf { http_only: true, same_site: Some(SameSite::Strict), ttl: Duration::from_secs(24 * 60 * 60), + path: Arc::new(Cow::Borrowed("/")), } } } @@ -147,6 +152,17 @@ impl Csrf { } } + /// Set the path for which the CSRF cookie should be set. + /// + /// By default, this is `"/"`. + #[must_use] + pub fn path(self, value: impl Into>) -> Self { + Self { + path: Arc::new(value.into()), + ..self + } + } + /// Sets the protection ttl. This will be used for both the cookie /// expiry and the time window over which CSRF tokens are considered /// valid. @@ -170,6 +186,7 @@ impl Middleware for Csrf { http_only: self.http_only, same_site: self.same_site, ttl: self.ttl, + path: Arc::clone(&self.path), }) } } @@ -184,6 +201,7 @@ pub struct CsrfEndpoint { http_only: bool, same_site: Option, ttl: Duration, + path: Arc>, } impl CsrfEndpoint { @@ -226,6 +244,7 @@ impl Endpoint for CsrfEndpoint { cookie.set_http_only(self.http_only); cookie.set_same_site(self.same_site); cookie.set_max_age(self.ttl); + cookie.set_path(&**self.path); cookie }; diff --git a/poem/src/middleware/mod.rs b/poem/src/middleware/mod.rs index 0fe8d26a22..5cb3e0e61d 100644 --- a/poem/src/middleware/mod.rs +++ b/poem/src/middleware/mod.rs @@ -301,6 +301,14 @@ impl Middleware for () { } } +impl> Middleware for &T { + type Output = T::Output; + + fn transform(&self, ep: E) -> Self::Output { + T::transform(self, ep) + } +} + /// A middleware that combines two middlewares. pub struct CombineMiddleware { a: A, @@ -322,9 +330,8 @@ where } } -/// The enum `EitherMiddleware` with variants `Left`` and `Right` is a general +/// The enum `EitherMiddleware` with variants `Left` and `Right` is a general /// purpose sum type with two cases. - pub enum EitherMiddleware { /// A middleware of type `A` A(A, PhantomData), diff --git a/poem/src/web/cookie.rs b/poem/src/web/cookie.rs index a241778b47..5f0265715f 100644 --- a/poem/src/web/cookie.rs +++ b/poem/src/web/cookie.rs @@ -542,7 +542,17 @@ impl CookieJar { if let Ok(value) = value.to_str() { for cookie_str in value.split(';').map(str::trim) { if let Ok(cookie) = libcookie::Cookie::parse_encoded(cookie_str) { - cookie_jar.add_original(cookie.into_owned()); + // This check is important. Poem currently only + // supports tracking a single cookie by name. + // RFC 6265, Section 5.4, says that user agents SHOULD + // sort cookies from most specific to least specific + // path. + // That means that poem should take the *first* cookie + // for a given name (instead of the *last*, as it would + // happen if this condition wasn't enforced). + if cookie_jar.get(cookie.name()).is_none() { + cookie_jar.add_original(cookie.into_owned()); + } } } } @@ -574,7 +584,7 @@ pub struct PrivateCookieJar<'a> { cookie_jar: &'a CookieJar, } -impl<'a> PrivateCookieJar<'a> { +impl PrivateCookieJar<'_> { /// Adds cookie to the parent jar. The cookie’s value is encrypted with /// authenticated encryption assuring confidentiality, integrity, and /// authenticity. @@ -608,7 +618,7 @@ pub struct SignedCookieJar<'a> { cookie_jar: &'a CookieJar, } -impl<'a> SignedCookieJar<'a> { +impl SignedCookieJar<'_> { /// Adds cookie to the parent jar. The cookie’s value is signed assuring /// integrity and authenticity. pub fn add(&self, cookie: Cookie) { diff --git a/poem/src/web/form.rs b/poem/src/web/form.rs index c1f7e48078..4ca19db73c 100644 --- a/poem/src/web/form.rs +++ b/poem/src/web/form.rs @@ -118,7 +118,7 @@ fn is_form_content_type(content_type: &str) -> bool { && (content_type.subtype() == "x-www-form-urlencoded" || content_type .suffix() - .map_or(false, |v| v == "x-www-form-urlencoded"))) + .is_some_and(|v| v == "x-www-form-urlencoded"))) } #[cfg(test)] diff --git a/poem/src/web/json.rs b/poem/src/web/json.rs index d40c13da10..88903ffc7c 100644 --- a/poem/src/web/json.rs +++ b/poem/src/web/json.rs @@ -137,7 +137,7 @@ fn is_json_content_type(content_type: &str) -> bool { && (content_type.subtype() == "json" || content_type .suffix() - .map_or(false, |v| v == "json"))) + .is_some_and(|v| v == "json"))) } impl IntoResponse for Json { diff --git a/poem/src/web/websocket/mod.rs b/poem/src/web/websocket/mod.rs index 914bb7b25f..c5941c56c7 100644 --- a/poem/src/web/websocket/mod.rs +++ b/poem/src/web/websocket/mod.rs @@ -137,25 +137,21 @@ mod tests { .unwrap(); client_stream - .send(tokio_tungstenite::tungstenite::Message::Text( - "aBc".to_string(), - )) + .send(tokio_tungstenite::tungstenite::Message::Text("aBc".into())) .await .unwrap(); assert_eq!( client_stream.next().await.unwrap().unwrap(), - tokio_tungstenite::tungstenite::Message::Text("ABC".to_string()) + tokio_tungstenite::tungstenite::Message::Text("ABC".into()) ); client_stream - .send(tokio_tungstenite::tungstenite::Message::Text( - "def".to_string(), - )) + .send(tokio_tungstenite::tungstenite::Message::Text("def".into())) .await .unwrap(); assert_eq!( client_stream.next().await.unwrap().unwrap(), - tokio_tungstenite::tungstenite::Message::Text("DEF".to_string()) + tokio_tungstenite::tungstenite::Message::Text("DEF".into()) ); handle.abort(); diff --git a/poem/src/web/websocket/utils.rs b/poem/src/web/websocket/utils.rs index c5e879b0cd..e0207bb028 100644 --- a/poem/src/web/websocket/utils.rs +++ b/poem/src/web/websocket/utils.rs @@ -41,10 +41,10 @@ impl From for Message { use tokio_tungstenite::tungstenite::Message::*; match msg { - Text(data) => Message::Text(data), - Binary(data) => Message::Binary(data), - Ping(data) => Message::Ping(data), - Pong(data) => Message::Pong(data), + Text(data) => Message::Text(data.to_string()), + Binary(data) => Message::Binary(data.as_slice().into()), + Ping(data) => Message::Ping(data.as_slice().into()), + Pong(data) => Message::Pong(data.as_slice().into()), Close(cf) => Message::Close(cf.map(|cf| (cf.code.into(), cf.reason.to_string()))), Frame(_) => unreachable!(), } @@ -54,13 +54,13 @@ impl From for Message { #[doc(hidden)] impl From for tokio_tungstenite::tungstenite::Message { fn from(msg: Message) -> Self { - use tokio_tungstenite::tungstenite::Message::*; + use tokio_tungstenite::tungstenite::{protocol::frame::Payload, Message::*}; match msg { - Message::Text(data) => Text(data), - Message::Binary(data) => Binary(data), - Message::Ping(data) => Ping(data), - Message::Pong(data) => Pong(data), + Message::Text(data) => Text(data.into()), + Message::Binary(data) => Binary(Payload::Vec(data)), + Message::Ping(data) => Ping(Payload::Vec(data)), + Message::Pong(data) => Pong(Payload::Vec(data)), Message::Close(cf) => Close(cf.map(|(code, reason)| CloseFrame { code: code.into(), reason: reason.into(), diff --git a/poem/src/web/xml.rs b/poem/src/web/xml.rs index 28c45eb85c..396ed0ba2d 100644 --- a/poem/src/web/xml.rs +++ b/poem/src/web/xml.rs @@ -130,7 +130,7 @@ fn is_xml_content_type(content_type: &str) -> bool { && (content_type.subtype() == "xml" || content_type .suffix() - .map_or(false, |v| v == "xml"))) + .is_some_and(|v| v == "xml"))) } impl IntoResponse for Xml { diff --git a/poem/src/web/yaml.rs b/poem/src/web/yaml.rs index 1960ec694b..60f361bab9 100644 --- a/poem/src/web/yaml.rs +++ b/poem/src/web/yaml.rs @@ -131,7 +131,7 @@ fn is_yaml_content_type(content_type: &str) -> bool { && (content_type.subtype() == "yaml" || content_type .suffix() - .map_or(false, |v| v == "yaml"))) + .is_some_and(|v| v == "yaml"))) } impl IntoResponse for Yaml {