qb 0.3.25 → 0.4.0

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