formeze 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,133 @@
1
+ Formeze: A little library for defining classes to handle form data/input
2
+ ========================================================================
3
+
4
+
5
+ Motivation
6
+ ----------
7
+
8
+ Most web apps built for end users will need to process urlencoded form data.
9
+ Registration forms, profile forms, checkout forms, contact forms, and forms
10
+ for adding/editing application specific data. As developers we would like to
11
+ process this data safely, to minimise the possibility of security holes
12
+ within our application that could be exploited. Formeze adopts the approach
13
+ of being "strict by default", forcing the application code to be explicit in
14
+ what it accepts as input.
15
+
16
+
17
+ Example usage
18
+ -------------
19
+
20
+ Forms are just "plain old ruby objects". Calling `Formeze.setup` will include
21
+ some class methods and instance methods, but will otherwise leave the object
22
+ untouched (i.e. you can define your own initialization). Here is a minimal
23
+ example, which defines a form with a single "title" field:
24
+
25
+ class ExampleForm
26
+ Formeze.setup(self)
27
+
28
+ field :title
29
+ end
30
+
31
+
32
+ This form can then be used to parse and validate form/input data as follows:
33
+
34
+ form = ExampleForm.new
35
+
36
+ form.parse('title=Title')
37
+
38
+ form.title # => "Title"
39
+
40
+
41
+ Detecting errors
42
+ ----------------
43
+
44
+ Formeze distinguishes between user errors (which are expected in the normal
45
+ running of your application), and key/value errors (which most likely indicate
46
+ either developer error, or form tampering).
47
+
48
+ For the latter case, the `parse` method that formeze provides will raise a
49
+ Formeze::KeyError or a Formeze::ValueError exception if the structure of the
50
+ form data does not match the field definitions.
51
+
52
+ After calling `parse` you can check that the form is valid by calling the
53
+ `#valid?` method. If it isn't you can call the `errors` method which will
54
+ return an array of error messages to display to the user.
55
+
56
+
57
+ Field options
58
+ -------------
59
+
60
+ By default fields cannot be blank, they are limited to 64 characters,
61
+ and they cannot contain newlines. These restrictions can be overrided
62
+ by setting various field options.
63
+
64
+ Defining a field without any options works well for a simple text input.
65
+ If the default character limit is too big or too small you can override
66
+ it by setting the `char_limit` option. For example:
67
+
68
+ field :title, char_limit: 200
69
+
70
+
71
+ If you are dealing with textareas (i.e. multiple lines of text) then you can
72
+ set the `multiline` option to allow newlines. For example:
73
+
74
+ field :description, char_limit: 500, multiline: true
75
+
76
+
77
+ Error messages will include the field label, which by default is set to the
78
+ field name, capitalized, and with underscores replace by spaces. If you want
79
+ to override this, set the `label` option. For example:
80
+
81
+ field :twitter, label: 'Twitter Username'
82
+
83
+
84
+ If you want to validate that the field value matches a specific pattern you
85
+ can specify the `pattern` option. This is useful for validating things with
86
+ well defined formats, like numbers. For example:
87
+
88
+ field :number, pattern: /\A[1-9]\d*\z/
89
+
90
+ field :card_security_code, char_limit: 5, value: /\A\d+\z/
91
+
92
+
93
+ If you want to validate that the field value belongs to a set of predefined
94
+ values then you can specify the `values` option. This is useful for dealing
95
+ with input from select boxes, where the values are known upfront. For example:
96
+
97
+ field :card_expiry_month, values: (1..12).map(&:to_s)
98
+
99
+
100
+ The `values` option is also useful for checkboxes. Specify the `key_required`
101
+ option to handle the case where the checkbox is unchecked. For example:
102
+
103
+ field :accept_terms, values: %w(true), key_required: false
104
+
105
+
106
+ Sometimes you'll have a field with multiple values. A multiple select input,
107
+ a set of checkboxes. For this case you can specify the `multiple` option to
108
+ allow multiple values. For example:
109
+
110
+ field :colour, multiple: true, values: Colour.keys
111
+
112
+
113
+ Unlike all the other examples so far, reading the attribute that corresponds
114
+ to this field will return an array of strings instead of a single string.
115
+
116
+
117
+ Rails usage
118
+ -----------
119
+
120
+ This is the basic pattern for using a formeze form in a rails controller:
121
+
122
+ form = SomeForm.new
123
+ form.parse(request.raw_post)
124
+
125
+ if form.valid?
126
+ # do something with form data
127
+ else
128
+ # display form.errors to user
129
+ end
130
+
131
+
132
+ Formeze will automatically define optional "utf8" and "authenticity_token"
133
+ fields on every form so that you don't have to specify those manually.
data/Rakefile.rb ADDED
@@ -0,0 +1,7 @@
1
+ require 'rake/testtask'
2
+
3
+ task :default => :test
4
+
5
+ Rake::TestTask.new do |t|
6
+ t.test_files = FileList['spec/*_spec.rb']
7
+ end
data/formeze.gemspec ADDED
@@ -0,0 +1,12 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'formeze'
3
+ s.version = '1.0.0'
4
+ s.platform = Gem::Platform::RUBY
5
+ s.authors = ['Tim Craft']
6
+ s.email = ['mail@timcraft.com']
7
+ s.homepage = 'http://github.com/timcraft/formeze'
8
+ s.description = 'A little library for defining classes to handle form data/input'
9
+ s.summary = 'See description'
10
+ s.files = Dir.glob('{lib,spec}/**/*') + %w(README.md Rakefile.rb formeze.gemspec)
11
+ s.require_path = 'lib'
12
+ end
data/lib/formeze.rb ADDED
@@ -0,0 +1,232 @@
1
+ require 'cgi'
2
+
3
+ module Formeze
4
+ class Label
5
+ def initialize(name)
6
+ @name = name
7
+ end
8
+
9
+ def to_s
10
+ @name.to_s.tr('_', ' ').capitalize
11
+ end
12
+ end
13
+
14
+ class Field
15
+ attr_reader :name
16
+
17
+ def initialize(name, options = {})
18
+ @name, @options = name, options
19
+ end
20
+
21
+ def validate(value, &error)
22
+ error.call(self, 'is required') if required? && value !~ /\S/
23
+
24
+ error.call(self, 'has too many lines') if !multiline? && value.lines.count > 1
25
+
26
+ error.call(self, 'has too many characters') if value.chars.count > char_limit
27
+
28
+ error.call(self, 'has too many words') if word_limit? && value.scan(/\w+/).length > word_limit
29
+
30
+ error.call(self, 'is invalid') if pattern? && value !~ pattern
31
+
32
+ error.call(self, 'is invalid') if values? && !values.include?(value)
33
+ end
34
+
35
+ def key
36
+ @key ||= @name.to_s
37
+ end
38
+
39
+ def key_required?
40
+ @options.fetch(:key_required) { true }
41
+ end
42
+
43
+ def label
44
+ @label ||= @options.fetch(:label) { Label.new(name) }
45
+ end
46
+
47
+ def required?
48
+ @options.fetch(:required) { true }
49
+ end
50
+
51
+ def multiline?
52
+ @options.fetch(:multiline) { false }
53
+ end
54
+
55
+ def multiple?
56
+ @options.fetch(:multiple) { false }
57
+ end
58
+
59
+ def char_limit
60
+ @options.fetch(:char_limit) { 64 }
61
+ end
62
+
63
+ def word_limit?
64
+ @options.has_key?(:word_limit)
65
+ end
66
+
67
+ def word_limit
68
+ @options.fetch(:word_limit)
69
+ end
70
+
71
+ def pattern?
72
+ @options.has_key?(:pattern)
73
+ end
74
+
75
+ def pattern
76
+ @options.fetch(:pattern)
77
+ end
78
+
79
+ def values?
80
+ @options.has_key?(:values)
81
+ end
82
+
83
+ def values
84
+ @options.fetch(:values)
85
+ end
86
+ end
87
+
88
+ module ArrayAttrAccessor
89
+ def array_attr_reader(name)
90
+ define_method(name) do
91
+ ivar = :"@#{name}"
92
+
93
+ values = instance_variable_get(ivar)
94
+
95
+ if values.nil?
96
+ values = []
97
+
98
+ instance_variable_set(ivar, values)
99
+ end
100
+
101
+ values
102
+ end
103
+ end
104
+
105
+ def array_attr_writer(name)
106
+ define_method(:"#{name}=") do |value|
107
+ ivar = :"@#{name}"
108
+
109
+ values = instance_variable_get(ivar)
110
+
111
+ if values.nil?
112
+ instance_variable_set(ivar, [value])
113
+ else
114
+ values << value
115
+ end
116
+ end
117
+ end
118
+
119
+ def array_attr_accessor(name)
120
+ array_attr_reader(name)
121
+ array_attr_writer(name)
122
+ end
123
+ end
124
+
125
+ module ClassMethods
126
+ include ArrayAttrAccessor
127
+
128
+ def fields
129
+ @fields ||= []
130
+ end
131
+
132
+ def field(*args)
133
+ field = Field.new(*args)
134
+
135
+ fields << field
136
+
137
+ if field.multiple?
138
+ array_attr_accessor field.name
139
+ else
140
+ attr_accessor field.name
141
+ end
142
+ end
143
+
144
+ def guard(&block)
145
+ fields << block
146
+ end
147
+
148
+ def checks
149
+ @checks ||= []
150
+ end
151
+
152
+ def check(&block)
153
+ checks << block
154
+ end
155
+
156
+ def errors
157
+ @errors ||= []
158
+ end
159
+
160
+ def error(message)
161
+ errors << message
162
+ end
163
+ end
164
+
165
+ class KeyError < StandardError; end
166
+
167
+ class ValueError < StandardError; end
168
+
169
+ class UserError < StandardError; end
170
+
171
+ module InstanceMethods
172
+ def parse(encoded_form_data)
173
+ form_data = CGI.parse(encoded_form_data)
174
+
175
+ self.class.fields.each do |field|
176
+ unless field.respond_to?(:key)
177
+ instance_eval(&field) ? return : next
178
+ end
179
+
180
+ unless form_data.has_key?(field.key)
181
+ next if field.multiple? || !field.key_required?
182
+
183
+ raise KeyError
184
+ end
185
+
186
+ values = form_data.delete(field.key)
187
+
188
+ if values.length > 1
189
+ raise ValueError unless field.multiple?
190
+ end
191
+
192
+ values.each do |value|
193
+ field.validate(value) do |error|
194
+ errors << UserError.new("#{field.label} #{error}")
195
+ end
196
+
197
+ send(:"#{field.name}=", value)
198
+ end
199
+ end
200
+
201
+ raise KeyError unless form_data.empty?
202
+
203
+ self.class.checks.zip(self.class.errors) do |check, error|
204
+ instance_eval(&check) ? next : errors << UserError.new(error)
205
+ end
206
+ end
207
+
208
+ def errors
209
+ @errors ||= []
210
+ end
211
+
212
+ def valid?
213
+ errors.empty?
214
+ end
215
+ end
216
+
217
+ def self.setup(form)
218
+ form.send :include, InstanceMethods
219
+
220
+ form.extend ClassMethods
221
+
222
+ if on_rails?
223
+ form.field(:utf8, key_required: false)
224
+
225
+ form.field(:authenticity_token, key_required: false)
226
+ end
227
+ end
228
+
229
+ def self.on_rails?
230
+ defined?(Rails)
231
+ end
232
+ end
@@ -0,0 +1,399 @@
1
+ require 'minitest/autorun'
2
+
3
+ require 'formeze'
4
+
5
+ class FormWithField
6
+ Formeze.setup(self)
7
+
8
+ field :title
9
+ end
10
+
11
+ describe 'FormWithField' do
12
+ before do
13
+ @form = FormWithField.new
14
+ end
15
+
16
+ describe 'title method' do
17
+ it 'should return nil' do
18
+ @form.title.must_be_nil
19
+ end
20
+ end
21
+
22
+ describe 'title equals method' do
23
+ it 'should set the value of the title attribute' do
24
+ @form.title = 'Untitled'
25
+ @form.title.must_equal('Untitled')
26
+ end
27
+ end
28
+
29
+ describe 'parse method' do
30
+ it 'should set the value of the title attribute' do
31
+ @form.parse('title=Untitled')
32
+ @form.title.must_equal('Untitled')
33
+ end
34
+
35
+ it 'should raise an exception when the key is missing' do
36
+ proc { @form.parse('') }.must_raise(Formeze::KeyError)
37
+ end
38
+
39
+ it 'should raise an exception when there are multiple values for the key' do
40
+ proc { @form.parse('title=foo&title=bar') }.must_raise(Formeze::ValueError)
41
+ end
42
+
43
+ it 'should raise an exception when there is an unexpected key' do
44
+ proc { @form.parse('title=Untitled&foo=bar') }.must_raise(Formeze::KeyError)
45
+ end
46
+ end
47
+ end
48
+
49
+ describe 'FormWithField after parsing valid input' do
50
+ before do
51
+ @form = FormWithField.new
52
+ @form.parse('title=Untitled')
53
+ end
54
+
55
+ describe 'valid query method' do
56
+ it 'should return true' do
57
+ @form.valid?.must_equal(true)
58
+ end
59
+ end
60
+
61
+ describe 'errors method' do
62
+ it 'should return an empty array' do
63
+ @form.errors.must_be_instance_of(Array)
64
+ @form.errors.must_be_empty
65
+ end
66
+ end
67
+ end
68
+
69
+ describe 'FormWithField after parsing blank input' do
70
+ before do
71
+ @form = FormWithField.new
72
+ @form.parse('title=')
73
+ end
74
+
75
+ describe 'valid query method' do
76
+ it 'should return false' do
77
+ @form.valid?.must_equal(false)
78
+ end
79
+ end
80
+ end
81
+
82
+ describe 'FormWithField after parsing input containing newlines' do
83
+ before do
84
+ @form = FormWithField.new
85
+ @form.parse('title=This+is+a+product.%0AIt+is+very+lovely.')
86
+ end
87
+
88
+ describe 'valid query method' do
89
+ it 'should return false' do
90
+ @form.valid?.must_equal(false)
91
+ end
92
+ end
93
+ end
94
+
95
+ class FormWithOptionalField
96
+ Formeze.setup(self)
97
+
98
+ field :title, required: false
99
+ end
100
+
101
+ describe 'FormWithOptionalField after parsing blank input' do
102
+ before do
103
+ @form = FormWithOptionalField.new
104
+ @form.parse('title=')
105
+ end
106
+
107
+ describe 'valid query method' do
108
+ it 'should return true' do
109
+ @form.valid?.must_equal(true)
110
+ end
111
+ end
112
+ end
113
+
114
+ class FormWithFieldThatCanHaveMultipleLines
115
+ Formeze.setup(self)
116
+
117
+ field :description, multiline: true
118
+ end
119
+
120
+ describe 'FormWithFieldThatCanHaveMultipleLines after parsing input containing newlines' do
121
+ before do
122
+ @form = FormWithFieldThatCanHaveMultipleLines.new
123
+ @form.parse('description=This+is+a+product.%0AIt+is+very+lovely.')
124
+ end
125
+
126
+ describe 'valid query method' do
127
+ it 'should return true' do
128
+ @form.valid?.must_equal(true)
129
+ end
130
+ end
131
+ end
132
+
133
+ class FormWithCharacterLimitedField
134
+ Formeze.setup(self)
135
+
136
+ field :title, char_limit: 16
137
+ end
138
+
139
+ describe 'FormWithCharacterLimitedField after parsing input with too many characters' do
140
+ before do
141
+ @form = FormWithCharacterLimitedField.new
142
+ @form.parse('title=This+Title+Will+Be+Too+Long')
143
+ end
144
+
145
+ describe 'valid query method' do
146
+ it 'should return false' do
147
+ @form.valid?.must_equal(false)
148
+ end
149
+ end
150
+ end
151
+
152
+ class FormWithWordLimitedField
153
+ Formeze.setup(self)
154
+
155
+ field :title, word_limit: 2
156
+ end
157
+
158
+ describe 'FormWithWordLimitedField after parsing input with too many words' do
159
+ before do
160
+ @form = FormWithWordLimitedField.new
161
+ @form.parse('title=This+Title+Will+Be+Too+Long')
162
+ end
163
+
164
+ describe 'valid query method' do
165
+ it 'should return false' do
166
+ @form.valid?.must_equal(false)
167
+ end
168
+ end
169
+ end
170
+
171
+ class FormWithFieldThatMustMatchPattern
172
+ Formeze.setup(self)
173
+
174
+ field :number, pattern: /\A\d+\z/
175
+ end
176
+
177
+ describe 'FormWithFieldThatMustMatchPattern after parsing input that matches the pattern' do
178
+ before do
179
+ @form = FormWithFieldThatMustMatchPattern.new
180
+ @form.parse('number=12345')
181
+ end
182
+
183
+ describe 'valid query method' do
184
+ it 'should return true' do
185
+ @form.valid?.must_equal(true)
186
+ end
187
+ end
188
+ end
189
+
190
+ describe 'FormWithFieldThatMustMatchPattern after parsing input that does not match the pattern' do
191
+ before do
192
+ @form = FormWithFieldThatMustMatchPattern.new
193
+ @form.parse('number=notanumber')
194
+ end
195
+
196
+ describe 'valid query method' do
197
+ it 'should return false' do
198
+ @form.valid?.must_equal(false)
199
+ end
200
+ end
201
+ end
202
+
203
+ class FormWithFieldThatCanHaveMultipleValues
204
+ Formeze.setup(self)
205
+
206
+ field :colour, multiple: true
207
+ end
208
+
209
+ describe 'FormWithFieldThatCanHaveMultipleValues' do
210
+ before do
211
+ @form = FormWithFieldThatCanHaveMultipleValues.new
212
+ end
213
+
214
+ describe 'colour method' do
215
+ it 'should return an empty array' do
216
+ @form.colour.must_be_instance_of(Array)
217
+ @form.colour.must_be_empty
218
+ end
219
+ end
220
+
221
+ describe 'colour equals method' do
222
+ it 'should add the argument to the colour attribute array' do
223
+ @form.colour = 'black'
224
+ @form.colour.must_include('black')
225
+ end
226
+ end
227
+
228
+ describe 'parse method' do
229
+ it 'should add the value to the colour attribute array' do
230
+ @form.parse('colour=black')
231
+ @form.colour.must_include('black')
232
+ end
233
+
234
+ it 'should not raise an exception when there are multiple values for the key' do
235
+ @form.parse('colour=black&colour=white')
236
+ end
237
+
238
+ it 'should not raise an exception when the key is missing' do
239
+ @form.parse('')
240
+ end
241
+ end
242
+ end
243
+
244
+ describe 'FormWithFieldThatCanHaveMultipleValues after parsing input with multiple values' do
245
+ before do
246
+ @form = FormWithFieldThatCanHaveMultipleValues.new
247
+ @form.parse('colour=black&colour=white')
248
+ end
249
+
250
+ describe 'colour method' do
251
+ it 'should return an array containing the values' do
252
+ @form.colour.must_be_instance_of(Array)
253
+ @form.colour.must_include('black')
254
+ @form.colour.must_include('white')
255
+ end
256
+ end
257
+
258
+ describe 'valid query method' do
259
+ it 'should return true' do
260
+ @form.valid?.must_equal(true)
261
+ end
262
+ end
263
+ end
264
+
265
+ describe 'FormWithFieldThatCanHaveMultipleValues after parsing input with no values' do
266
+ before do
267
+ @form = FormWithFieldThatCanHaveMultipleValues.new
268
+ @form.parse('')
269
+ end
270
+
271
+ describe 'colour method' do
272
+ it 'should return an empty array' do
273
+ @form.colour.must_be_instance_of(Array)
274
+ @form.colour.must_be_empty
275
+ end
276
+ end
277
+
278
+ describe 'valid query method' do
279
+ it 'should return true' do
280
+ @form.valid?.must_equal(true)
281
+ end
282
+ end
283
+ end
284
+
285
+ class FormWithFieldThatCanOnlyHaveSpecifiedValues
286
+ Formeze.setup(self)
287
+
288
+ field :answer, values: %w(yes no)
289
+ end
290
+
291
+ describe 'FormWithFieldThatCanOnlyHaveSpecifiedValues after parsing input with an invalid value' do
292
+ before do
293
+ @form = FormWithFieldThatCanOnlyHaveSpecifiedValues.new
294
+ @form.parse('answer=maybe')
295
+ end
296
+
297
+ describe 'valid query method' do
298
+ it 'should return false' do
299
+ @form.valid?.must_equal(false)
300
+ end
301
+ end
302
+ end
303
+
304
+ class FormWithGuard
305
+ Formeze.setup(self)
306
+
307
+ field :delivery_address
308
+
309
+ field :same_address, values: %w(yes no)
310
+
311
+ guard { same_address? }
312
+
313
+ field :billing_address
314
+
315
+ def same_address?
316
+ same_address == 'yes'
317
+ end
318
+ end
319
+
320
+ describe 'FormWithGuard after parsing input with same_address set and no billing address' do
321
+ before do
322
+ @form = FormWithGuard.new
323
+ @form.parse('delivery_address=123+Main+St&same_address=yes')
324
+ end
325
+
326
+ describe 'valid query method' do
327
+ it 'should return true' do
328
+ @form.valid?.must_equal(true)
329
+ end
330
+ end
331
+ end
332
+
333
+ class FormWithCustomValidation
334
+ Formeze.setup(self)
335
+
336
+ field :email
337
+
338
+ check { email.include?(?@) }
339
+ error 'Email is invalid'
340
+ end
341
+
342
+ describe 'FormWithCustomValidation after parsing invalid input' do
343
+ before do
344
+ @form = FormWithCustomValidation.new
345
+ @form.parse('email=alice')
346
+ end
347
+
348
+ describe 'valid query method' do
349
+ it 'should return false' do
350
+ @form.valid?.must_equal(false)
351
+ end
352
+ end
353
+ end
354
+
355
+ class FormWithOptionalKey
356
+ Formeze.setup(self)
357
+
358
+ field :accept_terms, values: %w(true), key_required: false
359
+ end
360
+
361
+ describe 'FormWithOptionalKey after parsing input without the key' do
362
+ before do
363
+ @form = FormWithOptionalKey.new
364
+ @form.parse('')
365
+ end
366
+
367
+ describe 'valid query method' do
368
+ it 'should return true' do
369
+ @form.valid?.must_equal(true)
370
+ end
371
+ end
372
+ end
373
+
374
+ Rails = Object.new
375
+
376
+ class RailsForm
377
+ Formeze.setup(self)
378
+
379
+ field :title
380
+ end
381
+
382
+ describe 'RailsForm' do
383
+ before do
384
+ @form = RailsForm.new
385
+ end
386
+
387
+ describe 'parse method' do
388
+ it 'should automatically process the utf8 and authenticity_token parameters' do
389
+ @form.parse('utf8=%E2%9C%93&authenticity_token=5RMc3sPZdR%2BZz4onNS8NfK&title=Test')
390
+ @form.authenticity_token.wont_be_empty
391
+ @form.utf8.wont_be_empty
392
+ end
393
+
394
+ it 'should not complain if the utf8 or authenticity_token parameters are missing' do
395
+ @form.parse('utf8=%E2%9C%93&title=Test')
396
+ @form.parse('authenticity_token=5RMc3sPZdR%2BZz4onNS8NfK&title=Test')
397
+ end
398
+ end
399
+ end
metadata ADDED
@@ -0,0 +1,50 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: formeze
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Tim Craft
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-04-09 00:00:00.000000000Z
13
+ dependencies: []
14
+ description: A little library for defining classes to handle form data/input
15
+ email:
16
+ - mail@timcraft.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - lib/formeze.rb
22
+ - spec/formeze_spec.rb
23
+ - README.md
24
+ - Rakefile.rb
25
+ - formeze.gemspec
26
+ homepage: http://github.com/timcraft/formeze
27
+ licenses: []
28
+ post_install_message:
29
+ rdoc_options: []
30
+ require_paths:
31
+ - lib
32
+ required_ruby_version: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ required_rubygems_version: !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ requirements: []
45
+ rubyforge_project:
46
+ rubygems_version: 1.8.10
47
+ signing_key:
48
+ specification_version: 3
49
+ summary: See description
50
+ test_files: []