Skip to main content

structfs_core_store/
record.rs

1//! The Record type - maybe-parsed data with format hint.
2
3use bytes::Bytes;
4
5use crate::{Codec, Error, Format, Value};
6
7/// A record that can be forwarded without parsing or parsed for inspection.
8///
9/// This is the core abstraction for zero-copy forwarding. A Record is either:
10/// - `Raw`: Unparsed bytes with a format hint. Can be forwarded without parsing.
11/// - `Parsed`: A parsed `Value` tree. Efficient for inspection and modification.
12///
13/// # Zero-Copy Forwarding
14///
15/// ```rust
16/// use structfs_core_store::{Record, Format};
17/// use bytes::Bytes;
18///
19/// // Data comes in as raw bytes
20/// let record = Record::raw(Bytes::from_static(b"{\"name\":\"Alice\"}"), Format::JSON);
21///
22/// // Forward without parsing - just pass the Record through
23/// // No JSON parsing happens!
24/// ```
25///
26/// # Lazy Parsing
27///
28/// ```rust
29/// use structfs_core_store::{Record, Format, Value};
30/// use bytes::Bytes;
31///
32/// let record = Record::raw(Bytes::from_static(b"..."), Format::JSON);
33///
34/// // Only parse when you need to inspect
35/// // let value = record.into_value(&codec)?;
36/// ```
37#[derive(Clone)]
38#[non_exhaustive]
39pub enum Record {
40    /// Unparsed bytes with format hint.
41    ///
42    /// The bytes can be forwarded without parsing. Use `into_value()` to
43    /// parse when you need to inspect or modify the data.
44    #[non_exhaustive]
45    Raw {
46        /// The raw bytes.
47        bytes: Bytes,
48        /// Hint about the format (JSON, protobuf, etc.)
49        format: Format,
50    },
51
52    /// Parsed tree structure.
53    ///
54    /// Efficient for inspection and modification. Use `into_bytes()` to
55    /// serialize when you need to send over the wire.
56    Parsed(Value),
57}
58
59impl Record {
60    // === Construction ===
61
62    /// Create a record from raw bytes.
63    pub fn raw(bytes: impl Into<Bytes>, format: Format) -> Self {
64        Record::Raw {
65            bytes: bytes.into(),
66            format,
67        }
68    }
69
70    /// Create a record from a parsed value.
71    pub fn parsed(value: Value) -> Self {
72        Record::Parsed(value)
73    }
74
75    // === Inspection (cheap) ===
76
77    /// Check if this record is in raw (unparsed) form.
78    pub fn is_raw(&self) -> bool {
79        matches!(self, Record::Raw { .. })
80    }
81
82    /// Check if this record is parsed.
83    pub fn is_parsed(&self) -> bool {
84        matches!(self, Record::Parsed(_))
85    }
86
87    /// Get the format hint.
88    ///
89    /// For `Parsed` records, returns `Format::VALUE`.
90    pub fn format(&self) -> Format {
91        match self {
92            Record::Raw { format, .. } => format.clone(),
93            Record::Parsed(_) => Format::VALUE,
94        }
95    }
96
97    /// Get raw bytes if available without serialization.
98    ///
99    /// Returns `None` for `Parsed` records (would require serialization).
100    pub fn as_bytes(&self) -> Option<&Bytes> {
101        match self {
102            Record::Raw { bytes, .. } => Some(bytes),
103            Record::Parsed(_) => None,
104        }
105    }
106
107    /// Get parsed value if available without parsing.
108    ///
109    /// Returns `None` for `Raw` records (would require parsing).
110    pub fn as_value(&self) -> Option<&Value> {
111        match self {
112            Record::Raw { .. } => None,
113            Record::Parsed(v) => Some(v),
114        }
115    }
116
117    // === Conversion (potentially costly) ===
118
119    /// Parse into a Value.
120    ///
121    /// - For `Parsed` records: returns the value (no cost).
122    /// - For `Raw` records: parses the bytes using the codec.
123    ///
124    /// This is where you pay the parsing cost.
125    pub fn into_value(self, codec: &dyn Codec) -> Result<Value, Error> {
126        match self {
127            Record::Parsed(v) => Ok(v),
128            Record::Raw { bytes, format } => codec.decode(&bytes, &format),
129        }
130    }
131
132    /// Serialize into bytes.
133    ///
134    /// - For `Raw` records with matching format: returns the bytes (no cost).
135    /// - For `Raw` records with different format: transcodes via Value.
136    /// - For `Parsed` records: serializes using the codec.
137    ///
138    /// This is where you pay the serialization cost.
139    pub fn into_bytes(self, codec: &dyn Codec, target_format: &Format) -> Result<Bytes, Error> {
140        match self {
141            Record::Raw { bytes, format } if &format == target_format => Ok(bytes),
142            Record::Raw { bytes, format } => {
143                // Transcode: parse then re-serialize
144                let value = codec.decode(&bytes, &format)?;
145                codec.encode(&value, target_format)
146            }
147            Record::Parsed(v) => codec.encode(&v, target_format),
148        }
149    }
150
151    /// Try to get bytes without serialization, returning the record if not possible.
152    ///
153    /// Useful when you want bytes if available, but don't want to pay serialization cost.
154    pub fn try_into_bytes(self, target_format: &Format) -> Result<Bytes, Self> {
155        match self {
156            Record::Raw { bytes, format } if &format == target_format => Ok(bytes),
157            other => Err(other),
158        }
159    }
160}
161
162impl std::fmt::Debug for Record {
163    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164        match self {
165            Record::Raw { bytes, format } => f
166                .debug_struct("Record::Raw")
167                .field("bytes_len", &bytes.len())
168                .field("format", format)
169                .finish(),
170            Record::Parsed(v) => f.debug_tuple("Record::Parsed").field(v).finish(),
171        }
172    }
173}
174
175impl From<Value> for Record {
176    fn from(v: Value) -> Self {
177        Record::Parsed(v)
178    }
179}
180
181impl From<Bytes> for Record {
182    fn from(bytes: Bytes) -> Self {
183        Record::Raw {
184            bytes,
185            format: Format::OCTET_STREAM,
186        }
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193    use std::collections::BTreeMap;
194
195    /// Simple test codec that handles JSON
196    struct TestJsonCodec;
197
198    impl Codec for TestJsonCodec {
199        fn decode(&self, bytes: &Bytes, format: &Format) -> Result<Value, Error> {
200            if format != &Format::JSON {
201                return Err(Error::UnsupportedFormat(format.clone()));
202            }
203            let json: serde_json::Value = serde_json::from_slice(bytes)
204                .map_err(|e| Error::decode(format.clone(), e.to_string()))?;
205            Ok(json_to_value(json))
206        }
207
208        fn encode(&self, value: &Value, format: &Format) -> Result<Bytes, Error> {
209            if format != &Format::JSON {
210                return Err(Error::UnsupportedFormat(format.clone()));
211            }
212            let json = value_to_json(value);
213            let bytes = serde_json::to_vec(&json)
214                .map_err(|e| Error::encode(format.clone(), e.to_string()))?;
215            Ok(Bytes::from(bytes))
216        }
217
218        fn supports(&self, format: &Format) -> bool {
219            format == &Format::JSON
220        }
221    }
222
223    fn json_to_value(json: serde_json::Value) -> Value {
224        match json {
225            serde_json::Value::Null => Value::Null,
226            serde_json::Value::Bool(b) => Value::Bool(b),
227            serde_json::Value::Number(n) => {
228                if let Some(i) = n.as_i64() {
229                    Value::Integer(i)
230                } else {
231                    Value::Float(n.as_f64().unwrap_or(0.0))
232                }
233            }
234            serde_json::Value::String(s) => Value::String(s),
235            serde_json::Value::Array(arr) => {
236                Value::Array(arr.into_iter().map(json_to_value).collect())
237            }
238            serde_json::Value::Object(obj) => {
239                let map: BTreeMap<String, Value> = obj
240                    .into_iter()
241                    .map(|(k, v)| (k, json_to_value(v)))
242                    .collect();
243                Value::Map(map)
244            }
245        }
246    }
247
248    fn value_to_json(value: &Value) -> serde_json::Value {
249        match value {
250            Value::Null => serde_json::Value::Null,
251            Value::Bool(b) => serde_json::Value::Bool(*b),
252            Value::Integer(i) => serde_json::Value::Number((*i).into()),
253            Value::Unsigned(i) => serde_json::Value::Number((*i).into()),
254            Value::Float(f) => serde_json::Number::from_f64(*f)
255                .map(serde_json::Value::Number)
256                .unwrap_or(serde_json::Value::Null),
257            Value::String(s) => serde_json::Value::String(s.clone()),
258            Value::Bytes(b) => serde_json::Value::String(format!("bytes:{}", b.len())),
259            Value::Array(arr) => serde_json::Value::Array(arr.iter().map(value_to_json).collect()),
260            Value::Map(map) => {
261                let obj: serde_json::Map<String, serde_json::Value> = map
262                    .iter()
263                    .map(|(k, v)| (k.clone(), value_to_json(v)))
264                    .collect();
265                serde_json::Value::Object(obj)
266            }
267        }
268    }
269
270    #[test]
271    fn raw_record_inspection() {
272        let record = Record::raw(Bytes::from_static(b"hello"), Format::JSON);
273
274        assert!(record.is_raw());
275        assert!(!record.is_parsed());
276        assert_eq!(record.format(), Format::JSON);
277        assert_eq!(record.as_bytes(), Some(&Bytes::from_static(b"hello")));
278        assert_eq!(record.as_value(), None);
279    }
280
281    #[test]
282    fn parsed_record_inspection() {
283        let record = Record::parsed(Value::from("hello"));
284
285        assert!(!record.is_raw());
286        assert!(record.is_parsed());
287        assert_eq!(record.format(), Format::VALUE);
288        assert_eq!(record.as_bytes(), None);
289        assert_eq!(record.as_value(), Some(&Value::from("hello")));
290    }
291
292    #[test]
293    fn try_into_bytes_matching_format() {
294        let bytes = Bytes::from_static(b"hello");
295        let record = Record::raw(bytes.clone(), Format::JSON);
296
297        let result = record.try_into_bytes(&Format::JSON);
298        assert_eq!(result.unwrap(), bytes);
299    }
300
301    #[test]
302    fn try_into_bytes_mismatched_format() {
303        let record = Record::raw(Bytes::from_static(b"hello"), Format::JSON);
304
305        let result = record.try_into_bytes(&Format::PROTOBUF);
306        assert!(result.is_err()); // Returns the record back
307    }
308
309    #[test]
310    fn into_value_parsed() {
311        let codec = TestJsonCodec;
312        let record = Record::parsed(Value::from("hello"));
313        let value = record.into_value(&codec).unwrap();
314        assert_eq!(value, Value::String("hello".to_string()));
315    }
316
317    #[test]
318    fn into_value_raw() {
319        let codec = TestJsonCodec;
320        let record = Record::raw(Bytes::from_static(b"{\"name\":\"Alice\"}"), Format::JSON);
321        let value = record.into_value(&codec).unwrap();
322        match value {
323            Value::Map(map) => {
324                assert_eq!(map.get("name"), Some(&Value::String("Alice".to_string())));
325            }
326            _ => panic!("expected map"),
327        }
328    }
329
330    #[test]
331    fn into_bytes_raw_matching_format() {
332        let codec = TestJsonCodec;
333        let bytes = Bytes::from_static(b"{\"a\":1}");
334        let record = Record::raw(bytes.clone(), Format::JSON);
335        let result = record.into_bytes(&codec, &Format::JSON).unwrap();
336        assert_eq!(result, bytes);
337    }
338
339    #[test]
340    fn into_bytes_parsed() {
341        let codec = TestJsonCodec;
342        let record = Record::parsed(Value::from("hello"));
343        let result = record.into_bytes(&codec, &Format::JSON).unwrap();
344        assert_eq!(result, Bytes::from_static(b"\"hello\""));
345    }
346
347    #[test]
348    fn into_bytes_raw_different_format_error() {
349        let codec = TestJsonCodec;
350        let record = Record::raw(Bytes::from_static(b"data"), Format::JSON);
351        // Trying to transcode to PROTOBUF, which our codec doesn't support
352        let result = record.into_bytes(&codec, &Format::PROTOBUF);
353        assert!(result.is_err());
354    }
355
356    #[test]
357    fn try_into_bytes_parsed_returns_err() {
358        let record = Record::parsed(Value::Null);
359        let result = record.try_into_bytes(&Format::JSON);
360        assert!(result.is_err());
361    }
362
363    #[test]
364    fn debug_raw_record() {
365        let record = Record::raw(Bytes::from_static(b"hello"), Format::JSON);
366        let debug = format!("{:?}", record);
367        assert!(debug.contains("Record::Raw"));
368        assert!(debug.contains("bytes_len"));
369        assert!(debug.contains("format"));
370    }
371
372    #[test]
373    fn debug_parsed_record() {
374        let record = Record::parsed(Value::from(42));
375        let debug = format!("{:?}", record);
376        assert!(debug.contains("Record::Parsed"));
377    }
378
379    #[test]
380    fn from_value_impl() {
381        let value = Value::from("test");
382        let record: Record = value.into();
383        assert!(record.is_parsed());
384        assert_eq!(record.as_value(), Some(&Value::String("test".to_string())));
385    }
386
387    #[test]
388    fn from_bytes_impl() {
389        let bytes = Bytes::from_static(b"test data");
390        let record: Record = bytes.clone().into();
391        assert!(record.is_raw());
392        assert_eq!(record.format(), Format::OCTET_STREAM);
393        assert_eq!(record.as_bytes(), Some(&bytes));
394    }
395
396    #[test]
397    fn clone_raw_record() {
398        let record = Record::raw(Bytes::from_static(b"data"), Format::JSON);
399        let cloned = record.clone();
400        assert!(cloned.is_raw());
401        assert_eq!(cloned.format(), Format::JSON);
402    }
403
404    #[test]
405    fn clone_parsed_record() {
406        let record = Record::parsed(Value::from(123));
407        let cloned = record.clone();
408        assert!(cloned.is_parsed());
409        assert_eq!(cloned.as_value(), Some(&Value::Integer(123)));
410    }
411
412    #[test]
413    fn raw_with_vec_bytes() {
414        let vec = vec![1u8, 2, 3, 4];
415        let record = Record::raw(vec, Format::OCTET_STREAM);
416        assert!(record.is_raw());
417        assert_eq!(record.as_bytes().map(|b| b.len()), Some(4));
418    }
419
420    /// Codec that supports both JSON and a custom format
421    struct MultiFormatCodec;
422
423    impl Codec for MultiFormatCodec {
424        fn decode(&self, bytes: &Bytes, format: &Format) -> Result<Value, Error> {
425            if format == &Format::JSON {
426                let json: serde_json::Value = serde_json::from_slice(bytes)
427                    .map_err(|e| Error::decode(format.clone(), e.to_string()))?;
428                Ok(json_to_value(json))
429            } else if format.as_str() == "text/plain" {
430                let s = String::from_utf8_lossy(bytes);
431                Ok(Value::String(s.to_string()))
432            } else {
433                Err(Error::UnsupportedFormat(format.clone()))
434            }
435        }
436
437        fn encode(&self, value: &Value, format: &Format) -> Result<Bytes, Error> {
438            if format == &Format::JSON {
439                let json = value_to_json(value);
440                let bytes = serde_json::to_vec(&json)
441                    .map_err(|e| Error::encode(format.clone(), e.to_string()))?;
442                Ok(Bytes::from(bytes))
443            } else if format.as_str() == "text/plain" {
444                match value {
445                    Value::String(s) => Ok(Bytes::from(s.clone())),
446                    _ => Ok(Bytes::from(format!("{:?}", value))),
447                }
448            } else {
449                Err(Error::UnsupportedFormat(format.clone()))
450            }
451        }
452
453        fn supports(&self, format: &Format) -> bool {
454            format == &Format::JSON || format.as_str() == "text/plain"
455        }
456    }
457
458    #[test]
459    fn into_bytes_transcode() {
460        // Test transcoding: Raw JSON -> text/plain
461        let codec = MultiFormatCodec;
462        let text_format = Format::new("text/plain");
463
464        // Create a raw JSON record
465        let record = Record::raw(Bytes::from_static(b"\"hello world\""), Format::JSON);
466
467        // Transcode to text/plain
468        let result = record.into_bytes(&codec, &text_format).unwrap();
469
470        // The JSON string "hello world" should be decoded then re-encoded as text/plain
471        assert_eq!(result.as_ref(), b"hello world");
472    }
473
474    #[test]
475    fn into_bytes_transcode_decode_error() {
476        // Test transcode error when decode fails
477        let codec = MultiFormatCodec;
478        let text_format = Format::new("text/plain");
479
480        // Create a raw record with invalid JSON
481        let record = Record::raw(Bytes::from_static(b"not valid json {{{"), Format::JSON);
482
483        // Transcode should fail during decode phase
484        let result = record.into_bytes(&codec, &text_format);
485        assert!(result.is_err());
486    }
487
488    #[test]
489    fn into_value_decode_error() {
490        let codec = TestJsonCodec;
491        let record = Record::raw(Bytes::from_static(b"not valid json"), Format::JSON);
492        let result = record.into_value(&codec);
493        assert!(result.is_err());
494    }
495
496    #[test]
497    fn into_bytes_parsed_encode_error() {
498        let codec = TestJsonCodec;
499        let record = Record::parsed(Value::from("test"));
500        // Try to encode as PROTOBUF which TestJsonCodec doesn't support
501        let result = record.into_bytes(&codec, &Format::PROTOBUF);
502        assert!(result.is_err());
503    }
504
505    #[test]
506    fn format_raw_format_clone() {
507        // Verify that format() returns a cloned format, not a reference
508        let record = Record::raw(Bytes::from_static(b"data"), Format::new("custom/format"));
509        let format = record.format();
510        assert_eq!(format.as_str(), "custom/format");
511    }
512
513    #[test]
514    fn into_value_unsupported_format() {
515        let codec = TestJsonCodec;
516        let record = Record::raw(Bytes::from_static(b"data"), Format::PROTOBUF);
517        let result = record.into_value(&codec);
518        assert!(result.is_err());
519    }
520}