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 ## Usage The encode/1 function translates UUIDs to base57 using lowercase and uppercase letters, as well as digits, while ensuring that similar-looking characters such as 'l', '1', 'I', 'O', and '0' are avoided. iex> ShortUUID.encode("ed7ba470-8e54-465e-825c-99712043e01c") {:ok, "kGSmvLEEuzjcFZTChXwZpz"} The decode/1 function will turn the ShortUUID back into a regular UUID iex> ShortUUID.decode("kGSmvLEEuzjcFZTChXwZpz") {:ok, "ed7ba470-8e54-465e-825c-99712043e01c"} ShortUUID strives to do one thing well, encode UUIDs. To generate UUIDs use any UUID library of your choice. Some options: [Ecto](https://hexdocs.pm/ecto/Ecto.UUID.html) [Elixir UUID](https://github.com/zyro/elixir-uuid) [Erlang UUID](https://github.com/okeuday/uuid) ## Notes ShortUUID supports the following input formats: * `2a162ee5-02f4-4701-9e87-72762cbce5e2` * `2a162ee502f447019e8772762cbce5e2` Letter case is not relevant. Since version v2.1.0, ShortUUID also supports the encoding of binary UUIDs. iex> ShortUUID.encode!(<<0xFA, 0x62, 0xAF, 0x80, 0xA8, 0x61, 0x45, 0x6C, 0xAB, 0x77, 0xD5, 0x67, 0x7E, 0x2E, 0x8B, 0xA8>>) "nZJHSqgNVBBSX2h6sRUQuP" ## Using ShortUUID with Ecto If you want to use ShortUUIDs with Ecto, you can explore the [ecto_shortuuid](https://github.com/gpedic/ecto_shortuuid) library. ## Acknowledgments This project was inspired by [skorokithakis/shortuuid](https://github.com/skorokithakis/shortuuid). """ import Bitwise @alphabet "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" @alphabet_tuple @alphabet |> String.graphemes() |> List.to_tuple() @codepoint_index String.to_charlist(@alphabet) |> Enum.with_index() @doc """ Encodes a UUID into a ShortUUID string. """ @spec encode(binary) :: {:ok, String.t()} | {:error, String.t()} def encode(uuid) def encode( <> ) do encode_uuid_string( <> ) end def encode(<<_::binary-size(32)>> = uuid) do encode_uuid_string(uuid) end def encode( <> ) do encode_uuid_string( <> ) end def encode(<>) do encode_uuid_string(<>) end def encode(<> = uuid) when is_binary(uuid) do uuid = int_to_string(int_value) |> pad_shortuuid() {:ok, uuid} end def encode(_), do: {:error, "Invalid UUID"} @doc """ Encodes a UUID into a ShortUUID string. Raises an ArgumentError if the UUID is invalid. """ @spec encode!(binary) :: String.t() | no_return() def encode!(uuid) do case encode(uuid) do {:ok, encoded_uuid} -> encoded_uuid {:error, msg} -> raise ArgumentError, message: msg end end @doc """ Decodes a ShortUUID string into a UUID. """ @spec decode(String.t()) :: {:ok, String.t()} | {:error, String.t()} def decode( <> ) do uuid_int_value = [ c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16, c17, c18, c19, c20, c21, c22 ] |> Enum.reduce(0, fn char, acc -> acc * 57 + char_to_index(char) end) # verify our int value is <= 128 bits # shift it right 128 bits, the result must be 0 case uuid_int_value >>> 128 do 0 -> uuid = <> |> Base.encode16(case: :lower) |> format_uuid() {:ok, uuid} _ -> {:error, "Invalid input"} end rescue _ -> {:error, "Invalid input"} end def decode(_string) do {:error, "Invalid input"} end @doc """ Decodes a ShortUUID string into a UUID. Raises an ArgumentError if the ShortUUID is invalid. """ @spec decode!(String.t()) :: String.t() | no_return() def decode!(string) do case decode(string) do {:ok, uuid} -> uuid {:error, message} -> raise ArgumentError, message: message end end ## Helper functions # # These helper functions are utilized to process UUIDs into their desired form: # - `format_uuid/1`: Formats a UUID into a standard string form. # - `int_to_string/2`: Converts a given integer to a string, according to the predefined alphabet. # - `pad_shortuuid/2`: Pads the given short UUID with the first character of the alphabet until its size reaches 22. # - `char_to_index/1`: Retrieves the index of a given character in the alphabet. defp encode_uuid_string(uuid) do uuid |> Base.decode16(case: :mixed) |> case do {:ok, decoded_uuid} -> encode(decoded_uuid) _ -> {:error, "Invalid UUID"} end end # Formats a UUID into a standard string form. defp format_uuid( <> ) do a <> "-" <> b <> "-" <> c <> "-" <> d <> "-" <> e end # Converts a given integer to a string, according to the predefined alphabet. defp int_to_string(number, acc \\ []) defp int_to_string(0, acc), do: acc |> to_string defp int_to_string(number, acc) when number > 0 do int_to_string(div(number, 57), [elem(@alphabet_tuple, rem(number, 57)) | acc]) end # Pads the given short UUID with the first character of the alphabet until its size reaches 22, 176 bits. defp pad_shortuuid(<<_::176>> = shortuuid), do: shortuuid defp pad_shortuuid(shortuuid), do: pad_shortuuid(<<50>> <> shortuuid) # Retrieves the index of a given character in the alphabet. # This function is generated for each character in the alphabet for quick access. @compile {:inline, char_to_index: 1} for {char, index} <- @codepoint_index do defp char_to_index(unquote(char)), do: unquote(index) end defp char_to_index(_), do: raise(ArgumentError) end