Skip to content

Instantly share code, notes, and snippets.

@oestrich
Last active December 14, 2017 02:38
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save oestrich/8640ef7351643cd9706f09164b16ad26 to your computer and use it in GitHub Desktop.
Save oestrich/8640ef7351643cd9706f09164b16ad26 to your computer and use it in GitHub Desktop.
Command parser with macros
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()
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