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

#46 in Caching

Download history 411/week @ 2024-09-30 218/week @ 2024-10-07 16/week @ 2024-10-14

645 downloads per month

MIT license

43KB
646 lines

Stateflow: A State Machine Library in Rust

License Crates.io Rust version CI

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 and RwLock 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

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

  1. Add Dependency

    Add the following to your Cargo.toml:

    [dependencies]
    stateflow = "0.4.0"
    
  2. 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, and validations.
  • Transitions: Specify from state, event triggering the transition, to state, any actions, and validations.
  • Actions: Each action includes an action_type and a command, 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!

  1. Fork the Repository

  2. Create a Feature Branch

    git checkout -b feature/YourFeature
    
  3. Commit Your Changes

  4. Push to Your Branch

    git push origin feature/YourFeature
    
  5. 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
~335K SLoC