473 lines
18 KiB
Elixir
473 lines
18 KiB
Elixir
defmodule JSON.LD.Expansion do
|
|
@moduledoc nil
|
|
|
|
import JSON.LD.{IRIExpansion, Utils}
|
|
|
|
alias JSON.LD.{Context, Options}
|
|
alias JSON.LD.Context.TermDefinition
|
|
|
|
@spec expand(map, Options.t() | Enum.t()) :: [map]
|
|
def expand(input, options \\ %Options{}) do
|
|
options = Options.new(options)
|
|
active_context = Context.new(options)
|
|
|
|
active_context =
|
|
case options.expand_context do
|
|
%{"@context" => context} -> Context.update(active_context, context)
|
|
%{} = context -> Context.update(active_context, context)
|
|
nil -> active_context
|
|
end
|
|
|
|
case do_expand(active_context, nil, input, options) do
|
|
result = %{"@graph" => graph} when map_size(result) == 1 -> graph
|
|
nil -> []
|
|
result when not is_list(result) -> [result]
|
|
result -> result
|
|
end
|
|
end
|
|
|
|
@spec do_expand(Context.t(), String.t() | nil, any | nil, Options.t()) :: map | [map] | nil
|
|
defp do_expand(active_context, active_property, element, options)
|
|
|
|
# 1) If element is null, return null.
|
|
defp do_expand(_, _, nil, _), do: nil
|
|
|
|
# 2) If element is a scalar, ...
|
|
defp do_expand(active_context, active_property, element, _options)
|
|
when is_binary(element) or is_number(element) or is_boolean(element) do
|
|
if active_property in [nil, "@graph"] do
|
|
nil
|
|
else
|
|
expand_value(active_context, active_property, element)
|
|
end
|
|
end
|
|
|
|
# 3) If element is an array, ...
|
|
defp do_expand(active_context, active_property, element, options)
|
|
when is_list(element) do
|
|
term_def = active_context.term_defs[active_property]
|
|
container_mapping = term_def && term_def.container_mapping
|
|
|
|
Enum.reduce(element, [], fn item, result ->
|
|
expanded_item = do_expand(active_context, active_property, item, options)
|
|
|
|
if (active_property == "@list" or container_mapping == "@list") and
|
|
(is_list(expanded_item) or Map.has_key?(expanded_item, "@list")) do
|
|
raise JSON.LD.ListOfListsError, message: "List of lists in #{inspect(element)}"
|
|
end
|
|
|
|
case expanded_item do
|
|
nil -> result
|
|
list when is_list(list) -> result ++ list
|
|
expanded_item -> result ++ [expanded_item]
|
|
end
|
|
end)
|
|
end
|
|
|
|
# 4) - 13)
|
|
@dialyzer {:nowarn_function, do_expand: 4}
|
|
defp do_expand(active_context, active_property, element, options)
|
|
when is_map(element) do
|
|
# 5)
|
|
active_context =
|
|
if Map.has_key?(element, "@context") do
|
|
Context.update(active_context, Map.get(element, "@context"), [], options)
|
|
else
|
|
active_context
|
|
end
|
|
|
|
# 6) and 7)
|
|
result =
|
|
element
|
|
|> Enum.sort_by(fn {key, _} -> key end)
|
|
|> Enum.reduce(%{}, fn {key, value}, result ->
|
|
# 7.1)
|
|
if key != "@context" do
|
|
expanded_property = expand_iri(key, active_context, false, true)
|
|
# 7.2)
|
|
# 7.3)
|
|
if expanded_property &&
|
|
(String.contains?(expanded_property, ":") || JSON.LD.keyword?(expanded_property)) do
|
|
# 7.4)
|
|
# expanded_property is not a keyword
|
|
if JSON.LD.keyword?(expanded_property) do
|
|
# 7.4.1)
|
|
if active_property == "@reverse" do
|
|
raise JSON.LD.InvalidReversePropertyMapError,
|
|
message:
|
|
"An invalid reverse property map has been detected. No keywords apart from @context are allowed in reverse property maps."
|
|
end
|
|
|
|
# 7.4.2)
|
|
if Map.has_key?(result, expanded_property) do
|
|
raise JSON.LD.CollidingKeywordsError,
|
|
message:
|
|
"Two properties which expand to the same keyword have been detected. This might occur if a keyword and an alias thereof are used at the same time."
|
|
end
|
|
|
|
expanded_value =
|
|
case expanded_property do
|
|
# 7.4.3)
|
|
"@id" ->
|
|
if is_binary(value) do
|
|
expand_iri(value, active_context, true)
|
|
else
|
|
raise JSON.LD.InvalidIdValueError,
|
|
message: "#{inspect(value)} is not a valid @id value"
|
|
end
|
|
|
|
# 7.4.4)
|
|
"@type" ->
|
|
cond do
|
|
is_binary(value) ->
|
|
expand_iri(value, active_context, true, true)
|
|
|
|
is_list(value) and Enum.all?(value, &is_binary/1) ->
|
|
Enum.map(value, fn item ->
|
|
expand_iri(item, active_context, true, true)
|
|
end)
|
|
|
|
true ->
|
|
raise JSON.LD.InvalidTypeValueError,
|
|
message: "#{inspect(value)} is not a valid @type value"
|
|
end
|
|
|
|
# 7.4.5)
|
|
"@graph" ->
|
|
do_expand(active_context, "@graph", value, options)
|
|
|
|
# 7.4.6)
|
|
"@value" ->
|
|
if scalar?(value) or is_nil(value) do
|
|
if is_nil(value) do
|
|
{:skip, Map.put(result, "@value", nil)}
|
|
else
|
|
value
|
|
end
|
|
else
|
|
raise JSON.LD.InvalidValueObjectValueError,
|
|
message:
|
|
"#{inspect(value)} is not a valid value for the @value member of a value object; neither a scalar nor null"
|
|
end
|
|
|
|
# 7.4.7)
|
|
"@language" ->
|
|
if is_binary(value) do
|
|
String.downcase(value)
|
|
else
|
|
raise JSON.LD.InvalidLanguageTaggedStringError,
|
|
message: "#{inspect(value)} is not a valid language-tag"
|
|
end
|
|
|
|
# 7.4.8)
|
|
"@index" ->
|
|
if is_binary(value) do
|
|
value
|
|
else
|
|
raise JSON.LD.InvalidIndexValueError,
|
|
message: "#{inspect(value)} is not a valid @index value"
|
|
end
|
|
|
|
# 7.4.9)
|
|
"@list" ->
|
|
# 7.4.9.1)
|
|
if active_property in [nil, "@graph"] do
|
|
{:skip, result}
|
|
else
|
|
value = do_expand(active_context, active_property, value, options)
|
|
|
|
# Spec FIXME: need to be sure that result is a list [from RDF.rb implementation]
|
|
value = if is_list(value), do: value, else: [value]
|
|
|
|
# If expanded value is a list object, a list of lists error has been detected and processing is aborted.
|
|
# Spec FIXME: Also look at each object if result is a list [from RDF.rb implementation]
|
|
if Enum.any?(value, fn v -> Map.has_key?(v, "@list") end) do
|
|
raise JSON.LD.ListOfListsError,
|
|
message: "List of lists in #{inspect(value)}"
|
|
end
|
|
|
|
value
|
|
end
|
|
|
|
# 7.4.10)
|
|
"@set" ->
|
|
do_expand(active_context, active_property, value, options)
|
|
|
|
# 7.4.11)
|
|
"@reverse" ->
|
|
unless is_map(value) do
|
|
raise JSON.LD.InvalidReverseValueError,
|
|
message: "#{inspect(value)} is not a valid @reverse value"
|
|
end
|
|
|
|
# 7.4.11.1)
|
|
expanded_value = do_expand(active_context, "@reverse", value, options)
|
|
|
|
# 7.4.11.2) If expanded value contains an @reverse member, i.e., properties that are reversed twice, execute for each of its property and item the following steps:
|
|
new_result =
|
|
if Map.has_key?(expanded_value, "@reverse") do
|
|
Enum.reduce(
|
|
expanded_value["@reverse"],
|
|
result,
|
|
fn {property, item}, new_result ->
|
|
items = if is_list(item), do: item, else: [item]
|
|
|
|
Map.update(new_result, property, items, fn members ->
|
|
members ++ items
|
|
end)
|
|
end
|
|
)
|
|
else
|
|
result
|
|
end
|
|
|
|
# 7.4.11.3)
|
|
new_result =
|
|
if Map.keys(expanded_value) != ["@reverse"] do
|
|
reverse_map =
|
|
Enum.reduce(expanded_value, Map.get(new_result, "@reverse", %{}), fn
|
|
{property, items}, reverse_map when property != "@reverse" ->
|
|
Enum.each(items, fn item ->
|
|
if Map.has_key?(item, "@value") or Map.has_key?(item, "@list") do
|
|
raise JSON.LD.InvalidReversePropertyValueError,
|
|
message:
|
|
"invalid value for a reverse property in #{inspect(item)}"
|
|
end
|
|
end)
|
|
|
|
Map.update(reverse_map, property, items, fn members ->
|
|
members ++ items
|
|
end)
|
|
|
|
_, reverse_map ->
|
|
reverse_map
|
|
end)
|
|
|
|
Map.put(new_result, "@reverse", reverse_map)
|
|
else
|
|
new_result
|
|
end
|
|
|
|
{:skip, new_result}
|
|
|
|
_ ->
|
|
nil
|
|
end
|
|
|
|
# 7.4.12)
|
|
case expanded_value do
|
|
nil -> result
|
|
{:skip, new_result} -> new_result
|
|
expanded_value -> Map.put(result, expanded_property, expanded_value)
|
|
end
|
|
else
|
|
term_def = active_context.term_defs[key]
|
|
|
|
expanded_value =
|
|
cond do
|
|
# 7.5) Otherwise, if key's container mapping in active context is @language and value is a JSON object then value is expanded from a language map as follows:
|
|
is_map(value) && term_def && term_def.container_mapping == "@language" ->
|
|
value
|
|
|> Enum.sort_by(fn {language, _} -> language end)
|
|
|> Enum.reduce([], fn {language, language_value}, language_map_result ->
|
|
language_map_result ++
|
|
(if(is_list(language_value), do: language_value, else: [language_value])
|
|
|> Enum.map(fn
|
|
item when is_binary(item) ->
|
|
%{"@value" => item, "@language" => String.downcase(language)}
|
|
|
|
item ->
|
|
raise JSON.LD.InvalidLanguageMapValueError,
|
|
message: "#{inspect(item)} is not a valid language map value"
|
|
end))
|
|
end)
|
|
|
|
# 7.6)
|
|
is_map(value) && term_def && term_def.container_mapping == "@index" ->
|
|
value
|
|
|> Enum.sort_by(fn {index, _} -> index end)
|
|
|> Enum.reduce([], fn {index, index_value}, index_map_result ->
|
|
index_map_result ++
|
|
(
|
|
index_value =
|
|
if is_list(index_value), do: index_value, else: [index_value]
|
|
|
|
index_value = do_expand(active_context, key, index_value, options)
|
|
|
|
Enum.map(index_value, fn item -> Map.put_new(item, "@index", index) end)
|
|
)
|
|
end)
|
|
|
|
# 7.7)
|
|
true ->
|
|
do_expand(active_context, key, value, options)
|
|
end
|
|
|
|
# 7.8)
|
|
if is_nil(expanded_value) do
|
|
result
|
|
else
|
|
# 7.9)
|
|
expanded_value =
|
|
if term_def && term_def.container_mapping == "@list" &&
|
|
!(is_map(expanded_value) && Map.has_key?(expanded_value, "@list")) do
|
|
%{
|
|
"@list" =>
|
|
if(is_list(expanded_value), do: expanded_value, else: [expanded_value])
|
|
}
|
|
else
|
|
expanded_value
|
|
end
|
|
|
|
# 7.10) Otherwise, if the term definition associated to key indicates that it is a reverse property
|
|
# Spec FIXME: this is not an otherwise [from RDF.rb implementation]
|
|
# 7.11)
|
|
if term_def && term_def.reverse_property do
|
|
reverse_map = Map.get(result, "@reverse", %{})
|
|
|
|
reverse_map =
|
|
if(is_list(expanded_value), do: expanded_value, else: [expanded_value])
|
|
|> Enum.reduce(reverse_map, fn item, reverse_map ->
|
|
if Map.has_key?(item, "@value") or Map.has_key?(item, "@list") do
|
|
raise JSON.LD.InvalidReversePropertyValueError,
|
|
message: "invalid value for a reverse property in #{inspect(item)}"
|
|
end
|
|
|
|
Map.update(reverse_map, expanded_property, [item], fn members ->
|
|
members ++ [item]
|
|
end)
|
|
end)
|
|
|
|
Map.put(result, "@reverse", reverse_map)
|
|
else
|
|
expanded_value =
|
|
if is_list(expanded_value), do: expanded_value, else: [expanded_value]
|
|
|
|
Map.update(result, expanded_property, expanded_value, fn values ->
|
|
expanded_value ++ values
|
|
end)
|
|
end
|
|
end
|
|
end
|
|
else
|
|
result
|
|
end
|
|
else
|
|
result
|
|
end
|
|
end)
|
|
|
|
result =
|
|
case result do
|
|
# 8)
|
|
%{"@value" => value} ->
|
|
# 8.1)
|
|
keys = Map.keys(result)
|
|
|
|
if Enum.any?(keys, &(&1 not in ~w[@value @language @type @index])) ||
|
|
("@language" in keys and "@type" in keys) do
|
|
raise JSON.LD.InvalidValueObjectError,
|
|
message: "value object with disallowed members"
|
|
end
|
|
|
|
cond do
|
|
# 8.2)
|
|
value == nil ->
|
|
nil
|
|
|
|
# 8.3)
|
|
!is_binary(value) and Map.has_key?(result, "@language") ->
|
|
raise JSON.LD.InvalidLanguageTaggedValueError,
|
|
message: "@value '#{inspect(value)}' is tagged with a language"
|
|
|
|
# 8.4)
|
|
(type = result["@type"]) && !RDF.uri?(type) ->
|
|
raise JSON.LD.InvalidTypedValueError,
|
|
message: "@value '#{inspect(value)}' has invalid type #{inspect(type)}"
|
|
|
|
true ->
|
|
result
|
|
end
|
|
|
|
# 9)
|
|
%{"@type" => type} when not is_list(type) ->
|
|
Map.put(result, "@type", [type])
|
|
|
|
# 10)
|
|
%{"@set" => set} ->
|
|
validate_set_or_list_object(result)
|
|
set
|
|
|
|
%{"@list" => _} ->
|
|
validate_set_or_list_object(result)
|
|
result
|
|
|
|
_ ->
|
|
result
|
|
end
|
|
|
|
# 11) 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
|
|
|
|
# 12) If active property is null or @graph, drop free-floating values as follows:
|
|
# Spec FIXME: Due to case 10) we might land with a list here; other implementations deal with that, by just returning in step 10)
|
|
result =
|
|
if is_map(result) and active_property in [nil, "@graph"] 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"))),
|
|
do: nil,
|
|
else: result
|
|
|
|
result
|
|
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.InvalidSetOrListObjectError,
|
|
message: "set or list object with disallowed members: #{inspect(object)}"
|
|
end
|
|
|
|
@doc """
|
|
Details at <http://json-ld.org/spec/latest/json-ld-api/#value-expansion>
|
|
"""
|
|
@spec expand_value(Context.t(), String.t(), any) :: map
|
|
def expand_value(active_context, active_property, value) do
|
|
term_def = Map.get(active_context.term_defs, active_property, %TermDefinition{})
|
|
|
|
cond do
|
|
term_def.type_mapping == "@id" ->
|
|
%{"@id" => expand_iri(value, active_context, true, false)}
|
|
|
|
term_def.type_mapping == "@vocab" ->
|
|
%{"@id" => expand_iri(value, active_context, true, true)}
|
|
|
|
type_mapping = term_def.type_mapping ->
|
|
%{"@value" => value, "@type" => type_mapping}
|
|
|
|
is_binary(value) ->
|
|
language_mapping = term_def.language_mapping
|
|
|
|
cond do
|
|
language_mapping ->
|
|
%{"@value" => value, "@language" => language_mapping}
|
|
|
|
language_mapping == false && active_context.default_language ->
|
|
%{"@value" => value, "@language" => active_context.default_language}
|
|
|
|
true ->
|
|
%{"@value" => value}
|
|
end
|
|
|
|
true ->
|
|
%{"@value" => value}
|
|
end
|
|
end
|
|
end
|