diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ee6301999e..7aedde9b0b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,7 +55,7 @@ jobs: # Switch to stable Rust - uses: actions-rs/toolchain@v1 with: - toolchain: 1.75.0 + toolchain: 1.76.0 components: rustfmt, clippy override: true - name: Cache Rust @@ -91,7 +91,7 @@ jobs: # Switch to stable Rust - uses: actions-rs/toolchain@v1 with: - toolchain: 1.75.0 + toolchain: 1.76.0 components: rustfmt, clippy - name: Cache Rust uses: Swatinem/rust-cache@v2 diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index f2bba3f2bc..88fdfc2fb2 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -21,7 +21,7 @@ jobs: - name: Install Stable Toolchain uses: dtolnay/rust-toolchain@stable with: - toolchain: 1.75.0 + toolchain: 1.76.0 components: rustfmt - name: Cache Rust uses: Swatinem/rust-cache@v2 diff --git a/.rustfmt.toml b/.rustfmt.toml index b2cfe19264..f87905ebe0 100644 --- a/.rustfmt.toml +++ b/.rustfmt.toml @@ -2,7 +2,7 @@ edition = "2021" newline_style = "unix" # comments normalize_comments = true -#wrap_comments=true +wrap_comments = true format_code_in_doc_comments = true # imports imports_granularity = "Crate" diff --git a/Cargo.toml b/Cargo.toml index ebdc7fbadf..0291523482 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,10 +20,10 @@ repository = "https://github.com/poem-web/poem" rust-version = "1.75" [workspace.dependencies] -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.2" } +poem = { path = "poem", version = "3.1.0", default-features = false } +poem-derive = { path = "poem-derive", version = "3.1.0" } +poem-openapi-derive = { path = "poem-openapi-derive", version = "5.1.0" } +poem-grpc-build = { path = "poem-grpc-build", version = "0.5.0" } proc-macro-crate = "3.0.0" proc-macro2 = "1.0.29" @@ -31,6 +31,7 @@ quote = "1.0.9" syn = { version = "2.0" } 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" regex = "1.5.5" diff --git a/README.md b/README.md index a853c09552..7a9003b6a2 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,9 @@ Unsafe Rust forbidden - - rustc 1.75.0+ + + rustc 1.76.0+ diff --git a/examples/grpc/helloworld_compressed/Cargo.toml b/examples/grpc/helloworld_compressed/Cargo.toml new file mode 100644 index 0000000000..d1723b85f5 --- /dev/null +++ b/examples/grpc/helloworld_compressed/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "example-grpc-helloworld-compressed" +version.workspace = true +edition.workspace = true +publish.workspace = true + +[dependencies] +poem.workspace = true +poem-grpc = { workspace = true, features = [ + "gzip", + "deflate", + "brotli", + "zstd", +] } +prost.workspace = true +tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } + +[build-dependencies] +poem-grpc-build.workspace = true + +[[bin]] +name = "grpc-helloworld-client" +path = "src/client.rs" diff --git a/examples/grpc/helloworld_compressed/build.rs b/examples/grpc/helloworld_compressed/build.rs new file mode 100644 index 0000000000..a388ebfa5d --- /dev/null +++ b/examples/grpc/helloworld_compressed/build.rs @@ -0,0 +1,7 @@ +use std::io::Result; + +use poem_grpc_build::compile_protos; + +fn main() -> Result<()> { + compile_protos(&["./proto/helloworld.proto"], &["./proto"]) +} diff --git a/examples/grpc/helloworld_compressed/proto/helloworld.proto b/examples/grpc/helloworld_compressed/proto/helloworld.proto new file mode 100644 index 0000000000..8de5d08ef4 --- /dev/null +++ b/examples/grpc/helloworld_compressed/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_compressed/src/client.rs b/examples/grpc/helloworld_compressed/src/client.rs new file mode 100644 index 0000000000..7f9abeaea9 --- /dev/null +++ b/examples/grpc/helloworld_compressed/src/client.rs @@ -0,0 +1,22 @@ +use poem_grpc::{ClientConfig, CompressionEncoding, Request}; + +poem_grpc::include_proto!("helloworld"); + +#[tokio::main] +async fn main() -> Result<(), Box> { + let mut client = GreeterClient::new( + ClientConfig::builder() + .uri("http://localhost:3000") + .build() + .unwrap(), + ); + client.set_send_compressed(CompressionEncoding::GZIP); + client.set_accept_compressed([CompressionEncoding::GZIP]); + + let request = Request::new(HelloRequest { + name: "Poem".into(), + }); + let response = client.say_hello(request).await?; + println!("RESPONSE={response:?}"); + Ok(()) +} diff --git a/examples/grpc/helloworld_compressed/src/main.rs b/examples/grpc/helloworld_compressed/src/main.rs new file mode 100644 index 0000000000..f6f4c918a4 --- /dev/null +++ b/examples/grpc/helloworld_compressed/src/main.rs @@ -0,0 +1,35 @@ +use poem::{listener::TcpListener, Server}; +use poem_grpc::{CompressionEncoding, Request, Response, RouteGrpc, Status}; + +poem_grpc::include_proto!("helloworld"); + +struct GreeterService; + +impl Greeter for GreeterService { + async fn say_hello( + &self, + request: Request, + ) -> Result, Status> { + let reply = HelloReply { + message: format!("Hello {}!", request.into_inner().name), + }; + Ok(Response::new(reply)) + } +} + +#[tokio::main] +async fn main() -> Result<(), std::io::Error> { + let route = RouteGrpc::new().add_service( + GreeterServer::new(GreeterService) + .send_compressed(CompressionEncoding::GZIP) + .accept_compressed([ + CompressionEncoding::GZIP, + CompressionEncoding::DEFLATE, + CompressionEncoding::BROTLI, + CompressionEncoding::ZSTD, + ]), + ); + Server::new(TcpListener::bind("0.0.0.0:3000")) + .run(route) + .await +} diff --git a/poem-derive/Cargo.toml b/poem-derive/Cargo.toml index 3e713e854b..122ba3c392 100644 --- a/poem-derive/Cargo.toml +++ b/poem-derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "poem-derive" -version = "3.0.4" +version = "3.1.0" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/poem-grpc-build/Cargo.toml b/poem-grpc-build/Cargo.toml index dfa382455f..34500d3355 100644 --- a/poem-grpc-build/Cargo.toml +++ b/poem-grpc-build/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "poem-grpc-build" -version = "0.4.2" +version = "0.5.0" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/poem-grpc-build/src/client.rs b/poem-grpc-build/src/client.rs index a27fea099f..89554d16e0 100644 --- a/poem-grpc-build/src/client.rs +++ b/poem-grpc-build/src/client.rs @@ -117,6 +117,16 @@ pub(crate) fn generate(config: &GrpcConfig, service: &Service, buf: &mut String) self } + /// Set the compression encoding for sending + pub fn set_send_compressed(&mut self, encoding: #crate_name::CompressionEncoding) { + self.cli.set_send_compressed(encoding); + } + + /// Set the compression encodings for accepting + pub fn set_accept_compressed(&mut self, encodings: impl ::std::convert::Into<::std::sync::Arc<[#crate_name::CompressionEncoding]>>) { + self.cli.set_accept_compressed(encodings); + } + #( #[allow(dead_code)] #methods diff --git a/poem-grpc-build/src/server.rs b/poem-grpc-build/src/server.rs index 73e5cde804..a8f1196548 100644 --- a/poem-grpc-build/src/server.rs +++ b/poem-grpc-build/src/server.rs @@ -99,8 +99,22 @@ pub(crate) fn generate(config: &GrpcConfig, service: &Service, buf: &mut String) } #[allow(unused_imports)] - #[derive(Clone)] - pub struct #server_ident(::std::sync::Arc); + pub struct #server_ident { + inner: ::std::sync::Arc, + send_compressd: ::std::option::Option<#crate_name::CompressionEncoding>, + accept_compressed: ::std::sync::Arc<[#crate_name::CompressionEncoding]>, + } + + impl ::std::clone::Clone for #server_ident { + #[inline] + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + send_compressd: self.send_compressd, + accept_compressed: self.accept_compressed.clone(), + } + } + } impl #crate_name::Service for #server_ident { const NAME: &'static str = #service_name; @@ -109,8 +123,29 @@ pub(crate) fn generate(config: &GrpcConfig, service: &Service, buf: &mut String) #[allow(dead_code)] impl #server_ident { + /// Create a new GRPC server pub fn new(service: T) -> Self { - Self(::std::sync::Arc::new(service)) + Self { + inner: ::std::sync::Arc::new(service), + send_compressd: ::std::option::Option::None, + accept_compressed: ::std::sync::Arc::new([]), + } + } + + /// Set the compression encoding for sending + pub fn send_compressed(self, encoding: #crate_name::CompressionEncoding) -> Self { + Self { + send_compressd: Some(encoding), + ..self + } + } + + /// Set the compression encodings for accepting + pub fn accept_compressed(self, encodings: impl ::std::convert::Into<::std::sync::Arc<[#crate_name::CompressionEncoding]>>) -> Self { + Self { + accept_compressed: encodings.into(), + ..self + } } } @@ -191,7 +226,7 @@ fn generate_unary(codec_list: &[Path], method_info: MethodInfo) -> TokenStream { crate_name, codec_list, quote! { - #crate_name::server::GrpcServer::new(codec).unary(#proxy_service_ident(svc.clone()), req).await + #crate_name::server::GrpcServer::new(codec, server.send_compressd, &server.accept_compressed).unary(#proxy_service_ident(server.inner.clone()), req).await }, ); @@ -211,9 +246,9 @@ fn generate_unary(codec_list: &[Path], method_info: MethodInfo) -> TokenStream { } route = route.at(#path, ::poem::endpoint::make({ - let svc = self.0.clone(); + let server = self.clone(); move |req| { - let svc = svc.clone(); + let server = server.clone(); async move { #call } } })); @@ -235,7 +270,7 @@ fn generate_client_streaming(codec_list: &[Path], method_info: MethodInfo) -> To crate_name, codec_list, quote! { - #crate_name::server::GrpcServer::new(codec).client_streaming(#proxy_service_ident(svc.clone()), req).await + #crate_name::server::GrpcServer::new(codec, server.send_compressd, &server.accept_compressed).client_streaming(#proxy_service_ident(server.inner.clone()), req).await }, ); @@ -255,9 +290,9 @@ fn generate_client_streaming(codec_list: &[Path], method_info: MethodInfo) -> To } route = route.at(#path, ::poem::endpoint::make({ - let svc = self.0.clone(); + let server = self.clone(); move |req| { - let svc = svc.clone(); + let server = server.clone(); async move { #call } } })); @@ -279,7 +314,7 @@ fn generate_server_streaming(codec_list: &[Path], method_info: MethodInfo) -> To crate_name, codec_list, quote! { - #crate_name::server::GrpcServer::new(codec).server_streaming(#proxy_service_ident(svc.clone()), req).await + #crate_name::server::GrpcServer::new(codec, server.send_compressd, &server.accept_compressed).server_streaming(#proxy_service_ident(server.inner.clone()), req).await }, ); @@ -299,9 +334,9 @@ fn generate_server_streaming(codec_list: &[Path], method_info: MethodInfo) -> To } route = route.at(#path, ::poem::endpoint::make({ - let svc = self.0.clone(); + let server = self.clone(); move |req| { - let svc = svc.clone(); + let server = server.clone(); async move { #call } } })); @@ -323,7 +358,7 @@ fn generate_bidirectional_streaming(codec_list: &[Path], method_info: MethodInfo crate_name, codec_list, quote! { - #crate_name::server::GrpcServer::new(codec).bidirectional_streaming(#proxy_service_ident(svc.clone()), req).await + #crate_name::server::GrpcServer::new(codec, server.send_compressd, &server.accept_compressed).bidirectional_streaming(#proxy_service_ident(server.inner.clone()), req).await }, ); @@ -343,9 +378,9 @@ fn generate_bidirectional_streaming(codec_list: &[Path], method_info: MethodInfo } route = route.at(#path, ::poem::endpoint::make({ - let svc = self.0.clone(); + let server = self.clone(); move |req| { - let svc = svc.clone(); + let server = server.clone(); async move { #call } } })); diff --git a/poem-grpc/CHANGELOG.md b/poem-grpc/CHANGELOG.md index 0c6dfd82cf..88ab5917bc 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.0] 2024-09-08 + +- add support for GRPC compression + # [0.4.2] 2024-07-19 - Fix #840: Grpc build emit package when package is empty [#841](https://github.com/poem-web/poem/pull/841) diff --git a/poem-grpc/Cargo.toml b/poem-grpc/Cargo.toml index 0efd641ae7..01a6aa3a12 100644 --- a/poem-grpc/Cargo.toml +++ b/poem-grpc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "poem-grpc" -version = "0.4.3" +version = "0.5.0" authors.workspace = true edition.workspace = true license.workspace = true @@ -16,6 +16,11 @@ categories = ["network-programming", "asynchronous"] [features] default = [] json-codec = ["serde", "serde_json"] +gzip = ["async-compression/gzip"] +deflate = ["async-compression/deflate"] +brotli = ["async-compression/brotli"] +zstd = ["async-compression/zstd"] +example_generated = [] [dependencies] poem = { workspace = true, default-features = true } @@ -23,7 +28,6 @@ poem = { workspace = true, default-features = true } futures-util.workspace = true async-stream = "0.3.3" tokio = { workspace = true, features = ["io-util", "rt", "sync", "net"] } -flate2 = "1.0.24" itoa = "1.0.2" percent-encoding = "2.1.0" bytes.workspace = true @@ -43,6 +47,8 @@ http-body-util = "0.1.0" tokio-rustls.workspace = true tower-service = "0.3.2" webpki-roots = "0.26" +async-compression = { version = "0.4.0", optional = true, features = ["tokio"] } +sync_wrapper = { version = "1.0.0", features = ["futures"] } [build-dependencies] poem-grpc-build.workspace = true diff --git a/poem-grpc/README.md b/poem-grpc/README.md index 54729377c5..c8d4ba8105 100644 --- a/poem-grpc/README.md +++ b/poem-grpc/README.md @@ -20,9 +20,9 @@ Unsafe Rust forbidden - - rustc 1.75.0+ + + rustc 1.76.0+ @@ -63,7 +63,7 @@ This crate uses `#![forbid(unsafe_code)]` to ensure everything is implemented in ## MSRV -The minimum supported Rust version for this crate is `1.75.0`. +The minimum supported Rust version for this crate is `1.76.0`. ## Contributing diff --git a/poem-grpc/build.rs b/poem-grpc/build.rs index 81dbd1a58c..661c6a4596 100644 --- a/poem-grpc/build.rs +++ b/poem-grpc/build.rs @@ -13,5 +13,12 @@ fn main() -> Result<()> { // for test poem_grpc_build::Config::new() .internal() - .compile(&["proto/test_harness.proto"], &["proto/"]) + .compile(&["proto/test_harness.proto"], &["proto/"])?; + + // example + poem_grpc_build::Config::new() + .internal() + .compile(&["src/example_generated/routeguide.proto"], &[] as &[&str])?; + + Ok(()) } diff --git a/poem-grpc/src/client.rs b/poem-grpc/src/client.rs index e42db405cc..f5ff77c46c 100644 --- a/poem-grpc/src/client.rs +++ b/poem-grpc/src/client.rs @@ -18,9 +18,10 @@ use rustls::ClientConfig as TlsClientConfig; use crate::{ codec::Codec, + compression::get_incoming_encodings, connector::HttpsConnector, encoding::{create_decode_response_body, create_encode_request_body}, - Code, Metadata, Request, Response, Status, Streaming, + Code, CompressionEncoding, Metadata, Request, Response, Status, Streaming, }; pub(crate) type BoxBody = http_body_util::combinators::BoxBody; @@ -155,6 +156,8 @@ impl ClientConfigBuilder { #[derive(Clone)] pub struct GrpcClient { ep: Arc + 'static>, + send_compressd: Option, + accept_compressed: Arc<[CompressionEncoding]>, } impl GrpcClient { @@ -162,6 +165,8 @@ impl GrpcClient { pub fn new(config: ClientConfig) -> Self { Self { ep: create_client_endpoint(config), + send_compressd: None, + accept_compressed: Arc::new([]), } } @@ -173,9 +178,19 @@ impl GrpcClient { { Self { ep: Arc::new(ToDynEndpoint(ep.map_to_response())), + send_compressd: None, + accept_compressed: Arc::new([]), } } + pub fn set_send_compressed(&mut self, encoding: CompressionEncoding) { + self.send_compressd = Some(encoding); + } + + pub fn set_accept_compressed(&mut self, encodings: impl Into>) { + self.accept_compressed = encodings.into(); + } + pub fn with(mut self, middleware: M) -> Self where M: Middleware + 'static>>, @@ -198,10 +213,12 @@ impl GrpcClient { message, extensions, } = request; - let mut http_request = create_http_request::(path, metadata, extensions); + let mut http_request = + create_http_request::(path, metadata, extensions, self.send_compressd); http_request.set_body(create_encode_request_body( codec.encoder(), Streaming::new(futures_util::stream::once(async move { Ok(message) })), + self.send_compressd, )); let mut resp = self @@ -218,7 +235,9 @@ impl GrpcClient { } let body = resp.take_body(); - let mut stream = create_decode_response_body(codec.decoder(), resp.headers(), body)?; + let incoming_encoding = get_incoming_encodings(resp.headers(), &self.accept_compressed)?; + let mut stream = + create_decode_response_body(codec.decoder(), resp.headers(), body, incoming_encoding)?; let message = stream .try_next() @@ -243,8 +262,13 @@ impl GrpcClient { message, extensions, } = request; - let mut http_request = create_http_request::(path, metadata, extensions); - http_request.set_body(create_encode_request_body(codec.encoder(), message)); + let mut http_request = + create_http_request::(path, metadata, extensions, self.send_compressd); + http_request.set_body(create_encode_request_body( + codec.encoder(), + message, + self.send_compressd, + )); let mut resp = self .ep @@ -260,7 +284,9 @@ impl GrpcClient { } let body = resp.take_body(); - let mut stream = create_decode_response_body(codec.decoder(), resp.headers(), body)?; + let incoming_encoding = get_incoming_encodings(resp.headers(), &self.accept_compressed)?; + let mut stream = + create_decode_response_body(codec.decoder(), resp.headers(), body, incoming_encoding)?; let message = stream .try_next() @@ -285,10 +311,12 @@ impl GrpcClient { message, extensions, } = request; - let mut http_request = create_http_request::(path, metadata, extensions); + let mut http_request = + create_http_request::(path, metadata, extensions, self.send_compressd); http_request.set_body(create_encode_request_body( codec.encoder(), Streaming::new(futures_util::stream::once(async move { Ok(message) })), + self.send_compressd, )); let mut resp = self @@ -305,7 +333,9 @@ impl GrpcClient { } let body = resp.take_body(); - let stream = create_decode_response_body(codec.decoder(), resp.headers(), body)?; + let incoming_encoding = get_incoming_encodings(resp.headers(), &self.accept_compressed)?; + let stream = + create_decode_response_body(codec.decoder(), resp.headers(), body, incoming_encoding)?; Ok(Response { metadata: Metadata { @@ -326,8 +356,13 @@ impl GrpcClient { message, extensions, } = request; - let mut http_request = create_http_request::(path, metadata, extensions); - http_request.set_body(create_encode_request_body(codec.encoder(), message)); + let mut http_request = + create_http_request::(path, metadata, extensions, self.send_compressd); + http_request.set_body(create_encode_request_body( + codec.encoder(), + message, + self.send_compressd, + )); let mut resp = self .ep @@ -343,7 +378,9 @@ impl GrpcClient { } let body = resp.take_body(); - let stream = create_decode_response_body(codec.decoder(), resp.headers(), body)?; + let incoming_encoding = get_incoming_encodings(resp.headers(), &self.accept_compressed)?; + let stream = + create_decode_response_body(codec.decoder(), resp.headers(), body, incoming_encoding)?; Ok(Response { metadata: Metadata { @@ -358,6 +395,7 @@ fn create_http_request( path: &str, metadata: Metadata, extensions: Extensions, + send_compressd: Option, ) -> HttpRequest { let mut http_request = HttpRequest::builder() .uri_str(path) @@ -368,6 +406,12 @@ fn create_http_request( .finish(); http_request.headers_mut().extend(metadata.headers); *http_request.extensions_mut() = extensions; + if let Some(send_compressd) = send_compressd { + http_request.headers_mut().insert( + "grpc-encoding", + HeaderValue::from_str(send_compressd.as_str()).expect("BUG: invalid encoding"), + ); + } http_request } diff --git a/poem-grpc/src/compression.rs b/poem-grpc/src/compression.rs new file mode 100644 index 0000000000..71fd738260 --- /dev/null +++ b/poem-grpc/src/compression.rs @@ -0,0 +1,186 @@ +use std::{io::Result as IoResult, str::FromStr}; + +use http::HeaderMap; + +use crate::{Code, Metadata, Status}; + +/// The compression encodings. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum CompressionEncoding { + /// gzip + #[cfg(feature = "gzip")] + #[cfg_attr(docsrs, doc(cfg(feature = "gzip")))] + GZIP, + /// deflate + #[cfg(feature = "deflate")] + #[cfg_attr(docsrs, doc(cfg(feature = "deflate")))] + DEFLATE, + /// brotli + #[cfg(feature = "brotli")] + #[cfg_attr(docsrs, doc(cfg(feature = "brotli")))] + BROTLI, + /// zstd + #[cfg(feature = "zstd")] + #[cfg_attr(docsrs, doc(cfg(feature = "zstd")))] + ZSTD, +} + +impl FromStr for CompressionEncoding { + type Err = (); + + #[inline] + fn from_str(s: &str) -> Result { + match s { + #[cfg(feature = "gzip")] + "gzip" => Ok(CompressionEncoding::GZIP), + #[cfg(feature = "deflate")] + "deflate" => Ok(CompressionEncoding::DEFLATE), + #[cfg(feature = "brotli")] + "br" => Ok(CompressionEncoding::BROTLI), + #[cfg(feature = "zstd")] + "zstd" => Ok(CompressionEncoding::ZSTD), + _ => Err(()), + } + } +} + +impl CompressionEncoding { + /// Returns the encoding name. + #[allow(unreachable_patterns)] + pub fn as_str(&self) -> &'static str { + match self { + #[cfg(feature = "gzip")] + CompressionEncoding::GZIP => "gzip", + #[cfg(feature = "deflate")] + CompressionEncoding::DEFLATE => "deflate", + #[cfg(feature = "brotli")] + CompressionEncoding::BROTLI => "br", + #[cfg(feature = "zstd")] + CompressionEncoding::ZSTD => "zstd", + _ => unreachable!(), + } + } + + #[allow( + unreachable_code, + unused_imports, + unused_mut, + unused_variables, + unreachable_patterns + )] + pub(crate) async fn encode(&self, data: &[u8]) -> IoResult> { + use tokio::io::AsyncReadExt; + + let mut buf = Vec::new(); + + match self { + #[cfg(feature = "gzip")] + CompressionEncoding::GZIP => { + async_compression::tokio::bufread::GzipEncoder::new(data) + .read_to_end(&mut buf) + .await?; + } + #[cfg(feature = "deflate")] + CompressionEncoding::DEFLATE => { + async_compression::tokio::bufread::DeflateEncoder::new(data) + .read_to_end(&mut buf) + .await?; + } + #[cfg(feature = "brotli")] + CompressionEncoding::BROTLI => { + async_compression::tokio::bufread::BrotliEncoder::new(data) + .read_to_end(&mut buf) + .await?; + } + #[cfg(feature = "zstd")] + CompressionEncoding::ZSTD => { + async_compression::tokio::bufread::ZstdEncoder::new(data) + .read_to_end(&mut buf) + .await?; + } + _ => unreachable!(), + } + + Ok(buf) + } + + #[allow( + unreachable_code, + unused_imports, + unused_mut, + unused_variables, + unreachable_patterns + )] + pub(crate) async fn decode(&self, data: &[u8]) -> IoResult> { + use tokio::io::AsyncReadExt; + + let mut buf = Vec::new(); + + match self { + #[cfg(feature = "gzip")] + CompressionEncoding::GZIP => { + async_compression::tokio::bufread::GzipDecoder::new(data) + .read_to_end(&mut buf) + .await?; + } + #[cfg(feature = "deflate")] + CompressionEncoding::DEFLATE => { + async_compression::tokio::bufread::DeflateDecoder::new(data) + .read_to_end(&mut buf) + .await?; + } + #[cfg(feature = "brotli")] + CompressionEncoding::BROTLI => { + async_compression::tokio::bufread::BrotliDecoder::new(data) + .read_to_end(&mut buf) + .await?; + } + #[cfg(feature = "zstd")] + CompressionEncoding::ZSTD => { + async_compression::tokio::bufread::ZstdDecoder::new(data) + .read_to_end(&mut buf) + .await?; + } + _ => unreachable!(), + } + + Ok(buf) + } +} + +fn unimplemented(accept_compressed: &[CompressionEncoding]) -> Status { + let mut md = Metadata::new(); + let mut accept_encoding = String::new(); + let mut iter = accept_compressed.iter(); + if let Some(encoding) = iter.next() { + accept_encoding.push_str(encoding.as_str()); + } + for encoding in iter { + accept_encoding.push_str(", "); + accept_encoding.push_str(encoding.as_str()); + } + md.append("grpc-accept-encoding", accept_encoding); + Status::new(Code::Unimplemented) + .with_metadata(md) + .with_message("unsupported encoding") +} + +pub(crate) fn get_incoming_encodings( + headers: &HeaderMap, + accept_compressed: &[CompressionEncoding], +) -> Result, Status> { + let Some(value) = headers.get("grpc-encoding") else { + return Ok(None); + }; + let Some(encoding) = value + .to_str() + .ok() + .and_then(|value| value.parse::().ok()) + else { + return Err(unimplemented(accept_compressed)); + }; + if !accept_compressed.contains(&encoding) { + return Err(unimplemented(accept_compressed)); + } + Ok(Some(encoding)) +} diff --git a/poem-grpc/src/encoding.rs b/poem-grpc/src/encoding.rs index 4a19411047..6a4e79c9bb 100644 --- a/poem-grpc/src/encoding.rs +++ b/poem-grpc/src/encoding.rs @@ -1,42 +1,60 @@ -use std::io::{Error as IoError, Result as IoResult}; +use std::io::Result as IoResult; use bytes::{Buf, BufMut, Bytes, BytesMut}; -use flate2::read::GzDecoder; use futures_util::StreamExt; use http_body_util::{BodyExt, StreamBody}; use hyper::{body::Frame, HeaderMap}; use poem::Body; -use tokio::sync::mpsc; -use tokio_stream::wrappers::ReceiverStream; +use sync_wrapper::SyncStream; use crate::{ client::BoxBody, codec::{Decoder, Encoder}, - Code, Status, Streaming, + Code, CompressionEncoding, Status, Streaming, }; -fn encode_data_frame( +async fn encode_data_frame( encoder: &mut T, buf: &mut BytesMut, message: T::Item, + compression: Option, ) -> IoResult { - buf.put_slice(&[0, 0, 0, 0, 0]); - encoder.encode(message, buf)?; + buf.put_slice(&[compression.is_some() as u8, 0, 0, 0, 0]); + + if let Some(compression) = compression { + let mut data = BytesMut::new(); + encoder.encode(message, &mut data)?; + let data = compression.encode(&data).await?; + buf.extend(data); + } else { + encoder.encode(message, buf)?; + } + let msg_len = (buf.len() - 5) as u32; buf.as_mut()[1..5].copy_from_slice(&msg_len.to_be_bytes()); Ok(buf.split().freeze()) } -#[derive(Default)] struct DataFrameDecoder { buf: BytesMut, + compression: Option, } impl DataFrameDecoder { + #[inline] + fn new(compression: Option) -> Self { + Self { + buf: BytesMut::new(), + compression, + } + } + + #[inline] fn put_slice(&mut self, data: impl AsRef<[u8]>) { self.buf.extend_from_slice(data.as_ref()); } + #[inline] fn check_incomplete(&self) -> Result<(), Status> { if !self.buf.is_empty() { return Err(Status::new(Code::Internal).with_message("incomplete request")); @@ -44,7 +62,7 @@ impl DataFrameDecoder { Ok(()) } - fn next(&mut self) -> Result, Status> { + async fn next(&mut self) -> Result, Status> { if self.buf.len() < 5 { return Ok(None); } @@ -62,11 +80,15 @@ impl DataFrameDecoder { let data = self.buf.split_to(len).freeze(); if compressed { - let mut decoder = GzDecoder::new(&*data); - let raw_data = BytesMut::new(); - let mut writer = raw_data.writer(); - std::io::copy(&mut decoder, &mut writer).map_err(Status::from_std_error)?; - Ok(Some(writer.into_inner().freeze())) + let compression = self.compression.ok_or_else(|| { + Status::new(Code::Unimplemented) + .with_message(format!("unsupported compressed flag: {compressed}")) + })?; + let data = compression + .decode(&data) + .await + .map_err(|err| Status::new(Code::Internal).with_message(err.to_string()))?; + Ok(Some(data.into())) } else { Ok(Some(data)) } @@ -79,18 +101,19 @@ impl DataFrameDecoder { pub(crate) fn create_decode_request_body( mut decoder: T, body: Body, + compression: Option, ) -> Streaming { let mut body: BoxBody = body.into(); Streaming::new(async_stream::try_stream! { - let mut frame_decoder = DataFrameDecoder::default(); + let mut frame_decoder = DataFrameDecoder::new(compression); loop { match body.frame().await.transpose().map_err(Status::from_std_error)? { Some(frame) => { if let Ok(data) = frame.into_data() { frame_decoder.put_slice(data); - while let Some(data) = frame_decoder.next()? { + while let Some(data) = frame_decoder.next().await? { let message = decoder.decode(&data).map_err(Status::from_std_error)?; yield message; } @@ -108,67 +131,53 @@ pub(crate) fn create_decode_request_body( pub(crate) fn create_encode_response_body( mut encoder: T, mut stream: Streaming, + compression: Option, ) -> Body { - let (tx, rx) = mpsc::channel(16); - - tokio::spawn(async move { + let stream = async_stream::try_stream! { let mut buf = BytesMut::new(); while let Some(item) = stream.next().await { match item { Ok(message) => { - if let Ok(data) = encode_data_frame(&mut encoder, &mut buf, message) { - if tx.send(Frame::data(data)).await.is_err() { - return; - } + if let Ok(data) = encode_data_frame(&mut encoder, &mut buf, message, compression).await { + yield Frame::data(data); } } Err(status) => { - _ = tx.send(Frame::trailers(status.to_headers())).await; - return; + yield Frame::trailers(status.to_headers()); } } } - _ = tx - .send(Frame::trailers(Status::new(Code::Ok).to_headers())) - .await; - }); + yield Frame::trailers(Status::new(Code::Ok).to_headers()); + }; - BodyExt::boxed(StreamBody::new( - ReceiverStream::new(rx).map(Ok::<_, IoError>), - )) - .into() + BodyExt::boxed(StreamBody::new(SyncStream::new(stream))).into() } pub(crate) fn create_encode_request_body( mut encoder: T, mut stream: Streaming, + compression: Option, ) -> Body { - let (tx, rx) = mpsc::channel(16); - - tokio::spawn(async move { + let stream = async_stream::try_stream! { let mut buf = BytesMut::new(); while let Some(Ok(message)) = stream.next().await { - if let Ok(data) = encode_data_frame(&mut encoder, &mut buf, message) { - if tx.send(Frame::data(data)).await.is_err() { - return; - } + if let Ok(data) = encode_data_frame(&mut encoder, &mut buf, message, compression).await { + yield Frame::data(data); } } - }); + }; - BodyExt::boxed(StreamBody::new( - ReceiverStream::new(rx).map(Ok::<_, IoError>), - )) - .into() + BodyExt::boxed(StreamBody::new(SyncStream::new(stream))).into() } pub(crate) fn create_decode_response_body( mut decoder: T, headers: &HeaderMap, body: Body, + compression: Option, ) -> Result, Status> { // check is trailers-only if let Some(status) = Status::from_headers(headers)? { @@ -182,14 +191,14 @@ pub(crate) fn create_decode_response_body( let mut body: BoxBody = body.into(); Ok(Streaming::new(async_stream::try_stream! { - let mut frame_decoder = DataFrameDecoder::default(); + let mut frame_decoder = DataFrameDecoder::new(compression); let mut status = None; while let Some(frame) = body.frame().await.transpose().map_err(Status::from_std_error)? { if frame.is_data() { let data = frame.into_data().unwrap(); frame_decoder.put_slice(data); - while let Some(data) = frame_decoder.next()? { + while let Some(data) = frame_decoder.next().await? { let message = decoder.decode(&data).map_err(Status::from_std_error)?; yield message; } @@ -254,7 +263,7 @@ mod tests { let mut codec = ProstCodec::::default(); let mut streaming = - create_decode_response_body(codec.decoder(), &HeaderMap::default(), body) + create_decode_response_body(codec.decoder(), &HeaderMap::default(), body, None) .expect("streaming"); let stream_msg = streaming diff --git a/poem-grpc/src/example_generated/mod.rs b/poem-grpc/src/example_generated/mod.rs new file mode 100644 index 0000000000..7dcdd38ca4 --- /dev/null +++ b/poem-grpc/src/example_generated/mod.rs @@ -0,0 +1,6 @@ +//! This module shows an example of code generated by the macro. **IT MUST NOT +//! BE USED OUTSIDE THIS CRATE**. + +#![allow(missing_docs)] + +include!(concat!(env!("OUT_DIR"), "/routeguide.rs")); diff --git a/poem-grpc/src/example_generated/routeguide.proto b/poem-grpc/src/example_generated/routeguide.proto new file mode 100644 index 0000000000..0f54eeaace --- /dev/null +++ b/poem-grpc/src/example_generated/routeguide.proto @@ -0,0 +1,110 @@ +// 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.routeguide"; +option java_outer_classname = "RouteGuideProto"; + +package routeguide; + +// Interface exported by the server. +service RouteGuide { + // A simple RPC. + // + // Obtains the feature at a given position. + // + // A feature with an empty name is returned if there's no feature at the given + // position. + rpc GetFeature(Point) returns (Feature) {} + + // A server-to-client streaming RPC. + // + // Obtains the Features available within the given Rectangle. Results are + // streamed rather than returned at once (e.g. in a response message with a + // repeated field), as the rectangle may cover a large area and contain a + // huge number of features. + rpc ListFeatures(Rectangle) returns (stream Feature) {} + + // A client-to-server streaming RPC. + // + // Accepts a stream of Points on a route being traversed, returning a + // RouteSummary when traversal is completed. + rpc RecordRoute(stream Point) returns (RouteSummary) {} + + // A Bidirectional streaming RPC. + // + // Accepts a stream of RouteNotes sent while a route is being traversed, + // while receiving other RouteNotes (e.g. from other users). + rpc RouteChat(stream RouteNote) returns (stream RouteNote) {} +} + +// Points are represented as latitude-longitude pairs in the E7 representation +// (degrees multiplied by 10**7 and rounded to the nearest integer). +// Latitudes should be in the range +/- 90 degrees and longitude should be in +// the range +/- 180 degrees (inclusive). +message Point { + int32 latitude = 1; + int32 longitude = 2; +} + +// A latitude-longitude rectangle, represented as two diagonally opposite +// points "lo" and "hi". +message Rectangle { + // One corner of the rectangle. + Point lo = 1; + + // The other corner of the rectangle. + Point hi = 2; +} + +// A feature names something at a given point. +// +// If a feature could not be named, the name is empty. +message Feature { + // The name of the feature. + string name = 1; + + // The point where the feature is detected. + Point location = 2; +} + +// A RouteNote is a message sent while at a given point. +message RouteNote { + // The location from which the message is sent. + Point location = 1; + + // The message to be sent. + string message = 2; +} + +// A RouteSummary is received in response to a RecordRoute rpc. +// +// It contains the number of individual points received, the number of +// detected features, and the total distance covered as the cumulative sum of +// the distance between each point. +message RouteSummary { + // The number of points received. + int32 point_count = 1; + + // The number of known features passed while traversing the route. + int32 feature_count = 2; + + // The distance covered in metres. + int32 distance = 3; + + // The duration of the traversal in seconds. + int32 elapsed_time = 4; +} \ No newline at end of file diff --git a/poem-grpc/src/lib.rs b/poem-grpc/src/lib.rs index 67cb9e13bf..c3fbbbaecb 100644 --- a/poem-grpc/src/lib.rs +++ b/poem-grpc/src/lib.rs @@ -20,8 +20,11 @@ pub mod service; pub mod codec; pub mod metadata; +mod compression; mod connector; mod encoding; +#[cfg(feature = "example_generated")] +pub mod example_generated; mod health; mod reflection; mod request; @@ -33,6 +36,7 @@ mod streaming; mod test_harness; pub use client::{ClientBuilderError, ClientConfig, ClientConfigBuilder}; +pub use compression::CompressionEncoding; pub use health::{health_service, HealthReporter, ServingStatus}; pub use metadata::Metadata; pub use reflection::Reflection; diff --git a/poem-grpc/src/server.rs b/poem-grpc/src/server.rs index d2ead0a91d..3e1b14de43 100644 --- a/poem-grpc/src/server.rs +++ b/poem-grpc/src/server.rs @@ -1,32 +1,54 @@ use futures_util::StreamExt; -use poem::{Request, Response}; +use http::HeaderValue; +use poem::{Body, Request, Response}; use crate::{ codec::Codec, + compression::get_incoming_encodings, encoding::{create_decode_request_body, create_encode_response_body}, service::{ BidirectionalStreamingService, ClientStreamingService, ServerStreamingService, UnaryService, }, - Code, Metadata, Request as GrpcRequest, Response as GrpcResponse, Status, Streaming, + Code, CompressionEncoding, Metadata, Request as GrpcRequest, Response as GrpcResponse, Status, + Streaming, }; #[doc(hidden)] -pub struct GrpcServer { +pub struct GrpcServer<'a, T> { codec: T, + send_compressd: Option, + accept_compressed: &'a [CompressionEncoding], } -impl GrpcServer { +impl<'a, T: Codec> GrpcServer<'a, T> { #[inline] - pub fn new(codec: T) -> Self { - Self { codec } + pub fn new( + codec: T, + send_compressd: Option, + accept_compressed: &'a [CompressionEncoding], + ) -> Self { + Self { + codec, + send_compressd, + accept_compressed, + } } - pub async fn unary(&mut self, service: S, request: Request) -> Response + pub async fn unary(mut self, service: S, request: Request) -> Response where S: UnaryService, { let (parts, body) = request.into_parts(); - let mut stream = create_decode_request_body(self.codec.decoder(), body); + let mut resp = Response::default().set_content_type(T::CONTENT_TYPES[0]); + let incoming_encoding = match get_incoming_encodings(&parts.headers, self.accept_compressed) + { + Ok(incoming_encoding) => incoming_encoding, + Err(status) => { + resp.headers_mut().extend(status.to_headers()); + return resp; + } + }; + let mut stream = create_decode_request_body(self.codec.decoder(), body, incoming_encoding); let res = match stream.next().await { Some(Ok(message)) => { @@ -44,32 +66,37 @@ impl GrpcServer { None => Err(Status::new(Code::Internal).with_message("missing request message")), }; - let mut resp = Response::default().set_content_type(T::CONTENT_TYPES[0]); - match res { Ok(grpc_resp) => { let GrpcResponse { metadata, message } = grpc_resp; let body = create_encode_response_body( self.codec.encoder(), Streaming::new(futures_util::stream::once(async move { Ok(message) })), + self.send_compressd, ); - resp.headers_mut().extend(metadata.headers); - resp.set_body(body); - } - Err(status) => { - resp.headers_mut().extend(status.to_headers()); + update_http_response(&mut resp, metadata, body, self.send_compressd); } + Err(status) => resp.headers_mut().extend(status.to_headers()), } resp } - pub async fn client_streaming(&mut self, service: S, request: Request) -> Response + pub async fn client_streaming(mut self, service: S, request: Request) -> Response where S: ClientStreamingService, { let (parts, body) = request.into_parts(); - let stream = create_decode_request_body(self.codec.decoder(), body); + let mut resp = Response::default().set_content_type(T::CONTENT_TYPES[0]); + let incoming_encoding = match get_incoming_encodings(&parts.headers, self.accept_compressed) + { + Ok(incoming_encoding) => incoming_encoding, + Err(status) => { + resp.headers_mut().extend(status.to_headers()); + return resp; + } + }; + let stream = create_decode_request_body(self.codec.decoder(), body, incoming_encoding); let res = service .call(GrpcRequest { @@ -81,17 +108,15 @@ impl GrpcServer { }) .await; - let mut resp = Response::default().set_content_type(T::CONTENT_TYPES[0]); - match res { Ok(grpc_resp) => { let GrpcResponse { metadata, message } = grpc_resp; let body = create_encode_response_body( self.codec.encoder(), Streaming::new(futures_util::stream::once(async move { Ok(message) })), + self.send_compressd, ); - resp.headers_mut().extend(metadata.headers); - resp.set_body(body); + update_http_response(&mut resp, metadata, body, self.send_compressd); } Err(status) => { resp.headers_mut().extend(status.to_headers()); @@ -101,12 +126,21 @@ impl GrpcServer { resp } - pub async fn server_streaming(&mut self, service: S, request: Request) -> Response + pub async fn server_streaming(mut self, service: S, request: Request) -> Response where S: ServerStreamingService, { let (parts, body) = request.into_parts(); - let mut stream = create_decode_request_body(self.codec.decoder(), body); + let mut resp = Response::default().set_content_type(T::CONTENT_TYPES[0]); + let incoming_encoding = match get_incoming_encodings(&parts.headers, self.accept_compressed) + { + Ok(incoming_encoding) => incoming_encoding, + Err(status) => { + resp.headers_mut().extend(status.to_headers()); + return resp; + } + }; + let mut stream = create_decode_request_body(self.codec.decoder(), body, incoming_encoding); let res = match stream.next().await { Some(Ok(message)) => { @@ -124,14 +158,12 @@ impl GrpcServer { None => Err(Status::new(Code::Internal).with_message("missing request message")), }; - let mut resp = Response::default().set_content_type(T::CONTENT_TYPES[0]); - match res { Ok(grpc_resp) => { let GrpcResponse { metadata, message } = grpc_resp; - let body = create_encode_response_body(self.codec.encoder(), message); - resp.headers_mut().extend(metadata.headers); - resp.set_body(body); + let body = + create_encode_response_body(self.codec.encoder(), message, self.send_compressd); + update_http_response(&mut resp, metadata, body, self.send_compressd); } Err(status) => { resp.headers_mut().extend(status.to_headers()); @@ -141,12 +173,21 @@ impl GrpcServer { resp } - pub async fn bidirectional_streaming(&mut self, service: S, request: Request) -> Response + pub async fn bidirectional_streaming(mut self, service: S, request: Request) -> Response where S: BidirectionalStreamingService, { let (parts, body) = request.into_parts(); - let stream = create_decode_request_body(self.codec.decoder(), body); + let mut resp = Response::default().set_content_type(T::CONTENT_TYPES[0]); + let incoming_encoding = match get_incoming_encodings(&parts.headers, self.accept_compressed) + { + Ok(incoming_encoding) => incoming_encoding, + Err(status) => { + resp.headers_mut().extend(status.to_headers()); + return resp; + } + }; + let stream = create_decode_request_body(self.codec.decoder(), body, incoming_encoding); let res = service .call(GrpcRequest { @@ -158,14 +199,12 @@ impl GrpcServer { }) .await; - let mut resp = Response::default().set_content_type(T::CONTENT_TYPES[0]); - match res { Ok(grpc_resp) => { let GrpcResponse { metadata, message } = grpc_resp; - let body = create_encode_response_body(self.codec.encoder(), message); - resp.headers_mut().extend(metadata.headers); - resp.set_body(body); + let body = + create_encode_response_body(self.codec.encoder(), message, self.send_compressd); + update_http_response(&mut resp, metadata, body, self.send_compressd); } Err(status) => { resp.headers_mut().extend(status.to_headers()); @@ -175,3 +214,19 @@ impl GrpcServer { resp } } + +fn update_http_response( + resp: &mut Response, + metadata: Metadata, + body: Body, + send_compressd: Option, +) { + resp.headers_mut().extend(metadata.headers); + if let Some(send_compressd) = send_compressd { + resp.headers_mut().insert( + "grpc-encoding", + HeaderValue::from_str(send_compressd.as_str()).expect("BUG: invalid encoding"), + ); + } + resp.set_body(body); +} diff --git a/poem-lambda/Cargo.toml b/poem-lambda/Cargo.toml index d9c227e1e1..52c493d338 100644 --- a/poem-lambda/Cargo.toml +++ b/poem-lambda/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "poem-lambda" -version = "5.0.1" +version = "5.1.0" authors.workspace = true edition.workspace = true license.workspace = true @@ -21,7 +21,7 @@ categories = [ [dependencies] poem = { workspace = true, default-features = false } -lambda_http = { version = "0.11.0" } +lambda_http = { version = "0.13.0" } [dev-dependencies] tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } diff --git a/poem-lambda/README.md b/poem-lambda/README.md index 63f34b2598..0dcec1fd3b 100644 --- a/poem-lambda/README.md +++ b/poem-lambda/README.md @@ -20,9 +20,9 @@ Unsafe Rust forbidden - - rustc 1.75.0+ + + rustc 1.76.0+ @@ -49,7 +49,7 @@ This crate uses `#![forbid(unsafe_code)]` to ensure everything is implemented in ## MSRV -The minimum supported Rust version for this crate is `1.75.0`. +The minimum supported Rust version for this crate is `1.76.0`. ## Contributing diff --git a/poem-openapi-derive/Cargo.toml b/poem-openapi-derive/Cargo.toml index 733f21e85d..13ff8c2ff8 100644 --- a/poem-openapi-derive/Cargo.toml +++ b/poem-openapi-derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "poem-openapi-derive" -version = "5.0.3" +version = "5.1.0" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/poem-openapi/CHANGELOG.md b/poem-openapi/CHANGELOG.md index 9244d0a61b..535b760177 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.0] 2024-09-08 + +- fix read_only_with_default test when only default features are enabled [#854](https://github.com/poem-web/poem/pulls) +- feat: add AsyncSeek trait to Upload::into_async_read return type [#853](https://github.com/poem-web/poem/pull/853) +- Added derivations for Type, ParseFromJSON and ToJSON for sqlx::types::Json. [#833](https://github.com/poem-web/poem/pull/833) +- chore(openapi): bump derive_more [#867](https://github.com/poem-web/poem/pull/867) + # [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) diff --git a/poem-openapi/Cargo.toml b/poem-openapi/Cargo.toml index be20f802eb..f001ce119f 100644 --- a/poem-openapi/Cargo.toml +++ b/poem-openapi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "poem-openapi" -version = "5.0.3" +version = "5.1.0" authors.workspace = true edition.workspace = true license.workspace = true @@ -23,6 +23,7 @@ hostname = ["hostname-validator"] static-files = ["poem/static-files"] websocket = ["poem/websocket"] geo = ["dep:geo-types", "dep:geojson"] +sonic-rs = ["poem/sonic-rs"] [dependencies] poem-openapi-derive.workspace = true @@ -42,7 +43,7 @@ quick-xml.workspace = true serde_urlencoded.workspace = true base64.workspace = true serde.workspace = true -derive_more = "0.99.16" +derive_more = { version = "1.0", features = ["display"] } num-traits = "0.2.14" regex.workspace = true mime.workspace = true @@ -69,10 +70,10 @@ bson = { version = "2.0.0", optional = true } rust_decimal = { version = "1.22.0", optional = true } humantime = { version = "2.1.0", optional = true } ipnet = { version = "2.7.1", optional = true } -prost-wkt-types = { version = "0.5.0", optional = true } +prost-wkt-types = { version = "0.6.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 = [ +sqlx = { version = "0.8.0", features = [ "json", "postgres", "sqlite", diff --git a/poem-openapi/README.md b/poem-openapi/README.md index 5918dca71f..974ab6a88a 100644 --- a/poem-openapi/README.md +++ b/poem-openapi/README.md @@ -21,9 +21,9 @@ Unsafe Rust forbidden - - rustc 1.75.0+ + + rustc 1.76.0+ @@ -69,6 +69,7 @@ To avoid compiling unused dependencies, Poem gates certain features, some of whi | prost-wkt-types | Integrate with the [`prost-wkt-types` crate](https://crates.io/crates/prost-wkt-types) | | static-files | Support for static file response | | websocket | Support for websocket | +|sonic-rs | Uses [`sonic-rs`](https://github.com/cloudwego/sonic-rs) instead of `serde_json`. Pls, checkout `sonic-rs` requirements to properly enable `sonic-rs` capabilities | ## Safety @@ -130,7 +131,7 @@ hello, sunli! ## MSRV -The minimum supported Rust version for this crate is `1.75.0`. +The minimum supported Rust version for this crate is `1.76.0`. ## Contributing diff --git a/poem-openapi/src/auth/mod.rs b/poem-openapi/src/auth/mod.rs index 4723342a2b..1ac7f2d7f6 100644 --- a/poem-openapi/src/auth/mod.rs +++ b/poem-openapi/src/auth/mod.rs @@ -32,7 +32,8 @@ pub trait ApiKeyAuthorization: Sized { ) -> Result; } -/// Facilitates the conversion of `Option` into `Results`, for `SecuritySchema` checker. +/// Facilitates the conversion of `Option` into `Results`, for `SecuritySchema` +/// checker. #[doc(hidden)] pub enum CheckerReturn { Result(Result), diff --git a/poem-openapi/src/base.rs b/poem-openapi/src/base.rs index b23f8f5749..4fc09bf1b5 100644 --- a/poem-openapi/src/base.rs +++ b/poem-openapi/src/base.rs @@ -95,65 +95,65 @@ impl Default for ExtractParamOptions { /// - **Path<T: Type>** /// /// Extract the parameters in the request path into -/// [`Path`](crate::param::Path). +/// [`Path`](crate::param::Path). /// /// - **Query<T: Type>** /// /// Extract the parameters in the query string into -/// [`Query`](crate::param::Query). +/// [`Query`](crate::param::Query). /// /// - **Header<T: Type>** /// /// Extract the parameters in the request header into -/// [`Header`](crate::param::Header). +/// [`Header`](crate::param::Header). /// /// - **Cookie<T: Type>** /// /// Extract the parameters in the cookie into -/// [`Cookie`](crate::param::Cookie). +/// [`Cookie`](crate::param::Cookie). /// /// - **CookiePrivate<T: Type>** /// /// Extract the parameters in the private cookie into -/// [`CookiePrivate`](crate::param::CookiePrivate). +/// [`CookiePrivate`](crate::param::CookiePrivate). /// /// - **CookieSigned<T: Type>** /// /// Extract the parameters in the signed cookie into -/// [`CookieSigned`](crate::param::CookieSigned). +/// [`CookieSigned`](crate::param::CookieSigned). /// /// - **Binary<T>** /// -/// Extract the request body as binary into -/// [`Binary`](crate::payload::Binary). +/// Extract the request body as binary into +/// [`Binary`](crate::payload::Binary). /// /// - **Json<T>** /// -/// Parse the request body in `JSON` format into -/// [`Json`](crate::payload::Json). +/// Parse the request body in `JSON` format into +/// [`Json`](crate::payload::Json). /// /// - **PlainText<T>** /// -/// Extract the request body as utf8 string into -/// [`PlainText`](crate::payload::PlainText). +/// Extract the request body as utf8 string into +/// [`PlainText`](crate::payload::PlainText). /// /// - **Any type derived from the [`ApiRequest`](crate::ApiRequest) macro** /// -/// Extract the complex request body derived from the `ApiRequest` macro. +/// Extract the complex request body derived from the `ApiRequest` macro. /// /// - **Any type derived from the [`Multipart`](crate::Multipart) macro** /// -/// Extract the multipart object derived from the `Multipart` macro. +/// Extract the multipart object derived from the `Multipart` macro. /// /// - **Any type derived from the [`SecurityScheme`](crate::SecurityScheme) /// macro** /// -/// Extract the authentication value derived from the `SecurityScheme` -/// macro. +/// Extract the authentication value derived from the `SecurityScheme` +/// macro. /// /// - **T: poem::FromRequest** /// -/// Use Poem's extractor. +/// Use Poem's extractor. #[allow(unused_variables)] pub trait ApiExtractor<'a>: Sized { /// The type of API extractor. @@ -250,24 +250,24 @@ impl ResponseContent for T { /// /// - **Binary<T: Type>** /// -/// A binary response with content type `application/octet-stream`. +/// A binary response with content type `application/octet-stream`. /// /// - **Json<T: Type>** /// -/// A JSON response with content type `application/json`. +/// A JSON response with content type `application/json`. /// /// - **PlainText<T: Type>** /// -/// A utf8 string response with content type `text/plain`. +/// A utf8 string response with content type `text/plain`. /// /// - **Attachment<T: Type>** /// -/// A file download response, the content type is -/// `application/octet-stream`. +/// A file download response, the content type is +/// `application/octet-stream`. /// /// - **Response<T: Type>** /// -/// A response type use it to modify the status code and HTTP headers. +/// A response type use it to modify the status code and HTTP headers. /// /// - **()** /// diff --git a/poem-openapi/src/lib.rs b/poem-openapi/src/lib.rs index 2586c14ba4..ad34c11e55 100644 --- a/poem-openapi/src/lib.rs +++ b/poem-openapi/src/lib.rs @@ -112,6 +112,7 @@ //! | prost-wkt-types | Integrate with the [`prost-wkt-types` crate](https://crates.io/crates/prost-wkt-types) | //! | static-files | Support for static file response | //! | websocket | Support for websocket | +//! |sonic-rs | Uses [`sonic-rs`](https://github.com/cloudwego/sonic-rs) instead of `serde_json`. Pls, checkout `sonic-rs` requirements to properly enable `sonic-rs` capabilities | #![doc(html_favicon_url = "https://raw.githubusercontent.com/poem-web/poem/master/favicon.ico")] #![doc(html_logo_url = "https://raw.githubusercontent.com/poem-web/poem/master/logo.png")] diff --git a/poem-openapi/src/registry/clean_unused.rs b/poem-openapi/src/registry/clean_unused.rs index 3ce3955f23..339eee44cf 100644 --- a/poem-openapi/src/registry/clean_unused.rs +++ b/poem-openapi/src/registry/clean_unused.rs @@ -24,7 +24,7 @@ impl<'a> Document<'a> { self.traverse_schema(used_types, schema_ref); } - for schema_ref in &schema.items { + if let Some(schema_ref) = &schema.items { self.traverse_schema(used_types, schema_ref); } diff --git a/poem-openapi/src/validation/max_items.rs b/poem-openapi/src/validation/max_items.rs index 858d1c5146..83cd688220 100644 --- a/poem-openapi/src/validation/max_items.rs +++ b/poem-openapi/src/validation/max_items.rs @@ -9,7 +9,7 @@ use crate::{ }; #[derive(Display)] -#[display(fmt = "maxItems({len})")] +#[display("maxItems({len})")] pub struct MaxItems { len: usize, } diff --git a/poem-openapi/src/validation/max_length.rs b/poem-openapi/src/validation/max_length.rs index 8bd0b9a10a..759ddcce2e 100644 --- a/poem-openapi/src/validation/max_length.rs +++ b/poem-openapi/src/validation/max_length.rs @@ -6,7 +6,7 @@ use crate::{ }; #[derive(Display)] -#[display(fmt = "maxLength({len})")] +#[display("maxLength({len})")] pub struct MaxLength { len: usize, } diff --git a/poem-openapi/src/validation/max_properties.rs b/poem-openapi/src/validation/max_properties.rs index 5250fa6d49..c3d6fa95f9 100644 --- a/poem-openapi/src/validation/max_properties.rs +++ b/poem-openapi/src/validation/max_properties.rs @@ -8,7 +8,7 @@ use crate::{ }; #[derive(Display)] -#[display(fmt = "maxProperties({len})")] +#[display("maxProperties({len})")] pub struct MaxProperties { len: usize, } diff --git a/poem-openapi/src/validation/maximum.rs b/poem-openapi/src/validation/maximum.rs index d09000a87d..5e2168b006 100644 --- a/poem-openapi/src/validation/maximum.rs +++ b/poem-openapi/src/validation/maximum.rs @@ -7,7 +7,7 @@ use crate::{ }; #[derive(Display)] -#[display(fmt = "maximum({n}, exclusive: {exclusive})")] +#[display("maximum({n}, exclusive: {exclusive})")] pub struct Maximum { n: f64, exclusive: bool, diff --git a/poem-openapi/src/validation/min_items.rs b/poem-openapi/src/validation/min_items.rs index 5f8ec00cb6..5b03687578 100644 --- a/poem-openapi/src/validation/min_items.rs +++ b/poem-openapi/src/validation/min_items.rs @@ -9,7 +9,7 @@ use crate::{ }; #[derive(Display)] -#[display(fmt = "minItems({len})")] +#[display("minItems({len})")] pub struct MinItems { len: usize, } diff --git a/poem-openapi/src/validation/min_length.rs b/poem-openapi/src/validation/min_length.rs index 2bc7fed9e8..a6f3db16e1 100644 --- a/poem-openapi/src/validation/min_length.rs +++ b/poem-openapi/src/validation/min_length.rs @@ -6,7 +6,7 @@ use crate::{ }; #[derive(Display)] -#[display(fmt = "minLength({len})")] +#[display("minLength({len})")] pub struct MinLength { len: usize, } diff --git a/poem-openapi/src/validation/min_properties.rs b/poem-openapi/src/validation/min_properties.rs index 58190f0991..4b2fe6177a 100644 --- a/poem-openapi/src/validation/min_properties.rs +++ b/poem-openapi/src/validation/min_properties.rs @@ -8,7 +8,7 @@ use crate::{ }; #[derive(Display)] -#[display(fmt = "minProperties({len})")] +#[display("minProperties({len})")] pub struct MinProperties { len: usize, } diff --git a/poem-openapi/src/validation/minimum.rs b/poem-openapi/src/validation/minimum.rs index 60251a7836..f710f451fe 100644 --- a/poem-openapi/src/validation/minimum.rs +++ b/poem-openapi/src/validation/minimum.rs @@ -7,7 +7,7 @@ use crate::{ }; #[derive(Display)] -#[display(fmt = "minimum({n}, exclusive: {exclusive})")] +#[display("minimum({n}, exclusive: {exclusive})")] pub struct Minimum { n: f64, exclusive: bool, diff --git a/poem-openapi/src/validation/multiple_of.rs b/poem-openapi/src/validation/multiple_of.rs index 30a654288b..08cf33feee 100644 --- a/poem-openapi/src/validation/multiple_of.rs +++ b/poem-openapi/src/validation/multiple_of.rs @@ -7,7 +7,7 @@ use crate::{ }; #[derive(Display)] -#[display(fmt = "multipleOf({n})")] +#[display("multipleOf({n})")] pub struct MultipleOf { n: f64, } diff --git a/poem-openapi/src/validation/pattern.rs b/poem-openapi/src/validation/pattern.rs index 8ff9c89399..e0b5663dc4 100644 --- a/poem-openapi/src/validation/pattern.rs +++ b/poem-openapi/src/validation/pattern.rs @@ -7,7 +7,7 @@ use crate::{ }; #[derive(Display)] -#[display(fmt = "pattern(\"{pattern}\")")] +#[display("pattern(\"{pattern}\")")] pub struct Pattern { pattern: &'static str, } diff --git a/poem-openapi/src/validation/unique_items.rs b/poem-openapi/src/validation/unique_items.rs index 93a020baa0..09fb43f578 100644 --- a/poem-openapi/src/validation/unique_items.rs +++ b/poem-openapi/src/validation/unique_items.rs @@ -9,7 +9,7 @@ use crate::{ }; #[derive(Display, Default)] -#[display(fmt = "uniqueItems()")] +#[display("uniqueItems()")] pub struct UniqueItems; impl UniqueItems { diff --git a/poem-openapi/tests/api.rs b/poem-openapi/tests/api.rs index 27d2d30017..c5b9e31a79 100644 --- a/poem-openapi/tests/api.rs +++ b/poem-openapi/tests/api.rs @@ -953,6 +953,7 @@ async fn hidden() { #[test] fn issue_405() { + #[allow(dead_code)] struct Api; #[OpenApi] @@ -963,11 +964,13 @@ fn issue_405() { operation_id = "hello", transform = "my_transformer" )] + #[allow(dead_code)] async fn index(&self) -> PlainText { PlainText("hello, world!".to_string()) } } + #[allow(dead_code)] fn my_transformer(ep: impl Endpoint) -> impl Endpoint { ep.map_to_response() } diff --git a/poem-openapi/tests/object.rs b/poem-openapi/tests/object.rs index 3945b07f3e..975f7badb7 100644 --- a/poem-openapi/tests/object.rs +++ b/poem-openapi/tests/object.rs @@ -1021,11 +1021,14 @@ fn object_default_override_by_field() { ); } -// NOTE(Rennorb): The `serialize_with` and `deserialize_with` attributes don't add any additional validation, -// it's up to the library consumer to use them in ways were they don't violate the OpenAPI specification of the underlying type. +// NOTE(Rennorb): The `serialize_with` and `deserialize_with` attributes don't +// add any additional validation, it's up to the library consumer to use them in +// ways were they don't violate the OpenAPI specification of the underlying +// type. // -// In practice `serialize_with` only exists for the rounding case below, which could not be implemented in a different way before this -// (only by using a larger type), and `deserialize_with` just exists for parity. +// In practice `serialize_with` only exists for the rounding case below, which +// could not be implemented in a different way before this (only by using a +// larger type), and `deserialize_with` just exists for parity. #[test] fn serialize_with() { @@ -1036,8 +1039,9 @@ fn serialize_with() { b: f32, } - // NOTE(Rennorb): Function signature in complice with `to_json` in the Type system. - // Would prefer the usual way of implementing this with a serializer reference, but this has to do for now. + // NOTE(Rennorb): Function signature in complice with `to_json` in the Type + // system. Would prefer the usual way of implementing this with a serializer + // reference, but this has to do for now. fn round(v: &f32) -> Option { Some(serde_json::Value::from((*v as f64 * 1e5).round() / 1e5)) } @@ -1055,8 +1059,9 @@ fn deserialize_with() { a: i32, } - // NOTE(Rennorb): Function signature in complice with `parse_from_json` in the Type system. - // Would prefer the usual way of implementing this with a serializer reference, but this has to do for now. + // NOTE(Rennorb): Function signature in complice with `parse_from_json` in the + // Type system. Would prefer the usual way of implementing this with a + // serializer reference, but this has to do for now. fn add(value: Option) -> poem_openapi::types::ParseResult { value .as_ref() diff --git a/poem-openapi/tests/webhook.rs b/poem-openapi/tests/webhook.rs index 4897cf13a9..618b9e559d 100644 --- a/poem-openapi/tests/webhook.rs +++ b/poem-openapi/tests/webhook.rs @@ -13,6 +13,7 @@ use poem_openapi::{ #[tokio::test] async fn name() { #[Webhook] + #[allow(dead_code)] trait MyWebhooks { #[oai(name = "a", method = "post")] fn test1(&self); @@ -28,6 +29,7 @@ async fn name() { #[tokio::test] async fn method() { #[Webhook] + #[allow(dead_code)] trait MyWebhooks { #[oai(method = "post")] fn test1(&self); @@ -43,6 +45,7 @@ async fn method() { #[tokio::test] async fn deprecated() { #[Webhook] + #[allow(dead_code)] trait MyWebhooks { #[oai(method = "post")] fn test1(&self); @@ -65,6 +68,7 @@ async fn tags() { } #[Webhook(tag = "MyTags::A")] + #[allow(dead_code)] trait MyWebhooks: Sync { #[oai(method = "post", tag = "MyTags::B", tag = "MyTags::C")] fn test1(&self); @@ -83,6 +87,7 @@ async fn tags() { #[tokio::test] async fn operation_id() { #[Webhook] + #[allow(dead_code)] trait MyWebhooks { #[oai(method = "post", operation_id = "a")] fn test1(&self); @@ -104,6 +109,7 @@ async fn operation_id() { #[tokio::test] async fn parameters() { #[Webhook] + #[allow(dead_code)] trait MyWebhooks { #[oai(method = "post")] fn test(&self, a: Query, b: Path); @@ -139,6 +145,7 @@ async fn request_body() { #[Webhook] trait MyWebhooks { #[oai(method = "post")] + #[allow(dead_code)] fn test(&self, req: Json); } @@ -160,6 +167,7 @@ async fn response() { #[Webhook] trait MyWebhooks { #[oai(method = "post")] + #[allow(dead_code)] fn test(&self) -> Json; } @@ -184,6 +192,7 @@ async fn create() { #[Webhook] trait MyWebhooks { #[oai(method = "post")] + #[allow(dead_code)] fn test(&self) -> Json; } @@ -198,6 +207,7 @@ async fn external_docs() { method = "post", external_docs = "https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md" )] + #[allow(dead_code)] fn test(&self); } diff --git a/poem/CHANGELOG.md b/poem/CHANGELOG.md index 23b3c48619..c04e0e7137 100644 --- a/poem/CHANGELOG.md +++ b/poem/CHANGELOG.md @@ -4,6 +4,20 @@ 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.0] 2024-09-28 + +- build(deps): update nix requirement from 0.28.0 to 0.29.0 [#851](https://github.com/poem-web/poem/pull/851) +- Add manual `Default` implementation for `#[handler]` [#848](https://github.com/poem-web/poem/pull/848) +- 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`, up `tokio`, `openssl` to avoid security warning [#857](https://github.com/poem-web/poem/pull/857) +- feat: allow to set a custom name for the csrf cookie (#801) [#864](https://github.com/poem-web/poem/pull/864) +- add `sonic-rs` feature to replace `serde_json` with `sonic_rs` [#819](https://github.com/poem-web/poem/pull/819) +- fix: setting path pattern in opentelemetry traces [#878](https://github.com/poem-web/poem/pull/878) +- update MSRV to `1.76.0` +- feat: implement StdErr for poem::Error [#868](https://github.com/poem-web/poem/pull/868) + + # [3.0.4] 2024-07-27 - Add manual Default implementation for `#[handler]` [#848](https://github.com/poem-web/poem/pull/848) diff --git a/poem/Cargo.toml b/poem/Cargo.toml index 7c07cc4fd4..3d789c2f28 100644 --- a/poem/Cargo.toml +++ b/poem/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "poem" -version = "3.0.4" +version = "3.1.0" authors.workspace = true edition.workspace = true license.workspace = true @@ -67,6 +67,7 @@ embed = ["rust-embed", "hex", "mime_guess"] xml = ["quick-xml"] yaml = ["serde_yaml"] requestid = ["dep:uuid"] +sonic-rs = ["dep:sonic-rs"] [dependencies] poem-derive.workspace = true @@ -80,6 +81,7 @@ http-body-util = "0.1.0" tokio = { workspace = true, features = ["sync", "time", "macros", "net"] } tokio-util = { version = "0.7.0", features = ["io"] } serde.workspace = true +sonic-rs = { workspace = true, optional = true } serde_json.workspace = true serde_urlencoded.workspace = true parking_lot = "0.12.0" @@ -130,7 +132,7 @@ libcookie = { package = "cookie", version = "0.18", features = [ ], optional = true } opentelemetry-http = { version = "0.13.0", optional = true } opentelemetry-semantic-conventions = { version = "0.16.0", optional = true } -opentelemetry-prometheus = { version = "0.16.0", optional = true } +opentelemetry-prometheus = { version = "0.17.0", optional = true } libprometheus = { package = "prometheus", version = "0.13.0", optional = true } libopentelemetry = { package = "opentelemetry", version = "0.24.0", features = [ "metrics", @@ -162,7 +164,7 @@ tokio-stream = { workspace = true, optional = true } # Feature optional dependencies anyhow = { version = "1.0.0", optional = true } -eyre06 = { package = "eyre", version = "0.6", optional = true } +eyre06 = { package = "eyre", version = "0.6.12", optional = true } uuid = { version = "1.8.0", optional = true, default-features = false, features = [ "v4", ] } diff --git a/poem/README.md b/poem/README.md index 285627081b..dab8626c4e 100644 --- a/poem/README.md +++ b/poem/README.md @@ -20,9 +20,9 @@ Unsafe Rust forbidden - - rustc 1.75.0+ + + rustc 1.76.0+ @@ -78,7 +78,7 @@ which are disabled by default: | xml | Integrate with [`quick-xml`](https://crates.io/crates/quick-xml) crate. | | yaml | Integrate with [`serde-yaml`](https://crates.io/crates/serde-yaml) crate. | |requestid |Associates an unique ID with each incoming request | - +|sonic-rs | Uses [`sonic-rs`](https://github.com/cloudwego/sonic-rs) instead of `serde_json`. Pls, checkout `sonic-rs` requirements to properly enable `sonic-rs` capabilities | ## Safety This crate uses `#![forbid(unsafe_code)]` to ensure everything is implemented in 100% Safe Rust. @@ -108,7 +108,7 @@ More examples can be found [here][examples]. ## MSRV -The minimum supported Rust version for this crate is `1.75.0`. +The minimum supported Rust version for this crate is `1.76.0`. ## Contributing diff --git a/poem/src/body.rs b/poem/src/body.rs index 85eebeddd5..8d8a690fc1 100644 --- a/poem/src/body.rs +++ b/poem/src/body.rs @@ -142,10 +142,17 @@ impl Body { } /// Create a body object from JSON. + #[cfg(not(feature = "sonic-rs"))] pub fn from_json(body: impl Serialize) -> serde_json::Result { Ok(serde_json::to_vec(&body)?.into()) } + /// Create a body object from JSON. + #[cfg(feature = "sonic-rs")] + pub fn from_json(body: impl Serialize) -> sonic_rs::Result { + Ok(sonic_rs::to_vec(&body)?.into()) + } + /// Create an empty body. #[inline] pub fn empty() -> Self { @@ -234,7 +241,14 @@ impl Body { /// - [`ReadBodyError`] /// - [`ParseJsonError`] pub async fn into_json(self) -> Result { - Ok(serde_json::from_slice(&self.into_vec().await?).map_err(ParseJsonError::Parse)?) + #[cfg(not(feature = "sonic-rs"))] + { + Ok(serde_json::from_slice(&self.into_vec().await?).map_err(ParseJsonError::Parse)?) + } + #[cfg(feature = "sonic-rs")] + { + Ok(sonic_rs::from_slice(&self.into_vec().await?).map_err(ParseJsonError::Parse)?) + } } /// Consumes this body object and parse it as `T`. diff --git a/poem/src/error.rs b/poem/src/error.rs index b55746208d..6c8c354e8c 100644 --- a/poem/src/error.rs +++ b/poem/src/error.rs @@ -218,6 +218,19 @@ impl Display for Error { } } +impl StdError for Error { + fn source(&self) -> Option<&(dyn StdError + 'static)> { + match &self.source { + Some(ErrorSource::BoxedError(err)) => Some(err.as_ref()), + #[cfg(feature = "anyhow")] + Some(ErrorSource::Anyhow(err)) => Some(err.as_ref()), + #[cfg(feature = "eyre06")] + Some(ErrorSource::Eyre06(err)) => Some(err.as_ref()), + None => None, + } + } +} + impl From for Error { fn from(_: Infallible) -> Self { unreachable!() @@ -689,7 +702,13 @@ pub enum ParseCookieError { /// Cookie value is illegal. #[error("cookie is illegal: {0}")] + #[cfg(not(feature = "sonic-rs"))] ParseJsonValue(#[from] serde_json::Error), + + /// Cookie value is illegal. + #[error("cookie is illegal: {0}")] + #[cfg(feature = "sonic-rs")] + ParseJsonValue(#[from] sonic_rs::Error), } #[cfg(feature = "cookie")] @@ -749,7 +768,13 @@ pub enum ParseJsonError { /// Url decode error. #[error("parse error: {0}")] + #[cfg(not(feature = "sonic-rs"))] Parse(#[from] serde_json::Error), + + /// Url decode error. + #[error("parse error: {0}")] + #[cfg(feature = "sonic-rs")] + Parse(#[from] sonic_rs::Error), } impl ResponseError for ParseJsonError { diff --git a/poem/src/lib.rs b/poem/src/lib.rs index 58c9c6f11f..3ef248278f 100644 --- a/poem/src/lib.rs +++ b/poem/src/lib.rs @@ -256,6 +256,7 @@ //! | embed | Integrate with [`rust-embed`](https://crates.io/crates/rust-embed) crate. | //! | xml | Integrate with [`quick-xml`](https://crates.io/crates/quick-xml) crate. | //! | yaml | Integrate with [`serde-yaml`](https://crates.io/crates/serde-yaml) crate. | +//! |sonic-rs | Uses [`sonic-rs`](https://github.com/cloudwego/sonic-rs) instead of `serde_json`. Pls, checkout `sonic-rs` requirements to properly enable `sonic-rs` capabilities | #![doc(html_favicon_url = "https://raw.githubusercontent.com/poem-web/poem/master/favicon.ico")] #![doc(html_logo_url = "https://raw.githubusercontent.com/poem-web/poem/master/logo.png")] diff --git a/poem/src/listener/acme/client.rs b/poem/src/listener/acme/client.rs index b3c4b55fea..85ae76e7c0 100644 --- a/poem/src/listener/acme/client.rs +++ b/poem/src/listener/acme/client.rs @@ -26,8 +26,9 @@ pub struct AcmeClient { } impl AcmeClient { - /// Create a new client. `directory_url` is the url for the ACME provider. `contacts` is a list - /// of URLS (ex: `mailto:`) the ACME service can use to reach you if there's issues with your certificates. + /// Create a new client. `directory_url` is the url for the ACME provider. + /// `contacts` is a list of URLS (ex: `mailto:`) the ACME service can + /// use to reach you if there's issues with your certificates. pub async fn try_new(directory_url: &str, contacts: Vec) -> IoResult { let client = Client::new(); let directory = get_directory(&client, directory_url).await?; diff --git a/poem/src/listener/acme/endpoint.rs b/poem/src/listener/acme/endpoint.rs index 2ede375aa4..0efee807b3 100644 --- a/poem/src/listener/acme/endpoint.rs +++ b/poem/src/listener/acme/endpoint.rs @@ -9,8 +9,8 @@ use crate::{error::NotFoundError, Endpoint, IntoResponse, Request, Response, Res pub struct Http01TokensMap(Arc>>); impl Http01TokensMap { - /// Create a new http01 challenge tokens storage for use in challenge endpoint - /// and [`issue_cert`]. + /// Create a new http01 challenge tokens storage for use in challenge + /// endpoint and [`issue_cert`]. #[inline] pub fn new() -> Self { Self::default() diff --git a/poem/src/listener/acme/jose.rs b/poem/src/listener/acme/jose.rs index e79bad5236..da7fb1a55a 100644 --- a/poem/src/listener/acme/jose.rs +++ b/poem/src/listener/acme/jose.rs @@ -32,9 +32,14 @@ impl<'a> Protected<'a> { nonce, url, }; + #[cfg(not(feature = "sonic-rs"))] let protected = serde_json::to_vec(&protected).map_err(|err| { IoError::new(ErrorKind::Other, format!("failed to encode jwt: {err}")) })?; + #[cfg(feature = "sonic-rs")] + let protected = sonic_rs::to_vec(&protected).map_err(|err| { + IoError::new(ErrorKind::Other, format!("failed to encode jwt: {err}")) + })?; Ok(URL_SAFE_NO_PAD.encode(protected)) } } @@ -78,9 +83,14 @@ impl Jwk { x: &self.x, y: &self.y, }; + #[cfg(not(feature = "sonic-rs"))] let json = serde_json::to_vec(&jwk_thumb).map_err(|err| { IoError::new(ErrorKind::Other, format!("failed to encode jwt: {err}")) })?; + #[cfg(feature = "sonic-rs")] + let json = sonic_rs::to_vec(&jwk_thumb).map_err(|err| { + IoError::new(ErrorKind::Other, format!("failed to encode jwt: {err}")) + })?; let hash = sha256(json); Ok(URL_SAFE_NO_PAD.encode(hash)) } @@ -111,9 +121,17 @@ pub(crate) async fn request( }; let protected = Protected::base64(jwk, kid, nonce, uri)?; let payload = match payload { - Some(payload) => serde_json::to_vec(&payload).map_err(|err| { - IoError::new(ErrorKind::Other, format!("failed to encode payload: {err}")) - })?, + Some(payload) => { + #[cfg(not(feature = "sonic-rs"))] + let res = serde_json::to_vec(&payload).map_err(|err| { + IoError::new(ErrorKind::Other, format!("failed to encode payload: {err}")) + })?; + #[cfg(feature = "sonic-rs")] + let res = sonic_rs::to_vec(&payload).map_err(|err| { + IoError::new(ErrorKind::Other, format!("failed to encode payload: {err}")) + })?; + res + } None => Vec::new(), }; let payload = URL_SAFE_NO_PAD.encode(payload); @@ -166,8 +184,16 @@ where .text() .await .map_err(|_| IoError::new(ErrorKind::Other, "failed to read response"))?; - serde_json::from_str(&data) - .map_err(|err| IoError::new(ErrorKind::Other, format!("bad response: {err}"))) + #[cfg(not(feature = "sonic-rs"))] + { + serde_json::from_str(&data) + .map_err(|err| IoError::new(ErrorKind::Other, format!("bad response: {err}"))) + } + #[cfg(feature = "sonic-rs")] + { + sonic_rs::from_str(&data) + .map_err(|err| IoError::new(ErrorKind::Other, format!("bad response: {err}"))) + } } pub(crate) fn key_authorization(key: &KeyPair, token: &str) -> IoResult { diff --git a/poem/src/listener/acme/listener.rs b/poem/src/listener/acme/listener.rs index 160b09ffe1..e088bea3b2 100644 --- a/poem/src/listener/acme/listener.rs +++ b/poem/src/listener/acme/listener.rs @@ -253,11 +253,11 @@ pub struct IssueCertResult { pub rustls_key: Arc, } -/// Generate a new certificate via ACME protocol. Returns the pub cert and private -/// key in PEM format, and the private key as a Rustls object. +/// Generate a new certificate via ACME protocol. Returns the pub cert and +/// private key in PEM format, and the private key as a Rustls object. /// -/// It is up to the caller to make use of the returned certificate, this function does -/// nothing outside for the ACME protocol procedure. +/// It is up to the caller to make use of the returned certificate, this +/// function does nothing outside for the ACME protocol procedure. pub async fn issue_cert>( client: &mut AcmeClient, resolver: &ResolveServerCert, diff --git a/poem/src/middleware/add_data.rs b/poem/src/middleware/add_data.rs index 1e07e79d4c..79a8a8171a 100644 --- a/poem/src/middleware/add_data.rs +++ b/poem/src/middleware/add_data.rs @@ -1,6 +1,6 @@ use crate::{Endpoint, Middleware, Request, Result}; -/// Middleware for add any data to request. +/// Middleware for adding any data to a request. pub struct AddData { value: T, } @@ -27,7 +27,7 @@ where } } -/// Endpoint for AddData middleware. +/// Endpoint for the AddData middleware. pub struct AddDataEndpoint { inner: E, value: T, diff --git a/poem/src/middleware/catch_panic.rs b/poem/src/middleware/catch_panic.rs index adc0294450..9b95ca5b8a 100644 --- a/poem/src/middleware/catch_panic.rs +++ b/poem/src/middleware/catch_panic.rs @@ -34,7 +34,7 @@ where } } -/// Middleware for catches panics and converts them into `500 INTERNAL SERVER +/// Middleware that catches panics and converts them into `500 INTERNAL SERVER /// ERROR` responses. /// /// # Example @@ -122,7 +122,7 @@ impl Middleware for CatchPanic { } } -/// Endpoint for `PanicHandler` middleware. +/// Endpoint for the `PanicHandler` middleware. pub struct CatchPanicEndpoint { inner: E, panic_handler: H, diff --git a/poem/src/middleware/compression.rs b/poem/src/middleware/compression.rs index 2ea4fb2ac8..2bd30ae059 100644 --- a/poem/src/middleware/compression.rs +++ b/poem/src/middleware/compression.rs @@ -68,11 +68,11 @@ fn parse_accept_encoding( .map(|(coding, _)| coding) } -/// Middleware for decompress request body and compress response body. +/// Middleware to decompress the request body and compress the response body. /// -/// It selects the decompression algorithm according to the request -/// `Content-Encoding` header, and selects the compression algorithm according -/// to the request `Accept-Encoding` header. +/// The decompression algorithm is selected according to the request +/// `Content-Encoding` header, and the compression algorithm is selected +/// according to the request `Accept-Encoding` header. #[cfg_attr(docsrs, doc(cfg(feature = "compression")))] #[derive(Default)] pub struct Compression { @@ -97,7 +97,7 @@ impl Compression { } } - /// Specify the enabled algorithms (default to all) + /// Specify the enabled algorithms (defaults to all) #[must_use] #[inline] pub fn algorithms(self, algorithms: impl IntoIterator) -> Self { @@ -120,7 +120,7 @@ impl Middleware for Compression { } } -/// Endpoint for Compression middleware. +/// Endpoint for the Compression middleware. #[cfg_attr(docsrs, doc(cfg(feature = "compression")))] pub struct CompressionEndpoint { ep: E, diff --git a/poem/src/middleware/cookie_jar_manager.rs b/poem/src/middleware/cookie_jar_manager.rs index 24a153a61a..5d8474e515 100644 --- a/poem/src/middleware/cookie_jar_manager.rs +++ b/poem/src/middleware/cookie_jar_manager.rs @@ -42,7 +42,7 @@ where } } -/// Endpoint for `CookieJarManager` middleware. +/// Endpoint for the `CookieJarManager` middleware. #[cfg_attr(docsrs, doc(cfg(feature = "cookie")))] pub struct CookieJarManagerEndpoint { inner: E, diff --git a/poem/src/middleware/cors.rs b/poem/src/middleware/cors.rs index 57a30feda3..8ae453af27 100644 --- a/poem/src/middleware/cors.rs +++ b/poem/src/middleware/cors.rs @@ -58,7 +58,7 @@ impl Cors { } } - /// Set allow credentials. + /// Set the allow credentials. #[must_use] pub fn allow_credentials(mut self, allow_credentials: bool) -> Self { self.allow_credentials = allow_credentials; @@ -67,7 +67,7 @@ impl Cors { /// Add an allow header. /// - /// NOTE: Default is allow any header. + /// NOTE: The default is to allow any header. #[must_use] pub fn allow_header(mut self, header: T) -> Self where @@ -95,7 +95,7 @@ impl Cors { /// Add an allow method. /// - /// NOTE: Default is allow any method. + /// NOTE: The default is to allow any method. #[must_use] pub fn allow_method(mut self, method: T) -> Self where @@ -123,7 +123,7 @@ impl Cors { /// Add an allow origin. /// - /// NOTE: Default is allow any origin. + /// NOTE: The default is to allow any origin. #[must_use] pub fn allow_origin(mut self, origin: T) -> Self where diff --git a/poem/src/middleware/csrf.rs b/poem/src/middleware/csrf.rs index 63b3e87731..103bdeab62 100644 --- a/poem/src/middleware/csrf.rs +++ b/poem/src/middleware/csrf.rs @@ -104,13 +104,22 @@ impl Csrf { Default::default() } + /// Sets the name of the csrf cookie. Default is `poem-csrf-token`. + #[must_use] + pub fn cookie_name(self, value: impl Into) -> Self { + Self { + cookie_name: value.into(), + ..self + } + } + /// Sets AES256 key to provide signed, encrypted CSRF tokens and cookies. #[must_use] pub fn key(self, key: [u8; 32]) -> Self { Self { key, ..self } } - /// Sets the `Secure` to the csrf cookie. Default is `true`. + /// Sets, whether `Secure` is set for the csrf cookie. Defaults to `true`. #[must_use] pub fn secure(self, value: bool) -> Self { Self { @@ -119,7 +128,7 @@ impl Csrf { } } - /// Sets the `HttpOnly` to the csrf cookie. Default is `true`. + /// Sets, whether `HttpOnly` is set for the csrf cookie. Defaults to `true`. #[must_use] pub fn http_only(self, value: bool) -> Self { Self { @@ -128,7 +137,7 @@ impl Csrf { } } - /// Sets the `SameSite` to the csrf cookie. Default is + /// Sets, whether `SameSite` is set for the csrf cookie. Defaults to `true`. /// [`SameSite::Strict`](libcookie::SameSite::Strict). #[must_use] pub fn same_site(self, value: impl Into>) -> Self { @@ -165,7 +174,7 @@ impl Middleware for Csrf { } } -/// Endpoint for Csrf middleware. +/// Endpoint for the Csrf middleware. #[cfg_attr(docsrs, doc(cfg(feature = "csrf")))] pub struct CsrfEndpoint { inner: E, diff --git a/poem/src/middleware/force_https.rs b/poem/src/middleware/force_https.rs index 0d8a209b78..ccdef17dc6 100644 --- a/poem/src/middleware/force_https.rs +++ b/poem/src/middleware/force_https.rs @@ -6,7 +6,7 @@ use crate::{web::Redirect, Endpoint, IntoResponse, Middleware, Request, Response type FilterFn = Arc bool + Send + Sync>; -/// Middleware for force redirect to HTTPS uri. +/// Middleware which forces redirects to a HTTPS uri. #[derive(Default)] pub struct ForceHttps { https_port: Option, @@ -14,12 +14,12 @@ pub struct ForceHttps { } impl ForceHttps { - /// Create new `ForceHttps` middleware. + /// Create a new `ForceHttps` middleware. pub fn new() -> Self { Default::default() } - /// Specify https port. + /// Specify the https port. #[must_use] pub fn https_port(self, port: u16) -> Self { Self { @@ -53,7 +53,7 @@ where } } -/// Endpoint for ForceHttps middleware. +/// Endpoint for the ForceHttps middleware. pub struct ForceHttpsEndpoint { inner: E, https_port: Option, diff --git a/poem/src/middleware/mod.rs b/poem/src/middleware/mod.rs index 79dbc78f98..bd5ce74f29 100644 --- a/poem/src/middleware/mod.rs +++ b/poem/src/middleware/mod.rs @@ -59,14 +59,14 @@ use crate::endpoint::Endpoint; /// Represents a middleware trait. /// -/// # Create you own middleware +/// # Create your own middleware /// /// ``` /// use poem::{ /// handler, test::TestClient, web::Data, Endpoint, EndpointExt, Middleware, Request, Result, /// }; /// -/// /// A middleware that extract token from HTTP headers. +/// /// A middleware that extracts token from HTTP headers. /// struct TokenMiddleware; /// /// impl Middleware for TokenMiddleware { diff --git a/poem/src/middleware/normalize_path.rs b/poem/src/middleware/normalize_path.rs index f903e0acea..5537807cbc 100644 --- a/poem/src/middleware/normalize_path.rs +++ b/poem/src/middleware/normalize_path.rs @@ -71,7 +71,7 @@ impl Middleware for NormalizePath { } } -/// Endpoint for NormalizePath middleware. +/// Endpoint for the NormalizePath middleware. pub struct NormalizePathEndpoint { inner: E, merge_slash: Regex, diff --git a/poem/src/middleware/opentelemetry_metrics.rs b/poem/src/middleware/opentelemetry_metrics.rs index 82da58d307..fdb53062e5 100644 --- a/poem/src/middleware/opentelemetry_metrics.rs +++ b/poem/src/middleware/opentelemetry_metrics.rs @@ -60,7 +60,7 @@ impl Middleware for OpenTelemetryMetrics { } } -/// Endpoint for OpenTelemetryMetrics middleware. +/// Endpoint for the OpenTelemetryMetrics middleware. #[cfg_attr(docsrs, doc(cfg(feature = "opentelemetry")))] pub struct OpenTelemetryMetricsEndpoint { request_count: Counter, diff --git a/poem/src/middleware/opentelemetry_tracing.rs b/poem/src/middleware/opentelemetry_tracing.rs index 04b996b86f..e09e9f5e61 100644 --- a/poem/src/middleware/opentelemetry_tracing.rs +++ b/poem/src/middleware/opentelemetry_tracing.rs @@ -45,7 +45,7 @@ where } } -/// Endpoint for `OpenTelemetryTracing` middleware. +/// Endpoint for the `OpenTelemetryTracing` middleware. #[cfg_attr(docsrs, doc(cfg(feature = "opentelemetry")))] pub struct OpenTelemetryTracingEndpoint { tracer: Arc, @@ -96,14 +96,10 @@ where format!("{:?}", req.version()), )); - if let Some(path_pattern) = req.data::() { - const HTTP_PATH_PATTERN: Key = Key::from_static_str("http.path_pattern"); - attributes.push(KeyValue::new(HTTP_PATH_PATTERN, path_pattern.0.to_string())); - } - + let method = req.method().to_string(); let mut span = self .tracer - .span_builder(format!("{} {}", req.method(), req.uri())) + .span_builder(format!("{} {}", method, req.uri())) .with_kind(SpanKind::Server) .with_attributes(attributes) .start_with_context(&*self.tracer, &parent_cx); @@ -118,6 +114,16 @@ where match res { Ok(resp) => { let resp = resp.into_response(); + + if let Some(path_pattern) = resp.data::() { + const HTTP_PATH_PATTERN: Key = Key::from_static_str("http.path_pattern"); + span.update_name(format!("{} {}", method, path_pattern.0)); + span.set_attribute(KeyValue::new( + HTTP_PATH_PATTERN, + path_pattern.0.to_string(), + )); + } + span.add_event("request.completed".to_string(), vec![]); span.set_attribute(KeyValue::new( attribute::HTTP_RESPONSE_STATUS_CODE, @@ -134,6 +140,15 @@ where Ok(resp) } Err(err) => { + if let Some(path_pattern) = err.data::() { + const HTTP_PATH_PATTERN: Key = Key::from_static_str("http.path_pattern"); + span.update_name(format!("{} {}", method, path_pattern.0)); + span.set_attribute(KeyValue::new( + HTTP_PATH_PATTERN, + path_pattern.0.to_string(), + )); + } + span.set_attribute(KeyValue::new( attribute::HTTP_RESPONSE_STATUS_CODE, err.status().as_u16() as i64, diff --git a/poem/src/middleware/propagate_header.rs b/poem/src/middleware/propagate_header.rs index d96de9f9c8..07f947a7a8 100644 --- a/poem/src/middleware/propagate_header.rs +++ b/poem/src/middleware/propagate_header.rs @@ -4,7 +4,7 @@ use http::{header::HeaderName, HeaderMap}; use crate::{Endpoint, IntoResponse, Middleware, Request, Response, Result}; -/// Middleware for propagate a header from the request to the response. +/// Middleware to propagate a header from the request to the response. #[derive(Default)] pub struct PropagateHeader { headers: HashSet, @@ -41,7 +41,7 @@ impl Middleware for PropagateHeader { } } -/// Endpoint for PropagateHeader middleware. +/// Endpoint for the PropagateHeader middleware. pub struct PropagateHeaderEndpoint { inner: E, headers: HashSet, diff --git a/poem/src/middleware/requestid.rs b/poem/src/middleware/requestid.rs index 913220e016..548c584b7b 100644 --- a/poem/src/middleware/requestid.rs +++ b/poem/src/middleware/requestid.rs @@ -8,7 +8,7 @@ use crate::{ const X_REQUEST_ID: &str = "x-request-id"; -/// Weather to use the request ID supplied in the request. +/// Whether to use the request ID supplied in the request. #[derive(Clone, Copy, PartialEq, Eq, Default)] #[cfg_attr(docsrs, doc(cfg(feature = "requestid")))] pub enum ReuseId { @@ -42,7 +42,7 @@ impl RequestId { } } - /// Configure weather to use the incoming ID. + /// Configure whether to use the incoming ID. #[must_use] pub fn reuse_id(self, reuse_id: ReuseId) -> Self { Self { @@ -72,7 +72,7 @@ impl Middleware for RequestId { } } -/// Endpoint for `RequestId` middleware. +/// Endpoint for the `RequestId` middleware. #[cfg_attr(docsrs, doc(cfg(feature = "requestid")))] pub struct RequestIdEndpoint { next: E, @@ -103,7 +103,7 @@ impl Endpoint for RequestIdEndpoint { } } -/// A request ID that can be extracted in handler functions. +/// A request ID which can be extracted in handler functions. #[cfg_attr(docsrs, doc(cfg(feature = "requestid")))] #[derive(Clone)] pub struct ReqId(String); diff --git a/poem/src/middleware/sensitive_header.rs b/poem/src/middleware/sensitive_header.rs index 631daf6d66..1ec393f901 100644 --- a/poem/src/middleware/sensitive_header.rs +++ b/poem/src/middleware/sensitive_header.rs @@ -12,7 +12,7 @@ enum AppliedTo { Both, } -/// Middleware for mark headers value represents sensitive information. +/// Middleware to mark that a headers' value represents sensitive information. /// /// Sensitive data could represent passwords or other data that should not be /// stored on disk or in memory. By marking header values as sensitive, @@ -84,7 +84,7 @@ impl Middleware for SensitiveHeader { } } -/// Endpoint for SensitiveHeader middleware. +/// Endpoint for the SensitiveHeader middleware. pub struct SensitiveHeaderEndpoint { inner: E, headers: HashSet, diff --git a/poem/src/middleware/set_header.rs b/poem/src/middleware/set_header.rs index 31142a5022..af27e2d5e8 100644 --- a/poem/src/middleware/set_header.rs +++ b/poem/src/middleware/set_header.rs @@ -9,7 +9,7 @@ enum Action { Append(HeaderName, HeaderValue), } -/// Middleware for override/append headers to response. +/// Middleware to override or append headers to a response. /// /// # Example /// @@ -54,10 +54,10 @@ impl SetHeader { Default::default() } - /// Inserts a header to response. + /// Inserts a header into the response. /// - /// If a previous value exists for the same header, it is - /// removed and replaced with the new header value. + /// If a previous value exists for the same header, it will + /// be overridden. #[must_use] pub fn overriding(mut self, key: K, value: V) -> Self where @@ -72,7 +72,7 @@ impl SetHeader { self } - /// Appends a header to response. + /// Appends a header to the response. /// /// If previous values exist, the header will have multiple values. #[must_use] @@ -101,7 +101,7 @@ impl Middleware for SetHeader { } } -/// Endpoint for SetHeader middleware. +/// Endpoint for the SetHeader middleware. pub struct SetHeaderEndpoint { inner: E, actions: Vec, diff --git a/poem/src/middleware/size_limit.rs b/poem/src/middleware/size_limit.rs index 737116966f..d35e78a623 100644 --- a/poem/src/middleware/size_limit.rs +++ b/poem/src/middleware/size_limit.rs @@ -2,10 +2,10 @@ use crate::{ error::SizedLimitError, web::headers::HeaderMapExt, Endpoint, Middleware, Request, Result, }; -/// Middleware for limit the request payload size. +/// Middleware to limit the request payload size. /// -/// If the incoming request does not contain the `Content-Length` header, it -/// will return `LENGTH_REQUIRED` status code. +/// If the incoming request does not contain the `Content-Length` header, the +/// middleware will return the `LENGTH_REQUIRED` status code. /// /// # Errors /// @@ -32,7 +32,7 @@ impl Middleware for SizeLimit { } } -/// Endpoint for SizeLimit middleware. +/// Endpoint for the SizeLimit middleware. pub struct SizeLimitEndpoint { inner: E, max_size: usize, diff --git a/poem/src/middleware/tokio_metrics_mw.rs b/poem/src/middleware/tokio_metrics_mw.rs index 8fd33870bc..8df1b6d740 100644 --- a/poem/src/middleware/tokio_metrics_mw.rs +++ b/poem/src/middleware/tokio_metrics_mw.rs @@ -39,9 +39,18 @@ impl TokioMetrics { pub fn exporter(&self) -> impl Endpoint { let metrics = self.metrics.clone(); RouteMethod::new().get(make_sync(move |_| { - serde_json::to_string(&*metrics.lock()) - .unwrap() - .with_content_type("application/json") + #[cfg(not(feature = "sonic-rs"))] + { + serde_json::to_string(&*metrics.lock()) + .unwrap() + .with_content_type("application/json") + } + #[cfg(feature = "sonic-rs")] + { + sonic_rs::to_string(&*metrics.lock()) + .unwrap() + .with_content_type("application/json") + } })) } } @@ -71,7 +80,7 @@ impl Middleware for TokioMetrics { } } -/// Endpoint for TokioMetrics middleware. +/// Endpoint for the TokioMetrics middleware. pub struct TokioMetricsEndpoint { inner: E, monitor: TaskMonitor, diff --git a/poem/src/middleware/tower_compat.rs b/poem/src/middleware/tower_compat.rs index e0cc059a90..a56907f031 100644 --- a/poem/src/middleware/tower_compat.rs +++ b/poem/src/middleware/tower_compat.rs @@ -57,7 +57,7 @@ where } } -/// An endpoint to tower service adapter. +/// An endpoint to the tower service adapter. pub struct EndpointToTowerService(Arc); impl Service for EndpointToTowerService diff --git a/poem/src/middleware/tracing_mw.rs b/poem/src/middleware/tracing_mw.rs index 1e9bbc17f8..c72063c180 100644 --- a/poem/src/middleware/tracing_mw.rs +++ b/poem/src/middleware/tracing_mw.rs @@ -19,7 +19,7 @@ impl Middleware for Tracing { } } -/// Endpoint for `Tracing` middleware. +/// Endpoint for the `Tracing` middleware. pub struct TracingEndpoint { inner: E, } diff --git a/poem/src/server.rs b/poem/src/server.rs index d14576e5da..8113c96495 100644 --- a/poem/src/server.rs +++ b/poem/src/server.rs @@ -87,8 +87,8 @@ where } } - /// Specify connection idle timeout. Connections will be terminated if there was no activity - /// within this period of time + /// Specify connection idle timeout. Connections will be terminated if there + /// was no activity within this period of time #[must_use] pub fn idle_timeout(self, timeout: Duration) -> Self { Self { @@ -110,7 +110,8 @@ where } } - /// Configures the maximum number of pending reset streams allowed before a GOAWAY will be sent. + /// Configures the maximum number of pending reset streams allowed before a + /// GOAWAY will be sent. /// /// This will default to the default value set by the [`h2` crate](https://crates.io/crates/h2). /// As of v0.4.0, it is 20. @@ -452,6 +453,7 @@ async fn serve_connection( // Init graceful shutdown for connection conn.as_mut().graceful_shutdown(); - // Continue awaiting after graceful-shutdown is initiated to handle existed requests. + // Continue awaiting after graceful-shutdown is initiated to handle existed + // requests. let _ = conn.await; } diff --git a/poem/src/session/cookie_session.rs b/poem/src/session/cookie_session.rs index c769889f40..715404abc8 100644 --- a/poem/src/session/cookie_session.rs +++ b/poem/src/session/cookie_session.rs @@ -50,7 +50,16 @@ impl Endpoint for CookieSessionEndpoint { let session = self .config .get_cookie_value(&cookie_jar) - .and_then(|value| serde_json::from_str::>(&value).ok()) + .and_then(|value| { + #[cfg(not(feature = "sonic-rs"))] + { + serde_json::from_str::>(&value).ok() + } + #[cfg(feature = "sonic-rs")] + { + sonic_rs::from_str::>(&value).ok() + } + }) .map(Session::new) .unwrap_or_default(); @@ -59,10 +68,16 @@ impl Endpoint for CookieSessionEndpoint { match session.status() { SessionStatus::Changed | SessionStatus::Renewed => { - self.config.set_cookie_value( - &cookie_jar, - &serde_json::to_string(&session.entries()).unwrap_or_default(), - ); + self.config.set_cookie_value(&cookie_jar, { + #[cfg(not(feature = "sonic-rs"))] + { + &serde_json::to_string(&session.entries()).unwrap_or_default() + } + #[cfg(feature = "sonic-rs")] + { + &sonic_rs::to_string(&session.entries()).unwrap_or_default() + } + }); } SessionStatus::Purged => { self.config.remove_cookie(&cookie_jar); diff --git a/poem/src/session/redis_storage.rs b/poem/src/session/redis_storage.rs index 341cebf95b..54e2959506 100644 --- a/poem/src/session/redis_storage.rs +++ b/poem/src/session/redis_storage.rs @@ -33,10 +33,16 @@ impl SessionStorage for RedisStorage .map_err(RedisSessionError::Redis)?; match data { - Some(data) => match serde_json::from_str::>(&data) { - Ok(entries) => Ok(Some(entries)), - Err(_) => Ok(None), - }, + Some(data) => { + #[cfg(not(feature = "sonic-rs"))] + let map = serde_json::from_str::>(&data); + #[cfg(feature = "sonic-rs")] + let map = sonic_rs::from_str::>(&data); + match map { + Ok(entries) => Ok(Some(entries)), + Err(_) => Ok(None), + } + } None => Ok(None), } } @@ -47,7 +53,10 @@ impl SessionStorage for RedisStorage entries: &'a BTreeMap, expires: Option, ) -> Result<()> { + #[cfg(not(feature = "sonic-rs"))] let value = serde_json::to_string(entries).unwrap_or_default(); + #[cfg(feature = "sonic-rs")] + let value = sonic_rs::to_string(entries).unwrap_or_default(); let cmd = match expires { Some(expires) => Cmd::set_ex(session_id, value, expires.as_secs()), None => Cmd::set(session_id, value), diff --git a/poem/src/web/cookie.rs b/poem/src/web/cookie.rs index 93f2d67ad7..94c8fa55da 100644 --- a/poem/src/web/cookie.rs +++ b/poem/src/web/cookie.rs @@ -39,10 +39,20 @@ impl Display for Cookie { impl Cookie { /// Creates a new Cookie with the given `name` and serialized `value`. pub fn new(name: impl Into, value: impl Serialize) -> Self { - Self(libcookie::Cookie::new( - name.into(), - serde_json::to_string(&value).unwrap_or_default(), - )) + #[cfg(not(feature = "sonic-rs"))] + { + Self(libcookie::Cookie::new( + name.into(), + serde_json::to_string(&value).unwrap_or_default(), + )) + } + #[cfg(feature = "sonic-rs")] + { + Self(libcookie::Cookie::new( + name.into(), + sonic_rs::to_string(&value).unwrap_or_default(), + )) + } } /// Creates a new Cookie with the given `name` and `value`. @@ -275,7 +285,12 @@ impl Cookie { /// Sets the value of `self` to the serialized `value`. pub fn set_value(&mut self, value: impl Serialize) { - if let Ok(value) = serde_json::to_string(&value) { + #[cfg(not(feature = "sonic-rs"))] + let json_string = serde_json::to_string(&value); + #[cfg(feature = "sonic-rs")] + let json_string = sonic_rs::to_string(&value); + + if let Ok(value) = json_string { self.0.set_value(value); } } @@ -287,7 +302,14 @@ impl Cookie { /// Returns the value of `self` to the deserialized `value`. pub fn value<'de, T: Deserialize<'de>>(&'de self) -> Result { - serde_json::from_str(self.0.value()).map_err(ParseCookieError::ParseJsonValue) + #[cfg(not(feature = "sonic-rs"))] + { + serde_json::from_str(self.0.value()).map_err(ParseCookieError::ParseJsonValue) + } + #[cfg(feature = "sonic-rs")] + { + sonic_rs::from_str(self.0.value()).map_err(ParseCookieError::ParseJsonValue) + } } } diff --git a/poem/src/web/json.rs b/poem/src/web/json.rs index 2ba14ef814..d40c13da10 100644 --- a/poem/src/web/json.rs +++ b/poem/src/web/json.rs @@ -114,10 +114,20 @@ impl<'a, T: DeserializeOwned> FromRequest<'a> for Json { return Err(ParseJsonError::InvalidContentType(content_type.into()).into()); } - Ok(Self( - serde_json::from_slice(&body.take()?.into_bytes().await?) - .map_err(ParseJsonError::Parse)?, - )) + #[cfg(not(feature = "sonic-rs"))] + { + Ok(Self( + serde_json::from_slice(&body.take()?.into_bytes().await?) + .map_err(ParseJsonError::Parse)?, + )) + } + #[cfg(feature = "sonic-rs")] + { + Ok(Self( + sonic_rs::from_slice(&body.take()?.into_bytes().await?) + .map_err(ParseJsonError::Parse)?, + )) + } } } @@ -132,7 +142,12 @@ fn is_json_content_type(content_type: &str) -> bool { impl IntoResponse for Json { fn into_response(self) -> Response { - let data = match serde_json::to_vec(&self.0) { + #[cfg(not(feature = "sonic-rs"))] + let vec = serde_json::to_vec(&self.0); + #[cfg(feature = "sonic-rs")] + let vec = sonic_rs::to_vec(&self.0); + + let data = match vec { Ok(data) => data, Err(err) => { return Response::builder() @@ -149,7 +164,10 @@ impl IntoResponse for Json { #[cfg(test)] mod tests { use serde::{Deserialize, Serialize}; - use serde_json::json; + #[cfg(not(feature = "sonic-rs"))] + use serde_json::{json, to_string}; + #[cfg(feature = "sonic-rs")] + use sonic_rs::{json, to_string}; use super::*; use crate::{handler, test::TestClient}; @@ -189,7 +207,7 @@ mod tests { let cli = TestClient::new(index); cli.post("/") // .header(header::CONTENT_TYPE, "application/json") - .body(serde_json::to_string(&create_resource).expect("Invalid json")) + .body(to_string(&create_resource).expect("Invalid json")) .send() .await .assert_status(StatusCode::UNSUPPORTED_MEDIA_TYPE); diff --git a/poem/src/web/mod.rs b/poem/src/web/mod.rs index d97c9830f0..51e8538f82 100644 --- a/poem/src/web/mod.rs +++ b/poem/src/web/mod.rs @@ -117,8 +117,7 @@ impl RequestBody { /// /// - **Option<T>** /// -/// Extracts `T` from the incoming request, returns [`None`] if it -/// fails. +/// Extracts `T` from the incoming request, returns [`None`] if it fails. /// /// - **&Request** /// @@ -177,28 +176,28 @@ impl RequestBody { /// Extracts the [`Json`] from the incoming request. /// /// _This extractor will take over the requested body, so you should avoid -/// using multiple extractors of this type in one handler._ +/// using multiple extractors of this type in one handler._ /// /// - **Xml<T>** /// /// Extracts the [`Xml`] from the incoming request. /// /// _This extractor will take over the requested body, so you should avoid -/// using multiple extractors of this type in one handler._ +/// using multiple extractors of this type in one handler._ /// /// - **TempFile** /// /// Extracts the [`TempFile`] from the incoming request. /// /// _This extractor will take over the requested body, so you should avoid -/// using multiple extractors of this type in one handler._ +/// using multiple extractors of this type in one handler._ /// /// - **Multipart** /// /// Extracts the [`Multipart`] from the incoming request. /// /// _This extractor will take over the requested body, so you should avoid -/// using multiple extractors of this type in one handler._ +/// using multiple extractors of this type in one handler._ /// /// - **&CookieJar** /// @@ -209,7 +208,7 @@ impl RequestBody { /// - **&Session** /// /// Extracts the [`Session`](crate::session::Session) from the incoming -/// request. +/// request. /// /// _Requires `CookieSession` or `RedisSession` middleware._ /// @@ -218,54 +217,53 @@ impl RequestBody { /// Extracts the [`Body`] from the incoming request. /// /// _This extractor will take over the requested body, so you should avoid -/// using multiple extractors of this type in one handler._ +/// using multiple extractors of this type in one handler._ /// /// - **String** /// /// Extracts the body from the incoming request and parse it into utf8 -/// [`String`]. +/// [`String`]. /// /// _This extractor will take over the requested body, so you should avoid -/// using multiple extractors of this type in one handler._ +/// using multiple extractors of this type in one handler._ /// /// - **Vec<u8>** /// /// Extracts the body from the incoming request and collect it into -/// [`Vec`]. +/// [`Vec`]. /// /// _This extractor will take over the requested body, so you should avoid -/// using multiple extractors of this type in one handler._ +/// using multiple extractors of this type in one handler._ /// /// - **Bytes** /// /// Extracts the body from the incoming request and collect it into -/// [`Bytes`]. +/// [`Bytes`]. /// /// _This extractor will take over the requested body, so you should avoid -/// using multiple extractors of this type in one handler._ +/// using multiple extractors of this type in one handler._ /// /// - **WebSocket** /// /// Ready to accept a websocket [`WebSocket`](websocket::WebSocket) -/// connection. +/// connection. /// /// - **Locale** /// -/// Extracts the [`Locale`](crate::i18n::Locale) from the incoming -/// request. +/// Extracts the [`Locale`](crate::i18n::Locale) from the incoming request. /// /// - **StaticFileRequest** /// -/// Ready to accept a static file request -/// [`StaticFileRequest`](static_file::StaticFileRequest). +/// Ready to accept a static file request +/// [`StaticFileRequest`](static_file::StaticFileRequest). /// /// - **Accept** /// -/// Extracts the `Accept` header from the incoming request. +/// Extracts the `Accept` header from the incoming request. /// /// - **PathPattern** /// -/// Extracts the matched path pattern from the incoming request. +/// Extracts the matched path pattern from the incoming request. /// /// # Create your own extractor /// @@ -347,44 +345,46 @@ pub trait FromRequest<'a>: Sized { /// - **&'static str** /// /// Sets the status to `OK` and the `Content-Type` to `text/plain`. The -/// string is used as the body of the response. +/// string is used as the body of the response. /// /// - **String** /// /// Sets the status to `OK` and the `Content-Type` to `text/plain`. The -/// string is used as the body of the response. +/// string is used as the body of the response. /// /// - **&'static [u8]** /// /// Sets the status to `OK` and the `Content-Type` to -/// `application/octet-stream`. The slice is used as the body of the response. +/// `application/octet-stream`. The slice is used as the body of the +/// response. /// /// - **Html<T>** /// /// Sets the status to `OK` and the `Content-Type` to `text/html`. `T` is -/// used as the body of the response. +/// used as the body of the response. /// /// - **Json<T>** /// /// Sets the status to `OK` and the `Content-Type` to `application/json`. Use -/// [`serde_json`](https://crates.io/crates/serde_json) to serialize `T` into a json string. +/// [`serde_json`](https://crates.io/crates/serde_json) to serialize `T` into a json string. /// /// /// - **Xml<T>** /// /// Sets the status to `OK` and the `Content-Type` to `application/xml`. Use -/// [`quick-xml`](https://crates.io/crates/quick-xml) to serialize `T` into a xml string. +/// [`quick-xml`](https://crates.io/crates/quick-xml) to serialize `T` into a xml string. /// /// - **Bytes** /// /// Sets the status to `OK` and the `Content-Type` to -/// `application/octet-stream`. The bytes is used as the body of the response. +/// `application/octet-stream`. The bytes is used as the body of the +/// response. /// /// - **Vec<u8>** /// /// Sets the status to `OK` and the `Content-Type` to -/// `application/octet-stream`. The vector’s data is used as the body of the -/// response. +/// `application/octet-stream`. The vector’s data is used as the body of the +/// response. /// /// - **Body** /// @@ -393,7 +393,7 @@ pub trait FromRequest<'a>: Sized { /// - **StatusCode** /// /// Sets the status to the specified status code [`StatusCode`] with an empty -/// body. +/// body. /// /// - **(StatusCode, T)** /// @@ -402,7 +402,7 @@ pub trait FromRequest<'a>: Sized { /// - **(StatusCode, HeaderMap, T)** /// /// Convert `T` to response and set the specified status code [`StatusCode`], -/// and then merge the specified [`HeaderMap`]. +/// and then merge the specified [`HeaderMap`]. /// /// - **Response** /// @@ -411,14 +411,14 @@ pub trait FromRequest<'a>: Sized { /// - **Compress<T>** /// /// Call `T::into_response` to get the response, then compress the response -/// body with the specified algorithm, and set the correct `Content-Encoding` -/// header. +/// body with the specified algorithm, and set the correct `Content-Encoding` +/// header. /// /// - **SSE** /// -/// Sets the status to `OK` and the `Content-Type` to `text/event-stream` -/// with an event stream body. Use the [`SSE::new`](sse::SSE::new) function to -/// create it. +/// Sets the status to `OK` and the `Content-Type` to `text/event-stream` +/// with an event stream body. Use the [`SSE::new`](sse::SSE::new) function +/// to create it. /// /// # Create you own response ///