Current section

22 Versions

Jump to

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…