-
Notifications
You must be signed in to change notification settings - Fork 53
Change rbx_reflection_database to support loading from FS #376
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 16 commits
7145f07
a4409b7
c5bfda2
ca2cbf0
f8c833f
69d6a70
6bd1b0b
f61eab3
d066132
a9f3beb
2413092
99171cb
d1eeac4
e5f8c05
17128fe
d95bc80
d525f03
7d639f3
a1d2d37
230c4ae
ec8a046
7328ded
bc78a6f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
use std::{fmt, io}; | ||
|
||
#[derive(Debug, Clone)] | ||
pub struct Error(String); | ||
|
||
impl std::error::Error for Error {} | ||
|
||
impl fmt::Display for Error { | ||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
write!(f, "{}", self.0) | ||
} | ||
} | ||
|
||
impl From<rmp_serde::decode::Error> for Error { | ||
fn from(value: rmp_serde::decode::Error) -> Self { | ||
Self(value.to_string()) | ||
} | ||
} | ||
|
||
impl From<io::Error> for Error { | ||
fn from(value: io::Error) -> Self { | ||
Self(value.to_string()) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,144 @@ | ||
//! Contains an API to get a Roblox reflection database using the types | ||
//! from [`rbx_reflection`]. This crate embeds a database for this purpose, | ||
//! but also provides an API for dependents to get a reflection database | ||
//! from a consistent location. | ||
//! | ||
//! The general way this crate should be used is via [`get`]. This method will | ||
//! search for a locally stored reflection database and return it if it's | ||
//! found. If it isn't, it will instead return the bundled one. | ||
//! | ||
//! Additionally, this crate exposes [`get_local`] and [`get_bundled`] for | ||
//! only loading the locally stored database or only the bundled one | ||
//! respectively. | ||
//! | ||
//! ## Local Details | ||
//! | ||
//! This crate will load a reflection database from the file system if one | ||
//! exists in the default location. This location varies upon the OS and is | ||
//! specified here: | ||
//! | ||
//! | OS | Location | | ||
//! |:--------|:--------------------------------------------------------------------| | ||
//! | Windows | `%localappdata%/.rbxreflection/database.msgpack` | | ||
//! | MacOS | `$HOME/Library/Application Support/.rbxreflection/database.msgpack` | | ||
//! | Linux | `$HOME/.rbxreflection/database.msgpack` | | ||
//! | ||
//! Additionally, a location override may be specified via the `RBX_DATABASE` | ||
//! environment variable. The `RBX_DATABASE` variable points to the override | ||
//! `database.msgpack` file, _not_ to an override `.rbxreflection` directory. | ||
//! | ||
//! Both the default `database.msgpack` files and any files pointed to by | ||
//! `RBX_DATABASE` must be valid MessagePack serializations of a | ||
//! [`ReflectionDatabase`] if they're present. | ||
mod error; | ||
|
||
use rbx_reflection::ReflectionDatabase; | ||
|
||
use std::{env, fs, path::PathBuf, sync::OnceLock}; | ||
|
||
pub use error::Error; | ||
|
||
/// An alias to avoid overly verbose types. | ||
type ResultOption<T> = Result<Option<T>, Error>; | ||
|
||
static ENCODED_DATABASE: &[u8] = include_bytes!("../database.msgpack"); | ||
|
||
/// The name of an environment variable that may be used to specify | ||
/// the location of a reflection database to use. The expected format of | ||
/// a file at this point is MessagePack. | ||
pub const OVERRIDE_PATH_VAR: &str = "RBX_DATABASE"; | ||
|
||
/// The name of the directory used for the local location for a reflection | ||
/// database. The directory will be placed inside the current user's | ||
/// local data folder on MacOS and Windows and inside | ||
/// the home directory on Linux. | ||
pub const LOCAL_DIR_NAME: &str = ".rbxreflection"; | ||
|
||
lazy_static::lazy_static! { | ||
static ref DATABASE: ReflectionDatabase<'static> = { | ||
static ref BUNDLED_DATABASE: ReflectionDatabase<'static> = { | ||
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)) | ||
}; | ||
} | ||
|
||
pub fn get() -> &'static ReflectionDatabase<'static> { | ||
&DATABASE | ||
static LOCAL_DATABASE: OnceLock<ResultOption<ReflectionDatabase<'static>>> = OnceLock::new(); | ||
Dekkonot marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
/// Returns a populated [`ReflectionDatabase`]. This will attempt to load one locally and | ||
/// if one can't be found, it will return one that is bundled with this crate. | ||
/// | ||
/// ## Errors | ||
/// | ||
/// Errors if a locally stored [`ReflectionDatabase`] could not be read | ||
/// or is invalid MessagePack. | ||
pub fn get() -> Result<&'static ReflectionDatabase<'static>, Error> { | ||
Ok(get_local()?.unwrap_or(&BUNDLED_DATABASE)) | ||
} | ||
Comment on lines
+82
to
+84
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not totally sure if breaking this interface is the right call. The way I'm thinking about it is: if On the other hand, then it's not clear that this function can fail, and removes the ability to e.g. print a warning if the local database is corrupt while using this function. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not totally sold on breaking this either, but the other option is hiding failures from users. It might be worth taking a middleground and just emitting a message into stderr if the local database exists but is corrupted? It may also be worth caching it if we do this though, so it doesn't print the message every time. |
||
|
||
/// Returns a reflection database from the file system, if one can be found. | ||
/// This is loaded from a location set by the `RBX_DATABASE` environment | ||
/// variable if it's set. Otherwise, the default location is checked. | ||
/// | ||
/// The default location varies depending upon OS: | ||
/// | ||
/// | OS | Location | | ||
/// |:--------|:--------------------------------------------------------------------| | ||
/// | Windows | `%localappdata%/.rbxreflection/database.msgpack` | | ||
/// | MacOS | `$HOME/Library/Application Support/.rbxreflection/database.msgpack` | | ||
/// | Linux | `$HOME/.rbxreflection/database.msgpack` | | ||
/// | ||
/// The file at the above location (or the one pointed to by `RBX_DATABASE`) | ||
/// must be valid MessagePack. | ||
/// | ||
/// ## Errors | ||
/// | ||
/// Errors if the file specified by `RBX_DATABASE` or in the default location | ||
/// exists but is invalid MessagePack. | ||
pub fn get_local() -> ResultOption<&'static ReflectionDatabase<'static>> { | ||
let inner = LOCAL_DATABASE.get_or_init(|| { | ||
if let Some(path) = get_local_location() { | ||
if path.exists() { | ||
let database: ReflectionDatabase<'static> = | ||
rmp_serde::from_slice(&fs::read(path)?)?; | ||
Ok(Some(database)) | ||
} else { | ||
Ok(None) | ||
} | ||
} else { | ||
Ok(None) | ||
} | ||
}); | ||
match inner { | ||
Ok(opt) => Ok(opt.as_ref()), | ||
// This clone could be avoided because these references are static, | ||
// but it'd involve some indirection and these errors are rare anyway. | ||
Err(e) => Err(e.clone()), | ||
} | ||
} | ||
|
||
/// Returns the locally bundled [`ReflectionDatabase`]. This database may or may | ||
/// not be up to date, but it will always exist. | ||
pub fn get_bundled() -> &'static ReflectionDatabase<'static> { | ||
&BUNDLED_DATABASE | ||
} | ||
|
||
/// Fetches the location a [`ReflectionDatabase`] is expected to be loaded from. | ||
/// This may return [`None`] if the local data directory cannot be found. | ||
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 { | ||
// Due to concerns about the local data directory existing | ||
// on Linux, we use the home directory instead. | ||
#[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)] | ||
|
@@ -19,13 +148,24 @@ mod test { | |
use super::*; | ||
|
||
#[test] | ||
fn smoke_test() { | ||
let _database = get(); | ||
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"); | ||
|
||
env::set_var(OVERRIDE_PATH_VAR, &test_path); | ||
let empty_db = get().unwrap(); | ||
println!("{:?}", empty_db.version); | ||
assert!(empty_db.version == [0, 0, 0, 0]); | ||
} | ||
|
||
#[test] | ||
fn superclasses_iter_test() { | ||
let database = get(); | ||
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>) { | ||
|
@@ -51,7 +191,7 @@ mod test { | |
|
||
#[test] | ||
fn has_superclass_test() { | ||
let database = get(); | ||
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)); | ||
|
Uh oh!
There was an error while loading. Please reload this page.