diff --git a/lib/statifier_examples_web/event_phrasing.ex b/lib/statifier_examples_web/event_phrasing.ex index d6c8f4b..fc51823 100644 --- a/lib/statifier_examples_web/event_phrasing.ex +++ b/lib/statifier_examples_web/event_phrasing.ex @@ -7,9 +7,9 @@ defmodule StatifierExamplesWeb.EventPhrasing do and it names an event by its name: "Send loan.closed", "Wait for copy.returned". A name is what a host sends and what a document listens for, so it is the right thing to author; it is not what a reader of the - Plan view should have to decode. The Plan view's map, its panel and its - description region draw a block's sentence through `sentence/1` here, so - the two teaching documents read in words. The list keeps the package's + Plan view should have to decode. The Plan view's panel draws a block's + sentence through `sentence/1` here, and its map and description region + read `phrase/1`, so the two teaching documents read in words. The list keeps the package's sentence, the name as authored, and so do the places a name is a value rather than prose: a block's settings and what a document or a rule listens for. @@ -26,7 +26,11 @@ defmodule StatifierExamplesWeb.EventPhrasing do |---|---| | sends an event | "Send word that" and the words | | waits for an event | "Wait until" and the words | - | names an event anywhere else | the words | + | starts a rule with an event | "When" and the words | + + Only a name in one of those three places is an event, so only those are + read as words. A known name anywhere else - a value a step sets, a + decision's arm - is left as written. """ alias StatifierBlocks.ViewModel @@ -47,10 +51,12 @@ defmodule StatifierExamplesWeb.EventPhrasing do "registration.deadline" => "the registration week is up" } - # An event name is dotted words, so a match is only whole where neither - # side runs on into another word character or dot. - @left "(? - if String.contains?(acc, event), do: phrased(acc, Regex.escape(event), words), else: acc - end) - end + def line(line) when is_binary(line), + do: Regex.replace(@event_position, line, &phrased/4) @doc "A block's sentence, its event name read as words." @spec sentence(Node.t()) :: String.t() def sentence(%Node{} = node), do: node |> ViewModel.sentence() |> line() - @spec phrased(String.t(), String.t(), String.t()) :: String.t() - defp phrased(line, name, words) do - line - |> replace("\\b([Ss]end) " <> @left <> name <> @right, "\\1 word that " <> words) - |> replace("\\b([Ww]ait) for " <> @left <> name <> @right, "\\1 until " <> words) - |> replace(@left <> name <> @right, words) + # One match of `@event_position`: the whole match, the "Send" or "send" + # it opened with (or ""), the "Wait" or "wait" (or ""), and the name. + @spec phrased(String.t(), String.t(), String.t(), String.t()) :: String.t() + defp phrased(whole, send, wait, event) do + case phrase(event) do + nil -> whole + words when send != "" -> send <> " word that " <> words + words when wait != "" -> wait <> " until " <> words + words -> "When " <> words + end end - - @spec replace(String.t(), String.t(), String.t()) :: String.t() - defp replace(line, pattern, replacement), - do: pattern |> Regex.compile!() |> Regex.replace(line, replacement) end diff --git a/test/statifier_examples_web/event_phrasing_test.exs b/test/statifier_examples_web/event_phrasing_test.exs index ef21bc3..2379cbd 100644 --- a/test/statifier_examples_web/event_phrasing_test.exs +++ b/test/statifier_examples_web/event_phrasing_test.exs @@ -76,6 +76,26 @@ defmodule StatifierExamplesWeb.EventPhrasingTest do assert EventPhrasing.line("Send old.loan.closed") == "Send old.loan.closed" end + # Sabotage: made the send rule match "Send" only, dropping the delayed + # send's lower-case "send"; this went red. Reverted from a copy. + test "a delayed send says what it announces" do + assert EventPhrasing.line("In 7 days, send registration.deadline") == + "In 7 days, send word that the registration week is up" + end + + # A known name is an event only after Send, Wait for or a rule's When; + # a decision's arm or a value a step sets keeps the name as written. + # + # Sabotage: put back the earlier rule that read a known name anywhere + # in the line as words; this went red. Reverted from a copy. + test "leaves a known name outside event position as written" do + assert EventPhrasing.line(~s(Decide: When "loan.closed", otherwise)) == + ~s(Decide: When "loan.closed", otherwise) + + assert EventPhrasing.line("Set loan.closed") == "Set loan.closed" + assert EventPhrasing.line("Wait 21d until copy.returned") == "Wait 21d until copy.returned" + end + test "leaves a line with no known event name as it is" do assert EventPhrasing.line("Send payment.settled") == "Send payment.settled" assert EventPhrasing.line("Wait 21d") == "Wait 21d" @@ -102,6 +122,42 @@ defmodule StatifierExamplesWeb.EventPhrasingTest do assert view_model |> ViewModel.find_node("wait") |> EventPhrasing.sentence() == "Wait 30s" end + + # A decision whose condition tests a value named like a known event, + # and a step that sets one, read exactly as the package writes them. + # + # Sabotage: put back the earlier rule that read a known name anywhere + # in the line as words; this went red on the set step. Reverted from a + # copy. + test "leaves a decision and a set step as the package writes them" do + root = + Block.new("core.sequence", + id: "root", + slots: %{ + "body" => [ + Block.new("core.assign", + id: "record", + config: %{"path" => "loan.closed", "expr" => "true"} + ), + Block.new("core.branch", + id: "decide", + config: %{"arms" => [%{"slot" => "arm_closed", "cond" => "loan.closed == true"}]}, + slots: %{"arm_closed" => [Block.new("core.wait", id: "pause")]} + ) + ] + } + ) + + view_model = root |> Document.new() |> ViewModel.build(Charts.palette(), []) + + for id <- ["record", "decide"] do + node = ViewModel.find_node(view_model, id) + assert EventPhrasing.sentence(node) == ViewModel.sentence(node) + end + + assert view_model |> ViewModel.find_node("record") |> EventPhrasing.sentence() == + "Set loan.closed" + end end # Every "event" config value in the document, and every event it accepts.