Add RDF.BlankNode.Generator
This commit is contained in:
parent
315828a1d6
commit
adc1d953dc
4 changed files with 244 additions and 0 deletions
87
lib/rdf/blank_node/generator.ex
Normal file
87
lib/rdf/blank_node/generator.ex
Normal 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
|
32
lib/rdf/blank_node/generator_algorithm.ex
Normal file
32
lib/rdf/blank_node/generator_algorithm.ex
Normal 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
|
57
lib/rdf/blank_node/increment.ex
Normal file
57
lib/rdf/blank_node/increment.ex
Normal 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
|
68
test/unit/blank_node/increment_test.exs
Normal file
68
test/unit/blank_node/increment_test.exs
Normal 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
|
Loading…
Reference in a new issue