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.
@@ -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