use std::borrow::Borrow; use anyhow::format_err; use rbx_dom_weak::types::{ Attributes, 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 { 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), Number(f64), Array2([f64; 2]), Array3([f64; 3]), Array4([f64; 4]), Array12([f64; 12]), Attributes(Attributes), } impl AmbiguousValue { pub fn resolve(self, class_name: &str, prop_name: &str) -> anyhow::Result { 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::>(); 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()) } (VariantType::Attributes, AmbiguousValue::Attributes(value)) => 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::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", AmbiguousValue::Attributes(_) => "an object containing attributes", } } } 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)), ); } }