Add RDF.BlankNode.Generator

This commit is contained in:
Marcel Otto 2018-08-28 01:14:44 +02:00
parent 315828a1d6
commit adc1d953dc
4 changed files with 244 additions and 0 deletions

View file

@ -0,0 +1,87 @@
defmodule RDF.BlankNode.Generator do
@moduledoc """
A GenServer generates `RDF.BlankNode`s using a `RDF.BlankNode.Generator.Algorithm`.
"""
use GenServer
# Client API ###############################################################
@doc """
Starts a blank node generator linked to the current process.
The state will be initialized according to the given `RDF.BlankNode.Generator.Algorithm`.
"""
def start_link(generation_mod, init_opts \\ %{}) do
GenServer.start_link(__MODULE__, {generation_mod, convert_opts(init_opts)})
end
@doc """
Starts a blank node generator process without links (outside of a supervision tree).
The state will be initialized according to the given `RDF.BlankNode.Generator.Algorithm`.
"""
def start(generation_mod, init_opts \\ %{}) do
GenServer.start(__MODULE__, {generation_mod, convert_opts(init_opts)})
end
defp convert_opts(nil), do: %{}
defp convert_opts(opts) when is_list(opts), do: Map.new(opts)
defp convert_opts(opts) when is_map(opts), do: opts
@doc """
Synchronously stops the blank node generator with the given `reason`.
It returns `:ok` if the agent terminates with the given reason. If the agent
terminates with another reason, the call will exit.
This function keeps OTP semantics regarding error reporting.
If the reason is any other than `:normal`, `:shutdown` or `{:shutdown, _}`, an
error report will be logged.
"""
def stop(pid, reason \\ :normal, timeout \\ :infinity) do
GenServer.stop(pid, reason, timeout)
end
@doc """
Generates a new blank node according to the `RDF.BlankNode.Generator.Algorithm` set up.
"""
def generate(pid) do
GenServer.call(pid, :generate)
end
@doc """
Generates a blank node for a given string according to the `RDF.BlankNode.Generator.Algorithm` set up.
"""
def generate_for(pid, string) do
GenServer.call(pid, {:generate_for, string})
end
# Server Callbacks #########################################################
@impl GenServer
def init({generation_mod, init_opts}) do
{:ok, {generation_mod, generation_mod.init(init_opts)}}
end
@impl GenServer
def handle_call(:generate, _from, {generation_mod, state}) do
with {bnode, new_state} = generation_mod.generate(state) do
{:reply, bnode, {generation_mod, new_state}}
end
end
@impl GenServer
def handle_call({:generate_for, string}, _from, {generation_mod, state}) do
with {bnode, new_state} = generation_mod.generate_for(string, state) do
{:reply, bnode, {generation_mod, new_state}}
end
end
end

View file

@ -0,0 +1,32 @@
defmodule RDF.BlankNode.Generator.Algorithm do
@moduledoc """
A behaviour for implementations of blank node identifier generation algorithms.
The `RDF.BlankNode.Generator` executes such an algorithm and holds its state.
"""
@doc """
Returns the initial state of the algorithm.
"""
@callback init(opts :: map | Keyword.t() | nil) :: map
@doc """
Generates a blank node.
An implementation should compute a blank node from the given state and return
a tuple consisting of the generated blank node and the new state.
"""
@callback generate(state :: map) :: {RDF.BlankNode.t, map}
@doc """
Generates a blank node for a given string.
Every call with the same string must return the same blank node.
An implementation should compute a blank node for the given string from the
given state and return a tuple consisting of the generated blank node and the
new state.
"""
@callback generate_for(string :: binary, state :: map) :: {RDF.BlankNode.t, map}
end

View file

@ -0,0 +1,57 @@
defmodule RDF.BlankNode.Increment do
@moduledoc """
An implementation of a `RDF.BlankNode.Generator.Algorithm` which returns `RDF.BlankNode`s with incremented identifiers.
The following options are supported when starting a `RDF.BlankNode.Generator`
with this algorithm:
- `prefix`: a string prepended to the generated blank node identifier
- `start_value`: the number from which the incremented counter starts
"""
@behaviour RDF.BlankNode.Generator.Algorithm
alias RDF.BlankNode
@impl BlankNode.Generator.Algorithm
def init(%{prefix: prefix} = opts) do
opts
|> Map.delete(:prefix)
|> init()
|> Map.put(:prefix, prefix)
end
@impl BlankNode.Generator.Algorithm
def init(opts) do
%{
map: %{},
counter: Map.get(opts, :start_value, 0)
}
end
@impl BlankNode.Generator.Algorithm
def generate(%{counter: counter} = state) do
{bnode(counter, state), %{state | counter: counter + 1}}
end
@impl BlankNode.Generator.Algorithm
def generate_for(string, %{map: map, counter: counter} = state) do
case Map.get(map, string) do
nil ->
{bnode(counter, state),
%{state | map: Map.put(map, string, counter), counter: counter + 1}}
previous ->
{bnode(previous, state), state}
end
end
defp bnode(counter, %{prefix: prefix}) do
BlankNode.new(prefix <> Integer.to_string(counter))
end
defp bnode(counter, _) do
BlankNode.new(counter)
end
end

View file

@ -0,0 +1,68 @@
defmodule RDF.BlankNode.IncrementTest do
use RDF.Test.Case
import RDF, only: [bnode: 1]
alias RDF.BlankNode.Generator
alias RDF.BlankNode.Increment
describe "generate/1" do
test "without prefix" do
assert Increment.generate(%{counter: 0, map: %{}}) ==
{bnode(0), (%{counter: 1, map: %{}})}
end
test "with prefix" do
assert Increment.generate(%{counter: 0, map: %{}, prefix: "b"}) ==
{bnode("b0"), (%{counter: 1, map: %{}, prefix: "b"})}
end
end
describe "generate_for/2" do
test "when the given string not exists in the map" do
assert Increment.generate_for("bar", %{counter: 1, map: %{"foo" => 0}}) ==
{bnode(1), (%{counter: 2, map: %{"foo" => 0, "bar" => 1}})}
end
test "when the given string exists in the map" do
assert Increment.generate_for("foo", %{counter: 1, map: %{"foo" => 0}}) ==
{bnode(0), (%{counter: 1, map: %{"foo" => 0}})}
end
test "with prefix" do
assert Increment.generate_for("bar", %{counter: 1, map: %{"foo" => 0}, prefix: "b"}) ==
{bnode("b1"), (%{counter: 2, map: %{"foo" => 0, "bar" => 1}, prefix: "b"})}
assert Increment.generate_for("foo", %{counter: 1, map: %{"foo" => 0}, prefix: "b"}) ==
{bnode("b0"), (%{counter: 1, map: %{"foo" => 0}, prefix: "b"})}
end
end
test "generator without prefix" do
{:ok, generator} = Generator.start_link(Increment)
assert Generator.generate(generator) == bnode(0)
assert Generator.generate(generator) == bnode(1)
assert Generator.generate_for(generator, "foo") == bnode(2)
assert Generator.generate(generator) == bnode(3)
assert Generator.generate_for(generator, "bar") == bnode(4)
assert Generator.generate(generator) == bnode(5)
assert Generator.generate_for(generator, "foo") == bnode(2)
assert Generator.generate(generator) == bnode(6)
end
test "generator with prefix" do
{:ok, generator} = Generator.start_link(Increment, prefix: "b")
assert Generator.generate(generator) == bnode("b0")
assert Generator.generate(generator) == bnode("b1")
assert Generator.generate_for(generator, "foo") == bnode("b2")
assert Generator.generate(generator) == bnode("b3")
assert Generator.generate_for(generator, "bar") == bnode("b4")
assert Generator.generate(generator) == bnode("b5")
assert Generator.generate_for(generator, "foo") == bnode("b2")
assert Generator.generate(generator) == bnode("b6")
end
end