forked from rojo-rbx/rojo
Move Rojo server into root of the repository
This commit is contained in:
7
src/snapshot_middleware/context.rs
Normal file
7
src/snapshot_middleware/context.rs
Normal 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;
|
||||
169
src/snapshot_middleware/csv.rs
Normal file
169
src/snapshot_middleware/csv.rs
Normal 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(),
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
121
src/snapshot_middleware/dir.rs
Normal file
121
src/snapshot_middleware/dir.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
91
src/snapshot_middleware/error.rs
Normal file
91
src/snapshot_middleware/error.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
189
src/snapshot_middleware/json_model.rs
Normal file
189
src/snapshot_middleware/json_model.rs
Normal 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(),
|
||||
},],
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
176
src/snapshot_middleware/lua.rs
Normal file
176
src/snapshot_middleware/lua.rs
Normal 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(),
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
27
src/snapshot_middleware/middleware.rs
Normal file
27
src/snapshot_middleware/middleware.rs
Normal 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()]
|
||||
}
|
||||
}
|
||||
76
src/snapshot_middleware/mod.rs
Normal file
76
src/snapshot_middleware/mod.rs
Normal 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,
|
||||
}
|
||||
501
src/snapshot_middleware/project.rs
Normal file
501
src/snapshot_middleware/project.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
90
src/snapshot_middleware/rbxm.rs
Normal file
90
src/snapshot_middleware/rbxm.rs
Normal 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
|
||||
}
|
||||
}
|
||||
95
src/snapshot_middleware/rbxmx.rs
Normal file
95
src/snapshot_middleware/rbxmx.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
147
src/snapshot_middleware/txt.rs
Normal file
147
src/snapshot_middleware/txt.rs
Normal 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(),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user