Current section

Files

Jump to
json_ld lib json ld expansion.ex
Raw

lib/json/ld/expansion.ex

defmodule JSON.LD.Expansion do
@moduledoc """
Implementation of the JSON-LD 1.1 Expansion Algorithms.
<https://www.w3.org/TR/json-ld11-api/#expansion-algorithm>
"""
import JSON.LD.{IRIExpansion, Utils}
alias JSON.LD.{Context, Options}
alias JSON.LD.Context.TermDefinition
defp init_options(options) do
options
|> Keyword.put_new(:frame_expansion, false)
|> Keyword.put_new(:ordered, false)
|> Keyword.put_new(:from_map, false)
end
def expand(active_context, active_property, element, options, processor_options) do
options = init_options(options)
# 2) If active property is @default, initialize the frameExpansion flag to false
options =
if active_property == "@default",
do: Keyword.put(options, :frame_expansion, false),
else: options
# 3) If active property has a term definition in active context with a local context, initialize property-scoped context to that local context.
property_scoped_context =
if term_def = active_property && active_context.term_defs[active_property] do
term_def.local_context
end
do_expand(
active_context,
active_property,
element,
Keyword.put(options, :property_scoped_context, property_scoped_context),
processor_options
)
end
# 1) If element is null, return null.
defp do_expand(_, _, nil, _, _), do: nil
# 4) If element is a scalar, ...
defp do_expand(active_context, active_property, element, options, processor_options)
when is_binary(element) or is_number(element) or is_boolean(element) do
if active_property in [nil, "@graph"] do
nil
else
active_context =
if property_scoped_context = options[:property_scoped_context] do
Context.update(active_context, property_scoped_context,
base: (term_def = active_context.term_defs[active_property]) && term_def.base_url,
processor_options: processor_options
)
else
active_context
end
expand_value(active_context, active_property, element, processor_options)
end
end
# 5) If element is an array, ...
defp do_expand(active_context, active_property, element, options, processor_options)
when is_list(element) do
term_def = active_context.term_defs[active_property]
container_mapping = term_def && term_def.container_mapping
Enum.flat_map(element, fn item ->
expanded_item = expand(active_context, active_property, item, options, processor_options)
if container_mapping && "@list" in container_mapping && is_list(expanded_item) do
%{"@list" => expanded_item}
else
expanded_item
end
|> List.wrap()
end)
end
# 6) - 20)
defp do_expand(active_context, active_property, element, options, processor_options)
when is_map(element) do
# 7)
active_context =
if active_context.previous_context do
expanded_elements =
Enum.map(element, fn {key, _} ->
expand_iri(key, active_context, processor_options, false, true)
end)
if !options[:from_map] && "@value" not in expanded_elements &&
expanded_elements != ["@id"] do
active_context.previous_context
else
active_context
end
else
active_context
end
# 8)
active_context =
if property_scoped_context = options[:property_scoped_context] do
Context.update(
active_context,
property_scoped_context,
base: (term_def = active_context.term_defs[active_property]) && term_def.base_url,
override_protected: true,
processor_options: processor_options
)
else
active_context
end
# 9)
active_context =
if Map.has_key?(element, "@context") do
Context.update(active_context, Map.get(element, "@context"), processor_options)
else
active_context
end
# 10)
type_scoped_context = active_context
# 11)
sorted = Enum.sort_by(element, fn {key, _} -> key end)
{active_context, first_type_key} =
Enum.reduce(sorted, {active_context, nil}, fn {key, value}, {context, first_type_key} ->
if expand_iri(key, context, processor_options, false, true) == "@type" do
{
value
|> List.wrap()
|> Enum.sort()
|> Enum.reduce(context, fn
term, context when is_binary(term) ->
type_scoped_term_def = type_scoped_context.term_defs[term]
if local_context = type_scoped_term_def && type_scoped_term_def.local_context do
term_def = context.term_defs[term]
Context.update(context, local_context,
base: (term_def && term_def.base_url) || processor_options.base,
propagate: false,
processor_options: processor_options
)
else
context
end
_, context ->
context
end),
first_type_key || key
}
else
{context, first_type_key}
end
end)
# 12)
input_type =
element[first_type_key]
|> List.wrap()
|> List.last()
|> case do
nil -> nil
input_type -> expand_iri(input_type, active_context, processor_options, false, true)
end
# 13)
result =
if(processor_options.ordered, do: sorted, else: element)
|> expand_object(
active_property,
active_context,
type_scoped_context,
input_type,
processor_options,
options
)
result =
case result do
# 15)
%{"@value" => value} ->
# 15.1)
keys = Map.keys(result)
if keys -- ~w[@direction @index @language @type @value] != [] ||
(("@language" in keys or "@direction" in keys) and "@type" in keys) do
raise JSON.LD.Error.invalid_value_object("value object with disallowed members")
end
cond do
# 15.2)
result["@type"] == "@json" ->
result
# 15.3)
value == nil or value == [] ->
nil
# 15.4)
!is_binary(value) and Map.has_key?(result, "@language") ->
raise JSON.LD.Error.invalid_language_tagged_value(
"@value '#{inspect(value)}' is tagged with a language"
)
# 15.5)
(type = result["@type"]) && !(is_binary(type) and valid_uri?(type)) ->
raise JSON.LD.Error.invalid_typed_value(value, type)
true ->
result
end
# 16)
%{"@type" => type} when not is_list(type) ->
Map.put(result, "@type", [type])
# 17)
%{"@set" => set} ->
validate_set_or_list_object(result)
set
%{"@list" => _} ->
validate_set_or_list_object(result)
result
_ ->
result
end
# 18) If result contains only the key @language, set result to null.
result =
if is_map(result) and map_size(result) == 1 and Map.has_key?(result, "@language"),
do: nil,
else: result
# 19) If active property is null or @graph, drop free-floating values
# SPEC ISSUE: the "only" in "or contains only the entries @value or @list" in 19.1 had to be ignored
if active_property in [nil, "@graph"] and is_map(result) and
(Enum.empty?(result) or
(Map.has_key?(result, "@value") or Map.has_key?(result, "@list") or
(map_size(result) == 1 and Map.has_key?(result, "@id") and
not processor_options.frame_expansion))),
do: nil,
else: result
end
defp expand_object(
element,
active_property,
active_context,
type_scoped_context,
input_type,
processor_options,
options
) do
processing_mode = processor_options.processing_mode
frame_expansion = processor_options.frame_expansion
# 13)
{result, nests} =
Enum.reduce(element, {%{}, []}, fn {key, value}, {result, nests} ->
# 13.1)
if key == "@context" do
{result, nests}
else
# 13.2)
expanded_property =
expand_iri(key, active_context, processor_options, false, true)
keyword? = JSON.LD.keyword?(expanded_property)
# 13.3)
if expanded_property && (String.contains?(expanded_property, ":") || keyword?) do
# 13.4)
if keyword? do
# 13.4.1)
if active_property == "@reverse" do
raise JSON.LD.Error.invalid_reverse_property_map()
end
# 13.4.2)
if Map.has_key?(result, expanded_property) and
(processing_mode == "json-ld-1.0" or
expanded_property not in ["@included", "@type"]) do
raise JSON.LD.Error.colliding_keywords(expanded_property)
end
expanded_value =
case expanded_property do
# 13.4.3)
"@id" ->
cond do
is_binary(value) ->
expand_iri(value, active_context, processor_options, true, false)
true ->
raise JSON.LD.Error.invalid_id_value(value)
end
# 13.4.4)
"@type" ->
expanded_value =
cond do
is_binary(value) ->
expand_iri(value, type_scoped_context, processor_options, true, true)
is_list(value) and Enum.all?(value, &is_binary/1) ->
Enum.map(value, fn item ->
expand_iri(item, type_scoped_context, processor_options, true, true)
end)
true ->
raise JSON.LD.Error.invalid_type_value(value)
end
if existing = result["@type"] do
List.wrap(existing) ++ List.wrap(expanded_value)
else
expanded_value
end
# 13.4.5)
"@graph" ->
active_context
|> expand("@graph", value, options, processor_options)
|> List.wrap()
# 13.4.6)
"@included" when processing_mode != "json-ld-1.0" ->
expanded_value =
active_context
|> expand(nil, value, options, processor_options)
|> to_list()
unless Enum.all?(expanded_value, &node?/1) do
raise JSON.LD.Error.invalid_included_value(expanded_value)
end
expanded_value ++ List.wrap(result["@included"])
# 13.4.7)
"@value" ->
cond do
input_type == "@json" and processing_mode != "json-ld-1.0" ->
value
frame_expansion and is_list(value) ->
value
frame_expansion and value == %{} ->
[value]
not (scalar?(value) or is_nil(value)) ->
raise JSON.LD.Error.invalid_value_object_value(value)
true ->
if is_nil(value) do
{:skip, Map.put(result, "@value", nil)}
else
value
end
end
# 13.4.8)
"@language" ->
cond do
frame_expansion and is_list(value) ->
Enum.map(value, &validate_and_normalize_language(&1, processor_options))
frame_expansion and value == %{} ->
[value]
is_binary(value) ->
expanded_value = validate_and_normalize_language(value, processor_options)
if frame_expansion do
[expanded_value]
else
expanded_value
end
true ->
raise JSON.LD.Error.invalid_language_tagged_string(value)
end
# 13.4.9)
"@direction" when processing_mode != "json-ld-1.0" ->
cond do
value in ~w[ltr rtl] ->
if frame_expansion do
[value]
else
value
end
frame_expansion and is_list(value) and
Enum.all?(value, &(&1 in ~w[ltr rtl])) ->
value
frame_expansion and value == %{} ->
[value]
true ->
raise JSON.LD.Error.invalid_base_direction(value)
end
# 13.4.10)
"@index" ->
if is_binary(value) do
value
else
raise JSON.LD.Error.invalid_index_value(value)
end
# 13.4.11)
"@list" ->
if active_property in [nil, "@graph"] do
{:skip, result}
else
active_context
|> expand(active_property, value, options, processor_options)
|> List.wrap()
end
# 13.4.12)
"@set" ->
expand(active_context, active_property, value, options, processor_options)
# 13.4.13)
"@reverse" ->
# 13.4.13.1)
unless is_map(value) do
raise JSON.LD.Error.invalid_reverse_value(value)
end
# 13.4.13.2)
expanded_value =
expand(active_context, "@reverse", value, options, processor_options)
# 13.4.13.3)
new_result =
if Map.has_key?(expanded_value, "@reverse") do
Enum.reduce(
expanded_value["@reverse"],
result,
fn {property, item}, new_result ->
items = List.wrap(item)
Map.update(new_result, property, items, &(&1 ++ items))
end
)
else
result
end
# 13.4.13.4)
new_result =
if Map.keys(expanded_value) != ["@reverse"] do
expanded_value = Map.delete(expanded_value, "@reverse")
reverse_map =
Enum.reduce(expanded_value, Map.get(new_result, "@reverse", %{}), fn
{property, items}, reverse_map ->
Enum.each(items, fn item ->
if value?(item) or list?(item) do
raise JSON.LD.Error.invalid_reverse_property_value(item)
end
end)
Map.update(reverse_map, property, items, &(&1 ++ items))
end)
Map.put(new_result, "@reverse", reverse_map)
else
new_result
end
{:skip, new_result}
# 13.4.14)
"@nest" ->
{:nests, [key | nests]}
# 13.4.15)
expanded_property
when frame_expansion and
expanded_property in ~w[@default @embed @explicit @omitDefault @requireAll] ->
active_context
|> expand(expanded_property, value, options, processor_options)
|> List.wrap()
_ ->
nil
end
# 13.4.16) and 13.4.17)
case {expanded_value, expanded_property, input_type} do
{{:nests, nests}, _, _} -> {result, nests}
{{:skip, new_result}, _, _} -> {new_result, nests}
{nil, "@value", type} when type != "@json" -> {result, nests}
_ -> {Map.put(result, expanded_property, expanded_value), nests}
end
else
term_def = active_context.term_defs[key]
# 13.5)
container_mapping = List.wrap(term_def && term_def.container_mapping)
expanded_value =
cond do
# 13.6)
term_def && term_def.type_mapping == "@json" ->
%{"@value" => value, "@type" => "@json"}
# 13.7)
is_map(value) and "@language" in container_mapping ->
direction = TermDefinition.direction(term_def, active_context)
value
|> maybe_sort_by(processor_options.ordered, fn {language, _} -> language end)
|> Enum.flat_map(fn {language, language_value} ->
expanded_language =
expand_iri(language, active_context, options, false, true)
language_value
|> List.wrap()
|> Enum.flat_map(fn
nil ->
[]
item when is_binary(item) ->
v = %{"@value" => item}
v =
if language == "@none" or expanded_language == "@none" do
v
else
Map.put(
v,
"@language",
validate_and_normalize_language(language, processor_options)
)
end
if direction do
Map.put(v, "@direction", to_string(direction))
else
v
end
|> List.wrap()
item ->
raise JSON.LD.Error.invalid_language_map_value(item)
end)
end)
# 13.8)
is_map(value) and Enum.any?(~w[@index @type @id], &(&1 in container_mapping)) ->
# 13.8.2)
index_key = (term_def && term_def.index_mapping) || "@index"
# 13.8.3.1)
# Note: We're doing this outside of the loop
container_context =
if "@type" in container_mapping or
"@id" in container_mapping do
active_context.previous_context
end || active_context
# 13.8.3)
value
|> maybe_sort_by(processor_options.ordered, fn {index, _} -> index end)
|> Enum.flat_map(fn {index, index_value} ->
# 13.8.3.1) is done above outside of this loop
# 13.8.3.2)
index_term_def = container_context.term_defs[index]
map_context =
if "@type" in container_mapping && index_term_def &&
index_term_def.local_context do
Context.update(container_context, index_term_def.local_context,
base: term_def.base_url,
processor_options: processor_options
)
else
container_context
end
# 13.8.3.4)
# SPEC ISSUE: which context should be used here?
expanded_index = expand_iri(index, map_context, options, false, true)
# 13.8.3.6)
map_context
|> expand(
key,
# 13.8.3.5)
List.wrap(index_value),
Keyword.put(options, :from_map, true),
processor_options
)
# 13.8.3.7)
|> Enum.map(fn item ->
# 13.8.3.7.1)
item =
if "@graph" in container_mapping and not graph?(item) do
%{"@graph" => List.wrap(item)}
else
item
end
cond do
expanded_index == "@none" ->
item
# 13.8.3.7.2)
"@index" in container_mapping and index_key != "@index" ->
re_expanded_index =
expand_value(active_context, index_key, index, processor_options)
# SPEC ISSUE: vocab option had to be set to true
expanded_index_key =
expand_iri(
index_key,
container_context,
processor_options,
false,
true
)
item =
Map.update(
item,
expanded_index_key,
[re_expanded_index],
&[re_expanded_index | List.wrap(&1)]
)
if value?(item) and map_size(Map.delete(item, "@value")) > 0 do
raise JSON.LD.Error.invalid_value_object(
"Attempt to add illegal key to value object: #{inspect(item)}"
)
else
item
end
# 13.8.3.7.3)
"@index" in container_mapping and not Map.has_key?(item, "@index") ->
Map.put(item, "@index", index)
# 13.8.3.7.4)
"@id" in container_mapping and not Map.has_key?(item, "@id") ->
Map.put(
item,
"@id",
expand_iri(index, map_context, processor_options, true, false)
)
# 13.8.3.7.5)
"@type" in container_mapping ->
Map.update(
item,
"@type",
[expanded_index],
&[expanded_index | List.wrap(&1)]
)
true ->
item
end
end)
end)
# 13.9)
true ->
expand(active_context, key, value, options, processor_options)
end
# 13.10)
if is_nil(expanded_value) do
{result, nests}
else
# 13.11)
expanded_value =
if "@list" in container_mapping and not list?(expanded_value) do
%{"@list" => List.wrap(expanded_value)}
else
expanded_value
end
# 13.12)
expanded_value =
if "@graph" in container_mapping and
"@id" not in container_mapping and
"@index" not in container_mapping do
expanded_value
|> List.wrap()
|> Enum.map(fn ev -> %{"@graph" => List.wrap(ev)} end)
else
expanded_value
end
# 13.13)
if term_def && term_def.reverse_property do
reverse_map = Map.get(result, "@reverse", %{})
reverse_map =
expanded_value
|> List.wrap()
|> Enum.reduce(reverse_map, fn item, reverse_map ->
if Map.has_key?(item, "@value") or Map.has_key?(item, "@list") do
raise JSON.LD.Error.invalid_reverse_property_value(item)
end
Map.update(reverse_map, expanded_property, [item], fn members ->
members ++ [item]
end)
end)
{Map.put(result, "@reverse", reverse_map), nests}
else
# 13.14)
expanded_value = List.wrap(expanded_value)
{
Map.update(result, expanded_property, expanded_value, fn values ->
expanded_value ++ values
end),
nests
}
end
end
end
else
{result, nests}
end
end
end)
# 14)
nests
|> maybe_sort(processor_options.ordered)
|> Enum.reduce(result, fn nesting_key, result ->
# SPEC ISSUE: this nest_context creation is not specified, but required to pass #tc037 and #tc038
term_def = active_context.term_defs[nesting_key]
nest_context =
if nest_context = term_def && term_def.local_context do
Context.update(
active_context,
nest_context,
override_protected: true,
processor_options: processor_options
)
else
active_context
end
nested_values =
case element do
element when is_map(element) ->
element[nesting_key]
element when is_list(element) ->
Enum.find_value(element, fn {k, v} -> if k == nesting_key, do: v end)
end
|> List.wrap()
Enum.reduce(nested_values, result, fn nested_value, result ->
if not is_map(nested_value) or
Enum.any?(nested_value, fn {k, _} ->
expand_iri(k, nest_context, processor_options, false, true) == "@value"
end) do
raise JSON.LD.Error.invalid_nest_value(invalid: nested_value)
end
deep_merge(
result,
expand_object(
nested_value,
active_property,
nest_context,
type_scoped_context,
input_type,
processor_options,
options
)
)
end)
end)
end
@spec validate_set_or_list_object(map) :: true
defp validate_set_or_list_object(object) when map_size(object) == 1, do: true
defp validate_set_or_list_object(%{"@index" => _} = object) when map_size(object) == 2, do: true
defp validate_set_or_list_object(object) do
raise JSON.LD.Error.invalid_set_or_list_object(object)
end
@doc """
Details at <http://json-ld.org/spec/latest/json-ld-api/#value-expansion>
"""
@spec expand_value(Context.t(), String.t(), any, Options.t()) :: map
def expand_value(active_context, active_property, value, options \\ Options.new()) do
term_def = Map.get(active_context.term_defs, active_property, %TermDefinition{})
cond do
# 1)
term_def.type_mapping == "@id" and is_binary(value) ->
%{"@id" => expand_iri(value, active_context, options, true, false)}
# 2)
term_def.type_mapping == "@vocab" and is_binary(value) ->
%{"@id" => expand_iri(value, active_context, options, true, true)}
true ->
result = %{"@value" => value}
cond do
(type_mapping = term_def.type_mapping) && type_mapping not in ~w[@id @vocab @none] ->
Map.put(result, "@type", type_mapping)
is_binary(value) ->
language = TermDefinition.language(term_def, active_context)
direction = TermDefinition.direction(term_def, active_context)
result =
if not is_nil(language) do
Map.put(result, "@language", language)
else
result
end
if not is_nil(direction) do
Map.put(result, "@direction", to_string(direction))
else
result
end
true ->
result
end
end
end
end