defmodule UUIDv7 do @moduledoc """ UUIDv7 for Elixir. Used for generating version 7 UUIDs using microseconds for increased clock precision. Includes `Ecto.Type` implementations. ## Examples iex> UUIDv7.generate() "018e90d8-06e8-7f9f-bfd7-6730ba98a51b" iex> UUIDv7.bingenerate() <<1, 142, 144, 216, 6, 232, 127, 159, 191, 215, 103, 48, 186, 152, 165, 27>> """ @typedoc """ A hex-encoded UUID string. """ @type t :: <<_::288>> @typedoc """ A raw binary representation of a UUID. """ @type raw :: <<_::128>> @doc """ Generates a version 7 UUID using microseconds for increased clock precision. ## Example iex> UUIDv7.generate() "018e90d8-06e8-7f9f-bfd7-6730ba98a51b" """ @spec generate() :: t def generate, do: bingenerate() |> encode() @doc """ Generates a version 7 UUID in the binary format. ## Example iex> UUIDv7.bingenerate() <<1, 142, 144, 216, 6, 232, 127, 159, 191, 215, 103, 48, 186, 152, 165, 27>> """ @spec bingenerate() :: raw def bingenerate do System.system_time(:microsecond) |> from_timestamp() end @doc """ Generates a version 7 UUID from an existing microsecond timestamp. ## Examples iex> timestamp = System.system_time(:microsecond) iex> UUIDv7.from_timestamp(timestamp) <<1, 142, 144, 216, 6, 232, 127, 159, 191, 215, 103, 48, 186, 152, 165, 27>> iex> timestamp = DateTime.utc_now() iex> UUIDv7.from_timestamp(timestamp) <<1, 142, 144, 216, 6, 232, 127, 159, 191, 215, 103, 48, 186, 152, 165, 27>> """ @spec from_timestamp(pos_integer() | DateTime.t()) :: raw() def from_timestamp(%DateTime{} = datetime) do DateTime.to_unix(datetime, :microsecond) |> from_timestamp() end def from_timestamp(time) when is_integer(time) do # Replace left-most random bits (rand_a) with increased clock precision. # We could use up to 12 bits, but since using microseconds, we only need # to use 10 bits. The remaining 2, can be rand_a. ms = div(time, 1000) us = rem(time, 1000) extra_time = trunc(us / 1000 * 1024) <> = :crypto.strong_rand_bytes(8) <> end @doc """ Encode a raw UUID to the string representation. ## Example iex> UUIDv7.encode(<<1, 142, 144, 216, 6, 232, 127, 159, 191, 215, 103, 48, 186, 152, 165, 27>>) "018e90d8-06e8-7f9f-bfd7-6730ba98a51b" """ @spec encode(raw) :: t def encode( <> ) do <> end @compile {:inline, e: 1} defp e(0), do: ?0 defp e(1), do: ?1 defp e(2), do: ?2 defp e(3), do: ?3 defp e(4), do: ?4 defp e(5), do: ?5 defp e(6), do: ?6 defp e(7), do: ?7 defp e(8), do: ?8 defp e(9), do: ?9 defp e(10), do: ?a defp e(11), do: ?b defp e(12), do: ?c defp e(13), do: ?d defp e(14), do: ?e defp e(15), do: ?f @doc """ Decode a string representation of a UUID to the raw binary version. ## Example iex> UUIDv7.decode("018e90d8-06e8-7f9f-bfd7-6730ba98a51b") <<1, 142, 144, 216, 6, 232, 127, 159, 191, 215, 103, 48, 186, 152, 165, 27>> """ @spec decode(t) :: raw | :error def decode( <> ) do <> catch :error -> :error end def decode(_), do: :error @compile {:inline, d: 1} defp d(?0), do: 0 defp d(?1), do: 1 defp d(?2), do: 2 defp d(?3), do: 3 defp d(?4), do: 4 defp d(?5), do: 5 defp d(?6), do: 6 defp d(?7), do: 7 defp d(?8), do: 8 defp d(?9), do: 9 defp d(?A), do: 10 defp d(?B), do: 11 defp d(?C), do: 12 defp d(?D), do: 13 defp d(?E), do: 14 defp d(?F), do: 15 defp d(?a), do: 10 defp d(?b), do: 11 defp d(?c), do: 12 defp d(?d), do: 13 defp d(?e), do: 14 defp d(?f), do: 15 defp d(_), do: throw(:error) if Code.ensure_loaded?(Ecto.Type) do use Ecto.Type @doc false @impl Ecto.Type def type, do: :uuid # Callback invoked by autogenerate fields. @doc false @impl Ecto.Type def autogenerate, do: generate() @doc """ Casts either a string in the canonical, human-readable UUID format or a 16-byte binary to a UUID in its canonical, human-readable UUID format. If `uuid` is neither of these, `:error` will be returned. Since both binaries and strings are represent as binaries, this means some strings you may not expect are actually also valid UUIDs in their binary form and so will be casted into their string form. ## Examples iex> raw = <<1, 141, 236, 237, 26, 200, 116, 82, 179, 112, 220, 56, 9, 179, 208, 93>> iex> UUIDv7.cast(raw) {:ok, "018deced-1ac8-7452-b370-dc3809b3d05d"} iex> UUIDv7.cast("018deced-1ac8-7452-b370-dc3809b3d05d") {:ok, "018deced-1ac8-7452-b370-dc3809b3d05d"} iex> UUIDv7.cast("warehouse worker") {:ok, "77617265-686f-7573-6520-776f726b6572"} """ @doc group: :ecto @impl Ecto.Type @spec cast(t | raw | any) :: {:ok, t} | :error def cast(uuid) def cast( <> ) do <> catch :error -> :error else hex_uuid -> {:ok, hex_uuid} end def cast(<<_::128>> = raw_uuid), do: {:ok, encode(raw_uuid)} def cast(_), do: :error @doc """ Same as `cast/1` but raises `Ecto.CastError` on invalid arguments. """ @doc group: :ecto @spec cast!(t | raw | any) :: t def cast!(uuid) do case cast(uuid) do {:ok, hex_uuid} -> hex_uuid :error -> raise Ecto.CastError, type: __MODULE__, value: uuid end end @compile {:inline, c: 1} defp c(?0), do: ?0 defp c(?1), do: ?1 defp c(?2), do: ?2 defp c(?3), do: ?3 defp c(?4), do: ?4 defp c(?5), do: ?5 defp c(?6), do: ?6 defp c(?7), do: ?7 defp c(?8), do: ?8 defp c(?9), do: ?9 defp c(?A), do: ?a defp c(?B), do: ?b defp c(?C), do: ?c defp c(?D), do: ?d defp c(?E), do: ?e defp c(?F), do: ?f defp c(?a), do: ?a defp c(?b), do: ?b defp c(?c), do: ?c defp c(?d), do: ?d defp c(?e), do: ?e defp c(?f), do: ?f defp c(_), do: throw(:error) @doc """ Converts a string representing a UUID into a raw binary. """ @doc group: :ecto @impl Ecto.Type @spec dump(uuid_string :: t | any) :: {:ok, raw} | :error def dump(uuid_string) def dump(uuid_string) do case decode(uuid_string) do :error -> :error raw_uuid -> {:ok, raw_uuid} end end @doc """ Same as `dump/1` but raises `Ecto.ArgumentError` on invalid arguments. """ @doc group: :ecto @spec dump!(t | any) :: raw def dump!(uuid) do with :error <- decode(uuid) do raise ArgumentError, "cannot dump given UUID to binary: #{inspect(uuid)}" end end @doc """ Converts a binary UUID into a string. """ @doc group: :ecto @impl Ecto.Type @spec load(raw | any) :: {:ok, t} | :error def load(<<_::128>> = raw_uuid), do: {:ok, encode(raw_uuid)} def load(<<_::64, ?-, _::32, ?-, _::32, ?-, _::32, ?-, _::96>> = string) do raise ArgumentError, "trying to load string UUID as UUID: #{inspect(string)}. " <> "Maybe you wanted to declare :uuid as your database field?" end def load(_), do: :error @doc """ Same as `load/1` but raises `Ecto.ArgumentError` on invalid arguments. """ @doc group: :ecto @spec load!(raw | any) :: t def load!(value) do case load(value) do {:ok, hex_uuid} -> hex_uuid :error -> raise ArgumentError, "cannot load given binary as UUID: #{inspect(value)}" end end end end