README
Adding a Map API to a GenServer or Module with Agent-held State.
Amlapio can be use-d to generate "wrapper" functions that call Map functions on the state of a GenServer, or a module using an Agent to hold its state.
Wrappers can be generated for the state itself or submaps of the state (e.g. buttons in the examples below).
Wrappers for just a subset of Map functions can be specified using funs.
The wrapper functions can be named explicitly by supplying a namer function.
See my blog post for some background.
Installation
Add amlapio to your list of dependencies in <span class="underline">mix.exs</span>:
def deps do
[{:amlapio, "~> 0.2.0"}]
endAgent Usage
The example below generates wrappers for the buttons, menus and checkboxes submaps of a Module using an Agent to hold its state. The names of the submap wrappers, by default, are of the form submap_function e.g. buttons_pop
It also generates three wrappers for the state itself by setting the submap names to nil (agent: nil). Also a namer (function) is given to name the state wrappers agent_state_get , agent_state_put, and agent_state_pop.
defmodule ExampleAgent1 do
# generate wrappers for three submaps
use Amlapio, agent: [:buttons, :menus, :checkboxes]
# generate *only* get, put and pop wrappers for the state itself and
# use a namer function to name the wrappers "agent_state_get",
# "agent_state_put" and "agent_state_pop"
use Amlapio, agent: nil, funs: [:get, :put, :pop],
namer: fn _map_name, fun_name ->
["agent_state_", to_string(fun_name)] |> Enum.join |> String.to_atom
end
# create the agent; note the default state is an empty map
def start_link(state \\ %{}) do
Agent.start_link(fn -> state end)
end
endThe state wrappers would be used as you'd expect and as shown in the test below:
test "agent_state1" do
buttons_state = %{1 => :button_back, 2 => :button_next, 3 => :button_exit}
menus_state = %{menu_a: 1, menu_b: :two, menu_c: "tre"}
checkboxes_state = %{checkbox_yesno: [:yes, :no], checkbox_bool: [true, false]}
agent_state = %{buttons: buttons_state, menus: menus_state, checkboxes: checkboxes_state}
# create the agent
{:ok, agent} = ExampleAgent1.start_link(agent_state)
# some usage examples
assert buttons_state == agent |> ExampleAgent1.agent_state_get(:buttons)
assert agent == agent |> ExampleAgent1.agent_state_put(:menus, 42)
assert 42 == agent |> Agent.get(fn s -> s end) |> Map.get(:menus)
assert {checkboxes_state, agent} == agent |> ExampleAgent1.agent_state_pop(:checkboxes)
assert %{buttons: buttons_state, menus: 42} == agent |> Agent.get(fn s -> s end)
assert 99 == agent |> ExampleAgent1.agent_state_get(:some_other_key, 99)
endSimilarly the submap wrappers as demonstrated in the test below:
test "agent_submap1" do
buttons_state = %{1 => :button_back, 2 => :button_next, 3 => :button_exit}
menus_state = %{menu_a: 1, menu_b: :two, menu_c: "tre"}
checkboxes_state = %{checkbox_yesno: [:yes, :no], checkbox_bool: [true, false]}
agent_state = %{buttons: buttons_state,
menus: menus_state, checkboxes: checkboxes_state}
# create the agent
{:ok, agent} = ExampleAgent1.start_link(agent_state)
# some usage examples
assert :button_back == agent |> ExampleAgent1.buttons_get(1)
assert :button_default ==
agent |> ExampleAgent1.buttons_get(99, :button_default)
assert agent == agent |> ExampleAgent1.menus_put(:menu_d, 42)
assert menus_state |> Map.put(:menu_d, 42) == agent |> ExampleAgent1.agent_state_get(:menus)
assert {[:yes, :no], agent} ==
agent |> ExampleAgent1.checkboxes_pop(:checkbox_yesno)
endGenServer Usage
Creating wrappers for a GenServer's state is very similar. However, each wrapper has two "parts": an api function and a handle_call function.
The api wrapper for e.g. `buttons_get/3` looks like:
# api wrapper for buttons_get
def buttons_get(pid, button_name, button_default \\ nil) do
GenServer.call(pid, {:buttons_get, button_name, button_default})
end... while the matching handle_call looks like:
def handle_call({:buttons_get, button_name, button_default}, _fromref, state) do
value = state |> Map.get(:buttons, %{}) |> Map.get(button_name, button_default)
{:reply, value, state}
endTo prevent compiler warnings all of the handle_call functions for a GenServer must be grouped together in the source. So there are two uses to define the wrappers: one for the apis and one for the handle_calls
As for an agent, the example below generates wrappers for the buttons, menus and checkboxes submaps of the GenServer's state.
In a minor difference to the agent example, the example generate four wrappers for the state itself and uses a namer (function) to name them state_get, state_put, state_pop and state_take.
defmodule ExampleGenServer1 do
# its a genserver
use GenServer
# generate API wrappers for three submaps
use Amlapio, genserver_api: [:buttons, :menus, :checkboxes]
# generate *only* get, put, pop and take wrappers for the state itself and
# use a namer function to name the wrappers "state_get",
# "state_put", "state_pop", and "state_take"
use Amlapio, genserver_api: nil, funs: [:get, :put, :pop, :take],
namer: fn _map_name, fun_name ->
["state_", to_string(fun_name)] |> Enum.join |> String.to_atom
end
# create the genserver; note the default state is an empty map
def start_link(state \\ %{}) do
GenServer.start_link(__MODULE__, state)
end
# << more functions>>
# handle_calls start here
# generate the handle_call functions for three submaps' wrappers
use Amlapio, genserver_handle_call: [:buttons, :menus, :checkboxes]
# generate the handle_call functions for the state wrappers.
use Amlapio, genserver_handle_call: nil, funs: [:get, :put, :pop, :take],
namer: fn _map_name, fun_name ->
["state_", to_string(fun_name)] |> Enum.join |> String.to_atom
end
endSome examples of the state wrappers:
test "genserver_state1" do
buttons_state = %{1 => :button_back, 2 => :button_next, 3 => :button_exit}
menus_state = %{menu_a: 1, menu_b: :two, menu_c: "tre"}
checkboxes_state = %{checkbox_yesno: [:yes, :no], checkbox_bool: [true, false]}
genserver_state = %{buttons: buttons_state, menus: menus_state, checkboxes: checkboxes_state}
# create the genserver
{:ok, genserver} = ExampleGenServer1.start_link(genserver_state)
# some examples
assert buttons_state == genserver |> ExampleGenServer1.state_get(:buttons)
assert genserver == genserver |> ExampleGenServer1.state_put(:menus, 42)
assert 42 == genserver |> ExampleGenServer1.state_get(:menus)
assert {checkboxes_state, genserver} == genserver |> ExampleGenServer1.state_pop(:checkboxes)
assert %{buttons: buttons_state, menus: 42} ==
genserver |> ExampleGenServer1.state_take([:buttons, :menus, :checkboxes])
assert 99 == genserver |> ExampleGenServer1.state_get(:some_other_key, 99)
endThe submap wrappers are used in an identical way to the agent example as demonstrated in the test below. Note these tests use the state functions.
test "genserver_submap1" do
buttons_state = %{1 => :button_back, 2 => :button_next, 3 => :button_exit}
menus_state = %{menu_a: 1, menu_b: :two, menu_c: "tre"}
checkboxes_state = %{checkbox_yesno: [:yes, :no], checkbox_bool: [true, false]}
genserver_state = %{buttons: buttons_state, menus: menus_state, checkboxes: checkboxes_state}
# create the genserver
{:ok, genserver} = ExampleGenServer1.start_link(genserver_state)
# some examples
assert :button_back == genserver |> ExampleGenServer1.buttons_get(1)
assert :button_default == genserver |> ExampleGenServer1.buttons_get(99, :button_default)
assert genserver == genserver |> ExampleGenServer1.menus_put(:menu_d, 42)
assert 42 == genserver |> ExampleGenServer1.state_get(:menus) |> Map.get(:menu_d)
assert {[:yes, :no], genserver} == genserver |> ExampleGenServer1.checkboxes_pop(:checkbox_yesno)
assert %{checkbox_bool: [true, false]} == genserver |> ExampleGenServer1.state_get(:checkboxes)
end