Module: QB::Package::Version::From

Defined in:
lib/qb/package/version/from.rb

Overview

Module of factory methods to create QB::Package::Version instances from other objects (strings, QB::Package::Version, etc.)

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.class_for(**values) ⇒ Class<QB::Package::Version>

Get class to instantiate for prop values - either QB::Package::Version or a specialized subclass like Leveled.

Parameters:

  • **values (Hash<Symbol, Object>)

    Prop values.

Returns:



26
27
28
29
30
31
32
# File 'lib/qb/package/version/from.rb', line 26

def self.class_for **values
  if QB::Package::Version::Leveled.level_for **values
    QB::Package::Version::Leveled
  else
    QB::Package::Version
  end
end

.docker_tag(source) ⇒ QB::Package::Version

Parse Docker image tag version and create an instance.

Parameters:

  • source (#to_s)

    String version to parse.

Returns:



255
256
257
258
# File 'lib/qb/package/version/from.rb', line 255

def self.docker_tag source
  source = source.to_s unless source.is_a?( String )
  self.string( source.gsub( '_', '+' ) ).merge raw: source
end

.file(path) ⇒ QB::Package::Version

Load a QB::Package::Version from a file.

Just reads the file and passes the contents to string.

Parameters:

  • file (String | Pathname | IO)

    File path or handle to read from.

Returns:



318
319
320
# File 'lib/qb/package/version/from.rb', line 318

def self.file path
  string File.read( path )
end

.gemver(source) ⇒ QB::Package::Version

Create an instance from a Gem-style version.

Parameters:

Returns:



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/qb/package/version/from.rb', line 54

def self.gemver source
  gem_version = case source
  when ::Gem::Version
    source
  else
    ::Gem::Version.new source.to_s
  end
  
  # release segments are everything before a string
  release_segments = gem_version.segments.take_while { |seg|
    !seg.is_a?(String)
  }
  
  prerelease_segments = gem_version.segments[release_segments.length..-1]
  
  prop_values \
    raw: source.to_s,
    major: release_segments[0],
    minor: release_segments[1] || 0,
    patch: release_segments[2] || 0,
    revision: release_segments[3..-1] || [],
    prerelease: prerelease_segments,
    build: []
end

.identifier_for(value) ⇒ String | Integer

Parse and/or validate version identifiers.

See QB::Package::Version for details on identifiers.

Parameters:

  • value (String | Integer)

    A value that is either already an identifier or a string that can be parsed into one.

Returns:

  • (String | Integer)

    A valid identifier.



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/qb/package/version/from.rb', line 98

def self.identifier_for value
  case value
  when QB::Package::Version::NUMBER_IDENTIFIER_RE
    value.to_i
  when QB::Package::Version::MIXED_SEGMENT
    value
  else
    raise ArgumentError.new binding.erb <<~END
      Can't parse identifier <%= value.inspect %>
      
      Expected one of:
      
      1.  <%= QB::Package::Version::NUMBER_IDENTIFIER_RE %>
      2.  <%= QB::Package::Version::MIXED_SEGMENT %>
      
    END
  end
end

.object(object) ⇒ Object



288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
# File 'lib/qb/package/version/from.rb', line 288

def self.object object
  case object
  when String
    string object
  when Hash
    prop_values **object
  when ::Gem::Version
    gemver object
  else
    raise TypeError.new binding.erb <<-END
      `object` must be String, Hash or Gem::Version
      
      Found:
      
          <%= object.pretty_inspect %>
      
    END
  end
end

.prop_values(**values) ⇒ QB::Package::Version

Instantiate an instance from prop values, using class_for to choose the possible specialized class.

Parameters:

  • **values (Hash<Symbol, Object>)

    Prop values.

Returns:



43
44
45
# File 'lib/qb/package/version/from.rb', line 43

def self.prop_values **values
  class_for( **values ).new **values
end

.repo(repo_or_path, add_build: true) ⇒ Object



324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
# File 'lib/qb/package/version/from.rb', line 324

def self.repo repo_or_path, add_build: true
  repo = t.match repo_or_path,
    QB::Repo, repo_or_path,
    t.path,   QB::Repo.method( :from_path )
  
  version_path = repo.root_path / 'VERSION'
  file_version = file version_path
  
  if  add_build &&
      file_version.level? &&
      file_version.dev?
    file_version.build_version \
      branch: repo.branch,
      ref: repo.head_short,
      dirty: !repo.clean?
  else
    file_version
  end
end

.segment_for(string) ⇒ Object



118
119
120
# File 'lib/qb/package/version/from.rb', line 118

def self.segment_for string
  split_identifiers( string ).map { |s| identifier_for s }
end

.semver(source, strict: false) ⇒ Object

Load a SemVer¹ string into a QB::Package::Version.

¹ Through a combination of need, failure and frustration we are a wee bit looser than the SemVer spec. This really comes from needing to be able to handle more than three release segments because:

  1. Well, some projects use more than three and we can't change that.
  2. It seemed over-complicated to add another "almost-semver" parsing option.

Details below. You can enforce our best attempt at pure SemVer with the strict: keyword option.

Gory Details

Oh, semver... what a pain you're been.

Right now, I just finished writing our own parser. It probably has a lot of problems and I can't imagine it conforms to the spec even where it's meant to. I didn't want to go this road, it was out of desperation.

First, QB was shelling-out to Node and using it's semver package, since that seems to kind of be the de-facto reference implementation of the spec.

That was far too slow to process large lists of version like you might get from git tag, and it means we depended on Node and had to bundle the semver package in or install it otherwise, which was a pain for a single function call.

Yeah, there are other ways to go about it, but they all suck too.

Next I tried the [semver2][Ruby semver2] Ruby gem. It never struck me as super solid, and when faced with 1.2.3.4-pre-style versions it just tossed everything after the 3 and didn't mention it, which led me to toss it too.

This happen when I realized we couldn't side-step "fourth release segment" because I needed the system to handle OpenResty's Docker image versions, which are M.m.p.r format, leading to add the QB::Package::Version#revision property and write my own parsing logic here.

Parameters:

  • source (#to_s)

    Where to get the source string.

  • strict: (Boolean) (defaults to: false)

    When true, we attempt to adhere strictly to the SemVer spec, raising if we find any departures.

Raises:

  • (ArgumentError)
    1. If there are less than 3 release segments.

      This helps us not loading things like 2018-new-stuff into a version, as might be found in a Git tag.

      It does not let us avoid loading 2018.10.11-new-stuff info a version, so fair warning.

    2. If strict: true and there are not exactly 3 release segments.

See Also:



190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
# File 'lib/qb/package/version/from.rb', line 190

def self.semver source, strict: false
  source = source.to_s unless source.is_a?( String )
  
  identifier_for_ref = method :identifier_for
  
  if  source.include?( '-' ) &&
      source.include?( '+' ) &&
      source.index( '-' ) < source.index( '+' )
    release_str, _, rest = source.partition '-'
    pre_str, _, build_str = rest.partition '+'
  elsif source.include?( '+' )
    release_str, _, build_str = source.partition '+'
    pre_str = ''
  elsif source.include?( '-' )
    release_str, _, pre_str = source.partition '-'
    build_str = ''
  else
    release_str = source
    pre_str = build_str = ''
  end
  
  release_segs, pre_segs, build_segs = \
    [release_str, pre_str, build_str].map { |str|
      split_identifiers( str ).map &identifier_for_ref
    }
  
  # Check release segments length
  if strict && release_segs.length != 3
    raise NRSER::ArgumentError.new \
      "Strict SemVer versions *MUST* have at exactly 3 release segments",
      source: source,
      release_segments: release_segs
      
  elsif release_segs.length < 3
    raise NRSER::ArgumentError.new \
      "SemVer versions *MUST* have at lease 3 release segments",
      source: source,
      release_segments: release_segs
    
  end
  
  prop_values **{
    raw: source,
    major: release_segs[0],
    minor: release_segs[1],
    patch: release_segs[2],
    revision: release_segs[3..-1] || [],
    prerelease: pre_segs,
    build: build_segs,
  }.compact
end

.split_identifiers(string) ⇒ Object



82
83
84
# File 'lib/qb/package/version/from.rb', line 82

def self.split_identifiers string
  string.split QB::Package::Version::IDENTIFIER_SEPARATOR
end

.string(source) ⇒ QB::Package::Version

Parse string version into an instance. Accept Semver, Ruby Gem and Docker image tag formats.

Parameters:

  • source (#to_s)

    String version to parse.

Returns:



269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
# File 'lib/qb/package/version/from.rb', line 269

def self.string source
  source = source.to_s unless source.is_a?( String )
  
  t.non_empty_str.check source
  
  if source.include? '_'
    docker_tag source
  elsif ( source.include?( '-' ) ||
          source.include?( '+' ) ) &&
        source =~ /\A\d+\.\d+\.\d+/
    semver source
  else
    gemver source
  end
end

Instance Method Details

#npm_version(source) ⇒ Object



243
244
245
# File 'lib/qb/package/version/from.rb', line 243

def npm_version source
  semver source, strict: true
end