Packages
Generic Elixir models and portable validation for RelayMark documents and manifests
Current section
Files
Jump to
Current section
Files
lib/relay_mark/manifest_projection.ex
defmodule RelayMark.ManifestProjection do
@moduledoc false
@commonmark_inline_types ~w(text hard_break soft_break link emphasis strong code_span)
@block_reference_types ~w(relay.evidence_ref relay.file_ref relay.note_ref relay.url_ref)
@spec project(map(), [map()]) :: map()
def project(document, diagnostics \\ []) when is_map(document) and is_list(diagnostics) do
resources_by_id =
document
|> Map.get("resources", [])
|> Map.new(&{&1["id"], &1})
manifest = %{
"type" => "relaymark.manifest",
"version" => 1,
"actions" => [],
"mentions" => [],
"references" => [],
"forms" => [],
"sources" => [],
"claims" => [],
"comparisons" => [],
"diagnostics" => diagnostics
}
manifest =
document
|> Map.get("blocks", [])
|> project_blocks(manifest, resources_by_id)
manifest
|> Map.update!("mentions", &Enum.uniq/1)
|> Map.update!("references", &Enum.uniq/1)
end
defp project_blocks(blocks, manifest, resources_by_id) do
Enum.reduce(blocks, manifest, fn block, acc ->
acc = project_block(block, acc, resources_by_id)
acc = collect_inline_references(block, acc)
child_blocks = Enum.filter(block["content"] || [], &block_node?/1)
project_blocks(child_blocks, acc, resources_by_id)
end)
end
defp project_block(%{"type" => "relay.action_row"} = block, manifest, _resources_by_id) do
props = block["props"] || %{}
action =
compact(%{
"block_id" => block["id"],
"proposal_id" => props["proposal_id"],
"proposal_record_id" => props["proposal_record_id"],
"proposal_revision" => props["proposal_revision"],
"action" => props["action"],
"target" => props["target"],
"destination" => props["destination"],
"visibility" => props["visibility"],
"review" => props["review"],
"decision_id" => props["decision_id"],
"execution_id" => props["execution_id"],
"undo_id" => props["undo_id"],
"evidence_refs" => evidence_refs_for(block),
"args" =>
Map.put(
props["args"] || %{},
"title",
block |> plain_text() |> trim_relaymark_whitespace()
)
})
manifest = append(manifest, "actions", action)
if is_binary(props["target"]) and String.starts_with?(props["target"], "entity:") do
append(manifest, "mentions", %{
"block_id" => block["id"],
"kind" => "entity",
"id" => String.replace_prefix(props["target"], "entity:", ""),
"label" => props["target"]
})
else
manifest
end
end
defp project_block(%{"type" => "relay.claim"} = block, manifest, _resources_by_id) do
props = block["props"] || %{}
append(
manifest,
"claims",
compact(%{
"block_id" => block["id"],
"key" => props["key"],
"subject" => props["subject"],
"status" => props["status"] || "unverified",
"confidence" => props["confidence"],
"evidence_refs" => evidence_refs_for(block),
"text" => block |> plain_text() |> trim_relaymark_whitespace()
})
)
end
defp project_block(%{"type" => "relay.comparison"} = block, manifest, _resources_by_id) do
props = block["props"] || %{}
append(
manifest,
"comparisons",
compact(%{
"block_id" => block["id"],
"title" => props["title"],
"subjects" => props["subjects"] || [],
"criteria" => props["criteria"] || [],
"values" => props["values"] || %{},
"preferred" => props["preferred"],
"layout" => props["layout"] || "table",
"evidence_refs" => evidence_refs_for(block),
"summary" => block |> plain_text() |> trim_relaymark_whitespace()
})
)
end
defp project_block(%{"type" => "relay.form_question"} = block, manifest, _resources_by_id) do
props = block["props"] || %{}
append(
manifest,
"forms",
compact(%{
"block_id" => block["id"],
"key" => props["key"],
"label" => props["label"],
"field_type" => props["field_type"],
"required" => props["required"] || false,
"options" => props["options"] || []
})
)
end
defp project_block(%{"type" => "relay.source_bundle"} = block, manifest, _resources_by_id) do
props = block["props"] || %{}
append(
manifest,
"sources",
compact(%{
"block_id" => block["id"],
"kind" => "source_bundle",
"source_id" => props["id"],
"bundle_revision" => props["revision"],
"source_hash" => props["source_hash"],
"label" => props["title"]
})
)
end
defp project_block(%{"type" => "relay.evidence_ref"} = block, manifest, resources_by_id) do
props = block["props"] || %{}
source_resource = resources_by_id[props["source"]]
scripture_release =
if props["source_type"] == "scripture" and is_map(source_resource) and
source_resource["type"] == "scripture" do
source_resource["props"]
end
excerpt =
block
|> plain_text()
|> trim_relaymark_whitespace()
|> empty_to_nil()
append(
manifest,
"sources",
compact(%{
"block_id" => block["id"],
"kind" => "evidence_ref",
"evidence_id" => props["id"],
"source_id" => props["source"],
"source_type" => props["source_type"],
"source_revision" => props["source_revision"],
"selector" => props["selector"],
"scope" => props["scope"],
"label" => props["label"],
"excerpt" => excerpt,
"scripture_release" => scripture_release
})
)
end
defp project_block(%{"type" => "relay.transcript_segment"} = block, manifest, _resources_by_id) do
props = block["props"] || %{}
append(
manifest,
"sources",
compact(%{
"block_id" => block["id"],
"kind" => "transcript_range",
"source_id" => props["source"],
"start" => props["start"],
"end" => props["end"],
"label" => props["speaker"]
})
)
end
defp project_block(%{"type" => "relay.file_ref"} = block, manifest, _resources_by_id) do
props = block["props"] || %{}
append(
manifest,
"sources",
compact(%{
"block_id" => block["id"],
"kind" => "file_ref",
"source_id" => props["id"],
"uri" => props["uri"],
"label" => props["label"]
})
)
end
defp project_block(%{"type" => "relay.note_ref"} = block, manifest, _resources_by_id) do
props = block["props"] || %{}
append(
manifest,
"sources",
compact(%{
"block_id" => block["id"],
"kind" => "note_ref",
"source_id" => props["document"] || props["block"],
"label" => props["label"]
})
)
end
defp project_block(%{"type" => "relay.url_ref"} = block, manifest, _resources_by_id) do
props = block["props"] || %{}
append(
manifest,
"sources",
compact(%{
"block_id" => block["id"],
"kind" => "url_ref",
"uri" => props["url"],
"label" => props["label"]
})
)
end
defp project_block(_block, manifest, _resources_by_id), do: manifest
defp collect_inline_references(block, manifest) do
Enum.reduce(inline_nodes(block["content"] || []), manifest, fn inline, acc ->
acc =
if is_binary(inline["type"]) and String.starts_with?(inline["type"], "relay.") and
String.ends_with?(inline["type"], "_ref") do
append(
acc,
"references",
compact(%{
"block_id" => block["id"],
"type" => inline["type"],
"id" => inline["id"],
"label" => inline["label"],
"href" => inline["href"]
})
)
else
acc
end
if inline["type"] == "relay.entity_ref" do
append(
acc,
"mentions",
compact(%{
"block_id" => block["id"],
"kind" => "entity",
"id" => inline["id"],
"label" => inline["label"]
})
)
else
acc
end
end)
end
defp inline_nodes(nodes) do
Enum.flat_map(nodes, fn node ->
cond do
not is_map(node) ->
[]
block_node?(node) ->
[]
inline_node?(node) ->
[node | inline_nodes(node["content"] || [])]
true ->
inline_nodes(node["content"] || [])
end
end)
end
defp plain_text(node) when is_list(node), do: Enum.map_join(node, "", &plain_text/1)
defp plain_text(%{"type" => "text"} = node), do: node["text"] || ""
defp plain_text(%{"type" => "relay." <> type, "label" => label})
when is_binary(label) do
if String.ends_with?(type, "_ref"), do: label, else: ""
end
defp plain_text(%{"content" => content}) when is_list(content), do: plain_text(content)
defp plain_text(_node), do: ""
defp block_node?(node) when is_map(node) do
is_binary(node["type"]) and not inline_node?(node)
end
defp block_node?(_node), do: false
defp inline_node?(%{"type" => type}) when type in @commonmark_inline_types, do: true
defp inline_node?(%{"type" => type}) when type in @block_reference_types, do: false
defp inline_node?(%{"type" => "relay." <> type}) do
String.ends_with?(type, "_ref")
end
defp inline_node?(_node), do: false
defp evidence_refs_for(block) do
cond do
is_list(block["evidence_refs"]) and block["evidence_refs"] != [] ->
block["evidence_refs"]
is_list(get_in(block, ["props", "evidence"])) ->
get_in(block, ["props", "evidence"])
true ->
[]
end
end
defp trim_relaymark_whitespace(value) do
value
|> String.to_charlist()
|> Enum.drop_while(&relaymark_whitespace?/1)
|> Enum.reverse()
|> Enum.drop_while(&relaymark_whitespace?/1)
|> Enum.reverse()
|> List.to_string()
end
defp relaymark_whitespace?(codepoint)
when codepoint in 0x0009..0x000D or codepoint in 0x2000..0x200A,
do: true
defp relaymark_whitespace?(codepoint)
when codepoint in [
0x0020,
0x0085,
0x00A0,
0x1680,
0x2028,
0x2029,
0x202F,
0x205F,
0x3000
],
do: true
defp relaymark_whitespace?(_codepoint), do: false
defp compact(map), do: Map.reject(map, fn {_key, value} -> is_nil(value) end)
defp append(manifest, key, value), do: Map.update!(manifest, key, &(&1 ++ [value]))
defp empty_to_nil(""), do: nil
defp empty_to_nil(value), do: value
end