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}