indexer 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 (47) hide show
  1. data/.index +54 -0
  2. data/HISTORY.md +9 -0
  3. data/README.md +145 -0
  4. data/bin/index +7 -0
  5. data/data/indexer/r2013/index.kwalify +175 -0
  6. data/data/indexer/r2013/index.yes +172 -0
  7. data/data/indexer/r2013/index.yesi +67 -0
  8. data/data/indexer/r2013/ruby.txt +35 -0
  9. data/data/indexer/r2013/yaml.txt +30 -0
  10. data/lib/indexer.rb +65 -0
  11. data/lib/indexer/attributes.rb +171 -0
  12. data/lib/indexer/command.rb +260 -0
  13. data/lib/indexer/components.rb +8 -0
  14. data/lib/indexer/components/author.rb +140 -0
  15. data/lib/indexer/components/conflict.rb +78 -0
  16. data/lib/indexer/components/copyright.rb +95 -0
  17. data/lib/indexer/components/dependency.rb +18 -0
  18. data/lib/indexer/components/organization.rb +133 -0
  19. data/lib/indexer/components/repository.rb +140 -0
  20. data/lib/indexer/components/requirement.rb +360 -0
  21. data/lib/indexer/components/resource.rb +209 -0
  22. data/lib/indexer/conversion.rb +14 -0
  23. data/lib/indexer/conversion/gemfile.rb +44 -0
  24. data/lib/indexer/conversion/gemspec.rb +114 -0
  25. data/lib/indexer/conversion/gemspec_exporter.rb +304 -0
  26. data/lib/indexer/core_ext.rb +4 -0
  27. data/lib/indexer/error.rb +23 -0
  28. data/lib/indexer/gemfile.rb +75 -0
  29. data/lib/indexer/importer.rb +144 -0
  30. data/lib/indexer/importer/file.rb +94 -0
  31. data/lib/indexer/importer/gemfile.rb +27 -0
  32. data/lib/indexer/importer/gemspec.rb +43 -0
  33. data/lib/indexer/importer/html.rb +289 -0
  34. data/lib/indexer/importer/markdown.rb +45 -0
  35. data/lib/indexer/importer/ruby.rb +47 -0
  36. data/lib/indexer/importer/version.rb +38 -0
  37. data/lib/indexer/importer/yaml.rb +46 -0
  38. data/lib/indexer/loadable.rb +159 -0
  39. data/lib/indexer/metadata.rb +879 -0
  40. data/lib/indexer/model.rb +237 -0
  41. data/lib/indexer/revision.rb +43 -0
  42. data/lib/indexer/valid.rb +287 -0
  43. data/lib/indexer/validator.rb +313 -0
  44. data/lib/indexer/version/constraint.rb +124 -0
  45. data/lib/indexer/version/exceptions.rb +11 -0
  46. data/lib/indexer/version/number.rb +497 -0
  47. metadata +141 -0
@@ -0,0 +1,313 @@
1
+ module Indexer
2
+
3
+ # Validator class models the strict *canonical* specification of the index
4
+ # file format. It is a one-to-one mapping with no method aliases or other
5
+ # conveniences.
6
+ #
7
+ class Validator < Model
8
+ include Attributes
9
+
10
+ #class << self
11
+ # private
12
+ # alias :create :new
13
+ #end
14
+
15
+ #
16
+ # Revision factory returns a versioned instance of the model class.
17
+ #
18
+ # @param [Hash] data
19
+ # The data to populate the instance.
20
+ #
21
+ def self.new(data={})
22
+ data = revise(data)
23
+ super(data)
24
+ end
25
+
26
+ #
27
+ # Load metadata, ensuring canoncial validity.
28
+ #
29
+ # @param [String] data
30
+ # The data to be validate and then to populate the instance.
31
+ #
32
+ def self.valid(data)
33
+ new(data)
34
+ end
35
+
36
+ #
37
+ # Update metadata to current revision if using old revision.
38
+ #
39
+ def self.revise(data)
40
+ Revision.upconvert(data)
41
+ end
42
+
43
+ # -- Writers ------------------------------------------------------------
44
+
45
+ #
46
+ def revision=(value)
47
+ if value.to_i != REVISION
48
+ # technically this should never happen
49
+ Valid.raise_invalid_message("revision is not current #{REVISION} -- #{value}")
50
+ end
51
+ super(value)
52
+ end
53
+
54
+ # Project's _type_ must be a string without spaces
55
+ # using only `[a-zA-Z0-9_-:/]`.
56
+ def type=(value)
57
+ Valid.type!(value, :type)
58
+ super(value)
59
+ end
60
+
61
+ # Project's _packaging name_ must be a string without spaces
62
+ # using only `[a-zA-Z0-9_-]`.
63
+ def name=(value)
64
+ Valid.name!(value, :name)
65
+ super(value)
66
+ end
67
+
68
+ #
69
+ def version=(value)
70
+ validate(value, :version, :version_string!)
71
+ super(value)
72
+ end
73
+
74
+ # Date must be a UTC formated string, time is optional.
75
+ def date=(value)
76
+ Valid.utc_date!(value, :date)
77
+ super(value)
78
+ end
79
+
80
+ # Title of package must be a single-line string.
81
+ def title=(value)
82
+ Valid.oneline!(value, :title)
83
+ super(value)
84
+ end
85
+
86
+ # Summary must be a single-line string.
87
+ def summary=(value)
88
+ Valid.oneline!(value, :summary)
89
+ super(value)
90
+ end
91
+
92
+ # Description must be string.
93
+ def description=(value)
94
+ Valid.string!(value, :description)
95
+ super(value)
96
+ end
97
+
98
+ # Codename must a single-line string.
99
+ def codename=(value)
100
+ Valid.oneline!(value, :codename)
101
+ super(value)
102
+ end
103
+
104
+ # Loadpath must be an Array of valid pathnames or a String of pathnames
105
+ # separated by colons or semi-colons.
106
+ def load_path=(value)
107
+ Valid.array!(value, :load_path)
108
+ value.each_with_index{ |path, i| Valid.path!(path, "load_path #{i}") }
109
+ super(value)
110
+ end
111
+
112
+ # List of language engine/version family supported.
113
+ def engines=(value)
114
+ Valid.array!(value)
115
+ super(value)
116
+ end
117
+
118
+ # List of platforms supported.
119
+ def platforms=(value)
120
+ Valid.array!(value)
121
+ super(value)
122
+ end
123
+
124
+ # Requirements must be a list of package references.
125
+ def requirements=(value)
126
+ Valid.array!(value, :requirements)
127
+ value.each_with_index do |r, i|
128
+ Valid.hash!(r, "requirements #{i}")
129
+ end
130
+ super(value)
131
+ end
132
+
133
+ # Dependencies must be a list of package references.
134
+ #def dependencies=(value)
135
+ # Valid.array!(value, :dependencies)
136
+ # value.each_with_index do |r, i|
137
+ # Valid.hash!(r, "dependencies #{i}")
138
+ # end
139
+ # super(value)
140
+ #end
141
+
142
+ # List of packages with which this project cannot function.
143
+ def conflicts=(value)
144
+ Valid.array!(value, :conflicts)
145
+ value.each_with_index do |c, i|
146
+ Valid.hash!(c, "conflicts #{i}")
147
+ end
148
+ super(value)
149
+ end
150
+
151
+ #
152
+ def alternatives=(value)
153
+ Valid.array!(value, :alternatives)
154
+ super(value)
155
+ end
156
+
157
+ # provides?
158
+
159
+ #
160
+ def categories=(value)
161
+ Valid.array!(value, :categories)
162
+ value.each_with_index do |c, i|
163
+ Valid.oneline!(c, "categories #{i}")
164
+ end
165
+ super(value)
166
+ end
167
+
168
+ # Suite must be a single line string.
169
+ def suite=(value)
170
+ Valid.oneline!(value, :suite)
171
+ super(value)
172
+ end
173
+
174
+ # The creation date must be a valide UTC formatted date.
175
+ def created=(value)
176
+ Valid.utc_date!(value, :created)
177
+ super(value)
178
+ end
179
+
180
+ # Set sequence of copyrights mappings.
181
+ def copyrights=(value)
182
+ Valid.array!(value, :copyrights)
183
+ value.each{ |h| Valid.hash!(h, :copyrights) }
184
+ super(value)
185
+ end
186
+
187
+ # Authors must an array of hashes in the form
188
+ # of `{name: ..., email: ..., website: ..., roles: [...] }`.
189
+ def authors=(value)
190
+ Valid.array!(value, :authors)
191
+ value.each{ |h| Valid.hash!(h, :authors) }
192
+ super(value)
193
+ end
194
+
195
+ # Organizations must be an array of hashes in the form
196
+ # of `{name: ..., email: ..., website: ..., roles: [...] }`.
197
+ def organizations=(value)
198
+ Valid.array!(value, :organizations)
199
+ value.each{ |h| Valid.hash!(h, :organizations) }
200
+ super(value)
201
+ end
202
+
203
+ # Resources must be an array of hashes in the form
204
+ # of `{uri: ..., name: ..., type: ...}`.
205
+ def resources=(value)
206
+ Valid.array!(value, :resources)
207
+ value.each{ |h| Valid.hash!(h, :resources) }
208
+ super(value)
209
+ end
210
+
211
+ # Repositores must be a mapping of <code>name => URL</code>.
212
+ def repositories=(value)
213
+ Valid.array! value, :repositories
214
+ value.each_with_index do |data, index|
215
+ Valid.hash! data, "repositories #{index}"
216
+ #Valid.uri! data['uri'], "repositories ##{index}"
217
+ Valid.oneline! data['uri'], "repositories ##{index}"
218
+ Valid.oneline! data['id'], "repositories ##{index}" if data['id']
219
+ Valid.word! data['scm'], "repositories ##{index}" if data['scm']
220
+ end
221
+ super value
222
+ end
223
+
224
+ # The post-installation message must be a String.
225
+ def install_message=(value)
226
+ Valid.string!(value)
227
+ super(value)
228
+ end
229
+
230
+ # TODO: How to handle project toplevel namespace?
231
+
232
+ # Namespace must be a single line string.
233
+ def namespace=(value)
234
+ Valid.oneline!(value, :namespace)
235
+ #raise ValidationError unless /^(class|module)/ =~ value
236
+ super(value)
237
+ end
238
+
239
+ # TODO: SCM ?
240
+
241
+ # SCM must be a word.
242
+ #def scm=(value)
243
+ # Valid.word!(value, :scm)
244
+ # super(value)
245
+ #end
246
+
247
+ # The webcvs prefix must be a valid URI.
248
+ def webcvs=(value)
249
+ Valid.uri!(value, :webcvs)
250
+ super(value)
251
+ end
252
+
253
+ #
254
+ def extra=(value)
255
+ Valid.hash!(value, :extra)
256
+ super(value)
257
+ end
258
+
259
+ # A specification is not valid without a name and verison.
260
+ #
261
+ # @return [Boolean] valid specification?
262
+ def valid?
263
+ return false unless name
264
+ return false unless version
265
+ true
266
+ end
267
+
268
+ # By saving via the Validator, we help ensure only the canoncial
269
+ # form even makes it to disk.
270
+ #
271
+ # TODO: Only save when when different?
272
+ #
273
+ def save!(file)
274
+ File.open(file, 'w') do |f|
275
+ f << to_h.to_yaml
276
+ end
277
+ end
278
+
279
+ protected
280
+
281
+ #
282
+ # Initializes the {Metadata} attributes.
283
+ #
284
+ def initialize_attributes
285
+ @data = {
286
+ :type => 'ruby',
287
+ :revision => REVISION,
288
+ :sources => [],
289
+ :authors => [],
290
+ :organizations => [],
291
+ :requirements => [],
292
+ :conflicts => [],
293
+ :alternatives => [],
294
+ :resources => [],
295
+ :repositories => [],
296
+ :categories => [],
297
+ :load_path => ['lib'],
298
+ :copyrights => []
299
+ }
300
+ end
301
+
302
+ private
303
+
304
+ #def validate_package_references(field, references)
305
+ # unless Array === references
306
+ # raise(ValidationError, "#{field} must be a hash")
307
+ # end
308
+ # # TODO: valid version and type
309
+ #end
310
+
311
+ end
312
+
313
+ end
@@ -0,0 +1,124 @@
1
+ module Indexer
2
+
3
+ module Version
4
+
5
+ # The Constraint class models a single version equality or inequality.
6
+ # It consists of the operator and the version number.
7
+ #--
8
+ # TODO: Please improve me!
9
+ #
10
+ # TODO: This should ultimately replace the class methods of Version::Number.
11
+ #
12
+ # TODO: Do we need to support version "from-to" spans ?
13
+ #++
14
+ class Constraint
15
+
16
+ #
17
+ def self.parse(constraint)
18
+ new(constraint)
19
+ end
20
+
21
+ #
22
+ def self.[](operator, number)
23
+ new([operator, number])
24
+ end
25
+
26
+ #
27
+ def initialize(constraint)
28
+ @operator, @number = parse(constraint || '0+')
29
+
30
+ case constraint
31
+ when Array
32
+ @stamp = "%s %s" % [@operator, @number]
33
+ when String
34
+ constraint = "0+" if constraint.strip == ""
35
+ @stamp = constraint
36
+ end
37
+ end
38
+
39
+ # Constraint operator.
40
+ attr :operator
41
+
42
+ # Verison number.
43
+ attr :number
44
+
45
+ #
46
+ def to_s
47
+ @stamp
48
+ end
49
+
50
+ # Converts the version into a constraint string recognizable
51
+ # by RubyGems.
52
+ #--
53
+ # TODO: Better name Constraint#to_s2.
54
+ #++
55
+ def to_gem_version
56
+ op = (operator == '=~' ? '~>' : operator)
57
+ "%s %s" % [op, number]
58
+ end
59
+
60
+ # Convert constraint to Proc object which can be
61
+ # used to test a version number.
62
+ def to_proc
63
+ lambda do |v|
64
+ n = Version::Number.parse(v)
65
+ n.send(operator, number)
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+ #
72
+ def parse(constraint)
73
+ case constraint
74
+ when Array
75
+ op, num = constraint
76
+ when ""
77
+ op, val = ">=", "0"
78
+ when /^(.*?)\~$/
79
+ op, val = "=~", $1
80
+ when /^(.*?)\+$/
81
+ op, val = ">=", $1
82
+ when /^(.*?)\-$/
83
+ op, val = "<", $1
84
+ when /^(=~|~>|<=|>=|==|=|<|>)?\s*(\d+(:?[-.]\w+)*)$/
85
+ if op = $1
86
+ op = '=~' if op == '~>'
87
+ op = '==' if op == '='
88
+ val = $2.split(/\W+/)
89
+ else
90
+ op = '=='
91
+ val = constraint.split(/\W+/)
92
+ end
93
+ else
94
+ raise ArgumentError #constraint.split(/\s+/)
95
+ end
96
+ return op, Version::Number.new(*val)
97
+ end
98
+
99
+ # Parse package entry into name and version constraint.
100
+ #def parse(package)
101
+ # parts = package.strip.split(/\s+/)
102
+ # name = parts.shift
103
+ # vers = parts.empty? ? nil : parts.join(' ')
104
+ # [name, vers]
105
+ #end
106
+
107
+ public
108
+
109
+ # Parses a string constraint returning the operation as a lambda.
110
+ def self.constraint_lambda(constraint)
111
+ new(constraint).to_proc
112
+ end
113
+
114
+ # Parses a string constraint returning the operator and value.
115
+ def self.parse_constraint(constraint)
116
+ c = new(constraint)
117
+ return c.operator, c.number
118
+ end
119
+
120
+ end
121
+
122
+ end
123
+
124
+ end
@@ -0,0 +1,11 @@
1
+ module Indexer
2
+
3
+ module Version
4
+
5
+ # Base calss for all version errors.
6
+ class Exception < RuntimeError
7
+ end
8
+
9
+ end
10
+
11
+ end
@@ -0,0 +1,497 @@
1
+ module Indexer
2
+
3
+ module Version
4
+
5
+ # Represents a versiou number. Developer SHOULD use three point
6
+ # SemVer standard, but this class is mildly flexible in it's support
7
+ # for variations.
8
+ #
9
+ # @see http://semver.org/
10
+ #
11
+ class Number
12
+ include Enumerable
13
+ include Comparable
14
+
15
+ # Recognized build states in order of completion.
16
+ # This is only used when by #bump_state.
17
+ STATES = ['alpha', 'beta', 'pre', 'rc']
18
+
19
+ #
20
+ # Creates a new version.
21
+ #
22
+ # @param points [Array] version points
23
+ #
24
+ def initialize(*points)
25
+ @crush = false
26
+ points.map! do |point|
27
+ sane_point(point)
28
+ end
29
+ @tuple = points.flatten.compact
30
+ end
31
+
32
+ # Shortcut for creating a new verison number
33
+ # given segmented elements.
34
+ #
35
+ # VersionNumber[1,0,0].to_s
36
+ # #=> "1.0.0"
37
+ #
38
+ # VersionNumber[1,0,0,:pre,2].to_s
39
+ # #=> "1.0.0.pre.2"
40
+ #
41
+ def self.[](*args)
42
+ new(*args)
43
+ end
44
+
45
+ #
46
+ # Parses a version string.
47
+ #
48
+ # @param [String] string
49
+ # The version string.
50
+ #
51
+ # @return [Version]
52
+ # The parsed version.
53
+ #
54
+ def self.parse(version)
55
+ case version
56
+ when String
57
+ new(*version.split('.'))
58
+ when Number #self.class
59
+ new(*version.to_a)
60
+ else
61
+ new(*version.to_ary) #to_a) ?
62
+ end
63
+ end
64
+
65
+ #
66
+ def self.cmp(version1, version2)
67
+ # TODO: class level compare might be handy
68
+ end
69
+
70
+ # Major version number
71
+ def major
72
+ (state_index && state_index == 0) ? nil : self[0]
73
+ end
74
+
75
+ # Minor version number
76
+ def minor
77
+ (state_index && state_index <= 1) ? nil : self[1]
78
+ end
79
+
80
+ # Patch version number
81
+ def patch
82
+ (state_index && state_index <= 2) ? nil : self[2]
83
+ end
84
+
85
+ # The build.
86
+ def build
87
+ if b = state_index
88
+ str = @tuple[b..-1].join('.')
89
+ str = crush_point(str) if crush?
90
+ str
91
+ elsif @tuple[3].nil?
92
+ nil
93
+ else
94
+ str = @tuple[3..-1].join('.')
95
+ str = crush_point(str) if crush?
96
+ str
97
+ end
98
+ end
99
+
100
+ #
101
+ def state
102
+ state_index ? @tuple[state_index] : nil
103
+ end
104
+
105
+ #
106
+ alias status state
107
+
108
+ # Return the state revision count. This is the
109
+ # number that occurs after the state.
110
+ #
111
+ # Version::Number[1,2,0,:rc,4].build_number
112
+ # #=> 4
113
+ #
114
+ def build_number #revision
115
+ if i = state_index
116
+ self[i+1] || 0
117
+ else
118
+ nil
119
+ end
120
+ end
121
+
122
+ # @param [Integer] major
123
+ # The major version number.
124
+ def major=(number)
125
+ @tuple[0] = number.to_i
126
+ end
127
+
128
+ # @param [Integer, nil] minor
129
+ # The minor version number.
130
+ def minor=(number)
131
+ @tuple[1] = number.to_i
132
+ end
133
+
134
+ # @param [Integer, nil] patch
135
+ # The patch version number.
136
+ def patch=(number)
137
+ @tuple[2] = number.to_i
138
+ end
139
+
140
+ # @param [Integer, nil] build (nil)
141
+ # The build version number.
142
+ def build=(point)
143
+ @tuple = @tuple[0...state_index] + sane_point(point)
144
+ end
145
+
146
+ #
147
+ def stable?
148
+ build.nil?
149
+ end
150
+
151
+ alias_method :stable_release?, :stable?
152
+
153
+ #
154
+ def alpha?
155
+ s = status.dowcase
156
+ s == 'alpha' or s == 'a'
157
+ end
158
+
159
+ #
160
+ def beta?
161
+ s = status.dowcase
162
+ s == 'beta' or s == 'b'
163
+ end
164
+
165
+ #
166
+ def prerelease?
167
+ status == 'pre'
168
+ end
169
+
170
+ #
171
+ def release_candidate?
172
+ status == 'rc'
173
+ end
174
+
175
+ # Fetch a sepecific segement by index number.
176
+ # In no value is found at that position than
177
+ # zero (0) is returned instead.
178
+ #
179
+ # v = Version::Number[1,2,0]
180
+ # v[0] #=> 1
181
+ # v[1] #=> 2
182
+ # v[3] #=> 0
183
+ # v[4] #=> 0
184
+ #
185
+ # Zero is returned instead of +nil+ to make different
186
+ # version numbers easier to compare.
187
+ def [](index)
188
+ @tuple.fetch(index,0)
189
+ end
190
+
191
+ # Returns a duplicate of the underlying version tuple.
192
+ #
193
+ def to_a
194
+ @tuple.dup
195
+ end
196
+
197
+ # Converts version to a dot-separated string.
198
+ #
199
+ # Version::Number[1,2,0].to_s
200
+ # #=> "1.2.0"
201
+ #
202
+ # TODO: crush
203
+ def to_s
204
+ str = @tuple.compact.join('.')
205
+ str = crush_point(str) if crush?
206
+ return str
207
+ end
208
+
209
+ # This method is the same as #to_s. It is here becuase
210
+ # `File.join` calls it instead of #to_s.
211
+ #
212
+ # VersionNumber[1,2,0].to_str
213
+ # #=> "1.2.0"
214
+ #
215
+ def to_str
216
+ to_s
217
+ end
218
+
219
+ # Returns a String detaling the version number.
220
+ # Essentially it is the same as #to_s.
221
+ #
222
+ # VersionNumber[1,2,0].inspect
223
+ # #=> "1.2.0"
224
+ #
225
+ def inspect
226
+ to_s
227
+ end
228
+
229
+ #
230
+ # Converts the version to YAML.
231
+ #
232
+ # @param [Hash] opts
233
+ # Options supporte by YAML.
234
+ #
235
+ # @return [String]
236
+ # The resulting YAML.
237
+ #
238
+ #--
239
+ # TODO: Should this be here?
240
+ #++
241
+ def to_yaml(opts={})
242
+ to_s.to_yaml(opts)
243
+ end
244
+
245
+ #
246
+ #def ==(other)
247
+ # (self <=> other) == 0
248
+ #end
249
+
250
+ # Compare versions.
251
+ def <=>(other)
252
+ [@tuple.size, other.size].max.times do |i|
253
+ p1, p2 = (@tuple[i] || 0), (other[i] || 0)
254
+ # this is bit tricky, basically a string < integer.
255
+ if p1.class != p2.class
256
+ cmp = p2.to_s <=> p1.to_s
257
+ else
258
+ cmp = p1 <=> p2
259
+ end
260
+ return cmp unless cmp == 0
261
+ end
262
+ #(@tuple.size <=> other.size) * -1
263
+ return 0
264
+ end
265
+
266
+ # For pessimistic constraint (like '~>' in gems).
267
+ #
268
+ # FIXME: Ensure it can handle trailing state.
269
+ def =~(other)
270
+ upver = other.bump(:last)
271
+ #@segments >= other and @segments < upver
272
+ self >= other and self < upver
273
+ end
274
+
275
+ # Iterate of each segment of the version. This allows
276
+ # all enumerable methods to be used.
277
+ #
278
+ # Version::Number[1,2,3].map{|i| i + 1}
279
+ # #=> [2,3,4]
280
+ #
281
+ # Though keep in mind that the state segment is not
282
+ # a number (and techincally any segment can be a string
283
+ # instead of an integer).
284
+ def each(&block)
285
+ @tuple.each(&block)
286
+ end
287
+
288
+ # Return the number of version segements.
289
+ #
290
+ # Version::Number[1,2,3].size
291
+ # #=> 3
292
+ #
293
+ def size
294
+ @tuple.size
295
+ end
296
+
297
+ # Bump the version returning a new version number object.
298
+ # Select +which+ segement to bump by name: +major+, +minor+,
299
+ # +patch+, +state+, +build+ and also +last+.
300
+ #
301
+ # Version::Number[1,2,0].bump(:patch).to_s
302
+ # #=> "1.2.1"
303
+ #
304
+ # Version::Number[1,2,1].bump(:minor).to_s
305
+ # #=> "1.3.0"
306
+ #
307
+ # Version::Number[1,3,0].bump(:major).to_s
308
+ # #=> "2.0.0"
309
+ #
310
+ # Version::Number[1,3,0,:pre,1].bump(:build).to_s
311
+ # #=> "1.3.0.pre.2"
312
+ #
313
+ # Version::Number[1,3,0,:pre,2].bump(:state).to_s
314
+ # #=> "1.3.0.rc.1"
315
+ #
316
+ def bump(which=:patch)
317
+ case which.to_sym
318
+ when :major, :first
319
+ bump_major
320
+ when :minor
321
+ bump_minor
322
+ when :patch
323
+ bump_patch
324
+ when :state, :status
325
+ bump_state
326
+ when :build
327
+ bump_build
328
+ when :revision
329
+ bump_revision
330
+ when :last
331
+ bump_last
332
+ else
333
+ self.class.new(@tuple.dup.compact)
334
+ end
335
+ end
336
+
337
+ #
338
+ def bump_major
339
+ self.class[inc(major), 0, 0]
340
+ end
341
+
342
+ #
343
+ def bump_minor
344
+ self.class[major, inc(minor), 0]
345
+ end
346
+
347
+ #
348
+ def bump_patch
349
+ self.class[major, minor, inc(patch)]
350
+ end
351
+
352
+ #
353
+ def bump_state
354
+ if i = state_index
355
+ if n = inc(@tuple[i])
356
+ v = @tuple[0...i] + [n] + (@tuple[i+1] ? [1] : [])
357
+ else
358
+ v = @tuple[0...i]
359
+ end
360
+ else
361
+ v = @tuple.dup
362
+ end
363
+ self.class.new(v.compact)
364
+ end
365
+
366
+ #
367
+ alias :bump_status :bump_state
368
+
369
+ #
370
+ def bump_build
371
+ if i = state_index
372
+ if i == @tuple.size - 1
373
+ v = @tuple + [1]
374
+ else
375
+ v = @tuple[0...-1] + [inc(@tuple.last)]
376
+ end
377
+ else
378
+ if @tuple.size <= 3
379
+ v = @tuple + [1]
380
+ else
381
+ v = @tuple[0...-1] + [inc(@tuple.last)]
382
+ end
383
+ end
384
+ self.class.new(v.compact)
385
+ end
386
+
387
+ #
388
+ def bump_build_number #revision
389
+ if i = state_index
390
+ v = @tuple[0...-1] + [inc(@tuple.last)]
391
+ else
392
+ v = @tuple[0..2] + ['alpha', 1]
393
+ end
394
+ self.class.new(v.compact)
395
+ end
396
+
397
+ #
398
+ def bump_last
399
+ v = @tuple[0...-1] + [inc(@tuple.last)]
400
+ self.class.new(v.compact)
401
+ end
402
+
403
+ # Return a new version have the same major, minor and
404
+ # patch levels, but with a new state and revision count.
405
+ #
406
+ # Version::Number[1,2,3].restate(:pre,2).to_s
407
+ # #=> "1.2.3.pre.2"
408
+ #
409
+ # Version::Number[1,2,3,:pre,2].restate(:rc,4).to_s
410
+ # #=> "1.2.3.rc.4"
411
+ #
412
+ def restate(state, revision=1)
413
+ if i = state_index
414
+ v = @tuple[0...i] + [state.to_s] + [revision]
415
+ else
416
+ v = @tuple[0...3] + [state.to_s] + [revision]
417
+ end
418
+ self.class.new(v)
419
+ end
420
+
421
+ # Does the version string representation compact
422
+ # string segments with the subsequent number segement?
423
+ def crush?
424
+ @crush
425
+ end
426
+
427
+ # Does this version match a given constraint? The constraint is a String
428
+ # in the form of "{operator} {version number}".
429
+ #--
430
+ # TODO: match? will change as Constraint class is improved.
431
+ #++
432
+ def match?(*constraints)
433
+ constraints.all? do |c|
434
+ Constraint.constraint_lambda(c).call(self)
435
+ end
436
+ end
437
+
438
+ protected
439
+
440
+ # Return the undelying segments array.
441
+ attr :tuple
442
+
443
+ private
444
+
445
+ # Convert a segment into an integer or string.
446
+ def sane_point(point)
447
+ point = point.to_s if Symbol === point
448
+ case point
449
+ when Integer
450
+ point
451
+ when /^\d+$/
452
+ point.to_i
453
+ when /^(\d+)(\w+)(\d+)$/
454
+ @crush = true
455
+ [$1.to_i, $2, $3.to_i]
456
+ when /^(\w+)(\d+)$/
457
+ @crush = true
458
+ [$1, $2.to_i]
459
+ else
460
+ point
461
+ end
462
+ end
463
+
464
+ # Take a point string rendering of a version and crush it!
465
+ def crush_point(string)
466
+ string.gsub(/(^|\.)(\D+)\.(\d+)(\.|$)/, '\2\3')
467
+ end
468
+
469
+ # Return the index of the first recognized state.
470
+ #
471
+ # VersionNumber[1,2,3,'pre',3].state_index
472
+ # #=> 3
473
+ #
474
+ # You might ask why this is needed, since the state
475
+ # position should always be 3. However, there isn't
476
+ # always a state entry, which means this method will
477
+ # return +nil+, and we also leave open the potential
478
+ # for extra-long version numbers --though we do not
479
+ # recommend the idea, it is possible.
480
+ def state_index
481
+ @tuple.index{ |s| String === s }
482
+ end
483
+
484
+ # Segement incrementor.
485
+ def inc(val)
486
+ if i = STATES.index(val.to_s)
487
+ STATES[i+1]
488
+ else
489
+ val.succ
490
+ end
491
+ end
492
+
493
+ end
494
+
495
+ end
496
+
497
+ end