From ad701cb88a915a71ce010897ed57d4825b700fbd Mon Sep 17 00:00:00 2001 From: Colin Roberts Date: Mon, 5 Feb 2024 16:25:11 -0700 Subject: [PATCH] mdbook contributions for new crates (#845) docs(arbiter-engine / arbiter-macros): mdbook contributions for new crates and their usage. --- Cargo.lock | 2 +- .../src/examples/minter/token_minter.rs | 75 +++++++++++++++ .../src/examples/timed_message/config.toml | 6 ++ .../src/examples/timed_message/mod.rs | 4 + arbiter-macros/Cargo.toml | 1 - documentation/src/SUMMARY.md | 7 +- .../getting_started/setting_up_simulations.md | 2 +- documentation/src/usage/arbiter_engine.md | 5 - .../arbiter_engine/agents_and_engines.md | 58 +++++++++++ .../src/usage/arbiter_engine/behaviors.md | 95 +++++++++++++++++++ .../src/usage/arbiter_engine/configuration.md | 66 +++++++++++++ .../src/usage/arbiter_engine/index.md | 27 ++++++ .../arbiter_engine/worlds_and_universes.md | 85 +++++++++++++++++ documentation/src/usage/arbiter_macros.md | 5 + 14 files changed, 429 insertions(+), 9 deletions(-) create mode 100644 arbiter-engine/src/examples/minter/token_minter.rs delete mode 100644 documentation/src/usage/arbiter_engine.md create mode 100644 documentation/src/usage/arbiter_engine/agents_and_engines.md create mode 100644 documentation/src/usage/arbiter_engine/behaviors.md create mode 100644 documentation/src/usage/arbiter_engine/configuration.md create mode 100644 documentation/src/usage/arbiter_engine/index.md create mode 100644 documentation/src/usage/arbiter_engine/worlds_and_universes.md create mode 100644 documentation/src/usage/arbiter_macros.md diff --git a/Cargo.lock b/Cargo.lock index 0d78808a6..0010a7c11 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -314,7 +314,7 @@ dependencies = [ [[package]] name = "arbiter-macros" -version = "0.1.0" +version = "0.0.0" dependencies = [ "quote", "syn 2.0.48", diff --git a/arbiter-engine/src/examples/minter/token_minter.rs b/arbiter-engine/src/examples/minter/token_minter.rs new file mode 100644 index 000000000..e1df78e29 --- /dev/null +++ b/arbiter-engine/src/examples/minter/token_minter.rs @@ -0,0 +1,75 @@ +use std::{str::FromStr, time::Duration}; + +use agents::{token_admin::TokenAdmin, token_requester::TokenRequester}; +use arbiter_core::data_collection::EventLogger; +use arbiter_macros::Behaviors; +use ethers::types::Address; +use tokio::time::timeout; + +use super::*; +use crate::world::World; + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn token_minter_simulation() { + let mut world = World::new("test_world"); + let client = RevmMiddleware::new(&world.environment, None).unwrap(); + + // Create the token admin agent + let token_admin = Agent::builder(TOKEN_ADMIN_ID); + let mut token_admin_behavior = TokenAdmin::new(Some(4)); + token_admin_behavior.add_token(TokenData { + name: TOKEN_NAME.to_owned(), + symbol: TOKEN_SYMBOL.to_owned(), + decimals: TOKEN_DECIMALS, + address: None, + }); + // Create the token requester agent + let token_requester = Agent::builder(REQUESTER_ID); + let mut token_requester_behavior = TokenRequester::new(Some(4)); + world.add_agent(token_requester.with_behavior(token_requester_behavior)); + + world.add_agent(token_admin.with_behavior(token_admin_behavior)); + + let arb = ArbiterToken::new( + Address::from_str("0x240a76d4c8a7dafc6286db5fa6b589e8b21fc00f").unwrap(), + client.clone(), + ); + let transfer_event = arb.transfer_filter(); + + let transfer_stream = EventLogger::builder() + .add_stream(arb.transfer_filter()) + .stream() + .unwrap(); + let mut stream = Box::pin(transfer_stream); + world.run().await; + let mut idx = 0; + + loop { + match timeout(Duration::from_secs(1), stream.next()).await { + Ok(Some(event)) => { + println!("Event received in outside world: {:?}", event); + idx += 1; + if idx == 4 { + break; + } + } + _ => { + panic!("Timeout reached. Test failed."); + } + } + } +} + +#[derive(Serialize, Deserialize, Debug, Behaviors)] +enum Behaviors { + TokenAdmin(TokenAdmin), + TokenRequester(TokenRequester), +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn config_test() { + let mut world = World::new("world"); + world.build_with_config::("src/examples/minter/config.toml"); + + world.run().await; +} diff --git a/arbiter-engine/src/examples/timed_message/config.toml b/arbiter-engine/src/examples/timed_message/config.toml index cd82e936c..8ef7e5ddd 100644 --- a/arbiter-engine/src/examples/timed_message/config.toml +++ b/arbiter-engine/src/examples/timed_message/config.toml @@ -1,5 +1,11 @@ [[ping]] TimedMessage = { delay = 1, send_data = "ping", receive_data = "pong", startup_message = "ping" } +[[ping]] +TimedMessage = { delay = 1, send_data = "zam", receive_data = "zim", startup_message = "zam" } + [[pong]] TimedMessage = { delay = 1, send_data = "pong", receive_data = "ping" } + +[[pong]] +TimedMessage = { delay = 1, send_data = "zim", receive_data = "zam" } diff --git a/arbiter-engine/src/examples/timed_message/mod.rs b/arbiter-engine/src/examples/timed_message/mod.rs index 86da65228..2fd858d25 100644 --- a/arbiter-engine/src/examples/timed_message/mod.rs +++ b/arbiter-engine/src/examples/timed_message/mod.rs @@ -16,6 +16,10 @@ fn default_max_count() -> Option { Some(3) } +fn default_max_count() -> Option { + Some(3) +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub(crate) struct TimedMessage { delay: u64, diff --git a/arbiter-macros/Cargo.toml b/arbiter-macros/Cargo.toml index 8ea92a884..3754898f2 100644 --- a/arbiter-macros/Cargo.toml +++ b/arbiter-macros/Cargo.toml @@ -1,6 +1,5 @@ [package] name = "arbiter-macros" -version = "0.1.0" [lib] proc-macro = true diff --git a/documentation/src/SUMMARY.md b/documentation/src/SUMMARY.md index 81d2ed47c..34d6472f8 100644 --- a/documentation/src/SUMMARY.md +++ b/documentation/src/SUMMARY.md @@ -10,7 +10,12 @@ - [Arbiter Core](./usage/arbiter_core/index.md) - [Environment](./usage/arbiter_core/environment.md) - [Middleware](./usage/arbiter_core/middleware.md) - - [Arbiter Engine](./usage/arbiter_engine.md) + - [Arbiter Engine](./usage/arbiter_engine/index.md) + - [Behaviors](./usage/arbiter_engine/behaviors.md) + - [Agents and Engines](./usage/arbiter_engine/agents_and_engines.md) + - [Worlds and Universes](./usage/arbiter_engine/worlds_and_universes.md) + - [Configuration](./usage/arbiter_engine/configuration.md) + - [Arbiter Macros](./usage/arbiter_macros.md) - [Techniques](./usage/techniques/index.md) - [Stateful Testing](./usage/techniques/stateful_testing.md) - [Anomaly Detection](./usage/techniques/anomaly_detection.md) diff --git a/documentation/src/getting_started/setting_up_simulations.md b/documentation/src/getting_started/setting_up_simulations.md index 5c9b6f475..2ec682d0f 100644 --- a/documentation/src/getting_started/setting_up_simulations.md +++ b/documentation/src/getting_started/setting_up_simulations.md @@ -38,7 +38,7 @@ The stage is now set for you to begin writing your simulation code. This will consist of the following: - Creating a [`Environment`](../usage/arbiter_core/environment.md) for your simulation. - Creating agents. -(TODO: More documentation here for [`arbiter-engine`](../usage/arbiter_engine.md)) +(TODO: More documentation here for [`arbiter-engine`](../usage/arbiter_engine/index.md)) - Creating a [`RevmMiddleware`](../usage/arbiter_core/middleware.md) for each agent in your simulation. - Deploy contracts using the binding's `MyContract::deploy()` method which will need a client `Arc` and constructor arguments passed as a tuple. Or, if you want to use a forked state, use the binding's `MyContract::new()` method and pass it the relevant client and address. diff --git a/documentation/src/usage/arbiter_engine.md b/documentation/src/usage/arbiter_engine.md deleted file mode 100644 index f52d104a0..000000000 --- a/documentation/src/usage/arbiter_engine.md +++ /dev/null @@ -1,5 +0,0 @@ -# Arbiter Engine - -WORK IN PROGRESS - NOT STABLE FOR USE YET. - -MORE WILL BE ADDED HERE SOON. \ No newline at end of file diff --git a/documentation/src/usage/arbiter_engine/agents_and_engines.md b/documentation/src/usage/arbiter_engine/agents_and_engines.md new file mode 100644 index 000000000..193f53c41 --- /dev/null +++ b/documentation/src/usage/arbiter_engine/agents_and_engines.md @@ -0,0 +1,58 @@ +# Agents and Engines +`Behavior`s are the heartbeat of your `Agent`s and they are wrapped by `Engine`s. +The main idea here is that you can have an `Agent` that has as many `Behavior`s as you like, and each of those behaviors may process different types of events. +This gives flexibility in how you want to design your `Agent`s and what emergent properties you want to observe. + +## Design Principles +It is up to you whether or not you prefer to have `Agent`s have multiple `Behavior`s or not or if you want them to have a single `Behavior` that processes all events. +For the former case, you will build `Behavior` for different types `E` and place these inside of an `Agent`. +For the latter, you will create an `enum` that wraps all the different types of events that you want to process and then implement `Behavior` on that `enum`. +The latter will also require a `stream::select` type of operation to merge all the different event streams into one, though this is not difficult to do. + +## `struct Agent` +The `Agent` struct is the primary struct that you will be working with. +It contains an ID, a client (`Arc`) that provides means to send calls and transactions to an Arbiter `Environment`, and a `Messager`. +It looks like this: +```rust +pub struct Agent { + pub id: String, + pub messager: Messager, + pub client: Arc, + pub(crate) behavior_engines: Vec>, +} +``` + +Your work will only be to define `Behavior`s and then add them to an `Agent` with the `Agent::with_behavior` method. + +The `Agent` is inactive until it is paired with a `World` and then it is ready to be run. +This is handled by creating a world (see: [Worlds and Universes](./worlds_and_universes.md)) and then adding the `Agent` to the `World` with the `World::add_agent` method. +Some of the intermediary representations are below: + +#### `struct AgentBuilder` +The `AgentBuilder` struct is a builder pattern for creating `Agent`s. +This is essentially invisible for the end-user, but it is used internally so that `Agent`s can be built in a more ergonomic way. + +#### `struct Engine` +Briefly, the `Engine` struct provides the machinery to run a `Behavior` and it is not necessary for you to handle this directly. +The purpose of this design is to encapsulate the `Behavior` and the event stream `Stream` that the `Behavior` will use for processing. +This encapsulation also allows the `Agent` to hold onto `Behavior` for various different types of `E` all at once. + +## Example +Let's create an `Agent` that has two `Behavior`s using the `Replier` behavior from before. +```rust +use arbiter_engine::agent::Agent; +use crate::Replier; + +fn setup() { + let ping_replier = Replier::new("ping", "pong", 5, None); + let pong_replier = Replier::new("pong", "ping", 5, Some("ping")); + let agent = Agent::new("my_agent") + .with_behavior(ping_replier) + .with_behavior(pong_replier); +} +``` +In this example, we have created an `Agent` with two `Replier` behaviors. +The `ping_replier` will reply to a message with "pong" and the `pong_replier` will reply to a message with "ping". +Given that the `pong_replier` has a `startup_message` of "ping", it will send a message to everyone (including the "my_agent" itself who holds the `ping_replier` behavior) when it starts up. +This will start a chain of messages that will continue in a "ping" "pong" fashion until the `max_count` is reached. +``` \ No newline at end of file diff --git a/documentation/src/usage/arbiter_engine/behaviors.md b/documentation/src/usage/arbiter_engine/behaviors.md new file mode 100644 index 000000000..dd51377e6 --- /dev/null +++ b/documentation/src/usage/arbiter_engine/behaviors.md @@ -0,0 +1,95 @@ +# Behaviors +The design of `arbiter-engine` is centered around the concept of `Agent`s and `Behavior`s. +At the core, we place `Behavior`s as the event-driven machinery that defines the entire simulation. +What we want is that your simulation is defined completely with how your `Agent`s behaviors are defined. +All you should be looking for is how to define your `Agent`s behaviors and what emergent properties you want to observe. + +## `trait Behavior` +To define a `Behavior`, you need to implement the `Behavior` trait on a struct of your own design. +The `Behavior` trait is defined as follows: +```rust +pub trait Behavior { + fn startup(&mut self, client: Arc, messager: Messager) -> EventStream; + fn process(&mut self, event: E) -> Option; +} +``` +To outline the design principles here: +- `startup` is a method that initializes the `Behavior` and returns an `EventStream` that the `Behavior` will use for processing. + - This method yields a client and messager from the `Agent` that owns the `Behavior`. + In this method you should take the client and messager and store them in your struct if you will need them in the processing of events. + Note, you may not need both or even either! +- `process` is a method that processes an event of type `E` and returns an `Option`. + - If `process` returns `Some(MachineHalt)`, then the `Behavior` will stop processing events completely. + +**Summary:** A `Behavior` is tantamount to engage the processing some events of type `E`. + +**Advice:** `Behavior`s should be limited in scope and should be a simplistic action driven from a single event. +Otherwise you risk having a `Behavior` that is too complex and difficult to understand and maintain. + +### Example +To see this in use, let's take a look at an example of a `Behavior` called `Replier` that replies to a message with a message of its own, and stops once it has replied a certain number of times. +```rust +use std::sync::Arc; +use arbiter_core::middleware::RevmMiddleware; +use arbiter_engine::{ + machine::{Behavior, MachineHalt}, + messager::{Messager, To}, + EventStream}; + +pub struct Replier { + receive_data: String, + send_data: String, + max_count: u64, + startup_message: Option, + count: u64, + messager: Option, +} + +impl Replier { + pub fn new( + receive_data: String, + send_data: String, + max_count: u64, + startup_message: Option, + ) -> Self { + Self { + receive_data, + send_data, + startup_message, + max_count, + count: 0, + messager: None, + } + } +} + +impl Behavior for Replier { + async fn startup( + &mut self, + client: Arc, + messager: Messager, + ) -> EventStream { + if let Some(startup_message) = &self.startup_message { + messager.send(To::All, startup_message).await; + } + self.messager = Some(messager.clone()); + return messager.stream(); + } + + async fn process(&mut self, event: Message) -> Option { + if event.data == self.receive_data { + self.messager.unwrap().messager.send(To::All, send_data).await; + self.count += 1; + } + if self.count == self.max_count { + return Some(MachineHalt); + } + return None + } +} +``` +In this example, we have a `Behavior` that upon `startup` will see if there is a `startup_message` assigned and if so, send it to all `Agent`s that are listening to their `Messager`. +Then, it will store the `Messager` for sending messages later on and start a stream of incoming messages so that we have `E = Message` in this case. +Once these are completed, the `Behavior` automatically transitions into the `process`ing stage where events are popped from the `EventStream` and fed to the `process` method. + +As messages come in, if the `receive_data` matches the incoming message, then the `Behavior` will send the `send_data` to all `Agent`s listening to their `Messager` a message with data `send_data`. \ No newline at end of file diff --git a/documentation/src/usage/arbiter_engine/configuration.md b/documentation/src/usage/arbiter_engine/configuration.md new file mode 100644 index 000000000..071145db1 --- /dev/null +++ b/documentation/src/usage/arbiter_engine/configuration.md @@ -0,0 +1,66 @@ +# Configuration +To make it so you rarely have to recompile your project, you can use a configuration file to set the parameters of your simulation once your `Behavior`s have been defined. +Let's take a look at how to do this. + +## Behavior Enum +It is good practice to take your `Behavior`s and wrap them in an `enum` so that you can use them in a configuration file. +For instance, let's say you have two struct `Maker` and `Taker` that implement `Behavior` for their own `E`. +Then you can make your `enum` like this: +```rust +use arbiter_macros::Behaviors; + +#[derive(Behaviors)] +pub enum Behaviors { + Maker(Maker), + Taker(Taker), +} +``` +Notice that we used the `Behaviors` derive macro from the `arbiter_macros` crate. +This macro will generate an implementation of a `CreateStateMachine` trait for the `Behaviors` enum and ultimately save you from having to write a lot of boilerplate code. +The macro solely requires that the `Behavior`s you have implement the `Behavior` trait and that the necessary imports are in scope. + +## Configuration File +Now that you have your `enum` of `Behavior`s, you can configure your `World` and the `Agent`s inside of it from configuration file. +Since the `World` and your simulation is completely defined by the `Agent` `Behavior`s you make, all you need to do is specify your `Agent`s in the configuration file. +For example, let's say we have the `Replier` behavior from before, so we have: +```rust +#[derive(Behaviors)] +pub enum Behaviors { + Replier(Replier), +} + +pub struct Replier { + receive_data: String, + send_data: String, + max_count: u64, + startup_message: Option, + count: u64, + messager: Option, +} +``` +Then, we can specify the "ping" and "pong" `Behavior`s like this: +```toml +[[my_agent]] +Replier = { send_data = "ping", receive_data = "pong", max_count = 5, startup_message = "ping" } + +[[my_agent]] +Replier = { send_data = "pong", receive_data = "ping", max_count = 5 } +``` +If you instead wanted to specify two `Agent`s "Alice" and "Bob" each with one of the `Replier` `Behavior`s, you could do it like this: +```toml +[[alice]] +Replier = { send_data = "ping", receive_data = "pong", max_count = 5, startup_message = "ping" } + +[[bob]] +Replier = { send_data = "pong", receive_data = "ping", max_count = 5 } +``` + +## Loading the Configuration +Once you have your configuration file located at `./path/to/config.toml`, you can load it and run your simulation like this: +```rust +fn main() { + let world = World::from_config("./path/to/config.toml")?; + world.run().await; +} +``` +At the moment, we do not configure `Universe`s from a configuration file, but this is a feature that is planned for the future. \ No newline at end of file diff --git a/documentation/src/usage/arbiter_engine/index.md b/documentation/src/usage/arbiter_engine/index.md new file mode 100644 index 000000000..e3104ad90 --- /dev/null +++ b/documentation/src/usage/arbiter_engine/index.md @@ -0,0 +1,27 @@ +# Arbiter Engine +`arbiter-engine` provides the machinery to build agent based / event driven simulations and should be the primary entrypoint for using Arbiter. +The goal of this crate is to abstract away the work required to set up agents, their behaviors, and the worlds they live in. +At the moment, all interaction of agents is done through the `arbiter-core` crate and is meant to be for local simulations and it is not yet generalized for the case of live network automation. + +## Heirarchy +The primary components of `arbiter-engine` are, from the bottom up: +- `Behavior`: This is an event-driven behavior that takes in some item of type `E` and can act on that. +The `Behavior` has two methods: `startup` and `process`. + - `startup` is meant to initialize the `Behavior` and any context around it. + An example could be an agent that deploys token contracts on startup. + - `process` is meant to be a stage that runs on every event that comes in. + An example could be an agent that deployed token contracts on startup, and now wants to process queries about the tokens deployed in the simulation (e.g., what their addresses are). +- `Engine` and `StateMachine`: The `Engine` is a struct that implements the `StateMachine` trait as an entrypoint to run `Behavior`s. + - `Engine` is a struct owns a `B: Behavior` and the event stream `Stream` that the `Behavior` will use for processing. + - `StateMachine` is a trait that reduces the interface to `Engine` to a single method: `execute`. + This trait allows `Agent`s to have multiple behaviors that may not use the same event type. +- `Agent` a struct that contains an ID, a client (`Arc`) that provides means to send calls and transactions to an Arbiter `Environment`, and a `Messager`. + - `Messager` is a struct that owns a `Sender` and `Receiver` for sending and receiving messages. + This is a way for `Agent`s to communicate with each other. + It can also be streamed and used for processing messages in a `Behavior`. + - `Agent` also owns a `Vec>` which is a list of `StateMachine`s that the `Agent` will run. + This is a way for `Agent`s to have multiple `Behavior`s that may not use the same event type. +- `World` is a struct that has an ID, an Arbiter `Environment`, a mapping of `Agent`s, and a `Messager`. + - The `World` is tasked with letting `Agent`s join in, and when they do so, to connect them to the `Environment` with a client and `Messager` with the `Agent`'s ID. +- `Universe` is a struct that wraps a mapping of `World`s. + - The `Universe` is tasked with letting `World`s join in and running those `World`s in parallel. diff --git a/documentation/src/usage/arbiter_engine/worlds_and_universes.md b/documentation/src/usage/arbiter_engine/worlds_and_universes.md new file mode 100644 index 000000000..3503ce2cf --- /dev/null +++ b/documentation/src/usage/arbiter_engine/worlds_and_universes.md @@ -0,0 +1,85 @@ +# Worlds and Universes +`Universes` are the top-level struct that you will be working with in the Arbiter Engine. +They are tasked with letting `World`s join in and running those `World`s in parallel. +By no means are you required to use `Universe`s, but they will be useful for running multiple simulations at once or, in the future, they will allow for running `World`s that have different internal environments. +For instance, one could have a `World` that consists of `Agent`s acting on the Ethereum mainnet, another `World` that consists of `Agent`s acting on Optimism, and finally a `World` that has an Arbiter `Environment` as the network analogue. +Using these in tandem is a long-term goal of the Arbiter project. + +Depending on your needs, you will either use the `Universe` if you want to run multiple `World`s in parallel or you will use the `World` if you only want to run a single simulation. +The choice is yours. + +## `struct Universe` +The `Universe` struct looks like this: +```rust +pub struct Universe { + worlds: Option>, + world_tasks: Option>>, +} +``` +The `Universe` is a struct that wraps a mapping of `World`s where the key of the map is the `World`'s ID. +Also, the `Universe` manages the running of those `World`s in parallel by storing the running `World`s as tasks. +In the future, more introspection and control will be added to the `Universe` to allow for debugging and managing the running `World`s. + +The `Universe::run_worlds` currently iterates through the `World`s and starts them in concurrent tasks. + +## `struct World` +The `World` struct looks like this: +```rust +pub struct World { + pub id: String, + pub agents: Option>, + pub environment: Environment, + pub messager: Messager, +} +``` +The `World` is a struct that has an ID, an Arbiter `Environment`, a mapping of `Agent`s, and a `Messager`. +The `World` is tasked with letting `Agent`s join in, and when they do so, to connect them to the `Environment` with a client and `Messager` with the `Agent`'s ID. +Then the `World` stores the `Agent`s in a map where the key is the `Agent`'s ID. + +The main methods to use with the world is `World::add_agent` which adds an agent to the `World` and `World::run` which will engage all of the `Agent` `Behavior`s. + +In future development, the `World` will be generic over your choice of `Provider` that encapsulates the Ethereum-like execution environment you want to use (e.g., Ethereum mainnet, Optimism, or an Arbiter `Environment`). + +## Example +Let's first do a quick example where we take a `World` and add an `Agent` to it. +```rust +use arbiter_engine::{agent::Agent, world::World}; +use crate::Replier; + +fn setup_world(id: &str) -> World { + let ping_replier = Replier::new("ping", "pong", 5, None); + let pong_replier = Replier::new("pong", "ping", 5, Some("ping")); + let agent = Agent::new("my_agent") + .with_behavior(ping_replier) + .with_behavior(pong_replier); + let mut world = World::new(id); + world.add_agent(agent); +} + +fn main() { + let world = setup_world("my_world"); + world.run().await; +} +``` +If you wanted to extend this to use a `Universe`, you would simply create a `Universe` and add the `World` to it. +```rust +use arbiter_engine::{agent::Agent, world::World}; +use crate::Replier; + +fn setup_world(id: &str) -> World { + let ping_replier = Replier::new("ping", "pong", 5, None); + let pong_replier = Replier::new("pong", "ping", 5, Some("ping")); + let agent = Agent::new("my_agent") + .with_behavior(ping_replier) + .with_behavior(pong_replier); + let mut world = World::new(id); + world.add_agent(agent); +} + +fn main() { + let mut universe = Universe::new(); + universe.add_world(setup_world("my_world")); + universe.add_world(setup_world("my_other_world")); + universe.run_worlds().await; +} +``` \ No newline at end of file diff --git a/documentation/src/usage/arbiter_macros.md b/documentation/src/usage/arbiter_macros.md new file mode 100644 index 000000000..ee85cd6b2 --- /dev/null +++ b/documentation/src/usage/arbiter_macros.md @@ -0,0 +1,5 @@ +# Arbiter macros +`arbiter_macros` provides a set of macros to help with the use of `arbiter-engine` and `arbiter-core`. +At the moment, we only have one proc macro: `#[derive(Behaviors)]`. +This macro will generate an implementation of a `CreateStateMachine` trait for the `Behaviors` enum and ultimately save you from having to write a lot of boilerplate code. +See the [Configuration](./arbiter_engine/configuration.md) page for more information on how to use this macro. \ No newline at end of file