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.

+
+

+ To dynamically inject values into the query use double curly braces, like {{name}}. +

+
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