acts 0.6.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`], [`Job`], [`Branch`], [`Step`] and [`Act`]. Every workflow can have more jobs, every job can have 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
jobs:
  - id: job1
    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:
jobs:
  - id: job1
    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") }
jobs:
  - id: job1
    steps:
      - name: step1
      - name: step2
      - name: step3
```

### Jobs


Use `jobs` to add multiple job to a workflow, the job and run concurrently. Or set it one by one running orderly by use the property `needs`
```yml
name: model name
jobs:
  - id: job1
  - id: job2
    needs: [ "job1" ]
```
### Steps
Use `steps` to add step to the job
```yml
name: model name
jobs:
  - id: job1
    steps:
      - id: step1
        name: step 1
      - id: step2
        name: step 2
```

### Branches

Use `branches` to add branch to the step
```yml
name: model name
jobs:
  - id: job1
    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:
jobs:
  - id: job1
    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
jobs:
  - id: job1
    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
jobs:
  - name: job1
    id: job1
    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
```