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.
- data/History.rdoc +31 -0
- data/README.rdoc +144 -0
- data/Rakefile +93 -12
- data/lib/versionomy/errors.rb +30 -11
- data/lib/versionomy/format/base.rb +96 -0
- data/lib/versionomy/format/delimiter.rb +951 -0
- data/lib/versionomy/format.rb +26 -112
- data/lib/versionomy/formats/standard.rb +346 -0
- data/lib/versionomy/formats.rb +79 -0
- data/lib/versionomy/interface.rb +23 -17
- data/lib/versionomy/schema/field.rb +500 -0
- data/lib/versionomy/schema/wrapper.rb +177 -0
- data/lib/versionomy/schema.rb +41 -500
- data/lib/versionomy/value.rb +129 -157
- data/lib/versionomy/version.rb +8 -8
- data/lib/versionomy.rb +25 -10
- data/tests/tc_custom_format.rb +66 -0
- data/tests/tc_readme_examples.rb +121 -0
- data/tests/tc_standard_basic.rb +17 -16
- data/tests/tc_standard_bump.rb +11 -10
- data/tests/tc_standard_change.rb +2 -1
- data/tests/tc_standard_comparison.rb +2 -1
- data/tests/tc_standard_parse.rb +41 -23
- metadata +28 -31
- data/History.txt +0 -21
- data/Manifest.txt +0 -17
- data/README.txt +0 -133
- data/lib/versionomy/standard.rb +0 -392
@@ -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
|