b670452381a729c6f2ed01137247c5eb9b961f28
[cascardo/ovs.git] / ovsdb / ovsdb-doc
1 #! /usr/bin/python
2
3 from datetime import date
4 import getopt
5 import os
6 import re
7 import sys
8 import xml.dom.minidom
9
10 import ovs.json
11 from ovs.db import error
12 import ovs.db.schema
13
14 argv0 = sys.argv[0]
15
16 def textToNroff(s, font=r'\fR'):
17     def escape(match):
18         c = match.group(0)
19         if c.startswith('-'):
20             if c != '-' or font == r'\fB':
21                 return '\\' + c
22             else:
23                 return '-'
24         if c == '\\':
25             return r'\e'
26         elif c == '"':
27             return r'\(dq'
28         elif c == "'":
29             return r'\(cq'
30         else:
31             raise error.Error("bad escape")
32
33     # Escape - \ " ' as needed by nroff.
34     s = re.sub('(-[0-9]|[-"\'\\\\])', escape, s)
35     if s.startswith('.'):
36         s = '\\' + s
37     return s
38
39 def escapeNroffLiteral(s):
40     return r'\fB%s\fR' % textToNroff(s, r'\fB')
41
42 def inlineXmlToNroff(node, font):
43     if node.nodeType == node.TEXT_NODE:
44         return textToNroff(node.data, font)
45     elif node.nodeType == node.ELEMENT_NODE:
46         if node.tagName in ['code', 'em', 'option']:
47             s = r'\fB'
48             for child in node.childNodes:
49                 s += inlineXmlToNroff(child, r'\fB')
50             return s + font
51         elif node.tagName == 'ref':
52             s = r'\fB'
53             if node.hasAttribute('column'):
54                 s += node.attributes['column'].nodeValue
55                 if node.hasAttribute('key'):
56                     s += ':' + node.attributes['key'].nodeValue
57             elif node.hasAttribute('table'):
58                 s += node.attributes['table'].nodeValue
59             elif node.hasAttribute('group'):
60                 s += node.attributes['group'].nodeValue
61             else:
62                 raise error.Error("'ref' lacks required attributes: %s" % node.attributes.keys())
63             return s + font
64         elif node.tagName == 'var':
65             s = r'\fI'
66             for child in node.childNodes:
67                 s += inlineXmlToNroff(child, r'\fI')
68             return s + font
69         else:
70             raise error.Error("element <%s> unknown or invalid here" % node.tagName)
71     else:
72         raise error.Error("unknown node %s in inline xml" % node)
73
74 def blockXmlToNroff(nodes, para='.PP'):
75     s = ''
76     for node in nodes:
77         if node.nodeType == node.TEXT_NODE:
78             s += textToNroff(node.data)
79             s = s.lstrip()
80         elif node.nodeType == node.ELEMENT_NODE:
81             if node.tagName in ['ul', 'ol']:
82                 if s != "":
83                     s += "\n"
84                 s += ".RS\n"
85                 i = 0
86                 for liNode in node.childNodes:
87                     if (liNode.nodeType == node.ELEMENT_NODE
88                         and liNode.tagName == 'li'):
89                         i += 1
90                         if node.tagName == 'ul':
91                             s += ".IP \\(bu\n"
92                         else:
93                             s += ".IP %d. .25in\n" % i
94                         s += blockXmlToNroff(liNode.childNodes, ".IP")
95                     elif (liNode.nodeType != node.TEXT_NODE
96                           or not liNode.data.isspace()):
97                         raise error.Error("<%s> element may only have <li> children" % node.tagName)
98                 s += ".RE\n"
99             elif node.tagName == 'dl':
100                 if s != "":
101                     s += "\n"
102                 s += ".RS\n"
103                 prev = "dd"
104                 for liNode in node.childNodes:
105                     if (liNode.nodeType == node.ELEMENT_NODE
106                         and liNode.tagName == 'dt'):
107                         if prev == 'dd':
108                             s += '.TP\n'
109                         else:
110                             s += '.TQ\n'
111                         prev = 'dt'
112                     elif (liNode.nodeType == node.ELEMENT_NODE
113                           and liNode.tagName == 'dd'):
114                         if prev == 'dd':
115                             s += '.IP\n'
116                         prev = 'dd'
117                     elif (liNode.nodeType != node.TEXT_NODE
118                           or not liNode.data.isspace()):
119                         raise error.Error("<dl> element may only have <dt> and <dd> children")
120                     s += blockXmlToNroff(liNode.childNodes, ".IP")
121                 s += ".RE\n"
122             elif node.tagName == 'p':
123                 if s != "":
124                     if not s.endswith("\n"):
125                         s += "\n"
126                     s += para + "\n"
127                 s += blockXmlToNroff(node.childNodes, para)
128             elif node.tagName in ('h1', 'h2', 'h3'):
129                 if s != "":
130                     if not s.endswith("\n"):
131                         s += "\n"
132                 nroffTag = {'h1': 'SH', 'h2': 'SS', 'h3': 'ST'}[node.tagName]
133                 s += ".%s " % nroffTag
134                 for child_node in node.childNodes:
135                     s += inlineXmlToNroff(child_node, r'\fR')
136                 s += "\n"
137             else:
138                 s += inlineXmlToNroff(node, r'\fR')
139         else:
140             raise error.Error("unknown node %s in block xml" % node)
141     if s != "" and not s.endswith('\n'):
142         s += '\n'
143     return s
144
145 def typeAndConstraintsToNroff(column):
146     type = column.type.toEnglish(escapeNroffLiteral)
147     constraints = column.type.constraintsToEnglish(escapeNroffLiteral,
148                                                    textToNroff)
149     if constraints:
150         type += ", " + constraints
151     if column.unique:
152         type += " (must be unique within table)"
153     return type
154
155 def columnGroupToNroff(table, groupXml, documented_columns):
156     introNodes = []
157     columnNodes = []
158     for node in groupXml.childNodes:
159         if (node.nodeType == node.ELEMENT_NODE
160             and node.tagName in ('column', 'group')):
161             columnNodes += [node]
162         else:
163             if (columnNodes
164                 and not (node.nodeType == node.TEXT_NODE
165                          and node.data.isspace())):
166                 raise error.Error("text follows <column> or <group> inside <group>: %s" % node)
167             introNodes += [node]
168
169     summary = []
170     intro = blockXmlToNroff(introNodes)
171     body = ''
172     for node in columnNodes:
173         if node.tagName == 'column':
174             name = node.attributes['name'].nodeValue
175             documented_columns.add(name)
176             column = table.columns[name]
177             if node.hasAttribute('key'):
178                 key = node.attributes['key'].nodeValue
179                 if node.hasAttribute('type'):
180                     type_string = node.attributes['type'].nodeValue
181                     type_json = ovs.json.from_string(str(type_string))
182                     if type(type_json) in (str, unicode):
183                         raise error.Error("%s %s:%s has invalid 'type': %s" 
184                                           % (table.name, name, key, type_json))
185                     type_ = ovs.db.types.BaseType.from_json(type_json)
186                 else:
187                     type_ = column.type.value
188
189                 nameNroff = "%s : %s" % (name, key)
190
191                 if column.type.value:
192                     typeNroff = "optional %s" % column.type.value.toEnglish(
193                         escapeNroffLiteral)
194                     if (column.type.value.type == ovs.db.types.StringType and
195                         type_.type == ovs.db.types.BooleanType):
196                         # This is a little more explicit and helpful than
197                         # "containing a boolean"
198                         typeNroff += r", either \fBtrue\fR or \fBfalse\fR"
199                     else:
200                         if type_.type != column.type.value.type:
201                             type_english = type_.toEnglish()
202                             if type_english[0] in 'aeiou':
203                                 typeNroff += ", containing an %s" % type_english
204                             else:
205                                 typeNroff += ", containing a %s" % type_english
206                         constraints = (
207                             type_.constraintsToEnglish(escapeNroffLiteral,
208                                                        textToNroff))
209                         if constraints:
210                             typeNroff += ", %s" % constraints
211                 else:
212                     typeNroff = "none"
213             else:
214                 nameNroff = name
215                 typeNroff = typeAndConstraintsToNroff(column)
216             if not column.mutable:
217                 typeNroff = "immutable %s" % typeNroff
218             body += '.IP "\\fB%s\\fR: %s"\n' % (nameNroff, typeNroff)
219             body += blockXmlToNroff(node.childNodes, '.IP') + "\n"
220             summary += [('column', nameNroff, typeNroff)]
221         elif node.tagName == 'group':
222             title = node.attributes["title"].nodeValue
223             subSummary, subIntro, subBody = columnGroupToNroff(
224                 table, node, documented_columns)
225             summary += [('group', title, subSummary)]
226             body += '.ST "%s:"\n' % textToNroff(title)
227             body += subIntro + subBody
228         else:
229             raise error.Error("unknown element %s in <table>" % node.tagName)
230     return summary, intro, body
231
232 def tableSummaryToNroff(summary, level=0):
233     s = ""
234     for type, name, arg in summary:
235         if type == 'column':
236             s += ".TQ %.2fin\n\\fB%s\\fR\n%s\n" % (3 - level * .25, name, arg)
237         else:
238             s += ".TQ .25in\n\\fI%s:\\fR\n.RS .25in\n" % name
239             s += tableSummaryToNroff(arg, level + 1)
240             s += ".RE\n"
241     return s
242
243 def tableToNroff(schema, tableXml):
244     tableName = tableXml.attributes['name'].nodeValue
245     table = schema.tables[tableName]
246
247     documented_columns = set()
248     s = """.bp
249 .SH "%s TABLE"
250 """ % tableName
251     summary, intro, body = columnGroupToNroff(table, tableXml,
252                                               documented_columns)
253     s += intro
254     s += '.SS "Summary:\n'
255     s += tableSummaryToNroff(summary)
256     s += '.SS "Details:\n'
257     s += body
258
259     schema_columns = set(table.columns.keys())
260     undocumented_columns = schema_columns - documented_columns
261     for column in undocumented_columns:
262         raise error.Error("table %s has undocumented column %s"
263                           % (tableName, column))
264
265     return s
266
267 def docsToNroff(schemaFile, xmlFile, erFile, title=None, version=None):
268     schema = ovs.db.schema.DbSchema.from_json(ovs.json.from_file(schemaFile))
269     doc = xml.dom.minidom.parse(xmlFile).documentElement
270
271     schemaDate = os.stat(schemaFile).st_mtime
272     xmlDate = os.stat(xmlFile).st_mtime
273     d = date.fromtimestamp(max(schemaDate, xmlDate))
274
275     if title == None:
276         title = schema.name
277
278     if version == None:
279         version = "UNKNOWN"
280
281     # Putting '\" p as the first line tells "man" that the manpage
282     # needs to be preprocessed by "pic".
283     s = r''''\" p
284 .TH "%s" 5 " DB Schema %s" "Open vSwitch %s" "Open vSwitch Manual"
285 .\" -*- nroff -*-
286 .de TQ
287 .  br
288 .  ns
289 .  TP "\\$1"
290 ..
291 .de ST
292 .  PP
293 .  RS -0.15in
294 .  I "\\$1"
295 .  RE
296 ..
297 .SH NAME
298 %s \- %s database schema
299 .PP
300 ''' % (title, schema.version, version, textToNroff(schema.name), schema.name)
301
302     tables = ""
303     introNodes = []
304     tableNodes = []
305     summary = []
306     for dbNode in doc.childNodes:
307         if (dbNode.nodeType == dbNode.ELEMENT_NODE
308             and dbNode.tagName == "table"):
309             tableNodes += [dbNode]
310
311             name = dbNode.attributes['name'].nodeValue
312             if dbNode.hasAttribute("title"):
313                 title = dbNode.attributes['title'].nodeValue
314             else:
315                 title = name + " configuration."
316             summary += [(name, title)]
317         else:
318             introNodes += [dbNode]
319
320     documented_tables = set((name for (name, title) in summary))
321     schema_tables = set(schema.tables.keys())
322     undocumented_tables = schema_tables - documented_tables
323     for table in undocumented_tables:
324         raise error.Error("undocumented table %s" % table)
325
326     s += blockXmlToNroff(introNodes) + "\n"
327
328     s += r"""
329 .SH "TABLE SUMMARY"
330 .PP
331 The following list summarizes the purpose of each of the tables in the
332 \fB%s\fR database.  Each table is described in more detail on a later
333 page.
334 .IP "Table" 1in
335 Purpose
336 """ % schema.name
337     for name, title in summary:
338         s += r"""
339 .TQ 1in
340 \fB%s\fR
341 %s
342 """ % (name, textToNroff(title))
343
344     if erFile:
345         s += """
346 .\\" check if in troff mode (TTY)
347 .if t \{
348 .bp
349 .SH "TABLE RELATIONSHIPS"
350 .PP
351 The following diagram shows the relationship among tables in the
352 database.  Each node represents a table.  Tables that are part of the
353 ``root set'' are shown with double borders.  Each edge leads from the
354 table that contains it and points to the table that its value
355 represents.  Edges are labeled with their column names, followed by a
356 constraint on the number of allowed values: \\fB?\\fR for zero or one,
357 \\fB*\\fR for zero or more, \\fB+\\fR for one or more.  Thick lines
358 represent strong references; thin lines represent weak references.
359 .RS -1in
360 """
361         erStream = open(erFile, "r")
362         for line in erStream:
363             s += line + '\n'
364         erStream.close()
365         s += ".RE\\}\n"
366
367     for node in tableNodes:
368         s += tableToNroff(schema, node) + "\n"
369     return s
370
371 def usage():
372     print """\
373 %(argv0)s: ovsdb schema documentation generator
374 Prints documentation for an OVSDB schema as an nroff-formatted manpage.
375 usage: %(argv0)s [OPTIONS] SCHEMA XML
376 where SCHEMA is an OVSDB schema in JSON format
377   and XML is OVSDB documentation in XML format.
378
379 The following options are also available:
380   --er-diagram=DIAGRAM.PIC    include E-R diagram from DIAGRAM.PIC
381   --title=TITLE               use TITLE as title instead of schema name
382   --version=VERSION           use VERSION to display on document footer
383   -h, --help                  display this help message\
384 """ % {'argv0': argv0}
385     sys.exit(0)
386
387 if __name__ == "__main__":
388     try:
389         try:
390             options, args = getopt.gnu_getopt(sys.argv[1:], 'hV',
391                                               ['er-diagram=', 'title=',
392                                                'version=', 'help'])
393         except getopt.GetoptError, geo:
394             sys.stderr.write("%s: %s\n" % (argv0, geo.msg))
395             sys.exit(1)
396
397         er_diagram = None
398         title = None
399         version = None
400         for key, value in options:
401             if key == '--er-diagram':
402                 er_diagram = value
403             elif key == '--title':
404                 title = value
405             elif key == '--version':
406                 version = value
407             elif key in ['-h', '--help']:
408                 usage()
409             else:
410                 sys.exit(0)
411
412         if len(args) != 2:
413             sys.stderr.write("%s: exactly 2 non-option arguments required "
414                              "(use --help for help)\n" % argv0)
415             sys.exit(1)
416
417         # XXX we should warn about undocumented tables or columns
418         s = docsToNroff(args[0], args[1], er_diagram, title, version)
419         for line in s.split("\n"):
420             line = line.strip()
421             if len(line):
422                 print line
423
424     except error.Error, e:
425         sys.stderr.write("%s: %s\n" % (argv0, e.msg))
426         sys.exit(1)
427
428 # Local variables:
429 # mode: python
430 # End: