From 467861eb391c01a1959af986d2af6bbde47af218 Mon Sep 17 00:00:00 2001 From: JohnnyT Date: Fri, 22 Aug 2025 04:49:46 -0600 Subject: [PATCH 1/4] Adds dialyxer --- mix.exs | 1 + mix.lock | 2 ++ 2 files changed, 3 insertions(+) diff --git a/mix.exs b/mix.exs index 8b49ed8..c531f59 100644 --- a/mix.exs +++ b/mix.exs @@ -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}, diff --git a/mix.lock b/mix.lock index b5da2ab..719c2e9 100644 --- a/mix.lock +++ b/mix.lock @@ -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"}, From 71937f0c241b2a0d2ec2a78409741dcd5db47ede Mon Sep 17 00:00:00 2001 From: JohnnyT Date: Fri, 22 Aug 2025 04:50:20 -0600 Subject: [PATCH 2/4] Adds codec for dialyzer and parameterized type --- lib/uxid.ex | 70 ++++++++------------------------------ lib/uxid/codec.ex | 34 ++++++++++++++++++ lib/uxid/encoder.ex | 52 ++++++++++++++++------------ test/uxid/encoder_test.exs | 32 ++++++++--------- 4 files changed, 95 insertions(+), 93 deletions(-) create mode 100644 lib/uxid/codec.ex diff --git a/lib/uxid.ex b/lib/uxid.ex index 620b05c..69a7421 100644 --- a/lib/uxid.ex +++ b/lib/uxid.ex @@ -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()} | @@ -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, diff --git a/lib/uxid/codec.ex b/lib/uxid/codec.ex new file mode 100644 index 0000000..050ab5b --- /dev/null +++ b/lib/uxid/codec.ex @@ -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 diff --git a/lib/uxid/encoder.ex b/lib/uxid/encoder.ex index ffcb35c..bba1c59 100644 --- a/lib/uxid/encoder.ex +++ b/lib/uxid/encoder.ex @@ -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() @@ -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() @@ -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(<>, case) %{uxid | time_encoded: string} end @@ -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() @@ -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 diff --git a/test/uxid/encoder_test.exs b/test/uxid/encoder_test.exs index c172820..37ed9f9 100644 --- a/test/uxid/encoder_test.exs +++ b/test/uxid/encoder_test.exs @@ -1,7 +1,7 @@ defmodule UXID.EncoderTest do use ExUnit.Case, async: true - alias UXID.Encoder + alias UXID.{Codec, Encoder} describe "process/1 with blank UXID using upper case" do test "app config returns a lowercase 26 character string (ULID)" do @@ -10,13 +10,13 @@ defmodule UXID.EncoderTest do Application.delete_env(:uxid, :case) end - {:ok, %UXID{string: uxid}} = Encoder.process(%UXID{}) + {:ok, %Codec{string: uxid}} = Encoder.process(%Codec{}) assert String.length(uxid) == 26 refute uxid == String.downcase(uxid) end test "call config returns a lowercase 26 character string (ULID)" do - {:ok, %UXID{string: uxid}} = Encoder.process(%UXID{case: :upper}) + {:ok, %Codec{string: uxid}} = Encoder.process(%Codec{case: :upper}) assert String.length(uxid) == 26 refute uxid == String.downcase(uxid) end @@ -24,7 +24,7 @@ defmodule UXID.EncoderTest do describe "process/1 with a blank UXID" do test "returns a 26 character string (ULID)" do - {:ok, %UXID{string: uxid}} = Encoder.process(%UXID{}) + {:ok, %Codec{string: uxid}} = Encoder.process(%Codec{}) assert String.length(uxid) == 26 assert uxid == String.downcase(uxid) end @@ -32,62 +32,62 @@ defmodule UXID.EncoderTest do describe "process/1" do test "returns a 26 character string with 10 bytes of randomness" do - {:ok, %UXID{string: uxid}} = Encoder.process(%UXID{rand_size: 10}) + {:ok, %Codec{string: uxid}} = Encoder.process(%Codec{rand_size: 10}) assert String.length(uxid) == 26 end test "returns a 25 character string with 9 bytes of randomness" do - {:ok, %UXID{string: uxid}} = Encoder.process(%UXID{rand_size: 9}) + {:ok, %Codec{string: uxid}} = Encoder.process(%Codec{rand_size: 9}) assert String.length(uxid) == 25 end test "returns a 23 character string with 8 bytes of randomness" do - {:ok, %UXID{string: uxid}} = Encoder.process(%UXID{rand_size: 8}) + {:ok, %Codec{string: uxid}} = Encoder.process(%Codec{rand_size: 8}) assert String.length(uxid) == 23 end test "returns a 22 character string with 7 bytes of randomness" do - {:ok, %UXID{string: uxid}} = Encoder.process(%UXID{rand_size: 7}) + {:ok, %Codec{string: uxid}} = Encoder.process(%Codec{rand_size: 7}) assert String.length(uxid) == 22 end test "returns a 20 character string with 6 bytes of randomness" do - {:ok, %UXID{string: uxid}} = Encoder.process(%UXID{rand_size: 6}) + {:ok, %Codec{string: uxid}} = Encoder.process(%Codec{rand_size: 6}) assert String.length(uxid) == 20 end test "returns a 18 character string with 5 bytes of randomness" do - {:ok, %UXID{string: uxid}} = Encoder.process(%UXID{rand_size: 5}) + {:ok, %Codec{string: uxid}} = Encoder.process(%Codec{rand_size: 5}) assert String.length(uxid) == 18 end test "returns a 17 character string with 4 bytes of randomness" do - {:ok, %UXID{string: uxid}} = Encoder.process(%UXID{rand_size: 4}) + {:ok, %Codec{string: uxid}} = Encoder.process(%Codec{rand_size: 4}) assert String.length(uxid) == 17 end test "returns a 15 character string with 3 bytes of randomness" do - {:ok, %UXID{string: uxid}} = Encoder.process(%UXID{rand_size: 3}) + {:ok, %Codec{string: uxid}} = Encoder.process(%Codec{rand_size: 3}) assert String.length(uxid) == 15 end test "returns a 14 character string with 2 bytes of randomness" do - {:ok, %UXID{string: uxid}} = Encoder.process(%UXID{rand_size: 2}) + {:ok, %Codec{string: uxid}} = Encoder.process(%Codec{rand_size: 2}) assert String.length(uxid) == 14 end test "returns a 12 character string with 1 bytes of randomness" do - {:ok, %UXID{string: uxid}} = Encoder.process(%UXID{rand_size: 1}) + {:ok, %Codec{string: uxid}} = Encoder.process(%Codec{rand_size: 1}) assert String.length(uxid) == 12 end test "returns a 10 character string with 0 bytes of randomness" do - {:ok, %UXID{string: uxid}} = Encoder.process(%UXID{rand_size: 0}) + {:ok, %Codec{string: uxid}} = Encoder.process(%Codec{rand_size: 0}) assert String.length(uxid) == 10 end test "returns a string delimited by a hyphen" do - {:ok, %UXID{string: uxid}} = Encoder.process(%UXID{prefix: "X", delimiter: "-"}) + {:ok, %Codec{string: uxid}} = Encoder.process(%Codec{prefix: "X", delimiter: "-"}) assert "X-" <> _uxid = uxid end end From 19c458519f806ba2d0893a8596a2d9caf7794561 Mon Sep 17 00:00:00 2001 From: JohnnyT Date: Fri, 22 Aug 2025 04:55:32 -0600 Subject: [PATCH 3/4] Removes extra link from docs --- mix.exs | 1 - 1 file changed, 1 deletion(-) diff --git a/mix.exs b/mix.exs index c531f59..927b20c 100644 --- a/mix.exs +++ b/mix.exs @@ -71,7 +71,6 @@ defmodule UXID.MixProject do [ licenses: ["MIT"], links: %{ - "UXID Project" => "https://github.com/riddler/uxid", "GitHub" => "https://github.com/riddler/uxid-ex" } ] From fef2d329440b97e213030fe9fa1c7705671c4e48 Mon Sep 17 00:00:00 2001 From: JohnnyT Date: Fri, 22 Aug 2025 05:03:17 -0600 Subject: [PATCH 4/4] Updates CHANGELOG --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e208536..d301092 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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