]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - lib/rbot/registry.rb
registry: add in-memory implementation for tests
[user/henk/code/ruby/rbot.git] / lib / rbot / registry.rb
1 #-- vim:sw=2:et
2 #++
3 #
4 # :title: Registry: Persistent storage interface and factory
5
6 # This class provides persistent storage for plugins via a hash interface.
7 # The default mode is an object store, so you can store ruby objects and
8 # reference them with hash keys. This is because the default store/restore
9 # methods of the plugins' RegistryAccessor are calls to Marshal.dump and
10 # Marshal.restore,
11 # for example:
12 #   blah = Hash.new
13 #   blah[:foo] = "fum"
14 #   @registry[:blah] = blah
15 # then, even after the bot is shut down and disconnected, on the next run you
16 # can access the blah object as it was, with:
17 #   blah = @registry[:blah]
18 # The registry can of course be used to store simple strings, fixnums, etc as
19 # well, and should be useful to store or cache plugin data or dynamic plugin
20 # configuration.
21 #
22 # WARNING:
23 # in object store mode, don't make the mistake of treating it like a live
24 # object, e.g. (using the example above)
25 #   @registry[:blah][:foo] = "flump"
26 # will NOT modify the object in the registry - remember that Registry#[]
27 # returns a Marshal.restore'd object, the object you just modified in place
28 # will disappear. You would need to:
29 #   blah = @registry[:blah]
30 #   blah[:foo] = "flump"
31 #   @registry[:blah] = blah
32 #
33 # If you don't need to store objects, and strictly want a persistant hash of
34 # strings, you can override the store/restore methods to suit your needs, for
35 # example (in your plugin):
36 #   def initialize
37 #     class << @registry
38 #       def store(val)
39 #         val
40 #       end
41 #       def restore(val)
42 #         val
43 #       end
44 #     end
45 #   end
46 # Your plugins section of the registry is private, it has its own namespace
47 # (derived from the plugin's class name, so change it and lose your data).
48 # Calls to registry.each etc, will only iterate over your namespace.
49
50 module Irc
51 class Bot
52
53 class Registry
54
55   # Dynamically loads the specified registry type library.
56   def initialize(format=nil)
57     @libpath = File.join(File.dirname(__FILE__), 'registry')
58     @format = format
59     load File.join(@libpath, @format+'.rb') if format
60     # The get_impl method will return all implementations of the
61     # abstract accessor interface, since we only ever load one
62     # (the configured one) accessor implementation, we can just assume
63     # it to be the correct accessor to use.
64     accessors = AbstractAccessor.get_impl
65     if accessors.length > 1
66       warning 'multiple accessor implementations loaded!'
67     end
68     @accessor_class = accessors.first
69   end
70
71   # Returns a list of supported registry database formats.
72   def discover
73     Dir.glob(File.join(@libpath, '*.rb')).map do |name|
74       File.basename(name, File.extname(name))
75     end
76   end
77
78   # Creates a new Accessor object for the specified database filename.
79   def create(path, filename)
80     db = @accessor_class.new(File.join(path, 'registry_' + @format, filename.downcase))
81     db.optimize
82     db
83   end
84
85   # Helper method that will return a list of supported registry formats.
86   def self.formats
87     @@formats ||= Registry.new.discover
88   end
89
90   # Will detect tokyocabinet registry location: ~/.rbot/registry/*.tdb
91   #  and move it to its new location ~/.rbot/registry_tc/*.tdb
92   def migrate_registry_folder(path)
93     old_name = File.join(path, 'registry')
94     new_name = File.join(path, 'registry_tc')
95     if @format == 'tc' and File.exists?(old_name) and
96         not File.exists?(new_name) and
97         not Dir.glob(File.join(old_name, '*.tdb')).empty?
98       File.rename(old_name, new_name)
99     end
100   end
101
102   # Abstract database accessor (a hash-like interface).
103   class AbstractAccessor
104
105     attr_reader :filename
106
107     # lets the user define a recovery procedure in case the Marshal
108     # deserialization fails, it might be manually recover data.
109     # NOTE: weird legacy stuff, used by markov plugin (WTH?)
110     attr_accessor :recovery
111
112     def initialize(filename)
113       debug "init registry accessor of #{self.class} for: #{filename}"
114       @filename = filename
115       @name = File.basename filename
116       @registry = nil
117       @default = nil
118       @recovery = nil
119       @sub_registries = {}
120     end
121
122     def sub_registry(prefix)
123       path = File.join(@filename.gsub(/\.[^\/\.]+$/,''), prefix.to_s)
124       @sub_registries[path] ||= self.class.new(path)
125     end
126
127     # creates the registry / subregistry folders
128     def create_folders
129       debug 'create folders for: ' + @filename
130       dirs = File.dirname(@filename).split("/")
131       dirs.length.times { |i|
132         dir = dirs[0,i+1].join("/")+"/"
133         unless File.exist?(dir)
134           Dir.mkdir(dir)
135         end
136       }
137     end
138
139     # Will return true if the database file exists.
140     def dbexists?
141       File.exists? @filename
142     end
143
144     # convert value to string form for storing in the registry
145     # defaults to Marshal.dump(val) but you can override this in your module's
146     # registry object to use any method you like.
147     # For example, if you always just handle strings use:
148     #   def store(val)
149     #     val
150     #   end
151     def store(val)
152       Marshal.dump(val)
153     end
154
155     # restores object from string form, restore(store(val)) must return val.
156     # If you override store, you should override restore to reverse the
157     # action.
158     # For example, if you always just handle strings use:
159     #   def restore(val)
160     #     val
161     #   end
162     def restore(val)
163       begin
164         Marshal.restore(val)
165       rescue Exception => e
166         error _("failed to restore marshal data for #{val.inspect}, attempting recovery or fallback to default")
167         debug e
168         if defined? @recovery and @recovery
169           begin
170             return @recovery.call(val)
171           rescue Exception => ee
172             error _("marshal recovery failed, trying default")
173             debug ee
174           end
175         end
176         return default
177       end
178     end
179
180     # Returned instead of nil if key wasnt found.
181     def set_default (default)
182       @default = default
183     end
184
185     def default
186       @default && (@default.dup rescue @default)
187     end
188
189     # Opens the database (if not already open) for read/write access.
190     def registry
191       create_folders unless dbexists?
192     end
193
194     # Forces flush/sync the database on disk.
195     def flush
196       return unless @registry
197       # if not supported by the database, close/reopen:
198       close
199       registry
200     end
201
202     # Should optimize/vacuum the database. (if supported)
203     def optimize
204     end
205
206     # Closes the database.
207     def close
208       return unless @registry
209       @registry.close
210       @registry = nil
211     end
212
213     # lookup a key in the registry
214     def [](key)
215       if dbexists? and registry.has_key?(key.to_s)
216         return restore(registry[key.to_s])
217       else
218         return default
219       end
220     end
221
222     # set a key in the registry
223     def []=(key,value)
224       registry[key.to_s] = store(value)
225     end
226
227     # like Hash#each
228     def each(&block)
229       return nil unless dbexists?
230       registry.each do |key, value|
231         block.call(key, restore(value))
232       end
233     end
234
235     alias each_pair each
236
237     # like Hash#each_key
238     def each_key(&block)
239       self.each do |key|
240         block.call(key)
241       end
242     end
243
244     # like Hash#each_value
245     def each_value(&block)
246       self.each do |key, value|
247         block.call(value)
248       end
249     end
250
251     # just like Hash#has_key?
252     def has_key?(key)
253       return nil unless dbexists?
254       return registry.has_key?(key.to_s)
255     end
256
257     alias include? has_key?
258     alias member? has_key?
259     alias key? has_key?
260
261     # just like Hash#has_value?
262     def has_value?(value)
263       return nil unless dbexists?
264       return registry.has_value?(store(value))
265     end
266
267     # just like Hash#index?
268     def index(value)
269       self.each do |k,v|
270         return k if v == value
271       end
272       return nil
273     end
274
275     # delete a key from the registry
276     # returns the value in success, nil otherwise
277     def delete(key)
278       return default unless dbexists?
279       value = registry.delete(key.to_s)
280       if value
281         restore(value)
282       end
283     end
284
285     # returns a list of your keys
286     def keys
287       return [] unless dbexists?
288       return registry.keys
289     end
290
291     # Return an array of all associations [key, value] in your namespace
292     def to_a
293       return [] unless dbexists?
294       ret = Array.new
295       self.each {|key, value|
296         ret << [key, value]
297       }
298       return ret
299     end
300
301     # Return an hash of all associations {key => value} in your namespace
302     def to_hash
303       return {} unless dbexists?
304       ret = Hash.new
305       self.each {|key, value|
306         ret[key] = value
307       }
308       return ret
309     end
310
311     # empties the registry (restricted to your namespace)
312     def clear
313       return unless dbexists?
314       registry.clear
315     end
316     alias truncate clear
317
318     # returns an array of the values in your namespace of the registry
319     def values
320       return [] unless dbexists?
321       ret = Array.new
322       self.each {|k,v|
323         ret << v
324       }
325       return ret
326     end
327
328     # returns the number of keys in your registry namespace
329     def length
330       return 0 unless dbexists?
331       registry.length
332     end
333     alias size length
334
335     # Returns all classes from the namespace that implement this interface
336     def self.get_impl
337       ObjectSpace.each_object(Class).select { |klass| klass.ancestors[1] == self }
338     end
339   end
340
341 end # Registry
342
343 end # Bot
344 end # Irc
345