summaryrefslogtreecommitdiff
path: root/lib/rbot/registry.rb
diff options
context:
space:
mode:
authorMatthias H <apoc@sixserv.org>2014-02-24 04:45:28 +0100
committerMatthias H <apoc@sixserv.org>2014-02-24 04:45:28 +0100
commit5512a41c689e231cee170fce3cfd015384921b47 (patch)
treeca1e4d95bf9752f45cb641ccc3ee65845cf45dab /lib/rbot/registry.rb
parent193edc468636e40ae21d1f0ea299f8eb0927ebba (diff)
[registry] refactoring into a abstract and factory
* a new abstract class AbstractAccessor is the new base for all concrete database implementations. * a factory now, dynamically discovers those implementations in the registry/ directory and will create the configured type for the plugins. * again: this makes db keys case-sensitive (aka 'the correct way of doing things' -.-) * re-added tokyocabinet
Diffstat (limited to 'lib/rbot/registry.rb')
-rw-r--r--lib/rbot/registry.rb326
1 files changed, 326 insertions, 0 deletions
diff --git a/lib/rbot/registry.rb b/lib/rbot/registry.rb
new file mode 100644
index 00000000..6c18df21
--- /dev/null
+++ b/lib/rbot/registry.rb
@@ -0,0 +1,326 @@
+#-- vim:sw=2:et
+#++
+#
+# :title: Registry: Persistent storage interface and factory
+#
+# This class provides persistent storage for plugins via a hash interface.
+# The default mode is an object store, so you can store ruby objects and
+# reference them with hash keys. This is because the default store/restore
+# methods of the plugins' RegistryAccessor are calls to Marshal.dump and
+# Marshal.restore,
+# for example:
+# blah = Hash.new
+# blah[:foo] = "fum"
+# @registry[:blah] = blah
+# then, even after the bot is shut down and disconnected, on the next run you
+# can access the blah object as it was, with:
+# blah = @registry[:blah]
+# The registry can of course be used to store simple strings, fixnums, etc as
+# well, and should be useful to store or cache plugin data or dynamic plugin
+# configuration.
+#
+# WARNING:
+# in object store mode, don't make the mistake of treating it like a live
+# object, e.g. (using the example above)
+# @registry[:blah][:foo] = "flump"
+# will NOT modify the object in the registry - remember that Registry#[]
+# returns a Marshal.restore'd object, the object you just modified in place
+# will disappear. You would need to:
+# blah = @registry[:blah]
+# blah[:foo] = "flump"
+# @registry[:blah] = blah
+#
+# If you don't need to store objects, and strictly want a persistant hash of
+# strings, you can override the store/restore methods to suit your needs, for
+# example (in your plugin):
+# def initialize
+# class << @registry
+# def store(val)
+# val
+# end
+# def restore(val)
+# val
+# end
+# end
+# end
+# Your plugins section of the registry is private, it has its own namespace
+# (derived from the plugin's class name, so change it and lose your data).
+# Calls to registry.each etc, will only iterate over your namespace.
+
+module Irc
+class Bot
+
+class Registry
+
+ # Dynamically loads the specified registry type library.
+ def initialize(format=nil)
+ @libpath = File.join(File.dirname(__FILE__), 'registry')
+ @format = format
+ load File.join(@libpath, @format+'.rb') if format
+ end
+
+ # Returns a list of supported registry database formats.
+ def discover
+ Dir.glob(File.join(@libpath, '*.rb')).map do |name|
+ File.basename(name, File.extname(name))
+ end
+ end
+
+ # Creates a new Accessor object for the specified database filename.
+ def create(path, filename)
+ # The get_impl method will return a list of all the classes that
+ # implement the accessor interface, since we only ever load one
+ # (the configured one) accessor implementation, we can just assume
+ # it to be the correct accessor to use.
+ cls = AbstractAccessor.get_impl.first
+ cls.new(File.join(path, 'registry_' + @format, filename.downcase))
+ end
+
+ # Helper method that will return a list of supported registry formats.
+ def self.formats
+ @@formats ||= Registry.new.discover
+ end
+
+ # Abstract database accessor (a hash-like interface).
+ class AbstractAccessor
+
+ # lets the user define a recovery procedure in case the Marshal
+ # deserialization fails, it might be manually recover data.
+ # NOTE: weird legacy stuff, used by markov plugin (WTH?)
+ attr_accessor :recovery
+
+ def initialize(filename)
+ debug 'init registry accessor for: ' + filename
+ @filename = filename
+ @name = File.basename filename
+ @registry = nil
+ @default = nil
+ @recovery = nil
+ end
+
+ def sub_registry(prefix)
+ path = File.join(@filename.gsub(/\.[^\/\.]+$/,''), prefix.to_s)
+ return self.class.new(path)
+ end
+
+ # creates the registry / subregistry folders
+ def create_folders
+ debug 'create folders for: ' + @filename
+ dirs = File.dirname(@filename).split("/")
+ dirs.length.times { |i|
+ dir = dirs[0,i+1].join("/")+"/"
+ unless File.exist?(dir)
+ Dir.mkdir(dir)
+ end
+ }
+ end
+
+ # Will return true if the database file exists.
+ def dbexists?
+ File.exists? @filename
+ end
+
+ # convert value to string form for storing in the registry
+ # defaults to Marshal.dump(val) but you can override this in your module's
+ # registry object to use any method you like.
+ # For example, if you always just handle strings use:
+ # def store(val)
+ # val
+ # end
+ def store(val)
+ Marshal.dump(val)
+ end
+
+ # restores object from string form, restore(store(val)) must return val.
+ # If you override store, you should override restore to reverse the
+ # action.
+ # For example, if you always just handle strings use:
+ # def restore(val)
+ # val
+ # end
+ def restore(val)
+ begin
+ Marshal.restore(val)
+ rescue Exception => e
+ error _("failed to restore marshal data for #{val.inspect}, attempting recovery or fallback to default")
+ debug e
+ if defined? @recovery and @recovery
+ begin
+ return @recovery.call(val)
+ rescue Exception => ee
+ error _("marshal recovery failed, trying default")
+ debug ee
+ end
+ end
+ return default
+ end
+ end
+
+ # Returned instead of nil if key wasnt found.
+ def set_default (default)
+ @default = default
+ end
+
+ def default
+ @default && (@default.dup rescue @default)
+ end
+
+ # Opens the database (if not already open) for read/write access.
+ def registry
+ create_folders unless dbexists?
+ end
+
+ # Forces flush/sync the database on disk.
+ def flush
+ return unless @registry
+ @registry.flush
+ end
+
+ # Should optimize/vacuum the database.
+ def optimize
+ return unless @registry
+ @registry.optimize
+ end
+
+ # Closes the database.
+ def close
+ return unless @registry
+ @registry.close
+ @registry = nil
+ end
+
+ # lookup a key in the registry
+ def [](key)
+ if dbexists? and registry.has_key?(key.to_s)
+ return restore(registry[key.to_s])
+ else
+ return default
+ end
+ end
+
+ # set a key in the registry
+ def []=(key,value)
+ registry[key.to_s] = store(value)
+ end
+
+ # like Hash#each
+ def each(&block)
+ return nil unless dbexists?
+ registry.each do |key|
+ block.call(key, self[key])
+ end
+ end
+
+ alias each_pair each
+
+ # like Hash#each_key
+ def each_key(&block)
+ self.each do |key|
+ block.call(key)
+ end
+ end
+
+ # like Hash#each_value
+ def each_value(&block)
+ self.each do |key, value|
+ block.call(value)
+ end
+ end
+
+ # just like Hash#has_key?
+ def has_key?(key)
+ return nil unless dbexists?
+ return registry.has_key?(key.to_s)
+ end
+
+ alias include? has_key?
+ alias member? has_key?
+ alias key? has_key?
+
+ # just like Hash#has_value?
+ def has_value?(value)
+ return nil unless dbexists?
+ return registry.has_value?(store(value))
+ end
+
+ # just like Hash#index?
+ def index(value)
+ self.each do |k,v|
+ return k if v == value
+ end
+ return nil
+ end
+
+ # delete a key from the registry
+ def delete(key)
+ return default unless dbexists?
+ return registry.delete(key.to_s)
+ end
+
+ # returns a list of your keys
+ def keys
+ return [] unless dbexists?
+ return registry.keys
+ end
+
+ # just like Hash#has_both?
+ def has_both?(key, value)
+ return false unless dbexists?
+ registry.has_key?(key.to_s) and registry.has_value?(store(value))
+ end
+
+ # Return an array of all associations [key, value] in your namespace
+ def to_a
+ return [] unless dbexists?
+ ret = Array.new
+ self.each {|key, value|
+ ret << [key, value]
+ }
+ return ret
+ end
+
+ # Return an hash of all associations {key => value} in your namespace
+ def to_hash
+ return {} unless dbexists?
+ ret = Hash.new
+ self.each {|key, value|
+ ret[key] = value
+ }
+ return ret
+ end
+
+ # empties the registry (restricted to your namespace)
+ def clear
+ return unless dbexists?
+ registry.clear
+ end
+ alias truncate clear
+
+ # returns an array of the values in your namespace of the registry
+ def values
+ return [] unless dbexists?
+ ret = Array.new
+ self.each {|k,v|
+ ret << v
+ }
+ return ret
+ end
+
+ # returns the number of keys in your registry namespace
+ def length
+ return 0 unless dbexists?
+ registry.length
+ end
+ alias size length
+
+ # Returns all classes from the namespace that implement this interface
+ def self.get_impl
+ ObjectSpace.each_object(Class).select { |klass| klass < self }
+ end
+ end
+
+end # Registry
+
+end # Bot
+end # Irc
+