4 releases (breaking)
new 0.4.0 | Oct 23, 2024 |
---|---|
0.3.0 | Oct 10, 2024 |
0.2.0 | Oct 1, 2024 |
0.1.0 | Sep 30, 2024 |
#45 in Caching
800 downloads per month
43KB
646 lines
Stateflow: A State Machine Library in Rust
A simple and extensible state machine library in Rust.
Features
- Configuration Caching: Parsed JSON configurations are cached in memory using an LRU cache, improving performance by avoiding redundant parsing and validation when the same configuration is used multiple times. The cache size can be configured via an environment variable.
- JSON Configuration: Define states, events, transitions, actions, and validations via a JSON file.
- On-Enter and On-Exit Actions: Execute specific actions when entering or exiting a state.
- Transition Actions: Perform actions during state transitions.
- Asynchronous Action Handling: Support for asynchronous action execution.
- Custom Action Handler: Implement your own logic for handling actions, with access to both memory and custom context.
- Thread-Safe: Designed with
Arc
andRwLock
for safe concurrent use. - State Persistence: Save and restore the current state for persistent workflows.
- Display Trait Implementation: Visualize the state machine's structure via the
Display
trait. - Data-Driven Validations: Define validation rules in the configuration to enforce constraints on memory.
- Conditional Validations: Apply validations conditionally based on memory values.
- Custom Context Support: Pass a custom context object to the state machine, accessible in the action handler.
Table of Contents
- Introduction
- Installation
- Usage
- Configuration
- Contributing
- License
- Credits
- Support
- Changelog
- Roadmap
- FAQ
- Code of Conduct
Introduction
This project provides a simple and extensible state machine library in Rust. It allows for defining states, transitions, actions, and validations triggered during state changes. The state machine configuration can be loaded from a JSON file, and custom actions can be executed on state transitions using a user-defined action handler. The state machine also supports custom context objects, allowing you to maintain additional state or data throughout the state machine's lifecycle.
- Why this project? State machines are essential for managing complex state-dependent logic. This library simplifies the creation and management of state machines in Rust applications, providing flexibility and extensibility.
Installation
Prerequisites
- Rust: Make sure you have Rust installed. You can install it from rustup.rs.
Steps
-
Add Dependency
Add the following to your
Cargo.toml
:[dependencies] stateflow = "0.4.0"
-
Update Crates
Run:
cargo update
Usage
Here's how to integrate the state machine into your project.
1. Define Your Configuration
Create a config.json
file:
{
"states": [
{
"name": "Idle",
"on_enter_actions": [],
"on_exit_actions": [],
"validations": []
},
{
"name": "Processing",
"on_enter_actions": [],
"on_exit_actions": [],
"validations": []
},
{
"name": "Finished",
"on_enter_actions": [],
"on_exit_actions": [],
"validations": []
}
],
"transitions": [
{
"from": "Idle",
"event": "start",
"to": "Processing",
"actions": [],
"validations": []
},
{
"from": "Processing",
"event": "finish",
"to": "Finished",
"actions": [],
"validations": []
}
]
}
2. Implement the Action Handler
Create an asynchronous function to handle actions, with access to both the memory and your custom context:
use stateflow::{Action, StateMachine};
use serde_json::{Map, Value};
struct MyContext {
// Your custom context fields
counter: i32,
}
async fn action_handler(
action: &Action,
memory: &mut Map<String, Value>,
context: &mut MyContext,
) {
match action.action_type.as_str() {
"log" => println!("Logging: {}", action.command),
"increment_counter" => {
context.counter += 1;
println!("Counter incremented to {}", context.counter);
}
_ => eprintln!("Unknown action: {}", action.command),
}
}
3. Initialize the State Machine
use stateflow::StateMachine;
use serde_json::{Map, Value};
use std::fs;
#[tokio::main]
async fn main() -> Result<(), String> {
let config_content = fs::read_to_string("config.json")
.map_err(|e| format!("Failed to read config file: {}", e))?;
// Initialize memory (can be empty or pre-populated)
let memory = Map::new();
// Initialize your custom context
let context = MyContext { counter: 0 };
let state_machine = StateMachine::new(
&config_content,
Some("Idle".to_string()),
|action, memory, context| Box::pin(action_handler(action, memory, context)),
memory,
context,
)?;
// Trigger events
state_machine.trigger("start").await?;
state_machine.trigger("finish").await?;
// Access the context after transitions
{
let context = state_machine.context.read().await;
println!("Final counter value: {}", context.counter);
}
Ok(())
}
4. Run Your Application
Compile and run your application:
cargo run
Note
Setting the Environment Variable:
To configure the cache size, set the STATEFLOW_LRU_CACHE_SIZE
environment variable before running your application.
export STATEFLOW_LRU_CACHE_SIZE=200
If not set, the cache size defaults to 100
.
Configuration
The state machine is highly configurable via a JSON file.
- States: Define each state's
name
,on_enter_actions
,on_exit_actions
, andvalidations
. - Transitions: Specify
from
state,event
triggering the transition,to
state, anyactions
, andvalidations
. - Actions: Each action includes an
action_type
and acommand
, which the action handler interprets. - Validations: Define validation rules to enforce constraints on memory fields, with optional conditions.
Example of a state with validations:
{
"name": "Processing",
"on_enter_actions": [],
"on_exit_actions": [],
"validations": [
{
"field": "age",
"rules": [
{ "type": "type_check", "expected_type": "number" },
{ "type": "min_value", "value": 18 }
]
}
]
}
Example of a transition with an action:
{
"from": "Idle",
"event": "start",
"to": "Processing",
"actions": [
{
"action_type": "log",
"command": "Transitioning to Processing state"
}
],
"validations": []
}
Contributing
We welcome contributions!
-
Fork the Repository
-
Create a Feature Branch
git checkout -b feature/YourFeature
-
Commit Your Changes
-
Push to Your Branch
git push origin feature/YourFeature
-
Open a Pull Request
For detailed guidelines, see CONTRIBUTING.md.
License
This project is licensed under the MIT License. See the LICENSE file for details.
Credits
- Serde: For serialization and deserialization.
- JSON Schema: For configuration validation.
- once_cell: For lazy static initialization.
- lru: For the LRU cache implementation
- Rust Community: For the rich ecosystem and support.
Support
If you have any questions or issues, please open an issue on GitHub.
Changelog
See CHANGELOG.md for version history.
Roadmap
- Visualization Tools: Generate visual diagrams of the state machine.
- Enhanced Error Handling: More descriptive errors and debugging tools.
- Extended Validations: Support for more complex validation rules.
- Integration Examples: Provide more examples and use cases.
FAQ
Q: Can I use this library in a multi-threaded environment?
A: Yes, the state machine is thread-safe using Arc
and RwLock
.
Q: How does the configuration caching work?
A: The state machine caches parsed configurations using an LRU cache. It uses a hash of the JSON configuration string to detect changes and invalidate cache entries, improving performance by avoiding redundant parsing and validation.
Q: How do I handle custom action types?
A: Implement your logic within the action_handler
function based on the action_type
.
Q: Can I pass my own context to the state machine?
A: Yes, you can pass a custom context of any type to the state machine, which is accessible in the action handler.
Code of Conduct
We expect all contributors to adhere to our Code of Conduct.
This README was updated to provide a comprehensive overview of the Stateflow library in Rust. We hope it helps you get started quickly!
Dependencies
~14–23MB
~336K SLoC