Skip to main content

sora_export/
json.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 JsonBundleExporter;
10
11impl DataExporter for JsonBundleExporter {
12    fn format_name(&self) -> &'static str {
13        "json"
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 =
30            serde_json::to_vec_pretty(&DataBundleView::new("json", request.ir, request.data)?)
31                .map_err(SoraError::SerializeData)?;
32        write_file(path, content)
33    }
34}
35
36#[cfg(test)]
37mod tests {
38    use super::*;
39    use crate::exporter::ExportOutput;
40    use sora_data::model::{ConfigData, RowData, TableData, Value};
41    use sora_ir::{model::ConfigIr, normalize::normalize_schema};
42    use sora_schema::model::SchemaFile;
43    use std::{
44        collections::BTreeMap,
45        fs,
46        path::PathBuf,
47        sync::atomic::{AtomicUsize, Ordering},
48        time::{SystemTime, UNIX_EPOCH},
49    };
50
51    #[test]
52    fn json_exporter_writes_bundle_file() {
53        let ir = example_ir();
54        let data = example_data();
55        let path = temp_dir().join("config.json");
56
57        JsonBundleExporter
58            .export(ExportRequest {
59                ir: &ir,
60                data: &data,
61                execution: &sora_execution::ExecutionContext::default(),
62                options: Default::default(),
63                output: ExportOutput::File(path.clone()),
64            })
65            .unwrap();
66
67        let value: serde_json::Value = serde_json::from_slice(&fs::read(&path).unwrap()).unwrap();
68        assert_eq!(value["format"], "json");
69        assert_eq!(value["format_version"], 1);
70        assert!(value["schema_fingerprint"].as_str().unwrap().len() > 8);
71        assert!(value["data_fingerprint"].as_str().unwrap().len() > 8);
72        assert_eq!(value["schema"]["package"], "game_config");
73        assert_eq!(value["data"]["tables"][0]["name"], "Item");
74
75        let _ = fs::remove_dir_all(path.parent().unwrap());
76    }
77
78    fn example_ir() -> ConfigIr {
79        let schema: SchemaFile = toml::from_str(
80            r#"
81package = "game_config"
82
83[[tables]]
84name = "Item"
85mode = "map"
86key = "id"
87
88[[tables.fields]]
89name = "id"
90type = "i32"
91"#,
92        )
93        .unwrap();
94        normalize_schema(schema).unwrap()
95    }
96
97    fn example_data() -> ConfigData {
98        ConfigData {
99            tables: vec![TableData {
100                name: "Item".to_owned(),
101                rows: vec![RowData {
102                    values: BTreeMap::from([
103                        ("id".to_owned(), Value::Integer(1001)),
104                        ("name".to_owned(), Value::String("Iron Sword".to_owned())),
105                    ]),
106                }],
107            }],
108        }
109    }
110
111    fn temp_dir() -> PathBuf {
112        static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
113        let unique = SystemTime::now()
114            .duration_since(UNIX_EPOCH)
115            .unwrap()
116            .as_nanos();
117        let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
118        std::env::temp_dir().join(format!("sora-export-json-test-{unique}-{id}"))
119    }
120}