From 8853aad620f46da426227cd9050c3d2ebf060c00 Mon Sep 17 00:00:00 2001 From: Filip Tibell Date: Fri, 21 Jul 2023 12:10:56 +0200 Subject: [PATCH] Implement reflection database accessible from lua in roblox builtin --- docs/typedefs/Roblox.luau | 165 ++++++++++++++++++++++++++ src/lune/builtins/roblox.rs | 28 +++-- src/roblox/datatypes/extension.rs | 6 +- src/roblox/mod.rs | 1 + src/roblox/reflection/class.rs | 148 +++++++++++++++++++++++ src/roblox/reflection/enums.rs | 67 +++++++++++ src/roblox/reflection/mod.rs | 138 +++++++++++++++++++++ src/roblox/reflection/property.rs | 95 +++++++++++++++ src/roblox/reflection/utils.rs | 56 +++++++++ src/tests.rs | 5 + tests/roblox/reflection/class.luau | 41 +++++++ tests/roblox/reflection/database.luau | 55 +++++++++ tests/roblox/reflection/enums.luau | 32 +++++ tests/roblox/reflection/property.luau | 17 +++ 14 files changed, 842 insertions(+), 12 deletions(-) create mode 100644 src/roblox/reflection/class.rs create mode 100644 src/roblox/reflection/enums.rs create mode 100644 src/roblox/reflection/mod.rs create mode 100644 src/roblox/reflection/property.rs create mode 100644 src/roblox/reflection/utils.rs create mode 100644 tests/roblox/reflection/class.luau create mode 100644 tests/roblox/reflection/database.luau create mode 100644 tests/roblox/reflection/enums.luau create mode 100644 tests/roblox/reflection/property.luau diff --git a/docs/typedefs/Roblox.luau b/docs/typedefs/Roblox.luau index 9a9432f..2801e32 100644 --- a/docs/typedefs/Roblox.luau +++ b/docs/typedefs/Roblox.luau @@ -1,3 +1,135 @@ +export type DatabaseScriptability = "None" | "Custom" | "Read" | "ReadWrite" | "Write" + +export type DatabasePropertyTag = + "Deprecated" + | "Hidden" + | "NotBrowsable" + | "NotReplicated" + | "NotScriptable" + | "ReadOnly" + | "WriteOnly" + +export type DatabaseClassTag = + "Deprecated" + | "NotBrowsable" + | "NotCreatable" + | "NotReplicated" + | "PlayerReplicated" + | "Service" + | "Settings" + | "UserSettings" + +export type DatabaseProperty = { + --[=[ + The name of the property. + ]=] + Name: string, + --[=[ + The datatype of the property. + + For normal datatypes this will be a string such as `string`, `Color3`, ... + + For enums this will be a string formatted as `Enum.EnumName`. + ]=] + Datatype: string, + --[=[ + The scriptability of this property, meaning if it can be written / read at runtime. + + All properties are writable and readable in Lune even if scriptability is not. + ]=] + Scriptability: DatabaseScriptability, + --[=[ + Tags describing the property. + + These include information such as if the property can be replicated to players + at runtime, if the property should be hidden in Roblox Studio, and more. + ]=] + Tags: { DatabasePropertyTag }, +} + +export type DatabaseClass = { + --[=[ + The name of the class. + ]=] + Name: string, + --[=[ + The superclass (parent class) of this class. + + May be nil if no parent class exists. + ]=] + Superclass: string?, + --[=[ + Known properties for this class. + ]=] + Properties: { [string]: DatabaseProperty }, + --[=[ + Default values for properties of this class. + + Note that these default properties use Lune's built-in datatype + userdatas, and that if there is a new datatype that Lune does + not yet know about, it may be missing from this table. + ]=] + DefaultProperties: { [string]: any }, + --[=[ + Tags describing the class. + + These include information such as if the class can be replicated + to players at runtime, and top-level class categories. + ]=] + Tags: { DatabaseClassTag }, +} + +export type DatabaseEnum = { + --[=[ + The name of this enum, for example `PartType` or `UserInputState`. + ]=] + Name: string, + --[=[ + Members of this enum. + + Note that this is a direct map of name -> enum values, + and does not actually use the EnumItem datatype itself. + ]=] + Items: { [string]: number }, +} + +export type Database = { + --[=[ + The current version of the reflection database. + + This will follow the format `x.y.z.w`, which most commonly looks something like `0.567.0.123456789` + ]=] + Version: string, + --[=[ + Retrieves a list of all currently known class names. + ]=] + GetClassNames: (self: Database) -> { string }, + --[=[ + Retrieves a list of all currently known enum names. + ]=] + GetEnumNames: (self: Database) -> { string }, + --[=[ + Gets a class with the exact given name, if one exists. + ]=] + GetClass: (self: Database, name: string) -> DatabaseClass?, + --[=[ + Gets an enum with the exact given name, if one exists. + ]=] + GetEnum: (self: Database, name: string) -> DatabaseEnum?, + --[=[ + Finds a class with the given name. + + This will use case-insensitive matching and ignore leading and trailing whitespace. + ]=] + FindClass: (self: Database, name: string) -> DatabaseClass?, + --[=[ + Finds an enum with the given name. + + This will use case-insensitive matching and ignore leading and trailing whitespace. + ]=] + FindEnum: (self: Database, name: string) -> DatabaseEnum?, +} + type InstanceProperties = { Parent: Instance?, ClassName: string, @@ -219,6 +351,39 @@ return { getAuthCookie = function(raw: boolean?): string? return nil :: any end, + --[=[ + @within Roblox + @must_use + + Gets the bundled reflection database. + + This database contains information about Roblox enums, classes, and their properties. + + ### Example usage + + ```lua + local roblox = require("@lune/roblox") + + local db = roblox.getReflectionDatabase() + + print("There are", #db:GetClassNames(), "classes in the reflection database") + + print("All base instance properties:") + + local class = db:GetClass("Instance") + for name, prop in class.Properties do + print(string.format( + "- %s with datatype %s and default value %s", + prop.Name, + prop.Datatype, + tostring(class.DefaultProperties[prop.Name]) + )) + end + ``` + ]=] + getReflectionDatabase = function(): Database + return nil :: any + end, -- TODO: Make typedefs for all of the datatypes as well... Instance = (nil :: any) :: { new: ((className: "DataModel") -> DataModel) & ((className: string) -> Instance), diff --git a/src/lune/builtins/roblox.rs b/src/lune/builtins/roblox.rs index 3f8e6dd..7c19386 100644 --- a/src/lune/builtins/roblox.rs +++ b/src/lune/builtins/roblox.rs @@ -1,14 +1,19 @@ use mlua::prelude::*; +use once_cell::sync::OnceCell; use crate::roblox::{ self, document::{Document, DocumentError, DocumentFormat, DocumentKind}, instance::Instance, + reflection::Database as ReflectionDatabase, }; + use tokio::task; use crate::lune::lua::table::TableBuilder; +static REFLECTION_DATABASE: OnceCell = OnceCell::new(); + pub fn create(lua: &'static Lua) -> LuaResult { let mut roblox_constants = Vec::new(); let roblox_module = roblox::module(lua)?; @@ -21,7 +26,8 @@ pub fn create(lua: &'static Lua) -> LuaResult { .with_async_function("deserializeModel", deserialize_model)? .with_async_function("serializePlace", serialize_place)? .with_async_function("serializeModel", serialize_model)? - .with_async_function("getAuthCookie", get_auth_cookie)? + .with_function("getAuthCookie", get_auth_cookie)? + .with_function("getReflectionDatabase", get_reflection_database)? .build_readonly() } @@ -85,14 +91,14 @@ async fn serialize_model<'lua>( lua.create_string(bytes) } -async fn get_auth_cookie(_: &Lua, raw: Option) -> LuaResult> { - task::spawn_blocking(move || { - if matches!(raw, Some(true)) { - Ok(rbx_cookie::get_value()) - } else { - Ok(rbx_cookie::get()) - } - }) - .await - .map_err(LuaError::external)? +fn get_auth_cookie(_: &Lua, raw: Option) -> LuaResult> { + if matches!(raw, Some(true)) { + Ok(rbx_cookie::get_value()) + } else { + Ok(rbx_cookie::get()) + } +} + +fn get_reflection_database(_: &Lua, _: ()) -> LuaResult { + Ok(*REFLECTION_DATABASE.get_or_init(ReflectionDatabase::new)) } diff --git a/src/roblox/datatypes/extension.rs b/src/roblox/datatypes/extension.rs index b384d5d..1aa0d20 100644 --- a/src/roblox/datatypes/extension.rs +++ b/src/roblox/datatypes/extension.rs @@ -4,7 +4,7 @@ use crate::roblox::instance::Instance; use super::*; -pub(super) trait DomValueExt { +pub(crate) trait DomValueExt { fn variant_name(&self) -> Option<&'static str>; } @@ -12,6 +12,7 @@ impl DomValueExt for DomType { fn variant_name(&self) -> Option<&'static str> { use DomType::*; Some(match self { + Attributes => "Attributes", Axes => "Axes", BinaryString => "BinaryString", Bool => "Bool", @@ -25,6 +26,7 @@ impl DomValueExt for DomType { Faces => "Faces", Float32 => "Float32", Float64 => "Float64", + Font => "Font", Int32 => "Int32", Int64 => "Int64", NumberRange => "NumberRange", @@ -37,8 +39,10 @@ impl DomValueExt for DomType { Region3int16 => "Region3int16", SharedString => "SharedString", String => "String", + Tags => "Tags", UDim => "UDim", UDim2 => "UDim2", + UniqueId => "UniqueId", Vector2 => "Vector2", Vector2int16 => "Vector2int16", Vector3 => "Vector3", diff --git a/src/roblox/mod.rs b/src/roblox/mod.rs index 4cfd689..b640d39 100644 --- a/src/roblox/mod.rs +++ b/src/roblox/mod.rs @@ -5,6 +5,7 @@ use crate::roblox::instance::Instance; pub mod datatypes; pub mod document; pub mod instance; +pub mod reflection; pub(crate) mod shared; diff --git a/src/roblox/reflection/class.rs b/src/roblox/reflection/class.rs new file mode 100644 index 0000000..13ce84a --- /dev/null +++ b/src/roblox/reflection/class.rs @@ -0,0 +1,148 @@ +use core::fmt; +use std::collections::HashMap; + +use mlua::prelude::*; + +use rbx_dom_weak::types::Variant as DomVariant; +use rbx_reflection::{ClassDescriptor, DataType}; + +use super::{property::DatabaseProperty, utils::*}; +use crate::roblox::datatypes::{ + conversion::DomValueToLua, types::EnumItem, userdata_impl_eq, userdata_impl_to_string, +}; + +type DbClass = &'static ClassDescriptor<'static>; + +/** + A wrapper for [`rbx_reflection::ClassDescriptor`] that + also provides access to the class descriptor from lua. +*/ +#[derive(Debug, Clone, Copy)] +pub struct DatabaseClass(DbClass); + +impl DatabaseClass { + pub(crate) fn new(inner: DbClass) -> Self { + Self(inner) + } + + /** + Get the name of this class. + */ + pub fn get_name(&self) -> String { + self.0.name.to_string() + } + + /** + Get the superclass (parent class) of this class. + + May be `None` if no parent class exists. + */ + pub fn get_superclass(&self) -> Option { + let sup = self.0.superclass.as_ref()?; + Some(sup.to_string()) + } + + /** + Get all known properties for this class. + */ + pub fn get_properties(&self) -> HashMap { + self.0 + .properties + .iter() + .map(|(name, prop)| (name.to_string(), DatabaseProperty::new(self.0, prop))) + .collect() + } + + /** + Get all default values for properties of this class. + */ + pub fn get_defaults(&self) -> HashMap { + self.0 + .default_properties + .iter() + .map(|(name, prop)| (name.to_string(), prop.clone())) + .collect() + } + + /** + Get all tags describing the class. + + These include information such as if the class can be replicated + to players at runtime, and top-level class categories. + */ + pub fn get_tags_str(&self) -> Vec<&'static str> { + self.0.tags.iter().map(class_tag_to_str).collect::>() + } +} + +impl LuaUserData for DatabaseClass { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("Name", |_, this| Ok(this.get_name())); + fields.add_field_method_get("Superclass", |_, this| Ok(this.get_superclass())); + fields.add_field_method_get("Properties", |_, this| Ok(this.get_properties())); + fields.add_field_method_get("DefaultProperties", |lua, this| { + let defaults = this.get_defaults(); + let mut map = HashMap::with_capacity(defaults.len()); + for (name, prop) in defaults { + let value = if let DomVariant::Enum(enum_value) = prop { + make_enum_value(this.0, &name, enum_value.to_u32()) + .and_then(|e| e.into_lua(lua)) + } else { + LuaValue::dom_value_to_lua(lua, &prop).into_lua_err() + }; + if let Ok(value) = value { + map.insert(name, value); + } + } + Ok(map) + }); + fields.add_field_method_get("Tags", |_, this| Ok(this.get_tags_str())); + } + + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq); + methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string); + } +} + +impl PartialEq for DatabaseClass { + fn eq(&self, other: &Self) -> bool { + self.0.name == other.0.name + } +} + +impl fmt::Display for DatabaseClass { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "ReflectionDatabaseClass({})", self.0.name) + } +} + +fn find_enum_name(inner: DbClass, name: impl AsRef) -> Option { + inner.properties.iter().find_map(|(prop_name, prop_info)| { + if prop_name == name.as_ref() { + if let DataType::Enum(enum_name) = &prop_info.data_type { + Some(enum_name.to_string()) + } else { + None + } + } else { + None + } + }) +} + +fn make_enum_value(inner: DbClass, name: impl AsRef, value: u32) -> LuaResult { + let name = name.as_ref(); + let enum_name = find_enum_name(inner, name).ok_or_else(|| { + LuaError::RuntimeError(format!( + "Failed to get default property '{}' - No enum descriptor was found", + name + )) + })?; + EnumItem::from_enum_name_and_value(&enum_name, value).ok_or_else(|| { + LuaError::RuntimeError(format!( + "Failed to get default property '{}' - Enum.{} does not contain numeric value {}", + name, enum_name, value + )) + }) +} diff --git a/src/roblox/reflection/enums.rs b/src/roblox/reflection/enums.rs new file mode 100644 index 0000000..2ed29a0 --- /dev/null +++ b/src/roblox/reflection/enums.rs @@ -0,0 +1,67 @@ +use std::{collections::HashMap, fmt}; + +use mlua::prelude::*; + +use rbx_reflection::EnumDescriptor; + +use crate::roblox::datatypes::{userdata_impl_eq, userdata_impl_to_string}; + +type DbEnum = &'static EnumDescriptor<'static>; + +/** + A wrapper for [`rbx_reflection::EnumDescriptor`] that + also provides access to the class descriptor from lua. +*/ +#[derive(Debug, Clone, Copy)] +pub struct DatabaseEnum(DbEnum); + +impl DatabaseEnum { + pub(crate) fn new(inner: DbEnum) -> Self { + Self(inner) + } + + /** + Get the name of this enum. + */ + pub fn get_name(&self) -> String { + self.0.name.to_string() + } + + /** + Get all known members of this enum. + + Note that this is a direct map of name -> enum values, + and does not actually use the EnumItem datatype itself. + */ + pub fn get_items(&self) -> HashMap { + self.0 + .items + .iter() + .map(|(k, v)| (k.to_string(), *v)) + .collect() + } +} + +impl LuaUserData for DatabaseEnum { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("Name", |_, this| Ok(this.get_name())); + fields.add_field_method_get("Items", |_, this| Ok(this.get_items())); + } + + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq); + methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string); + } +} + +impl PartialEq for DatabaseEnum { + fn eq(&self, other: &Self) -> bool { + self.0.name == other.0.name + } +} + +impl fmt::Display for DatabaseEnum { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "ReflectionDatabaseEnum({})", self.0.name) + } +} diff --git a/src/roblox/reflection/mod.rs b/src/roblox/reflection/mod.rs new file mode 100644 index 0000000..f8db49a --- /dev/null +++ b/src/roblox/reflection/mod.rs @@ -0,0 +1,138 @@ +use std::fmt; + +use mlua::prelude::*; + +use rbx_reflection::ReflectionDatabase; + +use crate::roblox::datatypes::userdata_impl_eq; + +mod class; +mod enums; +mod property; +mod utils; + +pub use class::DatabaseClass; +pub use enums::DatabaseEnum; +pub use property::DatabaseProperty; + +use super::datatypes::userdata_impl_to_string; + +type Db = &'static ReflectionDatabase<'static>; + +/** + A wrapper for [`rbx_reflection::ReflectionDatabase`] that + also provides access to the reflection database from lua. +*/ +#[derive(Debug, Clone, Copy)] +pub struct Database(Db); + +impl Database { + /** + Creates a new database struct, referencing the bundled reflection database. + */ + pub fn new() -> Self { + Self(rbx_reflection_database::get()) + } + + /** + Get the version string of the database. + + This will follow the format `x.y.z.w`, which most + commonly looks something like `0.567.0.123456789`. + */ + pub fn get_version(&self) -> String { + let [x, y, z, w] = self.0.version; + format!("{x}.{y}.{z}.{w}") + } + + /** + Retrieves a list of all currently known enum names. + */ + pub fn get_enum_names(&self) -> Vec { + self.0.enums.keys().map(|e| e.to_string()).collect() + } + + /** + Retrieves a list of all currently known class names. + */ + pub fn get_class_names(&self) -> Vec { + self.0.classes.keys().map(|e| e.to_string()).collect() + } + + /** + Gets an enum with the exact given name, if one exists. + */ + pub fn get_enum(&self, name: impl AsRef) -> Option { + let e = self.0.enums.get(name.as_ref())?; + Some(DatabaseEnum::new(e)) + } + + /** + Gets a class with the exact given name, if one exists. + */ + pub fn get_class(&self, name: impl AsRef) -> Option { + let c = self.0.classes.get(name.as_ref())?; + Some(DatabaseClass::new(c)) + } + + /** + Finds an enum with the given name. + + This will use case-insensitive matching and ignore leading and trailing whitespace. + */ + pub fn find_enum(&self, name: impl AsRef) -> Option { + let name = name.as_ref().trim().to_lowercase(); + let (ename, _) = self + .0 + .enums + .iter() + .find(|(ename, _)| ename.trim().to_lowercase() == name)?; + self.get_enum(ename) + } + + /** + Finds a class with the given name. + + This will use case-insensitive matching and ignore leading and trailing whitespace. + */ + pub fn find_class(&self, name: impl AsRef) -> Option { + let name = name.as_ref().trim().to_lowercase(); + let (cname, _) = self + .0 + .classes + .iter() + .find(|(cname, _)| cname.trim().to_lowercase() == name)?; + self.get_class(cname) + } +} + +impl LuaUserData for Database { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("Version", |_, this| Ok(this.get_version())) + } + + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq); + methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string); + methods.add_method("GetEnumNames", |_, this, _: ()| Ok(this.get_enum_names())); + methods.add_method("GetClassNames", |_, this, _: ()| Ok(this.get_class_names())); + methods.add_method("GetEnum", |_, this, name: String| Ok(this.get_enum(name))); + methods.add_method("GetClass", |_, this, name: String| Ok(this.get_class(name))); + methods.add_method("FindEnum", |_, this, name: String| Ok(this.find_enum(name))); + methods.add_method("FindClass", |_, this, name: String| { + Ok(this.find_class(name)) + }); + } +} + +impl PartialEq for Database { + fn eq(&self, _other: &Self) -> bool { + true // All database userdatas refer to the same underlying rbx-dom database + } +} + +impl fmt::Display for Database { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "ReflectionDatabase") + } +} diff --git a/src/roblox/reflection/property.rs b/src/roblox/reflection/property.rs new file mode 100644 index 0000000..d674d3c --- /dev/null +++ b/src/roblox/reflection/property.rs @@ -0,0 +1,95 @@ +use std::fmt; + +use mlua::prelude::*; + +use rbx_reflection::{ClassDescriptor, PropertyDescriptor}; + +use super::utils::*; +use crate::roblox::datatypes::{userdata_impl_eq, userdata_impl_to_string}; + +type DbClass = &'static ClassDescriptor<'static>; +type DbProp = &'static PropertyDescriptor<'static>; + +/** + A wrapper for [`rbx_reflection::PropertyDescriptor`] that + also provides access to the property descriptor from lua. +*/ +#[derive(Debug, Clone, Copy)] +pub struct DatabaseProperty(DbClass, DbProp); + +impl DatabaseProperty { + pub(crate) fn new(inner: DbClass, inner_prop: DbProp) -> Self { + Self(inner, inner_prop) + } + + /** + Get the name of this property. + */ + pub fn get_name(&self) -> String { + self.1.name.to_string() + } + + /** + Get the datatype name of the property. + + For normal datatypes this will be a string such as `string`, `Color3`, ... + + For enums this will be a string formatted as `Enum.EnumName`. + */ + pub fn get_datatype_name(&self) -> String { + data_type_to_str(self.1.data_type.clone()) + } + + /** + Get the scriptability of this property, meaning if it can be written / read at runtime. + + All properties are writable and readable in Lune even if scriptability is not. + */ + pub fn get_scriptability_str(&self) -> &'static str { + scriptability_to_str(&self.1.scriptability) + } + + /** + Get all known tags describing the property. + + These include information such as if the property can be replicated to players + at runtime, if the property should be hidden in Roblox Studio, and more. + */ + pub fn get_tags_str(&self) -> Vec<&'static str> { + self.1 + .tags + .iter() + .map(property_tag_to_str) + .collect::>() + } +} + +impl LuaUserData for DatabaseProperty { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("Name", |_, this| Ok(this.get_name())); + fields.add_field_method_get("Datatype", |_, this| Ok(this.get_datatype_name())); + fields.add_field_method_get("Scriptability", |_, this| Ok(this.get_scriptability_str())); + fields.add_field_method_get("Tags", |_, this| Ok(this.get_tags_str())); + } + + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq); + methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string); + } +} + +impl PartialEq for DatabaseProperty { + fn eq(&self, other: &Self) -> bool { + self.0.name == other.0.name && self.1.name == other.1.name + } +} + +impl fmt::Display for DatabaseProperty { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "ReflectionDatabaseProperty({} > {})", + self.0.name, self.1.name + ) + } +} diff --git a/src/roblox/reflection/utils.rs b/src/roblox/reflection/utils.rs new file mode 100644 index 0000000..84affd1 --- /dev/null +++ b/src/roblox/reflection/utils.rs @@ -0,0 +1,56 @@ +use rbx_reflection::{ClassTag, DataType, PropertyTag, Scriptability}; + +use crate::roblox::datatypes::extension::DomValueExt; + +pub fn data_type_to_str(data_type: DataType) -> String { + match data_type { + DataType::Enum(e) => format!("Enum.{e}"), + DataType::Value(v) => v + .variant_name() + .expect("Encountered unknown data type variant") + .to_string(), + _ => panic!("Encountered unknown data type"), + } +} + +/* + NOTE: Remember to add any new strings here to typedefs too! +*/ + +pub fn scriptability_to_str(scriptability: &Scriptability) -> &'static str { + match scriptability { + Scriptability::None => "None", + Scriptability::Custom => "Custom", + Scriptability::Read => "Read", + Scriptability::ReadWrite => "ReadWrite", + Scriptability::Write => "Write", + _ => panic!("Encountered unknown scriptability"), + } +} + +pub fn property_tag_to_str(tag: &PropertyTag) -> &'static str { + match tag { + PropertyTag::Deprecated => "Deprecated", + PropertyTag::Hidden => "Hidden", + PropertyTag::NotBrowsable => "NotBrowsable", + PropertyTag::NotReplicated => "NotReplicated", + PropertyTag::NotScriptable => "NotScriptable", + PropertyTag::ReadOnly => "ReadOnly", + PropertyTag::WriteOnly => "WriteOnly", + _ => panic!("Encountered unknown property tag"), + } +} + +pub fn class_tag_to_str(tag: &ClassTag) -> &'static str { + match tag { + ClassTag::Deprecated => "Deprecated", + ClassTag::NotBrowsable => "NotBrowsable", + ClassTag::NotCreatable => "NotCreatable", + ClassTag::NotReplicated => "NotReplicated", + ClassTag::PlayerReplicated => "PlayerReplicated", + ClassTag::Service => "Service", + ClassTag::Settings => "Settings", + ClassTag::UserSettings => "UserSettings", + _ => panic!("Encountered unknown class tag"), + } +} diff --git a/src/tests.rs b/src/tests.rs index f0b1407..b80f5bd 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -158,4 +158,9 @@ create_tests! { roblox_instance_methods_is_descendant_of: "roblox/instance/methods/IsDescendantOf", roblox_misc_typeof: "roblox/misc/typeof", + + roblox_reflection_class: "roblox/reflection/class", + roblox_reflection_database: "roblox/reflection/database", + roblox_reflection_enums: "roblox/reflection/enums", + roblox_reflection_property: "roblox/reflection/property", } diff --git a/tests/roblox/reflection/class.luau b/tests/roblox/reflection/class.luau new file mode 100644 index 0000000..fa20bd3 --- /dev/null +++ b/tests/roblox/reflection/class.luau @@ -0,0 +1,41 @@ +local roblox = require("@lune/roblox") + +local db = roblox.getReflectionDatabase() + +-- Make sure database classes exist + fields / properties are correct types + +for _, className in db:GetClassNames() do + local class = db:GetClass(className) + assert(class ~= nil, "Missing " .. className .. " class in database") + assert(type(class.Name) == "string", "Name property must be a string") + assert( + class.Superclass == nil or type(class.Superclass) == "string", + "Superclass property must be nil or a string" + ) + assert(type(class.Properties) == "table", "Properties property must be a table") + assert(type(class.DefaultProperties) == "table", "DefaultProperties property must be a table") + assert(type(class.Tags) == "table", "Tags property must be a table") +end + +-- Any property present in default properties must also +-- be in properties *or* the properties of a superclass + +for _, className in db:GetClassNames() do + local class = db:GetClass(className) + assert(class ~= nil) + for name, value in class.DefaultProperties do + local found = false + local current = class + while current ~= nil do + if current.Properties[name] ~= nil then + found = true + break + elseif current.Superclass ~= nil then + current = db:GetClass(current.Superclass) + else + break + end + end + assert(found, "Missing default property " .. name .. " in properties table") + end +end diff --git a/tests/roblox/reflection/database.luau b/tests/roblox/reflection/database.luau new file mode 100644 index 0000000..c43bbc3 --- /dev/null +++ b/tests/roblox/reflection/database.luau @@ -0,0 +1,55 @@ +local roblox = require("@lune/roblox") + +local db = roblox.getReflectionDatabase() +local db2 = roblox.getReflectionDatabase() + +-- Subsequent calls to getReflectionDatabase should return the same database +assert(db == db2, "Database should always compare as equal to other database") + +-- Database should not be empty +assert(#db:GetClassNames() > 0, "Database should not be empty (no class names)") +assert(#db:GetEnumNames() > 0, "Database should not be empty (no enum names)") + +-- Make sure our database finds classes correctly + +local class = db:GetClass("Instance") +assert(class ~= nil, "Missing Instance class in database") +local prop = class.Properties.Parent +assert(prop ~= nil, "Missing Parent property on Instance class in database") + +local class2 = db:FindClass(" instance ") +assert(class2 ~= nil, "Missing Instance class in database (2)") +local prop2 = class2.Properties.Parent +assert(prop2 ~= nil, "Missing Parent property on Instance class in database (2)") + +assert(class == class2, "Class userdatas from the database should compare as equal") +assert(prop == prop2, "Property userdatas from the database should compare as equal") + +assert(db:GetClass("PVInstance") ~= nil, "Missing PVInstance class in database") +assert(db:GetClass("BasePart") ~= nil, "Missing BasePart class in database") +assert(db:GetClass("Part") ~= nil, "Missing Part class in database") + +-- Make sure our database finds enums correctly + +local enum = db:GetEnum("PartType") +assert(enum ~= nil, "Missing PartType enum in database") + +local enum2 = db:FindEnum(" parttype ") +assert(enum2 ~= nil, "Missing PartType enum in database (2)") + +assert(enum == enum2, "Enum userdatas from the database should compare as equal") + +assert(db:GetEnum("UserInputType") ~= nil, "Missing UserInputType enum in database") +assert(db:GetEnum("NormalId") ~= nil, "Missing NormalId enum in database") +assert(db:GetEnum("Font") ~= nil, "Missing Font enum in database") + +-- All the class and enum names gotten from the database should be accessible + +for _, className in db:GetClassNames() do + assert(db:GetClass(className) ~= nil, "Missing " .. className .. " class in database (3)") + assert(db:FindClass(className) ~= nil, "Missing " .. className .. " class in database (4)") +end +for _, enumName in db:GetEnumNames() do + assert(db:GetEnum(enumName) ~= nil, "Missing " .. enumName .. " enum in database (3)") + assert(db:FindEnum(enumName) ~= nil, "Missing " .. enumName .. " enum in database (4)") +end diff --git a/tests/roblox/reflection/enums.luau b/tests/roblox/reflection/enums.luau new file mode 100644 index 0000000..aa6b163 --- /dev/null +++ b/tests/roblox/reflection/enums.luau @@ -0,0 +1,32 @@ +local roblox = require("@lune/roblox") + +local db = roblox.getReflectionDatabase() + +-- Make sure database enums exist + fields / properties are correct types + +for _, enumName in db:GetEnumNames() do + local enum = db:GetEnum(enumName) + assert(enum ~= nil, "Missing " .. enumName .. " enum in database") + assert(type(enum.Name) == "string", "Name property must be a string") + assert(type(enum.Items) == "table", "Items property must be a table") +end + +-- Enum items should be a non-empty map of string -> positive integer values + +for _, enumName in db:GetEnumNames() do + local enum = db:GetEnum(enumName) + assert(enum ~= nil) + local empty = true + for name, value in enum.Items do + assert( + type(name) == "string" and #name > 0, + "Enum items map must only contain non-empty string keys" + ) + assert( + type(value) == "number" and value >= 0 and math.floor(value) == value, + "Enum items map must only contain positive integer values" + ) + empty = false + end + assert(not empty, "Enum items map must not be empty") +end diff --git a/tests/roblox/reflection/property.luau b/tests/roblox/reflection/property.luau new file mode 100644 index 0000000..6f3448e --- /dev/null +++ b/tests/roblox/reflection/property.luau @@ -0,0 +1,17 @@ +local roblox = require("@lune/roblox") + +local db = roblox.getReflectionDatabase() + +-- Make sure database class properties exist + their fields / properties are correct types + +for _, className in db:GetClassNames() do + local class = db:GetClass(className) + assert(class ~= nil) + + for name, prop in class.Properties do + assert(type(prop.Name) == "string", "Name property must be a string") + assert(type(prop.Datatype) == "string", "Datatype property must be a string") + assert(type(prop.Scriptability) == "string", "Scriptability property must be a string") + assert(type(prop.Tags) == "table", "Tags property must be a table") + end +end