diff options
-rw-r--r-- | chef-config/lib/chef-config/package_task.rb | 4 | ||||
-rw-r--r-- | chef-config/lib/chef-config/version.rb | 4 | ||||
-rw-r--r-- | lib/chef/version.rb | 4 | ||||
-rw-r--r-- | lib/chef/version_string.rb | 172 | ||||
-rw-r--r-- | spec/unit/version_string_spec.rb | 119 |
5 files changed, 300 insertions, 3 deletions
diff --git a/chef-config/lib/chef-config/package_task.rb b/chef-config/lib/chef-config/package_task.rb index 6c4ca4f435..bb6c5ec725 100644 --- a/chef-config/lib/chef-config/package_task.rb +++ b/chef-config/lib/chef-config/package_task.rb @@ -220,9 +220,11 @@ module ChefConfig # task instead. #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +require 'chef/version_string' + #{class_or_module} #{module_name} #{module_name.upcase}_ROOT = File.expand_path("../..", __FILE__) - VERSION = "#{version}" + VERSION = Chef::VersionString.new("#{version}") end # diff --git a/chef-config/lib/chef-config/version.rb b/chef-config/lib/chef-config/version.rb index caa5cf1167..d600d5285c 100644 --- a/chef-config/lib/chef-config/version.rb +++ b/chef-config/lib/chef-config/version.rb @@ -19,9 +19,11 @@ # task instead. #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +require 'chef/version_string' + module ChefConfig CHEFCONFIG_ROOT = File.expand_path("../..", __FILE__) - VERSION = "13.1.21" + VERSION = Chef::VersionString.new("13.1.21") end # diff --git a/lib/chef/version.rb b/lib/chef/version.rb index 6bf1fb664b..b3949bd3f2 100644 --- a/lib/chef/version.rb +++ b/lib/chef/version.rb @@ -19,9 +19,11 @@ # task instead. #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +require 'chef/version_string' + class Chef CHEF_ROOT = File.expand_path("../..", __FILE__) - VERSION = "13.1.21" + VERSION = Chef::VersionString.new("13.1.21") end # diff --git a/lib/chef/version_string.rb b/lib/chef/version_string.rb new file mode 100644 index 0000000000..2777a635d7 --- /dev/null +++ b/lib/chef/version_string.rb @@ -0,0 +1,172 @@ +# Copyright:: Copyright 2017, Noah Kantrowitz +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +class Chef + # String-like object for version strings. + # + # @since 13.1 + # @api internal + class VersionString < String + # Parsed version object for the string. + # @return [Gem::Version] + attr_reader :parsed_version + + # Create a new VersionString from an input String. + # + # @param val [String] Version string to parse. + def initialize(val) + super + @parsed_version = ::Gem::Version.create(self) + end + + # @!group Compat wrappers for String + + # Compat wrapper for + to behave like a normal String. + # + # @param other [String] + # @return [String] + def +(other) + to_s + other + end + + # Compat wrapper for * to behave like a normal String. + # + # @param other [Integer] + # @return [String] + def *(other) + to_s * other + end + + # @!group Comparison operators + + # Compare a VersionString to an object. If compared to another VersionString + # then sort like `Gem::Version`, otherwise try to treat the other object as + # a version but fall back to normal string comparison. + # + # @param other [Object] + # @return [Integer] + def <=>(other) + other_ver = case other + when VersionString + other.pased_version + else + begin + Gem::Version.create(other.to_s) + rescue ArgumentError + # Comparing to a string that isn't a version. + return super + end + end + parsed_version <=> other_ver + end + + # Compat wrapper for == based on <=>. + # + # @param other [Object] + # @return [Boolean] + def ==(other) + (self <=> other) == 0 + end + + # Compat wrapper for != based on <=>. + # + # @param other [Object] + # @return [Boolean] + def !=(other) + (self <=> other) != 0 + end + + # Compat wrapper for < based on <=>. + # + # @param other [Object] + # @return [Boolean] + def <(other) + (self <=> other) < 0 + end + + # Compat wrapper for <= based on <=>. + # + # @param other [Object] + # @return [Boolean] + def <=(other) + (self <=> other) < 1 + end + + # Compat wrapper for > based on <=>. + # + # @param other [Object] + # @return [Boolean] + def >(other) + (self <=> other) > 0 + end + + # Compat wrapper for >= based on <=>. + # + # @param other [Object] + # @return [Boolean] + def >=(other) + (self <=> other) > -1 + end + + # @!group Matching operators + + # Matching operator to support checking against a requirement string. + # + # @param other [Regexp, String] + # @return [Boolean] + # @example Match against a Regexp + # Chef::VersionString.new('1.0.0') =~ /^1/ + # @example Match against a requirement + # Chef::VersionString.new('1.0.0') =~ '~> 1.0' + def =~(other) + case other + when Regexp + super + else + Gem::Requirement.create(other) =~ parsed_version + end + end + + # Grouping operator to support fancy `case` blocks. Requires the String + # monkeypatch below to function. + # + # @param other [Object] + # @return [Boolean] + # @example + # case Chef::VersionString.new('1.0.0') + # when '~> 2.0' + # :two + # when '~> 1.0' + # :one + # end + def ===(other) + self =~ other + rescue ArgumentError + super + end + + end +end + +# Monkeypatch string grouping to allow the nice case syntax shown above. +String.prepend(Module.new do + def ===(other) + if other.is_a?(Chef::VersionString) + other === self + else + super + end + end +end) diff --git a/spec/unit/version_string_spec.rb b/spec/unit/version_string_spec.rb new file mode 100644 index 0000000000..7e4a9246f9 --- /dev/null +++ b/spec/unit/version_string_spec.rb @@ -0,0 +1,119 @@ +# Copyright:: Copyright 2017, Noah Kantrowitz +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +require "spec_helper" +require "chef/version_string" + +describe Chef::VersionString do + let(:input) { '1.2.3' } + subject(:described_object) { described_class.new(input) } + + it { is_expected.to eq '1.2.3' } + it { is_expected.to eql '1.2.3' } + it { is_expected.to be == '1.2.3' } + it { is_expected.to be < 'abc' } + it { is_expected.to be > '0' } + + context 'with !=' do + subject { described_object != '1.2.4' } + it { is_expected.to be true } + end + + context 'with +' do + subject { described_object + 'asdf' } + it { is_expected.to eq '1.2.3asdf' } + end + + context 'with *' do + subject { described_object * 3 } + it { is_expected.to eq '1.2.31.2.31.2.3' } + end + + context 'with version-like comparisons' do + subject { described_class.new('1.02.3') } + + it { is_expected.to eq '1.2.3' } + it { is_expected.to be > '1.2.2' } + it { is_expected.to be > '1.2.3a' } + it { is_expected.to be < '1.2.4' } + end + + context 'with =~ Regexp' do + subject { described_object =~ /^1/ } + it { is_expected.to eq 0 } + end + + context 'with =~ Requirement' do + subject { described_object =~ Gem::Requirement.create('~> 1.0') } + it { is_expected.to be true } + end + + context 'with =~ String' do + subject { described_object =~ '~> 1.0' } + it { is_expected.to be true } + end + + context 'with Regexp =~' do + subject { /^2/ =~ described_object } + it { is_expected.to be nil } + end + + context 'with String =~' do + subject { '~> 1.0' =~ described_object } + it { expect { subject }.to raise_error TypeError } + end + + context 'with case statement' do + subject do + case described_object + when 'abc' + :a + when '1.2.3' + :b + when /^2\.0/ + :c + when '~> 2.0' + :d + when '~> 1.0' + :e + end + end + + context 'with 1.2.3' do + let(:input) { '1.2.3' } + it { is_expected.to eq :b } + end + + context 'with 2.0.0' do + let(:input) { '2.0.0' } + it { is_expected.to eq :c } + end + + context 'with 2.1.0' do + let(:input) { '2.1.0' } + it { is_expected.to eq :d } + end + + context 'with 1.2.4' do + let(:input) { '1.2.4' } + it { is_expected.to eq :e } + end + + context 'with 0' do + let(:input) { '0' } + it { is_expected.to be nil } + end + end +end |