Skip to main content

sora_export/
cbor.rs

1use sora_diagnostics::{Result, SoraError};
2
3use crate::{
4    bundle::DataBundleView,
5    exporter::{DataExporter, ExportOutput, ExportRequest, OutputKind},
6    fs_util::{create_parent_dir, write_file},
7};
8
9pub struct CborBundleExporter;
10
11impl DataExporter for CborBundleExporter {
12    fn format_name(&self) -> &'static str {
13        "cbor"
14    }
15
16    fn output_kind(&self) -> OutputKind {
17        OutputKind::File
18    }
19
20    fn export(&self, request: ExportRequest<'_>) -> Result<()> {
21        let ExportOutput::File(path) = request.output else {
22            return Err(SoraError::InvalidExportOutput {
23                format: self.format_name().to_owned(),
24                expected: "file",
25            });
26        };
27
28        create_parent_dir(&path)?;
29        let content = serde_cbor::to_vec(&DataBundleView::new("cbor", request.ir, request.data)?)
30            .map_err(|error| SoraError::SerializeDataFormat {
31            format: self.format_name(),
32            message: error.to_string(),
33        })?;
34        write_file(path, content)
35    }
36}
37
38#[cfg(test)]
39mod tests {
40    use super::*;
41    use crate::exporter::ExportOutput;
42    use sora_data::model::{ConfigData, RowData, TableData, Value};
43    use sora_ir::{model::ConfigIr, normalize::normalize_schema};
44    use sora_schema::model::SchemaFile;
45    use std::{
46        collections::BTreeMap,
47        fs,
48        path::PathBuf,
49        sync::atomic::{AtomicUsize, Ordering},
50        time::{SystemTime, UNIX_EPOCH},
51    };
52
53    #[test]
54    fn cbor_exporter_writes_bundle_file() {
55        let ir = example_ir();
56        let data = example_data();
57        let path = temp_dir().join("config.cbor");
58
59        CborBundleExporter
60            .export(ExportRequest {
61                ir: &ir,
62                data: &data,
63                locale_catalog: None,
64                execution: &sora_execution::ExecutionContext::default(),
65                options: Default::default(),
66                output: ExportOutput::File(path.clone()),
67            })
68            .unwrap();
69
70        let value: serde_cbor::Value = serde_cbor::from_slice(&fs::read(&path).unwrap()).unwrap();
71        let serde_cbor::Value::Map(fields) = value else {
72            panic!("expected cbor map");
73        };
74        assert!(fields.iter().any(|(key, value)| {
75            matches!(key, serde_cbor::Value::Text(key) if key == "format")
76                && matches!(value, serde_cbor::Value::Text(value) if value == "cbor")
77        }));
78        assert!(fields.iter().any(|(key, value)| {
79            matches!(key, serde_cbor::Value::Text(key) if key == "schema_fingerprint")
80                && matches!(value, serde_cbor::Value::Text(value) if value.len() > 8)
81        }));
82        assert!(fields.iter().any(|(key, value)| {
83            matches!(key, serde_cbor::Value::Text(key) if key == "data_fingerprint")
84                && matches!(value, serde_cbor::Value::Text(value) if value.len() > 8)
85        }));
86
87        let _ = fs::remove_dir_all(path.parent().unwrap());
88    }
89
90    fn example_ir() -> ConfigIr {
91        let schema: SchemaFile = toml::from_str(
92            r#"
93package = "game_config"
94
95[[tables]]
96name = "Item"
97mode = "map"
98key = "id"
99
100[[tables.fields]]
101name = "id"
102type = "i32"
103"#,
104        )
105        .unwrap();
106        normalize_schema(schema).unwrap()
107    }
108
109    fn example_data() -> ConfigData {
110        ConfigData {
111            tables: vec![TableData {
112                name: "Item".to_owned(),
113                rows: vec![RowData {
114                    values: BTreeMap::from([
115                        ("id".to_owned(), Value::Integer(1001)),
116                        ("name".to_owned(), Value::String("Iron Sword".to_owned())),
117                    ]),
118                }],
119            }],
120        }
121    }
122
123    fn temp_dir() -> PathBuf {
124        static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
125        let unique = SystemTime::now()
126            .duration_since(UNIX_EPOCH)
127            .unwrap()
128            .as_nanos();
129        let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
130        std::env::temp_dir().join(format!("sora-export-cbor-test-{unique}-{id}"))
131    }
132}