modify to support valid specs

This commit is contained in:
Sloane Perrault 2023-07-02 08:13:02 -04:00
parent 3337d6b2cd
commit 6d4a555585
No known key found for this signature in database
7 changed files with 216 additions and 4 deletions

View file

@ -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

36
lib/type_id/uuid.ex Normal file
View file

@ -0,0 +1,36 @@
defmodule TypeID.UUID do
def uuid7(opts \\ []) do
time = Keyword.get_lazy(opts, :time, fn -> System.system_time(:millisecond) end)
<<rand_a::12, _::6, rand_b::62>> = :crypto.strong_rand_bytes(10)
<<time::big-unsigned-integer-size(48), 7::4, rand_a::12, 2::2, rand_b::62>>
end
def to_string(
<<a1::4, a2::4, a3::4, a4::4, a5::4, a6::4, a7::4, a8::4, b1::4, b2::4, b3::4, b4::4,
c1::4, c2::4, c3::4, c4::4, d1::4, d2::4, d3::4, d4::4, e1::4, e2::4, e3::4, e4::4,
e5::4, e6::4, e7::4, e8::4, e9::4, e10::4, e11::4, e12::4>>
) do
<<e(a1), e(a2), e(a3), e(a4), e(a5), e(a6), e(a7), e(a8), ?-, e(b1), e(b2), e(b3), e(b4), ?-,
e(c1), e(c2), e(c3), e(c4), ?-, e(d1), e(d2), e(d3), e(d4), ?-, e(e1), e(e2), e(e3), e(e4),
e(e5), e(e6), e(e7), e(e8), e(e9), e(e10), e(e11), e(e12)>>
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

View file

@ -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

View file

@ -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"},
}

83
priv/spec/invalid.yml Normal file
View file

@ -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"

61
priv/spec/valid.yml Normal file
View file

@ -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"

20
test/spec_test.exs Normal file
View file

@ -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