forked from rojo-rbx/rojo
227 lines
6.2 KiB
Rust
227 lines
6.2 KiB
Rust
use std::{collections::BTreeMap, path::Path};
|
|
|
|
use anyhow::Context;
|
|
use memofs::{IoResultExt, Vfs};
|
|
use rbx_dom_weak::ustr;
|
|
use serde::Serialize;
|
|
|
|
use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot};
|
|
|
|
use super::{
|
|
dir::{dir_meta, snapshot_dir_no_meta},
|
|
meta_file::AdjacentMetadata,
|
|
};
|
|
|
|
pub fn snapshot_csv(
|
|
_context: &InstanceContext,
|
|
vfs: &Vfs,
|
|
path: &Path,
|
|
name: &str,
|
|
) -> anyhow::Result<Option<InstanceSnapshot>> {
|
|
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")
|
|
.property(ustr("Contents"), table_contents)
|
|
.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))
|
|
}
|
|
|
|
/// Attempts to snapshot an 'init' csv contained inside of a folder with
|
|
/// the given name.
|
|
///
|
|
/// csv named `init.csv`
|
|
/// their parents, which acts similarly to `__init__.py` from the Python world.
|
|
pub fn snapshot_csv_init(
|
|
context: &InstanceContext,
|
|
vfs: &Vfs,
|
|
init_path: &Path,
|
|
) -> anyhow::Result<Option<InstanceSnapshot>> {
|
|
let folder_path = init_path.parent().unwrap();
|
|
let dir_snapshot = snapshot_dir_no_meta(context, vfs, folder_path)?.unwrap();
|
|
|
|
if dir_snapshot.class_name != "Folder" {
|
|
anyhow::bail!(
|
|
"init.csv can only be used if the instance produced by \
|
|
the containing directory would be a Folder.\n\
|
|
\n\
|
|
The directory {} turned into an instance of class {}.",
|
|
folder_path.display(),
|
|
dir_snapshot.class_name
|
|
);
|
|
}
|
|
|
|
let mut init_snapshot = snapshot_csv(context, vfs, init_path, &dir_snapshot.name)?.unwrap();
|
|
|
|
init_snapshot.children = dir_snapshot.children;
|
|
init_snapshot.metadata = dir_snapshot.metadata;
|
|
|
|
if let Some(mut meta) = dir_meta(vfs, folder_path)? {
|
|
meta.apply_all(&mut init_snapshot)?;
|
|
}
|
|
|
|
Ok(Some(init_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 vfs = Vfs::new(imfs);
|
|
|
|
let instance_snapshot = snapshot_csv(
|
|
&InstanceContext::default(),
|
|
&vfs,
|
|
Path::new("/foo.csv"),
|
|
"foo",
|
|
)
|
|
.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 vfs = Vfs::new(imfs);
|
|
|
|
let instance_snapshot = snapshot_csv(
|
|
&InstanceContext::default(),
|
|
&vfs,
|
|
Path::new("/foo.csv"),
|
|
"foo",
|
|
)
|
|
.unwrap()
|
|
.unwrap();
|
|
|
|
insta::assert_yaml_snapshot!(instance_snapshot);
|
|
}
|
|
}
|