Last active
December 14, 2017 02:38
-
-
Save oestrich/8640ef7351643cd9706f09164b16ad26 to your computer and use it in GitHub Desktop.
Command parser with macros
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Code.require_file("router.exs") | |
defmodule Commands.User do | |
@behaviour Router.PreParser | |
@impl Router.PreParser | |
def call(state) do | |
%{state | assigns: Map.put(state.assigns, :user, %{id: 10})} | |
end | |
end | |
defmodule Commands.Aliases do | |
@behaviour Router.PreParser | |
@impl Router.PreParser | |
def call(state) do | |
case state.command do | |
"mm" -> %{state | command: "magic missile"} | |
_ -> state | |
end | |
end | |
end | |
defmodule Commands do | |
use Router, scope: Game.Command | |
pre_parse Commands.User | |
pre_parse Commands.Aliases | |
command ["north", "n"], Move, {:north} | |
command ["south", "s"], Move, {:south} | |
command ["east", "e"], Move, {:east} | |
command ["west", "w"], Move, {:west} | |
command ["up", "u"], Move, {:up} | |
command ["down", "d"], Move, {:down} | |
command "open " <> dir, Move, {:open, dir} | |
command "close " <> dir, Move, {:close, dir} | |
command ["shops buy " <> item, "buy " <> item], Shops, {:buy, item} | |
command ["shops sell " <> item, "sell " <> item], Shops, {:sell, item} | |
command "channels", Channels, {} | |
command "channels off " <> channel, Channels, {:leave, channel} | |
command "channels on " <> channel, Channels, {:join, channel} | |
Enum.each(["global", "newbie"], fn (channel) -> | |
command channel <> " " <> message, Channels, {channel, message} | |
end) | |
command "bug " <> title, Bug, {title} | |
command "drop " <> item, Drop, {item} | |
command "emote " <> emote, Emote, {emote} | |
command ["equipment", "eq"], Equipment | |
command "examine " <> item, Examine, {item} | |
command "help ", Help | |
command "help " <> topic, Help, {topic} | |
command ["info", "score"], Info | |
command ["inventory", "i"], Inventory | |
command ["look", "l"], Look | |
command ["look " <> thing, "look at " <> thing, "l " <> thing], Look, {thing} | |
command "map", Map | |
command ["get " <> item, "pick up " <> item], PickUp, {item} | |
command "run " <> directions, Run, {directions} | |
command "say " <> capture, Say, {capture} | |
command ["target", "t"], Target | |
command ["target " <> name, "t " <> name], Target, {name} | |
command "tell " <> message, Tell, {"tell", message} | |
command "reply " <> message, Tell, {"reply", message} | |
command "quit", Quit | |
command "version", Version | |
command "wear " <> item, Wear, {:wear, item} | |
command "remove " <> item, Wear, {:remove, item} | |
command "who", Who | |
command "wield " <> item, Wear, {:wield, item} | |
command "unwield " <> item, Wear, {:unwield, item} | |
command "magic missile", Skills, {"magic missile"} | |
# mistakes | |
command "kill " <> _, Mistake, {:auto_combat} | |
command "attack " <> _, Mistake, {:auto_combat} | |
end | |
defmodule Main do | |
def call() do | |
state = %Router.State{} | |
IO.inspect {:ok, Game.Command.Say, {"hello"}} = Commands.parse(state, "say hello") | |
IO.inspect {:ok, Game.Command.Move, {:north}} = Commands.parse(state, "north") | |
IO.inspect {:ok, Game.Command.Map, {}} = Commands.parse(state, "map") | |
IO.inspect {:ok, Game.Command.Quit, {}} = Commands.parse(state, "quit") | |
IO.inspect {:ok, Game.Command.Wear, {:wear, "full plate"}} = Commands.parse(state, "wear full plate") | |
IO.inspect {:ok, Game.Command.Channels, {"global", "howdy everyone"}} = Commands.parse(state, "global howdy everyone") | |
IO.inspect {:ok, Game.Command.Skills, {"magic missile"}} = Commands.parse(state, "mm") | |
IO.inspect {:error, :bad_parse, "unknown"} = Commands.parse(state, "unknown") | |
end | |
end | |
Main.call() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule Router.State do | |
defstruct [assigns: %{}, command: nil] | |
end | |
defmodule Router do | |
defmacro __using__(opts) do | |
scope = Keyword.get(opts, :scope) | |
quote do | |
import Router, only: [command: 2, command: 3, pre_parse: 1] | |
Module.register_attribute __MODULE__, :pre_parse, accumulate: true | |
@scope unquote(scope) | |
@before_compile Router | |
end | |
end | |
defmacro __before_compile__(_env) do | |
quote do | |
def parse(state, command) do | |
state = %{state | command: command} | |
@pre_parse | |
|> Router.pre_parse(state) | |
|> _parse() | |
end | |
defp _parse(state), do: Router.base_parse(state) | |
end | |
end | |
defmacro command(path, module) do | |
_command(path, module, {:{}, [], []}) | |
end | |
defmacro command(path, module, return) do | |
variables = Enum.map(__CALLER__.vars, &(elem(&1, 0))) | |
path = Macro.postwalk(path, fn (segment) -> maybe_unquote(segment, variables) end) | |
return = Macro.postwalk(return, fn (segment) -> maybe_unquote(segment, variables) end) | |
_command(path, module, return) | |
end | |
defmacro pre_parse(function) do | |
quote do | |
@pre_parse unquote(function) | |
end | |
end | |
defp maybe_unquote({var, extra, context}, variables) when is_atom(var) do | |
case var in variables do | |
true -> | |
{:unquote, [], [{var, extra, context}]} | |
false -> | |
{var, extra, context} | |
end | |
end | |
defp maybe_unquote(ast, _variables), do: ast | |
defp _command([], _module, _return), do: [] | |
defp _command([head | tail], module, return) do | |
[_command(head, module, return)] ++ _command(tail, module, return) | |
end | |
defp _command(path, module, return) do | |
quote do | |
defp _parse(%{command: unquote(path)}) do | |
{:ok, @scope.unquote(module), unquote(return)} | |
end | |
end | |
end | |
def pre_parse(parsers, command) do | |
parsers | |
|> Enum.reduce(command, fn (pre_parser, command) -> | |
apply(pre_parser, :call, [command]) | |
end) | |
end | |
def base_parse(%Router.State{command: command}) do | |
{:error, :bad_parse, command} | |
end | |
end | |
defmodule Router.PreParser do | |
@callback call(command :: String.t) :: String.t | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment