semverify 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Semverify
4
+ # Match a semver within a string
5
+ SEMVER_REGEXP = /
6
+ (?<semver>
7
+ (?<major>0|[1-9]\d*)
8
+ \.
9
+ (?<minor>0|[1-9]\d*)
10
+ \.
11
+ (?<patch>0|[1-9]\d*)
12
+ (?:-
13
+ (?<pre_release>
14
+ (?:
15
+ 0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*
16
+ )
17
+ (?:
18
+ \.
19
+ (?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)
20
+ )*
21
+ )
22
+ )?
23
+ (?:
24
+ \+
25
+ (?<build_metadata>
26
+ [0-9a-zA-Z-]+
27
+ (?:
28
+ \.
29
+ [0-9a-zA-Z-]+
30
+ )*
31
+ )
32
+ )?
33
+ )
34
+ /x
35
+
36
+ # Match a semver to the full string
37
+ SEMVER_REGEXP_FULL = /\A#{SEMVER_REGEXP.source}\z/x
38
+ end
@@ -0,0 +1,372 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'semverify/regexp'
4
+
5
+ module Semverify
6
+ # Parse and compare semver version strings
7
+ #
8
+ # This class will parse a semver version string that complies to Semantic
9
+ # Versioning 2.0.0.
10
+ #
11
+ # Two Semver objects can be compared using the spaceship operator (<=>)
12
+ # according to the rules of Semantic Versioning 2.0.0.
13
+ #
14
+ # @example Basic version parsing
15
+ # semver = Semverify::Semver.new('1.2.3')
16
+ # semver.major # => '1'
17
+ # semver.minor # => '2'
18
+ # semver.patch # => '3'
19
+ #
20
+ # @example Parsing a version with a pre-release identifier
21
+ # semver = Semverify::Semver.new('1.2.3-alpha.1')
22
+ # semver.pre_release # => 'alpha.1'
23
+ # semver.pre_release_identifiers # => ['alpha', '1']
24
+ #
25
+ # @example A version with build metadata
26
+ # semver = Semverify::Semver.new('1.2.3+build.1')
27
+ # semver.build_metadata # => 'build.1'
28
+ #
29
+ # @example Comparing versions
30
+ # semver1 = Semverify::Semver.new('1.2.3')
31
+ # semver2 = Semverify::Semver.new('1.2.4')
32
+ # semver1 <=> semver2 # => true
33
+ #
34
+ # See the Semantic Versioning 2.0.0 specification for more details.
35
+ #
36
+ # @see https://semver.org/spec/v2.0.0.html Semantic Versioning 2.0.0
37
+ #
38
+ # @api public
39
+ #
40
+ class Semver
41
+ include Comparable
42
+
43
+ # Create a new Semver object
44
+ #
45
+ # @example
46
+ # version = Semverify::Semver.new('1.2.3-alpha.1')
47
+ #
48
+ # @param version [String] The version string to parse
49
+ #
50
+ # @raise [Semverify::Error] version is not a string or not a valid semver version
51
+ #
52
+ def initialize(version)
53
+ assert_version_must_be_a_string(version)
54
+ @version = version
55
+ parse
56
+ assert_valid_version
57
+ end
58
+
59
+ # @!attribute version [r]
60
+ #
61
+ # The complete version string
62
+ #
63
+ # @example
64
+ # semver = Semverify::Semver.new('1.2.3-alpha.1+build.001')
65
+ # semver.version #=> '1.2.3-alpha.1+build.001'
66
+ #
67
+ # @return [String]
68
+ #
69
+ # @api public
70
+ #
71
+ attr_reader :version
72
+
73
+ # @attribute major [r]
74
+ #
75
+ # The major part of the version
76
+ #
77
+ # @example
78
+ # semver = Semverify::Semver.new('1.2.3-alpha.1+build.001')
79
+ # semver.major #=> '1'
80
+ #
81
+ # @return [String]
82
+ #
83
+ # @api public
84
+ #
85
+ attr_reader :major
86
+
87
+ # @attribute minor [r]
88
+ #
89
+ # The minor part of the version
90
+ #
91
+ # @example
92
+ # semver = Semverify::Semver.new('1.2.3-alpha.1+build.001')
93
+ # semver.minor #=> '2'
94
+ #
95
+ # @return [String]
96
+ #
97
+ # @api public
98
+ #
99
+ attr_reader :minor
100
+
101
+ # @attribute patch [r]
102
+ #
103
+ # The patch part of the version
104
+ #
105
+ # @example
106
+ # semver = Semverify::Semver.new('1.2.3-alpha.1+build.001')
107
+ # semver.patch #=> '3'
108
+ #
109
+ # @return [String]
110
+ #
111
+ # @api public
112
+ #
113
+ attr_reader :patch
114
+
115
+ # @attribute pre_release [r]
116
+ #
117
+ # The pre_release part of the version
118
+ #
119
+ # Will be an empty string if the version has no pre_release part.
120
+ #
121
+ # @example
122
+ # semver = Semverify::Semver.new('1.2.3-alpha.1+build.001')
123
+ # semver.pre_release #=> 'alpha.1'
124
+ #
125
+ # @example When the version has no pre_release part
126
+ # semver = Semverify::Semver.new('1.2.3')
127
+ # semver.pre_release #=> ''
128
+ #
129
+ # @return [String]
130
+ #
131
+ # @api public
132
+ #
133
+ attr_reader :pre_release
134
+
135
+ # @attribute pre_release_identifiers [r]
136
+ #
137
+ # The pre_release identifiers of the version
138
+ #
139
+ # @example
140
+ # semver = Semverify::Semver.new('1.2.3-alpha.1+build.001')
141
+ # semver.pre_release_identifiers #=> ['alpha', '1']
142
+ #
143
+ # @example When the version has no pre_release part
144
+ # semver = Semverify::Semver.new('1.2.3')
145
+ # semver.pre_release_identifiers #=> []
146
+ #
147
+ # @return [Array<String>]
148
+ #
149
+ # @api public
150
+ #
151
+ attr_reader :pre_release_identifiers
152
+
153
+ # @attribute build_metadata [r]
154
+ #
155
+ # The build_metadata part of the version
156
+ #
157
+ # Will be an empty string if the version has no build_metadata part.
158
+ #
159
+ # @example
160
+ # semver = Semverify::Semver.new('1.2.3-alpha.1+build.001')
161
+ # semver.build_metadata #=> 'build.001'
162
+ #
163
+ # @example When the version has no build_metadata part
164
+ # semver = Semverify::Semver.new('1.2.3')
165
+ # semver.build_metadata #=> ''
166
+ #
167
+ # @return [String]
168
+ #
169
+ # @api public
170
+ #
171
+ attr_reader :build_metadata
172
+
173
+ # Compare two Semver objects
174
+ #
175
+ # See the [Precedence Rules](https://semver.org/spec/v2.0.0.html#spec-item-11)
176
+ # in the Semantic Versioning 2.0.0 Specification for more details.
177
+ #
178
+ # @example
179
+ # semver1 = Semverify::Semver.new('1.2.3')
180
+ # semver2 = Semverify::Semver.new('1.2.4')
181
+ # semver1 <=> semver2 # => -1
182
+ # semver2 <=> semver1 # => 1
183
+ #
184
+ # @example A Semver is equal to itself
185
+ # semver1 = Semverify::Semver.new('1.2.3')
186
+ # semver1 <=> semver1 # => 0
187
+ #
188
+ # @example A pre-release version is always older than a normal version
189
+ # semver1 = Semverify::Semver.new('1.2.3-alpha.1')
190
+ # semver2 = Semverify::Semver.new('1.2.3')
191
+ # semver1 <=> semver2 # => -1
192
+ #
193
+ # @example Pre-releases are compared by the parts of the pre-release version
194
+ # semver1 = Semverify::Semver.new('1.2.3-alpha.1')
195
+ # semver2 = Semverify::Semver.new('1.2.3-alpha.2')
196
+ # semver1 <=> semver2 # => -1
197
+ #
198
+ # @example Build metadata is ignored when comparing versions
199
+ # semver1 = Semverify::Semver.new('1.2.3+build.100')
200
+ # semver2 = Semverify::Semver.new('1.2.3+build.101')
201
+ # semver1 <=> semver2 # => 0
202
+ #
203
+ # @param other [Semver] the other Semver to compare to
204
+ #
205
+ # @return [Integer] -1 if self < other, 0 if self == other, or 1 if self > other
206
+ #
207
+ # @raise [Semverify::Error] other is not a semver
208
+ #
209
+ def <=>(other)
210
+ assert_other_is_a_semver(other)
211
+
212
+ result = compare_core_parts(other)
213
+
214
+ return result unless result.zero? && pre_release != other.pre_release
215
+ return 1 if pre_release.empty?
216
+ return -1 if other.pre_release.empty?
217
+
218
+ compare_pre_release_part(other)
219
+ end
220
+
221
+ # Determine if the version string is a valid semver
222
+ #
223
+ # Override this method in a subclass to provide extra or custom validation.
224
+ #
225
+ # @example
226
+ # Semverify::Semver.new('1.2.3').valid? # => true
227
+ # Semverify::Semver.new('1.2.3-alpha.1+build.001').valid? # => true
228
+ # Semverify::Semver.new('bogus').valid? # => raises Semverify::Error
229
+ #
230
+ # @return [Boolean] true if the version string is a valid semver
231
+ #
232
+ def valid?
233
+ # If major is set, then so is everything else
234
+ !major.nil?
235
+ end
236
+
237
+ # Two versions are equal if their version strings are equal
238
+ #
239
+ # @example
240
+ # Semverify::Semver.new('1.2.3') == '1.2.3' # => true
241
+ #
242
+ # @param other [Semver] the other Semver to compare to
243
+ #
244
+ # @return [Boolean] true if the version strings are equal
245
+ #
246
+ def ==(other)
247
+ version == other.to_s
248
+ end
249
+
250
+ # The string representation of a Semver is its version string
251
+ #
252
+ # @example
253
+ # Semverify::Semver.new('1.2.3').to_s # => '1.2.3'
254
+ #
255
+ # @return [String] the version string
256
+ #
257
+ def to_s
258
+ version
259
+ end
260
+
261
+ private
262
+
263
+ # Parse @version into its parts
264
+ # @return [void]
265
+ # @api private
266
+ def parse
267
+ return unless (match_data = version.match(Semverify::SEMVER_REGEXP_FULL))
268
+
269
+ core_parts(match_data)
270
+ pre_release_part(match_data)
271
+ build_metadata_part(match_data)
272
+ end
273
+
274
+ # Compare the major, minor, and patch parts of this Semver to other
275
+ # @param other [Semver] the other Semver to compare to
276
+ # @return [Integer] -1 if self < other, 0 if self == other, or 1 if self > other
277
+ # @api private
278
+ def compare_core_parts(other)
279
+ identifiers = [major.to_i, minor.to_i, patch.to_i]
280
+ other_identifiers = [other.major.to_i, other.minor.to_i, other.patch.to_i]
281
+
282
+ identifiers <=> other_identifiers
283
+ end
284
+
285
+ # Compare two pre-release identifiers
286
+ #
287
+ # Implements the rules for precedence for comparing two pre-release identifiers
288
+ # from the Semantic Versioning 2.0.0 Specification.
289
+ #
290
+ # @param identifier [String, Integer] the identifier to compare
291
+ # @param other_identifier [String, Integer] the other identifier to compare
292
+ # @return [Integer] -1, 0, or 1
293
+ # @api private
294
+ def compare_identifiers(identifier, other_identifier)
295
+ return 1 if other_identifier.nil?
296
+ return -1 if identifier.is_a?(Integer) && other_identifier.is_a?(String)
297
+ return 1 if other_identifier.is_a?(Integer) && identifier.is_a?(String)
298
+
299
+ identifier <=> other_identifier
300
+ end
301
+
302
+ # Compare two pre-release version parts
303
+ #
304
+ # Implements the rules for precedence for comparing the pre-release part of
305
+ # one version with the pre-release part of another version from the Semantic
306
+ # Versioning 2.0.0 Specification.
307
+ #
308
+ # @param other [Semver] the other Semver to compare to
309
+ # @return [Integer] -1, 0, or 1
310
+ # @api private
311
+ def compare_pre_release_part(other)
312
+ pre_release_identifiers.zip(other.pre_release_identifiers).each do |field, other_field|
313
+ result = compare_identifiers(field, other_field)
314
+ return result unless result.zero?
315
+ end
316
+ pre_release_identifiers.size < other.pre_release_identifiers.size ? -1 : 0
317
+ end
318
+
319
+ # Raise a error if other is not a valid Semver
320
+ # @param other [Semver] the other to check
321
+ # @return [void]
322
+ # @raise [Semverify::Error] if other is not a valid Semver
323
+ # @api private
324
+ def assert_other_is_a_semver(other)
325
+ raise Semverify::Error, 'other must be a Semver' unless other.is_a?(Semver)
326
+ end
327
+
328
+ # Raise a error if the given version is not a string
329
+ # @param version [Semver] the version to check
330
+ # @return [void]
331
+ # @raise [Semverify::Error] if the given version is not a string
332
+ # @api private
333
+ def assert_version_must_be_a_string(version)
334
+ raise Semverify::Error, 'Version must be a string' unless version.is_a?(String)
335
+ end
336
+
337
+ # Raise a error if this version object is not a valid Semver
338
+ # @return [void]
339
+ # @raise [Semverify::Error] if other is not a valid Semver
340
+ # @api private
341
+ def assert_valid_version
342
+ raise Semverify::Error, "Not a valid version string: #{version}" unless valid?
343
+ end
344
+
345
+ # Set the major, minor, and patch parts of this Semver
346
+ # @param match_data [MatchData] the match data from the version string
347
+ # @return [void]
348
+ # @api private
349
+ def core_parts(match_data)
350
+ @major = match_data[:major]
351
+ @minor = match_data[:minor]
352
+ @patch = match_data[:patch]
353
+ end
354
+
355
+ # Set the pre-release of this Semver
356
+ # @param match_data [MatchData] the match data from the version string
357
+ # @return [void]
358
+ # @api private
359
+ def pre_release_part(match_data)
360
+ @pre_release = match_data[:pre_release] || ''
361
+ @pre_release_identifiers = @pre_release.split('.').map { |f| f =~ /\A\d+\z/ ? f.to_i : f }
362
+ end
363
+
364
+ # Set the build_metadata of this Semver
365
+ # @param match_data [MatchData] the match data from the version string
366
+ # @return [void]
367
+ # @api private
368
+ def build_metadata_part(match_data)
369
+ @build_metadata = match_data[:build_metadata] || ''
370
+ end
371
+ end
372
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Semverify
4
+ # The current version of this gem
5
+ VERSION = '0.3.0'
6
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Semverify
4
+ # Represents a file that contains the gem's version and can update the version
5
+ #
6
+ # Use VersionFileFactory.find create a VersionFile instance.
7
+ #
8
+ # @api public
9
+ #
10
+ class VersionFile
11
+ # Create an VersionFile instance
12
+ #
13
+ # Use VersionFileFactory.find create a VersionFile instance.
14
+ #
15
+ # @example
16
+ # version_file = Semverify::VersionFile.new('VERSION', 'VERSION = "', '1.2.3', '"')
17
+ #
18
+ # @param path [String] the path to the file relative to the current directory
19
+ # @param content_before [String] the content before the version
20
+ # @param version [String] the version
21
+ # @param content_after [String] the content after the version
22
+ #
23
+ # @raise [Semverify::Error] if the version is not an IncrementableSemver
24
+ #
25
+ # @api private
26
+ #
27
+ def initialize(path, content_before, version, content_after)
28
+ raise Semverify::Error, 'version must be an IncrementableSemver' unless
29
+ version.is_a?(Semverify::IncrementableSemver)
30
+
31
+ @path = path
32
+ @content_before = content_before
33
+ @version = version
34
+ @content_after = content_after
35
+ end
36
+
37
+ # @!attribute [r]
38
+ #
39
+ # The path to the file relative to the current directory
40
+ #
41
+ # @example
42
+ # version_file = Semverify::VersionFile.new('lib/semverify/version.rb', 'VERSION = "', '1.2.3', '"')
43
+ # version_file.path # => 'lib/semverify/version.rb'
44
+ # @return [String]
45
+ # @api public
46
+ attr_reader :path
47
+
48
+ # @!attribute [r]
49
+ #
50
+ # The content in the version file before the version
51
+ #
52
+ # @example
53
+ # version_file = Semverify::VersionFile.new('lib/semverify/version.rb', 'VERSION = "', '1.2.3', '"')
54
+ # version_file.content_before # => 'VERSION = "'
55
+ # @return [String]
56
+ # @api public
57
+ attr_reader :content_before
58
+
59
+ # @!attribute [r]
60
+ #
61
+ # The version from the version file
62
+ #
63
+ # @example
64
+ # version = Semverify::IncrementableSemver.new('1.2.3')
65
+ # version_file = Semverify::VersionFile.new('lib/semverify/version.rb', 'VERSION = "', version, '"')
66
+ # version_file.version.to_s # => '1.2.3'
67
+ # @return [Semverify::IncrementableSemver]
68
+ # @raise [Semverify::Error] if the version is not an IncrementableSemver
69
+ # @api public
70
+ attr_reader :version
71
+
72
+ # @!attribute [r]
73
+ #
74
+ # The content in the version file before the version
75
+ #
76
+ # @example
77
+ # version_file = Semverify::VersionFile.new('lib/semverify/version.rb', 'VERSION = "', '1.2.3', '"')
78
+ # version_file.content_after # => '"'
79
+ # @return [String]
80
+ # @api public
81
+ attr_reader :content_after
82
+
83
+ # Update the version in the version file
84
+ #
85
+ # @param new_version [Semverify::IncrementableSemver] the new version
86
+ # @example
87
+ # version_file = Semverify::VersionFile.new('lib/semverify/version.rb', 'VERSION = "', '1.2.3', '"')
88
+ # version_file.version = '1.2.4'
89
+ # @return [Void]
90
+ # @raise [Semverify::Error] if new_version is not an IncrementableSemver
91
+ # @api public
92
+ #
93
+ def version=(new_version)
94
+ raise Semverify::Error, 'new_version must be an IncrementableSemver' unless
95
+ new_version.is_a?(Semverify::IncrementableSemver)
96
+
97
+ @version = version
98
+ File.write(path, content_before + new_version.to_s + content_after)
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'semverify/version_file_sources'
4
+
5
+ module Semverify
6
+ # Finds the file that contains the gem's version and returns a VersionFile instance
7
+ #
8
+ # @api public
9
+ #
10
+ class VersionFileFactory
11
+ # The list of VersionFileSources to check for the version file
12
+ #
13
+ # The order of the list is important. The first VersionFileSource that finds a version file will be used.
14
+ #
15
+ VERSION_FILE_SOURCES = [
16
+ Semverify::VersionFileSources::Version,
17
+ Semverify::VersionFileSources::VersionRb,
18
+ Semverify::VersionFileSources::Gemspec
19
+ ].freeze
20
+
21
+ # Finds the version file for the gem
22
+ #
23
+ # @example
24
+ # version_file = Semverify::VersionFileFactory.find
25
+ # version_file.path # => 'lib/semverify/version.rb'
26
+ # version_file.version # => '1.2.3'
27
+ #
28
+ # @return [Semverify::VersionFile, nil] the version file or nil if no version file was found
29
+ #
30
+ def self.find
31
+ VERSION_FILE_SOURCES.each do |version_file_source|
32
+ version_file = version_file_source.find
33
+ return version_file if version_file
34
+ end
35
+ nil
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'semverify/version_file'
4
+
5
+ module Semverify
6
+ module VersionFileSources
7
+ # Base class for a version file source which implements #find
8
+ #
9
+ # @api public
10
+ #
11
+ class Base
12
+ # The first file from #glob whose content matches #content_regexp
13
+ #
14
+ # @example
15
+ # version_file = Semverify::VersionFileSources::Gemspec.find
16
+ #
17
+ # @return [Semverify::VersionFile, nil] the version file or nil if no version file was found
18
+ #
19
+ def self.find
20
+ Dir[glob].filter_map do |path|
21
+ if (match = File.read(path).match(content_regexp))
22
+ version = Semverify::IncrementableSemver.new(match[:version])
23
+ Semverify::VersionFile.new(path, match[:content_before], version, match[:content_after])
24
+ end
25
+ end.first
26
+ end
27
+
28
+ private
29
+
30
+ # The version file regexp
31
+ #
32
+ # A regular expression that matches the version file and has three named captures:
33
+ # - content_before: the content before the version
34
+ # - version: the version
35
+ # - content_after: the content after the version
36
+ #
37
+ # @return [Regexp]
38
+ # @api private
39
+ private_class_method def self.content_regexp
40
+ raise NotImplementedError, 'You must implement #content_regexp in a subclass'
41
+ end
42
+
43
+ # A glob that matches potential version files
44
+ #
45
+ # Files matching this glob will be checked to see if they match #version_file_regexp
46
+ #
47
+ # @return [String]
48
+ # @api private
49
+ private_class_method def self.glob
50
+ raise NotImplementedError, 'You must implement #glob in a subclass'
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'semverify/version_file_sources/base'
4
+
5
+ module Semverify
6
+ module VersionFileSources
7
+ # Checks for the gem's version in a file named *.gemspec
8
+ #
9
+ # @api public
10
+ #
11
+ class Gemspec < Base
12
+ # The regexp to find the version and surrounding content within the gemspec
13
+ VERSION_REGEXP = /
14
+ \A
15
+ (?<content_before>
16
+ .*
17
+ \.version\s*=\s*(?<quote>['"])
18
+ )
19
+ (?<version>#{Semverify::SEMVER_REGEXP.source})
20
+ (?<content_after>\k<quote>.*)
21
+ \z
22
+ /xm
23
+
24
+ private
25
+
26
+ # The version file regexp
27
+ # @return [Regexp]
28
+ # @api private
29
+ private_class_method def self.content_regexp = VERSION_REGEXP
30
+
31
+ # A glob that matches potential version files
32
+ # @return [String]
33
+ # @api private
34
+ private_class_method def self.glob = '*.gemspec'
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'semverify/version_file_sources/base'
4
+
5
+ module Semverify
6
+ module VersionFileSources
7
+ # Checks for the gem's version in a file named VERSION
8
+ #
9
+ # @api public
10
+ #
11
+ class Version < Base
12
+ # The regexp to find the version and surrounding content within the version file
13
+ VERSION_REGEXP = /
14
+ \A
15
+ (?<content_before>\s*)
16
+ (?<version>#{Semverify::SEMVER_REGEXP.source})
17
+ (?<content_after>\s*)
18
+ \z
19
+ /x
20
+
21
+ private
22
+
23
+ # The version file regexp
24
+ # @return [Regexp]
25
+ # @api private
26
+ private_class_method def self.content_regexp = VERSION_REGEXP
27
+
28
+ # A glob that matches potential version files
29
+ # @return [String]
30
+ # @api private
31
+ private_class_method def self.glob = 'VERSION'
32
+ end
33
+ end
34
+ end