From 6d4a555585ce48dc1ab9cfeeb99cd1417d1fe940 Mon Sep 17 00:00:00 2001 From: Sloane Perrault Date: Sun, 2 Jul 2023 08:13:02 -0400 Subject: [PATCH] modify to support valid specs --- lib/type_id.ex | 15 ++++++-- lib/type_id/uuid.ex | 36 +++++++++++++++++++ mix.exs | 3 +- mix.lock | 2 ++ priv/spec/invalid.yml | 83 +++++++++++++++++++++++++++++++++++++++++++ priv/spec/valid.yml | 61 +++++++++++++++++++++++++++++++ test/spec_test.exs | 20 +++++++++++ 7 files changed, 216 insertions(+), 4 deletions(-) create mode 100644 lib/type_id/uuid.ex create mode 100644 priv/spec/invalid.yml create mode 100644 priv/spec/valid.yml create mode 100644 test/spec_test.exs diff --git a/lib/type_id.ex b/lib/type_id.ex index dd0fc6c..874e360 100644 --- a/lib/type_id.ex +++ b/lib/type_id.ex @@ -73,6 +73,10 @@ defmodule TypeID do """ @spec to_string(tid :: t()) :: String.t() + def to_string(%__MODULE__{prefix: "", suffix: suffix}) do + suffix + end + def to_string(%__MODULE__{prefix: prefix, suffix: suffix}) do prefix <> "_" <> suffix end @@ -142,8 +146,13 @@ defmodule TypeID do """ @spec from_string!(String.t()) :: t() | no_return() def from_string!(str) do - [prefix, suffix] = String.split(str, "_") - from!(prefix, suffix) + case String.split(str, "_") do + [prefix, suffix] -> + from!(prefix, suffix) + + [suffix] -> + from!("", suffix) + end end @doc """ @@ -168,7 +177,7 @@ defmodule TypeID do """ @spec from_uuid!(prefix :: String.t(), uuid :: String.t()) :: t() | no_return() def from_uuid!(prefix, uuid) do - {:ok, %Uniq.UUID{bytes: uuid_bytes, version: 7}} = Uniq.UUID.parse(uuid) + uuid_bytes = Uniq.UUID.string_to_binary!(uuid) from_uuid_bytes!(prefix, uuid_bytes) end diff --git a/lib/type_id/uuid.ex b/lib/type_id/uuid.ex new file mode 100644 index 0000000..accf008 --- /dev/null +++ b/lib/type_id/uuid.ex @@ -0,0 +1,36 @@ +defmodule TypeID.UUID do + def uuid7(opts \\ []) do + time = Keyword.get_lazy(opts, :time, fn -> System.system_time(:millisecond) end) + <> = :crypto.strong_rand_bytes(10) + + <> + end + + def to_string( + <> + ) do + <> + end + + @compile {:inline, [e: 1]} + defp e(0), do: ?0 + defp e(1), do: ?1 + defp e(2), do: ?2 + defp e(3), do: ?3 + defp e(4), do: ?4 + defp e(5), do: ?5 + defp e(6), do: ?6 + defp e(7), do: ?7 + defp e(8), do: ?8 + defp e(9), do: ?9 + defp e(10), do: ?a + defp e(11), do: ?b + defp e(12), do: ?c + defp e(13), do: ?d + defp e(14), do: ?e + defp e(15), do: ?f +end diff --git a/mix.exs b/mix.exs index 59b3ac7..8c28a6e 100644 --- a/mix.exs +++ b/mix.exs @@ -33,7 +33,8 @@ defmodule TypeID.MixProject do defp deps do [ {:uniq, "~> 0.5.4"}, - {:ex_doc, "~> 0.27", only: :dev, runtime: false} + {:ex_doc, "~> 0.27", only: :dev, runtime: false}, + {:yaml_elixir, "~> 2.9", only: [:dev, :test], runtime: false} ] end end diff --git a/mix.lock b/mix.lock index 63c43cb..cf556f8 100644 --- a/mix.lock +++ b/mix.lock @@ -6,4 +6,6 @@ "makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"}, "nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"}, "uniq": {:hex, :uniq, "0.5.4", "0602d0c75682f14863c1edea48920bd3d278b29ff7801d56e040e5f9f51ae0e2", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "856ec690d713f97373a3746afada340143c7e37a6488fca5afa720bfb08f0fe5"}, + "yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"}, + "yaml_elixir": {:hex, :yaml_elixir, "2.9.0", "9a256da867b37b8d2c1ffd5d9de373a4fda77a32a45b452f1708508ba7bbcb53", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "0cb0e7d4c56f5e99a6253ed1a670ed0e39c13fc45a6da054033928607ac08dfc"}, } diff --git a/priv/spec/invalid.yml b/priv/spec/invalid.yml new file mode 100644 index 0000000..c1470a2 --- /dev/null +++ b/priv/spec/invalid.yml @@ -0,0 +1,83 @@ +# This file contains test data that should be treated as *invalid* TypeIDs by +# conforming implementations. +# +# Each example contains an invalid TypeID string. Implementations are expected +# to throw an error when attempting to parse/validate these strings. +# +# Last updated: 2023-06-29 + +- name: prefix-uppercase + typeid: "PREFIX_00000000000000000000000000" + description: "The prefix should be lowercase with no uppercase letters" + +- name: prefix-numeric + typeid: "12345_00000000000000000000000000" + description: "The prefix can't have numbers, it needs to be alphabetic" + +- name: prefix-period + typeid: "pre.fix_00000000000000000000000000" + description: "The prefix can't have symbols, it needs to be alphabetic" + +- name: prefix-underscore + typeid: "pre_fix_00000000000000000000000000" + description: "The prefix can't have symbols, it needs to be alphabetic" + +- name: prefix-non-ascii + typeid: "préfix_00000000000000000000000000" + description: "The prefix can only have ascii letters" + +- name: prefix-spaces + typeid: " prefix_00000000000000000000000000" + description: "The prefix can't have any spaces" + +- name: prefix-64-chars + # 123456789 123456789 123456789 123456789 123456789 123456789 1234 + typeid: "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl_00000000000000000000000000" + description: "The prefix can't be 64 characters, it needs to be 63 characters or less" + +- name: separator-empty-prefix + typeid: "_00000000000000000000000000" + description: "If the prefix is empty, the separator should not be there" + +- name: separator-empty + typeid: "_" + description: "A separator by itself should not be treated as the empty string" + +- name: suffix-short + typeid: "prefix_1234567890123456789012345" + description: "The suffix can't be 25 characters, it needs to be exactly 26 characters" + +- name: suffix-long + typeid: "prefix_123456789012345678901234567" + description: "The suffix can't be 27 characters, it needs to be exactly 26 characters" + +- name: suffix-spaces + # This example has the right length, so that the failure is caused by the space + # and not the suffix length + typeid: "prefix_1234567890123456789012345 " + description: "The suffix can't have any spaces" + +- name: suffix-uppercase + # This example is picked because it would be valid in lowercase + typeid: "prefix_0123456789ABCDEFGHJKMNPQRS" + description: "The suffix should be lowercase with no uppercase letters" + +- name: suffix-hyphens + # This example has the right length, so that the failure is caused by the hyphens + # and not the suffix length + typeid: "prefix_123456789-123456789-123456" + description: "The suffix should be lowercase with no uppercase letters" + +- name: suffix-wrong-alphabet + typeid: "prefix_ooooooiiiiiiuuuuuuulllllll" + description: "The suffix should only have letters from the spec's alphabet" + +- name: suffix-ambiguous-crockford + # This example would be valid if we were using the crockford disambiguation rules + typeid: "prefix_i23456789ol23456789oi23456" + description: "The suffix should not have any ambiguous characters from the crockford encoding" + +- name: suffix-hyphens-crockford + # This example would be valid if we were using the crockford hyphenation rules + typeid: "prefix_123456789-0123456789-0123456" + description: "The suffix can't ignore hyphens as in the crockford encoding" diff --git a/priv/spec/valid.yml b/priv/spec/valid.yml new file mode 100644 index 0000000..cf31f0c --- /dev/null +++ b/priv/spec/valid.yml @@ -0,0 +1,61 @@ +# This file contains test data that should parse as valid TypeIDs by conforming +# implementations. +# +# Each example contains: +# - The TypeID in its canonical string representation. +# - The prefix +# - The decoded UUID as a hex string +# +# Implementations should verify that they can encode/decode the data +# in both directions: +# 1. If the TypeID is decoded, it should result in the given prefix and UUID. +# 2. If the UUID is encoded as a TypeID with the given prefix, it should +# result in the given TypeID. +# +# In addition to using these examples, it's recommended that implementations +# generate a thousands of random ids during testing, and verify that after +# decoding and re-encoding the id, the result is the same as the original. +# +# In other words, the following property should always hold: +# random_typeid == encode(decode(random_typeid)) +# +# Finally, while implementations should be able to decode the values below, +# note that not all of them are UUIDv7s. When *generating* new random typeids, +# implementations should always use UUIDv7s. +# +# Last updated: 2023-06-29 + +- name: nil + typeid: "00000000000000000000000000" + prefix: "" + uuid: "00000000-0000-0000-0000-000000000000" + +- name: one + typeid: "00000000000000000000000001" + prefix: "" + uuid: "00000000-0000-0000-0000-000000000001" + +- name: ten + typeid: "0000000000000000000000000a" + prefix: "" + uuid: "00000000-0000-0000-0000-00000000000a" + +- name: sixteen + typeid: "0000000000000000000000000g" + prefix: "" + uuid: "00000000-0000-0000-0000-000000000010" + +- name: thirty-two + typeid: "00000000000000000000000010" + prefix: "" + uuid: "00000000-0000-0000-0000-000000000020" + +- name: valid-alphabet + typeid: "prefix_0123456789abcdefghjkmnpqrs" + prefix: "prefix" + uuid: "0110c853-1d09-52d8-d73e-1194e95b5f19" + +- name: valid-uuidv7 + typeid: "prefix_01h455vb4pex5vsknk084sn02q" + prefix: "prefix" + uuid: "01890a5d-ac96-774b-bcce-b302099a8057" diff --git a/test/spec_test.exs b/test/spec_test.exs new file mode 100644 index 0000000..024504b --- /dev/null +++ b/test/spec_test.exs @@ -0,0 +1,20 @@ +defmodule TypeID.SpecTest do + use ExUnit.Case + + @valid_specs :code.priv_dir(:typeid_elixir) + |> Path.join("spec/valid.yml") + |> YamlElixir.read_from_file!() + + describe "valid" do + for %{"name" => name, "typeid" => typeid, "prefix" => prefix, "uuid" => uuid} <- @valid_specs do + test "#{name}" do + assert {:ok, tid} = TypeID.from_uuid(unquote(prefix), unquote(uuid)) + assert unquote(typeid) == TypeID.to_string(tid) + assert {:ok, ^tid} = TypeID.from_string(unquote(typeid)) + end + end + end + + # describe "invalid" do + # end +end