diff --git a/lib/type_id.ex b/lib/type_id.ex index 53eaaee..8661ffc 100644 --- a/lib/type_id.ex +++ b/lib/type_id.ex @@ -174,17 +174,23 @@ defmodule TypeID do Like `from_string/1` but raises an error if the string is invalid. """ @spec from_string!(String.t()) :: t() | no_return() + def from_string!(str) when byte_size(str) <= 26, do: from!("", str) + def from_string!(str) do - case String.split(str, <<@seperator>>) do - [prefix, suffix] when prefix != "" -> - from!(prefix, suffix) + size = byte_size(str) - [suffix] -> - from!("", suffix) + prefix = + str + |> binary_part(0, size - 26) + |> String.replace(~r/_$/, "") - _ -> - raise ArgumentError, "invalid TypeID" + suffix = binary_part(str, size - 26, 26) + + if prefix == "" do + raise ArgumentError, "A TypeID without a prefix should not have a leading underscore" end + + from!(prefix, suffix) end @doc """ @@ -258,11 +264,26 @@ defmodule TypeID do end defp validate_prefix!(prefix) do - unless prefix =~ ~r/^[a-z]{0,63}$/ do - raise ArgumentError, "invalid prefix: #{prefix}. prefix should match [a-z]{0,63}" - end + cond do + String.starts_with?(prefix, "_") -> + invalid_prefix!(prefix, "cannot start with an underscore") - :ok + String.ends_with?(prefix, "_") -> + invalid_prefix!(prefix, "cannot end with an underscore") + + byte_size(prefix) > 63 -> + invalid_prefix!(prefix, "cannot be more than 63 characters") + + not Regex.match?(~r/^[a-z_]*$/, prefix) -> + invalid_prefix!(prefix, "can contain only lowercase letters and underscores") + + true -> + :ok + end + end + + defp invalid_prefix!(prefix, message) do + raise ArgumentError, "invalid prefix: #{prefix}. #{message}" end defp validate_suffix!(suffix) do diff --git a/priv/spec/invalid.yml b/priv/spec/invalid.yml index 6a2870b..1e19c33 100644 --- a/priv/spec/invalid.yml +++ b/priv/spec/invalid.yml @@ -4,7 +4,7 @@ # Each example contains an invalid TypeID string. Implementations are expected # to throw an error when attempting to parse/validate these strings. # -# Last updated: 2023-07-05 +# Last updated: 2024-04-10 (for version 0.3.0 of the spec) - name: prefix-uppercase typeid: "PREFIX_00000000000000000000000000" @@ -18,9 +18,10 @@ 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" +# Test removed in v0.3.0 – we now allow underscores in the prefix +# - 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" @@ -85,4 +86,14 @@ - name: suffix-overflow # This is the first suffix that overflows into 129 bits typeid: "prefix_8zzzzzzzzzzzzzzzzzzzzzzzzz" - description: "The should encode at most 128-bits" + description: "The suffix should encode at most 128-bits" + +# Tests below were added in v0.3.0 when we started allowing '_' within the +# type prefix. +- name: prefix-underscore-start + typeid: "_prefix_00000000000000000000000000" + description: "The prefix can't start with an underscore" + +- name: prefix-underscore-end + typeid: "prefix__00000000000000000000000000" + description: "The prefix can't end with an underscore" diff --git a/priv/spec/valid.yml b/priv/spec/valid.yml index 8f63250..47c5328 100644 --- a/priv/spec/valid.yml +++ b/priv/spec/valid.yml @@ -23,7 +23,7 @@ # note that not all of them are UUIDv7s. When *generating* new random typeids, # implementations should always use UUIDv7s. # -# Last updated: 2023-07-05 +# Last updated: 2024-04-10 (for version 0.3.0 of the spec) - name: nil typeid: "00000000000000000000000000" @@ -64,3 +64,10 @@ typeid: "prefix_01h455vb4pex5vsknk084sn02q" prefix: "prefix" uuid: "01890a5d-ac96-774b-bcce-b302099a8057" + +# Tests below were added in v0.3.0 when we started allowing '_' within the +# type prefix. +- name: prefix-underscore + typeid: "pre_fix_00000000000000000000000000" + prefix: "pre_fix" + uuid: "00000000-0000-0000-0000-000000000000" diff --git a/scripts/update_spec.exs b/scripts/update_spec.exs new file mode 100755 index 0000000..dc0a11c --- /dev/null +++ b/scripts/update_spec.exs @@ -0,0 +1,27 @@ +#!/usr/bin/env -S ERL_FLAGS=+B elixir + +Mix.install(req: "~> 0.4") + +files = [ + {"https://raw.githubusercontent.com/jetpack-io/typeid/main/spec/invalid.yml", "priv/spec/invalid.yml"}, + {"https://raw.githubusercontent.com/jetpack-io/typeid/main/spec/valid.yml", "priv/spec/valid.yml"} +] + +IO.puts("Updating spec YAML files") + +:ok = for {src, dest} <- files, reduce: :ok do + :ok -> + IO.write("Downloading #{src} to #{dest}... ") + with {:ok, io} <- File.open(dest, [:write]), + {:ok, _} <- Req.get(src, into: IO.binstream(io, 500)) do + IO.puts("OK") + :ok + else + other -> + IO.puts("ERROR") + other + end + failure -> failure +end + +IO.puts("Done!") diff --git a/scripts/update_spec.sh b/scripts/update_spec.sh deleted file mode 100755 index 0a7678c..0000000 --- a/scripts/update_spec.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env bash -ex - -wget https://raw.githubusercontent.com/jetpack-io/typeid/main/spec/invalid.yml -O priv/spec/invalid.yml -wget https://raw.githubusercontent.com/jetpack-io/typeid/main/spec/valid.yml -O priv/spec/valid.yml