Current section
Files
Jump to
Current section
Files
lib/component.ex
defmodule Virta.Component do
@moduledoc """
The component specification.
It provides the behaviour and `__using__` macro which helps to create a component. For example:
```elixir
defmodule Add do
@inports [ :addend, :augend ]
@outports [ :sum ]
use Virta.Component
@impl true
def run(request_id, inport_args, _outport_args, _instance_pid) do
value = Map.get(inport_args, :augend) + Map.get(inport_args, :addend)
{ request_id, :reply, %{ sum: value } }
end
end
```
A Component is simply a module which implements the `Virta.Component` behaviour such that it
allows Virta to run it.
#### Concept
A component communicates with other components using ports. There are two type of ports:
* In-ports
* Out-ports
The component waits for message to arrive on each inport. Once all the messages are received, it
executes the component functional logic.
Once the execution is done, the messages are passed on to the outports (if any).
#### Implementation
Each component must publish a list of inports and outports.
The `loop` method maintains the state of the component, listens to the incoming messages and once
all the messages arrive over the inports and invokes the `run` method, which executes the
component logic and returns a token. Which is received by the `dispatch`.
The token can be of two types:
* `{ request_id, :noreply }` - if the component doesn't have any outports.
* `{ request_id, :reply, value }` - if the component has outports.
The dispatch handles dispatching the respective messages to the outports.
`Virta.Component` provides a `__using__` macro which defines the inports, outports, loop and
dispatch methods. It can be used for most of the use cases. `Virta` also provides three special
components which overrides the loop method implemented by the `__using__` macro.
* `Virta.Core.In`
* `Virta.Core.Out`
* `Virta.Core.Workflow`
Check the documentation of the above components to know more.
"""
@doc """
Returns a list of inports
#### This is the method defined in __using__ macro
```elixir
def inports, do: @inports
```
"""
@callback inports :: [atom]
@doc """
Returns a list of outports
#### This is the method defined in __using__ macro
```elixir
def outports, do: @outports
```
"""
@callback outports :: [atom]
@doc """
Dispatches the repsonses to the components connected over the outports
The token returned by the `run` method is received by dispatch:
* `{ request_id, :noreply }` - if the component doesn't have any outports.
* `{ request_id, :reply, value }` - if the component has outports.
The dispatch handles dispatching the respective messages to the outports.
The second argument contains a `List` of outport configs, which contains information required for
passing the output over the connections.
#### This is the method defined in __using__ macro
```elixir
# Dispatch for a component with no outports will be
def dispatch({ request_id, :noreply }, outport_args) do
unless length(outport_args) == 0 do
raise ":reply expected"
end
end
def dispatch({ request_id, :reply, args }, outport_args) do
Enum.map(outport_args, fn(outport_arg) ->
%{ pid: pid, to: to, from: from } = outport_arg
send(pid, { request_id, to, Map.get(args, from) })
end)
end
```
"""
@callback dispatch({any(), :noreply} | {any(), :reply, any()}, %{}) :: any
@doc """
Maintains the state of the component, listens to the incoming messages and once all the messages
arrive over the inports, it invokes the `run` method.
The first argument is an empty Map which stores the value received over the inports with inport
as the key and its respective values.
The second argument contains a `List` of outport configs, which contains information required for
passing the output over the connections.
The third argument contains a pid of the process that spawned this process.
#### This is the method defined in __using__ macro
```elixir
def loop(inport_args, outport_args, instance_pid) do
receive do
{ request_id, port, value } when port in @inports ->
inport_args = Map.put(inport_args, port, value)
if(@inports |> Enum.all?(&(Map.has_key?(inport_args, &1)))) do
run(request_id, inport_args, outport_args, instance_pid)
|> dispatch(outport_args)
loop(%{}, outport_args, instance_pid)
else
loop(inport_args, outport_args, instance_pid)
end
end
end
```
"""
@callback loop(%{}, %{}, pid) :: any
@doc """
Contains the logic of the component. Once executed, returns a token which is received by the
`dispatch` method.
The first argument is the request_id
The second argument contains a `List` of outport configs, which contains information required for
passing the output over the connections.
The second argument contains a `Map` which contains the messages passed to components with the
port name as the key.
The second argument contains a `List` of outport configs, which contains information required for
passing the output over the connections.
The fourth argument contains a pid of the process that spawned this process.
Once the execution is done, the following is returned
* `{ request_id, :noreply }` - if the component doesn't have any outports.
* `{ request_id, :reply, value }` - if the component has outports.
The value is a Map with keys as the outport port name with its respective values.
#### Sample without outports
```elixir
def run(request_id, inport_args, _outport_args, _instance_pid) do
value = Map.get(inport_args, :data)
IO.inspect(value)
{ request_id, :noreply }
end
```
#### Sample with outports
The following example has two outports :sum, :product
```elixir
def run(request_id, inport_args, _outport_args, _instance_pid) do
sum = Map.get(inport_args, :augend) + Map.get(inport_args, :addend)
product = Map.get(inport_args, :augend) * Map.get(inport_args, :addend)
{ request_id, :reply, %{ sum: sun, product: product } }
end
```
"""
@callback run(any, %{}, %{}, pid) :: { any, :noreply } | { any, :reply, any }
defmacro __using__(_) do
quote do
@behaviour Virta.Component
@impl true
def inports, do: @inports
@impl true
def outports, do: @outports
@impl true
def loop(inport_args, outport_args, instance_pid) do
receive do
{ request_id, port, value } when port in @inports ->
inport_args = Map.put(inport_args, port, value)
if(@inports |> Enum.all?(&(Map.has_key?(inport_args, &1)))) do
run(request_id, inport_args, outport_args, instance_pid)
|> dispatch(outport_args)
loop(%{}, outport_args, instance_pid)
else
loop(inport_args, outport_args, instance_pid)
end
end
end
@impl true
def dispatch({ request_id, :noreply }, outport_args) do
unless length(outport_args) == 0 do
raise ":reply expected"
end
end
@impl true
def dispatch({ request_id, :reply, args }, outport_args) do
Enum.map(outport_args, fn(outport_arg) ->
%{ pid: pid, to: to, from: from } = outport_arg
send(pid, { request_id, to, Map.get(args, from) })
end)
end
defoverridable loop: 3
end
end
end