Skip to content

Instantly share code, notes, and snippets.

@hl
Last active May 7, 2024 09:53
Show Gist options
  • Save hl/4c23592cf9e86a8821041df6cd84533e to your computer and use it in GitHub Desktop.
Save hl/4c23592cf9e86a8821041df6cd84533e to your computer and use it in GitHub Desktop.
defmodule Engine do
@moduledoc """
Engine that can run a series of stages.
It will return a tuple containing the last effect and all the effects.
## Example
defmodule HelloWorld do
import Engine
def greeting(name) do
new()
|> run(:greeting, &construct_greeting/2)
|> run(:greeting_and_name, &add_name_to_greeting/2)
|> execute(%{name: name})
end
def construct_greeting(_effects_so_far, _attrs) do
{:ok, "Hello, {{name}}!"}
end
def add_name_to_greeting(%{greeting: greeting}, %{name: name}) do
{:ok, String.replace(greeting, "{{name}}", name)}
end
end
iex> HelloWorld.greeting("Jane")
{:ok, "Hello, Jane!", %{greeting: "Hello, {{name}}!", greeting_and_name: "Hello, Jane!"}}
"""
defstruct stages: []
@type t :: %__MODULE__{stages: stages()}
@type stages :: Keyword.t()
@type last_effect :: term()
@type effects_so_far :: %{atom() => last_effect()}
@type result :: {:ok, last_effect(), effects_so_far()} | {:error, term()}
@type effect_result :: {:ok, term()} | {:error, term()}
@spec new() :: t()
def new, do: %__MODULE__{}
@spec run(t(), atom(), fun()) :: Token.t()
def run(%__MODULE__{} = token, key, fun) when is_atom(key) and is_function(fun) do
%{token | stages: [{key, fun} | token.stages]}
end
@spec execute(t(), term()) :: result()
def execute(%__MODULE__{stages: stages}, attrs) do
stages
|> Enum.reverse()
|> Enum.reduce_while({_last_effect = nil, _effects_so_far = %{}}, fn
{key, fun}, {_last_effect, effects_so_far} ->
case fun.(effects_so_far, attrs) do
{:ok, last_effect} ->
effects_so_far = Map.put(effects_so_far, key, last_effect)
{:cont, {last_effect, effects_so_far}}
{:error, error} ->
{:halt, {:error, error}}
end
end)
|> then(fn
{:error, error} -> {:error, error}
{last_effect, effects_so_far} -> {:ok, last_effect, effects_so_far}
end)
end
end
defmodule Payroll do
import Engine
@data [
%{user: "Joe", salary: 2000},
%{user: "Jane", salary: 3000}
]
@rules [
%{"name" => "add", "value" => 100}
]
@contracts []
@spec call(list(), list(), list()) :: Engine.result()
def call(data \\ @data, rules \\ @rules, contracts \\ @contracts) do
new()
|> run(:rules_map, &generate_rules_map/2)
|> run(:salaries, &add_to_salaries/2)
|> execute(%{data: data, rules: rules, contracts: contracts})
end
@spec generate_rules_map(Engine.effects_so_far(), map()) :: Engine.effect_result()
def generate_rules_map(_effects_so_far, attrs) do
%{rules: rules} = attrs
{:ok, Map.new(rules, &{&1["name"], &1})}
end
@spec add_to_salaries(Engine.effects_so_far(), map()) :: Engine.effect_result()
def add_to_salaries(effects_so_far, attrs) do
%{rules_map: %{"add" => rule}} = effects_so_far
%{data: data} = attrs
{:ok,
Enum.map(data, fn row ->
%{row | salary: row.salary + rule["value"]}
end)}
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment