blob: 711df5249e16b2b2f495043a0417861370b6a52d [file] [log] [blame]
Matthew Barthdb440d42017-04-17 15:49:37 -05001#!/usr/bin/env python
2
Brad Bishop34a7acd2017-04-27 23:47:23 -04003'''Phosphor DBus Monitor YAML parser and code generator.
4
5The parser workflow is broken down as follows:
6 1 - Import YAML files as native python type(s) instance(s).
7 2 - Create an instance of the Everything class from the
8 native python type instance(s) with the Everything.load
9 method.
10 3 - The Everything class constructor orchestrates conversion of the
11 native python type(s) instances(s) to render helper types.
12 Each render helper type constructor imports its attributes
13 from the native python type(s) instances(s).
14 4 - Present the converted YAML to the command processing method
15 requested by the script user.
16'''
17
Matthew Barthdb440d42017-04-17 15:49:37 -050018import os
19import sys
20import yaml
Brad Bishop34a7acd2017-04-27 23:47:23 -040021import mako.lookup
Matthew Barthdb440d42017-04-17 15:49:37 -050022from argparse import ArgumentParser
Brad Bishop34a7acd2017-04-27 23:47:23 -040023from sdbusplus.renderer import Renderer
Brad Bishop05b0c1e2017-05-23 00:24:01 -040024from sdbusplus.namedelement import NamedElement
25
26
27class InvalidConfigError(BaseException):
28 '''General purpose config file parsing error.'''
29
30 def __init__(self, path, msg):
31 '''Display configuration file with the syntax
32 error and the error message.'''
33
34 self.config = path
35 self.msg = msg
36
37
38class NotUniqueError(InvalidConfigError):
39 '''Within a config file names must be unique.
40 Display the config file with the duplicate and
41 the duplicate itself.'''
42
43 def __init__(self, path, cls, *names):
44 fmt = 'Duplicate {0}: "{1}"'
45 super(NotUniqueError, self).__init__(
46 path, fmt.format(cls, ' '.join(names)))
47
48
49def get_index(objs, cls, name, config=None):
50 '''Items are usually rendered as C++ arrays and as
51 such are stored in python lists. Given an item name
52 its class, and an optional config file filter, find
53 the item index.'''
54
55 for i, x in enumerate(objs.get(cls, [])):
56 if config and x.configfile != config:
57 continue
58 if x.name != name:
59 continue
60
61 return i
62 raise InvalidConfigError(config, 'Could not find name: "{0}"'.format(name))
63
64
65def exists(objs, cls, name, config=None):
66 '''Check to see if an item already exists in a list given
67 the item name.'''
68
69 try:
70 get_index(objs, cls, name, config)
71 except:
72 return False
73
74 return True
75
76
77def add_unique(obj, *a, **kw):
78 '''Add an item to one or more lists unless already present,
79 with an option to constrain the search to a specific config file.'''
80
81 for container in a:
82 if not exists(container, obj.cls, obj.name, config=kw.get('config')):
83 container.setdefault(obj.cls, []).append(obj)
Matthew Barthdb440d42017-04-17 15:49:37 -050084
85
Brad Bishop34a7acd2017-04-27 23:47:23 -040086class Indent(object):
87 '''Help templates be depth agnostic.'''
Matthew Barthdb440d42017-04-17 15:49:37 -050088
Brad Bishop34a7acd2017-04-27 23:47:23 -040089 def __init__(self, depth=0):
90 self.depth = depth
Matthew Barthdb440d42017-04-17 15:49:37 -050091
Brad Bishop34a7acd2017-04-27 23:47:23 -040092 def __add__(self, depth):
93 return Indent(self.depth + depth)
94
95 def __call__(self, depth):
96 '''Render an indent at the current depth plus depth.'''
97 return 4*' '*(depth + self.depth)
98
99
Brad Bishop05b0c1e2017-05-23 00:24:01 -0400100class ConfigEntry(NamedElement):
101 '''Base interface for rendered items.'''
102
103 def __init__(self, *a, **kw):
104 '''Pop the configfile/class/subclass keywords.'''
105
106 self.configfile = kw.pop('configfile')
107 self.cls = kw.pop('class')
108 self.subclass = kw.pop(self.cls)
109 super(ConfigEntry, self).__init__(**kw)
110
111 def factory(self, objs):
112 ''' Optional factory interface for subclasses to add
113 additional items to be rendered.'''
114
115 pass
116
117 def setup(self, objs):
118 ''' Optional setup interface for subclasses, invoked
119 after all factory methods have been run.'''
120
121 pass
122
123
124class Group(ConfigEntry):
125 '''Pop the members keyword for groups.'''
126
127 def __init__(self, *a, **kw):
128 self.members = kw.pop('members')
129 super(Group, self).__init__(**kw)
130
131
132class ImplicitGroup(Group):
133 '''Provide a factory method for groups whose members are
134 not explicitly declared in the config files.'''
135
136 def __init__(self, *a, **kw):
137 super(ImplicitGroup, self).__init__(**kw)
138
139 def factory(self, objs):
140 '''Create group members.'''
141
142 factory = Everything.classmap(self.subclass, 'element')
143 for m in self.members:
144 args = {
145 'class': self.subclass,
146 self.subclass: 'element',
147 'name': m
148 }
149
150 obj = factory(configfile=self.configfile, **args)
151 add_unique(obj, objs)
152 obj.factory(objs)
153
154 super(ImplicitGroup, self).factory(objs)
155
156
Brad Bishop34a7acd2017-04-27 23:47:23 -0400157class Everything(Renderer):
158 '''Parse/render entry point.'''
159
160 @staticmethod
Brad Bishop05b0c1e2017-05-23 00:24:01 -0400161 def classmap(cls, sub=None):
162 '''Map render item class and subclass entries to the appropriate
163 handler methods.'''
164
165 class_map = {
166 }
167
168 if cls not in class_map:
169 raise NotImplementedError('Unknown class: "{0}"'.format(cls))
170 if sub not in class_map[cls]:
171 raise NotImplementedError('Unknown {0} type: "{1}"'.format(
172 cls, sub))
173
174 return class_map[cls][sub]
175
176 @staticmethod
177 def load_one_yaml(path, fd, objs):
178 '''Parse a single YAML file. Parsing occurs in three phases.
179 In the first phase a factory method associated with each
180 configuration file directive is invoked. These factory
181 methods generate more factory methods. In the second
182 phase the factory methods created in the first phase
183 are invoked. In the last phase a callback is invoked on
184 each object created in phase two. Typically the callback
185 resolves references to other configuration file directives.'''
186
187 factory_objs = {}
188 for x in yaml.safe_load(fd.read()) or {}:
189
190 # Create factory object for this config file directive.
191 cls = x['class']
192 sub = x.get(cls)
193 if cls == 'group':
194 cls = '{0}group'.format(sub)
195
196 factory = Everything.classmap(cls, sub)
197 obj = factory(configfile=path, **x)
198
199 # For a given class of directive, validate the file
200 # doesn't have any duplicate names (duplicates are
201 # ok across config files).
202 if exists(factory_objs, obj.cls, obj.name, config=path):
203 raise NotUniqueError(path, cls, obj.name)
204
205 factory_objs.setdefault(cls, []).append(obj)
206 objs.setdefault(cls, []).append(obj)
207
208 for cls, items in factory_objs.items():
209 for obj in items:
210 # Add objects for template consumption.
211 obj.factory(objs)
212
213 @staticmethod
Brad Bishop34a7acd2017-04-27 23:47:23 -0400214 def load(args):
215 '''Aggregate all the YAML in the input directory
216 into a single aggregate.'''
217
Brad Bishop05b0c1e2017-05-23 00:24:01 -0400218 objs = {}
219 yaml_files = filter(
220 lambda x: x.endswith('.yaml'),
221 os.listdir(args.inputdir))
Brad Bishop34a7acd2017-04-27 23:47:23 -0400222
Brad Bishop05b0c1e2017-05-23 00:24:01 -0400223 yaml_files.sort()
Brad Bishop34a7acd2017-04-27 23:47:23 -0400224
Brad Bishop05b0c1e2017-05-23 00:24:01 -0400225 for x in yaml_files:
226 path = os.path.join(args.inputdir, x)
227 with open(path, 'r') as fd:
228 Everything.load_one_yaml(path, fd, objs)
229
230 # Configuration file directives reference each other via
231 # the name attribute; however, when rendered the reference
232 # is just an array index.
233 #
234 # At this point all objects have been created but references
235 # have not been resolved to array indicies. Instruct objects
236 # to do that now.
237 for cls, items in objs.items():
238 for obj in items:
239 obj.setup(objs)
240
241 return Everything(**objs)
Brad Bishop34a7acd2017-04-27 23:47:23 -0400242
243 def __init__(self, *a, **kw):
244 super(Everything, self).__init__(**kw)
245
246 def generate_cpp(self, loader):
247 '''Render the template with the provided data.'''
Brad Bishope3a01af2017-05-15 17:09:04 -0400248 with open(args.output, 'w') as fd:
Brad Bishop34a7acd2017-04-27 23:47:23 -0400249 fd.write(
250 self.render(
251 loader,
Brad Bishope3a01af2017-05-15 17:09:04 -0400252 args.template,
Brad Bishop34a7acd2017-04-27 23:47:23 -0400253 indent=Indent()))
Matthew Barthdb440d42017-04-17 15:49:37 -0500254
255if __name__ == '__main__':
Brad Bishop34a7acd2017-04-27 23:47:23 -0400256 script_dir = os.path.dirname(os.path.realpath(__file__))
257 valid_commands = {
258 'generate-cpp': 'generate_cpp',
259 }
260
261 parser = ArgumentParser(
262 description='Phosphor DBus Monitor (PDM) YAML '
263 'scanner and code generator.')
264
Matthew Barthdb440d42017-04-17 15:49:37 -0500265 parser.add_argument(
Brad Bishope3a01af2017-05-15 17:09:04 -0400266 "-o", "--out", dest="output",
267 default='generated.cpp',
268 help="Generated output file name and path.")
269 parser.add_argument(
270 '-t', '--template', dest='template',
Brad Bishop870c3fc2017-05-22 23:23:13 -0400271 default='generated.mako.hpp',
Brad Bishope3a01af2017-05-15 17:09:04 -0400272 help='The top level template to render.')
273 parser.add_argument(
274 '-p', '--template-path', dest='template_search',
275 default=script_dir,
276 help='The space delimited mako template search path.')
Brad Bishop34a7acd2017-04-27 23:47:23 -0400277 parser.add_argument(
278 '-d', '--dir', dest='inputdir',
279 default=os.path.join(script_dir, 'example'),
280 help='Location of files to process.')
281 parser.add_argument(
282 'command', metavar='COMMAND', type=str,
283 choices=valid_commands.keys(),
284 help='%s.' % " | ".join(valid_commands.keys()))
Matthew Barthdb440d42017-04-17 15:49:37 -0500285
Brad Bishop34a7acd2017-04-27 23:47:23 -0400286 args = parser.parse_args()
287
288 if sys.version_info < (3, 0):
289 lookup = mako.lookup.TemplateLookup(
Brad Bishope3a01af2017-05-15 17:09:04 -0400290 directories=args.template_search.split(),
Brad Bishop34a7acd2017-04-27 23:47:23 -0400291 disable_unicode=True)
292 else:
293 lookup = mako.lookup.TemplateLookup(
Brad Bishope3a01af2017-05-15 17:09:04 -0400294 directories=args.template_search.split())
Brad Bishop05b0c1e2017-05-23 00:24:01 -0400295 try:
296 function = getattr(
297 Everything.load(args),
298 valid_commands[args.command])
299 function(lookup)
300 except InvalidConfigError as e:
301 sys.stdout.write('{0}: {1}\n\n'.format(e.config, e.msg))
302 raise