defmodule ShortUUID do @moduledoc """ ShortUUID - generate concise, unambiguous, URL-safe UUIDs ## Installation Add ShortUUID to your list of dependencies in `mix.exs`: def deps do [{:shortuuid, "~> #{ShortUUID.Mixfile.project()[:version] |> String.slice(0,3)}"}] end `encode/1` will translate UUIDs to base57 using lowercase and uppercase letters and digits while avoiding similar-looking characters such as l, 1, I, O and 0. ## Typical usage ShortUUID strives to do one thing well, encode UUIDs. To generate the UUIDs use any UUID library you like. Some of options out there are [Ecto](https://hexdocs.pm/ecto/Ecto.UUID.html), [Elixir UUID](https://github.com/zyro/elixir-uuid) and [Erlang UUID](https://github.com/okeuday/uuid). ## Notes The output is padded to a length of 22 with the first character of the alphabet (2). iex> ShortUUID.encode!("00000000-0000-0000-0000-000000000000") "2222222222222222222222" iex> ShortUUID.encode!("00000001-0001-0001-0001-000000000001") "UD6ibhr3V4YXvriP822222" iex> ShortUUID.decode!("UD6ibhr3V4YXvriP822222") "00000001-0001-0001-0001-000000000001" The input format is quite flexible as any non base16 chars are stripped from the input which is then downcased, however only the following formats are covered by tests thus guaranteed to work. * `2a162ee5-02f4-4701-9e87-72762cbce5e2` * `2a162ee502f447019e8772762cbce5e2` * `{2a162ee5-02f4-4701-9e87-72762cbce5e2}` * `{2a162ee502f447019e8772762cbce5e2}` Letter case is not relevant. Also supported since `v2.1.0` is the encoding of binary UUIDs iex> ShortUUID.encode!(<<0xFA, 0x62, 0xAF, 0x80, 0xA8, 0x61, 0x45, 0x6C, 0xAB, 0x77, 0xD5, 0x67, 0x7E, 0x2E, 0x8B, 0xA8>>) "PuQURs6h2XSBBVNgqSHJZn" ## Using ShortUUID with Ecto If you would like to use ShortUUIDs with Ecto check out [ecto_shortuuid](https://github.com/gpedic/ecto_shortuuid). ## Acknowledgments This project was inspired by [skorokithakis/shortuuid](https://github.com/skorokithakis/shortuuid). """ @alphabet "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" @alphabet_tuple @alphabet |> String.split("", trim: true) |> List.to_tuple() @char_to_int_map @alphabet |> String.split("", trim: true) |> Enum.with_index() |> Enum.reduce(%{}, fn {char, idx}, acc -> Map.put_new(acc, char, idx) end) @alphabet_length 57 @padding_length 22 @padding_char "2" @only_alphabet ~r/^[#{@alphabet}]*$/ @doc """ Encode a UUID to ShortUUID. ## Examples iex> ShortUUID.encode("2a162ee5-02f4-4701-9e87-72762cbce5e2") {:ok, "keATfB8JP2ggT7U9JZrpV9"} """ @spec encode(binary) :: {:ok, String.t()} | {:error, String.t()} def encode(<>) do {:ok, uuid |> int_to_string |> pad_shortuuid} end def encode(uuid) when is_binary(uuid) do stripped_uuid = strip_uuid(uuid) case stripped_uuid |> String.length() == 32 do true -> encoded_uuid = stripped_uuid |> String.to_integer(16) |> int_to_string |> pad_shortuuid {:ok, encoded_uuid} _anything_else -> {:error, "Invalid UUID"} end end def encode(_), do: {:error, "Invalid UUID"} @doc """ Encode a UUID to ShortUUID. Similar to `encode/1` but raises an ArgumentError if it cannot process the UUID. ## Examples iex> ShortUUID.encode!("2a162ee5-02f4-4701-9e87-72762cbce5e2") "keATfB8JP2ggT7U9JZrpV9" """ @spec encode!(binary) :: String.t() def encode!(uuid) do case encode(uuid) do {:ok, encoded_uuid} -> encoded_uuid {:error, msg} -> raise ArgumentError, message: msg end end @doc """ Decode a ShortUUID. ## Examples iex> ShortUUID.decode("keATfB8JP2ggT7U9JZrpV9") {:ok, "2a162ee5-02f4-4701-9e87-72762cbce5e2"} """ @spec decode(String.t()) :: {:ok, String.t()} | {:error, String.t()} def decode(string) when is_binary(string) do case string |> matches_alphabet? do true -> string_to_int(string) |> int_to_hex_string |> pad_uuid |> format_uuid_string false -> {:error, "Invalid input"} end end def decode(_), do: {:error, "Invalid input"} @doc """ Decode a ShortUUID. Similar to `decode/1` but raises an ArgumentError if the encoded UUID is invalid. ## Examples iex> ShortUUID.decode!("keATfB8JP2ggT7U9JZrpV9") "2a162ee5-02f4-4701-9e87-72762cbce5e2" """ @spec decode!(String.t()) :: String.t() def decode!(string) do case decode(string) do {:ok, uuid} -> uuid {:error, message} -> raise ArgumentError, message: message end end defp int_to_hex_string(number) do number |> to_hex_string |> String.downcase() end @spec divmod(Integer.t(), Integer.t()) :: [Integer.t()] defp divmod(dividend, divisor) do [div(dividend, divisor), rem(dividend, divisor)] end @spec int_to_string(Integer.t(), list()) :: [String.t()] defp int_to_string(number, acc \\ []) defp int_to_string(number, acc) when number > 0 do [result, remainder] = divmod(number, @alphabet_length) int_to_string(result, [acc | elem(@alphabet_tuple, remainder)]) end defp int_to_string(0, acc), do: acc |> to_string defp strip_uuid(uuid) do uuid |> String.downcase() |> String.replace(~r/[^0-9a-f]/, "") end defp matches_alphabet?(string) do Regex.match?(@only_alphabet, string) end defp string_to_int(string) do string |> String.split("", trim: true) |> Enum.reverse() |> Enum.reduce(0, fn char, acc -> %{^char => index} = @char_to_int_map acc * @alphabet_length + index end) end defp to_hex_string(number) do number |> Integer.to_string(16) end defp pad_shortuuid(encoded_uuid) do encoded_uuid |> String.pad_trailing(@padding_length, @padding_char) end defp pad_uuid(string) do string |> String.pad_leading(32, "0") end defp format_uuid_string(<>) do {:ok, <>} end defp format_uuid_string(_invalid), do: {:error, "Invalid UUID"} end