versionable 0.1.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.
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Phil Smith
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,55 @@
1
+ = Versionable
2
+
3
+ Versionable lets a ruby module or class declare multiple numbered versions of itself, and provides a way to select one based on a gem-like requirement.
4
+
5
+ The jist:
6
+ class Versioned
7
+ include Versionable
8
+
9
+ def foo; :past_foo; end
10
+ def baz; :baz; end
11
+
12
+ version "0.5"
13
+
14
+ def foo; :foo; end
15
+ def self.bar; :bar; end
16
+
17
+ version "1" do
18
+ def foo; :future_foo; end
19
+ end
20
+
21
+ version "2" do
22
+ def foo; :far_future_foo; end
23
+ end
24
+ end
25
+
26
+ And then:
27
+ Versioned['0'].new.foo # => :past_foo
28
+ Versioned['0.5'].new.foo # => :foo
29
+ Versioned['1'].new.foo # => :future_foo
30
+ Versioned['2'].new.foo # => :far_future_foo
31
+ Versioned == Versioned['0.5'] # => true
32
+ Versioned['>= 1'] == Versioned['2'] # => true
33
+ Versioned['< 1'] == Versioned['0.5'] # => true
34
+
35
+ It turns out <tt>Class#dup</tt> can do some crazy things. Each version is cloned from the previous and then includes its own changes. This means <tt>def self.class_methods()</tt> and <tt>@@class_variables</tt> end up versioned as well; not just instance methods.
36
+
37
+ The default version (the one you get without a [requirement]) is determined by the use of blocks passed to the +version+ calls. The last call without a block is the default one.
38
+
39
+ == Note on Patches/Pull Requests
40
+
41
+ * Fork the project.
42
+ * Make your feature addition or bug fix.
43
+ * Add tests for it. This is important so I don't break it in a
44
+ future version unintentionally.
45
+ * Commit, do not mess with rakefile, version, or history.
46
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
47
+ * Send me a pull request. Bonus points for topic branches.
48
+
49
+ == Thanks
50
+
51
+ Thanks to SEOmoz (http://seomoz.org) for letting me build this at my desk in the afternoons instead of on the couch in the middle of the night ^_^.
52
+
53
+ == Copyright
54
+
55
+ Copyright (c) 2010 Phil Smith. See LICENSE for details.
@@ -0,0 +1,45 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "versionable"
8
+ gem.summary = %Q{An api for versioning ruby modules}
9
+ gem.description = %Q{Versionable lets a ruby module or class declare multiple numbered versions of itself, and provides a way to select one based on a gem-like requirement.}
10
+ gem.email = "phil.h.smith@gmail.com"
11
+ gem.homepage = "http://github.com/phs/versionable"
12
+ gem.authors = ["Phil Smith"]
13
+ gem.add_development_dependency "rspec", ">= 1.2.9"
14
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
15
+ end
16
+ Jeweler::GemcutterTasks.new
17
+ rescue LoadError
18
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
19
+ end
20
+
21
+ require 'spec/rake/spectask'
22
+ Spec::Rake::SpecTask.new(:spec) do |spec|
23
+ spec.libs << 'lib' << 'spec'
24
+ spec.spec_files = FileList['spec/**/*_spec.rb']
25
+ end
26
+
27
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
28
+ spec.libs << 'lib' << 'spec'
29
+ spec.pattern = 'spec/**/*_spec.rb'
30
+ spec.rcov = true
31
+ end
32
+
33
+ task :spec => :check_dependencies
34
+
35
+ task :default => :spec
36
+
37
+ require 'rake/rdoctask'
38
+ Rake::RDocTask.new do |rdoc|
39
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
40
+
41
+ rdoc.rdoc_dir = 'rdoc'
42
+ rdoc.title = "versionable #{version}"
43
+ rdoc.rdoc_files.include('README*')
44
+ rdoc.rdoc_files.include('lib/**/*.rb')
45
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,41 @@
1
+ module Versionable
2
+ # A StandardError representing an error when creating versions.
3
+ class VersioningError < StandardError
4
+ end
5
+ end
6
+
7
+ require 'versionable/version_number'
8
+ require 'versionable/versions'
9
+
10
+ # A Mixin enabling versioning of the mixer. See ClassMethods.
11
+ module Versionable
12
+
13
+ def self.included(base)
14
+ base.extend ClassMethods
15
+ end
16
+
17
+ # Provides methods for creating and accessing versions of the class or module.
18
+ module ClassMethods
19
+
20
+ # Build a new version of the class or module.
21
+ #
22
+ # See Versions#build.
23
+ def version(version_number, &block)
24
+ versions.build(version_number, &block)
25
+ end
26
+
27
+ # Find a version by number or with a requirement, such as with VersionedClass["< 3.0"].
28
+ #
29
+ # See Versions#find.
30
+ def [](version_requirement)
31
+ versions.find(version_requirement)
32
+ end
33
+
34
+ # Get the Versions collection on this class or module.
35
+ def versions
36
+ @versions ||= Versions.new(self)
37
+ end
38
+
39
+ end
40
+
41
+ end
@@ -0,0 +1,55 @@
1
+ module Versionable
2
+ # A String containing a dotted sequence of positive integers.
3
+ class VersionNumber < String
4
+ VERSION_NUMBER_REGEX = /^(?:0|[1-9]\d*)(?:\.(?:0|[1-9]\d*))*$/
5
+
6
+ # Construct a VersionNumber from something that responds to <tt>#to_s</tt>.
7
+ #
8
+ # If the to_s'd argument does not match VERSION_NUMBER_REGEX, an ArgumentError is raised.
9
+ def initialize(v)
10
+ v = v.to_s
11
+ raise ArgumentError.new "#{v.inspect} is not a dotted sequence of positive integers" unless v =~ VERSION_NUMBER_REGEX
12
+ super v
13
+ end
14
+
15
+ # Compare version numbers as lists of integers.
16
+ #
17
+ # If one version number has more segments than the other, the shorter one is right-extended with zeros.
18
+ #
19
+ # For example, when comparing 0.2 to 0.10.1, the 0.2 is extended to 0.2.0. The left two 0s are equal, so comparison shifts to 2 and 10: 0.10.1 is the greater version.
20
+ def <=>(other)
21
+ my_segments, their_segments = to_s.split('.'), other.to_s.split('.')
22
+ size_difference = my_segments.size - their_segments.size
23
+
24
+ if size_difference > 0
25
+ their_segments.concat(['0'] * size_difference)
26
+ elsif size_difference < 0
27
+ my_segments.concat(['0'] * -size_difference)
28
+ end
29
+
30
+ partwise = my_segments.zip(their_segments).collect { |mine, theirs| mine.to_i <=> theirs.to_i }
31
+ partwise.detect { |cmp| cmp != 0 } || 0
32
+ end
33
+
34
+ # Test equality based on the <=> operator.
35
+ #
36
+ # Particularly, 2 == 2.0 == 2.0.0.0
37
+ def ==(other)
38
+ (self <=> other) == 0
39
+ end
40
+
41
+ # Bump to the next most-significant version number.
42
+ #
43
+ # VersionNumber.new("2.1").next.to_s # => "3"
44
+ def next
45
+ self.class.new(split('.', 2).first.to_i + 1)
46
+ end
47
+
48
+ # Hash the version number string after stripping any trailing zeroes.
49
+ #
50
+ # This preserves the hash/equality contract: 2 == 2.0, so they both hash the same as well.
51
+ def hash
52
+ sub(/(\.0)+$/, '').to_s.hash
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,75 @@
1
+ module Versionable
2
+ # A container for all versions of a given module.
3
+ class Versions
4
+ COMPARISON_REGEX = /^(<|<=|=|>=|>)\s+((?:0|[1-9]\d*)(?:\.(?:0|[1-9]\d*))*)$/
5
+
6
+ # Construct a Versions recording versions of the passed module.
7
+ def initialize(versioned_module)
8
+ @latest_version = versioned_module
9
+ end
10
+
11
+ # Build and store a new version with the given number.
12
+ #
13
+ # Versions must be built in increasing order: if version_number is not greater than the previous version, an ArgumentError is raised.
14
+ # The initial version number is always 0.
15
+ #
16
+ # If a block parameter is passed, it is included in the created version.
17
+ # If this is the first time a block is passed, then record the _previous_ version as the default one.
18
+ # Once build is called with a block, subsequent versions must also pass a block or a VersioningError will be raised.
19
+ def build(version_number, &block)
20
+ version_number = VersionNumber.new(version_number)
21
+
22
+ raise ArgumentError.new "Can't bump to #{version_number} from #{latest_version_number}" unless latest_version_number < version_number
23
+ raise VersioningError.new "Once called with a block, all subsequent versions must pass a block." if default_version and not block
24
+
25
+ if block and default_version.nil?
26
+ self.default_version = latest_version
27
+ self.latest_version = latest_version.dup
28
+
29
+ versions[latest_version_number] = default_version
30
+ else
31
+ versions[latest_version_number] = latest_version.dup
32
+ end
33
+
34
+ self.latest_version_number = version_number
35
+ latest_version.module_eval &block if block
36
+ end
37
+
38
+ # Find the maximal version satisifying the given requirement.
39
+ #
40
+ # The requirement may be a version string such as "1.0.7", or a comparator followed by a version such as "< 3.0".
41
+ #
42
+ # Returns the matching module if found, nil otherwise.
43
+ def find(version_requirement)
44
+ if version_requirement =~ VersionNumber::VERSION_NUMBER_REGEX
45
+ versions[VersionNumber.new(version_requirement)]
46
+ elsif version_requirement =~ Versions::COMPARISON_REGEX
47
+ comparator, version_requirement = $1, VersionNumber.new($2)
48
+ comparator = '==' if comparator == '=' # stick that in your pipe and smoke it.
49
+ comparator = comparator.to_sym
50
+
51
+ match = (versions.keys + [latest_version_number]).select { |v| v.send comparator, version_requirement }.max
52
+
53
+ versions[match]
54
+ else
55
+ nil
56
+ end
57
+ end
58
+
59
+ private
60
+
61
+ attr_accessor :latest_version # :nodoc:
62
+ attr_accessor :default_version # :nodoc:
63
+ attr_writer :latest_version_number # :nodoc:
64
+
65
+ def latest_version_number
66
+ @latest_version_number ||= VersionNumber.new("0")
67
+ end
68
+
69
+ def versions
70
+ @versions ||= Hash.new do |hash, key|
71
+ latest_version_number == key ? latest_version : nil
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1 @@
1
+ --color
@@ -0,0 +1,9 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+ require 'versionable'
4
+ require 'spec'
5
+ require 'spec/autorun'
6
+
7
+ Spec::Runner.configure do |config|
8
+
9
+ end
@@ -0,0 +1,44 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe Versionable::VersionNumber do
4
+
5
+ def v(version)
6
+ Versionable::VersionNumber.new(version)
7
+ end
8
+
9
+ it "is a string" do
10
+ v("1.0").should be_a String
11
+ end
12
+
13
+ it "rejects versions that aren't dotted sequences of positive integers" do
14
+ lambda { v "dog" }.should raise_error ArgumentError
15
+ lambda { v "-1" }.should raise_error ArgumentError
16
+ end
17
+
18
+ describe "<=>" do
19
+ it "sorts by segments from left to right" do
20
+ v("10.0").should > v("2.0")
21
+ v("10.1").should < v("10.2")
22
+ end
23
+
24
+ it "right-pads missing segments with zeros" do
25
+ v("1").should == v("1.0")
26
+ v("1.0").should == v("1.0.0.0.0.0.0.0.0")
27
+ v("10").should > v("3.2.1")
28
+ end
29
+ end
30
+
31
+ describe "#next" do
32
+ it "bumps most significant segment by 1 and drops remainder" do
33
+ v("1.0").next.should == v("2")
34
+ end
35
+ end
36
+
37
+ describe "#hash" do
38
+ it "is the string hash after stripping trailing zero segments" do
39
+ v("1").hash.should == v("1.0.0.0.0").hash
40
+ v("1").hash.should_not == v("2").hash
41
+ end
42
+ end
43
+
44
+ end
@@ -0,0 +1,104 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe Versionable do
4
+
5
+ it "does seemingly nothing by default" do
6
+ class Unmolested
7
+ include Versionable
8
+ def foo; :foo; end
9
+ end
10
+
11
+ Unmolested.new.foo.should == :foo
12
+ end
13
+
14
+ describe ".version" do
15
+ it "takes a version number" do
16
+ class Versioned
17
+ include Versionable
18
+
19
+ def foo; :past_foo; end
20
+ def baz; :baz; end
21
+
22
+ version "1"
23
+
24
+ def foo; :foo; end
25
+ def self.bar; :bar; end
26
+ end
27
+ end
28
+
29
+ it "rejects bad version numbers" do
30
+ lambda do
31
+ class Versioned
32
+ version "chowder!"
33
+ end
34
+ end.should raise_error ArgumentError
35
+ end
36
+
37
+ it "rejects non-increasing version numbers" do
38
+ lambda do
39
+ class Versioned; version "0"; end
40
+ end.should raise_error ArgumentError
41
+
42
+ lambda do
43
+ class Versioned; version "1"; end
44
+ end.should raise_error ArgumentError
45
+ end
46
+
47
+ it "takes a block for versions after the default" do
48
+ class Versioned
49
+ version "2" do
50
+ def foo; :future_foo; end
51
+ end
52
+ end
53
+ end
54
+
55
+ it "returns the value of its block, if any" do
56
+ class Versioned
57
+ (version "3" do
58
+ def foo; :far_future_foo; end
59
+
60
+ :returned
61
+ end).should == :returned
62
+ end
63
+ end
64
+
65
+ it "rejects blockless calls after block ones" do
66
+ lambda do
67
+ class Versioned; version "4"; end
68
+ end.should raise_error Versionable::VersioningError
69
+ end
70
+ end
71
+
72
+ describe "[]" do
73
+ it "sends versions to classes" do
74
+ Unmolested['0'].should be_a Class
75
+ end
76
+
77
+ it "sends the default version to itself" do
78
+ Unmolested['0'].should == Unmolested
79
+ Versioned['1'].should == Versioned
80
+ end
81
+
82
+ it "sends other versions to similar classes, representing older or newer functionality" do
83
+ Versioned['0'].should_not == Versioned
84
+ Versioned['0'].new.foo.should == :past_foo
85
+
86
+ Versioned['1'].bar.should == :bar
87
+ lambda { Versioned['0'].bar }.should raise_error NoMethodError
88
+
89
+ Versioned['2'].should_not == Versioned
90
+ Versioned['2'].new.foo.should == :future_foo
91
+
92
+ Versioned['3'].new.foo.should == :far_future_foo
93
+ end
94
+
95
+ it "takes comparator expressions" do
96
+ Versioned[">= 0"].should == Versioned['3']
97
+ Versioned["<= 2"].should == Versioned['2']
98
+ Versioned["< 2"].should == Versioned['1']
99
+ Versioned["= 0"].should == Versioned['0']
100
+ Versioned["> 0"].should == Versioned['3']
101
+ end
102
+ end
103
+
104
+ end
@@ -0,0 +1,59 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{versionable}
8
+ s.version = "0.1.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Phil Smith"]
12
+ s.date = %q{2010-04-22}
13
+ s.description = %q{Versionable lets a ruby module or class declare multiple numbered versions of itself, and provides a way to select one based on a gem-like requirement.}
14
+ s.email = %q{phil.h.smith@gmail.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE",
17
+ "README.rdoc"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ ".gitignore",
22
+ "LICENSE",
23
+ "README.rdoc",
24
+ "Rakefile",
25
+ "VERSION",
26
+ "lib/versionable.rb",
27
+ "lib/versionable/version_number.rb",
28
+ "lib/versionable/versions.rb",
29
+ "spec/spec.opts",
30
+ "spec/spec_helper.rb",
31
+ "spec/versionable/version_number_spec.rb",
32
+ "spec/versionable_spec.rb",
33
+ "versionable.gemspec"
34
+ ]
35
+ s.homepage = %q{http://github.com/phs/versionable}
36
+ s.rdoc_options = ["--charset=UTF-8"]
37
+ s.require_paths = ["lib"]
38
+ s.rubygems_version = %q{1.3.6}
39
+ s.summary = %q{An api for versioning ruby modules}
40
+ s.test_files = [
41
+ "spec/spec_helper.rb",
42
+ "spec/versionable/version_number_spec.rb",
43
+ "spec/versionable_spec.rb"
44
+ ]
45
+
46
+ if s.respond_to? :specification_version then
47
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
48
+ s.specification_version = 3
49
+
50
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
51
+ s.add_development_dependency(%q<rspec>, [">= 1.2.9"])
52
+ else
53
+ s.add_dependency(%q<rspec>, [">= 1.2.9"])
54
+ end
55
+ else
56
+ s.add_dependency(%q<rspec>, [">= 1.2.9"])
57
+ end
58
+ end
59
+
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: versionable
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 0
9
+ version: 0.1.0
10
+ platform: ruby
11
+ authors:
12
+ - Phil Smith
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-04-22 00:00:00 -07:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: rspec
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 1
29
+ - 2
30
+ - 9
31
+ version: 1.2.9
32
+ type: :development
33
+ version_requirements: *id001
34
+ description: Versionable lets a ruby module or class declare multiple numbered versions of itself, and provides a way to select one based on a gem-like requirement.
35
+ email: phil.h.smith@gmail.com
36
+ executables: []
37
+
38
+ extensions: []
39
+
40
+ extra_rdoc_files:
41
+ - LICENSE
42
+ - README.rdoc
43
+ files:
44
+ - .document
45
+ - .gitignore
46
+ - LICENSE
47
+ - README.rdoc
48
+ - Rakefile
49
+ - VERSION
50
+ - lib/versionable.rb
51
+ - lib/versionable/version_number.rb
52
+ - lib/versionable/versions.rb
53
+ - spec/spec.opts
54
+ - spec/spec_helper.rb
55
+ - spec/versionable/version_number_spec.rb
56
+ - spec/versionable_spec.rb
57
+ - versionable.gemspec
58
+ has_rdoc: true
59
+ homepage: http://github.com/phs/versionable
60
+ licenses: []
61
+
62
+ post_install_message:
63
+ rdoc_options:
64
+ - --charset=UTF-8
65
+ require_paths:
66
+ - lib
67
+ required_ruby_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ segments:
72
+ - 0
73
+ version: "0"
74
+ required_rubygems_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ segments:
79
+ - 0
80
+ version: "0"
81
+ requirements: []
82
+
83
+ rubyforge_project:
84
+ rubygems_version: 1.3.6
85
+ signing_key:
86
+ specification_version: 3
87
+ summary: An api for versioning ruby modules
88
+ test_files:
89
+ - spec/spec_helper.rb
90
+ - spec/versionable/version_number_spec.rb
91
+ - spec/versionable_spec.rb