use azure_core::{fmt::SafeDebug, Bytes};
use serde::de::DeserializeOwned;
#[derive(Clone, Default, SafeDebug)]
pub enum ResponseBody {
#[default]
NoPayload,
Bytes(Bytes),
Items(Vec<Bytes>),
}
impl ResponseBody {
pub fn empty() -> Self {
Self::NoPayload
}
pub fn from_bytes(bytes: impl Into<Bytes>) -> Self {
let bytes = bytes.into();
if bytes.is_empty() {
Self::NoPayload
} else {
Self::Bytes(bytes)
}
}
pub fn from_items(items: Vec<Bytes>) -> Self {
Self::Items(items)
}
pub fn is_empty(&self) -> bool {
match self {
Self::NoPayload => true,
Self::Bytes(b) => b.is_empty(),
Self::Items(items) => items.is_empty(),
}
}
pub fn single(self) -> crate::error::Result<Bytes> {
match self {
Self::NoPayload => Ok(Bytes::new()),
Self::Bytes(b) => Ok(b),
Self::Items(items) => Err(crate::error::CosmosError::builder()
.with_status(crate::error::CosmosStatus::new(
azure_core::http::StatusCode::BadRequest,
))
.with_message(format!(
"expected single response body, found feed response with {} item(s)",
items.len()
))
.build()),
}
}
pub fn items(self) -> crate::error::Result<Vec<Bytes>> {
match self {
Self::NoPayload => Ok(Vec::new()),
Self::Bytes(b) => Ok(vec![b]),
Self::Items(items) => Ok(items),
}
}
pub fn into_single<T: DeserializeOwned>(self) -> crate::error::Result<T> {
let bytes = self.single()?;
serde_json::from_slice(&bytes).map_err(|e| {
crate::error::CosmosError::builder()
.with_status(crate::error::CosmosStatus::SERIALIZATION_RESPONSE_BODY_INVALID)
.with_message("failed to deserialize response body")
.with_source(e)
.build()
})
}
pub fn into_items<T: DeserializeOwned>(self) -> crate::error::Result<Vec<T>> {
match self {
Self::NoPayload => Ok(Vec::new()),
Self::Bytes(b) => {
let item = serde_json::from_slice(&b).map_err(|e| {
crate::error::CosmosError::builder()
.with_status(
crate::error::CosmosStatus::SERIALIZATION_RESPONSE_BODY_INVALID,
)
.with_message("failed to deserialize response body")
.with_source(e)
.build()
})?;
Ok(vec![item])
}
Self::Items(items) => items
.into_iter()
.map(|b| {
serde_json::from_slice(&b).map_err(|e| {
crate::error::CosmosError::builder()
.with_status(
crate::error::CosmosStatus::SERIALIZATION_RESPONSE_BODY_INVALID,
)
.with_message("failed to deserialize feed item")
.with_source(e)
.build()
})
})
.collect(),
}
}
}
impl From<Bytes> for ResponseBody {
fn from(bytes: Bytes) -> Self {
Self::from_bytes(bytes)
}
}
impl From<Vec<u8>> for ResponseBody {
fn from(bytes: Vec<u8>) -> Self {
Self::from_bytes(Bytes::from(bytes))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_is_no_payload() {
let body = ResponseBody::default();
assert!(matches!(body, ResponseBody::NoPayload));
assert!(body.is_empty());
}
#[test]
fn empty_constructor_is_no_payload() {
let body = ResponseBody::empty();
assert!(matches!(body, ResponseBody::NoPayload));
assert!(body.is_empty());
}
#[test]
fn no_payload_single_yields_empty_bytes() {
let body = ResponseBody::NoPayload;
let bytes = body.single().expect("NoPayload should yield empty Bytes");
assert!(bytes.is_empty());
}
#[test]
fn no_payload_into_items_yields_empty_vec() {
let items: Vec<serde_json::Value> = ResponseBody::NoPayload.into_items().unwrap();
assert!(items.is_empty());
}
#[test]
fn no_payload_into_item_errors() {
let body = ResponseBody::NoPayload;
let result: crate::error::Result<serde_json::Value> = body.into_single();
assert!(result.is_err());
}
#[test]
fn from_empty_bytes_becomes_no_payload() {
let body: ResponseBody = Bytes::new().into();
assert!(matches!(body, ResponseBody::NoPayload));
}
#[test]
fn from_empty_vec_u8_becomes_no_payload() {
let body: ResponseBody = Vec::<u8>::new().into();
assert!(matches!(body, ResponseBody::NoPayload));
}
#[test]
fn from_bytes_roundtrip() {
let body: ResponseBody = Bytes::from_static(b"hello").into();
match &body {
ResponseBody::Bytes(b) => assert_eq!(&b[..], b"hello"),
_ => panic!("expected Bytes variant"),
}
let bytes = body.single().expect("single");
assert_eq!(&bytes[..], b"hello");
}
#[test]
fn items_roundtrip() {
let body = ResponseBody::from_items(vec![
Bytes::from_static(b"a"),
Bytes::from_static(b"bc"),
Bytes::from_static(b"def"),
]);
assert!(!body.is_empty());
let items: Vec<Bytes> = match body {
ResponseBody::Items(items) => items,
_ => panic!("expected Items variant"),
};
assert_eq!(items.len(), 3);
}
#[test]
fn single_errors_on_items() {
let body =
ResponseBody::from_items(vec![Bytes::from_static(b"a"), Bytes::from_static(b"b")]);
assert!(body.single().is_err());
}
#[test]
fn into_item_deserializes() {
#[derive(serde::Deserialize, PartialEq, Debug)]
struct Foo {
id: u32,
}
let body = ResponseBody::Bytes(Bytes::from_static(br#"{"id":7}"#));
let foo: Foo = body.into_single().unwrap();
assert_eq!(foo, Foo { id: 7 });
}
#[test]
fn into_items_from_items_variant() {
#[derive(serde::Deserialize, PartialEq, Debug)]
struct Foo {
id: u32,
}
let body = ResponseBody::from_items(vec![
Bytes::from_static(br#"{"id":1}"#),
Bytes::from_static(br#"{"id":2}"#),
]);
let items: Vec<Foo> = body.into_items().unwrap();
assert_eq!(items, vec![Foo { id: 1 }, Foo { id: 2 }]);
}
#[test]
fn into_items_from_bytes_variant_yields_one() {
#[derive(serde::Deserialize, PartialEq, Debug)]
struct Foo {
id: u32,
}
let body = ResponseBody::Bytes(Bytes::from_static(br#"{"id":42}"#));
let items: Vec<Foo> = body.into_items().unwrap();
assert_eq!(items, vec![Foo { id: 42 }]);
}
#[test]
fn from_vec_u8_via_into() {
let body: ResponseBody = vec![1u8, 2, 3].into();
match &body {
ResponseBody::Bytes(b) => assert_eq!(&b[..], &[1u8, 2, 3]),
_ => panic!("expected Bytes variant"),
}
}
#[test]
fn from_bytes_constructor() {
let body = ResponseBody::from_bytes(Bytes::from_static(b"abc"));
match &body {
ResponseBody::Bytes(b) => assert_eq!(&b[..], b"abc"),
_ => panic!("expected Bytes variant"),
}
}
#[test]
fn from_items_constructor() {
let body = ResponseBody::from_items(vec![Bytes::from_static(b"a")]);
match &body {
ResponseBody::Items(v) => assert_eq!(v.len(), 1),
_ => panic!("expected Items variant"),
}
}
#[test]
fn is_empty_true_for_empty_items_vec() {
assert!(ResponseBody::from_items(Vec::new()).is_empty());
}
#[test]
fn is_empty_false_for_items_with_entry() {
let body = ResponseBody::from_items(vec![Bytes::new()]);
assert!(!body.is_empty());
}
#[test]
fn is_empty_true_for_no_payload() {
assert!(ResponseBody::NoPayload.is_empty());
}
#[test]
fn is_empty_false_for_non_empty_bytes() {
assert!(!ResponseBody::Bytes(Bytes::from_static(b"x")).is_empty());
}
}