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