defmodule Base62UUID do @moduledoc """ A base 62-encoded v4 UUID. """ @length 22 @uuid_length 32 @doc """ Generate a 22-byte base-62-encoded v4 UUID. """ @spec generate :: String.t() def generate do encode!(UUID.uuid4()) end @doc """ Encode a v4 UUID to a 22-byte encoded UUID. Returns an error message if the UUID was invalid. # Example iex> Base62UUID.encode("063cd93e-dd59-43b6-928b-2d00a49087fc") {:ok, "0BllEZppLhVt2a9PljPUJ2"} """ @spec encode(String.t()) :: {:ok, String.t()} | {:error, String.t()} def encode(uuid) do {:ok, encode!(uuid)} rescue e in ArgumentError -> {:error, e.message} end @doc """ Encode a v4 UUID to a 22-byte encoded UUID, and raise on an error. # Example iex> Base62UUID.encode!("063cd93e-dd59-43b6-928b-2d00a49087fc") "0BllEZppLhVt2a9PljPUJ2" """ @spec encode!(String.t()) :: String.t() def encode!(uuid) do uuid |> String.replace("-", "") |> String.to_integer(16) |> Base62.encode() |> ensure_length(@length) end @doc """ Decode a an encoded UUID. Returns an error tuple if the UUID is invalid. # Example iex> Base62UUID.decode("0BllEZppLhVt2a9PljPUJ2") {:ok, "063cd93e-dd59-43b6-928b-2d00a49087fc"} """ @spec decode(String.t()) :: {:ok, String.t()} | {:error, String.t()} def decode(uuid) do with {:ok, int} <- Base62.decode(uuid), raw_uuid = Integer.to_string(int, 16), raw_uuid = ensure_length(raw_uuid, @uuid_length), {:ok, uuid} <- make_uuid(raw_uuid) do {:ok, uuid} else _ -> {:error, "Invalid base-62 UUID"} end end @doc """ Decode a an encoded UUID. # Example iex> Base62UUID.decode!("0BllEZppLhVt2a9PljPUJ2") "063cd93e-dd59-43b6-928b-2d00a49087fc" """ @spec decode!(String.t()) :: String.t() def decode!(uuid) do case decode(uuid) do {:ok, uuid} -> uuid {:error, error} -> raise error end end @spec ensure_length(String.t(), pos_integer) :: String.t() defp ensure_length(encoded, length) when byte_size(encoded) == length, do: encoded defp ensure_length(encoded, length) do difference = length - String.length(encoded) String.duplicate("0", difference) <> encoded end @spec make_uuid(String.t()) :: {:ok, String.t()} | :error defp make_uuid(raw_uuid) do with <> <- String.downcase(raw_uuid) do {:ok, [gp_1, gp_2, gp_3, gp_4, gp_5] |> Enum.join("-")} else _ -> :error end end end