Add RDF.Namespace builder
This commit is contained in:
parent
ef626faf7d
commit
5919a9c93e
8 changed files with 407 additions and 3 deletions
|
@ -1,5 +1,7 @@
|
|||
locals_without_parens = [
|
||||
defvocab: 2,
|
||||
defnamespace: 2,
|
||||
defnamespace: 3,
|
||||
def_facet_constraint: 2,
|
||||
def_applicable_facet: 1,
|
||||
bgp: 1,
|
||||
|
|
|
@ -7,6 +7,10 @@ This project adheres to [Semantic Versioning](http://semver.org/) and
|
|||
|
||||
## Unreleased
|
||||
|
||||
### Added
|
||||
|
||||
- `RDF.Namespace` builders `defnamespace/3` and `create/4`
|
||||
|
||||
### Changed
|
||||
|
||||
- `RDF.Data.merge/2` and `RDF.Data.equal?/2` are now commutative, i.e. structs
|
||||
|
|
|
@ -8,6 +8,7 @@ defmodule RDF.Namespace do
|
|||
"""
|
||||
|
||||
alias RDF.IRI
|
||||
alias RDF.Namespace.Builder
|
||||
|
||||
import RDF.Guards
|
||||
|
||||
|
@ -19,7 +20,39 @@ defmodule RDF.Namespace do
|
|||
@doc """
|
||||
All terms of a `RDF.Namespace`.
|
||||
"""
|
||||
@callback __terms__() :: [atom]
|
||||
@callback __terms__ :: [atom]
|
||||
|
||||
@doc """
|
||||
All `RDF.IRI`s of a `RDF.Namespace`.
|
||||
"""
|
||||
@callback __iris__ :: [IRI.t()]
|
||||
|
||||
defmacro defnamespace({:__aliases__, _, [module]}, term_mapping, opts \\ []) do
|
||||
env = __CALLER__
|
||||
module = module(env, module)
|
||||
|
||||
quote do
|
||||
result =
|
||||
Builder.create!(
|
||||
unquote(module),
|
||||
unquote(term_mapping),
|
||||
unquote(Macro.escape(env)),
|
||||
unquote(opts)
|
||||
)
|
||||
|
||||
alias unquote(module)
|
||||
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
defdelegate create(module, term_mapping, location), to: Builder
|
||||
defdelegate create(module, term_mapping, location, opts), to: Builder
|
||||
defdelegate create!(module, term_mapping, location), to: Builder
|
||||
defdelegate create!(module, term_mapping, location, opts), to: Builder
|
||||
|
||||
@doc false
|
||||
def module(env, module), do: Module.concat(env.module, module)
|
||||
|
||||
@doc """
|
||||
Resolves a qualified term to a `RDF.IRI`.
|
||||
|
|
200
lib/rdf/namespace/builder.ex
Normal file
200
lib/rdf/namespace/builder.ex
Normal file
|
@ -0,0 +1,200 @@
|
|||
defmodule RDF.Namespace.Builder do
|
||||
@moduledoc false
|
||||
|
||||
alias RDF.Description
|
||||
|
||||
import RDF.Utils
|
||||
|
||||
@type term_mapping :: map | keyword
|
||||
|
||||
@spec create(module, term_mapping, Macro.Env.t() | keyword, keyword) ::
|
||||
{:ok, {:module, module(), binary(), term()}} | {:error, any}
|
||||
def create(module, term_mapping, location, opts \\ []) do
|
||||
moduledoc = opts[:moduledoc]
|
||||
|
||||
with {:ok, term_mapping} <- normalize_term_mapping(term_mapping) do
|
||||
property_terms = property_terms(term_mapping)
|
||||
|
||||
body =
|
||||
List.wrap(define_module_header(moduledoc)) ++
|
||||
Enum.map(property_terms, &define_property_function/1) ++
|
||||
List.wrap(
|
||||
Keyword.get_lazy(opts, :namespace_functions, fn ->
|
||||
define_namespace_functions(term_mapping)
|
||||
end)
|
||||
) ++
|
||||
List.wrap(Keyword.get(opts, :add_after))
|
||||
|
||||
{:ok, Module.create(module, body, location)}
|
||||
end
|
||||
end
|
||||
|
||||
@spec create!(module, term_mapping, Macro.Env.t() | keyword, keyword) ::
|
||||
{:module, module(), binary(), term()}
|
||||
def create!(module, term_mapping, location, opts \\ []) do
|
||||
case create(module, term_mapping, location, opts) do
|
||||
{:ok, result} -> result
|
||||
{:error, error} -> raise error
|
||||
end
|
||||
end
|
||||
|
||||
defp define_module_header(moduledoc) do
|
||||
quote do
|
||||
@moduledoc unquote(moduledoc)
|
||||
|
||||
@behaviour Elixir.RDF.Namespace
|
||||
|
||||
import Kernel,
|
||||
except: [
|
||||
min: 2,
|
||||
max: 2,
|
||||
div: 2,
|
||||
rem: 2,
|
||||
abs: 1,
|
||||
ceil: 1,
|
||||
floor: 1,
|
||||
elem: 2,
|
||||
send: 2,
|
||||
apply: 2,
|
||||
destructure: 2,
|
||||
get_and_update_in: 2,
|
||||
get_in: 2,
|
||||
pop_in: 2,
|
||||
put_in: 2,
|
||||
put_elem: 2,
|
||||
update_in: 2,
|
||||
raise: 2,
|
||||
reraise: 2,
|
||||
inspect: 2,
|
||||
struct: 1,
|
||||
struct: 2,
|
||||
use: 1,
|
||||
use: 2
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
defp define_property_function({term, iri}) do
|
||||
quote do
|
||||
@doc "<#{unquote(to_string(iri))}>"
|
||||
def unquote(term)(), do: unquote(Macro.escape(iri))
|
||||
|
||||
@doc "`RDF.Description` builder for `#{unquote(term)}/0`"
|
||||
def unquote(term)(subject, object)
|
||||
|
||||
def unquote(term)(%Description{} = subject, object) do
|
||||
Description.add(subject, {unquote(Macro.escape(iri)), object})
|
||||
end
|
||||
|
||||
def unquote(term)(subject, object) do
|
||||
Description.new(subject, init: {unquote(Macro.escape(iri)), object})
|
||||
end
|
||||
|
||||
@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
|
||||
|
||||
def define_namespace_functions(term_mapping) do
|
||||
quote do
|
||||
@term_mapping unquote(Macro.escape(term_mapping))
|
||||
@impl Elixir.RDF.Namespace
|
||||
def __terms__, do: Map.keys(@term_mapping)
|
||||
|
||||
@impl Elixir.RDF.Namespace
|
||||
def __iris__, do: Map.values(@term_mapping)
|
||||
|
||||
@impl Elixir.RDF.Namespace
|
||||
def __resolve_term__(term) do
|
||||
if iri = @term_mapping[term] do
|
||||
{:ok, iri}
|
||||
else
|
||||
{:error,
|
||||
%Elixir.RDF.Namespace.UndefinedTermError{
|
||||
message: "undefined term #{term} in namespace #{__MODULE__}"
|
||||
}}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp normalize_term_mapping(term_mapping) do
|
||||
Enum.reduce_while(term_mapping, {:ok, %{}}, fn {term, iri}, {:ok, normalized} ->
|
||||
if valid_term?(term) do
|
||||
{:cont, {:ok, Map.put(normalized, term, RDF.iri(iri))}}
|
||||
else
|
||||
{:halt,
|
||||
{:error, %RDF.Namespace.InvalidTermError{message: "invalid term: #{inspect(term)}"}}}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp property_terms(term_mapping) do
|
||||
for {term, iri} <- term_mapping, downcase?(term), into: %{} do
|
||||
{term, iri}
|
||||
end
|
||||
end
|
||||
|
||||
@reserved_terms ~w[
|
||||
and
|
||||
or
|
||||
xor
|
||||
in
|
||||
fn
|
||||
def
|
||||
defp
|
||||
defdelegate
|
||||
defexception
|
||||
defguard
|
||||
defguardp
|
||||
defimpl
|
||||
defmacro
|
||||
defmacrop
|
||||
defmodule
|
||||
defoverridable
|
||||
defprotocol
|
||||
defstruct
|
||||
function_exported?
|
||||
macro_exported?
|
||||
when
|
||||
if
|
||||
unless
|
||||
for
|
||||
case
|
||||
with
|
||||
quote
|
||||
unquote
|
||||
unquote_splicing
|
||||
alias
|
||||
import
|
||||
require
|
||||
super
|
||||
__aliases__
|
||||
]a
|
||||
|
||||
@doc false
|
||||
def reserved_terms, do: @reserved_terms
|
||||
|
||||
def reserved_term?(term) when term in @reserved_terms, do: true
|
||||
def reserved_term?(_), do: false
|
||||
|
||||
def valid_characters?(term) when is_atom(term),
|
||||
do: term |> Atom.to_string() |> valid_characters?()
|
||||
|
||||
def valid_characters?(term), do: Regex.match?(~r/^[a-zA-Z_]\w*$/, term)
|
||||
|
||||
def valid_term?(term), do: not reserved_term?(term) and valid_characters?(term)
|
||||
end
|
|
@ -119,7 +119,7 @@ defmodule RDF.Vocabulary.Namespace do
|
|||
@doc """
|
||||
Returns all known IRIs of the vocabulary.
|
||||
"""
|
||||
@spec __iris__ :: [Elixir.RDF.IRI.t()]
|
||||
@impl Elixir.RDF.Namespace
|
||||
def __iris__ do
|
||||
@terms
|
||||
|> Enum.map(fn
|
||||
|
|
21
test/support/test_namespaces.ex
Normal file
21
test/support/test_namespaces.ex
Normal file
|
@ -0,0 +1,21 @@
|
|||
defmodule RDF.TestNamespaces do
|
||||
import RDF.Sigils
|
||||
import RDF.Namespace
|
||||
|
||||
alias RDF.PropertyMap
|
||||
|
||||
defnamespace SimpleNS,
|
||||
[
|
||||
foo: ~I<http://example.com/foo>,
|
||||
bar: "http://example.com/bar",
|
||||
Baz: ~I<http://example.com/Baz>,
|
||||
Baaz: "http://example.com/Baaz"
|
||||
],
|
||||
moduledoc: "Example doc"
|
||||
|
||||
defnamespace NSfromPropertyMap,
|
||||
PropertyMap.new(
|
||||
foo: ~I<http://example.com/foo>,
|
||||
bar: "http://example.com/bar"
|
||||
)
|
||||
end
|
55
test/unit/namespace/builder_test.exs
Normal file
55
test/unit/namespace/builder_test.exs
Normal file
|
@ -0,0 +1,55 @@
|
|||
defmodule RDF.Namespace.BuilderTest do
|
||||
use RDF.Test.Case
|
||||
|
||||
alias RDF.Namespace.Builder
|
||||
import RDF.Sigils
|
||||
|
||||
@compile {:no_warn_undefined, ToplevelNS}
|
||||
|
||||
describe "create/3" do
|
||||
test "creates a module" do
|
||||
assert {:ok, {:module, ToplevelNS, _, _}} =
|
||||
Builder.create(
|
||||
ToplevelNS,
|
||||
[foo: ~I<http://example.com/foo>],
|
||||
Macro.Env.location(__ENV__)
|
||||
)
|
||||
|
||||
assert Elixir.ToplevelNS.foo() == ~I<http://example.com/foo>
|
||||
end
|
||||
|
||||
test "terms with invalid characters" do
|
||||
%{
|
||||
number_at_start: "42foo",
|
||||
colon: "foo:",
|
||||
bracket: "f(oo",
|
||||
square_bracket: "f[oo"
|
||||
}
|
||||
|> Enum.each(fn {label, invalid_term} ->
|
||||
assert Builder.create(
|
||||
:"NamespaceWithInvalidCharacter#{label}",
|
||||
[{invalid_term, ~I<http://example.com/invalid>}],
|
||||
Macro.Env.location(__ENV__)
|
||||
) ==
|
||||
{:error,
|
||||
%RDF.Namespace.InvalidTermError{
|
||||
message: "invalid term: #{inspect(invalid_term)}"
|
||||
}}
|
||||
end)
|
||||
end
|
||||
|
||||
test "terms with a special meaning for Elixir" do
|
||||
Enum.each(Builder.reserved_terms(), fn invalid_term ->
|
||||
assert Builder.create(
|
||||
:"NamespaceWithInvalidTerm#{invalid_term}",
|
||||
[{invalid_term, ~I<http://example.com/invalid>}],
|
||||
Macro.Env.location(__ENV__)
|
||||
) ==
|
||||
{:error,
|
||||
%RDF.Namespace.InvalidTermError{
|
||||
message: "invalid term: #{inspect(invalid_term)}"
|
||||
}}
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,5 +1,94 @@
|
|||
defmodule RDF.NamespaceTest do
|
||||
use ExUnit.Case
|
||||
use RDF.Test.Case
|
||||
|
||||
doctest RDF.Namespace
|
||||
|
||||
alias RDF.Namespace
|
||||
import RDF.Sigils
|
||||
|
||||
alias RDF.TestNamespaces.{SimpleNS, NSfromPropertyMap}
|
||||
|
||||
@compile {:no_warn_undefined, RDF.NamespaceTest.RelativeNS}
|
||||
|
||||
describe "defnamespace/2" do
|
||||
test "create module is relative to current namespace" do
|
||||
assert {:module, RDF.NamespaceTest.RelativeNS, _, _} =
|
||||
Namespace.defnamespace(RelativeNS,
|
||||
foo: ~I<http://example.com/foo>
|
||||
)
|
||||
|
||||
assert RDF.NamespaceTest.RelativeNS.foo() == ~I<http://example.com/foo>
|
||||
end
|
||||
end
|
||||
|
||||
describe "property functions" do
|
||||
test "returns IRI without args" do
|
||||
assert SimpleNS.foo() == ~I<http://example.com/foo>
|
||||
assert SimpleNS.bar() == ~I<http://example.com/bar>
|
||||
|
||||
assert NSfromPropertyMap.foo() == ~I<http://example.com/foo>
|
||||
assert NSfromPropertyMap.bar() == ~I<http://example.com/bar>
|
||||
end
|
||||
|
||||
test "description builder" do
|
||||
assert ~I<http://example.com/foo> |> SimpleNS.foo(~I<http://example.com/bar>) ==
|
||||
RDF.description(~I<http://example.com/foo>,
|
||||
init: {SimpleNS.foo(), ~I<http://example.com/bar>}
|
||||
)
|
||||
|
||||
assert EX.Foo |> SimpleNS.foo(EX.Bar) ==
|
||||
RDF.description(~I<http://example.com/Foo>,
|
||||
init: {SimpleNS.foo(), ~I<http://example.com/Bar>}
|
||||
)
|
||||
|
||||
assert EX.Foo |> SimpleNS.foo([1, 2, 3]) ==
|
||||
RDF.description(~I<http://example.com/Foo>,
|
||||
init: {SimpleNS.foo(), [1, 2, 3]}
|
||||
)
|
||||
|
||||
assert EX.Foo |> SimpleNS.foo(1, 2) ==
|
||||
RDF.description(~I<http://example.com/Foo>,
|
||||
init: {SimpleNS.foo(), [1, 2]}
|
||||
)
|
||||
|
||||
assert EX.Foo |> SimpleNS.foo(1, 2, 3) ==
|
||||
RDF.description(~I<http://example.com/Foo>,
|
||||
init: {SimpleNS.foo(), [1, 2, 3]}
|
||||
)
|
||||
|
||||
assert EX.Foo |> SimpleNS.foo(1, 2, 3, 4) ==
|
||||
RDF.description(~I<http://example.com/Foo>,
|
||||
init: {SimpleNS.foo(), [1, 2, 3, 4]}
|
||||
)
|
||||
|
||||
assert EX.Foo |> SimpleNS.foo(1, 2, 3, 4, 5) ==
|
||||
RDF.description(~I<http://example.com/Foo>,
|
||||
init: {SimpleNS.foo(), [1, 2, 3, 4, 5]}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
test "resolving module name atoms for non-property terms" do
|
||||
assert RDF.iri(SimpleNS.Baz) == ~I<http://example.com/Baz>
|
||||
assert RDF.iri(SimpleNS.Baaz) == ~I<http://example.com/Baaz>
|
||||
end
|
||||
|
||||
test "__terms__" do
|
||||
assert SimpleNS.__terms__() == [:Baaz, :Baz, :bar, :foo]
|
||||
assert NSfromPropertyMap.__terms__() == [:bar, :foo]
|
||||
end
|
||||
|
||||
test "__iris__" do
|
||||
assert SimpleNS.__iris__() == [
|
||||
~I<http://example.com/Baaz>,
|
||||
~I<http://example.com/Baz>,
|
||||
~I<http://example.com/bar>,
|
||||
~I<http://example.com/foo>
|
||||
]
|
||||
|
||||
assert NSfromPropertyMap.__iris__() == [
|
||||
~I<http://example.com/bar>,
|
||||
~I<http://example.com/foo>
|
||||
]
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue