Current section
22 Versions
Jump to
Current section
22 Versions
Compare versions
8
files changed
+311
additions
-15
deletions
| @@ -1,5 +1,16 @@ | |
| 1 1 | ### Upcoming |
| 2 2 | |
| 3 | + ### 2.3.0 / 2026-01-16 |
| 4 | + |
| 5 | + * Adds min_size config option to enforce minimum UXID sizes (useful for test environments) |
| 6 | + * Adds compact_time feature for improved collision resistance in small UXIDs: |
| 7 | + - Global policy via `compact_small_times` config automatically compacts :xs/:xsmall/:s/:small sizes |
| 8 | + - Per-call override via `compact_time: true/false` option works for any size |
| 9 | + - Uses 40-bit timestamps (8 chars) instead of 48-bit (10 chars), freeing 8 bits for randomness |
| 10 | + - Example: :small gains 50% more randomness (24 bits vs 16 bits) |
| 11 | + - Decoder automatically detects compact format from length and reconstructs full timestamp |
| 12 | + - K-sortability maintained until ~September 2039 |
| 13 | + |
| 3 14 | ### 2.2.0 / 2025-09-12 |
| 4 15 | |
| 5 16 | * Adds UXID.Decoder module with full pipeline processing and uppercase/lowercase support |
| @@ -33,6 +33,9 @@ UXID.generate! prefix: "cus" # "cus_01emdgjf0dqxqj8fm78xe97y3h" | |
| 33 33 | # T-Shirt sizes can be used (xs, s, m, l, xl) or (xsmall, small, medium, large, xlarge) |
| 34 34 | UXID.generate! prefix: "cus", size: :small # "cus_01eqrh884aqyy1" |
| 35 35 | |
| 36 | + # Compact time mode trades timestamp precision for more randomness (good for collision resistance) |
| 37 | + UXID.generate! prefix: "sess", size: :small, compact_time: true # "sess_kf3ng7s1mf41b" |
| 38 | + |
| 36 39 | # Uppercase can be used to match previous UXID versions |
| 37 40 | UXID.generate! case: :upper # "01EMDGJF0DQXQJ8FM78XE97Y3H" |
| 38 41 | ``` |
| @@ -47,12 +50,100 @@ defmodule YourApp.User do | |
| 47 50 | |
| 48 51 | @primary_key {:id, UXID, autogenerate: true, prefix: "usr", size: :medium} |
| 49 52 | schema "users" do |
| 50 | - field :api_key, UXID, autogenerate: true, prefix: "apikey", size: :small |
| 53 | + field :api_key, UXID, autogenerate: true, prefix: "apikey", size: :small, compact_time: true |
| 51 54 | field :api_secret, UXID, autogenerate: true, prefix: "apisecret", size: :xlarge |
| 52 55 | end |
| 53 56 | end |
| 54 57 | ``` |
| 55 58 | |
| 59 | + ### Configuration |
| 60 | + |
| 61 | + #### Case |
| 62 | + |
| 63 | + The `:case` config option controls the default case for generated UXIDs. By default, UXIDs are lowercase (`:lower`), but you can configure uppercase (`:upper`) globally or per-call. |
| 64 | + |
| 65 | + ```elixir |
| 66 | + # config/config.exs |
| 67 | + config :uxid, case: :upper |
| 68 | + |
| 69 | + # All generated UXIDs will be uppercase by default |
| 70 | + UXID.generate!() |
| 71 | + # => "01EMDGJF0DQXQJ8FM78XE97Y3H" |
| 72 | + |
| 73 | + # Override per-call if needed |
| 74 | + UXID.generate!(case: :lower) |
| 75 | + # => "01emdgjf0dqxqj8fm78xe97y3h" |
| 76 | + ``` |
| 77 | + |
| 78 | + #### Minimum Size |
| 79 | + |
| 80 | + The `:min_size` config option enforces a minimum UXID size regardless of what size is requested. This is useful in test environments where many IDs are generated rapidly, as smaller sizes have limited randomness that can cause duplicate key violations. |
| 81 | + |
| 82 | + ```elixir |
| 83 | + # config/test.exs |
| 84 | + config :uxid, min_size: :medium |
| 85 | + |
| 86 | + # In application code - requests :small but gets :medium in test env |
| 87 | + UXID.generate!(prefix: "usr", size: :small) |
| 88 | + # => Returns 18 character UXID instead of 14 |
| 89 | + ``` |
| 90 | + |
| 91 | + When configured, any requested size smaller than `:min_size` will be automatically upgraded. Larger sizes are not affected. |
| 92 | + |
| 93 | + #### Compact Time |
| 94 | + |
| 95 | + The `:compact_small_times` config option and per-call `compact_time` option provide improved collision resistance for small UXIDs by using shorter timestamps and more randomness. |
| 96 | + |
| 97 | + **Global Policy:** |
| 98 | + |
| 99 | + ```elixir |
| 100 | + # config/test.exs |
| 101 | + config :uxid, compact_small_times: true |
| 102 | + |
| 103 | + # Automatically compacts :xs/:xsmall and :s/:small sizes |
| 104 | + UXID.generate!(size: :small) |
| 105 | + # => 13 chars (8 time + 5 rand = 24 bits random vs 16 bits standard) |
| 106 | + ``` |
| 107 | + |
| 108 | + **Per-Call Override:** |
| 109 | + |
| 110 | + ```elixir |
| 111 | + # Override for any size - works even when global policy is off |
| 112 | + UXID.generate!(size: :large, compact_time: true) |
| 113 | + # => 21 chars with extra randomness |
| 114 | + |
| 115 | + # Opt out of global policy for specific calls |
| 116 | + UXID.generate!(size: :small, compact_time: false) |
| 117 | + # => 14 chars with standard randomness |
| 118 | + ``` |
| 119 | + |
| 120 | + **In Ecto Schemas:** |
| 121 | + |
| 122 | + ```elixir |
| 123 | + defmodule YourApp.Session do |
| 124 | + use Ecto.Schema |
| 125 | + |
| 126 | + @primary_key {:id, UXID, autogenerate: true, prefix: "sess", size: :small, compact_time: true} |
| 127 | + schema "sessions" do |
| 128 | + # This session ID will always use compact mode for better collision resistance |
| 129 | + end |
| 130 | + end |
| 131 | + ``` |
| 132 | + |
| 133 | + **How it works:** |
| 134 | + |
| 135 | + - Reduces timestamp from 48 bits (10 chars) to 40 bits (8 chars) |
| 136 | + - Frees 8 bits for additional randomness (e.g., :small gets 24 bits vs 16 bits) |
| 137 | + - Perfect 5-bit Crockford Base32 alignment |
| 138 | + - K-sortability maintained until ~September 2039 |
| 139 | + - Decoder automatically detects compact format and reconstructs full timestamp using epoch inference |
| 140 | + |
| 141 | + **When to use:** |
| 142 | + |
| 143 | + - Test environments with rapid ID generation |
| 144 | + - Resources with small cardinality that need better collision resistance |
| 145 | + - Any scenario where you want to maximize randomness within a given length constraint |
| 146 | + |
| 56 147 | ## Installation |
| 57 148 | |
| 58 149 | The package can be installed by adding `uxid` to your list of dependencies in `mix.exs`: |
| @@ -60,7 +151,7 @@ The package can be installed by adding `uxid` to your list of dependencies in `m | |
| 60 151 | ```elixir |
| 61 152 | def deps do |
| 62 153 | [ |
| 63 | - {:uxid, "~> 2.2"} |
| 154 | + {:uxid, "~> 2.3"} |
| 64 155 | ] |
| 65 156 | end |
| 66 157 | ``` |
| @@ -1,6 +1,6 @@ | |
| 1 1 | {<<"links">>,[{<<"GitHub">>,<<"https://github.com/riddler/uxid-ex">>}]}. |
| 2 2 | {<<"name">>,<<"uxid">>}. |
| 3 | - {<<"version">>,<<"2.2.0">>}. |
| 3 | + {<<"version">>,<<"2.3.0">>}. |
| 4 4 | {<<"description">>, |
| 5 5 | <<"Generates UX focused IDs like: usr_01epey2p06tr1rtv07xa82zgjj (K-sortable with prefix - like Stripe)">>}. |
| 6 6 | {<<"elixir">>,<<"~> 1.8">>}. |
| @@ -63,10 +63,12 @@ defmodule UXID do | |
| 63 63 | rand_size = Keyword.get(opts, :rand_size) |
| 64 64 | size = Keyword.get(opts, :size) |
| 65 65 | delimiter = Keyword.get(opts, :delimiter) |
| 66 | + compact_time = Keyword.get(opts, :compact_time) |
| 66 67 | timestamp = Keyword.get(opts, :time, System.system_time(:millisecond)) |
| 67 68 | |
| 68 69 | %Codec{ |
| 69 70 | case: case, |
| 71 | + compact_time: compact_time, |
| 70 72 | prefix: prefix, |
| 71 73 | rand_size: rand_size, |
| 72 74 | size: size, |
| @@ -78,6 +80,22 @@ defmodule UXID do | |
| 78 80 | |
| 79 81 | def encode_case(), do: Application.get_env(:uxid, :case, :lower) |
| 80 82 | |
| 83 | + @doc """ |
| 84 | + Returns the minimum size configuration. |
| 85 | + When set, any requested size smaller than this will be upgraded. |
| 86 | + """ |
| 87 | + def min_size(), do: Application.get_env(:uxid, :min_size, nil) |
| 88 | + |
| 89 | + @doc """ |
| 90 | + Returns whether compact time encoding is enabled for small sizes. |
| 91 | + When true, :xs/:xsmall and :s/:small use 40 bits for timestamp instead of 48, |
| 92 | + adding 8 bits to randomness. This is a global policy that can be overridden |
| 93 | + per-call with the `compact_time` option. |
| 94 | + This provides perfect 5-bit Crockford Base32 alignment (8 chars vs 10 chars). |
| 95 | + K-sortability is maintained until ~September 2039. |
| 96 | + """ |
| 97 | + def compact_small_times(), do: Application.get_env(:uxid, :compact_small_times, false) |
| 98 | + |
| 81 99 | @spec decode(String.t()) :: {:ok, %Codec{}} |
| 82 100 | @doc """ |
| 83 101 | Decodes a UXID string and returns a Codec struct with extracted components. |
| @@ -102,13 +120,15 @@ defmodule UXID do | |
| 102 120 | size = Map.get(opts, :size) |
| 103 121 | rand_size = Map.get(opts, :rand_size) |
| 104 122 | delimiter = Map.get(opts, :delimiter) |
| 123 | + compact_time = Map.get(opts, :compact_time) |
| 105 124 | |
| 106 125 | __MODULE__.generate!( |
| 107 126 | case: case, |
| 108 127 | prefix: prefix, |
| 109 128 | size: size, |
| 110 129 | rand_size: rand_size, |
| 111 | - delimiter: delimiter |
| 130 | + delimiter: delimiter, |
| 131 | + compact_time: compact_time |
| 112 132 | ) |
| 113 133 | end |
| @@ -5,6 +5,7 @@ defmodule UXID.Codec do | |
| 5 5 | |
| 6 6 | defstruct [ |
| 7 7 | :case, |
| 8 | + :compact_time, |
| 8 9 | :delimiter, |
| 9 10 | :encoded, |
| 10 11 | :prefix, |
| @@ -20,6 +21,7 @@ defmodule UXID.Codec do | |
| 20 21 | @typedoc "A UXID struct during encoding" |
| 21 22 | @type t() :: %__MODULE__{ |
| 22 23 | case: atom() | nil, |
| 24 | + compact_time: boolean() | nil, |
| 23 25 | encoded: String.t() | nil, |
| 24 26 | prefix: String.t() | nil, |
| 25 27 | delimiter: String.t() | nil, |
Loading more files…