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}
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 = append_interpolation(args, expression)
build_interpolation_args(rest, args)
else
_ ->
args = append_string(args, "{{")
build_interpolation_args(rest, args)
end
end

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

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

args ++ [interpolation_node]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You want to avoid appending to the end of the list, because it is expensive. The idea is that you always prepend and at the end you call Enum.reverse. This will also make operations such as append_string faster and easier, because you only need to access the head of the list. :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. ✅

end

defp append_string(args, string) do
last_arg = List.last(args)

if is_binary(last_arg) do
last_string = last_arg <> string
List.replace_at(args, -1, last_string)
else
args ++ [string]
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 == "\"Hi \#{first_name} \#{last_name}! 🎉\""
hugobarauna marked this conversation as resolved.
Show resolved Hide resolved
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 == "\"One plus one is: \#{1 + 1}\""
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)
generated_code = Macro.to_string(interpolated_ast)
{interpolated_message, _} = Code.eval_quoted(interpolated_ast, binding())

assert generated_code == "\"Do you \#{first_name}, know \#{1 + 1} ?\""
assert interpolated_message == "Do you Hugo, know 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 == "\"hi {{ \#{first_name}\""
assert interpolated_message == "hi {{ Hugo"
end
end