qb 0.3.25 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (113) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/ansible.cfg +10 -1
  4. data/exe/.qb_interop_receive +3 -10
  5. data/exe/qb +8 -2
  6. data/lib/python/qb/__init__.py +6 -0
  7. data/{roles/qb/ruby/rspec/setup/tasks/persistence.yml → lib/python/qb/ansible/__init__.py} +0 -0
  8. data/lib/python/qb/ansible/modules/__init__.py +0 -0
  9. data/lib/python/qb/ansible/modules/docker/__init__.py +0 -0
  10. data/lib/python/qb/ansible/modules/docker/client.py +177 -0
  11. data/lib/python/qb/ansible/modules/docker/image_manager.py +754 -0
  12. data/lib/python/qb/ipc/__init__.py +0 -0
  13. data/lib/python/qb/ipc/stdio/__init__.py +99 -0
  14. data/lib/python/qb/ipc/stdio/logging.py +151 -0
  15. data/lib/qb.rb +3 -3
  16. data/lib/qb/ansible/cmds/playbook.rb +5 -14
  17. data/lib/qb/ansible/env.rb +36 -6
  18. data/lib/qb/ansible/module.rb +396 -152
  19. data/lib/qb/ansible/module/response.rb +195 -0
  20. data/lib/qb/ansible/modules.rb +42 -0
  21. data/lib/qb/ansible/modules/docker/image.rb +273 -0
  22. data/lib/qb/cli.rb +5 -18
  23. data/lib/qb/cli/run.rb +2 -2
  24. data/lib/qb/data.rb +22 -0
  25. data/lib/qb/data/immutable.rb +39 -0
  26. data/lib/qb/docker.rb +2 -0
  27. data/lib/qb/docker/cli.rb +430 -0
  28. data/lib/qb/docker/image.rb +207 -0
  29. data/lib/qb/docker/image/name.rb +309 -0
  30. data/lib/qb/docker/image/tag.rb +113 -0
  31. data/lib/qb/docker/repo.rb +0 -0
  32. data/lib/qb/errors.rb +17 -3
  33. data/lib/qb/execution.rb +83 -0
  34. data/lib/qb/ipc.rb +48 -0
  35. data/lib/qb/ipc/stdio.rb +32 -0
  36. data/lib/qb/ipc/stdio/client.rb +267 -0
  37. data/lib/qb/ipc/stdio/server.rb +229 -0
  38. data/lib/qb/ipc/stdio/server/in_service.rb +18 -0
  39. data/lib/qb/ipc/stdio/server/log_service.rb +168 -0
  40. data/lib/qb/ipc/stdio/server/out_service.rb +20 -0
  41. data/lib/qb/ipc/stdio/server/service.rb +229 -0
  42. data/lib/qb/options.rb +360 -502
  43. data/lib/qb/options/option.rb +293 -115
  44. data/lib/qb/options/option/option_parser_concern.rb +228 -0
  45. data/lib/qb/options/types.rb +73 -0
  46. data/lib/qb/package.rb +0 -1
  47. data/lib/qb/package/version.rb +179 -58
  48. data/lib/qb/package/version/from.rb +192 -51
  49. data/lib/qb/package/version/leveled.rb +1 -1
  50. data/lib/qb/path.rb +3 -2
  51. data/lib/qb/repo/git.rb +9 -85
  52. data/lib/qb/role/default_dir.rb +2 -2
  53. data/lib/qb/role/errors.rb +2 -8
  54. data/lib/qb/util.rb +1 -2
  55. data/lib/qb/util/bundler.rb +73 -43
  56. data/lib/qb/util/decorators.rb +99 -0
  57. data/lib/qb/util/interop.rb +7 -8
  58. data/lib/qb/util/resource.rb +12 -13
  59. data/lib/qb/version.rb +10 -0
  60. data/library/path_facts +5 -10
  61. data/library/qb.module.rb +105 -0
  62. data/library/stream +6 -26
  63. data/load/ansible/module/autorun.rb +25 -0
  64. data/load/ansible/module/script.rb +123 -0
  65. data/load/rebundle.rb +39 -0
  66. data/plugins/filter/dict_filters.py +56 -0
  67. data/plugins/{filter_plugins/path_plugins.py → filter/path_filters.py} +0 -0
  68. data/plugins/{filter_plugins/ruby_interop_plugins.py → filter/ruby_interop_filters.py} +1 -17
  69. data/plugins/{filter_plugins/string_plugins.py → filter/string_filters.py} +1 -20
  70. data/plugins/{filter_plugins/version_plugins.py → filter/version_filters.py} +3 -18
  71. data/plugins/{lookup_plugins/every.py → lookup/every_lookups.py} +0 -0
  72. data/plugins/{lookup_plugins/resolve.py → lookup/resolve_lookups.py} +0 -0
  73. data/plugins/{lookup_plugins/version.py → lookup/version_lookups.py} +0 -16
  74. data/plugins/test/dict_tests.py +36 -0
  75. data/plugins/test/string_tests.py +36 -0
  76. data/qb.gemspec +7 -3
  77. data/roles/nrser.rb/library/set_fact_with_ruby.rb +3 -9
  78. data/roles/nrser.state_mate/library/state +3 -17
  79. data/roles/qb/call/meta/qb.yml +1 -1
  80. data/roles/qb/dev/ref/repo/git/meta/qb.yml +1 -1
  81. data/roles/qb/{ruby/rspec/setup → docker/mac/kubernetes}/defaults/main.yml +1 -1
  82. data/roles/qb/{ruby/rspec/setup → docker/mac/kubernetes}/meta/main.yml +3 -2
  83. data/roles/qb/{ruby/rspec/setup → docker/mac/kubernetes}/meta/qb.yml +12 -7
  84. data/roles/qb/docker/mac/kubernetes/tasks/main.yml +45 -0
  85. data/roles/qb/git/check/clean/meta/qb.yml +1 -1
  86. data/roles/qb/git/ignore/meta/qb +10 -3
  87. data/roles/qb/git/submodule/update/library/git_submodule_update +17 -27
  88. data/roles/qb/github/pages/setup/meta/qb.yml +1 -1
  89. data/roles/qb/labs/atom/apm/meta/qb.yml +1 -1
  90. data/roles/qb/osx/git/change_case/meta/qb.yml +1 -1
  91. data/roles/qb/osx/notif/meta/qb.yml +1 -1
  92. data/roles/qb/pkg/bump/library/bump +4 -16
  93. data/roles/qb/role/qb/defaults/main.yml +2 -0
  94. data/roles/qb/role/qb/meta/qb.yml +10 -5
  95. data/roles/qb/role/qb/templates/qb.yml.j2 +7 -2
  96. data/roles/qb/role/templates/library/module.rb.j2 +12 -23
  97. data/roles/qb/role/templates/meta/main.yml.j2 +14 -1
  98. data/roles/qb/ruby/bundler/meta/qb.yml +1 -1
  99. data/roles/qb/ruby/dependency/meta/qb.yml +1 -1
  100. data/roles/qb/ruby/gem/bin_stubs/meta/qb.yml +1 -1
  101. data/roles/qb/ruby/gem/bin_stubs/templates/console +8 -2
  102. data/roles/qb/ruby/gem/build/meta/qb.yml +1 -1
  103. data/roles/qb/ruby/gem/new/meta/qb.yml +1 -1
  104. data/roles/qb/ruby/nrser/rspex/generate/meta/qb.yml +5 -5
  105. data/roles/qb/ruby/nrser/rspex/issue/meta/qb.yml +1 -1
  106. data/roles/qb/ruby/yard/clean/meta/qb.yml +1 -1
  107. data/roles/qb/ruby/yard/config/library/yard.get_output_dir +5 -15
  108. data/roles/qb/ruby/yard/config/meta/qb.yml +1 -1
  109. data/roles/qb/ruby/yard/setup/meta/qb.yml +1 -1
  110. metadata +71 -22
  111. data/lib/qb/ansible_module.rb +0 -5
  112. data/lib/qb/util/stdio.rb +0 -187
  113. data/roles/qb/ruby/rspec/setup/tasks/main.yml +0 -4
@@ -0,0 +1,73 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ # Requirements
5
+ # =======================================================================
6
+
7
+ # Stdlib
8
+ # -----------------------------------------------------------------------
9
+
10
+ # Deps
11
+ # -----------------------------------------------------------------------
12
+
13
+ require 'nrser/refinements/types'
14
+
15
+ # Project / Package
16
+ # -----------------------------------------------------------------------
17
+
18
+
19
+ # Refinements
20
+ # =======================================================================
21
+
22
+ using NRSER::Types
23
+
24
+
25
+ # Definitions
26
+ # =======================================================================
27
+
28
+ # Custom types, available by factory name in QB metadata. Neat huh?!
29
+ #
30
+ module QB
31
+ class Options
32
+ module Types
33
+ extend t::Factory
34
+
35
+ def_factory :glob do
36
+ t.array t.path, name: 'FileGlob', from_s: ->( glob ) {
37
+ if glob.start_with? '//'
38
+ glob = NRSER.git_root( Dir.getwd ).join( glob[2..-1] ).to_s
39
+ end
40
+
41
+ Dir[glob]
42
+ }
43
+ end
44
+
45
+
46
+ def_factory(
47
+ :qb_default_dir_strategy,
48
+ aliases: [ :default_dir_strategy ],
49
+ ) do |name: 'QBDefaultDirStrategy', **options|
50
+ t.one_of \
51
+ t.nil,
52
+ t.false,
53
+ 'cwd',
54
+ 'git_root',
55
+ t.shape( 'exe' => t.path ),
56
+ t.shape( 'find_up' => t.rel_path ),
57
+ t.shape( 'from_role' => t.non_empty_str ),
58
+ name: name,
59
+ **options
60
+ end
61
+
62
+
63
+ def_factory(
64
+ :qb_default_dir,
65
+ aliases: [ :default_dir ],
66
+ ) do |name: 'QBDefaultDir', **options|
67
+ t.one_of \
68
+ qb_default_dir_strategy,
69
+ t.array( qb_default_dir_strategy ),
70
+ **options
71
+ end
72
+
73
+ end; end; end # module QB::Options::Types
@@ -18,7 +18,6 @@ require_relative './package/version'
18
18
  # Refinements
19
19
  # =======================================================================
20
20
 
21
- using NRSER
22
21
  using NRSER::Types
23
22
 
24
23
 
@@ -1,3 +1,6 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
1
4
  # Requirements
2
5
  # =======================================================================
3
6
 
@@ -17,20 +20,10 @@ require 'qb/util/resource'
17
20
  # Refinements
18
21
  # =======================================================================
19
22
 
20
- require 'nrser/refinements'
21
- using NRSER
22
-
23
23
  require 'nrser/refinements/types'
24
24
  using NRSER::Types
25
25
 
26
26
 
27
- # Declarations
28
- # =======================================================================
29
-
30
- module QB; end
31
- class QB::Package < QB::Util::Resource; end
32
-
33
-
34
27
  # Definitions
35
28
  # =======================================================================
36
29
 
@@ -39,7 +32,104 @@ class QB::Package < QB::Util::Resource; end
39
32
  #
40
33
  # Intended to be immutable for practical purposes.
41
34
  #
42
- class QB::Package::Version < QB::Util::Resource
35
+ # Based off [SemVer 2][] and - in particular - the [Node semver package][]
36
+ # interpretation / implementation, though we don't use that package at all
37
+ # at this point (sub-shelling out became too expensive, and explorations into
38
+ # Ruby Racer, etc. didn't pan out (don't remember exactly why)).
39
+ #
40
+ # [SemVer 2]: https://semver.org/spec/v2.0.0.html
41
+ # [Node semver package]: https://www.npmjs.com/package/semver
42
+ #
43
+ # Let's start the show with some fun...
44
+ #
45
+ # Terminology
46
+ # ----------------------------------------------------------------------------
47
+ #
48
+ # Working off what's in the [SemVer 2][] spec as much as possible.
49
+ #
50
+ # We're going to start from the bottom and build up...
51
+ #
52
+ #
53
+ # ### Identifiers
54
+ #
55
+ # *Identifiers* ([SemVer 2][] spec term) are the atoms of the version:
56
+ # the values that will not be further divided.
57
+ #
58
+ # They come in two types (my terms):
59
+ #
60
+ # 1. *Number Identifiers*
61
+ #
62
+ # Non-negative integers. Their string representations may not include
63
+ # leading zeros.
64
+ #
65
+ # 2. *Name Identifiers*
66
+ #
67
+ # Non-empty strings that contain only `a-z`, `A-Z` and `-` and are
68
+ # **not** number identifiers.
69
+ #
70
+ # All identifiers must be exclusively one type or the other.
71
+ #
72
+ # Parse and validate identifiers with
73
+ # {QB::Package::Version::From.identifier_for}.
74
+ #
75
+ #
76
+ # ### Segments
77
+ #
78
+ # *Segments* (my term) are sequences of zero or more *identifiers*.
79
+ #
80
+ # In string representation, the *identifiers* in a *segment* are separated
81
+ # by the dot (`.`) character.
82
+ #
83
+ # > **NOTE**
84
+ # >
85
+ # > As identifiers can not be empty, a segment's string representation may not
86
+ # > start or end with `.`, and may not contain consecutive `.`.
87
+ #
88
+ # There are three types of segments:
89
+ #
90
+ # 1. *Release Segment*
91
+ #
92
+ # Composed of exactly three *number identifiers*:
93
+ #
94
+ # 1. *Major*
95
+ # 2. *Minor*
96
+ # 3. *Patch*
97
+ #
98
+ # > **NOTE**
99
+ # >
100
+ # > Ruby's Gem version format doesn't require anything but the *major*
101
+ # > identifier, in which case we default missing ones to `0`.
102
+ #
103
+ # 2. *Prerelease Segment*
104
+ #
105
+ # Composed of zero or more *identifiers* - number or name, in any order.
106
+ #
107
+ # 3. *Build Segment*
108
+ #
109
+ # Composed of zero or more *identifiers* - number or name, in any order.
110
+ #
111
+ #
112
+ # ### Versions
113
+ #
114
+ # A *version* is exactly one release segment, prerelease segment and build
115
+ # segment, in which the prerelease and build segments may be empty as noted
116
+ # above.
117
+ #
118
+ # In SemVer string representation, the release segment is always present, and
119
+ # a non-empty prerelease segment may follow it, separated by a `-` character.
120
+ #
121
+ # A non-empty build segment may follow those, separated by a `+` character.
122
+ #
123
+ module QB
124
+ class Package < QB::Util::Resource
125
+ class Version < QB::Util::Resource
126
+
127
+ # Sub-Tree Requirements
128
+ # ========================================================================
129
+
130
+ require_relative './version/leveled'
131
+ require_relative './version/from'
132
+
43
133
 
44
134
  # Mixins
45
135
  # =====================================================================
@@ -51,9 +141,23 @@ class QB::Package::Version < QB::Util::Resource
51
141
  # Constants
52
142
  # =====================================================================
53
143
 
144
+ # Pattern to match string *identifiers* that are version "numlets" (the
145
+ # non-negative integer number part of version "numbers").
146
+ #
147
+ # @return [Regexp]
148
+ #
149
+ NUMBER_IDENTIFIER_RE = /\A(?:0|(?:[1-9]\d*))\z/
150
+
151
+
152
+ # What separates *identifiers* (the base undivided values).
153
+ #
154
+ # @return [String]
155
+ #
156
+ IDENTIFIER_SEPARATOR = '.'
157
+
54
158
  NUMBER_SEGMENT = t.non_neg_int
55
- NAME_SEGMENT = t.str
56
- MIXED_SEGMENT = t.union NUMBER_SEGMENT, NAME_SEGMENT
159
+ NAME_SEGMENT = t.str & /\A[0-9A-Za-z\-]+\z/
160
+ MIXED_SEGMENT = t.xor NUMBER_SEGMENT, NAME_SEGMENT
57
161
 
58
162
 
59
163
  # Reasonably simple regular expression to extract things that might be
@@ -89,27 +193,53 @@ class QB::Package::Version < QB::Util::Resource
89
193
  # Props
90
194
  # =====================================================================
91
195
 
92
- prop :raw, type: t.maybe(t.str), default: nil
93
- prop :major, type: NUMBER_SEGMENT
94
- prop :minor, type: NUMBER_SEGMENT, default: 0
95
- prop :patch, type: NUMBER_SEGMENT, default: 0
96
- prop :prerelease, type: t.array(MIXED_SEGMENT), default: ->{ [] }
97
- prop :build, type: t.array(MIXED_SEGMENT), default: ->{ [] }
98
-
99
- prop :release, type: t.str, source: :@release
100
- prop :is_release, type: t.bool, source: :release?
101
- prop :is_prerelease, type: t.bool, source: :prerelease?
102
- prop :is_build, type: t.bool, source: :build?
103
- prop :semver, type: t.str, source: :semver
104
- prop :docker_tag, type: t.str, source: :docker_tag
105
- prop :build_commit, type: t.maybe(t.str), source: :build_commit
106
- prop :is_build_dirty, type: t.maybe(t.bool), source: :build_dirty?
107
-
108
-
109
- # Attributes
110
- # =====================================================================
111
-
112
- attr_reader :release
196
+ prop :raw, type: t.maybe(t.str),
197
+ default: nil
198
+
199
+ prop :major, type: NUMBER_SEGMENT
200
+
201
+ prop :minor, type: NUMBER_SEGMENT,
202
+ default: 0
203
+
204
+ prop :patch, type: NUMBER_SEGMENT,
205
+ default: 0
206
+
207
+ prop :revision, type: t.array( NUMBER_SEGMENT ),
208
+ default: ->{ [] }
209
+
210
+ prop :prerelease, type: t.array( MIXED_SEGMENT ),
211
+ default: ->{ [] }
212
+
213
+ prop :build, type: t.array( MIXED_SEGMENT ),
214
+ default: ->{ [] }
215
+
216
+
217
+ # Derived Props
218
+ # --------------------------------------------------------------------------
219
+
220
+ prop :release, type: t.str,
221
+ source: :@release
222
+
223
+ prop :is_release, type: t.bool,
224
+ source: :release?
225
+
226
+ prop :is_prerelease, type: t.bool,
227
+ source: :prerelease?
228
+
229
+ prop :is_build, type: t.bool,
230
+ source: :build?
231
+
232
+ prop :semver, type: t.str,
233
+ source: :semver
234
+
235
+ prop :docker_tag, type: t.str,
236
+ source: :docker_tag
237
+
238
+ prop :build_commit, type: t.maybe(t.str),
239
+ source: :build_commit
240
+
241
+ prop :is_build_dirty, type: t.maybe(t.bool),
242
+ source: :build_dirty?
113
243
 
114
244
 
115
245
  # Class Methods
@@ -201,26 +331,24 @@ class QB::Package::Version < QB::Util::Resource
201
331
  end
202
332
 
203
333
 
204
- # @todo Document build_dirty? method.
334
+ # Is the build "dirty"?
205
335
  #
206
- # @param [type] arg_name
207
- # @todo Add name param description.
208
- #
209
- # @return [return_type]
210
- # @todo Document return value.
336
+ # @return [Boolean]
211
337
  #
212
338
  def build_dirty?
213
339
  if build?
214
- build.include? 'dirty'
340
+ build.any? { |seg| seg.is_a?( String ) && seg.include?( 'dirty' ) }
215
341
  end
216
342
  end # #build_dirty?
217
343
 
344
+ alias_method :dirty?, :build_dirty?
345
+
218
346
 
219
347
  # Derived Properties
220
348
  # ---------------------------------------------------------------------
221
349
 
222
350
  def release
223
- [major, minor, patch].join '.'
351
+ [major, minor, patch, *revision].join '.'
224
352
  end
225
353
 
226
354
 
@@ -291,22 +419,21 @@ class QB::Package::Version < QB::Util::Resource
291
419
  #
292
420
  # @return [QB::Package::Version]
293
421
  #
294
- def build_version branch: nil, ref: nil, time: nil, dirty: nil
295
- time = self.class.to_time_segment(time) unless time.nil?
422
+ def build_version *build, branch: nil, ref: nil, time: nil, dirty: nil
296
423
 
297
- segments = [
424
+ repo_segments = [
298
425
  branch,
299
426
  ref,
300
427
  ('dirty' if dirty),
301
- time,
302
- ].reject &:nil?
428
+ (self.class.to_time_segment(time) if dirty && time),
429
+ ].compact
303
430
 
304
- if segments.empty?
431
+ if repo_segments.empty?
305
432
  raise ArgumentError,
306
- "Need to provide at least one of branch, ref, time."
433
+ "Need to provide at least one of branch, ref, dirty."
307
434
  end
308
435
 
309
- merge raw: nil, build: segments
436
+ merge raw: nil, build: [*build, repo_segments.join( '-' )]
310
437
  end
311
438
 
312
439
 
@@ -367,6 +494,7 @@ class QB::Package::Version < QB::Util::Resource
367
494
  major,
368
495
  minor,
369
496
  patch,
497
+ revision,
370
498
  prerelease,
371
499
  build,
372
500
  ]
@@ -387,11 +515,4 @@ class QB::Package::Version < QB::Util::Resource
387
515
  "#<QB::Package::Version #{ @raw }>"
388
516
  end
389
517
 
390
- end # class QB::Package::Version
391
-
392
-
393
- # Post-Processing
394
- # =======================================================================
395
-
396
- require 'qb/package/version/leveled'
397
- require 'qb/package/version/from'
518
+ end; end; end # class QB::Package::Version
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+
3
4
  # Refinements
4
5
  # =======================================================================
5
6
 
6
- using NRSER
7
7
  using NRSER::Types
8
8
 
9
9
 
@@ -51,91 +51,232 @@ module QB::Package::Version::From
51
51
  #
52
52
  # @return [QB::Package::Version]
53
53
  #
54
- def self.gemver version
55
- version = Gem::Version.new( version ) if version.is_a?( String )
54
+ def self.gemver source
55
+ gem_version = case source
56
+ when ::Gem::Version
57
+ source
58
+ else
59
+ ::Gem::Version.new source.to_s
60
+ end
56
61
 
57
62
  # release segments are everything before a string
58
- release_segments = version.segments.take_while { |seg|
63
+ release_segments = gem_version.segments.take_while { |seg|
59
64
  !seg.is_a?(String)
60
65
  }
61
66
 
62
- # We don't support > 3 release segments to make life somewhat
63
- # reasonable. Yeah, I think I've seen projects do it. We'll cross that
64
- # bridge if and when we get to it.
65
- if release_segments.length > 3
66
- raise ArgumentError,
67
- "We don't handle releases with more than 3 segments " +
68
- "(found #{ release_segments.inspect } in #{ version })"
69
- end
70
-
71
- prerelease_segments = version.segments[release_segments.length..-1]
67
+ prerelease_segments = gem_version.segments[release_segments.length..-1]
72
68
 
73
69
  prop_values \
74
- raw: version.to_s,
75
- major: release_segments[0] || 0,
70
+ raw: source.to_s,
71
+ major: release_segments[0],
76
72
  minor: release_segments[1] || 0,
77
73
  patch: release_segments[2] || 0,
74
+ revision: release_segments[3..-1] || [],
78
75
  prerelease: prerelease_segments,
79
76
  build: []
80
77
  end
81
78
 
82
79
  singleton_class.send :alias_method, :gem_version, :gemver
83
-
84
-
85
- def self.semver version
86
- stmt = NRSER.squish <<-END
87
- var Semver = require('semver');
88
-
89
- console.log(
90
- JSON.stringify(
91
- Semver(#{ JSON.dump version })
92
- )
93
- );
94
- END
80
+
81
+
82
+ def self.split_identifiers string
83
+ string.split QB::Package::Version::IDENTIFIER_SEPARATOR
84
+ end
85
+
86
+
87
+ # Parse and/or validate version *identifiers*.
88
+ #
89
+ # See {QB::Package::Version} for details on *identifiers*.
90
+ #
91
+ # @param [String | Integer] value
92
+ # A value that is either already an *identifier* or a string that can
93
+ # be parsed into one.
94
+ #
95
+ # @return [String | Integer]
96
+ # A valid *identifier*.
97
+ #
98
+ def self.identifier_for value
99
+ case value
100
+ when QB::Package::Version::NUMBER_IDENTIFIER_RE
101
+ value.to_i
102
+ when QB::Package::Version::MIXED_SEGMENT
103
+ value
104
+ else
105
+ raise ArgumentError.new binding.erb <<~END
106
+ Can't parse identifier <%= value.inspect %>
107
+
108
+ Expected one of:
109
+
110
+ 1. <%= QB::Package::Version::NUMBER_IDENTIFIER_RE %>
111
+ 2. <%= QB::Package::Version::MIXED_SEGMENT %>
112
+
113
+ END
114
+ end
115
+ end # .identifier_for
116
+
117
+
118
+ def self.segment_for string
119
+ split_identifiers( string ).map { |s| identifier_for s }
120
+ end
121
+
122
+
123
+ # Load a SemVer¹ string into a {QB::Package::Version}.
124
+ #
125
+ # @see https://semver.org
126
+ #
127
+ # > **¹**
128
+ # > Through a combination of need, failure and frustration we are a
129
+ # > *wee bit* looser than the SemVer spec. This really comes from needing
130
+ # > to be able to handle more than three *release segments* because:
131
+ # >
132
+ # > 1. Well, some projects use more than three and we can't change that.
133
+ # > 2. It seemed over-complicated to add another "almost-semver" parsing
134
+ # > option.
135
+ # >
136
+ # > Details below. You can enforce our best attempt at pure SemVer with
137
+ # > the `strict:` keyword option.
138
+ #
139
+ # ##### Gory Details #####
140
+ #
141
+ # Oh, semver... what a pain you're been.
142
+ #
143
+ # Right now, I just finished writing our own parser. It probably has a lot
144
+ # of problems and I can't imagine it conforms to the spec even where it's
145
+ # meant to. I didn't want to go this road, it was out of desperation.
146
+ #
147
+ # First, QB was shelling-out to Node and using it's [semver][Node semver]
148
+ # package, since that seems to kind of be the de-facto reference
149
+ # implementation of the spec.
150
+ #
151
+ # That was far too slow to process large lists of version like you might
152
+ # get from `git tag`, and it means we depended on Node and had to bundle
153
+ # the `semver` package in or install it otherwise, which was a pain for
154
+ # a single function call.
155
+ #
156
+ # Yeah, there are other ways to go about it, but they all suck too.
157
+ #
158
+ # Next I tried the [semver2][Ruby semver2] Ruby gem. It never struck me as
159
+ # super solid, and when faced with `1.2.3.4-pre`-style versions it just
160
+ # tossed everything after the `3` and didn't mention it, which led me to
161
+ # toss it too.
162
+ #
163
+ # This happen when I realized we couldn't side-step "fourth release segment"
164
+ # because I needed the system to handle OpenResty's Docker image versions,
165
+ # which are `M.m.p.r` format, leading to add the
166
+ # {QB::Package::Version#revision} property and write my own parsing logic
167
+ # here.
168
+ #
169
+ # [Node semver]: https://www.npmjs.com/package/semver
170
+ #
171
+ # @param [#to_s] source
172
+ # Where to get the source string.
173
+ #
174
+ # @param [Boolean] strict:
175
+ # When `true`, we attempt to adhere strictly to the SemVer spec, raising
176
+ # if we find any departures.
177
+ #
178
+ # @raise [ArgumentError]
179
+ # 1. If there are **less than** `3` *release segments*.
180
+ #
181
+ # This helps us not loading things like `2018-new-stuff` into a
182
+ # version, as might be found in a Git tag.
183
+ #
184
+ # It does not let us avoid loading `2018.10.11-new-stuff` info a
185
+ # version, so fair warning.
186
+ #
187
+ # 2. If `strict: true` and there are not **exactly** `3`
188
+ # *release segments*.
189
+ #
190
+ def self.semver source, strict: false
191
+ source = source.to_s unless source.is_a?( String )
95
192
 
96
- parse = JSON.load Cmds.new(
97
- "node --eval %s", args: [stmt], chdir: QB::ROOT
98
- ).out!
193
+ identifier_for_ref = method :identifier_for
99
194
 
100
- prop_values \
101
- raw: version,
102
- major: parse['major'],
103
- minor: parse['minor'],
104
- patch: parse['patch'],
105
- prerelease: parse['prerelease'],
106
- build: parse['build']
195
+ if source.include?( '-' ) && source.include?( '+' )
196
+ release_str, _, rest = source.partition '-'
197
+ pre_str, _, build_str = rest.partition '+'
198
+ elsif source.include?( '-' )
199
+ release_str, _, pre_str = source.partition '-'
200
+ build_str = ''
201
+ elsif source.include?( '+' )
202
+ release_str, _, build_str = source.partition '+'
203
+ pre_str = ''
204
+ else
205
+ release_str = source
206
+ pre_str = build_str = ''
207
+ end
208
+
209
+ release_segs, pre_segs, build_segs = \
210
+ [release_str, pre_str, build_str].map { |str|
211
+ split_identifiers( str ).map &identifier_for_ref
212
+ }
213
+
214
+ # Check release segments length
215
+ if strict && release_segs.length != 3
216
+ raise NRSER::ArgumentError.new \
217
+ "Strict SemVer versions *MUST* have at exactly 3 release segments",
218
+ source: source,
219
+ release_segments: release_segs
220
+
221
+ elsif release_segs.length < 3
222
+ raise NRSER::ArgumentError.new \
223
+ "SemVer versions *MUST* have at lease 3 release segments",
224
+ source: source,
225
+ release_segments: release_segs
226
+
227
+ end
228
+
229
+ prop_values **{
230
+ raw: source,
231
+ major: release_segs[0],
232
+ minor: release_segs[1],
233
+ patch: release_segs[2],
234
+ revision: release_segs[3..-1] || [],
235
+ prerelease: pre_segs,
236
+ build: build_segs,
237
+ }.compact
107
238
  end
108
239
 
109
- singleton_class.send :alias_method, :npm_version, :semver
240
+
241
+ def npm_version source
242
+ semver source, strict: true
243
+ end
110
244
 
111
245
 
112
246
  # Parse Docker image tag version and create an instance.
113
247
  #
114
- # @param [String] version
248
+ # @param [#to_s] source
115
249
  # String version to parse.
116
250
  #
117
251
  # @return [QB::Package::Version]
118
252
  #
119
- def self.docker_tag version
120
- string( version.gsub( '_', '+' ) ).merge raw: version
253
+ def self.docker_tag source
254
+ source = source.to_s unless source.is_a?( String )
255
+ self.string( source.gsub( '_', '+' ) ).merge raw: source
121
256
  end # .docker_tag
122
257
 
123
258
 
124
259
  # Parse string version into an instance. Accept Semver, Ruby Gem and
125
260
  # Docker image tag formats.
126
261
  #
127
- # @param [String]
262
+ # @param [#to_s] source
128
263
  # String version to parse.
129
264
  #
130
265
  # @return [QB::Package::Version]
131
266
  #
132
- def self.string string
133
- if string.include? '_'
134
- docker_tag string
135
- elsif string.include?( '-' ) || string.include?( '+' )
136
- semver string
267
+ def self.string source
268
+ source = source.to_s unless source.is_a?( String )
269
+
270
+ t.non_empty_str.check source
271
+
272
+ if source.include? '_'
273
+ docker_tag source
274
+ elsif ( source.include?( '-' ) ||
275
+ source.include?( '+' ) ) &&
276
+ source =~ /\A\d+\.\d+\.\d+/
277
+ semver source
137
278
  else
138
- gem_version string
279
+ gemver source
139
280
  end
140
281
  end
141
282
 
@@ -148,8 +289,8 @@ module QB::Package::Version::From
148
289
  string object
149
290
  when Hash
150
291
  prop_values **object
151
- when Gem::Version
152
- gem_version object
292
+ when ::Gem::Version
293
+ gemver object
153
294
  else
154
295
  raise TypeError.new binding.erb <<-END
155
296
  `object` must be String, Hash or Gem::Version