defmodule UUID do @moduledoc """ A boxed UUID — a struct that wraps the raw 16-byte binary. Version-agnostic: any 16-byte UUID (v1, v4, v7, …) can live inside. The version is encoded in the bytes themselves, so the struct doesn't need to know which generator produced it. ## Why a struct? Plain hex strings (`"019d..."`) force a `binary ↔ hex` round-trip on every Ecto insert. `%UUID{bytes: <<_::128>>}` carries the raw bytes and only formats to hex on `to_string/1`, `inspect/1`, or JSON encoding — so the formatting cost is paid once, when something actually displays the value, instead of on every database write. ## Use with Ecto See `UUIDv7.Boxed` for an Ecto type that autogenerates `%UUID{}` values with version 7 timestamps. ## Constructing UUID.wrap(<<_::128>> = raw_bytes) UUID.parse("019d2566-fe40-7000-8000-000000000000") UUIDv7.Boxed.autogenerate() """ @type t :: %__MODULE__{bytes: <<_::128>>} defstruct [:bytes] @doc """ Builds a `%UUID{}` from a 16-byte raw binary or a 36-char hex string. Raises `ArgumentError` on anything else — use `parse/1` for the `{:ok, _} | :error` variant. ## Examples iex> UUID.new("018e90d8-06e8-7f9f-bfd7-6730ba98a51b") |> UUID.to_string() "018e90d8-06e8-7f9f-bfd7-6730ba98a51b" iex> raw = <<1, 142, 144, 216, 6, 232, 127, 159, 191, 215, 103, 48, 186, 152, 165, 27>> iex> UUID.new(raw).bytes == raw true """ @spec new(<<_::128>> | binary()) :: t def new(<<_::128>> = raw), do: %__MODULE__{bytes: raw} def new(<<_::288>> = hex) do case parse(hex) do {:ok, uuid} -> uuid :error -> raise ArgumentError, "invalid UUID hex string: #{inspect(hex)}" end end def new(other), do: raise(ArgumentError, "expected 16-byte binary or 36-char hex string, got: #{inspect(other)}") @doc """ Returns the raw 16-byte binary inside a `%UUID{}`. """ @spec unwrap(t) :: <<_::128>> def unwrap(%__MODULE__{bytes: bytes}), do: bytes @doc """ Parses a 36-char hex string into a `%UUID{}`. Returns `{:ok, %UUID{}}` or `:error`. Mirrors `Date.from_iso8601/1`. ## Example iex> {:ok, uuid} = UUID.parse("018e90d8-06e8-7f9f-bfd7-6730ba98a51b") iex> uuid.bytes <<1, 142, 144, 216, 6, 232, 127, 159, 191, 215, 103, 48, 186, 152, 165, 27>> """ @spec parse(binary()) :: {:ok, t} | :error def parse(<<_::288>> = hex) do case UUIDv7.decode(hex) do :error -> :error <<_::128>> = raw -> {:ok, %__MODULE__{bytes: raw}} end end def parse(_), do: :error @doc """ Renders the boxed UUID as its canonical hex string. """ @spec to_string(t) :: binary() def to_string(%__MODULE__{bytes: bytes}), do: UUIDv7.encode(bytes) defimpl String.Chars do def to_string(%UUID{bytes: bytes}), do: UUIDv7.encode(bytes) end defimpl Inspect do # Renders as `UUID.new("...")` so the inspected form is valid Elixir # that can be pasted back into code without any import. def inspect(%UUID{bytes: bytes}, _opts) do ~s|UUID.new("| <> UUIDv7.encode(bytes) <> ~s|")| end end if Code.ensure_loaded?(JSON.Encoder) do defimpl JSON.Encoder do def encode(%UUID{bytes: bytes}, encoder) do encoder.(UUIDv7.encode(bytes), encoder) end end end if Code.ensure_loaded?(Jason.Encoder) do defimpl Jason.Encoder do def encode(%UUID{bytes: bytes}, opts) do Jason.Encode.string(UUIDv7.encode(bytes), opts) end end end end