
# dep-obj
Dependency object: effective reactive heterogeneous container.
## Example: simple dependency type
The dependency objects system bases on [`component-arena`](https://crates.io/crates/components-arena).
A component may have multiply dependency objects as its parts. Some of them may be dynamically typed
and/or optional. Lets see an example of simple component with one dependency object of fixed type.
Consider as an example carriable game object `Item`.
To describe abstract entity `Item`, we will need the following list of types:
- the component data holder `ItemComponent`;
- the id of component `Item`;
- the dependency object type `ItemProps`;
- the components arena `Items`.
First, define the component containing the dependency object:
```rust
macro_attr! {
#[derive(Debug, Component!)]
struct ItemComponent {
props: ItemProps,
}
}
```
Then, to maintain an encapsulation, wrap `Id<ItemComponent>` into a newtype:
```rust
macro_attr! {
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, NewtypeComponentId!)]
pub struct Item(Id<ItemComponent>);
}
```
Dependency objects support properties inheritance. The tree structure of objects is defined
by the `DepObjId` trait implementation. We do not need inheritance for `Item`, and we
can express it and get appropriate «empty» `DepObjId` implementation
by marking it with the `DetachedDepObjId` trait:
```rust
impl DetachedDepObjId for Item { }
```
All components need be emplaced in an appropriate arena. Lets create it:
```rust
#[derive(Debug)]
pub struct Items {
items: Arena<ItemComponent>,
}
```
An another foundation the dependency object system based on is
the [`dyn-context`](https://crates.io/crates/dyn-context) crate.
To make `Items` usage more convenient it is worth to mark it as `SelfState`,
i.e. a [state](https://docs.rs/dyn-context/latest/dyn_context/state/trait.State.html)
containing the only one part, which is `Items` itself:
```rust
impl SelfState for Items { }
```
Now we are ready to specify the dependency type itself:
```rust
dep_type! {
#[derive(Debug)]
pub struct ItemProps = Item[ItemProps] {
name: Cow<'static, str> = Cow::Borrowed(""),
base_weight: f32 = 0.0,
weight: f32 = 0.0,
equipped: bool = false,
cursed: bool = false,
}
}
```
Now we have all structures encoded and can write `Item` methods.
First, we need a way to construct a new `Item`:
```rust
pub fn new(state: &mut dyn State) -> Item {
let items: &mut Items = state.get_mut();
items.0.insert(|id| (ItemComponent { props: ItemProps::new_priv() }, Item(id)))
}
```
The `ItemProps::new_priv` is a constructor, generated by the `dep_type!` macro.
Next, we need a way to destroy unneeded items:
```rust
pub fn drop_self(self, state: &mut dyn State) {
self.drop_bindings_priv(state);
let items: &mut Items = state.get_mut();
items.0.remove(self.0);
}
```
And now the last, but not least: an indirect definition
of the function providing access to the dependency object in
the `props` field:
```rust
impl_dep_obj!(Item {
fn<ItemProps>() -> (ItemProps) { Items | .props }
});
```
The `impl_dep_obj` macro also generates the `drop_bindigs_priv` method
we used in the `drop_self` method earlier.
Lets take a look at our `mod items` as a whole:
```rust
mod items {
use components_arena::{Arena, Component, NewtypeComponentId, Id};
use dep_obj::{DetachedDepObjId, dep_type, impl_dep_obj};
use dyn_context::{SelfState, State, StateExt};
use macro_attr_2018::macro_attr;
use std::borrow::Cow;
macro_attr! {
#[derive(Debug, Component!)]
struct ItemComponent {
props: ItemProps,
}
}
macro_attr! {
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, NewtypeComponentId!)]
pub struct Item(Id<ItemComponent>);
}
impl DetachedDepObjId for Item { }
impl Item {
pub fn new(state: &mut dyn State) -> Item {
let items: &mut Items = state.get_mut();
items.0.insert(|id| (ItemComponent { props: ItemProps::new_priv() }, Item(id)))
}
pub fn drop_self(self, state: &mut dyn State) {
self.drop_bindings_priv(state);
let items: &mut Items = state.get_mut();
items.0.remove(self.0);
}
}
impl_dep_obj!(Item {
fn<ItemProps>() -> (ItemProps) { Items | .props }
});
#[derive(Debug)]
pub struct Items(Arena<ItemComponent>);
impl SelfState for Items { }
dep_type! {
#[derive(Debug)]
pub struct ItemProps = Item[ItemProps] {
name: Cow<'static, str> = Cow::Borrowed(""),
base_weight: f32 = 0.0,
weight: f32 = 0.0,
equipped: bool = false,
cursed: bool = false,
}
}
}
```
The things we lack here are `Items` constructor, and, unfortunately, destructor.
Adding constructor is straightforward:
```rust
impl Items {
pub fn new() -> Items {
Items(Arena::new())
}
}
```
The destructor however is tricky. The `Item::drop_self` method do two things:
first, it drops all bindings item owes, and, second, it removes items from arena.
The second thing would do automatically, but bindings require manual destroying.
Thus we need explicit `Items` destructor to correctly drop all `Item`s' bindings.
But we cannot just implement `Drop` for `Items` because we need `State` parameter
to call `Item::drop_bindings_priv`. Unfortunately, Rust does not support a
linear types concept, which would allow to have parameters in `drop` method.
But `dyn-context` and `components-arena` crates contain some helpful things,
allowing to express such type properties as good as it is possible in Rust for now.
The `Arena` implements special trait, `Stop`, that is an analogue of `Drop` with
`State` parameter. Out wrap `Items`, however, does not implement it. Lets fix it:
```rust
#[derive(Debug, Stop)]
pub struct Items(Arena<ItemComponent>);
```
A thing, we want `Item::stop` function to do, is call `drop_bindings_priv` for
every `Item`. To tell it, we need to define some struct (lets call it `ItemStop`),
and let `ItemComponent` uses it to properly «stop» our `Item`s. It is easily achieved
with the `Component` derive macro `stop` parameter:
```rust
#[derive(Debug, Component!(stop=ItemStop)]
struct ItemComponent {
props: ItemProps,
}
```
If we try to compile, we would get an error pointing to the fact, that
the `ComponentStop` trait is not implemented for `ItemStop`.
So lets implement it:
```rust
impl ComponentStop for ItemStop {
with_arena_in_state_part!(Items);
fn stop(&self, state: &mut dyn State, id: Id<ItemComponent>) {
Item(id).drop_bindings_priv(state);
}
}
```
Thanks to the `with_arena_in_state_part` macro, the only function we were need
to implement manually is `stop`.
Take a look at out `mod items`:
```rust
mod items {
use components_arena::{Arena, Component, ComponentStop, NewtypeComponentId, Id, with_arena_in_state_part};
use dep_obj::{DetachedDepObjId, dep_type, impl_dep_obj};
use dyn_context::{SelfState, State, StateExt, Stop};
use macro_attr_2018::macro_attr;
use std::borrow::Cow;
macro_attr! {
#[derive(Debug, Component!(stop=ItemStop))]
struct ItemComponent {
props: ItemProps,
}
}
impl ComponentStop for ItemStop {
with_arena_in_state_part!(Items);
fn stop(&self, state: &mut dyn State, id: Id<ItemComponent>) {
Item(id).drop_bindings_priv(state);
}
}
macro_attr! {
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, NewtypeComponentId!)]
pub struct Item(Id<ItemComponent>);
}
impl DetachedDepObjId for Item { }
impl Item {
pub fn new(state: &mut dyn State) -> Item {
let items: &mut Items = state.get_mut();
items.0.insert(|id| (ItemComponent { props: ItemProps::new_priv() }, Item(id)))
}
pub fn drop_self(self, state: &mut dyn State) {
self.drop_bindings_priv(state);
let items: &mut Items = state.get_mut();
items.0.remove(self.0);
}
}
impl_dep_obj!(Item {
fn<ItemProps>() -> (ItemProps) { Items | .props }
});
#[derive(Debug, Stop)]
pub struct Items(Arena<ItemComponent>);
impl SelfState for Items { }
impl Items {
pub fn new() -> Items {
Items(Arena::new())
}
}
dep_type! {
#[derive(Debug)]
pub struct ItemProps = Item[ItemProps] {
name: Cow<'static, str> = Cow::Borrowed(""),
base_weight: f32 = 0.0,
weight: f32 = 0.0,
equipped: bool = false,
cursed: bool = false,
}
}
}
```
For now `Item` does not have any meaningful behavior. Lets add some.
```rust
pub fn new(state: &mut dyn State) -> Item {
let items: &mut Items = state.get_mut();
let item = items.0.insert(|id| (ItemComponent { props: ItemProps::new_priv() }, Item(id)));
item.bind_weight(state);
item
}
fn bind_weight(self, state: &mut dyn State) {
let weight = Binding3::new(state, (), |(), base_weight, cursed, equipped| Some(
if equipped && cursed { base_weight + 100.0 } else { base_weight }
));
ItemProps::WEIGHT.bind(state, self, weight);
weight.set_source_1(state, &mut ItemProps::BASE_WEIGHT.value_source(self));
weight.set_source_2(state, &mut ItemProps::CURSED.value_source(self));
weight.set_source_3(state, &mut ItemProps::EQUIPPED.value_source(self));
}
```
With the code above we have created functional dependency between four `Item`
properties, and now `weight` being a function of other three properties
will be updated automatically when any of them changes.
Finally, lets write some test code to make our just built game system work:
```rust
fn track_weight(state: &mut dyn State, item: Item) {
let weight = Binding2::new(state, (), |(), name, weight: Option<Change<f32>>|
weight.map(|weight| (name, weight.new))
);
weight.set_target_fn(state, (), |_state, (), (name, weight)| {
println!("\n{name} now weights {weight}.");
});
item.add_binding::<ItemProps, _>(state, weight);
weight.set_source_1(state, &mut ItemProps::NAME.value_source(item));
weight.set_source_2(state, &mut ItemProps::WEIGHT.change_source(item));
}
fn run(state: &mut dyn State) {
let the_item = Item::new(state);
track_weight(state, the_item);
ItemProps::NAME.set(state, the_item, Cow::Borrowed("The Item")).immediate();
println!("\n> the_item.base_weight = 5.0");
ItemProps::BASE_WEIGHT.set(state, the_item, 5.0).immediate();
println!("\n> the_item.cursed = true");
ItemProps::CURSED.set(state, the_item, true).immediate();
println!("\n> the_item.equipped = true");
ItemProps::EQUIPPED.set(state, the_item, true).immediate();
println!("\n> the_item.cursed = false");
ItemProps::CURSED.set(state, the_item, false).immediate();
the_item.drop_self(state);
}
```
And the really last thing to do: construct `State` instance and call `run`.
Our system requires `State` containing `Items` and special arena for bindings.
It can be easily achieved with `merge_mut_and_then` method, combining two
state objects into a single one. And, of course, we should not forget to
call `Items::stop` at the end:
```rust
fn main() {
(&mut Items::new()).merge_mut_and_then(|state| {
run(state);
Items::stop(state);
}, &mut Bindings::new());
}
```
## Example: builders
When you need to setup initial values for just constructed object,
it is boring to call `Type::PROP.set(state, ...).immediate()` many times.
The `dep-obj` has a tool for avoid it: object builders.
It is very simple to enable it in the project:
```rust
impl Item {
with_builder!(ItemProps);
}
```
This macro declares function `build`, which can be used in the following way:
```rust
the_item.build(state, |props| props
.name(Cow::Borrowed("The Item"))
.base_weight(5.0)
.cursed(true)
);
```
## Example: dynamically typed dependency object
Lets add some properties, which are not universal for all `Item`s.
To do it we need to use another library: [`downcast-rs`](https://crates.io/crates/downcast-rs).
Using this crate, lets define base trait for extended properties dependency type:
```rust
pub trait ItemObj: Downcast + DepType<Id=Item> { }
impl_downcast!(ItemObj);
```
We need a new field in the component:
```rust
macro_attr! {
#[derive(Debug, Component!(stop=ItemStop))]
struct ItemComponent {
props: ItemProps,
obj: Box<dyn ItemObj>,
}
}
```
Modified `Item` constructor:
```rust
pub fn new(state: &mut dyn State, obj: Box<dyn ItemObj>) -> Item {
let items: &mut Items = state.get_mut();
let item = items.0.insert(|id| (ItemComponent {
props: ItemProps::new_priv(),
obj
}, Item(id)));
item.bind_weight(state);
item
}
```
And the way to access an object (`impl_dep_obj` handles
all dirty work including downcasting):
```rust
impl_dep_obj!(Item {
fn<ItemProps>() -> (ItemProps) { Items | .props }
fn<ItemObjKey>() -> dyn(ItemObj) { Items | .obj }
});
```
Base part is done, and we are ready to code `ItemObj` specific variants:
```rust
mod weapon {
use dep_obj::dep_type;
use dep_obj::binding::Binding3;
use dyn_context::State;
use crate::items::*;
dep_type! {
#[derive(Debug)]
pub struct Weapon = Item[ItemObjKey] {
base_damage: f32 = 0.0,
damage: f32 = 0.0,
}
}
impl ItemObj for Weapon { }
impl Weapon {
#[allow(clippy::new_ret_no_self)]
pub fn new(state: &mut dyn State) -> Item {
let item = Item::new(state, Box::new(Self::new_priv()));
Self::bind_damage(state, item);
item
}
fn bind_damage(state: &mut dyn State, item: Item) {
let damage = Binding3::new(state, (), |(), base_damage, cursed, equipped| Some(
if equipped && cursed { base_damage / 2.0 } else { base_damage }
));
Weapon::DAMAGE.bind(state, item, damage);
damage.set_source_1(state, &mut Weapon::BASE_DAMAGE.value_source(item));
damage.set_source_2(state, &mut ItemProps::CURSED.value_source(item));
damage.set_source_3(state, &mut ItemProps::EQUIPPED.value_source(item));
}
}
}
```