rdf-ex/lib/rdf/property_map.ex
Marcel Otto eef64b9253 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.
2020-10-11 11:42:30 +02:00

221 lines
6.4 KiB
Elixir

defmodule RDF.PropertyMap do
defstruct iris: %{},
terms: %{}
alias RDF.IRI
import RDF.Guards
import RDF.Utils, only: [downcase?: 1]
@type t :: %__MODULE__{
iris: %{atom => IRI.t()},
terms: %{IRI.t() => atom}
}
@behaviour Access
def new(), do: %__MODULE__{}
def new(%__MODULE__{} = initial), do: initial
def new(initial) do
{:ok, property_map} = new() |> add(initial)
property_map
end
def iri(%__MODULE__{} = property_map, term) do
Map.get(property_map.iris, coerce_term(term))
end
def term(%__MODULE__{} = property_map, iri) do
Map.get(property_map.terms, IRI.new(iri))
end
def iri_defined?(%__MODULE__{} = property_map, term) do
Map.has_key?(property_map.iris, coerce_term(term))
end
def term_defined?(%__MODULE__{} = property_map, iri) do
Map.has_key?(property_map.terms, IRI.new(iri))
end
@impl Access
def fetch(%__MODULE__{} = property_map, term) do
Access.fetch(property_map.iris, coerce_term(term))
end
def add(%__MODULE__{} = property_map, term, iri) 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
{:cont, {:ok, property_map}}
else
error -> {:halt, error}
end
end)
end
def put(%__MODULE__{} = property_map, term, iri) do
{:ok, added} = do_set(property_map, :put, coerce_term(term), IRI.new(iri))
added
end
def put(%__MODULE__{} = property_map, mappings) do
Enum.reduce(mappings, property_map, fn {term, iri}, property_map ->
put(property_map, term, iri)
end)
end
defp do_set(property_map, op, term, iri) do
do_set(property_map, op, term, iri, Map.get(property_map.iris, term))
end
defp do_set(property_map, op, term, new_iri, old_iri) do
do_set(property_map, op, term, new_iri, old_iri, Map.get(property_map.terms, new_iri))
end
defp do_set(property_map, _, _, iri, iri, _), do: {:ok, property_map}
defp do_set(property_map, _, term, iri, nil, nil) do
{:ok,
%__MODULE__{
property_map
| iris: Map.put(property_map.iris, term, iri),
terms: Map.put(property_map.terms, iri, term)
}}
end
defp do_set(_context, :add, term, new_iri, old_iri, nil) do
{:error, "conflicting mapping for #{term}: #{new_iri}; already mapped to #{old_iri}"}
end
defp do_set(_context, :add, term, iri, _, old_term) do
{:error,
"conflicting mapping for #{term}: #{iri}; IRI already mapped to #{inspect(old_term)}"}
end
defp do_set(property_map, :put, term, new_iri, old_iri, nil) do
%__MODULE__{property_map | terms: Map.delete(property_map.terms, old_iri)}
|> do_set(:put, term, new_iri, nil, nil)
end
defp do_set(property_map, :put, term, new_iri, old_iri, old_term) do
%__MODULE__{property_map | iris: Map.delete(property_map.iris, old_term)}
|> do_set(:put, term, new_iri, old_iri, nil)
end
def delete(%__MODULE__{} = property_map, term) do
term = coerce_term(term)
if iri = Map.get(property_map.iris, term) do
%__MODULE__{
property_map
| iris: Map.delete(property_map.iris, term),
terms: Map.delete(property_map.terms, iri)
}
else
property_map
end
end
def drop(%__MODULE__{} = property_map, terms) when is_list(terms) do
Enum.reduce(terms, property_map, fn term, property_map ->
delete(property_map, term)
end)
end
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
{nil, _} ->
{nil, property_map}
{iri, new_context_map} ->
{iri, %__MODULE__{iris: new_context_map}}
end
end
@impl Access
def get_and_update(property_map, term, fun) do
term = coerce_term(term)
current = iri(property_map, term)
case fun.(current) do
{old_iri, new_iri} ->
{:ok, property_map} = do_set(property_map, :put, term, IRI.new(new_iri), IRI.new(old_iri))
{old_iri, property_map}
:pop ->
{current, delete(property_map, term)}
other ->
raise "the given function must return a two-element tuple or :pop, got: #{inspect(other)}"
end
end
defimpl Enumerable do
alias RDF.PropertyMap
def reduce(%PropertyMap{iris: iris}, acc, fun), do: Enumerable.reduce(iris, acc, fun)
def member?(%PropertyMap{iris: iris}, mapping), do: Enumerable.member?(iris, mapping)
def count(%PropertyMap{iris: iris}), do: Enumerable.count(iris)
def slice(_property_map), do: {:error, __MODULE__}
end
defimpl Inspect do
import Inspect.Algebra
def inspect(property_map, opts) do
map = Map.to_list(property_map.iris)
open = color("%RDF.PropertyMap{", :map, opts)
sep = color(",", :map, opts)
close = color("}", :map, opts)
container_doc(open, map, close, opts, &to_map(&1, &2, color(" <=> ", :map, opts)),
separator: sep,
break: :strict
)
end
defp to_map({key, value}, opts, sep) do
concat(concat(to_doc(key, opts), sep), to_doc(value, opts))
end
end
end