json_ld: JSON-LD to RDF deserialization
This commit is contained in:
parent
f208f2e22e
commit
7d80675fa2
6 changed files with 857 additions and 12 deletions
|
@ -7,16 +7,9 @@ defmodule JSON.LD.Flattening do
|
|||
|
||||
def flatten(input, context \\ nil, options \\ %JSON.LD.Options{}) do
|
||||
with options = JSON.LD.Options.new(options),
|
||||
expanded = JSON.LD.expand(input, options)
|
||||
expanded = JSON.LD.expand(input, options),
|
||||
node_map = node_map(expanded)
|
||||
do
|
||||
{:ok, node_id_map} = NodeIdentifierMap.start_link
|
||||
node_map =
|
||||
try do
|
||||
generate_node_map(expanded, %{"@default" => %{}}, node_id_map)
|
||||
after
|
||||
NodeIdentifierMap.stop(node_id_map)
|
||||
end
|
||||
|
||||
default_graph =
|
||||
Enum.reduce node_map, node_map["@default"], fn
|
||||
({"@default", _}, default_graph) -> default_graph
|
||||
|
@ -63,6 +56,20 @@ defmodule JSON.LD.Flattening do
|
|||
end
|
||||
end
|
||||
|
||||
def node_map(input, node_id_map \\ nil)
|
||||
|
||||
def node_map(input, nil) do
|
||||
{:ok, node_id_map} = NodeIdentifierMap.start_link
|
||||
try do
|
||||
node_map(input, node_id_map)
|
||||
after
|
||||
NodeIdentifierMap.stop(node_id_map)
|
||||
end
|
||||
end
|
||||
|
||||
def node_map(input, node_id_map) do
|
||||
generate_node_map(input, %{"@default" => %{}}, node_id_map)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Node Map Generation
|
||||
|
|
|
@ -9,6 +9,7 @@ defmodule JSON.LD.Options do
|
|||
compact_arrays: true,
|
||||
document_loader: nil,
|
||||
expand_context: nil,
|
||||
produce_generalized_rdf: false,
|
||||
processing_mode: "json-ld-1.0"
|
||||
|
||||
def new(), do: %JSON.LD.Options{}
|
||||
|
|
154
lib/json/ld/reader.ex
Normal file
154
lib/json/ld/reader.ex
Normal file
|
@ -0,0 +1,154 @@
|
|||
defmodule JSON.LD.Reader do
|
||||
@moduledoc """
|
||||
"""
|
||||
|
||||
use RDF.Reader
|
||||
|
||||
import JSON.LD.{NodeIdentifierMap, Utils}
|
||||
alias JSON.LD.NodeIdentifierMap
|
||||
alias RDF.{Dataset, Graph}
|
||||
alias RDF.NS.{XSD}
|
||||
|
||||
|
||||
def read_string(content, opts \\ []) do
|
||||
with {:ok, json_ld_object} <- parse_json(content),
|
||||
dataset = to_rdf(json_ld_object, opts) do
|
||||
{:ok, dataset}
|
||||
end
|
||||
end
|
||||
|
||||
def to_rdf(element, options \\ %JSON.LD.Options{}) do
|
||||
with options = JSON.LD.Options.new(options) do
|
||||
{:ok, node_id_map} = NodeIdentifierMap.start_link
|
||||
try do
|
||||
element
|
||||
|> JSON.LD.expand(options)
|
||||
|> JSON.LD.node_map(node_id_map)
|
||||
|> Enum.sort_by(fn {graph_name, _} -> graph_name end)
|
||||
|> Enum.reduce(Dataset.new, fn ({graph_name, graph}, dataset) ->
|
||||
unless relative_iri?(graph_name) do
|
||||
rdf_graph =
|
||||
graph
|
||||
|> Enum.sort_by(fn {subject, _} -> subject end)
|
||||
|> Enum.reduce(Graph.new, fn ({subject, node}, rdf_graph) ->
|
||||
unless relative_iri?(subject) do
|
||||
node
|
||||
|> Enum.sort_by(fn {property, _} -> property end)
|
||||
|> Enum.reduce(rdf_graph, fn ({property, values}, rdf_graph) ->
|
||||
cond do
|
||||
property == "@type" ->
|
||||
Graph.add rdf_graph,
|
||||
node_to_rdf(subject), RDF.NS.RDF.type,
|
||||
Enum.map(values, &node_to_rdf/1)
|
||||
JSON.LD.keyword?(property) ->
|
||||
rdf_graph
|
||||
not options.produce_generalized_rdf and
|
||||
blank_node_id?(property) ->
|
||||
rdf_graph
|
||||
relative_iri?(property) ->
|
||||
rdf_graph
|
||||
true ->
|
||||
Enum.reduce values, rdf_graph, fn
|
||||
(%{"@list" => list}, rdf_graph) ->
|
||||
with {list_triples, first} <-
|
||||
list_to_rdf(list, node_id_map) do
|
||||
rdf_graph
|
||||
|> Graph.add({node_to_rdf(subject), node_to_rdf(property), first})
|
||||
|> Graph.add(list_triples)
|
||||
end
|
||||
(item, rdf_graph) ->
|
||||
case object_to_rdf(item) do
|
||||
nil -> rdf_graph
|
||||
object ->
|
||||
Graph.add rdf_graph,
|
||||
{node_to_rdf(subject), node_to_rdf(property), object}
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
else
|
||||
rdf_graph
|
||||
end
|
||||
end)
|
||||
Dataset.add(dataset, rdf_graph,
|
||||
if(graph_name == "@default", do: nil, else: graph_name))
|
||||
else
|
||||
dataset
|
||||
end
|
||||
end)
|
||||
after
|
||||
NodeIdentifierMap.stop(node_id_map)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: This should not be dependent on Poison as a JSON parser in general,
|
||||
# but determine available JSON parsers and use one heuristically or by configuration
|
||||
def parse_json(content, opts \\ []) do
|
||||
Poison.Parser.parse(content)
|
||||
end
|
||||
|
||||
def parse_json!(content, opts \\ []) do
|
||||
Poison.Parser.parse!(content)
|
||||
end
|
||||
|
||||
def node_to_rdf(nil), do: nil
|
||||
def node_to_rdf(node) do
|
||||
if blank_node_id?(node) do
|
||||
node
|
||||
|> String.trim_leading("_:")
|
||||
|> RDF.bnode
|
||||
else
|
||||
RDF.uri(node)
|
||||
end
|
||||
end
|
||||
|
||||
defp object_to_rdf(%{"@id" => id}) do
|
||||
unless relative_iri?(id) do
|
||||
node_to_rdf(id)
|
||||
end
|
||||
end
|
||||
|
||||
defp object_to_rdf(%{"@value" => value} = item) do
|
||||
datatype = item["@type"]
|
||||
cond do
|
||||
is_boolean(value) ->
|
||||
value = to_string(value)
|
||||
datatype = if is_nil(datatype), do: XSD.boolean, else: datatype
|
||||
is_float(value) or (is_number(value) and datatype == to_string(XSD.double)) ->
|
||||
value = to_string(value) # TODO: canonicalize according to Data Round Tripping
|
||||
datatype = if is_nil(datatype), do: XSD.double, else: datatype
|
||||
is_integer(value) or (is_number(value) and datatype == to_string(XSD.integer)) ->
|
||||
value = to_string(value) # TODO: canonicalize according to Data Round Tripping
|
||||
datatype = if is_nil(datatype), do: XSD.integer, else: datatype
|
||||
is_nil(datatype) ->
|
||||
datatype =
|
||||
if Map.has_key?(item, "@language") do
|
||||
RDF.langString
|
||||
else
|
||||
XSD.string
|
||||
end
|
||||
true ->
|
||||
end
|
||||
RDF.Literal.new(value, datatype: datatype, language: item["@language"])
|
||||
end
|
||||
|
||||
defp list_to_rdf(list, node_id_map) do
|
||||
list
|
||||
|> Enum.reverse
|
||||
|> Enum.reduce({[], RDF.NS.RDF.nil}, fn (item, {list_triples, last}) ->
|
||||
case object_to_rdf(item) do
|
||||
nil -> {list_triples, last}
|
||||
object ->
|
||||
with bnode = node_to_rdf(generate_blank_node_id(node_id_map)) do
|
||||
{
|
||||
[{bnode, RDF.NS.RDF.first, object},
|
||||
{bnode, RDF.NS.RDF.rest, last } | list_triples],
|
||||
bnode
|
||||
}
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
end
|
|
@ -10,7 +10,7 @@ defmodule JSON.LD.Utils do
|
|||
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](http://tools.ietf.org/html/rfc3987#section-6.5)
|
||||
"""
|
||||
# TODO: This should be part of a dedicated URI/IRI implementation and properly tested.
|
||||
# TODO: This should be part of a dedicated RDF.IRI implementation and properly tested.
|
||||
def absolute_iri(value, base_iri)
|
||||
|
||||
def absolute_iri(value, nil), do: value
|
||||
|
@ -26,7 +26,6 @@ defmodule JSON.LD.Utils do
|
|||
|> to_string
|
||||
end
|
||||
|
||||
|
||||
@doc """
|
||||
Checks if the given value is an absolute IRI.
|
||||
|
||||
|
@ -35,9 +34,13 @@ defmodule JSON.LD.Utils do
|
|||
|
||||
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.
|
||||
# TODO: This should be part of a dedicated RDF.IRI implementation and properly tested.
|
||||
def absolute_iri?(value), do: RDF.uri?(value)
|
||||
|
||||
# TODO: This should be part of a dedicated RDF.IRI implementation and properly tested.
|
||||
def relative_iri?(value),
|
||||
do: not (JSON.LD.keyword?(value) or absolute_iri?(value) or blank_node_id?(value))
|
||||
|
||||
def compact_iri_parts(compact_iri, exclude_bnode \\ true) do
|
||||
with [prefix, suffix] when not(binary_part(suffix, 0, 2) == "//") and
|
||||
not(exclude_bnode and prefix == "_") <-
|
||||
|
|
|
@ -4,6 +4,7 @@ defmodule JSON.LD do
|
|||
@base
|
||||
@container
|
||||
@context
|
||||
@default
|
||||
@graph
|
||||
@id
|
||||
@index
|
||||
|
@ -93,4 +94,11 @@ defmodule JSON.LD do
|
|||
def context(context, options),
|
||||
do: JSON.LD.Context.create(%{"@context" => context}, options)
|
||||
|
||||
|
||||
@doc """
|
||||
Generator function for JSON-LD node maps.
|
||||
"""
|
||||
def node_map(input, node_id_map \\ nil),
|
||||
do: JSON.LD.Flattening.node_map(input, node_id_map)
|
||||
|
||||
end
|
||||
|
|
672
test/unit/reader_test.exs
Normal file
672
test/unit/reader_test.exs
Normal file
|
@ -0,0 +1,672 @@
|
|||
defmodule JSON.LD.ReaderTest do
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
doctest JSON.LD.Reader
|
||||
|
||||
alias RDF.{Dataset, Graph}
|
||||
alias RDF.NS
|
||||
alias RDF.NS.{XSD, RDFS}
|
||||
|
||||
|
||||
defmodule TestNS do
|
||||
use RDF.Vocabulary.Namespace
|
||||
defvocab EX, base_uri: "http://example.org/#", terms: [], strict: false
|
||||
defvocab S, base_uri: "http://schema.org/", terms: [], strict: false
|
||||
end
|
||||
|
||||
alias TestNS.{EX, S}
|
||||
|
||||
|
||||
test "an empty JSON document is deserialized to an empty graph" do
|
||||
assert JSON.LD.Reader.read!("{}") == Dataset.new(Graph.new)
|
||||
end
|
||||
|
||||
describe "unnamed nodes" do
|
||||
%{
|
||||
"no @id" => {
|
||||
~s({
|
||||
"http://example.com/foo": "bar"
|
||||
}),
|
||||
{RDF.bnode("b0"), RDF.uri("http://example.com/foo"), RDF.literal("bar")}
|
||||
},
|
||||
"@id with _:a" => {
|
||||
~s({
|
||||
"@id": "_:a",
|
||||
"http://example.com/foo": "bar"
|
||||
}),
|
||||
{RDF.bnode("b0"), RDF.uri("http://example.com/foo"), RDF.literal("bar")}
|
||||
},
|
||||
"@id with _:a and reference" => {
|
||||
~s({
|
||||
"@id": "_:a",
|
||||
"http://example.com/foo": {"@id": "_:a"}
|
||||
}),
|
||||
{RDF.bnode("b0"), RDF.uri("http://example.com/foo"), RDF.bnode("b0")}
|
||||
},
|
||||
}
|
||||
|> Enum.each(fn ({title, data}) ->
|
||||
@tag data: data
|
||||
test title, %{data: {input, output}} do
|
||||
assert JSON.LD.Reader.read_string!(input) == RDF.Dataset.new(output)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
describe "nodes with @id" do
|
||||
%{
|
||||
"with IRI" => {
|
||||
~s({
|
||||
"@id": "http://example.com/a",
|
||||
"http://example.com/foo": "bar"
|
||||
}),
|
||||
{RDF.uri("http://example.com/a"), RDF.uri("http://example.com/foo"), RDF.literal("bar")}
|
||||
},
|
||||
}
|
||||
|> Enum.each(fn ({title, data}) ->
|
||||
@tag data: data
|
||||
test title, %{data: {input, output}} do
|
||||
assert JSON.LD.Reader.read_string!(input) == RDF.Dataset.new(output)
|
||||
end
|
||||
end)
|
||||
|
||||
%{
|
||||
"base" => {
|
||||
~s({
|
||||
"@id": "",
|
||||
"@type": "#{RDF.uri(RDFS.Resource)}"
|
||||
}),
|
||||
{RDF.uri("http://example.org/"), NS.RDF.type, RDF.uri(RDFS.Resource)}
|
||||
},
|
||||
"relative" => {
|
||||
~s({
|
||||
"@id": "a/b",
|
||||
"@type": "#{RDF.uri(RDFS.Resource)}"
|
||||
}),
|
||||
{RDF.uri("http://example.org/a/b"), NS.RDF.type, RDF.uri(RDFS.Resource)}
|
||||
},
|
||||
"hash" => {
|
||||
~s({
|
||||
"@id": "#a",
|
||||
"@type": "#{RDF.uri(RDFS.Resource)}"
|
||||
}),
|
||||
{RDF.uri("http://example.org/#a"), NS.RDF.type, RDF.uri(RDFS.Resource)}
|
||||
},
|
||||
}
|
||||
|> Enum.each(fn ({title, data}) ->
|
||||
@tag data: data
|
||||
test "when relative IRIs #{title}", %{data: {input, output}} do
|
||||
assert JSON.LD.Reader.read_string!(input, base: "http://example.org/") ==
|
||||
RDF.Dataset.new(output)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
describe "typed nodes" do
|
||||
%{
|
||||
"one type" => {
|
||||
~s({
|
||||
"@type": "http://example.com/foo"
|
||||
}),
|
||||
{RDF.bnode("b0"), NS.RDF.type, RDF.uri("http://example.com/foo")}
|
||||
},
|
||||
"two types" => {
|
||||
~s({
|
||||
"@type": ["http://example.com/foo", "http://example.com/baz"]
|
||||
}),
|
||||
[
|
||||
{RDF.bnode("b0"), NS.RDF.type, RDF.uri("http://example.com/foo")},
|
||||
{RDF.bnode("b0"), NS.RDF.type, RDF.uri("http://example.com/baz")},
|
||||
]
|
||||
},
|
||||
"blank node type" => {
|
||||
~s({
|
||||
"@type": "_:foo"
|
||||
}),
|
||||
{RDF.bnode("b1"), NS.RDF.type, RDF.bnode("b0")}
|
||||
}
|
||||
}
|
||||
|> Enum.each(fn ({title, data}) ->
|
||||
@tag data: data
|
||||
test title, %{data: {input, output}} do
|
||||
assert JSON.LD.Reader.read_string!(input) == RDF.Dataset.new(output)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
describe "key/value" do
|
||||
%{
|
||||
"string" => {
|
||||
~s({
|
||||
"http://example.com/foo": "bar"
|
||||
}),
|
||||
{RDF.bnode("b0"), RDF.uri("http://example.com/foo"), RDF.literal("bar")}
|
||||
},
|
||||
"strings" => {
|
||||
~s({
|
||||
"http://example.com/foo": ["bar", "baz"]
|
||||
}),
|
||||
[
|
||||
{RDF.bnode("b0"), RDF.uri("http://example.com/foo"), RDF.literal("bar")},
|
||||
{RDF.bnode("b0"), RDF.uri("http://example.com/foo"), RDF.literal("baz")},
|
||||
]
|
||||
},
|
||||
"IRI" => {
|
||||
~s({
|
||||
"http://example.com/foo": {"@id": "http://example.com/bar"}
|
||||
}),
|
||||
{RDF.bnode("b0"), RDF.uri("http://example.com/foo"), RDF.uri("http://example.com/bar")}
|
||||
},
|
||||
"IRIs" => {
|
||||
~s({
|
||||
"http://example.com/foo": [{"@id": "http://example.com/bar"}, {"@id": "http://example.com/baz"}]
|
||||
}),
|
||||
[
|
||||
{RDF.bnode("b0"), RDF.uri("http://example.com/foo"), RDF.uri("http://example.com/bar")},
|
||||
{RDF.bnode("b0"), RDF.uri("http://example.com/foo"), RDF.uri("http://example.com/baz")},
|
||||
]
|
||||
},
|
||||
}
|
||||
|> Enum.each(fn ({title, data}) ->
|
||||
@tag data: data
|
||||
test title, %{data: {input, output}} do
|
||||
assert JSON.LD.Reader.read_string!(input) == RDF.Dataset.new(output)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
describe "literals" do
|
||||
%{
|
||||
"plain literal" =>
|
||||
{
|
||||
~s({"@id": "http://greggkellogg.net/foaf#me", "http://xmlns.com/foaf/0.1/name": "Gregg Kellogg"}),
|
||||
{RDF.uri("http://greggkellogg.net/foaf#me"), RDF.uri("http://xmlns.com/foaf/0.1/name"), RDF.literal("Gregg Kellogg")},
|
||||
},
|
||||
"explicit plain literal" =>
|
||||
{
|
||||
~s({"http://xmlns.com/foaf/0.1/name": {"@value": "Gregg Kellogg"}}),
|
||||
{RDF.bnode("b0"), RDF.uri("http://xmlns.com/foaf/0.1/name"), RDF.literal("Gregg Kellogg")}
|
||||
},
|
||||
"language tagged literal" =>
|
||||
{
|
||||
~s({"http://www.w3.org/2000/01/rdf-schema#label": {"@value": "A plain literal with a lang tag.", "@language": "en-us"}}),
|
||||
{RDF.bnode("b0"), RDFS.label, RDF.literal("A plain literal with a lang tag.", language: "en-us")}
|
||||
},
|
||||
"I18N literal with language" =>
|
||||
{
|
||||
~s([{
|
||||
"@id": "http://greggkellogg.net/foaf#me",
|
||||
"http://xmlns.com/foaf/0.1/knows": {"@id": "http://www.ivan-herman.net/foaf#me"}
|
||||
},{
|
||||
"@id": "http://www.ivan-herman.net/foaf#me",
|
||||
"http://xmlns.com/foaf/0.1/name": {"@value": "Herman Iván", "@language": "hu"}
|
||||
}]),
|
||||
[
|
||||
{RDF.uri("http://greggkellogg.net/foaf#me"), RDF.uri("http://xmlns.com/foaf/0.1/knows"), RDF.uri("http://www.ivan-herman.net/foaf#me")},
|
||||
{RDF.uri("http://www.ivan-herman.net/foaf#me"), RDF.uri("http://xmlns.com/foaf/0.1/name"), RDF.literal("Herman Iv\u00E1n", language: "hu")},
|
||||
]
|
||||
},
|
||||
"explicit datatyped literal" =>
|
||||
{
|
||||
~s({
|
||||
"@id": "http://greggkellogg.net/foaf#me",
|
||||
"http://purl.org/dc/terms/created": {"@value": "1957-02-27", "@type": "http://www.w3.org/2001/XMLSchema#date"}
|
||||
}),
|
||||
{RDF.uri("http://greggkellogg.net/foaf#me"), RDF.uri("http://purl.org/dc/terms/created"), RDF.literal("1957-02-27", datatype: XSD.date)},
|
||||
},
|
||||
}
|
||||
|> Enum.each(fn ({title, data}) ->
|
||||
@tag data: data
|
||||
test title, %{data: {input, output}} do
|
||||
assert JSON.LD.Reader.read_string!(input) == RDF.Dataset.new(output)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
describe "prefixes" do
|
||||
%{
|
||||
"empty prefix" => {
|
||||
~s({"@context": {"": "http://example.com/default#"}, ":foo": "bar"}),
|
||||
{RDF.bnode("b0"), RDF.uri("http://example.com/default#foo"), RDF.literal("bar")}
|
||||
},
|
||||
# TODO:
|
||||
"empty suffix" => {
|
||||
~s({"@context": {"prefix": "http://example.com/default#"}, "prefix:": "bar"}),
|
||||
{RDF.bnode("b0"), RDF.uri("http://example.com/default#"), RDF.literal("bar")}
|
||||
},
|
||||
"prefix:suffix" => {
|
||||
~s({"@context": {"prefix": "http://example.com/default#"}, "prefix:foo": "bar"}),
|
||||
{RDF.bnode("b0"), RDF.uri("http://example.com/default#foo"), RDF.literal("bar")}
|
||||
}
|
||||
}
|
||||
|> Enum.each(fn ({title, data}) ->
|
||||
if title == "empty suffix", do: @tag :skip
|
||||
@tag data: data
|
||||
test title, %{data: {input, output}} do
|
||||
assert JSON.LD.Reader.read_string!(input) == RDF.Dataset.new(output)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
describe "overriding keywords" do
|
||||
%{
|
||||
"'url' for @id, 'a' for @type" => {
|
||||
~s({
|
||||
"@context": {"url": "@id", "a": "@type", "name": "http://schema.org/name"},
|
||||
"url": "http://example.com/about#gregg",
|
||||
"a": "http://schema.org/Person",
|
||||
"name": "Gregg Kellogg"
|
||||
}),
|
||||
[
|
||||
{RDF.uri("http://example.com/about#gregg"), NS.RDF.type, RDF.uri("http://schema.org/Person")},
|
||||
{RDF.uri("http://example.com/about#gregg"), RDF.uri("http://schema.org/name"), RDF.literal("Gregg Kellogg")},
|
||||
]
|
||||
},
|
||||
}
|
||||
|> Enum.each(fn ({title, data}) ->
|
||||
@tag data: data
|
||||
test title, %{data: {input, output}} do
|
||||
assert JSON.LD.Reader.read_string!(input) == RDF.Dataset.new(output)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
describe "chaining" do
|
||||
%{
|
||||
"explicit subject" =>
|
||||
{
|
||||
~s({
|
||||
"@context": {"foaf": "http://xmlns.com/foaf/0.1/"},
|
||||
"@id": "http://greggkellogg.net/foaf#me",
|
||||
"foaf:knows": {
|
||||
"@id": "http://www.ivan-herman.net/foaf#me",
|
||||
"foaf:name": "Ivan Herman"
|
||||
}
|
||||
}),
|
||||
[
|
||||
{RDF.uri("http://greggkellogg.net/foaf#me"), RDF.uri("http://xmlns.com/foaf/0.1/knows"), RDF.uri("http://www.ivan-herman.net/foaf#me")},
|
||||
{RDF.uri("http://www.ivan-herman.net/foaf#me"), RDF.uri("http://xmlns.com/foaf/0.1/name"), RDF.literal("Ivan Herman")},
|
||||
]
|
||||
},
|
||||
"implicit subject" =>
|
||||
{
|
||||
~s({
|
||||
"@context": {"foaf": "http://xmlns.com/foaf/0.1/"},
|
||||
"@id": "http://greggkellogg.net/foaf#me",
|
||||
"foaf:knows": {
|
||||
"foaf:name": "Manu Sporny"
|
||||
}
|
||||
}),
|
||||
[
|
||||
{RDF.uri("http://greggkellogg.net/foaf#me"), RDF.uri("http://xmlns.com/foaf/0.1/knows"), RDF.bnode("b0")},
|
||||
{RDF.bnode("b0"), RDF.uri("http://xmlns.com/foaf/0.1/name"), RDF.literal("Manu Sporny")},
|
||||
]
|
||||
},
|
||||
}
|
||||
|> Enum.each(fn ({title, data}) ->
|
||||
@tag data: data
|
||||
test title, %{data: {input, output}} do
|
||||
assert JSON.LD.Reader.read_string!(input) == RDF.Dataset.new(output)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
describe "multiple values" do
|
||||
%{
|
||||
"literals" =>
|
||||
{
|
||||
~s({
|
||||
"@context": {"foaf": "http://xmlns.com/foaf/0.1/"},
|
||||
"@id": "http://greggkellogg.net/foaf#me",
|
||||
"foaf:knows": ["Manu Sporny", "Ivan Herman"]
|
||||
}),
|
||||
[
|
||||
{RDF.uri("http://greggkellogg.net/foaf#me"), RDF.uri("http://xmlns.com/foaf/0.1/knows"), RDF.literal("Manu Sporny")},
|
||||
{RDF.uri("http://greggkellogg.net/foaf#me"), RDF.uri("http://xmlns.com/foaf/0.1/knows"), RDF.literal("Ivan Herman")},
|
||||
]
|
||||
},
|
||||
}
|
||||
|> Enum.each(fn ({title, data}) ->
|
||||
@tag data: data
|
||||
test title, %{data: {input, output}} do
|
||||
assert JSON.LD.Reader.read_string!(input) == RDF.Dataset.new(output)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
describe "lists" do
|
||||
%{
|
||||
"Empty" => {
|
||||
~s({
|
||||
"@context": {"foaf": "http://xmlns.com/foaf/0.1/"},
|
||||
"@id": "http://greggkellogg.net/foaf#me",
|
||||
"foaf:knows": {"@list": []}
|
||||
}),
|
||||
{RDF.uri("http://greggkellogg.net/foaf#me"), RDF.uri("http://xmlns.com/foaf/0.1/knows"), NS.RDF.nil}
|
||||
},
|
||||
"single value" => {
|
||||
~s({
|
||||
"@context": {"foaf": "http://xmlns.com/foaf/0.1/"},
|
||||
"@id": "http://greggkellogg.net/foaf#me",
|
||||
"foaf:knows": {"@list": ["Manu Sporny"]}
|
||||
}),
|
||||
[
|
||||
{RDF.uri("http://greggkellogg.net/foaf#me"), RDF.uri("http://xmlns.com/foaf/0.1/knows"), RDF.bnode("b0")},
|
||||
{RDF.bnode("b0"), NS.RDF.first, RDF.literal("Manu Sporny")},
|
||||
{RDF.bnode("b0"), NS.RDF.rest, NS.RDF.nil},
|
||||
]
|
||||
},
|
||||
"single value (with coercion)" => {
|
||||
~s({
|
||||
"@context": {
|
||||
"foaf": "http://xmlns.com/foaf/0.1/",
|
||||
"foaf:knows": { "@container": "@list"}
|
||||
},
|
||||
"@id": "http://greggkellogg.net/foaf#me",
|
||||
"foaf:knows": ["Manu Sporny"]
|
||||
}),
|
||||
[
|
||||
{RDF.uri("http://greggkellogg.net/foaf#me"), RDF.uri("http://xmlns.com/foaf/0.1/knows"), RDF.bnode("b0")},
|
||||
{RDF.bnode("b0"), NS.RDF.first, RDF.literal("Manu Sporny")},
|
||||
{RDF.bnode("b0"), NS.RDF.rest, NS.RDF.nil},
|
||||
]
|
||||
},
|
||||
"multiple values" => {
|
||||
~s({
|
||||
"@context": {"foaf": "http://xmlns.com/foaf/0.1/"},
|
||||
"@id": "http://greggkellogg.net/foaf#me",
|
||||
"foaf:knows": {"@list": ["Manu Sporny", "Dave Longley"]}
|
||||
}),
|
||||
[
|
||||
{RDF.uri("http://greggkellogg.net/foaf#me"), RDF.uri("http://xmlns.com/foaf/0.1/knows"), RDF.bnode("b1")},
|
||||
{RDF.bnode("b1"), NS.RDF.first, RDF.literal("Manu Sporny")},
|
||||
{RDF.bnode("b1"), NS.RDF.rest, RDF.bnode("b0")},
|
||||
{RDF.bnode("b0"), NS.RDF.first, RDF.literal("Dave Longley")},
|
||||
{RDF.bnode("b0"), NS.RDF.rest, NS.RDF.nil},
|
||||
]
|
||||
},
|
||||
}
|
||||
|> Enum.each(fn ({title, data}) ->
|
||||
@tag data: data
|
||||
test title, %{data: {input, output}} do
|
||||
assert JSON.LD.Reader.read_string!(input) == RDF.Dataset.new(output)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
describe "context" do
|
||||
%{
|
||||
"@id coersion" =>
|
||||
{
|
||||
~s({
|
||||
"@context": {
|
||||
"knows": {"@id": "http://xmlns.com/foaf/0.1/knows", "@type": "@id"}
|
||||
},
|
||||
"@id": "http://greggkellogg.net/foaf#me",
|
||||
"knows": "http://www.ivan-herman.net/foaf#me"
|
||||
}),
|
||||
{RDF.uri("http://greggkellogg.net/foaf#me"), RDF.uri("http://xmlns.com/foaf/0.1/knows"), RDF.uri("http://www.ivan-herman.net/foaf#me")},
|
||||
},
|
||||
"datatype coersion" =>
|
||||
{
|
||||
~s({
|
||||
"@context": {
|
||||
"dcterms": "http://purl.org/dc/terms/",
|
||||
"xsd": "http://www.w3.org/2001/XMLSchema#",
|
||||
"created": {"@id": "http://purl.org/dc/terms/created", "@type": "xsd:date"}
|
||||
},
|
||||
"@id": "http://greggkellogg.net/foaf#me",
|
||||
"created": "1957-02-27"
|
||||
}),
|
||||
{RDF.uri("http://greggkellogg.net/foaf#me"), RDF.uri("http://purl.org/dc/terms/created"), RDF.literal("1957-02-27", datatype: XSD.date)},
|
||||
},
|
||||
"sub-objects with context" => {
|
||||
~s({
|
||||
"@context": {"foo": "http://example.com/foo"},
|
||||
"foo": {
|
||||
"@context": {"foo": "http://example.org/foo"},
|
||||
"foo": "bar"
|
||||
}
|
||||
}),
|
||||
[
|
||||
{RDF.bnode("b0"), RDF.uri("http://example.com/foo"), RDF.bnode("b1")},
|
||||
{RDF.bnode("b1"), RDF.uri("http://example.org/foo"), RDF.literal("bar")},
|
||||
]
|
||||
},
|
||||
"contexts with a list processed in order" => {
|
||||
~s({
|
||||
"@context": [
|
||||
{"foo": "http://example.com/foo"},
|
||||
{"foo": "http://example.org/foo"}
|
||||
],
|
||||
"foo": "bar"
|
||||
}),
|
||||
{RDF.bnode("b0"), RDF.uri("http://example.org/foo"), RDF.literal("bar")},
|
||||
},
|
||||
"term definition resolves term as IRI" => {
|
||||
~s({
|
||||
"@context": [
|
||||
{"foo": "http://example.com/foo"},
|
||||
{"bar": "foo"}
|
||||
],
|
||||
"bar": "bar"
|
||||
}),
|
||||
{RDF.bnode("b0"), RDF.uri("http://example.com/foo"), RDF.literal("bar")},
|
||||
},
|
||||
"term definition resolves prefix as IRI" => {
|
||||
~s({
|
||||
"@context": [
|
||||
{"foo": "http://example.com/foo#"},
|
||||
{"bar": "foo:bar"}
|
||||
],
|
||||
"bar": "bar"
|
||||
}),
|
||||
{RDF.bnode("b0"), RDF.uri("http://example.com/foo#bar"), RDF.literal("bar")},
|
||||
},
|
||||
"@language" => {
|
||||
~s({
|
||||
"@context": {
|
||||
"foo": "http://example.com/foo#",
|
||||
"@language": "en"
|
||||
},
|
||||
"foo:bar": "baz"
|
||||
}),
|
||||
{RDF.bnode("b0"), RDF.uri("http://example.com/foo#bar"), RDF.literal("baz", language: "en")},
|
||||
},
|
||||
"@language with override" => {
|
||||
~s({
|
||||
"@context": {
|
||||
"foo": "http://example.com/foo#",
|
||||
"@language": "en"
|
||||
},
|
||||
"foo:bar": {"@value": "baz", "@language": "fr"}
|
||||
}),
|
||||
{RDF.bnode("b0"), RDF.uri("http://example.com/foo#bar"), RDF.literal("baz", language: "fr")},
|
||||
},
|
||||
"@language with plain" => {
|
||||
~s({
|
||||
"@context": {
|
||||
"foo": "http://example.com/foo#",
|
||||
"@language": "en"
|
||||
},
|
||||
"foo:bar": {"@value": "baz"}
|
||||
}),
|
||||
{RDF.bnode("b0"), RDF.uri("http://example.com/foo#bar"), RDF.literal("baz")},
|
||||
},
|
||||
}
|
||||
|> Enum.each(fn ({title, data}) ->
|
||||
@tag data: data
|
||||
test title, %{data: {input, output}} do
|
||||
assert JSON.LD.Reader.read_string!(input) == RDF.Dataset.new(output)
|
||||
end
|
||||
end)
|
||||
|
||||
%{
|
||||
"dt with term" => {
|
||||
~s({
|
||||
"@context": [
|
||||
{"date": "http://www.w3.org/2001/XMLSchema#date", "term": "http://example.org/foo#"},
|
||||
{"foo": {"@id": "term", "@type": "date"}}
|
||||
],
|
||||
"foo": "bar"
|
||||
}),
|
||||
{RDF.bnode("b0"), RDF.uri("http://example.org/foo#"), RDF.literal("bar", datatype: XSD.date)},
|
||||
},
|
||||
"@id with term" => {
|
||||
~s({
|
||||
"@context": [
|
||||
{"foo": {"@id": "http://example.org/foo#bar", "@type": "@id"}}
|
||||
],
|
||||
"foo": "http://example.org/foo#bar"
|
||||
}),
|
||||
{RDF.bnode("b0"), RDF.uri("http://example.org/foo#bar"), RDF.uri("http://example.org/foo#bar")},
|
||||
},
|
||||
"coercion without term definition" => {
|
||||
~s({
|
||||
"@context": [
|
||||
{
|
||||
"xsd": "http://www.w3.org/2001/XMLSchema#",
|
||||
"dc": "http://purl.org/dc/terms/"
|
||||
},
|
||||
{
|
||||
"dc:date": {"@type": "xsd:date"}
|
||||
}
|
||||
],
|
||||
"dc:date": "2011-11-23"
|
||||
}),
|
||||
{RDF.bnode("b0"), RDF.uri("http://purl.org/dc/terms/date"), RDF.literal("2011-11-23", datatype: XSD.date)},
|
||||
},
|
||||
}
|
||||
|> Enum.each(fn ({title, data}) ->
|
||||
@tag data: data
|
||||
test "term def with @id + @type coercion: #{title}", %{data: {input, output}} do
|
||||
assert JSON.LD.Reader.read_string!(input) == RDF.Dataset.new(output)
|
||||
end
|
||||
end)
|
||||
|
||||
%{
|
||||
"dt with term" => {
|
||||
~s({
|
||||
"@context": [
|
||||
{"date": "http://www.w3.org/2001/XMLSchema#date", "term": "http://example.org/foo#"},
|
||||
{"foo": {"@id": "term", "@type": "date", "@container": "@list"}}
|
||||
],
|
||||
"foo": ["bar"]
|
||||
}),
|
||||
[
|
||||
{RDF.bnode("b0"), RDF.uri("http://example.org/foo#"), RDF.bnode("b1")},
|
||||
{RDF.bnode("b1"), NS.RDF.first, RDF.literal("bar", datatype: XSD.date)},
|
||||
{RDF.bnode("b1"), NS.RDF.rest, NS.RDF.nil},
|
||||
]
|
||||
},
|
||||
"@id with term" => {
|
||||
~s({
|
||||
"@context": [
|
||||
{"foo": {"@id": "http://example.org/foo#bar", "@type": "@id", "@container": "@list"}}
|
||||
],
|
||||
"foo": ["http://example.org/foo#bar"]
|
||||
}),
|
||||
[
|
||||
{RDF.bnode("b0"), RDF.uri("http://example.org/foo#bar"), RDF.bnode("b1")},
|
||||
{RDF.bnode("b1"), NS.RDF.first, RDF.uri("http://example.org/foo#bar")},
|
||||
{RDF.bnode("b1"), NS.RDF.rest, NS.RDF.nil},
|
||||
]
|
||||
},
|
||||
}
|
||||
|> Enum.each(fn ({title, data}) ->
|
||||
@tag data: data
|
||||
test "term def with @id + @type + @container list: #{title}", %{data: {input, output}} do
|
||||
assert JSON.LD.Reader.read_string!(input) == RDF.Dataset.new(output)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
describe "blank node predicates" do
|
||||
setup do
|
||||
{:ok, input: ~s({"@id": "http://example/subj", "_:foo": "bar"})}
|
||||
end
|
||||
|
||||
test "outputs statements with blank node predicates if :produceGeneralizedRdf is true",
|
||||
%{input: input} do
|
||||
dataset = JSON.LD.Reader.read_string!(input, produce_generalized_rdf: true)
|
||||
assert RDF.Dataset.statement_count(dataset) == 1
|
||||
end
|
||||
|
||||
test "rejects statements with blank node predicates if :produceGeneralizedRdf is false",
|
||||
%{input: input} do
|
||||
dataset = JSON.LD.Reader.read_string!(input, produce_generalized_rdf: false)
|
||||
assert RDF.Dataset.statement_count(dataset) == 0
|
||||
end
|
||||
end
|
||||
|
||||
describe "advanced features" do
|
||||
%{
|
||||
# TODO:
|
||||
"number syntax (decimal)" =>
|
||||
{
|
||||
~s({"@context": { "measure": "http://example/measure#"}, "measure:cups": 5.3}),
|
||||
{RDF.bnode("b0"), RDF.uri("http://example/measure#cups"), RDF.literal("5.3E0", datatype: XSD.double)}
|
||||
},
|
||||
# TODO:
|
||||
"number syntax (double)" =>
|
||||
{
|
||||
~s({"@context": { "measure": "http://example/measure#"}, "measure:cups": 5.3e0}),
|
||||
{RDF.bnode("b0"), RDF.uri("http://example/measure#cups"), RDF.literal("5.3E0", datatype: XSD.double)}
|
||||
},
|
||||
"number syntax (integer)" =>
|
||||
{
|
||||
~s({"@context": { "chem": "http://example/chem#"}, "chem:protons": 12}),
|
||||
{RDF.bnode("b0"), RDF.uri("http://example/chem#protons"), RDF.literal("12", datatype: XSD.integer)}
|
||||
},
|
||||
"boolan syntax" =>
|
||||
{
|
||||
~s({"@context": { "sensor": "http://example/sensor#"}, "sensor:active": true}),
|
||||
{RDF.bnode("b0"), RDF.uri("http://example/sensor#active"), RDF.literal("true", datatype: XSD.boolean)}
|
||||
},
|
||||
"Array top element" =>
|
||||
{
|
||||
~s([
|
||||
{"@id": "http://example.com/#me", "@type": "http://xmlns.com/foaf/0.1/Person"},
|
||||
{"@id": "http://example.com/#you", "@type": "http://xmlns.com/foaf/0.1/Person"}
|
||||
]),
|
||||
[
|
||||
{RDF.uri("http://example.com/#me"), NS.RDF.type, RDF.uri("http://xmlns.com/foaf/0.1/Person")},
|
||||
{RDF.uri("http://example.com/#you"), NS.RDF.type, RDF.uri("http://xmlns.com/foaf/0.1/Person")}
|
||||
]
|
||||
},
|
||||
"@graph with array of objects value" =>
|
||||
{
|
||||
~s({
|
||||
"@context": {"foaf": "http://xmlns.com/foaf/0.1/"},
|
||||
"@graph": [
|
||||
{"@id": "http://example.com/#me", "@type": "foaf:Person"},
|
||||
{"@id": "http://example.com/#you", "@type": "foaf:Person"}
|
||||
]
|
||||
}),
|
||||
[
|
||||
{RDF.uri("http://example.com/#me"), NS.RDF.type, RDF.uri("http://xmlns.com/foaf/0.1/Person")},
|
||||
{RDF.uri("http://example.com/#you"), NS.RDF.type, RDF.uri("http://xmlns.com/foaf/0.1/Person")}
|
||||
]
|
||||
},
|
||||
"XMLLiteral" =>
|
||||
{
|
||||
~s({
|
||||
"http://rdfs.org/sioc/ns#content": {
|
||||
"@value": "foo",
|
||||
"@type": "http://www.w3.org/1999/02/22-rdf-syntax-ns#XMLLiteral"
|
||||
}
|
||||
}),
|
||||
{RDF.bnode("b0"), RDF.uri("http://rdfs.org/sioc/ns#content"), RDF.literal("foo", datatype: "http://www.w3.org/1999/02/22-rdf-syntax-ns#XMLLiteral")}
|
||||
}
|
||||
}
|
||||
|> Enum.each(fn ({title, data}) ->
|
||||
if title in ["number syntax (decimal)", "number syntax (double)"] do
|
||||
@tag skip: "support float literals with exponential notation"
|
||||
end
|
||||
@tag data: data
|
||||
test title, %{data: {input, output}} do
|
||||
assert JSON.LD.Reader.read_string!(input) == RDF.Dataset.new(output)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
end
|
Loading…
Reference in a new issue