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:
parent
5d9ddeb7fe
commit
eef64b9253
5 changed files with 91 additions and 12 deletions
|
@ -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
|
||||
|
|
|
@ -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} ->
|
||||
|
|
|
@ -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, "")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>,
|
||||
|
|
Loading…
Reference in a new issue