versus 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/.index +45 -0
  2. data/.yardopts +7 -0
  3. data/HISTORY.md +11 -0
  4. data/Index.yml +37 -0
  5. data/LICENSE.txt +23 -0
  6. data/README.md +46 -0
  7. data/demo/applique/ae.rb +5 -0
  8. data/demo/applique/main.rb +1 -0
  9. data/demo/constraint/02_initialize.md +44 -0
  10. data/demo/constraint/04_to_proc.md +11 -0
  11. data/demo/constraint/09_to_gem_version.md +10 -0
  12. data/demo/number/02_initialize.md +14 -0
  13. data/demo/number/03_parse.md +23 -0
  14. data/demo/number/04_crush.md +17 -0
  15. data/demo/number/05_build.md +23 -0
  16. data/demo/number/06_cmp.md +100 -0
  17. data/demo/number/07_segments.md +51 -0
  18. data/demo/number/08_bump.md +46 -0
  19. data/demo/number/09_stable_release.md +23 -0
  20. data/demo/number/10_prerelease.md +19 -0
  21. data/demo/number/11_release_candidate.md +15 -0
  22. data/demo/number/19_match.md +10 -0
  23. data/demo/number/20_to_str.md +13 -0
  24. data/demo/resolver/01_initialize.md +23 -0
  25. data/demo/resolver/02_add.md +11 -0
  26. data/demo/resolver/03_libraries.md +24 -0
  27. data/demo/resolver/04_requirements.md +13 -0
  28. data/demo/resolver/05_possibilities.md +13 -0
  29. data/demo/resolver/06_resolve.md +42 -0
  30. data/lib/versus.rb +5 -0
  31. data/lib/versus/constraint.rb +130 -0
  32. data/lib/versus/core_ext.rb +4 -0
  33. data/lib/versus/core_ext/array.rb +11 -0
  34. data/lib/versus/core_ext/kernel.rb +19 -0
  35. data/lib/versus/core_ext/string.rb +10 -0
  36. data/lib/versus/exceptions.rb +8 -0
  37. data/lib/versus/file.rb +157 -0
  38. data/lib/versus/file/jeweler_format.rb +49 -0
  39. data/lib/versus/file/plain_format.rb +39 -0
  40. data/lib/versus/number.rb +516 -0
  41. data/lib/versus/resolver.rb +303 -0
  42. metadata +124 -0
@@ -0,0 +1,10 @@
1
+ require 'versus'
2
+
3
+ class String
4
+ #
5
+ # Converts the String into a version number.
6
+ #
7
+ def to_version
8
+ Version::Number.parse(self)
9
+ end
10
+ end
@@ -0,0 +1,8 @@
1
+ module Version
2
+
3
+ # Base calss for all version errors.
4
+ class Exception < RuntimeError
5
+ end
6
+
7
+ end
8
+
@@ -0,0 +1,157 @@
1
+ module Version
2
+ require 'versus/file/plain_format'
3
+ require 'versus/file/jeweler_format'
4
+
5
+ # Version::File class
6
+ #
7
+ class File
8
+
9
+ #
10
+ # Possible names for a version file to look for in automatic look-up.
11
+ #
12
+ NAMES = %w{
13
+ VERSION
14
+ VERSION.yml
15
+ VERSION.yaml
16
+ var/version
17
+ }
18
+
19
+ #
20
+ # Supported version file parse formats.
21
+ #
22
+ def self.supported_formats
23
+ [JewelerFormat, PlainFormat]
24
+ end
25
+
26
+ #
27
+ # Get current version by look-up of version file.
28
+ #
29
+ # If +path+ is nil, then caller is used to automatically set
30
+ # the look-up path. This allows for some very cool code-fu
31
+ # for those who keep their project version is a project file:
32
+ #
33
+ # module MyApp
34
+ # VERSION = Version::File.current
35
+ # end
36
+ #
37
+ # @return [Version::Number] version number
38
+ #
39
+ def self.current(path=nil)
40
+ vfile = lookup(path || File.dirname(caller.first))
41
+ vfile.version if vfile
42
+ end
43
+
44
+ #
45
+ # Look-up and return version file.
46
+ #
47
+ # @return [Version::File] version file instance
48
+ #
49
+ def self.lookup(path=nil)
50
+ # if path is nil, detect automatically; if path is a directory, detect
51
+ # automatically in the directory; if path is a filename, use it directly
52
+ file = if path
53
+ if ::File.file?(path)
54
+ ::File.expand_path(path)
55
+ else
56
+ version_file(path)
57
+ end
58
+ else
59
+ version_file(Dir.pwd)
60
+ end
61
+
62
+ return nil unless file && ::File.file?(file)
63
+
64
+ File.new(file)
65
+ end
66
+
67
+ #
68
+ # Attempts to detect the version file for the passed +filename+. Looks up
69
+ # the directory hierarchy for a file named VERSION or VERSION.yml. Returns
70
+ # a Pathname for the file if found, otherwise nil.
71
+ #
72
+ def self.version_file(path)
73
+ path = File.expand_path(path)
74
+ path = File.dirname(path) unless File.directory?(path)
75
+
76
+ return nil unless File.directory?(path)
77
+
78
+ home = File.expand_path('~')
79
+ done = nil
80
+
81
+ until path == '/' or path == home
82
+ NAMES.each do |name|
83
+ full = File.join(dir, name)
84
+ break(done = full) if File.file?(full)
85
+ end
86
+ break done if done
87
+ path = File.dirname(path)
88
+ end
89
+
90
+ done
91
+ end
92
+
93
+ #
94
+ # New Version::File instance.
95
+ #
96
+ def initialize(path)
97
+ @path = path
98
+ end
99
+
100
+ #
101
+ # Get the verison file format.
102
+ #
103
+ def format
104
+ @format ||= (
105
+ if read
106
+ fmt = self.class.supported_formats.find{ |fm| fm.match?(path, read) }
107
+ raise IOError, "Version file matches no known format."
108
+ else
109
+ PlainFormat
110
+ end
111
+ )
112
+ end
113
+
114
+ #
115
+ # Get a Version::Number instance from parsed file.
116
+ #
117
+ def number
118
+ @number ||= parse(read)
119
+ end
120
+ alias :version :number
121
+
122
+ #
123
+ # Change the number in the the file.
124
+ #
125
+ def change(number, file=nil)
126
+ @number = Number.parse(number)
127
+ save
128
+ end
129
+
130
+ #
131
+ # Read the version file.
132
+ #
133
+ def read
134
+ @read ||= File.read(path)
135
+ end
136
+
137
+ #
138
+ # Save the version file.
139
+ #
140
+ def save(file=nil)
141
+ file = file || path
142
+ text = format.render(number)
143
+ ::File.open(file, 'w'){ |f| f << text }
144
+ end
145
+
146
+ #
147
+ # Parse file constents.
148
+ #
149
+ # @return [Version::Number] version number
150
+ #
151
+ def parse(read)
152
+ format.parse(read)
153
+ end
154
+
155
+ end
156
+
157
+ end
@@ -0,0 +1,49 @@
1
+ module Version
2
+
3
+ class File
4
+
5
+ # Jeweler style VERSION file, e.g.
6
+ #
7
+ # ---
8
+ # :major: 1
9
+ # :minor: 0
10
+ # :patch: 0
11
+ # :build: pre.1
12
+ #
13
+ module JewelerFormat
14
+ extend self
15
+
16
+ #
17
+ #
18
+ #
19
+ def match?(path, data)
20
+ return false unless Hash === data
21
+ data = data.inject({}){|h,(k,v)| h[k.to_sym]=v; h}
22
+ keys = data.keys - [:major, :minor, :patch, :build]
23
+ keys.empty?
24
+ end
25
+
26
+ #
27
+ #
28
+ #
29
+ def render(number)
30
+ ":major: #{number[0]}\n" +
31
+ ":minor: #{number[1]}\n" +
32
+ ":patch: #{number[2]}\n" +
33
+ ":build: #{number[3..-1].join('.')}\n"
34
+ end
35
+
36
+ #
37
+ #
38
+ #
39
+ def parse(data)
40
+ data = data.inject({}){|h,(k,v)| h[k.to_sym]=v; h}
41
+ tuple = data.values_at(:major,:minor,:patch,:build).compact
42
+ Number.new(*tuple)
43
+ end
44
+
45
+ end
46
+
47
+ end
48
+
49
+ end
@@ -0,0 +1,39 @@
1
+ module Version
2
+
3
+ class File
4
+
5
+ # Plain style version file, e.g.
6
+ #
7
+ # 1.2.0
8
+ #
9
+ module PlainFormat
10
+ extend self
11
+
12
+ #
13
+ #
14
+ #
15
+ def match?(data)
16
+ return false unless String === data
17
+ # TODO: re-match here
18
+ return true
19
+ end
20
+
21
+ #
22
+ #
23
+ #
24
+ def render(number)
25
+ number.to_s
26
+ end
27
+
28
+ #
29
+ #
30
+ #
31
+ def parse(string)
32
+ Number.parse(string.strip)
33
+ end
34
+
35
+ end
36
+
37
+ end
38
+
39
+ end
@@ -0,0 +1,516 @@
1
+ module Version
2
+
3
+ #
4
+ # Shortcut for `Version::Number.parse()`.
5
+ #
6
+ def self.[](number)
7
+ Number.parse(number)
8
+ end
9
+
10
+ # Represents a versiou number. Developer SHOULD use three point
11
+ # SemVer standard, but this class is mildly flexible in it's support
12
+ # for variations.
13
+ #
14
+ # @see http://semver.org/
15
+ #
16
+ class Number
17
+ include Enumerable
18
+ include Comparable
19
+
20
+ # Recognized build states in order of completion.
21
+ # This is only used when by #bump_state.
22
+ STATES = ['alpha', 'beta', 'pre', 'rc']
23
+
24
+ #
25
+ # Creates a new version.
26
+ #
27
+ # @param points [Array] version points
28
+ #
29
+ def initialize(*points)
30
+ @crush = false
31
+ points.map! do |point|
32
+ sane_point(point)
33
+ end
34
+ @tuple = points.flatten.compact
35
+ end
36
+
37
+ #
38
+ #
39
+ #
40
+ def hash
41
+ @tuple.hash
42
+ end
43
+
44
+ # Shortcut for creating a new verison number
45
+ # given segmented elements.
46
+ #
47
+ # VersionNumber[1,0,0].to_s
48
+ # #=> "1.0.0"
49
+ #
50
+ # VersionNumber[1,0,0,:pre,2].to_s
51
+ # #=> "1.0.0.pre.2"
52
+ #
53
+ def self.[](*args)
54
+ new(*args)
55
+ end
56
+
57
+ #
58
+ # Parses a version string.
59
+ #
60
+ # @param [String] string
61
+ # The version string.
62
+ #
63
+ # @return [Version]
64
+ # The parsed version.
65
+ #
66
+ def self.parse(version)
67
+ case version
68
+ when String
69
+ new(*version.split('.'))
70
+ when Number #self.class
71
+ new(*version.to_a)
72
+ else
73
+ new(*version.to_ary) #to_a) ?
74
+ end
75
+ end
76
+
77
+ #
78
+ def self.cmp(version1, version2)
79
+ # TODO: class level compare might be handy
80
+ end
81
+
82
+ # Major version number
83
+ def major
84
+ (state_index && state_index == 0) ? nil : self[0]
85
+ end
86
+
87
+ # Minor version number
88
+ def minor
89
+ (state_index && state_index <= 1) ? nil : self[1]
90
+ end
91
+
92
+ # Patch version number
93
+ def patch
94
+ (state_index && state_index <= 2) ? nil : self[2]
95
+ end
96
+
97
+ # The build.
98
+ def build
99
+ if b = state_index
100
+ str = @tuple[b..-1].join('.')
101
+ str = crush_point(str) if crush?
102
+ str
103
+ elsif @tuple[3].nil?
104
+ nil
105
+ else
106
+ str = @tuple[3..-1].join('.')
107
+ str = crush_point(str) if crush?
108
+ str
109
+ end
110
+ end
111
+
112
+ #
113
+ def state
114
+ state_index ? @tuple[state_index] : nil
115
+ end
116
+
117
+ #
118
+ alias status state
119
+
120
+ # Return the state revision count. This is the
121
+ # number that occurs after the state.
122
+ #
123
+ # Version::Number[1,2,0,:rc,4].build_number
124
+ # #=> 4
125
+ #
126
+ def build_number #revision
127
+ if i = state_index
128
+ self[i+1] || 0
129
+ else
130
+ nil
131
+ end
132
+ end
133
+
134
+ # @param [Integer] major
135
+ # The major version number.
136
+ def major=(number)
137
+ @tuple[0] = number.to_i
138
+ end
139
+
140
+ # @param [Integer, nil] minor
141
+ # The minor version number.
142
+ def minor=(number)
143
+ @tuple[1] = number.to_i
144
+ end
145
+
146
+ # @param [Integer, nil] patch
147
+ # The patch version number.
148
+ def patch=(number)
149
+ @tuple[2] = number.to_i
150
+ end
151
+
152
+ # @param [Integer, nil] build (nil)
153
+ # The build version number.
154
+ def build=(point)
155
+ @tuple = @tuple[0...state_index] + sane_point(point)
156
+ end
157
+
158
+ #
159
+ def stable?
160
+ build.nil?
161
+ end
162
+
163
+ alias_method :stable_release?, :stable?
164
+
165
+ #
166
+ def alpha?
167
+ s = status.dowcase
168
+ s == 'alpha' or s == 'a'
169
+ end
170
+
171
+ #
172
+ def beta?
173
+ s = status.dowcase
174
+ s == 'beta' or s == 'b'
175
+ end
176
+
177
+ #
178
+ def prerelease?
179
+ status == 'pre'
180
+ end
181
+
182
+ #
183
+ def release_candidate?
184
+ status == 'rc'
185
+ end
186
+
187
+ # Fetch a sepecific segement by index number.
188
+ # In no value is found at that position than
189
+ # zero (0) is returned instead.
190
+ #
191
+ # v = Version::Number[1,2,0]
192
+ # v[0] #=> 1
193
+ # v[1] #=> 2
194
+ # v[3] #=> 0
195
+ # v[4] #=> 0
196
+ #
197
+ # Zero is returned instead of +nil+ to make different
198
+ # version numbers easier to compare.
199
+ def [](index)
200
+ @tuple.fetch(index,0)
201
+ end
202
+
203
+ # Returns a duplicate of the underlying version tuple.
204
+ #
205
+ def to_a
206
+ @tuple.dup
207
+ end
208
+
209
+ # Converts version to a dot-separated string.
210
+ #
211
+ # Version::Number[1,2,0].to_s
212
+ # #=> "1.2.0"
213
+ #
214
+ # TODO: crush
215
+ def to_s
216
+ str = @tuple.compact.join('.')
217
+ str = crush_point(str) if crush?
218
+ return str
219
+ end
220
+
221
+ # This method is the same as #to_s. It is here becuase
222
+ # `File.join` calls it instead of #to_s.
223
+ #
224
+ # VersionNumber[1,2,0].to_str
225
+ # #=> "1.2.0"
226
+ #
227
+ def to_str
228
+ to_s
229
+ end
230
+
231
+ # Returns a String detaling the version number.
232
+ # Essentially it is the same as #to_s.
233
+ #
234
+ # VersionNumber[1,2,0].inspect
235
+ # #=> "1.2.0"
236
+ #
237
+ def inspect
238
+ to_s
239
+ end
240
+
241
+ #
242
+ # Converts the version to YAML.
243
+ #
244
+ # @param [Hash] opts
245
+ # Options supporte by YAML.
246
+ #
247
+ # @return [String]
248
+ # The resulting YAML.
249
+ #
250
+ #--
251
+ # TODO: Should this be here?
252
+ #++
253
+ def to_yaml(opts={})
254
+ to_s.to_yaml(opts)
255
+ end
256
+
257
+ #
258
+ # Strict equality.
259
+ #
260
+ def eql?(other)
261
+ @tuple = other.tuple
262
+ end
263
+
264
+ #
265
+ #def ==(other)
266
+ # (self <=> other) == 0
267
+ #end
268
+
269
+ # Compare versions.
270
+ def <=>(other)
271
+ [@tuple.size, other.size].max.times do |i|
272
+ p1, p2 = (@tuple[i] || 0), (other[i] || 0)
273
+ # this is bit tricky, basically a string < integer.
274
+ if p1.class != p2.class
275
+ cmp = p2.to_s <=> p1.to_s
276
+ else
277
+ cmp = p1 <=> p2
278
+ end
279
+ return cmp unless cmp == 0
280
+ end
281
+ #(@tuple.size <=> other.size) * -1
282
+ return 0
283
+ end
284
+
285
+ # For pessimistic constraint (like '~>' in gems).
286
+ #
287
+ # FIXME: Ensure it can handle trailing state.
288
+ def =~(other)
289
+ upver = other.bump(:last)
290
+ #@segments >= other and @segments < upver
291
+ self >= other and self < upver
292
+ end
293
+
294
+ # Iterate of each segment of the version. This allows
295
+ # all enumerable methods to be used.
296
+ #
297
+ # Version::Number[1,2,3].map{|i| i + 1}
298
+ # #=> [2,3,4]
299
+ #
300
+ # Though keep in mind that the state segment is not
301
+ # a number (and techincally any segment can be a string
302
+ # instead of an integer).
303
+ def each(&block)
304
+ @tuple.each(&block)
305
+ end
306
+
307
+ # Return the number of version segements.
308
+ #
309
+ # Version::Number[1,2,3].size
310
+ # #=> 3
311
+ #
312
+ def size
313
+ @tuple.size
314
+ end
315
+
316
+ # Bump the version returning a new version number object.
317
+ # Select +which+ segement to bump by name: +major+, +minor+,
318
+ # +patch+, +state+, +build+ and also +last+.
319
+ #
320
+ # Version::Number[1,2,0].bump(:patch).to_s
321
+ # #=> "1.2.1"
322
+ #
323
+ # Version::Number[1,2,1].bump(:minor).to_s
324
+ # #=> "1.3.0"
325
+ #
326
+ # Version::Number[1,3,0].bump(:major).to_s
327
+ # #=> "2.0.0"
328
+ #
329
+ # Version::Number[1,3,0,:pre,1].bump(:build).to_s
330
+ # #=> "1.3.0.pre.2"
331
+ #
332
+ # Version::Number[1,3,0,:pre,2].bump(:state).to_s
333
+ # #=> "1.3.0.rc.1"
334
+ #
335
+ def bump(which=:patch)
336
+ case which.to_sym
337
+ when :major, :first
338
+ bump_major
339
+ when :minor
340
+ bump_minor
341
+ when :patch
342
+ bump_patch
343
+ when :state, :status
344
+ bump_state
345
+ when :build
346
+ bump_build
347
+ when :revision
348
+ bump_revision
349
+ when :last
350
+ bump_last
351
+ else
352
+ self.class.new(@tuple.dup.compact)
353
+ end
354
+ end
355
+
356
+ #
357
+ def bump_major
358
+ self.class[inc(major), 0, 0]
359
+ end
360
+
361
+ #
362
+ def bump_minor
363
+ self.class[major, inc(minor), 0]
364
+ end
365
+
366
+ #
367
+ def bump_patch
368
+ self.class[major, minor, inc(patch)]
369
+ end
370
+
371
+ #
372
+ def bump_state
373
+ if i = state_index
374
+ if n = inc(@tuple[i])
375
+ v = @tuple[0...i] + [n] + (@tuple[i+1] ? [1] : [])
376
+ else
377
+ v = @tuple[0...i]
378
+ end
379
+ else
380
+ v = @tuple.dup
381
+ end
382
+ self.class.new(v.compact)
383
+ end
384
+
385
+ #
386
+ alias :bump_status :bump_state
387
+
388
+ #
389
+ def bump_build
390
+ if i = state_index
391
+ if i == @tuple.size - 1
392
+ v = @tuple + [1]
393
+ else
394
+ v = @tuple[0...-1] + [inc(@tuple.last)]
395
+ end
396
+ else
397
+ if @tuple.size <= 3
398
+ v = @tuple + [1]
399
+ else
400
+ v = @tuple[0...-1] + [inc(@tuple.last)]
401
+ end
402
+ end
403
+ self.class.new(v.compact)
404
+ end
405
+
406
+ #
407
+ def bump_build_number #revision
408
+ if i = state_index
409
+ v = @tuple[0...-1] + [inc(@tuple.last)]
410
+ else
411
+ v = @tuple[0..2] + ['alpha', 1]
412
+ end
413
+ self.class.new(v.compact)
414
+ end
415
+
416
+ #
417
+ def bump_last
418
+ v = @tuple[0...-1] + [inc(@tuple.last)]
419
+ self.class.new(v.compact)
420
+ end
421
+
422
+ # Return a new version have the same major, minor and
423
+ # patch levels, but with a new state and revision count.
424
+ #
425
+ # Version::Number[1,2,3].restate(:pre,2).to_s
426
+ # #=> "1.2.3.pre.2"
427
+ #
428
+ # Version::Number[1,2,3,:pre,2].restate(:rc,4).to_s
429
+ # #=> "1.2.3.rc.4"
430
+ #
431
+ def restate(state, revision=1)
432
+ if i = state_index
433
+ v = @tuple[0...i] + [state.to_s] + [revision]
434
+ else
435
+ v = @tuple[0...3] + [state.to_s] + [revision]
436
+ end
437
+ self.class.new(v)
438
+ end
439
+
440
+ # Does the version string representation compact
441
+ # string segments with the subsequent number segement?
442
+ def crush?
443
+ @crush
444
+ end
445
+
446
+ # Does this version match a given constraint? The constraint is a String
447
+ # in the form of "{operator} {version number}".
448
+ #--
449
+ # TODO: match? will change as Constraint class is improved.
450
+ #++
451
+ def match?(*constraints)
452
+ constraints.all? do |c|
453
+ Constraint.constraint_lambda(c).call(self)
454
+ end
455
+ end
456
+
457
+ # protected
458
+
459
+ # Return the undelying segments array.
460
+ attr :tuple
461
+
462
+ private
463
+
464
+ # Convert a segment into an integer or string.
465
+ def sane_point(point)
466
+ point = point.to_s if Symbol === point
467
+ case point
468
+ when Integer
469
+ point
470
+ when /[.]/
471
+ point.split('.').map{ |p| sane_point(p) }
472
+ when /^\d+$/
473
+ point.to_i
474
+ when /^(\d+)(\w+)(\d+)$/
475
+ @crush = true
476
+ [$1.to_i, $2, $3.to_i]
477
+ when /^(\w+)(\d+)$/
478
+ @crush = true
479
+ [$1, $2.to_i]
480
+ else
481
+ point
482
+ end
483
+ end
484
+
485
+ # Take a point string rendering of a version and crush it!
486
+ def crush_point(string)
487
+ string.gsub(/(^|\.)(\D+)\.(\d+)(\.|$)/, '\2\3')
488
+ end
489
+
490
+ # Return the index of the first recognized state.
491
+ #
492
+ # VersionNumber[1,2,3,'pre',3].state_index
493
+ # #=> 3
494
+ #
495
+ # You might ask why this is needed, since the state
496
+ # position should always be 3. However, there isn't
497
+ # always a state entry, which means this method will
498
+ # return +nil+, and we also leave open the potential
499
+ # for extra-long version numbers --though we do not
500
+ # recommend the idea, it is possible.
501
+ def state_index
502
+ @tuple.index{ |s| String === s }
503
+ end
504
+
505
+ # Segement incrementor.
506
+ def inc(val)
507
+ if i = STATES.index(val.to_s)
508
+ STATES[i+1]
509
+ else
510
+ val.succ
511
+ end
512
+ end
513
+
514
+ end
515
+
516
+ end