Implement reflection database accessible from lua in roblox builtin

This commit is contained in:
Filip Tibell 2023-07-21 12:10:56 +02:00
parent 49ae85af03
commit 8853aad620
No known key found for this signature in database
14 changed files with 842 additions and 12 deletions

View file

@ -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),

View file

@ -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<ReflectionDatabase> = OnceCell::new();
pub fn create(lua: &'static Lua) -> LuaResult<LuaTable> {
let mut roblox_constants = Vec::new();
let roblox_module = roblox::module(lua)?;
@ -21,7 +26,8 @@ pub fn create(lua: &'static Lua) -> LuaResult<LuaTable> {
.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<bool>) -> LuaResult<Option<String>> {
task::spawn_blocking(move || {
fn get_auth_cookie(_: &Lua, raw: Option<bool>) -> LuaResult<Option<String>> {
if matches!(raw, Some(true)) {
Ok(rbx_cookie::get_value())
} else {
Ok(rbx_cookie::get())
}
})
.await
.map_err(LuaError::external)?
}
fn get_reflection_database(_: &Lua, _: ()) -> LuaResult<ReflectionDatabase> {
Ok(*REFLECTION_DATABASE.get_or_init(ReflectionDatabase::new))
}

View file

@ -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",

View file

@ -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;

View file

@ -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<String> {
let sup = self.0.superclass.as_ref()?;
Some(sup.to_string())
}
/**
Get all known properties for this class.
*/
pub fn get_properties(&self) -> HashMap<String, DatabaseProperty> {
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<String, DomVariant> {
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::<Vec<_>>()
}
}
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<str>) -> Option<String> {
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<str>, value: u32) -> LuaResult<EnumItem> {
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
))
})
}

View file

@ -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<String, u32> {
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)
}
}

View file

@ -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<String> {
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<String> {
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<str>) -> Option<DatabaseEnum> {
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<str>) -> Option<DatabaseClass> {
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<str>) -> Option<DatabaseEnum> {
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<str>) -> Option<DatabaseClass> {
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")
}
}

View file

@ -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::<Vec<_>>()
}
}
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
)
}
}

View file

@ -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"),
}
}

View file

@ -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",
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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