From 653101a742358bae27974692f60e69848498bc21 Mon Sep 17 00:00:00 2001 From: Brandon Weaver Date: Sat, 8 Nov 2025 19:01:53 -0800 Subject: [PATCH 1/4] Add pattern matching support to Gem::Version Implements deconstruct and deconstruct_keys methods to enable pattern matching on Gem::Version objects. Array pattern matching: case version in [major, minor, patch] # ... end Hash pattern matching: case version in major: 3.., minor: 2.. # ... end --- lib/rubygems/version.rb | 7 +++++++ test/rubygems/test_gem_version.rb | 32 +++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/lib/rubygems/version.rb b/lib/rubygems/version.rb index 25c412be4b6b..ce943b35dcbe 100644 --- a/lib/rubygems/version.rb +++ b/lib/rubygems/version.rb @@ -336,6 +336,13 @@ def segments # :nodoc: _segments.dup end + alias deconstruct segments + + def deconstruct_keys(keys) + major, minor, patch = segments + {major:, minor:, patch:} + end + ## # A recommended version for use with a ~> Requirement. diff --git a/test/rubygems/test_gem_version.rb b/test/rubygems/test_gem_version.rb index cf771bc5a14a..447bc127155a 100644 --- a/test/rubygems/test_gem_version.rb +++ b/test/rubygems/test_gem_version.rb @@ -226,6 +226,38 @@ def test_frozen_version assert_version_equal v("2"), v.bump end + def test_deconstruct + version = v("3.2.1") + major, minor, patch = version.deconstruct + assert_equal 3, major + assert_equal 2, minor + assert_equal 1, patch + end + + def test_deconstruct_keys + version = v("3.2.1") + assert_equal({major: 3, minor: 2, patch: 1}, version.deconstruct_keys(nil)) + end + + def test_pattern_matching_array + case v("3.2.1") + in [major, minor, patch] + assert_equal 3, major + assert_equal 2, minor + assert_equal 1, patch + else + flunk "Array pattern did not match" + end + end + + def test_pattern_matching_hash + result = case v("3.2.1") + in major: 3.., minor: 2.. then "matched" + else "no match" + end + assert_equal "matched", result + end + # Asserts that +version+ is a prerelease. def assert_prerelease(version) From 2a4c560e794d48317775cf77a156fa7fbfbfe335 Mon Sep 17 00:00:00 2001 From: Brandon Weaver Date: Sat, 8 Nov 2025 19:07:10 -0800 Subject: [PATCH 2/4] Fix RuboCop style offenses in pattern matching implementation --- lib/rubygems/version.rb | 4 ++-- test/rubygems/test_gem_version.rb | 11 ++++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/rubygems/version.rb b/lib/rubygems/version.rb index ce943b35dcbe..8519e225fa11 100644 --- a/lib/rubygems/version.rb +++ b/lib/rubygems/version.rb @@ -336,11 +336,11 @@ def segments # :nodoc: _segments.dup end - alias deconstruct segments + alias_method :deconstruct, :segments def deconstruct_keys(keys) major, minor, patch = segments - {major:, minor:, patch:} + { major:, minor:, patch: } end ## diff --git a/test/rubygems/test_gem_version.rb b/test/rubygems/test_gem_version.rb index 447bc127155a..e896f29e6b26 100644 --- a/test/rubygems/test_gem_version.rb +++ b/test/rubygems/test_gem_version.rb @@ -236,7 +236,7 @@ def test_deconstruct def test_deconstruct_keys version = v("3.2.1") - assert_equal({major: 3, minor: 2, patch: 1}, version.deconstruct_keys(nil)) + assert_equal({ major: 3, minor: 2, patch: 1 }, version.deconstruct_keys(nil)) end def test_pattern_matching_array @@ -251,10 +251,11 @@ def test_pattern_matching_array end def test_pattern_matching_hash - result = case v("3.2.1") - in major: 3.., minor: 2.. then "matched" - else "no match" - end + result = + case v("3.2.1") + in major: 3.., minor: 2.. then "matched" + else "no match" + end assert_equal "matched", result end From 423a743091467bb3bd7482a4f9b380a4f8f50ae7 Mon Sep 17 00:00:00 2001 From: Brandon Weaver Date: Mon, 10 Nov 2025 19:28:34 -0800 Subject: [PATCH 3/4] Add documentation and tests for versions with fewer segments --- lib/rubygems/version.rb | 29 +++++++++++++++++++++++++++++ test/rubygems/test_gem_version.rb | 10 ++++++++++ 2 files changed, 39 insertions(+) diff --git a/lib/rubygems/version.rb b/lib/rubygems/version.rb index 8519e225fa11..7f9a81da938b 100644 --- a/lib/rubygems/version.rb +++ b/lib/rubygems/version.rb @@ -336,8 +336,37 @@ def segments # :nodoc: _segments.dup end + ## + # Deconstructs the version into an array for pattern matching. + # Returns the version segments as an array. + # + # Gem::Version.new("3.2.1").deconstruct #=> [3, 2, 1] + # + # This enables array pattern matching: + # + # case Gem::Version.new("3.2.1") + # in [major, minor, patch] + # # major => 3, minor => 2, patch => 1 + # end alias_method :deconstruct, :segments + ## + # Deconstructs the version into a hash for pattern matching. + # Returns a hash with keys +:major+, +:minor+, and +:patch+. + # + # Gem::Version.new("3.2.1").deconstruct_keys(nil) #=> { major: 3, minor: 2, patch: 1 } + # + # This enables hash pattern matching: + # + # case Gem::Version.new("3.2.1") + # in major: 3.., minor: 2.. + # # matches versions >= 3.2 + # end + # + # Note: For versions with fewer than 3 segments, missing values are +nil+: + # + # Gem::Version.new("3.2").deconstruct_keys(nil) #=> { major: 3, minor: 2, patch: nil } + # Gem::Version.new("3").deconstruct_keys(nil) #=> { major: 3, minor: nil, patch: nil } def deconstruct_keys(keys) major, minor, patch = segments { major:, minor:, patch: } diff --git a/test/rubygems/test_gem_version.rb b/test/rubygems/test_gem_version.rb index e896f29e6b26..401576168e46 100644 --- a/test/rubygems/test_gem_version.rb +++ b/test/rubygems/test_gem_version.rb @@ -239,6 +239,16 @@ def test_deconstruct_keys assert_equal({ major: 3, minor: 2, patch: 1 }, version.deconstruct_keys(nil)) end + def test_deconstruct_keys_two_segments + version = v("3.2") + assert_equal({ major: 3, minor: 2, patch: nil }, version.deconstruct_keys(nil)) + end + + def test_deconstruct_keys_one_segment + version = v("3") + assert_equal({ major: 3, minor: nil, patch: nil }, version.deconstruct_keys(nil)) + end + def test_pattern_matching_array case v("3.2.1") in [major, minor, patch] From 8676f19b14c47156af8d5e53a2c4a8b25d7c9f6f Mon Sep 17 00:00:00 2001 From: Brandon Weaver Date: Mon, 10 Nov 2025 21:32:09 -0800 Subject: [PATCH 4/4] Update to use 'build' terminology and clarify hash pattern semantics - Changed 'patch' to 'build' in documentation and return values - Added warning that hash patterns check segments independently - Added test demonstrating difference between hash patterns and version comparison - Fixed endless range syntax with parentheses --- lib/rubygems/version.rb | 49 +++++++++++++++++++++++-------- test/rubygems/test_gem_version.rb | 26 ++++++++++++---- 2 files changed, 57 insertions(+), 18 deletions(-) diff --git a/lib/rubygems/version.rb b/lib/rubygems/version.rb index 7f9a81da938b..7cd2001223a0 100644 --- a/lib/rubygems/version.rb +++ b/lib/rubygems/version.rb @@ -340,36 +340,59 @@ def segments # :nodoc: # Deconstructs the version into an array for pattern matching. # Returns the version segments as an array. # - # Gem::Version.new("3.2.1").deconstruct #=> [3, 2, 1] + # Gem::Version.new("3.2.1").deconstruct #=> [3, 2, 1] + # Gem::Version.new("3.2.0.rc.2").deconstruct #=> [3, 2, 0, "rc", 2] # - # This enables array pattern matching: + # This enables array pattern matching on version segments: # # case Gem::Version.new("3.2.1") - # in [major, minor, patch] - # # major => 3, minor => 2, patch => 1 + # in [major, minor, build] + # # major => 3, minor => 2, build => 1 + # end + # + # case Gem::Version.new("3.2.0.rc.2") + # in [major, minor, build, pre, *] + # # Matches prerelease versions # end alias_method :deconstruct, :segments ## # Deconstructs the version into a hash for pattern matching. - # Returns a hash with keys +:major+, +:minor+, and +:patch+. + # Returns a hash with keys +:major+, +:minor+, and +:build+. + # + # Note: RubyGems does not enforce a specific versioning scheme, and the + # names "major", "minor", and "build" are conventional. # - # Gem::Version.new("3.2.1").deconstruct_keys(nil) #=> { major: 3, minor: 2, patch: 1 } + # Gem::Version.new("3.2.1").deconstruct_keys(nil) #=> { major: 3, minor: 2, build: 1 } + # Gem::Version.new("3.2").deconstruct_keys(nil) #=> { major: 3, minor: 2, build: nil } + # Gem::Version.new("3").deconstruct_keys(nil) #=> { major: 3, minor: nil, build: nil } # # This enables hash pattern matching: # # case Gem::Version.new("3.2.1") - # in major: 3.., minor: 2.. - # # matches versions >= 3.2 + # in major: 3, minor: 2, build: 1 + # # Matches exactly 3.2.1 + # end + # + # Important: Hash pattern matching checks each segment independently, which + # differs from version comparison semantics: + # + # # This matches "3.2" but NOT "4.0" (since 0 < 2) + # case Gem::Version.new("4.0") + # in major: (3..), minor: (2..) + # "matches" + # else + # "no match" # => "no match" # end # - # Note: For versions with fewer than 3 segments, missing values are +nil+: + # # However, version comparison shows 4.0 > 3.2 + # Gem::Version.new("4.0") >= Gem::Version.new("3.2") #=> true # - # Gem::Version.new("3.2").deconstruct_keys(nil) #=> { major: 3, minor: 2, patch: nil } - # Gem::Version.new("3").deconstruct_keys(nil) #=> { major: 3, minor: nil, patch: nil } + # For version comparison logic, use standard comparison operators or array + # patterns instead of hash patterns with ranges. def deconstruct_keys(keys) - major, minor, patch = segments - { major:, minor:, patch: } + major, minor, build = segments + { major:, minor:, build: } end ## diff --git a/test/rubygems/test_gem_version.rb b/test/rubygems/test_gem_version.rb index 401576168e46..d39f46bb22e6 100644 --- a/test/rubygems/test_gem_version.rb +++ b/test/rubygems/test_gem_version.rb @@ -236,25 +236,25 @@ def test_deconstruct def test_deconstruct_keys version = v("3.2.1") - assert_equal({ major: 3, minor: 2, patch: 1 }, version.deconstruct_keys(nil)) + assert_equal({ major: 3, minor: 2, build: 1 }, version.deconstruct_keys(nil)) end def test_deconstruct_keys_two_segments version = v("3.2") - assert_equal({ major: 3, minor: 2, patch: nil }, version.deconstruct_keys(nil)) + assert_equal({ major: 3, minor: 2, build: nil }, version.deconstruct_keys(nil)) end def test_deconstruct_keys_one_segment version = v("3") - assert_equal({ major: 3, minor: nil, patch: nil }, version.deconstruct_keys(nil)) + assert_equal({ major: 3, minor: nil, build: nil }, version.deconstruct_keys(nil)) end def test_pattern_matching_array case v("3.2.1") - in [major, minor, patch] + in [major, minor, build] assert_equal 3, major assert_equal 2, minor - assert_equal 1, patch + assert_equal 1, build else flunk "Array pattern did not match" end @@ -269,6 +269,22 @@ def test_pattern_matching_hash assert_equal "matched", result end + def test_pattern_matching_hash_vs_comparison + # Hash pattern checks each segment independently + version = v("4.0") + result = + case version + in major: (3..), minor: (2..) + "matched" + else + "no match" + end + assert_equal "no match", result + + # But version comparison shows 4.0 > 3.2 + assert_operator v("4.0"), :>=, v("3.2") + end + # Asserts that +version+ is a prerelease. def assert_prerelease(version)