reliar-core 0.3.0

Pure envelope/message model shared by every Reliar crate — no storage or transport dependency.
Documentation
//! Body ⇄ bytes conversion (ADR 0010).

use crate::{ContentType, Message};

/// Converts a typed [`Message`] body to and from bytes. Lives in `reliar-core`: it touches
/// neither storage nor transport (ADR 0010).
///
/// Stateless and cheap; implementations must never be placed behind a `dyn Serializer` on the
/// enqueue path — it runs once per message and static dispatch keeps it that cheap (ADR 0001).
///
/// ```
/// use bytes::Bytes;
/// use reliar_core::{ContentType, Message, Serializer};
///
/// /// A minimal serializer wrapping a fixed encoder, for hosts that already have one.
/// struct Fixed(Bytes);
///
/// impl Serializer for Fixed {
///     type Error = std::convert::Infallible;
///
///     fn content_type(&self) -> &ContentType {
///         &ContentType::JSON
///     }
///
///     fn serialize<T: Message>(&self, _body: &T) -> Result<Bytes, Self::Error> {
///         Ok(self.0.clone())
///     }
///
///     fn deserialize<T: Message>(&self, _bytes: &[u8]) -> Result<T, Self::Error> {
///         unimplemented!("example encoder, not a real round trip")
///     }
/// }
///
/// let serializer = Fixed(Bytes::from_static(b"{}"));
/// assert_eq!(serializer.content_type().as_str(), "application/json");
/// ```
pub trait Serializer: Send + Sync {
    /// The serializer's own error type.
    type Error: std::error::Error + Send + Sync + 'static;

    /// The content type this serializer produces. Populates both
    /// [`DeliveryMetadata::content_type`](crate::DeliveryMetadata::content_type) and a
    /// provider's `content_type` column — one value, chosen by the serializer, never by the
    /// call site.
    ///
    /// ```
    /// # use bytes::Bytes;
    /// # use reliar_core::{ContentType, Message, Serializer};
    /// # struct Fixed(Bytes);
    /// # impl Serializer for Fixed {
    /// #     type Error = std::convert::Infallible;
    /// #     fn content_type(&self) -> &ContentType { &ContentType::JSON }
    /// #     fn serialize<T: Message>(&self, _body: &T) -> Result<Bytes, Self::Error> { Ok(self.0.clone()) }
    /// #     fn deserialize<T: Message>(&self, _bytes: &[u8]) -> Result<T, Self::Error> { unimplemented!() }
    /// # }
    /// let serializer = Fixed(Bytes::from_static(b"{}"));
    /// assert_eq!(serializer.content_type().as_str(), "application/json");
    /// ```
    fn content_type(&self) -> &ContentType;

    /// Serializes a message body to bytes.
    ///
    /// # Errors
    ///
    /// Returns `Self::Error` if `body` cannot be represented in this serializer's format.
    ///
    /// ```
    /// # use bytes::Bytes;
    /// # use reliar_core::{ContentType, Message, Serializer};
    /// # struct Fixed(Bytes);
    /// # impl Serializer for Fixed {
    /// #     type Error = std::convert::Infallible;
    /// #     fn content_type(&self) -> &ContentType { &ContentType::JSON }
    /// #     fn serialize<T: Message>(&self, _body: &T) -> Result<Bytes, Self::Error> { Ok(self.0.clone()) }
    /// #     fn deserialize<T: Message>(&self, _bytes: &[u8]) -> Result<T, Self::Error> { unimplemented!() }
    /// # }
    /// # #[derive(serde::Serialize, serde::Deserialize)]
    /// # struct Ping;
    /// # impl Message for Ping { const TYPE: &'static str = "ping"; const VERSION: u16 = 1; }
    /// let serializer = Fixed(Bytes::from_static(b"{}"));
    /// let bytes = serializer.serialize(&Ping)?;
    /// assert_eq!(bytes.as_ref(), b"{}");
    /// # Ok::<(), std::convert::Infallible>(())
    /// ```
    fn serialize<T: Message>(&self, body: &T) -> Result<bytes::Bytes, Self::Error>;

    /// Deserializes bytes back into a message body.
    ///
    /// # Errors
    ///
    /// Returns `Self::Error` if `bytes` is not a valid encoding of `T` in this serializer's
    /// format.
    ///
    /// ```
    /// # #[cfg(feature = "json")]
    /// # fn run() -> Result<(), reliar_core::JsonError> {
    /// use reliar_core::{JsonSerializer, Message, Serializer};
    ///
    /// #[derive(serde::Serialize, serde::Deserialize, PartialEq, Debug)]
    /// struct Ping;
    /// impl Message for Ping {
    ///     const TYPE: &'static str = "ping";
    ///     const VERSION: u16 = 1;
    /// }
    ///
    /// let body: Ping = JsonSerializer.deserialize(b"null")?;
    /// assert_eq!(body, Ping);
    /// # Ok(())
    /// # }
    /// # #[cfg(feature = "json")]
    /// # run().unwrap();
    /// ```
    fn deserialize<T: Message>(&self, bytes: &[u8]) -> Result<T, Self::Error>;
}

#[cfg(feature = "json")]
#[cfg_attr(docsrs, doc(cfg(feature = "json")))]
mod json {
    use core::fmt;

    use bytes::Bytes;

    use super::Serializer;
    use crate::{ContentType, Message};

    /// The default [`Serializer`]: JSON via `serde_json`. Ships behind the default `json`
    /// feature; disable it to supply a different wire format (ADR 0010).
    ///
    /// ```
    /// use reliar_core::{JsonSerializer, Serializer};
    ///
    /// #[derive(serde::Serialize, serde::Deserialize)]
    /// struct Ping;
    /// impl reliar_core::Message for Ping {
    ///     const TYPE: &'static str = "ping";
    ///     const VERSION: u16 = 1;
    /// }
    ///
    /// let serializer = JsonSerializer;
    /// let bytes = serializer.serialize(&Ping)?;
    /// let _: Ping = serializer.deserialize(&bytes)?;
    /// assert_eq!(serializer.content_type().as_str(), "application/json");
    /// # Ok::<(), reliar_core::JsonError>(())
    /// ```
    #[derive(Clone, Debug, Default)]
    pub struct JsonSerializer;

    impl Serializer for JsonSerializer {
        type Error = JsonError;

        fn content_type(&self) -> &ContentType {
            &ContentType::JSON
        }

        fn serialize<T: Message>(&self, body: &T) -> Result<Bytes, Self::Error> {
            serde_json::to_vec(body)
                .map(Bytes::from)
                .map_err(|source| JsonError::Serialize { source })
        }

        fn deserialize<T: Message>(&self, bytes: &[u8]) -> Result<T, Self::Error> {
            serde_json::from_slice(bytes).map_err(|source| JsonError::Deserialize { source })
        }
    }

    /// [`JsonSerializer`] failures. `Display` names the operation, the error class
    /// (`serde_json::error::Category`), and the line/column — **never `serde_json::Error`'s own
    /// message**, which for a data error embeds a fragment of the value it rejected (e.g.
    /// `invalid type: string "sk-live-…", expected u64`). The full underlying error, message
    /// included, is still reachable via [`std::error::Error::source`] for a caller that
    /// deliberately wants it — that caller's own logging then owns not leaking a payload
    /// fragment, the same rule this type upholds by default.
    ///
    /// **`Debug` is a manual impl, never derived**: `serde_json::Error`'s own `Debug` embeds its
    /// `Display` message (the same payload fragment `Display` above must avoid), so deriving
    /// here would leak through `{:?}` even though `Display` is safe.
    ///
    /// ```
    /// use reliar_core::{JsonSerializer, Serializer};
    ///
    /// #[derive(serde::Serialize, serde::Deserialize)]
    /// struct Ping;
    /// impl reliar_core::Message for Ping {
    ///     const TYPE: &'static str = "ping";
    ///     const VERSION: u16 = 1;
    /// }
    ///
    /// let result = JsonSerializer.deserialize::<Ping>(b"not json");
    /// let err = match result {
    ///     Ok(_) => unreachable!("not valid JSON"),
    ///     Err(err) => err,
    /// };
    /// // The message never echoes a fragment of the rejected payload.
    /// assert!(err.to_string().starts_with("failed to deserialize from JSON:"));
    /// ```
    #[non_exhaustive]
    pub enum JsonError {
        /// Serializing a body to JSON failed.
        Serialize {
            /// The underlying `serde_json` error.
            source: serde_json::Error,
        },

        /// Deserializing bytes into a body failed.
        Deserialize {
            /// The underlying `serde_json` error.
            source: serde_json::Error,
        },
    }

    /// Renders a `serde_json::Error` as its classification and position only — never its
    /// `Display`, which embeds a fragment of the offending payload for data errors.
    fn describe(source: &serde_json::Error) -> String {
        let category = match source.classify() {
            serde_json::error::Category::Io => "io",
            serde_json::error::Category::Syntax => "syntax",
            serde_json::error::Category::Data => "data",
            serde_json::error::Category::Eof => "eof",
        };

        format!(
            "{category} error at line {}, column {}",
            source.line(),
            source.column()
        )
    }

    impl fmt::Display for JsonError {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            match self {
                Self::Serialize { source } => {
                    write!(f, "failed to serialize to JSON: {}", describe(source))
                }
                Self::Deserialize { source } => {
                    write!(f, "failed to deserialize from JSON: {}", describe(source))
                }
            }
        }
    }

    impl fmt::Debug for JsonError {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            let (variant, source) = match self {
                Self::Serialize { source } => ("Serialize", source),
                Self::Deserialize { source } => ("Deserialize", source),
            };

            f.debug_struct(variant)
                .field("classification", &describe(source))
                .finish()
        }
    }

    impl std::error::Error for JsonError {
        fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
            match self {
                Self::Serialize { source } | Self::Deserialize { source } => Some(source),
            }
        }
    }
}

#[cfg(feature = "json")]
#[cfg_attr(docsrs, doc(cfg(feature = "json")))]
pub use json::{JsonError, JsonSerializer};