diff --git a/README.md b/README.md index e18a704f..b908e274 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,9 @@ [![Build Status](https://github.com/kachick/ruby-ulid/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/kachick/ruby-ulid/actions/workflows/ci.yml?query=branch%3Amain) [![Gem Version](https://badge.fury.io/rb/ruby-ulid.svg)](http://badge.fury.io/rb/ruby-ulid) +This gem is in maintenance mode, I have no plan to add new features.\ +The reason is UUID v7 has been accepted in [IETF](https://www.rfc-editor.org/rfc/rfc9562.html) and [ruby's securerandom](https://github.com/ruby/securerandom/pull/19). See [UUID section](#uuid) for detail. + ## Overview [ulid/spec](https://github.com/ulid/spec) defines some useful features.\ @@ -378,44 +381,49 @@ ULID.parse_variant_format('01G70Y0Y7G-ZLXWDIREXERGSDoD') #=> ULID(2022-07-03 02: #### UUID -Both ULID and UUID are 128-bit IDs. But with different specs. Especially UUID has some versions probably UUIDv4. +Both ULID and UUID are 128-bit IDs. But with different specs. Especially, UUID has some versions, for example, UUIDv4 and UUIDv7. -All UUIDv4s can be converted to ULID, but this will not have the correct "timestamp".\ -Most ULIDs cannot be converted to UUIDv4 while maintaining reversibility, because UUIDv4 requires version and variants in the fields. +All UUIDs can be converted to ULID, but only [new versions](https://datatracker.ietf.org/doc/rfc9562/) have a correct "timestamp".\ +Most ULIDs cannot be converted to UUID while maintaining reversibility, because UUID requires version and variants in the fields. See also [ulid/spec#64](https://github.com/ulid/spec/issues/64) for further detail. -For now, this gem provides 4 methods for UUIDs. +For now, this gem provides some methods for UUIDs. - Reversibility is preferred: `ULID.from_uuidish`, `ULID.to_uuidish` -- Prefer UUIDv4 specification: `ULID.from_uuidv4`, `ULID.to_uuidv4` +- Prefer variants specification: `ULID.from_uuid_v4`, `ULID.from_uuid_v7`, `ULID.to_uuid_v4`, `ULID.to_uuid_v7` ```ruby -# All UUIDv4 IDs can be reversible even if converted to ULID -uuid = SecureRandom.uuid -ULID.from_uuidish(uuid) == ULID.from_uuidv4(uuid) #=> true -ULID.from_uuidish(uuid).to_uuidish == ULID.from_uuidv4(uuid).to_uuidv4 #=> true +# All UUIDv4 and UUIDv7 IDs can be reversible even if converted to ULID +uuid_v4 = SecureRandom.uuid_v4 +ULID.from_uuidish(uuid_v4) == ULID.from_uuid_v4(uuid_v4) #=> true +ULID.from_uuidish(uuid_v4).to_uuidish == ULID.from_uuid_v4(uuid_v4).to_uuid_v4 #=> true + +# v4 does not have timestamp, v7 has it. + +ULID.from_uuid_v4(SecureRandom.uuid_v4).to_time +# 'f80b3f53-043a-4298-a674-cd83a7fd5d22' => 10612-05-19 16:58:53.882 UTC -# But most ULIDs cannot be converted to UUIDv4 +ULID.from_uuid_v7(SecureRandom.uuid_v7).to_time +# '01946f9e-bf58-7be3-8fd4-4606606b05aa' => 2025-01-16 14:57:42.232 UTC +# ULID is officially defined milliseconds precision for the spec. So omit the nanoseconds precisions even if the UUID v7 ID was generated with extra_timestamp_bits >= 1. + +# However most ULIDs cannot be converted to versioned UUID ulid = ULID.parse('01F4A5Y1YAQCYAYCTC7GRMJ9AA') -ulid.to_uuidv4 #=> ULID::IrreversibleUUIDError +ulid.to_uuid_v4 #=> ULID::IrreversibleUUIDError # So 2 ways to get substitute strings that might satisfy the use case -ulid.to_uuidv4(force: true) #=> "0179145f-07ca-4b3c-af33-4c3c3149254a" this cannot be reverse to source ULID -ulid == ULID.from_uuidv4(ulid.to_uuidv4(force: true)) #=> false +ulid.to_uuid_v4(force: true) #=> "0179145f-07ca-4b3c-af33-4c3c3149254a" this cannot be reverse to source ULID +ulid == ULID.from_uuid_v4(ulid.to_uuid_v4(force: true)) #=> false ulid.to_uuidish #=> "0179145f-07ca-bb3c-af33-4c3c3149254a" does not satisfy UUIDv4 spec ulid == ULID.from_uuidish(ulid.to_uuidish) #=> true # Seeing boundary IDs makes it easier to understand ULID.min.to_uuidish #=> "00000000-0000-0000-0000-000000000000" -ULID.min.to_uuidv4(force: true) #=> "00000000-0000-4000-8000-000000000000" +ULID.min.to_uuid_v4(force: true) #=> "00000000-0000-4000-8000-000000000000" ULID.max.to_uuidish #=> "ffffffff-ffff-ffff-ffff-ffffffffffff" -ULID.max.to_uuidv4(force: true) #=> "ffffffff-ffff-4fff-bfff-ffffffffffff" +ULID.max.to_uuid_v4(force: true) #=> "ffffffff-ffff-4fff-bfff-ffffffffffff" ``` -[UUIDv6, UUIDv7, UUIDv8](https://www.ietf.org/archive/id/draft-ietf-uuidrev-rfc4122bis-02.html) are other candidates for sortable and randomness ID.\ -Latest [ruby/securerandom merged the UUIDv7 generator](https://github.com/ruby/securerandom/pull/19).\ -See [tracker](https://bugs.ruby-lang.org/issues/19735) for further detail. - ## Migration from other gems See [wiki page for gem migration](https://github.com/kachick/ruby-ulid/wiki/Gem-migration). diff --git a/lib/ulid.rb b/lib/ulid.rb index d4494976..f850804c 100644 --- a/lib/ulid.rb +++ b/lib/ulid.rb @@ -351,10 +351,17 @@ def self.from_uuidish(uuidish) # @param [String, #to_str] uuid # @return [ULID] # @raise [ParserError] if the given format is not correct for UUIDv4 specs - def self.from_uuidv4(uuid) + def self.from_uuid_v4(uuid) from_integer(UUID.parse_v4_to_int(uuid)) end + # @param [String, #to_str] uuid + # @return [ULID] + # @raise [ParserError] if the given format is not correct for UUIDv4 specs + def self.from_uuid_v7(uuid) + from_integer(UUID.parse_v7_to_int(uuid)) + end + attr_reader(:milliseconds, :entropy, :encoded) protected(:encoded) @@ -510,8 +517,8 @@ def to_ulid self end - # Generate a UUID-like string that does not set the version and variants field. - # It means wrong in UUIDv4 spec, but reversible + # Generate a UUID-like string that does not touch the version and variants field. + # It means basically wrong in UUID specs, but reversible # # @return [String] def to_uuidish @@ -526,8 +533,8 @@ def to_uuidish # @see https://github.com/kachick/ruby-ulid/issues/76 # @param [bool] force # @return [String] - def to_uuidv4(force: false) - v4 = UUID::Fields.forced_v4_from_octets(octets) + def to_uuid_v4(force: false) + v4 = UUID::Fields.forced_version_from_octets(octets, mask: 0x4000) unless force uuidish = UUID::Fields.raw_from_octets(octets) raise(IrreversibleUUIDError) unless uuidish == v4 @@ -536,6 +543,19 @@ def to_uuidv4(force: false) v4.to_s.freeze end + # @see [#to_uuid_v4] and https://datatracker.ietf.org/doc/rfc9562/ + # @param [bool] force + # @return [String] + def to_uuid_v7(force: false) + v7 = UUID::Fields.forced_version_from_octets(octets, mask: 0x7000) + unless force + uuidish = UUID::Fields.raw_from_octets(octets) + raise(IrreversibleUUIDError) unless uuidish == v7 + end + + v7.to_s.freeze + end + # @return [ULID] def dup super.freeze diff --git a/lib/ulid/uuid.rb b/lib/ulid/uuid.rb index 7c103bd6..3a3523ca 100644 --- a/lib/ulid/uuid.rb +++ b/lib/ulid/uuid.rb @@ -13,6 +13,7 @@ module UUID BASE_PATTERN = /\A[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}\z/i # Imported from https://stackoverflow.com/a/38191104/1212807, thank you! V4_PATTERN = /\A[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}\z/i + V7_PATTERN = /\A[0-9A-F]{8}-[0-9A-F]{4}-7[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}\z/i def self.parse_any_to_int(uuidish) encoded = String.try_convert(uuidish) @@ -38,6 +39,18 @@ def self.parse_v4_to_int(uuid) parse_any_to_int(encoded) end + + def self.parse_v7_to_int(uuid) + encoded = String.try_convert(uuid) + raise(ArgumentError, 'should pass a string for UUID parser') unless encoded + + prefix_trimmed = encoded.delete_prefix('urn:uuid:') + unless V7_PATTERN.match?(prefix_trimmed) + raise(ParserError, "given `#{encoded}` does not match to `#{V7_PATTERN.inspect}`") + end + + parse_any_to_int(encoded) + end end Ractor.make_shareable(UUID) diff --git a/lib/ulid/uuid/fields.rb b/lib/ulid/uuid/fields.rb index fb46bca4..87fe73a8 100644 --- a/lib/ulid/uuid/fields.rb +++ b/lib/ulid/uuid/fields.rb @@ -19,13 +19,13 @@ def self.raw_from_octets(octets) end end - def self.forced_v4_from_octets(octets) + def self.forced_version_from_octets(octets, mask:) case octets.pack('C*').unpack('NnnnnN') in [Integer => time_low, Integer => time_mid, Integer => time_hi_and_version, Integer => clock_seq_hi_and_res, Integer => clk_seq_low, Integer => node] new( time_low:, time_mid:, - time_hi_and_version: (time_hi_and_version & 0x0fff) | 0x4000, + time_hi_and_version: (time_hi_and_version & 0x0fff) | mask, clock_seq_hi_and_res: (clock_seq_hi_and_res & 0x3fff) | 0x8000, clk_seq_low:, node: diff --git a/scripts/generate_snapshots.rb b/scripts/generate_snapshots.rb index c11d3473..3ff1e526 100644 --- a/scripts/generate_snapshots.rb +++ b/scripts/generate_snapshots.rb @@ -47,7 +47,7 @@ octets: ulid.octets, inspect: ulid.inspect, uuidish: ulid.to_uuidish, - uuidv4: ulid.to_uuidv4(force: true) + uuidv4: ulid.to_uuid_v4(force: true) } end end diff --git a/sig/ulid.rbs b/sig/ulid.rbs index 88fa2686..dab1fcdf 100644 --- a/sig/ulid.rbs +++ b/sig/ulid.rbs @@ -37,9 +37,11 @@ class ULID < Object module UUID BASE_PATTERN: Regexp V4_PATTERN: Regexp + V7_PATTERN: Regexp def self.parse_any_to_int: (String) -> Integer def self.parse_v4_to_int: (String) -> Integer + def self.parse_v7_to_int: (String) -> Integer class Fields attr_reader time_low: Integer @@ -49,7 +51,7 @@ class ULID < Object attr_reader clk_seq_low: Integer attr_reader node: Integer def self.raw_from_octets: (octets) -> Fields - def self.forced_v4_from_octets: (octets) -> Fields + def self.forced_version_from_octets: (octets, mask: Integer) -> Fields def deconstruct: -> Array[Integer] @@ -285,20 +287,23 @@ class ULID < Object # #=> ULID(2605-08-20 10:28:29.979 UTC: 0J7S2PFT4V2B9T8NJ2CRA1EG00) # ``` # - # See also [ULID.from_uuidv4] + # See also [ULID.from_uuid_v4] def self.from_uuidish: (String uuidish) -> ULID # Load a UUIDv4 string with checking version and variants. # # ```ruby - # ULID.from_uuidv4('0983d0a2-ff15-4d83-8f37-7dd945b5aa39') + # ULID.from_uuid_v4('0983d0a2-ff15-4d83-8f37-7dd945b5aa39') # #=> ULID(2301-07-10 00:28:28.821 UTC: 09GF8A5ZRN9P1RYDVXV52VBAHS) - # ULID.from_uuidv4('123e4567-e89b-12d3-a456-426614174000') + # ULID.from_uuid_v4('123e4567-e89b-12d3-a456-426614174000') # #=> ULID::ParserError # ``` # # See also [ULID.from_uuidish] - def self.from_uuidv4: (String uuid) -> ULID + def self.from_uuid_v4: (String uuid) -> ULID + + # See also [ULID.from_uuid_v4] + def self.from_uuid_v7: (String uuid) -> ULID # Load integer as ULID # @@ -646,7 +651,7 @@ class ULID < Object # ULID.from_uuidish(ulid.to_uuidish) #=> ULID(2023-03-07 11:48:07.469 UTC: 01GTXYCWNDKRYH14DBZ77TRSD7) # ``` # - # See also [ULID.from_uuidish], [ULID#to_uuidv4], [ulid/spec#64](https://github.com/ulid/spec/issues/64) + # See also [ULID.from_uuidish], [ULID#to_uuid_v4], [ulid/spec#64](https://github.com/ulid/spec/issues/64) def to_uuidish: -> String # Generate a UUIDv4-like string that sets the version and variants field.\ @@ -655,18 +660,21 @@ class ULID < Object # # ```ruby # uuid = '0983d0a2-ff15-4d83-8f37-7dd945b5aa39' - # ulid = ULID.from_uuidv4(uuid) - # ulid.to_uuidv4 #=> 0983d0a2-ff15-4d83-8f37-7dd945b5aa39 + # ulid = ULID.from_uuid_v4(uuid) + # ulid.to_uuid_v4 #=> 0983d0a2-ff15-4d83-8f37-7dd945b5aa39 # ``` # # ```ruby # ulid = ULID.from_uuidish('0186bbe6-72ad-9e3d-1091-abf9cfac65a7') - # ulid.to_uuidv4 #=> ULID::IrreversibleUUIDError - # ulid.to_uuidv4(force: true) #=> '0186bbe6-72ad-4e3d-9091-abf9cfac65a7' + # ulid.to_uuid_v4 #=> ULID::IrreversibleUUIDError + # ulid.to_uuid_v4(force: true) #=> '0186bbe6-72ad-4e3d-9091-abf9cfac65a7' # ``` # - # See also [ULID.from_uuidv4], [ULID#to_uuidish], [ulid/spec#64](https://github.com/ulid/spec/issues/64) - def to_uuidv4: (?force: boolish) -> String + # See also [ULID.from_uuid_v4], [ULID#to_uuidish], [ulid/spec#64](https://github.com/ulid/spec/issues/64) + def to_uuid_v4: (?force: boolish) -> String + + # See also [ULID.from_uuid_v7], [ULID#to_uuidish] + def to_uuid_v7: (?force: boolish) -> String # Returns same ID with different Ruby object. def dup: -> ULID diff --git a/test/core/test_ulid_class.rb b/test/core/test_ulid_class.rb index d382816f..2ee1019d 100644 --- a/test/core/test_ulid_class.rb +++ b/test/core/test_ulid_class.rb @@ -59,7 +59,8 @@ def test_exposed_methods valid_as_variant_format? parse_variant_format from_uuidish - from_uuidv4 + from_uuid_v4 + from_uuid_v7 ].sort, exposed_methods.sort ) diff --git a/test/core/test_ulid_instance.rb b/test/core/test_ulid_instance.rb index 797a08bf..b68e79c8 100644 --- a/test/core/test_ulid_instance.rb +++ b/test/core/test_ulid_instance.rb @@ -31,7 +31,8 @@ class TestULIDInstance < Test::Unit::TestCase dup clone to_uuidish - to_uuidv4 + to_uuid_v4 + to_uuid_v7 ].freeze ULID_RETURNING_METHODS = %i[ diff --git a/test/core/test_uuid.rb b/test/core/test_uuid.rb index 3ecdf6db..b46a20d1 100644 --- a/test/core/test_uuid.rb +++ b/test/core/test_uuid.rb @@ -20,7 +20,7 @@ def test_ensure_testing_environment def test_generators_return_own_instance_and_does_not_raise_in_some_basic_comparison ulid = ULID.sample [ - Subclass.from_uuidv4(SecureRandom.uuid) + Subclass.from_uuid_v4(SecureRandom.uuid_v4) ].each do |instance| assert_not_instance_of(ULID, instance) assert_instance_of(Subclass, instance) @@ -38,13 +38,19 @@ def test_from_uuidish assert_equal(ULID.parse('09GF8A5ZRN9P1RYDVXV52VBAHS'), ULID.from_uuidish('0983d0a2-ff15-4d83-8f37-7dd945b5aa39')) assert_equal(ULID.parse('09GF8A5ZRN9P1RYDVXV52VBAHS'), ULID.from_uuidish('urn:uuid:0983d0a2-ff15-4d83-8f37-7dd945b5aa39')) - # UUIDv3, v5 + # UUIDv3, v5 - Not officially supported in this gem, but I expect it also works assert_equal(ULID.parse('3BMYW117DD278R1D00R17X8C68'), ULID.from_uuidish('6ba7b810-9dad-11d1-80b4-00c04fd430c8')) + # UUID v7 + assert_equal(ULID.parse('01JHQSXFTRFFHRZN260SG6P1DA'), ULID.from_uuidish('01946f9e-bf58-7be3-8fd4-4606606b05aa')) + # Rough tests ulids = 1000.times.map do - ULID.from_uuidish(SecureRandom.uuid) - end + [ + ULID.from_uuidish(SecureRandom.uuid_v4), + ULID.from_uuidish(SecureRandom.uuid_v7) + ] + end.flatten assert_true(ulids.uniq == ulids) # Ensure some invalid patterns (I'd like to add more examples) @@ -71,37 +77,74 @@ def test_from_uuidish end end - def test_from_uuidv4 + def test_from_uuid_v4 # The example value was taken from https://github.com/ahawker/ulid/tree/96bdb1daad7ce96f6db8c91ac0410b66d2e1c4c1#usage - assert_equal(ULID.parse('09GF8A5ZRN9P1RYDVXV52VBAHS'), ULID.from_uuidv4('0983d0a2-ff15-4d83-8f37-7dd945b5aa39')) - assert_equal(ULID.parse('09GF8A5ZRN9P1RYDVXV52VBAHS'), ULID.from_uuidv4('urn:uuid:0983d0a2-ff15-4d83-8f37-7dd945b5aa39')) + assert_equal(ULID.parse('09GF8A5ZRN9P1RYDVXV52VBAHS'), ULID.from_uuid_v4('0983d0a2-ff15-4d83-8f37-7dd945b5aa39')) + assert_equal(ULID.parse('09GF8A5ZRN9P1RYDVXV52VBAHS'), ULID.from_uuid_v4('urn:uuid:0983d0a2-ff15-4d83-8f37-7dd945b5aa39')) # Rough tests ulids = 1000.times.map do - ULID.from_uuidv4(SecureRandom.uuid) + ULID.from_uuid_v4(SecureRandom.uuid_v4) end assert_true(ulids.uniq == ulids) - # Ensure some invalid patterns (I'd like to add more examples) + # Ensure some invalid patterns [ '6ba7b810-9dad-11d1-80b4-00c04fd430c8', # UUIDv3, v5 + '01946f9e-bf58-7be3-8fd4-4606606b05aa', # UUIDv7 '0983d0a2-ff15-4d83-8f37-7dd945b5aa3', # Shortage '0983d0a2-ff15-4d83-8f37-7dd945b5aa390', # Excess "0983d0a2-ff15-4d83-8f37-7dd945b5aa39\n", # Line end '0983d0a2-ff15-4d83-8f37--7dd945b5aa39' # `-` excess ].each do |invalid_uuidv4| assert_raises(ULID::ParserError) do - ULID.from_uuidv4(invalid_uuidv4) + ULID.from_uuid_v4(invalid_uuidv4) end end assert_raises(ArgumentError) do - ULID.from_uuidv4 + ULID.from_uuid_v4 end [nil, 42, :'0983d0a2-ff15-4d83-8f37-7dd945b5aa39', BasicObject.new, Object.new, ulids.sample].each do |evil| err = assert_raises(ArgumentError) do - ULID.from_uuidv4(evil) + ULID.from_uuid_v4(evil) + end + assert_equal('should pass a string for UUID parser', err.message) + end + end + + def test_from_uuid_v7 + assert_equal(ULID.parse('01JHQSXFTRFFHRZN260SG6P1DA'), ULID.from_uuid_v7('01946f9e-bf58-7be3-8fd4-4606606b05aa')) + assert_equal(ULID.parse('01JHQSXFTRFFHRZN260SG6P1DA'), ULID.from_uuid_v7('urn:uuid:01946f9e-bf58-7be3-8fd4-4606606b05aa')) + + # Rough tests + ulids = 1000.times.map do + ULID.from_uuid_v7(SecureRandom.uuid_v7) + end + assert_true(ulids.uniq == ulids) + + # Ensure some invalid patterns + [ + '6ba7b810-9dad-11d1-80b4-00c04fd430c8', # UUIDv3, v5 + '0983d0a2-ff15-4d83-8f37-7dd945b5aa39', # UUIDv4 + '0983d0a2-ff15-4d83-8f37-7dd945b5aa3', # Shortage + '0983d0a2-ff15-4d83-8f37-7dd945b5aa390', # Excess + "0983d0a2-ff15-4d83-8f37-7dd945b5aa39\n", # Line end + '0983d0a2-ff15-4d83-8f37--7dd945b5aa39' # `-` excess + ].each do |invalid_uuidv7| + assert_raises(ULID::ParserError) do + ULID.from_uuid_v7(invalid_uuidv7) + end + end + + assert_raises(ArgumentError) do + ULID.from_uuid_v7 + end + + [nil, 42, :'0983d0a2-ff15-4d83-8f37-7dd945b5aa39', BasicObject.new, Object.new, ulids.sample].each do |evil| + err = assert_raises(ArgumentError) do + ULID.from_uuid_v7(evil) end assert_equal('should pass a string for UUID parser', err.message) end @@ -109,23 +152,37 @@ def test_from_uuidv4 def test_uuid_parser_for_boundary_example # This behavior is same as https://github.com/ahawker/ulid/tree/96bdb1daad7ce96f6db8c91ac0410b66d2e1c4c1 on CPython 3.9.4 - assert_equal(ULID.parse('00000000008008000000000000'), ULID.from_uuidv4('00000000-0000-4000-8000-000000000000')) - assert_equal(ULID.parse('7ZZZZZZZZZ9ZZVZZZZZZZZZZZZ'), ULID.from_uuidv4('ffffffff-ffff-4fff-bfff-ffffffffffff')) + assert_equal(ULID.parse('00000000008008000000000000'), ULID.from_uuid_v4('00000000-0000-4000-8000-000000000000')) + assert_equal(ULID.parse('7ZZZZZZZZZ9ZZVZZZZZZZZZZZZ'), ULID.from_uuid_v4('ffffffff-ffff-4fff-bfff-ffffffffffff')) + + # v7 + assert_equal(ULID.parse('0000000000E008000000000000'), ULID.from_uuid_v7('00000000-0000-7000-8000-000000000000')) + assert_equal(ULID.parse('7ZZZZZZZZZFZZVZZZZZZZZZZZZ'), ULID.from_uuid_v7('ffffffff-ffff-7fff-bfff-ffffffffffff')) - # Also uuidish can load as same + # Also uuidish can load as same as versioned methods assert_equal(ULID.parse('00000000008008000000000000'), ULID.from_uuidish('00000000-0000-4000-8000-000000000000')) assert_equal(ULID.parse('7ZZZZZZZZZ9ZZVZZZZZZZZZZZZ'), ULID.from_uuidish('ffffffff-ffff-4fff-bfff-ffffffffffff')) + assert_equal(ULID.parse('0000000000E008000000000000'), ULID.from_uuidish('00000000-0000-7000-8000-000000000000')) + assert_equal(ULID.parse('7ZZZZZZZZZFZZVZZZZZZZZZZZZ'), ULID.from_uuidish('ffffffff-ffff-7fff-bfff-ffffffffffff')) # Just ensuring - assert_not_equal(ULID.min, ULID.from_uuidv4('00000000-0000-4000-8000-000000000000')) - assert_not_equal(ULID.max, ULID.from_uuidv4('ffffffff-ffff-4fff-bfff-ffffffffffff')) + assert_not_equal(ULID.min, ULID.from_uuid_v4('00000000-0000-4000-8000-000000000000')) + assert_not_equal(ULID.min, ULID.from_uuid_v7('00000000-0000-7000-8000-000000000000')) + assert_not_equal(ULID.max, ULID.from_uuid_v4('ffffffff-ffff-4fff-bfff-ffffffffffff')) + assert_not_equal(ULID.max, ULID.from_uuid_v7('ffffffff-ffff-7fff-bfff-ffffffffffff')) - # Min and Max values does not have version and variants, so failed in v4 parser + # Min and Max values does not have version and variants, so failed in v4 and v7 parser assert_raises(ULID::ParserError) do - ULID.from_uuidv4('00000000-0000-0000-0000-000000000000') + ULID.from_uuid_v4('00000000-0000-0000-0000-000000000000') end assert_raises(ULID::ParserError) do - ULID.from_uuidv4('ffffffff-ffff-ffff-ffff-ffffffffffff') + ULID.from_uuid_v7('00000000-0000-0000-0000-000000000000') + end + assert_raises(ULID::ParserError) do + ULID.from_uuid_v4('ffffffff-ffff-ffff-ffff-ffffffffffff') + end + assert_raises(ULID::ParserError) do + ULID.from_uuid_v7('ffffffff-ffff-ffff-ffff-ffffffffffff') end # But relaxed parser parsed it with ignoring the version and variants specs @@ -133,15 +190,15 @@ def test_uuid_parser_for_boundary_example assert_equal(ULID.max, ULID.from_uuidish('ffffffff-ffff-ffff-ffff-ffffffffffff')) end - def test_to_uuidv4_for_typical_example + def test_to_uuid_v4_for_typical_example # The example value was taken from https://github.com/ahawker/ulid/tree/96bdb1daad7ce96f6db8c91ac0410b66d2e1c4c1#usage ulid = ULID.parse('09GF8A5ZRN9P1RYDVXV52VBAHS') - assert_equal('0983d0a2-ff15-4d83-8f37-7dd945b5aa39', ulid.to_uuidv4) - assert_equal('0983d0a2-ff15-4d83-8f37-7dd945b5aa39', ulid.to_uuidv4(force: false)) - assert_equal(ulid.to_uuidv4, ulid.to_uuidv4) - assert_not_same(ulid.to_uuidv4, ulid.to_uuidv4) - assert_true(ulid.to_uuidv4.frozen?) - assert_equal(Encoding::US_ASCII, ulid.to_uuidv4.encoding) + assert_equal('0983d0a2-ff15-4d83-8f37-7dd945b5aa39', ulid.to_uuid_v4) + assert_equal('0983d0a2-ff15-4d83-8f37-7dd945b5aa39', ulid.to_uuid_v4(force: false)) + assert_equal(ulid.to_uuid_v4, ulid.to_uuid_v4) + assert_not_same(ulid.to_uuid_v4, ulid.to_uuid_v4) + assert_true(ulid.to_uuid_v4.frozen?) + assert_equal(Encoding::US_ASCII, ulid.to_uuid_v4.encoding) end # @see https://github.com/kachick/ruby-ulid/issues/76 for further detail @@ -151,41 +208,67 @@ def test_v4compatible_or_not assert_equal(ulid, ULID.from_uuidish(ulid.to_uuidish)) assert_raises(ULID::ParserError) do - ULID.from_uuidv4(ulid.to_uuidish) + ULID.from_uuid_v4(ulid.to_uuidish) end assert_raises(ULID::IrreversibleUUIDError) do - ulid.to_uuidv4 + ulid.to_uuid_v4 end - assert_equal('4e30ea1c-6c14-423b-98a9-ecb5c3f43908', ulid.to_uuidv4(force: true)) - assert_equal(ULID.parse('2E63N1RV0M88XSHAFCPQ1Z8E88'), ULID.from_uuidv4(ulid.to_uuidv4(force: true))) - assert_equal(ULID.from_uuidish(ulid.to_uuidv4(force: true)), ULID.from_uuidv4(ulid.to_uuidv4(force: true))) + assert_equal('4e30ea1c-6c14-423b-98a9-ecb5c3f43908', ulid.to_uuid_v4(force: true)) + assert_equal(ULID.parse('2E63N1RV0M88XSHAFCPQ1Z8E88'), ULID.from_uuid_v4(ulid.to_uuid_v4(force: true))) + assert_equal(ULID.from_uuidish(ulid.to_uuid_v4(force: true)), ULID.from_uuid_v4(ulid.to_uuid_v4(force: true))) end - def test_to_uuidv4_for_boundary_example - assert_equal('00000000-0000-4000-8000-000000000000', ULID.min.to_uuidv4(force: true)) - assert_equal('ffffffff-ffff-4fff-bfff-ffffffffffff', ULID.max.to_uuidv4(force: true)) + def test_to_uuid_v4_for_boundary_example + assert_equal('00000000-0000-4000-8000-000000000000', ULID.min.to_uuid_v4(force: true)) + assert_equal('ffffffff-ffff-4fff-bfff-ffffffffffff', ULID.max.to_uuid_v4(force: true)) assert_raises(ULID::IrreversibleUUIDError) do - ULID.min.to_uuidv4 + ULID.min.to_uuid_v4 end assert_raises(ULID::IrreversibleUUIDError) do - ULID.max.to_uuidv4 + ULID.max.to_uuid_v4 + end + end + + def test_to_uuid_v7_for_boundary_example + assert_equal('00000000-0000-7000-8000-000000000000', ULID.min.to_uuid_v7(force: true)) + assert_equal('ffffffff-ffff-7fff-bfff-ffffffffffff', ULID.max.to_uuid_v7(force: true)) + + assert_raises(ULID::IrreversibleUUIDError) do + ULID.min.to_uuid_v7 + end + assert_raises(ULID::IrreversibleUUIDError) do + ULID.max.to_uuid_v7 end end def test_uuidv4_compatibility_with_many_random_data uuids = 1000.times.map do - SecureRandom.uuid + SecureRandom.uuid_v4 + end + + assert_true(uuids.uniq.size == 1000) + + ulids = uuids.map do |uuid| + ULID.from_uuid_v4(uuid) + end + + assert_equal(uuids, ulids.map(&:to_uuid_v4)) + end + + def test_uuidv7_compatibility_with_many_random_data + uuids = 1000.times.map do + SecureRandom.uuid_v7 end assert_true(uuids.uniq.size == 1000) ulids = uuids.map do |uuid| - ULID.from_uuidv4(uuid) + ULID.from_uuid_v7(uuid) end - assert_equal(uuids, ulids.map(&:to_uuidv4)) + assert_equal(uuids, ulids.map(&:to_uuid_v7)) end end diff --git a/test/many_data/test_snapshots.rb b/test/many_data/test_snapshots.rb index ea572c7a..062465ca 100644 --- a/test/many_data/test_snapshots.rb +++ b/test/many_data/test_snapshots.rb @@ -22,7 +22,7 @@ def assert_example(ulid, example) assert_equal(timestamp, ulid.timestamp) assert_equal(randomness, ulid.randomness) assert_equal(uuidish, ulid.to_uuidish) - assert_equal(uuidv4, ulid.to_uuidv4(force: true)) + assert_equal(uuidv4, ulid.to_uuid_v4(force: true)) assert_equal(to_time, ulid.to_time) assert_equal(octets, ulid.octets) else