Unify the RDF.Resource.Generator implementations

This commit is contained in:
Marcel Otto 2022-03-08 23:27:25 +01:00
parent 663b7b6ea9
commit 78a3e25bd8
8 changed files with 357 additions and 203 deletions

View file

@ -14,6 +14,7 @@ defmodule RDF.BlankNode do
defstruct [:value]
use RDF.Resource.Generator
alias RDF.Resource.Generator.ConfigError
@doc """
Creates a `RDF.BlankNode`.
@ -47,7 +48,18 @@ defmodule RDF.BlankNode do
def value(%__MODULE__{} = bnode), do: bnode.value
@impl RDF.Resource.Generator
def generate(), do: new()
def generate(_), do: new()
@impl RDF.Resource.Generator
def generate(_, _) do
raise(
ConfigError,
"""
Value-based resource generation is not supported by RDF.BlankNode.
Use RDF.BlankNode.Generator or another generator.
"""
)
end
@doc """
Tests for value equality of blank nodes.

View file

@ -71,8 +71,14 @@ defmodule RDF.BlankNode.Generator do
end
@impl RDF.Resource.Generator
def generate do
raise ArgumentError, "required pid of RDF.BlankNode.Generator missing"
def generate(pid, value) do
generate_for(pid, value)
end
@impl RDF.Resource.Generator
def generator_config(_, config) do
Keyword.get(config, :pid) ||
raise ArgumentError, "missing required :pid argument for RDF.BlankNode.Generator"
end
@doc """

View file

@ -57,3 +57,7 @@ end
defmodule RDF.Query.InvalidError do
defexception [:message]
end
defmodule RDF.Resource.Generator.ConfigError do
defexception [:message]
end

View file

@ -4,11 +4,10 @@ defmodule RDF.Resource do
@type t :: IRI.t() | BlankNode.t()
def generator do
def generator_config do
Application.get_env(:rdf, :resource, generator: BlankNode)
|> Generator.config()
end
def new(), do: generator() |> Generator.generate(nil)
def new(args), do: generator() |> Generator.generate(args)
def new(), do: generator_config() |> Generator.generate()
def new(value), do: generator_config() |> Generator.generate(value)
end

View file

@ -1,57 +1,52 @@
defmodule RDF.Resource.Generator do
defmodule Config do
@enforce_keys [:generator]
defstruct [:generator, :arguments]
@type id_type :: :random_based | :value_based
@type t :: %__MODULE__{generator: module, arguments: any}
end
@callback generate(config :: any) :: RDF.Resource.t()
@callback generator_config() :: Config.t()
@callback generator_config(args :: any) :: Config.t()
@callback generate(config :: any, value :: binary) :: RDF.Resource.t()
@callback generator_arguments(args :: any, defaults :: any) :: any
@callback generate() :: RDF.Resource.t()
@callback generate(args :: any) :: RDF.Resource.t()
@callback generator_config(id_type, keyword) :: any
defmacro __using__(_opts) do
quote do
@behaviour RDF.Resource.Generator
@impl RDF.Resource.Generator
def generator_config(defaults \\ nil) do
%RDF.Resource.Generator.Config{
generator: __MODULE__,
arguments: generator_arguments(defaults, nil)
}
end
def generator_config(_, config), do: config
@impl RDF.Resource.Generator
def generator_arguments(args, defaults) when is_list(args) and is_list(defaults),
do: Keyword.merge(defaults, args)
def generator_arguments(nil, defaults), do: defaults
def generator_arguments(args, defaults), do: args
@impl RDF.Resource.Generator
def generate(_args), do: generate()
defoverridable generate: 1, generator_config: 1, generator_arguments: 2
defoverridable generator_config: 2
end
end
@doc false
def config(config) do
{generator, args} = Keyword.pop!(config, :generator)
default_args = unless Enum.empty?(args), do: args
generator.generator_config(default_args)
def generate(config) do
{generator, config} = config(:random_based, config)
generator.generate(config)
end
@doc false
def generate(%Config{generator: generator, arguments: defaults}, args),
do: do_generate(generator, generator.generator_arguments(args, defaults))
def generate(config, value) do
{generator, config} = config(:value_based, config)
generator.generate(config, value)
end
defp do_generate(generator, nil), do: generator.generate()
defp do_generate(generator, args), do: generator.generate(args)
defp config(id_type, config) do
{random_config, config} = Keyword.pop(config, :random_based)
{value_based_config, config} = Keyword.pop(config, :value_based)
{generator, config} =
id_type
|> merge_config(config, random_config, value_based_config)
|> Keyword.pop!(:generator)
{generator, generator.generator_config(id_type, config)}
end
defp merge_config(:random_based, config, nil, _), do: config
defp merge_config(:random_based, config, random_config, _),
do: Keyword.merge(config, random_config)
defp merge_config(:value_based, config, _, nil), do: config
defp merge_config(:value_based, config, _, value_based_config),
do: Keyword.merge(config, value_based_config)
end

View file

@ -7,55 +7,72 @@ if Code.ensure_loaded?(UUID) do
use RDF.Resource.Generator
alias RDF.IRI
alias RDF.Resource.Generator.ConfigError
import RDF.Utils.Guards
@impl true
def generate do
UUID.uuid4(:urn) |> IRI.new()
end
def generate(config), do: config |> config!(:random_based) |> do_generate()
@impl true
def generate(args) do
{prefix, args} = Keyword.pop(args, :prefix)
{uuid_version, args} = Keyword.pop(args, :version, 4)
{uuid_format, args} = Keyword.pop(args, :format, :default)
def generate(config, value), do: config |> config!(:value_based) |> do_generate(value)
{namespace, name, args} =
if uuid_version in [3, 5] do
unless Keyword.has_key?(args, :namespace) and Keyword.has_key?(args, :name) do
raise ArgumentError,
"missing required :namespace and :name arguments for UUID version #{uuid_version}"
end
defp do_generate({1, format, prefix, _}), do: format |> UUID.uuid1() |> iri(format, prefix)
defp do_generate({4, format, prefix, _}), do: format |> UUID.uuid4() |> iri(format, prefix)
{namespace, args} = Keyword.pop!(args, :namespace)
{name, args} = Keyword.pop!(args, :name)
{namespace, name, args}
else
{nil, nil, args}
defp do_generate({version, _, _, _}) do
raise ConfigError,
"invalid :uuid_version for random resource generator: #{inspect(version)}; only version 1 and 4 are allowed"
end
defp do_generate({3, format, prefix, namespace}, value),
do: UUID.uuid3(namespace, value, format) |> iri(format, prefix)
defp do_generate({5, format, prefix, namespace}, value),
do: UUID.uuid5(namespace, value, format) |> iri(format, prefix)
defp do_generate({version, _, _, _}, _) do
raise ConfigError,
"invalid :uuid_version for value-based resource generator: #{inspect(version)}; only version 3 and 5 are allowed"
end
defp default_uuid_version(:random_based), do: 4
defp default_uuid_version(:value_based), do: 5
defp config!(config, id_type) do
{prefix, config} = Keyword.pop(config, :prefix)
{uuid_version, config} = Keyword.pop(config, :uuid_version, default_uuid_version(id_type))
{uuid_format, config} =
Keyword.pop(config, :uuid_format, if(prefix, do: :default, else: :urn))
{namespace, _config} =
cond do
uuid_version in [3, 5] ->
unless Keyword.has_key?(config, :uuid_namespace) do
raise ConfigError,
"missing required :uuid_namespace argument for UUID version #{uuid_version}"
end
Keyword.pop!(config, :uuid_namespace)
uuid_version in [1, 4] ->
{nil, config}
true ->
raise ConfigError, "invalid :uuid_version: #{uuid_version}"
end
unless Enum.empty?(args) do
raise ArgumentError, "unknown arguments: #{inspect(args)}"
end
case uuid_version do
1 -> UUID.uuid1(uuid_format)
4 -> UUID.uuid4(uuid_format)
3 -> UUID.uuid3(namespace, name, uuid_format)
5 -> UUID.uuid5(namespace, name, uuid_format)
_ -> raise ArgumentError, "unknown UUID version: #{uuid_version}"
end
|> iri(uuid_format, prefix)
{uuid_version, uuid_format, prefix, namespace}
end
defp iri(uuid, :urn, nil), do: IRI.new(uuid)
defp iri(_uuid, :urn, _),
do: raise(ArgumentError, "prefix option not support on URN UUIDs")
do: raise(ConfigError, "prefix option not support on URN UUIDs")
defp iri(_, _, nil),
do: raise(ArgumentError, "missing required :prefix argument on non-URN UUIDs")
do: raise(ConfigError, "missing required :prefix argument on non-URN UUIDs")
defp iri(uuid, format, prefix) when maybe_module(prefix),
do: iri(uuid, format, prefix.__base_iri__())

View file

@ -5,25 +5,38 @@ defmodule RDF.ResourceId.GeneratorTest do
alias RDF.Resource.Generator
test "RDF.BlankNode as a generator" do
assert %BlankNode{} = bnode1 = Generator.generate(BlankNode.generator_config(), nil)
assert %BlankNode{} = bnode2 = Generator.generate(BlankNode.generator_config(), nil)
assert bnode1 != bnode2
describe "RDF.BlankNode as a generator" do
test "generate/0" do
assert %BlankNode{} = bnode1 = Generator.generate(generator: BlankNode)
assert %BlankNode{} = bnode2 = Generator.generate(generator: BlankNode)
assert bnode1 != bnode2
end
test "generate/1" do
assert_raise Generator.ConfigError, fn ->
Generator.generate([generator: BlankNode], "test1")
end
end
end
test "RDF.BlankNode.Generator as a generator" do
{:ok, generator} = start_supervised({RDF.BlankNode.Generator, RDF.BlankNode.Increment})
describe "RDF.BlankNode.Generator as a generator" do
test "generate/0" do
{:ok, generator} = start_supervised({RDF.BlankNode.Generator, RDF.BlankNode.Increment})
assert RDF.BlankNode.Generator.generator_config(generator)
|> Generator.generate(nil) ==
RDF.bnode(0)
config = [generator: BlankNode.Generator, pid: generator]
assert RDF.BlankNode.Generator.generator_config(generator)
|> Generator.generate(nil) ==
RDF.bnode(1)
assert Generator.generate(config) == RDF.bnode(0)
assert Generator.generate(config) == RDF.bnode(1)
end
assert RDF.BlankNode.Generator.generator_config(:foo)
|> Generator.generate(generator) ==
RDF.bnode(2)
test "generate/1" do
{:ok, generator} = start_supervised({RDF.BlankNode.Generator, RDF.BlankNode.Increment})
config = [generator: BlankNode.Generator, pid: generator]
assert Generator.generate(config, "test1") == RDF.bnode(0)
assert Generator.generate(config, "test2") == RDF.bnode(1)
assert Generator.generate(config, "test1") == RDF.bnode(0)
end
end
end

View file

@ -4,132 +4,240 @@ defmodule RDF.IRI.UUID.GeneratorTest do
doctest RDF.IRI.UUID.Generator
alias RDF.Resource.Generator
alias RDF.Resource.Generator.ConfigError
test "without arguments, a URN is generated" do
assert %IRI{value: "urn:uuid:" <> _} =
IRI.UUID.Generator.generator_config()
|> Generator.generate(nil)
end
describe "generate/0" do
test "valid general config" do
for version <- [1, 4], format <- [:default, :hex] do
assert %IRI{value: "http://example.com/ns/" <> uuid} =
generator_config(
prefix: "http://example.com/ns/",
uuid_version: version,
uuid_format: format
)
|> Generator.generate()
test "setting the prefix function arguments" do
assert %IRI{value: "http://example.com/ns/" <> _} =
IRI.UUID.Generator.generator_config(prefix: "http://example.com/ns/")
|> Generator.generate(nil)
assert %IRI{} =
iri1 =
IRI.UUID.Generator.generator_config(prefix: EX)
|> Generator.generate(nil)
assert String.starts_with?(iri1.value, EX.__base_iri__())
assert %IRI{} =
iri2 =
IRI.UUID.Generator.generator_config(prefix: EX)
|> Generator.generate(nil)
assert iri1 != iri2
end
test "setting UUID params via defaults" do
for version <- [1, 4], format <- [:default, :hex] do
assert %IRI{value: "http://example.com/ns/" <> uuid} =
IRI.UUID.Generator.generator_config(
prefix: "http://example.com/ns/",
version: version,
format: format
)
|> Generator.generate(nil)
uuid_info = UUID.info!(uuid)
assert Keyword.get(uuid_info, :version) == version
assert Keyword.get(uuid_info, :type) == format
uuid_info = UUID.info!(uuid)
assert Keyword.get(uuid_info, :version) == version
assert Keyword.get(uuid_info, :type) == format
end
end
for version <- [3, 5],
format <- [:default, :hex],
namespace <- [:dns, :url, UUID.uuid4()] do
assert %IRI{value: "http://example.com/ns/" <> uuid} =
IRI.UUID.Generator.generator_config(
prefix: "http://example.com/ns/",
version: version,
format: format,
namespace: namespace,
name: "test"
test "valid random_based-specific config" do
assert %IRI{value: uuid} =
generator_config(
uuid_version: 1,
random_based: [uuid_version: 4]
)
|> Generator.generate(nil)
|> Generator.generate()
uuid_info = UUID.info!(uuid)
assert Keyword.get(uuid_info, :version) == version
assert Keyword.get(uuid_info, :type) == format
assert uuid |> UUID.info!() |> Keyword.get(:version) == 4
assert uuid |> UUID.info!() |> Keyword.get(:type) == :urn
assert %IRI{value: "http://example.com/ns/" <> uuid} =
generator_config(
prefix: "http://example.com/ns/",
random_based: [
uuid_version: 1,
uuid_format: :hex
]
)
|> Generator.generate()
assert uuid |> UUID.info!() |> Keyword.get(:version) == 1
assert uuid |> UUID.info!() |> Keyword.get(:type) == :hex
assert %IRI{value: "http://example.com/ns/" <> uuid} =
generator_config(
uuid_version: 1,
random_based: [
prefix: "http://example.com/ns/"
]
)
|> Generator.generate()
assert uuid |> UUID.info!() |> Keyword.get(:version) == 1
assert uuid |> UUID.info!() |> Keyword.get(:type) == :default
end
test "setting the prefix as a vocabulary namespace" do
assert %IRI{} = iri = generator_config(prefix: EX) |> Generator.generate()
assert String.starts_with?(iri.value, EX.__base_iri__())
assert %IRI{} = iri = generator_config(random_based: [prefix: EX]) |> Generator.generate()
assert String.starts_with?(iri.value, EX.__base_iri__())
end
test "defaults" do
assert %IRI{value: "urn:uuid:" <> _} = generator_config() |> Generator.generate()
assert %IRI{value: uuid} = generator_config(uuid_version: 1) |> Generator.generate()
assert uuid |> UUID.info!() |> Keyword.get(:version) == 1
assert uuid |> UUID.info!() |> Keyword.get(:type) == :urn
assert %IRI{value: "http://example.com/ns/" <> uuid} =
generator_config(prefix: "http://example.com/ns/")
|> Generator.generate()
assert uuid |> UUID.info!() |> Keyword.get(:version) == 4
assert uuid |> UUID.info!() |> Keyword.get(:type) == :default
end
test "uuid_namespace is ignored" do
assert %IRI{value: "http://example.com/ns/" <> uuid} =
generator_config(
prefix: "http://example.com/ns/",
uuid_version: 1,
uuid_namespace: :url
)
|> Generator.generate()
assert uuid |> UUID.info!() |> Keyword.get(:version) == 1
end
test "invalid config" do
# improper UUID version
assert_raise ConfigError, fn ->
generator_config(uuid_version: 5) |> Generator.generate()
end
assert_raise ConfigError, fn ->
generator_config(random_based: [uuid_version: 5]) |> Generator.generate()
end
# non-URN format without prefix
assert_raise ConfigError, fn ->
generator_config(uuid_format: :default) |> Generator.generate()
end
assert_raise ConfigError, fn ->
generator_config(random_based: [uuid_format: :default]) |> Generator.generate()
end
end
end
test "setting UUID params on generate/2" do
for version <- [1, 4], format <- [:default, :hex] do
assert %IRI{value: "http://example.com/ns/" <> uuid} =
IRI.UUID.Generator.generator_config()
|> Generator.generate(
prefix: "http://example.com/ns/",
version: version,
format: format
)
describe "generate/1" do
test "valid general config" do
for version <- [3, 5],
format <- [:default, :hex],
namespace <- [:dns, :url, UUID.uuid4()] do
assert %IRI{value: "http://example.com/ns/" <> uuid} =
generator_config(
prefix: "http://example.com/ns/",
uuid_version: version,
uuid_format: format,
uuid_namespace: namespace
)
|> Generator.generate("test")
uuid_info = UUID.info!(uuid)
assert Keyword.get(uuid_info, :version) == version
assert Keyword.get(uuid_info, :type) == format
uuid_info = UUID.info!(uuid)
assert Keyword.get(uuid_info, :version) == version
assert Keyword.get(uuid_info, :type) == format
end
end
for version <- [3, 5],
format <- [:default, :hex],
namespace <- [:dns, :url, UUID.uuid4()] do
assert %IRI{value: "http://example.com/ns/" <> uuid} =
IRI.UUID.Generator.generator_config()
|> Generator.generate(
prefix: "http://example.com/ns/",
version: version,
format: format,
namespace: namespace,
name: "test"
test "valid value_based-specific config" do
assert %IRI{value: uuid} =
generator_config(
uuid_version: 5,
uuid_namespace: :dns,
value_based: [uuid_version: 3]
)
|> Generator.generate("test")
uuid_info = UUID.info!(uuid)
assert Keyword.get(uuid_info, :version) == version
assert Keyword.get(uuid_info, :type) == format
assert uuid |> UUID.info!() |> Keyword.get(:version) == 3
assert uuid |> UUID.info!() |> Keyword.get(:type) == :urn
assert %IRI{value: "http://example.com/ns/" <> uuid} =
generator_config(
prefix: "http://example.com/ns/",
uuid_namespace: :dns,
value_based: [
uuid_version: 5,
uuid_format: :hex
]
)
|> Generator.generate("test")
assert uuid |> UUID.info!() |> Keyword.get(:version) == 5
assert uuid |> UUID.info!() |> Keyword.get(:type) == :hex
assert %IRI{value: "http://example.com/ns/" <> uuid} =
generator_config(
uuid_version: 5,
value_based: [
prefix: "http://example.com/ns/",
uuid_namespace: :dns
]
)
|> Generator.generate("test")
assert uuid |> UUID.info!() |> Keyword.get(:version) == 5
assert uuid |> UUID.info!() |> Keyword.get(:type) == :default
end
test "setting the prefix as a vocabulary namespace" do
assert %IRI{} =
iri =
generator_config(prefix: EX, uuid_namespace: :dns) |> Generator.generate("test")
assert String.starts_with?(iri.value, EX.__base_iri__())
end
test "defaults" do
assert %IRI{value: "urn:uuid:" <> _} =
generator_config(uuid_namespace: :dns) |> Generator.generate("test")
assert %IRI{value: uuid} =
generator_config(uuid_version: 3, uuid_namespace: :dns)
|> Generator.generate("test")
assert uuid |> UUID.info!() |> Keyword.get(:version) == 3
assert uuid |> UUID.info!() |> Keyword.get(:type) == :urn
assert %IRI{value: "http://example.com/ns/" <> uuid} =
generator_config(prefix: "http://example.com/ns/", uuid_namespace: :dns)
|> Generator.generate("test")
assert uuid |> UUID.info!() |> Keyword.get(:version) == 5
assert uuid |> UUID.info!() |> Keyword.get(:type) == :default
end
test "invalid config" do
# missing UUID namespace
assert_raise ConfigError, fn ->
generator_config(uuid_version: 5) |> Generator.generate("test")
end
assert_raise ConfigError, fn ->
generator_config(value_based: [uuid_version: 5]) |> Generator.generate("test")
end
assert_raise ConfigError, fn ->
generator_config() |> Generator.generate("test")
end
# improper UUID version
assert_raise ConfigError, fn ->
generator_config(uuid_version: 1) |> Generator.generate("test")
end
assert_raise ConfigError, fn ->
generator_config(value_based: [uuid_version: 1]) |> Generator.generate("test")
end
# non-URN format without prefix
assert_raise ConfigError, fn ->
generator_config(uuid_format: :default) |> Generator.generate("test")
end
assert_raise ConfigError, fn ->
generator_config(value_based: [uuid_format: :default]) |> Generator.generate("test")
end
end
end
test "overwriting default UUID params on generate/2" do
assert %IRI{value: "http://example.com/ns/" <> uuid} =
IRI.UUID.Generator.generator_config(
prefix: "http://example.com/ns/",
version: 4,
format: :default
)
|> Generator.generate(
version: 1,
format: :hex
)
uuid_info = UUID.info!(uuid)
assert Keyword.get(uuid_info, :version) == 1
assert Keyword.get(uuid_info, :type) == :hex
assert %IRI{value: "http://example.com/ns/" <> uuid} =
IRI.UUID.Generator.generator_config(
prefix: "http://example.com/ns/",
version: 3,
format: :hex,
namespace: :url
)
|> Generator.generate(
version: 5,
namespace: :dns,
name: "example.com"
)
uuid_info = UUID.info!(uuid)
assert Keyword.get(uuid_info, :version) == 5
assert Keyword.get(uuid_info, :type) == :hex
defp generator_config(config \\ []) do
Keyword.put(config, :generator, RDF.IRI.UUID.Generator)
end
end