:toc: macro :toclevels: 5 :figure-caption!:

:option_parser_link: https://alchemists.io/articles/ruby_option_parser[OptionParser] :semver_link: https://semver.org[Semantic Versioning] :strict_semver_link: https://alchemists.io/articles/strict_semantic_versioning[Strict Semantic Versioning]

Versionaire

Ruby doesn’t provide a primitive version type by default so Versionaire fills this gap by providing immutable and thread-safe {strict_semver_link} so you can leverage versions within your applications. This new ‘Version` type behaves and feels a lot like other primitives (i.e. `String`, `Array`, `Hash`, `Proc`, etc) and can be cast/converted from other primitives.

toc::[]

Features

Requirements

. www.ruby-lang.org[Ruby].

Setup

To install with security, run:

source,bash

# 💡 Skip this line if you already have the public certificate installed. gem cert –add <(curl –compressed –location alchemists.io/gems.pem) gem install versionaire –trust-policy HighSecurity


To install without security, run:

source,bash

gem install versionaire


You can also add the gem directly to your project:

source,bash

bundle add versionaire


Once the gem is installed, you only need to require it:

source,ruby

require “versionaire”


Usage

Initialization

A new version can be initialized in a variety of ways:

source,ruby

Versionaire::Version.new # “0.0.0” Versionaire::Version[major: 1] # “1.0.0” Versionaire::Version[major: 1, minor: 2] # “1.2.0” Versionaire::Version[major: 1, minor: 2, patch: 3] # “1.2.3”


Equality

Value (‘+#==+`)

Equality is determined by the state of the object. This means that a version is equal to another version as long as all of the values (i.e. state) are equal to each other. Example:

source,ruby

version_a = Versionaire::Version[major: 1] version_b = Versionaire::Version[major: 2] version_c = Versionaire::Version[major: 1]

version_a == version_a # true version_a == version_b # false version_a == version_c # true


Knowing this, versions can be compared against one another too:

source,ruby

version_a > version_b # false version_a < version_b # true version_a.between? version_c, version_b # true


Hash (‘#eql?`)

Behaves exactly as ‘#==`.

Case (‘#===`)

Behaves exactly as ‘#==`.

Identity (‘#equal?`)

Works like any other standard Ruby object where an object is equal only to itself.

source,ruby

version_a = Versionaire::Version[major: 1] version_b = Versionaire::Version[major: 2] version_c = Versionaire::Version[major: 1]

version_a.equal? version_a # true version_a.equal? version_b # false version_a.equal? version_c # false


Conversions

Function

Use the ‘Versionaire::Version` function to explicitly cast to a version:

source,ruby

version = Versionaire::Version[major: 1]

Versionaire::Version “1.0.0” Versionaire::Version [1, 0, 0] Versionaire::Version major: 1, minor: 0, patch: 0 Versionaire::Version version


Each of these conversions will result in a version object that represents “‘1.0.0`”.

When attempting to convert an unsupported type, a ‘Versionaire::Error` exception will be thrown.

Refinement

Building upon the above examples, a more elegant solution is to use a https://alchemists.io/articles/ruby_refinements[refinement]:

source,ruby

using Versionaire::Cast

version = Versionaire::Version[major: 1]

Version “1.0.0” Version [1, 0, 0] Version major: 1, minor: 0, patch: 0 Version version


By adding ‘using Versionaire::Cast` to your implementation, this allows Versionaire to refine `Kernel` so you have a top-level `Version` conversion function much like Kernel’s native support for ‘Integer`, `String`, `Array`, `Hash`, etc. The benefit to this approach is to reduce the amount of typing so you don’t pollute your entire object space, like a monkey patch, while providing an idiomatic approach to casting like any other primitive.

Implicit

Implicit conversion to a ‘String` is supported:

source,ruby

“1.0.0”.match Versionaire::Version[major: 1] # <MatchData “1.0.0”>


Explicit

Explicit conversion to a ‘String`, `Array`, `Hash`, or `Proc` is supported:

source,ruby

version = Versionaire::Version.new

version.to_s # “0.0.0” version.to_a # [0, 0, 0] version.to_h # {major: 0, minor: 0, patch: 0} version.to_proc #


To elaborate on procs, this means the following is possible where you might want to collect all minor verions values or make use of version information in other useful ways:

source,ruby

using Versionaire::Cast

version = Version “1.2.3”

version.to_proc.call :major # 1

version, version, version].map(&:minor) # [2, 2, 2

Inspections

You can inspect a version which is the equivalent of an escaped string representation. Example:

source,ruby

using Versionaire::Cast

Version(“1.2.3”).inspect # “"1.2.3"”


Comparisons

All versions are comparable which means any of the operators from the ‘Comparable` module will work. Example:

source,ruby

version_1 = Versionaire::Version “1.0.0” version_2 = Versionaire::Version “2.0.0”

version_1 < version_2 # true version_1 <= version_2 # true version_1 == version_2 # false (see Equality section above for details) version_1 > version_2 # false version_1 >= version_2 # false version_1.between? version_1, version_2 # true version_1.clamp version_1, version_2 # version_1 (added in Ruby 2.4.0)


Bumping

Versions can be bumped to next logical version with respect current version. Example:

source,ruby

version = Versionaire::Version.new # “0.0.0” version.bump :patch # “0.0.1” version.bump :minor # “0.1.0” version.bump :major # “1.0.0”

Versionaire::Version[major: 1, minor: 2, patch: 3].bump :major # “2.0.0” Versionaire::Version[major: 1, minor: 2, patch: 3].bump :minor # “1.3.0” Versionaire::Version[major: 1, minor: 2, patch: 3].bump :patch # “1.2.4”


You’ll notice, when bumping the major or minor versions, lower precision gets zeroed out in order to provide the next logical version.

Math

Versions can be added, subtracted, sequentially increased, or sequentially decreased from each other.

Addition

Versions can be added together to produce a resulting version sum.

source,ruby

version_1 = Versionaire::Version[major: 1, minor: 2, patch: 3] version_2 = Versionaire::Version[major: 2, minor: 5, patch: 7] version_1 + version_2 # “3.7.10”


Subtraction

Versions can be substracted from each other as long as there isn’t a negative result.

source,ruby

version_1 = Versionaire::Version[major: 1, minor: 2, patch: 3] version_2 = Versionaire::Version[major: 1, minor: 1, patch: 1] version_1 - version_2 # “0.1.2”

version_1 = Versionaire::Version[major: 1] version_2 = Versionaire::Version[major: 5] version_1 - version_2 # Versionaire::Error


Up

Versions can be sequentially increased or given a specific version to jump to.

source,ruby

version = Versionaire::Version[major: 1, minor: 1, patch: 1] version.up :major # => “2.1.1” version.up :major, 3 # => “4.1.1” version.up :minor # => “1.2.1” version.up :minor, 3 # => “1.4.1” version.up :patch # => “1.1.2” version.up :patch, 3 # => “1.1.4”


Down

Versions can be sequentially decreased or given a specific version to jump to as long as the result is not negative.

source,ruby

version = Versionaire::Version[major: 5, minor: 5, patch: 5] version.down :major # => “4.5.5” version.down :major, 3 # => “2.5.5” version.down :minor # => “5.4.5” version.down :minor, 3 # => “5.2.5” version.down :patch # => “5.5.4” version.down :patch, 3 # => “5.5.2” version.down :major, 6 # => Versionaire::Error


Extensions

This project supports libraries which might desire native ‘Version` types. Each extension _must be explicitly required_ in order to be used since they are optional by default. See below for details.

OptionParser

{option_parser_link} is one of Ruby’s https://stdgems.org[default gems] which can accept additional types not native to Ruby by default. To extend ‘OptionParser` with the `Version` type, all you need to do is add these two lines to your implementation:

. ‘require “versionaire/extensions/option_parser”`: This will load dependencies and register the `Version` type with `OptionParser`. . `act.on “–tag VERSION”, Versionaire::Version`: Specifying `Versionaire::Version` as the second argument will ensure `OptionParser` properly casts command line input as a `Version` type.

Here’s an example implementation that demonstrates full usage:

source,ruby

require “versionaire/extensions/option_parser”

options = {}

parser = OptionParser.new do |act|

act.on "--tag VERSION", Versionaire::Version, "Casts to version." do |value|
  options[:version] = value
end

end

parser.parse %w[–tag 1.2.3] puts options


The above will ensure ‘–tag 1.2.3` is parsed as `{version: “1.2.3”}` within your `options` variable. Should `OptionParser` parse an invalid version, you’ll get a ‘OptionParser::InvalidArgument` instead.

Development

To contribute, run:

source,bash

git clone github.com/bkuhlmann/versionaire cd versionaire bin/setup


You can also use the IRB console for direct access to all objects:

source,bash

bin/console


Tests

To test, run:

source,bash

bin/rake


Credits