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