lib/uuidv7.ex

defmodule UUIDv7 do
  @moduledoc """
  A UUID v7 implementation and `Ecto.Type` for Elixir.

  The RFC for the version 7 UUID: [RFC 9562](https://datatracker.ietf.org/doc/rfc9562/).

  This library includes an `Ecto.Type` to (auto-)generate version 7 UUIDs in `Ecto.Schema` and beyond.

  ## Installation

  The package can be installed by adding `uuidv7` to your list of dependencies in `mix.exs`:

  ```elixir
  def deps do
    [{:uuidv7, "~> 1.0"}]
  end
  ```

  ## Usage

  In your database schema, change primary key attribute from `:binary_id` to `UUIDv7`:

  ```elixir
  def MyApp.Schemas.User do
    @primary_key {:id, UUIDv7, autogenerate: true}
  end
  ```
  """

  use Ecto.Type

  @typedoc """
  A hex-encoded UUID string.
  """
  @type t :: <<_::288>>

  @typedoc """
  A raw binary representation of a UUID.
  """
  @type raw :: <<_::128>>

  @doc false
  @spec type() :: :uuid
  def type, do: :uuid

  defdelegate cast(value), to: Ecto.UUID

  defdelegate cast!(value), to: Ecto.UUID

  defdelegate dump(value), to: Ecto.UUID

  defdelegate dump!(value), to: Ecto.UUID

  defdelegate load(value), to: Ecto.UUID

  defdelegate load!(value), to: Ecto.UUID

  @doc false
  @spec autogenerate() :: t()
  def autogenerate, do: generate()

  @doc """
  Generates a version 7 UUID.
  """
  @spec generate() :: t()
  def generate, do: Ecto.UUID.cast!(bingenerate())

  @doc """
  Generates a version 7 UUID based on the timestamp (ms).
  """
  @spec generate(non_neg_integer()) :: t()
  def generate(milliseconds), do: milliseconds |> bingenerate() |> Ecto.UUID.cast!()

  @doc """
  Generates a version 7 UUID in the binary format.
  """
  @spec bingenerate() :: raw()
  def bingenerate, do: :millisecond |> System.system_time() |> bingenerate()

  @doc """
  Generates a version 7 UUID in the binary format based on the timestamp (ms).
  """
  @spec bingenerate(non_neg_integer()) :: raw()
  def bingenerate(milliseconds) do
    <<u1::12, u2::62, _::6>> = :crypto.strong_rand_bytes(10)
    <<milliseconds::48, 7::4, u1::12, 2::2, u2::62>>
  end

  @doc """
  Extracts the timestamp (ms) from the version 7 UUID.
  """
  @spec timestamp(t() | raw()) :: non_neg_integer()
  def timestamp(<<milliseconds::48, 7::4, _::76>>), do: milliseconds
  def timestamp(<<_::288>> = uuid), do: uuid |> Ecto.UUID.dump!() |> timestamp()
end