commit b7f7d214220c8cb982ed8e5dfdb68bd7a199e1d0
Author: Marc Dequènes (Duck) <Duck@DuckCorp.org>
Date:   Mon Aug 16 02:46:12 2010 +0200

    [evol] MapMaker / Cyborg server interface: cleanup, reorg, ...

diff --git a/bin/mapmaker b/bin/mapmaker
index d136283..a99cff7 100755
--- a/bin/mapmaker
+++ b/bin/mapmaker
@@ -55,7 +55,7 @@ module CyborgHood
 
     class MapMakerInterface
       include CyborgServerInterface
-      include CyborgServerDefaultInterface
+      include CyborgServerRootInterfaceAddon
 
       class DNS < Services::DNS::System
         include CyborgServerInterface
@@ -67,8 +67,17 @@ module CyborgHood
           "coucou: " + data.inspect
         end
 
-        class Zones
+        class Taiste
           include CyborgServerInterface
+
+          def api_methods
+            ["coucou", "toto", "plop"]
+          end
+
+          dynamic_interface {|node_name| ">>> #{node_name} <<<" }
+        end
+
+        class Zones
           include CyborgServerStatefulInterface
 
           def initialize
@@ -79,17 +88,7 @@ module CyborgHood
             @dns.zones
           end
 
-          def method_missing(method_name, *args)
-            session = args.shift
-
-            zone_name = method_name.to_s
-            if api_methods.include?(zone_name)
-              resource_key = "dns/zones/#{zone_name}"
-              session.store.get(resource_key) { Zone.new(zone_name) }
-            else
-              super
-            end
-          end
+          stateful_dynamic_interface("dns/zones/#NODE#") {|node_name| Zone.new(node_name) }
 
           class Zone < Services::DNS::Zone
             include CyborgServerEmbeddedInterface
diff --git a/lib/cyborghood/cyborg/conversation.rb b/lib/cyborghood/cyborg/conversation.rb
new file mode 100644
index 0000000..bde0d41
--- /dev/null
+++ b/lib/cyborghood/cyborg/conversation.rb
@@ -0,0 +1,143 @@
+#--
+# CyborgHood, a distributed system management software.
+# Copyright (c) 2009-2010 Marc Dequènes (Duck) <Duck@DuckCorp.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#++
+
+require 'cyborghood/cyborg/session'
+
+
+module CyborgHood
+  class Conversation < EventMachine::Protocols::LineAndTextProtocol
+    private_class_method :new
+
+    # don't rely on EventMachine's default, it may change one day
+    MaxLineLength = 16*1024
+
+    EOD = "\033[0J"
+    COMMAND_PATTERN = "^#{CyborgServerInterfaceBase::NODE_PATTERN}(?: ([+?]+))?$"
+
+    def initialize(interface)
+      @interface = interface
+
+      super
+
+      @config = Config.instance
+      @split_data_mode = false
+      @split_data_cmd = nil
+      @split_data = []
+      @session = Session.new
+    end
+
+    def send_line(msg)
+      send_data "#{msg}\n"
+      logger.debug "Sent data [#{identifier}]: #{msg}"
+    end
+
+    def post_init
+      logger.debug "New conversation with #{identifier}"
+      send_line "220 #{PRODUCT} #{VERSION} - #{@config.bot_name}"
+    end
+
+    def receive_line(data)
+
+      return if data.empty?
+
+      if data == EOD
+        logger.debug "Received EOD [#{identifier}]"
+        exit_split_mode
+      else
+        if @split_data_mode
+          logger.debug "Received data (split mode) [#{identifier}]: #{data}"
+
+          @split_data << data
+        else
+          logger.debug "Received data [#{identifier}]: #{data}"
+
+          unless data =~ Regexp.new(COMMAND_PATTERN)
+            logger.error "Error [#{identifier}]: syntax error"
+            send_line "552 syntax error in command"
+            return
+          end
+          cmd = $1
+          flags = $2 || ""
+
+          if flags.index '?'
+            send_line "250+ ok"
+            send_line({'exists' => @interface.has_node?(cmd)}.to_yaml)
+            return
+          end
+
+          if flags.index '+'
+            enter_split_mode(cmd)
+          else
+            receive_command(cmd)
+          end
+        end
+      end
+    end
+
+    def receive_command(cmd, data = nil)
+      logger.debug "Executing command '#{cmd}' [#{identifier}]"
+      send_line @interface.call(@session, cmd, data)
+    end
+
+    def receive_error(msg)
+      logger.error "Error [#{identifier}]: #{msg}"
+    end
+
+    def unbind
+      logger.debug "Conversation finished with #{identifier}"
+      @session.clear
+    end
+
+    protected
+
+    def enter_split_mode(cmd)
+      if @split_data_mode
+        logger.error "Error [#{identifier}]: already in split mode"
+        send_line "551 protocol error"
+        @split_data_mode = false
+        @split_data_cmd = nil
+      else
+        logger.debug "Entered split mode for command '#{cmd}' [#{identifier}]"
+        @split_data_mode = true
+        @split_data_cmd = cmd
+      end
+      @split_data = []
+    end
+
+    def exit_split_mode
+      if @split_data_mode
+        logger.debug "Quit split mode for command '#{@split_data_cmd}' [#{identifier}]"
+        receive_command(@split_data_cmd, @split_data.join("\n"))
+      else
+        logger.error "Error [#{identifier}]: not in split mode"
+        send_line "551 protocol error"
+      end
+      @split_data_mode = false
+      @split_data_cmd = nil
+      @split_data = []
+    end
+  end
+
+  class ConversationUNIXSocket < Conversation
+    public_class_method :new
+
+    def identifier
+      "unix_socket/#{@signature}"
+    end
+  end
+end
diff --git a/lib/cyborghood/cyborg/interface.rb b/lib/cyborghood/cyborg/interface.rb
index a5a3852..3b7b95f 100644
--- a/lib/cyborghood/cyborg/interface.rb
+++ b/lib/cyborghood/cyborg/interface.rb
@@ -20,23 +20,7 @@ require 'singleton'
 
 
 module CyborgHood
-  module CyborgServerInterface
-    def self.included(base)
-      base.class_eval("include CyborgServerInterfaceBase")
-      base.class_eval("include Singleton")
-    end
-  end
-
-  module CyborgServerEmbeddedInterface
-    def self.included(base)
-      base.class_eval("include CyborgServerInterfaceBase")
-      base.export_parent_methods
-    end
-  end
-
-  module CyborgServerStatefulInterface
-  end
-
+  # the base mixin (not intended to be used directly, but...)
   module CyborgServerInterfaceBase
     NODE_PATTERN = "((?:\/|(?:\/[a-zA-Z0-9._]+)+[?=]?))"
 
@@ -173,7 +157,75 @@ module CyborgHood
     end
   end
 
-  module CyborgServerDefaultInterface
+  # structural mixins
+
+  module CyborgServerInterface
+    def self.included(base)
+      base.class_eval("include CyborgServerInterfaceBase")
+      base.class_eval("include Singleton")
+      base.extend(ClassMethods)
+    end
+
+    module ClassMethods
+      def dynamic_interface(&resource_generator)
+        class_eval do
+          class_inheritable_reader :resource_generator
+
+          def method_missing(method_name, *args)
+            node_name = method_name.to_s
+            if api_methods.include?(node_name)
+              self.resource_generator.call(node_name)
+            else
+              super
+            end
+          end
+        end
+
+        write_inheritable_attribute(:resource_generator, resource_generator)
+      end
+    end
+  end
+
+  module CyborgServerEmbeddedInterface
+    def self.included(base)
+      base.class_eval("include CyborgServerInterfaceBase")
+      base.export_parent_methods
+    end
+  end
+
+  module CyborgServerStatefulInterface
+    def self.included(base)
+      base.class_eval("include CyborgServerInterfaceBase")
+      base.extend(ClassMethods)
+    end
+
+    module ClassMethods
+      def stateful_dynamic_interface(resource_key_pattern, &resource_generator)
+        class_eval do
+          class_inheritable_reader :resource_key_pattern, :resource_generator
+
+          def method_missing(method_name, *args)
+            session = args.shift
+
+            node_name = method_name.to_s
+            if api_methods.include?(node_name)
+              resource_key = self.resource_key_pattern.gsub("#NODE#", node_name)
+              session.store.get(resource_key) { self.resource_generator.call(node_name) }
+            else
+              super
+            end
+          end
+        end
+
+        write_inheritable_attribute(:resource_key_pattern, resource_key_pattern)
+        write_inheritable_attribute(:resource_generator, resource_generator)
+      end
+    end
+  end
+
+  # additional mixin
+
+  module CyborgServerRootInterfaceAddon
     PROTOCOL_VERSION = "0.1~"
 
     def self.included(base)
diff --git a/lib/cyborghood/cyborg/server.rb b/lib/cyborghood/cyborg/server.rb
index cfa6d9d..c63414b 100644
--- a/lib/cyborghood/cyborg/server.rb
+++ b/lib/cyborghood/cyborg/server.rb
@@ -18,7 +18,7 @@
 
 require 'eventmachine'
 require 'cyborghood/cyborg/interface'
-require 'shellwords'
+require 'cyborghood/cyborg/conversation'
 
 
 module CyborgHood
@@ -74,130 +74,10 @@ module CyborgHood
     end
   end
 
-  class Conversation < EventMachine::Protocols::LineAndTextProtocol
-    private_class_method :new
-
-    # don't rely on EventMachine's default, it may change one day
-    MaxLineLength = 16*1024
-
-    EOD = "\033[0J"
-    COMMAND_PATTERN = "^#{CyborgServerInterfaceBase::NODE_PATTERN}(?: ([+?]+))?$"
-
-    def initialize(interface)
-      @interface = interface
-
-      super
-
-      @config = Config.instance
-      @split_data_mode = false
-      @split_data_cmd = nil
-      @split_data = []
-      @session = Session.new
-    end
-
-    def send_line(msg)
-      send_data "#{msg}\n"
-      logger.debug "Sent data [#{identifier}]: #{msg}"
-    end
-
-    def post_init
-      logger.debug "New conversation with #{identifier}"
-      send_line "220 #{PRODUCT} #{VERSION} - #{@config.bot_name}"
-    end
-
-    def receive_line(data)
-
-      return if data.empty?
-
-      if data == EOD
-        logger.debug "Received EOD [#{identifier}]"
-        exit_split_mode
-      else
-        if @split_data_mode
-          logger.debug "Received data (split mode) [#{identifier}]: #{data}"
-
-          @split_data << data
-        else
-          logger.debug "Received data [#{identifier}]: #{data}"
-
-          unless data =~ Regexp.new(COMMAND_PATTERN)
-            logger.error "Error [#{identifier}]: syntax error"
-            send_line "552 syntax error in command"
-            return
-          end
-          cmd = $1
-          flags = $2 || ""
-
-          if flags.index '?'
-            send_line "250+ ok"
-            send_line({'exists' => @interface.has_node?(cmd)}.to_yaml)
-            return
-          end
-
-          if flags.index '+'
-            enter_split_mode(cmd)
-          else
-            receive_command(cmd)
-          end
-        end
-      end
-    end
-
-    def receive_command(cmd, data = nil)
-      logger.debug "Executing command '#{cmd}' [#{identifier}]"
-      send_line @interface.call(@session, cmd, data)
-    end
-
-    def receive_error(msg)
-      logger.error "Error [#{identifier}]: #{msg}"
-    end
-
-    def unbind
-      logger.debug "Conversation finished with #{identifier}"
-      @session.clear
-    end
-
-    protected
-
-    def enter_split_mode(cmd)
-      if @split_data_mode
-        logger.error "Error [#{identifier}]: already in split mode"
-        send_line "551 protocol error"
-        @split_data_mode = false
-        @split_data_cmd = nil
-      else
-        logger.debug "Entered split mode for command '#{cmd}' [#{identifier}]"
-        @split_data_mode = true
-        @split_data_cmd = cmd
-      end
-      @split_data = []
-    end
-
-    def exit_split_mode
-      if @split_data_mode
-        logger.debug "Quit split mode for command '#{@split_data_cmd}' [#{identifier}]"
-        receive_command(@split_data_cmd, @split_data.join("\n"))
-      else
-        logger.error "Error [#{identifier}]: not in split mode"
-        send_line "551 protocol error"
-      end
-      @split_data_mode = false
-      @split_data_cmd = nil
-      @split_data = []
-    end
-  end
-
-  class ConversationUNIXSocket < Conversation
-    public_class_method :new
-
-    def identifier
-      "unix_socket/#{@signature}"
-    end
-  end
-
+  # default interface if not overridden
   class EmptyInterface
     include CyborgServerInterface
-    include CyborgServerDefaultInterface
+    include CyborgServerRootInterfaceAddon
   end
 
   module SimpleServer
@@ -220,30 +100,4 @@ module CyborgHood
       EmptyInterface.instance
     end
   end
-
-  class Session
-    attr_reader :store
-
-    def initialize
-      @store = Store.new
-    end
-
-    def clear
-      @store.values.each do |obj|
-        obj.__destroy if obj.respond_to? :__destroy
-      end
-      @store.clear
-    end
-
-    class Store < Hash
-      def get(key)
-        obj = self[key]
-        if obj.nil? and block_given?
-          obj = yield
-          self[key] = obj
-        end
-        obj
-      end
-    end
-  end
 end
diff --git a/lib/cyborghood/cyborg/session.rb b/lib/cyborghood/cyborg/session.rb
new file mode 100644
index 0000000..a356ac6
--- /dev/null
+++ b/lib/cyborghood/cyborg/session.rb
@@ -0,0 +1,46 @@
+#--
+# CyborgHood, a distributed system management software.
+# Copyright (c) 2009-2010 Marc Dequènes (Duck) <Duck@DuckCorp.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#++
+
+
+module CyborgHood
+  class Session
+    attr_reader :store
+
+    def initialize
+      @store = Store.new
+    end
+
+    def clear
+      @store.values.each do |obj|
+        obj.__destroy if obj.respond_to? :__destroy
+      end
+      @store.clear
+    end
+
+    class Store < Hash
+      def get(key)
+        obj = self[key]
+        if obj.nil? and block_given?
+          obj = yield
+          self[key] = obj
+        end
+        obj
+      end
+    end
+  end
+end
