use std::borrow::Cow;
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
use thiserror::Error;
use crate::codec::{self, DecodeError, EncodeError, RawJson};
#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[non_exhaustive]
pub enum ContentError {
#[error("content must be a JSON string, object or array")]
Shape,
#[error(transparent)]
Encode(#[from] EncodeError),
#[error(transparent)]
Decode(#[from] DecodeError),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Content<'a> {
repr: Repr<'a>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum Repr<'a> {
Text(Cow<'a, str>),
Json(RawJson),
}
impl<'a> Content<'a> {
#[must_use]
pub fn text(text: impl Into<Cow<'a, str>>) -> Self {
Self { repr: Repr::Text(text.into()) }
}
pub fn json<T>(value: &T) -> Result<Self, ContentError>
where
T: Serialize + ?Sized,
{
let mut buffer = Vec::new();
codec::encode_into(&mut buffer, value)?;
let text = String::from_utf8(buffer).expect("invariant: the codec emits UTF-8");
Self::from_raw(Cow::Owned(text))
}
#[must_use]
pub fn as_text(&self) -> Option<&str> {
match &self.repr {
Repr::Text(text) => Some(text),
Repr::Json(_) => None,
}
}
#[must_use]
pub fn as_json(&self) -> Option<&RawJson> {
match &self.repr {
Repr::Text(_) => None,
Repr::Json(raw) => Some(raw),
}
}
#[must_use]
pub fn into_owned(self) -> Content<'static> {
Content {
repr: match self.repr {
Repr::Text(text) => Repr::Text(Cow::Owned(text.into_owned())),
Repr::Json(raw) => Repr::Json(raw),
},
}
}
fn from_raw(raw: Cow<'a, str>) -> Result<Self, ContentError> {
match raw.as_bytes().first() {
Some(b'"') => match raw {
Cow::Borrowed(text) => Ok(Self::text(unquote(text)?)),
Cow::Owned(text) => Ok(Self::text(unquote(&text)?.into_owned())),
},
Some(b'{' | b'[') => {
Ok(Self { repr: Repr::Json(RawJson::from_text(raw.into_owned())) })
}
_ => Err(ContentError::Shape),
}
}
}
impl<'a> From<&'a str> for Content<'a> {
fn from(text: &'a str) -> Self {
Self::text(text)
}
}
impl From<String> for Content<'_> {
fn from(text: String) -> Self {
Self::text(text)
}
}
impl<'a> From<Cow<'a, str>> for Content<'a> {
fn from(text: Cow<'a, str>) -> Self {
Self::text(text)
}
}
fn unquote(raw: &str) -> Result<Cow<'_, str>, ContentError> {
let inner = raw.get(1..raw.len().saturating_sub(1)).ok_or(ContentError::Shape)?;
if inner.as_bytes().contains(&b'\\') {
Ok(Cow::Owned(codec::decode::<String>(raw.as_bytes())?))
} else {
Ok(Cow::Borrowed(inner))
}
}
impl Serialize for Content<'_> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match &self.repr {
Repr::Text(text) => serializer.serialize_str(text),
Repr::Json(raw) => raw.serialize(serializer),
}
}
}
impl<'de: 'a, 'a> Deserialize<'de> for Content<'a> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let raw = codec::deserialize_raw(deserializer)?;
Self::from_raw(raw).map_err(de::Error::custom)
}
}
#[cfg(test)]
#[path = "content_tests.rs"]
mod tests;