From 4207cf444d4a960df7086365dfe5f22153633724 Mon Sep 17 00:00:00 2001 From: Samuel Hicks Date: Wed, 7 Aug 2024 17:43:17 +0100 Subject: [PATCH] Added support for deserializing JSON values using the Json Extractor. Resolves #15 --- CHANGELOG.md | 6 ++ Cargo.toml | 5 +- .../hello_world_defmt/rust-toolchain.toml | 2 +- examples/embassy/rust-toolchain.toml | 2 +- src/extract.rs | 60 +++++++++++++++++++ src/json.rs | 3 + src/lib.rs | 2 + src/response/json.rs | 7 +-- 8 files changed, 79 insertions(+), 8 deletions(-) create mode 100644 src/json.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 3081755..6dfc4cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ 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.12.1] - 2024-08-07 + +### Added + +- Added support for deserializing JSON values using the [Json](https://docs.rs/picoserve/0.12.1/picoserve/extract/struct.Json.html) Extractor. + ## [0.12.0] - 2024-07-22 ### Breaking diff --git a/Cargo.toml b/Cargo.toml index af46e7a..1b5c91b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ exclude = [ [package] name = "picoserve" -version = "0.12.0" +version = "0.12.1" authors = ["Samuel Hicks"] edition = "2021" rust-version = "1.75" @@ -47,6 +47,7 @@ lhash = { version = "1.0.1", features = ["sha1"] } log = { version = "0.4.19", optional = true, default-features = false } ryu = "1.0.14" serde = { version = "1.0.171", default-features = false, features = ["derive"] } +serde-json-core = "0.6.0" tokio = { version = "1.32.0", optional = true, features = ["io-util", "net", "time"] } [features] @@ -56,7 +57,7 @@ alloc = [] tokio = ["dep:tokio", "std", "serde/std"] embassy = ["dep:embassy-time", "dep:embassy-net"] -defmt = ["dep:defmt", "embassy-net?/defmt"] +defmt = ["dep:defmt", "embassy-net?/defmt", "serde-json-core/defmt"] log = ["dep:log"] [dev-dependencies] diff --git a/examples/embassy/hello_world_defmt/rust-toolchain.toml b/examples/embassy/hello_world_defmt/rust-toolchain.toml index 29eb7db..e5c2be5 100644 --- a/examples/embassy/hello_world_defmt/rust-toolchain.toml +++ b/examples/embassy/hello_world_defmt/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] targets = ["thumbv6m-none-eabi"] -channel = "nightly-2023-12-20" \ No newline at end of file +channel = "nightly-2024-06-12" diff --git a/examples/embassy/rust-toolchain.toml b/examples/embassy/rust-toolchain.toml index 29eb7db..e5c2be5 100644 --- a/examples/embassy/rust-toolchain.toml +++ b/examples/embassy/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] targets = ["thumbv6m-none-eabi"] -channel = "nightly-2023-12-20" \ No newline at end of file +channel = "nightly-2024-06-12" diff --git a/src/extract.rs b/src/extract.rs index 8e41b97..c77507c 100644 --- a/src/extract.rs +++ b/src/extract.rs @@ -20,6 +20,14 @@ use crate::{ ResponseSent, }; +pub mod json { + pub use crate::json::Json; + + pub use serde_json_core::str; +} + +pub use crate::json::Json; + mod private { pub struct ViaRequest; pub struct ViaParts; @@ -453,6 +461,58 @@ impl<'r, State, T: serde::de::DeserializeOwned> FromRequest<'r, State> for Form< } } +#[cfg_attr(feature = "defmt", defmt::Format)] +pub enum JsonRejection { + IoError, + DeserializationError(serde_json_core::de::Error), +} + +impl IntoResponse for JsonRejection { + async fn write_to>( + self, + connection: crate::response::Connection<'_, R>, + response_writer: W, + ) -> Result { + match self { + Self::IoError => { + (StatusCode::INTERNAL_SERVER_ERROR, "IO Error") + .write_to(connection, response_writer) + .await + } + Self::DeserializationError(error) => { + ( + StatusCode::BAD_REQUEST, + format_args!("Failed to parse JSON body: {error}"), + ) + .write_to(connection, response_writer) + .await + } + } + } +} + +impl<'r, State, T: serde::Deserialize<'r>, const UNESCAPE_BUFFER_SIZE: usize> + FromRequest<'r, State, T> for Json +{ + type Rejection = JsonRejection; + + async fn from_request( + _state: &'r State, + _request_parts: RequestParts<'r>, + request_body: RequestBody<'r, R>, + ) -> Result { + serde_json_core::from_slice_escaped( + request_body + .read_all() + .await + .map_err(|_| JsonRejection::IoError)?, + &mut [0; UNESCAPE_BUFFER_SIZE], + ) + .map(|(value, _)| Self(value)) + .map_err(JsonRejection::DeserializationError) + } +} + /// Used to do reference to value conversions, mainly used with the [State] extractor to extract parts of the application state. pub trait FromRef { /// Perform the reference to value conversion diff --git a/src/json.rs b/src/json.rs new file mode 100644 index 0000000..3357f4d --- /dev/null +++ b/src/json.rs @@ -0,0 +1,3 @@ +/// A JSON encoded value. When serializing, the value might be serialized several times during sending, so the value must be serialized in the same way each time. +/// When values are deserialized, `UNESCAPE_BUFFER_SIZE` is the size of the temporary buffer used for unescaping strings. +pub struct Json(pub T); diff --git a/src/lib.rs b/src/lib.rs index b4122a3..a32d0fc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,6 +13,8 @@ compile_error!("You cannot enable both tokio and embassy support"); #[cfg(feature = "alloc")] extern crate alloc; +mod json; + #[macro_use] mod logging; diff --git a/src/response/json.rs b/src/response/json.rs index 87770c7..b383a67 100644 --- a/src/response/json.rs +++ b/src/response/json.rs @@ -1,4 +1,4 @@ -//! Support for serializing and deserializing JSON structures +//! Support for serializing JSON structures use core::fmt; @@ -6,6 +6,8 @@ use serde::Serialize; use crate::io::{FormatBuffer, FormatBufferWriteError, Write}; +pub use crate::json::Json; + #[derive(Debug)] struct SerializeError; @@ -496,9 +498,6 @@ impl super::Content for JsonBody { } } -/// Serializes the value in JSON form. The value might be serialized several times during sending, so the value must be serialized in the same way each time. -pub struct Json(pub T); - impl Json { pub(crate) async fn do_write_to(&self, writer: &mut W) -> Result<(), W::Error> { JsonStream::new(&self.0).write_json_value(writer).await