File: semver2.rb

package info (click to toggle)
ruby-semver-dialects 3.4.0-2
  • links: PTS, VCS
  • area: main
  • in suites: sid, trixie
  • size: 156 kB
  • sloc: ruby: 1,537; makefile: 4
file content (159 lines) | stat: -rw-r--r-- 4,526 bytes parent folder | download
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
# frozen_string_literal: true

require 'strscan'

module SemverDialects
  module Semver2
    # Represents a token that matches any major, minor, or patch number.
    ANY_NUMBER = 'x'

    class Version < BaseVersion
      def initialize(tokens, prerelease_tag: nil)
        @tokens = tokens
        @addition = prerelease_tag
      end

      def <=>(other)
        if (idx = tokens.index(ANY_NUMBER))
          a = tokens[0..(idx - 1)]
          b = other.tokens[0..(idx - 1)]
          return compare_tokens(a, b)
        end

        if (idx = other.tokens.index(ANY_NUMBER))
          a = tokens[0..(idx - 1)]
          b = other.tokens[0..(idx - 1)]
          return compare_tokens(a, b)
        end

        super
      end

      private

      # Compares pre-release tags as specified in https://semver.org/#spec-item-9.
      def compare_additions(a, b) # rubocop:disable Naming/MethodParameterName
        #  Pre-release versions have a lower precedence than the associated normal version.
        return -1 if !a.nil? && b.nil? # only self is a pre-release
        return 1 if a.nil? && !b.nil? # only other is a pre-release

        a <=> b
      end
    end

    class PrereleaseTag < BaseVersion
      def initialize(tokens)
        @tokens = tokens
      end

      # Returns true if the prerelease tag is empty.
      # In Semver 2 1.2.3-0 is NOT equivalent to 1.2.3.
      def is_zero?
        tokens.empty?
      end

      private

      # Compares pre-release identifiers as specified in https://semver.org/#spec-item-11.
      def compare_token_pair(a, b) # rubocop:disable Naming/MethodParameterName
        case a
        when Integer
          case b
          when String
            # Numeric identifiers always have lower precedence than non-numeric identifiers.
            return -1
          when nil
            # A larger set of pre-release fields has a higher precedence than a smaller set, if all of the preceding identifiers are equal.
            return 1
          end
        when String
          case b
          when Integer
            # Numeric identifiers always have lower precedence than non-numeric identifiers.
            return 1
          when nil
            # A larger set of pre-release fields has a higher precedence than a smaller set, if all of the preceding identifiers are equal.
            return 1
          end
        when nil
          case b
          when Integer
            # A larger set of pre-release fields has a higher precedence than a smaller set, if all of the preceding identifiers are equal.
            return -1
          when String
            # A larger set of pre-release fields has a higher precedence than a smaller set, if all of the preceding identifiers are equal.
            return -1
          end
        end
        # Identifiers have both the same type (numeric or non-numeric).
        # This returns nil if the identifiers can't be compared.
        a <=> b
      end
    end

    class VersionParser
      def self.parse(input)
        new(input).parse
      end

      attr_reader :input

      def initialize(input)
        @input = input
        @scanner = StringScanner.new(input)
      end

      def parse
        tokens = []
        prerelease_tag = nil

        # skip ignore leading v if any
        scanner.skip('v')

        until scanner.eos?
          if (s = scanner.scan(/\d+/))
            tokens << s.to_i
          elsif (s = scanner.scan(/\.x\z/i))
            tokens << ANY_NUMBER
          elsif (s = scanner.scan('.'))
            # continue
          elsif (s = scanner.scan('-'))
            prerelease_tag = parse_prerelease_tag
          elsif (s = scanner.scan(/\+.*/))
            # continue
          else
            raise IncompleteScanError, scanner.rest
          end
        end

        Version.new(tokens, prerelease_tag: prerelease_tag)
      end

      private

      attr_reader :scanner

      def parse_prerelease_tag
        tokens = []
        at_build_tag = false

        until scanner.eos? || at_build_tag
          if (s = scanner.scan(/\d+(?![a-zA-Z-])/))
            tokens << s.to_i
          elsif (s = scanner.scan(/[0-9a-zA-Z-]+/))
            tokens << s
          elsif (s = scanner.scan('.'))
            # continue
          elsif (s = scanner.scan('+'))
            scanner.unscan
            at_build_tag = true
          else
            raise IncompleteScanError, scanner.rest
          end
        end

        PrereleaseTag.new(tokens)
      end
    end
  end
end