reflex/payload/
mod.rs

1//! Payload encoding/decoding helpers.
2//!
3//! Reflex uses Tauq as a compact, schema-friendly representation of JSON payloads.
4
5use serde_json::Value;
6
7/// Errors returned by Tauq parsing.
8pub use tauq::error::TauqError;
9
10/// Encodes JSON values into Tauq.
11pub struct TauqEncoder;
12
13impl TauqEncoder {
14    /// Encodes a JSON value into Tauq text.
15    pub fn encode(value: &Value) -> String {
16        tauq::format_to_tauq(value)
17    }
18}
19
20/// Decodes Tauq text into JSON values.
21pub struct TauqDecoder;
22
23impl TauqDecoder {
24    /// Decodes Tauq into a single JSON value.
25    pub fn decode(input: &str) -> Result<Value, TauqError> {
26        tauq::compile_tauq(input)
27    }
28
29    /// Decodes Tauq into a batch (array) or a single value wrapped as a 1-item batch.
30    pub fn decode_batch(input: &str) -> Result<Vec<Value>, TauqError> {
31        let value = tauq::compile_tauq(input)?;
32        match value {
33            Value::Array(arr) => Ok(arr),
34            _ => Ok(vec![value]),
35        }
36    }
37}
38
39/// Encodes multiple JSON values into a Tauq array.
40pub struct TauqBatchEncoder;
41
42impl TauqBatchEncoder {
43    /// Encodes all values as a Tauq array.
44    pub fn encode_all(values: &[Value]) -> Result<String, TauqError> {
45        let array = Value::Array(values.to_vec());
46        Ok(tauq::format_to_tauq(&array))
47    }
48}
49
50/// Converts a payload to Tauq if it looks like JSON; otherwise returns it unchanged.
51///
52/// This is a best-effort helper: it never errors.
53///
54/// # Example
55///
56/// ```
57/// use reflex::payload::ensure_tauq_format;
58///
59/// // Valid JSON gets encoded
60/// let json_payload = r#"{"key": "value"}"#;
61/// let result = ensure_tauq_format(json_payload);
62/// // result is Tauq-encoded
63///
64/// // Invalid JSON passes through unchanged
65/// let invalid = "not valid json {";
66/// let result = ensure_tauq_format(invalid);
67/// assert_eq!(result, invalid);
68///
69/// // Plain text passes through unchanged
70/// let plain = "Hello, world!";
71/// let result = ensure_tauq_format(plain);
72/// assert_eq!(result, plain);
73/// ```
74pub fn ensure_tauq_format(payload: &str) -> String {
75    let trimmed = payload.trim();
76
77    if trimmed.starts_with('{') || trimmed.starts_with('[') {
78        match serde_json::from_str::<Value>(trimmed) {
79            Ok(json_value) => TauqEncoder::encode(&json_value),
80            Err(_) => payload.to_string(),
81        }
82    } else {
83        payload.to_string()
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90    use serde_json::json;
91
92    fn assert_json_eq(left: &Value, right: &Value) {
93        if left == right {
94            return;
95        }
96
97        match (left, right) {
98            (Value::Number(n1), Value::Number(n2)) => {
99                let f1 = n1.as_f64().unwrap_or(0.0);
100                let f2 = n2.as_f64().unwrap_or(0.0);
101                if (f1 - f2).abs() > 1e-9 {
102                    panic!("Numbers mismatch: {:?} != {:?}", n1, n2);
103                }
104            }
105            (Value::Object(o1), Value::Object(o2)) => {
106                if o1.len() != o2.len() {
107                    panic!("Object length mismatch: {:?} != {:?}", o1, o2);
108                }
109                for (k, v1) in o1 {
110                    if let Some(v2) = o2.get(k) {
111                        assert_json_eq(v1, v2);
112                    } else {
113                        panic!("Key missing in right: {}", k);
114                    }
115                }
116            }
117            (Value::Array(a1), Value::Array(a2)) => {
118                if a1.len() != a2.len() {
119                    panic!("Array length mismatch: {:?} != {:?}", a1, a2);
120                }
121                for (v1, v2) in a1.iter().zip(a2.iter()) {
122                    assert_json_eq(v1, v2);
123                }
124            }
125            _ => panic!("Mismatch: {:?} != {:?}", left, right),
126        }
127    }
128
129    #[test]
130    fn test_encode_simple_string() {
131        let json = json!("hello world");
132        let tauq = TauqEncoder::encode(&json);
133        assert!(tauq.contains("hello world"));
134    }
135
136    #[test]
137    fn test_roundtrip_simple_object() {
138        let json = json!({
139            "Response": {
140                "text": "Hello world",
141                "confidence": 0.95,
142                "timestamp": 1735689600
143            }
144        });
145
146        let tauq = TauqEncoder::encode(&json);
147        let decoded = TauqDecoder::decode(&tauq).unwrap();
148        assert_json_eq(&json, &decoded);
149    }
150
151    #[test]
152    fn test_roundtrip_with_special_chars() {
153        let json = json!({
154            "Response": {
155                "text": "Line 1\nLine 2\twith\ttabs",
156                "quote": "He said \"hello\""
157            }
158        });
159
160        let tauq = TauqEncoder::encode(&json);
161        let decoded = TauqDecoder::decode(&tauq).unwrap();
162        assert_json_eq(&json, &decoded);
163    }
164
165    #[test]
166    fn test_roundtrip_complex() {
167        let json = json!({
168            "Response": {
169                "text": "To reset your password, go to Settings > Security and click Reset Password",
170                "confidence": 0.99,
171                "timestamp": 1735689600,
172                "source": "knowledge_base",
173                "tokens_used": 42,
174                "metadata": {
175                    "cached": true,
176                    "ttl": 3600
177                },
178                "alternatives": ["option1", "option2"]
179            }
180        });
181
182        let tauq = TauqEncoder::encode(&json);
183        let decoded = TauqDecoder::decode(&tauq).unwrap();
184        assert_json_eq(&json, &decoded);
185    }
186
187    #[test]
188    fn test_compression_ratio_single_object() {
189        let json = json!({
190            "Response": {
191                "text": "To reset your password, go to Settings > Security and click Reset Password. You will receive an email with a reset link. The link expires in 24 hours.",
192                "confidence": 0.99,
193                "timestamp": 1735689600,
194                "source": "knowledge_base",
195                "tokens_used": 42
196            }
197        });
198
199        let json_str = serde_json::to_string(&json).unwrap();
200        let tauq = TauqEncoder::encode(&json);
201
202        let json_bytes = json_str.len();
203        let tauq_bytes = tauq.len();
204
205        println!(
206            "Single object - JSON: {} bytes, Tauq: {} bytes",
207            json_bytes, tauq_bytes
208        );
209
210        assert!(
211            tauq_bytes as f64 <= json_bytes as f64 * 1.1,
212            "Tauq should not be significantly larger than JSON"
213        );
214    }
215
216    #[test]
217    fn test_compression_ratio_batch_with_shared_schema() {
218        let values = vec![
219            json!({"Response": {"confidence": 0.95, "source": "cache", "text": "First response text here with some content", "timestamp": 1735689600}}),
220            json!({"Response": {"confidence": 0.87, "source": "cache", "text": "Second response with different text content", "timestamp": 1735689700}}),
221            json!({"Response": {"confidence": 0.92, "source": "cache", "text": "Third response also quite lengthy text data", "timestamp": 1735689800}}),
222            json!({"Response": {"confidence": 0.88, "source": "cache", "text": "Fourth response with more example content", "timestamp": 1735689900}}),
223            json!({"Response": {"confidence": 0.91, "source": "cache", "text": "Fifth response completing our batch example", "timestamp": 1735690000}}),
224        ];
225
226        let tauq_batch = TauqBatchEncoder::encode_all(&values).unwrap();
227
228        let json_entries: Vec<String> = values
229            .iter()
230            .map(|v| serde_json::to_string(v).unwrap())
231            .collect();
232        let _json_batch = json_entries.join("\n");
233
234        let decoded = TauqDecoder::decode_batch(&tauq_batch).unwrap();
235        assert_eq!(values.len(), decoded.len());
236        for (original, decoded_value) in values.iter().zip(decoded.iter()) {
237            assert_json_eq(original, decoded_value);
238        }
239    }
240}