rdf-ex/lib/rdf/namespace/builder.ex
2022-06-03 22:19:16 +02:00

201 lines
4.9 KiB
Elixir

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