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    #[ts(type = "Array<number> | PayloadElision")]
15    bytes: Vec<u8>,
16}
17
18/// Wire marker that replaces `Payload` bytes when an HTTP projection elides a
19/// payload larger than the caller's byte limit.
20///
21/// Elision applies to serialized JSON projections only — the durable payload
22/// is untouched. `size_bytes` reports the original byte length so consoles
23/// can offer on-demand loading. The marker field is always `true` on the
24/// wire, which the generated TypeScript states as a literal.
25#[derive(Serialize, ts_rs::TS, Clone, Copy, Debug, PartialEq, Eq)]
26pub struct PayloadElision {
27    #[serde(rename = "__elided")]
28    #[ts(type = "true")]
29    elided: bool,
30    size_bytes: u64,
31}
32
33impl PayloadElision {
34    /// Creates the elision marker for an original payload of `size_bytes`.
35    #[must_use]
36    pub const fn new(size_bytes: u64) -> Self {
37        Self {
38            elided: true,
39            size_bytes,
40        }
41    }
42}
43
44impl Payload {
45    /// Creates an opaque payload from a content type and raw bytes.
46    ///
47    /// No validation is performed — the bytes are not checked against the
48    /// content type. Prefer [`Payload::from_json`] when constructing from
49    /// a known value. Conversion methods (e.g. [`Payload::to_json`])
50    /// validate on read.
51    #[must_use]
52    pub fn new(content_type: ContentType, bytes: Vec<u8>) -> Self {
53        Self {
54            content_type,
55            bytes,
56        }
57    }
58
59    /// Serializes a JSON value into a payload tagged as JSON.
60    ///
61    /// # Errors
62    ///
63    /// Returns an error if the JSON value cannot be serialized.
64    pub fn from_json(value: &serde_json::Value) -> Result<Self, PayloadError> {
65        let bytes = serde_json::to_vec(value)?;
66        Ok(Self::new(ContentType::Json, bytes))
67    }
68
69    /// Deserializes this payload as a JSON value.
70    ///
71    /// # Errors
72    ///
73    /// Returns an error if the payload is not tagged as JSON or the bytes do not contain valid
74    /// JSON.
75    pub fn to_json(&self) -> Result<serde_json::Value, PayloadError> {
76        match self.content_type {
77            ContentType::Json => Ok(serde_json::from_slice(&self.bytes)?),
78        }
79    }
80
81    /// Returns the payload content type tag.
82    #[must_use]
83    pub const fn content_type(&self) -> &ContentType {
84        &self.content_type
85    }
86
87    /// Returns the opaque serialized bytes.
88    #[must_use]
89    pub fn bytes(&self) -> &[u8] {
90        &self.bytes
91    }
92}
93
94/// Stable tag describing the encoding used for a payload's bytes.
95#[derive(Serialize, Deserialize, ts_rs::TS, Clone, Debug, PartialEq, Eq, Hash)]
96pub enum ContentType {
97    /// A `serde_json::Value` serialized as UTF-8 JSON bytes.
98    Json,
99}
100
101/// Errors produced when converting payloads to or from typed values.
102#[derive(thiserror::Error, Debug)]
103pub enum PayloadError {
104    /// JSON serialization or deserialization failed.
105    #[error("json payload conversion failed: {0}")]
106    Json(#[from] serde_json::Error),
107}
108
109#[cfg(test)]
110mod tests {
111    use serde_json::json;
112
113    use super::{ContentType, Payload};
114
115    #[test]
116    fn json_values_round_trip_losslessly() -> Result<(), Box<dyn std::error::Error>> {
117        let values = [
118            serde_json::Value::Null,
119            json!(true),
120            json!(123.45),
121            json!("hello"),
122            json!([null, false, 7, "item"]),
123            json!({"nested": {"value": 1}, "array": [true, false]}),
124        ];
125
126        for value in values {
127            let payload = Payload::from_json(&value)?;
128            assert_eq!(payload.content_type(), &ContentType::Json);
129            assert_eq!(payload.to_json()?, value);
130        }
131
132        Ok(())
133    }
134}