versionomy 0.0.4 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,500 @@
1
+ # -----------------------------------------------------------------------------
2
+ #
3
+ # Versionomy schema field class
4
+ #
5
+ # -----------------------------------------------------------------------------
6
+ # Copyright 2008-2009 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
+ require 'set'
38
+
39
+
40
+ module Versionomy
41
+
42
+ module Schema
43
+
44
+
45
+ # Objects of this class represent fields in a schema.
46
+
47
+ class Field
48
+
49
+
50
+ # Create a field with the given name.
51
+ #
52
+ # Recognized options include:
53
+ #
54
+ # <tt>:type</tt>::
55
+ # Type of field. This should be <tt>:integer</tt>, <tt>:string</tt>,
56
+ # or <tt>:symbol</tt>. Default is <tt>:integer</tt>.
57
+ # <tt>:default_value</tt>::
58
+ # Default value for the field if no value is explicitly set. Default
59
+ # is 0 for an integer field, the empty string for a string field, or
60
+ # the first symbol added for a symbol field.
61
+ #
62
+ # You may provide an optional block. Within the block, you may call
63
+ # methods of Versionomy::Schema::FieldBuilder to further customize the
64
+ # field, or add child fields.
65
+ #
66
+ # Raises Versionomy::Errors::IllegalValueError if the given default
67
+ # value is not legal.
68
+
69
+ def initialize(name_, opts_={}, &block_)
70
+ @name = name_.to_sym
71
+ @type = opts_[:type] || :integer
72
+ @default_value = opts_[:default_value]
73
+ if @type == :symbol
74
+ @symbol_info = ::Hash.new
75
+ @symbol_order = ::Array.new
76
+ else
77
+ @symbol_info = nil
78
+ @symbol_order = nil
79
+ end
80
+ @bump_proc = nil
81
+ @compare_proc = nil
82
+ @canonicalize_proc = nil
83
+ @ranges = nil
84
+ @default_child = nil
85
+ @children = []
86
+ ::Blockenspiel.invoke(block_, Schema::FieldBuilder.new(self)) if block_
87
+ @default_value = canonicalize_value(@default_value)
88
+ end
89
+
90
+
91
+ def _set_default_value(value_) # :nodoc:
92
+ @default_value = value_
93
+ end
94
+
95
+ def _add_symbol(symbol_, opts_={}) # :nodoc:
96
+ if @type != :symbol
97
+ raise Errors::TypeMismatchError
98
+ end
99
+ if @symbol_info.has_key?(symbol_)
100
+ raise Errors::SymbolRedefinedError
101
+ end
102
+ @symbol_info[symbol_] = [@symbol_order.size, opts_[:bump]]
103
+ @symbol_order << symbol_
104
+ if @default_value.nil?
105
+ @default_value = symbol_
106
+ end
107
+ end
108
+
109
+ def _set_bump_proc(block_) # :nodoc:
110
+ @bump_proc = block_
111
+ end
112
+
113
+ def _set_canonicalize_proc(block_) # :nodoc:
114
+ @canonicalize_proc = block_
115
+ end
116
+
117
+ def _set_compare_proc(block_) # :nodoc:
118
+ @compare_proc = block_
119
+ end
120
+
121
+
122
+ def inspect # :nodoc:
123
+ to_s
124
+ end
125
+
126
+ def to_s # :nodoc:
127
+ "#<#{self.class}:0x#{object_id.to_s(16)} name=#{@name}>"
128
+ end
129
+
130
+
131
+ # The name of the field.
132
+
133
+ def name
134
+ @name
135
+ end
136
+
137
+
138
+ # The type of the field.
139
+ # Possible values are <tt>:integer</tt>, <tt>:string</tt>, or
140
+ # <tt>:symbol</tt>.
141
+
142
+ def type
143
+ @type
144
+ end
145
+
146
+
147
+ # The default value of the field
148
+
149
+ def default_value
150
+ @default_value
151
+ end
152
+
153
+
154
+ # Returns a list of possible values for this field, if the type is
155
+ # <tt>:symbol</tt>. Returns nil for any other type
156
+
157
+ def possible_values
158
+ @symbol_order ? @symbol_order.dup : nil
159
+ end
160
+
161
+
162
+ # Given a value, bump it to the "next" value.
163
+ # Utilizes a bump procedure if given;
164
+ # otherwise uses default behavior depending on the type.
165
+
166
+ def bump_value(value_)
167
+ if @bump_proc
168
+ nvalue_ = @bump_proc.call(value_)
169
+ nvalue_ || value_
170
+ elsif @type == :integer || @type == :string
171
+ value_.next
172
+ else
173
+ info_ = @symbol_info[value_]
174
+ info_ ? info_[1] || value_ : nil
175
+ end
176
+ end
177
+
178
+
179
+ # Perform a standard comparison on two values.
180
+ # Returns an integer that may be positive, negative, or 0.
181
+ # Utilizes a comparison procedure if given;
182
+ # otherwise uses default behavior depending on the type.
183
+
184
+ def compare_values(val1_, val2_)
185
+ if @compare_proc
186
+ @compare_proc.call(val1_, val2_)
187
+ elsif @type == :integer || @type == :string
188
+ val1_ <=> val2_
189
+ else
190
+ info1_ = @symbol_info[val1_]
191
+ info2_ = @symbol_info[val2_]
192
+ info1_ && info2_ ? info1_[0] <=> info2_[0] : nil
193
+ end
194
+ end
195
+
196
+
197
+ # Given a value, return a "canonical" value for this field.
198
+ # Utilizes a canonicalization procedure if given;
199
+ # otherwise uses default behavior depending on the type.
200
+ #
201
+ # Raises Versionomy::Errors::IllegalValueError if the given value is
202
+ # not legal.
203
+
204
+ def canonicalize_value(value_)
205
+ orig_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 rescue nil
212
+ when :string
213
+ value_ = value_.to_s rescue nil
214
+ when :symbol
215
+ value_ = value_.to_sym rescue nil
216
+ end
217
+ end
218
+ if value_.nil? || (@type == :symbol && !@symbol_info.has_key?(value_))
219
+ raise Errors::IllegalValueError, "#{@name} does not allow the value #{orig_value_.inspect}"
220
+ end
221
+ value_
222
+ end
223
+
224
+
225
+ # Returns the child field associated with the given value.
226
+ # Returns nil if this field has no child for the given value.
227
+
228
+ def child(value_) # :nodoc:
229
+ if @ranges
230
+ @ranges.each do |r_|
231
+ if !r_[0].nil?
232
+ cmp_ = compare_values(r_[0], value_)
233
+ next if cmp_.nil? || cmp_ > 0
234
+ end
235
+ if !r_[1].nil?
236
+ cmp_ = compare_values(r_[1], value_)
237
+ next if cmp_.nil? || cmp_ < 0
238
+ end
239
+ return r_[2]
240
+ end
241
+ end
242
+ @default_child
243
+ end
244
+
245
+
246
+ # Adds the given child field for the given range.
247
+ #
248
+ # If you provide a range of nil, adds the given child field as the
249
+ # default child for values that do not fall into any other
250
+ # explicitly specified range.
251
+ #
252
+ # Otherwise, the ranges parameter must be an array of "range" objects.
253
+ # Each of these range objects must be either a single String, Symbol,
254
+ # or Integer to specify a single value; or a two-element array or a
255
+ # Range object (only inclusive ends are supported) to specify a range
256
+ # of values.
257
+ #
258
+ # Raises Versionomy::Errors::RangeOverlapError if the specified
259
+ # range overlaps another previously specified range, or if more than
260
+ # one default child has been set.
261
+ #
262
+ # Raises Versionomy::Errors::RangeSpecificationError if the range
263
+ # is incorrectly specified.
264
+ #
265
+ # Raises Versionomy::Errors::CircularDescendantError if adding this
266
+ # child will result in a circular reference.
267
+
268
+ def add_child(child_, ranges_=nil)
269
+ if child_._descendant_fields.include?(self)
270
+ raise Errors::CircularDescendantError
271
+ end
272
+ @children << child_
273
+ if ranges_.nil?
274
+ if @default_child
275
+ raise Errors::RangeOverlapError("Cannot have more than one default child")
276
+ end
277
+ @default_child = child_
278
+ return
279
+ end
280
+ ranges_ = [ranges_] unless ranges_.is_a?(Array)
281
+ ranges_.each do |range_|
282
+ case range_
283
+ when ::Range
284
+ if range_.exclude_end?
285
+ raise Errors::RangeSpecificationError("Ranges must be inclusive")
286
+ end
287
+ normalized_range_ = [range_.first, range_.last]
288
+ when ::Array
289
+ if range_.size != 2
290
+ raise Errors::RangeSpecificationError("Range array should have two elements")
291
+ end
292
+ normalized_range_ = range_.dup
293
+ when ::String, ::Symbol, ::Integer
294
+ normalized_range_ = [range_, range_]
295
+ else
296
+ raise Errors::RangeSpecificationError("Unrecognized range type #{range_.class}")
297
+ end
298
+ normalized_range_.map! do |elem_|
299
+ if elem_.nil?
300
+ elem_
301
+ else
302
+ case @type
303
+ when :integer
304
+ elem_.to_i
305
+ when :string
306
+ elem_.to_s
307
+ when :symbol
308
+ begin
309
+ elem_.to_sym
310
+ rescue
311
+ raise Errors::RangeSpecificationError("Bad symbol value: #{elem_.inspect}")
312
+ end
313
+ end
314
+ end
315
+ end
316
+ normalized_range_ << child_
317
+ @ranges ||= Array.new
318
+ insert_index_ = @ranges.size
319
+ @ranges.each_with_index do |r_, i_|
320
+ if normalized_range_[0] && r_[1]
321
+ cmp_ = compare_values(normalized_range_[0], r_[1])
322
+ if cmp_.nil?
323
+ raise Errors::RangeSpecificationError
324
+ end
325
+ if cmp_ > 0
326
+ next
327
+ end
328
+ end
329
+ if normalized_range_[1] && r_[0]
330
+ cmp_ = compare_values(normalized_range_[1], r_[0])
331
+ if cmp_.nil?
332
+ raise Errors::RangeSpecificationError
333
+ end
334
+ if cmp_ < 0
335
+ insert_index_ = i_
336
+ break
337
+ end
338
+ end
339
+ raise Errors::RangeOverlapError
340
+ end
341
+ @ranges.insert(insert_index_, normalized_range_)
342
+ end
343
+ end
344
+
345
+
346
+ # Compute descendants as a hash of names to fields, including this field.
347
+
348
+ def _descendants_by_name # :nodoc:
349
+ hash_ = {@name => self}
350
+ @children.each{ |child_| hash_.merge!(child_._descendants_by_name) }
351
+ hash_
352
+ end
353
+
354
+
355
+ # Return a set of all descendant fields, including this field.
356
+
357
+ def _descendant_fields(set_=nil) # :nodoc:
358
+ set_ ||= Set.new
359
+ set_ << self
360
+ @children.each{ |child_| child_._descendant_fields(set_) }
361
+ set_
362
+ end
363
+
364
+
365
+ end
366
+
367
+
368
+ # These methods are available in a schema field definition block.
369
+
370
+ class FieldBuilder
371
+
372
+ include ::Blockenspiel::DSL
373
+
374
+ def initialize(field_) # :nodoc:
375
+ @field = field_
376
+ end
377
+
378
+
379
+ # Define the given symbol.
380
+ #
381
+ # Recognized options include:
382
+ #
383
+ # <tt>:bump</tt>::
384
+ # The symbol to transition to when "bump" is called.
385
+ # Default is to remain on the same value.
386
+ #
387
+ # Raises Versionomy::Errors::TypeMismatchError if called when the current field
388
+ # is not of type <tt>:symbol</tt>.
389
+ #
390
+ # Raises Versionomy::Errors::SymbolRedefinedError if the given symbol name is
391
+ # already defined.
392
+
393
+ def symbol(symbol_, opts_={})
394
+ @field._add_symbol(symbol_, opts_)
395
+ end
396
+
397
+
398
+ # Provide a default value.
399
+
400
+ def default_value(value_)
401
+ @field._set_default_value(value_)
402
+ end
403
+
404
+
405
+ # Provide a "bump" procedure.
406
+ # The given block should take a value, and return the value to transition to.
407
+ # If you return nil, the value will remain the same.
408
+
409
+ def to_bump(&block_)
410
+ @field._set_bump_proc(block_)
411
+ end
412
+
413
+
414
+ # Provide a "compare" procedure.
415
+ # The given block should take two values and compare them.
416
+ # It should return a negative integer if the first is less than the second,
417
+ # a positive integer if the first is greater than the second, or 0 if the
418
+ # two values are equal. If the values cannot be compared, return nil.
419
+
420
+ def to_compare(&block_)
421
+ @field._set_compare_proc(block_)
422
+ end
423
+
424
+
425
+ # Provide a "canonicalize" procedure.
426
+ # The given block should take a value and return a canonicalized value.
427
+ # Return nil if the given value is illegal.
428
+
429
+ def to_canonicalize(&block_)
430
+ @field._set_canonicalize_proc(block_)
431
+ end
432
+
433
+
434
+ # Add a child field.
435
+ #
436
+ # Recognized options include:
437
+ #
438
+ # <tt>:only</tt>::
439
+ # The child should be available only for the given values of this
440
+ # field. See below for ways to specify this constraint.
441
+ # <tt>:type</tt>::
442
+ # Type of field. This should be <tt>:integer</tt>, <tt>:string</tt>,
443
+ # or <tt>:symbol</tt>. Default is <tt>:integer</tt>.
444
+ # <tt>:default_value</tt>::
445
+ # Default value for the field if no value is explicitly set. Default
446
+ # is 0 for an integer field, the empty string for a string field, or
447
+ # the first symbol added for a symbol field.
448
+ #
449
+ # You may provide an optional block. Within the block, you may call
450
+ # methods of this class again to customize the child.
451
+ #
452
+ # Raises Versionomy::Errors::IllegalValueError if the given default
453
+ # value is not legal.
454
+ #
455
+ # The <tt>:only</tt> constraint may be specified in one of the
456
+ # following ways:
457
+ #
458
+ # * A single value (integer, string, or symbol)
459
+ # * The result of calling range() to define an inclusive range of
460
+ # integers, strings, or symbols. In this case, either element may be
461
+ # nil, specifying an open end of the range. If the field type is
462
+ # symbol, the ordering of symbols for the range is defined by the
463
+ # order in which the symbols were added to this schema.
464
+ # * A Range object defining a range of integers or strings.
465
+ # Only inclusive, not exclusive, ranges are supported.
466
+ # * An array of the above.
467
+ #
468
+ # Raises Versionomy::Errors::RangeSpecificationError if the given
469
+ # ranges are not legal.
470
+ #
471
+ # Raises Versionomy::Errors::RangeOverlapError if the given ranges
472
+ # overlap previously specified ranges, or more than one default schema
473
+ # is specified.
474
+
475
+ def field(name_, opts_={}, &block_)
476
+ only_ = opts_.delete(:only)
477
+ @field.add_child(Schema::Field.new(name_, opts_, &block_), only_)
478
+ end
479
+
480
+
481
+ # Define a range for the <tt>:only</tt> parameter to +child+.
482
+ #
483
+ # This creates an object that +child+ interprets like a standard ruby Range. However, it
484
+ # is customized for the use of +child+ in the following ways:
485
+ #
486
+ # * It supports only inclusive, not exclusive ranges.
487
+ # * It supports open-ended ranges by setting either endpoint to nil.
488
+ # * It supports symbol ranges under Ruby 1.8.
489
+
490
+ def range(first_, last_)
491
+ [first_, last_]
492
+ end
493
+
494
+
495
+ end
496
+
497
+
498
+ end
499
+
500
+ end
@@ -0,0 +1,177 @@
1
+ # -----------------------------------------------------------------------------
2
+ #
3
+ # Versionomy schema wrapper class
4
+ #
5
+ # -----------------------------------------------------------------------------
6
+ # Copyright 2008-2009 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
+ module Schema
40
+
41
+
42
+ # Creates a schema.
43
+ # Returns an object of type Versionomy::Schema::Wrapper.
44
+ #
45
+ # You may either pass a root field, or provide a block to use to build
46
+ # fields. If you provide a block, you must use the methods in
47
+ # Versionomy::Schema::Builder in the block to create the root field.
48
+
49
+ def self.create(field_=nil, &block_)
50
+ if field_ && block_
51
+ raise ::ArgumentError, 'You may provide either a root field or block but not both'
52
+ end
53
+ if block_
54
+ builder_ = Schema::Builder.new
55
+ ::Blockenspiel.invoke(block_, builder_)
56
+ field_ = builder_._get_field
57
+ end
58
+ Schema::Wrapper.new(field_)
59
+ end
60
+
61
+
62
+ # Schemas are generally referenced through an object of this class.
63
+
64
+ class Wrapper
65
+
66
+
67
+ # Create a new schema wrapper object given a root field.
68
+ # This is a low-level method. Usually you should call
69
+ # Versionomy::Schema#create instead.
70
+
71
+ def initialize(field_)
72
+ @root_field = field_
73
+ @names = @root_field._descendants_by_name
74
+ end
75
+
76
+
77
+ # Returns true if this schema is equivalent to the other schema.
78
+ # Two schemas are equivalent if their root fields are the same--
79
+ # which means that the entire field tree is the same.
80
+
81
+ def eql?(obj_)
82
+ return false unless obj_.kind_of?(Schema::Wrapper)
83
+ return @root_field == obj_.root_field
84
+ end
85
+
86
+
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.
90
+
91
+ def ==(obj_)
92
+ eql?(obj_)
93
+ end
94
+
95
+
96
+ def hash # :nodoc:
97
+ @hash ||= @root_field.hash
98
+ end
99
+
100
+
101
+ # Returns the root (most significant) field in this schema.
102
+
103
+ def root_field
104
+ @root_field
105
+ end
106
+
107
+
108
+ # Return the field with the given name, or nil if the given name
109
+ # is not found in this schema.
110
+
111
+ def field_named(name_)
112
+ @names[name_.to_sym]
113
+ end
114
+
115
+
116
+ # Returns an array of names present in this schema, in no particular
117
+ # order.
118
+
119
+ def names
120
+ @names.keys
121
+ end
122
+
123
+
124
+ end
125
+
126
+
127
+ # These methods are available in a schema definition block given to
128
+ # Versionomy::Schema#create.
129
+
130
+ class Builder
131
+
132
+ include ::Blockenspiel::DSL
133
+
134
+ def initialize() # :nodoc:
135
+ @field = nil
136
+ end
137
+
138
+
139
+ # Create the root field.
140
+ #
141
+ # Recognized options include:
142
+ #
143
+ # <tt>:type</tt>::
144
+ # Type of field. This should be <tt>:integer</tt>, <tt>:string</tt>,
145
+ # or <tt>:symbol</tt>. Default is <tt>:integer</tt>.
146
+ # <tt>:default_value</tt>::
147
+ # Default value for the field if no value is explicitly set. Default
148
+ # is 0 for an integer field, the empty string for a string field, or
149
+ # the first symbol added for a symbol field.
150
+ #
151
+ # You may provide an optional block. Within the block, you may call
152
+ # methods of Versionomy::Schema::FieldBuilder to customize this field.
153
+ #
154
+ # Raises Versionomy::Errors::IllegalValueError if the given default
155
+ # value is not legal.
156
+ #
157
+ # Raises Versionomy::Errors::RangeOverlapError if a root field has
158
+ # already been created.
159
+
160
+ def field(name_, opts_={}, &block_)
161
+ if @field
162
+ raise Errors::RangeOverlapError, "Root field already defined"
163
+ end
164
+ @field = Schema::Field.new(name_, opts_, &block_)
165
+ end
166
+
167
+
168
+ def _get_field # :nodoc:
169
+ @field
170
+ end
171
+
172
+ end
173
+
174
+
175
+ end
176
+
177
+ end