Permalink
Browse files

YAML support and various fixes

  • Loading branch information...
dazuma committed Nov 5, 2009
1 parent 5118922 commit 175d88df513b1748a52079d1c4ebc96690e2a126
View
@@ -1,14 +1,16 @@
=== 0.2.0 / 2009-11-05
+* API CHANGE: Slight change to value comparison semantics. Value#eql? returns true only if the schemas are the same, whereas Value#== and the greater than and less than comparisons attempt to compare the semantic value, and thus may perform automatic schema conversion on the RHS.
+* API CHANGE: Merged Formats namespace into Format. Versionomy::Formats is now a (deprecated) alias for Versionomy::Format.
+* API CHANGE: The delimiter parsing algorithm now defaults :extra_characters to :error instead of :ignore.
* Added a mechanism for converting from one format/schema to another.
* Added Rubygems format, along with conversions to and from the standard format.
-* Slight change to value comparison semantics. Value#eql? returns true only if the schemas are the same, whereas Value#== and the greater than and less than comparisons attempt to compare the semantic value, and thus may perform automatic schema conversion on the RHS.
* Values now include Comparable.
-* Values can now be marshalled and unmarshalled.
-* Schemas can now add custom methods to value objects.
+* Values can now be serialized using Marshal and YAML.
+* Schemas can now add custom methods to value objects. Added "prerelease?" and "release" methods to rubygems and standard format values.
* Added default field settings to schema DSL.
* Implemented #=== for schemas and formats.
-* Merged Formats namespace into Format. Versionomy::Formats is now a (deprecated) alias for Versionomy::Format.
+* Many minor bug fixes.
=== 0.1.3 / 2009-10-29
View
@@ -4,6 +4,31 @@ Versionomy is a generalized version number library.
It provides tools to represent, manipulate, parse, and compare version
numbers in the wide variety of versioning schemes in use.
+=== Version numbers done right?
+
+Let's be honest. Version numbers are not easy to deal with, and very
+seldom seem to be done right.
+Imagine the common case of testing the ruby version. Most of us, if we
+need to worry about Ruby VM compatibility, will do something like:
+
+ do_something if RUBY_VERSION >= "1.8.7"
+
+Treating the version number as a string is all well and good, until it
+isn't. The above code works for Ruby 1.8.6, 1.8.7, 1.8.8, and 1.9.1. But
+it will fail if the version is "1.8.10".
+
+There are a few version number classes out there that do better than
+treating version numbers as plain strings. Perhaps the most well known and
+often used is Gem::Version, part of rubygems. This class separates the
+version into fields and lets you manipulate and compare version numbers
+more robustly. It provides limited support for "prerelease" versions
+through using string-valued fields. However, it's still a little clumsy.
+A prerelease version has to be represented like this: "1.9.2.b.1" or
+"1.9.2.preview.2". Wouldn't it be nice to be able to parse more typical
+version number formats such as "1.9.2b1" and "1.9.2 preview-2"?
+
+Yes you can!
+
=== Some examples
require 'versionomy'
@@ -86,8 +111,9 @@ semantics for comparing, parsing, and modifying version numbers.
=== Requirements
-* Ruby 1.8.6 or later (1.8.7 recommended), Ruby 1.9.1 or later, or JRuby 1.4 or later.
-* blockenspiel gem.
+* Ruby 1.8.6 or later (1.8.7 recommended), Ruby 1.9.1 or later, or JRuby
+ 1.4 or later.
+* blockenspiel 0.3 or later.
=== Installation
@@ -51,6 +51,12 @@ module Versionomy
# it will use your conversion. You can register the same conversion object
# for multiple pairs of schemas, but you can register only one conversion
# object for any pair.
+ #
+ # A common technique for doing conversions is to unparse the version to a
+ # string, and then parse it in the new format. Versionomy provides a tool,
+ # Versionomy::Conversion::Parsing, for performing such conversions. The
+ # conversions between the standard and rubygems formats uses this tool.
+ # See Versionomy::Conversion::Rubygems for annotated examples.
module Conversion
@@ -106,17 +106,9 @@ def self.create_standard_to_rubygems
def self.create_rubygems_to_standard
# We'll use a parsing conversion to do this conversion.
- Conversion::Parsing.new do
-
- # The standard format generally will understand the rubygems
- # format, except that the standard format supports only four
- # fields (plus prerelease versions or patchlevel). So cause the
- # parser to error out if rubygems unparses too many fields.
- to_generate_parse_params do |convert_params_|
- {:extra_characters => :error}
- end
-
- end
+ # The standard format generally understands the rubygems format
+ # already, so we don't need to do any special configuration.
+ Conversion::Parsing.new
end
View
@@ -53,7 +53,7 @@ module Versionomy
# (e.g. alpha, beta, release candidate, etc.) forms and patchlevels.
#
# You may also create your own formats, either by implementing the
- # format contract (see Versionomy::Format::Base) or by using the
+ # format contract (see Versionomy::Format::Base), or by using the
# Versionomy::Format::Delimiter tool, which can be used to construct
# parsers for many version number formats.
#
@@ -88,11 +88,17 @@ def get(name_, strict_=false)
# Register the given format under the given name.
#
+ # Valid names may contain only letters, digits, underscores, dashes,
+ # and periods.
+ #
# Raises Versionomy::Errors::FormatRedefinedError if the name has
# already been defined.
def register(name_, format_)
name_ = name_.to_s
+ unless name_ =~ /^[\w.-]+$/
+ raise ::ArgumentError, "Illegal name: #{name_.inspect}"
+ end
if @names_to_formats.include?(name_)
raise Errors::FormatRedefinedError, name_
end
@@ -103,10 +109,17 @@ def register(name_, format_)
# Get the canonical name for the given format, as a string.
# This is the first name the format was registered under.
- # Returns nil if this format was never registered.
+ #
+ # If the given format was never registered, and strict is set to true,
+ # raises Versionomy::Errors::UnknownFormatError. If strict is set to
+ # false, returns nil if the given format was never registered.
- def canonical_name_for(format_)
- @formats_to_names[format_.object_id]
+ def canonical_name_for(format_, strict_=false)
+ name_ = @formats_to_names[format_.object_id]
+ if name_.nil? && strict_
+ raise Errors::UnknownFormatError
+ end
+ name_
end
@@ -44,12 +44,14 @@ module Format
#
# This format doesn't actually do anything useful. It causes all strings
# to parse to the schema's default value, and unparses all values to the
- # empty string.
+ # empty string. Instead, the purpose here is to define the API for a
+ # format.
#
- # Instead, the purpose here is to define the API for a format. All
- # formats must define the methods +schema+, +parse+, and +unparse+.
+ # All formats must define the methods +schema+, +parse+, and +unparse+.
# It is also recommended that formats define the <tt>===</tt> method,
- # though this is not strictly required.
+ # though this is not strictly required. Finally, formats may optionally
+ # implement <tt>uparse_for_serialize</tt>.
+ #
# Formats need not extend this base class, as long as they duck-type
# these methods.
@@ -101,6 +103,30 @@ def unparse(value_, params_=nil)
end
+ # An optional method that does unparsing especially for serialization.
+ # Implement this if normal unparsing is "lossy" and doesn't guarantee
+ # reconstruction of the version number. This method should attempt to
+ # unparse in such a way that the entire version value can be
+ # reconstructed from the unparsed string. Serialization routines will
+ # first attempt to call this method to unparse for serialization. If
+ # this method is not present, the normal unparse method will be used.
+ #
+ # Return either the unparsed string, or an array consisting of the
+ # unparsed string and a hash of parse params to pass to the parser
+ # when the string is to be reconstructed. You may also either return
+ # nil or raise Versionomy::Errors::UnparseError if the unparsing
+ # cannot be done satisfactorily for serialization. In this case,
+ # serialization will be done using the raw value data rather than an
+ # unparsed string.
+ #
+ # This default implementation just turns around and calls unparse.
+ # Thus it is equivalent to the method not being present at all.
+
+ def unparse_for_serialization(value_)
+ unparse(value_)
+ end
+
+
# Determine whether the given value uses this format.
def ===(obj_)
@@ -128,11 +128,11 @@ def schema
#
# <tt>:extra_characters</tt>::
# Determines what to do if the entire string cannot be consumed by
- # the parsing process. If set to <tt>:ignore</tt> (the default),
- # any extra characters are ignored. If set to <tt>:suffix</tt>,
- # the extra characters are set as the <tt>:suffix</tt> unparse
- # parameter and are thus appended to the end of the string when
- # unparsing takes place. If set to <tt>:error</tt>, causes a
+ # the parsing process. If set to <tt>:ignore</tt>, any extra
+ # characters are ignored. If set to <tt>:suffix</tt>, the extra
+ # characters are set as the <tt>:suffix</tt> unparse parameter and
+ # are thus appended to the end of the string when unparsing takes
+ # place. If set to <tt>:error</tt> (the default), causes a
# Versionomy::Errors::ParseError to be raised if there are
# uninterpreted characters.
@@ -152,10 +152,12 @@ def parse(string_, params_=nil)
end
if parse_params_[:string].length > 0
case parse_params_[:extra_characters]
- when :error
- raise Errors::ParseError, "Extra characters: #{parse_params_[:string].inspect}"
+ when :ignore
+ # do nothing
when :suffix
unparse_params_[:suffix] = parse_params_[:string]
+ else
+ raise Errors::ParseError, "Extra characters: #{parse_params_[:string].inspect}"
end
end
Value.new(values_, self, unparse_params_)
@@ -689,15 +691,15 @@ def setup(field_, value_regexp_, opts_)
@style = opts_[:style]
@default_value_optional = opts_[:default_value_optional]
@regexp_options = opts_[:case_sensitive] ? nil : ::Regexp::IGNORECASE
- @value_regexp = ::Regexp.new("^(#{value_regexp_})", @regexp_options)
+ @value_regexp = ::Regexp.new("\\A(#{value_regexp_})", @regexp_options)
regexp_ = opts_[:delimiter_regexp] || '\.'
- @delimiter_regexp = regexp_.length > 0 ? ::Regexp.new("^(#{regexp_})", @regexp_options) : nil
- @full_delimiter_regexp = regexp_.length > 0 ? ::Regexp.new("^(#{regexp_})$", @regexp_options) : nil
+ @delimiter_regexp = regexp_.length > 0 ? ::Regexp.new("\\A(#{regexp_})", @regexp_options) : nil
+ @full_delimiter_regexp = regexp_.length > 0 ? ::Regexp.new("\\A(#{regexp_})\\z", @regexp_options) : nil
regexp_ = opts_[:post_delimiter_regexp] || ''
- @post_delimiter_regexp = regexp_.length > 0 ? ::Regexp.new("^(#{regexp_})", @regexp_options) : nil
- @full_post_delimiter_regexp = regexp_.length > 0 ? ::Regexp.new("^(#{regexp_})$", @regexp_options) : nil
+ @post_delimiter_regexp = regexp_.length > 0 ? ::Regexp.new("\\A(#{regexp_})", @regexp_options) : nil
+ @full_post_delimiter_regexp = regexp_.length > 0 ? ::Regexp.new("\\A(#{regexp_})\\z", @regexp_options) : nil
regexp_ = opts_[:expected_follower_regexp] || ''
- @follower_regexp = regexp_.length > 0 ? ::Regexp.new("^(#{regexp_})", @regexp_options) : nil
+ @follower_regexp = regexp_.length > 0 ? ::Regexp.new("\\A(#{regexp_})", @regexp_options) : nil
@default_delimiter = opts_[:default_delimiter] || '.'
@default_post_delimiter = opts_[:default_post_delimiter] || ''
@requires_previous_field = opts_.fetch(:requires_previous_field, true)
@@ -931,7 +933,7 @@ def initialize(field_, opts_={}, &block_)
regexps_ = @mappings_in_order.map{ |map_| "(#{map_[0]})" }
setup(field_, regexps_.join('|'), opts_)
@mappings_in_order.each do |map_|
- map_[0] = ::Regexp.new("^(#{map_[0]})", @regexp_options)
+ map_[0] = ::Regexp.new("\\A(#{map_[0]})", @regexp_options)
end
end
@@ -123,7 +123,7 @@ def self.create
val_
else
val_ = val_.to_s
- if val_ =~ /^\d*$/
+ if val_ =~ /\A\d*\z/
val_.to_i
else
val_
@@ -358,7 +358,7 @@ def self.create
:delimiter_regexp => '(-|\.|\s?)p|-')
recognize_letter(:style => :letter, :default_delimiter => '',
:delimiter_regexp => '-|\.|\s?',
- :expected_follower_regexp => '$')
+ :expected_follower_regexp => '\z')
end
field(:patchlevel_minor) do
recognize_number(:default_value_optional => true)
@@ -91,6 +91,10 @@ def default_format=(format_)
# is not registered.
def create(values_=nil, format_=nil, unparse_params_=nil)
+ if format_.kind_of?(::Hash) && unparse_params_.nil?
+ unparse_params_ = format_
+ format_ = nil
+ end
if format_.kind_of?(::String) || format_.kind_of?(::Symbol)
format_ = Format.get(format_, true)
end
@@ -115,6 +119,10 @@ def create(values_=nil, format_=nil, unparse_params_=nil)
# May raise Versionomy::Errors::ParseError if parsing failed.
def parse(str_, format_=nil, parse_params_=nil)
+ if format_.kind_of?(::Hash) && parse_params_.nil?
+ parse_params_ = format_
+ format_ = nil
+ end
if format_.kind_of?(::String) || format_.kind_of?(::Symbol)
format_ = Format.get(format_, true)
end
View
@@ -34,6 +34,9 @@
;
+require 'yaml'
+
+
module Versionomy
@@ -112,23 +115,72 @@ def to_s
# Marshal this version number.
- def marshal_dump
- format_name_ = Format.canonical_name_for(@format)
- unless format_name_
- raise Errors::SerializationError, "Cannot marshal because the format is not registered"
+ def marshal_dump # :nodoc:
+ format_name_ = Format.canonical_name_for(@format, true)
+ unparsed_data_ = nil
+ if @format.respond_to?(:unparse_for_serialization)
+ unparsed_data_ = @format.unparse_for_serialization(self) rescue nil
end
- [format_name_, @unparse_params, values_array]
+ unparsed_data_ ||= @format.unparse(self) rescue nil
+ data_ = [format_name_]
+ case unparsed_data_
+ when ::Array
+ data_ << unparsed_data_[0]
+ data_ << unparsed_data_[1] if unparsed_data_[1]
+ when ::String
+ data_ << unparsed_data_
+ else
+ data_ << values_array
+ data_ << @unparse_params if @unparse_params
+ end
+ data_
end
# Unmarshal this version number.
- def marshal_load(data_)
- format_ = Format.get(data_[0])
- unless format_
- raise Errors::SerializationError, "Cannot unmarshal because the format is not registered"
+ def marshal_load(data_) # :nodoc:
+ format_ = Format.get(data_[0], true)
+ if data_[1].kind_of?(::String)
+ val_ = format_.parse(data_[1], data_[2])
+ initialize(val_.values_array, format_, val_.unparse_params)
+ else
+ initialize(data_[1], format_, data_[2])
+ end
+ end
+
+
+ yaml_as "tag:danielazuma.com,2009:version"
+
+
+ def self.yaml_new(klass_, tag_, data_) # :nodoc:
+ unless data_.kind_of?(::Hash)
+ raise ::YAML::TypeError, "Invalid version format: #{val_.inspect}"
+ end
+ format_ = Format.get(data_['format'], true)
+ value_ = data_['value']
+ if value_
+ format_.parse(value_, data_['parse_params'])
+ else
+ Value.new(format_, data_['fields'], data_['unparse_params'])
+ end
+ end
+
+
+ def to_yaml(opts_={}) # :nodoc:
+ data_ = marshal_dump
+ ::YAML::quick_emit(nil, opts_) do |out_|
+ out_.map(taguri, to_yaml_style) do |map_|
+ map_.add('format', data_[0])
+ if data_[1].kind_of?(::String)
+ map_.add('value', data_[1])
+ map_.add('parse_params', data_[2]) if data_[2]
+ else
+ map_.add('fields', data_[1])
+ map_.add('unparse_params', data_[2]) if data_[2]
+ end
+ end
end
- initialize(data_[2], format_, data_[1])
end
Oops, something went wrong.

0 comments on commit 175d88d

Please sign in to comment.