Rewrite RDF.Vocabulary.Namespace
This commit is contained in:
parent
52369c289c
commit
9449fce988
7 changed files with 744 additions and 678 deletions
|
@ -7,13 +7,22 @@ This project adheres to [Semantic Versioning](http://semver.org/) and
|
|||
|
||||
## Unreleased
|
||||
|
||||
In this version `RDF.Namespace` and `RDF.Vocabulary.Namespace` were completely rewritten.
|
||||
The generated namespaces are much more flexible now and compile faster.
|
||||
|
||||
### Added
|
||||
|
||||
- `RDF.Namespace` builders `defnamespace/3` and `create/4`
|
||||
- `RDF.Vocabulary.Namespace.create/5` for dynamic creation of `RDF.Vocabulary.Namespace`s
|
||||
- `RDF.IRI.starts_with?/2` and `RDF.IRI.ends_with?/2`
|
||||
|
||||
### Changed
|
||||
|
||||
- Aliases on a `RDF.Vocabulary.Namespace` can now be specified directly in the
|
||||
`:terms` list
|
||||
- When defining an alias for a term of vocabulary which would be invalid as an
|
||||
Elixir term, the original term is now implicitly ignored and won't any longer
|
||||
be returned by the `__terms__/0` function of a `RDF.Vocabulary.Namespace`.
|
||||
- `RDF.Data.merge/2` and `RDF.Data.equal?/2` are now commutative, i.e. structs
|
||||
which implement the `RDF.Data` protocol can be given also as the second argument
|
||||
(previously custom structs with `RDF.Data` protocol implementations always
|
||||
|
|
|
@ -11,8 +11,9 @@ defmodule RDF.Namespace.Builder do
|
|||
{:ok, {:module, module(), binary(), term()}} | {:error, any}
|
||||
def create(module, term_mapping, location, opts \\ []) do
|
||||
moduledoc = opts[:moduledoc]
|
||||
skip_normalization = opts[:skip_normalization]
|
||||
|
||||
with {:ok, term_mapping} <- normalize_term_mapping(term_mapping) do
|
||||
with {:ok, term_mapping} <- normalize_term_mapping(term_mapping, skip_normalization) do
|
||||
property_terms = property_terms(term_mapping)
|
||||
|
||||
body =
|
||||
|
@ -42,7 +43,7 @@ defmodule RDF.Namespace.Builder do
|
|||
quote do
|
||||
@moduledoc unquote(moduledoc)
|
||||
|
||||
@behaviour Elixir.RDF.Namespace
|
||||
@behaviour RDF.Namespace
|
||||
|
||||
import Kernel,
|
||||
except: [
|
||||
|
@ -131,7 +132,9 @@ defmodule RDF.Namespace.Builder do
|
|||
end
|
||||
end
|
||||
|
||||
defp normalize_term_mapping(term_mapping) do
|
||||
defp normalize_term_mapping(term_mapping, true), do: {:ok, term_mapping}
|
||||
|
||||
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))}}
|
||||
|
|
156
lib/rdf/vocabulary/namespace/case_validation.ex
Normal file
156
lib/rdf/vocabulary/namespace/case_validation.ex
Normal file
|
@ -0,0 +1,156 @@
|
|||
defmodule RDF.Vocabulary.Namespace.CaseValidation do
|
||||
import RDF.Vocabulary, only: [term_to_iri: 2]
|
||||
import RDF.Utils, only: [downcase?: 1]
|
||||
|
||||
alias RDF.Utils.ResourceClassifier
|
||||
|
||||
def validate_case!(terms_and_ignored, nil, _, _, _), do: terms_and_ignored
|
||||
|
||||
def validate_case!({terms, ignored_terms}, data, base_iri, aliases, opts) do
|
||||
handling = Keyword.get(opts, :case_violations, :warn)
|
||||
|
||||
if handling == :ignore do
|
||||
{terms, ignored_terms}
|
||||
else
|
||||
handle_case_violations(
|
||||
{terms, ignored_terms},
|
||||
detect_case_violations(terms, data, base_iri, Keyword.values(aliases)),
|
||||
handling,
|
||||
base_iri
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
defp detect_case_violations(terms, data, base_iri, aliased_terms) do
|
||||
Enum.filter(terms, fn
|
||||
{term, true} -> if term not in aliased_terms, do: improper_case?(term, base_iri, term, data)
|
||||
{term, original_term} -> improper_case?(term, base_iri, original_term, data)
|
||||
end)
|
||||
end
|
||||
|
||||
defp improper_case?(term, base_iri, iri_suffix, data) when is_atom(term),
|
||||
do: improper_case?(Atom.to_string(term), base_iri, iri_suffix, data)
|
||||
|
||||
defp improper_case?("_" <> _, _, _, _), do: false
|
||||
|
||||
defp improper_case?(term, base_iri, iri_suffix, data) do
|
||||
case ResourceClassifier.property?(term_to_iri(base_iri, iri_suffix), data) do
|
||||
true -> not downcase?(term)
|
||||
false -> downcase?(term)
|
||||
nil -> downcase?(term)
|
||||
end
|
||||
end
|
||||
|
||||
defp group_case_violations(violations) do
|
||||
violations
|
||||
|> Enum.group_by(fn
|
||||
{term, true} -> if downcase?(term), do: :lowercased_term, else: :capitalized_term
|
||||
{term, _original} -> if downcase?(term), do: :lowercased_alias, else: :capitalized_alias
|
||||
end)
|
||||
end
|
||||
|
||||
defp handle_case_violations(terms_and_ignored, [], _, _), do: terms_and_ignored
|
||||
|
||||
defp handle_case_violations(terms_and_ignored, violations, handling, base_uri) do
|
||||
do_handle_case_violations(
|
||||
terms_and_ignored,
|
||||
group_case_violations(violations),
|
||||
handling,
|
||||
base_uri
|
||||
)
|
||||
end
|
||||
|
||||
defp do_handle_case_violations(_, violations, :fail, base_iri) do
|
||||
resource_name_violations = fn violations ->
|
||||
violations
|
||||
|> Enum.map(fn {term, true} -> base_iri |> term_to_iri(term) |> to_string() end)
|
||||
|> Enum.join("\n- ")
|
||||
end
|
||||
|
||||
alias_violations = fn violations ->
|
||||
violations
|
||||
|> Enum.map(fn {term, original} ->
|
||||
"alias #{term} for #{term_to_iri(base_iri, original)}"
|
||||
end)
|
||||
|> Enum.join("\n- ")
|
||||
end
|
||||
|
||||
violation_error_lines =
|
||||
violations
|
||||
|> Enum.map(fn
|
||||
{:capitalized_term, violations} ->
|
||||
"""
|
||||
Terms for properties should be lowercased, but the following properties are
|
||||
capitalized:
|
||||
|
||||
- #{resource_name_violations.(violations)}
|
||||
|
||||
"""
|
||||
|
||||
{:lowercased_term, violations} ->
|
||||
"""
|
||||
Terms for non-property resource should be capitalized, but the following
|
||||
non-properties are lowercased:
|
||||
|
||||
- #{resource_name_violations.(violations)}
|
||||
|
||||
"""
|
||||
|
||||
{:capitalized_alias, violations} ->
|
||||
"""
|
||||
Terms for properties should be lowercased, but the following aliases for
|
||||
properties are capitalized:
|
||||
|
||||
- #{alias_violations.(violations)}
|
||||
|
||||
"""
|
||||
|
||||
{:lowercased_alias, violations} ->
|
||||
"""
|
||||
Terms for non-property resource should be capitalized, but the following
|
||||
aliases for non-properties are lowercased:
|
||||
|
||||
- #{alias_violations.(violations)}
|
||||
|
||||
"""
|
||||
end)
|
||||
|> Enum.join()
|
||||
|
||||
raise RDF.Namespace.InvalidTermError, """
|
||||
Case violations detected
|
||||
|
||||
#{violation_error_lines}
|
||||
You have the following options:
|
||||
|
||||
- if you are in control of the vocabulary, consider renaming the resource
|
||||
- define a properly cased alias with the :alias option on defvocab
|
||||
- change the handling of case violations with the :case_violations option on defvocab
|
||||
- ignore the resource with the :ignore option on defvocab
|
||||
"""
|
||||
end
|
||||
|
||||
defp do_handle_case_violations(terms_and_ignored, violation_groups, :warn, base_iri) do
|
||||
for {type, violations} <- violation_groups,
|
||||
{term, original} <- violations do
|
||||
case_violation_warning(type, term, original, base_iri)
|
||||
end
|
||||
|
||||
terms_and_ignored
|
||||
end
|
||||
|
||||
defp case_violation_warning(:capitalized_term, term, _, base_iri) do
|
||||
IO.warn("'#{term_to_iri(base_iri, term)}' is a capitalized property")
|
||||
end
|
||||
|
||||
defp case_violation_warning(:lowercased_term, term, _, base_iri) do
|
||||
IO.warn("'#{term_to_iri(base_iri, term)}' is a lowercased non-property resource")
|
||||
end
|
||||
|
||||
defp case_violation_warning(:capitalized_alias, term, _, _) do
|
||||
IO.warn("capitalized alias '#{term}' for a property")
|
||||
end
|
||||
|
||||
defp case_violation_warning(:lowercased_alias, term, _, _) do
|
||||
IO.warn("lowercased alias '#{term}' for a non-property resource")
|
||||
end
|
||||
end
|
177
lib/rdf/vocabulary/namespace/term_mapping.ex
Normal file
177
lib/rdf/vocabulary/namespace/term_mapping.ex
Normal file
|
@ -0,0 +1,177 @@
|
|||
defmodule RDF.Vocabulary.Namespace.TermMapping do
|
||||
@moduledoc false
|
||||
|
||||
import RDF.Namespace.Builder, only: [valid_term?: 1, valid_characters?: 1, reserved_term?: 1]
|
||||
import RDF.Vocabulary, only: [term_to_iri: 2]
|
||||
|
||||
def normalize_terms(module, terms, ignored_terms, strict, opts) do
|
||||
aliases =
|
||||
opts
|
||||
|> Keyword.get(:alias, [])
|
||||
|> Keyword.new(fn {alias, original_term} ->
|
||||
{normalize_term(alias), normalize_term(original_term)}
|
||||
end)
|
||||
|
||||
terms = Map.new(terms, &{normalize_term(&1), true})
|
||||
|
||||
normalized_terms =
|
||||
Enum.reduce(aliases, terms, fn {alias, original_term}, terms ->
|
||||
cond do
|
||||
reserved_term?(alias) ->
|
||||
raise RDF.Namespace.InvalidAliasError,
|
||||
"alias '#{alias}' in vocabulary namespace #{module} is a reserved term and can't be used as an alias"
|
||||
|
||||
not valid_characters?(alias) ->
|
||||
raise RDF.Namespace.InvalidAliasError,
|
||||
"alias '#{alias}' in vocabulary namespace #{module} contains invalid characters"
|
||||
|
||||
Map.get(terms, alias) == true ->
|
||||
raise RDF.Namespace.InvalidAliasError,
|
||||
"alias '#{alias}' in vocabulary namespace #{module} already defined"
|
||||
|
||||
strict and not Map.has_key?(terms, original_term) ->
|
||||
raise RDF.Namespace.InvalidAliasError,
|
||||
"term '#{original_term}' is not a term in vocabulary namespace #{module}"
|
||||
|
||||
Map.get(terms, original_term, true) != true ->
|
||||
raise RDF.Namespace.InvalidAliasError,
|
||||
"'#{original_term}' is already an alias in vocabulary namespace #{module}"
|
||||
|
||||
true ->
|
||||
if alias in ignored_terms do
|
||||
IO.warn("ignoring alias '#{alias}' in vocabulary namespace #{module}")
|
||||
end
|
||||
|
||||
if valid_term?(original_term) and original_term not in ignored_terms do
|
||||
terms
|
||||
else
|
||||
Map.delete(terms, original_term)
|
||||
end
|
||||
|> Map.put(alias, normalize_aliased_term(original_term))
|
||||
end
|
||||
end)
|
||||
|> Map.drop(MapSet.to_list(ignored_terms))
|
||||
|
||||
{normalized_terms, aliases}
|
||||
end
|
||||
|
||||
def term_mapping(base_uri, terms, ignored_terms) do
|
||||
Enum.flat_map(terms, fn
|
||||
{term, true} ->
|
||||
[{term, term_to_iri(base_uri, term)}]
|
||||
|
||||
{term, original} ->
|
||||
iri = term_to_iri(base_uri, original)
|
||||
original = normalize_term(original)
|
||||
|
||||
if valid_term?(original) and original not in ignored_terms do
|
||||
[{term, iri}, {original, iri}]
|
||||
else
|
||||
[{term, iri}]
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp normalize_term(term) when is_atom(term), do: term
|
||||
defp normalize_term(term) when is_binary(term), do: String.to_atom(term)
|
||||
defp normalize_term(term), do: raise(RDF.Namespace.InvalidTermError, inspect(term))
|
||||
|
||||
defp normalize_aliased_term(term) when is_binary(term), do: term
|
||||
defp normalize_aliased_term(term) when is_atom(term), do: Atom.to_string(term)
|
||||
|
||||
def normalize_ignored_terms(terms), do: MapSet.new(terms, &normalize_term/1)
|
||||
|
||||
def extract_aliases(terms, opts) do
|
||||
aliases = opts |> Keyword.get(:alias, []) |> Keyword.new()
|
||||
|
||||
{terms, aliases} =
|
||||
Enum.reduce(terms, {[], aliases}, fn
|
||||
{_, term} = alias, {terms, aliases} -> {[term | terms], [alias | aliases]}
|
||||
term, {terms, aliases} -> {[term | terms], aliases}
|
||||
end)
|
||||
|
||||
{terms, Keyword.put(opts, :alias, aliases)}
|
||||
end
|
||||
|
||||
def aliases(terms) do
|
||||
for {alias, term} <- terms, term != true, do: alias
|
||||
end
|
||||
|
||||
def validate!({terms, ignored_terms}, opts) do
|
||||
{invalid_terms, invalid_characters} =
|
||||
Enum.reduce(terms, {[], []}, fn
|
||||
{term, _}, {invalid_terms, invalid_character_terms} ->
|
||||
cond do
|
||||
reserved_term?(term) ->
|
||||
{[term | invalid_terms], invalid_character_terms}
|
||||
|
||||
not valid_characters?(term) ->
|
||||
{invalid_terms, [term | invalid_character_terms]}
|
||||
|
||||
true ->
|
||||
{
|
||||
invalid_terms,
|
||||
invalid_character_terms
|
||||
}
|
||||
end
|
||||
end)
|
||||
|
||||
{terms, ignored_terms}
|
||||
|> handle_invalid_terms!(
|
||||
invalid_terms,
|
||||
Keyword.get(opts, :invalid_terms, :fail)
|
||||
)
|
||||
|> handle_invalid_characters!(
|
||||
invalid_characters,
|
||||
Keyword.get(opts, :invalid_characters, :fail)
|
||||
)
|
||||
end
|
||||
|
||||
defp handle_invalid_terms!(terms_and_ignored, [], _), do: terms_and_ignored
|
||||
|
||||
defp handle_invalid_terms!({terms, aliases}, invalid_terms, :ignore) do
|
||||
{Map.drop(terms, invalid_terms), MapSet.union(aliases, MapSet.new(invalid_terms))}
|
||||
end
|
||||
|
||||
defp handle_invalid_terms!(_, invalid_terms, :fail) do
|
||||
raise RDF.Namespace.InvalidTermError, """
|
||||
The following terms can not be used, because they conflict with the Elixir semantics:
|
||||
|
||||
- #{Enum.join(invalid_terms, "\n- ")}
|
||||
|
||||
You have the following options:
|
||||
|
||||
- define an alias with the :alias option on defvocab
|
||||
- ignore the resource with the :ignore option on defvocab
|
||||
"""
|
||||
end
|
||||
|
||||
defp handle_invalid_characters!(terms_and_ignored, [], _), do: terms_and_ignored
|
||||
|
||||
defp handle_invalid_characters!({terms, ignored_terms}, invalid_terms, :ignore) do
|
||||
{Map.drop(terms, invalid_terms), MapSet.union(ignored_terms, MapSet.new(invalid_terms))}
|
||||
end
|
||||
|
||||
defp handle_invalid_characters!(_, invalid_terms, :fail) do
|
||||
raise RDF.Namespace.InvalidTermError, """
|
||||
The following terms contain invalid characters:
|
||||
|
||||
- #{Enum.join(invalid_terms, "\n- ")}
|
||||
|
||||
You have the following options:
|
||||
|
||||
- if you are in control of the vocabulary, consider renaming the resource
|
||||
- define an alias with the :alias option on defvocab
|
||||
- change the handling of invalid characters with the :invalid_characters option on defvocab
|
||||
- ignore the resource with the :ignore option on defvocab
|
||||
"""
|
||||
end
|
||||
|
||||
defp handle_invalid_characters!(terms_and_ignored, invalid_terms, :warn) do
|
||||
Enum.each(invalid_terms, fn term ->
|
||||
IO.warn("'#{term}' is not valid term, since it contains invalid characters")
|
||||
end)
|
||||
|
||||
terms_and_ignored
|
||||
end
|
||||
end
|
33
lib/rdf/vocabulary/vocabulary.ex
Normal file
33
lib/rdf/vocabulary/vocabulary.ex
Normal file
|
@ -0,0 +1,33 @@
|
|||
defmodule RDF.Vocabulary do
|
||||
alias RDF.IRI
|
||||
|
||||
@path "priv/vocabs"
|
||||
def path, do: @path
|
||||
|
||||
def extract_terms(data, base_iri) do
|
||||
data
|
||||
|> RDF.Data.resources()
|
||||
|> Stream.filter(&match?(%IRI{}, &1))
|
||||
|> Stream.map(&to_string/1)
|
||||
|> Stream.map(&strip_base_iri(&1, base_iri))
|
||||
|> Stream.filter(&vocab_term?/1)
|
||||
|> Enum.map(&String.to_atom/1)
|
||||
end
|
||||
|
||||
defp strip_base_iri(iri, base_iri) do
|
||||
if String.starts_with?(iri, base_iri) do
|
||||
String.replace_prefix(iri, base_iri, "")
|
||||
end
|
||||
end
|
||||
|
||||
defp vocab_term?(""), do: false
|
||||
defp vocab_term?(term) when is_binary(term), do: not String.contains?(term, "/")
|
||||
defp vocab_term?(_), do: false
|
||||
|
||||
@doc false
|
||||
@spec term_to_iri(String.t(), String.t() | atom) :: IRI.t()
|
||||
def term_to_iri(base_iri, term) when is_atom(term),
|
||||
do: term_to_iri(base_iri, Atom.to_string(term))
|
||||
|
||||
def term_to_iri(base_iri, term), do: RDF.iri(base_iri <> term)
|
||||
end
|
|
@ -9,17 +9,13 @@ defmodule RDF.Vocabulary.Namespace do
|
|||
the `RDF.NS` module.
|
||||
"""
|
||||
|
||||
alias RDF.Description
|
||||
alias RDF.Utils.ResourceClassifier
|
||||
alias RDF.{Description, Graph, Dataset, Vocabulary, Namespace}
|
||||
|
||||
import RDF.Utils, only: [downcase?: 1]
|
||||
import RDF.Vocabulary.Namespace.{TermMapping, CaseValidation}
|
||||
import RDF.Vocabulary, only: [term_to_iri: 2, extract_terms: 2]
|
||||
|
||||
@type t :: module
|
||||
|
||||
# Note: We're not using :code.priv_dir/1 here on purpose, since vocabulary files should be
|
||||
# searched in the vocabs dir of the project in which the vocabulary namespace is defined.
|
||||
@vocabs_dir "priv/vocabs"
|
||||
|
||||
defmacro __using__(_opts) do
|
||||
quote do
|
||||
import unquote(__MODULE__)
|
||||
|
@ -27,651 +23,245 @@ defmodule RDF.Vocabulary.Namespace do
|
|||
end
|
||||
|
||||
@doc """
|
||||
Defines a `RDF.Namespace` module for a RDF vocabulary.
|
||||
Defines a `RDF.Vocabulary.Namespace` module for a RDF vocabulary.
|
||||
"""
|
||||
defmacro defvocab(name, opts) do
|
||||
strict = strict?(opts)
|
||||
base_iri = base_iri!(opts)
|
||||
file = filename!(opts)
|
||||
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)
|
||||
|
||||
{terms, data} =
|
||||
case source!(opts) do
|
||||
{:terms, terms} -> {terms, nil}
|
||||
{:data, data} -> {rdf_data_vocab_terms(data, base_iri), data}
|
||||
[
|
||||
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
|
||||
|
||||
unless Mix.env() == :test do
|
||||
IO.puts("Compiling vocabulary namespace for #{base_iri}")
|
||||
@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
|
||||
|
||||
ignored_terms = ignored_terms!(opts)
|
||||
defp no_warn_undefined(module) do
|
||||
quote do
|
||||
@compile {:no_warn_undefined, unquote(module)}
|
||||
end
|
||||
end
|
||||
|
||||
terms =
|
||||
terms
|
||||
|> term_mapping!(opts)
|
||||
|> Map.drop(MapSet.to_list(ignored_terms))
|
||||
|> validate_terms!
|
||||
|> validate_characters!(opts)
|
||||
|> validate_case!(data, base_iri, opts)
|
||||
def create(module, base_uri, vocab, location, opts)
|
||||
|
||||
case_separated_terms = group_terms_by_case(terms)
|
||||
lowercased_terms = Map.get(case_separated_terms, :lowercased, %{})
|
||||
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
|
||||
vocabdoc = Module.delete_attribute(__MODULE__, :vocabdoc)
|
||||
if unquote(file) do
|
||||
@external_resource unquote(file)
|
||||
end
|
||||
|
||||
defmodule unquote(name) do
|
||||
@moduledoc vocabdoc
|
||||
@strict unquote(strict)
|
||||
@spec __strict__ :: boolean
|
||||
def __strict__, do: @strict
|
||||
|
||||
@behaviour Elixir.RDF.Namespace
|
||||
@base_iri unquote(base_iri)
|
||||
@spec __base_iri__ :: String.t()
|
||||
def __base_iri__, do: @base_iri
|
||||
|
||||
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
|
||||
]
|
||||
@terms unquote(Macro.escape(terms))
|
||||
@impl Elixir.RDF.Namespace
|
||||
def __terms__, do: Map.keys(@terms)
|
||||
|
||||
if unquote(file) do
|
||||
@external_resource unquote(file)
|
||||
end
|
||||
@spec __term_aliases__ :: [atom]
|
||||
def __term_aliases__, do: RDF.Vocabulary.Namespace.TermMapping.aliases(@terms)
|
||||
|
||||
@base_iri unquote(base_iri)
|
||||
@spec __base_iri__ :: String.t()
|
||||
def __base_iri__, do: @base_iri
|
||||
@ignored_terms unquote(Macro.escape(ignored_terms))
|
||||
|
||||
@strict unquote(strict)
|
||||
@spec __strict__ :: boolean
|
||||
def __strict__, do: @strict
|
||||
@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
|
||||
|
||||
@terms unquote(Macro.escape(terms))
|
||||
@impl Elixir.RDF.Namespace
|
||||
def __terms__, do: @terms |> Map.keys()
|
||||
|
||||
@spec __term_aliases__ :: [atom]
|
||||
def __term_aliases__ do
|
||||
for {alias, term} <- @terms, term != true, do: alias
|
||||
end
|
||||
|
||||
@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
|
||||
|
||||
define_vocab_terms(unquote(lowercased_terms), unquote(base_iri))
|
||||
|
||||
@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 ->
|
||||
@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)}
|
||||
|
||||
original_term ->
|
||||
{:ok, term_to_iri(@base_iri, original_term)}
|
||||
end
|
||||
end
|
||||
|
||||
if not @strict 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
|
||||
true ->
|
||||
{:ok, term_to_iri(@base_iri, term)}
|
||||
|
||||
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
|
||||
original_term ->
|
||||
{:ok, term_to_iri(@base_iri, original_term)}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
defmacro define_vocab_terms(terms, base_iri) do
|
||||
terms
|
||||
|> Stream.filter(fn
|
||||
{term, true} -> valid_term?(term)
|
||||
{_, _} -> true
|
||||
end)
|
||||
|> Stream.map(fn
|
||||
{term, true} -> {term, term}
|
||||
{term, original_term} -> {term, original_term}
|
||||
end)
|
||||
|> Enum.map(fn {term, iri_suffix} ->
|
||||
iri = term_to_iri(base_iri, iri_suffix)
|
||||
|
||||
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})
|
||||
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
|
||||
|
||||
def unquote(term)(subject, object) do
|
||||
Description.new(subject, init: {unquote(Macro.escape(iri)), 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])
|
||||
term_to_iri(@base_iri, term)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp strict?(opts),
|
||||
do: Keyword.get(opts, :strict, true)
|
||||
def unquote(:"$handle_undefined_function")(term, [subject | objects]) do
|
||||
if MapSet.member?(@ignored_terms, term) do
|
||||
raise UndefinedFunctionError
|
||||
end
|
||||
|
||||
defp base_iri!(opts) do
|
||||
base_iri = Keyword.fetch!(opts, :base_iri)
|
||||
objects =
|
||||
case objects do
|
||||
[objects] when is_list(objects) -> objects
|
||||
_ -> objects
|
||||
end
|
||||
|
||||
unless is_binary(base_iri) and String.ends_with?(base_iri, ~w[/ # .]) do
|
||||
raise RDF.Namespace.InvalidVocabBaseIRIError,
|
||||
"a base_iri without a trailing '/' or '#' is invalid"
|
||||
else
|
||||
base_iri
|
||||
case subject do
|
||||
%Description{} -> subject
|
||||
_ -> Description.new(subject)
|
||||
end
|
||||
|> Description.add({term_to_iri(@base_iri, term), objects})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp source!(opts) do
|
||||
defp vocab_from_path(vocab_path) do
|
||||
cond do
|
||||
Keyword.has_key?(opts, :file) ->
|
||||
{:data, filename!(opts) |> RDF.read_file!(base_iri: nil)}
|
||||
File.exists?(vocab_path) ->
|
||||
vocab_path
|
||||
|
||||
rdf_data = Keyword.get(opts, :data) ->
|
||||
{:data, raw_rdf_data(rdf_data)}
|
||||
|
||||
terms = Keyword.get(opts, :terms) ->
|
||||
{:terms, terms_from_user_input!(terms)}
|
||||
File.exists?(expanded_filename = Path.expand(vocab_path, Vocabulary.path())) ->
|
||||
expanded_filename
|
||||
|
||||
true ->
|
||||
raise KeyError, key: ~w[terms data file], term: opts
|
||||
raise File.Error, path: vocab_path, action: "find", reason: :enoent
|
||||
end
|
||||
end
|
||||
|
||||
defp terms_from_user_input!(terms) do
|
||||
# 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, [], rdf_data_env())
|
||||
|
||||
Enum.map(terms, 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)
|
||||
end
|
||||
|
||||
defp raw_rdf_data(%Description{} = rdf_data), do: rdf_data
|
||||
defp raw_rdf_data(%RDF.Graph{} = rdf_data), do: rdf_data
|
||||
defp raw_rdf_data(%RDF.Dataset{} = rdf_data), do: rdf_data
|
||||
|
||||
defp raw_rdf_data(rdf_data) do
|
||||
# TODO: find an alternative to Code.eval_quoted
|
||||
{rdf_data, _} = Code.eval_quoted(rdf_data, [], rdf_data_env())
|
||||
rdf_data
|
||||
end
|
||||
|
||||
defp ignored_terms!(opts) do
|
||||
# TODO: find an alternative to Code.eval_quoted - We want to support that the terms can be given as sigils ...
|
||||
with terms = Keyword.get(opts, :ignore, []) do
|
||||
{terms, _} = Code.eval_quoted(terms, [], rdf_data_env())
|
||||
|
||||
terms
|
||||
|> Enum.map(fn
|
||||
term when is_atom(term) -> term
|
||||
term when is_binary(term) -> String.to_atom(term)
|
||||
term -> raise RDF.Namespace.InvalidTermError, inspect(term)
|
||||
end)
|
||||
|> MapSet.new()
|
||||
end
|
||||
end
|
||||
|
||||
defp 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
|
||||
not valid_characters?(alias) ->
|
||||
raise RDF.Namespace.InvalidAliasError,
|
||||
"alias '#{alias}' contains invalid characters"
|
||||
|
||||
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 ->
|
||||
Map.put(terms, alias, to_string(original_term))
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp aliased_terms(terms) do
|
||||
terms
|
||||
|> Map.values()
|
||||
|> MapSet.new()
|
||||
|> MapSet.delete(true)
|
||||
|> Enum.map(&String.to_atom/1)
|
||||
end
|
||||
|
||||
@invalid_terms MapSet.new(~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)
|
||||
|
||||
def invalid_terms, do: @invalid_terms
|
||||
|
||||
defp validate_terms!(terms) do
|
||||
aliased_terms = aliased_terms(terms)
|
||||
|
||||
for {term, _} <- terms, term not in aliased_terms and not valid_term?(term) do
|
||||
term
|
||||
end
|
||||
|> handle_invalid_terms!
|
||||
|
||||
terms
|
||||
end
|
||||
|
||||
defp valid_term?(term), do: term not in @invalid_terms
|
||||
|
||||
defp handle_invalid_terms!([]), do: nil
|
||||
|
||||
defp handle_invalid_terms!(invalid_terms) do
|
||||
raise RDF.Namespace.InvalidTermError, """
|
||||
The following terms can not be used, because they conflict with the Elixir semantics:
|
||||
|
||||
- #{Enum.join(invalid_terms, "\n- ")}
|
||||
|
||||
You have the following options:
|
||||
|
||||
- define an alias with the :alias option on defvocab
|
||||
- ignore the resource with the :ignore option on defvocab
|
||||
"""
|
||||
end
|
||||
|
||||
defp validate_characters!(terms, opts) do
|
||||
if (handling = Keyword.get(opts, :invalid_characters, :fail)) == :ignore do
|
||||
terms
|
||||
defp normalize_base_uri(base_uri) do
|
||||
unless is_binary(base_uri) and String.ends_with?(base_uri, ~w[/ # .]) do
|
||||
raise RDF.Namespace.InvalidVocabBaseIRIError, "invalid base IRI: #{inspect(base_uri)}"
|
||||
else
|
||||
terms
|
||||
|> detect_invalid_characters
|
||||
|> handle_invalid_characters(handling, terms)
|
||||
base_uri
|
||||
end
|
||||
end
|
||||
|
||||
defp detect_invalid_characters(terms) do
|
||||
aliased_terms = aliased_terms(terms)
|
||||
for {term, _} <- terms, term not in aliased_terms and not valid_characters?(term), do: term
|
||||
end
|
||||
|
||||
defp handle_invalid_characters([], _, terms), do: terms
|
||||
|
||||
defp handle_invalid_characters(invalid_terms, :fail, _) do
|
||||
raise RDF.Namespace.InvalidTermError, """
|
||||
The following terms contain invalid characters:
|
||||
|
||||
- #{Enum.join(invalid_terms, "\n- ")}
|
||||
|
||||
You have the following options:
|
||||
|
||||
- if you are in control of the vocabulary, consider renaming the resource
|
||||
- define an alias with the :alias option on defvocab
|
||||
- change the handling of invalid characters with the :invalid_characters option on defvocab
|
||||
- ignore the resource with the :ignore option on defvocab
|
||||
"""
|
||||
end
|
||||
|
||||
defp handle_invalid_characters(invalid_terms, :warn, terms) do
|
||||
Enum.each(invalid_terms, fn term ->
|
||||
IO.warn("'#{term}' is not valid term, since it contains invalid characters")
|
||||
end)
|
||||
|
||||
terms
|
||||
end
|
||||
|
||||
defp valid_characters?(term) when is_atom(term),
|
||||
do: valid_characters?(Atom.to_string(term))
|
||||
|
||||
defp valid_characters?(term),
|
||||
do: Regex.match?(~r/^[a-zA-Z_]\w*$/, term)
|
||||
|
||||
defp validate_case!(terms, nil, _, _), do: terms
|
||||
|
||||
defp validate_case!(terms, data, base_iri, opts) do
|
||||
if (handling = Keyword.get(opts, :case_violations, :warn)) == :ignore do
|
||||
terms
|
||||
else
|
||||
terms
|
||||
|> detect_case_violations(data, base_iri)
|
||||
|> group_case_violations
|
||||
|> handle_case_violations(handling, terms, base_iri, opts)
|
||||
end
|
||||
end
|
||||
|
||||
defp detect_case_violations(terms, data, base_iri) do
|
||||
aliased_terms = aliased_terms(terms)
|
||||
|
||||
terms
|
||||
|> Enum.filter(fn {term, _} ->
|
||||
not (Atom.to_string(term) |> String.starts_with?("_"))
|
||||
end)
|
||||
|> Enum.filter(fn
|
||||
{term, true} ->
|
||||
if term not in aliased_terms do
|
||||
improper_case?(term, base_iri, Atom.to_string(term), data)
|
||||
end
|
||||
|
||||
{term, original_term} ->
|
||||
improper_case?(term, base_iri, original_term, data)
|
||||
end)
|
||||
end
|
||||
|
||||
defp improper_case?(term, base_iri, iri_suffix, data) do
|
||||
case ResourceClassifier.property?(term_to_iri(base_iri, iri_suffix), data) do
|
||||
true -> not downcase?(term)
|
||||
false -> downcase?(term)
|
||||
nil -> downcase?(term)
|
||||
end
|
||||
end
|
||||
|
||||
defp group_case_violations(violations) do
|
||||
violations
|
||||
|> Enum.group_by(fn
|
||||
{term, true} ->
|
||||
if downcase?(term),
|
||||
do: :lowercased_term,
|
||||
else: :capitalized_term
|
||||
|
||||
{term, _original} ->
|
||||
if downcase?(term),
|
||||
do: :lowercased_alias,
|
||||
else: :capitalized_alias
|
||||
end)
|
||||
end
|
||||
|
||||
defp handle_case_violations(%{} = violations, _, terms, _, _) when map_size(violations) == 0,
|
||||
do: terms
|
||||
|
||||
defp handle_case_violations(violations, :fail, _, base_iri, _) do
|
||||
resource_name_violations = fn violations ->
|
||||
violations
|
||||
|> Enum.map(fn {term, true} -> term_to_iri(base_iri, term) end)
|
||||
|> Enum.map(&to_string/1)
|
||||
|> Enum.join("\n- ")
|
||||
end
|
||||
|
||||
alias_violations = fn violations ->
|
||||
violations
|
||||
|> Enum.map(fn {term, original} ->
|
||||
"alias #{term} for #{term_to_iri(base_iri, original)}"
|
||||
end)
|
||||
|> Enum.join("\n- ")
|
||||
end
|
||||
|
||||
violation_error_lines =
|
||||
violations
|
||||
|> Enum.map(fn
|
||||
{:capitalized_term, violations} ->
|
||||
"""
|
||||
Terms for properties should be lowercased, but the following properties are
|
||||
capitalized:
|
||||
|
||||
- #{resource_name_violations.(violations)}
|
||||
|
||||
"""
|
||||
|
||||
{:lowercased_term, violations} ->
|
||||
"""
|
||||
Terms for non-property resource should be capitalized, but the following
|
||||
non-properties are lowercased:
|
||||
|
||||
- #{resource_name_violations.(violations)}
|
||||
|
||||
"""
|
||||
|
||||
{:capitalized_alias, violations} ->
|
||||
"""
|
||||
Terms for properties should be lowercased, but the following aliases for
|
||||
properties are capitalized:
|
||||
|
||||
- #{alias_violations.(violations)}
|
||||
|
||||
"""
|
||||
|
||||
{:lowercased_alias, violations} ->
|
||||
"""
|
||||
Terms for non-property resource should be capitalized, but the following
|
||||
aliases for non-properties are lowercased:
|
||||
|
||||
- #{alias_violations.(violations)}
|
||||
|
||||
"""
|
||||
end)
|
||||
|> Enum.join()
|
||||
|
||||
raise RDF.Namespace.InvalidTermError, """
|
||||
Case violations detected
|
||||
|
||||
#{violation_error_lines}
|
||||
You have the following options:
|
||||
|
||||
- if you are in control of the vocabulary, consider renaming the resource
|
||||
- define a properly cased alias with the :alias option on defvocab
|
||||
- change the handling of case violations with the :case_violations option on defvocab
|
||||
- ignore the resource with the :ignore option on defvocab
|
||||
"""
|
||||
end
|
||||
|
||||
defp handle_case_violations(violations, :warn, terms, base_iri, _) do
|
||||
for {type, violations} <- violations,
|
||||
{term, original} <- violations do
|
||||
case_violation_warning(type, term, original, base_iri)
|
||||
end
|
||||
|
||||
terms
|
||||
end
|
||||
|
||||
defp case_violation_warning(:capitalized_term, term, _, base_iri) do
|
||||
IO.warn("'#{term_to_iri(base_iri, term)}' is a capitalized property")
|
||||
end
|
||||
|
||||
defp case_violation_warning(:lowercased_term, term, _, base_iri) do
|
||||
IO.warn("'#{term_to_iri(base_iri, term)}' is a lowercased non-property resource")
|
||||
end
|
||||
|
||||
defp case_violation_warning(:capitalized_alias, term, _, _) do
|
||||
IO.warn("capitalized alias '#{term}' for a property")
|
||||
end
|
||||
|
||||
defp case_violation_warning(:lowercased_alias, term, _, _) do
|
||||
IO.warn("lowercased alias '#{term}' for a non-property resource")
|
||||
end
|
||||
|
||||
defp 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 rdf_data_env do
|
||||
import RDF.Sigils, warn: false
|
||||
__ENV__
|
||||
end
|
||||
|
||||
defp rdf_data_vocab_terms(data, base_iri) do
|
||||
data
|
||||
|> RDF.Data.resources()
|
||||
|> Stream.filter(&match?(%RDF.IRI{}, &1))
|
||||
|> Stream.map(&to_string/1)
|
||||
|> Stream.map(&strip_base_iri(&1, base_iri))
|
||||
|> Stream.filter(&vocab_term?/1)
|
||||
|> Enum.map(&String.to_atom/1)
|
||||
end
|
||||
|
||||
defp group_terms_by_case(terms) do
|
||||
terms
|
||||
|> Enum.group_by(fn {term, _} ->
|
||||
if downcase?(term),
|
||||
do: :lowercased,
|
||||
else: :capitalized
|
||||
end)
|
||||
|> Map.new(fn {group, term_mapping} ->
|
||||
{group, Map.new(term_mapping)}
|
||||
end)
|
||||
end
|
||||
|
||||
defp strip_base_iri(iri, base_iri) do
|
||||
if String.starts_with?(iri, base_iri) do
|
||||
String.replace_prefix(iri, base_iri, "")
|
||||
end
|
||||
end
|
||||
|
||||
defp vocab_term?(""), do: false
|
||||
defp vocab_term?(term) when is_binary(term), do: not String.contains?(term, "/")
|
||||
defp vocab_term?(_), do: false
|
||||
|
||||
@doc false
|
||||
@spec term_to_iri(String.t(), String.t() | atom) :: RDF.IRI.t()
|
||||
def term_to_iri(base_iri, term) when is_atom(term),
|
||||
do: term_to_iri(base_iri, Atom.to_string(term))
|
||||
|
||||
def term_to_iri(base_iri, term),
|
||||
do: RDF.iri(base_iri <> term)
|
||||
|
||||
@doc false
|
||||
@spec vocabulary_namespace?(module) :: boolean
|
||||
def vocabulary_namespace?(name) do
|
||||
|
|
|
@ -26,6 +26,12 @@ defmodule RDF.Vocabulary.NamespaceTest do
|
|||
RDF.Vocabulary.NamespaceTest.NSWithIgnoredTerms.ExampleIgnoredLowercasedTermWithAlias}
|
||||
@compile {:no_warn_undefined,
|
||||
RDF.Vocabulary.NamespaceTest.NSWithIgnoredTerms.ExampleIgnoredLowercasedAlias}
|
||||
@compile {:no_warn_undefined,
|
||||
RDF.Vocabulary.NamespaceTest.NSWithExplicitlyIgnoredElixirTerms.Example}
|
||||
@compile {:no_warn_undefined,
|
||||
RDF.Vocabulary.NamespaceTest.IgnoredAliasTest.ExampleIgnoredLowercasedAlias}
|
||||
@compile {:no_warn_undefined,
|
||||
RDF.Vocabulary.NamespaceTest.IgnoredAliasTest.ExampleIgnoredCapitalizedAlias}
|
||||
|
||||
defmodule TestNS do
|
||||
use RDF.Vocabulary.Namespace
|
||||
|
@ -92,6 +98,15 @@ defmodule RDF.Vocabulary.NamespaceTest do
|
|||
term4: "term-4"
|
||||
]
|
||||
|
||||
defvocab StrictExampleFromImplicitAliasedTerms,
|
||||
base_iri: "http://example.com/strict_from_aliased_terms#",
|
||||
terms: [
|
||||
Term1: "term1",
|
||||
term2: "Term2",
|
||||
Term3: "Term-3",
|
||||
term4: "term-4"
|
||||
]
|
||||
|
||||
defvocab NonStrictExampleFromAliasedTerms,
|
||||
base_iri: "http://example.com/non_strict_from_aliased_terms#",
|
||||
terms: ~w[],
|
||||
|
@ -107,11 +122,15 @@ defmodule RDF.Vocabulary.NamespaceTest do
|
|||
base_iri: "http://example.com/ex#",
|
||||
terms: ~w[bar Bar],
|
||||
alias: [foo: "bar", baz: "bar", Foo: "Bar", Baz: "Bar"]
|
||||
|
||||
defvocab ExampleWithSynonymImplicitAliases,
|
||||
base_iri: "http://example.com/ex#",
|
||||
terms: [foo: "bar", baz: "bar", Foo: "Bar", Baz: "Bar"]
|
||||
end
|
||||
|
||||
describe "defvocab with bad base iri" do
|
||||
test "without a base_iri, an error is raised" do
|
||||
assert_raise KeyError, fn ->
|
||||
assert_raise RDF.Namespace.InvalidVocabBaseIRIError, fn ->
|
||||
defmodule NSWithoutBaseIRI do
|
||||
use RDF.Vocabulary.Namespace
|
||||
|
||||
|
@ -409,7 +428,7 @@ defmodule RDF.Vocabulary.NamespaceTest do
|
|||
assert %Description{} = EX.S |> Example.update_in(EX.O1, EX.O2)
|
||||
end
|
||||
|
||||
describe "defvocab with invalid terms" do
|
||||
describe "defvocab with reserved terms" do
|
||||
test "terms with a special meaning for Elixir cause a failure" do
|
||||
assert_raise RDF.Namespace.InvalidTermError, ~r/unquote_splicing/s, fn ->
|
||||
defmodule NSWithElixirTerms do
|
||||
|
@ -417,62 +436,79 @@ defmodule RDF.Vocabulary.NamespaceTest do
|
|||
|
||||
defvocab Example,
|
||||
base_iri: "http://example.com/example#",
|
||||
terms: RDF.Vocabulary.Namespace.invalid_terms()
|
||||
terms: RDF.Namespace.Builder.reserved_terms()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
test "alias terms with a special meaning for Elixir cause a failure" do
|
||||
assert_raise RDF.Namespace.InvalidTermError, ~r/unquote_splicing/s, fn ->
|
||||
defmodule NSWithElixirAliasTerms do
|
||||
use RDF.Vocabulary.Namespace
|
||||
assert_raise RDF.Namespace.InvalidAliasError,
|
||||
~r/alias 'and' in vocabulary namespace.*Example is a reserved term and can't be used as an alias/s,
|
||||
fn ->
|
||||
defmodule NSWithElixirAliasTerms do
|
||||
use RDF.Vocabulary.Namespace
|
||||
|
||||
defvocab Example,
|
||||
base_iri: "http://example.com/example#",
|
||||
terms: ~w[foo],
|
||||
alias: [
|
||||
and: "foo",
|
||||
or: "foo",
|
||||
xor: "foo",
|
||||
in: "foo",
|
||||
fn: "foo",
|
||||
def: "foo",
|
||||
when: "foo",
|
||||
if: "foo",
|
||||
for: "foo",
|
||||
case: "foo",
|
||||
with: "foo",
|
||||
quote: "foo",
|
||||
unquote: "foo",
|
||||
unquote_splicing: "foo",
|
||||
alias: "foo",
|
||||
import: "foo",
|
||||
require: "foo",
|
||||
super: "foo",
|
||||
__aliases__: "foo"
|
||||
]
|
||||
end
|
||||
end
|
||||
defvocab Example,
|
||||
base_iri: "http://example.com/example#",
|
||||
terms: ~w[foo],
|
||||
alias: [
|
||||
and: "foo",
|
||||
or: "foo",
|
||||
xor: "foo",
|
||||
in: "foo",
|
||||
fn: "foo",
|
||||
def: "foo",
|
||||
when: "foo",
|
||||
if: "foo",
|
||||
for: "foo",
|
||||
case: "foo",
|
||||
with: "foo",
|
||||
quote: "foo",
|
||||
unquote: "foo",
|
||||
unquote_splicing: "foo",
|
||||
alias: "foo",
|
||||
import: "foo",
|
||||
require: "foo",
|
||||
super: "foo",
|
||||
__aliases__: "foo"
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
test "terms with a special meaning for Elixir don't cause a failure when they are ignored" do
|
||||
test "terms with a special meaning for Elixir don't cause a failure when they are ignored via invalid_terms: :ignore" do
|
||||
defmodule NSWithIgnoredElixirTerms do
|
||||
use RDF.Vocabulary.Namespace
|
||||
|
||||
defvocab Example,
|
||||
base_iri: "http://example.com/example#",
|
||||
terms: RDF.Vocabulary.Namespace.invalid_terms(),
|
||||
ignore: RDF.Vocabulary.Namespace.invalid_terms()
|
||||
terms: RDF.Namespace.Builder.reserved_terms() ++ [:foo],
|
||||
invalid_terms: :ignore
|
||||
|
||||
assert NSWithIgnoredElixirTerms.Example.__terms__() == [:foo]
|
||||
end
|
||||
end
|
||||
|
||||
test "terms with a special meaning for Elixir don't cause a failure when they are ignored explicitly" do
|
||||
defmodule NSWithExplicitlyIgnoredElixirTerms do
|
||||
use RDF.Vocabulary.Namespace
|
||||
|
||||
defvocab Example,
|
||||
base_iri: "http://example.com/example#",
|
||||
terms: RDF.Namespace.Builder.reserved_terms(),
|
||||
ignore: RDF.Namespace.Builder.reserved_terms()
|
||||
end
|
||||
|
||||
assert NSWithExplicitlyIgnoredElixirTerms.Example.__terms__() == []
|
||||
end
|
||||
|
||||
test "terms with a special meaning for Elixir don't cause a failure when an alias is defined" do
|
||||
defmodule NSWithAliasesForElixirTerms do
|
||||
use RDF.Vocabulary.Namespace
|
||||
|
||||
defvocab Example,
|
||||
base_iri: "http://example.com/example#",
|
||||
terms: RDF.Vocabulary.Namespace.invalid_terms(),
|
||||
terms: RDF.Namespace.Builder.reserved_terms(),
|
||||
alias: [
|
||||
and_: "and",
|
||||
or_: "or",
|
||||
|
@ -548,11 +584,25 @@ defmodule RDF.Vocabulary.NamespaceTest do
|
|||
assert Example.function_exported() == ~I<http://example.com/example#function_exported?>
|
||||
assert Example.macro_exported() == ~I<http://example.com/example#macro_exported?>
|
||||
end
|
||||
|
||||
test "failures for reserved as aliased can't be ignored" do
|
||||
assert_raise RDF.Namespace.InvalidAliasError, ~r/super/s, fn ->
|
||||
defmodule NSWithElixirAliasTerms do
|
||||
use RDF.Vocabulary.Namespace
|
||||
|
||||
defvocab Example,
|
||||
base_iri: "http://example.com/example#",
|
||||
terms: ~w[foo],
|
||||
alias: [super: "foo"],
|
||||
invalid_terms: :ignore
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "defvocab invalid character handling" do
|
||||
test "when a term contains unallowed characters and no alias defined, it fails when invalid_characters = :fail" do
|
||||
assert_raise RDF.Namespace.InvalidTermError, ~r/Foo-bar.*foo-bar/s, fn ->
|
||||
test "when a term contains disallowed characters and no alias defined, it fails when invalid_characters: :fail" do
|
||||
assert_raise RDF.Namespace.InvalidTermError, ~r/foo-bar.*Foo-bar/s, fn ->
|
||||
defmodule NSWithInvalidTerms1 do
|
||||
use RDF.Vocabulary.Namespace
|
||||
|
||||
|
@ -563,7 +613,7 @@ defmodule RDF.Vocabulary.NamespaceTest do
|
|||
end
|
||||
end
|
||||
|
||||
test "when a term contains unallowed characters it does not fail when invalid_characters = :ignore" do
|
||||
test "when a term contains disallowed characters it does not fail when invalid_characters: :ignore" do
|
||||
defmodule NSWithInvalidTerms2 do
|
||||
use RDF.Vocabulary.Namespace
|
||||
|
||||
|
@ -573,6 +623,22 @@ defmodule RDF.Vocabulary.NamespaceTest do
|
|||
invalid_characters: :ignore
|
||||
end
|
||||
end
|
||||
|
||||
test "when a term contains disallowed characters it does not fail when invalid_characters: :warn" do
|
||||
err =
|
||||
ExUnit.CaptureIO.capture_io(:stderr, fn ->
|
||||
defmodule NSWithInvalidTerms3 do
|
||||
use RDF.Vocabulary.Namespace
|
||||
|
||||
defvocab Example,
|
||||
base_iri: "http://example.com/example#",
|
||||
terms: ~w[Foo-bar foo-bar],
|
||||
invalid_characters: :warn
|
||||
end
|
||||
end)
|
||||
|
||||
assert err =~ "'foo-bar' is not valid term, since it contains invalid characters"
|
||||
end
|
||||
end
|
||||
|
||||
describe "defvocab case violation handling" do
|
||||
|
@ -751,18 +817,6 @@ defmodule RDF.Vocabulary.NamespaceTest do
|
|||
alias: [bar: "Bar"],
|
||||
ignore: ~w[Bar]a
|
||||
|
||||
defvocab ExampleIgnoredLowercasedAlias,
|
||||
base_iri: "http://example.com/",
|
||||
terms: ~w[foo Bar],
|
||||
alias: [bar: "Bar"],
|
||||
ignore: ~w[bar]a
|
||||
|
||||
defvocab ExampleIgnoredCapitalizedAlias,
|
||||
base_iri: "http://example.com/",
|
||||
terms: ~w[foo Bar],
|
||||
alias: [Foo: "foo"],
|
||||
ignore: ~w[Foo]a
|
||||
|
||||
defvocab ExampleIgnoredNonStrictLowercasedTerm,
|
||||
base_iri: "http://example.com/",
|
||||
terms: ~w[],
|
||||
|
@ -809,18 +863,42 @@ defmodule RDF.Vocabulary.NamespaceTest do
|
|||
assert RDF.iri(ExampleIgnoredCapitalizedTermWithAlias.bar()) == ~I<http://example.com/Bar>
|
||||
end
|
||||
|
||||
test "resolution of ignored lowercased alias on a strict vocab fails" do
|
||||
alias NSWithIgnoredTerms.ExampleIgnoredLowercasedAlias
|
||||
test "ignored aliases raise a warning, but are still properly ignored" do
|
||||
err =
|
||||
ExUnit.CaptureIO.capture_io(:stderr, fn ->
|
||||
defmodule IgnoredAliasTest do
|
||||
use RDF.Vocabulary.Namespace
|
||||
|
||||
defvocab ExampleIgnoredLowercasedAlias,
|
||||
base_iri: "http://example.com/",
|
||||
terms: ~w[foo Bar],
|
||||
alias: [bar: "Bar"],
|
||||
ignore: ~w[bar]a
|
||||
|
||||
defvocab ExampleIgnoredCapitalizedAlias,
|
||||
base_iri: "http://example.com/",
|
||||
terms: ~w[foo Bar],
|
||||
alias: [Foo: "foo"],
|
||||
ignore: ~w[Foo]a
|
||||
end
|
||||
end)
|
||||
|
||||
assert err =~ "warning"
|
||||
assert err =~ "ignoring alias 'bar'"
|
||||
assert err =~ "ignoring alias 'Foo'"
|
||||
|
||||
alias RDF.Vocabulary.NamespaceTest.IgnoredAliasTest.{
|
||||
ExampleIgnoredLowercasedAlias,
|
||||
ExampleIgnoredCapitalizedAlias
|
||||
}
|
||||
|
||||
assert ExampleIgnoredLowercasedAlias.__terms__() == [:Bar, :foo]
|
||||
assert RDF.iri(ExampleIgnoredLowercasedAlias.Bar) == ~I<http://example.com/Bar>
|
||||
|
||||
assert_raise UndefinedFunctionError, fn ->
|
||||
RDF.iri(ExampleIgnoredLowercasedAlias.bar())
|
||||
end
|
||||
end
|
||||
|
||||
test "resolution of ignored capitalized alias on a strict vocab fails" do
|
||||
alias NSWithIgnoredTerms.ExampleIgnoredCapitalizedAlias
|
||||
assert ExampleIgnoredCapitalizedAlias.__terms__() == [:Bar, :foo]
|
||||
assert RDF.iri(ExampleIgnoredCapitalizedAlias.foo()) == ~I<http://example.com/foo>
|
||||
|
||||
|
@ -931,9 +1009,9 @@ defmodule RDF.Vocabulary.NamespaceTest do
|
|||
end
|
||||
|
||||
test "includes aliases" do
|
||||
assert length(StrictExampleFromAliasedTerms.__terms__()) == 8
|
||||
assert length(StrictExampleFromAliasedTerms.__terms__()) == 6
|
||||
|
||||
for term <- ~w[term1 Term1 term2 Term2 Term3 term4 Term-3 term-4]a do
|
||||
for term <- ~w[term1 Term1 term2 Term2 Term3 term4]a do
|
||||
assert term in StrictExampleFromAliasedTerms.__terms__()
|
||||
end
|
||||
end
|
||||
|
@ -958,7 +1036,12 @@ defmodule RDF.Vocabulary.NamespaceTest do
|
|||
end
|
||||
|
||||
describe "term resolution in a strict vocab namespace" do
|
||||
alias TestNS.{ExampleFromGraph, ExampleFromNTriplesFile, StrictExampleFromTerms}
|
||||
alias TestNS.{
|
||||
EXS,
|
||||
ExampleFromGraph,
|
||||
ExampleFromNTriplesFile,
|
||||
StrictExampleFromTerms
|
||||
}
|
||||
|
||||
test "undefined terms" do
|
||||
assert_raise UndefinedFunctionError, fn ->
|
||||
|
@ -996,6 +1079,7 @@ defmodule RDF.Vocabulary.NamespaceTest do
|
|||
end
|
||||
|
||||
test "lowercased terms" do
|
||||
assert EXS.foo() == ~I<http://example.com/strict#foo>
|
||||
assert ExampleFromGraph.foo() == ~I<http://example.com/from_graph#foo>
|
||||
assert RDF.iri(ExampleFromGraph.foo()) == ~I<http://example.com/from_graph#foo>
|
||||
|
||||
|
@ -1058,24 +1142,38 @@ defmodule RDF.Vocabulary.NamespaceTest do
|
|||
end
|
||||
|
||||
describe "term resolution of aliases on a strict vocabulary" do
|
||||
alias TestNS.StrictExampleFromAliasedTerms, as: Example
|
||||
alias TestNS.StrictExampleFromAliasedTerms, as: Ex1
|
||||
alias TestNS.StrictExampleFromImplicitAliasedTerms, as: Ex2
|
||||
|
||||
test "the alias resolves to the correct IRI" do
|
||||
assert RDF.iri(Example.Term1) == ~I<http://example.com/strict_from_aliased_terms#term1>
|
||||
assert RDF.iri(Example.term2()) == ~I<http://example.com/strict_from_aliased_terms#Term2>
|
||||
assert RDF.iri(Example.Term3) == ~I<http://example.com/strict_from_aliased_terms#Term-3>
|
||||
assert RDF.iri(Example.term4()) == ~I<http://example.com/strict_from_aliased_terms#term-4>
|
||||
assert RDF.iri(Ex1.Term1) == ~I<http://example.com/strict_from_aliased_terms#term1>
|
||||
assert RDF.iri(Ex1.term2()) == ~I<http://example.com/strict_from_aliased_terms#Term2>
|
||||
assert RDF.iri(Ex1.Term3) == ~I<http://example.com/strict_from_aliased_terms#Term-3>
|
||||
assert RDF.iri(Ex1.term4()) == ~I<http://example.com/strict_from_aliased_terms#term-4>
|
||||
|
||||
assert RDF.iri(Ex2.Term1) == ~I<http://example.com/strict_from_aliased_terms#term1>
|
||||
assert RDF.iri(Ex2.term2()) == ~I<http://example.com/strict_from_aliased_terms#Term2>
|
||||
assert RDF.iri(Ex2.Term3) == ~I<http://example.com/strict_from_aliased_terms#Term-3>
|
||||
assert RDF.iri(Ex2.term4()) == ~I<http://example.com/strict_from_aliased_terms#term-4>
|
||||
end
|
||||
|
||||
test "the old term remains resolvable" do
|
||||
assert RDF.iri(Example.term1()) == ~I<http://example.com/strict_from_aliased_terms#term1>
|
||||
assert RDF.iri(Example.Term2) == ~I<http://example.com/strict_from_aliased_terms#Term2>
|
||||
assert RDF.iri(Ex1.term1()) == ~I<http://example.com/strict_from_aliased_terms#term1>
|
||||
assert RDF.iri(Ex1.Term2) == ~I<http://example.com/strict_from_aliased_terms#Term2>
|
||||
|
||||
assert RDF.iri(Ex2.term1()) == ~I<http://example.com/strict_from_aliased_terms#term1>
|
||||
assert RDF.iri(Ex2.Term2) == ~I<http://example.com/strict_from_aliased_terms#Term2>
|
||||
end
|
||||
|
||||
test "defining multiple aliases for a term" do
|
||||
alias TestNS.ExampleWithSynonymAliases, as: Example
|
||||
assert Example.foo() == Example.baz()
|
||||
assert RDF.iri(Example.foo()) == RDF.iri(Example.baz())
|
||||
alias TestNS.ExampleWithSynonymAliases, as: Ex1
|
||||
alias TestNS.ExampleWithSynonymImplicitAliases, as: Ex2
|
||||
|
||||
assert Ex1.foo() == Ex1.baz()
|
||||
assert RDF.iri(Ex1.foo()) == RDF.iri(Ex1.baz())
|
||||
|
||||
assert Ex2.foo() == Ex2.baz()
|
||||
assert RDF.iri(Ex2.foo()) == RDF.iri(Ex2.baz())
|
||||
end
|
||||
end
|
||||
|
||||
|
|
Loading…
Reference in a new issue