qb 0.3.7 → 0.3.8

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a666712edbad3cabb581a2f91202b3d25448d9df
4
- data.tar.gz: 0f396ef64b4ec6986705ba766ec8274e8c73c43f
3
+ metadata.gz: fbc3b61137b1ac315d4a59b946216ffc49a698c5
4
+ data.tar.gz: 382fa7a1f0d99d90adfef8421811464811ecb855
5
5
  SHA512:
6
- metadata.gz: cd24c18f6efb9a5725062494fc7d50336380a6bd9f17808db5dfe12c8b2d9ac1dfb03e9ba1a114509a3c3f6846d8549129a1d8b7bb54dc14981ab31bc5d59b0d
7
- data.tar.gz: 0d1b2fd63fa86988cb3aff0deed0103a18b328f1f39c4906775b98443d1e9adb0028c8586c052cab90a4b168fd0ecced313cbc822e7955f0f7a1812fae8c6572
6
+ metadata.gz: 2399c2e4af9def73a5589e91a005aff11085663fe388d16010f2eb1684b36073faed4c778958056a4b63f84e107e810b43141723b01c18980e97805a301f8808
7
+ data.tar.gz: ef7ef9dd5b12cd9c8aab404a5ede52e5afdb642b1a4205c8f1a5fdc0ea45aa60aee2f9ef7c3bcbdb11177a37739804635aeb550a13b74da2a18ae455481449bf
data/lib/qb.rb CHANGED
@@ -3,7 +3,7 @@ require 'nrser/extras'
3
3
  require_relative './qb/errors'
4
4
  require_relative './qb/version'
5
5
  require_relative './qb/util'
6
- require_relative './qb/ansible_module'
6
+ require_relative './qb/path'
7
7
 
8
8
  module QB
9
9
  ROOT = (Pathname.new(__FILE__).dirname + '..').expand_path
@@ -38,6 +38,12 @@ end
38
38
  # needs QB::*_ROLES_DIR
39
39
  require 'qb/role'
40
40
  require 'qb/options'
41
- require_relative './qb/repo'
42
- require_relative './qb/cli'
43
- require_relative './qb/ansible'
41
+ require 'qb/repo'
42
+ require 'qb/cli'
43
+
44
+ require 'qb/ansible'
45
+ # Depreciated namespace:
46
+ require 'qb/ansible_module'
47
+
48
+ require 'qb/package'
49
+
@@ -0,0 +1,102 @@
1
+ # Requirements
2
+ # =======================================================================
3
+
4
+ # Stdlib
5
+ # -----------------------------------------------------------------------
6
+
7
+ # Deps
8
+ # -----------------------------------------------------------------------
9
+
10
+ # Project / Package
11
+ # -----------------------------------------------------------------------
12
+
13
+ require 'qb/util/resource'
14
+
15
+ require_relative './package/version'
16
+
17
+
18
+ # Refinements
19
+ # =======================================================================
20
+
21
+ require 'nrser/refinements'
22
+ using NRSER
23
+
24
+ require 'nrser/refinements/types'
25
+ using NRSER::Types
26
+
27
+
28
+ # Declarations
29
+ # =======================================================================
30
+
31
+ module QB; end
32
+
33
+
34
+ # Definitions
35
+ # =======================================================================
36
+
37
+ # Common properties and methods of package resources, aimed at packages
38
+ # represented as directories in projects.
39
+ #
40
+ class QB::Package < QB::Util::Resource
41
+
42
+ # Constants
43
+ # ======================================================================
44
+
45
+
46
+ # Class Methods
47
+ # ======================================================================
48
+
49
+
50
+ # Properties
51
+ # ======================================================================
52
+
53
+ # @!attribute [r] ref_path
54
+ # User-provided path value used to construct the resource instance, if any.
55
+ #
56
+ # This may not be the same as a root path for the resource, such as with
57
+ # resource classes that can be constructed from any path *inside* the
58
+ # directory, like a {QB::Repo::Git}.
59
+ #
60
+ # @return [String | Pathname]
61
+ # If the resource instance was constructed based on a path argument.
62
+ #
63
+ # @return [nil]
64
+ # If the resource instance was *not* constructed based on a path
65
+ # argument.
66
+ #
67
+ prop :ref_path, type: t.maybe( t.dir_path )
68
+
69
+
70
+ # @!attribute [r] root_path
71
+ # Absolute path to the gem's root directory.
72
+ #
73
+ # @return [Pathname]
74
+ #
75
+ prop :root_path, type: t.dir_path
76
+
77
+
78
+ # @!attribute [r] version
79
+ # Version of the package.
80
+ #
81
+ # @return [QB::Package::Version]
82
+ #
83
+ prop :version, type: QB::Package::Version
84
+
85
+
86
+ # @!attribute [r] name
87
+ # The string name the package goes by.
88
+ #
89
+ # @return [String]
90
+ # Non-empty string.
91
+ #
92
+ prop :name, type: t.non_empty_str
93
+
94
+
95
+ end # class QB::Package
96
+
97
+
98
+ # Post-Processing
99
+ # =======================================================================
100
+
101
+ require_relative './package/gem'
102
+
@@ -0,0 +1,153 @@
1
+ # Requirements
2
+ # =======================================================================
3
+
4
+ # Stdlib
5
+ # -----------------------------------------------------------------------
6
+
7
+ # Deps
8
+ # -----------------------------------------------------------------------
9
+
10
+ # Project / Package
11
+ # -----------------------------------------------------------------------
12
+ require 'qb/package'
13
+
14
+
15
+ # Refinements
16
+ # =======================================================================
17
+
18
+ require 'nrser/refinements'
19
+ using NRSER
20
+
21
+ require 'nrser/refinements/types'
22
+ using NRSER::Types
23
+
24
+
25
+ # Definitions
26
+ # =======================================================================
27
+
28
+ # Package resource for a Ruby Gem.
29
+ #
30
+ class QB::Package::Gem < QB::Package
31
+
32
+ # Constants
33
+ # ======================================================================
34
+
35
+
36
+ # Eigenclass (Singleton Class)
37
+ # ========================================================================
38
+ #
39
+ class << self
40
+
41
+ # Find the only `*.gemspec` path in the `root_path` Gem directory.
42
+ #
43
+ # @param [String | Pathname] root_path
44
+ # Path to the gem's root directory.
45
+ #
46
+ def gemspec_path root_path
47
+ paths = Pathname.glob( root_path.to_pn / '*.gemspec' )
48
+
49
+ case paths.length
50
+ when 0
51
+ nil
52
+ when 1
53
+ paths[0]
54
+ else
55
+ nil
56
+ end
57
+ end # #gemspec_path
58
+
59
+
60
+ # @todo Document from_path method.
61
+ #
62
+ # @param [String | Pathname] path
63
+ # Path to gem root directory.
64
+ #
65
+ # @return [QB::Package::Gem]
66
+ # If `path` is the root directory of a Ruby gem.
67
+ #
68
+ # @return [nil]
69
+ # If `path` is not the root directory of a Ruby gem.
70
+ #
71
+ # @raise [QB::FSStateError]
72
+ # If `path` is not a directory.
73
+ #
74
+ def from_root_path path
75
+ # Values we will use to construct the resource instance.
76
+ values = {}
77
+
78
+ # Whatever we were passes is the reference path
79
+ values[:ref_path] = path
80
+
81
+ # Cast to {Pathname} if it's not already and expand it to create the
82
+ # root path
83
+ values[:root_path] = path.to_pn.expand_path
84
+
85
+ # Check that we're working with a directory, returning `nil` if we're not
86
+ return nil unless values[:root_path].directory?
87
+
88
+ # Get the path to the (single) Gemspec file.
89
+ values[:gemspec_path] = self.gemspec_path values[:root_path]
90
+
91
+ # Check that we got it, returning `nil` if we don't
92
+ return nil if values[:gemspec_path].nil?
93
+
94
+ # Load up the gemspec ad version
95
+ values[:spec] = ::Gem::Specification::load values[:gemspec_path].to_s
96
+
97
+ # Get the name from the spec
98
+ values[:name] = values[:spec].name
99
+
100
+ # Get the version from the spec
101
+ values[:version] = QB::Package::Version.from_gem_version \
102
+ values[:spec].version
103
+
104
+ # Construct the resource instance and return it.
105
+ new **values
106
+ end # #from_root_path
107
+
108
+
109
+ # Like {.from_root_path} but raises an error if the path is not a gem
110
+ # root directory.
111
+ #
112
+ # @param path see .from_root_path
113
+ #
114
+ # @return [QB::Package::Gem]
115
+ #
116
+ # @raise [QB::FSStateError]
117
+ # - If `path` is not a directory.
118
+ #
119
+ # - If `path` is not a Gem directory.
120
+ #
121
+ def from_root_path! path
122
+ from_root_path( path ).tap { |gem|
123
+ if gem.nil?
124
+ raise QB::FSStateError.squished <<-END
125
+ Path #{ path.inspect } does not appear to be the root directory
126
+ of a Ruby gem.
127
+ END
128
+ end
129
+ }
130
+ end # #from_root_path!
131
+
132
+
133
+
134
+ end # class << self (Eigenclass)
135
+
136
+
137
+ # Properties
138
+ # =====================================================================
139
+
140
+ # Principle Properties
141
+ # ---------------------------------------------------------------------
142
+
143
+ prop :gemspec_path,
144
+ type: t.file_path
145
+
146
+ prop :spec,
147
+ type: ::Gem::Specification
148
+
149
+
150
+ # Instance Methods
151
+ # ======================================================================
152
+
153
+ end # class QB::Package::Gem
@@ -1,434 +1,457 @@
1
+ # Requirements
2
+ # =======================================================================
3
+
4
+ # Stdlib
5
+ # -----------------------------------------------------------------------
1
6
  require 'time'
2
7
 
3
- require 'nrser/refinements/types'
8
+ # Deps
9
+ # -----------------------------------------------------------------------
10
+
11
+ # Project / Package
12
+ # -----------------------------------------------------------------------
13
+ require 'qb/util/docker_mixin'
14
+ require 'qb/util/resource'
15
+
16
+
17
+ # Refinements
18
+ # =======================================================================
4
19
 
20
+ require 'nrser/refinements/types'
5
21
  using NRSER::Types
6
22
 
7
- require 'qb/util/docker_mixin'
8
23
 
9
- module QB
10
- module Package
11
- # An attempt to unify NPM and Gem version schemes to a reasonable extend,
12
- # and hopefully cover whatever else the cat may drag in.
13
- #
14
- # Intended to be immutable for practical purposes.
15
- #
16
- class Version < NRSER::Meta::Props::Base
17
-
18
- # Mixins
19
- # =====================================================================
20
-
21
- include QB::Util::DockerMixin
22
-
23
-
24
- # Constants
25
- # =====================================================================
26
-
27
- NUMBER_SEGMENT = t.non_neg_int
28
- NAME_SEGMENT = t.str
29
- MIXED_SEGMENT = t.union NUMBER_SEGMENT, NAME_SEGMENT
30
-
31
-
32
- # Props
33
- # =====================================================================
24
+ # Declarations
25
+ # =======================================================================
34
26
 
35
- prop :raw, type: t.maybe(t.str), default: nil
36
- prop :major, type: NUMBER_SEGMENT
37
- prop :minor, type: NUMBER_SEGMENT, default: 0
38
- prop :patch, type: NUMBER_SEGMENT, default: 0
39
- prop :prerelease, type: t.array(MIXED_SEGMENT), default: []
40
- prop :build, type: t.array(MIXED_SEGMENT), default: []
27
+ module QB; end
28
+ class QB::Package < QB::Util::Resource; end
41
29
 
42
- prop :release, type: t.str, source: :@release
43
- prop :level, type: t.str, source: :@level
44
- prop :is_release, type: t.bool, source: :release?
45
- prop :is_prerelease, type: t.bool, source: :prerelease?
46
- prop :is_build, type: t.bool, source: :build?
47
- prop :is_dev, type: t.bool, source: :dev?
48
- prop :is_rc, type: t.bool, source: :rc?
49
- prop :has_level, type: t.bool, source: :level?
50
- prop :semver, type: t.str, source: :semver
51
- prop :docker_tag, type: t.str, source: :docker_tag
52
- prop :build_commit, type: t.maybe(t.str), source: :build_commit
53
- prop :is_build_dirty, type: t.maybe(t.bool), source: :build_dirty?
54
30
 
31
+ # Definitions
32
+ # =======================================================================
55
33
 
56
- # Attributes
57
- # =====================================================================
34
+ # An attempt to unify NPM and Gem version schemes to a reasonable extend,
35
+ # and hopefully cover whatever else the cat may drag in.
36
+ #
37
+ # Intended to be immutable for practical purposes.
38
+ #
39
+ class QB::Package::Version < QB::Util::Resource
40
+
41
+ # Mixins
42
+ # =====================================================================
43
+
44
+ include QB::Util::DockerMixin
45
+
46
+
47
+ # Constants
48
+ # =====================================================================
49
+
50
+ NUMBER_SEGMENT = t.non_neg_int
51
+ NAME_SEGMENT = t.str
52
+ MIXED_SEGMENT = t.union NUMBER_SEGMENT, NAME_SEGMENT
53
+
54
+
55
+ # Props
56
+ # =====================================================================
58
57
 
59
- attr_reader :release,
60
- :level
61
-
62
-
63
- # Class Methods
64
- # =====================================================================
65
-
66
- # Utilities
67
- # ---------------------------------------------------------------------
68
-
69
- # @return [String]
70
- # Time formatted to be stuck in a version segment per Semver spec.
71
- # We also strip out '-' to avoid possible parsing weirdness.
72
- def self.to_time_segment time
73
- time.utc.iso8601.gsub /[^0-9A-Za-z]/, ''
74
- end
75
-
76
-
77
- # Instance Builders
78
- # ---------------------------------------------------------------------
79
-
80
- # Create a Version instance from a Gem::Version
81
- def self.from_gem_version version
82
- # release segments are everything before a string
83
- release_segments = version.segments.take_while { |seg|
84
- !seg.is_a?(String)
85
- }
86
-
87
- # We don't support > 3 release segments to make life somewhat
88
- # reasonable. Yeah, I think I've seen projects do it. We'll cross that
89
- # bridge if and when we get to it.
90
- if release_segments.length > 3
91
- raise ArgumentError,
92
- "We don't handle releases with more than 3 segments " +
93
- "(found #{ release_segments.inspect } in #{ version })"
94
- end
95
-
96
- prerelease_segments = version.segments[release_segments.length..-1]
97
-
98
- new raw: version.to_s,
99
- major: release_segments[0] || 0,
100
- minor: release_segments[1] || 0,
101
- patch: release_segments[2] || 0,
102
- prerelease: prerelease_segments,
103
- build: []
104
- end
105
-
106
- def self.from_npm_version version
107
- stmt = NRSER.squish <<-END
108
- var Semver = require('semver');
109
-
110
- console.log(
111
- JSON.stringify(
112
- Semver(#{ JSON.dump version })
113
- )
114
- );
115
- END
116
-
117
- parse = JSON.load Cmds.new(
118
- "node --eval %s", args: [stmt], chdir: QB::ROOT
119
- ).out!
120
-
121
- new raw: version,
122
- major: parse['major'],
123
- minor: parse['minor'],
124
- patch: parse['patch'],
125
- prerelease: parse['prerelease'],
126
- build: parse['build']
127
- end
128
-
129
-
130
- # Parse Docker image tag version into a string. Reverse of
131
- # {QB::Package::Version#docker_tag}.
132
- #
133
- # @param [String] version
134
- # String version to parse.
135
- #
136
- # @return [QB::Package::Version]
137
- #
138
- def self.from_docker_tag version
139
- from_string(version.gsub('_', '+')).merge raw: version
140
- end # .from_docker_tag
141
-
142
-
143
-
144
- # Parse string version into an instance. Accept Semver, Ruby Gem and
145
- # Docker image tag formats.
146
- #
147
- # @param [String]
148
- # String version to parse.
149
- #
150
- # @return [QB::Package::Version]
151
- #
152
- def self.from_string string
153
- if string.include? '_'
154
- self.from_docker_tag string
155
- elsif string.include?( '-' ) || string.include?( '+' )
156
- self.from_npm_version string
157
- else
158
- self.from_gem_version Gem::Version.new(string)
159
- end
160
- end
161
-
162
-
163
- # Constructor
164
- # =====================================================================
165
-
166
- # Construct a new Version
167
- def initialize **values
168
- super **values
169
-
170
- @release = [major, minor, patch].join '.'
171
-
172
- @level = t.match prerelease[0], {
173
- t.is(nil) => ->(_) {
174
- if build.empty?
175
- 'release'
176
- end
177
- },
178
-
179
- NAME_SEGMENT => ->(str) { str },
180
-
181
- NUMBER_SEGMENT => ->(int) { nil },
182
- }
183
- end
184
-
185
-
186
- # Instance Methods
187
- # =====================================================================
188
-
189
- # Tests
190
- # ---------------------------------------------------------------------
191
-
192
- # @return [Boolean]
193
- # True if this version is a release (no prerelease or build values).
194
- #
195
- def release?
196
- prerelease.empty? && build.empty?
197
- end
198
-
199
-
200
- # @return [Boolean]
201
- # True if any prerelease segments are present (stuff after '-' in
202
- # SemVer / "NPM" format, or the first string segment and anything
203
- # following it in "Gem" format). Tests if {@prerelease} is not
204
- # empty.
205
- #
206
- def prerelease?
207
- !prerelease.empty?
208
- end
209
-
210
-
211
- # @return [Boolean]
212
- # True if any build segments are present (stuff after '+' character
213
- # in SemVer / "NPM" format). Tests if {@build} is empty.
214
- #
215
- # As of writing, we don't have a way to convey build segments in
216
- # "Gem" version format, so this will always be false when loading a
217
- # Gem version.
218
- #
219
- def build?
220
- !build.empty?
221
- end
222
-
223
-
224
-
225
- # @todo Document build_dirty? method.
226
- #
227
- # @param [type] arg_name
228
- # @todo Add name param description.
229
- #
230
- # @return [return_type]
231
- # @todo Document return value.
232
- #
233
- def build_dirty?
234
- if build?
235
- build.include? 'dirty'
236
- end
237
- end # #build_dirty?
238
-
239
-
240
-
241
- # @return [Boolean]
242
- # True if self is a prerelease version that starts with a string that
243
- # we consider the 'level'.
244
- #
245
- def level?
246
- !level.nil?
247
- end
248
-
249
-
250
- # @return [Boolean]
251
- # True if this version is a dev prerelease (first prerelease element
252
- # is 'dev').
253
- #
254
- def dev?
255
- level == 'dev'
256
- end
257
-
258
-
259
- # @return [Boolean]
260
- # True if this version is a release candidate (first prerelease element
261
- # is 'rc').
262
- #
263
- def rc?
264
- level == 'rc'
265
- end
266
-
267
-
268
- # Derived Properties
269
- # ---------------------------------------------------------------------
270
-
271
- # @return [String]
272
- # The Semver version string
273
- # (`Major.minor.patch-prerelease+build` format).
274
- #
275
- def semver
276
- result = release
277
-
278
- unless prerelease.empty?
279
- result += "-#{ prerelease.join '.' }"
280
- end
281
-
282
- unless build.empty?
283
- result += "+#{ build.join '.' }"
284
- end
285
-
286
- result
287
- end # #semver
288
-
289
- alias_method :normalized, :semver
290
-
291
-
292
- # @todo Document commit method.
293
- #
294
- # @param [type] arg_name
295
- # @todo Add name param description.
296
- #
297
- # @return [return_type]
298
- # @todo Document return value.
299
- #
300
- def build_commit
301
- if build?
302
- build.find { |seg| seg =~ /[0-9a-f]{7}/ }
303
- end
304
- end # #commit
305
-
306
-
307
- # Docker image tag for the version.
308
- #
309
- # See {QB::Util::DockerMixin::ClassMethods#to_docker_tag}.
310
- #
311
- # @return [String]
312
- #
313
- def docker_tag
314
- self.class.to_docker_tag semver
315
- end # #docker_tag
316
-
317
-
318
- # Related Versions
319
- # ---------------------------------------------------------------------
320
- #
321
- # Functions that construct new version instances based on the current
322
- # one as well as additional information provided.
323
- #
324
-
325
- # @return [QB::Package::Version]
326
- # A new {QB::Package::Version} created from {#release}. Even if `self`
327
- # *is* a release version already, still returns a new instance.
328
- #
329
- def release_version
330
- self.class.from_string release
331
- end # #release_version
332
-
333
-
334
- # Return a new {QB::Package::Version} with build information added.
335
- #
336
- # @return [QB::Package::Version]
337
- #
338
- def build_version branch: nil, ref: nil, time: nil, dirty: nil
339
- time = self.class.to_time_segment(time) unless time.nil?
340
-
341
- segments = [
342
- branch,
343
- ref,
344
- ('dirty' if dirty),
345
- time,
346
- ].reject &:nil?
347
-
348
- if segments.empty?
349
- raise ArgumentError,
350
- "Need to provide at least one of branch, ref, time."
58
+ prop :raw, type: t.maybe(t.str), default: nil
59
+ prop :major, type: NUMBER_SEGMENT
60
+ prop :minor, type: NUMBER_SEGMENT, default: 0
61
+ prop :patch, type: NUMBER_SEGMENT, default: 0
62
+ prop :prerelease, type: t.array(MIXED_SEGMENT), default: []
63
+ prop :build, type: t.array(MIXED_SEGMENT), default: []
64
+
65
+ prop :release, type: t.str, source: :@release
66
+ prop :level, type: t.str, source: :@level
67
+ prop :is_release, type: t.bool, source: :release?
68
+ prop :is_prerelease, type: t.bool, source: :prerelease?
69
+ prop :is_build, type: t.bool, source: :build?
70
+ prop :is_dev, type: t.bool, source: :dev?
71
+ prop :is_rc, type: t.bool, source: :rc?
72
+ prop :has_level, type: t.bool, source: :level?
73
+ prop :semver, type: t.str, source: :semver
74
+ prop :docker_tag, type: t.str, source: :docker_tag
75
+ prop :build_commit, type: t.maybe(t.str), source: :build_commit
76
+ prop :is_build_dirty, type: t.maybe(t.bool), source: :build_dirty?
77
+
78
+
79
+ # Attributes
80
+ # =====================================================================
81
+
82
+ attr_reader :release,
83
+ :level
84
+
85
+
86
+ # Class Methods
87
+ # =====================================================================
88
+
89
+ # Utilities
90
+ # ---------------------------------------------------------------------
91
+
92
+ # @return [String]
93
+ # Time formatted to be stuck in a version segment per Semver spec.
94
+ # We also strip out '-' to avoid possible parsing weirdness.
95
+ def self.to_time_segment time
96
+ time.utc.iso8601.gsub /[^0-9A-Za-z]/, ''
97
+ end
98
+
99
+
100
+ # Instance Builders
101
+ # ---------------------------------------------------------------------
102
+
103
+ # Create a Version instance from a {Gem::Version}.
104
+ #
105
+ # @param [Gem::Version] version
106
+ #
107
+ # @return [QB::Package::Version]
108
+ #
109
+ def self.from_gem_version version
110
+ # release segments are everything before a string
111
+ release_segments = version.segments.take_while { |seg|
112
+ !seg.is_a?(String)
113
+ }
114
+
115
+ # We don't support > 3 release segments to make life somewhat
116
+ # reasonable. Yeah, I think I've seen projects do it. We'll cross that
117
+ # bridge if and when we get to it.
118
+ if release_segments.length > 3
119
+ raise ArgumentError,
120
+ "We don't handle releases with more than 3 segments " +
121
+ "(found #{ release_segments.inspect } in #{ version })"
122
+ end
123
+
124
+ prerelease_segments = version.segments[release_segments.length..-1]
125
+
126
+ new raw: version.to_s,
127
+ major: release_segments[0] || 0,
128
+ minor: release_segments[1] || 0,
129
+ patch: release_segments[2] || 0,
130
+ prerelease: prerelease_segments,
131
+ build: []
132
+ end
133
+
134
+ def self.from_npm_version version
135
+ stmt = NRSER.squish <<-END
136
+ var Semver = require('semver');
137
+
138
+ console.log(
139
+ JSON.stringify(
140
+ Semver(#{ JSON.dump version })
141
+ )
142
+ );
143
+ END
144
+
145
+ parse = JSON.load Cmds.new(
146
+ "node --eval %s", args: [stmt], chdir: QB::ROOT
147
+ ).out!
148
+
149
+ new raw: version,
150
+ major: parse['major'],
151
+ minor: parse['minor'],
152
+ patch: parse['patch'],
153
+ prerelease: parse['prerelease'],
154
+ build: parse['build']
155
+ end
156
+
157
+
158
+ # Parse Docker image tag version into a string. Reverse of
159
+ # {QB::Package::Version#docker_tag}.
160
+ #
161
+ # @param [String] version
162
+ # String version to parse.
163
+ #
164
+ # @return [QB::Package::Version]
165
+ #
166
+ def self.from_docker_tag version
167
+ from_string(version.gsub('_', '+')).merge raw: version
168
+ end # .from_docker_tag
169
+
170
+
171
+ # Parse string version into an instance. Accept Semver, Ruby Gem and
172
+ # Docker image tag formats.
173
+ #
174
+ # @param [String]
175
+ # String version to parse.
176
+ #
177
+ # @return [QB::Package::Version]
178
+ #
179
+ def self.from_string string
180
+ if string.include? '_'
181
+ self.from_docker_tag string
182
+ elsif string.include?( '-' ) || string.include?( '+' )
183
+ self.from_npm_version string
184
+ else
185
+ self.from_gem_version Gem::Version.new(string)
186
+ end
187
+ end
188
+
189
+
190
+ # Constructor
191
+ # =====================================================================
192
+
193
+ # Construct a new Version
194
+ def initialize **values
195
+ super **values
196
+
197
+ @release = [major, minor, patch].join '.'
198
+
199
+ @level = t.match prerelease[0], {
200
+ t.is(nil) => ->(_) {
201
+ if build.empty?
202
+ 'release'
351
203
  end
352
-
353
- merge raw: nil, build: segments
354
- end
355
-
356
-
357
- # @return [QB::Package::Version]
358
- # A new {QB::Package::Version} created from {#release} and
359
- # {#prerelease} data, but without any build information.
360
- #
361
- def prerelease_version
362
- merge raw: nil, build: []
363
- end # #prerelease_version
364
-
365
-
366
-
367
- # Language Interface
368
- # =====================================================================
369
-
370
- # Test for equality.
371
- #
372
- # Compares classes then {QB::Package::Version#to_a} results.
373
- #
374
- # @param [Object] other
375
- # Object to compare to self.
376
- #
377
- # @return [Boolean]
378
- # True if self and other are considered equal.
379
- #
380
- def == other
381
- other.class == self.class &&
382
- other.to_a == self.to_a
383
- end # #==
384
-
385
-
386
- # Return array of the version elements in order from greatest to least
387
- # precedence.
388
- #
389
- # This is considered the representative structure for the object's data,
390
- # from which all other values are dependently derived, and is used in
391
- # {#==}, {#hash} and {#eql?}.
392
- #
393
- # @example
394
- #
395
- # version = QB::Package::Version.from_string(
396
- # "0.1.2-rc.10+master.0ab1c3d"
397
- # )
398
- #
399
- # version.to_a
400
- # # => [0, 1, 2, ['rc', 10], ['master', '0ab1c3d']]
401
- #
402
- # QB::Package::Version.from_string('1').to_a
403
- # # => [1, nil, nil, [], []]
404
- #
405
- # @return [Array]
406
- #
407
- def to_a
408
- [
409
- major,
410
- minor,
411
- patch,
412
- prerelease,
413
- build,
414
- ]
415
- end # #to_a
416
-
417
-
418
- def hash
419
- to_a.hash
420
- end
421
-
422
-
423
- def eql? other
424
- self == other && self.hash == other.hash
425
- end
426
-
427
-
428
- def to_s
429
- "#<QB::Package::Version #{ @raw }>"
430
- end
431
-
432
- end # class Version
433
- end # Package
434
- end # QB
204
+ },
205
+
206
+ NAME_SEGMENT => ->(str) { str },
207
+
208
+ NUMBER_SEGMENT => ->(int) { nil },
209
+ }
210
+ end
211
+
212
+
213
+ # Instance Methods
214
+ # =====================================================================
215
+
216
+ # Tests
217
+ # ---------------------------------------------------------------------
218
+
219
+ # @return [Boolean]
220
+ # True if this version is a release (no prerelease or build values).
221
+ #
222
+ def release?
223
+ prerelease.empty? && build.empty?
224
+ end
225
+
226
+
227
+ # @return [Boolean]
228
+ # True if any prerelease segments are present (stuff after '-' in
229
+ # SemVer / "NPM" format, or the first string segment and anything
230
+ # following it in "Gem" format). Tests if {@prerelease} is not
231
+ # empty.
232
+ #
233
+ def prerelease?
234
+ !prerelease.empty?
235
+ end
236
+
237
+
238
+ # @return [Boolean]
239
+ # True if any build segments are present (stuff after '+' character
240
+ # in SemVer / "NPM" format). Tests if {@build} is empty.
241
+ #
242
+ # As of writing, we don't have a way to convey build segments in
243
+ # "Gem" version format, so this will always be false when loading a
244
+ # Gem version.
245
+ #
246
+ def build?
247
+ !build.empty?
248
+ end
249
+
250
+
251
+ # @todo Document build_dirty? method.
252
+ #
253
+ # @param [type] arg_name
254
+ # @todo Add name param description.
255
+ #
256
+ # @return [return_type]
257
+ # @todo Document return value.
258
+ #
259
+ def build_dirty?
260
+ if build?
261
+ build.include? 'dirty'
262
+ end
263
+ end # #build_dirty?
264
+
265
+
266
+ # @return [Boolean]
267
+ # True if self is a prerelease version that starts with a string that
268
+ # we consider the 'level'.
269
+ #
270
+ def level?
271
+ !level.nil?
272
+ end
273
+
274
+
275
+ # @return [Boolean]
276
+ # True if this version is a dev prerelease (first prerelease element
277
+ # is 'dev').
278
+ #
279
+ def dev?
280
+ level == 'dev'
281
+ end
282
+
283
+
284
+ # @return [Boolean]
285
+ # True if this version is a release candidate (first prerelease element
286
+ # is 'rc').
287
+ #
288
+ def rc?
289
+ level == 'rc'
290
+ end
291
+
292
+
293
+ # Derived Properties
294
+ # ---------------------------------------------------------------------
295
+
296
+ # @return [String]
297
+ # The Semver version string
298
+ # (`Major.minor.patch-prerelease+build` format).
299
+ #
300
+ def semver
301
+ result = release
302
+
303
+ unless prerelease.empty?
304
+ result += "-#{ prerelease.join '.' }"
305
+ end
306
+
307
+ unless build.empty?
308
+ result += "+#{ build.join '.' }"
309
+ end
310
+
311
+ result
312
+ end # #semver
313
+
314
+ alias_method :normalized, :semver
315
+
316
+
317
+ # @todo Document commit method.
318
+ #
319
+ # @param [type] arg_name
320
+ # @todo Add name param description.
321
+ #
322
+ # @return [return_type]
323
+ # @todo Document return value.
324
+ #
325
+ def build_commit
326
+ if build?
327
+ build.find { |seg| seg =~ /[0-9a-f]{7}/ }
328
+ end
329
+ end # #commit
330
+
331
+
332
+ # Docker image tag for the version.
333
+ #
334
+ # See {QB::Util::DockerMixin::ClassMethods#to_docker_tag}.
335
+ #
336
+ # @return [String]
337
+ #
338
+ def docker_tag
339
+ self.class.to_docker_tag semver
340
+ end # #docker_tag
341
+
342
+
343
+ # Related Versions
344
+ # ---------------------------------------------------------------------
345
+ #
346
+ # Functions that construct new version instances based on the current
347
+ # one as well as additional information provided.
348
+ #
349
+
350
+ # @return [QB::Package::Version]
351
+ # A new {QB::Package::Version} created from {#release}. Even if `self`
352
+ # *is* a release version already, still returns a new instance.
353
+ #
354
+ def release_version
355
+ self.class.from_string release
356
+ end # #release_version
357
+
358
+
359
+ # Return a new {QB::Package::Version} with build information added.
360
+ #
361
+ # @return [QB::Package::Version]
362
+ #
363
+ def build_version branch: nil, ref: nil, time: nil, dirty: nil
364
+ time = self.class.to_time_segment(time) unless time.nil?
365
+
366
+ segments = [
367
+ branch,
368
+ ref,
369
+ ('dirty' if dirty),
370
+ time,
371
+ ].reject &:nil?
372
+
373
+ if segments.empty?
374
+ raise ArgumentError,
375
+ "Need to provide at least one of branch, ref, time."
376
+ end
377
+
378
+ merge raw: nil, build: segments
379
+ end
380
+
381
+
382
+ # @return [QB::Package::Version]
383
+ # A new {QB::Package::Version} created from {#release} and
384
+ # {#prerelease} data, but without any build information.
385
+ #
386
+ def prerelease_version
387
+ merge raw: nil, build: []
388
+ end # #prerelease_version
389
+
390
+
391
+
392
+ # Language Interface
393
+ # =====================================================================
394
+
395
+ # Test for equality.
396
+ #
397
+ # Compares classes then {QB::Package::Version#to_a} results.
398
+ #
399
+ # @param [Object] other
400
+ # Object to compare to self.
401
+ #
402
+ # @return [Boolean]
403
+ # True if self and other are considered equal.
404
+ #
405
+ def == other
406
+ other.class == self.class &&
407
+ other.to_a == self.to_a
408
+ end # #==
409
+
410
+
411
+ # Return array of the version elements in order from greatest to least
412
+ # precedence.
413
+ #
414
+ # This is considered the representative structure for the object's data,
415
+ # from which all other values are dependently derived, and is used in
416
+ # {#==}, {#hash} and {#eql?}.
417
+ #
418
+ # @example
419
+ #
420
+ # version = QB::Package::Version.from_string(
421
+ # "0.1.2-rc.10+master.0ab1c3d"
422
+ # )
423
+ #
424
+ # version.to_a
425
+ # # => [0, 1, 2, ['rc', 10], ['master', '0ab1c3d']]
426
+ #
427
+ # QB::Package::Version.from_string('1').to_a
428
+ # # => [1, nil, nil, [], []]
429
+ #
430
+ # @return [Array]
431
+ #
432
+ def to_a
433
+ [
434
+ major,
435
+ minor,
436
+ patch,
437
+ prerelease,
438
+ build,
439
+ ]
440
+ end # #to_a
441
+
442
+
443
+ def hash
444
+ to_a.hash
445
+ end
446
+
447
+
448
+ def eql? other
449
+ self == other && self.hash == other.hash
450
+ end
451
+
452
+
453
+ def to_s
454
+ "#<QB::Package::Version #{ @raw }>"
455
+ end
456
+
457
+ end # class QB::Package::Version