librojo/snapshot_middleware/
csv.rs1use std::{collections::BTreeMap, path::Path};
2
3use anyhow::Context;
4use memofs::{IoResultExt, Vfs};
5use rbx_dom_weak::ustr;
6use serde::Serialize;
7
8use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot};
9
10use super::{
11 dir::{dir_meta, snapshot_dir_no_meta},
12 meta_file::AdjacentMetadata,
13};
14
15pub fn snapshot_csv(
16 _context: &InstanceContext,
17 vfs: &Vfs,
18 path: &Path,
19 name: &str,
20) -> anyhow::Result<Option<InstanceSnapshot>> {
21 let meta_path = path.with_file_name(format!("{}.meta.json", name));
22 let contents = vfs.read(path)?;
23
24 let table_contents = convert_localization_csv(&contents).with_context(|| {
25 format!(
26 "File was not a valid LocalizationTable CSV file: {}",
27 path.display()
28 )
29 })?;
30
31 let mut snapshot = InstanceSnapshot::new()
32 .name(name)
33 .class_name("LocalizationTable")
34 .property(ustr("Contents"), table_contents)
35 .metadata(
36 InstanceMetadata::new()
37 .instigating_source(path)
38 .relevant_paths(vec![path.to_path_buf(), meta_path.clone()]),
39 );
40
41 if let Some(meta_contents) = vfs.read(&meta_path).with_not_found()? {
42 let mut metadata = AdjacentMetadata::from_slice(&meta_contents, meta_path)?;
43 metadata.apply_all(&mut snapshot)?;
44 }
45
46 Ok(Some(snapshot))
47}
48
49pub fn snapshot_csv_init(
55 context: &InstanceContext,
56 vfs: &Vfs,
57 init_path: &Path,
58) -> anyhow::Result<Option<InstanceSnapshot>> {
59 let folder_path = init_path.parent().unwrap();
60 let dir_snapshot = snapshot_dir_no_meta(context, vfs, folder_path)?.unwrap();
61
62 if dir_snapshot.class_name != "Folder" {
63 anyhow::bail!(
64 "init.csv can only be used if the instance produced by \
65 the containing directory would be a Folder.\n\
66 \n\
67 The directory {} turned into an instance of class {}.",
68 folder_path.display(),
69 dir_snapshot.class_name
70 );
71 }
72
73 let mut init_snapshot = snapshot_csv(context, vfs, init_path, &dir_snapshot.name)?.unwrap();
74
75 init_snapshot.children = dir_snapshot.children;
76 init_snapshot.metadata = dir_snapshot.metadata;
77
78 if let Some(mut meta) = dir_meta(vfs, folder_path)? {
79 meta.apply_all(&mut init_snapshot)?;
80 }
81
82 Ok(Some(init_snapshot))
83}
84
85#[derive(Debug, Default, Serialize)]
90#[serde(rename_all = "camelCase")]
91struct LocalizationEntry<'a> {
92 #[serde(skip_serializing_if = "Option::is_none")]
93 key: Option<&'a str>,
94
95 #[serde(skip_serializing_if = "Option::is_none")]
96 context: Option<&'a str>,
97
98 #[serde(skip_serializing_if = "Option::is_none")]
99 example: Option<&'a str>,
100
101 #[serde(skip_serializing_if = "Option::is_none")]
102 source: Option<&'a str>,
103
104 values: BTreeMap<&'a str, &'a str>,
106}
107
108fn convert_localization_csv(contents: &[u8]) -> Result<String, csv::Error> {
118 let mut reader = csv::Reader::from_reader(contents);
119
120 let headers = reader.headers()?.clone();
121
122 let mut records = Vec::new();
123
124 for record in reader.into_records() {
125 records.push(record?);
126 }
127
128 let mut entries = Vec::new();
129
130 for record in &records {
131 let mut entry = LocalizationEntry::default();
132
133 for (header, value) in headers.iter().zip(record.into_iter()) {
134 if header.is_empty() || value.is_empty() {
135 continue;
136 }
137
138 match header {
139 "Key" => entry.key = Some(value),
140 "Source" => entry.source = Some(value),
141 "Context" => entry.context = Some(value),
142 "Example" => entry.example = Some(value),
143 _ => {
144 entry.values.insert(header, value);
145 }
146 }
147 }
148
149 if entry.key.is_none() && entry.source.is_none() {
150 continue;
151 }
152
153 entries.push(entry);
154 }
155
156 let encoded =
157 serde_json::to_string(&entries).expect("Could not encode JSON for localization table");
158
159 Ok(encoded)
160}
161
162#[cfg(test)]
163mod test {
164 use super::*;
165
166 use memofs::{InMemoryFs, VfsSnapshot};
167
168 #[test]
169 fn csv_from_vfs() {
170 let mut imfs = InMemoryFs::new();
171 imfs.load_snapshot(
172 "/foo.csv",
173 VfsSnapshot::file(
174 r#"
175Key,Source,Context,Example,es
176Ack,Ack!,,An exclamation of despair,¡Ay!"#,
177 ),
178 )
179 .unwrap();
180
181 let vfs = Vfs::new(imfs);
182
183 let instance_snapshot = snapshot_csv(
184 &InstanceContext::default(),
185 &vfs,
186 Path::new("/foo.csv"),
187 "foo",
188 )
189 .unwrap()
190 .unwrap();
191
192 insta::assert_yaml_snapshot!(instance_snapshot);
193 }
194
195 #[test]
196 fn csv_with_meta() {
197 let mut imfs = InMemoryFs::new();
198 imfs.load_snapshot(
199 "/foo.csv",
200 VfsSnapshot::file(
201 r#"
202Key,Source,Context,Example,es
203Ack,Ack!,,An exclamation of despair,¡Ay!"#,
204 ),
205 )
206 .unwrap();
207 imfs.load_snapshot(
208 "/foo.meta.json",
209 VfsSnapshot::file(r#"{ "ignoreUnknownInstances": true }"#),
210 )
211 .unwrap();
212
213 let vfs = Vfs::new(imfs);
214
215 let instance_snapshot = snapshot_csv(
216 &InstanceContext::default(),
217 &vfs,
218 Path::new("/foo.csv"),
219 "foo",
220 )
221 .unwrap()
222 .unwrap();
223
224 insta::assert_yaml_snapshot!(instance_snapshot);
225 }
226}