Compare commits

..

1 Commits

Author SHA1 Message Date
Lucien Greathouse
b96a236333 Refactor project file into its own crate 2022-05-26 05:00:57 -04:00
30 changed files with 798 additions and 532 deletions

80
Cargo.lock generated
View File

@@ -738,6 +738,7 @@ dependencies = [
"fnv", "fnv",
"log", "log",
"regex", "regex",
"serde",
] ]
[[package]] [[package]]
@@ -1492,26 +1493,6 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "profiling"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f61dcf0b917cd75d4521d7343d1ffff3d1583054133c9b5cbea3375c703c40d"
dependencies = [
"profiling-procmacros",
"superluminal-perf",
]
[[package]]
name = "profiling-procmacros"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98eee3c112f2a6f784b6713fe1d7fb7d6506e066121c0a49371fdb976f72bae5"
dependencies = [
"quote 1.0.18",
"syn",
]
[[package]] [[package]]
name = "quote" name = "quote"
version = "0.6.13" version = "0.6.13"
@@ -1816,15 +1797,7 @@ dependencies = [
] ]
[[package]] [[package]]
name = "rojo-insta-ext" name = "rojo"
version = "0.1.0"
dependencies = [
"serde",
"serde_yaml",
]
[[package]]
name = "rojo-smallstring"
version = "7.1.1" version = "7.1.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
@@ -1850,7 +1823,6 @@ dependencies = [
"opener", "opener",
"paste", "paste",
"pretty_assertions", "pretty_assertions",
"profiling",
"rbx_binary", "rbx_binary",
"rbx_dom_weak", "rbx_dom_weak",
"rbx_reflection", "rbx_reflection",
@@ -1860,10 +1832,10 @@ dependencies = [
"ritz", "ritz",
"roblox_install", "roblox_install",
"rojo-insta-ext", "rojo-insta-ext",
"rojo-project",
"serde", "serde",
"serde_json", "serde_json",
"serde_yaml", "serde_yaml",
"smol_str",
"tempfile", "tempfile",
"termcolor", "termcolor",
"thiserror", "thiserror",
@@ -1873,6 +1845,28 @@ dependencies = [
"winreg 0.10.1", "winreg 0.10.1",
] ]
[[package]]
name = "rojo-insta-ext"
version = "0.1.0"
dependencies = [
"serde",
"serde_yaml",
]
[[package]]
name = "rojo-project"
version = "0.1.0"
dependencies = [
"anyhow",
"globset",
"log",
"rbx_dom_weak",
"rbx_reflection",
"rbx_reflection_database",
"serde",
"serde_json",
]
[[package]] [[package]]
name = "rustc-demangle" name = "rustc-demangle"
version = "0.1.21" version = "0.1.21"
@@ -2037,15 +2031,6 @@ version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32"
[[package]]
name = "smol_str"
version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7475118a28b7e3a2e157ce0131ba8c5526ea96e90ee601d9f6bb2e286a35ab44"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "snax" name = "snax"
version = "0.2.0" version = "0.2.0"
@@ -2077,21 +2062,6 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d67a5a62ba6e01cb2192ff309324cb4875d0c451d55fe2319433abe7a05a8ee" checksum = "2d67a5a62ba6e01cb2192ff309324cb4875d0c451d55fe2319433abe7a05a8ee"
[[package]]
name = "superluminal-perf"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80ed8ddf5d2a4a849fa7dc75b3e0be740adb882fe7fee87e79584402ac9b1e60"
dependencies = [
"superluminal-perf-sys",
]
[[package]]
name = "superluminal-perf-sys"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0342a02bcc62538822a46f54294130677f026666c2e19d078fc213b7bc07ff16"
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.95" version = "1.0.95"

View File

@@ -1,5 +1,5 @@
[package] [package]
name = "rojo-smallstring" name = "rojo"
version = "7.1.1" version = "7.1.1"
authors = ["Lucien Greathouse <me@lpghatguy.com>"] authors = ["Lucien Greathouse <me@lpghatguy.com>"]
description = "Enables professional-grade development tools for Roblox developers" description = "Enables professional-grade development tools for Roblox developers"
@@ -39,6 +39,7 @@ name = "build"
harness = false harness = false
[dependencies] [dependencies]
rojo-project = { path = "crates/rojo-project" }
memofs = { version = "0.2.0", path = "crates/memofs" } memofs = { version = "0.2.0", path = "crates/memofs" }
# These dependencies can be uncommented when working on rbx-dom simultaneously # These dependencies can be uncommented when working on rbx-dom simultaneously
@@ -80,8 +81,6 @@ thiserror = "1.0.30"
tokio = { version = "1.12.0", features = ["rt", "rt-multi-thread"] } tokio = { version = "1.12.0", features = ["rt", "rt-multi-thread"] }
uuid = { version = "1.0.0", features = ["v4", "serde"] } uuid = { version = "1.0.0", features = ["v4", "serde"] }
clap = { version = "3.1.18", features = ["derive"] } clap = { version = "3.1.18", features = ["derive"] }
smol_str = "0.1.23"
profiling = { version = "1.0.6", features = ["profile-with-superluminal"] }
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
winreg = "0.10.1" winreg = "0.10.1"

View File

@@ -0,0 +1,16 @@
[package]
name = "rojo-project"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.57"
globset = { version = "0.4.8", features = ["serde1"] }
log = "0.4.17"
rbx_dom_weak = "2.3.0"
rbx_reflection = "4.2.0"
rbx_reflection_database = "0.2.4"
serde = { version = "1.0.137", features = ["derive"] }
serde_json = "1.0.81"

View File

@@ -0,0 +1,4 @@
# rojo-project
Project file format crate for [Rojo].
[Rojo]: https://rojo.space

View File

@@ -1,6 +1,3 @@
//! Wrapper around globset's Glob type that has better serialization
//! characteristics by coupling Glob and GlobMatcher into a single type.
use std::path::Path; use std::path::Path;
use globset::{Glob as InnerGlob, GlobMatcher}; use globset::{Glob as InnerGlob, GlobMatcher};
@@ -8,6 +5,8 @@ use serde::{de::Error as _, Deserialize, Deserializer, Serialize, Serializer};
pub use globset::Error; pub use globset::Error;
/// Wrapper around globset's Glob type that has better serialization
/// characteristics by coupling Glob and GlobMatcher into a single type.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Glob { pub struct Glob {
inner: InnerGlob, inner: InnerGlob,

View File

@@ -0,0 +1,7 @@
pub mod glob;
mod path_serializer;
mod project;
mod resolution;
pub use project::{OptionalPathNode, PathNode, Project, ProjectNode};
pub use resolution::{AmbiguousValue, UnresolvedValue};

View File

@@ -0,0 +1,21 @@
//! Path serializer is used to serialize absolute paths in a cross-platform way,
//! by replacing all directory separators with /.
use std::path::Path;
use serde::Serializer;
pub fn serialize_absolute<S, T>(path: T, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
T: AsRef<Path>,
{
let as_str = path
.as_ref()
.as_os_str()
.to_str()
.expect("Invalid Unicode in file path, cannot serialize");
let replaced = as_str.replace("\\", "/");
serializer.serialize_str(&replaced)
}

View File

@@ -0,0 +1,363 @@
use std::collections::{BTreeMap, HashMap, HashSet};
use std::fs;
use std::net::IpAddr;
use std::path::{Path, PathBuf};
use anyhow::Context;
use serde::{Deserialize, Serialize};
use crate::glob::Glob;
use crate::resolution::UnresolvedValue;
static PROJECT_FILENAME: &str = "default.project.json";
/// Contains all of the configuration for a Rojo-managed project.
///
/// Rojo project files are stored in `.project.json` files.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct Project {
/// The name of the top-level instance described by the project.
pub name: String,
/// The tree of instances described by this project. Projects always
/// describe at least one instance.
pub tree: ProjectNode,
/// If specified, sets the default port that `rojo serve` should use when
/// using this project for live sync.
///
/// Can be overriden with the `--port` flag.
#[serde(skip_serializing_if = "Option::is_none")]
pub serve_port: Option<u16>,
/// If specified, sets the default IP address that `rojo serve` should use
/// when using this project for live sync.
///
/// Can be overridden with the `--address` flag.
#[serde(skip_serializing_if = "Option::is_none")]
pub serve_address: Option<IpAddr>,
/// If specified, contains the set of place IDs that this project is
/// compatible with when doing live sync.
///
/// This setting is intended to help prevent syncing a Rojo project into the
/// wrong Roblox place.
#[serde(skip_serializing_if = "Option::is_none")]
pub serve_place_ids: Option<HashSet<u64>>,
/// If specified, sets the current place's place ID when connecting to the
/// Rojo server from Roblox Studio.
#[serde(skip_serializing_if = "Option::is_none")]
pub place_id: Option<u64>,
/// If specified, sets the current place's game ID when connecting to the
/// Rojo server from Roblox Studio.
#[serde(skip_serializing_if = "Option::is_none")]
pub game_id: Option<u64>,
/// A list of globs, relative to the folder the project file is in, that
/// match files that should be excluded if Rojo encounters them.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub glob_ignore_paths: Vec<Glob>,
/// The path to the file that this project came from. Relative paths in the
/// project should be considered relative to the parent of this field, also
/// given by `Project::folder_location`.
#[serde(skip)]
pub file_location: PathBuf,
}
impl Project {
/// Tells whether the given path describes a Rojo project.
pub fn is_project_file(path: &Path) -> bool {
path.file_name()
.and_then(|name| name.to_str())
.map(|name| name.ends_with(".project.json"))
.unwrap_or(false)
}
/// Loads a project file from a slice and a path that indicates where the
/// project should resolve paths relative to.
pub fn load_from_slice(contents: &[u8], project_file_location: &Path) -> anyhow::Result<Self> {
let mut project: Self = serde_json::from_slice(&contents).with_context(|| {
format!(
"Error parsing Rojo project at {}",
project_file_location.display()
)
})?;
project.file_location = project_file_location.to_path_buf();
project.check_compatibility();
Ok(project)
}
/// Fuzzy-find a Rojo project and load it.
pub fn load_fuzzy(fuzzy_project_location: &Path) -> anyhow::Result<Option<Self>> {
if let Some(project_path) = Self::locate(fuzzy_project_location) {
let project = Self::load_exact(&project_path)?;
Ok(Some(project))
} else {
Ok(None)
}
}
/// Gives the path that all project file paths should resolve relative to.
pub fn folder_location(&self) -> &Path {
self.file_location.parent().unwrap()
}
/// Attempt to locate a project represented by the given path.
///
/// This will find a project if the path refers to a `.project.json` file,
/// or is a folder that contains a `default.project.json` file.
fn locate(path: &Path) -> Option<PathBuf> {
let meta = fs::metadata(path).ok()?;
if meta.is_file() {
if Project::is_project_file(path) {
Some(path.to_path_buf())
} else {
None
}
} else {
let child_path = path.join(PROJECT_FILENAME);
let child_meta = fs::metadata(&child_path).ok()?;
if child_meta.is_file() {
Some(child_path)
} else {
// This is a folder with the same name as a Rojo default project
// file.
//
// That's pretty weird, but we can roll with it.
None
}
}
}
fn load_exact(project_file_location: &Path) -> anyhow::Result<Self> {
let contents = fs::read_to_string(project_file_location)?;
let mut project: Project = serde_json::from_str(&contents).with_context(|| {
format!(
"Error parsing Rojo project at {}",
project_file_location.display()
)
})?;
project.file_location = project_file_location.to_path_buf();
project.check_compatibility();
Ok(project)
}
/// Checks if there are any compatibility issues with this project file and
/// warns the user if there are any.
fn check_compatibility(&self) {
self.tree.validate_reserved_names();
}
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct OptionalPathNode {
#[serde(serialize_with = "crate::path_serializer::serialize_absolute")]
pub optional: PathBuf,
}
impl OptionalPathNode {
pub fn new(optional: PathBuf) -> Self {
OptionalPathNode { optional }
}
}
/// Describes a path that is either optional or required
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum PathNode {
Required(#[serde(serialize_with = "crate::path_serializer::serialize_absolute")] PathBuf),
Optional(OptionalPathNode),
}
impl PathNode {
pub fn path(&self) -> &Path {
match self {
PathNode::Required(pathbuf) => &pathbuf,
PathNode::Optional(OptionalPathNode { optional }) => &optional,
}
}
}
/// Describes an instance and its descendants in a project.
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct ProjectNode {
/// If set, defines the ClassName of the described instance.
///
/// `$className` MUST be set if `$path` is not set.
///
/// `$className` CANNOT be set if `$path` is set and the instance described
/// by that path has a ClassName other than Folder.
#[serde(rename = "$className", skip_serializing_if = "Option::is_none")]
pub class_name: Option<String>,
/// Contains all of the children of the described instance.
#[serde(flatten)]
pub children: BTreeMap<String, ProjectNode>,
/// The properties that will be assigned to the resulting instance.
#[serde(
rename = "$properties",
default,
skip_serializing_if = "HashMap::is_empty"
)]
pub properties: HashMap<String, UnresolvedValue>,
/// Defines the behavior when Rojo encounters unknown instances in Roblox
/// Studio during live sync. `$ignoreUnknownInstances` should be considered
/// a large hammer and used with care.
///
/// If set to `true`, those instances will be left alone. This may cause
/// issues when files that turn into instances are removed while Rojo is not
/// running.
///
/// If set to `false`, Rojo will destroy any instances it does not
/// recognize.
///
/// If unset, its default value depends on other settings:
/// - If `$path` is not set, defaults to `true`
/// - If `$path` is set, defaults to `false`
#[serde(
rename = "$ignoreUnknownInstances",
skip_serializing_if = "Option::is_none"
)]
pub ignore_unknown_instances: Option<bool>,
/// Defines that this instance should come from the given file path. This
/// path can point to any file type supported by Rojo, including Lua files
/// (`.lua`), Roblox models (`.rbxm`, `.rbxmx`), and localization table
/// spreadsheets (`.csv`).
#[serde(rename = "$path", skip_serializing_if = "Option::is_none")]
pub path: Option<PathNode>,
}
impl ProjectNode {
fn validate_reserved_names(&self) {
for (name, child) in &self.children {
if name.starts_with('$') {
log::warn!(
"Keys starting with '$' are reserved by Rojo to ensure forward compatibility."
);
log::warn!(
"This project uses the key '{}', which should be renamed.",
name
);
}
child.validate_reserved_names();
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn path_node_required() {
let path_node: PathNode = serde_json::from_str(r#""src""#).unwrap();
assert_eq!(path_node, PathNode::Required(PathBuf::from("src")));
}
#[test]
fn path_node_optional() {
let path_node: PathNode = serde_json::from_str(r#"{ "optional": "src" }"#).unwrap();
assert_eq!(
path_node,
PathNode::Optional(OptionalPathNode::new(PathBuf::from("src")))
);
}
#[test]
fn project_node_required() {
let project_node: ProjectNode = serde_json::from_str(
r#"{
"$path": "src"
}"#,
)
.unwrap();
assert_eq!(
project_node.path,
Some(PathNode::Required(PathBuf::from("src")))
);
}
#[test]
fn project_node_optional() {
let project_node: ProjectNode = serde_json::from_str(
r#"{
"$path": { "optional": "src" }
}"#,
)
.unwrap();
assert_eq!(
project_node.path,
Some(PathNode::Optional(OptionalPathNode::new(PathBuf::from(
"src"
))))
);
}
#[test]
fn project_node_none() {
let project_node: ProjectNode = serde_json::from_str(
r#"{
"$className": "Folder"
}"#,
)
.unwrap();
assert_eq!(project_node.path, None);
}
#[test]
fn project_node_optional_serialize_absolute() {
let project_node: ProjectNode = serde_json::from_str(
r#"{
"$path": { "optional": "..\\src" }
}"#,
)
.unwrap();
let serialized = serde_json::to_string(&project_node).unwrap();
assert_eq!(serialized, r#"{"$path":{"optional":"../src"}}"#);
}
#[test]
fn project_node_optional_serialize_absolute_no_change() {
let project_node: ProjectNode = serde_json::from_str(
r#"{
"$path": { "optional": "../src" }
}"#,
)
.unwrap();
let serialized = serde_json::to_string(&project_node).unwrap();
assert_eq!(serialized, r#"{"$path":{"optional":"../src"}}"#);
}
#[test]
fn project_node_optional_serialize_optional() {
let project_node: ProjectNode = serde_json::from_str(
r#"{
"$path": "..\\src"
}"#,
)
.unwrap();
let serialized = serde_json::to_string(&project_node).unwrap();
assert_eq!(serialized, r#"{"$path":"../src"}"#);
}
}

View File

@@ -0,0 +1,294 @@
use std::borrow::Borrow;
use anyhow::format_err;
use rbx_dom_weak::types::{
CFrame, Color3, Content, Enum, Matrix3, Tags, Variant, VariantType, Vector2, Vector3,
};
use rbx_reflection::{DataType, PropertyDescriptor};
use serde::{Deserialize, Serialize};
/// A user-friendly version of `Variant` that supports specifying ambiguous
/// values. Ambiguous values need a reflection database to be resolved to a
/// usable value.
///
/// This type is used in Rojo projects and JSON models to make specifying the
/// most common types of properties, like strings or vectors, much easier.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum UnresolvedValue {
FullyQualified(Variant),
Ambiguous(AmbiguousValue),
}
impl UnresolvedValue {
pub fn resolve(self, class_name: &str, prop_name: &str) -> anyhow::Result<Variant> {
match self {
UnresolvedValue::FullyQualified(full) => Ok(full),
UnresolvedValue::Ambiguous(partial) => partial.resolve(class_name, prop_name),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum AmbiguousValue {
Bool(bool),
String(String),
StringArray(Vec<String>),
Number(f64),
Array2([f64; 2]),
Array3([f64; 3]),
Array4([f64; 4]),
Array12([f64; 12]),
}
impl AmbiguousValue {
pub fn resolve(self, class_name: &str, prop_name: &str) -> anyhow::Result<Variant> {
let property = find_descriptor(class_name, prop_name)
.ok_or_else(|| format_err!("Unknown property {}.{}", class_name, prop_name))?;
match &property.data_type {
DataType::Enum(enum_name) => {
let database = rbx_reflection_database::get();
let enum_descriptor = database.enums.get(enum_name).ok_or_else(|| {
format_err!("Unknown enum {}. This is a Rojo bug!", enum_name)
})?;
let error = |what: &str| {
let mut all_values = enum_descriptor
.items
.keys()
.map(|value| value.borrow())
.collect::<Vec<_>>();
all_values.sort();
let examples = nonexhaustive_list(&all_values);
format_err!(
"Invalid value for property {}.{}. Got {} but \
expected a member of the {} enum such as {}",
class_name,
prop_name,
what,
enum_name,
examples,
)
};
let value = match self {
AmbiguousValue::String(value) => value,
unresolved => return Err(error(unresolved.describe())),
};
let resolved = enum_descriptor
.items
.get(value.as_str())
.ok_or_else(|| error(value.as_str()))?;
Ok(Enum::from_u32(*resolved).into())
}
DataType::Value(variant_ty) => match (variant_ty, self) {
(VariantType::Bool, AmbiguousValue::Bool(value)) => Ok(value.into()),
(VariantType::Float32, AmbiguousValue::Number(value)) => Ok((value as f32).into()),
(VariantType::Float64, AmbiguousValue::Number(value)) => Ok(value.into()),
(VariantType::Int32, AmbiguousValue::Number(value)) => Ok((value as i32).into()),
(VariantType::Int64, AmbiguousValue::Number(value)) => Ok((value as i64).into()),
(VariantType::String, AmbiguousValue::String(value)) => Ok(value.into()),
(VariantType::Tags, AmbiguousValue::StringArray(value)) => {
Ok(Tags::from(value).into())
}
(VariantType::Content, AmbiguousValue::String(value)) => {
Ok(Content::from(value).into())
}
(VariantType::Vector2, AmbiguousValue::Array2(value)) => {
Ok(Vector2::new(value[0] as f32, value[1] as f32).into())
}
(VariantType::Vector3, AmbiguousValue::Array3(value)) => {
Ok(Vector3::new(value[0] as f32, value[1] as f32, value[2] as f32).into())
}
(VariantType::Color3, AmbiguousValue::Array3(value)) => {
Ok(Color3::new(value[0] as f32, value[1] as f32, value[2] as f32).into())
}
(VariantType::CFrame, AmbiguousValue::Array12(value)) => {
let value = value.map(|v| v as f32);
let pos = Vector3::new(value[0], value[1], value[2]);
let orientation = Matrix3::new(
Vector3::new(value[3], value[4], value[5]),
Vector3::new(value[6], value[7], value[8]),
Vector3::new(value[9], value[10], value[11]),
);
Ok(CFrame::new(pos, orientation).into())
}
(_, unresolved) => Err(format_err!(
"Wrong type of value for property {}.{}. Expected {:?}, got {}",
class_name,
prop_name,
variant_ty,
unresolved.describe(),
)),
},
_ => Err(format_err!(
"Unknown data type for property {}.{}",
class_name,
prop_name
)),
}
}
fn describe(&self) -> &'static str {
match self {
AmbiguousValue::Bool(_) => "a bool",
AmbiguousValue::String(_) => "a string",
AmbiguousValue::StringArray(_) => "an array of strings",
AmbiguousValue::Number(_) => "a number",
AmbiguousValue::Array2(_) => "an array of two numbers",
AmbiguousValue::Array3(_) => "an array of three numbers",
AmbiguousValue::Array4(_) => "an array of four numbers",
AmbiguousValue::Array12(_) => "an array of twelve numbers",
}
}
}
fn find_descriptor(
class_name: &str,
prop_name: &str,
) -> Option<&'static PropertyDescriptor<'static>> {
let database = rbx_reflection_database::get();
let mut current_class_name = class_name;
loop {
let class = database.classes.get(current_class_name)?;
if let Some(descriptor) = class.properties.get(prop_name) {
return Some(descriptor);
}
current_class_name = class.superclass.as_deref()?;
}
}
/// Outputs a string containing up to MAX_ITEMS entries from the given list. If
/// there are more than MAX_ITEMS items, the number of remaining items will be
/// listed.
fn nonexhaustive_list(values: &[&str]) -> String {
use std::fmt::Write;
const MAX_ITEMS: usize = 8;
let mut output = String::new();
let last_index = values.len() - 1;
let main_length = last_index.min(9);
let main_list = &values[..main_length];
for value in main_list {
output.push_str(value);
output.push_str(", ");
}
if values.len() > MAX_ITEMS {
write!(output, "or {} more", values.len() - main_length).unwrap();
} else {
output.push_str("or ");
output.push_str(values[values.len() - 1]);
}
output
}
#[cfg(test)]
mod test {
use super::*;
fn resolve(class: &str, prop: &str, json_value: &str) -> Variant {
let unresolved: UnresolvedValue = serde_json::from_str(json_value).unwrap();
unresolved.resolve(class, prop).unwrap()
}
#[test]
fn bools() {
assert_eq!(resolve("BoolValue", "Value", "false"), Variant::Bool(false));
// Script.Disabled is inherited from BaseScript
assert_eq!(resolve("Script", "Disabled", "true"), Variant::Bool(true));
}
#[test]
fn strings() {
// String literals can stay as strings
assert_eq!(
resolve("StringValue", "Value", "\"Hello!\""),
Variant::String("Hello!".into()),
);
// String literals can also turn into Content
assert_eq!(
resolve("Sky", "MoonTextureId", "\"rbxassetid://12345\""),
Variant::Content("rbxassetid://12345".into()),
);
// What about BinaryString values? For forward-compatibility reasons, we
// don't support any shorthands for BinaryString.
//
// assert_eq!(
// resolve("Folder", "Tags", "\"a\\u0000b\\u0000c\""),
// Variant::BinaryString(b"a\0b\0c".to_vec().into()),
// );
}
#[test]
fn numbers() {
assert_eq!(
resolve("Part", "CollisionGroupId", "123"),
Variant::Int32(123),
);
assert_eq!(
resolve("Folder", "SourceAssetId", "532413"),
Variant::Int64(532413),
);
assert_eq!(resolve("Part", "Transparency", "1"), Variant::Float32(1.0));
assert_eq!(resolve("NumberValue", "Value", "1"), Variant::Float64(1.0));
}
#[test]
fn vectors() {
assert_eq!(
resolve("ParticleEmitter", "SpreadAngle", "[1, 2]"),
Variant::Vector2(Vector2::new(1.0, 2.0)),
);
assert_eq!(
resolve("Part", "Position", "[4, 5, 6]"),
Variant::Vector3(Vector3::new(4.0, 5.0, 6.0)),
);
}
#[test]
fn colors() {
assert_eq!(
resolve("Part", "Color", "[1, 1, 1]"),
Variant::Color3(Color3::new(1.0, 1.0, 1.0)),
);
// There aren't any user-facing Color3uint8 properties. If there are
// some, we should treat them the same in the future.
}
#[test]
fn enums() {
assert_eq!(
resolve("Lighting", "Technology", "\"Voxel\""),
Variant::Enum(Enum::from_u32(1)),
);
}
}

View File

@@ -46,7 +46,6 @@ pub struct ChangeProcessor {
impl ChangeProcessor { impl ChangeProcessor {
/// Spin up the ChangeProcessor, connecting it to the given tree, VFS, and /// Spin up the ChangeProcessor, connecting it to the given tree, VFS, and
/// outbound message queue. /// outbound message queue.
#[profiling::function]
pub fn start( pub fn start(
tree: Arc<Mutex<RojoTree>>, tree: Arc<Mutex<RojoTree>>,
vfs: Arc<Vfs>, vfs: Arc<Vfs>,

View File

@@ -97,7 +97,6 @@ fn xml_encode_config() -> rbx_xml::EncodeOptions {
rbx_xml::EncodeOptions::new().property_behavior(rbx_xml::EncodePropertyBehavior::WriteUnknown) rbx_xml::EncodeOptions::new().property_behavior(rbx_xml::EncodePropertyBehavior::WriteUnknown)
} }
#[profiling::function]
fn write_model( fn write_model(
session: &ServeSession, session: &ServeSession,
output: &Path, output: &Path,

View File

@@ -9,7 +9,6 @@ mod tree_view;
mod auth_cookie; mod auth_cookie;
mod change_processor; mod change_processor;
mod glob;
mod lua_ast; mod lua_ast;
mod message_queue; mod message_queue;
mod multimap; mod multimap;
@@ -18,7 +17,6 @@ mod project;
mod resolution; mod resolution;
mod serve_session; mod serve_session;
mod session_id; mod session_id;
mod small_string;
mod snapshot; mod snapshot;
mod snapshot_middleware; mod snapshot_middleware;
mod web; mod web;

View File

@@ -6,8 +6,6 @@ use clap::Parser;
use librojo::cli::Options; use librojo::cli::Options;
fn main() { fn main() {
profiling::register_thread!("Main Thread");
panic::set_hook(Box::new(|panic_info| { panic::set_hook(Box::new(|panic_info| {
// PanicInfo's payload is usually a &'static str or String. // PanicInfo's payload is usually a &'static str or String.
// See: https://doc.rust-lang.org/beta/std/panic/struct.PanicInfo.html#method.payload // See: https://doc.rust-lang.org/beta/std/panic/struct.PanicInfo.html#method.payload

View File

@@ -1,379 +1,3 @@
use std::{ pub use rojo_project::{OptionalPathNode, PathNode, Project, ProjectNode};
collections::{BTreeMap, HashMap, HashSet},
fs, io,
net::IpAddr,
path::{Path, PathBuf},
};
use serde::{Deserialize, Serialize}; pub use anyhow::Error as ProjectError;
use thiserror::Error;
use crate::{glob::Glob, resolution::UnresolvedValue};
static PROJECT_FILENAME: &str = "default.project.json";
/// Error type returned by any function that handles projects.
#[derive(Debug, Error)]
#[error(transparent)]
pub struct ProjectError(#[from] Error);
#[derive(Debug, Error)]
enum Error {
#[error(transparent)]
Io {
#[from]
source: io::Error,
},
#[error("Error parsing Rojo project in path {}", .path.display())]
Json {
source: serde_json::Error,
path: PathBuf,
},
}
/// Contains all of the configuration for a Rojo-managed project.
///
/// Project files are stored in `.project.json` files.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct Project {
/// The name of the top-level instance described by the project.
pub name: String,
/// The tree of instances described by this project. Projects always
/// describe at least one instance.
pub tree: ProjectNode,
/// If specified, sets the default port that `rojo serve` should use when
/// using this project for live sync.
#[serde(skip_serializing_if = "Option::is_none")]
pub serve_port: Option<u16>,
/// If specified, contains the set of place IDs that this project is
/// compatible with when doing live sync.
///
/// This setting is intended to help prevent syncing a Rojo project into the
/// wrong Roblox place.
#[serde(skip_serializing_if = "Option::is_none")]
pub serve_place_ids: Option<HashSet<u64>>,
/// If specified, sets the current place's place ID when connecting to the
/// Rojo server from Roblox Studio.
#[serde(skip_serializing_if = "Option::is_none")]
pub place_id: Option<u64>,
/// If specified, sets the current place's game ID when connecting to the
/// Rojo server from Roblox Studio.
#[serde(skip_serializing_if = "Option::is_none")]
pub game_id: Option<u64>,
/// If specified, this address will be used in place of the default address
/// As long as --address is unprovided.
#[serde(skip_serializing_if = "Option::is_none")]
pub serve_address: Option<IpAddr>,
/// A list of globs, relative to the folder the project file is in, that
/// match files that should be excluded if Rojo encounters them.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub glob_ignore_paths: Vec<Glob>,
/// The path to the file that this project came from. Relative paths in the
/// project should be considered relative to the parent of this field, also
/// given by `Project::folder_location`.
#[serde(skip)]
pub file_location: PathBuf,
}
impl Project {
/// Tells whether the given path describes a Rojo project.
pub fn is_project_file(path: &Path) -> bool {
path.file_name()
.and_then(|name| name.to_str())
.map(|name| name.ends_with(".project.json"))
.unwrap_or(false)
}
/// Attempt to locate a project represented by the given path.
///
/// This will find a project if the path refers to a `.project.json` file,
/// or is a folder that contains a `default.project.json` file.
fn locate(path: &Path) -> Option<PathBuf> {
let meta = fs::metadata(path).ok()?;
if meta.is_file() {
if Project::is_project_file(path) {
Some(path.to_path_buf())
} else {
None
}
} else {
let child_path = path.join(PROJECT_FILENAME);
let child_meta = fs::metadata(&child_path).ok()?;
if child_meta.is_file() {
Some(child_path)
} else {
// This is a folder with the same name as a Rojo default project
// file.
//
// That's pretty weird, but we can roll with it.
None
}
}
}
pub fn load_from_slice(
contents: &[u8],
project_file_location: &Path,
) -> Result<Self, ProjectError> {
let mut project: Self =
serde_json::from_slice(&contents).map_err(|source| Error::Json {
source,
path: project_file_location.to_owned(),
})?;
project.file_location = project_file_location.to_path_buf();
project.check_compatibility();
Ok(project)
}
pub fn load_fuzzy(fuzzy_project_location: &Path) -> Result<Option<Self>, ProjectError> {
if let Some(project_path) = Self::locate(fuzzy_project_location) {
let project = Self::load_exact(&project_path)?;
Ok(Some(project))
} else {
Ok(None)
}
}
fn load_exact(project_file_location: &Path) -> Result<Self, Error> {
let contents = fs::read_to_string(project_file_location)?;
let mut project: Project =
serde_json::from_str(&contents).map_err(|source| Error::Json {
source,
path: project_file_location.to_owned(),
})?;
project.file_location = project_file_location.to_path_buf();
project.check_compatibility();
Ok(project)
}
/// Checks if there are any compatibility issues with this project file and
/// warns the user if there are any.
fn check_compatibility(&self) {
self.tree.validate_reserved_names();
}
pub fn folder_location(&self) -> &Path {
self.file_location.parent().unwrap()
}
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct OptionalPathNode {
#[serde(serialize_with = "crate::path_serializer::serialize_absolute")]
pub optional: PathBuf,
}
impl OptionalPathNode {
pub fn new(optional: PathBuf) -> Self {
OptionalPathNode { optional }
}
}
/// Describes a path that is either optional or required
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum PathNode {
Required(#[serde(serialize_with = "crate::path_serializer::serialize_absolute")] PathBuf),
Optional(OptionalPathNode),
}
impl PathNode {
pub fn path(&self) -> &Path {
match self {
PathNode::Required(pathbuf) => &pathbuf,
PathNode::Optional(OptionalPathNode { optional }) => &optional,
}
}
}
/// Describes an instance and its descendants in a project.
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct ProjectNode {
/// If set, defines the ClassName of the described instance.
///
/// `$className` MUST be set if `$path` is not set.
///
/// `$className` CANNOT be set if `$path` is set and the instance described
/// by that path has a ClassName other than Folder.
#[serde(rename = "$className", skip_serializing_if = "Option::is_none")]
pub class_name: Option<String>,
/// Contains all of the children of the described instance.
#[serde(flatten)]
pub children: BTreeMap<String, ProjectNode>,
/// The properties that will be assigned to the resulting instance.
///
// TODO: Is this legal to set if $path is set?
#[serde(
rename = "$properties",
default,
skip_serializing_if = "HashMap::is_empty"
)]
pub properties: HashMap<String, UnresolvedValue>,
/// Defines the behavior when Rojo encounters unknown instances in Roblox
/// Studio during live sync. `$ignoreUnknownInstances` should be considered
/// a large hammer and used with care.
///
/// If set to `true`, those instances will be left alone. This may cause
/// issues when files that turn into instances are removed while Rojo is not
/// running.
///
/// If set to `false`, Rojo will destroy any instances it does not
/// recognize.
///
/// If unset, its default value depends on other settings:
/// - If `$path` is not set, defaults to `true`
/// - If `$path` is set, defaults to `false`
#[serde(
rename = "$ignoreUnknownInstances",
skip_serializing_if = "Option::is_none"
)]
pub ignore_unknown_instances: Option<bool>,
/// Defines that this instance should come from the given file path. This
/// path can point to any file type supported by Rojo, including Lua files
/// (`.lua`), Roblox models (`.rbxm`, `.rbxmx`), and localization table
/// spreadsheets (`.csv`).
#[serde(rename = "$path", skip_serializing_if = "Option::is_none")]
pub path: Option<PathNode>,
}
impl ProjectNode {
fn validate_reserved_names(&self) {
for (name, child) in &self.children {
if name.starts_with('$') {
log::warn!(
"Keys starting with '$' are reserved by Rojo to ensure forward compatibility."
);
log::warn!(
"This project uses the key '{}', which should be renamed.",
name
);
}
child.validate_reserved_names();
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn path_node_required() {
let path_node: PathNode = serde_json::from_str(r#""src""#).unwrap();
assert_eq!(path_node, PathNode::Required(PathBuf::from("src")));
}
#[test]
fn path_node_optional() {
let path_node: PathNode = serde_json::from_str(r#"{ "optional": "src" }"#).unwrap();
assert_eq!(
path_node,
PathNode::Optional(OptionalPathNode::new(PathBuf::from("src")))
);
}
#[test]
fn project_node_required() {
let project_node: ProjectNode = serde_json::from_str(
r#"{
"$path": "src"
}"#,
)
.unwrap();
assert_eq!(
project_node.path,
Some(PathNode::Required(PathBuf::from("src")))
);
}
#[test]
fn project_node_optional() {
let project_node: ProjectNode = serde_json::from_str(
r#"{
"$path": { "optional": "src" }
}"#,
)
.unwrap();
assert_eq!(
project_node.path,
Some(PathNode::Optional(OptionalPathNode::new(PathBuf::from(
"src"
))))
);
}
#[test]
fn project_node_none() {
let project_node: ProjectNode = serde_json::from_str(
r#"{
"$className": "Folder"
}"#,
)
.unwrap();
assert_eq!(project_node.path, None);
}
#[test]
fn project_node_optional_serialize_absolute() {
let project_node: ProjectNode = serde_json::from_str(
r#"{
"$path": { "optional": "..\\src" }
}"#,
)
.unwrap();
let serialized = serde_json::to_string(&project_node).unwrap();
assert_eq!(serialized, r#"{"$path":{"optional":"../src"}}"#);
}
#[test]
fn project_node_optional_serialize_absolute_no_change() {
let project_node: ProjectNode = serde_json::from_str(
r#"{
"$path": { "optional": "../src" }
}"#,
)
.unwrap();
let serialized = serde_json::to_string(&project_node).unwrap();
assert_eq!(serialized, r#"{"$path":{"optional":"../src"}}"#);
}
#[test]
fn project_node_optional_serialize_optional() {
let project_node: ProjectNode = serde_json::from_str(
r#"{
"$path": "..\\src"
}"#,
)
.unwrap();
let serialized = serde_json::to_string(&project_node).unwrap();
assert_eq!(serialized, r#"{"$path":"../src"}"#);
}
}

View File

@@ -16,7 +16,7 @@ use thiserror::Error;
use crate::{ use crate::{
change_processor::ChangeProcessor, change_processor::ChangeProcessor,
message_queue::MessageQueue, message_queue::MessageQueue,
project::{Project, ProjectError}, project::Project,
session_id::SessionId, session_id::SessionId,
snapshot::{ snapshot::{
apply_patch_set, compute_patch_set, AppliedPatchSet, InstanceContext, InstanceSnapshot, apply_patch_set, compute_patch_set, AppliedPatchSet, InstanceContext, InstanceSnapshot,
@@ -96,7 +96,6 @@ impl ServeSession {
/// The project file is expected to be loaded out-of-band since it's /// The project file is expected to be loaded out-of-band since it's
/// currently loaded from the filesystem directly instead of through the /// currently loaded from the filesystem directly instead of through the
/// in-memory filesystem layer. /// in-memory filesystem layer.
#[profiling::function]
pub fn new<P: AsRef<Path>>(vfs: Vfs, start_path: P) -> Result<Self, ServeSessionError> { pub fn new<P: AsRef<Path>>(vfs: Vfs, start_path: P) -> Result<Self, ServeSessionError> {
let start_path = start_path.as_ref(); let start_path = start_path.as_ref();
let start_time = Instant::now(); let start_time = Instant::now();
@@ -238,12 +237,6 @@ pub enum ServeSessionError {
source: io::Error, source: io::Error,
}, },
#[error(transparent)]
Project {
#[from]
source: ProjectError,
},
#[error(transparent)] #[error(transparent)]
Other { Other {
#[from] #[from]

View File

@@ -1 +0,0 @@
pub use smol_str::SmolStr as SmallString;

View File

@@ -1,6 +1,6 @@
//! Defines the structure of an instance snapshot. //! Defines the structure of an instance snapshot.
use std::collections::HashMap; use std::{borrow::Cow, collections::HashMap};
use rbx_dom_weak::{ use rbx_dom_weak::{
types::{Ref, Variant}, types::{Ref, Variant},
@@ -8,8 +8,6 @@ use rbx_dom_weak::{
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::small_string::SmallString;
use super::InstanceMetadata; use super::InstanceMetadata;
/// A lightweight description of what an instance should look like. /// A lightweight description of what an instance should look like.
@@ -27,13 +25,13 @@ pub struct InstanceSnapshot {
pub metadata: InstanceMetadata, pub metadata: InstanceMetadata,
/// Correpsonds to the Name property of the instance. /// Correpsonds to the Name property of the instance.
pub name: SmallString, pub name: Cow<'static, str>,
/// Corresponds to the ClassName property of the instance. /// Corresponds to the ClassName property of the instance.
pub class_name: SmallString, pub class_name: Cow<'static, str>,
/// All other properties of the instance, weakly-typed. /// All other properties of the instance, weakly-typed.
pub properties: HashMap<SmallString, Variant>, pub properties: HashMap<String, Variant>,
/// The children of the instance represented as more snapshots. /// The children of the instance represented as more snapshots.
/// ///
@@ -46,37 +44,37 @@ impl InstanceSnapshot {
Self { Self {
snapshot_id: None, snapshot_id: None,
metadata: InstanceMetadata::default(), metadata: InstanceMetadata::default(),
name: "DEFAULT".into(), name: Cow::Borrowed("DEFAULT"),
class_name: "DEFAULT".into(), class_name: Cow::Borrowed("DEFAULT"),
properties: HashMap::new(), properties: HashMap::new(),
children: Vec::new(), children: Vec::new(),
} }
} }
pub fn name(self, name: impl Into<SmallString>) -> Self { pub fn name(self, name: impl Into<String>) -> Self {
Self { Self {
name: name.into(), name: Cow::Owned(name.into()),
..self ..self
} }
} }
pub fn class_name(self, class_name: impl Into<SmallString>) -> Self { pub fn class_name(self, class_name: impl Into<String>) -> Self {
Self { Self {
class_name: class_name.into(), class_name: Cow::Owned(class_name.into()),
..self ..self
} }
} }
pub fn property<K, V>(mut self, key: K, value: V) -> Self pub fn property<K, V>(mut self, key: K, value: V) -> Self
where where
K: Into<SmallString>, K: Into<String>,
V: Into<Variant>, V: Into<Variant>,
{ {
self.properties.insert(key.into(), value.into()); self.properties.insert(key.into(), value.into());
self self
} }
pub fn properties(self, properties: impl Into<HashMap<SmallString, Variant>>) -> Self { pub fn properties(self, properties: impl Into<HashMap<String, Variant>>) -> Self {
Self { Self {
properties: properties.into(), properties: properties.into(),
..self ..self
@@ -114,18 +112,12 @@ impl InstanceSnapshot {
.map(|id| Self::from_tree(tree, id)) .map(|id| Self::from_tree(tree, id))
.collect(); .collect();
let properties = instance
.properties
.iter()
.map(|(key, value)| (key.into(), value.clone()))
.collect();
Self { Self {
snapshot_id: Some(id), snapshot_id: Some(id),
metadata: InstanceMetadata::default(), metadata: InstanceMetadata::default(),
name: SmallString::from(&instance.name), name: Cow::Owned(instance.name.clone()),
class_name: SmallString::from(&instance.class), class_name: Cow::Owned(instance.class.clone()),
properties, properties: instance.properties.clone(),
children, children,
} }
} }

View File

@@ -4,9 +4,10 @@ use std::{
sync::Arc, sync::Arc,
}; };
use rojo_project::glob::Glob;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{glob::Glob, path_serializer, project::ProjectNode}; use crate::{path_serializer, project::ProjectNode};
/// Rojo-specific metadata that can be associated with an instance or a snapshot /// Rojo-specific metadata that can be associated with an instance or a snapshot
/// of an instance. /// of an instance.

View File

@@ -5,8 +5,6 @@ use std::collections::HashMap;
use rbx_dom_weak::types::{Ref, Variant}; use rbx_dom_weak::types::{Ref, Variant};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::small_string::SmallString;
use super::{InstanceMetadata, InstanceSnapshot}; use super::{InstanceMetadata, InstanceSnapshot};
/// A set of different kinds of patches that can be applied to an WeakDom. /// A set of different kinds of patches that can be applied to an WeakDom.
@@ -42,12 +40,12 @@ pub struct PatchAdd {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PatchUpdate { pub struct PatchUpdate {
pub id: Ref, pub id: Ref,
pub changed_name: Option<SmallString>, pub changed_name: Option<String>,
pub changed_class_name: Option<SmallString>, pub changed_class_name: Option<String>,
/// Contains all changed properties. If a property is assigned to `None`, /// Contains all changed properties. If a property is assigned to `None`,
/// then that property has been removed. /// then that property has been removed.
pub changed_properties: HashMap<SmallString, Option<Variant>>, pub changed_properties: HashMap<String, Option<Variant>>,
/// Changed Rojo-specific metadata, if any of it changed. /// Changed Rojo-specific metadata, if any of it changed.
pub changed_metadata: Option<InstanceMetadata>, pub changed_metadata: Option<InstanceMetadata>,
@@ -85,9 +83,9 @@ pub struct AppliedPatchUpdate {
pub id: Ref, pub id: Ref,
// TODO: Store previous values in order to detect application conflicts // TODO: Store previous values in order to detect application conflicts
pub changed_name: Option<SmallString>, pub changed_name: Option<String>,
pub changed_class_name: Option<SmallString>, pub changed_class_name: Option<String>,
pub changed_properties: HashMap<SmallString, Option<Variant>>, pub changed_properties: HashMap<String, Option<Variant>>,
pub changed_metadata: Option<InstanceMetadata>, pub changed_metadata: Option<InstanceMetadata>,
} }

View File

@@ -4,8 +4,6 @@ use std::collections::HashMap;
use rbx_dom_weak::types::{Ref, Variant}; use rbx_dom_weak::types::{Ref, Variant};
use crate::small_string::SmallString;
use super::{ use super::{
patch::{AppliedPatchSet, AppliedPatchUpdate, PatchSet, PatchUpdate}, patch::{AppliedPatchSet, AppliedPatchUpdate, PatchSet, PatchUpdate},
InstanceSnapshot, RojoTree, InstanceSnapshot, RojoTree,
@@ -14,7 +12,6 @@ use super::{
/// Consumes the input `PatchSet`, applying all of its prescribed changes to the /// Consumes the input `PatchSet`, applying all of its prescribed changes to the
/// tree and returns an `AppliedPatchSet`, which can be used to keep another /// tree and returns an `AppliedPatchSet`, which can be used to keep another
/// tree in sync with Rojo's. /// tree in sync with Rojo's.
#[profiling::function]
pub fn apply_patch_set(tree: &mut RojoTree, patch_set: PatchSet) -> AppliedPatchSet { pub fn apply_patch_set(tree: &mut RojoTree, patch_set: PatchSet) -> AppliedPatchSet {
let mut context = PatchApplyContext::default(); let mut context = PatchApplyContext::default();
@@ -71,7 +68,7 @@ struct PatchApplyContext {
/// ///
/// This doesn't affect updated instances, since they're always applied /// This doesn't affect updated instances, since they're always applied
/// after we've added all the instances from the patch. /// after we've added all the instances from the patch.
added_instance_properties: HashMap<Ref, HashMap<SmallString, Variant>>, added_instance_properties: HashMap<Ref, HashMap<String, Variant>>,
/// The current applied patch result, describing changes made to the tree. /// The current applied patch result, describing changes made to the tree.
applied_patch_set: AppliedPatchSet, applied_patch_set: AppliedPatchSet,
@@ -103,9 +100,7 @@ fn finalize_patch_application(context: PatchApplyContext, tree: &mut RojoTree) -
} }
} }
instance instance.properties_mut().insert(key, property_value);
.properties_mut()
.insert(key.to_string(), property_value);
} }
} }
@@ -169,13 +164,13 @@ fn apply_update_child(context: &mut PatchApplyContext, tree: &mut RojoTree, patc
}; };
if let Some(name) = patch.changed_name { if let Some(name) = patch.changed_name {
*instance.name_mut() = name.to_string(); *instance.name_mut() = name.clone();
applied_patch.changed_name = Some(name.into()); applied_patch.changed_name = Some(name);
} }
if let Some(class_name) = patch.changed_class_name { if let Some(class_name) = patch.changed_class_name {
*instance.class_name_mut() = class_name.to_string(); *instance.class_name_mut() = class_name.clone();
applied_patch.changed_class_name = Some(class_name.into()); applied_patch.changed_class_name = Some(class_name);
} }
for (key, property_entry) in patch.changed_properties { for (key, property_entry) in patch.changed_properties {
@@ -200,15 +195,13 @@ fn apply_update_child(context: &mut PatchApplyContext, tree: &mut RojoTree, patc
instance instance
.properties_mut() .properties_mut()
.insert(key.to_string(), Variant::Ref(new_referent)); .insert(key.clone(), Variant::Ref(new_referent));
} }
Some(ref value) => { Some(ref value) => {
instance instance.properties_mut().insert(key.clone(), value.clone());
.properties_mut()
.insert(key.to_string(), value.clone());
} }
None => { None => {
instance.properties_mut().remove(key.as_str()); instance.properties_mut().remove(&key);
} }
} }

View File

@@ -10,7 +10,6 @@ use super::{
InstanceSnapshot, InstanceWithMeta, RojoTree, InstanceSnapshot, InstanceWithMeta, RojoTree,
}; };
#[profiling::function]
pub fn compute_patch_set( pub fn compute_patch_set(
snapshot: Option<&InstanceSnapshot>, snapshot: Option<&InstanceSnapshot>,
tree: &RojoTree, tree: &RojoTree,
@@ -103,13 +102,13 @@ fn compute_property_patches(
let changed_name = if snapshot.name == instance.name() { let changed_name = if snapshot.name == instance.name() {
None None
} else { } else {
Some(snapshot.name.clone()) Some(snapshot.name.clone().into_owned())
}; };
let changed_class_name = if snapshot.class_name == instance.class_name() { let changed_class_name = if snapshot.class_name == instance.class_name() {
None None
} else { } else {
Some(snapshot.class_name.clone()) Some(snapshot.class_name.clone().into_owned())
}; };
let changed_metadata = if &snapshot.metadata == instance.metadata() { let changed_metadata = if &snapshot.metadata == instance.metadata() {
@@ -121,7 +120,7 @@ fn compute_property_patches(
for (name, snapshot_value) in &snapshot.properties { for (name, snapshot_value) in &snapshot.properties {
visited_properties.insert(name.as_str()); visited_properties.insert(name.as_str());
match instance.properties().get(name.as_str()) { match instance.properties().get(name) {
Some(instance_value) => { Some(instance_value) => {
if snapshot_value != instance_value { if snapshot_value != instance_value {
changed_properties.insert(name.clone(), Some(snapshot_value.clone())); changed_properties.insert(name.clone(), Some(snapshot_value.clone()));
@@ -138,7 +137,7 @@ fn compute_property_patches(
continue; continue;
} }
changed_properties.insert(name.into(), None); changed_properties.insert(name.clone(), None);
} }
if changed_properties.is_empty() if changed_properties.is_empty()

View File

@@ -30,7 +30,7 @@ pub fn snapshot_csv(
.name(name) .name(name)
.class_name("LocalizationTable") .class_name("LocalizationTable")
.properties(hashmap! { .properties(hashmap! {
"Contents".into() => table_contents.into(), "Contents".to_owned() => table_contents.into(),
}) })
.metadata( .metadata(
InstanceMetadata::new() InstanceMetadata::new()

View File

@@ -25,7 +25,7 @@ pub fn snapshot_json(
let as_lua = json_to_lua(value).to_string(); let as_lua = json_to_lua(value).to_string();
let properties = hashmap! { let properties = hashmap! {
"Source".into() => as_lua.into(), "Source".to_owned() => as_lua.into(),
}; };
let meta_path = path.with_file_name(format!("{}.meta.json", name)); let meta_path = path.with_file_name(format!("{}.meta.json", name));

View File

@@ -1,4 +1,4 @@
use std::{collections::HashMap, path::Path, str}; use std::{borrow::Cow, collections::HashMap, path::Path, str};
use anyhow::Context; use anyhow::Context;
use memofs::Vfs; use memofs::Vfs;
@@ -85,14 +85,14 @@ impl JsonModelCore {
let mut properties = HashMap::with_capacity(self.properties.len()); let mut properties = HashMap::with_capacity(self.properties.len());
for (key, unresolved) in self.properties { for (key, unresolved) in self.properties {
let value = unresolved.resolve(&class_name, &key)?; let value = unresolved.resolve(&class_name, &key)?;
properties.insert(key.into(), value); properties.insert(key, value);
} }
Ok(InstanceSnapshot { Ok(InstanceSnapshot {
snapshot_id: None, snapshot_id: None,
metadata: Default::default(), metadata: Default::default(),
name: name.into(), name: Cow::Owned(name),
class_name: class_name.into(), class_name: Cow::Owned(class_name),
properties, properties,
children, children,
}) })

View File

@@ -38,7 +38,7 @@ pub fn snapshot_lua(
.name(instance_name) .name(instance_name)
.class_name(class_name) .class_name(class_name)
.properties(hashmap! { .properties(hashmap! {
"Source".into() => contents_str.into(), "Source".to_owned() => contents_str.into(),
}) })
.metadata( .metadata(
InstanceMetadata::new() InstanceMetadata::new()

View File

@@ -1,4 +1,4 @@
use std::{collections::HashMap, path::PathBuf}; use std::{borrow::Cow, collections::HashMap, path::PathBuf};
use anyhow::{format_err, Context}; use anyhow::{format_err, Context};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -49,7 +49,7 @@ impl AdjacentMetadata {
.resolve(&snapshot.class_name, &key) .resolve(&snapshot.class_name, &key)
.with_context(|| format!("error applying meta file {}", path.display()))?; .with_context(|| format!("error applying meta file {}", path.display()))?;
snapshot.properties.insert(key.into(), value); snapshot.properties.insert(key, value);
} }
Ok(()) Ok(())
@@ -116,7 +116,7 @@ impl DirectoryMetadata {
)); ));
} }
snapshot.class_name = class_name.into(); snapshot.class_name = Cow::Owned(class_name);
} }
Ok(()) Ok(())
@@ -136,7 +136,7 @@ impl DirectoryMetadata {
.resolve(&snapshot.class_name, &key) .resolve(&snapshot.class_name, &key)
.with_context(|| format!("error applying meta file {}", path.display()))?; .with_context(|| format!("error applying meta file {}", path.display()))?;
snapshot.properties.insert(key.into(), value); snapshot.properties.insert(key, value);
} }
Ok(()) Ok(())

View File

@@ -40,7 +40,6 @@ pub use self::project::snapshot_project_node;
/// The main entrypoint to the snapshot function. This function can be pointed /// The main entrypoint to the snapshot function. This function can be pointed
/// at any path and will return something if Rojo knows how to deal with it. /// at any path and will return something if Rojo knows how to deal with it.
#[profiling::function]
pub fn snapshot_from_vfs( pub fn snapshot_from_vfs(
context: &InstanceContext, context: &InstanceContext,
vfs: &Vfs, vfs: &Vfs,

View File

@@ -6,7 +6,6 @@ use rbx_reflection::ClassTag;
use crate::{ use crate::{
project::{PathNode, Project, ProjectNode}, project::{PathNode, Project, ProjectNode},
small_string::SmallString,
snapshot::{ snapshot::{
InstanceContext, InstanceMetadata, InstanceSnapshot, InstigatingSource, PathIgnoreRule, InstanceContext, InstanceMetadata, InstanceSnapshot, InstigatingSource, PathIgnoreRule,
}, },
@@ -68,10 +67,13 @@ pub fn snapshot_project_node(
) -> anyhow::Result<Option<InstanceSnapshot>> { ) -> anyhow::Result<Option<InstanceSnapshot>> {
let project_folder = project_path.parent().unwrap(); let project_folder = project_path.parent().unwrap();
let class_name_from_project = node.class_name.as_ref().map(|name| SmallString::from(name)); let class_name_from_project = node
.class_name
.as_ref()
.map(|name| Cow::Owned(name.clone()));
let mut class_name_from_path = None; let mut class_name_from_path = None;
let name = SmallString::from(instance_name); let name = Cow::Owned(instance_name.to_owned());
let mut properties = HashMap::new(); let mut properties = HashMap::new();
let mut children = Vec::new(); let mut children = Vec::new();
let mut metadata = InstanceMetadata::default(); let mut metadata = InstanceMetadata::default();
@@ -226,7 +228,7 @@ pub fn snapshot_project_node(
_ => {} _ => {}
} }
properties.insert(key.into(), value); properties.insert(key.clone(), value);
} }
// If the user specified $ignoreUnknownInstances, overwrite the existing // If the user specified $ignoreUnknownInstances, overwrite the existing
@@ -260,7 +262,7 @@ pub fn snapshot_project_node(
})) }))
} }
fn infer_class_name(name: &str, parent_class: Option<&str>) -> Option<SmallString> { fn infer_class_name(name: &str, parent_class: Option<&str>) -> Option<Cow<'static, str>> {
// If className wasn't defined from another source, we may be able // If className wasn't defined from another source, we may be able
// to infer one. // to infer one.
@@ -273,13 +275,13 @@ fn infer_class_name(name: &str, parent_class: Option<&str>) -> Option<SmallStrin
let descriptor = rbx_reflection_database::get().classes.get(name)?; let descriptor = rbx_reflection_database::get().classes.get(name)?;
if descriptor.tags.contains(&ClassTag::Service) { if descriptor.tags.contains(&ClassTag::Service) {
return Some(name.into()); return Some(Cow::Owned(name.to_owned()));
} }
} else if parent_class == "StarterPlayer" { } else if parent_class == "StarterPlayer" {
// StarterPlayer has two special members with their own classes. // StarterPlayer has two special members with their own classes.
if name == "StarterPlayerScripts" || name == "StarterCharacterScripts" { if name == "StarterPlayerScripts" || name == "StarterCharacterScripts" {
return Some(name.into()); return Some(Cow::Owned(name.to_owned()));
} }
} }

View File

@@ -21,7 +21,7 @@ pub fn snapshot_txt(
.to_owned(); .to_owned();
let properties = hashmap! { let properties = hashmap! {
"Value".into() => contents_str.into(), "Value".to_owned() => contents_str.into(),
}; };
let meta_path = path.with_file_name(format!("{}.meta.json", name)); let meta_path = path.with_file_name(format!("{}.meta.json", name));

View File

@@ -12,7 +12,6 @@ use serde::{Deserialize, Serialize};
use crate::{ use crate::{
session_id::SessionId, session_id::SessionId,
small_string::SmallString,
snapshot::{ snapshot::{
AppliedPatchSet, InstanceMetadata as RojoInstanceMetadata, InstanceWithMeta, RojoTree, AppliedPatchSet, InstanceMetadata as RojoInstanceMetadata, InstanceWithMeta, RojoTree,
}, },
@@ -84,13 +83,13 @@ impl<'a> SubscribeMessage<'a> {
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct InstanceUpdate { pub struct InstanceUpdate {
pub id: Ref, pub id: Ref,
pub changed_name: Option<SmallString>, pub changed_name: Option<String>,
pub changed_class_name: Option<SmallString>, pub changed_class_name: Option<String>,
// TODO: Transform from HashMap<_, Option<_>> to something else, since // TODO: Transform from HashMap<String, Option<_>> to something else, since
// null will get lost when decoding from JSON in some languages. // null will get lost when decoding from JSON in some languages.
#[serde(default)] #[serde(default)]
pub changed_properties: HashMap<SmallString, Option<Variant>>, pub changed_properties: HashMap<String, Option<Variant>>,
pub changed_metadata: Option<InstanceMetadata>, pub changed_metadata: Option<InstanceMetadata>,
} }