rbot 0.9.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. data/AUTHORS +16 -0
  2. data/COPYING +21 -0
  3. data/ChangeLog +418 -0
  4. data/INSTALL +8 -0
  5. data/README +44 -0
  6. data/REQUIREMENTS +34 -0
  7. data/TODO +5 -0
  8. data/Usage_en.txt +129 -0
  9. data/bin/rbot +81 -0
  10. data/data/rbot/contrib/plugins/figlet.rb +20 -0
  11. data/data/rbot/contrib/plugins/ri.rb +83 -0
  12. data/data/rbot/contrib/plugins/stats.rb +232 -0
  13. data/data/rbot/contrib/plugins/vandale.rb +49 -0
  14. data/data/rbot/languages/dutch.lang +73 -0
  15. data/data/rbot/languages/english.lang +75 -0
  16. data/data/rbot/languages/french.lang +39 -0
  17. data/data/rbot/languages/german.lang +67 -0
  18. data/data/rbot/plugins/autoop.rb +68 -0
  19. data/data/rbot/plugins/autorejoin.rb +16 -0
  20. data/data/rbot/plugins/cal.rb +15 -0
  21. data/data/rbot/plugins/dice.rb +81 -0
  22. data/data/rbot/plugins/eightball.rb +19 -0
  23. data/data/rbot/plugins/excuse.rb +470 -0
  24. data/data/rbot/plugins/fish.rb +61 -0
  25. data/data/rbot/plugins/fortune.rb +22 -0
  26. data/data/rbot/plugins/freshmeat.rb +98 -0
  27. data/data/rbot/plugins/google.rb +51 -0
  28. data/data/rbot/plugins/host.rb +14 -0
  29. data/data/rbot/plugins/httpd.rb.disabled +35 -0
  30. data/data/rbot/plugins/insult.rb +258 -0
  31. data/data/rbot/plugins/karma.rb +85 -0
  32. data/data/rbot/plugins/lart.rb +181 -0
  33. data/data/rbot/plugins/math.rb +122 -0
  34. data/data/rbot/plugins/nickserv.rb +89 -0
  35. data/data/rbot/plugins/nslookup.rb +43 -0
  36. data/data/rbot/plugins/opme.rb +19 -0
  37. data/data/rbot/plugins/quakeauth.rb +51 -0
  38. data/data/rbot/plugins/quotes.rb +321 -0
  39. data/data/rbot/plugins/remind.rb +228 -0
  40. data/data/rbot/plugins/roshambo.rb +54 -0
  41. data/data/rbot/plugins/rot13.rb +10 -0
  42. data/data/rbot/plugins/roulette.rb +147 -0
  43. data/data/rbot/plugins/rss.rb.disabled +414 -0
  44. data/data/rbot/plugins/seen.rb +89 -0
  45. data/data/rbot/plugins/slashdot.rb +94 -0
  46. data/data/rbot/plugins/spell.rb +36 -0
  47. data/data/rbot/plugins/tube.rb +71 -0
  48. data/data/rbot/plugins/url.rb +88 -0
  49. data/data/rbot/plugins/weather.rb +649 -0
  50. data/data/rbot/plugins/wserver.rb +71 -0
  51. data/data/rbot/plugins/xmlrpc.rb.disabled +52 -0
  52. data/data/rbot/templates/keywords.rbot +4 -0
  53. data/data/rbot/templates/lart/larts +98 -0
  54. data/data/rbot/templates/lart/praises +5 -0
  55. data/data/rbot/templates/levels.rbot +30 -0
  56. data/data/rbot/templates/users.rbot +1 -0
  57. data/lib/rbot/auth.rb +203 -0
  58. data/lib/rbot/channel.rb +54 -0
  59. data/lib/rbot/config.rb +363 -0
  60. data/lib/rbot/dbhash.rb +112 -0
  61. data/lib/rbot/httputil.rb +141 -0
  62. data/lib/rbot/ircbot.rb +808 -0
  63. data/lib/rbot/ircsocket.rb +185 -0
  64. data/lib/rbot/keywords.rb +433 -0
  65. data/lib/rbot/language.rb +69 -0
  66. data/lib/rbot/message.rb +256 -0
  67. data/lib/rbot/messagemapper.rb +262 -0
  68. data/lib/rbot/plugins.rb +291 -0
  69. data/lib/rbot/post-install.rb +8 -0
  70. data/lib/rbot/rbotconfig.rb +36 -0
  71. data/lib/rbot/registry.rb +271 -0
  72. data/lib/rbot/rfc2812.rb +1104 -0
  73. data/lib/rbot/timer.rb +201 -0
  74. data/lib/rbot/utils.rb +83 -0
  75. data/setup.rb +1360 -0
  76. metadata +129 -0
@@ -0,0 +1,69 @@
1
+ module Irc
2
+ module Language
3
+
4
+ class Language
5
+ BotConfig.register BotConfigEnumValue.new('core.language',
6
+ :default => "english", :wizard => true,
7
+ :values => Proc.new{|bot|
8
+ Dir.new(Config::datadir + "/languages").collect {|f|
9
+ f =~ /\.lang$/ ? f.gsub(/\.lang$/, "") : nil
10
+ }.compact
11
+ },
12
+ :on_change => Proc.new {|bot, v| bot.lang.set_language v},
13
+ :desc => "Which language file the bot should use")
14
+
15
+ def initialize(language)
16
+ set_language language
17
+ end
18
+
19
+ def set_language(language)
20
+ file = Config::datadir + "/languages/#{language}.lang"
21
+ unless(FileTest.exist?(file))
22
+ raise "no such language: #{language} (no such file #{file})"
23
+ end
24
+ @language = language
25
+ @file = file
26
+ scan
27
+ end
28
+
29
+ def scan
30
+ @strings = Hash.new
31
+ current_key = nil
32
+ IO.foreach(@file) {|l|
33
+ next if l =~ /^$/
34
+ next if l =~ /^\s*#/
35
+ if(l =~ /^(\S+):$/)
36
+ @strings[$1] = Array.new
37
+ current_key = $1
38
+ elsif(l =~ /^\s*(.*)$/)
39
+ @strings[current_key] << $1
40
+ end
41
+ }
42
+ end
43
+
44
+ def rescan
45
+ scan
46
+ end
47
+
48
+ def get(key)
49
+ if(@strings.has_key?(key))
50
+ return @strings[key][rand(@strings[key].length)]
51
+ else
52
+ raise "undefined language key"
53
+ end
54
+ end
55
+
56
+ def save
57
+ File.open(@file, "w") {|file|
58
+ @strings.each {|key,val|
59
+ file.puts "#{key}:"
60
+ val.each_value {|v|
61
+ file.puts " #{v}"
62
+ }
63
+ }
64
+ }
65
+ end
66
+ end
67
+
68
+ end
69
+ end
@@ -0,0 +1,256 @@
1
+ module Irc
2
+
3
+ # base user message class, all user messages derive from this
4
+ # (a user message is defined as having a source hostmask, a target
5
+ # nick/channel and a message part)
6
+ class BasicUserMessage
7
+
8
+ # associated bot
9
+ attr_reader :bot
10
+
11
+ # when the message was received
12
+ attr_reader :time
13
+
14
+ # hostmask of message source
15
+ attr_reader :source
16
+
17
+ # nick of message source
18
+ attr_reader :sourcenick
19
+
20
+ # url part of message source
21
+ attr_reader :sourceaddress
22
+
23
+ # nick/channel message was sent to
24
+ attr_reader :target
25
+
26
+ # contents of the message
27
+ attr_accessor :message
28
+
29
+ # has the message been replied to/handled by a plugin?
30
+ attr_accessor :replied
31
+
32
+ # instantiate a new Message
33
+ # bot:: associated bot class
34
+ # source:: hostmask of the message source
35
+ # target:: nick/channel message is destined for
36
+ # message:: message part
37
+ def initialize(bot, source, target, message)
38
+ @time = Time.now
39
+ @bot = bot
40
+ @source = source
41
+ @address = false
42
+ @target = target
43
+ @message = BasicUserMessage.stripcolour message
44
+ @replied = false
45
+
46
+ # split source into consituent parts
47
+ if source =~ /^((\S+)!(\S+))$/
48
+ @sourcenick = $2
49
+ @sourceaddress = $3
50
+ end
51
+
52
+ if target && target.downcase == @bot.nick.downcase
53
+ @address = true
54
+ end
55
+
56
+ end
57
+
58
+ # returns true if the message was addressed to the bot.
59
+ # This includes any private message to the bot, or any public message
60
+ # which looks like it's addressed to the bot, e.g. "bot: foo", "bot, foo",
61
+ # a kick message when bot was kicked etc.
62
+ def address?
63
+ return @address
64
+ end
65
+
66
+ # has this message been replied to by a plugin?
67
+ def replied?
68
+ return @replied
69
+ end
70
+
71
+ # strip mIRC colour escapes from a string
72
+ def BasicUserMessage.stripcolour(string)
73
+ return "" unless string
74
+ ret = string.gsub(/\cC\d\d?(?:,\d\d?)?/, "")
75
+ #ret.tr!("\x00-\x1f", "")
76
+ ret
77
+ end
78
+
79
+ end
80
+
81
+ # class for handling IRC user messages. Includes some utilities for handling
82
+ # the message, for example in plugins.
83
+ # The +message+ member will have any bot addressing "^bot: " removed
84
+ # (address? will return true in this case)
85
+ class UserMessage < BasicUserMessage
86
+
87
+ # for plugin messages, the name of the plugin invoked by the message
88
+ attr_reader :plugin
89
+
90
+ # for plugin messages, the rest of the message, with the plugin name
91
+ # removed
92
+ attr_reader :params
93
+
94
+ # convenience member. Who to reply to (i.e. would be sourcenick for a
95
+ # privately addressed message, or target (the channel) for a publicly
96
+ # addressed message
97
+ attr_reader :replyto
98
+
99
+ # channel the message was in, nil for privately addressed messages
100
+ attr_reader :channel
101
+
102
+ # for PRIVMSGs, true if the message was a CTCP ACTION (CTCP stuff
103
+ # will be stripped from the message)
104
+ attr_reader :action
105
+
106
+ # instantiate a new UserMessage
107
+ # bot:: associated bot class
108
+ # source:: hostmask of the message source
109
+ # target:: nick/channel message is destined for
110
+ # message:: message part
111
+ def initialize(bot, source, target, message)
112
+ super(bot, source, target, message)
113
+ @target = target
114
+ @private = false
115
+ @plugin = nil
116
+ @action = false
117
+
118
+ if target.downcase == @bot.nick.downcase
119
+ @private = true
120
+ @address = true
121
+ @channel = nil
122
+ @replyto = @sourcenick
123
+ else
124
+ @replyto = @target
125
+ @channel = @target
126
+ end
127
+
128
+ # check for option extra addressing prefixes, e.g "|search foo", or
129
+ # "!version" - first match wins
130
+ bot.addressing_prefixes.each {|mprefix|
131
+ if @message.gsub!(/^#{Regexp.escape(mprefix)}\s*/, "")
132
+ @address = true
133
+ break
134
+ end
135
+ }
136
+
137
+ # even if they used above prefixes, we allow for silly people who
138
+ # combine all possible types, e.g. "|rbot: hello", or
139
+ # "/msg rbot rbot: hello", etc
140
+ if @message.gsub!(/^\s*#{bot.nick}\s*([:;,>]|\s)\s*/, "")
141
+ @address = true
142
+ end
143
+
144
+ if(@message =~ /^\001ACTION\s(.+)\001/)
145
+ @message = $1
146
+ @action = true
147
+ end
148
+
149
+ # free splitting for plugins
150
+ @params = @message.dup
151
+ if @params.gsub!(/^\s*(\S+)[\s$]*/, "")
152
+ @plugin = $1.downcase
153
+ @params = nil unless @params.length > 0
154
+ end
155
+ end
156
+
157
+ # returns true for private messages, e.g. "/msg bot hello"
158
+ def private?
159
+ return @private
160
+ end
161
+
162
+ # returns true if the message was in a channel
163
+ def public?
164
+ return !@private
165
+ end
166
+
167
+ def action?
168
+ return @action
169
+ end
170
+
171
+ # convenience method to reply to a message, useful in plugins. It's the
172
+ # same as doing:
173
+ # <tt>@bot.say m.replyto, string</tt>
174
+ # So if the message is private, it will reply to the user. If it was
175
+ # in a channel, it will reply in the channel.
176
+ def reply(string)
177
+ @bot.say @replyto, string
178
+ @replied = true
179
+ end
180
+
181
+ # convenience method to reply "okay" in the current language to the
182
+ # message
183
+ def okay
184
+ @bot.say @replyto, @bot.lang.get("okay")
185
+ end
186
+
187
+ end
188
+
189
+ # class to manage IRC PRIVMSGs
190
+ class PrivMessage < UserMessage
191
+ end
192
+
193
+ # class to manage IRC NOTICEs
194
+ class NoticeMessage < UserMessage
195
+ end
196
+
197
+ # class to manage IRC KICKs
198
+ # +address?+ can be used as a shortcut to see if the bot was kicked,
199
+ # basically, +target+ was kicked from +channel+ by +source+ with +message+
200
+ class KickMessage < BasicUserMessage
201
+ # channel user was kicked from
202
+ attr_reader :channel
203
+
204
+ def initialize(bot, source, target, channel, message="")
205
+ super(bot, source, target, message)
206
+ @channel = channel
207
+ end
208
+ end
209
+
210
+ # class to pass IRC Nick changes in. @message contains the old nickame,
211
+ # @sourcenick contains the new one.
212
+ class NickMessage < BasicUserMessage
213
+ def initialize(bot, source, oldnick, newnick)
214
+ super(bot, source, oldnick, newnick)
215
+ end
216
+ end
217
+
218
+ class QuitMessage < BasicUserMessage
219
+ def initialize(bot, source, target, message="")
220
+ super(bot, source, target, message)
221
+ end
222
+ end
223
+
224
+ class TopicMessage < BasicUserMessage
225
+ # channel topic
226
+ attr_reader :topic
227
+ # topic set at (unixtime)
228
+ attr_reader :timestamp
229
+ # topic set on channel
230
+ attr_reader :channel
231
+
232
+ def initialize(bot, source, channel, timestamp, topic="")
233
+ super(bot, source, channel, topic)
234
+ @topic = topic
235
+ @timestamp = timestamp
236
+ @channel = channel
237
+ end
238
+ end
239
+
240
+ # class to manage channel joins
241
+ class JoinMessage < BasicUserMessage
242
+ # channel joined
243
+ attr_reader :channel
244
+ def initialize(bot, source, channel, message="")
245
+ super(bot, source, channel, message)
246
+ @channel = channel
247
+ # in this case sourcenick is the nick that could be the bot
248
+ @address = (sourcenick.downcase == @bot.nick.downcase)
249
+ end
250
+ end
251
+
252
+ # class to manage channel parts
253
+ # same as a join, but can have a message too
254
+ class PartMessage < JoinMessage
255
+ end
256
+ end
@@ -0,0 +1,262 @@
1
+ module Irc
2
+
3
+ # +MessageMapper+ is a class designed to reduce the amount of regexps and
4
+ # string parsing plugins and bot modules need to do, in order to process
5
+ # and respond to messages.
6
+ #
7
+ # You add templates to the MessageMapper which are examined by the handle
8
+ # method when handling a message. The templates tell the mapper which
9
+ # method in its parent class (your class) to invoke for that message. The
10
+ # string is split, optionally defaulted and validated before being passed
11
+ # to the matched method.
12
+ #
13
+ # A template such as "foo :option :otheroption" will match the string "foo
14
+ # bar baz" and, by default, result in method +foo+ being called, if
15
+ # present, in the parent class. It will receive two parameters, the
16
+ # Message (derived from BasicUserMessage) and a Hash containing
17
+ # {:option => "bar", :otheroption => "baz"}
18
+ # See the #map method for more details.
19
+ class MessageMapper
20
+ # used to set the method name used as a fallback for unmatched messages.
21
+ # The default fallback is a method called "usage".
22
+ attr_writer :fallback
23
+
24
+ # parent:: parent class which will receive mapped messages
25
+ #
26
+ # create a new MessageMapper with parent class +parent+. This class will
27
+ # receive messages from the mapper via the handle() method.
28
+ def initialize(parent)
29
+ @parent = parent
30
+ @templates = Array.new
31
+ @fallback = 'usage'
32
+ end
33
+
34
+ # args:: hash format containing arguments for this template
35
+ #
36
+ # map a template string to an action. example:
37
+ # map 'myplugin :parameter1 :parameter2'
38
+ # (other examples follow). By default, maps a matched string to an
39
+ # action with the name of the first word in the template. The action is
40
+ # a method which takes a message and a parameter hash for arguments.
41
+ #
42
+ # The :action => 'method_name' option can be used to override this
43
+ # default behaviour. Example:
44
+ # map 'myplugin :parameter1 :parameter2', :action => 'mymethod'
45
+ #
46
+ # By default whether a handler is fired depends on an auth check. The
47
+ # first component of the string is used for the auth check, unless
48
+ # overridden via the :auth => 'auth_name' option.
49
+ #
50
+ # Static parameters (not prefixed with ':' or '*') must match the
51
+ # respective component of the message exactly. Example:
52
+ # map 'myplugin :foo is :bar'
53
+ # will only match messages of the form "myplugin something is
54
+ # somethingelse"
55
+ #
56
+ # Dynamic parameters can be specified by a colon ':' to match a single
57
+ # component (whitespace seperated), or a * to such up all following
58
+ # parameters into an array. Example:
59
+ # map 'myplugin :parameter1 *rest'
60
+ #
61
+ # You can provide defaults for dynamic components using the :defaults
62
+ # parameter. If a component has a default, then it is optional. e.g:
63
+ # map 'myplugin :foo :bar', :defaults => {:bar => 'qux'}
64
+ # would match 'myplugin param param2' and also 'myplugin param'. In the
65
+ # latter case, :bar would be provided from the default.
66
+ #
67
+ # Components can be validated before being allowed to match, for
68
+ # example if you need a component to be a number:
69
+ # map 'myplugin :param', :requirements => {:param => /^\d+$/}
70
+ # will only match strings of the form 'myplugin 1234' or some other
71
+ # number.
72
+ #
73
+ # Templates can be set not to match public or private messages using the
74
+ # :public or :private boolean options.
75
+ #
76
+ # Further examples:
77
+ #
78
+ # # match 'karmastats' and call my stats() method
79
+ # map 'karmastats', :action => 'stats'
80
+ # # match 'karma' with an optional 'key' and call my karma() method
81
+ # map 'karma :key', :defaults => {:key => false}
82
+ # # match 'karma for something' and call my karma() method
83
+ # map 'karma for :key'
84
+ #
85
+ # # two matches, one for public messages in a channel, one for
86
+ # # private messages which therefore require a channel argument
87
+ # map 'urls search :channel :limit :string', :action => 'search',
88
+ # :defaults => {:limit => 4},
89
+ # :requirements => {:limit => /^\d+$/},
90
+ # :public => false
91
+ # plugin.map 'urls search :limit :string', :action => 'search',
92
+ # :defaults => {:limit => 4},
93
+ # :requirements => {:limit => /^\d+$/},
94
+ # :private => false
95
+ #
96
+ def map(*args)
97
+ @templates << Template.new(*args)
98
+ end
99
+
100
+ def each
101
+ @templates.each {|tmpl| yield tmpl}
102
+ end
103
+ def last
104
+ @templates.last
105
+ end
106
+
107
+ # m:: derived from BasicUserMessage
108
+ #
109
+ # examine the message +m+, comparing it with each map()'d template to
110
+ # find and process a match. Templates are examined in the order they
111
+ # were map()'d - first match wins.
112
+ #
113
+ # returns +true+ if a match is found including fallbacks, +false+
114
+ # otherwise.
115
+ def handle(m)
116
+ return false if @templates.empty?
117
+ failures = []
118
+ @templates.each do |tmpl|
119
+ options, failure = tmpl.recognize(m)
120
+ if options.nil?
121
+ failures << [tmpl, failure]
122
+ else
123
+ action = tmpl.options[:action] ? tmpl.options[:action] : tmpl.items[0]
124
+ next unless @parent.respond_to?(action)
125
+ auth = tmpl.options[:auth] ? tmpl.options[:auth] : tmpl.items[0]
126
+ debug "checking auth for #{auth}"
127
+ if m.bot.auth.allow?(auth, m.source, m.replyto)
128
+ debug "template match found and auth'd: #{action.inspect} #{options.inspect}"
129
+ @parent.send(action, m, options)
130
+ return true
131
+ end
132
+ debug "auth failed for #{auth}"
133
+ # if it's just an auth failure but otherwise the match is good,
134
+ # don't try any more handlers
135
+ return false
136
+ end
137
+ end
138
+ debug failures.inspect
139
+ debug "no handler found, trying fallback"
140
+ if @fallback != nil && @parent.respond_to?(@fallback)
141
+ if m.bot.auth.allow?(@fallback, m.source, m.replyto)
142
+ @parent.send(@fallback, m, {})
143
+ return true
144
+ end
145
+ end
146
+ return false
147
+ end
148
+
149
+ end
150
+
151
+ class Template
152
+ attr_reader :defaults # The defaults hash
153
+ attr_reader :options # The options hash
154
+ attr_reader :items
155
+ def initialize(template, hash={})
156
+ raise ArgumentError, "Second argument must be a hash!" unless hash.kind_of?(Hash)
157
+ @defaults = hash[:defaults].kind_of?(Hash) ? hash.delete(:defaults) : {}
158
+ @requirements = hash[:requirements].kind_of?(Hash) ? hash.delete(:requirements) : {}
159
+ self.items = template
160
+ @options = hash
161
+ end
162
+ def items=(str)
163
+ items = str.split(/\s+/).collect {|c| (/^(:|\*)(\w+)$/ =~ c) ? (($1 == ':' ) ? $2.intern : "*#{$2}".intern) : c} if str.kind_of?(String) # split and convert ':xyz' to symbols
164
+ items.shift if items.first == ""
165
+ items.pop if items.last == ""
166
+ @items = items
167
+
168
+ if @items.first.kind_of? Symbol
169
+ raise ArgumentError, "Illegal template -- first component cannot be dynamic\n #{str.inspect}"
170
+ end
171
+
172
+ # Verify uniqueness of each component.
173
+ @items.inject({}) do |seen, item|
174
+ if item.kind_of? Symbol
175
+ raise ArgumentError, "Illegal template -- duplicate item #{item}\n #{str.inspect}" if seen.key? item
176
+ seen[item] = true
177
+ end
178
+ seen
179
+ end
180
+ end
181
+
182
+ # Recognize the provided string components, returning a hash of
183
+ # recognized values, or [nil, reason] if the string isn't recognized.
184
+ def recognize(m)
185
+ components = m.message.split(/\s+/)
186
+ options = {}
187
+
188
+ @items.each do |item|
189
+ if /^\*/ =~ item.to_s
190
+ if components.empty?
191
+ value = @defaults.has_key?(item) ? @defaults[item].clone : []
192
+ else
193
+ value = components.clone
194
+ end
195
+ components = []
196
+ def value.to_s() self.join(' ') end
197
+ options[item.to_s.sub(/^\*/,"").intern] = value
198
+ elsif item.kind_of? Symbol
199
+ value = components.shift || @defaults[item]
200
+ if passes_requirements?(item, value)
201
+ options[item] = value
202
+ else
203
+ if @defaults.has_key?(item)
204
+ options[item] = @defaults[item]
205
+ # push the test-failed component back on the stack
206
+ components.unshift value
207
+ else
208
+ return nil, requirements_for(item)
209
+ end
210
+ end
211
+ else
212
+ return nil, "No value available for component #{item.inspect}" if components.empty?
213
+ component = components.shift
214
+ return nil, "Value for component #{item.inspect} doesn't match #{component}" if component != item
215
+ end
216
+ end
217
+
218
+ return nil, "Unused components were left: #{components.join '/'}" unless components.empty?
219
+
220
+ return nil, "template is not configured for private messages" if @options.has_key?(:private) && !@options[:private] && m.private?
221
+ return nil, "template is not configured for public messages" if @options.has_key?(:public) && !@options[:public] && !m.private?
222
+
223
+ options.delete_if {|k, v| v.nil?} # Remove nil values.
224
+ return options, nil
225
+ end
226
+
227
+ def inspect
228
+ when_str = @requirements.empty? ? "" : " when #{@requirements.inspect}"
229
+ default_str = @defaults.empty? ? "" : " || #{@defaults.inspect}"
230
+ "<#{self.class.to_s} #{@items.collect{|c| c.kind_of?(String) ? c : c.inspect}.join(' ').inspect}#{default_str}#{when_str}>"
231
+ end
232
+
233
+ # Verify that the given value passes this template's requirements
234
+ def passes_requirements?(name, value)
235
+ return @defaults.key?(name) && @defaults[name].nil? if value.nil? # Make sure it's there if it should be
236
+
237
+ case @requirements[name]
238
+ when nil then true
239
+ when Regexp then
240
+ value = value.to_s
241
+ match = @requirements[name].match(value)
242
+ match && match[0].length == value.length
243
+ else
244
+ @requirements[name] == value.to_s
245
+ end
246
+ end
247
+
248
+ def requirements_for(name)
249
+ name = name.to_s.sub(/^\*/,"").intern if (/^\*/ =~ name.inspect)
250
+ presence = (@defaults.key?(name) && @defaults[name].nil?)
251
+ requirement = case @requirements[name]
252
+ when nil then nil
253
+ when Regexp then "match #{@requirements[name].inspect}"
254
+ else "be equal to #{@requirements[name].inspect}"
255
+ end
256
+ if presence && requirement then "#{name} must be present and #{requirement}"
257
+ elsif presence || requirement then "#{name} must #{requirement || 'be present'}"
258
+ else "#{name} has no requirements"
259
+ end
260
+ end
261
+ end
262
+ end