commit 7bfcd98b929705cc5c65b729a60f022827a66bfe
Author: Marc Dequènes (Duck) <Duck@DuckCorp.org>
Date:   Thu Mar 3 03:34:09 2011 +0100

    [evol] Librarian API work §3

diff --git a/bin/test_client b/bin/test_client
index 0829c50..c4d733b 100755
--- a/bin/test_client
+++ b/bin/test_client
@@ -40,10 +40,12 @@ module CyborgHood
           ask "MapMaker", :info1, "/_cyborg_"
           ask "Librarian", :info2, "/_cyborg_"
           ask "Librarian", :library, "/Records"
-          ask "Librarian", :persons, "/Records/persons"
-          ask "Librarian", :dns_domains, "/Records/dns_domains"
+          ask "Librarian", :persons, "/Records/Persons"
+          ask "Librarian", :dns_domains, "/Records/DnsDomains"
           ask "Librarian", :gorou, "/Records/Persons/gorou"
           ask "Librarian", :guihome_net, "/Records/DnsDomains/guihome.net"
+          ask "Librarian", :person_search, "/Records/Persons/?", {:uidNumber => 10033}
+          ask "Librarian", :person_search_null, "/Records/Persons/?"
           #ask "MapMaker", :zones, "/Zones"
           #ask "MapMaker", :wanted_failure, "/prout"
           #ask "MapMaker", :dns, "/Services/DNS"
diff --git a/lib/cyborghood-librarian/interface/0_base.rb b/lib/cyborghood-librarian/interface/0_base.rb
index 072b71c..cd9b247 100644
--- a/lib/cyborghood-librarian/interface/0_base.rb
+++ b/lib/cyborghood-librarian/interface/0_base.rb
@@ -1,30 +1,23 @@
 node 'Records' do
   node ['Persons', 'DnsDomains'] do
-    object_list = Proc.new{|obj_name| bot.services.records.send(node_name.underscore, obj_name) }
+    object_list = Proc.new do |*args|
+      obj_name, options = args
+      options ||= {}
+
+      bot.services.records.send(node_name.underscore, obj_name, options)
+    end
 
     on_request do |request|
       request.reply.results = object_list.call
     end
 
+    attr_search_node do |criterias|
+      object_list.call(:search, :criterias => criterias, :format => :data_hash)
+    end
+
     node object_list do
       on_request do |request|
-        prepare_value = Proc.new do |v|
-          if v.is_a? Array
-            v.collect!{|v2| prepare_value.call(v2) }
-          elsif v.is_a? ActiveLdap::DistinguishedName
-            v.to_s
-          else
-            v
-          end
-        end
-
-        obj_attrs = object_list.call(node_name).attributes
-        obj_attrs.delete('objectClass')
-        obj_attrs.each do |k, v|
-          obj_attrs[k] = prepare_value.call(v)
-        end
-
-        request.reply.results = obj_attrs
+        request.reply.results = object_list.call(node_name, :format => :data_hash)
       end
     end
   end
diff --git a/lib/cyborghood-librarian/interface_dsl_addons.rb b/lib/cyborghood-librarian/interface_dsl_addons.rb
deleted file mode 100644
index b5b09c8..0000000
--- a/lib/cyborghood-librarian/interface_dsl_addons.rb
+++ /dev/null
@@ -1,2 +0,0 @@
-
--> ldap search /?
diff --git a/lib/cyborghood-librarian/records.rb b/lib/cyborghood-librarian/records.rb
index 25777a5..a1c2c78 100644
--- a/lib/cyborghood-librarian/records.rb
+++ b/lib/cyborghood-librarian/records.rb
@@ -25,27 +25,88 @@ end
 module CyborgHood
   module LibrarianLand
     class Records
-      def persons(obj_name = nil)
+      HIDDEN_ATTRIBUTES = ['objectClass', 'userPassword', 'hasSubordinates', 'entryUUID', 'entryDN', 'structuralObjectClass', 'subschemaSubentry']
+
+      def persons(*args)
+        find_records(LdapPerson, 'uid', *args)
+      end
+
+      def dns_domains(*args)
+        find_records(LdapDnsDomain, 'cn', *args)
+      end
+
+      protected
+
+      # obj_name:
+      #   - nil: return list of objects'names
+      #   - <value>: return object whose primary attribute is equal to <value>
+      #   - :all: return all objects
+      def find_records(klass, primary_attr, obj_name = nil, options = {})
         if obj_name.nil?
-          LdapPerson.find(:all, :attributes => ['uid']).collect{|i| i.uid }
+          klass.find(:all, :attributes => [primary_attr]).collect{|i| i.send(primary_attr) }
+        elsif obj_name == :search
+          filter_parts = []
+          options[:criterias].each do |k, v|
+            filter_parts << "(#{k}=#{v})"
+          end
+          filter = (filter_parts.size == 1) ? filter_parts.first : '(&' + filter_parts.join + ')'
+
+          list = klass.find(:all, :filter => filter)
+          index_result(list, primary_attr) if options[:indexed]
+          format_result(list, options[:format])
+        elsif obj_name == :all
+          list = klass.find(:all)
+          index_result(list, primary_attr) if options[:indexed]
+          format_result(list, options[:format])
         else
-          list = LdapPerson.find(:all, :attribute => 'uid', :value => obj_name)
+          list = klass.find(:all, :attribute => primary_attr, :value => obj_name)
           return if list.nil? or list.empty?
-          return list.first if list.size == 1
-          raise CyberError.new(:unrecoverable, "db/ldap", "broken DB: multiple Person with the same uid")
+          return format_result(list.first, options[:format]) if list.size == 1
+          raise CyberError.new(:unrecoverable, "db/ldap", "broken DB: multiple #{klass} with the same #{primary_attr}")
         end
       end
 
-      def dns_domains(obj_name = nil)
-        if obj_name.nil?
-          LdapDnsDomain.find(:all, :attributes => ['cn']).collect{|i| i.cn }
+      def format_value(v)
+        if v.is_a? Array
+          v.collect!{|v2| format_value(v2) }
+        elsif v.is_a? ActiveLdap::DistinguishedName
+          v.to_s
         else
-          list = LdapDnsDomain.find(:all, :attribute => 'cn', :value => obj_name)
-          return if list.nil? or list.empty?
-          return list.first if list.size == 1
-          raise CyberError.new(:unrecoverable, "db/ldap", "broken DB: multiple DnsDomain with the same cn")
+          v
         end
       end
+
+      def dh_format_element(element)
+        obj_attrs = {}
+        element.attributes.each do |k, v|
+          next if HIDDEN_ATTRIBUTES.include? k
+          obj_attrs[k.to_sym] = format_value(v)
+        end
+        obj_attrs
+      end
+
+      def format_result(list, format = nil)
+        case format
+        when :data_hash
+          if list.is_a? Array
+            list.collect{|element| dh_format_element(element) }
+          elsif list.is_a? Hash
+            result = {}
+            list.each{|element_name, element| result[element_name] = dh_format_element(element) }
+            result
+          else
+            dh_format_element(list)
+          end
+        else
+          list
+        end
+      end
+
+      def index_result(list, primary_attr)
+        idx_list = {}
+        list.each{|i| idx_list[i.send(primary_attr)] = i }
+        idx_list
+      end
     end
   end # LibrarianLand
 end
diff --git a/lib/cyborghood/cyborg/botnet/interface.rb b/lib/cyborghood/cyborg/botnet/interface.rb
index ec82137..2e0c556 100644
--- a/lib/cyborghood/cyborg/botnet/interface.rb
+++ b/lib/cyborghood/cyborg/botnet/interface.rb
@@ -95,7 +95,9 @@ module CyborgHood
         end
       end
 
-      def attr_search_node
+      def attr_search_node(list = nil, &block)
+        list ||= block
+
         lookup_node = self
 
         node '?', :hidden => true do
@@ -103,29 +105,8 @@ module CyborgHood
             if request.args.empty?
               request.reply.results = lookup_node.__send__(:visible_nodes_names)
             else
-              node_names_list = []
-
-              lookup_node.__send__(:visible_nodes).each do |match, node|
-                if match.is_a? String
-                  match_list = [match]
-                elsif match.is_a? Proc
-                  match_list = match.call
-                else
-                  next
-                end
-
-                match_list.each do |child_node_name|
-                  node.__send__(:load, child_node_name)
-                  result = node.__send__(:request, request.session)
-                  next unless result.respond_to? :to_hash
-
-                  if hash_match_criterias(result.to_hash, request.args.first)
-                    node_names_list << child_node_name
-                  end
-                end
-              end
-
-              request.reply.results = node_names_list
+              request.reply.results = list.nil? ? search_among_children(request, lookup_node) :
+                search_in_list(request, lookup_node, list)
             end
           end
         end
@@ -142,10 +123,10 @@ module CyborgHood
 
       def _call(session, node_path, args = nil)
         args ||= []
-        raise CyberError.new(:unrecoverable, 'api/cyborghood', "wrong format for arguments") unless args.is_a? Array
+        raise CyberError.new(:unrecoverable, 'api/cyborghood', "wrong format for arguments when calling node '#{node_path}'") unless args.is_a? Array
 
         node = find_node(session, node_path)
-        raise CyberError.new(:unrecoverable, 'api/cyborghood', "unknown node") if node.nil?
+        raise CyberError.new(:unrecoverable, 'api/cyborghood', "unknown node '#{node_path}'") if node.nil?
 
         logger.debug "[Server API] Node '#{node_path}' found"
         node.__send__(:request, session, args)
@@ -247,7 +228,7 @@ module CyborgHood
           rescue
             logger.debug "node request error message: " + $!
             logger.debug "node request error backtrace: " + $!.backtrace.join("\n")
-            raise CyberError.new(:unrecoverable, 'api/cyborghood', "method call failed: " + $!)
+            raise CyberError.new(:unrecoverable, 'api/cyborghood', "call failed on node '#{node_path}': " + $!)
           end
         else
           visible_nodes_names
@@ -288,7 +269,8 @@ module CyborgHood
 
       def hash_match_criterias(hash, crit)
         crit.each do |key, wanted_value|
-          value = hash[key]
+          # force cast to string in case of a composite object
+          value = hash[key].to_s
 
           if wanted_value.is_a? Array
             return false unless wanted_value.include?(value)
@@ -301,6 +283,54 @@ module CyborgHood
 
         true
       end
+
+      def search_among_children(request, lookup_node)
+        node_names_list = []
+
+        lookup_node.__send__(:visible_nodes).each do |match, node|
+          if match.is_a? String
+            match_list = [match]
+          elsif match.is_a? Proc
+            match_list = match.call
+          else
+            next
+          end
+
+          match_list.each do |child_node_name|
+            node.__send__(:load, child_node_name)
+            result = node.__send__(:request, request.session)
+            next unless result.respond_to? :to_hash
+
+            if hash_match_criterias(result.to_hash, request.args.first)
+              node_names_list << child_node_name
+            end
+          end
+        end
+
+        node_names_list
+      end
+
+      def search_in_list(request, lookup_node, list)
+        criterias = request.args.first
+        return [] unless criterias.is_a? Hash
+
+        if list.is_a? Proc
+          list = list.call(criterias)
+          # if result is an array, then a final list of names is given, return it
+          return list if list.is_a? Array
+          # else it must be a Hash which needs to be filtered
+        end
+
+        node_names_list = []
+
+        list.each do |element_name, element|
+          if hash_match_criterias(element.to_hash, criterias)
+            node_names_list << element_name
+          end
+        end
+
+        node_names_list
+      end
     end
-  end
+  end # DSL
 end
