Skip to content

Commit

Permalink
Add RDF.BlankNode.Generator.UUID
Browse files Browse the repository at this point in the history
  • Loading branch information
marcelotto committed Jul 13, 2024
1 parent a82122d commit f1b5e08
Show file tree
Hide file tree
Showing 7 changed files with 149 additions and 5 deletions.
5 changes: 5 additions & 0 deletions lib/rdf/blank_node_generator/random.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
defmodule RDF.BlankNode.Generator.Random do
@moduledoc """
An implementation of a `RDF.BlankNode.Generator.Algorithm` which returns `RDF.BlankNode`s with random identifiers.
Note, although this generator is faster than the `RDF.BlankNode.Generator.UUID` generator,
which also produces random identifiers, the random identifiers produced by
`RDF.BlankNode.Generator.Random` are not unique across multiple application runs,
since they are based on numbers returned by `:erlang.unique_integer/1`.
"""

@behaviour RDF.BlankNode.Generator.Algorithm
Expand Down
52 changes: 52 additions & 0 deletions lib/rdf/blank_node_generator/uuid.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
if Code.ensure_loaded?(UUID) do
defmodule RDF.BlankNode.Generator.UUID do
@moduledoc """
An implementation of a `RDF.BlankNode.Generator.Algorithm` which returns `RDF.BlankNode`s with random UUID identifiers.
This module is only available if the optional `:elixir_uuid` dependency is available.
"""

@behaviour RDF.BlankNode.Generator.Algorithm

defstruct prefix: "b"

@type t :: %__MODULE__{prefix: String.t()}

alias RDF.BlankNode

@doc """
Creates a struct with the state of the algorithm.
## Options
- `prefix`: a string prepended to the generated blank node identifier
"""
def new(attrs \\ []) do
struct(__MODULE__, attrs)
end

@impl BlankNode.Generator.Algorithm
def generate(%__MODULE__{} = state) do
{bnode(state, uuid()), state}
end

@impl BlankNode.Generator.Algorithm
def generate_for(%__MODULE__{} = state, value) do
{bnode(state, uuid_for(value)), state}
end

defp bnode(%__MODULE__{prefix: nil}, uuid), do: BlankNode.new(uuid)
defp bnode(%__MODULE__{} = state, uuid), do: BlankNode.new(state.prefix <> uuid)

defp uuid, do: UUID.uuid4(:hex)

# 74fdf771-1a01-5e8f-a491-1c9e61f1adc6
@uuid_namespace UUID.uuid5(:url, "https://rdf-elixir.dev/blank-node-generator/uuid")

defp uuid_for(value) when is_binary(value),
do: UUID.uuid5(@uuid_namespace, value, :hex)

defp uuid_for(value),
do: value |> :erlang.term_to_binary() |> uuid_for()
end
end
4 changes: 0 additions & 4 deletions lib/rdf/resource_generator/generators/iri_uuid_generator.ex
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
# Since optional dependencies don't get started, dialyzer can't find these functions.
# We're ignoring these warnings (via .dialyzer_ignore).
# See https://elixirforum.com/t/confusing-behavior-of-optional-deps-in-mix-exs/17719/4

if Code.ensure_loaded?(UUID) do
defmodule RDF.IRI.UUID.Generator do
@moduledoc """
Expand Down
6 changes: 6 additions & 0 deletions lib/rdf/serializations/turtle_trig/decoder/state.ex
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ defmodule RDF.TurtleTriG.Decoder.State do
if bnode_gen = default_bnode_gen(), do: bnode_generator(bnode_gen)
end

if Code.ensure_loaded?(UUID) do
defp bnode_generator(:uuid), do: bnode_generator(BlankNode.Generator.UUID)
else
raise "elixir_uuid dependency not available"
end

defp bnode_generator(:random), do: bnode_generator(BlankNode.Generator.Random)
defp bnode_generator(:increment), do: bnode_generator(BlankNode.Generator.Increment)
defp bnode_generator(algorithm) when is_atom(algorithm), do: struct(algorithm)
Expand Down
35 changes: 34 additions & 1 deletion test/unit/blank_node_generator/generator_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ defmodule RDF.BlankNode.GeneratorTest do
import RDF, only: [bnode: 1]

alias RDF.BlankNode.Generator
alias RDF.BlankNode.Generator.{Increment, Random}
alias RDF.BlankNode.Generator.{Increment, Random, UUID}

describe "Increment generator" do
test "generator without prefix" do
Expand Down Expand Up @@ -92,4 +92,37 @@ defmodule RDF.BlankNode.GeneratorTest do
assert Generator.generate_for(generator, {:foo, 42}) == bnode
end
end

describe "UUID generator" do
test "generator without prefix" do
{:ok, generator} = start_supervised({Generator, UUID})

assert %RDF.BlankNode{} = bnode1 = Generator.generate(generator)
assert %RDF.BlankNode{} = bnode2 = Generator.generate(generator)

assert bnode1 != bnode2

assert %RDF.BlankNode{} = bnode1 = Generator.generate_for(generator, "foo")
assert Generator.generate_for(generator, "foo") == bnode1
assert %RDF.BlankNode{} = bnode2 = Generator.generate_for(generator, "bar")

assert bnode1 != bnode2
end

test "generator with prefix" do
{:ok, generator} = start_supervised({Generator, UUID.new(prefix: "b")})

assert %RDF.BlankNode{value: "b" <> _} = bnode1 = Generator.generate(generator)
assert %RDF.BlankNode{value: "b" <> _} = bnode2 = Generator.generate(generator)

assert bnode1 != bnode2
end

test "generator with non-string values" do
{:ok, generator} = start_supervised({Generator, UUID.new(prefix: "b")})

assert %RDF.BlankNode{} = bnode = Generator.generate_for(generator, {:foo, 42})
assert Generator.generate_for(generator, {:foo, 42}) == bnode
end
end
end
39 changes: 39 additions & 0 deletions test/unit/blank_node_generator/uuid_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
defmodule RDF.BlankNode.Generator.UUIDTest do
use RDF.Test.Case

import RDF, only: [bnode: 1]

alias RDF.BlankNode.Generator.UUID

describe "generate/1" do
test "without prefix" do
assert {%BlankNode{}, %UUID{}} = UUID.generate(%UUID{})
end

test "with prefix" do
assert {%BlankNode{value: "b" <> _}, %UUID{prefix: "b"}} =
UUID.generate(%UUID{prefix: "b"})
end
end

describe "generate_for/2" do
test "returns the same id for the same value" do
assert {%BlankNode{value: "b12df7fcd25a15b2d84b9db6d881aefdc"}, %UUID{}} =
UUID.generate_for(%UUID{}, "foo")

assert UUID.generate_for(%UUID{}, "foo") ==
UUID.generate_for(%UUID{}, "foo")

assert UUID.generate_for(%UUID{}, {:foo, "foo", ~U[2024-07-13 02:21:45.085932Z]}) ==
UUID.generate_for(%UUID{}, {:foo, "foo", ~U[2024-07-13 02:21:45.085932Z]})
end

test "with prefix" do
assert {%BlankNode{value: "x03c964986571507facc25bcf9ae8e8a6"}, %UUID{prefix: "x"}} =
UUID.generate_for(%UUID{prefix: "x"}, "bar")

assert UUID.generate_for(%UUID{prefix: "x"}, "foo") ==
{bnode("x12df7fcd25a15b2d84b9db6d881aefdc"), %UUID{prefix: "x"}}
end
end
end
13 changes: 13 additions & 0 deletions test/unit/serializations/turtle_decoder_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,19 @@ defmodule RDF.Turtle.DecoderTest do
""",
bnode_gen: BlankNode.Generator.Random
)

assert Turtle.Decoder.decode!(
"""
[ <http://example.org/#foo> 42 ] .
""",
bnode_gen: :uuid
) !=
Turtle.Decoder.decode!(
"""
[ <http://example.org/#foo> 42 ] .
""",
bnode_gen: :uuid
)
end

test "blank node property list on object position" do
Expand Down

0 comments on commit f1b5e08

Please sign in to comment.