Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .formatter.exs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
locals_without_parens = [
async: 2,
before: 1,
command: 2
]
Expand Down
133 changes: 112 additions & 21 deletions lib/slash/builder.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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 <dynamic arg>`).

### 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.
Expand Down Expand Up @@ -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
Expand All @@ -313,6 +346,8 @@ defmodule Slash.Builder do

quote do
unquote(ast)
unquote(default_clause)

unquote(help_ast)
end
end
Expand All @@ -322,42 +357,98 @@ defmodule Slash.Builder do
name = opts[:name]
formatter = opts[:formatter]

help_text =
help_commands =
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} ->
{"<command>", help}
end

%{
title: humanized_name,
text: "```#{help}```",
mrkdwn_in: ["text"],
color: "#00d1b2"
}
{name, help || "No help text provided."}
end)
|> 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
def match_command(_help, _command) 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
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("help", _command), do: apply(__MODULE__, :match_help, [""])
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
Expand Down
26 changes: 26 additions & 0 deletions test/slash/builder_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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