Upgrade to rbx_dom_weak 2.0 (#377)

* Mostly mechanical port bits

* Almost there

* It builds again!

* Turn on all the code again

* Tests compiling but not passing

* Stub work for value resolution

* Implement resolution minus enums and derived properties

* Implement property descriptor resolution

* Update referent snapshots

* Update unions test project

Using a place file instead of a model yields better
error messages in Roblox Studio.

* Add easy shortcut to testing with local rbx-dom

* Update rbx-dom

* Add enum resolution

* Update init.meta.json to use UnresolvedValue

* Expand value resolution support, add test

* Filter SharedString values from web API

* Add 'property' builder method to InstanceSnapshot

* Change InstanceSnapshot/InstanceBuilder boundary

* Fix remove_file crash

* rustfmt

* Update to latest rbx_dom_lua

* Update dependencies, including rbx_dom_weak

* Update to latest rbx-dom

* Update dependencies

* Update rbx-dom, fixing more bugs

* Remove experimental warning on binary place builds

* Remove unused imports
This commit is contained in:
Lucien Greathouse
2021-02-18 20:56:09 -05:00
committed by GitHub
parent b84aab0960
commit 59ef5f05ea
63 changed files with 45602 additions and 21004 deletions

View File

@@ -6,7 +6,7 @@ use std::{
use crossbeam_channel::{select, Receiver, RecvError, Sender};
use jod_thread::JoinHandle;
use memofs::{IoResultExt, Vfs, VfsEvent};
use rbx_dom_weak::{RbxId, RbxValue};
use rbx_dom_weak::types::{Ref, Variant};
use crate::{
message_queue::MessageQueue,
@@ -179,7 +179,7 @@ impl JobThreadContext {
InstigatingSource::Path(path) => fs::remove_file(path).unwrap(),
InstigatingSource::ProjectNode(_, _, _, _) => {
log::warn!(
"Cannot remove instance {}, it's from a project file",
"Cannot remove instance {:?}, it's from a project file",
id
);
}
@@ -187,12 +187,12 @@ impl JobThreadContext {
} else {
// TODO
log::warn!(
"Cannot remove instance {}, it is not an instigating source.",
"Cannot remove instance {:?}, it is not an instigating source.",
id
);
}
} else {
log::warn!("Cannot remove instance {}, it does not exist.", id);
log::warn!("Cannot remove instance {:?}, it does not exist.", id);
}
}
@@ -219,7 +219,7 @@ impl JobThreadContext {
{
match instigating_source {
InstigatingSource::Path(path) => {
if let Some(RbxValue::String { value }) = changed_value {
if let Some(Variant::String(value)) = changed_value {
fs::write(path, value).unwrap();
} else {
log::warn!("Cannot change Source to non-string value.");
@@ -227,14 +227,14 @@ impl JobThreadContext {
}
InstigatingSource::ProjectNode(_, _, _, _) => {
log::warn!(
"Cannot remove instance {}, it's from a project file",
"Cannot remove instance {:?}, it's from a project file",
id
);
}
}
} else {
log::warn!(
"Cannot update instance {}, it is not an instigating source.",
"Cannot update instance {:?}, it is not an instigating source.",
id
);
}
@@ -243,7 +243,7 @@ impl JobThreadContext {
}
}
} else {
log::warn!("Cannot update instance {}, it does not exist.", id);
log::warn!("Cannot update instance {:?}, it does not exist.", id);
}
}
@@ -254,7 +254,7 @@ impl JobThreadContext {
}
}
fn compute_and_apply_changes(tree: &mut RojoTree, vfs: &Vfs, id: RbxId) -> Option<AppliedPatchSet> {
fn compute_and_apply_changes(tree: &mut RojoTree, vfs: &Vfs, id: Ref) -> Option<AppliedPatchSet> {
let metadata = tree
.get_metadata(id)
.expect("metadata missing for instance present in tree");
@@ -263,7 +263,7 @@ fn compute_and_apply_changes(tree: &mut RojoTree, vfs: &Vfs, id: RbxId) -> Optio
Some(path) => path,
None => {
log::error!(
"Instance {} did not have an instigating source, but was considered for an update.",
"Instance {:?} did not have an instigating source, but was considered for an update.",
id
);
log::error!("This is a bug. Please file an issue!");

View File

@@ -88,7 +88,7 @@ fn write_model(tree: &RojoTree, options: &BuildCommand) -> Result<(), anyhow::Er
}
OutputKind::Rbxlx => {
// Place files don't contain an entry for the DataModel, but our
// RbxTree representation does.
// WeakDom representation does.
let root_instance = tree.get_instance(root_id).unwrap();
let top_level_ids = root_instance.children();
@@ -96,17 +96,13 @@ fn write_model(tree: &RojoTree, options: &BuildCommand) -> Result<(), anyhow::Er
rbx_xml::to_writer(&mut file, tree.inner(), top_level_ids, xml_encode_config())?;
}
OutputKind::Rbxm => {
rbx_binary::encode(tree.inner(), &[root_id], &mut file)?;
rbx_binary::to_writer_default(&mut file, tree.inner(), &[root_id])?;
}
OutputKind::Rbxl => {
log::warn!("Support for building binary places (rbxl) is still experimental.");
log::warn!("Using the XML place format (rbxlx) is recommended instead.");
log::warn!("For more info, see https://github.com/rojo-rbx/rojo/issues/180");
let root_instance = tree.get_instance(root_id).unwrap();
let top_level_ids = root_instance.children();
rbx_binary::encode(tree.inner(), top_level_ids, &mut file)?;
rbx_binary::to_writer_default(&mut file, tree.inner(), top_level_ids)?;
}
}

View File

@@ -49,7 +49,7 @@ pub fn install_plugin() -> Result<()> {
let tree = session.tree();
let root_id = tree.get_root_id();
rbx_binary::encode(tree.inner(), &[root_id], &mut file)?;
rbx_binary::to_writer_default(&mut file, tree.inner(), &[root_id])?;
Ok(())
}

View File

@@ -29,12 +29,11 @@ pub fn upload(options: UploadCommand) -> Result<(), anyhow::Error> {
let tree = session.tree();
let inner_tree = tree.inner();
let root_id = inner_tree.get_root_id();
let root_instance = inner_tree.get_instance(root_id).unwrap();
let root = inner_tree.root();
let encode_ids = match root_instance.class_name.as_str() {
"DataModel" => root_instance.get_children_ids().to_vec(),
_ => vec![root_id],
let encode_ids = match root.class.as_str() {
"DataModel" => root.children().to_vec(),
_ => vec![root.referent()],
};
let mut buffer = Vec::new();

View File

@@ -15,6 +15,7 @@ mod message_queue;
mod multimap;
mod path_serializer;
mod project;
mod resolution;
mod serve_session;
mod session_id;
mod snapshot;

View File

@@ -4,11 +4,10 @@ use std::{
path::{Path, PathBuf},
};
use rbx_dom_weak::UnresolvedRbxValue;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::glob::Glob;
use crate::{glob::Glob, resolution::UnresolvedValue};
static PROJECT_FILENAME: &str = "default.project.json";
@@ -183,7 +182,7 @@ pub struct ProjectNode {
default,
skip_serializing_if = "HashMap::is_empty"
)]
pub properties: HashMap<String, UnresolvedRbxValue>,
pub properties: HashMap<String, UnresolvedValue>,
/// Defines the behavior when Rojo encounters unknown instances in Roblox
/// Studio during live sync. `$ignoreUnknownInstances` should be considered

159
src/resolution.rs Normal file
View File

@@ -0,0 +1,159 @@
use anyhow::format_err;
use rbx_dom_weak::types::{
Color3, Color3uint8, Content, Enum, Variant, VariantType, Vector2, Vector3,
};
use rbx_reflection::{DataType, PropertyDescriptor};
use serde::{Deserialize, Serialize};
#[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),
Number(f64),
Array2([f64; 2]),
Array3([f64; 3]),
Array4([f64; 4]),
Array16([f64; 16]),
}
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 sample_values = enum_descriptor
.items
.keys()
.take(3)
.map(|name| format!(r#""{}""#, name))
.collect::<Vec<_>>()
.join(", ");
format_err!(
"Invalid value for property {}.{}. Got {} but \
expected a member of the {} enum such as {}",
class_name,
prop_name,
what,
enum_name,
sample_values
)
};
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::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::Color3uint8, AmbiguousValue::Array3(value)) => {
let value = Color3uint8::new(
(value[0] / 255.0) as u8,
(value[1] / 255.0) as u8,
(value[2] / 255.0) as u8,
);
Ok(value.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::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::Array16(_) => "an array of 16 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()?;
}
}

View File

@@ -10,7 +10,6 @@ use std::{
use crossbeam_channel::Sender;
use memofs::IoResultExt;
use memofs::Vfs;
use rbx_dom_weak::RbxInstanceProperties;
use thiserror::Error;
use crate::{
@@ -19,8 +18,8 @@ use crate::{
project::{Project, ProjectError},
session_id::SessionId,
snapshot::{
apply_patch_set, compute_patch_set, AppliedPatchSet, InstanceContext,
InstancePropertiesWithMeta, PatchSet, RojoTree,
apply_patch_set, compute_patch_set, AppliedPatchSet, InstanceContext, InstanceSnapshot,
PatchSet, RojoTree,
},
snapshot_middleware::snapshot_from_vfs,
};
@@ -118,14 +117,7 @@ impl ServeSession {
}
};
let mut tree = RojoTree::new(InstancePropertiesWithMeta {
properties: RbxInstanceProperties {
name: "ROOT".to_owned(),
class_name: "Folder".to_owned(),
properties: Default::default(),
},
metadata: Default::default(),
});
let mut tree = RojoTree::new(InstanceSnapshot::new());
let root_id = tree.get_root_id();

View File

@@ -2,7 +2,10 @@
use std::{borrow::Cow, collections::HashMap};
use rbx_dom_weak::{RbxId, RbxTree, RbxValue};
use rbx_dom_weak::{
types::{Ref, Variant},
WeakDom,
};
use serde::{Deserialize, Serialize};
use super::InstanceMetadata;
@@ -11,11 +14,12 @@ use super::InstanceMetadata;
///
// Possible future improvements:
// - Use refcounted/interned strings
// - Replace use of RbxValue with a sum of RbxValue + borrowed value
// - Replace use of Variant with a sum of Variant + borrowed value
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct InstanceSnapshot {
// FIXME: Don't use Option<Ref> anymore!
/// A temporary ID applied to the snapshot that's used for Ref properties.
pub snapshot_id: Option<RbxId>,
pub snapshot_id: Option<Ref>,
/// Rojo-specific metadata associated with the instance.
pub metadata: InstanceMetadata,
@@ -27,7 +31,7 @@ pub struct InstanceSnapshot {
pub class_name: Cow<'static, str>,
/// All other properties of the instance, weakly-typed.
pub properties: HashMap<String, RbxValue>,
pub properties: HashMap<String, Variant>,
/// The children of the instance represented as more snapshots.
///
@@ -61,7 +65,16 @@ impl InstanceSnapshot {
}
}
pub fn properties(self, properties: impl Into<HashMap<String, RbxValue>>) -> Self {
pub fn property<K, V>(mut self, key: K, value: V) -> Self
where
K: Into<String>,
V: Into<Variant>,
{
self.properties.insert(key.into(), value.into());
self
}
pub fn properties(self, properties: impl Into<HashMap<String, Variant>>) -> Self {
Self {
properties: properties.into(),
..self
@@ -75,6 +88,13 @@ impl InstanceSnapshot {
}
}
pub fn snapshot_id(self, snapshot_id: Option<Ref>) -> Self {
Self {
snapshot_id,
..self
}
}
pub fn metadata(self, metadata: impl Into<InstanceMetadata>) -> Self {
Self {
metadata: metadata.into(),
@@ -82,15 +102,13 @@ impl InstanceSnapshot {
}
}
pub fn from_tree(tree: &RbxTree, id: RbxId) -> Self {
let instance = tree
.get_instance(id)
.expect("instance did not exist in tree");
pub fn from_tree(tree: &WeakDom, id: Ref) -> Self {
let instance = tree.get_by_ref(id).expect("instance did not exist in tree");
let children = instance
.get_children_ids()
.children()
.iter()
.cloned()
.copied()
.map(|id| Self::from_tree(tree, id))
.collect();
@@ -98,7 +116,7 @@ impl InstanceSnapshot {
snapshot_id: Some(id),
metadata: InstanceMetadata::default(),
name: Cow::Owned(instance.name.clone()),
class_name: Cow::Owned(instance.class_name.clone()),
class_name: Cow::Owned(instance.class.clone()),
properties: instance.properties.clone(),
children,
}

View File

@@ -2,19 +2,19 @@
use std::collections::HashMap;
use rbx_dom_weak::{RbxId, RbxValue};
use rbx_dom_weak::types::{Ref, Variant};
use serde::{Deserialize, Serialize};
use super::{InstanceMetadata, InstanceSnapshot};
/// A set of different kinds of patches that can be applied to an RbxTree.
/// A set of different kinds of patches that can be applied to an WeakDom.
///
/// These patches shouldn't be persisted: there's no mechanism in place to make
/// sure that another patch wasn't applied before this one that could cause a
/// conflict!
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
pub struct PatchSet {
pub removed_instances: Vec<RbxId>,
pub removed_instances: Vec<Ref>,
pub added_instances: Vec<PatchAdd>,
pub updated_instances: Vec<PatchUpdate>,
}
@@ -32,20 +32,20 @@ impl<'a> PatchSet {
/// A patch containing an instance that was added to the tree.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PatchAdd {
pub parent_id: RbxId,
pub parent_id: Ref,
pub instance: InstanceSnapshot,
}
/// A patch indicating that properties of an instance changed.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PatchUpdate {
pub id: RbxId,
pub id: Ref,
pub changed_name: Option<String>,
pub changed_class_name: Option<String>,
/// Contains all changed properties. If a property is assigned to `None`,
/// then that property has been removed.
pub changed_properties: HashMap<String, Option<RbxValue>>,
pub changed_properties: HashMap<String, Option<Variant>>,
/// Changed Rojo-specific metadata, if any of it changed.
pub changed_metadata: Option<InstanceMetadata>,
@@ -63,8 +63,8 @@ pub struct PatchUpdate {
// current values in all fields.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AppliedPatchSet {
pub removed: Vec<RbxId>,
pub added: Vec<RbxId>,
pub removed: Vec<Ref>,
pub added: Vec<Ref>,
pub updated: Vec<AppliedPatchUpdate>,
}
@@ -80,17 +80,17 @@ impl AppliedPatchSet {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppliedPatchUpdate {
pub id: RbxId,
pub id: Ref,
// TODO: Store previous values in order to detect application conflicts
pub changed_name: Option<String>,
pub changed_class_name: Option<String>,
pub changed_properties: HashMap<String, Option<RbxValue>>,
pub changed_properties: HashMap<String, Option<Variant>>,
pub changed_metadata: Option<InstanceMetadata>,
}
impl AppliedPatchUpdate {
pub fn new(id: RbxId) -> Self {
pub fn new(id: Ref) -> Self {
Self {
id,
changed_name: None,

View File

@@ -2,11 +2,11 @@
use std::collections::HashMap;
use rbx_dom_weak::{RbxId, RbxInstanceProperties, RbxValue};
use rbx_dom_weak::types::{Ref, Variant};
use super::{
patch::{AppliedPatchSet, AppliedPatchUpdate, PatchSet, PatchUpdate},
InstancePropertiesWithMeta, InstanceSnapshot, RojoTree,
InstanceSnapshot, RojoTree,
};
/// Consumes the input `PatchSet`, applying all of its prescribed changes to the
@@ -37,7 +37,7 @@ pub fn apply_patch_set(tree: &mut RojoTree, patch_set: PatchSet) -> AppliedPatch
struct PatchApplyContext {
/// A map from transient snapshot IDs (generated by snapshot middleware) to
/// instance IDs in the actual tree. These are both the same data type so
/// that they fit into the same `RbxValue::Ref` type.
/// that they fit into the same `Variant::Ref` type.
///
/// At this point in the patch process, IDs in instance properties have been
/// partially translated from 'snapshot space' into 'tree space' by the
@@ -53,7 +53,7 @@ struct PatchApplyContext {
/// #2 should not occur in well-formed projects, but is indistinguishable
/// from #1 right now. It could happen if two model files try to reference
/// eachother.
snapshot_id_to_instance_id: HashMap<RbxId, RbxId>,
snapshot_id_to_instance_id: HashMap<Ref, Ref>,
/// The properties of instances added by the current `PatchSet`.
///
@@ -68,7 +68,7 @@ struct PatchApplyContext {
///
/// This doesn't affect updated instances, since they're always applied
/// after we've added all the instances from the patch.
added_instance_properties: HashMap<RbxId, HashMap<String, RbxValue>>,
added_instance_properties: HashMap<Ref, HashMap<String, Variant>>,
/// The current applied patch result, describing changes made to the tree.
applied_patch_set: AppliedPatchSet,
@@ -93,11 +93,10 @@ fn finalize_patch_application(context: PatchApplyContext, tree: &mut RojoTree) -
.expect("Invalid instance ID in deferred property map");
for (key, mut property_value) in properties {
if let RbxValue::Ref { value: Some(id) } = property_value {
if let Some(&instance_id) = context.snapshot_id_to_instance_id.get(&id) {
property_value = RbxValue::Ref {
value: Some(instance_id),
};
if let Variant::Ref(referent) = property_value {
if let Some(&instance_referent) = context.snapshot_id_to_instance_id.get(&referent)
{
property_value = Variant::Ref(instance_referent);
}
}
@@ -108,50 +107,40 @@ fn finalize_patch_application(context: PatchApplyContext, tree: &mut RojoTree) -
context.applied_patch_set
}
fn apply_remove_instance(context: &mut PatchApplyContext, tree: &mut RojoTree, removed_id: RbxId) {
match tree.remove_instance(removed_id) {
Some(_) => context.applied_patch_set.removed.push(removed_id),
None => {
log::warn!(
"Patch misapplication: Tried to remove instance {} but it did not exist.",
removed_id
);
}
}
fn apply_remove_instance(context: &mut PatchApplyContext, tree: &mut RojoTree, removed_id: Ref) {
tree.remove(removed_id);
context.applied_patch_set.removed.push(removed_id);
}
fn apply_add_child(
context: &mut PatchApplyContext,
tree: &mut RojoTree,
parent_id: RbxId,
parent_id: Ref,
snapshot: InstanceSnapshot,
) {
let properties = InstancePropertiesWithMeta {
properties: RbxInstanceProperties {
name: snapshot.name.into_owned(),
class_name: snapshot.class_name.into_owned(),
let snapshot_id = snapshot.snapshot_id;
let properties = snapshot.properties;
let children = snapshot.children;
// Property assignment is deferred until after we know about all
// instances in this patch. See `PatchApplyContext` for details.
properties: HashMap::new(),
},
metadata: snapshot.metadata,
};
let id = tree.insert_instance(properties, parent_id);
// Property application is deferred until after all children
// are constructed. This helps apply referents correctly.
let remaining_snapshot = InstanceSnapshot::new()
.name(snapshot.name)
.class_name(snapshot.class_name)
.metadata(snapshot.metadata)
.snapshot_id(snapshot.snapshot_id);
let id = tree.insert_instance(parent_id, remaining_snapshot);
context.applied_patch_set.added.push(id);
context
.added_instance_properties
.insert(id, snapshot.properties);
context.added_instance_properties.insert(id, properties);
if let Some(snapshot_id) = snapshot.snapshot_id {
if let Some(snapshot_id) = snapshot_id {
context.snapshot_id_to_instance_id.insert(snapshot_id, id);
}
for child_snapshot in snapshot.children {
apply_add_child(context, tree, id, child_snapshot);
for child in children {
apply_add_child(context, tree, id, child);
}
}
@@ -167,7 +156,7 @@ fn apply_update_child(context: &mut PatchApplyContext, tree: &mut RojoTree, patc
Some(instance) => instance,
None => {
log::warn!(
"Patch misapplication: Instance {}, referred to by update patch, did not exist.",
"Patch misapplication: Instance {:?}, referred to by update patch, did not exist.",
patch.id
);
return;
@@ -189,23 +178,24 @@ fn apply_update_child(context: &mut PatchApplyContext, tree: &mut RojoTree, patc
// Ref values need to be potentially rewritten from snapshot IDs to
// instance IDs if they referred to an instance that was created as
// part of this patch.
Some(RbxValue::Ref { value: Some(id) }) => {
Some(Variant::Ref(referent)) => {
if referent.is_none() {
continue;
}
// If our ID is not found in this map, then it either refers to
// an existing instance NOT added by this patch, or there was an
// error. See `PatchApplyContext::snapshot_id_to_instance_id`
// for more info.
let new_id = context
let new_referent = context
.snapshot_id_to_instance_id
.get(&id)
.get(&referent)
.copied()
.unwrap_or(id);
.unwrap_or(referent);
instance.properties_mut().insert(
key.clone(),
RbxValue::Ref {
value: Some(new_id),
},
);
instance
.properties_mut()
.insert(key.clone(), Variant::Ref(new_referent));
}
Some(ref value) => {
instance.properties_mut().insert(key.clone(), value.clone());
@@ -225,10 +215,10 @@ fn apply_update_child(context: &mut PatchApplyContext, tree: &mut RojoTree, patc
mod test {
use super::*;
use std::{borrow::Cow, collections::HashMap};
use std::borrow::Cow;
use maplit::hashmap;
use rbx_dom_weak::RbxValue;
use rbx_dom_weak::types::Variant;
use super::super::PatchAdd;
@@ -236,14 +226,7 @@ mod test {
fn add_from_empty() {
let _ = env_logger::try_init();
let mut tree = RojoTree::new(InstancePropertiesWithMeta {
properties: RbxInstanceProperties {
name: "Folder".to_owned(),
class_name: "Folder".to_owned(),
properties: HashMap::new(),
},
metadata: Default::default(),
});
let mut tree = RojoTree::new(InstanceSnapshot::new());
let root_id = tree.get_root_id();
@@ -253,7 +236,7 @@ mod test {
name: Cow::Borrowed("Foo"),
class_name: Cow::Borrowed("Bar"),
properties: hashmap! {
"Baz".to_owned() => RbxValue::Int32 { value: 5 },
"Baz".to_owned() => Variant::Int32(5),
},
children: Vec::new(),
};
@@ -282,18 +265,14 @@ mod test {
fn update_existing() {
let _ = env_logger::try_init();
let mut tree = RojoTree::new(InstancePropertiesWithMeta {
properties: RbxInstanceProperties {
name: "OldName".to_owned(),
class_name: "OldClassName".to_owned(),
properties: hashmap! {
"Foo".to_owned() => RbxValue::Int32 { value: 7 },
"Bar".to_owned() => RbxValue::Int32 { value: 3 },
"Unchanged".to_owned() => RbxValue::Int32 { value: -5 },
},
},
metadata: Default::default(),
});
let mut tree = RojoTree::new(
InstanceSnapshot::new()
.class_name("OldClassName")
.name("OldName")
.property("Foo", 7i32)
.property("Bar", 3i32)
.property("Unchanged", -5i32),
);
let root_id = tree.get_root_id();
@@ -303,13 +282,13 @@ mod test {
changed_class_name: Some("NewClassName".to_owned()),
changed_properties: hashmap! {
// The value of Foo has changed
"Foo".to_owned() => Some(RbxValue::Int32 { value: 8 }),
"Foo".to_owned() => Some(Variant::Int32(8)),
// Bar has been deleted
"Bar".to_owned() => None,
// Baz has been added
"Baz".to_owned() => Some(RbxValue::Int32 { value: 10 }),
"Baz".to_owned() => Some(Variant::Int32(10)),
},
changed_metadata: None,
};
@@ -322,9 +301,9 @@ mod test {
apply_patch_set(&mut tree, patch_set);
let expected_properties = hashmap! {
"Foo".to_owned() => RbxValue::Int32 { value: 8 },
"Baz".to_owned() => RbxValue::Int32 { value: 10 },
"Unchanged".to_owned() => RbxValue::Int32 { value: -5 },
"Foo".to_owned() => Variant::Int32(8),
"Baz".to_owned() => Variant::Int32(10),
"Unchanged".to_owned() => Variant::Int32(-5),
};
let root_instance = tree.get_instance(root_id).unwrap();

View File

@@ -3,14 +3,14 @@
use std::collections::{HashMap, HashSet};
use rbx_dom_weak::{RbxId, RbxValue};
use rbx_dom_weak::types::{Ref, Variant};
use super::{
patch::{PatchAdd, PatchSet, PatchUpdate},
InstanceSnapshot, InstanceWithMeta, RojoTree,
};
pub fn compute_patch_set(snapshot: &InstanceSnapshot, tree: &RojoTree, id: RbxId) -> PatchSet {
pub fn compute_patch_set(snapshot: &InstanceSnapshot, tree: &RojoTree, id: Ref) -> PatchSet {
let mut patch_set = PatchSet::new();
let mut context = ComputePatchContext::default();
@@ -26,17 +26,15 @@ pub fn compute_patch_set(snapshot: &InstanceSnapshot, tree: &RojoTree, id: RbxId
#[derive(Default)]
struct ComputePatchContext {
snapshot_id_to_instance_id: HashMap<RbxId, RbxId>,
snapshot_id_to_instance_id: HashMap<Ref, Ref>,
}
fn rewrite_refs_in_updates(context: &ComputePatchContext, updates: &mut [PatchUpdate]) {
for update in updates {
for property_value in update.changed_properties.values_mut() {
if let Some(RbxValue::Ref { value: Some(id) }) = property_value {
if let Some(&instance_id) = context.snapshot_id_to_instance_id.get(id) {
*property_value = Some(RbxValue::Ref {
value: Some(instance_id),
});
if let Some(Variant::Ref(referent)) = property_value {
if let Some(&instance_ref) = context.snapshot_id_to_instance_id.get(referent) {
*property_value = Some(Variant::Ref(instance_ref));
}
}
}
@@ -51,11 +49,9 @@ fn rewrite_refs_in_additions(context: &ComputePatchContext, additions: &mut [Pat
fn rewrite_refs_in_snapshot(context: &ComputePatchContext, snapshot: &mut InstanceSnapshot) {
for property_value in snapshot.properties.values_mut() {
if let RbxValue::Ref { value: Some(id) } = property_value {
if let Some(&instance_id) = context.snapshot_id_to_instance_id.get(id) {
*property_value = RbxValue::Ref {
value: Some(instance_id),
};
if let Variant::Ref(referent) = property_value {
if let Some(&instance_referent) = context.snapshot_id_to_instance_id.get(referent) {
*property_value = Variant::Ref(instance_referent);
}
}
}
@@ -69,7 +65,7 @@ fn compute_patch_set_internal(
context: &mut ComputePatchContext,
snapshot: &InstanceSnapshot,
tree: &RojoTree,
id: RbxId,
id: Ref,
patch_set: &mut PatchSet,
) {
if let Some(snapshot_id) = snapshot.snapshot_id {
@@ -154,7 +150,7 @@ fn compute_children_patches(
context: &mut ComputePatchContext,
snapshot: &InstanceSnapshot,
tree: &RojoTree,
id: RbxId,
id: Ref,
patch_set: &mut PatchSet,
) {
let instance = tree
@@ -224,9 +220,6 @@ mod test {
use std::borrow::Cow;
use maplit::hashmap;
use rbx_dom_weak::RbxInstanceProperties;
use super::super::InstancePropertiesWithMeta;
/// This test makes sure that rewriting refs in instance update patches to
/// instances that already exists works. We should be able to correlate the
@@ -234,26 +227,17 @@ mod test {
/// value before returning from compute_patch_set.
#[test]
fn rewrite_ref_existing_instance_update() {
let tree = RojoTree::new(InstancePropertiesWithMeta {
properties: RbxInstanceProperties {
name: "foo".to_owned(),
class_name: "foo".to_owned(),
properties: HashMap::new(),
},
metadata: Default::default(),
});
let tree = RojoTree::new(InstanceSnapshot::new().name("foo").class_name("foo"));
let root_id = tree.get_root_id();
// This snapshot should be identical to the existing tree except for the
// addition of a prop named Self, which is a self-referential Ref.
let snapshot_id = RbxId::new();
let snapshot_id = Ref::new();
let snapshot = InstanceSnapshot {
snapshot_id: Some(snapshot_id),
properties: hashmap! {
"Self".to_owned() => RbxValue::Ref {
value: Some(snapshot_id),
}
"Self".to_owned() => Variant::Ref(snapshot_id),
},
metadata: Default::default(),
@@ -270,9 +254,7 @@ mod test {
changed_name: None,
changed_class_name: None,
changed_properties: hashmap! {
"Self".to_owned() => Some(RbxValue::Ref {
value: Some(root_id),
}),
"Self".to_owned() => Some(Variant::Ref(root_id)),
},
changed_metadata: None,
}],
@@ -288,26 +270,17 @@ mod test {
/// one.
#[test]
fn rewrite_ref_existing_instance_addition() {
let tree = RojoTree::new(InstancePropertiesWithMeta {
properties: RbxInstanceProperties {
name: "foo".to_owned(),
class_name: "foo".to_owned(),
properties: HashMap::new(),
},
metadata: Default::default(),
});
let tree = RojoTree::new(InstanceSnapshot::new().name("foo").class_name("foo"));
let root_id = tree.get_root_id();
// This patch describes the existing instance with a new child added.
let snapshot_id = RbxId::new();
let snapshot_id = Ref::new();
let snapshot = InstanceSnapshot {
snapshot_id: Some(snapshot_id),
children: vec![InstanceSnapshot {
properties: hashmap! {
"Self".to_owned() => RbxValue::Ref {
value: Some(snapshot_id),
},
"Self".to_owned() => Variant::Ref(snapshot_id),
},
snapshot_id: None,
@@ -332,9 +305,7 @@ mod test {
snapshot_id: None,
metadata: Default::default(),
properties: hashmap! {
"Self".to_owned() => RbxValue::Ref {
value: Some(root_id),
},
"Self".to_owned() => Variant::Ref(root_id),
},
name: Cow::Borrowed("child"),
class_name: Cow::Borrowed("child"),

View File

@@ -1,11 +1,10 @@
use insta::assert_yaml_snapshot;
use maplit::hashmap;
use rbx_dom_weak::{RbxInstanceProperties, RbxValue};
use rojo_insta_ext::RedactionMap;
use crate::{
snapshot::{apply_patch_set, InstancePropertiesWithMeta, PatchSet, PatchUpdate, RojoTree},
snapshot::{apply_patch_set, InstanceSnapshot, PatchSet, PatchUpdate, RojoTree},
tree_view::{intern_tree, view_tree},
};
@@ -49,9 +48,7 @@ fn add_property() {
changed_name: None,
changed_class_name: None,
changed_properties: hashmap! {
"Foo".to_owned() => Some(RbxValue::String {
value: "Value of Foo".to_owned(),
}),
"Foo".to_owned() => Some("Value of Foo".into()),
},
changed_metadata: None,
}],
@@ -78,12 +75,9 @@ fn remove_property() {
let root_id = tree.get_root_id();
let mut root_instance = tree.get_instance_mut(root_id).unwrap();
root_instance.properties_mut().insert(
"Foo".to_owned(),
RbxValue::String {
value: "Should be removed".to_owned(),
},
);
root_instance
.properties_mut()
.insert("Foo".to_owned(), "Should be removed".into());
}
let tree_view = view_tree(&tree, &mut redactions);
@@ -112,12 +106,5 @@ fn remove_property() {
}
fn empty_tree() -> RojoTree {
RojoTree::new(InstancePropertiesWithMeta {
properties: RbxInstanceProperties {
name: "ROOT".to_owned(),
class_name: "ROOT".to_owned(),
properties: Default::default(),
},
metadata: Default::default(),
})
RojoTree::new(InstanceSnapshot::new().name("ROOT").class_name("ROOT"))
}

View File

@@ -2,11 +2,10 @@ use std::borrow::Cow;
use insta::assert_yaml_snapshot;
use maplit::hashmap;
use rbx_dom_weak::{RbxInstanceProperties, RbxValue};
use rojo_insta_ext::RedactionMap;
use crate::snapshot::{compute_patch_set, InstancePropertiesWithMeta, InstanceSnapshot, RojoTree};
use crate::snapshot::{compute_patch_set, InstanceSnapshot, RojoTree};
#[test]
fn set_name_and_class_name() {
@@ -43,9 +42,7 @@ fn set_property() {
name: Cow::Borrowed("ROOT"),
class_name: Cow::Borrowed("ROOT"),
properties: hashmap! {
"PropertyName".to_owned() => RbxValue::String {
value: "Hello, world!".to_owned(),
},
"PropertyName".to_owned() => "Hello, world!".into(),
},
children: Vec::new(),
};
@@ -68,9 +65,7 @@ fn remove_property() {
let mut root_instance = tree.get_instance_mut(root_id).unwrap();
root_instance.properties_mut().insert(
"Foo".to_owned(),
RbxValue::String {
value: "This should be removed by the patch.".to_owned(),
},
"This should be removed by the patch.".into(),
);
}
@@ -128,15 +123,8 @@ fn remove_child() {
{
let root_id = tree.get_root_id();
let new_id = tree.insert_instance(
InstancePropertiesWithMeta {
properties: RbxInstanceProperties {
name: "Should not appear in snapshot".to_owned(),
class_name: "Folder".to_owned(),
properties: Default::default(),
},
metadata: Default::default(),
},
root_id,
InstanceSnapshot::new().name("Should not appear in snapshot"),
);
redactions.intern(new_id);
@@ -158,12 +146,5 @@ fn remove_child() {
}
fn empty_tree() -> RojoTree {
RojoTree::new(InstancePropertiesWithMeta {
properties: RbxInstanceProperties {
name: "ROOT".to_owned(),
class_name: "ROOT".to_owned(),
properties: Default::default(),
},
metadata: Default::default(),
})
RojoTree::new(InstanceSnapshot::new().name("ROOT").class_name("ROOT"))
}

View File

@@ -1,26 +1,29 @@
use std::{
collections::HashMap,
collections::{HashMap, VecDeque},
path::{Path, PathBuf},
};
use rbx_dom_weak::{Descendants, RbxId, RbxInstance, RbxInstanceProperties, RbxTree, RbxValue};
use rbx_dom_weak::{
types::{Ref, Variant},
Instance, InstanceBuilder, WeakDom,
};
use crate::multimap::MultiMap;
use super::InstanceMetadata;
use super::{InstanceMetadata, InstanceSnapshot};
/// An expanded variant of rbx_dom_weak's `RbxTree` that tracks additional
/// An expanded variant of rbx_dom_weak's `WeakDom` that tracks additional
/// metadata per instance that's Rojo-specific.
///
/// This tree is also optimized for doing fast incremental updates and patches.
#[derive(Debug)]
pub struct RojoTree {
/// Contains the instances without their Rojo-specific metadata.
inner: RbxTree,
inner: WeakDom,
/// Metadata associated with each instance that is kept up-to-date with the
/// set of actual instances.
metadata_map: HashMap<RbxId, InstanceMetadata>,
metadata_map: HashMap<Ref, InstanceMetadata>,
/// A multimap from source paths to all of the root instances that were
/// constructed from that path.
@@ -29,31 +32,42 @@ pub struct RojoTree {
/// value portion of the map is also a set in order to support the same path
/// appearing multiple times in the same Rojo project. This is sometimes
/// called "path aliasing" in various Rojo documentation.
path_to_ids: MultiMap<PathBuf, RbxId>,
path_to_ids: MultiMap<PathBuf, Ref>,
}
impl RojoTree {
pub fn new(root: InstancePropertiesWithMeta) -> RojoTree {
pub fn new(snapshot: InstanceSnapshot) -> RojoTree {
let root_builder = InstanceBuilder::new(snapshot.class_name.to_owned())
.with_name(snapshot.name.to_owned())
.with_properties(snapshot.properties);
let mut tree = RojoTree {
inner: RbxTree::new(root.properties),
inner: WeakDom::new(root_builder),
metadata_map: HashMap::new(),
path_to_ids: MultiMap::new(),
};
tree.insert_metadata(tree.inner.get_root_id(), root.metadata);
let root_ref = tree.inner.root_ref();
tree.insert_metadata(root_ref, snapshot.metadata);
for child in snapshot.children {
tree.insert_instance(root_ref, child);
}
tree
}
pub fn inner(&self) -> &RbxTree {
pub fn inner(&self) -> &WeakDom {
&self.inner
}
pub fn get_root_id(&self) -> RbxId {
self.inner.get_root_id()
pub fn get_root_id(&self) -> Ref {
self.inner.root_ref()
}
pub fn get_instance(&self, id: RbxId) -> Option<InstanceWithMeta> {
if let Some(instance) = self.inner.get_instance(id) {
pub fn get_instance(&self, id: Ref) -> Option<InstanceWithMeta> {
if let Some(instance) = self.inner.get_by_ref(id) {
let metadata = self.metadata_map.get(&id).unwrap();
Some(InstanceWithMeta { instance, metadata })
@@ -62,8 +76,8 @@ impl RojoTree {
}
}
pub fn get_instance_mut(&mut self, id: RbxId) -> Option<InstanceWithMetaMut> {
if let Some(instance) = self.inner.get_instance_mut(id) {
pub fn get_instance_mut(&mut self, id: Ref) -> Option<InstanceWithMetaMut> {
if let Some(instance) = self.inner.get_by_ref_mut(id) {
let metadata = self.metadata_map.get_mut(&id).unwrap();
Some(InstanceWithMetaMut { instance, metadata })
@@ -72,38 +86,38 @@ impl RojoTree {
}
}
pub fn insert_instance(
&mut self,
properties: InstancePropertiesWithMeta,
parent_id: RbxId,
) -> RbxId {
let id = self.inner.insert_instance(properties.properties, parent_id);
self.insert_metadata(id, properties.metadata);
id
pub fn insert_instance(&mut self, parent_ref: Ref, snapshot: InstanceSnapshot) -> Ref {
let builder = InstanceBuilder::new(snapshot.class_name.to_owned())
.with_name(snapshot.name.to_owned())
.with_properties(snapshot.properties);
let referent = self.inner.insert(parent_ref, builder);
self.insert_metadata(referent, snapshot.metadata);
for child in snapshot.children {
self.insert_instance(referent, child);
}
referent
}
pub fn remove_instance(&mut self, id: RbxId) -> Option<RojoTree> {
if let Some(inner) = self.inner.remove_instance(id) {
let mut metadata_map = HashMap::new();
let mut path_to_ids = MultiMap::new();
pub fn remove(&mut self, id: Ref) {
let mut to_move = VecDeque::new();
to_move.push_back(id);
self.move_metadata(id, &mut metadata_map, &mut path_to_ids);
for instance in inner.descendants(id) {
self.move_metadata(instance.get_id(), &mut metadata_map, &mut path_to_ids);
while let Some(id) = to_move.pop_front() {
self.remove_metadata(id);
if let Some(instance) = self.inner.get_by_ref(id) {
to_move.extend(instance.children().iter().copied());
}
Some(RojoTree {
inner,
metadata_map,
path_to_ids,
})
} else {
None
}
self.inner.destroy(id);
}
/// Replaces the metadata associated with the given instance ID.
pub fn update_metadata(&mut self, id: RbxId, metadata: InstanceMetadata) {
pub fn update_metadata(&mut self, id: Ref, metadata: InstanceMetadata) {
use std::collections::hash_map::Entry;
match self.metadata_map.entry(id) {
@@ -131,22 +145,22 @@ impl RojoTree {
}
}
pub fn descendants(&self, id: RbxId) -> RojoDescendants<'_> {
RojoDescendants {
inner: self.inner.descendants(id),
tree: self,
}
pub fn descendants(&self, id: Ref) -> RojoDescendants<'_> {
let mut queue = VecDeque::new();
queue.push_back(id);
RojoDescendants { queue, tree: self }
}
pub fn get_ids_at_path(&self, path: &Path) -> &[RbxId] {
pub fn get_ids_at_path(&self, path: &Path) -> &[Ref] {
self.path_to_ids.get(path)
}
pub fn get_metadata(&self, id: RbxId) -> Option<&InstanceMetadata> {
pub fn get_metadata(&self, id: Ref) -> Option<&InstanceMetadata> {
self.metadata_map.get(&id)
}
fn insert_metadata(&mut self, id: RbxId, metadata: InstanceMetadata) {
fn insert_metadata(&mut self, id: Ref, metadata: InstanceMetadata) {
for path in &metadata.relevant_paths {
self.path_to_ids.insert(path.clone(), id);
}
@@ -156,25 +170,17 @@ impl RojoTree {
/// Moves the Rojo metadata from the instance with the given ID from this
/// tree into some loose maps.
fn move_metadata(
&mut self,
id: RbxId,
metadata_map: &mut HashMap<RbxId, InstanceMetadata>,
path_to_ids: &mut MultiMap<PathBuf, RbxId>,
) {
fn remove_metadata(&mut self, id: Ref) {
let metadata = self.metadata_map.remove(&id).unwrap();
for path in &metadata.relevant_paths {
self.path_to_ids.remove(path, id);
path_to_ids.insert(path.clone(), id);
}
metadata_map.insert(id, metadata);
}
}
pub struct RojoDescendants<'a> {
inner: Descendants<'a>,
queue: VecDeque<Ref>,
tree: &'a RojoTree,
}
@@ -182,50 +188,43 @@ impl<'a> Iterator for RojoDescendants<'a> {
type Item = InstanceWithMeta<'a>;
fn next(&mut self) -> Option<Self::Item> {
let instance = self.inner.next()?;
let id = self.queue.pop_front()?;
let instance = self
.tree
.inner
.get_by_ref(id)
.expect("Instance did not exist");
let metadata = self
.tree
.get_metadata(instance.get_id())
.get_metadata(instance.referent())
.expect("Metadata did not exist for instance");
self.queue.extend(instance.children().iter().copied());
Some(InstanceWithMeta { instance, metadata })
}
}
/// RojoTree's equivalent of `RbxInstanceProperties`.
#[derive(Debug, Clone)]
pub struct InstancePropertiesWithMeta {
pub properties: RbxInstanceProperties,
pub metadata: InstanceMetadata,
}
impl InstancePropertiesWithMeta {
pub fn new(properties: RbxInstanceProperties, metadata: InstanceMetadata) -> Self {
InstancePropertiesWithMeta {
properties,
metadata,
}
}
}
/// RojoTree's equivalent of `&'a RbxInstance`.
/// RojoTree's equivalent of `&'a Instance`.
///
/// This has to be a value type for RojoTree because the instance and metadata
/// are stored in different places. The mutable equivalent is
/// `InstanceWithMetaMut`.
#[derive(Debug, Clone, Copy)]
pub struct InstanceWithMeta<'a> {
instance: &'a RbxInstance,
instance: &'a Instance,
metadata: &'a InstanceMetadata,
}
impl<'a> InstanceWithMeta<'a> {
pub fn id(&self) -> RbxId {
self.instance.get_id()
pub fn id(&self) -> Ref {
self.instance.referent()
}
pub fn parent(&self) -> Option<RbxId> {
self.instance.get_parent_id()
pub fn parent(&self) -> Ref {
self.instance.parent()
}
pub fn name(&self) -> &'a str {
@@ -233,15 +232,15 @@ impl<'a> InstanceWithMeta<'a> {
}
pub fn class_name(&self) -> &'a str {
&self.instance.class_name
&self.instance.class
}
pub fn properties(&self) -> &'a HashMap<String, RbxValue> {
pub fn properties(&self) -> &'a HashMap<String, Variant> {
&self.instance.properties
}
pub fn children(&self) -> &'a [RbxId] {
self.instance.get_children_ids()
pub fn children(&self) -> &'a [Ref] {
self.instance.children()
}
pub fn metadata(&self) -> &'a InstanceMetadata {
@@ -249,20 +248,20 @@ impl<'a> InstanceWithMeta<'a> {
}
}
/// RojoTree's equivalent of `&'a mut RbxInstance`.
/// RojoTree's equivalent of `&'a mut Instance`.
///
/// This has to be a value type for RojoTree because the instance and metadata
/// are stored in different places. The immutable equivalent is
/// `InstanceWithMeta`.
#[derive(Debug)]
pub struct InstanceWithMetaMut<'a> {
instance: &'a mut RbxInstance,
instance: &'a mut Instance,
metadata: &'a mut InstanceMetadata,
}
impl InstanceWithMetaMut<'_> {
pub fn id(&self) -> RbxId {
self.instance.get_id()
pub fn id(&self) -> Ref {
self.instance.referent()
}
pub fn name(&self) -> &str {
@@ -274,23 +273,23 @@ impl InstanceWithMetaMut<'_> {
}
pub fn class_name(&self) -> &str {
&self.instance.class_name
&self.instance.class
}
pub fn class_name_mut(&mut self) -> &mut String {
&mut self.instance.class_name
&mut self.instance.class
}
pub fn properties(&self) -> &HashMap<String, RbxValue> {
pub fn properties(&self) -> &HashMap<String, Variant> {
&self.instance.properties
}
pub fn properties_mut(&mut self) -> &mut HashMap<String, RbxValue> {
pub fn properties_mut(&mut self) -> &mut HashMap<String, Variant> {
&mut self.instance.properties
}
pub fn children(&self) -> &[RbxId] {
self.instance.get_children_ids()
pub fn children(&self) -> &[Ref] {
self.instance.children()
}
pub fn metadata(&self) -> &InstanceMetadata {

View File

@@ -3,7 +3,6 @@ use std::{collections::BTreeMap, path::Path};
use anyhow::Context;
use maplit::hashmap;
use memofs::{IoResultExt, Vfs};
use rbx_dom_weak::RbxValue;
use serde::Serialize;
use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot};
@@ -30,9 +29,7 @@ pub fn snapshot_csv(
.name(instance_name)
.class_name("LocalizationTable")
.properties(hashmap! {
"Contents".to_owned() => RbxValue::String {
value: table_contents,
},
"Contents".to_owned() => table_contents.into(),
})
.metadata(
InstanceMetadata::new()
@@ -41,8 +38,8 @@ pub fn snapshot_csv(
);
if let Some(meta_contents) = vfs.read(&meta_path).with_not_found()? {
let mut metadata = AdjacentMetadata::from_slice(&meta_contents, &meta_path)?;
metadata.apply_all(&mut snapshot);
let mut metadata = AdjacentMetadata::from_slice(&meta_contents, meta_path)?;
metadata.apply_all(&mut snapshot)?;
}
Ok(Some(snapshot))

View File

@@ -60,8 +60,8 @@ pub fn snapshot_dir(context: &InstanceContext, vfs: &Vfs, path: &Path) -> Snapsh
);
if let Some(meta_contents) = vfs.read(&meta_path).with_not_found()? {
let mut metadata = DirectoryMetadata::from_slice(&meta_contents, &meta_path)?;
metadata.apply_all(&mut snapshot);
let mut metadata = DirectoryMetadata::from_slice(&meta_contents, meta_path)?;
metadata.apply_all(&mut snapshot)?;
}
Ok(Some(snapshot))

View File

@@ -3,7 +3,6 @@ use std::path::Path;
use anyhow::Context;
use maplit::hashmap;
use memofs::{IoResultExt, Vfs};
use rbx_dom_weak::RbxValue;
use crate::{
lua_ast::{Expression, Statement},
@@ -26,9 +25,7 @@ pub fn snapshot_json(
let as_lua = json_to_lua(value).to_string();
let properties = hashmap! {
"Source".to_owned() => RbxValue::String {
value: as_lua,
},
"Source".to_owned() => as_lua.into(),
};
let meta_path = path.with_file_name(format!("{}.meta.json", instance_name));
@@ -45,8 +42,8 @@ pub fn snapshot_json(
);
if let Some(meta_contents) = vfs.read(&meta_path).with_not_found()? {
let mut metadata = AdjacentMetadata::from_slice(&meta_contents, &meta_path)?;
metadata.apply_all(&mut snapshot);
let mut metadata = AdjacentMetadata::from_slice(&meta_contents, meta_path)?;
metadata.apply_all(&mut snapshot)?;
}
Ok(Some(snapshot))

View File

@@ -2,11 +2,12 @@ use std::{borrow::Cow, collections::HashMap, path::Path};
use anyhow::Context;
use memofs::Vfs;
use rbx_dom_weak::UnresolvedRbxValue;
use rbx_reflection::try_resolve_value;
use serde::Deserialize;
use crate::snapshot::{InstanceContext, InstanceSnapshot};
use crate::{
resolution::UnresolvedValue,
snapshot::{InstanceContext, InstanceSnapshot},
};
use super::middleware::SnapshotInstanceResult;
@@ -20,7 +21,10 @@ pub fn snapshot_json_model(
let instance: JsonModel = serde_json::from_slice(&contents)
.with_context(|| format!("File is not a valid JSON model: {}", path.display()))?;
let mut snapshot = instance.core.into_snapshot(instance_name.to_owned());
let mut snapshot = instance
.core
.into_snapshot(instance_name.to_owned())
.with_context(|| format!("Could not load JSON model: {}", path.display()))?;
snapshot.metadata = snapshot
.metadata
@@ -58,36 +62,32 @@ struct JsonModelCore {
children: Vec<JsonModelInstance>,
#[serde(default = "HashMap::new", skip_serializing_if = "HashMap::is_empty")]
properties: HashMap<String, UnresolvedRbxValue>,
properties: HashMap<String, UnresolvedValue>,
}
impl JsonModelCore {
fn into_snapshot(self, name: String) -> InstanceSnapshot {
fn into_snapshot(self, name: String) -> anyhow::Result<InstanceSnapshot> {
let class_name = self.class_name;
let children = self
.children
.into_iter()
.map(|child| child.core.into_snapshot(child.name))
.collect();
let mut children = Vec::with_capacity(self.children.len());
for child in self.children {
children.push(child.core.into_snapshot(child.name)?);
}
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");
let mut properties = HashMap::with_capacity(self.properties.len());
for (key, unresolved) in self.properties {
let value = unresolved.resolve(&class_name, &key)?;
properties.insert(key, value);
}
InstanceSnapshot {
Ok(InstanceSnapshot {
snapshot_id: None,
metadata: Default::default(),
name: Cow::Owned(name),
class_name: Cow::Owned(class_name),
properties,
children,
}
})
}
}

View File

@@ -3,7 +3,6 @@ use std::{path::Path, str};
use anyhow::Context;
use maplit::hashmap;
use memofs::{IoResultExt, Vfs};
use rbx_dom_weak::RbxValue;
use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot};
@@ -38,9 +37,7 @@ pub fn snapshot_lua(context: &InstanceContext, vfs: &Vfs, path: &Path) -> Snapsh
.name(instance_name)
.class_name(class_name)
.properties(hashmap! {
"Source".to_owned() => RbxValue::String {
value: contents_str,
},
"Source".to_owned() => contents_str.into(),
})
.metadata(
InstanceMetadata::new()
@@ -50,8 +47,8 @@ pub fn snapshot_lua(context: &InstanceContext, vfs: &Vfs, path: &Path) -> Snapsh
);
if let Some(meta_contents) = vfs.read(&meta_path).with_not_found()? {
let mut metadata = AdjacentMetadata::from_slice(&meta_contents, &meta_path)?;
metadata.apply_all(&mut snapshot);
let mut metadata = AdjacentMetadata::from_slice(&meta_contents, meta_path)?;
metadata.apply_all(&mut snapshot)?;
}
Ok(Some(snapshot))

View File

@@ -1,11 +1,9 @@
use std::{borrow::Cow, collections::HashMap, path::Path};
use std::{borrow::Cow, collections::HashMap, path::PathBuf};
use anyhow::Context;
use rbx_dom_weak::UnresolvedRbxValue;
use rbx_reflection::try_resolve_value;
use anyhow::{format_err, Context};
use serde::{Deserialize, Serialize};
use crate::snapshot::InstanceSnapshot;
use crate::{resolution::UnresolvedValue, snapshot::InstanceSnapshot};
/// Represents metadata in a sibling file with the same basename.
///
@@ -18,17 +16,23 @@ pub struct AdjacentMetadata {
pub ignore_unknown_instances: Option<bool>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub properties: HashMap<String, UnresolvedRbxValue>,
pub properties: HashMap<String, UnresolvedValue>,
#[serde(skip)]
pub path: PathBuf,
}
impl AdjacentMetadata {
pub fn from_slice(slice: &[u8], path: &Path) -> anyhow::Result<Self> {
serde_json::from_slice(slice).with_context(|| {
pub fn from_slice(slice: &[u8], path: PathBuf) -> anyhow::Result<Self> {
let mut meta: Self = serde_json::from_slice(slice).with_context(|| {
format!(
"File contained malformed .meta.json data: {}",
path.display()
)
})
})?;
meta.path = path;
Ok(meta)
}
pub fn apply_ignore_unknown_instances(&mut self, snapshot: &mut InstanceSnapshot) {
@@ -37,23 +41,24 @@ impl AdjacentMetadata {
}
}
pub fn apply_properties(&mut self, snapshot: &mut InstanceSnapshot) {
let class_name = &snapshot.class_name;
pub fn apply_properties(&mut self, snapshot: &mut InstanceSnapshot) -> anyhow::Result<()> {
let path = &self.path;
let source_properties = self.properties.drain().map(|(key, value)| {
try_resolve_value(class_name, &key, &value)
.map(|resolved| (key, resolved))
.expect("TODO: Handle rbx_reflection errors")
});
for (key, unresolved) in self.properties.drain() {
let value = unresolved
.resolve(&snapshot.class_name, &key)
.with_context(|| format!("error applying meta file {}", path.display()))?;
for (key, value) in source_properties {
snapshot.properties.insert(key, value);
}
Ok(())
}
pub fn apply_all(&mut self, snapshot: &mut InstanceSnapshot) {
pub fn apply_all(&mut self, snapshot: &mut InstanceSnapshot) -> anyhow::Result<()> {
self.apply_ignore_unknown_instances(snapshot);
self.apply_properties(snapshot);
self.apply_properties(snapshot)?;
Ok(())
}
// TODO: Add method to allow selectively applying parts of metadata and
@@ -71,37 +76,50 @@ pub struct DirectoryMetadata {
pub ignore_unknown_instances: Option<bool>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub properties: HashMap<String, UnresolvedRbxValue>,
pub properties: HashMap<String, UnresolvedValue>,
#[serde(skip_serializing_if = "Option::is_none")]
pub class_name: Option<String>,
#[serde(skip)]
pub path: PathBuf,
}
impl DirectoryMetadata {
pub fn from_slice(slice: &[u8], path: &Path) -> anyhow::Result<Self> {
serde_json::from_slice(slice).with_context(|| {
pub fn from_slice(slice: &[u8], path: PathBuf) -> anyhow::Result<Self> {
let mut meta: Self = serde_json::from_slice(slice).with_context(|| {
format!(
"File contained malformed init.meta.json data: {}",
path.display()
)
})
})?;
meta.path = path;
Ok(meta)
}
pub fn apply_all(&mut self, snapshot: &mut InstanceSnapshot) {
pub fn apply_all(&mut self, snapshot: &mut InstanceSnapshot) -> anyhow::Result<()> {
self.apply_ignore_unknown_instances(snapshot);
self.apply_class_name(snapshot);
self.apply_properties(snapshot);
self.apply_class_name(snapshot)?;
self.apply_properties(snapshot)?;
Ok(())
}
fn apply_class_name(&mut self, snapshot: &mut InstanceSnapshot) {
fn apply_class_name(&mut self, snapshot: &mut InstanceSnapshot) -> anyhow::Result<()> {
if let Some(class_name) = self.class_name.take() {
if snapshot.class_name != "Folder" {
// TODO: Turn into error type
panic!("className in init.meta.json can only be specified if the affected directory would turn into a Folder instance.");
return Err(format_err!(
"className in init.meta.json can only be specified if the \
affected directory would turn into a Folder instance."
));
}
snapshot.class_name = Cow::Owned(class_name);
}
Ok(())
}
fn apply_ignore_unknown_instances(&mut self, snapshot: &mut InstanceSnapshot) {
@@ -110,17 +128,17 @@ impl DirectoryMetadata {
}
}
fn apply_properties(&mut self, snapshot: &mut InstanceSnapshot) {
let class_name = &snapshot.class_name;
fn apply_properties(&mut self, snapshot: &mut InstanceSnapshot) -> anyhow::Result<()> {
let path = &self.path;
let source_properties = self.properties.drain().map(|(key, value)| {
try_resolve_value(class_name, &key, &value)
.map(|resolved| (key, resolved))
.expect("TODO: Handle rbx_reflection errors")
});
for (key, unresolved) in self.properties.drain() {
let value = unresolved
.resolve(&snapshot.class_name, &key)
.with_context(|| format!("error applying meta file {}", path.display()))?;
for (key, value) in source_properties {
snapshot.properties.insert(key, value);
}
Ok(())
}
}

View File

@@ -2,7 +2,7 @@ use std::{borrow::Cow, collections::HashMap, path::Path};
use anyhow::Context;
use memofs::Vfs;
use rbx_reflection::{get_class_descriptor, try_resolve_value};
use rbx_reflection::ClassTag;
use crate::{
project::{Project, ProjectNode},
@@ -140,9 +140,9 @@ pub fn snapshot_project_node(
// Members of DataModel with names that match known services are
// probably supposed to be those services.
let descriptor = get_class_descriptor(&name)?;
let descriptor = rbx_reflection_database::get().classes.get(&name)?;
if descriptor.is_service() {
if descriptor.tags.contains(&ClassTag::Service) {
return Some(name.clone());
}
} else if parent_class == "StarterPlayer" {
@@ -171,11 +171,18 @@ pub fn snapshot_project_node(
}
}
for (key, value) in &node.properties {
let resolved_value = try_resolve_value(&class_name, key, value)
.expect("TODO: Properly handle value resolution errors");
for (key, unresolved) in &node.properties {
let value = unresolved
.clone()
.resolve(&class_name, key)
.with_context(|| {
format!(
"Unresolvable property in project at path {}",
project_path.display()
)
})?;
properties.insert(key.clone(), resolved_value);
properties.insert(key.clone(), value);
}
// If the user specified $ignoreUnknownInstances, overwrite the existing

View File

@@ -1,8 +1,7 @@
use std::{collections::HashMap, path::Path};
use std::path::Path;
use anyhow::Context;
use memofs::Vfs;
use rbx_dom_weak::{RbxInstanceProperties, RbxTree};
use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot};
@@ -14,18 +13,11 @@ pub fn snapshot_rbxm(
path: &Path,
instance_name: &str,
) -> SnapshotInstanceResult {
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, vfs.read(path)?.as_slice())
let temp_tree = rbx_binary::from_reader_default(vfs.read(path)?.as_slice())
.with_context(|| format!("Malformed rbxm file: {}", path.display()))?;
let root_instance = temp_tree.get_instance(root_id).unwrap();
let children = root_instance.get_children_ids();
let root_instance = temp_tree.root();
let children = root_instance.children();
if children.len() == 1 {
let snapshot = InstanceSnapshot::from_tree(&temp_tree, children[0])

View File

@@ -19,8 +19,8 @@ pub fn snapshot_rbxmx(
let temp_tree = rbx_xml::from_reader(vfs.read(path)?.as_slice(), options)
.with_context(|| format!("Malformed rbxm file: {}", path.display()))?;
let root_instance = temp_tree.get_instance(temp_tree.get_root_id()).unwrap();
let children = root_instance.get_children_ids();
let root_instance = temp_tree.root();
let children = root_instance.children();
if children.len() == 1 {
let snapshot = InstanceSnapshot::from_tree(&temp_tree, children[0])

View File

@@ -14,7 +14,7 @@ name: foo
class_name: IntValue
properties:
Value:
Type: Int32
Type: Int64
Value: 5
children:
- snapshot_id: ~

View File

@@ -3,7 +3,6 @@ use std::{path::Path, str};
use anyhow::Context;
use maplit::hashmap;
use memofs::{IoResultExt, Vfs};
use rbx_dom_weak::RbxValue;
use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot};
@@ -21,9 +20,7 @@ pub fn snapshot_txt(
.to_owned();
let properties = hashmap! {
"Value".to_owned() => RbxValue::String {
value: contents_str,
},
"Value".to_owned() => contents_str.into(),
};
let meta_path = path.with_file_name(format!("{}.meta.json", instance_name));
@@ -40,8 +37,8 @@ pub fn snapshot_txt(
);
if let Some(meta_contents) = vfs.read(&meta_path).with_not_found()? {
let mut metadata = AdjacentMetadata::from_slice(&meta_contents, &meta_path)?;
metadata.apply_all(&mut snapshot);
let mut metadata = AdjacentMetadata::from_slice(&meta_contents, meta_path)?;
metadata.apply_all(&mut snapshot)?;
}
Ok(Some(snapshot))

View File

@@ -1,6 +1,6 @@
use std::collections::HashMap;
use rbx_dom_weak::{RbxId, RbxValue};
use rbx_dom_weak::types::{Ref, Variant};
use rojo_insta_ext::RedactionMap;
use serde::Serialize;
@@ -29,15 +29,15 @@ pub fn intern_tree(tree: &RojoTree, redactions: &mut RedactionMap) {
/// Copy of data from RojoTree in the right shape to have useful snapshots.
#[derive(Debug, Serialize)]
struct InstanceView {
id: RbxId,
id: Ref,
name: String,
class_name: String,
properties: HashMap<String, RbxValue>,
properties: HashMap<String, Variant>,
metadata: InstanceMetadata,
children: Vec<InstanceView>,
}
fn extract_instance_view(tree: &RojoTree, id: RbxId) -> InstanceView {
fn extract_instance_view(tree: &RojoTree, id: Ref) -> InstanceView {
let instance = tree.get_instance(id).unwrap();
InstanceView {

View File

@@ -1,12 +1,12 @@
//! Defines Rojo's HTTP API, all under /api. These endpoints generally return
//! JSON.
use std::{collections::HashMap, fs, path::PathBuf, sync::Arc};
use std::{collections::HashMap, fs, path::PathBuf, str::FromStr, sync::Arc};
use futures::{Future, Stream};
use hyper::{service::Service, Body, Method, Request, StatusCode};
use rbx_dom_weak::RbxId;
use rbx_dom_weak::types::Ref;
use crate::{
serve_session::ServeSession,
@@ -200,11 +200,11 @@ impl ApiService {
fn handle_api_read(&self, request: Request<Body>) -> <Self as Service>::Future {
let argument = &request.uri().path()["/api/read/".len()..];
let requested_ids: Option<Vec<RbxId>> = argument.split(',').map(RbxId::parse_str).collect();
let requested_ids: Result<Vec<Ref>, _> = argument.split(',').map(Ref::from_str).collect();
let requested_ids = match requested_ids {
Some(ids) => ids,
None => {
Ok(ids) => ids,
Err(_) => {
return json(
ErrorResponse::bad_request("Malformed ID list"),
StatusCode::BAD_REQUEST,
@@ -239,9 +239,9 @@ impl ApiService {
/// Open a script with the given ID in the user's default text editor.
fn handle_api_open(&self, request: Request<Body>) -> <Self as Service>::Future {
let argument = &request.uri().path()["/api/open/".len()..];
let requested_id = match RbxId::parse_str(argument) {
Some(id) => id,
None => {
let requested_id = match Ref::from_str(argument) {
Ok(id) => id,
Err(_) => {
return json(
ErrorResponse::bad_request("Invalid instance ID"),
StatusCode::BAD_REQUEST,

View File

@@ -5,7 +5,7 @@ use std::{
collections::{HashMap, HashSet},
};
use rbx_dom_weak::{RbxId, RbxValue};
use rbx_dom_weak::types::{Ref, Variant};
use serde::{Deserialize, Serialize};
use crate::{
@@ -23,22 +23,22 @@ pub const PROTOCOL_VERSION: u64 = 3;
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SubscribeMessage<'a> {
pub removed: Vec<RbxId>,
pub added: HashMap<RbxId, Instance<'a>>,
pub removed: Vec<Ref>,
pub added: HashMap<Ref, Instance<'a>>,
pub updated: Vec<InstanceUpdate>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InstanceUpdate {
pub id: RbxId,
pub id: Ref,
pub changed_name: Option<String>,
pub changed_class_name: Option<String>,
// TODO: Transform from HashMap<String, Option<_>> to something else, since
// null will get lost when decoding from JSON in some languages.
#[serde(default)]
pub changed_properties: HashMap<String, Option<RbxValue>>,
pub changed_properties: HashMap<String, Option<Variant>>,
pub changed_metadata: Option<InstanceMetadata>,
}
@@ -59,23 +59,36 @@ impl InstanceMetadata {
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct Instance<'a> {
pub id: RbxId,
pub parent: Option<RbxId>,
pub id: Ref,
pub parent: Ref,
pub name: Cow<'a, str>,
pub class_name: Cow<'a, str>,
pub properties: Cow<'a, HashMap<String, RbxValue>>,
pub children: Cow<'a, [RbxId]>,
pub properties: HashMap<String, Cow<'a, Variant>>,
pub children: Cow<'a, [Ref]>,
pub metadata: Option<InstanceMetadata>,
}
impl<'a> Instance<'a> {
pub(crate) fn from_rojo_instance(source: InstanceWithMeta<'_>) -> Instance<'_> {
let properties = source
.properties()
.iter()
.filter_map(|(key, value)| {
// SharedString values can't be serialized via Serde
if matches!(value, Variant::SharedString(_)) {
return None;
}
Some((key.clone(), Cow::Borrowed(value)))
})
.collect();
Instance {
id: source.id(),
parent: source.parent(),
name: Cow::Borrowed(source.name()),
class_name: Cow::Borrowed(source.class_name()),
properties: Cow::Borrowed(source.properties()),
properties,
children: Cow::Borrowed(source.children()),
metadata: Some(InstanceMetadata::from_rojo_metadata(source.metadata())),
}
@@ -91,7 +104,7 @@ pub struct ServerInfoResponse {
pub protocol_version: u64,
pub project_name: String,
pub expected_place_ids: Option<HashSet<u64>>,
pub root_instance_id: RbxId,
pub root_instance_id: Ref,
}
/// Response body from /api/read/{id}
@@ -100,17 +113,17 @@ pub struct ServerInfoResponse {
pub struct ReadResponse<'a> {
pub session_id: SessionId,
pub message_cursor: u32,
pub instances: HashMap<RbxId, Instance<'a>>,
pub instances: HashMap<Ref, Instance<'a>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WriteRequest {
pub session_id: SessionId,
pub removed: Vec<RbxId>,
pub removed: Vec<Ref>,
#[serde(default)]
pub added: HashMap<RbxId, ()>,
pub added: HashMap<Ref, ()>,
pub updated: Vec<InstanceUpdate>,
}

View File

@@ -5,7 +5,7 @@ use std::{borrow::Cow, sync::Arc, time::Duration};
use futures::{future, Future};
use hyper::{header, service::Service, Body, Method, Request, Response, StatusCode};
use maplit::hashmap;
use rbx_dom_weak::{RbxId, RbxValue};
use rbx_dom_weak::types::{Ref, Variant};
use ritz::{html, Fragment, HtmlContent, HtmlSelfClosingTag};
use crate::{
@@ -93,7 +93,7 @@ impl UiService {
.unwrap()
}
fn instance(tree: &RojoTree, id: RbxId) -> HtmlContent<'_> {
fn instance(tree: &RojoTree, id: Ref) -> HtmlContent<'_> {
let instance = tree.get_instance(id).unwrap();
let children_list: Vec<_> = instance
.children()
@@ -126,7 +126,7 @@ impl UiService {
.map(|(key, value)| {
html! {
<div class="instance-property" title={ Self::display_value(value) }>
{ key.clone() } ": " { format!("{:?}", value.get_type()) }
{ key.clone() } ": " { format!("{:?}", value.ty()) }
</div>
}
})
@@ -198,7 +198,7 @@ impl UiService {
html! {
<div class="instance">
<label class="instance-title" for={ format!("instance-{}", id) }>
<label class="instance-title" for={ format!("instance-{:?}", id) }>
{ instance.name().to_owned() }
{ class_name_specifier }
</label>
@@ -209,10 +209,10 @@ impl UiService {
}
}
fn display_value(value: &RbxValue) -> String {
fn display_value(value: &Variant) -> String {
match value {
RbxValue::String { value } => value.clone(),
RbxValue::Bool { value } => value.to_string(),
Variant::String(value) => value.clone(),
Variant::Bool(value) => value.to_string(),
_ => format!("{:?}", value),
}
}
@@ -288,14 +288,14 @@ impl UiService {
struct ExpandableSection<'a> {
title: &'a str,
class_name: &'a str,
id: RbxId,
id: Ref,
expanded: bool,
content: HtmlContent<'a>,
}
impl<'a> ExpandableSection<'a> {
fn render(self) -> HtmlContent<'a> {
let input_id = format!("{}-{}", self.class_name, self.id);
let input_id = format!("{}-{:?}", self.class_name, self.id);
// We need to specify this input manually because Ritz doesn't have
// support for conditional attributes like `checked`.