Permalink
Browse files

Move versionable out into a library.

The jist:

class Versioned
  include Versionable

  def foo; :past_foo; end
  def baz; :baz; end

  version "1"

  def foo; :foo; end
  def self.bar; :bar; end

  version "2" do
    def foo; :future_foo; end
  end

  version "3" do
    def foo; :far_future_foo; end
  end
end

Versioned['0'].new.foo                  # => :past_foo
Versioned['1'].new.foo                  # => :foo
Versioned['2'].new.foo                  # => :future_foo
Versioned['3'].new.foo                  # => :far_future_foo
Versioned == Versioned['1']             # => true
Versioned['>= 1'] == Versioned['3']     # => true
Versioned['< 3'] == Versioned['2']      # => true

Turns out Class#dup can do some crazy things.

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.
  • Loading branch information...
phs committed Apr 22, 2010
1 parent eaa09b7 commit 9d799c00a5432384d4a48c186b2ffd7987076273
View
@@ -1,4 +1,4 @@
Copyright (c) 2009 Phil Smith
Copyright (c) 2010 Phil Smith
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
View
@@ -1,6 +1,6 @@
= versionable
Description goes here.
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.
== Note on Patches/Pull Requests
View
@@ -0,0 +1,31 @@
module Versionable
class VersioningError < StandardError
end
end
require 'versionable/version_number'
require 'versionable/versions'
module Versionable
def self.included(base)
base.extend ClassMethods
end
module ClassMethods
def version(version_number, &block)
versions.build(version_number, block)
end
def [](version_requirement)
versions.find(version_requirement)
end
def versions
@versions ||= Versions.new(self)
end
end
end
@@ -0,0 +1,37 @@
module Versionable
class VersionNumber < String
REGEX = /^\d+(?:\.\d+)*$/
def initialize(v)
v = v.to_s
raise ArgumentError.new "#{v.inspect} is not a dotted sequence of positive integers" unless v =~ REGEX
super v
end
def <=>(other)
my_segments, their_segments = to_s.split('.'), other.to_s.split('.')
size_difference = my_segments.size - their_segments.size
if size_difference > 0
their_segments.concat(['0'] * size_difference)
elsif size_difference < 0
my_segments.concat(['0'] * -size_difference)
end
partwise = my_segments.zip(their_segments).collect { |mine, theirs| mine.to_i <=> theirs.to_i }
partwise.detect { |cmp| cmp != 0 } || 0
end
def ==(other)
(self <=> other) == 0
end
def next
self.class.new(split('.', 2).first.to_i + 1)
end
def hash
sub(/(\.0+)+$/, '').to_s.hash
end
end
end
@@ -0,0 +1,65 @@
module Versionable
class Versions
def initialize(versioned_class)
@latest_version = versioned_class
end
def build(version_number, block)
version_number = VersionNumber.new(version_number)
raise ArgumentError.new "Can't bump to #{version_number} from #{latest_version_number}" unless latest_version_number < version_number
raise VersioningError.new "Once called with a block, all subsequent versions must pass a block." if default_version and not block
if block and default_version.nil?
self.default_version = latest_version
self.latest_version = latest_version.dup
versions[latest_version_number] = default_version
else
versions[latest_version_number] = latest_version.dup
end
latest_version.module_eval &block if block
self.latest_version_number = version_number
end
def find(version_requirement)
if version_requirement =~ VersionNumber::REGEX
versions[VersionNumber.new(version_requirement)]
elsif version_requirement =~ Versions::COMPARISON_REGEX
comparator, version_requirement = $1, VersionNumber.new($2)
comparator = '==' if comparator == '=' # stick that in your pipe and smoke it.
comparator = comparator.to_sym
match = (versions.keys + [latest_version_number]).select { |v| v.send comparator, version_requirement }.max
versions[match]
else
nil
end
end
private
COMPARISON_REGEX = /^(<|<=|=|>=|>)\s+(\d+(?:\.\d+)*)$/
attr_accessor :latest_version
attr_accessor :default_version
attr_writer :latest_version_number
def latest_version_number
@latest_version_number ||= VersionNumber.new("0")
end
def default_version_is_open?
end
def versions
@versions ||= Hash.new do |hash, key|
latest_version_number == key ? latest_version : nil
end
end
end
end
@@ -0,0 +1,44 @@
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
describe Versionable::VersionNumber do
def v(version)
Versionable::VersionNumber.new(version)
end
it "is a string" do
v("1.0").should be_a String
end
it "rejects versions that aren't dotted sequences of positive integers" do
lambda { v "dog" }.should raise_error ArgumentError
lambda { v "-1" }.should raise_error ArgumentError
end
describe "<=>" do
it "sorts by segments from left to right" do
v("10.0").should > v("2.0")
v("10.1").should < v("10.2")
end
it "right-pads missing segments with zeros" do
v("1").should == v("1.0")
v("1.0").should == v("1.0.0.0.0.0.0.0.0")
v("10").should > v("3.2.1")
end
end
describe "#next" do
it "bumps most significant segment by 1 and drops remainder" do
v("1.0").next.should == v("2")
end
end
describe "#hash" do
it "is the string hash after stripping trailing zero segments" do
v("1").hash.should == v("1.0.000").hash
v("1").hash.should_not == v("2").hash
end
end
end
View
@@ -1,7 +1,98 @@
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
describe "Versionable" do
it "fails" do
fail "hey buddy, you should probably rename this file and start specing for real"
describe Versionable do
it "does seemingly nothing by default" do
class Unmolested
include Versionable
def foo; :foo; end
end
Unmolested.new.foo.should == :foo
end
describe ".version" do
it "takes a version number" do
class Versioned
include Versionable
def foo; :past_foo; end
def baz; :baz; end
version "1"
def foo; :foo; end
def self.bar; :bar; end
end
end
it "rejects bad version numbers" do
lambda do
class Versioned
version "chowder!"
end
end.should raise_error ArgumentError
end
it "rejects non-increasing version numbers" do
lambda do
class Versioned; version "0"; end
end.should raise_error ArgumentError
lambda do
class Versioned; version "1"; end
end.should raise_error ArgumentError
end
it "takes a block for versions after the default" do
class Versioned
version "2" do
def foo; :future_foo; end
end
version "3" do
def foo; :far_future_foo; end
end
end
end
it "rejects blockless calls after block ones" do
lambda do
class Versioned; version "4"; end
end.should raise_error Versionable::VersioningError
end
end
describe "[]" do
it "sends versions to classes" do
Unmolested['0'].should be_a Class
end
it "sends the default version to itself" do
Unmolested['0'].should == Unmolested
Versioned['1'].should == Versioned
end
it "sends other versions to similar classes, representing older or newer functionality" do
Versioned['0'].should_not == Versioned
Versioned['0'].new.foo.should == :past_foo
Versioned['1'].bar.should == :bar
lambda { Versioned['0'].bar }.should raise_error NoMethodError
Versioned['2'].should_not == Versioned
Versioned['2'].new.foo.should == :future_foo
Versioned['3'].new.foo.should == :far_future_foo
end
it "takes comparator expressions" do
Versioned[">= 0"].should == Versioned['3']
Versioned["<= 2"].should == Versioned['2']
Versioned["< 2"].should == Versioned['1']
Versioned["= 0"].should == Versioned['0']
Versioned["> 0"].should == Versioned['3']
end
end
end

0 comments on commit 9d799c0

Please sign in to comment.