user_input 1.0.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.
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