versionomy 0.1.3 → 0.2.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.
@@ -46,7 +46,7 @@ module Versionomy
46
46
  # fields. If you provide a block, you must use the methods in
47
47
  # Versionomy::Schema::Builder in the block to create the root field.
48
48
 
49
- def self.create(field_=nil, &block_)
49
+ def self.create(field_=nil, opts_={}, &block_)
50
50
  if field_ && block_
51
51
  raise ::ArgumentError, 'You may provide either a root field or block but not both'
52
52
  end
@@ -54,8 +54,11 @@ module Versionomy
54
54
  builder_ = Schema::Builder.new
55
55
  ::Blockenspiel.invoke(block_, builder_)
56
56
  field_ = builder_._get_field
57
+ modules_ = builder_._get_modules
58
+ else
59
+ modules_ = opts_[:modules] || []
57
60
  end
58
- Schema::Wrapper.new(field_)
61
+ Schema::Wrapper.new(field_, modules_)
59
62
  end
60
63
 
61
64
 
@@ -68,33 +71,59 @@ module Versionomy
68
71
  # This is a low-level method. Usually you should call
69
72
  # Versionomy::Schema#create instead.
70
73
 
71
- def initialize(field_)
74
+ def initialize(field_, modules_=[])
72
75
  @root_field = field_
73
76
  @names = @root_field._descendants_by_name
77
+ @modules = modules_
78
+ end
79
+
80
+
81
+ def inspect # :nodoc:
82
+ "#<#{self.class}:0x#{object_id.to_s(16)} root=#{@root_field.inspect}>"
83
+ end
84
+
85
+ def to_s # :nodoc:
86
+ inspect
74
87
  end
75
88
 
76
89
 
77
90
  # Returns true if this schema is equivalent to the other schema.
78
91
  # Two schemas are equivalent if their root fields are the same--
79
- # which means that the entire field tree is the same.
92
+ # which means that the entire field tree is the same-- and they
93
+ # include the same value modules.
94
+ # Note that this is different from the definition of <tt>==</tt>.
80
95
 
81
96
  def eql?(obj_)
82
97
  return false unless obj_.kind_of?(Schema::Wrapper)
83
- return @root_field == obj_.root_field
98
+ return @root_field == obj_.root_field && @modules == obj_.modules
84
99
  end
85
100
 
86
101
 
87
- # Returns true if this schema is equivalent to the other schema.
88
- # Two schemas are equivalent if their root fields are the same--
89
- # which means that the entire field tree is the same.
102
+ # Returns true if this schema is compatible with the other schema.
103
+ # Two schemas are compatible if their root fields are the same--
104
+ # which means that the entire field tree is the same. They may,
105
+ # however, include different value modules.
106
+ # Note that this is different from the definition of <tt>eql?</tt>.
90
107
 
91
108
  def ==(obj_)
92
109
  eql?(obj_)
93
110
  end
94
111
 
95
112
 
113
+ # If the RHS is a schema, returns true if the schemas are equivalent.
114
+ # If the RHS is a value, returns true if the value uses this schema.
115
+
116
+ def ===(obj_)
117
+ if obj_.kind_of?(Value)
118
+ obj_.schema == self
119
+ else
120
+ obj_ == self
121
+ end
122
+ end
123
+
124
+
96
125
  def hash # :nodoc:
97
- @hash ||= @root_field.hash
126
+ @hash ||= @root_field.hash ^ @modules.hash
98
127
  end
99
128
 
100
129
 
@@ -121,6 +150,14 @@ module Versionomy
121
150
  end
122
151
 
123
152
 
153
+ # Returns an array of modules that should be included in values that
154
+ # use this schema.
155
+
156
+ def modules
157
+ @modules.dup
158
+ end
159
+
160
+
124
161
  end
125
162
 
126
163
 
@@ -133,6 +170,8 @@ module Versionomy
133
170
 
134
171
  def initialize() # :nodoc:
135
172
  @field = nil
173
+ @modules = []
174
+ @defaults = { :integer => {}, :string => {}, :symbol => {} }
136
175
  end
137
176
 
138
177
 
@@ -161,7 +200,34 @@ module Versionomy
161
200
  if @field
162
201
  raise Errors::RangeOverlapError, "Root field already defined"
163
202
  end
164
- @field = Schema::Field.new(name_, opts_, &block_)
203
+ @field = Schema::Field.new(name_, opts_.merge(:master_builder => self), &block_)
204
+ end
205
+
206
+
207
+ # Add a module to values that use this schema.
208
+
209
+ def add_module(mod_)
210
+ @modules << mod_
211
+ end
212
+
213
+
214
+ def to_bump_type(type_, &block_)
215
+ @defaults[type_][:bump] = block_
216
+ end
217
+
218
+
219
+ def to_compare_type(type_, &block_)
220
+ @defaults[type_][:compare] = block_
221
+ end
222
+
223
+
224
+ def to_canonicalize_type(type_, &block_)
225
+ @defaults[type_][:canonicalize] = block_
226
+ end
227
+
228
+
229
+ def default_value_for_type(type_, value_)
230
+ @defaults[type_][:value] = value_
165
231
  end
166
232
 
167
233
 
@@ -169,6 +235,14 @@ module Versionomy
169
235
  @field
170
236
  end
171
237
 
238
+ def _get_modules # :nodoc:
239
+ @modules
240
+ end
241
+
242
+ def _get_default_setting(type_, setting_) # :nodoc:
243
+ @defaults[type_][setting_]
244
+ end
245
+
172
246
  end
173
247
 
174
248
 
@@ -43,17 +43,20 @@ module Versionomy
43
43
  # The schema controls what fields are present in the version, how
44
44
  # version numbers are compared, what the default values are, and how
45
45
  # values can change. Version numbers with the same schema can be
46
- # compared with one another, and version numbers can be converted to
47
- # formats that share the same schema.
46
+ # compared with one another, and version numbers can be converted
47
+ # trivially to formats that share the same schema, without requiring a
48
+ # Conversion implementation.
48
49
  #
49
50
  # At its simplest, a version number is defined as a sequence of fields,
50
51
  # each with a name and data type. These fields may be integer-valued,
51
52
  # string-valued, or symbolic, though most will probably be integers.
52
- # Symbolic fields are useful, for example, if you want a field to specify
53
- # the type of prerelease (e.g. "alpha", "beta", or "release candidate").
53
+ # Symbolic fields are enumerated types that are useful, for example, if
54
+ # you want a field to specify the type of prerelease (e.g. "alpha",
55
+ # "beta", or "release candidate").
54
56
  #
55
- # As a simple example, you could construct a schema for versions numbers
56
- # of the form "major.minor.tiny" like this:
57
+ # As a simple conceptual example, you could construct a schema for
58
+ # version numbers of the form "major.minor.tiny" like this. (This is a
59
+ # conceptual diagram, not actual syntax.)
57
60
  #
58
61
  # ("major": integer), ("minor": integer), ("tiny": integer)
59
62
  #
@@ -34,6 +34,9 @@
34
34
  ;
35
35
 
36
36
 
37
+ require 'yaml'
38
+
39
+
37
40
  module Versionomy
38
41
 
39
42
 
@@ -64,18 +67,21 @@ module Versionomy
64
67
  unless values_.kind_of?(::Hash) || values_.kind_of?(::Array)
65
68
  raise ::ArgumentError, "Expected hash or array but got #{values_.class}"
66
69
  end
67
- @format = format_
68
- @unparse_params = unparse_params_
69
- @field_path = []
70
- @values = {}
71
- field_ = @format.schema.root_field
70
+ @_format = format_
71
+ @_unparse_params = unparse_params_
72
+ @_field_path = []
73
+ @_values = {}
74
+ schema_ = @_format.schema
75
+ field_ = schema_.root_field
72
76
  while field_
73
- value_ = values_.kind_of?(Hash) ? values_[field_.name] : values_.shift
77
+ value_ = values_.kind_of?(::Hash) ? values_[field_.name] : values_.shift
74
78
  value_ = value_ ? field_.canonicalize_value(value_) : field_.default_value
75
- @field_path << field_
76
- @values[field_.name] = value_
79
+ @_field_path << field_
80
+ @_values[field_.name] = value_
77
81
  field_ = field_.child(value_)
78
82
  end
83
+ modules_ = schema_.modules
84
+ extend(*modules_) if modules_.size > 0
79
85
  end
80
86
 
81
87
 
@@ -90,7 +96,7 @@ module Versionomy
90
96
 
91
97
  def _inspect # :nodoc:
92
98
  "#<#{self.class}:0x#{object_id.to_s(16)} " +
93
- @field_path.map{ |field_| "#{field_.name}=#{@values[field_.name].inspect}" }.join(' ')
99
+ @_field_path.map{ |field_| "#{field_.name}=#{@_values[field_.name].inspect}" }.join(' ')
94
100
  end
95
101
 
96
102
 
@@ -107,26 +113,103 @@ module Versionomy
107
113
  end
108
114
 
109
115
 
110
- # Unparse this version number.
116
+ # Marshal this version number.
117
+
118
+ def marshal_dump # :nodoc:
119
+ format_name_ = Format.canonical_name_for(@_format, true)
120
+ unparsed_data_ = nil
121
+ if @_format.respond_to?(:unparse_for_serialization)
122
+ unparsed_data_ = @_format.unparse_for_serialization(self) rescue nil
123
+ end
124
+ unparsed_data_ ||= @_format.unparse(self) rescue nil
125
+ data_ = [format_name_]
126
+ case unparsed_data_
127
+ when ::Array
128
+ data_ << unparsed_data_[0]
129
+ data_ << unparsed_data_[1] if unparsed_data_[1]
130
+ when ::String
131
+ data_ << unparsed_data_
132
+ else
133
+ data_ << values_array
134
+ data_ << @_unparse_params if @_unparse_params
135
+ end
136
+ data_
137
+ end
138
+
139
+
140
+ # Unmarshal this version number.
141
+
142
+ def marshal_load(data_) # :nodoc:
143
+ format_ = Format.get(data_[0], true)
144
+ if data_[1].kind_of?(::String)
145
+ val_ = format_.parse(data_[1], data_[2])
146
+ initialize(val_.values_array, format_, val_.unparse_params)
147
+ else
148
+ initialize(data_[1], format_, data_[2])
149
+ end
150
+ end
151
+
152
+
153
+ yaml_as "tag:danielazuma.com,2009:version"
154
+
155
+
156
+ # Deserialize a version number from YAML
157
+
158
+ def self.yaml_new(klass_, tag_, data_) # :nodoc:
159
+ unless data_.kind_of?(::Hash)
160
+ raise ::YAML::TypeError, "Invalid version format: #{val_.inspect}"
161
+ end
162
+ format_ = Format.get(data_['format'], true)
163
+ value_ = data_['value']
164
+ if value_
165
+ format_.parse(value_, data_['parse_params'])
166
+ else
167
+ Value.new(format_, data_['fields'], data_['unparse_params'])
168
+ end
169
+ end
170
+
171
+
172
+ # Serialize this version number to YAML format.
173
+
174
+ def to_yaml(opts_={})
175
+ data_ = marshal_dump
176
+ ::YAML::quick_emit(nil, opts_) do |out_|
177
+ out_.map(taguri, to_yaml_style) do |map_|
178
+ map_.add('format', data_[0])
179
+ if data_[1].kind_of?(::String)
180
+ map_.add('value', data_[1])
181
+ map_.add('parse_params', data_[2]) if data_[2]
182
+ else
183
+ map_.add('fields', data_[1])
184
+ map_.add('unparse_params', data_[2]) if data_[2]
185
+ end
186
+ end
187
+ end
188
+ end
189
+
190
+
191
+ # Unparse this version number and return a string.
111
192
  #
112
193
  # Raises Versionomy::Errors::UnparseError if unparsing failed.
113
194
 
114
195
  def unparse(params_=nil)
115
- @format.unparse(self, params_)
196
+ @_format.unparse(self, params_)
116
197
  end
117
198
 
118
199
 
119
- # Return the schema defining the form of this version number
200
+ # Return the schema defining the structure and semantics of this
201
+ # version number.
120
202
 
121
203
  def schema
122
- @format.schema
204
+ @_format.schema
123
205
  end
124
206
 
125
207
 
126
- # Return the format defining the form of this version number
208
+ # Return the format defining the schema and formatting/parsing of
209
+ # this version number.
127
210
 
128
211
  def format
129
- @format
212
+ @_format
130
213
  end
131
214
 
132
215
 
@@ -134,15 +217,15 @@ module Versionomy
134
217
  # Returns nil if this value was not created using a parser.
135
218
 
136
219
  def unparse_params
137
- @unparse_params ? @unparse_params.dup : nil
220
+ @_unparse_params ? @_unparse_params.dup : nil
138
221
  end
139
222
 
140
223
 
141
224
  # Iterates over each field, in field order, yielding the field name and value.
142
225
 
143
226
  def each_field
144
- @field_path.each do |field_|
145
- yield(field_, @values[field_.name])
227
+ @_field_path.each do |field_|
228
+ yield(field_, @_values[field_.name])
146
229
  end
147
230
  end
148
231
 
@@ -151,16 +234,18 @@ module Versionomy
151
234
  # Versionomy::Schema::Field object and value.
152
235
 
153
236
  def each_field_object # :nodoc:
154
- @field_path.each do |field_|
155
- yield(field_, @values[field_.name])
237
+ @_field_path.each do |field_|
238
+ yield(field_, @_values[field_.name])
156
239
  end
157
240
  end
158
241
 
159
242
 
160
243
  # Returns an array of recognized field names for this value, in field order.
244
+ # This is the order of the fields actually present in this value, in
245
+ # order from most to least significant.
161
246
 
162
247
  def field_names
163
- @field_path.map{ |field_| field_.name }
248
+ @_field_path.map{ |field_| field_.name }
164
249
  end
165
250
 
166
251
 
@@ -170,11 +255,11 @@ module Versionomy
170
255
  def has_field?(field_)
171
256
  case field_
172
257
  when Schema::Field
173
- @field_path.include?(field_)
258
+ @_field_path.include?(field_)
174
259
  when ::Integer
175
- @field_path.size > field_ && field_ >= 0
260
+ @_field_path.size > field_ && field_ >= 0
176
261
  when ::String, ::Symbol
177
- @values.has_key?(field_.to_sym)
262
+ @_values.has_key?(field_.to_sym)
178
263
  else
179
264
  raise ::ArgumentError
180
265
  end
@@ -186,45 +271,47 @@ module Versionomy
186
271
  # or field index.
187
272
 
188
273
  def [](field_)
189
- field_ = @field_path[field_] if field_.kind_of?(::Integer)
274
+ field_ = @_field_path[field_] if field_.kind_of?(::Integer)
190
275
  field_ = field_.name if field_.kind_of?(Schema::Field)
191
- field_ ? @values[field_.to_sym] : nil
276
+ field_ ? @_values[field_.to_sym] : nil
192
277
  end
193
278
 
194
279
 
195
280
  # Returns the value as an array of field values, in field order.
281
+ # This is the order of the fields actually present in this value, in
282
+ # order from most to least significant.
196
283
 
197
284
  def values_array
198
- @field_path.map{ |field_| @values[field_.name] }
285
+ @_field_path.map{ |field_| @_values[field_.name] }
199
286
  end
200
287
 
201
288
 
202
289
  # Returns the value as a hash of values keyed by field name.
203
290
 
204
291
  def values_hash
205
- @values.dup
292
+ @_values.dup
206
293
  end
207
294
 
208
295
 
209
296
  # Returns a new version number created by bumping the given field. The
210
297
  # field may be specified as a field object, field name, or field index.
211
- # Returns self if value could not be changed.
212
- # Returns nil if the field was not recognized.
298
+ # Returns self unchanged if the field was not recognized or could not
299
+ # be modified.
213
300
 
214
301
  def bump(name_)
215
- name_ = @field_path[name_] if name_.kind_of?(::Integer)
302
+ name_ = @_field_path[name_] if name_.kind_of?(::Integer)
216
303
  name_ = name_.name if name_.kind_of?(Schema::Field)
217
- return nil unless name_
304
+ return self unless name_
218
305
  name_ = name_.to_sym
219
- return nil unless @values.include?(name_)
306
+ return self unless @_values.include?(name_)
220
307
  values_ = []
221
- @field_path.each do |field_|
222
- oldval_ = @values[field_.name]
308
+ @_field_path.each do |field_|
309
+ oldval_ = @_values[field_.name]
223
310
  if field_.name == name_
224
311
  newval_ = field_.bump_value(oldval_)
225
312
  return self if newval_ == oldval_
226
313
  values_ << newval_
227
- return Value.new(values_, @format, @unparse_params)
314
+ return Value.new(values_, @_format, @_unparse_params)
228
315
  else
229
316
  values_ << oldval_
230
317
  end
@@ -233,63 +320,109 @@ module Versionomy
233
320
  end
234
321
 
235
322
 
236
- # Returns a new version number created by changing the given field values.
323
+ # Returns a new version number created by cloning this version number
324
+ # and changing the given field values.
325
+ #
326
+ # You should pass in a hash of field names to values. These are the
327
+ # fields to modify; any other fields will be left alone, unless they
328
+ # are implicitly changed by the modifications you are making.
329
+ # For example, changing the :release_type on a value using the standard
330
+ # format, may change which fields are present in the resulting value.
331
+ #
332
+ # You may also pass a delta hash to modify the unparse params stored in
333
+ # the value.
237
334
 
238
335
  def change(values_={}, unparse_params_={})
239
- unparse_params_ = @unparse_params.merge(unparse_params_) if @unparse_params
240
- Value.new(@values.merge(values_), @format, unparse_params_)
336
+ unparse_params_ = @_unparse_params.merge(unparse_params_) if @_unparse_params
337
+ Value.new(@_values.merge(values_), @_format, unparse_params_)
338
+ end
339
+
340
+
341
+ # Attempts to convert this value to the given format, and returns the
342
+ # resulting value.
343
+ #
344
+ # Raises Versionomy::Errors::ConversionError if the value could not
345
+ # be converted.
346
+
347
+ def convert(format_, convert_params_=nil)
348
+ if format_.kind_of?(::String) || format_.kind_of?(::Symbol)
349
+ format_ = Format.get(format_)
350
+ end
351
+ return self if @_format == format_
352
+ from_schema_ = @_format.schema
353
+ to_schema_ = format_.schema
354
+ if from_schema_ == to_schema_
355
+ return Value.new(@_values, format_, convert_params_)
356
+ end
357
+ conversion_ = Conversion.get(from_schema_, to_schema_, true)
358
+ conversion_.convert_value(self, format_, convert_params_)
241
359
  end
242
360
 
243
361
 
244
362
  def hash # :nodoc:
245
- @hash ||= @values.hash
363
+ @_hash ||= @_values.hash
246
364
  end
247
365
 
248
366
 
249
- # Returns true if this version number is equal to the given verison number.
250
- # Equality means the schemas and values are the same.
367
+ # Returns true if this version number is equivalent to the given number.
368
+ # This type of equality means their schemas are compatible and their
369
+ # field values are equal.
370
+ # Note that this is different from the definition of <tt>==</tt>.
251
371
 
252
372
  def eql?(obj_)
253
373
  if obj_.kind_of?(::String)
254
- obj_ = @format.parse(obj_) rescue nil
374
+ obj_ = @_format.parse(obj_) rescue nil
255
375
  end
256
376
  return false unless obj_.kind_of?(Value)
257
377
  index_ = 0
258
378
  obj_.each_field_object do |field_, value_|
259
- return false if field_ != @field_path[index_] || value_ != @values[field_.name]
379
+ return false if field_ != @_field_path[index_] || value_ != @_values[field_.name]
260
380
  index_ += 1
261
381
  end
262
382
  true
263
383
  end
264
384
 
265
385
 
266
- # Returns true if this version number is equal to the given verison number.
267
- # Equality means the schemas and values are the same.
386
+ # Returns true if this version number is value-equal to the given number.
387
+ # This type of equality means that they are equivalent, or that it is
388
+ # possible to convert the RHS to the LHS's format, and that they would
389
+ # be equivalent after such a conversion has taken place.
390
+ # Note that this is different from the definition of <tt>eql?</tt>.
268
391
 
269
392
  def ==(obj_)
270
- eql?(obj_)
393
+ (self <=> obj_) == 0
271
394
  end
272
395
 
273
396
 
274
- # Compare this version number with the given version number.
397
+ # Compare this version number with the given version number,
398
+ # returning 0 if the two are value-equal, a negative number if the RHS
399
+ # is greater, or a positive number if the LHS is greater.
400
+ # The comparison may succeed even if the two have different schemas,
401
+ # if the RHS can be converted to the LHS's format.
275
402
 
276
403
  def <=>(obj_)
277
404
  if obj_.kind_of?(::String)
278
- obj_ = @format.parse(obj_)
405
+ obj_ = @_format.parse(obj_)
279
406
  end
280
407
  return nil unless obj_.kind_of?(Value)
281
- index_ = 0
408
+ if obj_.schema != @_format.schema
409
+ begin
410
+ obj_ = obj_.convert(@_format)
411
+ rescue
412
+ return nil
413
+ end
414
+ end
282
415
  obj_.each_field_object do |field_, value_|
283
- return nil unless field_ == @field_path[index_]
284
- val_ = field_.compare_values(@values[field_.name], value_)
416
+ val_ = field_.compare_values(@_values[field_.name], value_)
285
417
  return val_ if val_ != 0
286
- index_ += 1
287
418
  end
288
419
  0
289
420
  end
290
421
 
291
422
 
292
423
  # Compare this version number with the given version number.
424
+ # The comparison may succeed even if the two have different schemas,
425
+ # if the RHS can be converted to the LHS's format.
293
426
 
294
427
  def <(obj_)
295
428
  val_ = (self <=> obj_)
@@ -301,6 +434,8 @@ module Versionomy
301
434
 
302
435
 
303
436
  # Compare this version number with the given version number.
437
+ # The comparison may succeed even if the two have different schemas,
438
+ # if the RHS can be converted to the LHS's format.
304
439
 
305
440
  def >(obj_)
306
441
  val_ = (self <=> obj_)
@@ -311,6 +446,9 @@ module Versionomy
311
446
  end
312
447
 
313
448
 
449
+ include ::Comparable
450
+
451
+
314
452
  # Field values may be retrieved by calling them as methods.
315
453
 
316
454
  def method_missing(symbol_)
@@ -37,7 +37,7 @@
37
37
  module Versionomy
38
38
 
39
39
  # Current gem version, as a frozen string.
40
- VERSION_STRING = '0.1.3'.freeze
40
+ VERSION_STRING = '0.2.0'.freeze
41
41
 
42
42
  # Current gem version, as a Versionomy::Value.
43
43
  VERSION = ::Versionomy.parse(VERSION_STRING, :standard)
data/lib/versionomy.rb CHANGED
@@ -52,9 +52,13 @@ includes_ = [
52
52
  'format',
53
53
  'format/base',
54
54
  'format/delimiter',
55
- 'formats',
56
- 'formats/standard',
55
+ 'format/standard',
56
+ 'format/rubygems',
57
57
  'value',
58
+ 'conversion',
59
+ 'conversion/base',
60
+ 'conversion/parsing',
61
+ 'conversion/rubygems',
58
62
  'interface',
59
63
  'version',
60
64
  ]
@@ -36,24 +36,24 @@
36
36
 
37
37
 
38
38
  require 'test/unit'
39
- require File.expand_path("#{File.dirname(__FILE__)}/../lib/versionomy.rb")
39
+ require ::File.expand_path("#{::File.dirname(__FILE__)}/../lib/versionomy.rb")
40
40
 
41
41
 
42
42
  module Versionomy
43
43
  module Tests # :nodoc:
44
44
 
45
- class TestCustomFormat < Test::Unit::TestCase # :nodoc:
45
+ class TestCustomFormat < ::Test::Unit::TestCase # :nodoc:
46
46
 
47
47
 
48
48
  # Test parsing with custom format for patchlevel
49
49
 
50
50
  def test_parsing_custom_patchlevel_format
51
- format_ = Versionomy.default_format.modified_copy do
51
+ format_ = ::Versionomy.default_format.modified_copy do
52
52
  field(:patchlevel, :requires_previous_field => false) do
53
53
  recognize_number(:delimiter_regexp => '\s?sp', :default_delimiter => ' SP')
54
54
  end
55
55
  end
56
- value1_ = Versionomy.parse('2008 SP2', format_)
56
+ value1_ = ::Versionomy.parse('2008 SP2', format_)
57
57
  assert_equal(2, value1_.patchlevel)
58
58
  value2_ = value1_.format.parse('2008 sp3')
59
59
  assert_equal(3, value2_.patchlevel)