butler 1.8.0 → 1.8.1

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.
Files changed (57) hide show
  1. data/CHANGELOG +40 -0
  2. data/README +9 -9
  3. data/Rakefile +15 -71
  4. data/bin/botcontrol +151 -146
  5. data/data/butler/dialogs/botcontrol.rb +8 -3
  6. data/data/butler/dialogs/create.rb +18 -18
  7. data/data/butler/dialogs/create_config.rb +8 -0
  8. data/data/butler/dialogs/en/create_config.yaml +2 -0
  9. data/data/butler/dialogs/en/list.yaml +2 -1
  10. data/data/butler/dialogs/info.rb +2 -2
  11. data/data/butler/dialogs/list.rb +13 -8
  12. data/data/butler/plugins/games/countdown.rb +41 -0
  13. data/data/butler/plugins/games/roll.rb +59 -0
  14. data/lib/access.rb +6 -107
  15. data/lib/access/admin.rb +3 -0
  16. data/lib/access/role.rb +37 -2
  17. data/lib/access/savable.rb +5 -0
  18. data/lib/access/user.rb +21 -2
  19. data/lib/access/yamlbase.rb +4 -0
  20. data/lib/butler.rb +4 -4
  21. data/lib/butler/bot.rb +13 -13
  22. data/lib/butler/irc/client.rb +10 -2
  23. data/lib/butler/irc/parser.rb +7 -2
  24. data/lib/butler/irc/parser/commands.rb +24 -7
  25. data/lib/butler/irc/parser/generic.rb +27 -315
  26. data/lib/butler/irc/parser/rfc2812.rb +328 -0
  27. data/lib/butler/irc/socket.rb +1 -1
  28. data/lib/butler/irc/user.rb +13 -0
  29. data/lib/butler/plugin.rb +1 -1
  30. data/lib/butler/plugin/configproxy.rb +4 -4
  31. data/lib/butler/plugins.rb +1 -1
  32. data/lib/butler/version.rb +1 -1
  33. data/lib/configuration.rb +22 -71
  34. data/lib/dialogline.rb +12 -0
  35. data/lib/event.rb +5 -2
  36. data/lib/installer.rb +52 -24
  37. data/lib/iterator.rb +17 -7
  38. data/lib/log.rb +32 -5
  39. data/lib/log/comfort.rb +33 -16
  40. data/lib/log/entry.rb +25 -5
  41. data/lib/log/fakeio.rb +1 -0
  42. data/lib/log/splitter.rb +6 -2
  43. data/lib/ostructfixed.rb +5 -0
  44. data/lib/ruby/exception/detailed.rb +3 -3
  45. data/lib/scheduler.rb +19 -4
  46. data/lib/scriptfile.rb +9 -2
  47. data/lib/string.rb +176 -0
  48. data/lib/string/ascii.rb +31 -0
  49. data/lib/string/mbencoded.rb +79 -0
  50. data/lib/string/sbencoded.rb +77 -0
  51. data/lib/string/utf8.rb +157 -0
  52. data/lib/templater.rb +68 -10
  53. data/lib/w3validator.rb +86 -0
  54. data/test/irc/serverlistings/test_rpl_hiddenhost.txt +60 -0
  55. data/test/test_access.rb +101 -0
  56. data/test/test_configuration.rb +63 -0
  57. metadata +19 -2
@@ -0,0 +1,328 @@
1
+ #--
2
+ # Copyright 2007 by Stefan Rusterholz.
3
+ # All rights reserved.
4
+ # See LICENSE.txt for permissions.
5
+ #++
6
+
7
+
8
+
9
+ # A list with RFC2812 compatible IRC-Commands and it's parsing instructions
10
+ # Currently many of them are still in generic.rb
11
+
12
+
13
+
14
+ # --- Text based ----------------------------------------
15
+ add("error", :ERROR) # ERROR :<error-message>
16
+ add("invite", :INVITE, /^(\S*) :(.*)/, [:invited, :channel])
17
+ add("join", :JOIN, /^:(.*)/, [:channel]) { |message, parser|
18
+ if message.from && message.channel then
19
+ message.from.add_channel(message.channel, :join)
20
+ message.channel.add_user(message.from, :join)
21
+ end
22
+ }
23
+ add("kick", :KICK, /^(\S*) (\S*) :(.*)/, [:channel, :for, :text]) { |message, parser|
24
+ parser.leave_channel(message, :kick, :kicked)
25
+ }
26
+ add("kill", :KILL, /^(\S*) (\S*) (.*)/, [:channel, :for, :text]) { |message, parser|
27
+ if message.for then
28
+ message.for.kill
29
+ parser.channels.delete_user(message.for, :kill)
30
+ end
31
+ }
32
+ # FIXME, really ':?' or just plain ':'?
33
+ add("mode", :MODE, /^(\S*) :?(.*)/, [:for, :arguments]) { |message, parser|
34
+ modifiers = message[:arguments].split(" ")
35
+ modes = modifiers.shift.split("")
36
+ flags = {"o" => User::Flags::OP, "v" => User::Flags::VOICE, "u" => User::Flags::UOP}
37
+ message.create_member(:modes, [])
38
+ argumentIndex = 0
39
+ direction = "+"
40
+
41
+ modes.each { |mode|
42
+ # MODEs direction
43
+ if (["+", "-"].include?(mode)) then
44
+ direction = mode
45
+ # MODEs taking an argument
46
+ elsif (["k", "o", "v", "u"].include?(mode) || (mode == "l" && direction == "+")) then
47
+ if ["o", "v", "u"].include?(mode) then
48
+ if (direction == "+") then
49
+ parser.users[modifiers[argumentIndex]].add_flags(message.channel, flags[mode]) #adding flags to user
50
+ else
51
+ parser.users[modifiers[argumentIndex]].delete_flags(message.channel, flags[mode]) #removing flags from user
52
+ end
53
+ end
54
+ message.modes << [direction, mode, modifiers[argumentIndex]]
55
+ argumentIndex = argumentIndex+1
56
+ # MODEs without argument
57
+ else
58
+ message.modes << [direction, mode, nil]
59
+ end
60
+ }
61
+ }
62
+ add("nick", :NICK, /^:(.*)/, [:nick]) { |message, parser|
63
+ message.create_member(:old_nick, message.from.nick)
64
+ message.from.nick = message.nick if message.from
65
+ }
66
+ add("notice", :NOTICE, /(\S+) :(.*)/, [:for, :text]) { |message, parser|
67
+ if message.channel then
68
+ message.create_member(:realm, :public)
69
+ message.create_method(:private?) { false }
70
+ message.create_method(:public?) { true }
71
+ else
72
+ message.create_member(:realm, :private)
73
+ message.create_method(:private?) { true }
74
+ message.create_method(:public?) { false }
75
+ end
76
+ }
77
+ add("part", :PART, /^([^\x00\x07\x10\x0D\x20,:]+)(?: :(.*))?/, [:channel, :reason]) { |message, parser|
78
+ parser.leave_channel(message, :part, :parted)
79
+ }
80
+ add("ping", :PING, /:(.*)/, [:pong])
81
+ add("pong", :PONG)
82
+ add("privmsg", :PRIVMSG, /(\S+) :(.*)/, [:for, :text]) { |message, parser|
83
+ if message.channel then
84
+ message.create_member(:realm, :public)
85
+ message.create_method(:private?) { false }
86
+ message.create_method(:public?) { true }
87
+ else
88
+ message.create_member(:realm, :private)
89
+ message.create_method(:private?) { true }
90
+ message.create_method(:public?) { false }
91
+ end
92
+ }
93
+ add("quit", :QUIT, /(.*)/, [:text]) { |message, parser|
94
+ if message.from then
95
+ message.from.quit
96
+ parser.channels.delete_user(message.from, :quit)
97
+ parser.users.delete(message.from, :quit)
98
+ end
99
+ }
100
+ add("topic", :TOPIC, /(\S+) :(.*)/, [:channel, :text])
101
+
102
+
103
+
104
+ # --- 0** Codes ----------------------------------------
105
+ add("001", :RPL_WELCOME)
106
+ add("002", :RPL_YOURHOST)
107
+ add("003", :RPL_CREATED)
108
+ add("004", :RPL_MYINFO)
109
+ add("005", :RPL_ISUPPORT)
110
+
111
+
112
+
113
+
114
+ # --- 2** Codes ----------------------------------------
115
+ add("200", :RPL_TRACELINK)
116
+ add("201", :RPL_TRACECONNECTING)
117
+ add("202", :RPL_TRACEHANDSHAKE)
118
+ add("203", :RPL_TRACEUNKNOWN)
119
+ add("204", :RPL_TRACEOPERATOR)
120
+ add("205", :RPL_TRACEUSER)
121
+ add("206", :RPL_TRACESERVER)
122
+ add("207", :RPL_TRACESERVICE)
123
+ add("208", :RPL_TRACENEWTYPE)
124
+ add("209", :RPL_TRACECLASS)
125
+ add("210", :RPL_TRACERECONNECT)
126
+ add("211", :RPL_STATSLINKINFO)
127
+ add("212", :RPL_STATSCOMMANDS)
128
+ add("213", :RPL_STATSCLINE)
129
+ add("214", :RPL_STATSNLINE)
130
+ add("215", :RPL_STATSILINE)
131
+ add("216", :RPL_STATSKLINE)
132
+ add("217", :RPL_STATSQLINE)
133
+ add("218", :RPL_STATSYLINE)
134
+ add("219", :RPL_ENDOFSTATS)
135
+
136
+ add("221", :RPL_UMODEIS)
137
+
138
+ add("231", :RPL_SERVICEINFO)
139
+ add("232", :RPL_ENDOFSERVICES)
140
+ add("233", :RPL_SERVICE)
141
+ add("234", :RPL_SERVLIST)
142
+ add("235", :RPL_SERVLISTEND)
143
+
144
+ add("240", :RPL_STATSVLINE)
145
+ add("241", :RPL_STATSLLINE)
146
+ add("242", :RPL_STATSUPTIME)
147
+ add("243", :RPL_STATSOLINE)
148
+ add("244", :RPL_STATSHLINE)
149
+ add("245", :RPL_STATSSLINE) # RFC 2812 seems to be erroneous, it assigns 244 double
150
+ add("246", :RPL_STATSPING)
151
+ add("247", :RPL_STATSBLINE)
152
+ add("250", :RPL_STATSCONN)
153
+ add("251", :RPL_LUSERCLIENT)
154
+ add("252", :RPL_LUSEROP)
155
+ add("253", :RPL_LUSERUNKNOWN)
156
+ add("254", :RPL_LUSERCHANNELS)
157
+ add("255", :RPL_LUSERME)
158
+ add("256", :RPL_ADMINME)
159
+ add("257", :RPL_ADMINLOC1)
160
+ add("258", :RPL_ADMINLOC2)
161
+ add("259", :RPL_ADMINEMAIL)
162
+
163
+ add("261", :RPL_TRACELOG)
164
+ add("262", :RPL_TRACEEND)
165
+
166
+ add("263", :RPL_TRYAGAIN)
167
+
168
+
169
+
170
+ # --- 3** Codes ----------------------------------------
171
+ add("300", :RPL_NONE)
172
+ add("301", :RPL_AWAY)
173
+ add("302", :RPL_USERHOST)
174
+ add("303", :RPL_ISON)
175
+ add("305", :RPL_UNAWAY)
176
+ add("306", :RPL_NOWAWAY)
177
+ add("311", :RPL_WHOISUSER, /^(\S+) (\S+) (\S+) (\S+) \* :(.*)/, [:for, :nick, :user, :host, :real])
178
+ add("312", :RPL_WHOISSERVER)
179
+ add("313", :RPL_WHOISOPERATOR)
180
+ add("314", :RPL_WHOWASUSER)
181
+ add("315", :RPL_ENDOFWHO)
182
+ add("316", :RPL_WHOISCHANOP)
183
+ add("317", :RPL_WHOISIDLE, /^(\S+) (\S+) ([^:]+) :(.*)/, [:for, :nick, :values, :descriptions]) { |message, parser|
184
+ values = message[:values].split(" ");
185
+ descriptions = message[:descriptions].split(", ").map { |desc| desc.gsub(" ", "_").to_sym };
186
+ message.delete(:values)
187
+ message.delete(:descriptions)
188
+ 0.upto([values.length, descriptions.length].min-1) { |index|
189
+ message[descriptions[index]] = values[index]
190
+ }
191
+ }
192
+ add("318", :RPL_ENDOFWHOIS)
193
+ add("319", :RPL_WHOISCHANNELS, /^(\S+) (\S+) :(.*)/, [:for, :nick, :channels]) { |message, parser|
194
+ # FIXME
195
+ message.alter_member(:channels, message.channels.split(" ").map { |channel| parser.channels.create(channel) })
196
+ }
197
+ add("321", :RPL_LISTSTART)
198
+ add("322", :RPL_LIST, /^(\S+) (\S+) (\d+) :(.*)/, [:for, :channelname, :usercount, :topic])
199
+ add("323", :RPL_LISTEND)
200
+ add("324", :RPL_CHANNELMODEIS)
201
+ add("325", :RPL_UNIQOPIS)
202
+ add("331", :RPL_NOTOPIC)
203
+ # :irc.server.net 332 YourNickname #channel :Topic
204
+ add("332", :RPL_TOPIC, /^(\S+) (\S+) :(.*)/, [:for, :channel, :topic]) { |message, parser|
205
+ message.channel.topic.text = message[:topic]
206
+ }
207
+ add("341", :RPL_INVITING)
208
+ add("342", :RPL_SUMMONING)
209
+ add("346", :RPL_INVITELIST)
210
+ add("347", :RPL_ENDOFINVITELIST)
211
+ add("348", :RPL_EXCEPTLIST)
212
+ add("349", :RPL_ENDOFEXCEPTLIST)
213
+ add("351", :RPL_VERSION)
214
+ # :irc.server.net 352 YourNickname <channel> <user> <host> <server> <nick> ( "H" / "G" > ["*"] [ ( "@" / "+" ) ] :<hopcount> <real name>
215
+ add("352", :RPL_WHOREPLY,
216
+ /(\S+) (\S+) (\S+) (\S+) (\S+) (\S+) ([HG])[*%]{0,2}([@+-]{0,3}) :(\d+) (.*)/,
217
+ [:for, :channel, :user, :host, :server, :nick, :status, :flags, :hopcount, :real]) { |message, parser|
218
+ #"for", "channel", "user", "host", "server", "nick", "status", "flags", "hopcount", "real"
219
+ user = parser.users.create(message[:nick])
220
+ user.update(message[:user], message[:host], message[:real])
221
+ user.away = message[:status] == "G"
222
+ user.add_channel(message.channel, :joined)
223
+ message.channel.add_user(user, :joined)
224
+ user.add_flags(message.channel, message[:flags].to_flags)
225
+ }
226
+ add("353", :RPL_NAMEREPLY, /(\S+) [=@] (\S+) :(.*)/, [:for, :channel, :users]) { |message, parser|
227
+ users = message[:users]
228
+ message.alter_member(:users, [])
229
+ users.split(/ /).each { |nick|
230
+ user = parser.users.create(nick)
231
+ message[:users] << user
232
+ user.add_channel(message.channel, :joined)
233
+ message.channel.add_user(user, :joined)
234
+ }
235
+ }
236
+ add("361", :RPL_KILLDONE)
237
+ add("362", :RPL_CLOSING)
238
+ add("363", :RPL_CLOSEEND)
239
+ add("364", :RPL_LINKS)
240
+ add("365", :RPL_ENDOFLINKS)
241
+ add("366", :RPL_ENDOFNAMES)
242
+ add("367", :RPL_BANLIST,
243
+ /(\S+) (\S+) (\S+) (\S+) (\d+)/,
244
+ [:for, :channel, :banmask, :banned_by, :bantime]
245
+ ) { |message, parser| #367 nickname #channel nick!user@host nickname 1140125288
246
+ message[:bantime] = Time.at(message[:bantime].to_i)
247
+ message[:banmask] = Hostmask.new(message[:banmask])
248
+ }
249
+ add("369", :RPL_ENDOFWHOWAS)
250
+
251
+ add("368", :RPL_ENDOFBANLIST)
252
+ add("371", :RPL_INFO)
253
+ add("372", :RPL_MOTD)
254
+ add("373", :RPL_INFOSTART)
255
+ add("374", :RPL_ENDOFINFO)
256
+ add("375", :RPL_MOTDSTART)
257
+ add("376", :RPL_ENDOFMOTD)
258
+ add("381", :RPL_YOUREOPER)
259
+ add("382", :RPL_REHASHING)
260
+ add("383", :RPL_YOURESERVICE)
261
+ add("384", :RPL_MYPORTIS)
262
+ add("391", :RPL_TIME)
263
+ add("392", :RPL_USERSSTART)
264
+ add("393", :RPL_USERS)
265
+ add("394", :RPL_ENDOFUSERS)
266
+ add("395", :RPL_NOUSERS)
267
+
268
+
269
+
270
+ # --- 4** Codes ----------------------------------------
271
+ add("401", :ERR_NOSUCHNICK)
272
+ add("402", :ERR_NOSUCHSERVER)
273
+ add("403", :ERR_NOSUCHCHANNEL)
274
+ add("404", :ERR_CANNOTSENDTOCHAN)
275
+ add("405", :ERR_TOOMANYCHANNELS)
276
+ add("406", :ERR_WASNOSUCHNICK)
277
+ add("407", :ERR_TOOMANYTARGETS)
278
+ add("408", :ERR_NOSUCHSERVICE)
279
+ add("409", :ERR_NOORIGIN)
280
+ add("411", :ERR_NORECIPIENT)
281
+ add("412", :ERR_NOTEXTTOSEND)
282
+ add("413", :ERR_NOTOPLEVEL)
283
+ add("414", :ERR_WILDTOPLEVEL)
284
+ add("415", :ERR_BADMASK)
285
+ add("421", :ERR_UNKNOWNCOMMAND)
286
+ add("422", :ERR_NOMOTD)
287
+ add("423", :ERR_NOADMININFO)
288
+ add("424", :ERR_FILEERROR)
289
+ add("431", :ERR_NONICKNAMEGIVEN)
290
+ add("432", :ERR_ERRONEUSNICKNAME)
291
+ add("433", :ERR_NICKNAMEINUSE)
292
+ add("436", :ERR_NICKCOLLISION)
293
+ add("437", :ERR_UNAVAILRESOURCE)
294
+ add("441", :ERR_USERNOTINCHANNEL)
295
+ add("442", :ERR_NOTONCHANNEL)
296
+ add("443", :ERR_USERONCHANNEL)
297
+ add("444", :ERR_NOLOGIN)
298
+ add("445", :ERR_SUMMONDISABLED)
299
+ add("446", :ERR_USERSDISABLED)
300
+ add("451", :ERR_NOTREGISTERED)
301
+ add("461", :ERR_NEEDMOREPARAMS)
302
+ add("462", :ERR_ALREADYREGISTRED)
303
+ add("463", :ERR_NOPERMFORHOST)
304
+ add("464", :ERR_PASSWDMISMATCH)
305
+ add("465", :ERR_YOUREBANNEDCREEP)
306
+ add("466", :ERR_YOUWILLBEBANNED)
307
+ add("467", :ERR_KEYSET)
308
+ add("471", :ERR_CHANNELISFULL)
309
+ add("472", :ERR_UNKNOWNMODE)
310
+ add("473", :ERR_INVITEONLYCHAN)
311
+ add("474", :ERR_BANNEDFROMCHAN)
312
+ add("475", :ERR_BADCHANNELKEY)
313
+ add("476", :ERR_BADCHANMASK)
314
+ add("477", :ERR_NOCHANMODES)
315
+ add("478", :ERR_BANLISTFULL)
316
+ add("481", :ERR_NOPRIVILEGES)
317
+ add("482", :ERR_CHANOPRIVSNEEDED)
318
+ add("483", :ERR_CANTKILLSERVER)
319
+ add("484", :ERR_RESTRICTED)
320
+ add("485", :ERR_UNIQOPPRIVSNEEDED)
321
+ add("491", :ERR_NOOPERHOST)
322
+ add("492", :ERR_NOSERVICEHOST)
323
+
324
+
325
+
326
+ # --- 5** Codes ----------------------------------------
327
+ add("501", :ERR_UMODEUNKNOWNFLAG)
328
+ add("502", :ERR_USERSDONTMATCH)
@@ -87,7 +87,7 @@ class Butler
87
87
  #
88
88
  def initialize(server, options={})
89
89
  options = OptionsDefault.merge(options)
90
- @log_device = options.delete(:log)
90
+ @logger = options.delete(:log)
91
91
  @server = server # options.delete(:server)
92
92
  @port = options.delete(:port)
93
93
  @eol = options.delete(:eol).dup.freeze
@@ -87,6 +87,19 @@ class Butler
87
87
  @real = real.freeze if real
88
88
  self
89
89
  end
90
+
91
+ # :nodoc:
92
+ # DO NOT USE THIS METHOD
93
+ # This method is intended to be used by IRC::Parser or IRC::Client
94
+ # in case the server alters parts about 'myself'
95
+ # examples: some ircd's change the 'user' part (prefix it), some
96
+ # ircd's allow hiding the host, ...
97
+ def force_update(user=nil, host=nil, real=nil)
98
+ @user = user.freeze if user
99
+ @host = host.freeze if host
100
+ @real = real.freeze if real
101
+ self
102
+ end
90
103
 
91
104
  # set users nickname (should only be used by Butler::IRC::Parser)
92
105
  def nick=(nick) #:nodoc:
@@ -41,7 +41,7 @@ class Butler
41
41
  @name = File.basename(base).freeze
42
42
  @commands = []
43
43
  @listener = []
44
- @config = ConfigProxy.new(@butler.config, "plugin.#{name}")
44
+ @config = ConfigProxy.new(@butler.config, "plugin/#{name}")
45
45
 
46
46
  if File.directory?(path) then
47
47
  raise "Not supported yet" # FIXME
@@ -15,20 +15,20 @@ class Butler
15
15
  end
16
16
 
17
17
  def []=(key, value)
18
- @config[key.empty? ? @base : "#{@base}.#{key}"] = value
18
+ @config[key.empty? ? @base : "#{@base}/#{key}"] = value
19
19
  end
20
20
 
21
21
  def [](key, *args)
22
- @config[(key.empty? ? @base : "#{@base}.#{key}"), *args]
22
+ @config[(key.empty? ? @base : "#{@base}/#{key}"), *args]
23
23
  end
24
24
 
25
25
  def exist?(key)
26
- @config.exist?(key.empty? ? @base : "#{@base}.#{key}")
26
+ @config.exist?(key.empty? ? @base : "#{@base}/#{key}")
27
27
  end
28
28
  alias has_key? exist?
29
29
 
30
30
  def delete(key)
31
- @config.delete(key.empty? ? @base : "#{@base}.#{key}")
31
+ @config.delete(key.empty? ? @base : "#{@base}/#{key}")
32
32
  end
33
33
  end
34
34
  end
@@ -114,7 +114,7 @@ class Butler
114
114
  constant = "%s_%08X" % [name, rand(0xffffffff)]
115
115
  end while Butler::Plugins.const_defined?(constant)
116
116
  plugin = Butler::Plugins.const_set(constant, Class.new(Plugin))
117
- plugin.log_device = @butler.log_device
117
+ plugin.logger = @butler.logger
118
118
  begin
119
119
  plugin.load_plugin(@butler, base, path)
120
120
  plugin.class_eval(File.read(codefile), codefile)
@@ -10,7 +10,7 @@ class Butler #:nodoc:
10
10
  module VERSION #:nodoc:
11
11
  MAJOR = 1
12
12
  MINOR = 8
13
- TINY = 0
13
+ TINY = 1
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY].join('.')
16
16
  end
@@ -13,10 +13,24 @@ require 'yaml'
13
13
 
14
14
  class String
15
15
  def config_key # FIXME %2e instead of . is just ugly...
16
- gsub(/\./, '%2e')
16
+ gsub(/\//, '%2f')
17
17
  end
18
18
  end
19
19
 
20
+ # == Indexing
21
+ # Author: Stefan Rusterholz
22
+ # Contact: apeiros@gmx.net>
23
+ # Version: 1.0.0
24
+ # Date: 2007-10-12
25
+ #
26
+ # == About
27
+ # Map configuration keys/data to files
28
+ #
29
+ # == Synopsis
30
+ # config = Configuration.new("my/config")
31
+ # config["some/key"] = "Some value"
32
+ # config["some/key"] = config["some/key"].upcase # can't use upcase!
33
+ #
20
34
  class Configuration
21
35
  module VERSION
22
36
  MAJOR = 0
@@ -28,7 +42,7 @@ class Configuration
28
42
  attr_reader :key
29
43
  def initialize(key)
30
44
  @key = key
31
- super("Keys must not be empty nor contain '.'.")
45
+ super("Keys must not be empty")
32
46
  end
33
47
  end
34
48
 
@@ -105,25 +119,26 @@ class Configuration
105
119
  def setup(dirs)
106
120
  FileUtils.mkdir_p(dirs.map { |dir| "#@base/#{dir}" })
107
121
  end
108
-
122
+
109
123
  private
110
124
  def key_exist?(file, key)
111
125
  File.exist?(file) && (!key || load(file).has_key?(key))
112
126
  end
113
127
 
114
128
  def split(key)
115
- keys = Array === key ? key.map { |k| String(k) } : key.split(/\./)
129
+ keys = Array === key ? key.map { |k| String(k) } : key.split(/\//)
116
130
  file = @base.dup
117
- raise InvalidKey.new(key) if (keys.empty? || keys.join.include?("."))
131
+ raise InvalidKey.new(key) if keys.empty?
118
132
  file << "/#{encode_filename(keys.shift)}" while (File.directory?(file) && keys.first)
119
133
  file << ".yaml"
120
134
  return [file, keys.empty? ? nil : keys.join(".")]
121
135
  end
122
136
 
123
137
  def encode_filename(name)
124
- name.gsub(/[\x00-\x1f%\x7f]/) { |m| "%%%02x"%m[0] } # {}()\[\]~
138
+ # FIXME: {}()\[\]~ what about those?
139
+ name.gsub(/[\x00-\x1f%\x2f\x7f]/) { |m| "%%%02x"%m[0] }.sub(/^\x2e/, '%2e')
125
140
  end
126
-
141
+
127
142
  def decode_filename(name)
128
143
  name.gsub(/%[a-f\d]{2}/) { |m| m[1,2].to_i(16).chr }
129
144
  end
@@ -157,67 +172,3 @@ class Configuration
157
172
  end
158
173
  end
159
174
  end
160
-
161
- if $0 == __FILE__ then
162
- require 'test/unit'
163
- class TestConfiguration < Test::Unit::TestCase
164
- def setup
165
- @conf = Configuration.new("./test")
166
- end
167
-
168
- def test_setup
169
- assert(@conf)
170
- assert(@conf.base)
171
- assert(File.directory?(@conf.base))
172
- end
173
-
174
- def test_accessor
175
- assert(@conf["file1"] = "bar")
176
- assert_equal(@conf[[:file1]], "bar")
177
- assert_equal(@conf[["file1"]], "bar")
178
- assert_equal(@conf["file1"], "bar")
179
- assert(File.exist?("#{@conf.base}/file1.yaml"))
180
-
181
- assert(@conf["file2"] = "foo")
182
- assert_equal(@conf[:file2], "foo")
183
- assert_equal(@conf["file2"], "foo")
184
- assert(File.exist?("#{@conf.base}/file2.yaml"))
185
-
186
- assert(@conf[[:file3, :sub]] = "baz")
187
- assert_equal(@conf[[:file3, :sub]], "baz")
188
- assert_equal(@conf["file3", "sub"], "baz")
189
- assert(File.exist?("#{@conf.base}/file3.yaml"))
190
- assert(!File.exist?("#{@conf.base}/file3"))
191
-
192
- assert(@conf.exist?(:file3))
193
- assert(!@conf.exist?(:file))
194
- #assert_raise { @conf
195
- end
196
-
197
- def test_nesting
198
- end
199
-
200
- def xtest_common_usecase
201
- @conf.setup(%w(
202
- main
203
- plugins
204
- plugins/demo
205
- ))
206
- @conf.merge(
207
- "main.language" => "de",
208
- "main.channels" => %w(foo bar baz),
209
- "plugins.demo.value" => 42
210
- )
211
- @conf["plugins.demo.other"] = 24
212
- @conf["main.language"].replace("en")
213
- @conf.update
214
- file, key = @conf.send(:split, "main.language")
215
- File.open(file, "w") { |fh| fh.write({"language" => "en"}.to_yaml) }
216
- @conf.rehash
217
- end
218
-
219
- def teardown
220
- FileUtils.rm_r(@conf.base)
221
- end
222
- end
223
- end