Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
### Upcoming

* (None yet)
* Adds delimiter option (default is '_')
* Adds UXID.Codec with encoding struct and type
* Fixes Dialyzer issues

### 2.0.0 / 2025-04-27

#### Breaking Changes

* Adds case config and functionality
* Makes lowercase the default
* Removes deprecated Ecto.UXID
Expand Down
70 changes: 15 additions & 55 deletions lib/uxid.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,6 @@ defmodule UXID do
Many of the concepts of Stripe IDs have been used in this library.
"""

defstruct [
:case,
:delimiter,
:encoded,
:prefix,
:rand_size,
:rand,
:rand_encoded,
:size,
:string,
:time,
:time_encoded
]

@typedoc "Options for generating a UXID"
@type option ::
{:case, atom()} |
Expand All @@ -43,68 +29,42 @@ defmodule UXID do
@type options :: [option()]

@typedoc "A UXID represented as a String"
@type uxid_string :: String.t()

@typedoc "An error string returned by the library if generation fails"
@type error_string :: String.t()

@typedoc "A UXID struct"
@type t() :: %__MODULE__{
case: atom() | nil,
encoded: String.t() | nil,
prefix: String.t() | nil,
delimiter: String.t() | nil,
rand_size: pos_integer() | nil,
rand: binary() | nil,
rand_encoded: String.t() | nil,
size: atom() | nil,
string: String.t() | nil,
time: pos_integer() | nil,
time_encoded: String.t() | nil
}

alias UXID.Encoder

@default_delimiter "_"

@spec generate(opts :: options()) :: {:ok, uxid_string()} | {:error, error_string()}
@type t() :: String.t()

alias UXID.{Codec, Encoder}

@spec generate(opts :: options()) :: {:ok, __MODULE__.t()}
@doc """
Returns an encoded UXID string along with response status.
"""
def generate(opts \\ []) do
case new(opts) do
{:ok, %__MODULE__{string: string}} ->
{:ok, string}
{:ok, %Codec{string: string}} = new(opts)

_other ->
{:error, "Unknown error"}
end
{:ok, string}
end

@spec generate!(opts :: options()) :: uxid_string()
@spec generate!(opts :: options()) :: __MODULE__.t()
@doc """
Returns an unwrapped encoded UXID string or raises on error.
Returns an unwrapped encoded UXID string.
"""
def generate!(opts \\ []) do
case generate(opts) do
{:ok, uxid} -> uxid
{:error, error} -> raise error
end
{:ok, uxid} = generate(opts)
uxid
end

@spec new(opts :: options()) :: {:ok, __MODULE__.t()} | {:error, error_string()}
@spec new(opts :: options()) :: {:ok, Codec.t()}
@doc """
Returns a new UXID struct. This is useful for development.
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, @default_delimiter)
delimiter = Keyword.get(opts, :delimiter)
timestamp = Keyword.get(opts, :time, System.system_time(:millisecond))

%__MODULE__{
%Codec{
case: case,
prefix: prefix,
rand_size: rand_size,
Expand Down
34 changes: 34 additions & 0 deletions lib/uxid/codec.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
defmodule UXID.Codec do
@moduledoc """
This represents a UXID during encoding with all of the fields split out.
"""

defstruct [
:case,
:delimiter,
:encoded,
:prefix,
:rand_size,
:rand,
:rand_encoded,
:size,
:string,
:time,
:time_encoded
]

@typedoc "A UXID struct during encoding"
@type t() :: %__MODULE__{
case: atom() | nil,
encoded: String.t() | nil,
prefix: String.t() | nil,
delimiter: String.t() | nil,
rand_size: pos_integer() | nil,
rand: binary() | nil,
rand_encoded: String.t() | nil,
size: atom() | nil,
string: String.t() | nil,
time: pos_integer() | nil,
time_encoded: String.t() | nil
}
end
52 changes: 30 additions & 22 deletions lib/uxid/encoder.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,19 @@ defmodule UXID.Encoder do
Encodes UXID structs into strings
"""

alias UXID.Codec

@default_delimiter "_"
@default_rand_size 10

def process(%UXID{} = struct) do
def process(%Codec{} = struct) do
uxid =
struct
|> ensure_time()
|> ensure_rand_size()
|> ensure_rand()
|> ensure_case()
|> ensure_delimiter()
|> encode()
|> prefix()

Expand All @@ -20,59 +24,63 @@ defmodule UXID.Encoder do

# === Private helpers

defp ensure_time(%UXID{time: nil} = uxid),
defp ensure_time(%Codec{time: nil} = uxid),
do: %{uxid | time: System.system_time(:millisecond)}

defp ensure_time(uxid),
do: uxid

defp ensure_rand_size(%UXID{rand_size: nil, size: :xs} = uxid),
defp ensure_rand_size(%Codec{rand_size: nil, size: :xs} = uxid),
do: %{uxid | rand_size: 0}

defp ensure_rand_size(%UXID{rand_size: nil, size: :xsmall} = uxid),
defp ensure_rand_size(%Codec{rand_size: nil, size: :xsmall} = uxid),
do: %{uxid | rand_size: 0}

defp ensure_rand_size(%UXID{rand_size: nil, size: :s} = uxid),
defp ensure_rand_size(%Codec{rand_size: nil, size: :s} = uxid),
do: %{uxid | rand_size: 2}

defp ensure_rand_size(%UXID{rand_size: nil, size: :small} = uxid),
defp ensure_rand_size(%Codec{rand_size: nil, size: :small} = uxid),
do: %{uxid | rand_size: 2}

defp ensure_rand_size(%UXID{rand_size: nil, size: :m} = uxid),
defp ensure_rand_size(%Codec{rand_size: nil, size: :m} = uxid),
do: %{uxid | rand_size: 5}

defp ensure_rand_size(%UXID{rand_size: nil, size: :medium} = uxid),
defp ensure_rand_size(%Codec{rand_size: nil, size: :medium} = uxid),
do: %{uxid | rand_size: 5}

defp ensure_rand_size(%UXID{rand_size: nil, size: :l} = uxid),
defp ensure_rand_size(%Codec{rand_size: nil, size: :l} = uxid),
do: %{uxid | rand_size: 7}

defp ensure_rand_size(%UXID{rand_size: nil, size: :large} = uxid),
defp ensure_rand_size(%Codec{rand_size: nil, size: :large} = uxid),
do: %{uxid | rand_size: 7}

defp ensure_rand_size(%UXID{rand_size: nil, size: :xl} = uxid),
defp ensure_rand_size(%Codec{rand_size: nil, size: :xl} = uxid),
do: %{uxid | rand_size: 10}

defp ensure_rand_size(%UXID{rand_size: nil, size: :xlarge} = uxid),
defp ensure_rand_size(%Codec{rand_size: nil, size: :xlarge} = uxid),
do: %{uxid | rand_size: 10}

defp ensure_rand_size(%UXID{rand_size: nil} = uxid),
defp ensure_rand_size(%Codec{rand_size: nil} = uxid),
do: %{uxid | rand_size: @default_rand_size}

defp ensure_rand_size(uxid), do: uxid

defp ensure_rand(%UXID{rand_size: rand_size, rand: nil} = uxid),
defp ensure_rand(%Codec{rand_size: rand_size, rand: nil} = uxid),
do: %{uxid | rand: :crypto.strong_rand_bytes(rand_size)}

defp ensure_rand(uxid), do: uxid

defp ensure_case(%UXID{case: nil} = uxid),
defp ensure_case(%Codec{case: nil} = uxid),
do: %{uxid | case: UXID.encode_case()}

defp ensure_case(%UXID{case: encode_case} = uxid),
do: %{uxid | case: encode_case}
defp ensure_case(uxid), do: uxid

defp ensure_delimiter(%Codec{delimiter: nil} = uxid),
do: %{uxid | delimiter: @default_delimiter}

defp ensure_delimiter(uxid), do: uxid

defp encode(%UXID{} = input) do
defp encode(%Codec{} = input) do
uxid =
input
|> encode_time()
Expand All @@ -81,7 +89,7 @@ defmodule UXID.Encoder do
%{uxid | encoded: uxid.time_encoded <> uxid.rand_encoded}
end

defp encode_time(%UXID{case: case, time: time, time_encoded: nil} = uxid) do
defp encode_time(%Codec{case: case, time: time, time_encoded: nil} = uxid) do
string = encode_time(<<time::unsigned-size(48)>>, case)
%{uxid | time_encoded: string}
end
Expand All @@ -102,7 +110,7 @@ defmodule UXID.Encoder do
time_encoded -> time_encoded
end

defp encode_rand(%UXID{case: case, rand_size: rand_size, rand_encoded: nil} = uxid) do
defp encode_rand(%Codec{case: case, rand_size: rand_size, rand_encoded: nil} = uxid) do
string =
rand_size
|> :crypto.strong_rand_bytes()
Expand Down Expand Up @@ -316,9 +324,9 @@ defmodule UXID.Encoder do

defp encode_rand(_, _any), do: :error

defp prefix(%UXID{prefix: nil, encoded: encoded} = uxid), do: %{uxid | string: encoded}
defp prefix(%Codec{prefix: nil, encoded: encoded} = uxid), do: %{uxid | string: encoded}

defp prefix(%UXID{prefix: prefix, encoded: encoded, delimiter: delimiter} = uxid),
defp prefix(%Codec{prefix: prefix, encoded: encoded, delimiter: delimiter} = uxid),
do: %{uxid | string: prefix <> delimiter <> encoded}

# Encode functions
Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ defmodule UXID.MixProject do
{:ecto, "~> 3.12", optional: true},

# Development, Documentation, Testing, ...
{:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false},
{:ex_doc, "~> 0.23", only: :dev},
{:benchee, "~> 1.0", only: :dev},
{:benchee_html, "~> 1.0", only: :dev},
Expand Down Expand Up @@ -70,7 +71,6 @@ defmodule UXID.MixProject do
[
licenses: ["MIT"],
links: %{
"UXID Project" => "https://github.com/riddler/uxid",
"GitHub" => "https://github.com/riddler/uxid-ex"
}
]
Expand Down
2 changes: 2 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
"benchee_json": {:hex, :benchee_json, "1.0.0", "cc661f4454d5995c08fe10dd1f2f72f229c8f0fb1c96f6b327a8c8fc96a91fe5", [:mix], [{:benchee, ">= 0.99.0 and < 2.0.0", [hex: :benchee, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "da05d813f9123505f870344d68fb7c86a4f0f9074df7d7b7e2bb011a63ec231c"},
"decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
"deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"},
"dialyxir": {:hex, :dialyxir, "1.4.6", "7cca478334bf8307e968664343cbdb432ee95b4b68a9cba95bdabb0ad5bdfd9a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "8cf5615c5cd4c2da6c501faae642839c8405b49f8aa057ad4ae401cb808ef64d"},
"earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"},
"ecto": {:hex, :ecto, "3.12.5", "4a312960ce612e17337e7cefcf9be45b95a3be6b36b6f94dfb3d8c361d631866", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6eb18e80bef8bb57e17f5a7f068a1719fbda384d40fc37acb8eb8aeca493b6ea"},
"ecto_ulid": {:hex, :ecto_ulid, "0.3.0", "fd6426ff30da547d6f5c31e43170ad307cbda2e680c7793c891e9ef86bd68dbe", [:mix], [{:ecto, "~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "82cb3be73635587700f1a4aec08e4ad894e7b3d2f6ed63236b9f3afd3859c74d"},
"erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"},
"ex_doc": {:hex, :ex_doc, "0.37.3", "f7816881a443cd77872b7d6118e8a55f547f49903aef8747dbcb345a75b462f9", [:mix], [{:earmark_parser, "~> 1.4.42", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "e6aebca7156e7c29b5da4daa17f6361205b2ae5f26e5c7d8ca0d3f7e18972233"},
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
"makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"},
Expand Down
Loading