rdf-ex/lib/rdf/prefix_map.ex

448 lines
14 KiB
Elixir
Raw Normal View History

2019-03-25 23:31:43 +00:00
defmodule RDF.PrefixMap do
@moduledoc """
A mapping a prefix atoms to IRI namespaces.
`RDF.PrefixMap` implements the `Enumerable` protocol.
"""
2020-02-28 17:51:48 +00:00
alias RDF.IRI
2020-06-29 08:37:42 +00:00
@type prefix :: atom
@type namespace :: IRI.t()
2020-03-02 22:42:15 +00:00
2020-06-29 08:37:42 +00:00
@type coercible_prefix :: atom | String.t()
2020-10-13 13:24:31 +00:00
@type coercible_namespace :: RDF.Vocabulary.Namespace.t() | String.t() | IRI.t()
2020-03-02 01:07:31 +00:00
@type prefix_map :: %{prefix => namespace}
2020-03-02 22:42:15 +00:00
@type conflict_resolver ::
(coercible_prefix, coercible_namespace, coercible_namespace -> coercible_namespace)
| :ignore
| :overwrite
2020-03-02 01:07:31 +00:00
2020-02-28 17:51:48 +00:00
@type t :: %__MODULE__{
2020-03-02 01:07:31 +00:00
map: prefix_map
2020-06-29 08:37:42 +00:00
}
2020-02-28 17:51:48 +00:00
2019-03-25 23:31:43 +00:00
defstruct map: %{}
@doc """
2019-03-27 22:47:23 +00:00
Creates an empty `RDF.PrefixMap`.
2019-03-25 23:31:43 +00:00
"""
2020-03-02 01:07:31 +00:00
@spec new :: t
def new, do: %__MODULE__{}
2019-03-25 23:31:43 +00:00
@doc """
2020-10-13 13:24:31 +00:00
Creates a new `RDF.PrefixMap` with initial mappings.
2019-03-25 23:31:43 +00:00
2020-10-13 13:24:31 +00:00
The initial prefix mappings can be passed as keyword lists or maps.
2019-03-25 23:31:43 +00:00
The keys for the prefixes can be given as atoms or strings and will be normalized to atoms.
The namespaces can be given as `RDF.IRI`s or strings and will be normalized to `RDF.IRI`s.
"""
2020-03-02 01:07:31 +00:00
@spec new(t | map | keyword) :: t
2019-03-25 23:31:43 +00:00
def new(map)
def new(%__MODULE__{} = prefix_map), do: prefix_map
2019-03-25 23:31:43 +00:00
def new(map) when is_map(map) do
%__MODULE__{map: Map.new(map, &normalize/1)}
end
def new(map) when is_list(map) do
map |> Map.new() |> new()
end
defp normalize({prefix, namespace}) when is_atom(prefix),
do: {prefix, IRI.coerce_base(namespace)}
2019-03-25 23:31:43 +00:00
defp normalize({prefix, namespace}) when is_binary(prefix),
do: normalize({String.to_atom(prefix), namespace})
defp normalize({prefix, namespace}),
do:
raise(ArgumentError, "Invalid prefix mapping: #{inspect(prefix)} => #{inspect(namespace)}")
2019-03-25 23:31:43 +00:00
@doc """
2020-10-13 13:24:31 +00:00
Adds a prefix mapping to `prefix_map`.
2019-03-25 23:31:43 +00:00
2020-10-13 13:24:31 +00:00
Unless a mapping of `prefix` to a different namespace already exists,
an `:ok` tuple is returned, otherwise an `:error` tuple.
2019-03-25 23:31:43 +00:00
"""
2020-06-29 08:37:42 +00:00
@spec add(t, coercible_prefix, coercible_namespace) :: {:ok, t} | {:error, String.t()}
2019-03-25 23:31:43 +00:00
def add(prefix_map, prefix, namespace)
def add(%__MODULE__{map: map}, prefix, %IRI{} = namespace) when is_atom(prefix) do
if conflicts?(map, prefix, namespace) do
{:error, "prefix #{inspect(prefix)} is already mapped to another namespace"}
else
{:ok, %__MODULE__{map: Map.put(map, prefix, namespace)}}
end
end
def add(%__MODULE__{} = prefix_map, prefix, namespace) do
with {prefix, namespace} = normalize({prefix, namespace}) do
add(prefix_map, prefix, namespace)
end
end
@doc """
Adds a prefix mapping to the given `RDF.PrefixMap` and raises an exception in error cases.
"""
2020-03-02 22:42:15 +00:00
@spec add!(t, coercible_prefix, coercible_namespace) :: t
2019-03-25 23:31:43 +00:00
def add!(prefix_map, prefix, namespace) do
with {:ok, new_prefix_map} <- add(prefix_map, prefix, namespace) do
new_prefix_map
else
{:error, error} -> raise error
end
end
2020-10-21 10:59:20 +00:00
@doc """
Adds a prefix mapping to `prefix_map` overwriting an existing mapping.
"""
@spec put(t, coercible_prefix, coercible_namespace) :: t
def put(prefix_map, prefix, namespace)
def put(%__MODULE__{map: map}, prefix, %IRI{} = namespace) when is_atom(prefix) do
%__MODULE__{map: Map.put(map, prefix, namespace)}
end
def put(%__MODULE__{} = prefix_map, prefix, namespace) do
with {prefix, namespace} = normalize({prefix, namespace}) do
put(prefix_map, prefix, namespace)
end
end
2019-03-25 23:31:43 +00:00
@doc """
Merges two `RDF.PrefixMap`s.
The second prefix map can also be given as any structure which can converted
to a `RDF.PrefixMap` via `new/1`.
If the prefix maps can be merged without conflicts, that is there are no
prefixes mapped to different namespaces an `:ok` tuple is returned.
Otherwise an `:error` tuple with the list of prefixes with conflicting
namespaces is returned.
See also `merge/3` which allows you to resolve conflicts with a function.
2019-03-25 23:31:43 +00:00
"""
2020-06-29 08:37:42 +00:00
@spec merge(t, t | map | keyword) :: {:ok, t} | {:error, [atom | String.t()]}
2019-03-25 23:31:43 +00:00
def merge(prefix_map1, prefix_map2)
def merge(%__MODULE__{map: map1}, %__MODULE__{map: map2}) do
2022-04-10 21:09:44 +00:00
case merge_conflicts(map1, map2) do
[] -> {:ok, %__MODULE__{map: Map.merge(map1, map2)}}
conflicts -> {:error, conflicts}
2019-03-25 23:31:43 +00:00
end
end
def merge(%__MODULE__{} = prefix_map, other_prefixes) do
merge(prefix_map, new(other_prefixes))
rescue
FunctionClauseError ->
raise ArgumentError, "#{inspect(other_prefixes)} is not convertible to a RDF.PrefixMap"
end
@doc """
Merges two `RDF.PrefixMap`s, resolving conflicts through the given `conflict_resolver` function.
The second prefix map can also be given as any structure which can converted
to a `RDF.PrefixMap` via `new/1`.
The given function will be invoked when there are conflicting mappings of
prefixes to different namespaces; its arguments are `prefix`, `namespace1`
(the namespace for the prefix in the first prefix map),
and `namespace2` (the namespace for the prefix in the second prefix map).
The value returned by the `conflict_resolver` function is used as the namespace
for the prefix in the resulting prefix map.
Non-`RDF.IRI` values will be tried to be converted to `RDF.IRI`s via
`RDF.IRI.new` implicitly.
The most common conflict resolution strategies on can be chosen directly with
the following atoms:
- `:ignore`: keep the original namespace from `prefix_map1`
- `:overwrite`: use the other namespace from `prefix_map2`
If a conflict can't be resolved, the provided function can return `nil`.
This will result in an overall return of an `:error` tuple with the list of
prefixes for which the conflict couldn't be resolved.
If everything could be merged, an `:ok` tuple is returned.
"""
2020-06-29 08:37:42 +00:00
@spec merge(t, t | map | keyword, conflict_resolver | nil) ::
{:ok, t} | {:error, [atom | String.t()]}
def merge(prefix_map1, prefix_map2, conflict_resolver)
def merge(prefix_map1, prefix_map2, :ignore) do
merge(prefix_map1, prefix_map2, fn _, ns, _ -> ns end)
end
def merge(prefix_map1, prefix_map2, :overwrite) do
merge(prefix_map1, prefix_map2, fn _, _, ns -> ns end)
end
def merge(%__MODULE__{map: map1}, %__MODULE__{map: map2}, conflict_resolver)
when is_function(conflict_resolver) do
conflict_resolution = fn prefix, namespace1, namespace2 ->
case conflict_resolver.(prefix, namespace1, namespace2) do
nil -> :conflict
result -> IRI.new(result)
end
end
2022-04-10 21:09:44 +00:00
resolved_merge = Map.merge(map1, map2, conflict_resolution)
case resolved_merge_rest_conflicts(resolved_merge) do
[] -> {:ok, %__MODULE__{map: resolved_merge}}
conflicts -> {:error, conflicts}
end
end
def merge(%__MODULE__{} = prefix_map1, prefix_map2, conflict_resolver)
when is_function(conflict_resolver) do
merge(prefix_map1, new(prefix_map2), conflict_resolver)
end
def merge(prefix_map1, prefix_map2, nil), do: merge(prefix_map1, prefix_map2)
defp resolved_merge_rest_conflicts(map) do
Enum.reduce(map, [], fn
{prefix, :conflict}, conflicts -> [prefix | conflicts]
_, conflicts -> conflicts
end)
end
2019-03-25 23:31:43 +00:00
defp merge_conflicts(map1, map2) do
Enum.reduce(map1, [], fn {prefix, namespace}, conflicts ->
if conflicts?(map2, prefix, namespace) do
[prefix | conflicts]
else
conflicts
end
end)
end
defp conflicts?(map, prefix, namespace) do
(existing_namespace = Map.get(map, prefix)) && existing_namespace != namespace
end
2019-03-26 22:00:00 +00:00
@doc """
Merges two `RDF.PrefixMap`s and raises an exception in error cases.
See `merge/2` and `merge/3` for more information on merging prefix maps.
2019-03-26 22:00:00 +00:00
"""
2020-03-02 01:07:31 +00:00
@spec merge!(t, t | map | keyword, conflict_resolver | nil) :: t
def merge!(prefix_map1, prefix_map2, conflict_resolver \\ nil) do
with {:ok, new_prefix_map} <- merge(prefix_map1, prefix_map2, conflict_resolver) do
2019-03-26 22:00:00 +00:00
new_prefix_map
else
{:error, conflicts} ->
2020-03-02 01:07:31 +00:00
conflicts = conflicts |> Stream.map(&inspect/1) |> Enum.join(", ")
raise "conflicting prefix mappings: #{conflicts}"
2019-03-26 22:00:00 +00:00
end
end
2019-03-25 23:31:43 +00:00
@doc """
2020-10-13 13:24:31 +00:00
Deletes the prefix mapping for `prefix` from `prefix_map`.
If no mapping for `prefix` exists, `prefix_map` is returned unchanged.
2019-03-25 23:31:43 +00:00
"""
2020-03-02 22:42:15 +00:00
@spec delete(t, coercible_prefix) :: t
2019-03-25 23:31:43 +00:00
def delete(prefix_map, prefix)
def delete(%__MODULE__{map: map}, prefix) when is_atom(prefix) do
%__MODULE__{map: Map.delete(map, prefix)}
end
def delete(prefix_map, prefix) when is_binary(prefix) do
delete(prefix_map, String.to_atom(prefix))
end
2019-03-30 19:29:48 +00:00
@doc """
2020-10-13 13:24:31 +00:00
Drops the given `prefixes` from `prefix_map`.
2019-03-30 19:29:48 +00:00
If `prefixes` contains prefixes that are not in `prefix_map`, they're simply ignored.
"""
2020-03-02 22:42:15 +00:00
@spec drop(t, [coercible_prefix]) :: t
2019-03-30 19:29:48 +00:00
def drop(prefix_map, prefixes)
def drop(%__MODULE__{map: map}, prefixes) do
%__MODULE__{
map:
Map.drop(
map,
Enum.map(prefixes, fn
prefix when is_binary(prefix) -> String.to_atom(prefix)
other -> other
end)
)
}
end
2019-03-25 23:31:43 +00:00
@doc """
2020-10-13 13:24:31 +00:00
Returns the namespace for the given `prefix` in `prefix_map`.
2019-03-25 23:31:43 +00:00
Returns `nil`, when the given `prefix` is not present in `prefix_map`.
"""
2020-03-02 22:42:15 +00:00
@spec namespace(t, coercible_prefix) :: namespace | nil
2019-03-25 23:31:43 +00:00
def namespace(prefix_map, prefix)
def namespace(%__MODULE__{map: map}, prefix) when is_atom(prefix) do
Map.get(map, prefix)
end
def namespace(prefix_map, prefix) when is_binary(prefix) do
namespace(prefix_map, String.to_atom(prefix))
end
@doc """
2020-10-13 13:24:31 +00:00
Returns the prefix for the given `namespace` in `prefix_map`.
2019-03-25 23:31:43 +00:00
Returns `nil`, when the given `namespace` is not present in `prefix_map`.
"""
2020-03-02 22:42:15 +00:00
@spec prefix(t, coercible_namespace) :: coercible_prefix | nil
2019-03-25 23:31:43 +00:00
def prefix(prefix_map, namespace)
def prefix(%__MODULE__{map: map}, %IRI{} = namespace) do
Enum.find_value(map, fn {prefix, ns} -> ns == namespace && prefix end)
end
def prefix(prefix_map, namespace) when is_binary(namespace) do
prefix(prefix_map, IRI.new(namespace))
end
@doc """
Returns whether the given prefix exists in the given `RDF.PrefixMap`.
"""
2020-03-02 22:42:15 +00:00
@spec has_prefix?(t, coercible_prefix) :: boolean
2019-03-25 23:31:43 +00:00
def has_prefix?(prefix_map, prefix)
def has_prefix?(%__MODULE__{map: map}, prefix) when is_atom(prefix) do
Map.has_key?(map, prefix)
end
def has_prefix?(prefix_map, prefix) when is_binary(prefix) do
has_prefix?(prefix_map, String.to_atom(prefix))
end
@doc """
Returns all prefixes from the given `RDF.PrefixMap`.
"""
2020-03-02 22:42:15 +00:00
@spec prefixes(t) :: [coercible_prefix]
2019-03-25 23:31:43 +00:00
def prefixes(%__MODULE__{map: map}) do
Map.keys(map)
end
@doc """
Returns all namespaces from the given `RDF.PrefixMap`.
"""
2020-03-02 22:42:15 +00:00
@spec namespaces(t) :: [coercible_namespace]
2019-03-25 23:31:43 +00:00
def namespaces(%__MODULE__{map: map}) do
Map.values(map)
end
@doc """
Converts an IRI into a prefixed name.
Returns `nil` when no prefix for the namespace of `iri` is defined in `prefix_map`.
## Examples
iex> RDF.PrefixMap.new(ex: "http://example.com/")
...> |> RDF.PrefixMap.prefixed_name(~I<http://example.com/Foo>)
"ex:Foo"
iex> RDF.PrefixMap.new(ex: "http://example.com/")
...> |> RDF.PrefixMap.prefixed_name("http://example.com/Foo")
"ex:Foo"
"""
@spec prefixed_name(t, IRI.t() | String.t()) :: String.t() | nil
def prefixed_name(prefix_map, iri)
def prefixed_name(%__MODULE__{} = prefix_map, %IRI{} = iri) do
prefixed_name(prefix_map, IRI.to_string(iri))
end
def prefixed_name(%__MODULE__{} = prefix_map, iri) when is_binary(iri) do
case prefix_name_pair(prefix_map, iri) do
{prefix, name} -> prefix <> ":" <> name
_ -> nil
end
end
@doc false
@spec prefix_name_pair(t, IRI.t() | String.t()) :: {String.t(), String.t()} | nil
def prefix_name_pair(prefix_map, iri)
def prefix_name_pair(%__MODULE__{} = prefix_map, %IRI{} = iri) do
prefix_name_pair(prefix_map, IRI.to_string(iri))
end
def prefix_name_pair(%__MODULE__{} = prefix_map, iri) when is_binary(iri) do
Enum.find_value(prefix_map, fn {prefix, namespace} ->
case String.trim_leading(iri, IRI.to_string(namespace)) do
^iri ->
nil
truncated_name ->
unless String.contains?(truncated_name, ~w[/ #]) do
{to_string(prefix), truncated_name}
end
end
end)
end
@doc """
Converts a prefixed name into an IRI.
Returns `nil` when the prefix in `prefixed_name` is not defined in `prefix_map`.
## Examples
iex> RDF.PrefixMap.new(ex: "http://example.com/")
...> |> RDF.PrefixMap.prefixed_name_to_iri("ex:Foo")
~I<http://example.com/Foo>
"""
@spec prefixed_name_to_iri(t, String.t()) :: IRI.t() | nil
def prefixed_name_to_iri(%__MODULE__{} = prefix_map, prefixed_name)
when is_binary(prefixed_name) do
case String.split(prefixed_name, ":", parts: 2) do
[prefix, name] ->
if ns = namespace(prefix_map, prefix) do
RDF.iri(ns.value <> name)
end
_ ->
nil
end
end
2019-03-25 23:31:43 +00:00
defimpl Enumerable do
def reduce(%RDF.PrefixMap{map: map}, acc, fun), do: Enumerable.reduce(map, acc, fun)
def member?(%RDF.PrefixMap{map: map}, mapping), do: Enumerable.member?(map, mapping)
def count(%RDF.PrefixMap{map: map}), do: Enumerable.count(map)
def slice(%RDF.PrefixMap{map: map}), do: Enumerable.slice(map)
2019-03-25 23:31:43 +00:00
end
defimpl Inspect do
import Inspect.Algebra
def inspect(prefix_map, opts) do
map = Map.to_list(prefix_map.map)
open = color("%RDF.PrefixMap{", :map, opts)
sep = color(",", :map, opts)
close = color("}", :map, opts)
container_doc(open, map, close, opts, &Inspect.List.keyword/2,
separator: sep,
break: :strict
)
end
end
2019-03-25 23:31:43 +00:00
end