qb 0.3.7 → 0.3.8

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.
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