json_ld: implementation of auxiliary compaction algorithms
- Inverse Context Creation - Term Selection - IRI Compaction - Value Compaction
This commit is contained in:
parent
e5348bca3b
commit
14301b9525
367
lib/json/ld/compaction.ex
Normal file
367
lib/json/ld/compaction.ex
Normal file
|
@ -0,0 +1,367 @@
|
|||
defmodule JSON.LD.Compaction do
|
||||
@moduledoc nil
|
||||
|
||||
alias JSON.LD.Context
|
||||
|
||||
@doc """
|
||||
Flattens the given input according to the steps in the JSON-LD Flattening Algorithm.
|
||||
|
||||
> Compaction is the process of applying a developer-supplied context to shorten
|
||||
> IRIs to terms or compact IRIs and JSON-LD values expressed in expanded form
|
||||
> to simple values such as strings or numbers. Often this makes it simpler to
|
||||
> work with document as the data is expressed in application-specific terms.
|
||||
> Compacted documents are also typically easier to read for humans.
|
||||
|
||||
-- <https://www.w3.org/TR/json-ld/#compacted-document-form>
|
||||
|
||||
Details at <https://www.w3.org/TR/json-ld-api/#compaction-algorithms>
|
||||
"""
|
||||
def compact(input, context, opts \\ []) do
|
||||
with active_context = JSON.LD.context(context),
|
||||
compact_arrays = Keyword.get(opts, :compact_arrays, true)
|
||||
do
|
||||
do_compact(JSON.LD.expand(input), active_context, Context.inverse(active_context),
|
||||
nil, compact_arrays)
|
||||
end
|
||||
end
|
||||
|
||||
defp do_compact(element, active_context, inverse_context, active_property,
|
||||
compact_arrays \\ true)
|
||||
|
||||
|
||||
# 1) If element is a scalar, it is already in its most compact form, so simply return element.
|
||||
defp do_compact(element, _, _, _, _)
|
||||
when is_binary(element) or is_number(element) or is_boolean(element),
|
||||
do: element
|
||||
|
||||
# 2) If element is an array
|
||||
defp do_compact(element, active_context, inverse_context, active_property, compact_arrays)
|
||||
when is_list(element) do
|
||||
result = Enum.reduce(element, [], fn (item, result) ->
|
||||
case do_compact(item, active_context, inverse_context, active_property, compact_arrays) do
|
||||
nil -> result
|
||||
compacted_item -> [compacted_item | result]
|
||||
end
|
||||
end) |> Enum.reverse
|
||||
|
||||
end
|
||||
|
||||
defp do_compact(element, active_context, inverse_context, active_property, compact_arrays) do
|
||||
|
||||
end
|
||||
|
||||
|
||||
@doc """
|
||||
IRI Compaction
|
||||
|
||||
Details at <https://www.w3.org/TR/json-ld-api/#iri-compaction>
|
||||
"""
|
||||
def compact_iri(iri, active_context, inverse_context,
|
||||
value \\ nil, vocab \\ false, reverse \\ false)
|
||||
|
||||
# 1) If iri is null, return null.
|
||||
def compact_iri(nil, _, _, _, _, _), do: nil
|
||||
|
||||
def compact_iri(iri, active_context, inverse_context, value, vocab, reverse) do
|
||||
# 2) If vocab is true and iri is a key in inverse context:
|
||||
term = if vocab && Map.has_key?(inverse_context, iri) do
|
||||
# 2.1) Initialize default language to active context's default language, if it has one, otherwise to @none.
|
||||
default_language = active_context.default_language || "@none"
|
||||
# 2.3) Initialize type/language to @language, and type/language value to @null. These two variables will keep track of the preferred type mapping or language mapping for a term, based on what is compatible with value.
|
||||
type_language = "@language"
|
||||
type_language_value = "@null"
|
||||
# 2.2) Initialize containers to an empty array. This array will be used to keep track of an ordered list of preferred container mappings for a term, based on what is compatible with value.
|
||||
# 2.4) If value is a JSON object that contains the key @index, then append the value @index to containers.
|
||||
containers = if is_map(value) and Map.has_key?(value, "@index"),
|
||||
do: ["@index"], else: []
|
||||
cond do
|
||||
# 2.5) If reverse is true, set type/language to @type, type/language value to @reverse, and append @set to containers.
|
||||
reverse ->
|
||||
containers = containers ++ ["@set"]
|
||||
type_language = "@type"
|
||||
type_language_value = "@reverse"
|
||||
# 2.6) Otherwise, if value is a list object, then set type/language and type/language value to the most specific values that work for all items in the list as follows:
|
||||
is_map(value) and Map.has_key?(value, "@list") ->
|
||||
# 2.6.1) If @index is a not key in value, then append @list to containers.
|
||||
if not Map.has_key?(value, "@index"),
|
||||
do: containers = containers ++ ["@list"]
|
||||
# 2.6.2) Initialize list to the array associated with the key @list in value.
|
||||
list = value["@list"]
|
||||
# 2.6.3) Initialize common type and common language to null. If list is empty, set common language to default language.
|
||||
{common_type, common_language} = {nil, nil}
|
||||
if Enum.empty?(list) do
|
||||
common_language = default_language
|
||||
else
|
||||
# 2.6.4) For each item in list:
|
||||
{common_type, common_language} = Enum.reduce_while list, {common_type, common_language},
|
||||
fn (item, {common_type, common_language}) ->
|
||||
# 2.6.4.1) Initialize item language to @none and item type to @none.
|
||||
{item_type, item_language} = {"@none", "@none"}
|
||||
# 2.6.4.2) If item contains the key @value:
|
||||
if Map.has_key?(item, "@value") do
|
||||
cond do
|
||||
# 2.6.4.2.1) If item contains the key @language, then set item language to its associated value.
|
||||
Map.has_key?(item, "@language") ->
|
||||
item_language = item["@language"]
|
||||
# 2.6.4.2.2) Otherwise, if item contains the key @type, set item type to its associated value.
|
||||
Map.has_key?(item, "@type") ->
|
||||
item_type = item["@type"]
|
||||
# 2.6.4.2.3) Otherwise, set item language to @null.
|
||||
true ->
|
||||
item_language = "@null"
|
||||
end
|
||||
# 2.6.4.3) Otherwise, set item type to @id.
|
||||
else
|
||||
item_type = "@id"
|
||||
end
|
||||
cond do
|
||||
# 2.6.4.4) If common language is null, set it to item language.
|
||||
is_nil(common_language) ->
|
||||
common_language = item_language
|
||||
# 2.6.4.5) Otherwise, if item language does not equal common language and item contains the key @value, then set common language to @none because list items have conflicting languages.
|
||||
item_language != common_language and Map.has_key?(item, "@value") ->
|
||||
common_language = "@none"
|
||||
true ->
|
||||
end
|
||||
cond do
|
||||
# 2.6.4.6) If common type is null, set it to item type.
|
||||
is_nil(common_type) ->
|
||||
common_type = item_type
|
||||
# 2.6.4.7) Otherwise, if item type does not equal common type, then set common type to @none because list items have conflicting types.
|
||||
item_type != common_type ->
|
||||
common_type = "@none"
|
||||
true ->
|
||||
end
|
||||
# 2.6.4.8) If common language is @none and common type is @none, then stop processing items in the list because it has been detected that there is no common language or type amongst the items.
|
||||
if common_language == "@none" and common_type == "@none" do
|
||||
{:halt, {common_type, common_language}}
|
||||
else
|
||||
{:cont, {common_type, common_language}}
|
||||
end
|
||||
end
|
||||
# 2.6.5) If common language is null, set it to @none.
|
||||
if is_nil(common_language), do: common_language = "@none"
|
||||
# 2.6.6) If common type is null, set it to @none.
|
||||
if is_nil(common_type), do: common_type = "@none"
|
||||
# 2.6.7) If common type is not @none then set type/language to @type and type/language value to common type.
|
||||
if common_type != "@none" do
|
||||
type_language = "@type"
|
||||
type_language_value = common_type
|
||||
# 2.6.8) Otherwise, set type/language value to common language.
|
||||
else
|
||||
type_language_value = common_language
|
||||
end
|
||||
end
|
||||
# 2.7) Otherwise
|
||||
true ->
|
||||
# 2.7.1) If value is a value object:
|
||||
if is_map(value) and Map.has_key?(value, "@value") do
|
||||
# 2.7.1.1) If value contains the key @language and does not contain the key @index, then set type/language value to its associated value and append @language to containers.
|
||||
if Map.has_key?(value, "@language") and not Map.has_key?(value, "@index") do
|
||||
type_language_value = value["@language"]
|
||||
containers = containers ++ ["@language"]
|
||||
else
|
||||
# 2.7.1.2) Otherwise, if value contains the key @type, then set type/language value to its associated value and set type/language to @type.
|
||||
if Map.has_key?(value, "@type") do
|
||||
type_language_value = value["@type"]
|
||||
type_language = "@type"
|
||||
end
|
||||
end
|
||||
# 2.7.2) Otherwise, set type/language to @type and set type/language value to @id.
|
||||
else
|
||||
type_language = "@type"
|
||||
type_language_value = "@id"
|
||||
end
|
||||
# 2.7.3) Append @set to containers.
|
||||
containers = containers ++ ["@set"]
|
||||
end
|
||||
# 2.8) Append @none to containers. This represents the non-existence of a container mapping, and it will be the last container mapping value to be checked as it is the most generic.
|
||||
containers = containers ++ ["@none"]
|
||||
# 2.9) If type/language value is null, set it to @null. This is the key under which null values are stored in the inverse context entry.
|
||||
if is_nil(type_language_value), do: type_language_value = "@null"
|
||||
# 2.10) Initialize preferred values to an empty array. This array will indicate, in order, the preferred values for a term's type mapping or language mapping.
|
||||
preferred_values = []
|
||||
# 2.11) If type/language value is @reverse, append @reverse to preferred values.
|
||||
if type_language_value == "@reverse",
|
||||
do: preferred_values = preferred_values ++ ["@reverse"]
|
||||
# 2.12) If type/language value is @id or @reverse and value has an @id member:
|
||||
if type_language_value in ~w[@id @reverse] and is_map(value) and Map.has_key?(value, "@id") do
|
||||
# 2.12.1) If the result of using the IRI compaction algorithm, passing active context, inverse context, the value associated with the @id key in value for iri, true for vocab, and true for document relative has a term definition in the active context with an IRI mapping that equals the value associated with the @id key in value, then append @vocab, @id, and @none, in that order, to preferred values.
|
||||
# TODO: Spec fixme? document_relative is not a specified parameter of compact_iri
|
||||
compact_id = compact_iri(value["@id"], active_context, inverse_context, nil, true)
|
||||
if (term_def = active_context.term_defs[compact_id]) && term_def.iri_mapping == value["@id"] do
|
||||
preferred_values = preferred_values ++ ~w[@vocab @id @none]
|
||||
# 2.12.2) Otherwise, append @id, @vocab, and @none, in that order, to preferred values.
|
||||
else
|
||||
preferred_values = preferred_values ++ ~w[@id @vocab @none]
|
||||
end
|
||||
# 2.13) Otherwise, append type/language value and @none, in that order, to preferred values.
|
||||
else
|
||||
preferred_values = preferred_values ++ [type_language_value, "@none"]
|
||||
end
|
||||
# 2.14) Initialize term to the result of the Term Selection algorithm, passing inverse context, iri, containers, type/language, and preferred values.
|
||||
select_term(inverse_context, iri, containers, type_language, preferred_values)
|
||||
end
|
||||
cond do
|
||||
# 2.15) If term is not null, return term.
|
||||
not is_nil(term) ->
|
||||
term
|
||||
# 3) At this point, there is no simple term that iri can be compacted to. If vocab is true and active context has a vocabulary mapping:
|
||||
# 3.1) If iri begins with the vocabulary mapping's value but is longer, then initialize suffix to the substring of iri that does not match. If suffix does not have a term definition in active context, then return suffix.
|
||||
vocab && active_context.vocab &&
|
||||
String.starts_with?(iri, active_context.vocab) &&
|
||||
(suffix = String.replace_prefix(iri, active_context.vocab, "")) != "" &&
|
||||
is_nil(active_context.term_defs[suffix]) ->
|
||||
suffix
|
||||
true ->
|
||||
# 4) The iri could not be compacted using the active context's vocabulary mapping. Try to create a compact IRI, starting by initializing compact IRI to null. This variable will be used to tore the created compact IRI, if any.
|
||||
compact_iri =
|
||||
# 5) For each key term and value term definition in the active context:
|
||||
Enum.reduce(active_context.term_defs, nil, fn ({term, term_def}, compact_iri) ->
|
||||
cond do
|
||||
# 5.1) If the term contains a colon (:), then continue to the next term because terms with colons can't be used as prefixes.
|
||||
String.contains?(term, ":") ->
|
||||
compact_iri
|
||||
# 5.2) If the term definition is null, its IRI mapping equals iri, or its IRI mapping is not a substring at the beginning of iri, the term cannot be used as a prefix because it is not a partial match with iri. Continue with the next term.
|
||||
is_nil(term_def) || term_def.iri_mapping == iri ||
|
||||
not String.starts_with?(iri, term_def.iri_mapping) ->
|
||||
compact_iri
|
||||
true ->
|
||||
# 5.3) Initialize candidate by concatenating term, a colon (:), and the substring of iri that follows after the value of the term definition's IRI mapping.
|
||||
candidate = term <> ":" <> (String.split_at(iri, String.length(term_def.iri_mapping)) |> elem(1))
|
||||
# 5.4) If either compact IRI is null or candidate is shorter or the same length but lexicographically less than compact IRI and candidate does not have a term definition in active context or if the term definition has an IRI mapping that equals iri and value is null, set compact IRI to candidate.
|
||||
# TODO: Spec fixme: The specified expression is pretty ambiguous without brackets ...
|
||||
# TODO: Spec fixme: "if the term definition has an IRI mapping that equals iri" is already catched in 5.2, so will never happen here ...
|
||||
if (is_nil(compact_iri) or shortest_or_least?(candidate, compact_iri)) and
|
||||
(is_nil(active_context.term_defs[candidate]) or
|
||||
(term_def.iri_mapping == iri and is_nil(value))) do
|
||||
candidate
|
||||
else
|
||||
compact_iri
|
||||
end
|
||||
end
|
||||
end)
|
||||
cond do
|
||||
# 6) If compact IRI is not null, return compact IRI.
|
||||
not is_nil(compact_iri) ->
|
||||
compact_iri
|
||||
# 7) If vocab is false then transform iri to a relative IRI using the document's base IRI.
|
||||
not vocab ->
|
||||
remove_base(iri, active_context.base_iri)
|
||||
# 8) Finally, return iri as is.
|
||||
true ->
|
||||
iri
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp shortest_or_least?(a, b) do
|
||||
(a_len = String.length(a)) < (b_len = String.length(b)) or
|
||||
(a_len == b_len and a < b)
|
||||
end
|
||||
|
||||
defp remove_base(iri, nil), do: iri
|
||||
|
||||
defp remove_base(iri, base) do
|
||||
base_len = String.length(base)
|
||||
if String.starts_with?(iri, base) and String.at(iri, base_len) in ~w(? #) do
|
||||
String.split_at(iri, base_len) |> elem(1)
|
||||
else
|
||||
case URI.parse(base) do
|
||||
%URI{path: nil} -> iri
|
||||
base ->
|
||||
do_remove_base(iri, %URI{base | path: Path.dirname(base.path)}, 0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp do_remove_base(iri, base, index) do
|
||||
base_str = URI.to_string(base)
|
||||
cond do
|
||||
String.starts_with?(iri, base_str) ->
|
||||
case String.duplicate("../", index) <>
|
||||
(String.split_at(iri, String.length(base_str)) |> elem(1)) do
|
||||
"" -> "./"
|
||||
rel -> rel
|
||||
end
|
||||
base.path == "/" -> iri
|
||||
true ->
|
||||
do_remove_base(iri, %URI{base | path: Path.dirname(base.path)}, index + 1)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@doc """
|
||||
Value Compaction
|
||||
|
||||
Details at <https://www.w3.org/TR/json-ld-api/#value-compaction>
|
||||
"""
|
||||
def compact_value(value, active_context, inverse_context, active_property) do
|
||||
term_def = active_context.term_defs[active_property]
|
||||
# 1) Initialize number members to the number of members value contains.
|
||||
number_members = Enum.count(value)
|
||||
# 2) If value has an @index member and the container mapping associated to active property is set to @index, decrease number members by 1.
|
||||
number_members =
|
||||
if Map.has_key?(value, "@index") and term_def.container_mapping == "@index",
|
||||
do: number_members - 1, else: number_members
|
||||
# 3) If number members is greater than 2, return value as it cannot be compacted.
|
||||
unless number_members > 2 do
|
||||
{type_mapping, language_mapping} = if term_def,
|
||||
do: {term_def.type_mapping, term_def.language_mapping},
|
||||
else: {nil, nil}
|
||||
cond do
|
||||
# 4) If value has an @id member
|
||||
id = Map.get(value, "@id") ->
|
||||
cond do
|
||||
# 4.1) If number members is 1 and the type mapping of active property is set to @id, return the result of using the IRI compaction algorithm, passing active context, inverse context, and the value of the @id member for iri.
|
||||
number_members == 1 and type_mapping == "@id" ->
|
||||
compact_iri(id, active_context, active_property)
|
||||
# 4.2) Otherwise, if number members is 1 and the type mapping of active property is set to @vocab, return the result of using the IRI compaction algorithm, passing active context, inverse context, the value of the @id member for iri, and true for vocab.
|
||||
number_members == 1 and type_mapping == "@vocab" ->
|
||||
compact_iri(id, active_context, active_property, nil, true)
|
||||
# 4.3) Otherwise, return value as is.
|
||||
true ->
|
||||
value
|
||||
end
|
||||
# 5) Otherwise, if value has an @type member whose value matches the type mapping of active property, return the value associated with the @value member of value.
|
||||
(type = Map.get(value, "@type")) && type == type_mapping ->
|
||||
value["@value"]
|
||||
# 6) Otherwise, if value has an @language member whose value matches the language mapping of active property, return the value associated with the @value member of value.
|
||||
(language = Map.get(value, "@language")) &&
|
||||
# TODO: Spec fixme: doesn't specify to check default language as well
|
||||
language in [language_mapping, active_context.default_language] ->
|
||||
value["@value"]
|
||||
true ->
|
||||
# 7) Otherwise, if number members equals 1 and either the value of the @value member is not a string, or the active context has no default language, or the language mapping of active property is set to null,, return the value associated with the @value member.
|
||||
if number_members == 1 and
|
||||
(not is_binary(value_value = value["@value"]) or
|
||||
!active_context.default_language or
|
||||
# TODO: Spec fixme: doesn't specify to check default language as well
|
||||
Context.language(active_context, active_property) == nil) do
|
||||
value_value
|
||||
# 8) Otherwise, return value as is.
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Term Selection
|
||||
|
||||
Details at <https://www.w3.org/TR/json-ld-api/#term-selection>
|
||||
"""
|
||||
def select_term(inverse_context, iri, containers, type_language, preferred_values) do
|
||||
container_map = inverse_context[iri]
|
||||
Enum.find_value containers, fn container ->
|
||||
if type_language_map = container_map[container] do
|
||||
value_map = type_language_map[type_language]
|
||||
Enum.find_value preferred_values, fn item -> value_map[item] end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
|
@ -99,6 +99,13 @@ defmodule JSON.LD.Context do
|
|||
do: raise JSON.LD.InvalidDefaultLanguageError,
|
||||
message: "#{inspect language} is not a valid language"
|
||||
|
||||
def language(active, term) do
|
||||
case Map.get(active.term_defs, term, %TermDefinition{}).language_mapping do
|
||||
false -> active.default_language
|
||||
language -> language
|
||||
end
|
||||
end
|
||||
|
||||
defp create_term_definitions(active, local, defined \\ %{}) do
|
||||
{active, _} =
|
||||
Enum.reduce local, {active, defined}, fn ({term, value}, {active, defined}) ->
|
||||
|
|
373
test/unit/iri_compaction_test.exs
Normal file
373
test/unit/iri_compaction_test.exs
Normal file
|
@ -0,0 +1,373 @@
|
|||
defmodule JSON.LD.IRICompactionTest do
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
import JSON.LD.Compaction, only: [compact_iri: 3, compact_iri: 4, compact_iri: 5]
|
||||
|
||||
setup do
|
||||
context = JSON.LD.context(%{
|
||||
"@base" => "http://base/",
|
||||
"xsd" => "http://www.w3.org/2001/XMLSchema#",
|
||||
"ex" => "http://example.org/",
|
||||
"" => "http://empty/", # TODO: "Invalid JSON-LD syntax; a term cannot be an empty string."
|
||||
"_" => "http://underscore/",
|
||||
"rex" => %{"@reverse" => "ex"},
|
||||
"lex" => %{"@id" => "ex", "@language" => "en"},
|
||||
"tex" => %{"@id" => "ex", "@type" => "xsd:string"},
|
||||
"exp" => %{"@id" => "ex:pert"},
|
||||
"experts" => %{"@id" => "ex:perts"}
|
||||
})
|
||||
%{example_context: context, inverse_context: JSON.LD.Context.inverse(context)}
|
||||
end
|
||||
|
||||
%{
|
||||
"nil" => [nil, nil],
|
||||
"absolute IRI" => ["http://example.com/", "http://example.com/"],
|
||||
"prefix:suffix" => ["ex:suffix", "http://example.org/suffix"],
|
||||
"keyword" => ["@type", "@type"],
|
||||
"empty" => [":suffix", "http://empty/suffix"],
|
||||
"unmapped" => ["foo", "foo"],
|
||||
"bnode" => ["_:a", "_:a"],
|
||||
"relative" => ["foo/bar", "http://base/foo/bar"],
|
||||
"odd CURIE" => ["exp:s", "http://example.org/perts"]
|
||||
}
|
||||
|> Enum.each(fn {title, data} ->
|
||||
@tag data: data
|
||||
test title, %{data: [result, input], example_context: context,
|
||||
inverse_context: inverse_context} do
|
||||
assert compact_iri(input, context, inverse_context) == result
|
||||
end
|
||||
end)
|
||||
|
||||
describe "with :vocab option" do
|
||||
%{
|
||||
"absolute IRI" => ["http://example.com/", "http://example.com/"],
|
||||
"prefix:suffix" => ["ex:suffix", "http://example.org/suffix"],
|
||||
"keyword" => ["@type", "@type"],
|
||||
"empty" => [":suffix", "http://empty/suffix"],
|
||||
"unmapped" => ["foo", "foo"],
|
||||
"bnode" => ["_:a", "_:a"],
|
||||
"relative" => ["http://base/foo/bar", "http://base/foo/bar"],
|
||||
"odd CURIE" => ["experts", "http://example.org/perts"]
|
||||
}
|
||||
|> Enum.each(fn {title, data} ->
|
||||
@tag data: data
|
||||
test title, %{data: [result, input], example_context: context,
|
||||
inverse_context: inverse_context} do
|
||||
assert compact_iri(input, context, inverse_context, nil, true) == result
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
describe "with @vocab" do
|
||||
setup %{example_context: context} do
|
||||
context = %JSON.LD.Context{context | vocab: "http://example.org/"}
|
||||
%{example_context: context, inverse_context: JSON.LD.Context.inverse(context)}
|
||||
end
|
||||
|
||||
%{
|
||||
"absolute IRI" => ["http://example.com/", "http://example.com/"],
|
||||
"prefix:suffix" => ["suffix", "http://example.org/suffix"],
|
||||
"keyword" => ["@type", "@type"],
|
||||
"empty" => [":suffix", "http://empty/suffix"],
|
||||
"unmapped" => ["foo", "foo"],
|
||||
"bnode" => ["_:a", "_:a"],
|
||||
"relative" => ["http://base/foo/bar", "http://base/foo/bar"],
|
||||
"odd CURIE" => ["experts", "http://example.org/perts"]
|
||||
}
|
||||
|> Enum.each(fn {title, data} ->
|
||||
@tag data: data
|
||||
test title, %{data: [result, input], example_context: context,
|
||||
inverse_context: inverse_context} do
|
||||
assert compact_iri(input, context, inverse_context, nil, true) == result
|
||||
end
|
||||
end)
|
||||
|
||||
# TODO: we don't support 'position: :predicate'"
|
||||
# test "does not use @vocab if it would collide with a term" do
|
||||
# subject.set_mapping("name", "http://xmlns.com/foaf/0.1/name")
|
||||
# subject.set_mapping("ex", nil)
|
||||
# expect(subject.compact_iri("http://example.org/name", position: :predicate)).
|
||||
# to produce("lex:name", logger)
|
||||
# end
|
||||
end
|
||||
|
||||
describe "with value" do
|
||||
setup do
|
||||
context = JSON.LD.context(%{
|
||||
"xsd" => RDF.XSD.__base_uri__,
|
||||
"plain" => "http://example.com/plain",
|
||||
"lang" => %{"@id" => "http://example.com/lang", "@language" => "en"},
|
||||
"bool" => %{"@id" => "http://example.com/bool", "@type" => "xsd:boolean"},
|
||||
"integer" => %{"@id" => "http://example.com/integer", "@type" => "xsd:integer"},
|
||||
"double" => %{"@id" => "http://example.com/double", "@type" => "xsd:double"},
|
||||
"date" => %{"@id" => "http://example.com/date", "@type" => "xsd:date"},
|
||||
"id" => %{"@id" => "http://example.com/id", "@type" => "@id"},
|
||||
"listplain" => %{"@id" => "http://example.com/plain", "@container" => "@list"},
|
||||
"listlang" => %{"@id" => "http://example.com/lang", "@language" => "en", "@container" => "@list"},
|
||||
"listbool" => %{"@id" => "http://example.com/bool", "@type" => "xsd:boolean", "@container" => "@list"},
|
||||
"listinteger" => %{"@id" => "http://example.com/integer", "@type" => "xsd:integer", "@container" => "@list"},
|
||||
"listdouble" => %{"@id" => "http://example.com/double", "@type" => "xsd:double", "@container" => "@list"},
|
||||
"listdate" => %{"@id" => "http://example.com/date", "@type" => "xsd:date", "@container" => "@list"},
|
||||
"listid" => %{"@id" => "http://example.com/id", "@type" => "@id", "@container" => "@list"},
|
||||
"setplain" => %{"@id" => "http://example.com/plain", "@container" => "@set"},
|
||||
"setlang" => %{"@id" => "http://example.com/lang", "@language" => "en", "@container" => "@set"},
|
||||
"setbool" => %{"@id" => "http://example.com/bool", "@type" => "xsd:boolean", "@container" => "@set"},
|
||||
"setinteger" => %{"@id" => "http://example.com/integer", "@type" => "xsd:integer", "@container" => "@set"},
|
||||
"setdouble" => %{"@id" => "http://example.com/double", "@type" => "xsd:double", "@container" => "@set"},
|
||||
"setdate" => %{"@id" => "http://example.com/date", "@type" => "xsd:date", "@container" => "@set"},
|
||||
"setid" => %{"@id" => "http://example.com/id", "@type" => "@id", "@container" => "@set"},
|
||||
"langmap" => %{"@id" => "http://example.com/langmap", "@container" => "@language"},
|
||||
})
|
||||
%{example_context: context, inverse_context: JSON.LD.Context.inverse(context)}
|
||||
end
|
||||
|
||||
%{
|
||||
"langmap" => %{"@value" => "en", "@language" => "en"},
|
||||
#"plain" => %{"@value" => "foo"},
|
||||
"setplain" => %{"@value" => "foo", "@language" => "pl"}
|
||||
}
|
||||
|> Enum.each(fn {prop, value} ->
|
||||
@tag data: {prop, value}
|
||||
test "uses #{prop} for #{inspect value}",
|
||||
%{data: {prop, value}, example_context: context,
|
||||
inverse_context: inverse_context} do
|
||||
assert compact_iri("http://example.com/#{String.replace(prop, "set", "")}",
|
||||
context, inverse_context, value, true) == prop
|
||||
end
|
||||
end)
|
||||
|
||||
%{
|
||||
"listplain" => [
|
||||
[%{"@value" => "foo"}],
|
||||
[%{"@value" => "foo"}, %{"@value" => "bar"}, %{"@value" => "baz"}],
|
||||
[%{"@value" => "foo"}, %{"@value" => "bar"}, %{"@value" => 1}],
|
||||
[%{"@value" => "foo"}, %{"@value" => "bar"}, %{"@value" => 1.1}],
|
||||
[%{"@value" => "foo"}, %{"@value" => "bar"}, %{"@value" => true}],
|
||||
[%{"@value" => "de", "@language" => "de"}, %{"@value" => "jp", "@language" => "jp"}],
|
||||
[%{"@value" => true}],
|
||||
[%{"@value" => false}],
|
||||
[%{"@value" => 1}], [%{"@value" => 1.1}],
|
||||
],
|
||||
"listlang" => [[%{"@value" => "en", "@language" => "en"}]],
|
||||
"listbool" => [[%{"@value" => "true", "@type" => to_string(RDF.XSD.boolean)}]],
|
||||
"listinteger" => [[%{"@value" => "1", "@type" => to_string(RDF.XSD.integer)}]],
|
||||
"listdouble" => [[%{"@value" => "1", "@type" => to_string(RDF.XSD.double)}]],
|
||||
"listdate" => [[%{"@value" => "2012-04-17", "@type" => to_string(RDF.XSD.date)}]],
|
||||
}
|
||||
|> Enum.each(fn {prop, values} ->
|
||||
Enum.each values, fn value ->
|
||||
@tag data: {prop, value}
|
||||
test "for @list uses #{prop} for #{inspect %{"@list" => value}}",
|
||||
%{data: {prop, value}, example_context: context,
|
||||
inverse_context: inverse_context} do
|
||||
assert compact_iri("http://example.com/#{String.replace(prop, "list", "")}",
|
||||
context, inverse_context, %{"@list" => value}, true) == prop
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
# describe "with :simple_compact_iris" do
|
||||
# before(:each) { subject.instance_variable_get(:@options)[:simple_compact_iris] = true}
|
||||
#
|
||||
# %{
|
||||
# "nil" => [nil, nil],
|
||||
# "absolute IRI" => ["http://example.com/", "http://example.com/"],
|
||||
# "prefix:suffix" => ["ex:suffix", "http://example.org/suffix"],
|
||||
# "keyword" => ["@type", "@type"],
|
||||
# "empty" => [":suffix", "http://empty/suffix"],
|
||||
# "unmapped" => ["foo", "foo"],
|
||||
# "bnode" => ["_:a", RDF::Node("a")],
|
||||
# "relative" => ["foo/bar", "http://base/foo/bar"],
|
||||
# "odd CURIE" => ["ex:perts", "http://example.org/perts"]
|
||||
# }.each do |title, (result, input)|
|
||||
# test title do
|
||||
# expect(subject.compact_iri(input)).to produce(result, logger)
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# describe "and @vocab" do
|
||||
# before(:each) { subject.vocab = "http://example.org/"}
|
||||
#
|
||||
# %{
|
||||
# "absolute IRI" => ["http://example.com/", "http://example.com/"],
|
||||
# "prefix:suffix" => ["suffix", "http://example.org/suffix"],
|
||||
# "keyword" => ["@type", "@type"],
|
||||
# "empty" => [":suffix", "http://empty/suffix"],
|
||||
# "unmapped" => ["foo", "foo"],
|
||||
# "bnode" => ["_:a", RDF::Node("a")],
|
||||
# "relative" => ["http://base/foo/bar", "http://base/foo/bar"],
|
||||
# "odd CURIE" => ["experts", "http://example.org/perts"]
|
||||
# }.each do |title, (result, input)|
|
||||
# test title do
|
||||
# expect(subject.compact_iri(input, vocab: true)).to produce(result, logger)
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
|
||||
describe "compact-0018" do
|
||||
setup do
|
||||
context = JSON.LD.context(Poison.Parser.parse! """
|
||||
{
|
||||
"id1": "http://example.com/id1",
|
||||
"type1": "http://example.com/t1",
|
||||
"type2": "http://example.com/t2",
|
||||
"@language": "de",
|
||||
"term": {
|
||||
"@id": "http://example.com/term"
|
||||
},
|
||||
"term1": {
|
||||
"@id": "http://example.com/term",
|
||||
"@container": "@list"
|
||||
},
|
||||
"term2": {
|
||||
"@id": "http://example.com/term",
|
||||
"@container": "@list",
|
||||
"@language": "en"
|
||||
},
|
||||
"term3": {
|
||||
"@id": "http://example.com/term",
|
||||
"@container": "@list",
|
||||
"@language": null
|
||||
},
|
||||
"term4": {
|
||||
"@id": "http://example.com/term",
|
||||
"@container": "@list",
|
||||
"@type": "type1"
|
||||
},
|
||||
"term5": {
|
||||
"@id": "http://example.com/term",
|
||||
"@container": "@list",
|
||||
"@type": "type2"
|
||||
}
|
||||
}
|
||||
""")
|
||||
%{example_context: context, inverse_context: JSON.LD.Context.inverse(context)}
|
||||
end
|
||||
|
||||
|
||||
%{
|
||||
"term" => [
|
||||
'{ "@value": "v0.1", "@language": "de" }',
|
||||
'{ "@value": "v0.2", "@language": "en" }',
|
||||
'{ "@value": "v0.3"}',
|
||||
'{ "@value": 4}',
|
||||
'{ "@value": true}',
|
||||
'{ "@value": false}'
|
||||
],
|
||||
"term1" => """
|
||||
{
|
||||
"@list": [
|
||||
{ "@value": "v1.1", "@language": "de" },
|
||||
{ "@value": "v1.2", "@language": "en" },
|
||||
{ "@value": "v1.3"},
|
||||
{ "@value": 14},
|
||||
{ "@value": true},
|
||||
{ "@value": false}
|
||||
]
|
||||
}
|
||||
""",
|
||||
"term2" => """
|
||||
{
|
||||
"@list": [
|
||||
{ "@value": "v2.1", "@language": "en" },
|
||||
{ "@value": "v2.2", "@language": "en" },
|
||||
{ "@value": "v2.3", "@language": "en" },
|
||||
{ "@value": "v2.4", "@language": "en" },
|
||||
{ "@value": "v2.5", "@language": "en" },
|
||||
{ "@value": "v2.6", "@language": "en" }
|
||||
]
|
||||
}
|
||||
""",
|
||||
"term3" => """
|
||||
{
|
||||
"@list": [
|
||||
{ "@value": "v3.1"},
|
||||
{ "@value": "v3.2"},
|
||||
{ "@value": "v3.3"},
|
||||
{ "@value": "v3.4"},
|
||||
{ "@value": "v3.5"},
|
||||
{ "@value": "v3.6"}
|
||||
]
|
||||
}
|
||||
""",
|
||||
"term4" => """
|
||||
{
|
||||
"@list": [
|
||||
{ "@value": "v4.1", "@type": "http://example.com/t1" },
|
||||
{ "@value": "v4.2", "@type": "http://example.com/t1" },
|
||||
{ "@value": "v4.3", "@type": "http://example.com/t1" },
|
||||
{ "@value": "v4.4", "@type": "http://example.com/t1" },
|
||||
{ "@value": "v4.5", "@type": "http://example.com/t1" },
|
||||
{ "@value": "v4.6", "@type": "http://example.com/t1" }
|
||||
]
|
||||
}
|
||||
""",
|
||||
"term5" => """
|
||||
{
|
||||
"@list": [
|
||||
{ "@value": "v5.1", "@type": "http://example.com/t2" },
|
||||
{ "@value": "v5.2", "@type": "http://example.com/t2" },
|
||||
{ "@value": "v5.3", "@type": "http://example.com/t2" },
|
||||
{ "@value": "v5.4", "@type": "http://example.com/t2" },
|
||||
{ "@value": "v5.5", "@type": "http://example.com/t2" },
|
||||
{ "@value": "v5.6", "@type": "http://example.com/t2" }
|
||||
]
|
||||
}
|
||||
""",
|
||||
}
|
||||
|> Enum.each(fn {term, values} ->
|
||||
values = if is_binary(values),
|
||||
do: [values],
|
||||
else: values
|
||||
Enum.each(values, fn value ->
|
||||
value = Poison.Parser.parse!(value)
|
||||
@tag data: {term, value}
|
||||
test "uses #{term} for #{inspect value, limit: 3}",
|
||||
%{data: {term, value}, example_context: context,
|
||||
inverse_context: inverse_context} do
|
||||
assert compact_iri("http://example.com/term", context, inverse_context,
|
||||
value, true) == term
|
||||
end
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
describe "compact-0020" do
|
||||
setup do
|
||||
context = JSON.LD.context(%{
|
||||
"ex" => "http://example.org/ns#",
|
||||
"ex:property" => %{"@container" => "@list"}
|
||||
})
|
||||
%{example_context: context, inverse_context: JSON.LD.Context.inverse(context)}
|
||||
end
|
||||
|
||||
@tag skip: "TODO: we don't support 'position: :subject'"
|
||||
test "Compact @id that is a property IRI when @container is @list", %{
|
||||
example_context: context, inverse_context: inverse_context} do
|
||||
assert compact_iri("http://example.org/ns#property", context, inverse_context) == "ex:property"
|
||||
# expect(ctx.compact_iri("http://example.org/ns#property", position: :subject)).
|
||||
# to produce("ex:property", logger)
|
||||
end
|
||||
end
|
||||
|
||||
describe "compact-0041" do
|
||||
setup do
|
||||
context = JSON.LD.context(%{
|
||||
"name" => %{"@id" => "http://example.com/property", "@container" => "@list"}
|
||||
})
|
||||
%{example_context: context, inverse_context: JSON.LD.Context.inverse(context)}
|
||||
end
|
||||
|
||||
test "Does not use @list with @index", %{
|
||||
example_context: context, inverse_context: inverse_context} do
|
||||
assert compact_iri("http://example.com/property", context, inverse_context,
|
||||
%{
|
||||
"@list" => ["one item"],
|
||||
"@index" => "an annotation"
|
||||
}) == "http://example.com/property"
|
||||
end
|
||||
end
|
||||
|
||||
end
|
105
test/unit/value_compaction_test.exs
Normal file
105
test/unit/value_compaction_test.exs
Normal file
|
@ -0,0 +1,105 @@
|
|||
defmodule JSON.LD.ValueCompactionTest do
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
import JSON.LD.Compaction, only: [compact_value: 4]
|
||||
|
||||
setup do
|
||||
context = JSON.LD.context(%{
|
||||
"dc" => "http://purl.org/dc/terms/", # TODO: RDF::Vocab::DC.to_uri.to_s,
|
||||
"ex" => "http://example.org/",
|
||||
"foaf" => "http://xmlns.com/foaf/0.1/", # TODO: RDF::Vocab::FOAF.to_uri.to_s,
|
||||
"xsd" => to_string(RDF.XSD.__base_uri__),
|
||||
"langmap" => %{"@id" => "http://example.com/langmap", "@container" => "@language"},
|
||||
"list" => %{"@id" => "http://example.org/list", "@container" => "@list"},
|
||||
"nolang" => %{"@id" => "http://example.org/nolang", "@language" => nil},
|
||||
"dc:created" => %{"@type" => to_string(RDF.XSD.date)},
|
||||
"foaf:age" => %{"@type" => to_string(RDF.XSD.integer)},
|
||||
"foaf:knows" => %{"@type" => "@id"},
|
||||
})
|
||||
%{example_context: context, inverse_context: JSON.LD.Context.inverse(context)}
|
||||
end
|
||||
|
||||
%{
|
||||
"absolute IRI" => ["foaf:knows", "http://example.com/", %{"@id" => "http://example.com/"}],
|
||||
"prefix:suffix" => ["foaf:knows", "ex:suffix", %{"@id" => "http://example.org/suffix"}],
|
||||
"integer" => ["foaf:age", "54", %{"@value" => "54", "@type" => to_string(RDF.XSD.integer)}],
|
||||
"date " => ["dc:created", "2011-12-27Z", %{"@value" => "2011-12-27Z", "@type" => to_string(RDF.XSD.date)}],
|
||||
"no IRI" => ["foo", %{"@id" => "http://example.com/"}, %{"@id" => "http://example.com/"}],
|
||||
"no IRI (CURIE)" => ["foo", %{"@id" => "http://xmlns.com/foaf/0.1/Person"}, %{"@id" => "http://xmlns.com/foaf/0.1/Person"}],
|
||||
"no boolean" => ["foo", %{"@value" => "true", "@type" => to_string(RDF.XSD.boolean)},%{"@value" => "true", "@type" => to_string(RDF.XSD.boolean)}],
|
||||
"no integer" => ["foo", %{"@value" => "54", "@type" => to_string(RDF.XSD.integer)},%{"@value" => "54", "@type" => to_string(RDF.XSD.integer)}],
|
||||
"no date " => ["foo", %{"@value" => "2011-12-27Z", "@type" => to_string(RDF.XSD.date)}, %{"@value" => "2011-12-27Z", "@type" => to_string(RDF.XSD.date)}],
|
||||
"no string " => ["foo", "string", %{"@value" => "string"}],
|
||||
"no lang " => ["nolang", "string", %{"@value" => "string"}],
|
||||
"native boolean" => ["foo", true, %{"@value" => true}],
|
||||
"native integer" => ["foo", 1, %{"@value" => 1}],
|
||||
"native integer(list)"=>["list", 1, %{"@value" => 1}],
|
||||
"native double" => ["foo", 1.1e1, %{"@value" => 1.1E1}],
|
||||
}
|
||||
|> Enum.each(fn ({title, data}) ->
|
||||
@tag data: data
|
||||
test title, %{data: [key, compacted, expanded], example_context: context,
|
||||
inverse_context: inverse_context} do
|
||||
assert compact_value(expanded, context, inverse_context, key) == compacted
|
||||
end
|
||||
end)
|
||||
|
||||
describe "@language" do
|
||||
setup %{example_context: context} do
|
||||
context = %JSON.LD.Context{context | default_language: "en"}
|
||||
%{example_context: context, inverse_context: JSON.LD.Context.inverse(context)}
|
||||
end
|
||||
|
||||
%{
|
||||
"@id" => ["foo", %{"@id" => "foo"}, %{"@id" => "foo"}],
|
||||
"integer" => ["foo", %{"@value" => "54", "@type" => to_string(RDF.XSD.integer)}, %{"@value" => "54", "@type" => to_string(RDF.XSD.integer)}],
|
||||
"date" => ["foo", %{"@value" => "2011-12-27Z","@type" => to_string(RDF.XSD.date)},%{"@value" => "2011-12-27Z", "@type" => to_string(RDF.XSD.date)}],
|
||||
"no lang" => ["foo", %{"@value" => "foo" }, %{"@value" => "foo"}],
|
||||
"same lang" => ["foo", "foo", %{"@value" => "foo", "@language" => "en"}],
|
||||
"other lang" => ["foo", %{"@value" => "foo", "@language" => "bar"}, %{"@value" => "foo", "@language" => "bar"}],
|
||||
"langmap" => ["langmap", "en", %{"@value" => "en", "@language" => "en"}],
|
||||
"no lang with @type coercion" => ["dc:created", %{"@value" => "foo"}, %{"@value" => "foo"}],
|
||||
"no lang with @id coercion" => ["foaf:knows", %{"@value" => "foo"}, %{"@value" => "foo"}],
|
||||
"no lang with @language=null" => ["nolang", "string", %{"@value" => "string"}],
|
||||
"same lang with @type coercion" => ["dc:created", %{"@value" => "foo"}, %{"@value" => "foo"}],
|
||||
"same lang with @id coercion" => ["foaf:knows", %{"@value" => "foo"}, %{"@value" => "foo"}],
|
||||
"other lang with @type coercion" => ["dc:created", %{"@value" => "foo", "@language" => "bar"}, %{"@value" => "foo", "@language" => "bar"}],
|
||||
"other lang with @id coercion" => ["foaf:knows", %{"@value" => "foo", "@language" => "bar"}, %{"@value" => "foo", "@language" => "bar"}],
|
||||
"native boolean" => ["foo", true, %{"@value" => true}],
|
||||
"native integer" => ["foo", 1, %{"@value" => 1}],
|
||||
"native integer(list)" => ["list", 1, %{"@value" => 1}],
|
||||
"native double" => ["foo", 1.1e1, %{"@value" => 1.1E1}],
|
||||
}
|
||||
|> Enum.each(fn ({title, data}) ->
|
||||
@tag data: data
|
||||
test title, %{data: [key, compacted, expanded], example_context: context,
|
||||
inverse_context: inverse_context} do
|
||||
assert compact_value(expanded, context, inverse_context, key) == compacted
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
# TODO
|
||||
# describe "keywords" do
|
||||
# before(:each) do
|
||||
# subject.set_mapping("id", "@id")
|
||||
# subject.set_mapping("type", "@type")
|
||||
# subject.set_mapping("list", "@list")
|
||||
# subject.set_mapping("set", "@set")
|
||||
# subject.set_mapping("language", "@language")
|
||||
# subject.set_mapping("literal", "@value")
|
||||
# end
|
||||
#
|
||||
# %{
|
||||
# "@id" => [%{"id" => "http://example.com/"}, %{"@id" => "http://example.com/"}],
|
||||
# "@type" => [%{"literal" => "foo", "type" => "http://example.com/"},
|
||||
# %{"@value" => "foo", "@type" => "http://example.com/"}],
|
||||
# "@value" => [%{"literal" => "foo", "language" => "bar"}, %{"@value" => "foo", "@language" => "bar"}],
|
||||
# }.each do |title, (compacted, expanded)|
|
||||
# test title do
|
||||
# expect(subject.compact_value("foo", expanded)).to produce(compacted, logger)
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
|
||||
end
|
Loading…
Reference in a new issue