diff --git a/spec/std/uuid_spec.cr b/spec/std/uuid_spec.cr index 12e497829b16..48cc3351a3c6 100644 --- a/spec/std/uuid_spec.cr +++ b/spec/std/uuid_spec.cr @@ -269,4 +269,21 @@ describe "UUID" do UUID.v5_x500(data).v5?.should eq(true) end end + + describe "v7" do + it "generates a v7 UUID" do + uuid = UUID.v7 + uuid.v7?.should eq true + uuid.variant.rfc9562?.should eq true + end + + pending_wasm32 "generates UUIDs that are sortable with 1ms precision" do + uuids = Array.new(10) do + sleep 1.millisecond + UUID.v7 + end + + uuids.should eq uuids.sort + end + end end diff --git a/src/uuid.cr b/src/uuid.cr index c7aaee0a605c..b1a043785472 100644 --- a/src/uuid.cr +++ b/src/uuid.cr @@ -23,6 +23,8 @@ struct UUID NCS # Reserved for RFC 4122 Specification (default). RFC4122 + # Reserved for RFC 9562 Specification (default for v7). + RFC9562 = RFC4122 # Reserved by Microsoft for backward compatibility. Microsoft # Reserved for future expansion. @@ -43,6 +45,8 @@ struct UUID V4 = 4 # SHA1 hash and namespace. V5 = 5 + # Prefixed with a UNIX timestamp with millisecond precision, filled in with randomness. + V7 = 7 end # A Domain represents a Version 2 domain (DCE security). @@ -80,7 +84,7 @@ struct UUID # do nothing when Variant::NCS @bytes[8] = (@bytes[8] & 0x7f) - when Variant::RFC4122 + when Variant::RFC4122, Variant::RFC9562 @bytes[8] = (@bytes[8] & 0x3f) | 0x80 when Variant::Microsoft @bytes[8] = (@bytes[8] & 0x1f) | 0xc0 @@ -321,6 +325,30 @@ struct UUID end {% end %} + # Generates an RFC9562-compatible v7 UUID, allowing the values to be sorted + # chronologically (with 1ms precision) by their raw or hexstring + # representation. + def self.v7(random r : Random = Random::Secure) + buffer = uninitialized UInt8[18] + value = buffer.to_slice + + # Generate the first 48 bits of the UUID with the current timestamp. We + # allocated enough room for a 64-bit timestamp to accommodate the + # NetworkEndian.encode call here, but we only need 48 bits of it so we chop + # off the first 2 bytes. + IO::ByteFormat::NetworkEndian.encode Time.utc.to_unix_ms, value + value = value[2..] + + # Fill in the rest with random bytes + r.random_bytes(value[6..]) + + # Set the version and variant + value[6] = (value[6] & 0x3F) | 0x70 + value[8] = (value[8] & 0x0F) | 0x80 + + new(value, variant: :rfc9562, version: :v7) + end + # Generates an empty UUID. # # ``` @@ -375,6 +403,7 @@ struct UUID when 3 then Version::V3 when 4 then Version::V4 when 5 then Version::V5 + when 7 then Version::V7 else Version::Unknown end end @@ -442,7 +471,7 @@ struct UUID class Error < Exception end - {% for v in %w(1 2 3 4 5) %} + {% for v in %w(1 2 3 4 5 7) %} # Returns `true` if UUID is a V{{ v.id }}, `false` otherwise. def v{{ v.id }}? variant == Variant::RFC4122 && version == Version::V{{ v.id }}