diff --git a/.vscode/cspell.json b/.vscode/cspell.json index 334529b6dc..e94fed3d92 100644 --- a/.vscode/cspell.json +++ b/.vscode/cspell.json @@ -72,6 +72,7 @@ "rustup", "seekable", "servicebus", + "spector", "stylesheet", "subclient", "telemetered", diff --git a/sdk/storage/azure_storage_blob/src/clients/block_blob_client.rs b/sdk/storage/azure_storage_blob/src/clients/block_blob_client.rs index b0976a7e18..0b18247f71 100644 --- a/sdk/storage/azure_storage_blob/src/clients/block_blob_client.rs +++ b/sdk/storage/azure_storage_blob/src/clients/block_blob_client.rs @@ -95,7 +95,7 @@ impl BlockBlobClient { /// * `options` - Optional configuration for the request. pub async fn commit_block_list( &self, - blocks: RequestContent, + blocks: RequestContent, options: Option>, ) -> Result> { self.client.commit_block_list(blocks, options).await diff --git a/sdk/storage/azure_storage_blob/src/generated/clients/blob_client.rs b/sdk/storage/azure_storage_blob/src/generated/clients/blob_client.rs index 3343f21872..bdddfd95fc 100644 --- a/sdk/storage/azure_storage_blob/src/generated/clients/blob_client.rs +++ b/sdk/storage/azure_storage_blob/src/generated/clients/blob_client.rs @@ -1465,7 +1465,7 @@ impl BlobClient { #[tracing::function("Storage.Blob.Container.Blob.setTags")] pub async fn set_tags( &self, - tags: RequestContent, + tags: RequestContent, options: Option>, ) -> Result> { let options = options.unwrap_or_default(); diff --git a/sdk/storage/azure_storage_blob/src/generated/clients/blob_container_client.rs b/sdk/storage/azure_storage_blob/src/generated/clients/blob_container_client.rs index 2e92184f90..916954695a 100644 --- a/sdk/storage/azure_storage_blob/src/generated/clients/blob_container_client.rs +++ b/sdk/storage/azure_storage_blob/src/generated/clients/blob_container_client.rs @@ -992,7 +992,7 @@ impl BlobContainerClient { #[tracing::function("Storage.Blob.Container.setAccessPolicy")] pub async fn set_access_policy( &self, - container_acl: RequestContent>, + container_acl: RequestContent, XmlFormat>, options: Option>, ) -> Result> { let options = options.unwrap_or_default(); diff --git a/sdk/storage/azure_storage_blob/src/generated/clients/blob_service_client.rs b/sdk/storage/azure_storage_blob/src/generated/clients/blob_service_client.rs index c0c3cf5aff..f8ef860e42 100644 --- a/sdk/storage/azure_storage_blob/src/generated/clients/blob_service_client.rs +++ b/sdk/storage/azure_storage_blob/src/generated/clients/blob_service_client.rs @@ -293,7 +293,7 @@ impl BlobServiceClient { #[tracing::function("Storage.Blob.getUserDelegationKey")] pub async fn get_user_delegation_key( &self, - key_info: RequestContent, + key_info: RequestContent, options: Option>, ) -> Result> { let options = options.unwrap_or_default(); @@ -428,7 +428,7 @@ impl BlobServiceClient { #[tracing::function("Storage.Blob.setProperties")] pub async fn set_properties( &self, - storage_service_properties: RequestContent, + storage_service_properties: RequestContent, options: Option>, ) -> Result> { let options = options.unwrap_or_default(); diff --git a/sdk/storage/azure_storage_blob/src/generated/clients/block_blob_client.rs b/sdk/storage/azure_storage_blob/src/generated/clients/block_blob_client.rs index fe1e71fc12..1e5e04844e 100644 --- a/sdk/storage/azure_storage_blob/src/generated/clients/block_blob_client.rs +++ b/sdk/storage/azure_storage_blob/src/generated/clients/block_blob_client.rs @@ -111,7 +111,7 @@ impl BlockBlobClient { #[tracing::function("Storage.Blob.Container.Blob.BlockBlob.commitBlockList")] pub async fn commit_block_list( &self, - blocks: RequestContent, + blocks: RequestContent, options: Option>, ) -> Result> { let options = options.unwrap_or_default(); @@ -454,7 +454,7 @@ impl BlockBlobClient { #[tracing::function("Storage.Blob.Container.Blob.BlockBlob.query")] pub async fn query( &self, - query_request: RequestContent, + query_request: RequestContent, options: Option>, ) -> Result> { let options = options.unwrap_or_default(); diff --git a/sdk/storage/azure_storage_blob/src/generated/models/models_impl.rs b/sdk/storage/azure_storage_blob/src/generated/models/models_impl.rs index 07e90d3439..3621ba90d9 100644 --- a/sdk/storage/azure_storage_blob/src/generated/models/models_impl.rs +++ b/sdk/storage/azure_storage_blob/src/generated/models/models_impl.rs @@ -4,37 +4,41 @@ // Code generated by Microsoft (R) Rust Code Generator. DO NOT EDIT. use super::{BlobTags, BlockLookupList, KeyInfo, QueryRequest, StorageServiceProperties}; -use azure_core::{http::RequestContent, xml::to_xml, Result}; +use azure_core::{ + http::{RequestContent, XmlFormat}, + xml::to_xml, + Result, +}; -impl TryFrom for RequestContent { +impl TryFrom for RequestContent { type Error = azure_core::Error; fn try_from(value: BlobTags) -> Result { RequestContent::try_from(to_xml(&value)?) } } -impl TryFrom for RequestContent { +impl TryFrom for RequestContent { type Error = azure_core::Error; fn try_from(value: BlockLookupList) -> Result { RequestContent::try_from(to_xml(&value)?) } } -impl TryFrom for RequestContent { +impl TryFrom for RequestContent { type Error = azure_core::Error; fn try_from(value: KeyInfo) -> Result { RequestContent::try_from(to_xml(&value)?) } } -impl TryFrom for RequestContent { +impl TryFrom for RequestContent { type Error = azure_core::Error; fn try_from(value: QueryRequest) -> Result { RequestContent::try_from(to_xml(&value)?) } } -impl TryFrom for RequestContent { +impl TryFrom for RequestContent { type Error = azure_core::Error; fn try_from(value: StorageServiceProperties) -> Result { RequestContent::try_from(to_xml(&value)?) diff --git a/sdk/storage/azure_storage_blob/tests/block_blob_client.rs b/sdk/storage/azure_storage_blob/tests/block_blob_client.rs index aee899d9a6..a0e5cafee3 100644 --- a/sdk/storage/azure_storage_blob/tests/block_blob_client.rs +++ b/sdk/storage/azure_storage_blob/tests/block_blob_client.rs @@ -74,10 +74,8 @@ async fn test_block_list(ctx: TestContext) -> Result<(), Box> { uncommitted: Some(Vec::new()), }; - let request_content = RequestContent::try_from(block_lookup_list)?; - block_blob_client - .commit_block_list(request_content, None) + .commit_block_list(block_lookup_list.try_into()?, None) .await?; // Three Committed Blocks Scenario diff --git a/sdk/typespec/typespec_client_core/Cargo.toml b/sdk/typespec/typespec_client_core/Cargo.toml index 7032a61fb6..c13e5d1634 100644 --- a/sdk/typespec/typespec_client_core/Cargo.toml +++ b/sdk/typespec/typespec_client_core/Cargo.toml @@ -22,7 +22,7 @@ rand.workspace = true reqwest = { workspace = true, optional = true } rust_decimal = { workspace = true, optional = true } serde.workspace = true -serde_json.workspace = true +serde_json = { workspace = true, features = ["raw_value"] } time.workspace = true tokio = { workspace = true, optional = true } tracing.workspace = true diff --git a/sdk/typespec/typespec_client_core/src/fs/tokio.rs b/sdk/typespec/typespec_client_core/src/fs/tokio.rs index 349d36afb9..ff999da035 100644 --- a/sdk/typespec/typespec_client_core/src/fs/tokio.rs +++ b/sdk/typespec/typespec_client_core/src/fs/tokio.rs @@ -172,14 +172,14 @@ impl From for Body { } #[cfg(not(target_arch = "wasm32"))] -impl From<&FileStream> for RequestContent { +impl From<&FileStream> for RequestContent { fn from(stream: &FileStream) -> Self { Body::from(stream).into() } } #[cfg(not(target_arch = "wasm32"))] -impl From for RequestContent { +impl From for RequestContent { fn from(stream: FileStream) -> Self { Body::from(stream).into() } diff --git a/sdk/typespec/typespec_client_core/src/http/format.rs b/sdk/typespec/typespec_client_core/src/http/format.rs index d4d1afad61..19f7dbec6d 100644 --- a/sdk/typespec/typespec_client_core/src/http/format.rs +++ b/sdk/typespec/typespec_client_core/src/http/format.rs @@ -1,9 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +use crate::http::{response::ResponseBody, Body}; use serde::de::DeserializeOwned; - -use crate::http::response::ResponseBody; +use std::collections::HashMap; /// A marker trait used to indicate the serialization format used for a response body. /// @@ -21,13 +21,15 @@ pub trait Format: std::fmt::Debug {} /// /// This trait defines the `deserialize_with` method, which takes a [`ResponseBody`] and returns the deserialized value. /// The `F` type parameter allows for different implementations of the `deserialize_with` method based on the specific [`Format`] marker type used. +/// +/// Defining our own trait allows us to implement it on foreign types and better customize deserialization for different scenarios. #[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] pub trait DeserializeWith: Sized { async fn deserialize_with(body: ResponseBody) -> typespec::Result; } -/// Implements [`DeserializeWith`] for an arbitrary type `D` +/// Implements [`DeserializeWith`] for an arbitrary type `D` /// that implements [`serde::de::DeserializeOwned`] by deserializing the response body to the specified type using [`serde_json`]. #[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] @@ -37,6 +39,16 @@ impl DeserializeWith for D { } } +/// A trait used to describe a type that can be serialized using the specified [`Format`]. +/// +/// This trait defines the `serialize_with` method, which takes a value and returns a [`Body`]. +/// The `F` type parameter allows for different implementations of the `serialize_with` method based on the specific [`Format`] marker type used. +/// +/// Defining our own trait allows us to implement it on foreign types and better customize serialization for different scenarios. +pub trait SerializeWith: Sized { + fn serialize_with(value: Self) -> typespec::Result; +} + /// A [`Format`] that deserializes response bodies using JSON. /// This is the default format used for deserialization. /// @@ -78,3 +90,215 @@ impl DeserializeWith for D { pub struct NoFormat; impl Format for NoFormat {} + +#[cfg(feature = "json")] +mod json { + use super::*; + use crate::{ + error::{Error, ErrorKind, ResultExt as _}, + time::OffsetDateTime, + }; + use bytes::BufMut as _; + use serde::Serializer; + use serde_json::value::RawValue; + use time::format_description::well_known::Rfc3339; + + macro_rules! impl_serialize_with { + ($t:ty) => { + impl $crate::http::SerializeWith<$crate::http::JsonFormat> for $t { + fn serialize_with(value: Self) -> $crate::Result<$crate::http::Body> { + Ok($crate::json::to_json(&value)?.into()) + } + } + }; + + ($($t:ty),*) => { + $(impl_serialize_with!($t);)* + }; + } + + impl_serialize_with!(bool); + impl_serialize_with!(&str, String); + impl_serialize_with!(i32, i64); + impl_serialize_with!(f32, f64); + impl_serialize_with!(serde_json::Value); + + impl SerializeWith for OffsetDateTime { + fn serialize_with(value: Self) -> typespec::Result { + let value = value + .format(&Rfc3339) + .with_context(ErrorKind::DataConversion, || "failed formatting datetime")?; + Ok(crate::json::to_json(&value)?.into()) + } + } + + #[cfg(feature = "decimal")] + impl SerializeWith for rust_decimal::Decimal { + fn serialize_with(value: Self) -> typespec::Result { + Ok(crate::json::to_json(&value.to_string())?.into()) + } + } + + impl> SerializeWith for Vec { + fn serialize_with(value: Self) -> typespec::Result { + use serde::ser::SerializeSeq; + + let mut buf = vec![].writer(); + let mut ser = serde_json::Serializer::new(&mut buf); + let mut seq = ser + .serialize_seq(Some(value.len())) + .with_context(ErrorKind::Io, || "failed sequence start")?; + for elem in value { + let Body::Bytes(raw) = T::serialize_with(elem)? else { + return Err(Error::new( + ErrorKind::DataConversion, + "failed json serialization", + )); + }; + let raw = RawValue::from_string(String::from_utf8(raw.to_vec())?)?; + seq.serialize_element(&raw) + .with_context(ErrorKind::Io, || "failed sequence element")?; + } + seq.end() + .with_context(ErrorKind::Io, || "failed sequence end")?; + + let buf = buf.into_inner(); + Ok(buf.into()) + } + } + + impl> SerializeWith for HashMap { + fn serialize_with(value: Self) -> typespec::Result { + use serde::ser::SerializeMap; + + let mut buf = vec![].writer(); + let mut ser = serde_json::Serializer::new(&mut buf); + let mut seq = ser + .serialize_map(Some(value.len())) + .with_context(ErrorKind::Io, || "failed map start")?; + for (key, elem) in value { + let Body::Bytes(raw) = T::serialize_with(elem)? else { + return Err(Error::new( + ErrorKind::DataConversion, + "failed json serialization", + )); + }; + let raw = RawValue::from_string(String::from_utf8(raw.to_vec())?)?; + seq.serialize_entry(&key, &raw) + .with_context(ErrorKind::Io, || "failed map entry")?; + } + seq.end().with_context(ErrorKind::Io, || "failed map end")?; + + let buf = buf.into_inner(); + Ok(buf.into()) + } + } +} + +#[cfg(feature = "xml")] +mod xml { + use super::*; + use crate::{ + error::{Error, ErrorKind, ResultExt as _}, + time::OffsetDateTime, + }; + use serde::Serializer; + use time::format_description::well_known::Rfc3339; + + macro_rules! impl_serialize_with { + ($t:ty) => { + impl $crate::http::SerializeWith<$crate::http::XmlFormat> for $t { + fn serialize_with(value: Self) -> $crate::Result<$crate::http::Body> { + let value = ::quick_xml::se::to_string(&value).with_context(ErrorKind::DataConversion, || { + let t = core::any::type_name::<$t>(); + format!("failed to serialize {t} into xml") + })?; + Ok(value.into()) + } + } + }; + + ($($t:ty),*) => { + $(impl_serialize_with!($t);)* + }; + } + + impl_serialize_with!(bool); + impl_serialize_with!(&str, String); + impl_serialize_with!(i32, i64); + impl_serialize_with!(f32, f64); + impl_serialize_with!(serde_json::Value); + + impl SerializeWith for OffsetDateTime { + fn serialize_with(value: Self) -> typespec::Result { + let value = value + .format(&Rfc3339) + .with_context(ErrorKind::DataConversion, || "failed formatting datetime")?; + Ok(crate::xml::to_xml(&value)?.into()) + } + } + + #[cfg(feature = "decimal")] + impl SerializeWith for rust_decimal::Decimal { + fn serialize_with(value: Self) -> typespec::Result { + Ok(crate::xml::to_xml(&value.to_string())?.into()) + } + } + + impl> SerializeWith for Vec { + fn serialize_with(value: Self) -> typespec::Result { + use serde::ser::SerializeSeq; + + let mut buf = bytes::BytesMut::new(); + let ser = quick_xml::se::Serializer::with_root(&mut buf, Some("root")) + .with_context(ErrorKind::Io, || "failed xml root")?; + let mut seq = ser + .serialize_seq(Some(value.len())) + .with_context(ErrorKind::Io, || "failed sequence start")?; + for elem in value { + let Body::Bytes(raw) = T::serialize_with(elem)? else { + return Err(Error::new( + ErrorKind::DataConversion, + "failed xml serialization", + )); + }; + let raw = String::from_utf8(raw.to_vec())?; + seq.serialize_element(&raw) + .with_context(ErrorKind::Io, || "failed sequence element")?; + } + seq.end() + .with_context(ErrorKind::Io, || "failed sequence end")?; + + let buf: crate::Bytes = buf.into(); + Ok(buf.into()) + } + } + + impl> SerializeWith for HashMap { + fn serialize_with(value: Self) -> typespec::Result { + use serde::ser::SerializeMap; + + let mut buf = bytes::BytesMut::new(); + let ser = quick_xml::se::Serializer::with_root(&mut buf, Some("root")) + .with_context(ErrorKind::Io, || "failed xml root")?; + let mut seq = ser + .serialize_map(Some(value.len())) + .with_context(ErrorKind::Io, || "failed map start")?; + for (key, elem) in value { + let Body::Bytes(raw) = T::serialize_with(elem)? else { + return Err(Error::new( + ErrorKind::DataConversion, + "failed xml serialization", + )); + }; + let raw = String::from_utf8(raw.to_vec())?; + seq.serialize_entry(&key, &raw) + .with_context(ErrorKind::Io, || "failed map entry")?; + } + seq.end().with_context(ErrorKind::Io, || "failed map end")?; + + let buf: crate::Bytes = buf.into(); + Ok(buf.into()) + } + } +} diff --git a/sdk/typespec/typespec_client_core/src/http/request/mod.rs b/sdk/typespec/typespec_client_core/src/http/request/mod.rs index 7a67ec149a..4def3905cb 100644 --- a/sdk/typespec/typespec_client_core/src/http/request/mod.rs +++ b/sdk/typespec/typespec_client_core/src/http/request/mod.rs @@ -10,19 +10,13 @@ use crate::stream::SeekableStream; use crate::{ http::{ headers::{AsHeaders, Header, HeaderName, HeaderValue, Headers}, - Method, Url, + Format, JsonFormat, Method, SerializeWith, Url, }, json::to_json, - time::OffsetDateTime, }; use bytes::Bytes; use serde::Serialize; -use serde_json::Value; use std::{collections::HashMap, convert::Infallible, fmt, marker::PhantomData, str::FromStr}; -use time::format_description::well_known::Rfc3339; - -#[cfg(feature = "decimal")] -use rust_decimal::Decimal; /// An HTTP Body. #[derive(Clone)] @@ -59,11 +53,23 @@ impl Body { Body::SeekableStream(stream) => stream.reset().await, } } + + #[cfg(test)] + fn from_static(value: &'static [u8]) -> Self { + Self::Bytes(Bytes::from_static(value)) + } } impl fmt::Debug for Body { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { + #[cfg(test)] + Self::Bytes(v) if !v.is_empty() => f + .debug_struct("Bytes") + .field("len", &v.len()) + .field("data", &v) + .finish_non_exhaustive(), + #[cfg(not(test))] Self::Bytes(v) if !v.is_empty() => f.write_str("Bytes { .. }"), Self::Bytes(_) => f.write_str("Bytes {}"), #[cfg(not(target_arch = "wasm32"))] @@ -240,12 +246,12 @@ impl fmt::Debug for Request { /// The body content of a service client request. /// This allows callers to pass a model to serialize or raw content to client methods. #[derive(Clone, Debug)] -pub struct RequestContent { +pub struct RequestContent { body: Body, - phantom: PhantomData, + phantom: PhantomData<(T, F)>, } -impl RequestContent { +impl RequestContent { /// Gets the body of the request. pub fn body(&self) -> &Body { &self.body @@ -261,19 +267,19 @@ impl RequestContent { } #[cfg(test)] -impl PartialEq for RequestContent { +impl PartialEq for RequestContent { fn eq(&self, other: &Self) -> bool { self.body.eq(&other.body) } } -impl From> for Body { - fn from(content: RequestContent) -> Self { +impl From> for Body { + fn from(content: RequestContent) -> Self { content.body } } -impl From for RequestContent { +impl From for RequestContent { fn from(body: Body) -> Self { Self { body, @@ -282,7 +288,7 @@ impl From for RequestContent { } } -impl TryFrom for RequestContent { +impl TryFrom for RequestContent { type Error = crate::Error; fn try_from(body: Bytes) -> Result { Ok(Self { @@ -292,17 +298,17 @@ impl TryFrom for RequestContent { } } -impl TryFrom> for RequestContent { +impl<'a, T, F> TryFrom<&'a [u8]> for RequestContent { type Error = crate::Error; - fn try_from(body: Vec) -> Result { + fn try_from(body: &'a [u8]) -> Result { Ok(Self { - body: Bytes::from(body).into(), + body: Bytes::copy_from_slice(body).into(), phantom: PhantomData, }) } } -impl TryFrom<&'static str> for RequestContent { +impl TryFrom<&'static str> for RequestContent { type Error = crate::Error; fn try_from(body: &'static str) -> Result { Ok(Self { @@ -312,7 +318,7 @@ impl TryFrom<&'static str> for RequestContent { } } -impl TryFrom for RequestContent { +impl TryFrom for RequestContent { type Error = Infallible; fn try_from(body: bool) -> Result { Ok(Self { @@ -322,212 +328,34 @@ impl TryFrom for RequestContent { } } -#[cfg(feature = "decimal")] -impl TryFrom> for RequestContent { - type Error = Infallible; - fn try_from(body: Option) -> Result { - Ok(Self { - body: Bytes::from(body.map(|d| d.to_string()).unwrap_or_default()).into(), - phantom: PhantomData, - }) +impl, F: Format> TryFrom> for RequestContent, F> +where + Option: SerializeWith, +{ + type Error = crate::Error; + fn try_from(value: Option) -> Result { + Ok( as SerializeWith>::serialize_with(value)?.into()) } } -pub mod json { - use super::*; - - impl TryFrom for RequestContent { - type Error = crate::Error; - fn try_from(body: Value) -> Result { - Ok(Self { - body: Bytes::from(serde_json::to_string(&body)?).into(), - phantom: PhantomData, - }) - } - } - - impl TryFrom> for RequestContent { - type Error = crate::Error; - fn try_from(body: Vec) -> Result { - Ok(Self { - body: Bytes::from(serde_json::to_string(&body)?).into(), - phantom: PhantomData, - }) - } - } - - impl TryFrom> for RequestContent { - type Error = crate::Error; - fn try_from(body: Vec) -> Result { - Ok(Self { - body: Bytes::from(serde_json::to_string( - &body - .iter() - .map(|v| v.format(&Rfc3339).unwrap_or_else(|_| v.to_string())) - .collect::>(), - )?) - .into(), - phantom: PhantomData, - }) - } - } - - impl TryFrom> for RequestContent { - type Error = crate::Error; - fn try_from(body: Vec) -> Result { - Ok(Self { - body: Bytes::from(serde_json::to_string(&body)?).into(), - phantom: PhantomData, - }) - } - } - - impl TryFrom> for RequestContent { - type Error = crate::Error; - fn try_from(body: Vec<&str>) -> Result { - Ok(Self { - body: Bytes::from(serde_json::to_string(&body)?).into(), - phantom: PhantomData, - }) - } - } - - impl TryFrom> for RequestContent { - type Error = crate::Error; - fn try_from(body: Vec) -> Result { - Ok(Self { - body: Bytes::from(serde_json::to_string(&body)?).into(), - phantom: PhantomData, - }) - } - } - - impl TryFrom> for RequestContent { - type Error = crate::Error; - fn try_from(body: Vec) -> Result { - Ok(Self { - body: Bytes::from(serde_json::to_string(&body)?).into(), - phantom: PhantomData, - }) - } - } - - impl TryFrom> for RequestContent { - type Error = crate::Error; - fn try_from(body: Vec) -> Result { - Ok(Self { - body: Bytes::from(serde_json::to_string(&body)?).into(), - phantom: PhantomData, - }) - } - } - - impl TryFrom> for RequestContent { - type Error = crate::Error; - fn try_from(body: Vec) -> Result { - Ok(Self { - body: Bytes::from(serde_json::to_string(&body)?).into(), - phantom: PhantomData, - }) - } - } - - impl TryFrom> for RequestContent { - type Error = crate::Error; - fn try_from(body: Vec) -> Result { - Ok(Self { - body: Bytes::from(serde_json::to_string(&body)?).into(), - phantom: PhantomData, - }) - } - } - - impl TryFrom> for RequestContent { - type Error = crate::Error; - fn try_from(body: HashMap) -> Result { - Ok(Self { - body: Bytes::from(serde_json::to_string(&body)?).into(), - phantom: PhantomData, - }) - } - } - - impl TryFrom> for RequestContent { - type Error = crate::Error; - fn try_from(body: HashMap) -> Result { - let body_rfc3339: HashMap = body - .into_iter() - .map(|(k, v)| { - let formatted = v.format(&Rfc3339).unwrap_or_else(|_| v.to_string()); - (k, formatted) - }) - .collect(); - - Ok(Self { - body: Bytes::from(serde_json::to_string(&body_rfc3339)?).into(), - phantom: PhantomData, - }) - } - } - - impl TryFrom> for RequestContent { - type Error = crate::Error; - fn try_from(body: HashMap) -> Result { - Ok(Self { - body: Bytes::from(serde_json::to_string(&body)?).into(), - phantom: PhantomData, - }) - } - } - - impl TryFrom> for RequestContent { - type Error = crate::Error; - fn try_from(body: HashMap) -> Result { - Ok(Self { - body: Bytes::from(serde_json::to_string(&body)?).into(), - phantom: PhantomData, - }) - } - } - - impl TryFrom> for RequestContent { - type Error = crate::Error; - fn try_from(body: HashMap) -> Result { - Ok(Self { - body: Bytes::from(serde_json::to_string(&body)?).into(), - phantom: PhantomData, - }) - } - } - - impl TryFrom> for RequestContent { - type Error = crate::Error; - fn try_from(body: HashMap) -> Result { - Ok(Self { - body: Bytes::from(serde_json::to_string(&body)?).into(), - phantom: PhantomData, - }) - } - } - - impl TryFrom> for RequestContent { - type Error = crate::Error; - fn try_from(body: HashMap) -> Result { - Ok(Self { - body: Bytes::from(serde_json::to_string(&body)?).into(), - phantom: PhantomData, - }) - } +impl, F: Format> TryFrom> for RequestContent, F> +where + Vec: SerializeWith, +{ + type Error = crate::Error; + fn try_from(value: Vec) -> Result { + Ok( as SerializeWith>::serialize_with(value)?.into()) } +} - impl TryFrom> for RequestContent { - type Error = crate::Error; - fn try_from(body: HashMap) -> Result { - Ok(Self { - body: Bytes::from(serde_json::to_string(&body)?).into(), - phantom: PhantomData, - }) - } +impl, F: Format> TryFrom> + for RequestContent, F> +where + HashMap: SerializeWith, +{ + type Error = crate::Error; + fn try_from(value: HashMap) -> Result { + Ok( as SerializeWith>::serialize_with(value)?.into()) } } @@ -546,7 +374,15 @@ impl FromStr for RequestContent { #[cfg(test)] mod tests { use super::*; - use std::sync::LazyLock; + use crate::{ + error::{Error, ErrorKind, ResultExt as _}, + time::OffsetDateTime, + }; + use bytes::BufMut as _; + use serde::Serializer; + use serde_json::{value::RawValue, Value}; + use std::{collections::BTreeMap, sync::LazyLock}; + use time::macros::datetime; #[derive(Debug, Serialize)] struct Expected { @@ -567,6 +403,49 @@ mod tests { phantom: PhantomData, }); + // Serialization support for an ordered HashMap i.e, BTreeMap. + impl> SerializeWith for BTreeMap { + fn serialize_with(value: Self) -> typespec::Result { + use serde::ser::SerializeMap; + + let mut buf = vec![].writer(); + let mut ser = serde_json::Serializer::new(&mut buf); + let mut seq = ser + .serialize_map(Some(value.len())) + .with_context(ErrorKind::Io, || "failed map start")?; + for (key, elem) in value { + let Body::Bytes(raw) = T::serialize_with(elem)? else { + return Err(Error::new( + ErrorKind::DataConversion, + "failed json serialization", + )); + }; + let raw = RawValue::from_string(String::from_utf8(raw.to_vec())?)?; + seq.serialize_entry(&key, &raw) + .with_context(ErrorKind::Io, || "failed map entry")?; + } + seq.end().with_context(ErrorKind::Io, || "failed map end")?; + + let buf = buf.into_inner(); + Ok(buf.into()) + } + } + + // Serialization support for an ordered HashMap i.e, BTreeMap. + impl, F: Format> TryFrom> + for RequestContent, F> + where + std::collections::BTreeMap: SerializeWith, + { + type Error = crate::Error; + fn try_from(value: std::collections::BTreeMap) -> Result { + Ok( + as SerializeWith>::serialize_with(value)? + .into(), + ) + } + } + #[test] fn tryfrom_t() { let actual = Expected { @@ -585,7 +464,7 @@ mod tests { #[test] fn tryfrom_vec() { - let actual: Vec = r#"{"str":"test","num":1,"b":true}"#.bytes().collect(); + let actual = br#"{"str":"test","num":1,"b":true}"#.as_ref(); assert_eq!(*EXPECTED, actual.try_into().unwrap()); } @@ -601,4 +480,154 @@ mod tests { r#"{"str":"test","num":1,"b":true}"#.parse().unwrap(); assert_eq!(*EXPECTED, actual); } + + #[test] + fn spector_vec_bool() { + let actual: RequestContent> = vec![true, false].try_into().unwrap(); + assert_eq!(actual.body(), &Body::from_static(br#"[true,false]"#)); + } + + #[test] + fn spector_vec_offset_date_time() { + let actual: RequestContent> = + vec![datetime!(2022-08-26 18:38:00 UTC)].try_into().unwrap(); + assert_eq!( + actual.body(), + &Body::from_static(br#"["2022-08-26T18:38:00Z"]"#) + ); + } + + #[test] + fn spector_vec_duration() { + let actual: RequestContent> = + vec!["P123DT22H14M12.011S".into()].try_into().unwrap(); + assert_eq!( + actual.body(), + &Body::from_static(br#"["P123DT22H14M12.011S"]"#) + ); + } + + #[test] + fn spector_vec_f32() { + let actual: RequestContent> = vec![43.125f32].try_into().unwrap(); + assert_eq!(actual.body(), &Body::from_static(br#"[43.125]"#)); + } + + #[test] + fn spector_vec_i64() { + let actual: RequestContent> = vec![9007199254740991i64, -9007199254740991i64] + .try_into() + .unwrap(); + assert_eq!( + actual.body(), + &Body::from_static(br#"[9007199254740991,-9007199254740991]"#) + ); + } + + #[test] + fn spector_vec_string() { + let actual: RequestContent> = + vec!["hello".into(), "".into()].try_into().unwrap(); + assert_eq!(actual.body(), &Body::from_static(br#"["hello",""]"#)); + } + + #[test] + fn spector_vec_value() { + let actual: RequestContent> = vec![ + Value::Number(1.into()), + Value::String("hello".into()), + Value::Null, + ] + .try_into() + .unwrap(); + assert_eq!(actual.body(), &Body::from_static(br#"[1,"hello",null]"#)); + } + + #[test] + fn spector_dictionary_bool() { + let actual: RequestContent> = + BTreeMap::from_iter(vec![("k1".into(), true), ("k2".into(), false)]) + .try_into() + .unwrap(); + assert_eq!( + actual.body(), + &Body::from_static(br#"{"k1":true,"k2":false}"#) + ); + } + + #[test] + fn spector_dictionary_offset_date_time() { + let actual: RequestContent> = + BTreeMap::from_iter(vec![("k1".into(), datetime!(2022-08-26 18:38:00 UTC))]) + .try_into() + .unwrap(); + assert_eq!( + actual.body(), + &Body::from_static(br#"{"k1":"2022-08-26T18:38:00Z"}"#) + ); + } + + #[test] + fn spector_dictionary_duration() { + let actual: RequestContent> = + BTreeMap::from_iter(vec![("k1".into(), "P123DT22H14M12.011S".into())]) + .try_into() + .unwrap(); + assert_eq!( + actual.body(), + &Body::from_static(br#"{"k1":"P123DT22H14M12.011S"}"#) + ); + } + + #[test] + fn spector_dictionary_f32() { + let actual: RequestContent> = + BTreeMap::from_iter(vec![("k1".into(), 43.125f32)]) + .try_into() + .unwrap(); + assert_eq!(actual.body(), &Body::from_static(br#"{"k1":43.125}"#)); + } + + #[test] + fn spector_dictionary_i64() { + let actual: RequestContent> = BTreeMap::from_iter(vec![ + ("k1".into(), 9007199254740991i64), + ("k2".into(), -9007199254740991i64), + ]) + .try_into() + .unwrap(); + assert_eq!( + actual.body(), + &Body::from_static(br#"{"k1":9007199254740991,"k2":-9007199254740991}"#) + ); + } + + #[test] + fn spector_dictionary_string() { + let actual: RequestContent> = BTreeMap::from_iter(vec![ + ("k1".into(), "hello".into()), + ("k2".into(), "".into()), + ]) + .try_into() + .unwrap(); + assert_eq!( + actual.body(), + &Body::from_static(br#"{"k1":"hello","k2":""}"#) + ); + } + + #[test] + fn spector_dictionary_value() { + let actual: RequestContent> = BTreeMap::from_iter(vec![ + ("k1".into(), Value::Number(1.into())), + ("k2".into(), Value::String("hello".into())), + ("k3".into(), Value::Null), + ]) + .try_into() + .unwrap(); + assert_eq!( + actual.body(), + &Body::from_static(br#"{"k1":1,"k2":"hello","k3":null}"#) + ); + } }