Parse match rules

The 'pimgen.py' parser will parse one or more yaml files containing
inventory manager match rules and generate the required c++ header
file.

Change-Id: Id3b116450bd56487e266590dd339b93db9bc7d27
Signed-off-by: Brad Bishop <bradleyb@fuzziesquirrel.com>
diff --git a/README.md b/README.md
index 5318177..a06a572 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,47 @@
-## To Build
+Phosphor Inventory Manager (PIM) is an implementation of the
+xyz.openbmc_project.Inventory.Manager DBus interface, and supporting tools.
+PIM uses a combination of build-time YAML files and run-time calls to the
+Notify method of the Manager interface to provide a generalized inventory
+state management solution.
+
+## YAML
+PIM includes a YAML parser (pimgen.py).  For PIM to do anything useful, a
+set of YAML files must be provided externally that tell it what to do.
+An example can be found in the examples directory.
+
+The following top level YAML tags are supported:
+
+* description - An optional description of the file.
+* events - One or more events that PIM should monitor.
+
+----
+**events**
+Supported event tags are:
+
+* name - A globally unique event name.
+* type - The event type.  Supported types are: *match*.
+
+Subsequent tags are defined by the event type.
+
+----
+**match**
+Supported match tags are:
+
+* signature - A DBus match specification.
+
+----
+
+## Building
+After running pimgen.py, build PIM using the following steps:
+
 ```
-To build this package, do the following steps:
+    ./bootstrap.sh
+    ./configure ${CONFIGURE_FLAGS}
+    make
+```
 
-    1. ./bootstrap.sh
-    2. ./configure ${CONFIGURE_FLAGS}
-    3. make
+To clean the repository run:
 
-To full clean the repository again run `./bootstrap.sh clean`.
+```
+ ./bootstrap.sh clean
 ```
diff --git a/examples/match1.yaml b/examples/match1.yaml
new file mode 100644
index 0000000..9388a90
--- /dev/null
+++ b/examples/match1.yaml
@@ -0,0 +1,14 @@
+description: >
+    An example inventory match rule.
+
+events:
+    - name: Example Match(1)
+      description: >
+          Matches any PropertiesChanged signal.
+      type: match
+      signature:
+          type: signal
+          interface: org.freedesktop.DBus.Properties
+          member: PropertiesChanged
+
+# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
diff --git a/examples/match2.yaml b/examples/match2.yaml
new file mode 100644
index 0000000..3e11531
--- /dev/null
+++ b/examples/match2.yaml
@@ -0,0 +1,16 @@
+description: >
+    An example inventory match rule.
+
+events:
+    - name: Example Match(2)
+      description: >
+          Matches any PropertiesChanged signal emitted
+          by /xyz/openbmc_project/testing.
+      type: match
+      signature:
+          type: signal
+          path: /xyz/openbmc_project/testing
+          interface: org.freedesktop.DBus.Properties
+          member: PropertiesChanged
+
+# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
diff --git a/pimgen.py b/pimgen.py
new file mode 100755
index 0000000..f59e320
--- /dev/null
+++ b/pimgen.py
@@ -0,0 +1,137 @@
+#!/usr/bin/env python
+
+import sys
+import os
+import re
+import argparse
+import yaml
+
+valid_c_name_pattern = re.compile('[\W_]+')
+ignore_list = ['description']
+all_names = []
+
+
+def get_parser(x, fmt, lmbda=lambda x: x.capitalize()):
+    try:
+        return getattr(
+            sys.modules[__name__],
+            '%s' % (fmt.format(lmbda(x))))
+    except AttributeError:
+        raise NotImplementedError("Don't know how to parse '%s'" % x)
+
+
+class RenderList(list):
+    def __init__(self, renderers):
+        self.extend(renderers)
+
+    def __call__(self, fd):
+        for x in self:
+            x(fd)
+
+
+class ParseList(list):
+    def __init__(self, parsers):
+        self.extend(parsers)
+
+    def __call__(self):
+        return RenderList([x() for x in self])
+
+
+class MatchRender(object):
+    def __init__(self, name, signature):
+        self.name = valid_c_name_pattern.sub('_', name).lower()
+        self.signature = signature
+
+        if self.name in all_names:
+            raise RuntimeError('The name "%s" is not unique.' % name)
+        else:
+            all_names.append(self.name)
+
+    def __call__(self, fd):
+        sig = ['%s=\'%s\'' % (k, v) for k, v in self.signature.iteritems()]
+        sig = ['%s,' % x for x in sig[:-1]] + [sig[-1]]
+        sig = ['"%s"' % x for x in sig]
+        sig = ['%s\n' % x for x in sig[:-1]] + [sig[-1]]
+
+        fd.write('    {\n')
+        fd.write('        "%s",\n' % self.name)
+        fd.write('        {\n')
+        for s in sig:
+            fd.write('            %s' % s)
+        fd.write(',\n')
+        fd.write('        },\n')
+        fd.write('    },\n')
+
+
+class MatchEventParse(object):
+    def __init__(self, match):
+        self.name = match['name']
+        self.signature = match['signature']
+
+    def __call__(self):
+        return MatchRender(
+            self.name,
+            self.signature)
+
+
+class EventsParse(object):
+    def __init__(self, event):
+        self.delegate = None
+        cls = event['type']
+        if cls not in ignore_list:
+            fmt = '{0}EventParse'
+            self.delegate = get_parser(cls, fmt)(event)
+
+    def __call__(self):
+        if self.delegate:
+            return self.delegate()
+        return lambda x: None
+
+
+class DictParse(ParseList):
+    def __init__(self, data):
+        fmt = '{0}Parse'
+        parse = set(data.iterkeys()).difference(ignore_list)
+        ParseList.__init__(
+            self, [get_parser(x, fmt)(*data[x]) for x in parse])
+
+
+if __name__ == '__main__':
+    parser = argparse.ArgumentParser(
+        description='Phosphor Inventory Manager (PIM) YAML '
+        'scanner and code generator.')
+    parser.add_argument(
+        '-o', '--output', dest='output',
+        default='generated.hpp', help='Output file name.')
+    parser.add_argument(
+        '-d', '--dir', dest='inputdir',
+        default='examples', help='Location of files to process.')
+
+    args = parser.parse_args()
+
+    yaml_files = filter(
+        lambda x: x.endswith('.yaml'),
+        os.listdir(args.inputdir))
+
+    def get_parsers(x):
+        with open(os.path.join(args.inputdir, x), 'r') as fd:
+            return DictParse(yaml.load(fd.read()))
+
+    head = """// This file was auto generated.  Do not edit.
+
+#pragma once
+
+const Manager::Events Manager::_events{
+"""
+
+    tail = """};
+"""
+
+    r = ParseList([get_parsers(x) for x in yaml_files])()
+    r.insert(0, lambda x: x.write(head))
+    r.append(lambda x: x.write(tail))
+
+    with open(args.output, 'w') as fd:
+        r(fd)
+
+# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4