use bevy::{ecs::system::SystemParam, prelude::*};
pub mod prelude {
pub use super::{AppTablesExt, Database, DatabaseMut, Table, Tables};
}
pub trait Table: Send + Sync + 'static {}
#[derive(Resource, Deref, DerefMut, Default)]
pub struct Tables<T: Table> {
table: T,
}
#[derive(SystemParam, Deref, DerefMut)]
pub struct Database<'w, E: Table> {
#[system_param(validation_message = "Table not initialized")]
tables: Res<'w, Tables<E>>,
}
#[derive(SystemParam, Deref, DerefMut)]
pub struct DatabaseMut<'w, E: Table> {
#[system_param(validation_message = "Table not initialized")]
tables: ResMut<'w, Tables<E>>,
}
pub trait AppTablesExt {
fn init_database_table<T: Table + Default>(&mut self) -> &mut Self;
fn insert_database_table<T: Table>(&mut self, table: T) -> &mut Self;
}
impl AppTablesExt for App {
fn init_database_table<T: Table + Default>(&mut self) -> &mut Self {
self.init_resource::<Tables<T>>();
self
}
fn insert_database_table<T: Table>(&mut self, table: T) -> &mut Self {
self.insert_resource(Tables { table });
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Default)]
struct MyTable {
pub value: bool,
}
impl Table for MyTable {}
#[test]
fn should_initialize_database_table_resource() {
let mut app = App::new();
app.init_database_table::<MyTable>();
assert!(
app.world().get_resource::<Tables<MyTable>>().is_some(),
"Tables<MyTable> should be created after init_database_table"
);
}
#[test]
fn should_access_table_through_database_system_params() {
let mut app = App::new();
app.insert_database_table(MyTable { value: false });
app.add_systems(PreUpdate, |table: Database<MyTable>| {
assert!(!table.value, "Initial value should be false");
})
.add_systems(Update, |mut table: DatabaseMut<MyTable>| {
table.value = true;
})
.add_systems(PostUpdate, |table: Database<MyTable>| {
assert!(table.value, "Value should be true after update");
});
app.update();
let resource = app.world().get_resource::<Tables<MyTable>>();
assert!(resource.is_some(), "Tables<MyTable> should still exist");
let table = resource.unwrap();
assert!(table.value, "Table value should be true after update");
}
#[test]
fn should_mutate_table_resource_directly() {
let mut app = App::new();
app.insert_database_table(MyTable { value: false });
{
let resource = app.world_mut().get_resource_mut::<Tables<MyTable>>();
assert!(
resource.is_some(),
"Tables<MyTable> resource should exist for mutation"
);
let mut table = resource.unwrap();
assert!(!table.value, "Initial value should be false");
table.value = true;
}
{
let resource = app.world().get_resource::<Tables<MyTable>>();
assert!(
resource.is_some(),
"Tables<MyTable> should exist after mutation"
);
let table = resource.unwrap();
assert!(table.value, "Table value should be true after mutation");
}
}
#[test]
fn should_keep_existing_table_when_initializing_again() {
let mut app = App::new();
app.insert_database_table(MyTable { value: true });
app.init_database_table::<MyTable>();
let table = app
.world()
.get_resource::<Tables<MyTable>>()
.expect("Tables<MyTable> should exist after initialization");
assert!(
table.value,
"init_database_table should preserve an already inserted table resource"
);
}
#[test]
fn should_overwrite_previous_table_when_inserting_again() {
let mut app = App::new();
app.insert_database_table(MyTable { value: false });
app.insert_database_table(MyTable { value: true });
let table = app
.world()
.get_resource::<Tables<MyTable>>()
.expect("Tables<MyTable> should exist after insertion");
assert!(
table.value,
"insert_database_table should overwrite the previous table resource"
);
}
#[test]
fn should_access_database_mut_through_prelude() {
use crate::prelude::{AppTablesExt, DatabaseMut, Table, Tables};
#[derive(Default)]
struct PreludeTable {
value: usize,
}
impl Table for PreludeTable {}
let mut app = App::new();
app.init_database_table::<PreludeTable>();
app.add_systems(Update, |mut table: DatabaseMut<PreludeTable>| {
table.value = 11;
});
app.update();
assert_eq!(
app.world()
.get_resource::<Tables<PreludeTable>>()
.expect("Tables<PreludeTable> should exist after initialization")
.value,
11,
"The prelude should expose DatabaseMut for mutable table access"
);
}
#[test]
fn should_chain_table_extension_methods() {
let mut app = App::new();
let returned = app
.init_database_table::<MyTable>()
.insert_database_table(MyTable { value: true });
assert!(
std::ptr::eq(returned, &app),
"AppTablesExt methods should return the same App reference for chaining"
);
}
}