Current section

Files

Jump to
uxid lib uxid.ex
Raw

lib/uxid.ex

defmodule UXID do
@moduledoc """
Generates UXIDs and acts as an Ecto ParameterizedType
**U**ser e**X**perience focused **ID**entifiers (UXIDs) are identifiers which:
* Describe the resource (aid in debugging and investigation)
* Work well with copy and paste (double clicking selects the entire ID)
* Can be shortened for low cardinality resources
* Are secure against enumeration attacks
* Can be generated by application code (not tied to the datastore)
* Are K-sortable (lexicographically sortable by time - works well with datastore indexing)
* Do not require any coordination (human or automated) at startup, or generation
* Are very unlikely to collide (more likely with less randomness)
* Are easily and accurately transmitted to another human using a telephone
Many of the concepts of Stripe IDs have been used in this library.
"""
@typedoc "Options for generating a UXID"
@type option ::
{:case, atom()}
| {:time, integer()}
| {:size, atom()}
| {:rand_size, integer()}
| {:prefix, String.t()}
| {:delimiter, String.t()}
@type options :: [option()]
@typedoc "A UXID represented as a String"
@type t() :: String.t()
alias UXID.{Codec, Encoder}
alias UXID.Decoder
@spec generate(opts :: options()) :: {:ok, __MODULE__.t()}
@doc """
Returns an encoded UXID string along with response status.
"""
def generate(opts \\ []) do
{:ok, %Codec{string: string}} = new(opts)
{:ok, string}
end
@spec generate!(opts :: options()) :: __MODULE__.t()
@doc """
Returns an unwrapped encoded UXID string.
"""
def generate!(opts \\ []) do
{:ok, uxid} = generate(opts)
uxid
end
@spec new(opts :: options()) :: {:ok, Codec.t()}
@doc """
Returns a new UXID.Codec struct. This is useful for development.
"""
def new(opts \\ []) do
case = Keyword.get(opts, :case, encode_case())
prefix = Keyword.get(opts, :prefix)
rand_size = Keyword.get(opts, :rand_size)
size = Keyword.get(opts, :size)
delimiter = Keyword.get(opts, :delimiter)
compact_time = Keyword.get(opts, :compact_time)
timestamp = Keyword.get(opts, :time, System.system_time(:millisecond))
%Codec{
case: case,
compact_time: compact_time,
prefix: prefix,
rand_size: rand_size,
size: size,
time: timestamp,
delimiter: delimiter
}
|> Encoder.process()
end
def encode_case(), do: Application.get_env(:uxid, :case, :lower)
@doc """
Returns the minimum size configuration.
When set, any requested size smaller than this will be upgraded.
"""
def min_size(), do: Application.get_env(:uxid, :min_size, nil)
@doc """
Returns whether compact time encoding is enabled for small sizes.
When true, :xs/:xsmall and :s/:small use 40 bits for timestamp instead of 48,
adding 8 bits to randomness. This is a global policy that can be overridden
per-call with the `compact_time` option.
This provides perfect 5-bit Crockford Base32 alignment (8 chars vs 10 chars).
K-sortability is maintained until ~September 2039.
"""
def compact_small_times(), do: Application.get_env(:uxid, :compact_small_times, false)
@spec decode(String.t()) :: {:ok, %Codec{}}
@doc """
Decodes a UXID string and returns a Codec struct with extracted components.
"""
def decode(uxid) do
%Codec{
string: uxid
}
|> Decoder.process()
end
# Define additional functions for custom Ecto type if Ecto is loaded
if Code.ensure_loaded?(Ecto.ParameterizedType) do
@behaviour Ecto.ParameterizedType
@doc """
Generates a loaded version of the UXID.
"""
def autogenerate(opts) do
case = Map.get(opts, :case, encode_case())
prefix = Map.get(opts, :prefix)
size = Map.get(opts, :size)
rand_size = Map.get(opts, :rand_size)
delimiter = Map.get(opts, :delimiter)
compact_time = Map.get(opts, :compact_time)
__MODULE__.generate!(
case: case,
prefix: prefix,
size: size,
rand_size: rand_size,
delimiter: delimiter,
compact_time: compact_time
)
end
@doc """
Returns the underlying schema type for a UXID.
"""
def type(_opts), do: :string
@doc """
Converts the options specified in the field macro into parameters to be used in other callbacks.
"""
def init(opts) do
# validate_opts(opts)
Enum.into(opts, %{})
end
@doc """
Casts the given input to the UXID ParameterizedType with the given parameters.
"""
def cast(data, _params) do
cast_binary(data)
end
defp cast_binary(nil), do: {:ok, nil}
defp cast_binary(term) when is_binary(term), do: {:ok, term}
defp cast_binary(_), do: :error
@doc """
Loads the given term into a UXID.
"""
def load(data, _loader, _params), do: {:ok, data}
@doc """
Dumps the given term into an Ecto native type.
"""
def dump(data, _dumper, _params), do: {:ok, data}
@doc """
Dictates how the type should be treated if embedded.
For UXIDs, we use :self since they're already strings.
"""
def embed_as(_format, _params), do: :self
@doc """
Checks if two UXIDs are equal.
"""
def equal?(left, right, _params), do: left == right
end
end