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