Add RDF.PrefixMap
This commit is contained in:
parent
27fc0945a7
commit
4f71d14377
4 changed files with 403 additions and 0 deletions
11
CHANGELOG.md
11
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
|
||||
|
|
|
@ -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
|
||||
|
|
197
lib/rdf/prefix_map.ex
Normal file
197
lib/rdf/prefix_map.ex
Normal file
|
@ -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
|
193
test/unit/prefix_map_test.exs
Normal file
193
test/unit/prefix_map_test.exs
Normal file
|
@ -0,0 +1,193 @@
|
|||
defmodule RDF.PrefixMapTest do
|
||||
use RDF.Test.Case
|
||||
|
||||
alias RDF.PrefixMap
|
||||
|
||||
@ex_ns1 ~I<http://example.com/foo/>
|
||||
@ex_ns2 ~I<http://example.com/bar#>
|
||||
@ex_ns3 ~I<http://example.com/baz#>
|
||||
|
||||
@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<http://other.com/foo>
|
||||
|
||||
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
|
Loading…
Reference in a new issue