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