File: format.rb

package info (click to toggle)
ruby-mixlib-versioning 1.1.0-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 320 kB
  • sloc: ruby: 1,374; makefile: 3
file content (353 lines) | stat: -rw-r--r-- 12,642 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
#
# Author:: Seth Chisamore (<schisamo@chef.io>)
# Copyright:: Copyright (c) 2013 Opscode, Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

require 'mixlib/versioning/format/git_describe'
require 'mixlib/versioning/format/opscode_semver'
require 'mixlib/versioning/format/rubygems'
require 'mixlib/versioning/format/semver'

module Mixlib
  class Versioning
    # @author Seth Chisamore (<schisamo@chef.io>)
    #
    # @!attribute [r] major
    #   @return [Integer] major identifier
    # @!attribute [r] minor
    #   @return [Integer] minor identifier
    # @!attribute [r] patch
    #   @return [Integer] patch identifier
    # @!attribute [r] prerelease
    #   @return [String] pre-release identifier
    # @!attribute [r] build
    #   @return [String] build identifier
    # @!attribute [r] iteration
    #   @return [String] build interation
    # @!attribute [r] input
    #   @return [String] original input version string that was parsed
    class Format
      include Comparable

      # Returns the {Mixlib::Versioning::Format} class that maps to the given
      # format type.
      #
      # @example
      #   Mixlib::Versioning::Format.for(:semver)
      #   Mixlib::Versioning::Format.for('semver')
      #   Mixlib::Versioning::Format.for(Mixlib::Versioning::Format::SemVer)
      #
      # @param format_type [String, Symbol, Mixlib::Versioning::Format] Name of
      #   a valid +Mixlib::Versioning::Format+ in Class or snake-case form.
      #
      # @raise [Mixlib::Versioning::UnknownFormatError] if the given format
      #   type doesn't exist
      #
      # @return [Class] the {Mixlib::Versioning::Format} class
      #
      def self.for(format_type)
        if format_type.is_a?(Class) &&
           format_type.ancestors.include?(Mixlib::Versioning::Format)
          format_type
        else
          case format_type.to_s
          when 'semver' then Mixlib::Versioning::Format::SemVer
          when 'opscode_semver' then Mixlib::Versioning::Format::OpscodeSemVer
          when 'git_describe' then Mixlib::Versioning::Format::GitDescribe
          when 'rubygems' then Mixlib::Versioning::Format::Rubygems
          else
            msg = "'#{format_type}' is not a supported Mixlib::Versioning format"
            fail Mixlib::Versioning::UnknownFormatError, msg
          end
        end
      end

      attr_reader :major, :minor, :patch, :prerelease, :build, :iteration, :input

      # @param version_string [String] string representation of the version
      def initialize(version_string)
        parse(version_string)
        @input = version_string
      end

      # Parses the version string splitting it into it's component version
      # identifiers for easy comparison and sorting of versions. This method
      # **MUST** be overriden by all descendants of this class.
      #
      # @param version_string [String] string representation of the version
      # @raise [Mixlib::Versioning::ParseError] raised if parsing fails
      def parse(_version_string)
        fail Error, 'You must override the #parse'
      end

      # @return [Boolean] Whether or not this is a release version
      def release?
        @prerelease.nil? && @build.nil?
      end

      # @return [Boolean] Whether or not this is a pre-release version
      def prerelease?
        !@prerelease.nil? && @build.nil?
      end

      # @return [Boolean] Whether or not this is a release build version
      def release_build?
        @prerelease.nil? && !@build.nil?
      end

      # @return [Boolean] Whether or not this is a pre-release build version
      def prerelease_build?
        !@prerelease.nil? && !@build.nil?
      end

      # @return [Boolean] Whether or not this is a build version
      def build?
        !@build.nil?
      end

      # Returns `true` if `other` and this {Format} share the same `major`,
      # `minor`, and `patch` values. Pre-release and build specifiers are not
      # taken into consideration.
      #
      # @return [Boolean]
      def in_same_release_line?(other)
        @major == other.major &&
          @minor == other.minor &&
          @patch == other.patch
      end

      # Returns `true` if `other` an share the same
      # `major`, `minor`, and `patch` values. Pre-release and build specifiers
      # are not taken into consideration.
      #
      # @return [Boolean]
      def in_same_prerelease_line?(other)
        @major == other.major &&
          @minor == other.minor &&
          @patch == other.patch &&
          @prerelease == other.prerelease
      end

      # @return [String] String representation of this {Format} instance
      def to_s
        @input
      end

      # Since the default implementation of `Object#inspect` uses `Object#to_s`
      # under the covers (which we override) we need to also override `#inspect`
      # to ensure useful debug information.
      def inspect
        vars = instance_variables.map do |n|
          "#{n}=#{instance_variable_get(n).inspect}"
        end
        format('#<%s:0x%x %s>', self.class, object_id, vars.join(', '))
      end

      # Returns SemVer compliant string representation of this {Format}
      # instance. The string returned will take on the form:
      #
      # ```text
      # MAJOR.MINOR.PATCH-PRERELEASE+BUILD
      # ```
      #
      # @return [String] SemVer compliant string representation of this
      #   {Format} instance
      # @todo create a proper serialization abstraction
      def to_semver_string
        s = [@major, @minor, @patch].join('.')
        s += "-#{@prerelease}" if @prerelease
        s += "+#{@build}" if @build
        s
      end

      # Returns Rubygems compliant string representation of this {Format}
      # instance. The string returned will take on the form:
      #
      # ```text
      # MAJOR.MINOR.PATCH.PRERELEASE
      # ```
      #
      # @return [String] Rubygems compliant string representation of this
      #   {Format} instance
      # @todo create a proper serialization abstraction
      def to_rubygems_string
        s = [@major, @minor, @patch].join('.')
        s += ".#{@prerelease}" if @prerelease
        s
      end

      # Compare this version number with the given version number, following
      # Semantic Versioning 2.0.0-rc.1 semantics.
      #
      # @param other [Mixlib::Versioning::Format]
      # @return [Integer] -1, 0, or 1 depending on whether the this version is
      #   less than, equal to, or greater than the other version
      def <=>(other)
        # Check whether the `other' is a String and if so, then get an
        # instance of *this* class (e.g., GitDescribe, OpscodeSemVer,
        # SemVer, Rubygems, etc.), so we can compare against it.
        other = self.class.new(other) if other.is_a?(String)

        # First, perform comparisons based on major, minor, and patch
        # versions.  These are always presnt and always non-nil
        maj = @major <=> other.major
        return maj unless maj == 0

        min = @minor <=> other.minor
        return min unless min == 0

        pat = @patch <=> other.patch
        return pat unless pat == 0

        # Next compare pre-release specifiers.  A pre-release sorts
        # before a release (e.g. 1.0.0-alpha.1 comes before 1.0.0), so
        # we need to take nil into account in our comparison.
        #
        # If both have pre-release specifiers, we need to compare both
        # on the basis of each component of the specifiers.
        if @prerelease && other.prerelease.nil?
          return -1
        elsif @prerelease.nil? && other.prerelease
          return 1
        elsif @prerelease && other.prerelease
          pre = compare_dot_components(@prerelease, other.prerelease)
          return pre unless pre == 0
        end

        # Build specifiers are compared like pre-release specifiers,
        # except that builds sort *after* everything else
        # (e.g. 1.0.0+build.123 comes after 1.0.0, and
        # 1.0.0-alpha.1+build.123 comes after 1.0.0-alpha.1)
        if @build.nil? && other.build
          return -1
        elsif @build && other.build.nil?
          return 1
        elsif @build && other.build
          build_ver = compare_dot_components(@build, other.build)
          return build_ver unless build_ver == 0
        end

        # Some older version formats improperly include a package iteration in
        # the version string. This is different than a build specifier and
        # valid release versions may include an iteration. We'll transparently
        # handle this case and compare iterations if it was parsed by the
        # implementation class.
        if @iteration.nil? && other.iteration
          return -1
        elsif @iteration && other.iteration.nil?
          return 1
        elsif @iteration && other.iteration
          return @iteration <=> other.iteration
        end

        # If we get down here, they're both equal
        0
      end

      # @param other [Mixlib::Versioning::Format]
      # @return [Boolean] returns true if the versions are equal, false
      #   otherwise.
      def eql?(other)
        @major == other.major &&
          @minor == other.minor &&
          @patch == other.patch &&
          @prerelease == other.prerelease &&
          @build == other.build
      end

      def hash
        [@major, @minor, @patch, @prerelease, @build].compact.join('.').hash
      end

      #########################################################################

      private

      # If a String `n` can be parsed as an Integer do so; otherwise, do
      # nothing.
      #
      # @param n [String, nil]
      # @return [Integer] the parsed {Integer}
      def maybe_int(n)
        Integer(n)
      rescue
        n
      end

      # Compares prerelease and build version component strings
      # according to SemVer 2.0.0-rc.1 semantics.
      #
      # Returns -1, 0, or 1, just like the spaceship operator (`<=>`),
      # and is used in the implemntation of `<=>` for this class.
      #
      # Pre-release and build specifiers are dot-separated strings.
      # Numeric components are sorted numerically; otherwise, sorting is
      # standard ASCII order.  Numerical components have a lower
      # precedence than string components.
      #
      # See http://www.semver.org for more.
      #
      # Both `a_item` and `b_item` should be Strings; `nil` is not a
      # valid input.
      def compare_dot_components(a_item, b_item)
        a_components = a_item.split('.')
        b_components = b_item.split('.')

        max_length = [a_components.length, b_components.length].max

        (0..(max_length - 1)).each do |i|
          # Convert the ith component into a number if possible
          a = maybe_int(a_components[i])
          b = maybe_int(b_components[i])

          # Since the components may be of differing lengths, the
          # shorter one will yield +nil+ at some point as we iterate.
          if a.nil? && !b.nil?
            # a_item was shorter
            return -1
          elsif !a.nil? && b.nil?
            # b_item was shorter
            return 1
          end

          # Now we need to compare appropriately based on type.
          #
          # Numbers have lower precedence than strings; therefore, if
          # the components are of differnt types (String vs. Integer),
          # we just return -1 for the numeric one and we're done.
          #
          # If both are the same type (Integer vs. Integer, or String
          # vs. String), we can just use the native comparison.
          #
          if a.is_a?(Integer) && b.is_a?(String)
            # a_item was "smaller"
            return -1
          elsif a.is_a?(String) && b.is_a?(Integer)
            # b_item was "smaller"
            return 1
          else
            comp = a <=> b
            return comp unless comp == 0
          end
        end # each

        # We've compared all components of both strings; if we've gotten
        # down here, they're totally the same
        0
      end
    end # Format
  end # Versioning
end # Mixlib