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
53 changes: 53 additions & 0 deletions lib/kino_slack/message_interpolator.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
defmodule KinoSlack.MessageInterpolator do
hugobarauna marked this conversation as resolved.
Show resolved Hide resolved
@moduledoc false

def interpolate(message) do
args = build_interpolation_args(message, [""])
args = Enum.reverse(args)
{:<<>>, [], args}
end

defp build_interpolation_args("", args) do
args
end

defp build_interpolation_args("{{" <> rest, args) do
with [inner, rest] <- String.split(rest, "}}", parts: 2),
{:ok, expression} <- Code.string_to_quoted(inner) do
args = add_interpolation(args, expression)
build_interpolation_args(rest, args)
else
_ ->
args = add_string(args, "{{")
build_interpolation_args(rest, args)
end
end

defp build_interpolation_args(<<char::utf8, rest::binary>>, args) do
args = add_string(args, <<char::utf8>>)
build_interpolation_args(rest, args)
end

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

[interpolation_node | args]
end

defp add_string(args, string) do
[head | tail] = args

if is_binary(head) do
[head <> string | tail]
else
[string | args]
end
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
53 changes: 53 additions & 0 deletions test/kino_slack/messsage_interpolator_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
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)
generated_code = Macro.to_string(interpolated_ast)
{interpolated_message, _} = Code.eval_quoted(interpolated_ast, binding())

assert generated_code == ~S/"Hi #{first_name} #{last_name}! 🎉"/
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
generated_code = Macro.to_string(interpolated_ast)
{interpolated_message, _} = Code.eval_quoted(interpolated_ast, binding())

assert generated_code == ~S/"One plus one is: #{1 + 1}"/
assert interpolated_message == "One plus one is: 2"
end

test "it interpolates funtion calls inside a message" do
sum = fn a, b -> a + b end
message = "1 + 1 is: {{sum.(1, 1)}}"

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

assert generated_code == ~S/"1 + 1 is: #{sum.(1, 1)}"/
assert interpolated_message == "1 + 1 is: 2"
end

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

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

assert generated_code == ~S/"hi {{ #{first_name}"/
assert interpolated_message == "hi {{ Hugo"
end
end