diff --git a/lib/assets/main.js b/lib/assets/main.js
index 5b20439..53fdbed 100644
--- a/lib/assets/main.js
+++ b/lib/assets/main.js
@@ -36,6 +36,11 @@ export function init(ctx, payload) {
create a Slack app and get your app's token.
+
diff --git a/lib/kino_slack/message_cell.ex b/lib/kino_slack/message_cell.ex
index cbdbfbe..5e91cb3 100644
--- a/lib/kino_slack/message_cell.ex
+++ b/lib/kino_slack/message_cell.ex
@@ -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
@@ -63,7 +64,7 @@ defmodule KinoSlack.MessageCell do
url: "/chat.postMessage",
json: %{
channel: unquote(attrs["channel"]),
- text: unquote(attrs["message"])
+ text: unquote(message_ast)
}
)
@@ -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
diff --git a/lib/kino_slack/message_interpolator.ex b/lib/kino_slack/message_interpolator.ex
new file mode 100644
index 0000000..0867529
--- /dev/null
+++ b/lib/kino_slack/message_interpolator.ex
@@ -0,0 +1,45 @@
+defmodule KinoSlack.MessageInterpolator do
+ @moduledoc false
+
+ def interpolate(message) do
+ args = build_interpolation_args(message, "", [])
+ args = Enum.reverse(args)
+ {:<<>>, [], args}
+ end
+
+ defp build_interpolation_args("", buffer, acc) do
+ prepend_buffer(buffer, acc)
+ end
+
+ defp build_interpolation_args("{{" <> rest, buffer, acc) do
+ with [inner, rest] <- String.split(rest, "}}", parts: 2),
+ {:ok, expression} <- Code.string_to_quoted(inner) do
+ acc = prepend_buffer(buffer, acc)
+ acc = prepend_interpolation(expression, acc)
+ build_interpolation_args(rest, "", acc)
+ else
+ _ ->
+ build_interpolation_args(rest, <>, acc)
+ end
+ end
+
+ defp build_interpolation_args(<>, buffer, acc) do
+ build_interpolation_args(rest, <>, acc)
+ end
+
+ defp prepend_interpolation(expression, acc) do
+ interpolation_node = {
+ :"::",
+ [],
+ [
+ {{:., [], [Kernel, :to_string]}, [], [expression]},
+ {:binary, [], Elixir}
+ ]
+ }
+
+ [interpolation_node | acc]
+ end
+
+ defp prepend_buffer("", acc), do: acc
+ defp prepend_buffer(buffer, acc), do: [buffer | acc]
+end
diff --git a/test/kino_slack/message_cell_test.exs b/test/kino_slack/message_cell_test.exs
index 4f09576..689eb4a 100644
--- a/test/kino_slack/message_cell_test.exs
+++ b/test/kino_slack/message_cell_test.exs
@@ -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",
diff --git a/test/kino_slack/messsage_interpolator_test.exs b/test/kino_slack/messsage_interpolator_test.exs
new file mode 100644
index 0000000..bd1180c
--- /dev/null
+++ b/test/kino_slack/messsage_interpolator_test.exs
@@ -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)
+ 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