pdmgen: Add core parsing logic
Add logic to parse each yaml file in three stages.
1 - Invoke factory methods for config file directives, generating
more factory methods.
2 - Invoke factory methods from 1 for the full set of renderable
items.
3 - Run 'setup' on all renderable items to resolve item cross
references.
Change-Id: I428a9ae1c41cf65e1efc05f3ec7177375822d772
Signed-off-by: Brad Bishop <bradleyb@fuzziesquirrel.com>
diff --git a/src/pdmgen.py b/src/pdmgen.py
index 6476a95..711df52 100755
--- a/src/pdmgen.py
+++ b/src/pdmgen.py
@@ -21,6 +21,66 @@
import mako.lookup
from argparse import ArgumentParser
from sdbusplus.renderer import Renderer
+from sdbusplus.namedelement import NamedElement
+
+
+class InvalidConfigError(BaseException):
+ '''General purpose config file parsing error.'''
+
+ def __init__(self, path, msg):
+ '''Display configuration file with the syntax
+ error and the error message.'''
+
+ self.config = path
+ self.msg = msg
+
+
+class NotUniqueError(InvalidConfigError):
+ '''Within a config file names must be unique.
+ Display the config file with the duplicate and
+ the duplicate itself.'''
+
+ def __init__(self, path, cls, *names):
+ fmt = 'Duplicate {0}: "{1}"'
+ super(NotUniqueError, self).__init__(
+ path, fmt.format(cls, ' '.join(names)))
+
+
+def get_index(objs, cls, name, config=None):
+ '''Items are usually rendered as C++ arrays and as
+ such are stored in python lists. Given an item name
+ its class, and an optional config file filter, find
+ the item index.'''
+
+ for i, x in enumerate(objs.get(cls, [])):
+ if config and x.configfile != config:
+ continue
+ if x.name != name:
+ continue
+
+ return i
+ raise InvalidConfigError(config, 'Could not find name: "{0}"'.format(name))
+
+
+def exists(objs, cls, name, config=None):
+ '''Check to see if an item already exists in a list given
+ the item name.'''
+
+ try:
+ get_index(objs, cls, name, config)
+ except:
+ return False
+
+ return True
+
+
+def add_unique(obj, *a, **kw):
+ '''Add an item to one or more lists unless already present,
+ with an option to constrain the search to a specific config file.'''
+
+ for container in a:
+ if not exists(container, obj.cls, obj.name, config=kw.get('config')):
+ container.setdefault(obj.cls, []).append(obj)
class Indent(object):
@@ -37,24 +97,148 @@
return 4*' '*(depth + self.depth)
+class ConfigEntry(NamedElement):
+ '''Base interface for rendered items.'''
+
+ def __init__(self, *a, **kw):
+ '''Pop the configfile/class/subclass keywords.'''
+
+ self.configfile = kw.pop('configfile')
+ self.cls = kw.pop('class')
+ self.subclass = kw.pop(self.cls)
+ super(ConfigEntry, self).__init__(**kw)
+
+ def factory(self, objs):
+ ''' Optional factory interface for subclasses to add
+ additional items to be rendered.'''
+
+ pass
+
+ def setup(self, objs):
+ ''' Optional setup interface for subclasses, invoked
+ after all factory methods have been run.'''
+
+ pass
+
+
+class Group(ConfigEntry):
+ '''Pop the members keyword for groups.'''
+
+ def __init__(self, *a, **kw):
+ self.members = kw.pop('members')
+ super(Group, self).__init__(**kw)
+
+
+class ImplicitGroup(Group):
+ '''Provide a factory method for groups whose members are
+ not explicitly declared in the config files.'''
+
+ def __init__(self, *a, **kw):
+ super(ImplicitGroup, self).__init__(**kw)
+
+ def factory(self, objs):
+ '''Create group members.'''
+
+ factory = Everything.classmap(self.subclass, 'element')
+ for m in self.members:
+ args = {
+ 'class': self.subclass,
+ self.subclass: 'element',
+ 'name': m
+ }
+
+ obj = factory(configfile=self.configfile, **args)
+ add_unique(obj, objs)
+ obj.factory(objs)
+
+ super(ImplicitGroup, self).factory(objs)
+
+
class Everything(Renderer):
'''Parse/render entry point.'''
@staticmethod
+ def classmap(cls, sub=None):
+ '''Map render item class and subclass entries to the appropriate
+ handler methods.'''
+
+ class_map = {
+ }
+
+ if cls not in class_map:
+ raise NotImplementedError('Unknown class: "{0}"'.format(cls))
+ if sub not in class_map[cls]:
+ raise NotImplementedError('Unknown {0} type: "{1}"'.format(
+ cls, sub))
+
+ return class_map[cls][sub]
+
+ @staticmethod
+ def load_one_yaml(path, fd, objs):
+ '''Parse a single YAML file. Parsing occurs in three phases.
+ In the first phase a factory method associated with each
+ configuration file directive is invoked. These factory
+ methods generate more factory methods. In the second
+ phase the factory methods created in the first phase
+ are invoked. In the last phase a callback is invoked on
+ each object created in phase two. Typically the callback
+ resolves references to other configuration file directives.'''
+
+ factory_objs = {}
+ for x in yaml.safe_load(fd.read()) or {}:
+
+ # Create factory object for this config file directive.
+ cls = x['class']
+ sub = x.get(cls)
+ if cls == 'group':
+ cls = '{0}group'.format(sub)
+
+ factory = Everything.classmap(cls, sub)
+ obj = factory(configfile=path, **x)
+
+ # For a given class of directive, validate the file
+ # doesn't have any duplicate names (duplicates are
+ # ok across config files).
+ if exists(factory_objs, obj.cls, obj.name, config=path):
+ raise NotUniqueError(path, cls, obj.name)
+
+ factory_objs.setdefault(cls, []).append(obj)
+ objs.setdefault(cls, []).append(obj)
+
+ for cls, items in factory_objs.items():
+ for obj in items:
+ # Add objects for template consumption.
+ obj.factory(objs)
+
+ @staticmethod
def load(args):
'''Aggregate all the YAML in the input directory
into a single aggregate.'''
- if os.path.exists(args.inputdir):
- yaml_files = filter(
- lambda x: x.endswith('.yaml'),
- os.listdir(args.inputdir))
+ objs = {}
+ yaml_files = filter(
+ lambda x: x.endswith('.yaml'),
+ os.listdir(args.inputdir))
- for x in yaml_files:
- with open(os.path.join(args.inputdir, x), 'r') as fd:
- yaml.safe_load(fd.read())
+ yaml_files.sort()
- return Everything()
+ for x in yaml_files:
+ path = os.path.join(args.inputdir, x)
+ with open(path, 'r') as fd:
+ Everything.load_one_yaml(path, fd, objs)
+
+ # Configuration file directives reference each other via
+ # the name attribute; however, when rendered the reference
+ # is just an array index.
+ #
+ # At this point all objects have been created but references
+ # have not been resolved to array indicies. Instruct objects
+ # to do that now.
+ for cls, items in objs.items():
+ for obj in items:
+ obj.setup(objs)
+
+ return Everything(**objs)
def __init__(self, *a, **kw):
super(Everything, self).__init__(**kw)
@@ -66,7 +250,6 @@
self.render(
loader,
args.template,
- events={},
indent=Indent()))
if __name__ == '__main__':
@@ -109,8 +292,11 @@
else:
lookup = mako.lookup.TemplateLookup(
directories=args.template_search.split())
-
- function = getattr(
- Everything.load(args),
- valid_commands[args.command])
- function(lookup)
+ try:
+ function = getattr(
+ Everything.load(args),
+ valid_commands[args.command])
+ function(lookup)
+ except InvalidConfigError as e:
+ sys.stdout.write('{0}: {1}\n\n'.format(e.config, e.msg))
+ raise
diff --git a/src/templates/generated.mako.hpp b/src/templates/generated.mako.hpp
index a2e1adf..2a9c2d1 100644
--- a/src/templates/generated.mako.hpp
+++ b/src/templates/generated.mako.hpp
@@ -2,6 +2,11 @@
/* This is an auto generated file. Do not edit. */
#pragma once
+#include <array>
+#include <string>
+
+using namespace std::string_literals;
+
namespace phosphor
{
namespace dbus