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}