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
34 changes: 30 additions & 4 deletions lib/statifier_examples/typed_send_step.ex
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,17 @@ defmodule StatifierExamples.TypedSendStep do

`payload` is a list of datamodel paths: dotted identifiers such as
`loan.id`, each named once. `validate_config/1` refuses an entry that is
not one, and a duplicate. It names paths and never literal values: what
reaches the receiver is whatever the chart's datamodel holds at those
paths, so a chart that holds only ids sends only ids, and the receiver
reads the rest by them.
not one, and a duplicate. It also refuses an entry that names a keyword
of the expression language a `<param>`'s `expr` is read in: `true`,
`false`, `null` or `undefined` would hand the receiver a literal rather
than the value at a path, and an operator or a statement word is no
expression at all. A keyword is refused as a path's first segment and
as a later one, except the five duration words (`ago`, `from`, `now`,
`next`, `last`), which the language reads as a property name after a
dot, so `patron.last` stays a path. It names paths and never literal
values: what reaches the receiver is whatever the chart's datamodel
holds at those paths, so a chart that holds only ids sends only ids, and
the receiver reads the rest by them.

Whether a path is *declared* in the document's datamodel is not
something this type can refuse: `validate_config/1` is handed the
Expand All @@ -72,6 +79,16 @@ defmodule StatifierExamples.TypedSendStep do

@path ~r/\A[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*\z/

# The words the expression language (predicator) reads as something other
# than a name: its literals, its operators in both cases, its statement
# words and its duration words. No path segment may be one of them, except
# that a segment after a dot may be a duration word, which the language
# reads as a property name there.
@keywords ~w(true false null undefined and or not in contains AND OR NOT IN CONTAINS) ++
~w(if else while ago from now next last)

@property_words ~w(ago from now next last)

@doc "The name this app registers the type under."
@spec type_name() :: String.t()
def type_name, do: @type_name
Expand Down Expand Up @@ -177,6 +194,9 @@ defmodule StatifierExamples.TypedSendStep do
not Enum.all?(payload, &path?/1) ->
[{@payload, "every entry must be a datamodel path, like loan.id"}]

Enum.any?(payload, &keyword?/1) ->
[{@payload, "must not name a keyword of the expression language, like true"}]

length(Enum.uniq(payload)) != length(payload) ->
[{@payload, "names a path more than once"}]

Expand All @@ -189,5 +209,11 @@ defmodule StatifierExamples.TypedSendStep do

defp path?(value), do: is_binary(value) and Regex.match?(@path, value)

@spec keyword?(String.t()) :: boolean()
defp keyword?(path) do
[first | rest] = String.split(path, ".")
first in @keywords or Enum.any?(rest, &(&1 in @keywords and &1 not in @property_words))
end

defp filled?(value), do: is_binary(value) and value != ""
end
49 changes: 48 additions & 1 deletion test/statifier_examples/typed_send_step_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ defmodule StatifierExamples.TypedSendStepTest do

# Sabotage: emitted the payload's params in reverse; this went red on the
# param order, then reverted.
test "with a payload the compiled send carries one param per declared path, in order" do
test "with a payload the compiled send carries one param per listed path, in order" do
config = Map.put(@config, "payload", ["loan.id", "loan.copy_id"])

assert send_element(config) ==
Expand All @@ -53,6 +53,46 @@ defmodule StatifierExamples.TypedSendStepTest do
assert :ok = TypedSendStep.validate_config(Map.put(@config, "payload", ["loan.id", "_x"]))
end

# Sabotage: dropped the keyword branch from payload_findings/1; this went
# red on "true", then reverted.
test "a payload entry that names a keyword of the expression language is refused" do
for payload <- [["true"], ["false"], ["null"], ["undefined"], ["true.id"], ["loan.null"]] do
assert {:error,
[{"payload", "must not name a keyword of the expression language, like true"}]} =
TypedSendStep.validate_config(Map.put(@config, "payload", payload))
end

assert :ok =
TypedSendStep.validate_config(Map.put(@config, "payload", ["patron.last", "nil"]))
end

# Every word the expression language reserves, written as a path's first
# segment, as a middle one and as the last, beside ordinary names. The
# step admits an entry exactly when the locked predicator reads it as a
# datamodel path: an identifier followed by property names, never a
# literal, an operator or a parse error. `loan.last` is a path (a property
# may be named by one of the duration words) and `nil` is one too (the
# language spells its null `null`).
# Sabotage: dropped the later-segment check from keyword?/1; this went red
# on "loan.true", then reverted. Dropping the duration-word exemption went
# red on "loan.ago", and checking only the last segment went red on
# "loan.true.id"; each was reverted.
test "a payload entry is admitted exactly when the expression language reads it as a path" do
words =
~w(true false null undefined and or not in contains if else while) ++
~w(AND OR NOT IN CONTAINS ago from now next last) ++
~w(loan id nil TRUE Null _x)

for word <- words,
entry <- [word, "loan." <> word, word <> ".id", "loan." <> word <> ".id"] do
admitted? = TypedSendStep.validate_config(Map.put(@config, "payload", [entry])) == :ok

assert admitted? == datamodel_path?(Predicator.parse(entry)),
"#{inspect(entry)}: the step answered admitted?=#{admitted?}, " <>
"the expression language reads #{inspect(Predicator.parse(entry))}"
end
end

# Sabotage: dropped "event" from @keys; this went red on the missing
# finding, then reverted.
test "type, target and event are each the author's to set" do
Expand All @@ -62,6 +102,13 @@ defmodule StatifierExamples.TypedSendStepTest do
end
end

defp datamodel_path?({:ok, {:identifier, _name, _pos}}), do: true

defp datamodel_path?({:ok, {:property_access, inner, _name, _pos}}),
do: datamodel_path?({:ok, inner})

defp datamodel_path?(_other), do: false

defp send_element(config) do
document = %{
"schema_version" => 1,
Expand Down
Loading