Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement ULID.parse_variant_format #206

Merged
merged 2 commits into from
Jul 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
20 changes: 15 additions & 5 deletions lib/ulid.rb
Original file line number Diff line number Diff line change
Expand Up @@ -268,16 +268,26 @@ 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`
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
Expand All @@ -292,23 +302,23 @@ 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
else
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.
#
# @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
Expand Down
25 changes: 20 additions & 5 deletions sig/ulid.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions steep_expectations.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
65 changes: 52 additions & 13 deletions test/core/test_ulid_class.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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))

[
'',
Expand All @@ -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

Expand Down