Rewrite pimgen in the image of sdbus++

Almost complete rewrite of pimgen and the mako template.

Adopt the sdbus++ application structure.  The hope is that
this will encourage additional sdbusplus features deemed
general purpose enough for reuse with other applications.

Change-Id: I007ff9f5fc9a64f0465159bd1301475ada139d55
Signed-off-by: Brad Bishop <bradleyb@fuzziesquirrel.com>
diff --git a/pimgen.py b/pimgen.py
index 072f7d6..e5771d6 100755
--- a/pimgen.py
+++ b/pimgen.py
@@ -1,122 +1,295 @@
 #!/usr/bin/env python
 
+'''Phosphor Inventory Manager YAML parser and code generator.
+
+The parser workflow is broken down as follows:
+  1 - Import YAML files as native python type(s) instance(s).
+  2 - Create an instance of the Everything class from the
+        native python type instance(s) with the Everything.load
+        method.
+  3 - The Everything class constructor orchestrates conversion of the
+        native python type(s) instances(s) to render helper types.
+        Each render helper type constructor imports its attributes
+        from the native python type(s) instances(s).
+  4 - Present the converted YAML to the command processing method
+        requested by the script user.
+'''
+
 import sys
 import os
-import re
 import argparse
-import yaml
 import subprocess
-from mako.template import Template
-
-valid_c_name_pattern = re.compile('[\W_]+')
+import yaml
+import mako.lookup
+import sdbusplus.property
+from sdbusplus.namedelement import NamedElement
+from sdbusplus.renderer import Renderer
 
 
-def parse_event(e):
-    e['name'] = valid_c_name_pattern.sub('_', e['name']).lower()
-    if e.get('filter') is None:
-        e.setdefault('filter', {}).setdefault('type', 'none')
-    if e.get('action') is None:
-        e.setdefault('action', {}).setdefault('type', 'noop')
-    return e
+class Interface(list):
+    '''Provide various interface transformations.'''
+
+    def __init__(self, iface):
+        super(Interface, self).__init__(iface.split('.'))
+
+    def namespace(self):
+        '''Represent as an sdbusplus namespace.'''
+        return '::'.join(['sdbusplus'] + self[:-1] + ['server', self[-1]])
+
+    def header(self):
+        '''Represent as an sdbusplus server binding header.'''
+        return os.sep.join(self + ['server.hpp'])
+
+    def __str__(self):
+        return '.'.join(self)
 
 
-def get_interfaces(args):
-    interfaces_dir = os.path.join(args.inputdir, 'interfaces.d')
-    yaml_files = filter(
-        lambda x: x.endswith('.yaml'),
-        os.listdir(interfaces_dir))
+class Argument(sdbusplus.property.Property):
+    '''Bridge sdbusplus property typenames to syntatically correct c++.'''
 
-    interfaces = []
-    for x in yaml_files:
-        with open(os.path.join(interfaces_dir, x), 'r') as fd:
-            for i in yaml.load(fd.read()):
-                interfaces.append(i)
+    def __init__(self, **kw):
+        self.value = kw.pop('value')
+        super(Argument, self).__init__(**kw)
 
-    return interfaces
+    def cppArg(self):
+        '''Transform string types to c++ string constants.'''
+        if self.typeName == 'string':
+            return '"%s"' % self.value
+
+        return self.value
 
 
-def list_interfaces(args):
-    print ' '.join(get_interfaces(args))
+class MethodCall(NamedElement, Renderer):
+    '''Render syntatically correct c++ method calls.'''
+
+    def __init__(self, **kw):
+        self.namespace = kw.pop('namespace', [])
+        self.pointer = kw.pop('pointer', False)
+        self.args = \
+            [Argument(**x) for x in kw.pop('args', [])]
+        super(MethodCall, self).__init__(**kw)
+
+    def bare_method(self):
+        '''Provide the method name and encompassing
+        namespace without any arguments.'''
+        return '::'.join(self.namespace + [self.name])
 
 
-def generate_cpp(args):
-    # Aggregate all the event YAML in the events.d directory
-    # into a single list of events.
-    events_dir = os.path.join(args.inputdir, 'events.d')
-    yaml_files = filter(
-        lambda x: x.endswith('.yaml'),
-        os.listdir(events_dir))
+class Filter(MethodCall):
+    '''Provide common attributes for any filter.'''
 
-    events = []
-    for x in yaml_files:
-        with open(os.path.join(events_dir, x), 'r') as fd:
-            for e in yaml.load(fd.read()).get('events', {}):
-                events.append(parse_event(e))
+    def __init__(self, **kw):
+        kw['namespace'] = ['filters']
+        super(Filter, self).__init__(**kw)
 
-    # Aggregate all the interface YAML in the interfaces.d
-    # directory into a single list of interfaces.
-    template = os.path.join(script_dir, 'generated.mako.cpp')
-    t = Template(filename=template)
 
-    interfaces = get_interfaces(args)
+class Action(MethodCall):
+    '''Provide common attributes for any action.'''
 
-    # Render the template with the provided events and interfaces.
-    template = os.path.join(script_dir, 'generated.mako.cpp')
-    t = Template(filename=template)
-    with open(os.path.join(args.outputdir, 'generated.cpp'), 'w') as fd:
-        fd.write(
-            t.render(
-                interfaces=interfaces,
-                events=events))
+    def __init__(self, **kw):
+        kw['namespace'] = ['actions']
+        super(Action, self).__init__(**kw)
 
-    # Invoke sdbus++ to generate any extra interface bindings for
-    # extra interfaces that aren't defined externally.
-    yaml_files = []
-    extra_ifaces_dir = os.path.join(args.inputdir, 'extra_interfaces.d')
-    if os.path.exists(extra_ifaces_dir):
-        for directory, _, files in os.walk(extra_ifaces_dir):
-            if not files:
-                continue
 
-            yaml_files += map(
-                lambda f: os.path.relpath(
-                    os.path.join(directory, f),
-                    extra_ifaces_dir),
-                filter(lambda f: f.endswith('.interface.yaml'), files))
+class DbusSignature(NamedElement, Renderer):
+    '''Represent a dbus signal match signature.'''
 
-    genfiles = {
-        'server-cpp': lambda x: '%s.cpp' % (
-            x.replace(os.sep, '.')),
-        'server-header': lambda x: os.path.join(
-            os.path.join(
-                *x.split('.')), 'server.hpp')
+    def __init__(self, **kw):
+        self.sig = {x: y for x, y in kw.iteritems()}
+        kw.clear()
+        super(DbusSignature, self).__init__(**kw)
+
+
+class DestroyObject(Action):
+    '''Render a destroyObject action.'''
+
+    def __init__(self, **kw):
+        mapped = kw.pop('args')
+        kw['args'] = [
+            {'value': mapped['path'], 'type':'string'},
+        ]
+        super(DestroyObject, self).__init__(**kw)
+
+
+class NoopAction(Action):
+    '''Render a noop action.'''
+
+    def __init__(self, **kw):
+        kw['pointer'] = True
+        super(NoopAction, self).__init__(**kw)
+
+
+class NoopFilter(Filter):
+    '''Render a noop filter.'''
+
+    def __init__(self, **kw):
+        kw['pointer'] = True
+        super(NoopFilter, self).__init__(**kw)
+
+
+class PropertyChanged(Filter):
+    '''Render a propertyChanged filter.'''
+
+    def __init__(self, **kw):
+        mapped = kw.pop('args')
+        kw['args'] = [
+            {'value': mapped['interface'], 'type':'string'},
+            {'value': mapped['property'], 'type':'string'},
+            mapped['value']
+        ]
+        super(PropertyChanged, self).__init__(**kw)
+
+
+class Event(NamedElement, Renderer):
+    '''Render an inventory manager event.'''
+
+    action_map = {
+        'noop': NoopAction,
+        'destroyObject': DestroyObject,
     }
 
-    for i in yaml_files:
-        iface = i.replace('.interface.yaml', '').replace(os.sep, '.')
-        for process, f in genfiles.iteritems():
+    def __init__(self, **kw):
+        self.cls = kw.pop('type')
+        self.actions = \
+            [self.action_map[x['name']](**x)
+                for x in kw.pop('actions', [{'name': 'noop'}])]
+        super(Event, self).__init__(**kw)
 
-            dest = os.path.join(args.outputdir, f(iface))
-            parent = os.path.dirname(dest)
-            if parent and not os.path.exists(parent):
-                os.makedirs(parent)
 
-            with open(dest, 'w') as fd:
-                subprocess.call([
-                    'sdbus++',
-                    '-r',
-                    extra_ifaces_dir,
-                    'interface',
-                    process,
-                    iface],
-                    stdout=fd)
+class MatchEvent(Event):
+    '''Associate one or more dbus signal match signatures with
+    a filter.'''
+
+    filter_map = {
+        'none': NoopFilter,
+        'propertyChangedTo': PropertyChanged,
+    }
+
+    def __init__(self, **kw):
+        self.signatures = \
+            [DbusSignature(**x) for x in kw.pop('signatures', [])]
+        self.filters = \
+            [self.filter_map[x['name']](**x)
+                for x in kw.pop('filters', [{'name': 'none'}])]
+        super(MatchEvent, self).__init__(**kw)
+
+
+class Everything(Renderer):
+    '''Parse/render entry point.'''
+
+    class_map = {
+        'match': MatchEvent,
+    }
+
+    @staticmethod
+    def load(args):
+        # Invoke sdbus++ to generate any extra interface bindings for
+        # extra interfaces that aren't defined externally.
+        yaml_files = []
+        extra_ifaces_dir = os.path.join(args.inputdir, 'extra_interfaces.d')
+        if os.path.exists(extra_ifaces_dir):
+            for directory, _, files in os.walk(extra_ifaces_dir):
+                if not files:
+                    continue
+
+                yaml_files += map(
+                    lambda f: os.path.relpath(
+                        os.path.join(directory, f),
+                        extra_ifaces_dir),
+                    filter(lambda f: f.endswith('.interface.yaml'), files))
+
+        genfiles = {
+            'server-cpp': lambda x: '%s.cpp' % (
+                x.replace(os.sep, '.')),
+            'server-header': lambda x: os.path.join(
+                os.path.join(
+                    *x.split('.')), 'server.hpp')
+        }
+
+        for i in yaml_files:
+            iface = i.replace('.interface.yaml', '').replace(os.sep, '.')
+            for process, f in genfiles.iteritems():
+
+                dest = os.path.join(args.outputdir, f(iface))
+                parent = os.path.dirname(dest)
+                if parent and not os.path.exists(parent):
+                    os.makedirs(parent)
+
+                with open(dest, 'w') as fd:
+                    subprocess.call([
+                        'sdbus++',
+                        '-r',
+                        extra_ifaces_dir,
+                        'interface',
+                        process,
+                        iface],
+                        stdout=fd)
+
+        # Aggregate all the event YAML in the events.d directory
+        # into a single list of events.
+
+        events_dir = os.path.join(args.inputdir, 'events.d')
+        yaml_files = filter(
+            lambda x: x.endswith('.yaml'),
+            os.listdir(events_dir))
+
+        events = []
+        for x in yaml_files:
+            with open(os.path.join(events_dir, x), 'r') as fd:
+                for e in yaml.load(fd.read()).get('events', {}):
+                    events.append(e)
+
+        return Everything(
+            *events,
+            interfaces=Everything.get_interfaces(args))
+
+    @staticmethod
+    def get_interfaces(args):
+        '''Aggregate all the interface YAML in the interfaces.d
+        directory into a single list of interfaces.'''
+
+        interfaces_dir = os.path.join(args.inputdir, 'interfaces.d')
+        yaml_files = filter(
+            lambda x: x.endswith('.yaml'),
+            os.listdir(interfaces_dir))
+
+        interfaces = []
+        for x in yaml_files:
+            with open(os.path.join(interfaces_dir, x), 'r') as fd:
+                for i in yaml.load(fd.read()):
+                    interfaces.append(i)
+
+        return interfaces
+
+    def __init__(self, *a, **kw):
+        self.interfaces = \
+            [Interface(x) for x in kw.pop('interfaces', [])]
+        self.events = [
+            self.class_map[x['type']](**x) for x in a]
+        super(Everything, self).__init__(**kw)
+
+    def list_interfaces(self, *a):
+        print ' '.join([str(i) for i in self.interfaces])
+
+    def generate_cpp(self, loader):
+        '''Render the template with the provided events and interfaces.'''
+        with open(os.path.join(
+                args.outputdir,
+                'generated.cpp'), 'w') as fd:
+            fd.write(
+                self.render(
+                    loader,
+                    'generated.mako.cpp',
+                    events=self.events,
+                    interfaces=self.interfaces))
 
 
 if __name__ == '__main__':
     script_dir = os.path.dirname(os.path.realpath(__file__))
     valid_commands = {
         'generate-cpp': 'generate_cpp',
-        'list-interfaces': 'list_interfaces'}
+        'list-interfaces': 'list_interfaces'
+    }
 
     parser = argparse.ArgumentParser(
         description='Phosphor Inventory Manager (PIM) YAML '
@@ -134,8 +307,19 @@
         help='Command to run.')
 
     args = parser.parse_args()
-    function = getattr(sys.modules[__name__], valid_commands[args.command])
-    function(args)
+
+    if sys.version_info < (3, 0):
+        lookup = mako.lookup.TemplateLookup(
+            directories=[script_dir],
+            disable_unicode=True)
+    else:
+        lookup = mako.lookup.TemplateLookup(
+            directories=[script_dir])
+
+    function = getattr(
+        Everything.load(args),
+        valid_commands[args.command])
+    function(lookup)
 
 
 # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4