diff --git a/Cargo.toml b/Cargo.toml index 86afce55ef..db4ce599a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ quote = "1.0.9" syn = { version = "2.0" } tokio = "1.17.0" 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/poem-openapi/Cargo.toml b/poem-openapi/Cargo.toml index f0af31b95e..5b8721924f 100644 --- a/poem-openapi/Cargo.toml +++ b/poem-openapi/Cargo.toml @@ -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 diff --git a/poem-openapi/README.md b/poem-openapi/README.md index 5918dca71f..6ed05600e6 100644 --- a/poem-openapi/README.md +++ b/poem-openapi/README.md @@ -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 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/Cargo.toml b/poem/Cargo.toml index 7f4a799e89..00e76c6dce 100644 --- a/poem/Cargo.toml +++ b/poem/Cargo.toml @@ -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" diff --git a/poem/README.md b/poem/README.md index 285627081b..756056ac12 100644 --- a/poem/README.md +++ b/poem/README.md @@ -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. diff --git a/poem/src/body.rs b/poem/src/body.rs index 85eebeddd5..2aace6261c 100644 --- a/poem/src/body.rs +++ b/poem/src/body.rs @@ -142,10 +142,16 @@ impl Body { } /// Create a body object from JSON. + #[cfg(not(feature = "sonic-rc"))] pub fn from_json(body: impl Serialize) -> serde_json::Result { Ok(serde_json::to_vec(&body)?.into()) } + #[cfg(feature = "sonic-rc")] + 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 +240,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 143449ba3e..a865941761 100644 --- a/poem/src/error.rs +++ b/poem/src/error.rs @@ -689,7 +689,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 +755,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/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/middleware/tokio_metrics_mw.rs b/poem/src/middleware/tokio_metrics_mw.rs index 8fd33870bc..186bc4c406 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") + } })) } } 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 ab80962e7c..5176f30dba 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);