From 01aa43fdbf13c597bd5da7f8bc69608d68fe3113 Mon Sep 17 00:00:00 2001 From: lthms Date: Sun, 30 Apr 2017 07:16:14 +0000 Subject: puppeteer: Add the macro defpuppeteer to specify a Puppeteer interface --- lib/lkn/core/component.ex | 135 +++++++++---------------------------- lib/lkn/core/puppeteer.ex | 167 +++++++++++++++++++++++----------------------- lib/lkn/core/specs.ex | 104 ++++++++++++++++++++++++++++- test/core_test.exs | 47 +++++++------ 4 files changed, 242 insertions(+), 211 deletions(-) diff --git a/lib/lkn/core/component.ex b/lib/lkn/core/component.ex index b79a6f6..51b5a63 100644 --- a/lib/lkn/core/component.ex +++ b/lib/lkn/core/component.ex @@ -15,110 +15,23 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # -alias Lkn.Core.Specs - defmodule Lkn.Core.Component do - defp cast_client(module_name, cast) do - name = cast.fun.name - entity_key_type = quote do - Lkn.Core.Entity.k - end - - cast_doc = if cast.doc != :none do - quote do - @doc unquote(cast.doc) - end - end - - arglist = Enum.map(cast.fun.arguments, &(&1.name)) - arglistcl = [{:entity_key, [], nil}|arglist] - argtypes = [entity_key_type|Enum.map(cast.fun.arguments, &(&1.type))] - - quote do - unquote(cast_doc) - @spec unquote({name, [], argtypes}) :: :ok - def unquote({name, [], arglistcl}) do - GenServer.cast(Lkn.Core.Name.component(unquote({:entity_key, [], nil}), unquote(module_name)), - {:spec, {unquote(name), unquote(arglist)}}) - end - end - end + alias Lkn.Core.Specs - defp call_client(module_name, call) do - name = call.fun.name - entity_key_type = quote do - Lkn.Core.Entity.k - end - - call_doc = if call.doc != :none do - quote do - @doc unquote(call.doc) - end - end - - arglist = Enum.map(call.fun.arguments, &(&1.name)) - arglistcl = [{:entity_key, [], nil}|arglist] - argtypes = [entity_key_type|Enum.map(call.fun.arguments, &(&1.type))] + @type state :: any - quote do - unquote(call_doc) - @spec unquote({name, [], argtypes}) :: unquote(call.ret) - def unquote({name, [], arglistcl}) do - GenServer.call(Lkn.Core.Name.component(unquote({:entity_key, [], nil}), unquote(module_name)), - {:spec, {unquote(name), unquote(arglist)}}) - end - end - end - - defp cast_behaviour(cast) do - name = cast.fun.name - entity_key_type = quote do - Lkn.Core.Entity.k - end - - arglistcl = [{:::, [], [{:entity_key, [], nil}, entity_key_type]} - |Enum.map(cast.fun.arguments, &({:::, [], [&1.name, &1.type]}))] - - quote do - @callback unquote({name, [], arglistcl}) :: :ok - end - end - - defp call_behaviour(call) do - name = call.fun.name - entity_key_type = quote do - Lkn.Core.Entity.k - end - - arglistcl = [{:::, [], [{:entity_key, [], nil}, entity_key_type]} - |Enum.map(call.fun.arguments, &({:::, [], [&1.name, &1.type]}))] - - quote do - @callback unquote({name, [], arglistcl}) :: unquote(call.ret) - end - end + @callback init_state(Lkn.Core.Entity.k) :: {:ok, state} | :error defmacro defcomponent(name, do: block) do - block = case block do - {:__block__, _, x} -> x - x -> [x] - end - - {casts, calls, legit} = Specs.parse_specs(block, [], [], []) - - casts_client = Enum.map(casts, &(cast_client(name, &1))) - calls_client = Enum.map(calls, &(call_client(name, &1))) - - casts_behaviour = Enum.map(casts, &(cast_behaviour(&1))) - calls_behaviour = Enum.map(calls, &(call_behaviour(&1))) + state_type = quote do Lkn.Core.Component.state end + key_type = quote do Lkn.Core.Component.k end + key_to_name = quote do + &(Lkn.Core.Name.component(&1, unquote(name))) + end quote do defmodule unquote(name) do - unquote(legit) - unquote(casts_client) - unquote(calls_client) - unquote(casts_behaviour) - unquote(calls_behaviour) + unquote(Specs.gen_server_from_specs(block, key_type, key_to_name, state_type)) def system do @system @@ -126,7 +39,20 @@ defmodule Lkn.Core.Component do defmacro __using__(_) do quote do + defmodule State do + defstruct [ + :entity_key, + :state, + ] + + @type t :: %State{ + entity_key: Lkn.Core.Entity.k, + state: Lkn.Core.Component.state, + } + end + @behaviour unquote(__MODULE__) + @behaviour Lkn.Core.Component use GenServer @@ -143,6 +69,11 @@ defmodule Lkn.Core.Component do GenServer.start_link(__MODULE__, entity_key, name: Name.component(entity_key, unquote(__MODULE__))) end + def init(entity_key) do + {:ok, s} = init_state(entity_key) + {:ok, %State{entity_key: entity_key, state: s}} + end + @spec read(Entity.k, Properties.prop) :: Option.t(Properties.value) defp read(entity_key, p) do Properties.read(entity_key, p) @@ -153,16 +84,16 @@ defmodule Lkn.Core.Component do Properties.write(entity_key, p, v) end - def handle_cast({:spec, {name, args}}, entity_key) do - :erlang.apply(__MODULE__, name, [entity_key|args]) + def handle_cast({:spec, {name, args}}, state) do + s = :erlang.apply(__MODULE__, name, [state.entity_key|args] ++ [state.state]) - {:noreply, entity_key} + {:noreply, %State{state|state: s}} end - def handle_call({:spec, {name, args}}, _call, entity_key) do - res = :erlang.apply(__MODULE__, name, [entity_key|args]) + def handle_call({:spec, {name, args}}, _call, state) do + {res, s} = :erlang.apply(__MODULE__, name, [state.entity_key|args] ++ [state.state]) - {:reply, res, entity_key} + {:reply, res, %State{state|state: s}} end end end diff --git a/lib/lkn/core/puppeteer.ex b/lib/lkn/core/puppeteer.ex index 5044865..a164e1a 100644 --- a/lib/lkn/core/puppeteer.ex +++ b/lib/lkn/core/puppeteer.ex @@ -18,6 +18,7 @@ defmodule Lkn.Core.Puppeteer do alias Lkn.Core.Instance alias Lkn.Core.Name + alias Lkn.Core.Specs @typedoc """ A key to identify and reach a given Puppeteer. @@ -30,94 +31,93 @@ defmodule Lkn.Core.Puppeteer do @type m :: module @type init_args :: any - @type state :: any - - defmacro __using__(state: state_t) do - quote do - use Lkn.Prelude - use GenServer - - alias Lkn.Core.Instance - alias Lkn.Core, as: L - - @type state :: unquote(state_t) - - defmodule State do - @moduledoc false - - defstruct [ - :puppeteer_key, - :map_key, - :instance_key, - :state, - ] - - @type t :: %State{ - puppeteer_key: Puppeteer.k, - instance_key: Lkn.Prelude.Option.t(Instance.k), - map_key: Lkn.Prelude.Option.t(L.Map.k), - state: unquote(state_t), - } - - @spec new(Puppeteer.t, unquote(state_t)) :: t - def new(pk, s) do - %State{ - puppeteer_key: pk, - instance_key: Lkn.Prelude.Option.nothing(), - map_key: Lkn.Prelude.Option.nothing(), - state: s, - } - end - end - - def init({puppeteer_key, args}) do - {:ok, s} = init_state(args) - {:ok, State.new(puppeteer_key, s)} - end - def handle_cast({:leave_instance, instance_key}, state) do - if state.instance_key == Lkn.Prelude.Option.some(instance_key) do - s2 = leave_instance(state.state, instance_key) + @type state :: any - Instance.unregister_puppeteer(instance_key, state.puppeteer_key) + defmacro defpuppeteer(name, do: block) do + state_type = quote do Lkn.Core.Puppeteer.state end + key_type = quote do Lkn.Core.Puppeteer.k end + key_to_name = &(Lkn.Core.Name.puppeteer(&1)) - {:noreply, %State{state|instance_key: Lkn.Prelude.Option.nothing(), state: s2}} - else - {:noreply, state} + quote do + defmodule unquote(name) do + unquote(Specs.gen_server_from_specs(block, key_type, key_to_name, state_type)) + + defmacro __using__(_) do + quote do + @behaviour unquote(__MODULE__) + @behaviour Lkn.Core.Puppeteer + + use GenServer + + alias Lkn.Core.Instance + alias Lkn.Core, as: L + + defmodule State do + @moduledoc false + + defstruct [ + :puppeteer_key, + :map_key, + :instance_key, + :state, + ] + + @type t :: %State{ + puppeteer_key: Puppeteer.k, + instance_key: Lkn.Prelude.Option.t(Instance.k), + map_key: Lkn.Prelude.Option.t(L.Map.k), + state: Lkn.Core.Puppeteer.state, + } + + @spec new(Puppeteer.t, Lkn.Core.Puppeteer.state) :: t + def new(pk, s) do + %State{ + puppeteer_key: pk, + instance_key: Lkn.Prelude.Option.nothing(), + map_key: Lkn.Prelude.Option.nothing(), + state: s, + } + end + end + + def init({puppeteer_key, args}) do + {:ok, s} = init_state(args) + {:ok, State.new(puppeteer_key, s)} + end + + def handle_cast({:leave_instance, instance_key}, state) do + if state.instance_key == Lkn.Prelude.Option.some(instance_key) do + s2 = leave_instance(state.state, instance_key) + + Instance.unregister_puppeteer(instance_key, state.puppeteer_key) + + {:noreply, %State{state|instance_key: Lkn.Prelude.Option.nothing(), state: s2}} + else + {:noreply, state} + end + end + def handle_cast({:spec, {name, args}}, state) do + s = :erlang.apply(__MODULE__, name, [state.puppeteer_key|args]++[state.state]) + + {:noreply, %State{state|state: s}} + end + + def handle_call({:find_instance, map_key}, _from, state) do + instance_key = Lkn.Core.Pool.register_puppeteer(map_key, state.puppeteer_key, __MODULE__) + + state = %State{state|instance_key: Lkn.Prelude.Option.some(instance_key)} + + {:reply, instance_key, state} + end + def handle_call({:spec, {name, args}}, _call, state) do + {res, s} = :erlang.apply(__MODULE__, name, [state.puppeteer_key|args] ++ [state.state]) + + {:reply, res, %State{state|state: s}} + end + end end end - def handle_cast({:specific, msg}, state) do - s2 = puppeteer_handle_cast(msg, state.state) - {:noreply, %State{state|state: s2}} - end - - def handle_call({:find_instance, map_key}, _from, state) do - instance_key = Lkn.Core.Pool.register_puppeteer(map_key, state.puppeteer_key, __MODULE__) - - state = %State{state|instance_key: Lkn.Prelude.Option.some(instance_key)} - - {:reply, instance_key, state} - end - - def puppeteer_handle_cast(_msg, state) do - state - end - - def handle_info(msg, state) do - state = %State{state|state: handle_message(state.state, msg)} - - {:noreply, state} - end - - def cast(puppeteer_key, msg) do - GenServer.cast(Name.puppeteer(puppeteer_key), {:specific, msg}) - end - - @behaviour Lkn.Core.Puppeteer - - defoverridable [ - puppeteer_handle_cast: 2, - ] end end @@ -126,7 +126,6 @@ defmodule Lkn.Core.Puppeteer do GenServer.start_link(module, {puppeteer_key, args}, name: Name.puppeteer(puppeteer_key)) end - @callback handle_message(state, any) :: state @callback init_state(init_args) :: {:ok, state}|:error @callback leave_instance(state, Instance.k) :: state diff --git a/lib/lkn/core/specs.ex b/lib/lkn/core/specs.ex index 4a62820..ceab803 100644 --- a/lib/lkn/core/specs.ex +++ b/lib/lkn/core/specs.ex @@ -1,7 +1,109 @@ defmodule Lkn.Core.Specs do @moduledoc false - def parse_specs(block, casts, calls, legit) do + def gen_server_from_specs(block, key_type, key_to_name, state_type) do + block = case block do + {:__block__, _, x} -> x + x -> [x] + end + + {casts, calls, legit} = parse_specs(block, [], [], []) + + casts_client = Enum.map(casts, &(cast_client(key_type, key_to_name, &1))) + calls_client = Enum.map(calls, &(call_client(key_type, key_to_name, &1))) + + casts_behaviour = Enum.map(casts, &(cast_behaviour(&1, state_type))) + calls_behaviour = Enum.map(calls, &(call_behaviour(&1, state_type))) + + quote do + unquote(legit) + unquote(casts_client) + unquote(calls_client) + unquote(casts_behaviour) + unquote(calls_behaviour) + end + end + + defp cast_client(key_type, key_to_name, cast) do + name = cast.fun.name + + cast_doc = if cast.doc != :none do + quote do + @doc unquote(cast.doc) + end + end + + arglist = Enum.map(cast.fun.arguments, &(&1.name)) + arglistcl = [{:key, [], nil}|arglist] + argtypes = [key_type|Enum.map(cast.fun.arguments, &(&1.type))] + + quote do + unquote(cast_doc) + @spec unquote({name, [], argtypes}) :: :ok + def unquote({name, [], arglistcl}) do + GenServer.cast(unquote(key_to_name).(unquote({:key, [], nil})), + {:spec, {unquote(name), unquote(arglist)}}) + end + end + end + + defp call_client(key_type, key_to_name, call) do + name = call.fun.name + + call_doc = if call.doc != :none do + quote do + @doc unquote(call.doc) + end + end + + arglist = Enum.map(call.fun.arguments, &(&1.name)) + arglistcl = [{:key, [], nil}|arglist] + argtypes = [key_type|Enum.map(call.fun.arguments, &(&1.type))] + + quote do + unquote(call_doc) + @spec unquote({name, [], argtypes}) :: unquote(call.ret) + def unquote({name, [], arglistcl}) do + GenServer.call(unquote(key_to_name).(unquote({:key, [], nil})), + {:spec, {unquote(name), unquote(arglist)}}) + end + end + end + + defp cast_behaviour(cast, state_type) do + name = cast.fun.name + entity_key_type = quote do + Lkn.Core.Entity.k + end + + arglistcl = [{:::, [], [{:entity_key, [], nil}, entity_key_type]} + |Enum.map(cast.fun.arguments, &({:::, [], [&1.name, &1.type]}))] ++ [ + {:::, [], [{:state, [], nil}, state_type]} + ] + + quote do + @callback unquote({name, [], arglistcl}) :: unquote(state_type) + end + end + + defp call_behaviour(call, state_type) do + name = call.fun.name + entity_key_type = quote do + Lkn.Core.Entity.k + end + + arglistcl = [{:::, [], [{:entity_key, [], nil}, entity_key_type]} + |Enum.map(call.fun.arguments, &({:::, [], [&1.name, &1.type]}))] ++ [ + {:::, [], [{:state, [], nil}, state_type]} + ] + + quote do + @callback unquote({name, [], arglistcl}) :: {unquote(state_type), unquote(call.ret)} + end + end + + + defp parse_specs(block, casts, calls, legit) do case block do [{:@, _, [{:doc, _, [docstring]}]}, {:@, _, [{:cast, _, [fun]}]} diff --git a/test/core_test.exs b/test/core_test.exs index b9a456a..982edc6 100644 --- a/test/core_test.exs +++ b/test/core_test.exs @@ -283,8 +283,10 @@ end ###################################################################### # SYSTEM # # # - -import Lkn.Core.Component +import Lkn.Core.Component, only: [defcomponent: 2] +import Lkn.Core.Map, only: [defmap: 2] +import Lkn.Core.Puppeteer, only: [defpuppeteer: 2] +import Lkn.Core.Puppet, only: [defpuppet: 2] defcomponent Test.System.Puppet do @system Test.System @@ -319,18 +321,19 @@ defmodule Test.System do end def puppet_enter(:ok, _entities, key) do - notify(&(Test.Puppeteer.emit(&1, {:enter, key}))) + notify(&(Test.Puppeteer.Specs.emit(&1, {:enter, key}))) :ok end def puppet_leave(:ok, _entities, key) do - notify(&(Test.Puppeteer.emit(&1, {:leave, key}))) + notify(&(Test.Puppeteer.Specs.emit(&1, {:leave, key}))) :ok end def system_cast({:level_up, entity_key}, _entities, :ok) do notif = Test.System.Puppet.level_up(entity_key) - notify(&(Test.Puppeteer.emit(&1, {:level_up, notif}))) + + notify(&(Test.Puppeteer.Specs.emit(&1, {:level_up, notif}))) :ok end @@ -343,12 +346,12 @@ end ###################################################################### # MAP # # # -import Lkn.Core.Map - defmodule Test.Map.Component do use Test.System.Map - def level_max(_), do: 7 + def level_max(_, _), do: 7 + + def init_state(_), do: {:ok, :ok} end defmap Test.Map do @@ -374,17 +377,17 @@ end defmodule Test.Entity.Component do use Test.System.Puppet - def level_up(entity_key) do + def init_state(_), do: {:ok, :ok} + + def level_up(entity_key, :ok) do Option.some(lvl) = read(entity_key, :level) write(entity_key, :level, lvl + 1) - {lvl, lvl + 1} + {{lvl, lvl + 1}, :ok} end end -import Lkn.Core.Puppet - defpuppet Test.Entity do @components [Test.Entity.Component] @@ -412,8 +415,13 @@ end ###################################################################### # PUPPETEER # # # + +defpuppeteer Test.Puppeteer.Specs do + @cast emit(msg :: any) +end + defmodule Test.Puppeteer do - use Puppeteer, state: pid() + use Test.Puppeteer.Specs, state: pid() def start_link(puppeteer_key) do Puppeteer.start_link(__MODULE__, puppeteer_key, self()) @@ -428,19 +436,10 @@ defmodule Test.Puppeteer do target end - def puppeteer_handle_cast({:emit, msg}, target) do - send(target, msg) - + def emit(_puppeteer_key, msg, target) do + send target, msg target end - - def handle_message(target, _msg) do - target - end - - def emit(puppeteer_key, msg) do - cast(puppeteer_key, {:emit, msg}) - end end -- cgit v1.2.3