# ALF
## Flow-Based Application Layer Framework
#### ALF is a set of abstractions built on top Elixir GenStage which allows writing program following [Flow-Based Programming](https://en.wikipedia.org/wiki/Flow-based_programming) approach.
#### ALF is a successor of the [Flowex](https://github.com/antonmi/flowex) project. Check its Readme to get the general idea. ALF adds conditional branching, packet cloning, goto statement, and other functionalities. Therefore, one can create application trees (graphs) of arbitrary complexity.
## Installation
Just add `:alf` as dependency to the `mix.exs` file.
ALF starts its own supervisor (`ALF.DynamicSupervisor`). All the pipelines and managers are started under the supervisor
## Quick start
Read a couple of sections of [Flowex README](https://github.com/antonmi/flowex#readme) to get the basic idea.
### Define your pipeline
```elixir
defmodule ThePipeline do
use ALF.DSL
@components [
stage(:add_one),
stage(:mult_by_two),
stage(:minus_three)
]
def add_one(datum, _opts), do: datum + 1
def mult_by_two(datum, _opts), do: datum * 2
def minus_three(datum, _opts), do: datum - 3
end
```
### Start the pipeline
```elixir
:ok = ALF.Manager.start(ThePipeline)
```
This starts a manager (GenServer) with `ThePipeline` name. The manager starts all the components and puts them under another DynamicSupervisor supervision tree.
### Use the pipeline
The only interface currently is the `stream_to` function (`stream_to/2` and `stream_to/3`).
It receives a stream or `Enumerable.t` and returns another stream where results will be streamed.
```elixir
inputs = [1,2,3]
output_stream = Manager.stream_to(inputs, Pipeline)
Enum.to_list(output_stream) # it returns [1, 3, 5]
```
Check [test/examples](https://github.com/antonmi/ALF/tree/main/test/examples) folder for more examples
## Components overview
### Stage
Stage is the main component where one puts a piece of application logic. It might be a simple 2-arity function or a module with `call/2` function:
```elixir
stage(:my_fun, opts: %{foo: bar})
# or
stage(:MyComponent, opts: %{})
```
where `MyComponent` is
```elixir
defmodule MyComponent do
# optional
def init(opts), do: %{opts | foo: :bar}
def call(datum, opts) do
# logic is here
new_datum
end
end
```
### Switch
Switch allows to forward IP (information packets) to different branches:
```elixir
switch(:my_switch,
branches: %{
part1: [stage(:foo)],
part2: [stage(:bar)]
},
cond: :cond_function
opts: [foo: :bar]
)
```
The `cond` function is 2-arity function that must return the key of the branch:
```elixir
def cond_function(datum, opts) do
if datum == opts[:foo], do: :part1, else: part: 2
end
```
### Clone
Clones an IP, useful for background actions.
```elixir
clone(:my_clone, to: [stage(:foo), dead_end(:dead_end)])
```
### Goto
Send packet to a given `goto_point`
```elixir
goto(:my_goto, to: :my_goto_point, if: :goto_function, opts: [foo: :bar])
```
The `if` function is 2-arity function that must return `true` of `false`
```elixir
def goto_function(datum, opts) do
datum == opts[:foo]
end
```
### GotoPoint
The `Goto` component companion
```elixir
goto_point(:goto_point)
```
### DeadEnd
IP won't propagate further. It's used alongside with the `Clone` component to avoid duplicate IPs in output
```elixir
dead_end(:dead_end)
```
## Components / Pipeline reusing
### `stages_from` macro
One can easily include components from another pipeline:
```elixir
defmodule ReusablePipeline do
use ALF.DSL
@components [
stage(:foo),
stage(:bar)
]
end
defmodule ThePipeline do
use ALF.DSL
@components stages_from(ReusablePipeline) ++ [stage(:baz)]
end
```
### `plug_with` macro
Use the macro if you include other components that expect different type/format/structure of input data.
```elixir
defmodule ThePipeline do
use ALF.DSL
@components [
plug_with(AdapterModuleBaz, do: [stage(:foo), stage(:bar)])
] ++
plug_with(AdapterModuleForReusablePipeline) do
stages_from(ReusablePipeline)
end
end
```
`plug_with` adds `Plug` component before the components in the block and `Unplug` at the end.
The first argument is an "adapter" module which must implement the `plug/2` and `unplug/3` functions
```elixir
def AdapterModuleBaz do
def init(opts), do: opts # optional
def plug(datum, _opts) do
# the function is called inside the `Plug` component
# `datum` will be put on the "AdapterModuleBaz" until IP has reached the "unplug"
# the function must return `new_datum` with the structure expected by the following component
new_datum
end
def unplug(datum, prev_datum, _opts) do
# here one can access previous "datum" in `prev_datum`
# transform the data back for the following components
end
end
```