mod error;
use rbx_reflection::ReflectionDatabase;
use std::{env, fs, path::PathBuf, sync::LazyLock};
pub use error::Error;
type ResultOption<T> = Result<Option<T>, Error>;
static ENCODED_DATABASE: &[u8] = include_bytes!("../database.msgpack");
pub const OVERRIDE_PATH_VAR: &str = "RBX_DATABASE";
pub const LOCAL_DIR_NAME: &str = ".rbxreflection";
static BUNDLED_DATABASE: LazyLock<ReflectionDatabase<'static>> = LazyLock::new(|| {
log::debug!("Loading bundled reflection database");
rmp_serde::decode::from_slice(ENCODED_DATABASE)
.unwrap_or_else(|e| panic!("could not decode reflection database because: {}", e))
});
static LOCAL_DATABASE: LazyLock<ResultOption<ReflectionDatabase<'static>>> = LazyLock::new(|| {
let Some(path) = get_local_location() else {
return Ok(None);
};
if path.exists() {
let database: ReflectionDatabase<'static> = rmp_serde::from_slice(&fs::read(path)?)?;
Ok(Some(database))
} else {
Ok(None)
}
});
pub fn get() -> Result<&'static ReflectionDatabase<'static>, Error> {
#[cfg(feature = "debug_always_use_bundled")]
{
Ok(&BUNDLED_DATABASE)
}
#[cfg(not(feature = "debug_always_use_bundled"))]
{
Ok(get_local()?.unwrap_or(&BUNDLED_DATABASE))
}
}
pub fn get_local() -> ResultOption<&'static ReflectionDatabase<'static>> {
let local_database: &ResultOption<ReflectionDatabase<'static>> = &LOCAL_DATABASE;
match local_database {
Ok(opt) => Ok(opt.as_ref()),
Err(e) => Err(e.clone()),
}
}
pub fn get_bundled() -> &'static ReflectionDatabase<'static> {
&BUNDLED_DATABASE
}
pub fn get_local_location() -> Option<PathBuf> {
if let Ok(location) = env::var(OVERRIDE_PATH_VAR) {
log::debug!("Using environment variable {OVERRIDE_PATH_VAR} to fetch reflection database");
Some(PathBuf::from(location))
} else {
get_local_location_no_var()
}
}
fn get_local_location_no_var() -> Option<PathBuf> {
#[cfg(target_os = "linux")]
let mut home = dirs::home_dir()?;
#[cfg(not(target_os = "linux"))]
let mut home = dirs::data_local_dir()?;
home.push(LOCAL_DIR_NAME);
home.push("database.msgpack");
Some(home)
}
#[cfg(test)]
mod test {
use rbx_reflection::ClassDescriptor;
use super::*;
#[test]
fn bundled() {
let _database = get_bundled();
}
#[test]
fn env_var() {
let mut test_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
test_path.push("empty.msgpack");
unsafe { env::set_var(OVERRIDE_PATH_VAR, &test_path) };
let empty_db = get_local().unwrap().unwrap();
assert!(empty_db.version == [0, 0, 0, 0]);
}
#[test]
fn local_location() {
#[allow(unused_mut, reason = "this path needs to be mutated on macos")]
let mut home_from_env;
#[cfg(target_os = "windows")]
#[allow(
clippy::unnecessary_operation,
reason = "attributes on statements are currently unstable so this cannot be reduced"
)]
{
home_from_env = PathBuf::from(env!("LOCALAPPDATA"));
}
#[cfg(target_os = "macos")]
#[allow(
clippy::unnecessary_operation,
reason = "attributes on statements are currently unstable so this cannot be reduced"
)]
{
home_from_env = PathBuf::from(env!("HOME"));
home_from_env.push("Library");
home_from_env.push("Application Support");
}
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
#[allow(
clippy::unnecessary_operation,
reason = "attributes on statements are currently unstable so this cannot be reduced"
)]
{
home_from_env = PathBuf::from(env!("HOME"))
};
let mut local_expected = home_from_env.join(LOCAL_DIR_NAME);
local_expected.push("database.msgpack");
assert_eq!(get_local_location_no_var().unwrap(), local_expected);
}
#[test]
fn superclasses_iter_test() {
let database = get_bundled();
let part_class_descriptor = database.classes.get("Part");
let mut iter = database.superclasses_iter(part_class_descriptor.unwrap());
fn class_descriptor_eq(lhs: Option<&ClassDescriptor>, rhs: Option<&ClassDescriptor>) {
let eq = match (lhs, rhs) {
(Some(lhs), Some(rhs)) => lhs.name == rhs.name,
(None, None) => true,
_ => false,
};
assert!(eq, "{:?} != {:?}", lhs, rhs);
}
class_descriptor_eq(iter.next(), part_class_descriptor);
let mut current_class_descriptor = part_class_descriptor.unwrap();
while let Some(superclass) = current_class_descriptor.superclass.as_ref() {
let superclass_descriptor = database.classes.get(superclass.as_ref());
class_descriptor_eq(iter.next(), superclass_descriptor);
current_class_descriptor = superclass_descriptor.unwrap();
}
class_descriptor_eq(iter.next(), None);
}
#[test]
fn has_superclass_test() {
let database = get_bundled();
let part_class_descriptor = database.classes.get("Part").unwrap();
let instance_class_descriptor = database.classes.get("Instance").unwrap();
assert!(database.has_superclass(part_class_descriptor, instance_class_descriptor));
assert!(!database.has_superclass(instance_class_descriptor, part_class_descriptor));
}
}