semverify 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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