versionomy 0.0.4 → 0.1.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.
@@ -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