versionomy 0.0.1

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,537 @@
1
+ # -----------------------------------------------------------------------------
2
+ #
3
+ # Versionomy schema
4
+ #
5
+ # -----------------------------------------------------------------------------
6
+ # Copyright 2008 Daniel Azuma
7
+ #
8
+ # All rights reserved.
9
+ #
10
+ # Redistribution and use in source and binary forms, with or without
11
+ # modification, are permitted provided that the following conditions are met:
12
+ #
13
+ # * Redistributions of source code must retain the above copyright notice,
14
+ # this list of conditions and the following disclaimer.
15
+ # * Redistributions in binary form must reproduce the above copyright notice,
16
+ # this list of conditions and the following disclaimer in the documentation
17
+ # and/or other materials provided with the distribution.
18
+ # * Neither the name of the copyright holder, nor the names of any other
19
+ # contributors to this software, may be used to endorse or promote products
20
+ # derived from this software without specific prior written permission.
21
+ #
22
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
23
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
24
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
25
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
26
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
27
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
28
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
29
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
30
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
31
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32
+ # POSSIBILITY OF SUCH DAMAGE.
33
+ # -----------------------------------------------------------------------------
34
+ ;
35
+
36
+
37
+ module Versionomy
38
+
39
+
40
+ # === Version number schema.
41
+ #
42
+ # A schema is a set of field specifications that specify how a version
43
+ # number is structured. Each field has a name, a type, an initial value,
44
+ # and other properties.
45
+ #
46
+ # Schema fields may be integer-valued, string-valued, or symbolic.
47
+ # Symbolic fields are useful, for example, if you want a field to specify
48
+ # the type of prerelease (e.g. "alpha", "beta", or "release candidate").
49
+ #
50
+ # The Schema object itself actually represents only a single field, the
51
+ # "most significant" field. The next most significant field is specified
52
+ # by a child of this object, the next by a child of that child, and so
53
+ # forth down the line. You could therefore think of a simple schema as a
54
+ # chain of cons-cells.
55
+ #
56
+ # For example, you could construct a schema for versions numbers of
57
+ # the form "major.minor.tiny" like this:
58
+ #
59
+ # Schema(major) -> Schema(minor) -> Schema(tiny) -> nil
60
+ #
61
+ # Some schemas may be more complex than that, however. It is possible for
62
+ # the form of a schema's child to depend on the value of the field.
63
+ # For example, suppose we wanted a schema in which if the value of the
64
+ # "minor" field is 0, then the "tiny" field doesn't exist. It's possible
65
+ # to construct a schema of this form:
66
+ #
67
+ # Schema(major) -> Schema(minor) -> [value == 0] : nil
68
+ # [otherwise] : Schema(tiny) -> nil
69
+
70
+ class Schema
71
+
72
+
73
+ # Create a version number schema, with the given field name.
74
+ #
75
+ # Recognized options include:
76
+ #
77
+ # <tt>:type</tt>::
78
+ # Type of field. This should be <tt>:integer</tt>, <tt>:string</tt>, or <tt>:symbol</tt>.
79
+ # Default is <tt>:integer</tt>.
80
+ # <tt>:initial</tt>::
81
+ # Initial value. Default is 0 for an integer field, the empty string for a string field,
82
+ # or the first symbol added for a symbol field.
83
+ #
84
+ # You may provide an optional block. Within the block, you may call methods of
85
+ # Versionomy::Schema::Builder to further customize the field, or add subschemas.
86
+ #
87
+ # Raises Versionomy::Errors::IllegalValueError if the given initial value is not legal.
88
+
89
+ def initialize(name_, opts_={}, &block_)
90
+ @name = name_.to_sym
91
+ @type = opts_[:type] || :integer
92
+ @initial_value = opts_[:initial]
93
+ @symbol_info = nil
94
+ @symbol_order = nil
95
+ if @type == :symbol
96
+ @symbol_info = Hash.new
97
+ @symbol_order = Array.new
98
+ end
99
+ @bump_proc = nil
100
+ @compare_proc = nil
101
+ @canonicalize_proc = nil
102
+ @ranges = nil
103
+ @default_subschema = nil
104
+ @formats = Hash.new
105
+ @default_format_name = nil
106
+ Blockenspiel.invoke(block_, Versionomy::Schema::Builder.new(self))
107
+ @initial_value = canonicalize_value(@initial_value)
108
+ end
109
+
110
+
111
+ def _set_initial_value(value_) # :nodoc:
112
+ @initial_value = value_
113
+ end
114
+
115
+ def _add_symbol(symbol_, opts_={}) # :nodoc:
116
+ if @type != :symbol
117
+ raise Versionomy::Errors::TypeMismatchError
118
+ end
119
+ if @symbol_info.has_key?(symbol_)
120
+ raise Versionomy::Errors::SymbolRedefinedError
121
+ end
122
+ @symbol_info[symbol_] = [@symbol_order.size, opts_[:bump]]
123
+ @symbol_order << symbol_
124
+ if @initial_value.nil?
125
+ @initial_value = symbol_
126
+ end
127
+ end
128
+
129
+ def _set_bump_proc(block_) # :nodoc:
130
+ @bump_proc = block_
131
+ end
132
+
133
+ def _set_canonicalize_proc(block_) # :nodoc:
134
+ @canonicalize_proc = block_
135
+ end
136
+
137
+ def _set_compare_proc(block_) # :nodoc:
138
+ @compare_proc = block_
139
+ end
140
+
141
+
142
+ # The name of the field.
143
+
144
+ def name
145
+ @name
146
+ end
147
+
148
+
149
+ # The type of the field.
150
+ # Possible values are <tt>:integer</tt>, <tt>:string</tt>, or <tt>:symbol</tt>.
151
+
152
+ def type
153
+ @type
154
+ end
155
+
156
+
157
+ # The initial value of the field
158
+
159
+ def initial_value
160
+ @initial_value
161
+ end
162
+
163
+
164
+ # Given a value, bump it to the "next" value.
165
+ # Utilizes a bump procedure if given;
166
+ # otherwise uses default behavior depending on the type.
167
+
168
+ def bump_value(value_)
169
+ if @bump_proc
170
+ nvalue_ = @bump_proc.call(value_)
171
+ nvalue_ || value_
172
+ elsif @type == :integer || @type == :string
173
+ value_.next
174
+ else
175
+ info_ = @symbol_info[value_]
176
+ info_ ? info_[1] || value_ : nil
177
+ end
178
+ end
179
+
180
+
181
+ # Perform a standard comparison on two values.
182
+ # Returns an integer that may be positive, negative, or 0.
183
+ # Utilizes a comparison procedure if given;
184
+ # otherwise uses default behavior depending on the type.
185
+
186
+ def compare_values(val1_, val2_)
187
+ if @compare_proc
188
+ @compare_proc.call(val1_, val2_)
189
+ elsif @type == :integer || @type == :string
190
+ val1_ <=> val2_
191
+ else
192
+ info1_ = @symbol_info[val1_]
193
+ info2_ = @symbol_info[val2_]
194
+ info1_ && info2_ ? info1_[0] <=> info2_[0] : nil
195
+ end
196
+ end
197
+
198
+
199
+ # Given a value, return a "canonical" value for this field.
200
+ # Utilizes a canonicalization procedure if given;
201
+ # otherwise uses default behavior depending on the type.
202
+ #
203
+ # Raises Versionomy::Errors::IllegalValueError if the given value is not legal.
204
+
205
+ def canonicalize_value(value_)
206
+ if @canonicalize_proc
207
+ value_ = @canonicalize_proc.call(value_)
208
+ else
209
+ case @type
210
+ when :integer
211
+ value_ = value_.to_i
212
+ when :string
213
+ value_ = value_.to_s
214
+ when :symbol
215
+ value_ = value_.to_sym
216
+ end
217
+ end
218
+ if value_.nil? || (@type == :symbol && !@symbol_info.has_key?(value_))
219
+ raise Versionomy::Errors::IllegalValueError
220
+ end
221
+ value_
222
+ end
223
+
224
+
225
+ # Define a formatter for this schema.
226
+ # You may either pass a formatter or provide a block
227
+ # that calls methods in Versionomy::Format::Builder.
228
+
229
+ def define_format(name_, formatter_=nil, &block_)
230
+ if block_
231
+ @formats[name_] = Versionomy::Format::Base.new(&block_)
232
+ else
233
+ @formats[name_] = formatter_
234
+ end
235
+ @default_format_name ||= name_
236
+ end
237
+
238
+
239
+ # Get the formatter with the given name.
240
+ # If the name is nil, returns the default formatter.
241
+ # If the name is not recognized, returns nil.
242
+
243
+ def get_format(name_)
244
+ @formats[name_ || @default_format_name]
245
+ end
246
+
247
+
248
+ # Returns the current default format name.
249
+
250
+ def default_format_name
251
+ @default_format_name
252
+ end
253
+
254
+
255
+ # Sets the default format by name.
256
+
257
+ def default_format_name=(name_)
258
+ if @formats[name_]
259
+ @default_format_name = name_
260
+ else
261
+ nil
262
+ end
263
+ end
264
+
265
+
266
+ # Create a new value with this schema.
267
+ #
268
+ # The values should either be a hash of field names and values, or an array
269
+ # of values that will be interpreted in field order.
270
+
271
+ def create(values_=nil)
272
+ Versionomy::Value._new(self, values_)
273
+ end
274
+
275
+
276
+ # Create a new value by parsing the given string.
277
+ #
278
+ # The optional parameters may include a <tt>:format</tt> parameter that
279
+ # specifies a format name. If no format is specified, the default format is used.
280
+ # The remaining parameters are passed into the formatter's parse method.
281
+ #
282
+ # Raises Versionomy::Errors::UnknownFormatError if the given format name is not recognized.
283
+ #
284
+ # Raises Versionomy::Errors::ParseError if parsing failed.
285
+
286
+ def parse(str_, params_={})
287
+ format_ = get_format(params_[:format])
288
+ if format_.nil?
289
+ raise Versionomy::Errors::UnknownFormatError
290
+ end
291
+ value_ = format_.parse(self, str_, params_)
292
+ end
293
+
294
+
295
+ # Returns the subschema associated with the given value.
296
+
297
+ def _subschema(value_) # :nodoc:
298
+ if @ranges
299
+ @ranges.each do |r_|
300
+ if !r_[0].nil?
301
+ cmp_ = compare_values(r_[0], value_)
302
+ next if cmp_.nil? || cmp_ > 0
303
+ end
304
+ if !r_[1].nil?
305
+ cmp_ = compare_values(r_[1], value_)
306
+ next if cmp_.nil? || cmp_ < 0
307
+ end
308
+ return r_[2]
309
+ end
310
+ end
311
+ @default_subschema
312
+ end
313
+
314
+
315
+ # Appends the given subschema for the given range
316
+
317
+ def _append_schema(schema_, ranges_=nil) # :nodoc:
318
+ if ranges_.nil?
319
+ if @default_subschema
320
+ raise Versionomy::Errors::RangeOverlapError
321
+ end
322
+ @default_subschema = schema_
323
+ return
324
+ end
325
+ if !ranges_.is_a?(Array) || range_.size == 2 &&
326
+ (range_[0].nil? || range_[0].is_a?(Symbol) ||
327
+ range_[0].kind_of?(Integer) || range_[0].is_a?(String)) &&
328
+ (range_[1].nil? || range_[1].is_a?(Symbol) ||
329
+ range_[1].kind_of?(Integer) || range_[1].is_a?(String))
330
+ then
331
+ ranges_ = [ranges_]
332
+ else
333
+ ranges_ = ranges_.dup
334
+ end
335
+ ranges_.each do |range_|
336
+ normalized_range_ = nil
337
+ if range_.kind_of?(Array) && range_.size != 2
338
+ raise Versionomy::Errors::RangeSpecificationError
339
+ end
340
+ case @type
341
+ when :integer
342
+ case range_
343
+ when Array
344
+ normalized_range_ = range_.map{ |elem_| elem_.nil? ? nil : elem_.to_i }
345
+ when Range
346
+ normalized_range_ = [range_.first, range_.exclude_end? ? range_.last-1 : range_.last]
347
+ when String, Symbol, Integer
348
+ range_ = range_.to_i
349
+ normalized_range_ = [range_, range_]
350
+ else
351
+ raise Versionomy::Errors::RangeSpecificationError
352
+ end
353
+ when :string
354
+ case range_
355
+ when Array
356
+ normalized_range_ = range_.map{ |elem_| elem_.nil? ? nil : elem_.to_s }
357
+ when Range
358
+ normalized_range_ = [range_.first.to_s,
359
+ range_.exclude_end? ? (range_.last-1).to_s : range_.last.to_s]
360
+ else
361
+ range_ = range_.to_s
362
+ normalized_range_ = [range_, range_]
363
+ end
364
+ when :symbol
365
+ case range_
366
+ when Array
367
+ normalized_range_ = range_.map do |elem_|
368
+ case elem_
369
+ when nil
370
+ nil
371
+ when Integer
372
+ elem_.to_s.to_sym
373
+ else
374
+ elem_.to_sym
375
+ end
376
+ end
377
+ when String, Integer
378
+ range_ = range_.to_s.to_sym
379
+ normalized_range_ = [range_, range_]
380
+ when Symbol
381
+ normalized_range_ = [range_, range_]
382
+ else
383
+ raise Versionomy::Errors::RangeSpecificationError
384
+ end
385
+ end
386
+ normalized_range_ << schema_
387
+ @ranges ||= Array.new
388
+ insert_index_ = @ranges.size
389
+ @ranges.each_with_index do |r_, i_|
390
+ if normalized_range_[0] && r_[1]
391
+ cmp_ = compare_values(normalized_range_[0], r_[1])
392
+ if cmp_.nil?
393
+ raise Versionomy::Errors::RangeSpecificationError
394
+ end
395
+ if cmp_ > 0
396
+ next
397
+ end
398
+ end
399
+ if normalized_range_[1] && r_[0]
400
+ cmp_ = compare_values(normalized_range_[1], r_[0])
401
+ if cmp_.nil?
402
+ raise Versionomy::Errors::RangeSpecificationError
403
+ end
404
+ if cmp_ < 0
405
+ insert_index_ = i_
406
+ break
407
+ end
408
+ end
409
+ raise Versionomy::Errors::RangeOverlapError
410
+ end
411
+ @ranges.insert(insert_index_, normalized_range_)
412
+ end
413
+ end
414
+
415
+
416
+ # These methods are available in a schema definition block.
417
+
418
+ class Builder
419
+
420
+ include Blockenspiel::DSL
421
+
422
+ def initialize(schema_) # :nodoc:
423
+ @schema = schema_
424
+ end
425
+
426
+
427
+ # Define the given symbol.
428
+ #
429
+ # Recognized options include:
430
+ #
431
+ # <tt>:bump</tt>::
432
+ # The symbol to transition to when "bump" is called.
433
+ # Default is to remain on the same value.
434
+ #
435
+ # Raises Versionomy::Errors::TypeMismatchError if called when the current schema
436
+ # is not of type <tt>:symbol</tt>.
437
+ #
438
+ # Raises Versionomy::Errors::SymbolRedefinedError if the given symbol name is
439
+ # already defined.
440
+
441
+ def symbol(symbol_, opts_={})
442
+ @schema._add_symbol(symbol_, opts_)
443
+ end
444
+
445
+
446
+ # Provide an initial value.
447
+
448
+ def initial_value(value_)
449
+ @schema._set_initial_value(value_)
450
+ end
451
+
452
+
453
+ # Provide a "bump" procedure.
454
+ # The given block should take a value, and return the value to transition to.
455
+ # If you return nil, the value will remain the same.
456
+
457
+ def to_bump(&block_)
458
+ @schema._set_bump_proc(block_)
459
+ end
460
+
461
+
462
+ # Provide a "compare" procedure.
463
+ # The given block should take two values and compare them.
464
+ # It should return a negative integer if the first is less than the second,
465
+ # a positive integer if the first is greater than the second, or 0 if the
466
+ # two values are equal. If the values cannot be compared, return nil.
467
+
468
+ def to_compare(&block_)
469
+ @schema._set_compare_proc(block_)
470
+ end
471
+
472
+
473
+ # Provide a "canonicalize" procedure.
474
+ # The given block should take a value and return a canonicalized value.
475
+ # Return nil if the given value is illegal.
476
+
477
+ def to_canonicalize(&block_)
478
+ @schema._set_canonicalize_proc(block_)
479
+ end
480
+
481
+
482
+ # Add a subschema.
483
+ #
484
+ # Recognized options include:
485
+ #
486
+ # <tt>:only</tt>::
487
+ # This subschema should be available only for the given values of this schema.
488
+ # See below for ways to specify this constraint.
489
+ # <tt>:type</tt>::
490
+ # Type of field. This should be <tt>:integer</tt>, <tt>:string</tt>, or <tt>:symbol</tt>.
491
+ # Default is <tt>:integer</tt>.
492
+ # <tt>:initial</tt>::
493
+ # Initial value. Default is 0 for an integer field, the empty string for a string field,
494
+ # or the first symbol added for a symbol field.
495
+ #
496
+ # You may provide an optional block. Within the block, you may call methods of this
497
+ # class again to customize the subschema.
498
+ #
499
+ # Raises Versionomy::Errors::IllegalValueError if the given initial value is not legal.
500
+ #
501
+ # The <tt>:only</tt> constraint may be specified in one of the following ways:
502
+ #
503
+ # * A single value (integer, string, or symbol)
504
+ # * A Range object defining a range of integers
505
+ # * A two-element array indicating a range of integers, strings, or symbols, inclusive.
506
+ # In this case, the ordering of symbols is defined by the order in which the symbols
507
+ # were added to this schema.
508
+ # If either element is nil, it is considered an open end of the range.
509
+ # * An array of arrays in the above form.
510
+
511
+ def schema(name_, opts_={}, &block_)
512
+ @schema._append_schema(Versionomy::Schema.new(name_, opts_, &block_), opts_.delete(:only))
513
+ end
514
+
515
+
516
+ # Define a formatter for this schema.
517
+ # You may either pass a formatter or provide a block
518
+ # that calls methods in Versionomy::Format::Builder.
519
+
520
+ def define_format(name_, formatter_=nil, &block_)
521
+ @schema.define_format(name_, formatter_, &block_)
522
+ end
523
+
524
+
525
+ # Sets the default format by name.
526
+
527
+ def set_default_format_name(name_)
528
+ @schema.default_format_name = name_
529
+ end
530
+
531
+
532
+ end
533
+
534
+
535
+ end
536
+
537
+ end