json_ld: move expansion algorithms in separate modules
This commit is contained in:
parent
653a8c7c34
commit
4d06504f8d
6 changed files with 474 additions and 462 deletions
|
@ -110,7 +110,7 @@ defmodule JSON.LD.Context do
|
|||
@doc """
|
||||
Expands the given input according to the steps in the JSON-LD Create Term Definition Algorithm.
|
||||
|
||||
see https://www.w3.org/TR/json-ld-api/#create-term-definition
|
||||
see <https://www.w3.org/TR/json-ld-api/#create-term-definition>
|
||||
"""
|
||||
def create_term_definition(active, local, term, value, defined)
|
||||
|
||||
|
|
375
lib/json/ld/expansion.ex
Normal file
375
lib/json/ld/expansion.ex
Normal file
|
@ -0,0 +1,375 @@
|
|||
defmodule JSON.LD.Expansion do
|
||||
@moduledoc nil
|
||||
|
||||
import JSON.LD
|
||||
|
||||
|
||||
@doc """
|
||||
Expands the given input according to the steps in the JSON-LD Expansion Algorithm.
|
||||
|
||||
> Expansion is the process of taking a JSON-LD document and applying a `@context`
|
||||
> such that all IRIs, types, and values are expanded so that the `@context` is
|
||||
> no longer necessary.
|
||||
|
||||
-- <https://www.w3.org/TR/json-ld/#expanded-document-form>
|
||||
|
||||
Details at <http://json-ld.org/spec/latest/json-ld-api/#expansion-algorithm>
|
||||
"""
|
||||
def expand(json_ld_object, opts \\ []) do
|
||||
case do_expand(JSON.LD.Context.new(opts), nil, json_ld_object, Keyword.delete(opts, :base)) do
|
||||
result = %{"@graph" => graph} when map_size(result) == 1 ->
|
||||
graph
|
||||
nil ->
|
||||
[]
|
||||
result when not is_list(result) ->
|
||||
[result]
|
||||
result -> result
|
||||
end
|
||||
end
|
||||
|
||||
defp do_expand(active_context, active_property, element, opts \\ [])
|
||||
|
||||
# 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, opts)
|
||||
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, opts)
|
||||
when is_list(element) do
|
||||
term_def = active_context.term_defs[active_property]
|
||||
container_mapping = term_def && term_def.container_mapping
|
||||
element
|
||||
|> Enum.reduce([], fn (item, result) ->
|
||||
expanded_item = do_expand(active_context, active_property, item)
|
||||
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}"
|
||||
case expanded_item do
|
||||
nil -> result
|
||||
list when is_list(list) ->
|
||||
result ++ list
|
||||
expanded_item ->
|
||||
result ++ [expanded_item]
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
# 4) - 13)
|
||||
defp do_expand(active_context, active_property, element, opts)
|
||||
when is_map(element) do
|
||||
# 5)
|
||||
if Map.has_key?(element, "@context") do
|
||||
active_context = JSON.LD.Context.update(active_context, Map.get(element, "@context"))
|
||||
end
|
||||
# 6) and 7)
|
||||
result = element
|
||||
|> Enum.sort_by(fn {key, _} -> key end)
|
||||
|> Enum.reduce(%{}, fn ({key, value}, result) ->
|
||||
if (key != "@context") && # 7.1)
|
||||
(expanded_property = expand_iri(key, active_context, false, true)) && # 7.2)
|
||||
(String.contains?(expanded_property, ":") || keyword?(expanded_property)) do # 7.3)
|
||||
if keyword?(expanded_property) do # 7.4)
|
||||
if active_property == "@reverse", # 7.4.1)
|
||||
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."
|
||||
if Map.has_key?(result, expanded_property), # 7.4.2)
|
||||
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."
|
||||
|
||||
expanded_value = case expanded_property do
|
||||
"@id" -> # 7.4.3)
|
||||
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
|
||||
"@type" -> # 7.4.4)
|
||||
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
|
||||
"@graph" -> # 7.4.5)
|
||||
do_expand(active_context, "@graph", value, opts)
|
||||
"@value" -> # 7.4.6)
|
||||
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
|
||||
"@language" -> # 7.4.7)
|
||||
if is_binary(value),
|
||||
do: String.downcase(value),
|
||||
else: raise JSON.LD.InvalidLanguageTaggedStringError,
|
||||
message: "#{inspect value} is not a valid language-tag"
|
||||
"@index" -> # 7.4.8)
|
||||
if is_binary(value),
|
||||
do: value,
|
||||
else: raise JSON.LD.InvalidIndexValueError,
|
||||
message: "#{inspect value} is not a valid @index value"
|
||||
"@list" -> # 7.4.9)
|
||||
if active_property in [nil, "@graph"] do # 7.4.9.1)
|
||||
{:skip, result}
|
||||
else
|
||||
value = do_expand(active_context, active_property, value, opts)
|
||||
|
||||
# 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}"
|
||||
value
|
||||
end
|
||||
"@set" -> # 7.4.10)
|
||||
do_expand(active_context, active_property, value, opts)
|
||||
"@reverse" -> # 7.4.11)
|
||||
unless is_map(value),
|
||||
do: raise JSON.LD.InvalidReverseValueError,
|
||||
message: "#{inspect value} is not a valid @reverse value"
|
||||
expanded_value = do_expand(active_context, "@reverse", value, opts) # 7.4.11.1)
|
||||
new_result =
|
||||
if Map.has_key?(expanded_value, "@reverse") do # 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:
|
||||
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
|
||||
if Map.keys(expanded_value) != ["@reverse"] do # 7.4.11.3)
|
||||
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)
|
||||
Map.update(reverse_map, property, items, fn members ->
|
||||
members ++ items
|
||||
end)
|
||||
(_, reverse_map) -> reverse_map
|
||||
end
|
||||
new_result = Map.put(new_result, "@reverse", reverse_map)
|
||||
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 # expanded_property is not a keyword
|
||||
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
|
||||
# |> IO.inspect(label: "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)
|
||||
)
|
||||
# |> IO.inspect(label: "result")
|
||||
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, opts)
|
||||
Enum.map(index_value, fn item ->
|
||||
Map.put_new(item, "@index", index)
|
||||
end)
|
||||
)
|
||||
end)
|
||||
# 7.7)
|
||||
true ->
|
||||
do_expand(active_context, key, value, opts)
|
||||
end
|
||||
# 7.8)
|
||||
if is_nil(expanded_value) do
|
||||
result
|
||||
else
|
||||
# 7.9)
|
||||
if (term_def && term_def.container_mapping == "@list") &&
|
||||
!(is_map(expanded_value) && Map.has_key?(expanded_value, "@list")) do
|
||||
expanded_value = %{"@list" =>
|
||||
(if is_list(expanded_value),
|
||||
do: 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]
|
||||
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}"
|
||||
Map.update reverse_map, expanded_property, [item], fn members ->
|
||||
members ++ [item]
|
||||
end
|
||||
end)
|
||||
Map.put(result, "@reverse", reverse_map)
|
||||
else # 7.11)
|
||||
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
|
||||
end)
|
||||
|
||||
result = case result do
|
||||
# 8)
|
||||
%{"@value" => value} ->
|
||||
with keys = Map.keys(result) do # 8.1)
|
||||
if Enum.any?(keys, &(not &1 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
|
||||
end
|
||||
cond do
|
||||
value == nil -> nil # 8.2)
|
||||
!is_binary(value) and Map.has_key?(result, "@language") -> # 8.3)
|
||||
raise JSON.LD.InvalidLanguageTaggedValueError,
|
||||
message: "@value '#{inspect value}' is tagged with a language"
|
||||
(type = result["@type"]) && !RDF.uri?(type) -> # 8.4)
|
||||
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" => list} ->
|
||||
validate_set_or_list_object(result)
|
||||
result
|
||||
_ -> result
|
||||
end
|
||||
|
||||
# 11) If result contains only the key @language, set result to null.
|
||||
if is_map(result) and map_size(result) == 1 and Map.has_key?(result, "@language"),
|
||||
do: result = nil
|
||||
|
||||
# 12) If active property is null or @graph, drop free-floating values as follows:
|
||||
if 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: result = nil
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
defp validate_set_or_list_object(object) when map_size(object) == 1, do: true
|
||||
defp validate_set_or_list_object(object = %{"@index" => _})
|
||||
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>
|
||||
"""
|
||||
def expand_value(active_context, active_property, value) do
|
||||
with term_def when term_def != nil <- active_context.term_defs[active_property] do
|
||||
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
|
||||
not language_mapping in [nil, :undefined] ->
|
||||
%{"@value" => value, "@language" => language_mapping}
|
||||
language_mapping == :undefined && active_context.default_language ->
|
||||
%{"@value" => value, "@language" => active_context.default_language}
|
||||
true ->
|
||||
%{"@value" => value}
|
||||
end
|
||||
true ->
|
||||
%{"@value" => value}
|
||||
end
|
||||
else
|
||||
_ -> %{"@value" => value}
|
||||
end
|
||||
end
|
||||
|
||||
end
|
75
lib/json/ld/iri_expansion.ex
Normal file
75
lib/json/ld/iri_expansion.ex
Normal file
|
@ -0,0 +1,75 @@
|
|||
defmodule JSON.LD.IRIExpansion do
|
||||
|
||||
import JSON.LD
|
||||
|
||||
@keywords JSON.LD.keywords # to allow this to be used in function guard clauses, we redefine this here
|
||||
|
||||
@doc """
|
||||
see http://json-ld.org/spec/latest/json-ld-api/#iri-expansion
|
||||
"""
|
||||
def expand_iri(value, active_context, doc_relative \\ false, vocab \\ false,
|
||||
local_context \\ nil, defined \\ nil)
|
||||
|
||||
# 1) If value is a keyword or null, return value as is.
|
||||
def expand_iri(value, active_context, _, _, local_context, defined)
|
||||
when is_nil(value) or value in @keywords do
|
||||
if local_context || defined do
|
||||
{value, active_context, defined}
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
|
||||
def expand_iri(value, active_context, doc_relative, vocab, local_context, defined) do
|
||||
# 2)
|
||||
if local_context && (local_def = local_context[value]) && defined[value] != true do
|
||||
{active_context, defined} =
|
||||
JSON.LD.Context.create_term_definition(
|
||||
active_context, local_context, value, local_def, defined)
|
||||
end
|
||||
|
||||
result = cond do
|
||||
# 3) If vocab is true and the active context has a term definition for value, return the associated IRI mapping.
|
||||
vocab && (term_def = active_context.term_defs[value]) ->
|
||||
term_def.iri_mapping
|
||||
# 4) If value contains a colon (:), it is either an absolute IRI, a compact IRI, or a blank node identifier
|
||||
String.contains?(value, ":") ->
|
||||
case compact_iri_parts(value) do
|
||||
[prefix, suffix] ->
|
||||
# 4.3)
|
||||
if local_context && (local_def = local_context[prefix]) && defined[prefix] != true do
|
||||
{active_context, defined} =
|
||||
JSON.LD.Context.create_term_definition(
|
||||
active_context, local_context, prefix, local_def, defined)
|
||||
end
|
||||
# 4.4)
|
||||
if prefix_def = active_context.term_defs[prefix] do
|
||||
prefix_def.iri_mapping <> suffix
|
||||
else
|
||||
value # 4.5)
|
||||
end
|
||||
nil -> value # 4.2)
|
||||
end
|
||||
# 5) If vocab is true, and active context has a vocabulary mapping, return the result of concatenating the vocabulary mapping with value.
|
||||
vocab && (vocabulary_mapping = active_context.vocab) ->
|
||||
vocabulary_mapping <> value
|
||||
# 6) Otherwise, if document relative is true, set value to the result of resolving value against the base IRI. Only the basic algorithm in section 5.2 of [RFC3986] is used; neither Syntax-Based Normalization nor Scheme-Based Normalization are performed. Characters additionally allowed in IRI references are treated in the same way that unreserved characters are treated in URI references, per section 6.5 of [RFC3987].
|
||||
doc_relative ->
|
||||
absolute_iri(value, active_context.base_iri)
|
||||
# TODO: RDF.rb's implementation differs from the spec here, by checking if base_iri is actually present in the previous clause and adding the following additional clause. Another Spec error?
|
||||
# if local_context && RDF::URI(value).relative?
|
||||
# # If local context is not null and value is not an absolute IRI, an invalid IRI mapping error has been detected and processing is aborted.
|
||||
# raise JSON.LD.InvalidIRIMappingError, message: "not an absolute IRI: #{value}"
|
||||
# 7) Return value as is.
|
||||
true -> value
|
||||
end
|
||||
|
||||
if local_context do
|
||||
{result, active_context, defined}
|
||||
else
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
end
|
470
lib/json_ld.ex
470
lib/json_ld.ex
|
@ -23,454 +23,26 @@ defmodule JSON.LD do
|
|||
def keyword?(value) when is_binary(value) and value in @keywords, do: true
|
||||
def keyword?(value), do: false
|
||||
|
||||
@doc """
|
||||
Expands the given input according to the steps in the JSON-LD Expansion Algorithm.
|
||||
|
||||
see http://json-ld.org/spec/latest/json-ld-api/#expansion-algorithm
|
||||
"""
|
||||
def expand(json_ld_object, opts \\ []) do
|
||||
case do_expand(JSON.LD.Context.new(opts), nil, json_ld_object, Keyword.delete(opts, :base)) do
|
||||
result = %{"@graph" => graph} when map_size(result) == 1 ->
|
||||
graph
|
||||
nil ->
|
||||
[]
|
||||
result when not is_list(result) ->
|
||||
[result]
|
||||
result -> result
|
||||
end
|
||||
end
|
||||
defdelegate expand(json_ld_object, opts \\ []),
|
||||
to: JSON.LD.Expansion
|
||||
|
||||
defp do_expand(active_context, active_property, element, opts \\ [])
|
||||
|
||||
# 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, opts)
|
||||
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, opts)
|
||||
when is_list(element) do
|
||||
term_def = active_context.term_defs[active_property]
|
||||
container_mapping = term_def && term_def.container_mapping
|
||||
element
|
||||
|> Enum.reduce([], fn (item, result) ->
|
||||
expanded_item = do_expand(active_context, active_property, item)
|
||||
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}"
|
||||
case expanded_item do
|
||||
nil -> result
|
||||
list when is_list(list) ->
|
||||
result ++ list
|
||||
expanded_item ->
|
||||
result ++ [expanded_item]
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
# 4) - 13)
|
||||
defp do_expand(active_context, active_property, element, opts)
|
||||
when is_map(element) do
|
||||
# 5)
|
||||
if Map.has_key?(element, "@context") do
|
||||
active_context = JSON.LD.Context.update(active_context, Map.get(element, "@context"))
|
||||
end
|
||||
# 6) and 7)
|
||||
result = element
|
||||
|> Enum.sort_by(fn {key, _} -> key end)
|
||||
|> Enum.reduce(%{}, fn ({key, value}, result) ->
|
||||
if (key != "@context") && # 7.1)
|
||||
(expanded_property = expand_iri(key, active_context, false, true)) && # 7.2)
|
||||
(String.contains?(expanded_property, ":") || keyword?(expanded_property)) do # 7.3)
|
||||
if keyword?(expanded_property) do # 7.4)
|
||||
if active_property == "@reverse", # 7.4.1)
|
||||
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."
|
||||
if Map.has_key?(result, expanded_property), # 7.4.2)
|
||||
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."
|
||||
|
||||
expanded_value = case expanded_property do
|
||||
"@id" -> # 7.4.3)
|
||||
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
|
||||
"@type" -> # 7.4.4)
|
||||
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
|
||||
"@graph" -> # 7.4.5)
|
||||
do_expand(active_context, "@graph", value, opts)
|
||||
"@value" -> # 7.4.6)
|
||||
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
|
||||
"@language" -> # 7.4.7)
|
||||
if is_binary(value),
|
||||
do: String.downcase(value),
|
||||
else: raise JSON.LD.InvalidLanguageTaggedStringError,
|
||||
message: "#{inspect value} is not a valid language-tag"
|
||||
"@index" -> # 7.4.8)
|
||||
if is_binary(value),
|
||||
do: value,
|
||||
else: raise JSON.LD.InvalidIndexValueError,
|
||||
message: "#{inspect value} is not a valid @index value"
|
||||
"@list" -> # 7.4.9)
|
||||
if active_property in [nil, "@graph"] do # 7.4.9.1)
|
||||
{:skip, result}
|
||||
else
|
||||
value = do_expand(active_context, active_property, value, opts)
|
||||
|
||||
# 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}"
|
||||
value
|
||||
end
|
||||
"@set" -> # 7.4.10)
|
||||
do_expand(active_context, active_property, value, opts)
|
||||
"@reverse" -> # 7.4.11)
|
||||
unless is_map(value),
|
||||
do: raise JSON.LD.InvalidReverseValueError,
|
||||
message: "#{inspect value} is not a valid @reverse value"
|
||||
expanded_value = do_expand(active_context, "@reverse", value, opts) # 7.4.11.1)
|
||||
new_result =
|
||||
if Map.has_key?(expanded_value, "@reverse") do # 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:
|
||||
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
|
||||
if Map.keys(expanded_value) != ["@reverse"] do # 7.4.11.3)
|
||||
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)
|
||||
Map.update(reverse_map, property, items, fn members ->
|
||||
members ++ items
|
||||
end)
|
||||
(_, reverse_map) -> reverse_map
|
||||
end
|
||||
new_result = Map.put(new_result, "@reverse", reverse_map)
|
||||
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 # expanded_property is not a keyword
|
||||
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
|
||||
# |> IO.inspect(label: "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)
|
||||
)
|
||||
# |> IO.inspect(label: "result")
|
||||
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, opts)
|
||||
Enum.map(index_value, fn item ->
|
||||
Map.put_new(item, "@index", index)
|
||||
end)
|
||||
)
|
||||
end)
|
||||
# 7.7)
|
||||
true ->
|
||||
do_expand(active_context, key, value, opts)
|
||||
end
|
||||
# 7.8)
|
||||
if is_nil(expanded_value) do
|
||||
result
|
||||
else
|
||||
# 7.9)
|
||||
if (term_def && term_def.container_mapping == "@list") &&
|
||||
!(is_map(expanded_value) && Map.has_key?(expanded_value, "@list")) do
|
||||
expanded_value = %{"@list" =>
|
||||
(if is_list(expanded_value),
|
||||
do: 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]
|
||||
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}"
|
||||
Map.update reverse_map, expanded_property, [item], fn members ->
|
||||
members ++ [item]
|
||||
end
|
||||
end)
|
||||
Map.put(result, "@reverse", reverse_map)
|
||||
else # 7.11)
|
||||
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
|
||||
end)
|
||||
|
||||
result = case result do
|
||||
# 8)
|
||||
%{"@value" => value} ->
|
||||
with keys = Map.keys(result) do # 8.1)
|
||||
if Enum.any?(keys, &(not &1 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
|
||||
end
|
||||
cond do
|
||||
value == nil -> nil # 8.2)
|
||||
!is_binary(value) and Map.has_key?(result, "@language") -> # 8.3)
|
||||
raise JSON.LD.InvalidLanguageTaggedValueError,
|
||||
message: "@value '#{inspect value}' is tagged with a language"
|
||||
(type = result["@type"]) && !RDF.uri?(type) -> # 8.4)
|
||||
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" => list} ->
|
||||
validate_set_or_list_object(result)
|
||||
result
|
||||
_ -> result
|
||||
end
|
||||
|
||||
# 11) If result contains only the key @language, set result to null.
|
||||
if is_map(result) and map_size(result) == 1 and Map.has_key?(result, "@language"),
|
||||
do: result = nil
|
||||
|
||||
# 12) If active property is null or @graph, drop free-floating values as follows:
|
||||
if 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: result = nil
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
defp validate_set_or_list_object(object) when map_size(object) == 1, do: true
|
||||
defp validate_set_or_list_object(object = %{"@index" => _})
|
||||
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
|
||||
defdelegate expand_iri(value, active_context, doc_relative \\ false,
|
||||
vocab \\ false, local_context \\ nil, defined \\ nil),
|
||||
to: JSON.LD.IRIExpansion
|
||||
|
||||
|
||||
@doc """
|
||||
see http://json-ld.org/spec/latest/json-ld-api/#value-expansion
|
||||
Generator function for `JSON.LD.Context`s.
|
||||
"""
|
||||
def expand_value(active_context, active_property, value) do
|
||||
with term_def when term_def != nil <- active_context.term_defs[active_property] do
|
||||
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
|
||||
not language_mapping in [nil, :undefined] ->
|
||||
%{"@value" => value, "@language" => language_mapping}
|
||||
language_mapping == :undefined && active_context.default_language ->
|
||||
%{"@value" => value, "@language" => active_context.default_language}
|
||||
true ->
|
||||
%{"@value" => value}
|
||||
end
|
||||
true ->
|
||||
%{"@value" => value}
|
||||
end
|
||||
else
|
||||
_ -> %{"@value" => value}
|
||||
end
|
||||
end
|
||||
def context(args, opts \\ [])
|
||||
|
||||
###########################################################################
|
||||
def context(%{"@context" => _} = object, opts),
|
||||
do: JSON.LD.Context.create(object, opts)
|
||||
|
||||
@doc """
|
||||
def context(context, opts),
|
||||
do: JSON.LD.Context.create(%{"@context" => context}, opts)
|
||||
|
||||
"""
|
||||
def flatten(json_ld_object, context \\ nil, opts \\ []) do
|
||||
json_ld_object
|
||||
end
|
||||
|
||||
###########################################################################
|
||||
|
||||
@doc """
|
||||
|
||||
"""
|
||||
def compact(json_ld_object, context, opts \\ []) do
|
||||
json_ld_object
|
||||
end
|
||||
|
||||
###########################################################################
|
||||
|
||||
@doc """
|
||||
see http://json-ld.org/spec/latest/json-ld-api/#iri-expansion
|
||||
"""
|
||||
def expand_iri(value, active_context, doc_relative \\ false, vocab \\ false,
|
||||
local_context \\ nil, defined \\ nil)
|
||||
|
||||
# 1) If value is a keyword or null, return value as is.
|
||||
def expand_iri(value, active_context, _, _, local_context, defined)
|
||||
when is_nil(value) or value in @keywords do
|
||||
if local_context || defined do
|
||||
{value, active_context, defined}
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
|
||||
def expand_iri(value, active_context, doc_relative, vocab, local_context, defined) do
|
||||
# 2)
|
||||
if local_context && (local_def = local_context[value]) && defined[value] != true do
|
||||
{active_context, defined} =
|
||||
JSON.LD.Context.create_term_definition(
|
||||
active_context, local_context, value, local_def, defined)
|
||||
end
|
||||
|
||||
result = cond do
|
||||
# 3) If vocab is true and the active context has a term definition for value, return the associated IRI mapping.
|
||||
vocab && (term_def = active_context.term_defs[value]) ->
|
||||
term_def.iri_mapping
|
||||
# 4) If value contains a colon (:), it is either an absolute IRI, a compact IRI, or a blank node identifier
|
||||
String.contains?(value, ":") ->
|
||||
case compact_iri_parts(value) do
|
||||
[prefix, suffix] ->
|
||||
# 4.3)
|
||||
if local_context && (local_def = local_context[prefix]) && defined[prefix] != true do
|
||||
{active_context, defined} =
|
||||
JSON.LD.Context.create_term_definition(
|
||||
active_context, local_context, prefix, local_def, defined)
|
||||
end
|
||||
# 4.4)
|
||||
if prefix_def = active_context.term_defs[prefix] do
|
||||
prefix_def.iri_mapping <> suffix
|
||||
else
|
||||
value # 4.5)
|
||||
end
|
||||
nil -> value # 4.2)
|
||||
end
|
||||
# 5) If vocab is true, and active context has a vocabulary mapping, return the result of concatenating the vocabulary mapping with value.
|
||||
vocab && (vocabulary_mapping = active_context.vocab) ->
|
||||
vocabulary_mapping <> value
|
||||
# 6) Otherwise, if document relative is true, set value to the result of resolving value against the base IRI. Only the basic algorithm in section 5.2 of [RFC3986] is used; neither Syntax-Based Normalization nor Scheme-Based Normalization are performed. Characters additionally allowed in IRI references are treated in the same way that unreserved characters are treated in URI references, per section 6.5 of [RFC3987].
|
||||
doc_relative ->
|
||||
absolute_iri(value, active_context.base_iri)
|
||||
# TODO: RDF.rb's implementation differs from the spec here, by checking if base_iri is actually present in the previous clause and adding the following additional clause. Another Spec error?
|
||||
# if local_context && RDF::URI(value).relative?
|
||||
# # If local context is not null and value is not an absolute IRI, an invalid IRI mapping error has been detected and processing is aborted.
|
||||
# raise JSON.LD.InvalidIRIMappingError, message: "not an absolute IRI: #{value}"
|
||||
# 7) Return value as is.
|
||||
true -> value
|
||||
end
|
||||
|
||||
if local_context do
|
||||
{result, active_context, defined}
|
||||
else
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
###########################################################################
|
||||
|
||||
|
@ -507,7 +79,7 @@ defmodule JSON.LD do
|
|||
An absolute IRI is defined in [RFC3987](http://www.ietf.org/rfc/rfc3987.txt)
|
||||
containing a scheme along with a path and optional query and fragment segments.
|
||||
|
||||
see https://www.w3.org/TR/json-ld-api/#dfn-absolute-iri
|
||||
see <https://www.w3.org/TR/json-ld-api/#dfn-absolute-iri>
|
||||
"""
|
||||
# TODO: This should be part of a dedicated URI/IRI implementation and properly tested.
|
||||
def absolute_iri?(value), do: RDF.uri?(value)
|
||||
|
@ -531,26 +103,14 @@ defmodule JSON.LD do
|
|||
|
||||
Blank node identifiers begin with `_:`
|
||||
|
||||
see https://www.w3.org/TR/json-ld-api/#dfn-blank-node-identifier
|
||||
see <https://www.w3.org/TR/json-ld-api/#dfn-blank-node-identifier>
|
||||
"""
|
||||
def blank_node_id?("_:" <> _), do: true
|
||||
def blank_node_id?(_), do: false
|
||||
|
||||
|
||||
defp scalar?(value) when is_binary(value) or is_number(value) or
|
||||
def scalar?(value) when is_binary(value) or is_number(value) or
|
||||
is_boolean(value), do: true
|
||||
defp scalar?(_), do: false
|
||||
|
||||
###########################################################################
|
||||
|
||||
|
||||
@doc """
|
||||
Generator function for `JSON.LD.Context`s.
|
||||
"""
|
||||
def context(args, opts \\ [])
|
||||
def context(%{"@context" => _} = object, opts),
|
||||
do: JSON.LD.Context.create(object, opts)
|
||||
def context(context, opts),
|
||||
do: JSON.LD.Context.create(%{"@context" => context}, opts)
|
||||
def scalar?(_), do: false
|
||||
|
||||
end
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
defmodule JSON.LD.ExpandTest do
|
||||
defmodule JSON.LD.ExpansionTest do
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
import JSON.LD.Expansion, only: [expand_value: 3]
|
||||
|
||||
test "Expanded form of a JSON-LD document (EXAMPLE 55 and 56 of https://www.w3.org/TR/json-ld/#expanded-document-form)" do
|
||||
input = Poison.Parser.parse! """
|
||||
{
|
||||
|
@ -917,7 +919,7 @@ defmodule JSON.LD.ExpandTest do
|
|||
@tag skip: "This seems to be RDF.rb specific. The @id keys are produced when value is an RDF::URI or RDF::Node. Do we need/want something similar?"
|
||||
@tag dt: dt
|
||||
test "expands datatype xsd:#{dt}", %{dt: dt, example_context: context} do
|
||||
assert JSON.LD.expand_value(context, "foo", apply(RDF.XSD, String.to_atom(dt), []) |> to_string) ==
|
||||
assert expand_value(context, "foo", apply(RDF.XSD, String.to_atom(dt), []) |> to_string) ==
|
||||
%{"@id" => "http://www.w3.org/2001/XMLSchema##{dt}"}
|
||||
end
|
||||
end)
|
||||
|
@ -950,7 +952,7 @@ defmodule JSON.LD.ExpandTest do
|
|||
|> Enum.each(fn ({title, data}) ->
|
||||
@tag data: data
|
||||
test title, %{data: [key, compacted, expanded], example_context: context} do
|
||||
assert JSON.LD.expand_value(context, key, compacted) == expanded
|
||||
assert expand_value(context, key, compacted) == expanded
|
||||
end
|
||||
end)
|
||||
|
||||
|
@ -970,7 +972,7 @@ defmodule JSON.LD.ExpandTest do
|
|||
@tag skip: "Why does this produce @language tags, although no term definition of foo exists? Is this also RDF.rb specific?"
|
||||
@tag data: data
|
||||
test "@language #{title}", %{data: [key, compacted, expanded], example_context: context} do
|
||||
assert JSON.LD.expand_value(context, key, compacted) == expanded
|
||||
assert expand_value(context, key, compacted) == expanded
|
||||
end
|
||||
end)
|
||||
|
||||
|
@ -991,7 +993,7 @@ defmodule JSON.LD.ExpandTest do
|
|||
|> Enum.each(fn ({title, data}) ->
|
||||
@tag data: data
|
||||
test "coercion #{title}", %{data: [key, compacted, expanded], example_context: context} do
|
||||
assert JSON.LD.expand_value(context, key, compacted) == expanded
|
||||
assert expand_value(context, key, compacted) == expanded
|
||||
end
|
||||
end)
|
||||
end
|
|
@ -1,4 +1,4 @@
|
|||
defmodule JSON.LD.ExpandIRITest do
|
||||
defmodule JSON.LD.IRIExpansionTest do
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
setup do
|
Loading…
Reference in a new issue