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) timestamp = Keyword.get(opts, :time, System.system_time(:millisecond)) %Codec{ case: case, 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) @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) __MODULE__.generate!( case: case, prefix: prefix, size: size, rand_size: rand_size, delimiter: delimiter ) 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