Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Interpolates messages with variables #4

Merged
merged 11 commits into from
Mar 17, 2023
5 changes: 5 additions & 0 deletions lib/assets/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ export function init(ctx, payload) {
<a href="https://api.slack.com/tutorials/tracks/getting-a-token" target="_blank">create a Slack app and get your app's token</a>.
</p>
</div>
<div class="section">
<p>
To dynamically inject values into the query use double curly braces, like {{name}}.
</p>
</div>
</div>
<div class="row">
<div class="field grow">
Expand Down
5 changes: 3 additions & 2 deletions lib/kino_slack/message_cell.ex
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ defmodule KinoSlack.MessageCell do
@impl true
def to_source(attrs) do
required_fields = ~w(token_secret_name channel message)
message_ast = KinoSlack.MessageInterpolator.interpolate(attrs["message"])

if all_fields_filled?(attrs, required_fields) do
quote do
Expand All @@ -63,7 +64,7 @@ defmodule KinoSlack.MessageCell do
url: "/chat.postMessage",
json: %{
channel: unquote(attrs["channel"]),
text: unquote(attrs["message"])
text: unquote(message_ast)
}
)

Expand All @@ -78,7 +79,7 @@ defmodule KinoSlack.MessageCell do
end
end

def all_fields_filled?(attrs, keys) do
defp all_fields_filled?(attrs, keys) do
Enum.all?(keys, fn key -> attrs[key] not in [nil, ""] end)
end
end
60 changes: 60 additions & 0 deletions lib/kino_slack/message_interpolator.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
defmodule KinoSlack.MessageInterpolator do
hugobarauna marked this conversation as resolved.
Show resolved Hide resolved
def interpolate(message) do
ast = quote do: <<"">>
interpolate(message, ast)
hugobarauna marked this conversation as resolved.
Show resolved Hide resolved
end

defp interpolate("", result_ast) do
result_ast
end

defp interpolate("{{" <> rest, ast) do
with [inner, rest] <- String.split(rest, "}}", parts: 2),
{:ok, expression} <- Code.string_to_quoted(inner) do
ast = append_interpolation(ast, expression)
interpolate(rest, ast)
else
_ ->
<<char::utf8>> = "{"
ast = append_char(ast, char)
ast = append_char(ast, char)
interpolate(rest, ast)
end
end

defp interpolate(<<char::utf8, rest::binary>>, ast) do
new_ast = append_char(ast, char)
interpolate(rest, new_ast)
end

defp append_interpolation(ast, expression) do
interpolation_node = {
:"::",
[],
[
{{:., [], [Kernel, :to_string]}, [], [expression]},
{:binary, [], Elixir}
]
}

{_, _, args} = ast
hugobarauna marked this conversation as resolved.
Show resolved Hide resolved
args = args ++ [interpolation_node]

{:<<>>, [], args}
end

defp append_char(ast, char) do
hugobarauna marked this conversation as resolved.
Show resolved Hide resolved
{_, _, args} = ast
hugobarauna marked this conversation as resolved.
Show resolved Hide resolved
last_arg = List.last(args)

new_args =
if is_binary(last_arg) do
last_string = last_arg <> <<char::utf8>>
List.replace_at(args, -1, last_string)
else
args ++ [<<char::utf8>>]
end

{:<<>>, [], new_args}
end
end
41 changes: 41 additions & 0 deletions test/kino_slack/message_cell_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,47 @@ defmodule KinoSlack.MessageCellTest do
assert generated_code == expected_code
end

test "generates source code with variable interpolation" do
{kino, _source} = start_smart_cell!(MessageCell, %{})

push_event(kino, "update_token_secret_name", "SLACK_TOKEN")
push_event(kino, "update_channel", "#slack-channel")
push_event(kino, "update_message", "Hello {{first_name}} {{last_name}}!")

assert_smart_cell_update(
kino,
%{
"token_secret_name" => "SLACK_TOKEN",
"channel" => "#slack-channel",
"message" => "Hello {{first_name}} {{last_name}}!"
},
generated_code
)

expected_code = ~S"""
req =
Req.new(
base_url: "https://slack.com/api",
auth: {:bearer, System.fetch_env!("LB_SLACK_TOKEN")}
)

response =
Req.post!(req,
url: "/chat.postMessage",
json: %{channel: "#slack-channel", text: "Hello #{first_name} #{last_name}!"}
)

case response.body do
%{"ok" => true} -> :ok
%{"ok" => false, "error" => error} -> {:error, error}
end
"""

expected_code = String.trim(expected_code)

assert generated_code == expected_code
end

test "generates source code from stored attributes" do
stored_attrs = %{
"token_secret_name" => "SLACK_TOKEN",
Expand Down
45 changes: 45 additions & 0 deletions test/kino_slack/messsage_interpolator_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
defmodule KinoSlack.MesssageInterpolatorTest do
use ExUnit.Case, async: true

alias KinoSlack.MessageInterpolator, as: Interpolator

test "it interpolates variables inside a message" do
first_name = "Hugo"
last_name = "Baraúna"
message = "Hi {{first_name}} {{last_name}}! 🎉"

interpolated_ast = Interpolator.interpolate(message)
{interpolated_message, _} = Code.eval_quoted(interpolated_ast, binding())

assert interpolated_message == "Hi Hugo Baraúna! 🎉"
end

test "it interpolates expressons inside a message" do
message = "One plus one is: {{1 + 1}}"

interpolated_ast = Interpolator.interpolate(message)
hugobarauna marked this conversation as resolved.
Show resolved Hide resolved
{interpolated_message, _} = Code.eval_quoted(interpolated_ast, binding())

assert interpolated_message == "One plus one is: 2"
end

test "it interpolates expressions with functinos and vars inside a message" do
first_name = "Hugo"
message = "Do you {{first_name}}, know {{1 + 1}} ?"

interpolated_ast = Interpolator.interpolate(message)
{interpolated_message, _} = Code.eval_quoted(interpolated_ast, binding())

assert interpolated_message == "Do you Hugo, know 2 ?"
end

test "it handles messags with only the beginning of interpolation syntax" do
first_name = "Hugo"
message = "hi {{ {{first_name}}"

interpolated_ast = Interpolator.interpolate(message)
{interpolated_message, _} = Code.eval_quoted(interpolated_ast, binding())

assert interpolated_message == "hi {{ Hugo"
end
end