core: aliases for terms of RDF.Vocabulary.Namespaces

and some refactorings
This commit is contained in:
Marcel Otto 2017-05-26 13:54:04 +02:00
parent 385183169d
commit af751dd2bb
3 changed files with 279 additions and 97 deletions

View file

@ -35,6 +35,14 @@ defmodule RDF.Namespace.InvalidVocabBaseURIError do
defexception [:message]
end
defmodule RDF.Namespace.InvalidTermError do
defexception [:message]
end
defmodule RDF.Namespace.InvalidAliasError do
defexception [:message]
end
defmodule RDF.Namespace.UndefinedTermError do
defexception [:message]
end

View file

@ -53,12 +53,11 @@ defmodule RDF.Vocabulary.Namespace do
"""
defmacro defvocab(name, opts) do
base_uri = base_uri!(opts)
file = file!(opts)
terms = terms!(opts)
strict = Keyword.get(opts, :strict, true)
file = filename!(opts)
terms = terms!(opts) |> term_mapping!(opts)
strict = strict?(opts)
case_separated_terms = group_terms_by_case(terms)
lowercased_terms = Map.get(case_separated_terms, :lowercased, [])
capitalized_terms = Map.get(case_separated_terms, :capitalized, [])
lowercased_terms = Map.get(case_separated_terms, :lowercased, %{})
quote do
vocabdoc = Module.delete_attribute(__MODULE__, :vocabdoc)
@ -78,27 +77,28 @@ defmodule RDF.Vocabulary.Namespace do
@strict unquote(strict)
def __strict__, do: @strict
@lowercased_terms unquote(lowercased_terms |> Enum.map(&String.to_atom/1))
@capitalized_terms unquote(capitalized_terms |> Enum.map(&String.to_atom/1))
@terms @lowercased_terms ++ @capitalized_terms
def __terms__, do: @terms
@terms unquote(Macro.escape(terms))
def __terms__, do: @terms |> Map.keys
define_vocab_terms unquote(lowercased_terms), unquote(base_uri)
if @strict do
def __resolve_term__(term) do
if Enum.member?(@capitalized_terms, term) do
term_to_uri(@base_uri, term)
else
case @terms[term] do
nil ->
if @strict do
raise RDF.Namespace.UndefinedTermError,
"undefined term #{term} in strict vocabulary #{__MODULE__}"
end
end
else
def __resolve_term__(term) do
term_to_uri(@base_uri, term)
end
true ->
term_to_uri(@base_uri, term)
original_term ->
term_to_uri(@base_uri, original_term)
end
end
if not @strict do
def unquote(:"$handle_undefined_function")(term, []) do
term_to_uri(@base_uri, term)
end
@ -113,6 +113,52 @@ defmodule RDF.Vocabulary.Namespace do
end
end
defmacro define_vocab_terms(terms, base_uri) do
terms
|> Stream.map(fn
{term, true} -> {term, term}
{term, original_term} -> {term, original_term}
end)
|> Enum.map(fn {term, uri_suffix} ->
# TODO: Why does this way of precompiling the URI not work? We're getting an "invalid quoted expression: %URI{...}"
# uri = term_to_uri(base_uri, term)
# quote bind_quoted: [uri: Macro.escape(uri), term: String.to_atom(term)] do
## @doc "<#{@tmp_uri}>"
# def unquote(term)() do
# unquote(uri)
# end
# end
# Temporary workaround:
quote do
@tmp_uri term_to_uri(@base_uri, unquote(uri_suffix))
@doc "<#{@tmp_uri}>"
def unquote(term)(), do: @tmp_uri
@doc "`RDF.Description` builder for <#{@tmp_uri}>"
def unquote(term)(subject, object) do
RDF.Description.new(subject, @tmp_uri, object)
end
# Is there a better way to support multiple objects via arguments?
@doc false
def unquote(term)(subject, o1, o2),
do: unquote(term)(subject, [o1, o2])
@doc false
def unquote(term)(subject, o1, o2, o3),
do: unquote(term)(subject, [o1, o2, o3])
@doc false
def unquote(term)(subject, o1, o2, o3, o4),
do: unquote(term)(subject, [o1, o2, o3, o4])
@doc false
def unquote(term)(subject, o1, o2, o3, o4, o5),
do: unquote(term)(subject, [o1, o2, o3, o4, o5])
end
end)
end
defp strict?(opts),
do: Keyword.get(opts, :strict, true)
defp base_uri!(opts) do
base_uri = Keyword.fetch!(opts, :base_uri)
unless is_binary(base_uri) and String.ends_with?(base_uri, ["/", "#"]) do
@ -126,91 +172,88 @@ defmodule RDF.Vocabulary.Namespace do
def terms!(opts) do
cond do
Keyword.has_key?(opts, :file) ->
opts
|> Keyword.delete(:file)
|> Keyword.put(:data, load_file(file!(opts)))
|> terms!
data = Keyword.get(opts, :data) ->
# TODO: support also RDF.Datasets ...
data = unless match?(%RDF.Graph{}, data) do
# TODO: find an alternative to Code.eval_quoted
{data, _ } = Code.eval_quoted(data, [], data_env())
data
else
data
end
data_vocab_terms(data, Keyword.fetch!(opts, :base_uri))
filename!(opts)
|> load_file
|> terms_from_rdf_data!(opts)
rdf_data = Keyword.get(opts, :data) ->
terms_from_rdf_data!(rdf_data, opts)
terms = Keyword.get(opts, :terms) ->
# TODO: find an alternative to Code.eval_quoted - We want to support that the terms can be given as sigils ...
{terms, _ } = Code.eval_quoted(terms, [], data_env())
{terms, _ } = Code.eval_quoted(terms, [], rdf_data_env())
terms
|> Enum.map(&to_string/1)
|> Enum.map(fn
term when is_atom(term) -> term
term when is_binary(term) -> String.to_atom(term)
term ->
raise RDF.Namespace.InvalidTermError,
"'#{term}' is not a valid vocabulary term"
end)
true ->
raise KeyError, key: ~w[terms data file], term: opts
end
end
def file!(opts) do
if file = Keyword.get(opts, :file) do
# TODO: support also RDF.Datasets ...
defp terms_from_rdf_data!(%RDF.Graph{} = rdf_data, opts) do
rdf_data_vocab_terms(rdf_data, Keyword.fetch!(opts, :base_uri))
end
defp terms_from_rdf_data!(rdf_data, opts) do
# TODO: find an alternative to Code.eval_quoted
{rdf_data, _} = Code.eval_quoted(rdf_data, [], rdf_data_env())
terms_from_rdf_data!(rdf_data, opts)
end
def term_mapping!(terms, opts) do
terms = Map.new terms, fn
term when is_atom(term) -> {term, true}
term -> {String.to_atom(term), true}
end
Keyword.get(opts, :alias, [])
|> Enum.reduce(terms, fn {alias, original_term}, terms ->
term = String.to_atom(original_term)
cond do
File.exists?(file) ->
file
File.exists?(expanded_file = Path.expand(file, @vocabs_dir)) ->
expanded_file
Map.get(terms, alias) == true ->
raise RDF.Namespace.InvalidAliasError,
"alias '#{alias}' already defined"
strict?(opts) and not Map.has_key?(terms, term) ->
raise RDF.Namespace.InvalidAliasError,
"term '#{original_term}' is not a term in this vocabulary"
Map.get(terms, term, true) != true ->
raise RDF.Namespace.InvalidAliasError,
"'#{original_term}' is already an alias"
true ->
raise File.Error, path: file, action: "find", reason: :enoent
Map.put(terms, alias, to_string(original_term))
end
end)
end
def filename!(opts) do
if filename = Keyword.get(opts, :file) do
cond do
File.exists?(filename) ->
filename
File.exists?(expanded_filename = Path.expand(filename, @vocabs_dir)) ->
expanded_filename
true ->
raise File.Error, path: filename, action: "find", reason: :enoent
end
end
end
defp load_file(file) do
RDF.NTriples.read_file!(file)
RDF.NTriples.read_file!(file) # TODO: support other formats
end
defp data_env do
defp rdf_data_env do
__ENV__
end
defmacro define_vocab_terms(terms, base_uri) do
Enum.map terms, fn term ->
name = String.to_atom(term)
# TODO: Why does this way of precompiling the URI not work? We're getting an "invalid quoted expression: %URI{...}"
# uri = term_to_uri(base_uri, term)
# quote bind_quoted: [uri: Macro.escape(uri), term: String.to_atom(term)] do
## @doc "<#{@tmp_uri}>"
# def unquote(term)() do
# unquote(uri)
# end
# end
# Temporary workaround:
quote do
@tmp_uri term_to_uri(@base_uri, unquote(term))
@doc "<#{@tmp_uri}>"
def unquote(name)(), do: @tmp_uri
@doc "`RDF.Description` builder for <#{@tmp_uri}>"
def unquote(name)(subject, object) do
RDF.Description.new(subject, @tmp_uri, object)
end
@doc false
def unquote(name)(subject, o1, o2),
do: unquote(name)(subject, [o1, o2])
@doc false
def unquote(name)(subject, o1, o2, o3),
do: unquote(name)(subject, [o1, o2, o3])
@doc false
def unquote(name)(subject, o1, o2, o3, o4),
do: unquote(name)(subject, [o1, o2, o3, o4])
@doc false
def unquote(name)(subject, o1, o2, o3, o4, o5),
do: unquote(name)(subject, [o1, o2, o3, o4, o5])
end
end
end
defp data_vocab_terms(data, base_uri) do
defp rdf_data_vocab_terms(data, base_uri) do
data
|> RDF.Graph.resources # TODO: support also RDF.Datasets ...
# filter URIs
@ -220,15 +263,20 @@ defmodule RDF.Vocabulary.Namespace do
end)
|> Stream.map(&to_string/1)
|> Stream.map(&(strip_base_uri(&1, base_uri)))
|> Enum.filter(&vocab_term?/1)
|> Stream.filter(&vocab_term?/1)
|> Enum.map(&String.to_atom/1)
end
defp group_terms_by_case(terms) do
Enum.group_by terms, fn term ->
terms
|> Enum.group_by(fn {term, _} ->
if lowercase?(term),
do: :lowercased,
else: :capitalized
end
end)
|> Map.new(fn {group, term_mapping} ->
{group, Map.new(term_mapping)}
end)
end
defp lowercase?(term) when is_atom(term),

View file

@ -36,6 +36,27 @@ defmodule RDF.Vocabulary.NamespaceTest do
base_uri: "http://example.com/example4#",
terms: ~w[foo Bar],
strict: false
defvocab Example5,
base_uri: "http://example.com/example5#",
terms: ~w[term1 Term2 Term-3 term-4],
alias: [
Term1: "term1",
term2: "Term2",
Term3: "Term-3",
term4: "term-4",
]
defvocab Example6,
base_uri: "http://example.com/example6#",
terms: ~w[],
alias: [
Term1: "term1",
term2: "Term2",
Term3: "Term-3",
term4: "term-4",
],
strict: false
end
@ -95,6 +116,60 @@ defmodule RDF.Vocabulary.NamespaceTest do
end
end
test "when trying to map an already existing term, an error is raised" do
assert_raise RDF.Namespace.InvalidAliasError, fn ->
defmodule BadNS6 do
use RDF.Vocabulary.Namespace
defvocab Example,
base_uri: "http://example.com/ex6#",
terms: ~w[foo bar],
alias: [foo: "bar"]
end
end
end
test "when strict and trying to map to a term not in the vocabulary, an error is raised" do
assert_raise RDF.Namespace.InvalidAliasError, fn ->
defmodule BadNS7 do
use RDF.Vocabulary.Namespace
defvocab Example,
base_uri: "http://example.com/ex7#",
terms: ~w[],
alias: [foo: "bar"]
end
end
end
test "when defining an alias for an alias, an error is raised" do
assert_raise RDF.Namespace.InvalidAliasError, fn ->
defmodule BadNS8 do
use RDF.Vocabulary.Namespace
defvocab Example,
base_uri: "http://example.com/ex8#",
terms: ~w[bar],
alias: [foo: "bar", baz: "foo"]
end
end
end
test "defining multiple aliases for a term" do
defmodule BadNS9 do
use RDF.Vocabulary.Namespace
defvocab Example,
base_uri: "http://example.com/ex8#",
terms: ~w[bar Bar],
alias: [foo: "bar", baz: "bar",
Foo: "Bar", Baz: "Bar"]
end
alias BadNS9.Example
assert Example.foo == Example.baz
assert RDF.uri(Example.foo) == RDF.uri(Example.baz)
end
end
test "__base_uri__ returns the base_uri" do
@ -105,20 +180,38 @@ defmodule RDF.Vocabulary.NamespaceTest do
assert SlashVocab.__base_uri__ == "http://example.com/example2/"
end
test "__terms__ returns a list of all defined terms" do
alias TestNS.Example1
describe "__terms__" do
alias TestNS.{Example1, Example5}
test "includes all defined terms" do
assert length(Example1.__terms__) == 2
assert :foo in Example1.__terms__
assert :Bar in Example1.__terms__
end
test "includes aliases" do
assert length(Example5.__terms__) == 8
assert :term1 in Example5.__terms__
assert :Term1 in Example5.__terms__
assert :term2 in Example5.__terms__
assert :Term2 in Example5.__terms__
assert :Term3 in Example5.__terms__
assert :term4 in Example5.__terms__
assert :"Term-3" in Example5.__terms__
assert :"term-4" in Example5.__terms__
end
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
assert_raise RDF.Namespace.UndefinedTermError, fn -> RDF.uri(:foo) end
end
describe "term resolution in a strict vocab namespace" do
alias TestNS.{Example1, Example2, Example3}
test "undefined terms" do
assert_raise UndefinedFunctionError, fn ->
Example1.undefined
@ -180,6 +273,39 @@ defmodule RDF.Vocabulary.NamespaceTest do
end
describe "term resolution of aliases on a strict vocabulary" do
alias TestNS.Example5
test "the alias resolves to the correct URI" do
assert RDF.uri(Example5.Term1) == URI.parse("http://example.com/example5#term1")
assert RDF.uri(Example5.term2) == URI.parse("http://example.com/example5#Term2")
assert RDF.uri(Example5.Term3) == URI.parse("http://example.com/example5#Term-3")
assert RDF.uri(Example5.term4) == URI.parse("http://example.com/example5#term-4")
end
test "the old term remains resolvable" do
assert RDF.uri(Example5.term1) == URI.parse("http://example.com/example5#term1")
assert RDF.uri(Example5.Term2) == URI.parse("http://example.com/example5#Term2")
end
end
describe "term resolution of aliases on a non-strict vocabulary" do
alias TestNS.Example6
test "the alias resolves to the correct URI" do
assert RDF.uri(Example6.Term1) == URI.parse("http://example.com/example6#term1")
assert RDF.uri(Example6.term2) == URI.parse("http://example.com/example6#Term2")
assert RDF.uri(Example6.Term3) == URI.parse("http://example.com/example6#Term-3")
assert RDF.uri(Example6.term4) == URI.parse("http://example.com/example6#term-4")
end
test "the old term remains resolvable" do
assert RDF.uri(Example6.term1) == URI.parse("http://example.com/example6#term1")
assert RDF.uri(Example6.Term2) == URI.parse("http://example.com/example6#Term2")
end
end
describe "Description DSL" do
alias TestNS.{EX, EXS}