rdf-ex/lib/rdf/vocabulary/namespace/vocabulary_namespace.ex
2022-06-06 01:59:18 +02:00

284 lines
7.8 KiB
Elixir

defmodule RDF.Vocabulary.Namespace do
@moduledoc """
An RDF vocabulary as a `RDF.Namespace`.
`RDF.Vocabulary.Namespace` modules represent a RDF vocabulary as a `RDF.Namespace`.
They can be defined with the `defvocab/2` macro of this module.
RDF.ex comes with predefined modules for some fundamental vocabularies in
the `RDF.NS` module.
"""
alias RDF.{Description, Graph, Dataset, Vocabulary, Namespace, IRI}
import RDF.Vocabulary.Namespace.{TermMapping, CaseValidation}
import RDF.Vocabulary, only: [term_to_iri: 2, extract_terms: 2]
@type t :: module
defmacro __using__(_opts) do
quote do
import unquote(__MODULE__)
end
end
@doc """
Defines a `RDF.Vocabulary.Namespace` module for a RDF vocabulary.
"""
defmacro defvocab({:__aliases__, _, [module]}, spec) do
env = __CALLER__
module = Namespace.module(env, module)
{base_iri, spec} = Keyword.pop(spec, :base_iri)
{input, opts} = input(module, spec)
no_warn_undefined = if Keyword.get(opts, :strict) == false, do: no_warn_undefined(module)
[
quote do
result =
create!(
unquote(module),
unquote(base_iri),
unquote(input),
unquote(Macro.escape(env)),
unquote(opts)
|> Keyword.put(:moduledoc, Module.delete_attribute(__MODULE__, :vocabdoc))
)
alias unquote(module)
result
end
| List.wrap(no_warn_undefined)
]
end
@required_input_opts ~w[file data terms]a
defp input(module, opts), do: do_input(module, nil, opts, @required_input_opts)
defp do_input(module, nil, _, []) do
raise ArgumentError,
"none of #{Enum.join(@required_input_opts, ", ")} are given on defvocab for #{module}"
end
defp do_input(_, input, opts, []), do: {input, opts}
defp do_input(module, input, opts, [opt | rest]) do
case Keyword.pop(opts, opt) do
{nil, opts} ->
do_input(module, input, opts, rest)
{value, opts} ->
if input do
raise ArgumentError,
"multiple values for #{Enum.join(@required_input_opts, ", ")} are given on defvocab for #{module}"
else
do_input(module, value, opts, rest)
end
end
end
defp no_warn_undefined(module) do
quote do
@compile {:no_warn_undefined, unquote(module)}
end
end
def create(module, base_uri, vocab, location, opts)
def create(module, base_uri, vocab_path, location, opts) when is_binary(vocab_path) do
file = vocab_from_path(vocab_path)
create(
module,
base_uri,
RDF.read_file!(file, base_iri: nil),
location,
Keyword.put(opts, :file, file)
)
end
def create(module, base_uri, %struct{} = data, location, opts)
when struct in [Graph, Dataset] do
do_create(
module,
base_uri,
extract_terms(data, base_uri),
location,
Keyword.put(opts, :data, data)
)
end
def create(module, base_uri, terms, location, opts) do
{terms, opts} = extract_aliases(terms, opts)
do_create(module, base_uri, terms, location, opts)
end
defp do_create(module, base_uri, terms, location, opts) do
base_uri = normalize_base_uri(base_uri)
strict = Keyword.get(opts, :strict, true)
ignored_terms = Keyword.get(opts, :ignore, []) |> normalize_ignored_terms()
{terms, aliases} = normalize_terms(module, terms, ignored_terms, strict, opts)
{terms, ignored_terms} =
{terms, ignored_terms}
|> validate!(opts)
|> validate_case!(Keyword.get(opts, :data), base_uri, aliases, opts)
Namespace.Builder.create(
module,
term_mapping(base_uri, terms, ignored_terms),
location,
if strict do
opts
else
Keyword.put(opts, :add_after, define_undefined_function_handler())
end
|> Keyword.put(
:namespace_functions,
define_namespace_functions(base_uri, terms, ignored_terms, strict, opts)
)
|> Keyword.put(:skip_normalization, true)
)
end
def create!(module, base_uri, vocab, env, opts) do
case create(module, base_uri, vocab, env, opts) do
{:ok, result} -> result
{:error, error} -> raise error
end
end
def define_namespace_functions(base_iri, terms, ignored_terms, strict, opts) do
file = Keyword.get(opts, :file)
quote do
if unquote(file) do
@external_resource unquote(file)
end
@strict unquote(strict)
@spec __strict__ :: boolean
def __strict__, do: @strict
@base_iri unquote(base_iri)
@spec __base_iri__ :: String.t()
def __base_iri__, do: @base_iri
@terms unquote(Macro.escape(terms))
@impl Elixir.RDF.Namespace
def __terms__, do: Map.keys(@terms)
@spec __term_aliases__ :: [atom]
def __term_aliases__, do: RDF.Vocabulary.Namespace.TermMapping.aliases(@terms)
@ignored_terms unquote(Macro.escape(ignored_terms))
@doc """
Returns all known IRIs of the vocabulary.
"""
@impl Elixir.RDF.Namespace
def __iris__ do
@terms
|> Enum.map(fn
{term, true} -> term_to_iri(@base_iri, term)
{_alias, term} -> term_to_iri(@base_iri, term)
end)
|> Enum.uniq()
end
@impl Elixir.RDF.Namespace
@dialyzer {:nowarn_function, __resolve_term__: 1}
def __resolve_term__(term) do
case @terms[term] do
nil ->
if @strict or MapSet.member?(@ignored_terms, term) do
{:error,
%Elixir.RDF.Namespace.UndefinedTermError{
message: "undefined term #{term} in strict vocabulary #{__MODULE__}"
}}
else
{:ok, term_to_iri(@base_iri, term)}
end
true ->
{:ok, term_to_iri(@base_iri, term)}
original_term ->
{:ok, term_to_iri(@base_iri, original_term)}
end
end
end
end
def define_undefined_function_handler do
quote do
def unquote(:"$handle_undefined_function")(term, []) do
if MapSet.member?(@ignored_terms, term) do
raise UndefinedFunctionError
end
term_to_iri(@base_iri, term)
end
def unquote(:"$handle_undefined_function")(term, [subject | objects]) do
if MapSet.member?(@ignored_terms, term) do
raise UndefinedFunctionError
end
objects =
case objects do
[objects] when is_list(objects) -> objects
_ -> objects
end
case subject do
%Description{} -> subject
_ -> Description.new(subject)
end
|> Description.add({term_to_iri(@base_iri, term), objects})
end
end
end
defp vocab_from_path(vocab_path) do
cond do
File.exists?(vocab_path) ->
vocab_path
File.exists?(expanded_filename = Path.expand(vocab_path, Vocabulary.path())) ->
expanded_filename
true ->
raise File.Error, path: vocab_path, action: "find", reason: :enoent
end
end
defp normalize_base_uri(%IRI{} = base_iri), do: IRI.to_string(base_iri)
defp normalize_base_uri(base_uri) when is_binary(base_uri) do
if IRI.valid?(base_uri) do
base_uri
else
raise RDF.Namespace.InvalidVocabBaseIRIError, "invalid base IRI: #{inspect(base_uri)}"
end
end
defp normalize_base_uri(base_uri) do
base_uri |> IRI.new() |> normalize_base_uri()
rescue
[Namespace.UndefinedTermError, IRI.InvalidError, FunctionClauseError] ->
reraise RDF.Namespace.InvalidVocabBaseIRIError,
"invalid base IRI: #{inspect(base_uri)}",
__STACKTRACE__
end
@doc false
@spec vocabulary_namespace?(module) :: boolean
def vocabulary_namespace?(name) do
case Code.ensure_compiled(name) do
{:module, name} -> function_exported?(name, :__base_iri__, 0)
_ -> false
end
end
end