user_input 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+ *.gemspec
21
+
22
+ ## PROJECT::SPECIFIC
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Graham Batty
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,61 @@
1
+ = user_input
2
+
3
+ This gem provides simple, convention-based, user input validation and coercion. It adds the method from_user_input to various built-in classes as both a class and instance method. On a class, the method returns a validated instance of that class if it can be coerced into one (or nil if not). On an instance, it validates more strictly against the value of that instance. The following examples demonstrate the intended behaviour:
4
+
5
+ require 'user_input'
6
+ String.from_user_input("blah") => "blah"
7
+ Integer.from_user_input("1") => 1
8
+ Integer.from_user_input("blorp") => nil
9
+ 1.from_user_input("1") => 1
10
+ 1.from_user_input("2") => nil
11
+ Array.from_user_input([1, 2, 3]) => [1, 2, 3]
12
+ Array.from_user_input("blah") => nil
13
+ [Integer].from_user_input([1, 2, 3]) => [1, 2, 3]
14
+ [Integer].from_user_input(["blorp"]) => nil
15
+ [Integer].from_user_input(["blorp", 1]) => [1]
16
+ {String => Integer}.from_user_input({"blah" => 1}) => {"blah" => 1}
17
+ {String => Integer}.from_user_input({"blah" => "blorp"}) => nil
18
+
19
+ See the specs and/or rdoc for more details.
20
+
21
+ It also provides a 'type safe hash' that uses the above functions to validate its contents. It can be used as a convenient tool for dealing with, as an example, an http params hash:
22
+
23
+ require 'user_input/type_safe_hash'
24
+ h = UserInput::TypeSafeHash.new("blah" => "blorp", "woozle" => 1, "goggle" => [1, 2, 3])
25
+ h["blah", String] => "blorp"
26
+ h["blah", Integer] => nil
27
+ h["blah", /hello/, "what?"] => "what?"
28
+ h["goggle", [/boom/], []] => []
29
+
30
+ And finally, there is a command line option parser that lets you validate this way. An example of its use is as follows:
31
+ require 'user_input/option_parser'
32
+ options = UserInput::OptionParser.new do |c|
33
+ c.argument 'c', 'config', "Config file to use", String, "dev"
34
+ c.argument 'i', 'ipaddr', "IP Address to listen on", IPAddr, IPAddr.new("127.0.0.1")
35
+ c.argument 'p', 'port', "Port to listen on", Integer, 1024
36
+ c.gap
37
+ c.flag 'h', 'help', "Display help message" do
38
+ puts(c)
39
+ exit(1)
40
+ end
41
+ c.flag 'v', 'version', "Display version" do
42
+ puts("1.0")
43
+ exit(1)
44
+ end
45
+ end
46
+ options.parse!(ARGV)
47
+ puts(options.config, options.ipaddr, options.port)
48
+
49
+ == Note on Patches/Pull Requests
50
+
51
+ * Fork the project.
52
+ * Make your feature addition or bug fix.
53
+ * Add tests for it. This is important so I don't break it in a
54
+ future version unintentionally.
55
+ * Commit, do not mess with rakefile, version, or history.
56
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
57
+ * Send me a pull request. Bonus points for topic branches.
58
+
59
+ == Copyright
60
+
61
+ Copyright (c) 2010 Graham Batty. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,45 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "user_input"
8
+ gem.summary = %Q{Provides simple, convention-based, user input validation and coercion. Also provides a 'type safe hash' and command line option parser that use it.}
9
+ gem.description = %Q{Provides simple, convention-based, user input validation and coercion. Also provides a 'type safe hash' and command line option parser that use it.}
10
+ gem.email = "graham@stormbrew.ca"
11
+ gem.homepage = "http://github.com/stormbrew/user_input"
12
+ gem.authors = ["Graham Batty"]
13
+ gem.add_development_dependency "rspec", ">= 1.2.9"
14
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
15
+ end
16
+ Jeweler::GemcutterTasks.new
17
+ rescue LoadError
18
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
19
+ end
20
+
21
+ require 'spec/rake/spectask'
22
+ Spec::Rake::SpecTask.new(:spec) do |spec|
23
+ spec.libs << 'lib' << 'spec'
24
+ spec.spec_files = FileList['spec/**/*_spec.rb']
25
+ end
26
+
27
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
28
+ spec.libs << 'lib' << 'spec'
29
+ spec.pattern = 'spec/**/*_spec.rb'
30
+ spec.rcov = true
31
+ end
32
+
33
+ task :spec => :check_dependencies
34
+
35
+ task :default => :spec
36
+
37
+ require 'rake/rdoctask'
38
+ Rake::RDocTask.new do |rdoc|
39
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
40
+
41
+ rdoc.rdoc_dir = 'rdoc'
42
+ rdoc.title = "user_input #{version}"
43
+ rdoc.rdoc_files.include('README*')
44
+ rdoc.rdoc_files.include('lib/**/*.rb')
45
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.0
@@ -0,0 +1,213 @@
1
+ require 'user_input'
2
+
3
+ module UserInput
4
+ # Handles parsing command line options such that it terminates on the first
5
+ # bareword argument, '--', or the end of the arguments. Uses from_user_input
6
+ # to validate input if requested.
7
+ class OptionParser
8
+ Info = Struct.new(:short_name, :long_name, :description, :flag, :default_value, :value, :validate)
9
+ class Info
10
+ attr_accessor :short_name, :long_name, :description, :flag, :default_value, :value, :validate
11
+ def initialize(short_name, long_name, description, flag, default_value, value, validate)
12
+ @short_name, @long_name, @description, @flag, @default_value, @value, @validate =
13
+ short_name, long_name, description, flag, default_value, value, validate
14
+ end
15
+
16
+ def value=(string)
17
+ if (validate.nil?)
18
+ @value = string
19
+ elsif (validate.respond_to? :call) # it's a proc, call it and replace with return value
20
+ @value = validate.call(string)
21
+ else
22
+ string = validate.from_user_input(string)
23
+ if (!string)
24
+ raise ArgumentError, "Validation of #{long_name} failed. Expected #{validate}"
25
+ end
26
+ @value = string
27
+ end
28
+ end
29
+ end
30
+
31
+ # The prefix for the program in the usage banner. Used only if banner is nil.
32
+ attr_accessor :program_prefix
33
+ # The banner to display above the help. Defaults to nil, in which case it's generated.
34
+ attr_writer :banner
35
+
36
+ # If a block is passed in, it is given self.
37
+ def initialize(program_prefix = $0)
38
+ @options = {}
39
+ @order = []
40
+ @program_prefix = program_prefix
41
+ @banner = nil
42
+
43
+ if (block_given?)
44
+ yield self
45
+ end
46
+ end
47
+
48
+ def define_value(short_name, long_name, description, flag, default_value, validate = nil)
49
+ short_name = short_name.to_s
50
+ long_name = long_name.to_s
51
+
52
+ if (short_name.length != 1)
53
+ raise ArgumentError, "Short name must be one character long (#{short_name})"
54
+ end
55
+ if (long_name.length < 2)
56
+ raise ArgumentError, "Long name must be more than one character long (#{long_name})"
57
+ end
58
+
59
+ info = Info.new(short_name, long_name, description, flag, default_value, nil, validate)
60
+ @options[long_name] = info
61
+ @options[short_name] = info
62
+
63
+ @order.push(info)
64
+
65
+ method_name = long_name.gsub('-','_')
66
+ method_name << "?" if (flag)
67
+ (class <<self; self; end).class_eval do
68
+ define_method(method_name.to_sym) do
69
+ return @options[long_name].value || @options[long_name].default_value
70
+ end
71
+ end
72
+
73
+ return self
74
+ end
75
+ private :define_value
76
+
77
+ # This defines a command line argument that takes a value.
78
+ def argument(short_name, long_name, description, default_value, validate = nil, &block)
79
+ return define_value(short_name, long_name, description, false, default_value, validate || block)
80
+ end
81
+
82
+ # This defines a command line argument that's either on or off based on the presense
83
+ # of the flag.
84
+ def flag(short_name, long_name, description, &block)
85
+ return define_value(short_name, long_name, description, true, false, block)
86
+ end
87
+
88
+ # This produces a gap in the output of the help display but does not otherwise
89
+ # affect the argument parsing.
90
+ def gap(count = 1)
91
+ 1.upto(count) { @order.push(nil) }
92
+ end
93
+
94
+ def parse!(argv = ARGV)
95
+ # this is a stack of arguments that need to have their values filled by subsequent arguments
96
+ argument_stack = []
97
+
98
+ while (argv.first)
99
+ arg = argv.first
100
+
101
+ # if there's a node on the argument stack, we fill it in.
102
+ if (argument_stack.first)
103
+ argument_stack.shift.value = arg
104
+ else
105
+ # figure out what type of argument it is
106
+ if (arg == '--')
107
+ # bare -- should cause the parser to consume and then stop, unlike bare word
108
+ # which should leave it in place.
109
+ argv.shift
110
+ return self
111
+ elsif (match = arg.match(/^\-\-(.+)$/))
112
+ arg_info = @options[match[1]]
113
+ if (!arg_info)
114
+ raise ArgumentError, "Unrecognized option #{match[1]}"
115
+ end
116
+ if (arg_info.flag)
117
+ arg_info.value = true
118
+ else
119
+ argument_stack.push(arg_info)
120
+ end
121
+ elsif (match = arg.match(/^-(.+)$/))
122
+ short_args = match[1].split("")
123
+ short_args.each {|short_arg|
124
+ arg_info = @options[short_arg]
125
+ if (!arg_info)
126
+ raise ArgumentError, "unrecognized option #{match[1]}"
127
+ end
128
+ if (arg_info.flag)
129
+ arg_info.value = true
130
+ else
131
+ argument_stack.push(arg_info)
132
+ end
133
+ }
134
+ else
135
+ # unrecognized bareword, so bail out and leave it to the caller to figure it out.
136
+ return self
137
+ end
138
+ end
139
+
140
+ argv.shift
141
+ end
142
+
143
+ # if we got here and there are still items on the argument stack,
144
+ # we didn't get all the values we expected so error out.
145
+ if (argument_stack.length > 0)
146
+ raise ArgumentError, "Missing value for argument #{argument_stack.first.long_name}"
147
+ end
148
+ return self
149
+ end
150
+ def parse(argv = ARGV)
151
+ self.parse!(argv.dup)
152
+ end
153
+
154
+ # returns either the banner set with banner= or a simple banner
155
+ # like "Usage: $0 [arguments]"
156
+ def banner
157
+ @banner || "Usage: #{program_prefix} [arguments]"
158
+ end
159
+
160
+ def longest
161
+ l = 0
162
+ @options.keys.each {|opt|
163
+ l = (opt.length > l)? opt.length : l
164
+ }
165
+ return l
166
+ end
167
+
168
+ # Outputs a help screen. Code largely taken from the awesome, but
169
+ # not quite right for my needs, Clip (http://github.com/alexvollmer/clip)
170
+ def to_s
171
+ out = ""
172
+ out << banner << "\n"
173
+
174
+ @order.each {|option|
175
+ if (option.nil?)
176
+ out << "\n"
177
+ next
178
+ end
179
+
180
+ line = sprintf("-%-2s --%-#{longest+6}s ",
181
+ option.short_name,
182
+ option.long_name + (option.flag ? "" : " [VAL]"))
183
+
184
+ out << line
185
+ if (line.length + option.description.length <= 80)
186
+ out << option.description
187
+ else
188
+ rem = 80 - line.length
189
+ desc = option.description
190
+ i = 0
191
+ while (i < desc.length)
192
+ out << "\n" if i > 0
193
+ j = [i + rem, desc.length].min
194
+ while desc[j..j] =~ /[\w\d]/
195
+ j -= 1
196
+ end
197
+ chunk = desc[i..j].strip
198
+ out << " " * line.length if i > 0
199
+ out << chunk
200
+ i = j + 1
201
+ end
202
+ end
203
+
204
+ if (!option.flag)
205
+ out << " (default: #{option.default_value})"
206
+ end
207
+
208
+ out << "\n"
209
+ }
210
+ return out
211
+ end
212
+ end
213
+ end
@@ -0,0 +1,134 @@
1
+ require 'user_input'
2
+
3
+ module UserInput
4
+ # TypeSafeHash is a read-only hash that requires you to specify an expected type
5
+ # when retrieving elements from it. When fetching an item from the hash, you must
6
+ # specify the type as the second argument. It uses the type specified's
7
+ # from_user_input member to determine if the value is valid and then uses it.
8
+ class TypeSafeHash
9
+ attr_reader :real_hash
10
+
11
+ include Enumerable
12
+
13
+ # Initializes the type safe hash based on an existing normal hash.
14
+ def initialize(hash = {})
15
+ if (hash.kind_of? TypeSafeHash)
16
+ @real_hash = hash.to_hash
17
+ elsif (hash.kind_of? Hash)
18
+ @real_hash = hash
19
+ else
20
+ raise ArgumentError, "TypeSafeHash expects a hash object for its initializer"
21
+ end
22
+ end
23
+
24
+ # Compares a type safe hash with another.
25
+ def ==(other)
26
+ if (other.respond_to?(:real_hash))
27
+ return real_hash == other.real_hash
28
+ else
29
+ return false
30
+ end
31
+ end
32
+
33
+ # Retrieves an item from the hash based on the key. If it's not there,
34
+ # or doesn't validate with type.from_user_input(value), returns default.
35
+ def fetch(key, type, default = nil)
36
+ if (real_hash.has_key?(key))
37
+ value = real_hash[key]
38
+
39
+ # if type is not an array, but value is, flatten it.
40
+ if (type != Array && !type.kind_of?(Array) && value.kind_of?(Array))
41
+ value = value[0]
42
+ end
43
+
44
+ real = type.from_user_input(value)
45
+ if (real != nil)
46
+ return real
47
+ end
48
+ end
49
+ return default
50
+ end
51
+ alias :[] :fetch
52
+
53
+ # Enumerates the keys in the hash.
54
+ def each_key()
55
+ real_hash.each_key() { |key| yield key; }
56
+ end
57
+ alias :each :each_key
58
+
59
+ # Enumerates the key, value pairs in the has.
60
+ def each_pair(type, default = nil)
61
+ real_hash.each_key() { |key|
62
+ value = fetch(key, type, default)
63
+ if (!value.nil?)
64
+ yield(key, value)
65
+ end
66
+ }
67
+ end
68
+
69
+ # Enumerates keys that match a regex, passing the match object and the
70
+ # value.
71
+ def each_match(regex, type, default = nil)
72
+ real_hash.each_key() { |key|
73
+ if (matchinfo = regex.match(key))
74
+ value = fetch(key, type, default)
75
+ if (!value.nil?)
76
+ yield(matchinfo, value)
77
+ end
78
+ end
79
+ }
80
+ end
81
+
82
+ # Returns true if the hash is empty.
83
+ def empty?()
84
+ return real_hash.empty?()
85
+ end
86
+
87
+ # Returns true if key is in the hash.
88
+ def has_key?(key)
89
+ return real_hash.has_key?(key)
90
+ end
91
+ alias :include? :has_key?
92
+ alias :key? :has_key?
93
+ alias :member? :has_key?
94
+
95
+ # Returns a string representation of the inner hash.
96
+ def inspect()
97
+ return real_hash.inspect()
98
+ end
99
+
100
+ # Returns an array of the keys in the hash.
101
+ def keys(sorted=false)
102
+ return sorted ? real_hash.keys().sort() : real_hash.keys()
103
+ end
104
+
105
+ # Returns an array of the values in the hash.
106
+ def values()
107
+ return real_hash.values()
108
+ end
109
+
110
+ # Returns the number of elements in the hash.
111
+ def length()
112
+ return real_hash.length()
113
+ end
114
+ alias :size :length
115
+
116
+ # Returns the inner hash.
117
+ def to_hash
118
+ return @real_hash
119
+ end
120
+
121
+ def self.from_user_input(value)
122
+ if (value.kind_of?(Hash))
123
+ return TypeSafeHash.new(value)
124
+ else
125
+ return nil
126
+ end
127
+ end
128
+
129
+ # Returns a string representation of the inner hash.
130
+ def to_s()
131
+ return real_hash.to_s
132
+ end
133
+ end
134
+ end