Skip to content

Commit a8f15ac

Browse files
committed
Add additional helpers for prefixed IDs
1 parent 303daef commit a8f15ac

File tree

1 file changed

+149
-3
lines changed

1 file changed

+149
-3
lines changed

lib/together/id.ex

Lines changed: 149 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,8 @@ if Code.ensure_loaded?(Ecto.ParameterizedType) do
129129

130130
@rfc_variant 2
131131

132-
@spec bingenerate() :: uuid_binary
132+
@doc false
133+
@spec bingenerate :: uuid_binary
133134
def bingenerate do
134135
time = System.system_time(:millisecond)
135136
<<rand_a::12, rand_b::62, _::6>> = :crypto.strong_rand_bytes(10)
@@ -175,6 +176,36 @@ if Code.ensure_loaded?(Ecto.ParameterizedType) do
175176
end
176177
end
177178

179+
@doc """
180+
Convert a UUID, regardless of current format, into its raw binary form
181+
182+
Same as `to_binary/1` but raises if conversion fails.
183+
184+
## Examples
185+
186+
iex> Together.ID.to_binary!(<<1, 147, 48, 97, 106, 163, 123, 39, 141, 46, 234, 126, 170, 90, 115, 70>>)
187+
<<1, 147, 48, 97, 106, 163, 123, 39, 141, 46, 234, 126, 170, 90, 115, 70>>
188+
189+
iex> Together.ID.to_binary!("01933061-6aa3-7b27-8d2e-ea7eaa5a7346")
190+
<<1, 147, 48, 97, 106, 163, 123, 39, 141, 46, 234, 126, 170, 90, 115, 70>>
191+
192+
iex> Together.ID.to_binary!("test_CHErKsMSgQVrdQxEj7nmB")
193+
<<1, 147, 48, 97, 106, 163, 123, 39, 141, 46, 234, 126, 170, 90, 115, 70>>
194+
195+
iex> Together.ID.to_binary!("bad data")
196+
** (ArgumentError) invalid UUID format
197+
198+
"""
199+
@spec to_binary!(uuid_binary) :: uuid_binary | no_return
200+
@spec to_binary!(uuid_string) :: uuid_binary | no_return
201+
@spec to_binary!(uuid_slug) :: uuid_binary | no_return
202+
def to_binary!(uuid) do
203+
case to_binary(uuid) do
204+
{:ok, uuid_binary} -> uuid_binary
205+
:error -> raise ArgumentError, "invalid UUID format"
206+
end
207+
end
208+
178209
@doc """
179210
Convert a UUID, regardless of current format, to a standard string form
180211
@@ -209,6 +240,36 @@ if Code.ensure_loaded?(Ecto.ParameterizedType) do
209240
end
210241
end
211242

243+
@doc """
244+
Convert a UUID, regardless of current format, to a standard string form
245+
246+
Same as `to_uuid/1` but raises if conversion fails.
247+
248+
## Examples
249+
250+
iex> Together.ID.to_uuid!(<<1, 147, 48, 97, 106, 163, 123, 39, 141, 46, 234, 126, 170, 90, 115, 70>>)
251+
"01933061-6aa3-7b27-8d2e-ea7eaa5a7346"
252+
253+
iex> Together.ID.to_uuid!("01933061-6aa3-7b27-8d2e-ea7eaa5a7346")
254+
"01933061-6aa3-7b27-8d2e-ea7eaa5a7346"
255+
256+
iex> Together.ID.to_uuid!("test_CHErKsMSgQVrdQxEj7nmB")
257+
"01933061-6aa3-7b27-8d2e-ea7eaa5a7346"
258+
259+
iex> Together.ID.to_uuid!("bad data")
260+
** (ArgumentError) invalid UUID format
261+
262+
"""
263+
@spec to_uuid!(uuid_binary) :: uuid_string | no_return
264+
@spec to_uuid!(uuid_string) :: uuid_string | no_return
265+
@spec to_uuid!(uuid_slug) :: uuid_string | no_return
266+
def to_uuid!(uuid) do
267+
case to_uuid(uuid) do
268+
{:ok, uuid_string} -> uuid_string
269+
:error -> raise ArgumentError, "invalid UUID format"
270+
end
271+
end
272+
212273
@doc """
213274
Convert a UUID, regardless of current format, to a base-58 encoded slug without prefix
214275
@@ -244,6 +305,36 @@ if Code.ensure_loaded?(Ecto.ParameterizedType) do
244305
end
245306
end
246307

308+
@doc """
309+
Convert a UUID, regardless of current format, to a base-58 encoded slug without prefix
310+
311+
Same as `to_slug/1` but raises if conversion fails.
312+
313+
## Examples
314+
315+
iex> Together.ID.to_slug!(<<1, 147, 48, 97, 106, 163, 123, 39, 141, 46, 234, 126, 170, 90, 115, 70>>)
316+
"CHErKsMSgQVrdQxEj7nmB"
317+
318+
iex> Together.ID.to_slug!("01933061-6aa3-7b27-8d2e-ea7eaa5a7346")
319+
"CHErKsMSgQVrdQxEj7nmB"
320+
321+
iex> Together.ID.to_slug!("test_CHErKsMSgQVrdQxEj7nmB")
322+
"test_CHErKsMSgQVrdQxEj7nmB"
323+
324+
iex> Together.ID.to_slug!("bad data")
325+
** (ArgumentError) invalid UUID format
326+
327+
"""
328+
@spec to_slug!(uuid_binary) :: uuid_slug | no_return
329+
@spec to_slug!(uuid_string) :: uuid_slug | no_return
330+
@spec to_slug!(uuid_slug) :: uuid_slug | no_return
331+
def to_slug!(uuid) do
332+
case to_slug(uuid) do
333+
{:ok, uuid_slug} -> uuid_slug
334+
:error -> raise ArgumentError, "invalid UUID format"
335+
end
336+
end
337+
247338
@doc """
248339
Convert a UUID, regardless of current format, to a base-58 encoded slug with prefix
249340
@@ -279,6 +370,36 @@ if Code.ensure_loaded?(Ecto.ParameterizedType) do
279370
end
280371
end
281372

373+
@doc """
374+
Convert a UUID, regardless of current format, to a base-58 encoded slug with prefix
375+
376+
Same as `to_slug/2` but raises if conversion fails.
377+
378+
## Examples
379+
380+
iex> Together.ID.to_slug!(<<1, 147, 48, 97, 106, 163, 123, 39, 141, 46, 234, 126, 170, 90, 115, 70>>, "test")
381+
"test_CHErKsMSgQVrdQxEj7nmB"
382+
383+
iex> Together.ID.to_slug!("01933061-6aa3-7b27-8d2e-ea7eaa5a7346", "test")
384+
"test_CHErKsMSgQVrdQxEj7nmB"
385+
386+
iex> Together.ID.to_slug!("test_CHErKsMSgQVrdQxEj7nmB", "test")
387+
"test_CHErKsMSgQVrdQxEj7nmB"
388+
389+
iex> Together.ID.to_slug!("bad data", "test")
390+
** (ArgumentError) invalid UUID format
391+
392+
"""
393+
@spec to_slug!(uuid_binary, String.t()) :: uuid_slug | no_return
394+
@spec to_slug!(uuid_string, String.t()) :: uuid_slug | no_return
395+
@spec to_slug!(uuid_slug, String.t()) :: uuid_slug | no_return
396+
def to_slug!(uuid, prefix) do
397+
case to_slug(uuid, prefix) do
398+
{:ok, uuid_slug} -> uuid_slug
399+
:error -> raise ArgumentError, "invalid UUID format"
400+
end
401+
end
402+
282403
@spec binary_to_slug(uuid_binary, String.t()) :: {:ok, uuid_slug} | :error
283404
defp binary_to_slug(<<_::128>> = uuid_binary, ""), do: {:ok, encode_base58(uuid_binary)}
284405

@@ -304,7 +425,7 @@ if Code.ensure_loaded?(Ecto.ParameterizedType) do
304425
end
305426

306427
@spec slug_to_uuid(uuid_slug) :: {:ok, String.t(), uuid_string} | :error
307-
def slug_to_uuid(uuid_slug) do
428+
defp slug_to_uuid(uuid_slug) do
308429
with {:ok, prefix, uuid_binary} <- slug_to_binary(uuid_slug),
309430
{:ok, uuid_string} <- binary_to_uuid(uuid_binary) do
310431
{:ok, prefix, uuid_string}
@@ -384,6 +505,31 @@ if Code.ensure_loaded?(Ecto.ParameterizedType) do
384505
# Helpers
385506
#
386507

508+
@doc """
509+
Extract the timestamp embedded in the UUIDv7
510+
511+
## Examples
512+
513+
iex> Together.ID.extract_timestamp("01933061-6aa3-7b27-8d2e-ea7eaa5a7346")
514+
{:ok, ~U[2024-11-15 15:11:50.947Z]}
515+
516+
iex> Together.ID.extract_timestamp("test_CQGFY5NK2muwxrpHsNJ28")
517+
{:ok, ~U[2025-06-18 19:38:02.580Z]}
518+
519+
iex> Together.ID.extract_timestamp("bad data")
520+
:error
521+
522+
"""
523+
@spec extract_timestamp(uuid_binary) :: {:ok, DateTime.t()} | :error
524+
def extract_timestamp(uuid) do
525+
with {:ok, <<timestamp::big-unsigned-integer-size(48), _rest::binary>>} <- to_binary(uuid),
526+
{:ok, datetime} <- DateTime.from_unix(timestamp, :millisecond) do
527+
{:ok, datetime}
528+
else
529+
_ -> :error
530+
end
531+
end
532+
387533
@doc """
388534
Get minimum and maximum UUID values for a given creation date
389535
@@ -394,7 +540,7 @@ if Code.ensure_loaded?(Ecto.ParameterizedType) do
394540
The lower bound is inclusive, meaning it is possible (though extremely unlikely) to generate
395541
an ID with that value. The upper bound is exclusive, as it belongs to the next calendar date.
396542
397-
## Examples
543+
## Example
398544
399545
iex> Together.ID.min_max(~D[2025-03-01])
400546
{"01954f00-b000-7000-8000-000000000000", "01955427-0c00-7000-8000-000000000000"}

0 commit comments

Comments
 (0)