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/.document
ADDED
data/.gitignore
ADDED
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
|