diff --git a/CHANGELOG.md b/CHANGELOG.md
index 64fe08a..0a1cf1a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,17 @@ This project adheres to [Semantic Versioning](http://semver.org/) and
[Keep a CHANGELOG](http://keepachangelog.com).
+## Unreleased
+
+### Added
+
+- `RDF.PrefixMap`
+
+
+[Compare v0.5.4...HEAD](https://github.com/marcelotto/rdf-ex/compare/v0.5.4...HEAD)
+
+
+
## 0.5.4 - 2019-01-17
### Fixed
diff --git a/lib/rdf.ex b/lib/rdf.ex
index 6600a67..5338997 100644
--- a/lib/rdf.ex
+++ b/lib/rdf.ex
@@ -189,6 +189,8 @@ defmodule RDF do
defdelegate datetime(value), to: RDF.DateTime, as: :new
defdelegate datetime(value, opts), to: RDF.DateTime, as: :new
+ defdelegate prefix_map(prefixes), to: RDF.PrefixMap, as: :new
+
for term <- ~w[type subject predicate object first rest value]a do
defdelegate unquote(term)(), to: RDF.NS.RDF
defdelegate unquote(term)(s, o), to: RDF.NS.RDF
diff --git a/lib/rdf/prefix_map.ex b/lib/rdf/prefix_map.ex
new file mode 100644
index 0000000..ad1d30d
--- /dev/null
+++ b/lib/rdf/prefix_map.ex
@@ -0,0 +1,197 @@
+defmodule RDF.PrefixMap do
+ @moduledoc """
+ A mapping a prefix atoms to IRI namespaces.
+
+ `RDF.PrefixMap` implements the `Enumerable` protocol.
+ """
+
+ defstruct map: %{}
+
+ alias RDF.IRI
+
+ @doc """
+ Creates an empty `RDF.PrefixMap.
+ """
+ def new(), do: %__MODULE__{}
+
+ @doc """
+ Creates a new `RDF.PrefixMap.
+
+ The prefix mappings can be passed as keyword lists or maps.
+ 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.
+ """
+ def new(map)
+
+ 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, %IRI{} = namespace}) when is_atom(prefix),
+ do: {prefix, namespace}
+
+ defp normalize({prefix, namespace}) when is_atom(prefix),
+ do: normalize({prefix, IRI.new(namespace)})
+
+ defp normalize({prefix, namespace}) when is_binary(prefix),
+ do: normalize({String.to_atom(prefix), namespace})
+
+ defp normalize({prefix, _}),
+ do: raise("Invalid prefix on PrefixMap: #{inspect(prefix)}}")
+
+ @doc """
+ Adds a prefix mapping the given `RDF.PrefixMap`.
+
+ Unless a mapping of the given prefix to a different namespace already exists,
+ an ok tuple is returned, other an error tuple.
+ """
+ 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.
+ """
+ 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
+
+ @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 there are conflicts between the prefix maps, that is prefixes mapped to
+ different namespaces and error tuple is returned, otherwise an ok tuple.
+ """
+ def merge(prefix_map1, prefix_map2)
+
+ def merge(%__MODULE__{map: map1}, %__MODULE__{map: map2}) do
+ with [] <- merge_conflicts(map1, map2) do
+ {:ok, %__MODULE__{map: Map.merge(map1, map2)}}
+ else
+ conflicts ->
+ {:error, "conflicting prefix mappings: #{conflicts |> Stream.map(&inspect/1) |> Enum.join(", ")}"}
+ 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
+
+ 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
+
+ @doc """
+ Deletes a prefix mapping from the given `RDF.PrefixMap`..
+ """
+ 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
+
+ @doc """
+ Returns the namespace for the given prefix in the given `RDF.PrefixMap`.
+
+ Returns `nil`, when the given `prefix` is not present in `prefix_map`.
+ """
+ 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 """
+ Returns the prefix for the given namespace in the given `RDF.PrefixMap`.
+
+ Returns `nil`, when the given `namespace` is not present in `prefix_map`.
+ """
+ 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`.
+ """
+ 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`.
+ """
+ def prefixes(%__MODULE__{map: map}) do
+ Map.keys(map)
+ end
+
+ @doc """
+ Returns all namespaces from the given `RDF.PrefixMap`.
+ """
+ def namespaces(%__MODULE__{map: map}) do
+ Map.values(map)
+ end
+
+ 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(_prefix_map), do: {:error, __MODULE__}
+ end
+end
diff --git a/test/unit/prefix_map_test.exs b/test/unit/prefix_map_test.exs
new file mode 100644
index 0000000..872cab0
--- /dev/null
+++ b/test/unit/prefix_map_test.exs
@@ -0,0 +1,193 @@
+defmodule RDF.PrefixMapTest do
+ use RDF.Test.Case
+
+ alias RDF.PrefixMap
+
+ @ex_ns1 ~I
+ @ex_ns2 ~I
+ @ex_ns3 ~I
+
+ @example1 %PrefixMap{map: %{ex1: @ex_ns1}}
+
+ @example2 %PrefixMap{map: %{
+ ex1: @ex_ns1,
+ ex2: @ex_ns2
+ }}
+
+ @example3 %PrefixMap{map: %{
+ ex1: @ex_ns1,
+ ex2: @ex_ns2,
+ ex3: @ex_ns3
+ }}
+
+ test "new/0" do
+ assert PrefixMap.new() == %PrefixMap{}
+ end
+
+ describe "new/1" do
+ test "with a map" do
+ assert PrefixMap.new(%{
+ "ex1" => "http://example.com/foo/",
+ "ex2" => "http://example.com/bar#"
+ }) == @example2
+ end
+
+ test "with a keyword map" do
+ assert PrefixMap.new(
+ ex1: "http://example.com/foo/",
+ ex2: "http://example.com/bar#"
+ ) == @example2
+ end
+ end
+
+ describe "add/3" do
+ test "when no mapping of the given prefix exists" do
+ assert PrefixMap.add(@example1, :ex2, @ex_ns2) == {:ok, @example2}
+ end
+
+ test "with the prefix is given as a string" do
+ assert PrefixMap.add(@example1, "ex2", @ex_ns2) == {:ok, @example2}
+ end
+
+ test "with the IRI namespace is given as a string" do
+ assert PrefixMap.add(@example1, :ex2, "http://example.com/bar#") == {:ok, @example2}
+ end
+
+ test "when a mapping of the given prefix to the same namespace already exists" do
+ assert PrefixMap.add(@example2, :ex2, "http://example.com/bar#") == {:ok, @example2}
+ end
+
+ test "when a mapping of the given prefix to a different namespace already exists" do
+ assert PrefixMap.add(@example2, :ex2, @ex_ns3) ==
+ {:error, "prefix :ex2 is already mapped to another namespace"}
+ end
+ end
+
+ describe "add!/3" do
+ test "when no mapping of the given prefix exists" do
+ assert PrefixMap.add!(@example1, :ex2, @ex_ns2) == @example2
+ end
+
+ test "when a mapping of the given prefix to a different namespace already exists" do
+ assert_raise RuntimeError, "prefix :ex2 is already mapped to another namespace", fn ->
+ PrefixMap.add!(@example2, :ex2, @ex_ns3)
+ end
+ end
+ end
+
+ describe "merge/2" do
+ @ex_ns4 ~I
+
+ test "when the prefix maps are disjunctive" do
+ other_prefix_map = PrefixMap.new(ex3: @ex_ns3)
+ assert PrefixMap.merge(@example2, other_prefix_map) == {:ok, @example3}
+ end
+
+ test "when the prefix maps share some prefixes, but both map to the same namespace" do
+ other_prefix_map = PrefixMap.new(ex3: @ex_ns3)
+ assert PrefixMap.merge(@example3, other_prefix_map) == {:ok, @example3}
+ end
+
+ test "when the prefix maps share some prefixes and both map to different namespaces" do
+ other_prefix_map = PrefixMap.new(ex3: @ex_ns4)
+ assert PrefixMap.merge(@example3, other_prefix_map) == {:error, "conflicting prefix mappings: :ex3"}
+ end
+
+ test "when the second prefix map is given as a structure convertible to a prefix map" do
+ assert PrefixMap.merge(@example2, %{ex3: @ex_ns3}) == {:ok, @example3}
+ assert PrefixMap.merge(@example2, ex3: @ex_ns3) == {:ok, @example3}
+ end
+
+ test "when the second argument is not convertible to a prefix map" do
+ assert_raise ArgumentError, ~S["not convertible" is not convertible to a RDF.PrefixMap], fn ->
+ PrefixMap.merge(@example2, "not convertible")
+ end
+ end
+ end
+
+ describe "delete/2" do
+ test "when a mapping of the given prefix exists" do
+ assert PrefixMap.delete(@example2, :ex2) == @example1
+ end
+
+ test "when no mapping of the given prefix exists" do
+ assert PrefixMap.delete(@example1, :ex2) == @example1
+ end
+
+ test "with the prefix is given as a string" do
+ assert PrefixMap.delete(@example2, "ex2") == @example1
+ end
+ end
+
+ describe "namespace/2" do
+ test "when a mapping of the given prefix exists" do
+ assert PrefixMap.namespace(@example2, :ex2) == @ex_ns2
+ end
+
+ test "when no mapping of the given prefix exists" do
+ assert PrefixMap.namespace(@example1, :ex2) == nil
+ end
+
+ test "with the prefix is given as a string" do
+ assert PrefixMap.namespace(@example2, "ex2") == @ex_ns2
+ end
+ end
+
+ describe "prefix/2" do
+ test "when a mapping to the given namespace exists" do
+ assert PrefixMap.prefix(@example2, @ex_ns2) == :ex2
+ end
+
+ test "when no mapping to the given namespace exists" do
+ assert PrefixMap.prefix(@example1, @ex_ns2) == nil
+ end
+
+ test "with the namespace is given as a string" do
+ assert PrefixMap.prefix(@example2, to_string(@ex_ns2)) == :ex2
+ end
+ end
+
+ describe "has_prefix?/2" do
+ test "when a mapping of the given prefix exists" do
+ assert PrefixMap.has_prefix?(@example2, :ex2) == true
+ end
+
+ test "when no mapping of the given prefix exists" do
+ assert PrefixMap.has_prefix?(@example1, :ex2) == false
+ end
+
+ test "with the prefix is given as a string" do
+ assert PrefixMap.has_prefix?(@example2, "ex2") == true
+ end
+ end
+
+ test "prefixes/1" do
+ assert PrefixMap.prefixes(@example2) == ~w[ex1 ex2]a
+ assert PrefixMap.prefixes(PrefixMap.new) == ~w[]a
+ end
+
+ describe "namespaces/1" do
+ assert PrefixMap.namespaces(@example2) == [@ex_ns1, @ex_ns2]
+ assert PrefixMap.namespaces(PrefixMap.new) == ~w[]a
+ end
+
+ describe "Enumerable protocol" do
+ test "Enum.count" do
+ assert Enum.count(PrefixMap.new) == 0
+ assert Enum.count(@example1) == 1
+ assert Enum.count(@example2) == 2
+ end
+
+ test "Enum.member?" do
+ assert Enum.member?(@example2, {:ex1, @ex_ns1})
+ assert Enum.member?(@example2, {:ex2, @ex_ns2})
+ refute Enum.member?(@example2, {:ex1, @ex_ns2})
+ refute Enum.member?(@example2, {:ex2, @ex_ns3})
+ end
+
+ test "Enum.reduce" do
+ assert Enum.reduce(@example2, [], fn(mapping, acc) -> [mapping | acc] end) ==
+ [{:ex2, @ex_ns2}, {:ex1, @ex_ns1}]
+ end
+ end
+end