From 49d7e23ac3930e01c53f99423bfba96216833117 Mon Sep 17 00:00:00 2001 From: Matt McFarland <783024+vanetix@users.noreply.github.com> Date: Wed, 15 May 2019 10:41:15 -0500 Subject: [PATCH 1/2] Add initial implementation of default commands --- .formatter.exs | 1 + lib/slash/builder.ex | 80 +++++++++++++++++++++++++++++++------ test/slash/builder_test.exs | 26 ++++++++++++ 3 files changed, 94 insertions(+), 13 deletions(-) diff --git a/.formatter.exs b/.formatter.exs index 570ff65..613b5ca 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -1,4 +1,5 @@ locals_without_parens = [ + async: 2, before: 1, command: 2 ] diff --git a/lib/slash/builder.ex b/lib/slash/builder.ex index 0973817..bc8221f 100644 --- a/lib/slash/builder.ex +++ b/lib/slash/builder.ex @@ -200,6 +200,31 @@ defmodule Slash.Builder do end end + @doc ~S""" + Defines a command that does not handle a specific command. This block will always be executed + if it is the only `command` block defined. This can be used for fall-through routes, or custom + functionality that `Slash` might not implemented (for example command in the style + `/bot `). + + ### Example + + command fn(%{text: text}) -> + "Sorry, I don't understand #{text}." + end + """ + @spec command((Command.t() -> command_response())) :: Macro.t() + defmacro command(func) do + func = Macro.escape(func) + + quote bind_quoted: [func: func] do + help_text = Module.get_attribute(__MODULE__, :help) + + Module.delete_attribute(__MODULE__, :help) + + @commands {func, help_text} + end + end + @doc """ Defines a function to be executed before the command is routed to the appropriate handler function. @@ -303,6 +328,14 @@ defmodule Slash.Builder do formatter = opts[:formatter] help_ast = compile_help(commands, opts) + default_clause = + commands + |> Enum.find(fn + {_, _} -> true + _ -> false + end) + |> compile_default_command() + ast = for {name, func, _help} <- commands do name @@ -313,6 +346,8 @@ defmodule Slash.Builder do quote do unquote(ast) + unquote(default_clause) + unquote(help_ast) end end @@ -325,16 +360,26 @@ defmodule Slash.Builder do help_text = commands |> Enum.sort_by(&elem(&1, 0)) - |> Enum.map(fn {name, _func, help} -> - help = help || "No help text provided." - - humanized_name = - name - |> to_string() - |> formatter.to_command_name() + |> Enum.map(fn command -> + {name, help} = + case command do + {name, _func, help} -> + humanized_name = + name + |> to_string() + |> formatter.to_command_name() + + {humanized_name, help} + + {_func, help} -> + {"", help} + end + {name, help || "No help text provided."} + end) + |> Enum.map(fn {name, help} -> %{ - title: humanized_name, + title: name, text: "```#{help}```", mrkdwn_in: ["text"], color: "#00d1b2" @@ -343,7 +388,7 @@ defmodule Slash.Builder do |> Macro.escape() quote do - def match_command(_help, _command) do + def match_help(_) do %{ text: unquote(name) <> " supports the following commands:", attachments: unquote(help_text) @@ -352,12 +397,21 @@ defmodule Slash.Builder do end end - # Compiles an AST for a specific command - defp compile_command(name, func) do - if name == "help" do - IO.puts("Warning: defining a `help` command will override the default help generation.") + # Compiles a default fallback clause + defp compile_default_command(nil) do + quote do + def match_command(_, command), do: apply(__MODULE__, :match_help, [""]) end + end + defp compile_default_command({func, _help}) do + quote do + def match_command(_, command), do: unquote(func).(command) + end + end + + # Compiles an AST for a specific command + defp compile_command(name, func) do quote do def match_command(unquote(name), command), do: unquote(func).(command) end diff --git a/test/slash/builder_test.exs b/test/slash/builder_test.exs index e96e436..e1a410b 100644 --- a/test/slash/builder_test.exs +++ b/test/slash/builder_test.exs @@ -102,6 +102,20 @@ defmodule Slash.BuilderTest do end end + defmodule DefaultCommandMock do + use Slash.Builder + + before :error when command in [:print] + + def error(_) do + throw(:not_implemented) + end + + command fn %{text: text} -> + text + end + end + setup _ do {:ok, conn: conn(:post, "/", %{})} end @@ -223,4 +237,16 @@ defmodule Slash.BuilderTest do end end end + + describe "router with default command block" do + test "should route all commands to default command", %{conn: conn} do + conn = + conn + |> send_command(DefaultCommandMock, "command") + |> DefaultCommandMock.call([]) + + assert %Plug.Conn{resp_body: body} = conn + assert %{"text" => "command"} = Jason.decode!(body) + end + end end From 9e206fa80d4626fc28f54fbd9affdbabce3b5ffc Mon Sep 17 00:00:00 2001 From: Matt McFarland <783024+vanetix@users.noreply.github.com> Date: Thu, 23 May 2019 12:19:04 -0500 Subject: [PATCH 2/2] Update help generation to the new Slack blocks response --- lib/slash/builder.ex | 57 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 47 insertions(+), 10 deletions(-) diff --git a/lib/slash/builder.ex b/lib/slash/builder.ex index bc8221f..c3552ef 100644 --- a/lib/slash/builder.ex +++ b/lib/slash/builder.ex @@ -357,7 +357,7 @@ defmodule Slash.Builder do name = opts[:name] formatter = opts[:formatter] - help_text = + help_commands = commands |> Enum.sort_by(&elem(&1, 0)) |> Enum.map(fn command -> @@ -377,21 +377,57 @@ defmodule Slash.Builder do {name, help || "No help text provided."} end) - |> Enum.map(fn {name, help} -> - %{ - title: name, - text: "```#{help}```", - mrkdwn_in: ["text"], - color: "#00d1b2" - } + |> Enum.map(fn {command, help} -> + blocks = [ + %{ + type: "context", + elements: [ + %{ + type: "mrkdwn", + text: "_*/#{String.downcase(name)} #{command}*_" + } + ] + }, + %{ + type: "section", + text: %{ + type: "mrkdwn", + text: help + } + } + ] + + {command, blocks} end) |> Macro.escape() + help_function_ast = + for {name, blocks} <- help_commands do + quote do + def match_help(unquote(name)), do: %{blocks: unquote(blocks)} + end + end + quote do + unquote(help_function_ast) + def match_help(_) do + blocks = Enum.map(unquote(help_commands), &elem(&1, 1)) + %{ - text: unquote(name) <> " supports the following commands:", - attachments: unquote(help_text) + blocks: + [ + %{ + type: "section", + text: %{ + type: "mrkdwn", + text: "*_" <> unquote(name) <> " supports the following commands_*:" + } + } + | blocks + ] + |> Enum.intersperse(%{type: "divider"}) + |> List.flatten() } end end @@ -406,6 +442,7 @@ defmodule Slash.Builder do defp compile_default_command({func, _help}) do quote do + def match_command("help", _command), do: apply(__MODULE__, :match_help, [""]) def match_command(_, command), do: unquote(func).(command) end end