Files
rojo/src/snapshot_middleware/csv.rs
Lucien Greathouse 82678235ab VFS Improvements (#259)
This PR refactors all of the methods on `Vfs` from accepting `&mut self` to
accepting `&self` and keeping data wrapped in a mutex. This builds on previous
changes to make reference count file contents and cleans up the last places
where we're returning borrowed data out of the VFS interface.

Once this change lands, there are two possible directions we can go that I see:
* Conservative: Refactor all remaining `&mut Vfs` handles to `&Vfs`
* Interesting: Embrace ref counting by changing `Vfs` methods to accept `self:
  Arc<Self>`, which makes the `VfsEntry` API no longer need an explicit `Vfs`
  argument for its operations.

* Change VfsFetcher to be immutable with internal locking
* Refactor Vfs::would_be_resident
* Refactor Vfs::read_if_not_exists
* Refactor Vfs::raise_file_removed
* Refactor Vfs::raise_file_changed
* Add Vfs::get_internal as bits of Vfs::get
* Switch Vfs to use internal locking
* Migrate all Vfs methods from &mut self to &self
* Make VfsEntry access Vfs immutably
* Remove outer VFS locking (#260)
* Refactor all snapshot middleware to accept &Vfs instead of &mut Vfs
* Remove outer VFS Mutex across the board
2019-10-16 15:45:23 -07:00

194 lines
5.7 KiB
Rust

use std::{borrow::Cow, collections::BTreeMap};
use maplit::hashmap;
use rbx_dom_weak::RbxValue;
use serde::Serialize;
use crate::{
snapshot::{InstanceMetadata, InstanceSnapshot},
vfs::{FsResultExt, Vfs, VfsEntry, VfsFetcher},
};
use super::{
context::InstanceSnapshotContext,
meta_file::AdjacentMetadata,
middleware::{SnapshotInstanceResult, SnapshotMiddleware},
util::match_file_name,
};
pub struct SnapshotCsv;
impl SnapshotMiddleware for SnapshotCsv {
fn from_vfs<F: VfsFetcher>(
_context: &mut InstanceSnapshotContext,
vfs: &Vfs<F>,
entry: &VfsEntry,
) -> SnapshotInstanceResult<'static> {
if entry.is_directory() {
return Ok(None);
}
let instance_name = match match_file_name(entry.path(), ".csv") {
Some(name) => name,
None => return Ok(None),
};
let meta_path = entry
.path()
.with_file_name(format!("{}.meta.json", instance_name));
let table_contents = convert_localization_csv(&entry.contents(vfs)?);
let mut snapshot = InstanceSnapshot {
snapshot_id: None,
metadata: InstanceMetadata {
instigating_source: Some(entry.path().to_path_buf().into()),
relevant_paths: vec![entry.path().to_path_buf(), meta_path.clone()],
..Default::default()
},
name: Cow::Owned(instance_name.to_owned()),
class_name: Cow::Borrowed("LocalizationTable"),
properties: hashmap! {
"Contents".to_owned() => RbxValue::String {
value: table_contents,
},
},
children: Vec::new(),
};
if let Some(meta_entry) = vfs.get(meta_path).with_not_found()? {
let meta_contents = meta_entry.contents(vfs)?;
let mut metadata = AdjacentMetadata::from_slice(&meta_contents);
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]) -> String {
let mut reader = csv::Reader::from_reader(contents);
let headers = reader.headers().expect("TODO: Handle csv errors").clone();
let mut records = Vec::new();
for record in reader.into_records() {
let record = record.expect("TODO: Handle csv errors");
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);
}
serde_json::to_string(&entries).expect("Could not encode JSON for localization table")
}
#[cfg(test)]
mod test {
use super::*;
use crate::vfs::{NoopFetcher, VfsDebug, VfsSnapshot};
use insta::assert_yaml_snapshot;
#[test]
fn csv_from_vfs() {
let mut vfs = Vfs::new(NoopFetcher);
let file = VfsSnapshot::file(
r#"
Key,Source,Context,Example,es
Ack,Ack!,,An exclamation of despair,¡Ay!"#,
);
vfs.debug_load_snapshot("/foo.csv", file);
let entry = vfs.get("/foo.csv").unwrap();
let instance_snapshot =
SnapshotCsv::from_vfs(&mut InstanceSnapshotContext::default(), &mut vfs, &entry)
.unwrap()
.unwrap();
assert_yaml_snapshot!(instance_snapshot);
}
#[test]
fn csv_with_meta() {
let mut vfs = Vfs::new(NoopFetcher);
let file = VfsSnapshot::file(
r#"
Key,Source,Context,Example,es
Ack,Ack!,,An exclamation of despair,¡Ay!"#,
);
let meta = VfsSnapshot::file(r#"{ "ignoreUnknownInstances": true }"#);
vfs.debug_load_snapshot("/foo.csv", file);
vfs.debug_load_snapshot("/foo.meta.json", meta);
let entry = vfs.get("/foo.csv").unwrap();
let instance_snapshot =
SnapshotCsv::from_vfs(&mut InstanceSnapshotContext::default(), &mut vfs, &entry)
.unwrap()
.unwrap();
assert_yaml_snapshot!(instance_snapshot);
}
}