user_input 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +22 -0
- data/LICENSE +20 -0
- data/README.rdoc +61 -0
- data/Rakefile +45 -0
- data/VERSION +1 -0
- data/lib/user_input/option_parser.rb +213 -0
- data/lib/user_input/type_safe_hash.rb +134 -0
- data/lib/user_input.rb +246 -0
- data/spec/option_parser_spec.rb +158 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +9 -0
- data/spec/type_safe_hash_spec.rb +123 -0
- data/spec/user_input_spec.rb +284 -0
- metadata +81 -0
data/lib/user_input.rb
ADDED
@@ -0,0 +1,246 @@
|
|
1
|
+
# Adds the method from_user_input to various built-in classes as both a class and instance method.
|
2
|
+
# On a class, the method returns a validated instance of that class if it can be coerced into one
|
3
|
+
# (or nil if not). On an instance, it validates more strictly against the value of that instance.
|
4
|
+
|
5
|
+
class String
|
6
|
+
# All strings validate as strings
|
7
|
+
def String.from_user_input(value)
|
8
|
+
return value.to_s
|
9
|
+
end
|
10
|
+
|
11
|
+
# instance form does a straight comparison of the string with self.
|
12
|
+
def from_user_input(value)
|
13
|
+
if (self == value.to_s)
|
14
|
+
return value.to_s
|
15
|
+
else
|
16
|
+
return nil
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class Boolean
|
22
|
+
# Must be a string that is either "true" or "false"
|
23
|
+
def Boolean.from_user_input(value)
|
24
|
+
if (value.kind_of?(TrueClass) || value.kind_of?(FalseClass) ||
|
25
|
+
/(true|false|on|off|y|n|enabled|disabled)/ =~ value.to_s)
|
26
|
+
return !!(/(true|on|y|enabled)/ =~ value.to_s)
|
27
|
+
else
|
28
|
+
return nil
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class TrueClass
|
34
|
+
# Either a 'positive' string or an actual instance of true.
|
35
|
+
def from_user_input(value)
|
36
|
+
if (value.kind_of?(TrueClass) || /(true|on|y|enabled)/ =~ value.to_s)
|
37
|
+
return true
|
38
|
+
else
|
39
|
+
return nil
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class FalseClass
|
45
|
+
# Either a 'negative' string or an actual instance of false.
|
46
|
+
def from_user_input(value)
|
47
|
+
if (value.kind_of?(FalseClass) || /(false|off|n|disabled)/ =~ value.to_s)
|
48
|
+
return false
|
49
|
+
else
|
50
|
+
return nil
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class Date
|
56
|
+
# Check for a string of the regex /[0-9]{1,2}\/[0-9]{1,2}\/[0-9]{4}/
|
57
|
+
# and make a Date object out of it
|
58
|
+
def Date.from_user_input(value)
|
59
|
+
if (value.kind_of?(Date))
|
60
|
+
return value
|
61
|
+
end
|
62
|
+
begin
|
63
|
+
return Date.parse(value.to_s)
|
64
|
+
rescue ArgumentError
|
65
|
+
return nil
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class Integer
|
71
|
+
# All characters must be numbers, except for the first which may be -
|
72
|
+
def Integer.from_user_input(value)
|
73
|
+
if (value.kind_of?(Integer) || /^\s*-?[0-9]+\s*$/ =~ value.to_s)
|
74
|
+
return value.to_i
|
75
|
+
else
|
76
|
+
return nil
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# instance form does a straight comparison of value.to_i with self.
|
81
|
+
def from_user_input(value)
|
82
|
+
if (!value.kind_of?(Float) && self == value.to_i)
|
83
|
+
return value.to_i
|
84
|
+
else
|
85
|
+
return nil
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
class Float
|
91
|
+
# All characters must be numbers, except there can be up to one decimal
|
92
|
+
# and a negative sign at the front
|
93
|
+
def Float.from_user_input(value)
|
94
|
+
if (value.kind_of?(Float) || /^\s*-?[0-9]*(\.[0-9]+)?\s*$/ =~ value.to_s)
|
95
|
+
return value.to_f
|
96
|
+
else
|
97
|
+
return nil
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# instance form does a straight comparison of value.to_f with self.
|
102
|
+
def from_user_input(value)
|
103
|
+
if (self == value.to_f)
|
104
|
+
return value.to_f
|
105
|
+
else
|
106
|
+
return nil
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
class Regexp
|
112
|
+
# Returns the string if value matches self's regex, returns nil otherwise.
|
113
|
+
def from_user_input(value)
|
114
|
+
if (value.kind_of?(String) && matches = self.match(value))
|
115
|
+
return matches
|
116
|
+
else
|
117
|
+
return nil
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
class Range
|
123
|
+
def from_user_input(value)
|
124
|
+
value = self.first.class.from_user_input(value)
|
125
|
+
|
126
|
+
if(!value || !(self === value))
|
127
|
+
return nil
|
128
|
+
end
|
129
|
+
|
130
|
+
return value
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
class Array
|
135
|
+
def Array.from_user_input(value)
|
136
|
+
return [*value]
|
137
|
+
end
|
138
|
+
|
139
|
+
# Checks each element of the value array to ensure that they match against
|
140
|
+
# the first element of self
|
141
|
+
def from_user_input(value)
|
142
|
+
value = [*value]
|
143
|
+
# eliminate the obvious
|
144
|
+
if (self.length != 1)
|
145
|
+
raise ArgumentError, "Must supply only one element to an array you're calling from_user_input on."
|
146
|
+
end
|
147
|
+
innertype = self[0]
|
148
|
+
# now check whether the inner elements of the array match
|
149
|
+
output = value.collect {|innervalue|
|
150
|
+
# if innertype is not an array, but value is, we need to flatten it
|
151
|
+
if (!innertype.kind_of?(Array) && innervalue.kind_of?(Array))
|
152
|
+
innervalue = innervalue[0]
|
153
|
+
end
|
154
|
+
innertype.from_user_input(innervalue); # returns
|
155
|
+
}.compact()
|
156
|
+
|
157
|
+
if (output.length > 0)
|
158
|
+
return output
|
159
|
+
else
|
160
|
+
return nil
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
class Hash
|
166
|
+
def from_user_input(value)
|
167
|
+
if (self.length != 1)
|
168
|
+
raise ArgumentError, "Must supply only one element to a hash you're calling from_user_input on."
|
169
|
+
end
|
170
|
+
if (!value.kind_of?(Hash))
|
171
|
+
return nil
|
172
|
+
end
|
173
|
+
keytype = nil
|
174
|
+
valtype = nil
|
175
|
+
self.each {|k,v| keytype = k; valtype = v}
|
176
|
+
output = {}
|
177
|
+
value.each {|k, v|
|
178
|
+
if (!(k = keytype.from_user_input(k)).nil? && !(v = valtype.from_user_input(v)).nil?)
|
179
|
+
output[k] = v
|
180
|
+
end
|
181
|
+
}
|
182
|
+
if (output.length > 0)
|
183
|
+
return output
|
184
|
+
else
|
185
|
+
return nil
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
class Set
|
191
|
+
def from_user_input(value)
|
192
|
+
each {|i|
|
193
|
+
val = i.from_user_input(value)
|
194
|
+
return val if val
|
195
|
+
}
|
196
|
+
return nil
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
class Symbol
|
201
|
+
def Symbol.from_user_input(value)
|
202
|
+
raise ArgumentError, "You should never arbitrarily turn user input into symbols. It can cause leaks that could lead to DoS."
|
203
|
+
end
|
204
|
+
|
205
|
+
# instance form does a straight comparison of value.to_sym with self.
|
206
|
+
def from_user_input(value)
|
207
|
+
if (self.to_s == value.to_s)
|
208
|
+
return self
|
209
|
+
else
|
210
|
+
return nil
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
class IPAddr
|
216
|
+
def IPAddr.from_user_input(value)
|
217
|
+
if (value.kind_of?(self))
|
218
|
+
return value
|
219
|
+
end
|
220
|
+
begin
|
221
|
+
return self.new(value.to_s)
|
222
|
+
rescue ArgumentError
|
223
|
+
return nil
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
class Class
|
229
|
+
def from_user_input(value)
|
230
|
+
if (value.kind_of?(Class) && value <= self)
|
231
|
+
return value
|
232
|
+
elsif (value.kind_of?(self))
|
233
|
+
return value
|
234
|
+
end
|
235
|
+
return nil
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
class Object
|
240
|
+
def from_user_input(value)
|
241
|
+
if (value == self)
|
242
|
+
return value
|
243
|
+
end
|
244
|
+
return nil
|
245
|
+
end
|
246
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
require 'lib/user_input/option_parser'
|
3
|
+
|
4
|
+
describe UserInput::OptionParser do
|
5
|
+
IOptionParser = UserInput::OptionParser
|
6
|
+
|
7
|
+
it "Should optionally take an argument for program prefix, setting it to $0 if none set" do
|
8
|
+
IOptionParser.new().program_prefix.should == $0
|
9
|
+
IOptionParser.new("blah").program_prefix.should == "blah"
|
10
|
+
end
|
11
|
+
|
12
|
+
it "Should let you set the program prefix after initialization" do
|
13
|
+
opt = IOptionParser.new
|
14
|
+
opt.program_prefix.should == $0
|
15
|
+
opt.program_prefix = "blah"
|
16
|
+
opt.program_prefix.should == "blah"
|
17
|
+
end
|
18
|
+
|
19
|
+
it "Should generate a default banner if none specified" do
|
20
|
+
IOptionParser.new().banner.should be_kind_of(String)
|
21
|
+
end
|
22
|
+
it "Should let you set the banner" do
|
23
|
+
opt = IOptionParser.new
|
24
|
+
opt.banner = "blah"
|
25
|
+
opt.banner.should == "blah"
|
26
|
+
end
|
27
|
+
|
28
|
+
it "Should yield the object for use if a block is given to the constructor" do
|
29
|
+
IOptionParser.new("boom") {|p|
|
30
|
+
p.should be_kind_of(IOptionParser)
|
31
|
+
p.program_prefix.should == "boom"
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
it "Should not allow multicharacter short options or single character long options" do
|
36
|
+
IOptionParser.new {|p|
|
37
|
+
proc { p.flag "boom", "boom", "what?" }.should raise_error(ArgumentError)
|
38
|
+
proc { p.flag "a", "a", "what?" }.should raise_error(ArgumentError)
|
39
|
+
}
|
40
|
+
end
|
41
|
+
|
42
|
+
before :each do
|
43
|
+
@opt = IOptionParser.new("testing")
|
44
|
+
@opt.flag "a", "abba", "stuff goes here"
|
45
|
+
@opt.flag "b", "boom", "this one goes boom" do raise "BOOM" end
|
46
|
+
@opt.argument "c", "cool", "this one is awesome", "whatever"
|
47
|
+
@opt.gap
|
48
|
+
@opt.argument "d", "dumb", "this one is dumb. It wants nothing but an integer.", 5, Integer
|
49
|
+
@opt.argument "e", "everything", "this one's really clever, it's always 'everything'", "what?" do "everything" end
|
50
|
+
@opt.argument "f", "fugged-aboudit", "this one has a hyphen, which makes it scary", "stuff"
|
51
|
+
end
|
52
|
+
|
53
|
+
it "Should have defined the right methods" do
|
54
|
+
[:abba?, :boom?, :cool, :dumb, :everything, :fugged_aboudit].each {|i|
|
55
|
+
@opt.respond_to?(i).should be_true
|
56
|
+
}
|
57
|
+
end
|
58
|
+
|
59
|
+
it "Should set the correct defaults" do
|
60
|
+
@opt.abba?.should be_false
|
61
|
+
@opt.boom?.should be_false
|
62
|
+
@opt.cool.should == "whatever"
|
63
|
+
@opt.dumb.should == 5
|
64
|
+
@opt.everything.should == "what?"
|
65
|
+
@opt.fugged_aboudit.should == "stuff"
|
66
|
+
end
|
67
|
+
|
68
|
+
it "Should return itself from both parse and parse!" do
|
69
|
+
@opt.parse(["-a"]).should == @opt
|
70
|
+
@opt.parse!(["-a"]).should == @opt
|
71
|
+
end
|
72
|
+
|
73
|
+
it "Should parse a simple short flag" do
|
74
|
+
@opt.parse(["-a"])
|
75
|
+
@opt.abba?.should be_true
|
76
|
+
end
|
77
|
+
|
78
|
+
it "Should parse a simple long flag" do
|
79
|
+
@opt.parse(["--abba"])
|
80
|
+
@opt.abba?.should be_true
|
81
|
+
end
|
82
|
+
|
83
|
+
it "Should raise an error if we try to set the exploding flag" do
|
84
|
+
proc { @opt.parse(["-b"]) }.should raise_error("BOOM")
|
85
|
+
end
|
86
|
+
|
87
|
+
it "Should raise if an argument isn't supplied to a normal argument" do
|
88
|
+
proc { @opt.parse(["-c"]) }.should raise_error(ArgumentError)
|
89
|
+
end
|
90
|
+
|
91
|
+
it "Should parse a simple argument with properly specified" do
|
92
|
+
@opt.parse(["-c", "stuff"])
|
93
|
+
@opt.cool.should == "stuff"
|
94
|
+
end
|
95
|
+
|
96
|
+
it "Should parse a simple argument in its long form properly specified" do
|
97
|
+
@opt.parse(["--cool", "stuff"])
|
98
|
+
@opt.cool.should == "stuff"
|
99
|
+
end
|
100
|
+
|
101
|
+
it "Should deal with a hyphen in the command line argument" do
|
102
|
+
@opt.parse(["--fugged-aboudit", "boom"])
|
103
|
+
@opt.fugged_aboudit.should == "boom"
|
104
|
+
end
|
105
|
+
|
106
|
+
it "should parse a flag and an argument separately" do
|
107
|
+
@opt.parse(["-a", "-c", "stuff"])
|
108
|
+
@opt.abba?.should be_true
|
109
|
+
@opt.cool.should == "stuff"
|
110
|
+
end
|
111
|
+
|
112
|
+
it "Should validate input using from_user_input" do
|
113
|
+
proc { @opt.parse(["-d", "whatever"]) }.should raise_error(ArgumentError)
|
114
|
+
@opt.dumb.should == 5
|
115
|
+
@opt.parse(["-d", "99"])
|
116
|
+
@opt.dumb.should == 99
|
117
|
+
end
|
118
|
+
|
119
|
+
it "Should validate input using a proc object" do
|
120
|
+
@opt.parse(["-e", "stufffff"])
|
121
|
+
@opt.everything.should == "everything"
|
122
|
+
end
|
123
|
+
|
124
|
+
it "should parse correctly if you specify multiple arguments in a group" do
|
125
|
+
@opt.parse(["-acd", "what", "1"])
|
126
|
+
@opt.abba?.should be_true
|
127
|
+
@opt.cool.should == "what"
|
128
|
+
@opt.dumb.should == 1
|
129
|
+
end
|
130
|
+
|
131
|
+
it "Should raise an error if you supply a flag or argument it doesn't understand" do
|
132
|
+
proc { @opt.parse(["-z"]) }.should raise_error(ArgumentError)
|
133
|
+
proc { @opt.parse(["--zoom"]) }.should raise_error(ArgumentError)
|
134
|
+
end
|
135
|
+
|
136
|
+
it "should parse destructively if you use parse!" do
|
137
|
+
arr = ["-a"]
|
138
|
+
@opt.parse!(arr)
|
139
|
+
arr.should == []
|
140
|
+
end
|
141
|
+
|
142
|
+
it "should stop parsing on finding a non-flag word unexpectedly and return the remainder" do
|
143
|
+
arr = ["-a", "boom", "whatever"]
|
144
|
+
@opt.parse!(arr)
|
145
|
+
arr.should == ["boom", "whatever"]
|
146
|
+
end
|
147
|
+
|
148
|
+
it "should stop parsing on finding --, but should still consume it" do
|
149
|
+
arr = ["-a", "--", "whatever"]
|
150
|
+
@opt.parse!(arr)
|
151
|
+
arr.should == ["whatever"]
|
152
|
+
end
|
153
|
+
|
154
|
+
it "should return a string from to_s" do
|
155
|
+
# Possibly this spec should include an example to compare against, but that seems too rigid.
|
156
|
+
@opt.to_s.should be_kind_of(String)
|
157
|
+
end
|
158
|
+
end
|
data/spec/spec.opts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
require 'lib/user_input/type_safe_hash'
|
3
|
+
|
4
|
+
describe UserInput::TypeSafeHash do
|
5
|
+
TypeSafeHash = UserInput::TypeSafeHash
|
6
|
+
|
7
|
+
describe "initializer" do
|
8
|
+
it "should take a normal hash as an argument" do
|
9
|
+
TypeSafeHash.new(:a => :b).should == TypeSafeHash.new(:a => :b)
|
10
|
+
end
|
11
|
+
it "should take another type safe hash as an argument" do
|
12
|
+
TypeSafeHash.new(TypeSafeHash.new(:a => :b)).should == TypeSafeHash.new(:a => :b)
|
13
|
+
end
|
14
|
+
it "should raise an error for anything else" do
|
15
|
+
proc {
|
16
|
+
TypeSafeHash.new("boom")
|
17
|
+
}.should raise_error(ArgumentError)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
before :each do
|
22
|
+
@hash = TypeSafeHash.new("blah" => "blorp", 1 => 2, 3 => [1, 2, 3])
|
23
|
+
@str_hash = TypeSafeHash.new("blah" => "waht", "woop" => "wonk", "woo" => "fluh")
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should properly implement to_hash to return the original hash object" do
|
27
|
+
@hash.to_hash.should be_kind_of(Hash)
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should compare to another TypeSafeHash as equal if they have the same real hash" do
|
31
|
+
(@hash == TypeSafeHash.new(@hash.to_hash)).should be_true
|
32
|
+
(@hash == TypeSafeHash.new()).should be_false
|
33
|
+
end
|
34
|
+
it "should compare to any other object as not equal" do
|
35
|
+
(@hash == Hash.new).should be_false
|
36
|
+
end
|
37
|
+
|
38
|
+
describe "fetch" do
|
39
|
+
it "should validate as user input any requests for keys" do
|
40
|
+
@hash.fetch("blah", String).should == "blorp"
|
41
|
+
@hash.fetch("blah", /orp/)[0].should == "blorp".match(/orp/)[0]
|
42
|
+
@hash.fetch("blah", Integer).should be_nil
|
43
|
+
|
44
|
+
@hash.fetch(1, Integer).should == 2
|
45
|
+
@hash.fetch(1, 2).should == 2
|
46
|
+
@hash.fetch(1, 3).should be_nil
|
47
|
+
|
48
|
+
@hash.fetch(3, [Integer]).should == [1,2,3]
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should flatten an array value to a single value if the requested type is not an array" do
|
52
|
+
@hash.fetch(3, Integer).should == 1
|
53
|
+
end
|
54
|
+
|
55
|
+
it "should take a default value for failed matching" do
|
56
|
+
@hash.fetch("blah", /woople/, "woople").should == "woople"
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should be aliased to the [] method" do
|
60
|
+
@hash["blah", String].should == "blorp"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should properly implement each_key" do
|
65
|
+
keys = []
|
66
|
+
@hash.each_key {|x| keys.push(x)}
|
67
|
+
keys == @hash.to_hash.keys
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should properly implement each_pair" do
|
71
|
+
out = {}
|
72
|
+
@str_hash.each_pair(String) {|key, val| out[key]=val}
|
73
|
+
out.should == @str_hash.to_hash
|
74
|
+
end
|
75
|
+
|
76
|
+
it "should properly implement each_match, returning only keys that match the regex" do
|
77
|
+
out = []
|
78
|
+
@str_hash.each_match(/woo/, String) {|match, val| out.push([match[0],val]) }
|
79
|
+
out.sort.should == [["woo", "fluh"], ["woo", "wonk"]].sort
|
80
|
+
end
|
81
|
+
|
82
|
+
it "empty? should return true for an empty hash and false for one with values" do
|
83
|
+
@hash.empty?.should == false
|
84
|
+
TypeSafeHash.new.empty?.should == true
|
85
|
+
end
|
86
|
+
|
87
|
+
it "should implement has_key? and all of its aliases (include?, key?, member?)" do
|
88
|
+
@hash.has_key?(3).should be_true
|
89
|
+
@hash.has_key?(4).should be_false
|
90
|
+
@hash.include?(3).should be_true
|
91
|
+
@hash.include?(4).should be_false
|
92
|
+
@hash.key?(3).should be_true
|
93
|
+
@hash.key?(4).should be_false
|
94
|
+
@hash.member?(3).should be_true
|
95
|
+
@hash.member?(4).should be_false
|
96
|
+
end
|
97
|
+
|
98
|
+
it "should implement inspect as an alias to its real hash's inspect" do
|
99
|
+
@hash.inspect.should == @hash.to_hash.inspect
|
100
|
+
end
|
101
|
+
|
102
|
+
it "should implement keys as returning the real hash's keys, both sorted and unsorted" do
|
103
|
+
@str_hash.keys.should == @str_hash.to_hash.keys
|
104
|
+
@str_hash.keys(true).should == @str_hash.to_hash.keys.sort
|
105
|
+
end
|
106
|
+
|
107
|
+
it "should implement values as returning the values from the real hash" do
|
108
|
+
@hash.values.should == @hash.to_hash.values
|
109
|
+
end
|
110
|
+
|
111
|
+
it "should return the number of items in the hash from length" do
|
112
|
+
@hash.length.should == @hash.to_hash.length
|
113
|
+
end
|
114
|
+
|
115
|
+
it "should implement to_s as returning the internal hash's string representation" do
|
116
|
+
@hash.to_s.should == @hash.to_hash.to_s
|
117
|
+
end
|
118
|
+
|
119
|
+
it "should implement TypeSafeHash.from_user_input as accepting only a hash" do
|
120
|
+
TypeSafeHash.from_user_input({}).should == TypeSafeHash.new({})
|
121
|
+
TypeSafeHash.from_user_input("Blah").should be_nil
|
122
|
+
end
|
123
|
+
end
|