Merge pull request #43 from salt-formulas/pr/38

Pr/38 - rebased
diff --git a/README-extentions.rst b/README-extentions.rst
index 97d78af..61364a4 100644
--- a/README-extentions.rst
+++ b/README-extentions.rst
@@ -208,6 +208,82 @@
   group_errors: True
 
 
+Use references in class names
+-----------------------------
+
+Allows to use references in the class names.
+
+References pointed to in class names cannot themselves reference another key, they should be simple strings.
+
+To avoid pitfalls do not over-engineer your class references. They should be used only for core conditions and only for them.
+A short example: `- system.wrodpress.db.${_class:database_backend}`.
+
+Best practices:
+- use references in class names always load your global class specification prior the reference is used.
+- structure your class references under parameters under one key (for example `_class`).
+- use class references as a kind of "context" or "global" available options you always know what they are set.
+
+Class referencing for existing reclass users. Frequently when constructing your models you had to load or not load some
+classes based on your setup. In most cases this lead to fork of a model or introducing kind of template generator (like cookiecutter) to
+create a model based on the base "context" or "global" variables. Class referencing is a simple way how to avoid
+"pre-processors" like this and if/else conditions around class section.
+
+
+Assuming following class setup:
+
+* node is loading `third.yml` class only
+
+
+Classes:
+
+.. code-block:: yaml
+  #/etc/reclass/classes/global.yml
+  parameters:
+    _class:
+      env:
+        override: 'env.dev'
+    lab:
+      name: default
+
+  #/etc/reclass/classes/lab/env/dev.yml
+  parameters:
+    lab:
+      name: dev
+
+  #/etc/reclass/classes/second.yml
+  classes:
+    - global
+    - lab.${_class:env:override}
+
+  #/etc/reclass/classes/third.yml
+  classes:
+    - global
+    - second
+
+
+Reclass --nodeinfo then returns:
+
+.. code-block:: yaml
+
+  ...
+  ...
+  applications: []
+  environment: base
+  exports: {}
+  classes:
+  - global
+  - lab.${_class:env:override}
+  - second
+  parameters:
+    _class:
+      env:
+        override: env.dev
+    lab:
+      name: dev
+    ...
+    ...
+
+
 Inventory Queries
 -----------------
 
diff --git a/reclass/core.py b/reclass/core.py
index 9a23d89..1a08db8 100644
--- a/reclass/core.py
+++ b/reclass/core.py
@@ -21,7 +21,8 @@
 from reclass.settings import Settings
 from reclass.output.yaml_outputter import ExplicitDumper
 from reclass.datatypes import Entity, Classes, Parameters, Exports
-from reclass.errors import MappingFormatError, ClassNotFound, InvQueryClassNotFound, InvQueryError, InterpolationError
+from reclass.errors import MappingFormatError, ClassNameResolveError, ClassNotFound, InvQueryClassNameResolveError, InvQueryClassNotFound, InvQueryError, InterpolationError, ResolveError
+from reclass.values.parser import Parser
 
 try:
     basestring
@@ -30,6 +31,8 @@
 
 class Core(object):
 
+    _parser = Parser()
+
     def __init__(self, storage, class_mappings, settings, input_data=None):
         self._storage = storage
         self._class_mappings = class_mappings
@@ -96,7 +99,7 @@
         p = Parameters(self._input_data, self._settings)
         return Entity(self._settings, parameters=p, name='input data')
 
-    def _recurse_entity(self, entity, merge_base=None, seen=None, nodename=None, environment=None):
+    def _recurse_entity(self, entity, merge_base=None, context=None, seen=None, nodename=None, environment=None):
         if seen is None:
             seen = {}
 
@@ -106,7 +109,19 @@
         if merge_base is None:
             merge_base = Entity(self._settings, name='empty (@{0})'.format(nodename))
 
+        if context is None:
+            context = Entity(self._settings, name='empty (@{0})'.format(nodename))
+
         for klass in entity.classes.as_list():
+            if klass.count('$') > 0:
+                try:
+                    klass = str(self._parser.parse(klass, self._settings).render(merge_base.parameters.as_dict(), {}))
+                except ResolveError as e:
+                    try:
+                        klass = str(self._parser.parse(klass, self._settings).render(context.parameters.as_dict(), {}))
+                    except ResolveError as e:
+                        raise ClassNameResolveError(klass, nodename, entity.uri)
+
             if klass not in seen:
                 try:
                     class_entity = self._storage.get_class(klass, environment, self._settings)
@@ -121,7 +136,7 @@
                     e.uri = entity.uri
                     raise
 
-                descent = self._recurse_entity(class_entity, seen=seen,
+                descent = self._recurse_entity(class_entity, context=merge_base, seen=seen,
                                                nodename=nodename, environment=environment)
                 # on every iteration, we merge the result of the recursive
                 # descent into what we have so far…
@@ -159,6 +174,8 @@
                     node = self._node_entity(nodename)
                 except ClassNotFound as e:
                     raise InvQueryClassNotFound(e)
+                except ClassNameResolveError as e:
+                    raise InvQueryClassNameResolveError(e)
                 if queries is None:
                     try:
                         node.interpolate_exports()
@@ -186,8 +203,8 @@
         seen = {}
         merge_base = self._recurse_entity(base_entity, seen=seen, nodename=nodename,
                                           environment=node_entity.environment)
-        return self._recurse_entity(node_entity, merge_base, seen=seen, nodename=nodename,
-                                    environment=node_entity.environment)
+        return self._recurse_entity(node_entity, merge_base=merge_base, context=merge_base, seen=seen,
+                                    nodename=nodename, environment=node_entity.environment)
 
     def _nodeinfo(self, nodename, inventory):
         try:
diff --git a/reclass/errors.py b/reclass/errors.py
index a96c47b..349e242 100644
--- a/reclass/errors.py
+++ b/reclass/errors.py
@@ -158,6 +158,17 @@
         return msg
 
 
+class ClassNameResolveError(InterpolationError):
+    def __init__(self, classname, nodename, uri):
+        super(ClassNameResolveError, self).__init__(msg=None, uri=uri, nodename=nodename)
+        self.name = classname
+
+    def _get_error_message(self):
+        msg = [ 'In {0}'.format(self.uri),
+                'Class name {0} not resolvable'.format(self.name) ]
+        return msg
+
+
 class InvQueryClassNotFound(InterpolationError):
 
     def __init__(self, classNotFoundError, nodename=''):
@@ -172,6 +183,19 @@
         return msg
 
 
+class InvQueryClassNameResolveError(InterpolationError):
+    def __init__(self, classNameResolveError, nodename=''):
+        super(InvQueryClassNameResolveError, self).__init__(msg=None, nodename=nodename)
+        self.classNameResolveError = classNameResolveError
+        self._traceback = self.classNameResolveError._traceback
+
+    def _get_error_message(self):
+        msg = [ 'Inventory Queries:',
+                '-> {0}'.format(self.classNameResolveError.nodename) ]
+        msg.append(self.classNameResolveError._get_error_message())
+        return msg
+
+
 class ResolveError(InterpolationError):
 
     def __init__(self, reference, uri=None, context=None):