Current section
Files
Jump to
Current section
Files
lib/uuid.ex
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