Current section
Files
Jump to
Current section
Files
lib/piper.ex
defmodule Adap.Piper do
@moduledoc ~S"""
Piper proposes an implementation of `Adap.Stream.Emitter` where
the distributed processing of each element is defined as a
succession of matching rules.
Each rule can use external data to process the element or emit new
ones. When external data is needed, a process is spawned on the node
containing it, will receive the element and continue to apply rules.
The principle is to make each element hop from node to node in
order to be processed using the locally present data.
The element will go to the stream sink when no more rule matches.
The `Adap.Stream` stream data by chunk, so that the
construction of the external state server can take as much time as
necessary without congestion: never more than the chunk size number
of elements will be queued.
Let's see a processing pipe example:
- the input is a product stream : stream of `{:product,%{field1: value1, field2: value2}}`
- `user@jsonserver1` contains a json file "/color.json" containing a COLOR mapping
- `user@jsonserver2` contains a json file "/size.json" containing a SIZE mapping
- you want to map product color and size according to these mappings
- you want to add a field "deleted" when the mapped color is red
This can be implemented using:
iex> Adap.Piper.defpipe ColorPipe, [{ColorPipe.Rules,[]}]
iex> defmodule JSONMap do
iex> use Adap.StateServer, ttl: 1_000
iex> def init(mapping) do
iex> {:ok,File.read!("/#{mapping}.json") |> JSON.decode!}
iex> end
iex> def node("color") do :"user@jsonserver1" end
iex> def node("size") do :"user@jsonserver2" end
iex> end
iex> defmodule ColorPipe.Rules do
iex> use Adap.Piper, for: :product
iex> defrule map_color(%{color: color}=prod,_) do
iex> {JSONMap,"color"},color_map->
iex> %{prod| color: color_map[color]}
iex> end
iex> defrule map_size(%{size: size}=prod,_) do
iex> {JSONMap,"size"},size_map->
iex> %{prod| size: size_map[size]}
iex> end
iex> defrule red_is_deleted(%{color: "red"}=prod,_) do
iex> Dict.put(prod,:deleted,true)
iex> end
iex> end
iex> [
iex> {:product,%{gender: "male", category: "ipad"}},
iex> {:product,%{color: "carmine", category: "shirt"}},
iex> {:product,%{color: "periwinkle", size: "xxl"}}
iex> ] |> Adap.Stream.new(ColorPipe) |> Enum.to_list
[{:product,%{gender: "male", category: "ipad"}},
{:product,%{color: "red", category: "shirt", deleted: true}},
{:product,%{color: "blue", size: "large"}}]
"""
@doc false
def next(type,elem,[{next,args}|nexts],sink), do:
next.pipe(type,elem,args,nexts,sink)
def next(type,elem,[],sink), do:
Adap.Stream.done(sink,{type,elem})
@doc false
def wrap_result(sink,{:emit,elems},prev_elem,prev_state), do:
(Adap.Stream.emit(sink,elems); {prev_elem,prev_state})
def wrap_result(sink,{:emit,elems,elem},_prev_elem,prev_state), do:
(Adap.Stream.emit(sink,elems); {elem,prev_state})
def wrap_result(sink,{:emit,elems,elem,state},_prev_elem,_prev_state), do:
(Adap.Stream.emit(sink,elems); {elem,state})
def wrap_result(_sink,{:newstate,state},prev_elem,_prev_state), do:
{prev_elem,state}
def wrap_result(_sink,{:newstate,state,elem},_prev_elem,_prev_state), do:
{elem,state}
def wrap_result(_sink,elem,_prev_elem,prev_state), do:
{elem,prev_state}
defmacro defpipe(alias,pipers) do
quote do
defmodule unquote(alias) do
use Adap.Stream.Emitter
def do_emit(sink,{type,elem}), do:
Adap.Piper.next(type,elem,unquote(pipers),sink)
end
end
end
defmacro __using__(opts) do
quote do
import Adap.Piper
@behaviour Adap.Piper
@rules []
@rules_for unquote(opts[:for])
@before_compile Adap.Piper
def pipe(type,elem,args,nexts,sink) do
{elem,pipe_state} = init(elem,args)
pipe(type,init_apply_map,elem,pipe_state,nexts,sink)
end
def init(e,arg), do: {e,arg}
defoverridable [init: 2]
end
end
use Behaviour
defcallback init(elem :: term,args :: term) :: {elem :: term,pipe_state :: term}
defmacro __before_compile__(_env) do # add to the end of your module (after parsing so before compilation)
quote do
def pipe(type,_apply_map,elem,_pipe_state,nexts,sink) do
Adap.Piper.next(type,elem,nexts,sink)
end
def init_apply_map, do:
(@rules|>Enum.map(&{&1,false})|>Enum.into(%{}))
end
end
defmacro defrule(sig,blocks) do
{name,[elem_q,pipestate_q],guards_q} = sig_normalizer(sig)
quote do
@rules [unquote(name)|@rules]
def pipe(@rules_for,%{unquote(name)=>false}=apply_map, unquote(elem_q)=prev_elem, unquote(pipestate_q)=prev_state,nexts,sink) when unquote(guards_q) do
unquote(rule_body(blocks,name))
end
end
end
defp sig_normalizer({:when ,_,[{name,_,params},guards]}), do: {name,params,guards}
defp sig_normalizer({name,_,params}), do: {name,params,true}
defp rule_body([do: [{:->, _,[[server_spec|args], body]}]],name) do
quote do
Adap.Unit.Router.cast(unquote(server_spec), fn unquote_splicing(args)->
spawn(fn->
{elem,state} = Adap.Piper.wrap_result(sink,unquote(body),prev_elem,prev_state)
pipe(@rules_for,%{apply_map|unquote(name)=>true},elem,state,nexts,sink)
end)
end)
end
end
defp rule_body([do: body],name) do
quote do
{elem,state} = Adap.Piper.wrap_result(sink,unquote(body),prev_elem,prev_state)
pipe(@rules_for,%{apply_map|unquote(name)=>true},elem,state,nexts,sink)
end
end
end