versionomy 0.0.4 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,9 +1,9 @@
1
1
  # -----------------------------------------------------------------------------
2
2
  #
3
- # Versionomy schema
3
+ # Versionomy schema module
4
4
  #
5
5
  # -----------------------------------------------------------------------------
6
- # Copyright 2008 Daniel Azuma
6
+ # Copyright 2008-2009 Daniel Azuma
7
7
  #
8
8
  # All rights reserved.
9
9
  #
@@ -39,513 +39,54 @@ module Versionomy
39
39
 
40
40
  # === Version number schema.
41
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.
42
+ # A schema defines the structure and semantics of a version number.
43
+ # The schema controls what fields are present in the version, how
44
+ # version numbers are compared, what the default values are, and how
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.
45
48
  #
46
- # Schema fields may be integer-valued, string-valued, or symbolic.
49
+ # At its simplest, a version number is defined as a sequence of fields,
50
+ # each with a name and data type. These fields may be integer-valued,
51
+ # string-valued, or symbolic, though most will probably be integers.
47
52
  # Symbolic fields are useful, for example, if you want a field to specify
48
53
  # the type of prerelease (e.g. "alpha", "beta", or "release candidate").
49
54
  #
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
+ # As a simple example, you could construct a schema for versions numbers
56
+ # of the form "major.minor.tiny" like this:
55
57
  #
56
- # For example, you could construct a schema for versions numbers of
57
- # the form "major.minor.tiny" like this:
58
+ # ("major": integer), ("minor": integer), ("tiny": integer)
58
59
  #
59
- # Schema(major) -> Schema(minor) -> Schema(tiny) -> nil
60
+ # More generally, fields are actually organized into a DAG (directed
61
+ # acyclic graph) in which the "most significant" field is the root, the
62
+ # next most significant is a child of that root, and so forth down the
63
+ # line. The simple schema above, then, is actually represented as a
64
+ # linked list (a graph with one path), like this:
60
65
  #
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
+ # ("major": integer) ->
67
+ # ("minor": integer) ->
68
+ # ("tiny": integer) ->
69
+ # nil
66
70
  #
67
- # Schema(major) -> Schema(minor) -> [value == 0] : nil
68
- # [otherwise] : Schema(tiny) -> nil
71
+ # It is, however, possible for the form of a field to depend on the value
72
+ # of the previous field. For example, suppose we wanted a schema in which
73
+ # if the value of the "minor" field is 0, then the "tiny" field doesn't
74
+ # exist. e.g.
75
+ #
76
+ # ("major": integer) ->
77
+ # ("minor": integer) ->
78
+ # [value == 0] : nil
79
+ # [otherwise] : ("tiny": integer) ->
80
+ # nil
81
+ #
82
+ # The Versionomy::Schema::Field class represents a field in this graph.
83
+ # The Versionomy::Schema::Wrapper class represents a full schema object.
84
+ #
85
+ # Generally, you should create schemas using Versionomy::Schema#create.
86
+ # This method provides a DSL that lets you quickly create the fields.
69
87
 
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)) if block_
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
- def inspect # :nodoc:
143
- to_s
144
- end
145
-
146
- def to_s # :nodoc:
147
- "#<#{self.class}:0x#{object_id.to_s(16)} name=#{@name}>"
148
- end
149
-
150
-
151
- # The name of the field.
152
-
153
- def name
154
- @name
155
- end
156
-
157
-
158
- # The type of the field.
159
- # Possible values are <tt>:integer</tt>, <tt>:string</tt>, or <tt>:symbol</tt>.
160
-
161
- def type
162
- @type
163
- end
164
-
165
-
166
- # The initial value of the field
167
-
168
- def initial_value
169
- @initial_value
170
- end
171
-
172
-
173
- # Given a value, bump it to the "next" value.
174
- # Utilizes a bump procedure if given;
175
- # otherwise uses default behavior depending on the type.
176
-
177
- def bump_value(value_)
178
- if @bump_proc
179
- nvalue_ = @bump_proc.call(value_)
180
- nvalue_ || value_
181
- elsif @type == :integer || @type == :string
182
- value_.next
183
- else
184
- info_ = @symbol_info[value_]
185
- info_ ? info_[1] || value_ : nil
186
- end
187
- end
188
-
189
-
190
- # Perform a standard comparison on two values.
191
- # Returns an integer that may be positive, negative, or 0.
192
- # Utilizes a comparison procedure if given;
193
- # otherwise uses default behavior depending on the type.
194
-
195
- def compare_values(val1_, val2_)
196
- if @compare_proc
197
- @compare_proc.call(val1_, val2_)
198
- elsif @type == :integer || @type == :string
199
- val1_ <=> val2_
200
- else
201
- info1_ = @symbol_info[val1_]
202
- info2_ = @symbol_info[val2_]
203
- info1_ && info2_ ? info1_[0] <=> info2_[0] : nil
204
- end
205
- end
206
-
207
-
208
- # Given a value, return a "canonical" value for this field.
209
- # Utilizes a canonicalization procedure if given;
210
- # otherwise uses default behavior depending on the type.
211
- #
212
- # Raises Versionomy::Errors::IllegalValueError if the given value is not legal.
213
-
214
- def canonicalize_value(value_)
215
- if @canonicalize_proc
216
- value_ = @canonicalize_proc.call(value_)
217
- else
218
- case @type
219
- when :integer
220
- value_ = value_.to_i
221
- when :string
222
- value_ = value_.to_s
223
- when :symbol
224
- value_ = value_.to_sym
225
- end
226
- end
227
- if value_.nil? || (@type == :symbol && !@symbol_info.has_key?(value_))
228
- raise Versionomy::Errors::IllegalValueError
229
- end
230
- value_
231
- end
232
-
233
-
234
- # Define a format for this schema.
235
- #
236
- # You may either:
237
- #
238
- # * pass a format, or
239
- # * pass a name and provide a block that calls methods in
240
- # Versionomy::Format::Builder.
241
-
242
- def define_format(format_=nil, &block_)
243
- format_ = Versionomy::Format::Base.new(format_, &block_) if block_
244
- @formats[format_.name] = format_
245
- @default_format_name ||= format_.name
246
- end
247
-
248
-
249
- # Get the formatter with the given name.
250
- # If the name is nil, returns the default formatter.
251
- # If the name is not recognized, returns nil.
252
-
253
- def get_format(name_)
254
- @formats[name_ || @default_format_name]
255
- end
256
-
257
-
258
- # Returns the current default format name.
259
-
260
- def default_format_name
261
- @default_format_name
262
- end
263
-
264
-
265
- # Sets the default format by name.
266
-
267
- def default_format_name=(name_)
268
- if @formats[name_]
269
- @default_format_name = name_
270
- else
271
- nil
272
- end
273
- end
274
-
275
-
276
- # Create a new value with this schema.
277
- #
278
- # The values should either be a hash of field names and values, or an array
279
- # of values that will be interpreted in field order.
280
-
281
- def create(values_=nil)
282
- Versionomy::Value._new(self, values_)
283
- end
284
-
285
-
286
- # Create a new value by parsing the given string.
287
- #
288
- # The optional parameters may include a <tt>:format</tt> parameter that
289
- # specifies a format name. If no format is specified, the default format is used.
290
- # The remaining parameters are passed into the formatter's parse method.
291
- #
292
- # Raises Versionomy::Errors::UnknownFormatError if the given format name is not recognized.
293
- #
294
- # Raises Versionomy::Errors::ParseError if parsing failed.
295
-
296
- def parse(str_, params_={})
297
- format_ = get_format(params_[:format])
298
- if format_.nil?
299
- raise Versionomy::Errors::UnknownFormatError
300
- end
301
- value_ = format_.parse(self, str_, params_)
302
- end
303
-
304
-
305
- # Returns the subschema associated with the given value.
306
-
307
- def _subschema(value_) # :nodoc:
308
- if @ranges
309
- @ranges.each do |r_|
310
- if !r_[0].nil?
311
- cmp_ = compare_values(r_[0], value_)
312
- next if cmp_.nil? || cmp_ > 0
313
- end
314
- if !r_[1].nil?
315
- cmp_ = compare_values(r_[1], value_)
316
- next if cmp_.nil? || cmp_ < 0
317
- end
318
- return r_[2]
319
- end
320
- end
321
- @default_subschema
322
- end
323
-
324
-
325
- # Appends the given subschema for the given range
326
-
327
- def _append_schema(schema_, ranges_=nil) # :nodoc:
328
- if ranges_.nil?
329
- if @default_subschema
330
- raise Versionomy::Errors::RangeOverlapError
331
- end
332
- @default_subschema = schema_
333
- return
334
- end
335
- if !ranges_.is_a?(Array) || range_.size == 2 &&
336
- (range_[0].nil? || range_[0].is_a?(Symbol) ||
337
- range_[0].kind_of?(Integer) || range_[0].is_a?(String)) &&
338
- (range_[1].nil? || range_[1].is_a?(Symbol) ||
339
- range_[1].kind_of?(Integer) || range_[1].is_a?(String))
340
- then
341
- ranges_ = [ranges_]
342
- else
343
- ranges_ = ranges_.dup
344
- end
345
- ranges_.each do |range_|
346
- normalized_range_ = nil
347
- if range_.kind_of?(Array) && range_.size != 2
348
- raise Versionomy::Errors::RangeSpecificationError
349
- end
350
- case @type
351
- when :integer
352
- case range_
353
- when Array
354
- normalized_range_ = range_.map{ |elem_| elem_.nil? ? nil : elem_.to_i }
355
- when Range
356
- normalized_range_ = [range_.first, range_.exclude_end? ? range_.last-1 : range_.last]
357
- when String, Symbol, Integer
358
- range_ = range_.to_i
359
- normalized_range_ = [range_, range_]
360
- else
361
- raise Versionomy::Errors::RangeSpecificationError
362
- end
363
- when :string
364
- case range_
365
- when Array
366
- normalized_range_ = range_.map{ |elem_| elem_.nil? ? nil : elem_.to_s }
367
- when Range
368
- normalized_range_ = [range_.first.to_s,
369
- range_.exclude_end? ? (range_.last-1).to_s : range_.last.to_s]
370
- else
371
- range_ = range_.to_s
372
- normalized_range_ = [range_, range_]
373
- end
374
- when :symbol
375
- case range_
376
- when Array
377
- normalized_range_ = range_.map do |elem_|
378
- case elem_
379
- when nil
380
- nil
381
- when Integer
382
- elem_.to_s.to_sym
383
- else
384
- elem_.to_sym
385
- end
386
- end
387
- when String, Integer
388
- range_ = range_.to_s.to_sym
389
- normalized_range_ = [range_, range_]
390
- when Symbol
391
- normalized_range_ = [range_, range_]
392
- else
393
- raise Versionomy::Errors::RangeSpecificationError
394
- end
395
- end
396
- normalized_range_ << schema_
397
- @ranges ||= Array.new
398
- insert_index_ = @ranges.size
399
- @ranges.each_with_index do |r_, i_|
400
- if normalized_range_[0] && r_[1]
401
- cmp_ = compare_values(normalized_range_[0], r_[1])
402
- if cmp_.nil?
403
- raise Versionomy::Errors::RangeSpecificationError
404
- end
405
- if cmp_ > 0
406
- next
407
- end
408
- end
409
- if normalized_range_[1] && r_[0]
410
- cmp_ = compare_values(normalized_range_[1], r_[0])
411
- if cmp_.nil?
412
- raise Versionomy::Errors::RangeSpecificationError
413
- end
414
- if cmp_ < 0
415
- insert_index_ = i_
416
- break
417
- end
418
- end
419
- raise Versionomy::Errors::RangeOverlapError
420
- end
421
- @ranges.insert(insert_index_, normalized_range_)
422
- end
423
- end
424
-
425
-
426
- # These methods are available in a schema definition block.
427
-
428
- class Builder
429
-
430
- include Blockenspiel::DSL
431
-
432
- def initialize(schema_) # :nodoc:
433
- @schema = schema_
434
- end
435
-
436
-
437
- # Define the given symbol.
438
- #
439
- # Recognized options include:
440
- #
441
- # <tt>:bump</tt>::
442
- # The symbol to transition to when "bump" is called.
443
- # Default is to remain on the same value.
444
- #
445
- # Raises Versionomy::Errors::TypeMismatchError if called when the current schema
446
- # is not of type <tt>:symbol</tt>.
447
- #
448
- # Raises Versionomy::Errors::SymbolRedefinedError if the given symbol name is
449
- # already defined.
450
-
451
- def symbol(symbol_, opts_={})
452
- @schema._add_symbol(symbol_, opts_)
453
- end
454
-
455
-
456
- # Provide an initial value.
457
-
458
- def initial_value(value_)
459
- @schema._set_initial_value(value_)
460
- end
461
-
462
-
463
- # Provide a "bump" procedure.
464
- # The given block should take a value, and return the value to transition to.
465
- # If you return nil, the value will remain the same.
466
-
467
- def to_bump(&block_)
468
- @schema._set_bump_proc(block_)
469
- end
470
-
471
-
472
- # Provide a "compare" procedure.
473
- # The given block should take two values and compare them.
474
- # It should return a negative integer if the first is less than the second,
475
- # a positive integer if the first is greater than the second, or 0 if the
476
- # two values are equal. If the values cannot be compared, return nil.
477
-
478
- def to_compare(&block_)
479
- @schema._set_compare_proc(block_)
480
- end
481
-
482
-
483
- # Provide a "canonicalize" procedure.
484
- # The given block should take a value and return a canonicalized value.
485
- # Return nil if the given value is illegal.
486
-
487
- def to_canonicalize(&block_)
488
- @schema._set_canonicalize_proc(block_)
489
- end
490
-
491
-
492
- # Add a subschema.
493
- #
494
- # Recognized options include:
495
- #
496
- # <tt>:only</tt>::
497
- # This subschema should be available only for the given values of this schema.
498
- # See below for ways to specify this constraint.
499
- # <tt>:type</tt>::
500
- # Type of field. This should be <tt>:integer</tt>, <tt>:string</tt>, or <tt>:symbol</tt>.
501
- # Default is <tt>:integer</tt>.
502
- # <tt>:initial</tt>::
503
- # Initial value. Default is 0 for an integer field, the empty string for a string field,
504
- # or the first symbol added for a symbol field.
505
- #
506
- # You may provide an optional block. Within the block, you may call methods of this
507
- # class again to customize the subschema.
508
- #
509
- # Raises Versionomy::Errors::IllegalValueError if the given initial value is not legal.
510
- #
511
- # The <tt>:only</tt> constraint may be specified in one of the following ways:
512
- #
513
- # * A single value (integer, string, or symbol)
514
- # * A Range object defining a range of integers
515
- # * A two-element array indicating a range of integers, strings, or symbols, inclusive.
516
- # In this case, the ordering of symbols is defined by the order in which the symbols
517
- # were added to this schema.
518
- # If either element is nil, it is considered an open end of the range.
519
- # * An array of arrays in the above form.
520
-
521
- def schema(name_, opts_={}, &block_)
522
- @schema._append_schema(Versionomy::Schema.new(name_, opts_, &block_), opts_.delete(:only))
523
- end
524
-
525
-
526
- # Define a format for this schema.
527
- #
528
- # You may either:
529
- #
530
- # * pass a format, or
531
- # * pass a name and provide a block that calls methods in
532
- # Versionomy::Format::Builder.
533
-
534
- def define_format(format_=nil, &block_)
535
- @schema.define_format(format_, &block_)
536
- end
537
-
538
-
539
- # Sets the default format by name.
540
-
541
- def set_default_format_name(name_)
542
- @schema.default_format_name = name_
543
- end
544
-
545
-
546
- end
547
-
548
-
88
+ module Schema
549
89
  end
550
90
 
91
+
551
92
  end