Skip to main content

rskit_dataset/record/
model.rs

1//! Format-agnostic structured dataset record.
2
3use std::collections::BTreeMap;
4
5use serde_json::Value;
6
7/// Format identifier for built-in dataset record readers and writers.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9#[non_exhaustive]
10pub enum DatasetFormat {
11    /// Comma-separated values with a header row.
12    Csv,
13    /// JSON array of records, intended for bounded fixture-style datasets.
14    JsonArray,
15    /// Newline-delimited JSON records.
16    JsonLines,
17}
18
19/// Format-agnostic structured dataset row.
20#[derive(Debug, Clone, PartialEq)]
21pub struct DatasetRecord {
22    fields: BTreeMap<String, Value>,
23}
24
25impl DatasetRecord {
26    /// Create a record from named fields.
27    #[must_use]
28    pub fn new(fields: BTreeMap<String, Value>) -> Self {
29        Self { fields }
30    }
31
32    /// Create a record from any iterator of named fields.
33    #[must_use]
34    pub fn from_fields<I, K>(fields: I) -> Self
35    where
36        I: IntoIterator<Item = (K, Value)>,
37        K: Into<String>,
38    {
39        Self {
40            fields: fields
41                .into_iter()
42                .map(|(key, value)| (key.into(), value))
43                .collect(),
44        }
45    }
46
47    /// Borrow a field by name.
48    #[must_use]
49    pub fn get(&self, name: &str) -> Option<&Value> {
50        self.fields.get(name)
51    }
52
53    /// Borrow all record fields in deterministic key order.
54    #[must_use]
55    pub fn fields(&self) -> &BTreeMap<String, Value> {
56        &self.fields
57    }
58
59    /// Consume this record into its fields.
60    #[must_use]
61    pub fn into_fields(self) -> BTreeMap<String, Value> {
62        self.fields
63    }
64
65    /// Return a projected record with only the requested columns.
66    #[must_use]
67    pub fn select(&self, columns: &[String]) -> Self {
68        let fields = columns
69            .iter()
70            .filter_map(|column| {
71                self.fields
72                    .get(column)
73                    .map(|value| (column.clone(), value.clone()))
74            })
75            .collect();
76        Self { fields }
77    }
78
79    /// Convert this record to a JSON object.
80    #[must_use]
81    pub fn into_json(self) -> Value {
82        Value::Object(self.fields.into_iter().collect())
83    }
84
85    /// Borrow this record as a JSON object without consuming it.
86    #[must_use]
87    pub fn to_json(&self) -> Value {
88        Value::Object(self.fields.clone().into_iter().collect())
89    }
90}
91
92impl crate::DatasetItem for DatasetRecord {}
93
94#[cfg(test)]
95mod tests {
96    use serde_json::json;
97
98    use super::*;
99
100    #[test]
101    fn dataset_record_projection_and_json_are_deterministic() {
102        let record = DatasetRecord::new(
103            DatasetRecord::from_fields([("b", json!(2)), ("a", json!("one")), ("c", Value::Null)])
104                .into_fields(),
105        );
106
107        assert_eq!(record.get("a"), Some(&json!("one")));
108        assert_eq!(
109            record.fields().keys().cloned().collect::<Vec<_>>(),
110            ["a", "b", "c"]
111        );
112        let selected = record.select(&["c".to_string(), "missing".to_string(), "a".to_string()]);
113        assert_eq!(
114            selected.fields().keys().cloned().collect::<Vec<_>>(),
115            ["a", "c"]
116        );
117        assert_eq!(selected.into_json(), json!({"a":"one","c":null}));
118    }
119}