user_input 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,9 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+ require 'user_input'
4
+ require 'spec'
5
+ require 'spec/autorun'
6
+
7
+ Spec::Runner.configure do |config|
8
+
9
+ end
@@ -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