Add support for adding terms of a vocab namespace to RDF.PropertyMap

For now only with RDF.PropertyMap.add/2 on purpose, since we want to
enforce a conscious usage of this feature, as put/2 would silently
overwrite terms.
This commit is contained in:
Marcel Otto 2020-10-11 11:42:30 +02:00
parent 5d9ddeb7fe
commit eef64b9253
5 changed files with 91 additions and 12 deletions

View file

@ -3,6 +3,8 @@ defmodule RDF.PropertyMap do
terms: %{}
alias RDF.IRI
import RDF.Guards
import RDF.Utils, only: [downcase?: 1]
@type t :: %__MODULE__{
iris: %{atom => IRI.t()},
@ -46,6 +48,20 @@ defmodule RDF.PropertyMap do
do_set(property_map, :add, coerce_term(term), IRI.new(iri))
end
def add(%__MODULE__{} = property_map, vocab_namespace) when maybe_ns_term(vocab_namespace) do
cond do
not RDF.Vocabulary.Namespace.vocabulary_namespace?(vocab_namespace) ->
raise ArgumentError, "expected a vocabulary namespace, but got #{vocab_namespace}"
not apply(vocab_namespace, :__strict__, []) ->
raise ArgumentError,
"expected a strict vocabulary namespace, but #{vocab_namespace} is non-strict"
true ->
add(property_map, mapping_from_vocab_namespace(vocab_namespace))
end
end
def add(%__MODULE__{} = property_map, mappings) do
Enum.reduce_while(mappings, {:ok, property_map}, fn {term, iri}, {:ok, property_map} ->
with {:ok, property_map} <- add(property_map, term, iri) do
@ -128,6 +144,23 @@ defmodule RDF.PropertyMap do
defp coerce_term(term) when is_atom(term), do: term
defp coerce_term(term) when is_binary(term), do: String.to_atom(term)
defp mapping_from_vocab_namespace(vocab_namespace) do
aliases = apply(vocab_namespace, :__term_aliases__, [])
apply(vocab_namespace, :__terms__, [])
|> Enum.filter(&downcase?/1)
|> Enum.map(fn term -> {term, apply(vocab_namespace, term, [])} end)
|> Enum.group_by(fn {_term, iri} -> iri end)
|> Map.new(fn
{_, [mapping]} ->
mapping
{_, mappings} ->
Enum.find(mappings, fn {term, _iri} -> term in aliases end) ||
raise "conflicting non-alias terms for IRI should not occur in a vocab namespace"
end)
end
@impl Access
def pop(%__MODULE__{} = property_map, term) do
case Access.pop(property_map.iris, coerce_term(term)) do

View file

@ -1,6 +1,9 @@
defmodule RDF.Utils do
@moduledoc false
def downcase?(term) when is_atom(term), do: term |> Atom.to_string() |> downcase?()
def downcase?(term), do: term =~ ~r/^(_|\p{Ll})/u
def lazy_map_update(map, key, init_fun, fun) do
case map do
%{^key => value} ->

View file

@ -12,6 +12,8 @@ defmodule RDF.Vocabulary.Namespace do
alias RDF.Description
alias RDF.Utils.ResourceClassifier
import RDF.Utils, only: [downcase?: 1]
@vocabs_dir "priv/vocabs"
defmacro __using__(_opts) do
@ -75,6 +77,13 @@ defmodule RDF.Vocabulary.Namespace do
@impl Elixir.RDF.Namespace
def __terms__, do: @terms |> Map.keys()
@spec __term_aliases__ :: [atom]
def __term_aliases__ do
@terms
|> Enum.filter(fn {_, term} -> term != true end)
|> Enum.map(fn {alias, _} -> alias end)
end
@ignored_terms unquote(Macro.escape(ignored_terms))
@doc """
@ -440,9 +449,9 @@ defmodule RDF.Vocabulary.Namespace do
defp proper_case?(term, base_iri, iri_suffix, data) do
case ResourceClassifier.property?(term_to_iri(base_iri, iri_suffix), data) do
true -> not lowercase?(term)
false -> lowercase?(term)
nil -> lowercase?(term)
true -> not downcase?(term)
false -> downcase?(term)
nil -> downcase?(term)
end
end
@ -450,12 +459,12 @@ defmodule RDF.Vocabulary.Namespace do
violations
|> Enum.group_by(fn
{term, true} ->
if lowercase?(term),
if downcase?(term),
do: :lowercased_term,
else: :capitalized_term
{term, _original} ->
if lowercase?(term),
if downcase?(term),
do: :lowercased_alias,
else: :capitalized_alias
end)
@ -595,7 +604,7 @@ defmodule RDF.Vocabulary.Namespace do
defp group_terms_by_case(terms) do
terms
|> Enum.group_by(fn {term, _} ->
if lowercase?(term),
if downcase?(term),
do: :lowercased,
else: :capitalized
end)
@ -604,12 +613,6 @@ defmodule RDF.Vocabulary.Namespace do
end)
end
defp lowercase?(term) when is_atom(term),
do: Atom.to_string(term) |> lowercase?
defp lowercase?(term),
do: term =~ ~r/^(_|\p{Ll})/u
defp strip_base_iri(iri, base_iri) do
if String.starts_with?(iri, base_iri) do
String.replace_prefix(iri, base_iri, "")

View file

@ -317,6 +317,10 @@ defmodule RDF.DescriptionTest do
assert description_includes_predication(desc, {EX.p2(), iri(EX.Object2)})
assert description_includes_predication(desc, {EX.p2(), iri(EX.Object3)})
assert description_includes_predication(desc, {EX.predicate(), iri(EX.Object4)})
desc = Description.add(description(), [type: EX.Class], context: RDF.NS.RDF)
assert Description.count(desc) == 1
assert description_includes_predication(desc, {RDF.type(), iri(EX.Class)})
end
test "triples with another subject are ignored" do

View file

@ -5,6 +5,15 @@ defmodule RDF.PropertyMapTest do
alias RDF.PropertyMap
defmodule TestNS do
use RDF.Vocabulary.Namespace
defvocab ExampleWithConflict,
base_iri: "http://example.com/",
terms: ~w[term],
alias: [alias_term: "term"]
end
@example_property_map %PropertyMap{
iris: %{
foo: ~I<http://example.com/test/foo>,
@ -130,6 +139,33 @@ defmodule RDF.PropertyMapTest do
)
end
test "with a strict vocabulary namespace" do
assert PropertyMap.add(PropertyMap.new(), RDF.NS.RDF) ==
{:ok,
PropertyMap.new(
type: RDF.type(),
first: RDF.first(),
langString: RDF.langString(),
nil: RDF.nil(),
object: RDF.object(),
predicate: RDF.predicate(),
rest: RDF.rest(),
subject: RDF.subject(),
value: RDF.value()
)}
end
test "with a vocabulary namespace with multiple terms for the same IRI" do
assert PropertyMap.add(PropertyMap.new(), TestNS.ExampleWithConflict) ==
{:ok, PropertyMap.new(alias_term: "http://example.com/term")}
end
test "with a non-strict vocabulary namespace" do
assert_raise ArgumentError, ~r/non-strict/, fn ->
PropertyMap.add(PropertyMap.new(), EX)
end
end
test "when a mapping to the same IRI exists" do
assert PropertyMap.add(@example_property_map,
foo: ~I<http://example.com/test/foo>,