Rewrite RDF.Vocabulary.Namespace

This commit is contained in:
Marcel Otto 2022-06-05 01:20:27 +02:00
parent 52369c289c
commit 9449fce988
7 changed files with 744 additions and 678 deletions

View file

@ -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

View file

@ -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))}}

View 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

View 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

View 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

View file

@ -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

View file

@ -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