Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 28 additions & 25 deletions lib/statifier_examples_web/event_phrasing.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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 "(?<![\\w.])"
@right "(?![\\w.])"
# A name in event position: after "Send" (or a delayed "send"), after
# "Wait for", or after a rule's "When". An event name is dotted words, so
# the name runs until neither a word character nor a dot follows, and a
# name that only starts like a known one is a different name. Compiled
# once, with the module.
@event_position ~r/\b(?:([Ss]end)|([Ww]ait) for|When) ([\w.]+)(?![\w.])/u

@doc """
The words for `event`, or `nil` for a name the library world does not
Expand All @@ -61,29 +67,26 @@ defmodule StatifierExamplesWeb.EventPhrasing do
def phrase(_other), do: nil

@doc """
`line` with every whole event name it knows read as words; anything else
in the line is left as it is.
`line` with every whole event name it knows in event position read as
words; anything else in the line is left as it is.
"""
@spec line(String.t()) :: String.t()
def line(line) when is_binary(line) do
Enum.reduce(@phrases, line, fn {event, words}, acc ->
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
56 changes: 56 additions & 0 deletions test/statifier_examples_web/event_phrasing_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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.
Expand Down
Loading