versionomy 0.2.5 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -34,6 +34,10 @@
34
34
  ;
35
35
 
36
36
 
37
+ require 'thread'
38
+ require 'monitor'
39
+
40
+
37
41
  module Versionomy
38
42
 
39
43
 
@@ -57,28 +61,96 @@ module Versionomy
57
61
  # Versionomy::Format::Delimiter tool, which can be used to construct
58
62
  # parsers for many version number formats.
59
63
  #
64
+ # === Format registry
65
+ #
60
66
  # Formats may be registered with Versionomy and given a name using the
61
67
  # methods of this module. This allows version numbers to be serialized
62
- # with their format.
68
+ # with their format. When a version number is serialized, its format
69
+ # name is written to the stream, along with the version number's string
70
+ # representation. When the version number is reconstructed, its format
71
+ # is looked up by name so versionomy can determine how to parse the
72
+ # string.
73
+ #
74
+ # Format names are strings that may include letters, numerals, dashes,
75
+ # underscores, and periods. By convention, periods are used as namespace
76
+ # delimiters. Format names without a namespace (that is, with no periods)
77
+ # are considered reserved for standard versionomy formats. If you define
78
+ # your own format, you should use a name that includes a namespace (e.g.
79
+ # "mycompany.LibraryVersion") to reduce the chance of name collisions.
63
80
  #
64
- # Finally, this module serves as a namespace for format implementations.
81
+ # You may register formats directly using the register method, or set it
82
+ # up to be autoloaded on demand. When a format is requested, if it has
83
+ # not been registered explicitly, Versionomy looks for a format definition
84
+ # file for that format. Such a file has the name of the format, with the
85
+ # ".rb" extension for ruby (e.g. "mycompany.LibraryVersion.rb") and must
86
+ # be located in a directory in versionomy's format lookup path. By
87
+ # default, the directory containing versionomy's predefined formats
88
+ # (such as "standard") is in this path. However, you may add your own
89
+ # directories using the add_directory method. This lets you autoload your
90
+ # own formats. A format definition file itself must contain ruby code
91
+ # that defines the format and registers it using the correct name. See
92
+ # the files in the "lib/versionomy/format_definitions/" directory for
93
+ # examples.
65
94
 
66
95
  module Format
67
96
 
68
- @names_to_formats = ::Hash.new
69
- @formats_to_names = ::Hash.new
97
+ @mutex = ::Mutex.new
98
+ @load_mutex = ::Monitor.new
99
+ @directories = [::File.expand_path(::File.dirname(__FILE__)+'/format_definitions')]
100
+ @names_to_formats = {}
101
+ @formats_to_names = {}
70
102
 
71
103
  class << self
72
104
 
73
105
 
106
+ # Add a directory to the format path.
107
+ #
108
+ # The format path is an array of directory paths. These directories
109
+ # are searched for format definitions if a format name that has not
110
+ # been registered is requested.
111
+ #
112
+ # If high_priority_ is set to true, the directory is added to the
113
+ # front of the lookup path; otherwise it is added to the back.
114
+
115
+ def add_directory(path_, high_priority_=false)
116
+ path_ = ::File.expand_path(path_)
117
+ @mutex.synchronize do
118
+ unless @directories.include?(path_)
119
+ if high_priority_
120
+ @directories.unshift(path_)
121
+ else
122
+ @directories.push(path_)
123
+ end
124
+ end
125
+ end
126
+ end
127
+
128
+
74
129
  # Get the format with the given name.
75
130
  #
76
- # If the given name has not been defined, and strict is set to true,
77
- # raises Versionomy::Errors::UnknownFormatError. If strict is set to
78
- # false, returns nil if the given name has not been defined.
131
+ # If the given name has not been defined, attempts to autoload it from
132
+ # a format definition file. See the description of the Format module
133
+ # for details on this procedure.
134
+ #
135
+ # If the given name still cannot be resolved, and strict is set to
136
+ # true, raises Versionomy::Errors::UnknownFormatError. If strict is
137
+ # set to false, returns nil if the given name cannot be resolved.
79
138
 
80
139
  def get(name_, strict_=false)
81
- format_ = @names_to_formats[name_.to_s]
140
+ name_ = _check_name(name_)
141
+ format_ = @mutex.synchronize{ @names_to_formats[name_] }
142
+ if format_.nil?
143
+ # Attempt to find the format in the directory path
144
+ dirs_ = @mutex.synchronize{ @directories.dup }
145
+ dirs_.each do |dir_|
146
+ path_ = "#{dir_}/#{name_}.rb"
147
+ if ::File.readable?(path_)
148
+ @load_mutex.synchronize{ ::Kernel.load(path_) }
149
+ end
150
+ format_ = @mutex.synchronize{ @names_to_formats[name_] }
151
+ break unless format_.nil?
152
+ end
153
+ end
82
154
  if format_.nil? && strict_
83
155
  raise Errors::UnknownFormatError, name_
84
156
  end
@@ -86,6 +158,15 @@ module Versionomy
86
158
  end
87
159
 
88
160
 
161
+ # Determines whether a format with the given name has been registered
162
+ # explicitly. Does not attempt to autoload the format.
163
+
164
+ def registered?(name_)
165
+ name_ = _check_name(name_)
166
+ @mutex.synchronize{ @names_to_formats.include?(name_) }
167
+ end
168
+
169
+
89
170
  # Register the given format under the given name.
90
171
  #
91
172
  # Valid names may contain only letters, digits, underscores, dashes,
@@ -94,16 +175,18 @@ module Versionomy
94
175
  # Raises Versionomy::Errors::FormatRedefinedError if the name has
95
176
  # already been defined.
96
177
 
97
- def register(name_, format_)
98
- name_ = name_.to_s
99
- unless name_ =~ /\A[\w.-]+\z/
100
- raise ::ArgumentError, "Illegal name: #{name_.inspect}"
101
- end
102
- if @names_to_formats.include?(name_)
103
- raise Errors::FormatRedefinedError, name_
178
+ def register(name_, format_, silent_=false)
179
+ name_ = _check_name(name_)
180
+ @mutex.synchronize do
181
+ if @names_to_formats.include?(name_)
182
+ unless silent_
183
+ raise Errors::FormatRedefinedError, name_
184
+ end
185
+ else
186
+ @names_to_formats[name_] = format_
187
+ @formats_to_names[format_.object_id] = name_
188
+ end
104
189
  end
105
- @names_to_formats[name_] = format_
106
- @formats_to_names[format_.object_id] = name_
107
190
  end
108
191
 
109
192
 
@@ -115,7 +198,7 @@ module Versionomy
115
198
  # false, returns nil if the given format was never registered.
116
199
 
117
200
  def canonical_name_for(format_, strict_=false)
118
- name_ = @formats_to_names[format_.object_id]
201
+ name_ = @mutex.synchronize{ @formats_to_names[format_.object_id] }
119
202
  if name_.nil? && strict_
120
203
  raise Errors::UnknownFormatError
121
204
  end
@@ -123,6 +206,17 @@ module Versionomy
123
206
  end
124
207
 
125
208
 
209
+ private
210
+
211
+ def _check_name(name_) # :nodoc:
212
+ name_ = name_.to_s
213
+ unless name_ =~ /\A[\w.-]+\z/
214
+ raise ::ArgumentError, "Illegal name: #{name_.inspect}"
215
+ end
216
+ name_
217
+ end
218
+
219
+
126
220
  end
127
221
 
128
222
  end
@@ -128,7 +128,7 @@ module Versionomy
128
128
  to_compare_type(:string) do |a_, b_|
129
129
  if a_.kind_of?(::Integer)
130
130
  if b_.kind_of?(::Integer)
131
- a_ - b_
131
+ a_ <=> b_
132
132
  else
133
133
  1
134
134
  end
@@ -174,6 +174,10 @@ module Versionomy
174
174
  end
175
175
  end
176
176
 
177
+ # Some field aliases providing alternate names for major fields
178
+ alias_field(:major, :field0)
179
+ alias_field(:minor, :field1)
180
+
177
181
  # Add the methods in this module to each value
178
182
  add_module(Format::Rubygems::ExtraMethods)
179
183
  end
@@ -226,9 +230,114 @@ module Versionomy
226
230
  end
227
231
 
228
232
 
229
- register('rubygems', Format::Rubygems.create) unless get('rubygems')
233
+ register('rubygems', Format::Rubygems.create, true)
230
234
 
231
235
 
232
236
  end
233
237
 
238
+
239
+ module Conversion
240
+
241
+
242
+ # This is a namespace for the implementation of the conversion between
243
+ # the rubygems and standard formats.
244
+
245
+ module Rubygems
246
+
247
+
248
+ # Create the conversion from standard to rubygems format.
249
+ # This method is called internally when Versionomy initializes itself,
250
+ # and you should not need to call it again. It is documented, however,
251
+ # so that you can inspect its source code from RDoc, since the source
252
+ # contains useful examples of how to use the conversion DSLs.
253
+
254
+ def self.create_standard_to_rubygems
255
+
256
+ # We'll use a parsing conversion.
257
+ Conversion::Parsing.new do
258
+
259
+ # We're going to modify how the standard format version is
260
+ # unparsed, so the rubygems format will have a better chance
261
+ # of parsing it.
262
+ to_modify_unparse_params do |params_, convert_params_|
263
+
264
+ params_ ||= {}
265
+
266
+ # If the standard format version has a prerelease notation,
267
+ # make sure it is set off using a delimiter that the rubygems
268
+ # format can recognize. So instead of "1.0b2", we force the
269
+ # unparsing to generate "1.0.b.2".
270
+ params_[:release_type_delim] = '.'
271
+ params_[:development_version_delim] = '.'
272
+ params_[:alpha_version_delim] = '.'
273
+ params_[:beta_version_delim] = '.'
274
+ params_[:release_candidate_version_delim] = '.'
275
+ params_[:preview_version_delim] = '.'
276
+
277
+ # If the standard format version has a patchlevel notation,
278
+ # force it to use the default number rather than letter style.
279
+ # So instead of "1.2c", we force the unparsing to generate
280
+ # "1.2-3".
281
+ params_[:patchlevel_style] = nil
282
+
283
+ # If the standard format version has a patchlevel notation,
284
+ # force it to use the default delimiter of "-" so the rubygems
285
+ # format will recognize it. So instead of "1.9.1p243", we force
286
+ # the unparsing to generate "1.9.1-243".
287
+ params_[:patchlevel_delim] = nil
288
+
289
+ # If the standard format version includes a "v" prefix, strip
290
+ # it because rubygems doesn't like it.
291
+ params_[:major_delim] = nil
292
+
293
+ params_
294
+ end
295
+
296
+ # Standard formats sometimes allow hyphens and spaces in field
297
+ # delimiters, but the rubygems format requires periods. So modify
298
+ # the unparsed string to conform to rubygems's expectations.
299
+ to_modify_string do |str_, convert_params_|
300
+ str_.gsub(/[\.\s-]+/, '.')
301
+ end
302
+
303
+ end
304
+
305
+ end
306
+
307
+
308
+ # Create the conversion from rubygems to standard format.
309
+ # This method is called internally when Versionomy initializes itself,
310
+ # and you should not need to call it again. It is documented, however,
311
+ # so that you can inspect its source code from RDoc, since the source
312
+ # contains useful examples of how to use the conversion DSLs.
313
+
314
+ def self.create_rubygems_to_standard
315
+
316
+ # We'll use a parsing conversion.
317
+ Conversion::Parsing.new do
318
+
319
+ # Handle the case where the rubygems version ends with a string
320
+ # field, e.g. "1.0.b". We want to treat this like "1.0b0" rather
321
+ # than "1.0-2" since the rubygems semantic states that this is a
322
+ # prerelease version. So we add 0 to the end of the parsed string
323
+ # if it ends in a letter.
324
+ to_modify_string do |str_, convert_params_|
325
+ str_.gsub(/([[:alpha:]])\z/, '\10')
326
+ end
327
+
328
+ end
329
+
330
+ end
331
+
332
+
333
+ end
334
+
335
+
336
+ register(:standard, :rubygems, Conversion::Rubygems.create_standard_to_rubygems, true)
337
+ register(:rubygems, :standard, Conversion::Rubygems.create_rubygems_to_standard, true)
338
+
339
+
340
+ end
341
+
342
+
234
343
  end
@@ -390,7 +390,7 @@ module Versionomy
390
390
  end
391
391
 
392
392
 
393
- register('standard', Format::Standard.create) unless get('standard')
393
+ register('standard', Format::Standard.create, true)
394
394
 
395
395
 
396
396
  end
@@ -158,14 +158,14 @@ module Versionomy
158
158
  # constants RUBY_VERSION and RUBY_PATCHLEVEL.
159
159
 
160
160
  def ruby_version
161
- unless @ruby_version
162
- @ruby_version = parse(::RUBY_VERSION, :standard)
163
- if @ruby_version.release_type == :final
164
- @ruby_version = @ruby_version.change({:patchlevel => ::RUBY_PATCHLEVEL},
161
+ @ruby_version ||= begin
162
+ version_ = parse(::RUBY_VERSION, :standard)
163
+ if version_.release_type == :final
164
+ version_ = version_.change({:patchlevel => ::RUBY_PATCHLEVEL},
165
165
  :patchlevel_required => true, :patchlevel_delim => '-p')
166
166
  end
167
+ version_
167
168
  end
168
- @ruby_version
169
169
  end
170
170
 
171
171
 
@@ -55,10 +55,11 @@ module Versionomy
55
55
  ::Blockenspiel.invoke(block_, builder_)
56
56
  field_ = builder_._get_field
57
57
  modules_ = builder_._get_modules
58
+ aliases_ = builder_._get_aliases
58
59
  else
59
60
  modules_ = opts_[:modules] || []
60
61
  end
61
- Schema::Wrapper.new(field_, modules_)
62
+ Schema::Wrapper.new(field_, modules_, aliases_)
62
63
  end
63
64
 
64
65
 
@@ -71,10 +72,18 @@ module Versionomy
71
72
  # This is a low-level method. Usually you should call
72
73
  # Versionomy::Schema#create instead.
73
74
 
74
- def initialize(field_, modules_=[])
75
+ def initialize(field_, modules_=[], aliases_={})
75
76
  @root_field = field_
76
77
  @names = @root_field._descendants_by_name
77
78
  @modules = modules_
79
+ @aliases = {}
80
+ aliases_.each do |k_,v_|
81
+ k_ = k_.to_sym
82
+ v_ = v_.to_sym
83
+ if @names.include?(v_) && !@names.include?(k_)
84
+ @aliases[k_] = v_
85
+ end
86
+ end
78
87
  end
79
88
 
80
89
 
@@ -95,7 +104,7 @@ module Versionomy
95
104
 
96
105
  def eql?(obj_)
97
106
  return false unless obj_.kind_of?(Schema::Wrapper)
98
- return @root_field == obj_.root_field && @modules == obj_.modules
107
+ return @root_field == obj_.root_field && @modules == obj_.modules && @aliases == obj_.aliases
99
108
  end
100
109
 
101
110
 
@@ -134,16 +143,29 @@ module Versionomy
134
143
  end
135
144
 
136
145
 
146
+ # Return the canonical field name given a name, or nil if the name
147
+ # is not recognized.
148
+
149
+ def canonical_name(name_)
150
+ name_ = name_.to_sym
151
+ name_ = @aliases[name_] || name_
152
+ @names.include?(name_) ? name_ : nil
153
+ end
154
+
155
+
137
156
  # Return the field with the given name, or nil if the given name
138
- # is not found in this schema.
157
+ # is not found in this schema. If include_aliases_ is set to true,
158
+ # this also supports lookup by alias.
139
159
 
140
- def field_named(name_)
141
- @names[name_.to_sym]
160
+ def field_named(name_, include_aliases_=false)
161
+ name_ = name_.to_sym
162
+ name_ = @aliases[name_] || name_ if include_aliases_
163
+ field_ = @names[name_]
142
164
  end
143
165
 
144
166
 
145
167
  # Returns an array of names present in this schema, in no particular
146
- # order.
168
+ # order. Does not include aliases.
147
169
 
148
170
  def names
149
171
  @names.keys
@@ -158,6 +180,13 @@ module Versionomy
158
180
  end
159
181
 
160
182
 
183
+ # Returns a hash of field name aliases.
184
+
185
+ def aliases
186
+ @aliases.dup
187
+ end
188
+
189
+
161
190
  end
162
191
 
163
192
 
@@ -171,6 +200,7 @@ module Versionomy
171
200
  def initialize() # :nodoc:
172
201
  @field = nil
173
202
  @modules = []
203
+ @aliases = {}
174
204
  @defaults = { :integer => {}, :string => {}, :symbol => {} }
175
205
  end
176
206
 
@@ -204,28 +234,64 @@ module Versionomy
204
234
  end
205
235
 
206
236
 
207
- # Add a module to values that use this schema.
237
+ # Create a field alias.
238
+
239
+ def alias_field(alias_name_, field_name_)
240
+ @aliases[alias_name_.to_sym] = field_name_.to_sym
241
+ end
242
+
243
+
244
+ # Add a module to the schema. All values that use this schema will
245
+ # include this module. This provides a way to add schema-specific
246
+ # capabilities to version numbers.
208
247
 
209
248
  def add_module(mod_)
210
249
  @modules << mod_
211
250
  end
212
251
 
213
252
 
253
+ # Provide a default bump procedure for the given type.
254
+ # The type should be <tt>:integer</tt>, <tt>:string</tt>, or
255
+ # <tt>:symbol</tt>. You must provide a block that takes a field value
256
+ # and returns the "bumped" value. This procedure will be used for
257
+ # all fields of this type, unless explicitly overridden by the field.
258
+
214
259
  def to_bump_type(type_, &block_)
215
260
  @defaults[type_][:bump] = block_
216
261
  end
217
262
 
218
263
 
264
+ # Provide a default compare procedure for the given type.
265
+ # The type should be <tt>:integer</tt>, <tt>:string</tt>, or
266
+ # <tt>:symbol</tt>. You must provide a block that takes two values
267
+ # and returns a standard comparison result-- that is, a negative
268
+ # integer if the first value is less, 0 if the values are equal, or a
269
+ # positive integer if the first value is greater. This procedure will
270
+ # be used for all fields of this type, unless explicitly overridden
271
+ # by the field.
272
+
219
273
  def to_compare_type(type_, &block_)
220
274
  @defaults[type_][:compare] = block_
221
275
  end
222
276
 
223
277
 
278
+ # Provide a default canonicalization procedure for the given type.
279
+ # The type should be <tt>:integer</tt>, <tt>:string</tt>, or
280
+ # <tt>:symbol</tt>. You must provide a block that takes a field value
281
+ # and returns the canonical value. This procedure will be used for
282
+ # all fields of this type, unless explicitly overridden by the field.
283
+
224
284
  def to_canonicalize_type(type_, &block_)
225
285
  @defaults[type_][:canonicalize] = block_
226
286
  end
227
287
 
228
288
 
289
+ # Provide a default value for the given type.
290
+ # The type should be <tt>:integer</tt>, <tt>:string</tt>, or
291
+ # <tt>:symbol</tt>. You must provide a default value that will be
292
+ # used for all fields of this type, unless explicitly overridden by
293
+ # the field.
294
+
229
295
  def default_value_for_type(type_, value_)
230
296
  @defaults[type_][:value] = value_
231
297
  end
@@ -239,6 +305,10 @@ module Versionomy
239
305
  @modules
240
306
  end
241
307
 
308
+ def _get_aliases # :nodoc:
309
+ @aliases
310
+ end
311
+
242
312
  def _get_default_setting(type_, setting_) # :nodoc:
243
313
  @defaults[type_][setting_]
244
314
  end