Move Rojo server into root of the repository

This commit is contained in:
Lucien Greathouse
2019-08-27 16:56:52 -07:00
parent ec9afba029
commit 6f7dbe99fe
48 changed files with 50 additions and 54 deletions

View File

@@ -0,0 +1,7 @@
pub struct InstanceSnapshotContext {
/// Empty struct that will be used later to fill out required Lua state for
/// user plugins.
pub plugin_context: Option<()>,
}
pub struct ImfsSnapshotContext;

View File

@@ -0,0 +1,169 @@
use std::{borrow::Cow, collections::BTreeMap};
use maplit::hashmap;
use rbx_dom_weak::{RbxId, RbxTree, RbxValue};
use serde::Serialize;
use crate::{
imfs::new::{Imfs, ImfsEntry, ImfsFetcher},
snapshot::InstanceSnapshot,
};
use super::middleware::{SnapshotFileResult, SnapshotInstanceResult, SnapshotMiddleware};
pub struct SnapshotCsv;
impl SnapshotMiddleware for SnapshotCsv {
fn from_imfs<F: ImfsFetcher>(
imfs: &mut Imfs<F>,
entry: &ImfsEntry,
) -> SnapshotInstanceResult<'static> {
if entry.is_directory() {
return Ok(None);
}
let file_name = entry.path().file_name().unwrap().to_string_lossy();
if !file_name.ends_with(".csv") {
return Ok(None);
}
let instance_name = entry
.path()
.file_stem()
.expect("Could not extract file stem")
.to_string_lossy()
.to_string();
let table_contents = convert_localization_csv(entry.contents(imfs)?);
Ok(Some(InstanceSnapshot {
snapshot_id: None,
name: Cow::Owned(instance_name),
class_name: Cow::Borrowed("LocalizationTable"),
properties: hashmap! {
"Contents".to_owned() => RbxValue::String {
value: table_contents,
},
},
children: Vec::new(),
}))
}
fn from_instance(_tree: &RbxTree, _id: RbxId) -> SnapshotFileResult {
unimplemented!("Snapshotting CSV localization tables");
}
}
/// 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::imfs::new::{ImfsSnapshot, NoopFetcher};
#[test]
fn csv_from_imfs() {
let mut imfs = Imfs::new(NoopFetcher);
let file = ImfsSnapshot::file(
r#"
Key,Source,Context,Example,es
Ack,Ack!,,An exclamation of despair,¡Ay!"#,
);
imfs.load_from_snapshot("/foo.csv", file);
let entry = imfs.get("/foo.csv").unwrap();
let instance_snapshot = SnapshotCsv::from_imfs(&mut imfs, &entry).unwrap().unwrap();
let expected_contents =
r#"[{"key":"Ack","example":"An exclamation of despair","source":"Ack!","values":{"es":"¡Ay!"}}]"#;
assert_eq!(instance_snapshot.name, "foo");
assert_eq!(instance_snapshot.class_name, "LocalizationTable");
assert_eq!(instance_snapshot.children, Vec::new());
assert_eq!(
instance_snapshot.properties,
hashmap! {
"Contents".to_owned() => RbxValue::String {
value: expected_contents.to_owned(),
},
}
);
}
}

View File

@@ -0,0 +1,121 @@
use std::{borrow::Cow, collections::HashMap};
use rbx_dom_weak::{RbxId, RbxTree};
use crate::{
imfs::new::{DirectorySnapshot, Imfs, ImfsEntry, ImfsFetcher, ImfsSnapshot},
snapshot::InstanceSnapshot,
};
use super::{
middleware::{SnapshotFileResult, SnapshotInstanceResult, SnapshotMiddleware},
snapshot_from_imfs, snapshot_from_instance,
};
pub struct SnapshotDir;
impl SnapshotMiddleware for SnapshotDir {
fn from_imfs<F: ImfsFetcher>(
imfs: &mut Imfs<F>,
entry: &ImfsEntry,
) -> SnapshotInstanceResult<'static> {
if entry.is_file() {
return Ok(None);
}
let children: Vec<ImfsEntry> = entry.children(imfs)?;
let mut snapshot_children = Vec::new();
for child in children.into_iter() {
if let Some(child_snapshot) = snapshot_from_imfs(imfs, &child)? {
snapshot_children.push(child_snapshot);
}
}
let instance_name = entry
.path()
.file_name()
.expect("Could not extract file name")
.to_str()
.unwrap()
.to_string();
Ok(Some(InstanceSnapshot {
snapshot_id: None,
name: Cow::Owned(instance_name),
class_name: Cow::Borrowed("Folder"),
properties: HashMap::new(),
children: snapshot_children,
}))
}
fn from_instance(tree: &RbxTree, id: RbxId) -> SnapshotFileResult {
let instance = tree.get_instance(id).unwrap();
if instance.class_name != "Folder" {
return None;
}
let mut children = HashMap::new();
for child_id in instance.get_children_ids() {
if let Some((name, child)) = snapshot_from_instance(tree, *child_id) {
children.insert(name, child);
}
}
let snapshot = ImfsSnapshot::Directory(DirectorySnapshot { children });
Some((instance.name.clone(), snapshot))
}
}
#[cfg(test)]
mod test {
use super::*;
use maplit::hashmap;
use crate::imfs::new::NoopFetcher;
#[test]
fn empty_folder() {
let mut imfs = Imfs::new(NoopFetcher);
let dir = ImfsSnapshot::dir::<String>(HashMap::new());
imfs.load_from_snapshot("/foo", dir);
let entry = imfs.get("/foo").unwrap();
let instance_snapshot = SnapshotDir::from_imfs(&mut imfs, &entry).unwrap().unwrap();
assert_eq!(instance_snapshot.name, "foo");
assert_eq!(instance_snapshot.class_name, "Folder");
assert_eq!(instance_snapshot.properties, HashMap::new());
assert_eq!(instance_snapshot.children, Vec::new());
}
#[test]
fn folder_in_folder() {
let mut imfs = Imfs::new(NoopFetcher);
let dir = ImfsSnapshot::dir(hashmap! {
"Child" => ImfsSnapshot::dir::<String>(HashMap::new()),
});
imfs.load_from_snapshot("/foo", dir);
let entry = imfs.get("/foo").unwrap();
let instance_snapshot = SnapshotDir::from_imfs(&mut imfs, &entry).unwrap().unwrap();
assert_eq!(instance_snapshot.name, "foo");
assert_eq!(instance_snapshot.class_name, "Folder");
assert_eq!(instance_snapshot.properties, HashMap::new());
assert_eq!(instance_snapshot.children.len(), 1);
let child = &instance_snapshot.children[0];
assert_eq!(child.name, "Child");
assert_eq!(child.class_name, "Folder");
assert_eq!(child.properties, HashMap::new());
assert_eq!(child.children, Vec::new());
}
}

View File

@@ -0,0 +1,91 @@
use std::{error::Error, fmt, path::PathBuf};
use crate::snapshot::InstanceSnapshot;
pub type SnapshotResult<'a> = Result<Option<InstanceSnapshot<'a>>, SnapshotError>;
#[derive(Debug)]
pub struct SnapshotError {
detail: SnapshotErrorDetail,
path: Option<PathBuf>,
}
impl SnapshotError {
pub fn new(detail: SnapshotErrorDetail, path: Option<impl Into<PathBuf>>) -> Self {
SnapshotError {
detail,
path: path.map(Into::into),
}
}
pub(crate) fn file_did_not_exist(path: impl Into<PathBuf>) -> SnapshotError {
SnapshotError {
detail: SnapshotErrorDetail::FileDidNotExist,
path: Some(path.into()),
}
}
pub(crate) fn file_name_bad_unicode(path: impl Into<PathBuf>) -> SnapshotError {
SnapshotError {
detail: SnapshotErrorDetail::FileNameBadUnicode,
path: Some(path.into()),
}
}
pub(crate) fn file_contents_bad_unicode(
inner: std::str::Utf8Error,
path: impl Into<PathBuf>,
) -> SnapshotError {
SnapshotError {
detail: SnapshotErrorDetail::FileContentsBadUnicode { inner },
path: Some(path.into()),
}
}
}
impl Error for SnapshotError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
self.detail.source()
}
}
impl fmt::Display for SnapshotError {
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
match &self.path {
Some(path) => write!(formatter, "{} in path {}", self.detail, path.display()),
None => write!(formatter, "{}", self.detail),
}
}
}
#[derive(Debug)]
pub enum SnapshotErrorDetail {
FileDidNotExist,
FileNameBadUnicode,
FileContentsBadUnicode { inner: std::str::Utf8Error },
}
impl SnapshotErrorDetail {
fn source(&self) -> Option<&(dyn Error + 'static)> {
use self::SnapshotErrorDetail::*;
match self {
FileContentsBadUnicode { inner } => Some(inner),
_ => None,
}
}
}
impl fmt::Display for SnapshotErrorDetail {
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
use self::SnapshotErrorDetail::*;
match self {
FileDidNotExist => write!(formatter, "file did not exist"),
FileNameBadUnicode => write!(formatter, "file name had malformed Unicode"),
FileContentsBadUnicode { inner } => {
write!(formatter, "file had malformed unicode: {}", inner)
}
}
}
}

View File

@@ -0,0 +1,189 @@
use std::{borrow::Cow, collections::HashMap};
use rbx_dom_weak::{RbxId, RbxTree, UnresolvedRbxValue};
use rbx_reflection::try_resolve_value;
use serde::Deserialize;
use crate::{
imfs::new::{Imfs, ImfsEntry, ImfsFetcher},
snapshot::InstanceSnapshot,
};
use super::middleware::{SnapshotFileResult, SnapshotInstanceResult, SnapshotMiddleware};
pub struct SnapshotJsonModel;
impl SnapshotMiddleware for SnapshotJsonModel {
fn from_imfs<F: ImfsFetcher>(
imfs: &mut Imfs<F>,
entry: &ImfsEntry,
) -> SnapshotInstanceResult<'static> {
if entry.is_directory() {
return Ok(None);
}
let file_name = entry.path().file_name().unwrap().to_string_lossy();
let instance_name = match match_trailing(&file_name, ".model.json") {
Some(name) => name.to_owned(),
None => return Ok(None),
};
let instance: JsonModel =
serde_json::from_slice(entry.contents(imfs)?).expect("TODO: Handle serde_json errors");
if let Some(json_name) = &instance.name {
if json_name != &instance_name {
log::warn!(
"Name from JSON model did not match its file name: {}",
entry.path().display()
);
log::warn!(
"In Rojo < alpha 14, this model is named \"{}\" (from its 'Name' property)",
json_name
);
log::warn!(
"In Rojo >= alpha 14, this model is named \"{}\" (from its file name)",
instance_name
);
log::warn!("'Name' for the top-level instance in a JSON model is now optional and will be ignored.");
}
}
let snapshot = instance.core.into_snapshot(instance_name);
Ok(Some(snapshot))
}
fn from_instance(_tree: &RbxTree, _id: RbxId) -> SnapshotFileResult {
unimplemented!("Snapshotting models");
}
}
fn match_trailing<'a>(input: &'a str, trailer: &str) -> Option<&'a str> {
if input.ends_with(trailer) {
let end = input.len().saturating_sub(trailer.len());
Some(&input[..end])
} else {
None
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct JsonModel {
name: Option<String>,
#[serde(flatten)]
core: JsonModelCore,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct JsonModelInstance {
name: String,
#[serde(flatten)]
core: JsonModelCore,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct JsonModelCore {
class_name: String,
#[serde(default = "Vec::new", skip_serializing_if = "Vec::is_empty")]
children: Vec<JsonModelInstance>,
#[serde(default = "HashMap::new", skip_serializing_if = "HashMap::is_empty")]
properties: HashMap<String, UnresolvedRbxValue>,
}
impl JsonModelCore {
fn into_snapshot(self, name: String) -> InstanceSnapshot<'static> {
let class_name = self.class_name;
let children = self
.children
.into_iter()
.map(|child| child.core.into_snapshot(child.name))
.collect();
let properties = self
.properties
.into_iter()
.map(|(key, value)| {
try_resolve_value(&class_name, &key, &value).map(|resolved| (key, resolved))
})
.collect::<Result<HashMap<_, _>, _>>()
.expect("TODO: Handle rbx_reflection errors");
InstanceSnapshot {
snapshot_id: None,
name: Cow::Owned(name),
class_name: Cow::Owned(class_name),
properties,
children,
}
}
}
#[cfg(test)]
mod test {
use super::*;
use maplit::hashmap;
use rbx_dom_weak::RbxValue;
use crate::imfs::new::{ImfsSnapshot, NoopFetcher};
#[test]
fn model_from_imfs() {
let mut imfs = Imfs::new(NoopFetcher);
let file = ImfsSnapshot::file(
r#"
{
"Name": "children",
"ClassName": "IntValue",
"Properties": {
"Value": 5
},
"Children": [
{
"Name": "The Child",
"ClassName": "StringValue"
}
]
}
"#,
);
imfs.load_from_snapshot("/foo.model.json", file);
let entry = imfs.get("/foo.model.json").unwrap();
let instance_snapshot = SnapshotJsonModel::from_imfs(&mut imfs, &entry)
.unwrap()
.unwrap();
assert_eq!(
instance_snapshot,
InstanceSnapshot {
snapshot_id: None,
name: Cow::Borrowed("foo"),
class_name: Cow::Borrowed("IntValue"),
properties: hashmap! {
"Value".to_owned() => RbxValue::Int32 {
value: 5,
},
},
children: vec![InstanceSnapshot {
snapshot_id: None,
name: Cow::Borrowed("The Child"),
class_name: Cow::Borrowed("StringValue"),
properties: HashMap::new(),
children: Vec::new(),
},],
}
);
}
}

View File

@@ -0,0 +1,176 @@
use std::{borrow::Cow, str};
use maplit::hashmap;
use rbx_dom_weak::{RbxId, RbxTree, RbxValue};
use crate::{
imfs::new::{FsResultExt, Imfs, ImfsEntry, ImfsFetcher},
snapshot::InstanceSnapshot,
};
use super::middleware::{SnapshotFileResult, SnapshotInstanceResult, SnapshotMiddleware};
pub struct SnapshotLua;
impl SnapshotMiddleware for SnapshotLua {
fn from_imfs<F: ImfsFetcher>(
imfs: &mut Imfs<F>,
entry: &ImfsEntry,
) -> SnapshotInstanceResult<'static> {
let file_name = entry.path().file_name().unwrap().to_string_lossy();
if entry.is_directory() {
let module_init_path = entry.path().join("init.lua");
if let Some(init_entry) = imfs.get(module_init_path).with_not_found()? {
if let Some(mut snapshot) = SnapshotLua::from_imfs(imfs, &init_entry)? {
snapshot.name = Cow::Owned(file_name.into_owned());
return Ok(Some(snapshot));
}
}
let server_init_path = entry.path().join("init.server.lua");
if let Some(init_entry) = imfs.get(server_init_path).with_not_found()? {
if let Some(mut snapshot) = SnapshotLua::from_imfs(imfs, &init_entry)? {
snapshot.name = Cow::Owned(file_name.into_owned());
return Ok(Some(snapshot));
}
}
let client_init_path = entry.path().join("init.client.lua");
if let Some(init_entry) = imfs.get(client_init_path).with_not_found()? {
if let Some(mut snapshot) = SnapshotLua::from_imfs(imfs, &init_entry)? {
snapshot.name = Cow::Owned(file_name.into_owned());
return Ok(Some(snapshot));
}
}
}
let (class_name, instance_name) =
if let Some(name) = match_trailing(&file_name, ".server.lua") {
("Script", name)
} else if let Some(name) = match_trailing(&file_name, ".client.lua") {
("LocalScript", name)
} else if let Some(name) = match_trailing(&file_name, ".lua") {
("ModuleScript", name)
} else {
return Ok(None);
};
let contents = entry.contents(imfs)?;
let contents_str = str::from_utf8(contents)
.expect("File content was not valid UTF-8")
.to_string();
let properties = hashmap! {
"Source".to_owned() => RbxValue::String {
value: contents_str,
},
};
Ok(Some(InstanceSnapshot {
snapshot_id: None,
name: Cow::Owned(instance_name.to_owned()),
class_name: Cow::Borrowed(class_name),
properties,
children: Vec::new(),
}))
}
fn from_instance(tree: &RbxTree, id: RbxId) -> SnapshotFileResult {
let instance = tree.get_instance(id).unwrap();
match instance.class_name.as_str() {
"ModuleScript" | "LocalScript" | "Script" => {
unimplemented!("Snapshotting Script instances")
}
_ => None,
}
}
}
fn match_trailing<'a>(input: &'a str, trailer: &str) -> Option<&'a str> {
if input.ends_with(trailer) {
let end = input.len().saturating_sub(trailer.len());
Some(&input[..end])
} else {
None
}
}
#[cfg(test)]
mod test {
use super::*;
use maplit::hashmap;
use crate::imfs::new::{ImfsSnapshot, NoopFetcher};
#[test]
fn module_from_imfs() {
let mut imfs = Imfs::new(NoopFetcher);
let file = ImfsSnapshot::file("Hello there!");
imfs.load_from_snapshot("/foo.lua", file);
let entry = imfs.get("/foo.lua").unwrap();
let instance_snapshot = SnapshotLua::from_imfs(&mut imfs, &entry).unwrap().unwrap();
assert_eq!(instance_snapshot.name, "foo");
assert_eq!(instance_snapshot.class_name, "ModuleScript");
assert_eq!(
instance_snapshot.properties,
hashmap! {
"Source".to_owned() => RbxValue::String {
value: "Hello there!".to_owned(),
},
}
);
}
#[test]
fn server_from_imfs() {
let mut imfs = Imfs::new(NoopFetcher);
let file = ImfsSnapshot::file("Hello there!");
imfs.load_from_snapshot("/foo.server.lua", file);
let entry = imfs.get("/foo.server.lua").unwrap();
let instance_snapshot = SnapshotLua::from_imfs(&mut imfs, &entry).unwrap().unwrap();
assert_eq!(instance_snapshot.name, "foo");
assert_eq!(instance_snapshot.class_name, "Script");
assert_eq!(
instance_snapshot.properties,
hashmap! {
"Source".to_owned() => RbxValue::String {
value: "Hello there!".to_owned(),
},
}
);
}
#[test]
fn client_from_imfs() {
let mut imfs = Imfs::new(NoopFetcher);
let file = ImfsSnapshot::file("Hello there!");
imfs.load_from_snapshot("/foo.client.lua", file);
let entry = imfs.get("/foo.client.lua").unwrap();
let instance_snapshot = SnapshotLua::from_imfs(&mut imfs, &entry).unwrap().unwrap();
assert_eq!(instance_snapshot.name, "foo");
assert_eq!(instance_snapshot.class_name, "LocalScript");
assert_eq!(
instance_snapshot.properties,
hashmap! {
"Source".to_owned() => RbxValue::String {
value: "Hello there!".to_owned(),
},
}
);
}
}

View File

@@ -0,0 +1,27 @@
use std::path::{Path, PathBuf};
use rbx_dom_weak::{RbxId, RbxTree};
use crate::{
imfs::{
new::{Imfs, ImfsEntry, ImfsFetcher, ImfsSnapshot},
FsResult,
},
snapshot::InstanceSnapshot,
};
pub type SnapshotInstanceResult<'a> = FsResult<Option<InstanceSnapshot<'a>>>;
pub type SnapshotFileResult = Option<(String, ImfsSnapshot)>;
pub trait SnapshotMiddleware {
fn from_imfs<F: ImfsFetcher>(
imfs: &mut Imfs<F>,
entry: &ImfsEntry,
) -> SnapshotInstanceResult<'static>;
fn from_instance(tree: &RbxTree, id: RbxId) -> SnapshotFileResult;
fn change_affects_paths(path: &Path) -> Vec<PathBuf> {
vec![path.to_path_buf()]
}
}

View File

@@ -0,0 +1,76 @@
//! Defines the semantics that Rojo uses to turn entries on the filesystem into
//! Roblox instances using the instance snapshot subsystem.
#![allow(dead_code)]
mod context;
mod csv;
mod dir;
mod error;
mod json_model;
mod lua;
mod middleware;
mod project;
mod rbxm;
mod rbxmx;
mod txt;
use rbx_dom_weak::{RbxId, RbxTree};
use self::{
csv::SnapshotCsv,
dir::SnapshotDir,
json_model::SnapshotJsonModel,
lua::SnapshotLua,
middleware::{SnapshotFileResult, SnapshotInstanceResult, SnapshotMiddleware},
project::SnapshotProject,
rbxm::SnapshotRbxm,
rbxmx::SnapshotRbxmx,
txt::SnapshotTxt,
};
use crate::imfs::new::{Imfs, ImfsEntry, ImfsFetcher};
macro_rules! middlewares {
( $($middleware: ident,)* ) => {
/// Generates a snapshot of instances from the given ImfsEntry.
pub fn snapshot_from_imfs<F: ImfsFetcher>(
imfs: &mut Imfs<F>,
entry: &ImfsEntry,
) -> SnapshotInstanceResult<'static> {
$(
log::trace!("trying middleware {} on {}", stringify!($middleware), entry.path().display());
if let Some(snapshot) = $middleware::from_imfs(imfs, entry)? {
log::trace!("middleware {} success on {}", stringify!($middleware), entry.path().display());
return Ok(Some(snapshot));
}
)*
log::trace!("no middleware returned Ok(Some)");
Ok(None)
}
/// Generates an in-memory filesystem snapshot of the given Roblox
/// instance.
pub fn snapshot_from_instance(tree: &RbxTree, id: RbxId) -> SnapshotFileResult {
$(
if let Some(result) = $middleware::from_instance(tree, id) {
return Some(result);
}
)*
None
}
};
}
middlewares! {
SnapshotProject,
SnapshotJsonModel,
SnapshotRbxmx,
SnapshotRbxm,
SnapshotLua,
SnapshotCsv,
SnapshotTxt,
SnapshotDir,
}

View File

@@ -0,0 +1,501 @@
use std::{borrow::Cow, collections::HashMap};
use rbx_dom_weak::{RbxId, RbxTree};
use rbx_reflection::try_resolve_value;
use crate::{
imfs::{
new::{Imfs, ImfsEntry, ImfsFetcher},
FsErrorKind,
},
project::{Project, ProjectNode},
snapshot::InstanceSnapshot,
};
use super::{
middleware::{SnapshotFileResult, SnapshotInstanceResult, SnapshotMiddleware},
snapshot_from_imfs,
};
pub struct SnapshotProject;
impl SnapshotMiddleware for SnapshotProject {
fn from_imfs<F: ImfsFetcher>(
imfs: &mut Imfs<F>,
entry: &ImfsEntry,
) -> SnapshotInstanceResult<'static> {
if entry.is_directory() {
let project_path = entry.path().join("default.project.json");
match imfs.get(project_path) {
Err(ref err) if err.kind() == FsErrorKind::NotFound => {}
Err(err) => return Err(err),
Ok(entry) => return SnapshotProject::from_imfs(imfs, &entry),
}
}
if !entry.path().to_string_lossy().ends_with(".project.json") {
return Ok(None);
}
let project = Project::load_from_slice(entry.contents(imfs)?, entry.path())
.expect("Invalid project file");
snapshot_project_node(&project.name, &project.tree, imfs)
}
fn from_instance(_tree: &RbxTree, _id: RbxId) -> SnapshotFileResult {
// TODO: Supporting turning instances into projects
None
}
}
fn snapshot_project_node<F: ImfsFetcher>(
instance_name: &str,
node: &ProjectNode,
imfs: &mut Imfs<F>,
) -> SnapshotInstanceResult<'static> {
assert!(
node.ignore_unknown_instances.is_none(),
"TODO: Support $ignoreUnknownInstances"
);
let name = Cow::Owned(instance_name.to_owned());
let mut class_name = node
.class_name
.as_ref()
.map(|name| Cow::Owned(name.clone()));
let mut properties = HashMap::new();
let mut children = Vec::new();
if let Some(path) = &node.path {
let entry = imfs.get(path)?;
if let Some(snapshot) = snapshot_from_imfs(imfs, &entry)? {
// If a class name was already specified, then it'll override the
// class name of this snapshot ONLY if it's a Folder.
//
// This restriction is in place to prevent applying properties to
// instances that don't make sense. The primary use-case for using
// $className and $path at the same time is to use a directory as a
// service in a place file.
class_name = match class_name {
Some(class_name) => {
if snapshot.class_name == "Folder" {
Some(class_name)
} else {
// TODO: Turn this into an error object.
panic!("If $className and $path are specified, $path must yield an instance of class Folder");
}
}
None => Some(snapshot.class_name),
};
// Properties from the snapshot are pulled in unchanged, and
// overridden by properties set on the project node.
properties.reserve(snapshot.properties.len());
for (key, value) in snapshot.properties.into_iter() {
properties.insert(key, value);
}
// The snapshot's children will be merged with the children defined
// in the project node, if there are any.
children.reserve(snapshot.children.len());
for child in snapshot.children.into_iter() {
children.push(child);
}
} else {
// TODO: Should this issue an error instead?
log::warn!(
"$path referred to a path that could not be turned into an instance by Rojo"
);
}
}
let class_name = class_name
// TODO: Turn this into an error object.
.expect("$className or $path must be specified");
for (child_name, child_project_node) in &node.children {
if let Some(child) = snapshot_project_node(child_name, child_project_node, imfs)? {
children.push(child);
}
}
for (key, value) in &node.properties {
let resolved_value = try_resolve_value(&class_name, key, value)
.expect("TODO: Properly handle value resolution errors");
properties.insert(key.clone(), resolved_value);
}
Ok(Some(InstanceSnapshot {
snapshot_id: None,
name,
class_name,
properties,
children,
}))
}
#[cfg(test)]
mod test {
use super::*;
use maplit::hashmap;
use rbx_dom_weak::RbxValue;
use crate::imfs::new::{ImfsSnapshot, NoopFetcher};
#[test]
fn project_from_folder() {
let _ = env_logger::try_init();
let mut imfs = Imfs::new(NoopFetcher);
let dir = ImfsSnapshot::dir(hashmap! {
"default.project.json" => ImfsSnapshot::file(r#"
{
"name": "indirect-project",
"tree": {
"$className": "Folder"
}
}
"#),
});
imfs.load_from_snapshot("/foo", dir);
let entry = imfs.get("/foo").unwrap();
let instance_snapshot = SnapshotProject::from_imfs(&mut imfs, &entry)
.expect("snapshot error")
.expect("snapshot returned no instances");
assert_eq!(instance_snapshot.name, "indirect-project");
assert_eq!(instance_snapshot.class_name, "Folder");
assert_eq!(instance_snapshot.properties, HashMap::new());
assert_eq!(instance_snapshot.children, Vec::new());
}
#[test]
fn project_from_direct_file() {
let _ = env_logger::try_init();
let mut imfs = Imfs::new(NoopFetcher);
let dir = ImfsSnapshot::dir(hashmap! {
"hello.project.json" => ImfsSnapshot::file(r#"
{
"name": "direct-project",
"tree": {
"$className": "Model"
}
}
"#),
});
imfs.load_from_snapshot("/foo", dir);
let entry = imfs.get("/foo/hello.project.json").unwrap();
let instance_snapshot = SnapshotProject::from_imfs(&mut imfs, &entry)
.expect("snapshot error")
.expect("snapshot returned no instances");
assert_eq!(instance_snapshot.name, "direct-project");
assert_eq!(instance_snapshot.class_name, "Model");
assert_eq!(instance_snapshot.properties, HashMap::new());
assert_eq!(instance_snapshot.children, Vec::new());
}
#[test]
fn project_with_resolved_properties() {
let _ = env_logger::try_init();
let mut imfs = Imfs::new(NoopFetcher);
let dir = ImfsSnapshot::dir(hashmap! {
"default.project.json" => ImfsSnapshot::file(r#"
{
"name": "resolved-properties",
"tree": {
"$className": "StringValue",
"$properties": {
"Value": {
"Type": "String",
"Value": "Hello, world!"
}
}
}
}
"#),
});
imfs.load_from_snapshot("/foo", dir);
let entry = imfs.get("/foo").unwrap();
let instance_snapshot = SnapshotProject::from_imfs(&mut imfs, &entry)
.expect("snapshot error")
.expect("snapshot returned no instances");
assert_eq!(instance_snapshot.name, "resolved-properties");
assert_eq!(instance_snapshot.class_name, "StringValue");
assert_eq!(
instance_snapshot.properties,
hashmap! {
"Value".to_owned() => RbxValue::String {
value: "Hello, world!".to_owned(),
},
}
);
assert_eq!(instance_snapshot.children, Vec::new());
}
#[test]
fn project_with_unresolved_properties() {
let _ = env_logger::try_init();
let mut imfs = Imfs::new(NoopFetcher);
let dir = ImfsSnapshot::dir(hashmap! {
"default.project.json" => ImfsSnapshot::file(r#"
{
"name": "unresolved-properties",
"tree": {
"$className": "StringValue",
"$properties": {
"Value": "Hi!"
}
}
}
"#),
});
imfs.load_from_snapshot("/foo", dir);
let entry = imfs.get("/foo").unwrap();
let instance_snapshot = SnapshotProject::from_imfs(&mut imfs, &entry)
.expect("snapshot error")
.expect("snapshot returned no instances");
assert_eq!(instance_snapshot.name, "unresolved-properties");
assert_eq!(instance_snapshot.class_name, "StringValue");
assert_eq!(
instance_snapshot.properties,
hashmap! {
"Value".to_owned() => RbxValue::String {
value: "Hi!".to_owned(),
},
}
);
assert_eq!(instance_snapshot.children, Vec::new());
}
#[test]
fn project_with_children() {
let _ = env_logger::try_init();
let mut imfs = Imfs::new(NoopFetcher);
let dir = ImfsSnapshot::dir(hashmap! {
"default.project.json" => ImfsSnapshot::file(r#"
{
"name": "children",
"tree": {
"$className": "Folder",
"Child": {
"$className": "Model"
}
}
}
"#),
});
imfs.load_from_snapshot("/foo", dir);
let entry = imfs.get("/foo").unwrap();
let instance_snapshot = SnapshotProject::from_imfs(&mut imfs, &entry)
.expect("snapshot error")
.expect("snapshot returned no instances");
assert_eq!(instance_snapshot.name, "children");
assert_eq!(instance_snapshot.class_name, "Folder");
assert_eq!(instance_snapshot.properties, HashMap::new());
assert_eq!(instance_snapshot.children.len(), 1);
let child = &instance_snapshot.children[0];
assert_eq!(child.name, "Child");
assert_eq!(child.class_name, "Model");
assert_eq!(child.properties, HashMap::new());
assert_eq!(child.children, Vec::new());
}
#[test]
fn project_with_path_to_txt() {
let _ = env_logger::try_init();
let mut imfs = Imfs::new(NoopFetcher);
let dir = ImfsSnapshot::dir(hashmap! {
"default.project.json" => ImfsSnapshot::file(r#"
{
"name": "path-project",
"tree": {
"$path": "other.txt"
}
}
"#),
"other.txt" => ImfsSnapshot::file("Hello, world!"),
});
imfs.load_from_snapshot("/foo", dir);
let entry = imfs.get("/foo").unwrap();
let instance_snapshot = SnapshotProject::from_imfs(&mut imfs, &entry)
.expect("snapshot error")
.expect("snapshot returned no instances");
assert_eq!(instance_snapshot.name, "path-project");
assert_eq!(instance_snapshot.class_name, "StringValue");
assert_eq!(
instance_snapshot.properties,
hashmap! {
"Value".to_owned() => RbxValue::String {
value: "Hello, world!".to_owned(),
},
}
);
assert_eq!(instance_snapshot.children, Vec::new());
}
#[test]
fn project_with_path_to_project() {
let _ = env_logger::try_init();
let mut imfs = Imfs::new(NoopFetcher);
let dir = ImfsSnapshot::dir(hashmap! {
"default.project.json" => ImfsSnapshot::file(r#"
{
"name": "path-project",
"tree": {
"$path": "other.project.json"
}
}
"#),
"other.project.json" => ImfsSnapshot::file(r#"
{
"name": "other-project",
"tree": {
"$className": "Model"
}
}
"#),
});
imfs.load_from_snapshot("/foo", dir);
let entry = imfs.get("/foo").unwrap();
let instance_snapshot = SnapshotProject::from_imfs(&mut imfs, &entry)
.expect("snapshot error")
.expect("snapshot returned no instances");
assert_eq!(instance_snapshot.name, "path-project");
assert_eq!(instance_snapshot.class_name, "Model");
assert_eq!(instance_snapshot.properties, HashMap::new());
assert_eq!(instance_snapshot.children, Vec::new());
}
#[test]
fn project_with_path_to_project_with_children() {
let _ = env_logger::try_init();
let mut imfs = Imfs::new(NoopFetcher);
let dir = ImfsSnapshot::dir(hashmap! {
"default.project.json" => ImfsSnapshot::file(r#"
{
"name": "path-child-project",
"tree": {
"$path": "other.project.json"
}
}
"#),
"other.project.json" => ImfsSnapshot::file(r#"
{
"name": "other-project",
"tree": {
"$className": "Folder",
"SomeChild": {
"$className": "Model"
}
}
}
"#),
});
imfs.load_from_snapshot("/foo", dir);
let entry = imfs.get("/foo").unwrap();
let instance_snapshot = SnapshotProject::from_imfs(&mut imfs, &entry)
.expect("snapshot error")
.expect("snapshot returned no instances");
assert_eq!(instance_snapshot.name, "path-child-project");
assert_eq!(instance_snapshot.class_name, "Folder");
assert_eq!(instance_snapshot.properties, HashMap::new());
assert_eq!(instance_snapshot.children.len(), 1);
let child = &instance_snapshot.children[0];
assert_eq!(child.name, "SomeChild");
assert_eq!(child.class_name, "Model");
assert_eq!(child.properties, HashMap::new());
assert_eq!(child.children, Vec::new());
}
/// Ensures that if a property is defined both in the resulting instance
/// from $path and also in $properties, that the $properties value takes
/// precedence.
#[test]
fn project_path_property_overrides() {
let _ = env_logger::try_init();
let mut imfs = Imfs::new(NoopFetcher);
let dir = ImfsSnapshot::dir(hashmap! {
"default.project.json" => ImfsSnapshot::file(r#"
{
"name": "path-property-override",
"tree": {
"$path": "other.project.json",
"$properties": {
"Value": "Changed"
}
}
}
"#),
"other.project.json" => ImfsSnapshot::file(r#"
{
"name": "other-project",
"tree": {
"$className": "StringValue",
"$properties": {
"Value": "Original"
}
}
}
"#),
});
imfs.load_from_snapshot("/foo", dir);
let entry = imfs.get("/foo").unwrap();
let instance_snapshot = SnapshotProject::from_imfs(&mut imfs, &entry)
.expect("snapshot error")
.expect("snapshot returned no instances");
assert_eq!(instance_snapshot.name, "path-property-override");
assert_eq!(instance_snapshot.class_name, "StringValue");
assert_eq!(
instance_snapshot.properties,
hashmap! {
"Value".to_owned() => RbxValue::String {
value: "Changed".to_owned(),
},
}
);
assert_eq!(instance_snapshot.children, Vec::new());
}
}

View File

@@ -0,0 +1,90 @@
use std::{borrow::Cow, collections::HashMap};
use rbx_dom_weak::{RbxId, RbxInstanceProperties, RbxTree};
use crate::{
imfs::new::{Imfs, ImfsEntry, ImfsFetcher},
snapshot::InstanceSnapshot,
};
use super::middleware::{SnapshotFileResult, SnapshotInstanceResult, SnapshotMiddleware};
pub struct SnapshotRbxm;
impl SnapshotMiddleware for SnapshotRbxm {
fn from_imfs<F: ImfsFetcher>(
imfs: &mut Imfs<F>,
entry: &ImfsEntry,
) -> SnapshotInstanceResult<'static> {
if entry.is_directory() {
return Ok(None);
}
let file_name = entry.path().file_name().unwrap().to_string_lossy();
if !file_name.ends_with(".rbxm") {
return Ok(None);
}
let instance_name = entry
.path()
.file_stem()
.expect("Could not extract file stem")
.to_string_lossy()
.to_string();
let mut temp_tree = RbxTree::new(RbxInstanceProperties {
name: "DataModel".to_owned(),
class_name: "DataModel".to_owned(),
properties: HashMap::new(),
});
let root_id = temp_tree.get_root_id();
rbx_binary::decode(&mut temp_tree, root_id, entry.contents(imfs)?)
.expect("TODO: Handle rbx_binary errors");
let root_instance = temp_tree.get_instance(root_id).unwrap();
let children = root_instance.get_children_ids();
if children.len() == 1 {
let mut snapshot = InstanceSnapshot::from_tree(&temp_tree, children[0]);
snapshot.name = Cow::Owned(instance_name);
Ok(Some(snapshot))
} else {
panic!("Rojo doesn't have support for model files with zero or more than one top-level instances yet.");
}
}
fn from_instance(_tree: &RbxTree, _id: RbxId) -> SnapshotFileResult {
unimplemented!("Snapshotting models");
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::imfs::new::{ImfsSnapshot, NoopFetcher};
#[test]
fn model_from_imfs() {
let mut imfs = Imfs::new(NoopFetcher);
let file = ImfsSnapshot::file(include_bytes!("../../assets/test-folder.rbxm").to_vec());
imfs.load_from_snapshot("/foo.rbxm", file);
let entry = imfs.get("/foo.rbxm").unwrap();
let instance_snapshot = SnapshotRbxm::from_imfs(&mut imfs, &entry).unwrap().unwrap();
assert_eq!(instance_snapshot.name, "foo");
assert_eq!(instance_snapshot.class_name, "Folder");
assert_eq!(instance_snapshot.children, Vec::new());
// We intentionally don't assert on properties. rbx_binary does not
// distinguish between String and BinaryString. The sample model was
// created by Roblox Studio and has an empty BinaryString "Tags"
// property that currently deserializes incorrectly.
// See: https://github.com/rojo-rbx/rbx-dom/issues/49
}
}

View File

@@ -0,0 +1,95 @@
use std::borrow::Cow;
use rbx_dom_weak::{RbxId, RbxTree};
use crate::{
imfs::new::{Imfs, ImfsEntry, ImfsFetcher},
snapshot::InstanceSnapshot,
};
use super::middleware::{SnapshotFileResult, SnapshotInstanceResult, SnapshotMiddleware};
pub struct SnapshotRbxmx;
impl SnapshotMiddleware for SnapshotRbxmx {
fn from_imfs<F: ImfsFetcher>(
imfs: &mut Imfs<F>,
entry: &ImfsEntry,
) -> SnapshotInstanceResult<'static> {
if entry.is_directory() {
return Ok(None);
}
let file_name = entry.path().file_name().unwrap().to_string_lossy();
if !file_name.ends_with(".rbxmx") {
return Ok(None);
}
let instance_name = entry
.path()
.file_stem()
.expect("Could not extract file stem")
.to_string_lossy()
.to_string();
let options = rbx_xml::DecodeOptions::new()
.property_behavior(rbx_xml::DecodePropertyBehavior::ReadUnknown);
let temp_tree = rbx_xml::from_reader(entry.contents(imfs)?, options)
.expect("TODO: Handle rbx_xml errors");
let root_instance = temp_tree.get_instance(temp_tree.get_root_id()).unwrap();
let children = root_instance.get_children_ids();
if children.len() == 1 {
let mut snapshot = InstanceSnapshot::from_tree(&temp_tree, children[0]);
snapshot.name = Cow::Owned(instance_name);
Ok(Some(snapshot))
} else {
panic!("Rojo doesn't have support for model files with zero or more than one top-level instances yet.");
}
}
fn from_instance(_tree: &RbxTree, _id: RbxId) -> SnapshotFileResult {
unimplemented!("Snapshotting models");
}
}
#[cfg(test)]
mod test {
use super::*;
use std::collections::HashMap;
use crate::imfs::new::{ImfsSnapshot, NoopFetcher};
#[test]
fn model_from_imfs() {
let mut imfs = Imfs::new(NoopFetcher);
let file = ImfsSnapshot::file(
r#"
<roblox version="4">
<Item class="Folder" referent="0">
<Properties>
<string name="Name">THIS NAME IS IGNORED</string>
</Properties>
</Item>
</roblox>
"#,
);
imfs.load_from_snapshot("/foo.rbxmx", file);
let entry = imfs.get("/foo.rbxmx").unwrap();
let instance_snapshot = SnapshotRbxmx::from_imfs(&mut imfs, &entry)
.unwrap()
.unwrap();
assert_eq!(instance_snapshot.name, "foo");
assert_eq!(instance_snapshot.class_name, "Folder");
assert_eq!(instance_snapshot.properties, HashMap::new());
assert_eq!(instance_snapshot.children, Vec::new());
}
}

View File

@@ -0,0 +1,147 @@
use std::{borrow::Cow, str};
use maplit::hashmap;
use rbx_dom_weak::{RbxId, RbxTree, RbxValue};
use crate::{
imfs::new::{FileSnapshot, Imfs, ImfsEntry, ImfsFetcher, ImfsSnapshot},
snapshot::InstanceSnapshot,
};
use super::middleware::{SnapshotFileResult, SnapshotInstanceResult, SnapshotMiddleware};
pub struct SnapshotTxt;
impl SnapshotMiddleware for SnapshotTxt {
fn from_imfs<F: ImfsFetcher>(
imfs: &mut Imfs<F>,
entry: &ImfsEntry,
) -> SnapshotInstanceResult<'static> {
if entry.is_directory() {
return Ok(None);
}
let extension = match entry.path().extension() {
Some(x) => x.to_str().unwrap(),
None => return Ok(None),
};
if extension != "txt" {
return Ok(None);
}
let instance_name = entry
.path()
.file_stem()
.expect("Could not extract file stem")
.to_str()
.unwrap()
.to_string();
let contents = entry.contents(imfs)?;
let contents_str = str::from_utf8(contents)
.expect("File content was not valid UTF-8")
.to_string();
let properties = hashmap! {
"Value".to_owned() => RbxValue::String {
value: contents_str,
},
};
Ok(Some(InstanceSnapshot {
snapshot_id: None,
name: Cow::Owned(instance_name),
class_name: Cow::Borrowed("StringValue"),
properties,
children: Vec::new(),
}))
}
fn from_instance(tree: &RbxTree, id: RbxId) -> SnapshotFileResult {
let instance = tree.get_instance(id).unwrap();
if instance.class_name != "StringValue" {
return None;
}
if !instance.get_children_ids().is_empty() {
return None;
}
let value = match instance.properties.get("Value") {
Some(RbxValue::String { value }) => value.clone(),
Some(_) => panic!("wrong type ahh"),
None => String::new(),
};
let snapshot = ImfsSnapshot::File(FileSnapshot {
contents: value.into_bytes(),
});
let mut file_name = instance.name.clone();
file_name.push_str(".txt");
Some((file_name, snapshot))
}
}
#[cfg(test)]
mod test {
use super::*;
use maplit::hashmap;
use rbx_dom_weak::RbxInstanceProperties;
use crate::imfs::new::NoopFetcher;
#[test]
fn instance_from_imfs() {
let mut imfs = Imfs::new(NoopFetcher);
let file = ImfsSnapshot::file("Hello there!");
imfs.load_from_snapshot("/foo.txt", file);
let entry = imfs.get("/foo.txt").unwrap();
let instance_snapshot = SnapshotTxt::from_imfs(&mut imfs, &entry).unwrap().unwrap();
assert_eq!(instance_snapshot.name, "foo");
assert_eq!(instance_snapshot.class_name, "StringValue");
assert_eq!(
instance_snapshot.properties,
hashmap! {
"Value".to_owned() => RbxValue::String {
value: "Hello there!".to_owned(),
},
}
);
}
#[test]
fn imfs_from_instance() {
let tree = RbxTree::new(string_value("Root", "Hello, world!"));
let root_id = tree.get_root_id();
let (_file_name, _file) = SnapshotTxt::from_instance(&tree, root_id).unwrap();
}
fn folder(name: impl Into<String>) -> RbxInstanceProperties {
RbxInstanceProperties {
name: name.into(),
class_name: "Folder".to_owned(),
properties: Default::default(),
}
}
fn string_value(name: impl Into<String>, value: impl Into<String>) -> RbxInstanceProperties {
RbxInstanceProperties {
name: name.into(),
class_name: "StringValue".to_owned(),
properties: hashmap! {
"Value".to_owned() => RbxValue::String {
value: value.into(),
},
},
}
}
}