From bef91dc9798e3bd226933c2a6cf3606624085786 Mon Sep 17 00:00:00 2001 From: Kenichi Kamiya Date: Sun, 3 Jul 2022 18:40:09 +0900 Subject: [PATCH] Implement `ULID.parse_variant_format` (#206) Closes #143 Resolves #57 Resolves #78 Updates #205 (Renamed non released method) --- README.md | 20 +++++------ lib/ulid.rb | 20 ++++++++--- sig/ulid.rbs | 25 +++++++++++--- steep_expectations.yml | 4 +-- test/core/test_ulid_class.rb | 65 ++++++++++++++++++++++++++++-------- 5 files changed, 99 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 07e11a21..6df45cb0 100644 --- a/README.md +++ b/README.md @@ -340,29 +340,29 @@ ULID.sample(5, period: ulid1.to_time..ulid2.to_time) I'm afraid so, we should consider [Current ULID spec](https://github.com/ulid/spec/tree/d0c7170df4517939e70129b4d6462cc162f2d5bf#universally-unique-lexicographically-sortable-identifier) has `orthographical variants of the format` possibilities. +>Case insensitive + +I can understand it might be considered in actual use-case. So `ULID.parse` accepts upcase and downcase. +However it is a controversial point, discussing in [ulid/spec#3](https://github.com/ulid/spec/issues/3). + >Uses Crockford's base32 for better efficiency and readability (5 bits per character) The original `Crockford's base32` maps `I`, `L` to `1`, `O` to `0`. And accepts freestyle inserting `Hyphens (-)`. To consider this patterns or not is different in each implementations. -Current parser/validator/matcher basically aims to cover `subset of Crockford's base32`. -I have suggested it would be clarified in [ulid/spec#57](https://github.com/ulid/spec/pull/57). - ->Case insensitive - -I can understand it might be considered in actual use-case. -But it is a controversial point, discussing in [ulid/spec#3](https://github.com/ulid/spec/issues/3). +I have suggested to clarify `subset of Crockford's base32` in [ulid/spec#57](https://github.com/ulid/spec/pull/57). -Be that as it may, this gem provides API for handling the nasty possibilities. +This gem provides some methods to handle the nasty possibilities. -`ULID.normalize`, `ULID.normalized?`, `ULID.valid_as_variants?` +`ULID.normalize`, `ULID.normalized?`, `ULID.valid_as_variant_format?` and `ULID.parse_variant_format` ```ruby ULID.normalize('01g70y0y7g-z1xwdarexergsddd') #=> "01G70Y0Y7GZ1XWDAREXERGSDDD" ULID.normalized?('01g70y0y7g-z1xwdarexergsddd') #=> false ULID.normalized?('01G70Y0Y7GZ1XWDAREXERGSDDD') #=> true -ULID.valid_as_variants?('01g70y0y7g-z1xwdarexergsddd') #=> true +ULID.valid_as_variant_format?('01g70y0y7g-z1xwdarexergsddd') #=> true +ULID.parse_variant_format('01G70Y0Y7G-ZLXWDIREXERGSDoD') #=> ULID(2022-07-03 02:25:22.672 UTC: 01G70Y0Y7GZ1XWD1REXERGSD0D) ``` #### UUIDv4 converter (experimental) diff --git a/lib/ulid.rb b/lib/ulid.rb index 195dea5a..8b213825 100644 --- a/lib/ulid.rb +++ b/lib/ulid.rb @@ -268,6 +268,17 @@ def self.parse(string) from_integer(CrockfordBase32.decode(string)) end + # @param [String, #to_str] string + # @return [ULID] + # @raise [ParserError] if the given format is not correct for ULID specs + def self.parse_variant_format(string) + string = String.try_convert(string) + raise(ArgumentError, 'ULID.parse_variant_format takes only strings') unless string + + normalized_in_crockford = CrockfordBase32.normalize(string) + parse(normalized_in_crockford) + end + # @param [String, #to_str] string # @return [String] # @raise [ParserError] if the given format is not correct for ULID specs, even if ignored `orthographical variants of the format` @@ -275,9 +286,8 @@ def self.normalize(string) string = String.try_convert(string) raise(ArgumentError, 'ULID.normalize takes only strings') unless string - normalized_in_crockford = CrockfordBase32.normalize(string) # Ensure the ULID correctness, because CrockfordBase32 does not always mean to satisfy ULID format - parse(normalized_in_crockford).to_s + parse_variant_format(string).to_s end # @param [String, #to_str] string @@ -292,7 +302,7 @@ def self.normalized?(string) # @param [String, #to_str] string # @return [Boolean] - def self.valid_as_variants?(string) + def self.valid_as_variant_format?(string) normalize(string) rescue Exception false @@ -300,7 +310,7 @@ def self.valid_as_variants?(string) true end - # @deprecated Use [.valid_as_variants?] or [.normalized?] instead + # @deprecated Use [.valid_as_variant_format?] or [.normalized?] instead # # Returns `true` if it is normalized string. # Basically the difference of normalized? is to accept downcase or not. This returns true for downcased ULIDs. @@ -308,7 +318,7 @@ def self.valid_as_variants?(string) # @return [Boolean] def self.valid?(string) warn_kwargs = (RUBY_VERSION >= '3.0') ? { category: :deprecated } : {} - Warning.warn('ULID.valid? is deprecated. Use ULID.valid_as_variants? or ULID.normalized? instead.', **warn_kwargs) + Warning.warn('ULID.valid? is deprecated. Use ULID.valid_as_variant_format? or ULID.normalized? instead.', **warn_kwargs) string = String.try_convert(string) string ? STRICT_PATTERN_WITH_CROCKFORD_BASE32_SUBSET.match?(string) : false end diff --git a/sig/ulid.rbs b/sig/ulid.rbs index 72e0257d..1dbc1ac1 100644 --- a/sig/ulid.rbs +++ b/sig/ulid.rbs @@ -213,6 +213,21 @@ class ULID < Object # ``` def self.parse: (_ToStr string) -> ULID + # Get ULID instance from unnormalized String that encoded in Crockford's base32. + # + # http://www.crockford.com/base32.html + # + # * Ignore Hyphens (-) + # * Mapping 0 O o => 0, 1 I i L l => 1 + # + # ```ruby + # ulid = ULID.parse_variant_format('01G70Y0Y7G-ZLXWDIREXERGSDoD') + # #=> ULID(2022-07-03 02:25:22.672 UTC: 01G70Y0Y7GZ1XWD1REXERGSD0D) + # ``` + # + # See also [ulid/spec#57](https://github.com/ulid/spec/pull/57) and [ulid/spec#3](https://github.com/ulid/spec/issues/3) + def self.parse_variant_format: (_ToStr string) -> ULID + # ```ruby # # Currently experimental feature, so needed to load the extension. # require 'ulid/uuid' @@ -337,16 +352,16 @@ class ULID < Object # Returns `true` if it is valid in ULID format variants # # ```ruby - # ULID.valid_as_variants?(ULID.generate.to_s.downcase) #=> true - # ULID.valid_as_variants?('01G70Y0Y7G-Z1XWDAREXERGSDDD') #=> true - # ULID.valid_as_variants?('01G70Y0Y7G_Z1XWDAREXERGSDDD') #=> false + # ULID.valid_as_variant_format?(ULID.generate.to_s.downcase) #=> true + # ULID.valid_as_variant_format?('01G70Y0Y7G-Z1XWDAREXERGSDDD') #=> true + # ULID.valid_as_variant_format?('01G70Y0Y7G_Z1XWDAREXERGSDDD') #=> false # ``` # # See also [ulid/spec#57](https://github.com/ulid/spec/pull/57) and [ulid/spec#3](https://github.com/ulid/spec/issues/3) - def self.valid_as_variants?: (_ToStr string) -> bool + def self.valid_as_variant_format?: (_ToStr string) -> bool | (untyped) -> false - # DEPRECATED Use valid_as_variants? instead + # DEPRECATED Use valid_as_variant_format? instead # # Returns `true` if it is normalized string. # Basically the difference of normalized? is to accept downcase or not. This returns true for downcased ULIDs. diff --git a/steep_expectations.yml b/steep_expectations.yml index 6b946e6f..30dd924d 100644 --- a/steep_expectations.yml +++ b/steep_expectations.yml @@ -3,10 +3,10 @@ diagnostics: - range: start: - line: 311 + line: 321 character: 12 end: - line: 311 + line: 321 character: 16 severity: ERROR message: Type `singleton(::Warning)` does not have method `warn` diff --git a/test/core/test_ulid_class.rb b/test/core/test_ulid_class.rb index b7508d88..bff6c4d0 100644 --- a/test/core/test_ulid_class.rb +++ b/test/core/test_ulid_class.rb @@ -37,7 +37,8 @@ def test_exposed_methods :at, :normalized?, :parse, - :valid_as_variants? + :valid_as_variant_format?, + :parse_variant_format ].sort, exposed_methods.sort ) @@ -106,6 +107,44 @@ def test_parse end end + def test_parse_variant_format + string = +'01G70Y0Y7G-ZLXWDIREXERGSDoD' + dup_string = string.dup + parsed = ULID.parse_variant_format(string) + + # Ensure the string is not modified in parser + assert_false(string.frozen?) + assert_equal(dup_string, string) + + assert_instance_of(ULID, parsed) + assert_equal('01G70Y0Y7GZ1XWD1REXERGSD0D', parsed.to_s) + assert_equal(ULID.parse_variant_format(string), ULID.parse_variant_format('01G70Y0Y7GZ1XWD1REXERGSD0D')) + + [ + '', + "01ARZ3NDEKTSV4RRFFQ69G5FAV\n", + '01ARZ3NDEKTSV4RRFFQ69G5FAU', + '01ARZ3NDEKTSV4RRFFQ69G5FA', + '01G70Y0Y7G_ZLXWDIREXERGSDoD' + ].each do |invalid| + err = assert_raises(ULID::ParserError) do + ULID.parse_variant_format(invalid) + end + assert_match(/does not match to/, err.message) + end + + assert_raises(ArgumentError) do + ULID.parse_variant_format + end + + [nil, 42, string.to_sym, BasicObject.new, Object.new, parsed].each do |evil| + err = assert_raises(ArgumentError) do + ULID.parse_variant_format(evil) + end + assert_equal('ULID.parse_variant_format takes only strings', err.message) + end + end + def test_new err = assert_raises(NoMethodError) do ULID.new(milliseconds: 0, entropy: 42) @@ -137,7 +176,7 @@ def test_from_milliseconds_and_entropy end def test_valid? - assert_warning('ULID.valid? is deprecated. Use ULID.valid_as_variants? or ULID.normalized? instead.') do + assert_warning('ULID.valid? is deprecated. Use ULID.valid_as_variant_format? or ULID.normalized? instead.') do assert_equal(false, ULID.valid?(nil)) assert_equal(false, ULID.valid?('')) assert_equal(false, ULID.valid?(BasicObject.new)) @@ -254,16 +293,16 @@ def test_normalized? end end - def test_valid_as_variants? - assert_true(ULID.valid_as_variants?('01G70Y0Y7G-Z1XWDAREXERGSDDD')) + def test_valid_as_variant_format? + assert_true(ULID.valid_as_variant_format?('01G70Y0Y7G-Z1XWDAREXERGSDDD')) nasty = '-olarz3-noekisv4rrff-q6ig5fav--' - assert_true(ULID.valid_as_variants?(nasty)) - assert_true(ULID.valid_as_variants?(ULID.normalize(nasty))) + assert_true(ULID.valid_as_variant_format?(nasty)) + assert_true(ULID.valid_as_variant_format?(ULID.normalize(nasty))) normalized = '01ARZ3NDEKTSV4RRFFQ69G5FAV' - assert_true(ULID.valid_as_variants?(normalized)) - assert_true(ULID.valid_as_variants?(normalized.downcase)) + assert_true(ULID.valid_as_variant_format?(normalized)) + assert_true(ULID.valid_as_variant_format?(normalized.downcase)) [ '', @@ -272,20 +311,20 @@ def test_valid_as_variants? '01ARZ3NDEKTSV4RRFFQ69G5FA', '80000000000000000000000000' ].each do |invalid| - assert_false(ULID.valid_as_variants?(invalid)) + assert_false(ULID.valid_as_variant_format?(invalid)) end ULID.sample(1000).each do |sample| - assert_true(ULID.valid_as_variants?(sample.to_s)) - assert_true(ULID.valid_as_variants?(sample.to_s.downcase)) + assert_true(ULID.valid_as_variant_format?(sample.to_s)) + assert_true(ULID.valid_as_variant_format?(sample.to_s.downcase)) end assert_raises(ArgumentError) do - ULID.valid_as_variants? + ULID.valid_as_variant_format? end [nil, 42, normalized.to_sym, BasicObject.new, Object.new, ULID.parse(normalized)].each do |evil| - assert_false(ULID.valid_as_variants?(evil)) + assert_false(ULID.valid_as_variant_format?(evil)) end end