core: basics of the RDF model

This commit is contained in:
Marcel Otto 2016-10-15 18:26:56 +02:00
parent 068539e825
commit 499714285b
17 changed files with 1268 additions and 10 deletions

101
lib/rdf.ex Normal file
View file

@ -0,0 +1,101 @@
defmodule RDF do
alias RDF.{Vocabulary, Literal, BlankNode, Triple}
defmodule InvalidURIError, do: defexception [:message]
@doc """
Generator function for URIs from strings or term atoms of a `RDF.Vocabulary`.
## Examples
iex> RDF.uri("http://www.example.com/foo")
%URI{authority: "www.example.com", fragment: nil, host: "www.example.com",
path: "/foo", port: 80, query: nil, scheme: "http", userinfo: nil}
iex> RDF.uri(RDF.RDFS.Class)
%URI{authority: "www.w3.org", fragment: "Class", host: "www.w3.org",
path: "/2000/01/rdf-schema", port: 80, query: nil, scheme: "http",
userinfo: nil}
iex> RDF.uri("not a uri")
** (RDF.InvalidURIError) string "not a uri" is not a valid URI
"""
@spec uri(URI.t | binary | atom) :: URI.t
def uri(atom) when is_atom(atom), do: Vocabulary.__uri__(atom)
def uri(string) do
parsed_uri = URI.parse(string)
if uri?(parsed_uri) do
parsed_uri
else
raise InvalidURIError, ~s(string "#{string}" is not a valid URI)
end
end
@doc """
Checks if the given value is an URI.
## Examples
iex> RDF.uri?("http://www.example.com/foo")
true
iex> RDF.uri?("not a uri")
false
"""
def uri?(some_uri = %URI{}) do
# The following was a suggested at http://stackoverflow.com/questions/30696761/check-if-a-url-is-valid-in-elixir
# TODO: Find a better way! Maybe https://github.com/marcelog/ex_rfc3986 ?
case some_uri do
%URI{scheme: nil} -> false
%URI{host: nil} -> false
%URI{path: nil} -> false
_uri -> true
end
end
def uri?(value) when is_binary(value), do: uri?(URI.parse(value))
def uri?(_), do: false
@doc """
Generator function for `RDF.Literal` values.
## Examples
iex> RDF.literal(42)
%RDF.Literal{value: 42, language: nil,
datatype: %URI{authority: "www.w3.org", fragment: "integer",
host: "www.w3.org", path: "/2001/XMLSchema", port: 80,
query: nil, scheme: "http", userinfo: nil}}
"""
def literal(value)
def literal(lit = %Literal{}), do: lit
def literal(value), do: Literal.new(value)
@doc """
Generator function for `RDF.Triple`s.
## Examples
iex> RDF.triple("http://example.com/S", "http://example.com/p", 42)
{RDF.uri("http://example.com/S"), RDF.uri("http://example.com/p"), RDF.literal(42)}
"""
def triple(subject, predicate, object), do: Triple.new(subject, predicate, object)
def triple(tuple), do: Triple.new(tuple)
@doc """
Generator function for `RDF.BlankNodes`.
"""
def bnode, do: BlankNode.new
@doc """
Generator function for `RDF.BlankNodes` with a user-defined identity.
## Examples
iex> RDF.bnode(:foo)
%RDF.BlankNode{id: :foo}
"""
def bnode(id), do: BlankNode.new(:foo)
end

12
lib/rdf/blank_node.ex Normal file
View file

@ -0,0 +1,12 @@
defmodule RDF.BlankNode do
@moduledoc """
"""
defstruct [:id]
@type t :: module
def new, do: %RDF.BlankNode{id: make_ref}
def new(id) when is_atom(id), do: %RDF.BlankNode{id: id}
end

126
lib/rdf/description.ex Normal file
View file

@ -0,0 +1,126 @@
defmodule RDF.Description do
@moduledoc """
Defines a RDF Description.
A `RDF.Description` represents a set of `RDF.Triple`s about a subject.
"""
defstruct subject: nil, predications: %{}
alias RDF.Triple
@type t :: module
@doc """
Creates a new `RDF.Description` about the given subject.
When given a triple, it must contain the subject.
When given a list of statements, the first one must contain a subject.
"""
@spec new(Triple.convertible_subject) :: RDF.Description.t
def new(subject)
def new({subject, predicate, object}),
do: new(subject) |> add({predicate, object})
def new([statement | more_statements]),
do: new(statement) |> add(more_statements)
def new(subject),
do: %RDF.Description{subject: Triple.convert_subject(subject)}
@doc """
Adds statements to a `RDF.Description`.
"""
def add(description, statements)
def add(desc = %RDF.Description{}, {predicate, object}) do
with triple_predicate = Triple.convert_predicate(predicate),
triple_object = Triple.convert_object(object),
predications = Map.update(desc.predications,
triple_predicate, %{triple_object => nil}, fn objects ->
Map.put_new(objects, triple_object, nil) end) do
%RDF.Description{subject: desc.subject, predications: predications}
end
end
def add(desc = %RDF.Description{}, {subject, predicate, object}) do
if RDF.uri(subject) == desc.subject,
do: add(desc, {predicate, object}),
else: desc
end
def add(desc, statements) when is_list(statements) do
Enum.reduce statements, desc, fn (statement, desc) ->
add(desc, statement)
end
end
@doc """
Returns the number of statements of the `RDF.Description`.
"""
def count(%RDF.Description{predications: predications}) do
Enum.reduce predications, 0,
fn ({_, objects}, count) -> count + Enum.count(objects) end
end
@doc """
Checks if the given statement exists within the `RDF.Description`.
"""
def include?(description, statement)
def include?(%RDF.Description{predications: predications},
{predicate, object}) do
with triple_predicate = Triple.convert_predicate(predicate),
triple_object = Triple.convert_object(object) do
predications
|> Map.get(triple_predicate, %{})
|> Map.has_key?(triple_object)
end
end
def include?(desc = %RDF.Description{subject: desc_subject},
{subject, predicate, object}) do
Triple.convert_subject(subject) == desc_subject &&
include?(desc, {predicate, object})
end
def include?(%RDF.Description{}, _), do: false
# TODO: Can/should we isolate and move the Enumerable specific part to the Enumerable implementation?
def reduce(%RDF.Description{predications: predications}, {:cont, acc}, _fun)
when map_size(predications) == 0, do: {:done, acc}
def reduce(description = %RDF.Description{}, {:cont, acc}, fun) do
{triple, rest} = RDF.Description.pop(description)
reduce(rest, fun.(triple, acc), fun)
end
def reduce(_, {:halt, acc}, _fun), do: {:halted, acc}
def reduce(description = %RDF.Description{}, {:suspend, acc}, fun) do
{:suspended, acc, &reduce(description, &1, fun)}
end
def pop(description = %RDF.Description{predications: predications})
when predications == %{}, do: {nil, description}
def pop(%RDF.Description{subject: subject, predications: predications}) do
# TODO: Find a faster way ...
predicate = List.first(Map.keys(predications))
[{object, _}] = Enum.take(objects = predications[predicate], 1)
popped = if Enum.count(objects) == 1,
do: elem(Map.pop(predications, predicate), 1),
else: elem(pop_in(predications, [predicate, object]), 1)
{{subject, predicate, object},
%RDF.Description{subject: subject, predications: popped}}
end
end
defimpl Enumerable, for: RDF.Description do
def reduce(desc, acc, fun), do: RDF.Description.reduce(desc, acc, fun)
def member?(desc, triple), do: {:ok, RDF.Description.include?(desc, triple)}
def count(desc), do: {:ok, RDF.Description.count(desc)}
end

97
lib/rdf/graph.ex Normal file
View file

@ -0,0 +1,97 @@
defmodule RDF.Graph do
@moduledoc """
Defines a RDF Graph.
A `RDF.Graph` represents a set of `RDF.Description`s.
Named vs. unnamed graphs ...
"""
defstruct name: nil, descriptions: %{}
alias RDF.{Description, Triple}
@type t :: module
@doc """
Creates a new `RDF.Graph`.
"""
def new, do: %RDF.Graph{}
def new(statement = {_, _, _}), do: new |> add(statement)
def new([statement | rest]), do: new(statement) |> add(rest)
def new(name), do: %RDF.Graph{name: RDF.uri(name)}
def new(name, statement = {_, _, _}), do: new(name) |> add(statement)
def new(name, [statement | rest]), do: new(name, statement) |> add(rest)
def add(%RDF.Graph{name: name, descriptions: descriptions},
{subject, predicate, object}) do
with triple_subject = Triple.convert_subject(subject),
updated_descriptions = Map.update(descriptions, triple_subject,
Description.new({triple_subject, predicate, object}), fn description ->
description |> Description.add({predicate, object})
end) do
%RDF.Graph{name: name, descriptions: updated_descriptions}
end
end
def add(graph, statements) when is_list(statements) do
Enum.reduce statements, graph, fn (statement, graph) ->
RDF.Graph.add(graph, statement)
end
end
def subject_count(graph), do: Enum.count(graph.descriptions)
def triple_count(%RDF.Graph{descriptions: descriptions}) do
Enum.reduce descriptions, 0, fn ({_subject, description}, count) ->
count + Description.count(description)
end
end
def include?(%RDF.Graph{descriptions: descriptions},
triple = {subject, _, _}) do
with triple_subject = Triple.convert_subject(subject),
%Description{} <- description = descriptions[triple_subject] do
Description.include?(description, triple)
else
_ -> false
end
end
# TODO: Can/should we isolate and move the Enumerable specific part to the Enumerable implementation?
def reduce(%RDF.Graph{descriptions: descriptions}, {:cont, acc}, _fun)
when map_size(descriptions) == 0, do: {:done, acc}
def reduce(graph = %RDF.Graph{}, {:cont, acc}, fun) do
{triple, rest} = RDF.Graph.pop(graph)
reduce(rest, fun.(triple, acc), fun)
end
def reduce(_, {:halt, acc}, _fun), do: {:halted, acc}
def reduce(graph = %RDF.Graph{}, {:suspend, acc}, fun) do
{:suspended, acc, &reduce(graph, &1, fun)}
end
def pop(graph = %RDF.Graph{descriptions: descriptions})
when descriptions == %{}, do: {nil, graph}
def pop(%RDF.Graph{name: name, descriptions: descriptions}) do
# TODO: Find a faster way ...
[{subject, description}] = Enum.take(descriptions, 1)
{triple, popped_description} = Description.pop(description)
popped = if Enum.empty?(popped_description),
do: descriptions |> Map.delete(subject),
else: descriptions |> Map.put(subject, popped_description)
{triple, %RDF.Graph{name: name, descriptions: popped}}
end
end
defimpl Enumerable, for: RDF.Graph do
def reduce(desc, acc, fun), do: RDF.Graph.reduce(desc, acc, fun)
def member?(desc, triple), do: {:ok, RDF.Graph.include?(desc, triple)}
def count(desc), do: {:ok, RDF.Graph.triple_count(desc)}
end

62
lib/rdf/literal.ex Normal file
View file

@ -0,0 +1,62 @@
defmodule RDF.Literal do
@moduledoc """
RDF literals are leaf nodes of a RDF graph containing raw data, like strings and numbers.
"""
defstruct [:value, :datatype, :language]
@type t :: module
alias RDF.XSD
defmodule InvalidLiteralError, do: defexception [:message]
@doc """
Creates a new `RDF.Literal` of the given value and tries to infer an appropriate XSD datatype.
Note: The `RDF.literal` function is a shortcut to this function.
The following mapping of Elixir types to XSD datatypes is applied:
| Elixir type | XSD datatype |
| :---------- | :----------- |
| string | |
| boolean | `boolean` |
| integer | `integer` |
| float | `float` |
| atom | |
| ... | |
# Examples
iex> RDF.Literal.new(42)
%RDF.Literal{value: 42, language: nil, datatype: RDF.uri(RDF.XSD.integer)}
"""
def new(value)
def new(value) when is_boolean(value),
do: %RDF.Literal{value: value, datatype: XSD.boolean}
def new(value) when is_integer(value),
do: %RDF.Literal{value: value, datatype: XSD.integer}
def new(value) when is_float(value),
do: %RDF.Literal{value: value, datatype: XSD.float}
# def new(value) when is_atom(value), do:
# def new(value) when is_binary(value), do:
# def new(value) when is_bitstring(value), do:
# def new(value) when is_list(value), do:
# def new(value) when is_tuple(value), do:
# def new(value) when is_map(value), do:
# def new(value) when is_function(value), do:
# def new(value) when is_pid(value), do:
# def new(value) when is_port(value), do:
# def new(value) when is_reference(value), do:
def new(value) do
raise InvalidLiteralError, "#{inspect value} not convertible to a RDF.Literal"
end
end

87
lib/rdf/triple.ex Normal file
View file

@ -0,0 +1,87 @@
defmodule RDF.Triple do
@moduledoc """
Defines a RDF Triple.
A Triple is a plain Elixir tuple consisting of three valid RDF values for
subject, predicate and object.
"""
alias RDF.{BlankNode, Literal}
@type subject :: URI.t | BlankNode.t
@type predicate :: URI.t
@type object :: URI.t | BlankNode.t | Literal.t
@type convertible_subject :: subject | atom | String.t
@type convertible_predicate :: predicate | atom | String.t
@type convertible_object :: object | atom | String.t # TODO: all basic Elixir types convertible to Literals
defmodule InvalidSubjectError do
defexception [:subject]
def message(%{subject: subject}),
do: "'#{inspect(subject)}' is not a valid subject of a RDF.Triple"
end
defmodule InvalidPredicateError do
defexception [:predicate]
def message(%{predicate: predicate}),
do: "'#{inspect(predicate)}' is not a valid predicate of a RDF.Triple"
end
@doc """
Creates a `RDF.Triple` with proper RDF values.
An error is raised when the given elements are not convertible to RDF values.
Note: The `RDF.triple` function is a shortcut to this function.
# Examples
iex> RDF.Triple.new("http://example.com/S", "http://example.com/p", 42)
{RDF.uri("http://example.com/S"), RDF.uri("http://example.com/p"), RDF.literal(42)}
"""
def new(subject, predicate, object) do
{
convert_subject(subject),
convert_predicate(predicate),
convert_object(object)
}
end
@doc """
Creates a `RDF.Triple` with proper RDF values.
An error is raised when the given elements are not convertible to RDF values.
Note: The `RDF.triple` function is a shortcut to this function.
# Examples
iex> RDF.Triple.new {"http://example.com/S", "http://example.com/p", 42}
{RDF.uri("http://example.com/S"), RDF.uri("http://example.com/p"), RDF.literal(42)}
"""
def new({subject, predicate, object}), do: new(subject, predicate, object)
@doc false
def convert_subject(uri)
def convert_subject(uri = %URI{}), do: uri
def convert_subject(bnode = %BlankNode{}), do: bnode
def convert_subject(uri) when is_atom(uri) or is_binary(uri), do: RDF.uri(uri)
def convert_subject(arg), do: raise InvalidSubjectError, subject: arg
@doc false
def convert_predicate(uri)
def convert_predicate(uri = %URI{}), do: uri
def convert_predicate(uri) when is_atom(uri) or is_binary(uri), do: RDF.uri(uri)
def convert_predicate(arg), do: raise InvalidPredicateError, predicate: arg
@doc false
def convert_object(uri)
def convert_object(uri = %URI{}), do: uri
def convert_object(literal = %Literal{}), do: literal
def convert_object(bnode = %BlankNode{}), do: bnode
def convert_object(atom) when is_atom(atom), do: RDF.uri(atom)
def convert_object(arg), do: Literal.new(arg)
end

View file

@ -0,0 +1,11 @@
defmodule RDF.RDFS do
@moduledoc """
The RDFS vocabulary.
See <https://www.w3.org/TR/rdf-schema/>
"""
# TODO: This should be a strict vocabulary and loaded from a file.
use RDF.Vocabulary, base_uri: "http://www.w3.org/2000/01/rdf-schema#"
end

View file

@ -0,0 +1,55 @@
defmodule RDF.XSD do
@moduledoc """
The XML Schema datatypes vocabulary.
See <https://www.w3.org/TR/xmlschema11-2/>
"""
# TODO: This should be a strict vocabulary and loaded from a file.
use RDF.Vocabulary, base_uri: "http://www.w3.org/2001/XMLSchema#"
defuri :string
defuri :normalizedString
defuri :token
defuri :language
defuri :Name
defuri :NCName
defuri :ID
defuri :IDREF
defuri :IDREFS
defuri :ENTITY
defuri :ENTITIES
defuri :NMTOKEN
defuri :NMTOKENS
defuri :boolean
defuri :float
defuri :double
defuri :decimal
defuri :integer
defuri :long
defuri :int
defuri :short
defuri :byte
defuri :nonPositiveInteger
defuri :negativeInteger
defuri :nonNegativeInteger
defuri :positiveInteger
defuri :unsignedLong
defuri :unsignedInt
defuri :unsignedShort
defuri :unsignedByte
defuri :duration
defuri :dateTime
defuri :time
defuri :date
defuri :gYearMonth
defuri :gYear
defuri :gMonthDay
defuri :gDay
defuri :gMonth
defuri :base64Binary
defuri :hexBinary
defuri :anyURI
defuri :QName
defuri :NOTATION
end

166
lib/rdf/vocabulary.ex Normal file
View file

@ -0,0 +1,166 @@
defmodule RDF.Vocabulary do # or RDF.URI.Namespace?
@moduledoc """
Defines a RDF Vocabulary.
A `RDF.Vocabulary` is a collection of URIs and serves as a namespace for its
elements, called terms. The terms can be accessed by qualification on the
resp. Vocabulary module.
## Using a Vocabulary
There are two types of terms in a `RDF.Vocabulary`, which are resolved
differently:
1. Lowercased terms (usually used for RDF properties, but this is not
enforced) are represented as functions on a Vocabulary module and return the
URI directly.
2. Uppercased terms are by standard Elixir semantics modules names, i.e.
atoms. In many in RDF.ex, where an URI is expected, you can use atoms
qualified with a `RDF.Vocabulary` directly, but if you want to resolve it
manually, you can pass the `RDF.Vocabulary` qualified atom to `RDF.uri`.
Examples:
iex> RDF.RDFS.subClassOf
%URI{authority: "www.w3.org", fragment: "subClassOf", host: "www.w3.org",
path: "/2000/01/rdf-schema", port: 80, query: nil, scheme: "http",
userinfo: nil}
iex> RDF.RDFS.Class
RDF.RDFS.Class
iex> RDF.uri(RDF.RDFS.Class)
%URI{authority: "www.w3.org", fragment: "Class", host: "www.w3.org",
path: "/2000/01/rdf-schema", port: 80, query: nil, scheme: "http",
userinfo: nil}
iex> RDF.triple(RDF.RDFS.Class, RDF.RDFS.subClass, RDF.RDFS.Resource)
{RDF.uri(RDF.RDFS.Class), RDF.uri(RDF.RDFS.subClass), RDF.uri(RDF.RDFS.Resource)}
## Strict vocabularies
What is a strict vocabulary and why should I use them over non-strict
vocabularies and define all terms ...
## Defining a vocabulary
There are two basic ways to define a vocabulary:
1. You can define all terms manually.
2. You can load all terms from a specified namespace in a given dataset or
graph.
Either way, you'll first have to define a new module for your vocabulary:
defmodule ExampleVocab do
use RDF.Vocabulary, base_uri: "http://www.example.com/ns/"
# Your term definitions
end
The `base_uri` argument with the URI prefix of all the terms in the defined
vocabulary is required and expects a valid URI ending with either a `"/"` or
a `"#"`.
## Reflection
`__base_uri__` and `__terms__` ...
"""
defmodule InvalidBaseURIError, do: defexception [:message]
defmodule UndefinedTermError, do: defexception [:message]
defmodule InvalidTermError, do: defexception [:message]
defmacro __using__(opts) do
quote bind_quoted: [opts: opts], unquote: true do
import unquote(__MODULE__)
# TODO: @terms should be a MapSet for faster term lookup
Module.register_attribute __MODULE__, :terms, accumulate: true
@before_compile unquote(__MODULE__)
@strict Keyword.get(opts, :strict, false)
with {:ok, base_uri} <- Keyword.fetch(opts, :base_uri),
true <- base_uri |> String.ends_with?(["/", "#"]) do
@base_uri base_uri
else
:error ->
raise InvalidBaseURIError, "required base_uri missing"
false ->
raise InvalidBaseURIError,
"a base_uri without a trailing '/' or '#' is invalid"
end
def __base_uri__, do: @base_uri
def __strict__, do: @strict
unless @strict do
def unquote(:"$handle_undefined_function")(term, args) do
RDF.Vocabulary.term_to_uri(@base_uri, term)
end
end
end
end
defmacro __before_compile__(_env) do
quote do
def __terms__, do: @terms
if @strict do
def uri(term) do
if Enum.member?(@terms, term) do
RDF.Vocabulary.term_to_uri(@base_uri, term)
else
raise UndefinedTermError,
"undefined term #{term} in strict vocabulary #{__MODULE__}"
end
end
else
def uri(term) do
RDF.Vocabulary.term_to_uri(@base_uri, term)
end
end
end
end
@doc """
Defines an URI via a term concatenated to the `base_uri` of the vocabulary
module.
"""
defmacro defuri(term) when is_atom(term) do
quote do
@terms unquote(term)
if Atom.to_string(unquote(term)) =~ ~r/^\p{Ll}/u do
# TODO: the URI should be built at compile-time
# uri = RDF.Vocabulary.term_to_uri(@base_uri, unquote(term))
def unquote(term)() do
URI.parse(__base_uri__ <> to_string(unquote(term)))
end
end
end
end
@doc false
def term_to_uri(base_uri, term) do
URI.parse(base_uri <> to_string(term))
end
@doc false
def __uri__(uri = %URI{}), do: uri
def __uri__(namespaced_atom) when is_atom(namespaced_atom) do
case namespaced_atom
|> to_string
|> String.reverse
|> String.split(".", parts: 2)
|> Enum.map(&String.reverse/1)
|> Enum.map(&String.to_existing_atom/1) do
[term, vocabulary] -> vocabulary.uri(term)
_ -> raise InvalidTermError, ""
end
end
end

View file

@ -1,2 +0,0 @@
defmodule RDF.Core do
end

View file

@ -1,8 +0,0 @@
defmodule RDF.CoreTest do
use ExUnit.Case
doctest RDF.Core
test "the truth" do
assert 1 + 1 == 2
end
end

View file

@ -0,0 +1,202 @@
defmodule RDF.DescriptionTest do
use ExUnit.Case
doctest RDF.Description
alias RDF.Description
import RDF, only: [uri: 1, literal: 1, bnode: 1]
defmodule EX, do:
use RDF.Vocabulary, base_uri: "http://example.com/description/"
def description, do: Description.new(EX.Subject)
def description_of_subject(%Description{subject: subject}, subject), do: true
def description_of_subject(_, _), do: false
def empty_description(%Description{predications: predications}) do
predications == %{}
end
def description_includes_predication(desc, {predicate, object}) do
desc.predications
|> Map.get(predicate, %{})
|> Enum.member?({object, nil})
end
describe "creating an empty description" do
test "with a subject URI" do
assert description_of_subject(Description.new(URI.parse("http://example.com/description/subject")),
URI.parse("http://example.com/description/subject"))
end
test "with a raw subject URI string" do
assert description_of_subject(Description.new("http://example.com/description/subject"),
URI.parse("http://example.com/description/subject"))
end
test "with an unresolved subject URI term atom" do
assert description_of_subject(Description.new(EX.Bar), uri(EX.Bar))
end
test "with a BlankNode subject" do
assert description_of_subject(Description.new(bnode(:foo)), bnode(:foo))
end
end
test "creating a description with a single initial triple" do
desc = Description.new({EX.Subject, EX.predicate, EX.Object})
assert description_of_subject(desc, uri(EX.Subject))
assert description_includes_predication(desc, {EX.predicate, uri(EX.Object)})
desc = Description.new({EX.Subject, EX.predicate, 42})
assert description_of_subject(desc, uri(EX.Subject))
assert description_includes_predication(desc, {EX.predicate, literal(42)})
end
test "creating a description with a list of initial triples" do
desc = Description.new([{EX.Subject, EX.predicate1, EX.Object1},
{EX.Subject, EX.predicate2, EX.Object2}])
assert description_of_subject(desc, uri(EX.Subject))
assert description_includes_predication(desc, {EX.predicate1, uri(EX.Object1)})
assert description_includes_predication(desc, {EX.predicate2, uri(EX.Object2)})
end
describe "adding triples" do
test "a predicate-object-pair of proper RDF terms" do
assert Description.add(description, {EX.predicate, uri(EX.Object)})
|> description_includes_predication({EX.predicate, uri(EX.Object)})
end
test "a predicate-object-pair of convertible RDF terms" do
assert Description.add(description,
{"http://example.com/description/predicate", uri(EX.Object)})
|> description_includes_predication({EX.predicate, uri(EX.Object)})
assert Description.add(description,
{"http://example.com/description/predicate", 42})
|> description_includes_predication({EX.predicate, literal(42)})
# TODO: Test a url-string as object ...
assert Description.add(description,
{"http://example.com/description/predicate", bnode(:foo)})
|> description_includes_predication({EX.predicate, bnode(:foo)})
end
test "a proper triple" do
assert Description.add(description,
{uri(EX.Subject), EX.predicate, uri(EX.Object)})
|> description_includes_predication({EX.predicate, uri(EX.Object)})
assert Description.add(description,
{uri(EX.Subject), EX.predicate, literal(42)})
|> description_includes_predication({EX.predicate, literal(42)})
assert Description.add(description,
{uri(EX.Subject), EX.predicate, bnode(:foo)})
|> description_includes_predication({EX.predicate, bnode(:foo)})
end
test "add ignores triples not about the subject of the Description struct" do
assert empty_description(
Description.add(description, {EX.Other, EX.predicate, uri(EX.Object)}))
end
test "a list of predicate-object-pairs" do
desc = Description.add(description,
[{EX.predicate, EX.Object1}, {EX.predicate, EX.Object2}])
assert description_includes_predication(desc, {EX.predicate, uri(EX.Object1)})
assert description_includes_predication(desc, {EX.predicate, uri(EX.Object2)})
end
test "a list of triples" do
desc = Description.add(description, [
{EX.Subject, EX.predicate1, EX.Object1},
{EX.Subject, EX.predicate2, EX.Object2}
])
assert description_includes_predication(desc, {EX.predicate1, uri(EX.Object1)})
assert description_includes_predication(desc, {EX.predicate2, uri(EX.Object2)})
end
test "a list of mixed triples and predicate-object-pairs" do
desc = Description.add(description, [
{EX.predicate, EX.Object1},
{EX.Subject, EX.predicate, EX.Object2},
{EX.Other, EX.predicate, EX.Object3}
])
assert description_of_subject(desc, uri(EX.Subject))
assert description_includes_predication(desc, {EX.predicate, uri(EX.Object1)})
assert description_includes_predication(desc, {EX.predicate, uri(EX.Object2)})
refute description_includes_predication(desc, {EX.predicate, uri(EX.Object3)})
end
test "duplicates are ignored" do
desc = Description.add(description, {EX.predicate, EX.Object})
assert Description.add(desc, {EX.predicate, EX.Object}) == desc
assert Description.add(desc, {EX.Subject, EX.predicate, EX.Object}) == desc
desc = Description.add(description, {EX.predicate, 42})
assert Description.add(desc, {EX.predicate, literal(42)}) == desc
end
test "non-convertible Triple elements are causing an error" do
assert_raise RDF.InvalidURIError, fn ->
Description.add(description, {"not a URI", uri(EX.Object)})
end
assert_raise RDF.Literal.InvalidLiteralError, fn ->
Description.add(description, {EX.prop, self})
end
end
end
test "pop a triple" do
assert Description.pop(Description.new(EX.S)) == {nil, Description.new(EX.S)}
{triple, desc} = Description.new({EX.S, EX.p, EX.O}) |> Description.pop
assert {uri(EX.S), uri(EX.p), uri(EX.O)} == triple
assert Enum.count(desc.predications) == 0
{{subject, predicate, _}, desc} =
Description.new([{EX.S, EX.p, EX.O1}, {EX.S, EX.p, EX.O2}])
|> Description.pop
assert {subject, predicate} == {uri(EX.S), uri(EX.p)}
assert Enum.count(desc.predications) == 1
{{subject, _, _}, desc} =
Description.new([{EX.S, EX.p1, EX.O1}, {EX.S, EX.p2, EX.O2}])
|> Description.pop
assert subject == uri(EX.S)
assert Enum.count(desc.predications) == 1
end
describe "Enumerable implementation" do
test "Enum.count" do
assert Enum.count(Description.new EX.foo) == 0
assert Enum.count(Description.new {EX.S, EX.p, EX.O}) == 1
assert Enum.count(Description.new [{EX.S, EX.p, EX.O1}, {EX.S, EX.p, EX.O2}]) == 2
end
test "Enum.member?" do
refute Enum.member?(Description.new(EX.S), {uri(EX.S), EX.p, uri(EX.O)})
assert Enum.member?(Description.new({EX.S, EX.p, EX.O}), {EX.S, EX.p, EX.O})
desc = Description.new([
{EX.Subject, EX.predicate1, EX.Object1},
{EX.Subject, EX.predicate2, EX.Object2},
{EX.predicate2, EX.Object3}])
assert Enum.member?(desc, {EX.Subject, EX.predicate1, EX.Object1})
assert Enum.member?(desc, {EX.Subject, EX.predicate2, EX.Object2})
assert Enum.member?(desc, {EX.Subject, EX.predicate2, EX.Object3})
refute Enum.member?(desc, {EX.Subject, EX.predicate1, EX.Object2})
end
test "Enum.reduce" do
desc = Description.new([
{EX.Subject, EX.predicate1, EX.Object1},
{EX.Subject, EX.predicate2, EX.Object2},
{EX.predicate2, EX.Object3}])
assert desc == Enum.reduce(desc, description,
fn(triple, acc) -> acc |> Description.add(triple) end)
end
end
end

180
test/unit/graph_test.exs Normal file
View file

@ -0,0 +1,180 @@
defmodule RDF.GraphTest do
use ExUnit.Case
doctest RDF.Graph
alias RDF.Graph
import RDF, only: [uri: 1]
defmodule EX, do:
use RDF.Vocabulary, base_uri: "http://example.com/graph/"
def graph, do: unnamed_graph
def unnamed_graph, do: Graph.new
def named_graph(name \\ EX.GraphName), do: Graph.new(name)
def unnamed_graph?(%Graph{name: nil}), do: true
def unnamed_graph?(_), do: false
def named_graph?(%Graph{name: %URI{}}), do: true
def named_graph?(_), do: false
def named_graph?(%Graph{name: name}, name), do: true
def named_graph?(_, _), do: false
def empty_graph?(%Graph{descriptions: descriptions}), do: descriptions == %{}
def graph_includes_statement?(graph, statement = {subject, _, _}) do
graph.descriptions
|> Map.get(uri(subject), %{})
|> Enum.member?(statement)
end
describe "construction" do
test "creating an empty unnamed graph" do
assert unnamed_graph?(unnamed_graph)
end
test "creating an empty graph with a proper graph name" do
refute unnamed_graph?(named_graph)
assert named_graph?(named_graph)
end
test "creating an empty graph with a convertible graph name" do
assert named_graph("http://example.com/graph/GraphName")
|> named_graph?(uri("http://example.com/graph/GraphName"))
assert named_graph(EX.Foo) |> named_graph?(uri(EX.Foo))
end
test "creating an unnamed graph with an initial triple" do
g = Graph.new({EX.Subject, EX.predicate, EX.Object})
assert unnamed_graph?(g)
assert graph_includes_statement?(g, {EX.Subject, EX.predicate, EX.Object})
end
test "creating a named graph with an initial triple" do
g = Graph.new(EX.GraphName, {EX.Subject, EX.predicate, EX.Object})
assert named_graph?(g, uri(EX.GraphName))
assert graph_includes_statement?(g, {EX.Subject, EX.predicate, EX.Object})
end
test "creating an unnamed graph with a list of initial triples" do
g = Graph.new([{EX.Subject1, EX.predicate1, EX.Object1},
{EX.Subject2, EX.predicate2, EX.Object2}])
assert unnamed_graph?(g)
assert graph_includes_statement?(g, {EX.Subject1, EX.predicate1, EX.Object1})
assert graph_includes_statement?(g, {EX.Subject2, EX.predicate2, EX.Object2})
end
test "creating a named graph with a list of initial triples" do
g = Graph.new(EX.GraphName, [{EX.Subject, EX.predicate1, EX.Object1},
{EX.Subject, EX.predicate2, EX.Object2}])
assert named_graph?(g, uri(EX.GraphName))
assert graph_includes_statement?(g, {EX.Subject, EX.predicate1, EX.Object1})
assert graph_includes_statement?(g, {EX.Subject, EX.predicate2, EX.Object2})
end
end
describe "adding triples" do
test "a proper triple" do
assert Graph.add(graph, {uri(EX.Subject), EX.predicate, uri(EX.Object)})
|> graph_includes_statement?({EX.Subject, EX.predicate, EX.Object})
end
test "a convertiable triple" do
assert Graph.add(graph,
{"http://example.com/graph/Subject", EX.predicate, EX.Object})
|> graph_includes_statement?({EX.Subject, EX.predicate, EX.Object})
end
test "a list of triples" do
g = Graph.add(graph, [
{EX.Subject1, EX.predicate1, EX.Object1},
{EX.Subject1, EX.predicate2, EX.Object2},
{EX.Subject3, EX.predicate3, EX.Object3}
])
assert graph_includes_statement?(g, {EX.Subject1, EX.predicate1, EX.Object1})
assert graph_includes_statement?(g, {EX.Subject1, EX.predicate2, EX.Object2})
assert graph_includes_statement?(g, {EX.Subject3, EX.predicate3, EX.Object3})
end
test "duplicates are ignored" do
g = Graph.add(graph, {EX.Subject, EX.predicate, EX.Object})
assert Graph.add(g, {EX.Subject, EX.predicate, EX.Object}) == g
end
test "non-convertible Triple elements are causing an error" do
assert_raise RDF.InvalidURIError, fn ->
Graph.add(graph, {"not a URI", EX.predicate, uri(EX.Object)})
end
assert_raise RDF.Literal.InvalidLiteralError, fn ->
Graph.add(graph, {EX.Subject, EX.prop, self})
end
end
end
test "subject_count" do
g = Graph.add(graph, [
{EX.Subject1, EX.predicate1, EX.Object1},
{EX.Subject1, EX.predicate2, EX.Object2},
{EX.Subject3, EX.predicate3, EX.Object3}
])
assert Graph.subject_count(g) == 2
end
test "pop a triple" do
assert Graph.pop(Graph.new) == {nil, Graph.new}
{triple, graph} = Graph.new({EX.S, EX.p, EX.O}) |> Graph.pop
assert {uri(EX.S), uri(EX.p), uri(EX.O)} == triple
assert Enum.count(graph.descriptions) == 0
{{subject, predicate, _}, graph} =
Graph.new([{EX.S, EX.p, EX.O1}, {EX.S, EX.p, EX.O2}])
|> Graph.pop
assert {subject, predicate} == {uri(EX.S), uri(EX.p)}
assert Enum.count(graph.descriptions) == 1
{{subject, _, _}, graph} =
Graph.new([{EX.S, EX.p1, EX.O1}, {EX.S, EX.p2, EX.O2}])
|> Graph.pop
assert subject == uri(EX.S)
assert Enum.count(graph.descriptions) == 1
end
describe "Enumerable implementation" do
test "Enum.count" do
assert Enum.count(Graph.new EX.foo) == 0
assert Enum.count(Graph.new {EX.S, EX.p, EX.O}) == 1
assert Enum.count(Graph.new [{EX.S, EX.p, EX.O1}, {EX.S, EX.p, EX.O2}]) == 2
g = Graph.add(graph, [
{EX.Subject1, EX.predicate1, EX.Object1},
{EX.Subject1, EX.predicate2, EX.Object2},
{EX.Subject3, EX.predicate3, EX.Object3}
])
assert Enum.count(g) == 3
end
test "Enum.member?" do
refute Enum.member?(Graph.new, {uri(EX.S), EX.p, uri(EX.O)})
assert Enum.member?(Graph.new({EX.S, EX.p, EX.O}), {EX.S, EX.p, EX.O})
g = Graph.add(graph, [
{EX.Subject1, EX.predicate1, EX.Object1},
{EX.Subject1, EX.predicate2, EX.Object2},
{EX.Subject3, EX.predicate3, EX.Object3}
])
assert Enum.member?(g, {EX.Subject1, EX.predicate1, EX.Object1})
assert Enum.member?(g, {EX.Subject1, EX.predicate2, EX.Object2})
assert Enum.member?(g, {EX.Subject3, EX.predicate3, EX.Object3})
end
test "Enum.reduce" do
g = Graph.add(graph, [
{EX.Subject1, EX.predicate1, EX.Object1},
{EX.Subject1, EX.predicate2, EX.Object2},
{EX.Subject3, EX.predicate3, EX.Object3}
])
assert g == Enum.reduce(g, graph,
fn(triple, acc) -> acc |> Graph.add(triple) end)
end
end
end

View file

@ -0,0 +1,27 @@
defmodule RDF.LiteralTest do
use ExUnit.Case
doctest RDF.Literal
alias RDF.{Literal, XSD}
describe "construction by type inference" do
test "creating an integer by type inference" do
int_literal = Literal.new(42)
assert int_literal.value == 42
assert int_literal.datatype == XSD.integer
end
test "creating a boolean by type inference" do
int_literal = Literal.new(true)
assert int_literal.value == true
assert int_literal.datatype == XSD.boolean
int_literal = Literal.new(false)
assert int_literal.value == false
assert int_literal.datatype == XSD.boolean
end
end
end

7
test/unit/rdf_test.exs Normal file
View file

@ -0,0 +1,7 @@
defmodule RDF.CoreTest do
use ExUnit.Case
doctest RDF
# alias RDF.{Triple, Literal, BlankNode}
end

View file

@ -0,0 +1,8 @@
defmodule RDF.TripleTest do
use ExUnit.Case
doctest RDF.Triple
# alias RDF.{Triple, Literal, BlankNode}
end

View file

@ -0,0 +1,127 @@
defmodule RDF.VocabularyTest do
use ExUnit.Case
doctest RDF.Vocabulary
defmodule StrictVocab, do:
use RDF.Vocabulary, base_uri: "http://example.com/strict_vocab/", strict: true
defmodule NonStrictVocab, do:
use RDF.Vocabulary, base_uri: "http://example.com/non_strict_vocab/"
defmodule HashVocab, do:
use RDF.Vocabulary, base_uri: "http://example.com/hash_vocab#"
defmodule SlashVocab, do:
use RDF.Vocabulary, base_uri: "http://example.com/slash_vocab/"
describe "base_uri" do
test "__base_uri__ returns the base_uri" do
assert SlashVocab.__base_uri__ == "http://example.com/slash_vocab/"
assert HashVocab.__base_uri__ == "http://example.com/hash_vocab#"
end
test "a Vocabulary can't be defined without a base_uri" do
assert_raise RDF.Vocabulary.InvalidBaseURIError, fn ->
defmodule TestBaseURIVocab3, do: use RDF.Vocabulary
end
end
test "it is not valid, when it doesn't end with '/' or '#'" do
assert_raise RDF.Vocabulary.InvalidBaseURIError, fn ->
defmodule TestBaseURIVocab4, do:
use RDF.Vocabulary, base_uri: "http://example.com/base_uri4"
end
end
@tag skip: "TODO: implement proper URI validation"
test "it is not valid, when it isn't a valid URI according to RFC 3986" do
assert_raise RDF.Vocabulary.InvalidBaseURIError, fn ->
defmodule TestBaseURIVocab5, do: use RDF.Vocabulary, base_uri: "foo/"
end
assert_raise RDF.Vocabulary.InvalidBaseURIError, fn ->
defmodule TestBaseURIVocab6, do: use RDF.Vocabulary, base_uri: :foo
end
end
end
test "__terms__ returns a list of all defined terms" do
defmodule VocabWithSomeTerms do
use RDF.Vocabulary, base_uri: "http://example.com/test5/"
defuri :prop
defuri :Foo
end
assert length(VocabWithSomeTerms.__terms__) == 2
assert :prop in VocabWithSomeTerms.__terms__
assert :Foo in VocabWithSomeTerms.__terms__
end
@tag skip: "TODO: Can we make RDF.uri(:foo) an undefined function call with guards or in another way?"
test "resolving an unqualified term raises an error" do
assert_raise UndefinedFunctionError, fn -> RDF.uri(:foo) end
# or: assert_raise InvalidTermError, fn -> RDF.uri(:foo) end
end
test "resolving undefined terms of a non-strict vocabulary" do
assert NonStrictVocab.foo ==
URI.parse("http://example.com/non_strict_vocab/foo")
assert RDF.uri(NonStrictVocab.Bar) ==
URI.parse("http://example.com/non_strict_vocab/Bar")
end
test "resolving undefined terms of a strict vocabulary" do
assert_raise UndefinedFunctionError, fn -> StrictVocab.foo end
assert_raise RDF.Vocabulary.UndefinedTermError, fn ->
RDF.uri(StrictVocab.Foo) end
end
test "resolving manually defined lowercase terms on a non-strict vocabulary" do
defmodule TestManualVocab1 do
use RDF.Vocabulary, base_uri: "http://example.com/manual_vocab1/"
defuri :prop
end
assert TestManualVocab1.prop ==
URI.parse("http://example.com/manual_vocab1/prop")
assert RDF.uri(TestManualVocab1.prop) ==
URI.parse("http://example.com/manual_vocab1/prop")
end
test "resolving manually defined uppercase terms on a non-strict vocabulary" do
defmodule TestManualVocab2 do
use RDF.Vocabulary, base_uri: "http://example.com/manual_vocab2/"
defuri :Foo
end
assert RDF.uri(TestManualVocab2.Foo) ==
URI.parse("http://example.com/manual_vocab2/Foo")
end
test "resolving manually defined lowercase terms on a strict vocabulary" do
defmodule TestManualStrictVocab1 do
use RDF.Vocabulary,
base_uri: "http://example.com/manual_strict_vocab1/", strict: true
defuri :prop
end
assert TestManualStrictVocab1.prop ==
URI.parse("http://example.com/manual_strict_vocab1/prop")
assert RDF.uri(TestManualStrictVocab1.prop) ==
URI.parse("http://example.com/manual_strict_vocab1/prop")
end
test "resolving manually defined uppercase terms on a strict vocabulary" do
defmodule TestManualStrictVocab2 do
use RDF.Vocabulary,
base_uri: "http://example.com/manual_strict_vocab2/", strict: true
defuri :Foo
end
assert RDF.uri(TestManualStrictVocab2.Foo) ==
URI.parse("http://example.com/manual_strict_vocab2/Foo")
end
end