forked from rojo-rbx/rojo
295 lines
9.8 KiB
Rust
295 lines
9.8 KiB
Rust
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)),
|
|
);
|
|
}
|
|
}
|