json_ld: implementation of auxiliary compaction algorithms

- Inverse Context Creation
- Term Selection
- IRI Compaction
- Value Compaction
This commit is contained in:
Marcel Otto 2017-02-26 23:48:37 +01:00
parent e5348bca3b
commit 14301b9525
4 changed files with 852 additions and 0 deletions

367
lib/json/ld/compaction.ex Normal file
View 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

View file

@ -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}) ->

View 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

View 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