Current section
Files
Jump to
Current section
Files
lib/shortuuid.ex
defmodule ShortUUID do
@moduledoc """
ShortUUID is a simple UUID shortener library.
## Installation
Add ShortUUID to your list of dependencies in `mix.exs`:
def deps do
[{:shortuuid, "~> #{ShortUUID.Mixfile.project[:version]}"}]
end
Optionally configure the alphabet to be used for encoding in `config.exs`:
config :shortuuid,
alphabet: "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
Note that for this to take effect currently requires recompiling the dependency.
The default alphabet (above) 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
Use ShortUUID to shorten UUID strings generated by other libraries such as
[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
For compatibility with the python version we also pad values to the length it takes
to store a UUID in the chosen alphabet.
Any UUID will be padded to length 22 in case of the default base 57 alphabet.
The pad character is always the first character of the alphabet which represents zero.
In the default alphabet that character is 2 since it doesn't include 0 and 1.
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"
If we didn't do the padding the nil UUID for example would simply encode into an empty string
since it's int value is 0.
Some supported UUID input formats are:
* `2a162ee5-02f4-4701-9e87-72762cbce5e2`
* `2a162ee502f447019e8772762cbce5e2`
* `{2a162ee5-02f4-4701-9e87-72762cbce5e2}`
Letter case is not relevant.
## Acknowledgments
This project was inspired by [skorokithakis/shortuuid](https://github.com/skorokithakis/shortuuid).
"""
@alphabet (Application.get_env(:shortuuid, :alphabet) ||
"23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz")
|> String.graphemes |> Enum.sort |> Enum.dedup |> List.to_string
@alphabet_graphemes String.graphemes(@alphabet)
@alphabet_length String.length(@alphabet)
# Support default padding for python lib compatibility
# Calculate the necessary length to fit the entire UUID given the current alphabet.
@encode_padding_min_length round(Float.ceil(:math.log(:math.pow(2, 128)) / :math.log(@alphabet_length)))
@only_alphabet ~r/^[#{@alphabet}]*$/
@doc """
Encode a UUID using the chosen alphabet.
## Examples
iex> ShortUUID.encode("2a162ee5-02f4-4701-9e87-72762cbce5e2")
{:ok, "keATfB8JP2ggT7U9JZrpV9"}
"""
@spec encode(String.t) :: {:ok, String.t} | {:error, String.t}
def encode(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
|> add_encode_padding
{:ok, encoded_uuid}
_anything_else ->
{:error, "Invalid UUID"}
end
end
@doc """
Encode a UUID using the chosen alphabet.
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!(String.t) :: 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 shortened UUID.
## Examples
iex> ShortUUID.decode!("keATfB8JP2ggT7U9JZrpV9")
"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
|> add_decode_padding
|> format_uuid_string
false -> {:error, "Alphabet doesn't match string #{string}"}
end
end
@doc """
Decode a shortened UUID.
Similar to `decode/1` but raises an ArgumentError if the encoded UUID is invalid.
"""
@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 | String.at(@alphabet, 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.reverse(string)
|> String.graphemes
|> Enum.reduce(0, fn(char, acc) ->
acc * @alphabet_length + Enum.find_index(@alphabet_graphemes, &(&1 == char))
end)
end
defp to_hex_string(number) do
number |> Integer.to_string(16)
end
defp add_encode_padding(encoded_uuid) do
encoded_uuid
|> String.pad_trailing(@encode_padding_min_length, List.first(@alphabet_graphemes))
end
defp add_decode_padding(string) do
string |> String.pad_leading(32, "0")
end
defp format_uuid_string(<<u0::64, u1::32, u2::32, u3::32, u4::96>>) do
{:ok, <<u0::64, ?-, u1::32, ?-, u2::32, ?-, u3::32, ?-, u4::96>>}
end
defp format_uuid_string(_invalid), do: {:error, "Decoded string is not a UUID"}
end