Add RDF.PropertyMap
This commit is contained in:
parent
ec0e60e3c8
commit
e681733652
4 changed files with 345 additions and 0 deletions
1
.iex.exs
1
.iex.exs
|
@ -20,6 +20,7 @@ alias RDF.{
|
||||||
Dataset,
|
Dataset,
|
||||||
|
|
||||||
PrefixMap,
|
PrefixMap,
|
||||||
|
PropertyMap
|
||||||
}
|
}
|
||||||
|
|
||||||
alias RDF.BlankNode, as: BNode
|
alias RDF.BlankNode, as: BNode
|
||||||
|
|
|
@ -252,6 +252,7 @@ defmodule RDF do
|
||||||
def list(native_list, opts), do: RDF.List.from(native_list, opts)
|
def list(native_list, opts), do: RDF.List.from(native_list, opts)
|
||||||
|
|
||||||
defdelegate prefix_map(prefixes), to: RDF.PrefixMap, as: :new
|
defdelegate prefix_map(prefixes), to: RDF.PrefixMap, as: :new
|
||||||
|
defdelegate property_map(property_map), to: RDF.PropertyMap, as: :new
|
||||||
|
|
||||||
defdelegate langString(value, opts), to: RDF.LangString, as: :new
|
defdelegate langString(value, opts), to: RDF.LangString, as: :new
|
||||||
defdelegate lang_string(value, opts), to: RDF.LangString, as: :new
|
defdelegate lang_string(value, opts), to: RDF.LangString, as: :new
|
||||||
|
|
143
lib/rdf/property_map.ex
Normal file
143
lib/rdf/property_map.ex
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
defmodule RDF.PropertyMap do
|
||||||
|
defstruct iris: %{},
|
||||||
|
terms: %{}
|
||||||
|
|
||||||
|
alias RDF.IRI
|
||||||
|
|
||||||
|
@type t :: %__MODULE__{
|
||||||
|
iris: %{atom => IRI.t()},
|
||||||
|
terms: %{IRI.t() => atom}
|
||||||
|
}
|
||||||
|
|
||||||
|
@behaviour Access
|
||||||
|
|
||||||
|
def new(), do: %__MODULE__{}
|
||||||
|
|
||||||
|
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, 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, _, _, iri, iri), do: {:ok, property_map}
|
||||||
|
|
||||||
|
defp do_set(property_map, _, term, iri, 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) do
|
||||||
|
{:error, "conflicting mapping for #{term}: #{new_iri}; already mapped to #{old_iri}"}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp do_set(property_map, :put, term, new_iri, old_iri) do
|
||||||
|
%__MODULE__{property_map | terms: Map.delete(property_map.terms, old_iri)}
|
||||||
|
|> do_set(:put, term, new_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)
|
||||||
|
|
||||||
|
@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
|
||||||
|
end
|
200
test/unit/property_map_test.exs
Normal file
200
test/unit/property_map_test.exs
Normal file
|
@ -0,0 +1,200 @@
|
||||||
|
defmodule RDF.PropertyMapTest do
|
||||||
|
use RDF.Test.Case
|
||||||
|
|
||||||
|
doctest RDF.PropertyMap
|
||||||
|
|
||||||
|
alias RDF.PropertyMap
|
||||||
|
|
||||||
|
@example_property_map %PropertyMap{
|
||||||
|
iris: %{
|
||||||
|
foo: ~I<http://example.com/test/foo>,
|
||||||
|
bar: ~I<http://example.com/test/bar>,
|
||||||
|
Baz: RDF.iri(EX.Baz)
|
||||||
|
},
|
||||||
|
terms: %{
|
||||||
|
~I<http://example.com/test/foo> => :foo,
|
||||||
|
~I<http://example.com/test/bar> => :bar,
|
||||||
|
RDF.iri(EX.Baz) => :Baz
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "new/1" do
|
||||||
|
assert PropertyMap.new(
|
||||||
|
foo: ~I<http://example.com/test/foo>,
|
||||||
|
bar: "http://example.com/test/bar",
|
||||||
|
Baz: EX.Baz
|
||||||
|
) == @example_property_map
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "iri/2" do
|
||||||
|
test "when the given term exists" do
|
||||||
|
assert PropertyMap.iri(@example_property_map, "foo") ==
|
||||||
|
~I<http://example.com/test/foo>
|
||||||
|
|
||||||
|
assert PropertyMap.iri(@example_property_map, :foo) ==
|
||||||
|
~I<http://example.com/test/foo>
|
||||||
|
end
|
||||||
|
|
||||||
|
test "when the given term not exists" do
|
||||||
|
assert PropertyMap.iri(PropertyMap.new(), "foo") == nil
|
||||||
|
assert PropertyMap.iri(PropertyMap.new(), :foo) == nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "term/2" do
|
||||||
|
test "when the given IRI exists" do
|
||||||
|
assert PropertyMap.term(@example_property_map, ~I<http://example.com/test/foo>) ==
|
||||||
|
:foo
|
||||||
|
|
||||||
|
assert PropertyMap.term(@example_property_map, "http://example.com/test/foo") ==
|
||||||
|
:foo
|
||||||
|
end
|
||||||
|
|
||||||
|
test "when the given IRI not exists" do
|
||||||
|
assert PropertyMap.term(PropertyMap.new(), "http://example.com/test/foo") ==
|
||||||
|
nil
|
||||||
|
|
||||||
|
assert PropertyMap.term(PropertyMap.new(), ~I<http://example.com/test/foo>) ==
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "iri_defined?/2" do
|
||||||
|
test "when the given term exists" do
|
||||||
|
assert PropertyMap.iri_defined?(@example_property_map, "foo") == true
|
||||||
|
assert PropertyMap.iri_defined?(@example_property_map, :foo) == true
|
||||||
|
end
|
||||||
|
|
||||||
|
test "when the given term not exists" do
|
||||||
|
assert PropertyMap.iri_defined?(PropertyMap.new(), "foo") == false
|
||||||
|
assert PropertyMap.iri_defined?(PropertyMap.new(), :foo) == false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "term_defined?/2" do
|
||||||
|
test "when the given IRI exists" do
|
||||||
|
assert PropertyMap.term_defined?(@example_property_map, ~I<http://example.com/test/foo>) ==
|
||||||
|
true
|
||||||
|
|
||||||
|
assert PropertyMap.term_defined?(@example_property_map, "http://example.com/test/foo") ==
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
test "when the given IRI not exists" do
|
||||||
|
assert PropertyMap.term_defined?(PropertyMap.new(), "http://example.com/test/foo") == false
|
||||||
|
|
||||||
|
assert PropertyMap.term_defined?(PropertyMap.new(), ~I<http://example.com/test/foo>) ==
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "add/2" do
|
||||||
|
test "with valid mappings as keyword options" do
|
||||||
|
assert PropertyMap.add(PropertyMap.new(),
|
||||||
|
foo: ~I<http://example.com/test/foo>,
|
||||||
|
bar: "http://example.com/test/bar",
|
||||||
|
Baz: EX.Baz
|
||||||
|
) == {:ok, @example_property_map}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "with valid mappings as a map" do
|
||||||
|
assert PropertyMap.add(PropertyMap.new(), %{
|
||||||
|
:foo => ~I<http://example.com/test/foo>,
|
||||||
|
"bar" => "http://example.com/test/bar",
|
||||||
|
"Baz" => EX.Baz
|
||||||
|
}) == {:ok, @example_property_map}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "when a mapping to the same IRI exists" do
|
||||||
|
assert PropertyMap.add(@example_property_map,
|
||||||
|
foo: ~I<http://example.com/test/foo>,
|
||||||
|
bar: "http://example.com/test/bar",
|
||||||
|
Baz: EX.Baz
|
||||||
|
) == {:ok, @example_property_map}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "when a mapping to another IRI exists" do
|
||||||
|
assert PropertyMap.add(@example_property_map, foo: ~I<http://example.com/test/other>) ==
|
||||||
|
{:error,
|
||||||
|
"conflicting mapping for foo: http://example.com/test/other; already mapped to http://example.com/test/foo"}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "put/2" do
|
||||||
|
test "with valid mappings as keyword options" do
|
||||||
|
assert PropertyMap.put(PropertyMap.new(),
|
||||||
|
foo: ~I<http://example.com/test/foo>,
|
||||||
|
bar: "http://example.com/test/bar",
|
||||||
|
Baz: EX.Baz
|
||||||
|
) == @example_property_map
|
||||||
|
end
|
||||||
|
|
||||||
|
test "with valid mappings as a map" do
|
||||||
|
assert PropertyMap.put(PropertyMap.new(), %{
|
||||||
|
:foo => ~I<http://example.com/test/foo>,
|
||||||
|
"bar" => "http://example.com/test/bar",
|
||||||
|
"Baz" => EX.Baz
|
||||||
|
}) == @example_property_map
|
||||||
|
end
|
||||||
|
|
||||||
|
test "when mapping exists" do
|
||||||
|
assert PropertyMap.put(@example_property_map,
|
||||||
|
bar: "http://example.com/test/bar",
|
||||||
|
Baz: EX.qux(),
|
||||||
|
quux: EX.quux()
|
||||||
|
) ==
|
||||||
|
PropertyMap.new(
|
||||||
|
foo: ~I<http://example.com/test/foo>,
|
||||||
|
bar: ~I<http://example.com/test/bar>,
|
||||||
|
Baz: EX.qux(),
|
||||||
|
quux: EX.quux()
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "delete/2" do
|
||||||
|
test "when a mapping for the given term exists" do
|
||||||
|
assert @example_property_map
|
||||||
|
|> PropertyMap.delete("foo")
|
||||||
|
|> PropertyMap.delete(:bar) == PropertyMap.new(Baz: EX.Baz)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "when a mapping for the given term not exists" do
|
||||||
|
assert @example_property_map
|
||||||
|
|> PropertyMap.delete("foobar")
|
||||||
|
|> PropertyMap.delete(:barfoo) == @example_property_map
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "drop/2" do
|
||||||
|
assert PropertyMap.drop(@example_property_map, ["foo", :bar, :other]) ==
|
||||||
|
PropertyMap.new(Baz: EX.Baz)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "expand_description/2" do
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "Access behaviour" do
|
||||||
|
test "fetch/2" do
|
||||||
|
assert @example_property_map[:foo] == ~I<http://example.com/test/foo>
|
||||||
|
assert @example_property_map["foo"] == ~I<http://example.com/test/foo>
|
||||||
|
assert @example_property_map[:missing] == nil
|
||||||
|
assert @example_property_map["missing"] == nil
|
||||||
|
end
|
||||||
|
|
||||||
|
test "get_and_update/2" do
|
||||||
|
update = fn current_value -> {current_value, to_string(current_value) <> "bar"} end
|
||||||
|
|
||||||
|
assert Access.get_and_update(@example_property_map, :foo, &{&1, IRI.append(&1, "bar")}) ==
|
||||||
|
{~I<http://example.com/test/foo>,
|
||||||
|
PropertyMap.put(@example_property_map, foo: ~I<http://example.com/test/foobar>)}
|
||||||
|
|
||||||
|
assert Access.get_and_update(@example_property_map, :foo, update) ==
|
||||||
|
{~I<http://example.com/test/foo>,
|
||||||
|
PropertyMap.put(@example_property_map, :foo, ~I<http://example.com/test/foobar>)}
|
||||||
|
|
||||||
|
assert Access.get_and_update(@example_property_map, :foo, fn _ -> :pop end) ==
|
||||||
|
{~I<http://example.com/test/foo>, PropertyMap.delete(@example_property_map, :foo)}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue