From 424909b1f973710a044340321137d7bf0dbb8bc9 Mon Sep 17 00:00:00 2001 From: Marcel Otto Date: Wed, 2 Mar 2022 01:55:31 +0100 Subject: [PATCH] Add RDF.IRI.UUID.Generator --- .dialyzer_ignore | 4 + .../resource_generators/iri_uuid_generator.ex | 65 +++++++++ mix.exs | 1 + mix.lock | 1 + .../iri_uuid_generator_test.exs | 135 ++++++++++++++++++ 5 files changed, 206 insertions(+) create mode 100644 lib/rdf/resource_generators/iri_uuid_generator.ex create mode 100644 test/unit/resource_generators/iri_uuid_generator_test.exs diff --git a/.dialyzer_ignore b/.dialyzer_ignore index e69de29..1fe6786 100644 --- a/.dialyzer_ignore +++ b/.dialyzer_ignore @@ -0,0 +1,4 @@ +Unknown function 'Elixir.UUID':uuid4/1 +Unknown function 'Elixir.UUID':uuid1/1 +Unknown function 'Elixir.UUID':uuid3/3 +Unknown function 'Elixir.UUID':uuid5/3 diff --git a/lib/rdf/resource_generators/iri_uuid_generator.ex b/lib/rdf/resource_generators/iri_uuid_generator.ex new file mode 100644 index 0000000..f1da33c --- /dev/null +++ b/lib/rdf/resource_generators/iri_uuid_generator.ex @@ -0,0 +1,65 @@ +# 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 + use RDF.Resource.Generator + + alias RDF.IRI + + import RDF.Utils.Guards + + @impl true + def generate do + UUID.uuid4(:urn) |> IRI.new() + end + + @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) + + {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 + + {namespace, args} = Keyword.pop!(args, :namespace) + {name, args} = Keyword.pop!(args, :name) + {namespace, name, args} + else + {nil, nil, args} + 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) + end + + defp iri(uuid, :urn, nil), do: IRI.new(uuid) + + defp iri(_uuid, :urn, _), + do: raise(ArgumentError, "prefix option not support on URN UUIDs") + + defp iri(_, _, nil), + do: raise(ArgumentError, "missing required :prefix argument on non-URN UUIDs") + + defp iri(uuid, format, prefix) when maybe_module(prefix), + do: iri(uuid, format, prefix.__base_iri__()) + + defp iri(uuid, _, prefix), do: IRI.new(prefix <> uuid) + end +end diff --git a/mix.exs b/mix.exs index da914db..4ac4fcd 100644 --- a/mix.exs +++ b/mix.exs @@ -72,6 +72,7 @@ defmodule RDF.Mixfile do [ {:decimal, "~> 1.5"}, {:protocol_ex, "~> 0.4.4"}, + {:elixir_uuid, "~> 1.2", optional: true}, {:credo, "~> 1.6", only: [:dev, :test], runtime: false}, {:dialyxir, "~> 1.1", only: [:dev, :test], runtime: false}, {:ex_doc, "~> 0.26", only: :dev, runtime: false}, diff --git a/mix.lock b/mix.lock index 7614472..30c531e 100644 --- a/mix.lock +++ b/mix.lock @@ -7,6 +7,7 @@ "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, "dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"}, "earmark_parser": {:hex, :earmark_parser, "1.4.18", "e1b2be73eb08a49fb032a0208bf647380682374a725dfb5b9e510def8397f6f2", [:mix], [], "hexpm", "114a0e85ec3cf9e04b811009e73c206394ffecfcc313e0b346de0d557774ee97"}, + "elixir_uuid": {:hex, :elixir_uuid, "1.2.1", "dce506597acb7e6b0daeaff52ff6a9043f5919a4c3315abb4143f0b00378c097", [:mix], [], "hexpm", "f7eba2ea6c3555cea09706492716b0d87397b88946e6380898c2889d68585752"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "ex_doc": {:hex, :ex_doc, "0.26.0", "1922164bac0b18b02f84d6f69cab1b93bc3e870e2ad18d5dacb50a9e06b542a3", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "2775d66e494a9a48355db7867478ffd997864c61c65a47d31c4949459281c78d"}, "excoveralls": {:hex, :excoveralls, "0.14.4", "295498f1ae47bdc6dce59af9a585c381e1aefc63298d48172efaaa90c3d251db", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "e3ab02f2df4c1c7a519728a6f0a747e71d7d6e846020aae338173619217931c1"}, diff --git a/test/unit/resource_generators/iri_uuid_generator_test.exs b/test/unit/resource_generators/iri_uuid_generator_test.exs new file mode 100644 index 0000000..6efa7c4 --- /dev/null +++ b/test/unit/resource_generators/iri_uuid_generator_test.exs @@ -0,0 +1,135 @@ +defmodule RDF.IRI.UUID.GeneratorTest do + use RDF.Test.Case + + doctest RDF.IRI.UUID.Generator + + alias RDF.Resource.Generator + + test "without arguments, a URN is generated" do + assert %IRI{value: "urn:uuid:" <> _} = + IRI.UUID.Generator.generator_config() + |> Generator.generate(nil) + end + + 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 + 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" + ) + |> Generator.generate(nil) + + uuid_info = UUID.info!(uuid) + assert Keyword.get(uuid_info, :version) == version + assert Keyword.get(uuid_info, :type) == format + 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 + ) + + uuid_info = UUID.info!(uuid) + assert Keyword.get(uuid_info, :version) == version + assert Keyword.get(uuid_info, :type) == format + 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" + ) + + uuid_info = UUID.info!(uuid) + assert Keyword.get(uuid_info, :version) == version + assert Keyword.get(uuid_info, :type) == format + 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 + end +end