acts 0.7.0

a fast, tiny, extensiable workflow engine
Documentation
# Acts workflow engine

`acts` is a fast, tiny, extensiable workflow engine, which provides the abilities to execute workflow based on yml model.

The yml workflow model is not as same as the tranditional workflow flow. such as bpmn.  The yml format is inspired by Github actions.  The main point of this workflow is to create a top abstraction to run the workflow logic and interact with the client via `act` node.

Every user's action can be regarded as a abstract act. these acts can be generated by some rules. such as `for` or `catches`. 

This workflow engine focus on the workflow logics itself and message distributions. the complex business logic will be completed by `act` via the act message. 

## Key Features

### Fast

Uses rust to create the lib, there is no virtual machine, no db dependencies. The feature local_store uses the rocksdb to make sure the store performance. 

Running benches\workflow.rs
start_workflow          time:   [842.58 µs 876.99 µs 912.59 µs]

### Tiny

The lib size is only 3.5mb (no local_store), you can use Adapter to create external store.

### Extensiable

Supports for extending the plugin
Supports for creating external store

## Installation

The easiest way to get the latest version of `acts` is to install it via `cargo`
```bash
cargo add acts
```

## Quickstart

1. Start the workflow engine by `engine.start`.
2. Load a yaml model to create a `workflow`. 
3. Deploy the model in step 2 by `engine.manager()`.
4. Config events by `engine.emitter()`.
5. Start the workflow by `engine.executor()`.

```rust
use acts::{Engine, Vars, Workflow};

#[tokio::main]

async fn main() {
    let engine = Engine::new();
    engine.start();

    let text = include_str!("../examples/simple/model.yml");
    let mut workflow = Workflow::from_yml(text).unwrap();

    let executor = engine.executor();
    engine.manager().deploy(&workflow).expect("fail to deploy workflow");

    let mut vars = Vars::new();
    vars.insert("input".into(), 3.into());
    vars.insert("pid".to_string(), "w1".into());
    executor.start(&workflow.id, &vars);
    let emitter = engine.emitter();

    emitter.on_start(|e| {
        println!("start: {}", e.start_time);
    });

    emitter.on_message(|e| {
        println!("message: {:?}", e);
    });

    emitter.on_complete(|e| {
        println!("outputs: {:?} end_time: {}", e.outputs(), e.end_time);
    });

    emitter.on_error(|e| {
        println!("error on proc id: {} model id: {}", e.pid, e.mid);
    });
}
```

## Examples


Please see [`examples`](<https://github.com/yaojianpin/acts/tree/main/examples>)

## Model Usage


The model uses the yaml file to create, there are different type of node, which is constructed by [`Workflow`], [`Branch`], [`Step`] and [`Act`]. Every workflow can have more more steps, a step can have more branches and a branch can have `if` property to judge the condition.

The `env` property can be set the initialzed vars in `workflow`, in the step's `run` scripts, you can use `env` moudle to get(`env.get`) or set(`env.set`) the value

The `run` property is the script based on [rhai script](https://github.com/rhaiscript/rhai)

```yml
name: model name
env:
  value: 0
steps:
  - name: step 1
    run: |
      print("step 1")

  - name: step 2
    branches:
      - name: branch 1
        if: ${ env.get("value") > 100 }
        run: |
            print("branch 1");

      - name: branch 2
        if: ${ env.get("value") <= 100 }
        steps:
            - name: step 3
              run: |
                print("branch 2")      
```
### Outputs

In the [`Workflow`], you can set the `outputs` to output the env to use.
```yml
name: model name
outputs:
  output_key:
steps:
  - name: step1
    run: |
      env.set("output_key", "output value");
```

### Actions

Add workflow `actions` to create custom event with client
```yml
name: model name
actions:
  - name: fn1
    id: fn1
    on: 
      - state: created
        nkind: workflow
      - state: completed
        nkind: workflow
  - name: fn2
    id: fn2
    on: 
      - state: completed
        nid: step2

  - name: fn3
    id: fn3
    on: 
      - state: completed
        nid: step3
    inputs:
      a: ${ env.get("value") }
steps:
  - name: step1
  - name: step2
  - name: step3
```

### Steps
Use `steps` to add step to the workflow
```yml
name: model name
steps:
  - id: step1
    name: step 1
  - id: step2
    name: step 2
```

### Branches

Use `branches` to add branch to the step
```yml
name: model name
steps:
  - id: step1
    name: step 1
    branches:
      - id: b1
        if: env.get("v") > 0
        steps: 
          - name: step a
          - name: step b
      - id: b2
        else: true
        steps:
          - name: step c
          - name: step d
  - id: step2
    name: step 2

```

### Acts

Use `acts` to create act to interact with client
```yml
name: model name
outputs:
  output_key:
steps:
  - name: step1
    acts:
      - id: init
        name: my act init
        inputs:
          a: 6
        outputs:
          c:
```

#### 1. for
There is a example to use `for` to generate acts, which can wait util calling the action to complete.
```yml
name: model name
steps:
  - name: step1
    acts:
      - for:
          by: any
          in: |
            let a = ["u1"];
            let b = ["u2"];
            a.union(b)
```
It will generate the user act and send message automationly according to the `in` collection.
The `by` tells the workflow how to pass the act. There are several `by` rules.

* by
1. **all** to match all of the acts to complete

2. **any** to match any of the acts to complete

3. **some(rule)** to match some acts by giving rule name. If there is some rule, it can also generate a some act to ask the client to pass or not.

4. **ord** or **ord(rule)** to generate the act one by one. If there is order rule, it can also generate a rule act to sort the collection.

* in
A collection to generate the acts.

The code `act.role("test_role")` uses the role rule to get the users through the role `test_role`
```yml
in: |
    let users = act.role("test_role");
    users
```
The following code uses the `relate` rule to find the user's owner of the department (`d.owner`).
```yml
users: |
    let users = act.relate("user(test_user).d.owner");
    users
```

#### 2. catches

Use the `catches` to capture the `act` error and start a new act to run.
```yml
name: a example to catch act error
id: catches
steps:
  - name: prepare
    id: prepare
    acts:
      - id: init
  - name: step1
    id: step1
    acts:
      - id: act1
        catches:
          - id: catch1
            err: err1
          - id: catch2
            err: err2
          - id: catch_others
  - name: final
    id: final
```