Allow classes to be namespaced with subdirectories

Classes files may now reside in subdirectories, which act as namespaces.
For instance, a class ``ssh.server`` will result in the class definition
to be read from ``ssh/server.yml``. Specifying just ``ssh`` will cause
the class data to be read from ``ssh/init.yml`` or ``ssh.yml``. Note,
however, that only one of those two may be present.

Signed-off-by: martin f. krafft <madduck@madduck.net>
diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst
index a0ab3db..25bda17 100644
--- a/doc/source/changelog.rst
+++ b/doc/source/changelog.rst
@@ -5,6 +5,8 @@
 ========= ========== ========================================================
 Version   Date       Changes
 ========= ========== ========================================================
+                     * yaml_fs: classes may be defined in subdirectories
+                       (closes: #12, #19, #20)
                      * Migrate Salt adapter to new core API (closes: #18)
                      * Fix --nodeinfo invocation in docs (closes: #21)
 1.2.2     2013-12-27 * Recurse classes obtained from class mappings
diff --git a/doc/source/operations.rst b/doc/source/operations.rst
index 42e8a86..365c198 100644
--- a/doc/source/operations.rst
+++ b/doc/source/operations.rst
@@ -42,11 +42,17 @@
              into which the node definition is supposed to be place.
 ============ ================================================================
 
-Nodes may be defined in subdirectories. However, node names (filename) must be
-unique across all subdirectories, and |reclass| will exit with an error if
-a node is defined multiple times. Subdirectories therefore really only exist
-for the administrator's local data structuring. They may be used in mappings
-(see below) to tag additional classes onto nodes.
+Classes files may reside in subdirectories, which act as namespaces. For
+instance, a class ``ssh.server`` will result in the class definition to be
+read from ``ssh/server.yml``. Specifying just ``ssh`` will cause the class
+data to be read from ``ssh/init.yml`` or ``ssh.yml``. Note, however, that only
+one of those two may be present.
+
+Nodes may also be defined in subdirectories. However, node names (filename)
+must be unique across all subdirectories, and |reclass| will exit with an
+error if a node is defined multiple times. Subdirectories therefore really
+only exist for the administrator's local data structuring. They may be used in
+mappings (see below) to tag additional classes onto nodes.
 
 Data merging
 ------------
diff --git a/doc/source/todo.rst b/doc/source/todo.rst
index 38df768..18b39ed 100644
--- a/doc/source/todo.rst
+++ b/doc/source/todo.rst
@@ -57,12 +57,4 @@
 Furthermore, ``$CWD`` and ``~`` might not make a lot of sense in all
 use-cases.
 
-Class subdirectories
---------------------
-It would be nice syntactic sugar to allow classes to sit in subdirectories,
-such that ``ssh.server`` would load a class in …/ssh/server.yml (assuming
-``yaml_fs``).
-
-See `this pull request for a discussion about it <https://github.com/madduck/reclass/pull/12>`_.
-
 .. include:: substs.inc
diff --git a/examples/classes/basenode.yml b/examples/classes/node/index.yml
similarity index 100%
rename from examples/classes/basenode.yml
rename to examples/classes/node/index.yml
diff --git a/examples/classes/unixnode.yml b/examples/classes/node/unix.yml
similarity index 83%
rename from examples/classes/unixnode.yml
rename to examples/classes/node/unix.yml
index 26c6583..12bbac1 100644
--- a/examples/classes/unixnode.yml
+++ b/examples/classes/node/unix.yml
@@ -1,5 +1,5 @@
 classes:
-  - basenode
+  - node
 applications:
   - motd
 parameters:
diff --git a/examples/nodes/localhost.yml b/examples/nodes/localhost.yml
index 2b2612e..45f712e 100644
--- a/examples/nodes/localhost.yml
+++ b/examples/nodes/localhost.yml
@@ -1,5 +1,5 @@
 classes:
-  - unixnode
+  - node.unix
   - mysite
 environment: testing
 parameters:
diff --git a/reclass/storage/yaml_fs/__init__.py b/reclass/storage/yaml_fs/__init__.py
index 6c917e0..e88fd7d 100644
--- a/reclass/storage/yaml_fs/__init__.py
+++ b/reclass/storage/yaml_fs/__init__.py
@@ -26,31 +26,43 @@
     def __init__(self, nodes_uri, classes_uri, default_environment=None):
         super(ExternalNodeStorage, self).__init__(STORAGE_NAME)
 
-        def _handle_node_duplicates(name, uri1, uri2):
-            raise reclass.errors.DuplicateNodeNameError(self._get_storage_name(),
-                                                        name, uri1, uri2)
+        def name_mangler(relpath, name):
+            # nodes are identified just by their basename
+            return name
         self._nodes_uri = nodes_uri
-        self._nodes = self._enumerate_inventory(nodes_uri,
-                                                duplicate_handler=_handle_node_duplicates)
+        self._nodes = self._enumerate_inventory(nodes_uri, name_mangler)
+
+        def name_mangler(relpath, name):
+            if relpath == '.':
+                return name
+            parts = relpath.split(os.path.sep)
+            if name != 'index':
+                parts.append(name)
+            return '.'.join(parts)
         self._classes_uri = classes_uri
-        self._classes = self._enumerate_inventory(classes_uri)
+        self._classes = self._enumerate_inventory(classes_uri, name_mangler)
 
         self._default_environment = default_environment
 
     nodes_uri = property(lambda self: self._nodes_uri)
     classes_uri = property(lambda self: self._classes_uri)
 
-    def _enumerate_inventory(self, basedir, duplicate_handler=None):
+    def _enumerate_inventory(self, basedir, name_mangler):
         ret = {}
         def register_fn(dirpath, filenames):
             filenames = fnmatch.filter(filenames, '*{0}'.format(FILE_EXTENSION))
             vvv('REGISTER {0} in path {1}'.format(filenames, dirpath))
             for f in filenames:
                 name = os.path.splitext(f)[0]
+                relpath = os.path.relpath(dirpath, basedir)
+                if callable(name_mangler):
+                    name = name_mangler(relpath, name)
                 uri = os.path.join(dirpath, f)
-                if name in ret and callable(duplicate_handler):
-                    duplicate_handler(name, os.path.join(basedir, ret[name]), uri)
-                ret[name] = os.path.relpath(uri, basedir)
+                if name in ret:
+                    E = reclass.errors.DuplicateNodeNameError
+                    raise E(self._get_storage_name(), name,
+                            os.path.join(basedir, ret[name]), uri)
+                ret[name] = os.path.join(relpath, f)
 
         d = Directory(basedir)
         d.walk(register_fn)
@@ -70,6 +82,7 @@
     def get_class(self, name, nodename=None):
         vvv('GET CLASS {0}'.format(name))
         try:
+            print self._classes
             path = os.path.join(self.classes_uri, self._classes[name])
         except KeyError, e:
             raise reclass.errors.ClassNotFound(self.name, name, self.classes_uri)