lib/hash.h: fix hash_2words
[sliver-openvswitch.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):
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             column = table.columns[name]
176             if node.hasAttribute('key'):
177                 key = node.attributes['key'].nodeValue
178                 if node.hasAttribute('type'):
179                     type_string = node.attributes['type'].nodeValue
180                     type_json = ovs.json.from_string(str(type_string))
181                     if type(type_json) in (str, unicode):
182                         raise error.Error("%s %s:%s has invalid 'type': %s" 
183                                           % (table.name, name, key, type_json))
184                     type_ = ovs.db.types.BaseType.from_json(type_json)
185                 else:
186                     type_ = column.type.value
187
188                 nameNroff = "%s : %s" % (name, key)
189
190                 if column.type.value:
191                     typeNroff = "optional %s" % column.type.value.toEnglish(
192                         escapeNroffLiteral)
193                     if (column.type.value.type == ovs.db.types.StringType and
194                         type_.type == ovs.db.types.BooleanType):
195                         # This is a little more explicit and helpful than
196                         # "containing a boolean"
197                         typeNroff += r", either \fBtrue\fR or \fBfalse\fR"
198                     else:
199                         if type_.type != column.type.value.type:
200                             type_english = type_.toEnglish()
201                             if type_english[0] in 'aeiou':
202                                 typeNroff += ", containing an %s" % type_english
203                             else:
204                                 typeNroff += ", containing a %s" % type_english
205                         constraints = (
206                             type_.constraintsToEnglish(escapeNroffLiteral,
207                                                        textToNroff))
208                         if constraints:
209                             typeNroff += ", %s" % constraints
210                 else:
211                     typeNroff = "none"
212             else:
213                 nameNroff = name
214                 typeNroff = typeAndConstraintsToNroff(column)
215             if not column.mutable:
216                 typeNroff = "immutable %s" % typeNroff
217             body += '.IP "\\fB%s\\fR: %s"\n' % (nameNroff, typeNroff)
218             body += blockXmlToNroff(node.childNodes, '.IP') + "\n"
219             summary += [('column', nameNroff, typeNroff)]
220         elif node.tagName == 'group':
221             title = node.attributes["title"].nodeValue
222             subSummary, subIntro, subBody = columnGroupToNroff(table, node)
223             summary += [('group', title, subSummary)]
224             body += '.ST "%s:"\n' % textToNroff(title)
225             body += subIntro + subBody
226         else:
227             raise error.Error("unknown element %s in <table>" % node.tagName)
228     return summary, intro, body
229
230 def tableSummaryToNroff(summary, level=0):
231     s = ""
232     for type, name, arg in summary:
233         if type == 'column':
234             s += ".TQ %.2fin\n\\fB%s\\fR\n%s\n" % (3 - level * .25, name, arg)
235         else:
236             s += ".TQ .25in\n\\fI%s:\\fR\n.RS .25in\n" % name
237             s += tableSummaryToNroff(arg, level + 1)
238             s += ".RE\n"
239     return s
240
241 def tableToNroff(schema, tableXml):
242     tableName = tableXml.attributes['name'].nodeValue
243     table = schema.tables[tableName]
244
245     s = """.bp
246 .SH "%s TABLE"
247 """ % tableName
248     summary, intro, body = columnGroupToNroff(table, tableXml)
249     s += intro
250     s += '.SS "Summary:\n'
251     s += tableSummaryToNroff(summary)
252     s += '.SS "Details:\n'
253     s += body
254     return s
255
256 def docsToNroff(schemaFile, xmlFile, erFile, title=None, version=None):
257     schema = ovs.db.schema.DbSchema.from_json(ovs.json.from_file(schemaFile))
258     doc = xml.dom.minidom.parse(xmlFile).documentElement
259
260     schemaDate = os.stat(schemaFile).st_mtime
261     xmlDate = os.stat(xmlFile).st_mtime
262     d = date.fromtimestamp(max(schemaDate, xmlDate))
263
264     if title == None:
265         title = schema.name
266
267     if version == None:
268         version = "UNKNOWN"
269
270     # Putting '\" p as the first line tells "man" that the manpage
271     # needs to be preprocessed by "pic".
272     s = r''''\" p
273 .TH "%s" 5 " DB Schema %s" "Open vSwitch %s" "Open vSwitch Manual"
274 .\" -*- nroff -*-
275 .de TQ
276 .  br
277 .  ns
278 .  TP "\\$1"
279 ..
280 .de ST
281 .  PP
282 .  RS -0.15in
283 .  I "\\$1"
284 .  RE
285 ..
286 .SH NAME
287 %s \- %s database schema
288 .PP
289 ''' % (title, schema.version, version, textToNroff(schema.name), schema.name)
290
291     tables = ""
292     introNodes = []
293     tableNodes = []
294     summary = []
295     for dbNode in doc.childNodes:
296         if (dbNode.nodeType == dbNode.ELEMENT_NODE
297             and dbNode.tagName == "table"):
298             tableNodes += [dbNode]
299
300             name = dbNode.attributes['name'].nodeValue
301             if dbNode.hasAttribute("title"):
302                 title = dbNode.attributes['title'].nodeValue
303             else:
304                 title = name + " configuration."
305             summary += [(name, title)]
306         else:
307             introNodes += [dbNode]
308
309     s += blockXmlToNroff(introNodes) + "\n"
310
311     s += r"""
312 .SH "TABLE SUMMARY"
313 .PP
314 The following list summarizes the purpose of each of the tables in the
315 \fB%s\fR database.  Each table is described in more detail on a later
316 page.
317 .IP "Table" 1in
318 Purpose
319 """ % schema.name
320     for name, title in summary:
321         s += r"""
322 .TQ 1in
323 \fB%s\fR
324 %s
325 """ % (name, textToNroff(title))
326
327     if erFile:
328         s += """
329 .\\" check if in troff mode (TTY)
330 .if t \{
331 .bp
332 .SH "TABLE RELATIONSHIPS"
333 .PP
334 The following diagram shows the relationship among tables in the
335 database.  Each node represents a table.  Tables that are part of the
336 ``root set'' are shown with double borders.  Each edge leads from the
337 table that contains it and points to the table that its value
338 represents.  Edges are labeled with their column names, followed by a
339 constraint on the number of allowed values: \\fB?\\fR for zero or one,
340 \\fB*\\fR for zero or more, \\fB+\\fR for one or more.  Thick lines
341 represent strong references; thin lines represent weak references.
342 .RS -1in
343 """
344         erStream = open(erFile, "r")
345         for line in erStream:
346             s += line + '\n'
347         erStream.close()
348         s += ".RE\\}\n"
349
350     for node in tableNodes:
351         s += tableToNroff(schema, node) + "\n"
352     return s
353
354 def usage():
355     print """\
356 %(argv0)s: ovsdb schema documentation generator
357 Prints documentation for an OVSDB schema as an nroff-formatted manpage.
358 usage: %(argv0)s [OPTIONS] SCHEMA XML
359 where SCHEMA is an OVSDB schema in JSON format
360   and XML is OVSDB documentation in XML format.
361
362 The following options are also available:
363   --er-diagram=DIAGRAM.PIC    include E-R diagram from DIAGRAM.PIC
364   --title=TITLE               use TITLE as title instead of schema name
365   --version=VERSION           use VERSION to display on document footer
366   -h, --help                  display this help message\
367 """ % {'argv0': argv0}
368     sys.exit(0)
369
370 if __name__ == "__main__":
371     try:
372         try:
373             options, args = getopt.gnu_getopt(sys.argv[1:], 'hV',
374                                               ['er-diagram=', 'title=',
375                                                'version=', 'help'])
376         except getopt.GetoptError, geo:
377             sys.stderr.write("%s: %s\n" % (argv0, geo.msg))
378             sys.exit(1)
379
380         er_diagram = None
381         title = None
382         version = None
383         for key, value in options:
384             if key == '--er-diagram':
385                 er_diagram = value
386             elif key == '--title':
387                 title = value
388             elif key == '--version':
389                 version = value
390             elif key in ['-h', '--help']:
391                 usage()
392             else:
393                 sys.exit(0)
394
395         if len(args) != 2:
396             sys.stderr.write("%s: exactly 2 non-option arguments required "
397                              "(use --help for help)\n" % argv0)
398             sys.exit(1)
399
400         # XXX we should warn about undocumented tables or columns
401         s = docsToNroff(args[0], args[1], er_diagram, title, version)
402         for line in s.split("\n"):
403             line = line.strip()
404             if len(line):
405                 print line
406
407     except error.Error, e:
408         sys.stderr.write("%s: %s\n" % (argv0, e.msg))
409         sys.exit(1)
410
411 # Local variables:
412 # mode: python
413 # End: