Skip to main content

hyperi_rustlib/transport/
payload.rs

1// Project:   hyperi-rustlib
2// File:      src/transport/payload.rs
3// Purpose:   Payload parsing and serialization (JSON/MsgPack)
4// Language:  Rust
5//
6// License:   FSL-1.1-ALv2
7// Copyright: (c) 2026 HYPERI PTY LIMITED
8
9//! # Payload Handling
10//!
11//! Unified JSON and MsgPack parsing with auto-detection.
12//! This module provides a common pattern for all HyperI projects.
13//!
14//! ## Performance Notes
15//!
16//! This implementation uses `serde_json` for JSON parsing. Projects requiring
17//! maximum JSON performance (like dfe-loader-clickhouse) should use `sonic-rs`
18//! directly for SIMD-accelerated parsing.
19//!
20//! ## Example
21//!
22//! ```rust,ignore
23//! use hyperi_rustlib::transport::payload::{parse_payload, PayloadValue};
24//!
25//! let json_bytes = br#"{"event": "login", "user_id": 123}"#;
26//! let value = parse_payload(json_bytes)?;
27//!
28//! if let PayloadValue::Json(obj) = value {
29//!     println!("Parsed JSON: {:?}", obj);
30//! }
31//! ```
32
33use super::error::{TransportError, TransportResult};
34use super::types::PayloadFormat;
35use serde::{Serialize, de::DeserializeOwned};
36
37/// Parsed payload value.
38#[derive(Debug, Clone)]
39pub enum PayloadValue {
40    /// JSON value (serde_json::Value).
41    Json(serde_json::Value),
42    /// MsgPack was converted to JSON value for uniform handling.
43    MsgPack(serde_json::Value),
44}
45
46impl PayloadValue {
47    /// Get the inner JSON value regardless of original format.
48    #[must_use]
49    pub fn as_json(&self) -> &serde_json::Value {
50        match self {
51            Self::Json(v) | Self::MsgPack(v) => v,
52        }
53    }
54
55    /// Take ownership of the inner JSON value.
56    #[must_use]
57    pub fn into_json(self) -> serde_json::Value {
58        match self {
59            Self::Json(v) | Self::MsgPack(v) => v,
60        }
61    }
62
63    /// Returns true if originally JSON.
64    #[must_use]
65    pub fn is_json(&self) -> bool {
66        matches!(self, Self::Json(_))
67    }
68
69    /// Returns true if originally MsgPack.
70    #[must_use]
71    pub fn is_msgpack(&self) -> bool {
72        matches!(self, Self::MsgPack(_))
73    }
74}
75
76/// Parse payload bytes into a JSON value.
77///
78/// Auto-detects format (JSON or MsgPack) and converts to serde_json::Value.
79/// MsgPack is converted to JSON for uniform downstream processing.
80///
81/// # Errors
82///
83/// Returns error if parsing fails for the detected format.
84pub fn parse_payload(bytes: &[u8]) -> TransportResult<PayloadValue> {
85    let format = PayloadFormat::detect(bytes);
86    parse_payload_with_format(bytes, format)
87}
88
89/// Parse payload bytes with explicit format.
90///
91/// # Errors
92///
93/// Returns error if parsing fails.
94pub fn parse_payload_with_format(
95    bytes: &[u8],
96    format: PayloadFormat,
97) -> TransportResult<PayloadValue> {
98    match format {
99        PayloadFormat::Auto => parse_payload(bytes),
100        PayloadFormat::Json => {
101            let value: serde_json::Value = serde_json::from_slice(bytes)
102                .map_err(|e| TransportError::Internal(format!("JSON parse error: {e}")))?;
103            Ok(PayloadValue::Json(value))
104        }
105        PayloadFormat::MsgPack => {
106            let value: serde_json::Value = rmp_serde::from_slice(bytes)
107                .map_err(|e| TransportError::Internal(format!("MsgPack parse error: {e}")))?;
108            Ok(PayloadValue::MsgPack(value))
109        }
110    }
111}
112
113/// Parse payload bytes into a typed struct.
114///
115/// Auto-detects format and deserializes directly to the target type.
116///
117/// # Errors
118///
119/// Returns error if parsing or deserialization fails.
120pub fn parse_payload_typed<T: DeserializeOwned>(bytes: &[u8]) -> TransportResult<T> {
121    let format = PayloadFormat::detect(bytes);
122    match format {
123        PayloadFormat::Json | PayloadFormat::Auto => serde_json::from_slice(bytes)
124            .map_err(|e| TransportError::Internal(format!("JSON deserialize error: {e}"))),
125        PayloadFormat::MsgPack => rmp_serde::from_slice(bytes)
126            .map_err(|e| TransportError::Internal(format!("MsgPack deserialize error: {e}"))),
127    }
128}
129
130/// Serialize a value to JSON bytes.
131///
132/// # Errors
133///
134/// Returns error if serialization fails.
135pub fn serialize_json<T: Serialize>(value: &T) -> TransportResult<Vec<u8>> {
136    serde_json::to_vec(value)
137        .map_err(|e| TransportError::Internal(format!("JSON serialize error: {e}")))
138}
139
140/// Serialize a value to MsgPack bytes.
141///
142/// # Errors
143///
144/// Returns error if serialization fails.
145pub fn serialize_msgpack<T: Serialize>(value: &T) -> TransportResult<Vec<u8>> {
146    rmp_serde::to_vec(value)
147        .map_err(|e| TransportError::Internal(format!("MsgPack serialize error: {e}")))
148}
149
150/// Serialize a value to the specified format.
151///
152/// # Errors
153///
154/// Returns error if serialization fails.
155pub fn serialize_payload<T: Serialize>(
156    value: &T,
157    format: PayloadFormat,
158) -> TransportResult<Vec<u8>> {
159    match format {
160        PayloadFormat::Json | PayloadFormat::Auto => serialize_json(value),
161        PayloadFormat::MsgPack => serialize_msgpack(value),
162    }
163}
164
165/// Extract a field from JSON bytes without full parsing.
166///
167/// This is a simple implementation. For high-performance field extraction,
168/// use sonic-rs `get_from_slice()` in performance-critical code.
169///
170/// # Errors
171///
172/// Returns error if the bytes are not valid JSON or field is not found.
173pub fn extract_field(bytes: &[u8], field: &str) -> TransportResult<Option<serde_json::Value>> {
174    let value: serde_json::Value = serde_json::from_slice(bytes)
175        .map_err(|e| TransportError::Internal(format!("JSON parse error: {e}")))?;
176
177    Ok(value.get(field).cloned())
178}
179
180/// Extract a nested field using dot notation (e.g., "tags.event.org_id").
181///
182/// # Errors
183///
184/// Returns error if the bytes are not valid JSON.
185pub fn extract_nested_field(
186    bytes: &[u8],
187    path: &str,
188) -> TransportResult<Option<serde_json::Value>> {
189    let value: serde_json::Value = serde_json::from_slice(bytes)
190        .map_err(|e| TransportError::Internal(format!("JSON parse error: {e}")))?;
191
192    let mut current = &value;
193    for part in path.split('.') {
194        match current.get(part) {
195            Some(v) => current = v,
196            None => return Ok(None),
197        }
198    }
199
200    Ok(Some(current.clone()))
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206
207    #[test]
208    fn parse_json_object() {
209        let bytes = br#"{"foo": "bar", "num": 42}"#;
210        let value = parse_payload(bytes).unwrap();
211        assert!(value.is_json());
212
213        let json = value.as_json();
214        assert_eq!(json["foo"], "bar");
215        assert_eq!(json["num"], 42);
216    }
217
218    #[test]
219    fn parse_msgpack() {
220        // MsgPack for {"foo": "bar"}
221        // fixmap(1) + fixstr(3) "foo" + fixstr(3) "bar"
222        let bytes = [
223            0x81, // fixmap with 1 element
224            0xa3, b'f', b'o', b'o', // fixstr(3) "foo"
225            0xa3, b'b', b'a', b'r', // fixstr(3) "bar"
226        ];
227        let value = parse_payload(&bytes).unwrap();
228        assert!(value.is_msgpack());
229
230        let json = value.as_json();
231        assert_eq!(json["foo"], "bar");
232    }
233
234    #[test]
235    fn extract_simple_field() {
236        let bytes = br#"{"event": "login", "user_id": 123}"#;
237        let field = extract_field(bytes, "event").unwrap();
238        assert_eq!(field, Some(serde_json::json!("login")));
239    }
240
241    #[test]
242    fn extract_nested_field_path() {
243        let bytes = br#"{"tags": {"event": {"org_id": "acme"}}}"#;
244        let field = extract_nested_field(bytes, "tags.event.org_id").unwrap();
245        assert_eq!(field, Some(serde_json::json!("acme")));
246    }
247
248    #[test]
249    fn extract_missing_field() {
250        let bytes = br#"{"foo": "bar"}"#;
251        let field = extract_field(bytes, "missing").unwrap();
252        assert_eq!(field, None);
253    }
254
255    #[test]
256    fn serialize_roundtrip() {
257        #[derive(Debug, PartialEq, Serialize, serde::Deserialize)]
258        struct Event {
259            name: String,
260            value: i32,
261        }
262
263        let event = Event {
264            name: "test".to_string(),
265            value: 42,
266        };
267
268        // JSON roundtrip
269        let json_bytes = serialize_json(&event).unwrap();
270        let parsed: Event = parse_payload_typed(&json_bytes).unwrap();
271        assert_eq!(event, parsed);
272
273        // MsgPack roundtrip
274        let msgpack_bytes = serialize_msgpack(&event).unwrap();
275        let parsed: Event = parse_payload_typed(&msgpack_bytes).unwrap();
276        assert_eq!(event, parsed);
277    }
278}