Files
rojo/src/snapshot_middleware/csv.rs
2021-08-24 17:59:53 -04:00

183 lines
5.0 KiB
Rust

use std::{collections::BTreeMap, path::Path};
use anyhow::Context;
use maplit::hashmap;
use memofs::{IoResultExt, Vfs};
use serde::Serialize;
use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot};
use super::{meta_file::AdjacentMetadata, util::PathExt};
pub fn snapshot_csv(
_context: &InstanceContext,
vfs: &Vfs,
path: &Path,
) -> anyhow::Result<Option<InstanceSnapshot>> {
let name = path.file_name_trim_end(".csv")?;
let meta_path = path.with_file_name(format!("{}.meta.json", name));
let contents = vfs.read(path)?;
let table_contents = convert_localization_csv(&contents).with_context(|| {
format!(
"File was not a valid LocalizationTable CSV file: {}",
path.display()
)
})?;
let mut snapshot = InstanceSnapshot::new()
.name(name)
.class_name("LocalizationTable")
.properties(hashmap! {
"Contents".to_owned() => table_contents.into(),
})
.metadata(
InstanceMetadata::new()
.instigating_source(path)
.relevant_paths(vec![path.to_path_buf(), meta_path.clone()]),
);
if let Some(meta_contents) = vfs.read(&meta_path).with_not_found()? {
let mut metadata = AdjacentMetadata::from_slice(&meta_contents, meta_path)?;
metadata.apply_all(&mut snapshot)?;
}
Ok(Some(snapshot))
}
/// Struct that holds any valid row from a Roblox CSV translation table.
///
/// We manually deserialize into this table from CSV, but let serde_json handle
/// serialization.
#[derive(Debug, Default, Serialize)]
#[serde(rename_all = "camelCase")]
struct LocalizationEntry<'a> {
#[serde(skip_serializing_if = "Option::is_none")]
key: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
context: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
example: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
source: Option<&'a str>,
// We use a BTreeMap here to get deterministic output order.
values: BTreeMap<&'a str, &'a str>,
}
/// Normally, we'd be able to let the csv crate construct our struct for us.
///
/// However, because of a limitation with Serde's 'flatten' feature, it's not
/// possible presently to losslessly collect extra string values while using
/// csv+Serde.
///
/// https://github.com/BurntSushi/rust-csv/issues/151
///
/// This function operates in one step in order to minimize data-copying.
fn convert_localization_csv(contents: &[u8]) -> Result<String, csv::Error> {
let mut reader = csv::Reader::from_reader(contents);
let headers = reader.headers()?.clone();
let mut records = Vec::new();
for record in reader.into_records() {
records.push(record?);
}
let mut entries = Vec::new();
for record in &records {
let mut entry = LocalizationEntry::default();
for (header, value) in headers.iter().zip(record.into_iter()) {
if header.is_empty() || value.is_empty() {
continue;
}
match header {
"Key" => entry.key = Some(value),
"Source" => entry.source = Some(value),
"Context" => entry.context = Some(value),
"Example" => entry.example = Some(value),
_ => {
entry.values.insert(header, value);
}
}
}
if entry.key.is_none() && entry.source.is_none() {
continue;
}
entries.push(entry);
}
let encoded =
serde_json::to_string(&entries).expect("Could not encode JSON for localization table");
Ok(encoded)
}
#[cfg(test)]
mod test {
use super::*;
use memofs::{InMemoryFs, VfsSnapshot};
#[test]
fn csv_from_vfs() {
let mut imfs = InMemoryFs::new();
imfs.load_snapshot(
"/foo.csv",
VfsSnapshot::file(
r#"
Key,Source,Context,Example,es
Ack,Ack!,,An exclamation of despair,¡Ay!"#,
),
)
.unwrap();
let mut vfs = Vfs::new(imfs);
let instance_snapshot =
snapshot_csv(&InstanceContext::default(), &mut vfs, Path::new("/foo.csv"))
.unwrap()
.unwrap();
insta::assert_yaml_snapshot!(instance_snapshot);
}
#[test]
fn csv_with_meta() {
let mut imfs = InMemoryFs::new();
imfs.load_snapshot(
"/foo.csv",
VfsSnapshot::file(
r#"
Key,Source,Context,Example,es
Ack,Ack!,,An exclamation of despair,¡Ay!"#,
),
)
.unwrap();
imfs.load_snapshot(
"/foo.meta.json",
VfsSnapshot::file(r#"{ "ignoreUnknownInstances": true }"#),
)
.unwrap();
let mut vfs = Vfs::new(imfs);
let instance_snapshot =
snapshot_csv(&InstanceContext::default(), &mut vfs, Path::new("/foo.csv"))
.unwrap()
.unwrap();
insta::assert_yaml_snapshot!(instance_snapshot);
}
}