diff --git a/stdlib/rubygems/0/rubygems.rbs b/stdlib/rubygems/0/rubygems.rbs index 09128e136..d1b91825d 100644 --- a/stdlib/rubygems/0/rubygems.rbs +++ b/stdlib/rubygems/0/rubygems.rbs @@ -1,8 +1,3 @@ -# TODO: Is this defined as a built-in type? -interface _HashLike[K, V] - def each_pair: () { ([ K, V ]) -> untyped } -> self -end - # RubyGems is the Ruby standard for publishing and managing third party # libraries. # @@ -99,6 +94,10 @@ end # -The RubyGems Team # module Gem + interface _HashLike[K, V] + def each_pair: () { ([ K, V ]) -> untyped } -> self + end + DEFAULT_HOST: String GEM_DEP_FILES: Array[String] diff --git a/stdlib/rubygems/0/version.rbs b/stdlib/rubygems/0/version.rbs index 1cde02c68..59f797aa6 100644 --- a/stdlib/rubygems/0/version.rbs +++ b/stdlib/rubygems/0/version.rbs @@ -1,3 +1,228 @@ -class Gem::Version - # TODO: Add sinatures... +module Gem + # The Version class processes string versions into comparable values. A version + # string should normally be a series of numbers separated by periods. Each part + # (digits separated by periods) is considered its own number, and these are used + # for sorting. So for instance, 3.10 sorts higher than 3.2 because ten is + # greater than two. + # + # If any part contains letters (currently only a-z are supported) then that + # version is considered prerelease. Versions with a prerelease part in the Nth + # part sort less than versions with N-1 parts. Prerelease parts are sorted + # alphabetically using the normal Ruby string sorting rules. If a prerelease + # part contains both letters and numbers, it will be broken into multiple parts + # to provide expected sort behavior (1.0.a10 becomes 1.0.a.10, and is greater + # than 1.0.a9). + # + # Prereleases sort between real releases (newest to oldest): + # + # 1. 1.0 + # 2. 1.0.b1 + # 3. 1.0.a.2 + # 4. 0.9 + # + # + # If you want to specify a version restriction that includes both prereleases + # and regular releases of the 1.x series this is the best way: + # + # s.add_dependency 'example', '>= 1.0.0.a', '< 2.0.0' + # + # ## How Software Changes + # + # Users expect to be able to specify a version constraint that gives them some + # reasonable expectation that new versions of a library will work with their + # software if the version constraint is true, and not work with their software + # if the version constraint is false. In other words, the perfect system will + # accept all compatible versions of the library and reject all incompatible + # versions. + # + # Libraries change in 3 ways (well, more than 3, but stay focused here!). + # + # 1. The change may be an implementation detail only and have no effect on the + # client software. + # 2. The change may add new features, but do so in a way that client software + # written to an earlier version is still compatible. + # 3. The change may change the public interface of the library in such a way + # that old software is no longer compatible. + # + # + # Some examples are appropriate at this point. Suppose I have a Stack class + # that supports a `push` and a `pop` method. + # + # ### Examples of Category 1 changes: + # + # * Switch from an array based implementation to a linked-list based + # implementation. + # * Provide an automatic (and transparent) backing store for large stacks. + # + # + # ### Examples of Category 2 changes might be: + # + # * Add a `depth` method to return the current depth of the stack. + # * Add a `top` method that returns the current top of stack (without changing + # the stack). + # * Change `push` so that it returns the item pushed (previously it had no + # usable return value). + # + # + # ### Examples of Category 3 changes might be: + # + # * Changes `pop` so that it no longer returns a value (you must use `top` to + # get the top of the stack). + # * Rename the methods to `push_item` and `pop_item`. + # + # + # ## RubyGems Rational Versioning + # + # * Versions shall be represented by three non-negative integers, separated by + # periods (e.g. 3.1.4). The first integers is the "major" version number, + # the second integer is the "minor" version number, and the third integer is + # the "build" number. + # + # * A category 1 change (implementation detail) will increment the build + # number. + # + # * A category 2 change (backwards compatible) will increment the minor + # version number and reset the build number. + # + # * A category 3 change (incompatible) will increment the major build number + # and reset the minor and build numbers. + # + # * Any "public" release of a gem should have a different version. Normally + # that means incrementing the build number. This means a developer can + # generate builds all day long, but as soon as they make a public release, + # the version must be updated. + # + # + # ### Examples + # + # Let's work through a project lifecycle using our Stack example from above. + # + # Version 0.0.1 + # : The initial Stack class is release. + # Version 0.0.2 + # : Switched to a linked=list implementation because it is cooler. + # Version 0.1.0 + # : Added a `depth` method. + # Version 1.0.0 + # : Added `top` and made `pop` return nil (`pop` used to return the old top + # item). + # Version 1.1.0 + # : `push` now returns the value pushed (it used it return nil). + # Version 1.1.1 + # : Fixed a bug in the linked list implementation. + # Version 1.1.2 + # : Fixed a bug introduced in the last fix. + # + # + # Client A needs a stack with basic push/pop capability. They write to the + # original interface (no `top`), so their version constraint looks like: + # + # gem 'stack', '>= 0.0' + # + # Essentially, any version is OK with Client A. An incompatible change to the + # library will cause them grief, but they are willing to take the chance (we + # call Client A optimistic). + # + # Client B is just like Client A except for two things: (1) They use the `depth` + # method and (2) they are worried about future incompatibilities, so they write + # their version constraint like this: + # + # gem 'stack', '~> 0.1' + # + # The `depth` method was introduced in version 0.1.0, so that version or + # anything later is fine, as long as the version stays below version 1.0 where + # incompatibilities are introduced. We call Client B pessimistic because they + # are worried about incompatible future changes (it is OK to be pessimistic!). + # + # ## Preventing Version Catastrophe: + # + # From: http://blog.zenspider.com/2008/10/rubygems-howto-preventing-cata.html + # + # Let's say you're depending on the fnord gem version 2.y.z. If you specify your + # dependency as ">= 2.0.0" then, you're good, right? What happens if fnord 3.0 + # comes out and it isn't backwards compatible with 2.y.z? Your stuff will break + # as a result of using ">=". The better route is to specify your dependency with + # an "approximate" version specifier ("~>"). They're a tad confusing, so here is + # how the dependency specifiers work: + # + # Specification From ... To (exclusive) + # ">= 3.0" 3.0 ... ∞ + # "~> 3.0" 3.0 ... 4.0 + # "~> 3.0.0" 3.0.0 ... 3.1 + # "~> 3.5" 3.5 ... 4.0 + # "~> 3.5.0" 3.5.0 ... 3.6 + # "~> 3" 3.0 ... 4.0 + # + # For the last example, single-digit versions are automatically extended with a + # zero to give a sensible result. + # + class Version + include Comparable + + # True if the `version` string matches RubyGems' requirements. + # + def self.correct?: (_ToS version) -> bool + + # Factory method to create a Version object. Input may be a Version or a String. + # Intended to simplify client code. + # + # ver1 = Version.create('1.3.17') # -> (Version object) + # ver2 = Version.create(ver1) # -> (ver1) + # ver3 = Version.create(nil) # -> nil + # + def self.create: (_ToS | Version | nil input) -> instance? + + # Constructs a Version from the `version` string. A version string is a series + # of digits or ASCII letters separated by dots. + # + def initialize: (_ToS version) -> void + + # is larger, the same, or smaller than this one. Attempts to compare to + # something that's not a `Gem::Version` return `nil`. + # + def <=>: (untyped other) -> Integer? + + # A recommended version for use with a ~> Requirement. + # + def approximate_recommendation: () -> String + + # Return a new version object where the next to the last revision number is one + # greater (e.g., 5.3.1 => 5.4). + # + # Pre-release (alpha) parts, e.g, 5.3.1.b.2 => 5.4, are ignored. + # + def bump: () -> instance + + def canonical_segments: () -> Array[Integer | String] + + # A Version is only eql? to another version if it's specified to the same + # precision. Version "1.0" is not the same as version "1". + # + def eql?: (untyped other) -> bool + + # Dump only the raw version string, not the complete object. It's a string for + # backwards (RubyGems 1.3.5 and earlier) compatibility. + # + def marshal_dump: () -> Array[String] + + # Load custom marshal format. It's a string for backwards (RubyGems 1.3.5 and + # earlier) compatibility. + # + def marshal_load: (Array[String] array) -> void + + # A version is considered a prerelease if it contains a letter. + # + def prerelease?: () -> bool + + # The release for this version (e.g. 1.2.0.a -> 1.2.0). Non-prerelease versions + # return themselves. + # + def release: () -> instance + + # A string representation of this Version. + # + def version: () -> String + + alias to_s version + end end diff --git a/test/stdlib/rubygems/GemVersion_test.rb b/test/stdlib/rubygems/GemVersion_test.rb new file mode 100644 index 000000000..b306b9634 --- /dev/null +++ b/test/stdlib/rubygems/GemVersion_test.rb @@ -0,0 +1,96 @@ +require_relative "../test_helper" + +class GemVersionSingletonTest < Test::Unit::TestCase + include TypeAssertions + + library "rubygems" + testing "singleton(::Gem::Version)" + + def test_correct? + assert_send_type "(String) -> bool", + Gem::Version, :correct?, "1.2.3" + assert_send_type "(ToS) -> bool", + Gem::Version, :correct?, ToS.new + end + + def test_create + assert_send_type "(String) -> Gem::Version", + Gem::Version, :create, "1.2.3" + assert_send_type "(ToS) -> Gem::Version", + Gem::Version, :create, ToS.new("1.2.3") + assert_send_type "(Gem::Version) -> Gem::Version", + Gem::Version, :create, Gem::Version.new("1.2.3") + assert_send_type "(nil) -> nil", + Gem::Version, :create, nil + end + + def test_new + assert_send_type "(String) -> Gem::Version", + Gem::Version, :new, "1.2.3" + assert_send_type "(ToS) -> Gem::Version", + Gem::Version, :new, ToS.new("1.2.3") + end +end + +class GemVersionInstanceTest < Test::Unit::TestCase + include TypeAssertions + + library "rubygems" + testing "::Gem::Version" + + def test_comparable + assert_send_type "(Gem::Version) -> Integer", + Gem::Version.new("0.0.1"), :<=>, Gem::Version.new("1.0.0") + assert_send_type "(String) -> nil", + Gem::Version.new("0.0.0"), :<=>, "1.0.0" + end + + def test_approximate_recommendation + assert_send_type "() -> String", + Gem::Version.new("0.0.1"), :approximate_recommendation + end + + def test_bump + assert_send_type "() -> Gem::Version", + Gem::Version.new("0.0.1"), :bump + end + + def test_canonical_segments + assert_send_type "() -> Array[Integer]", + Gem::Version.new("0.0.1"), :canonical_segments + assert_send_type "() -> Array[Integer | String]", + Gem::Version.new("0.0.1-alpha.1"), :canonical_segments + end + + def test_eql? + assert_send_type "(Gem::Version) -> bool", + Gem::Version.new("0.0.1"), :eql?, Gem::Version.new("0.0.1") + assert_send_type "(String) -> bool", + Gem::Version.new("0.0.1"), :eql?, "1.0.0" + end + + def test_marshal_dump + assert_send_type "() -> Array[String]", + Gem::Version.new("0.0.1"), :marshal_dump + end + + def test_marshal_load + assert_send_type "(Array[String]) -> void", + Gem::Version.new("0.0.1"), :marshal_load, ["1.0.0"] + end + + def test_prerelease? + assert_send_type "() -> bool", + Gem::Version.new("0.0.1"), :prerelease? + end + + def test_release + assert_send_type "() -> Gem::Version", + Gem::Version.new("1.0.0.a"), :release + end + + def test_version + assert_send_type "() -> String", + Gem::Version.new("1.0.0"), :version + end +end