core: basics of the RDF model
This commit is contained in:
parent
068539e825
commit
499714285b
17 changed files with 1268 additions and 10 deletions
101
lib/rdf.ex
Normal file
101
lib/rdf.ex
Normal 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
12
lib/rdf/blank_node.ex
Normal 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
126
lib/rdf/description.ex
Normal 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
97
lib/rdf/graph.ex
Normal 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
62
lib/rdf/literal.ex
Normal 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
87
lib/rdf/triple.ex
Normal 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
|
11
lib/rdf/vocabularies/rdfs.ex
Normal file
11
lib/rdf/vocabularies/rdfs.ex
Normal 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
|
55
lib/rdf/vocabularies/xsd.ex
Normal file
55
lib/rdf/vocabularies/xsd.ex
Normal 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
166
lib/rdf/vocabulary.ex
Normal 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
|
|
@ -1,2 +0,0 @@
|
|||
defmodule RDF.Core do
|
||||
end
|
|
@ -1,8 +0,0 @@
|
|||
defmodule RDF.CoreTest do
|
||||
use ExUnit.Case
|
||||
doctest RDF.Core
|
||||
|
||||
test "the truth" do
|
||||
assert 1 + 1 == 2
|
||||
end
|
||||
end
|
202
test/unit/description_test.exs
Normal file
202
test/unit/description_test.exs
Normal 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
180
test/unit/graph_test.exs
Normal 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
|
27
test/unit/literal_test.exs
Normal file
27
test/unit/literal_test.exs
Normal 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
7
test/unit/rdf_test.exs
Normal file
|
@ -0,0 +1,7 @@
|
|||
defmodule RDF.CoreTest do
|
||||
use ExUnit.Case
|
||||
doctest RDF
|
||||
|
||||
# alias RDF.{Triple, Literal, BlankNode}
|
||||
|
||||
end
|
8
test/unit/triple_test.exs
Normal file
8
test/unit/triple_test.exs
Normal file
|
@ -0,0 +1,8 @@
|
|||
defmodule RDF.TripleTest do
|
||||
use ExUnit.Case
|
||||
|
||||
doctest RDF.Triple
|
||||
|
||||
# alias RDF.{Triple, Literal, BlankNode}
|
||||
|
||||
end
|
127
test/unit/vocabulary_test.exs
Normal file
127
test/unit/vocabulary_test.exs
Normal 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
|
Loading…
Reference in a new issue