From bee98f3e757091358daf1cd54f2f3b3327bd4478 Mon Sep 17 00:00:00 2001 From: Marcel Otto Date: Sat, 30 Mar 2019 00:01:46 +0100 Subject: [PATCH] Add RDF.PrefixMap.merge/3 and RDF.PrefixMap.merge!/3 --- lib/rdf/prefix_map.ex | 57 +++++++++++++++++++++++++++++++++-- test/unit/prefix_map_test.exs | 44 +++++++++++++++++++++++++-- 2 files changed, 96 insertions(+), 5 deletions(-) diff --git a/lib/rdf/prefix_map.ex b/lib/rdf/prefix_map.ex index 9fd119e..2992504 100644 --- a/lib/rdf/prefix_map.ex +++ b/lib/rdf/prefix_map.ex @@ -96,6 +96,8 @@ defmodule RDF.PrefixMap do 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. """ def merge(prefix_map1, prefix_map2) @@ -114,6 +116,53 @@ defmodule RDF.PrefixMap do raise ArgumentError, "#{inspect(other_prefixes)} is not convertible to a RDF.PrefixMap" end + @doc """ + Merges two `RDF.PrefixMap`s, resolving conflicts through the given `fun`. + + 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 `fun` is used as the namespace for the prefix in the + resulting prefix map. + Non-`RDF.IRI` values will be tried to be converted to converted to `RDF.IRI` + via `RDF.IRI.new` implicitly. + + 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. + + """ + def merge(%__MODULE__{map: map1}, %__MODULE__{map: map2}, fun) when is_function(fun) do + conflict_resolution = fn prefix, namespace1, namespace2 -> + case fun.(prefix, namespace1, namespace2) do + nil -> :conflict + result -> IRI.new(result) + end + end + + with resolved_merge = Map.merge(map1, map2, conflict_resolution), + [] <- resolved_merge_rest_conflicts(resolved_merge) do + {:ok, %__MODULE__{map: resolved_merge}} + else + conflicts -> {:error, conflicts} + end + 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 + defp merge_conflicts(map1, map2) do Enum.reduce(map1, [], fn {prefix, namespace}, conflicts -> if conflicts?(map2, prefix, namespace) do @@ -130,9 +179,11 @@ defmodule RDF.PrefixMap do @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. """ - def merge!(prefix_map1, prefix_map2) do - with {:ok, new_prefix_map} <- merge(prefix_map1, prefix_map2) do + def merge!(prefix_map1, prefix_map2, fun \\ nil) do + with {:ok, new_prefix_map} <- merge(prefix_map1, prefix_map2, fun) do new_prefix_map else {:error, conflicts} -> @@ -143,7 +194,7 @@ defmodule RDF.PrefixMap do end @doc """ - Deletes a prefix mapping from the given `RDF.PrefixMap`.. + Deletes a prefix mapping from the given `RDF.PrefixMap`. """ def delete(prefix_map, prefix) diff --git a/test/unit/prefix_map_test.exs b/test/unit/prefix_map_test.exs index 9ff1a34..cbe3816 100644 --- a/test/unit/prefix_map_test.exs +++ b/test/unit/prefix_map_test.exs @@ -6,6 +6,7 @@ defmodule RDF.PrefixMapTest do @ex_ns1 ~I @ex_ns2 ~I @ex_ns3 ~I + @ex_ns4 ~I @example1 %PrefixMap{map: %{ex1: @ex_ns1}} @@ -98,8 +99,6 @@ defmodule RDF.PrefixMapTest do 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} @@ -128,6 +127,32 @@ defmodule RDF.PrefixMapTest do end end + describe "merge/3" do + test "with a function resolving conflicts by choosing one of the inputs" do + other_prefix_map = PrefixMap.new(ex3: @ex_ns4) + assert PrefixMap.merge(@example3, other_prefix_map, + fn _prefix, ns1, _ns2 -> ns1 end) == {:ok, @example3} + end + + test "with a function which does not resolve by returning nil" do + other_prefix_map = PrefixMap.new(ex3: @ex_ns4) + assert PrefixMap.merge(@example3, other_prefix_map, fn _, _, _ -> nil end) == + PrefixMap.merge(@example3, other_prefix_map) + end + + test "with a function just partially resolving handling conflicts" do + assert PrefixMap.merge(@example3, @example3, + fn prefix, ns1, _ -> if prefix == :ex2, do: ns1 end) == + {:error, [:ex3, :ex1]} + end + + test "when the function returns a non-IRI value which is convertible" do + assert PrefixMap.merge(@example1, @example1, + fn _, _, _ -> "http://example.com/" end) == + {:ok, PrefixMap.new(ex1: "http://example.com/")} + end + end + describe "merge!/2" do test "when the prefix maps can be merged" do other_prefix_map = PrefixMap.new(ex3: @ex_ns3) @@ -141,6 +166,21 @@ defmodule RDF.PrefixMapTest do end end + describe "merge!/3" do + test "when all conflicts can be resolved" do + other_prefix_map = PrefixMap.new(ex3: @ex_ns4) + assert PrefixMap.merge!(@example3, other_prefix_map, + fn _prefix, ns1, _ns2 -> ns1 end) == @example3 + end + + test "when not all conflicts can be resolved" do + assert_raise RuntimeError, "conflicting prefix mappings: :ex1", fn -> + PrefixMap.merge!(@example2, @example2, + fn prefix, ns1, _ -> if prefix == :ex2, do: ns1 end) + end + end + end + describe "delete/2" do test "when a mapping of the given prefix exists" do assert PrefixMap.delete(@example2, :ex2) == @example1