versionomy 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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