Skip to main content

aion_core/
payload.rs

1//! Opaque serialized payloads carried through histories and errors.
2
3use serde::{Deserialize, Serialize};
4
5/// Type-erased user data with an explicit content type tag.
6///
7/// Payload is a dumb carrier: it stores bytes and a tag but does not
8/// validate that the bytes match the tag on construction. Validation
9/// happens on read (e.g. [`Payload::to_json`] returns `Result`).
10/// Use [`Payload::from_json`] for a validated construction path.
11#[derive(Serialize, Deserialize, ts_rs::TS, Clone, Debug, PartialEq, Eq)]
12pub struct Payload {
13    content_type: ContentType,
14    bytes: Vec<u8>,
15}
16
17impl Payload {
18    /// Creates an opaque payload from a content type and raw bytes.
19    ///
20    /// No validation is performed — the bytes are not checked against the
21    /// content type. Prefer [`Payload::from_json`] when constructing from
22    /// a known value. Conversion methods (e.g. [`Payload::to_json`])
23    /// validate on read.
24    #[must_use]
25    pub fn new(content_type: ContentType, bytes: Vec<u8>) -> Self {
26        Self {
27            content_type,
28            bytes,
29        }
30    }
31
32    /// Serializes a JSON value into a payload tagged as JSON.
33    ///
34    /// # Errors
35    ///
36    /// Returns an error if the JSON value cannot be serialized.
37    pub fn from_json(value: &serde_json::Value) -> Result<Self, PayloadError> {
38        let bytes = serde_json::to_vec(value)?;
39        Ok(Self::new(ContentType::Json, bytes))
40    }
41
42    /// Deserializes this payload as a JSON value.
43    ///
44    /// # Errors
45    ///
46    /// Returns an error if the payload is not tagged as JSON or the bytes do not contain valid
47    /// JSON.
48    pub fn to_json(&self) -> Result<serde_json::Value, PayloadError> {
49        match self.content_type {
50            ContentType::Json => Ok(serde_json::from_slice(&self.bytes)?),
51        }
52    }
53
54    /// Returns the payload content type tag.
55    #[must_use]
56    pub const fn content_type(&self) -> &ContentType {
57        &self.content_type
58    }
59
60    /// Returns the opaque serialized bytes.
61    #[must_use]
62    pub fn bytes(&self) -> &[u8] {
63        &self.bytes
64    }
65}
66
67/// Stable tag describing the encoding used for a payload's bytes.
68#[derive(Serialize, Deserialize, ts_rs::TS, Clone, Debug, PartialEq, Eq, Hash)]
69pub enum ContentType {
70    /// A `serde_json::Value` serialized as UTF-8 JSON bytes.
71    Json,
72}
73
74/// Errors produced when converting payloads to or from typed values.
75#[derive(thiserror::Error, Debug)]
76pub enum PayloadError {
77    /// JSON serialization or deserialization failed.
78    #[error("json payload conversion failed: {0}")]
79    Json(#[from] serde_json::Error),
80}
81
82#[cfg(test)]
83mod tests {
84    use serde_json::json;
85
86    use super::{ContentType, Payload};
87
88    #[test]
89    fn json_values_round_trip_losslessly() -> Result<(), Box<dyn std::error::Error>> {
90        let values = [
91            serde_json::Value::Null,
92            json!(true),
93            json!(123.45),
94            json!("hello"),
95            json!([null, false, 7, "item"]),
96            json!({"nested": {"value": 1}, "array": [true, false]}),
97        ];
98
99        for value in values {
100            let payload = Payload::from_json(&value)?;
101            assert_eq!(payload.content_type(), &ContentType::Json);
102            assert_eq!(payload.to_json()?, value);
103        }
104
105        Ok(())
106    }
107}