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 execution: &sora_execution::ExecutionContext::default(),
64 options: Default::default(),
65 output: ExportOutput::File(path.clone()),
66 })
67 .unwrap();
68
69 let value: serde_cbor::Value = serde_cbor::from_slice(&fs::read(&path).unwrap()).unwrap();
70 let serde_cbor::Value::Map(fields) = value else {
71 panic!("expected cbor map");
72 };
73 assert!(fields.iter().any(|(key, value)| {
74 matches!(key, serde_cbor::Value::Text(key) if key == "format")
75 && matches!(value, serde_cbor::Value::Text(value) if value == "cbor")
76 }));
77 assert!(fields.iter().any(|(key, value)| {
78 matches!(key, serde_cbor::Value::Text(key) if key == "schema_fingerprint")
79 && matches!(value, serde_cbor::Value::Text(value) if value.len() > 8)
80 }));
81 assert!(fields.iter().any(|(key, value)| {
82 matches!(key, serde_cbor::Value::Text(key) if key == "data_fingerprint")
83 && matches!(value, serde_cbor::Value::Text(value) if value.len() > 8)
84 }));
85
86 let _ = fs::remove_dir_all(path.parent().unwrap());
87 }
88
89 fn example_ir() -> ConfigIr {
90 let schema: SchemaFile = toml::from_str(
91 r#"
92package = "game_config"
93
94[[tables]]
95name = "Item"
96mode = "map"
97key = "id"
98
99[[tables.fields]]
100name = "id"
101type = "i32"
102"#,
103 )
104 .unwrap();
105 normalize_schema(schema).unwrap()
106 }
107
108 fn example_data() -> ConfigData {
109 ConfigData {
110 tables: vec![TableData {
111 name: "Item".to_owned(),
112 rows: vec![RowData {
113 values: BTreeMap::from([
114 ("id".to_owned(), Value::Integer(1001)),
115 ("name".to_owned(), Value::String("Iron Sword".to_owned())),
116 ]),
117 }],
118 }],
119 }
120 }
121
122 fn temp_dir() -> PathBuf {
123 static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
124 let unique = SystemTime::now()
125 .duration_since(UNIX_EPOCH)
126 .unwrap()
127 .as_nanos();
128 let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
129 std::env::temp_dir().join(format!("sora-export-cbor-test-{unique}-{id}"))
130 }
131}