util: Fix non-ANSI function declaration.
[openvswitch] / ovsdb / ovsdb-doc.in
1 #! @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 == '-':
20             if font == r'\fB':
21                 return r'\-'
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('([-"\'\\\\])', 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 column and table attributes")
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             else:
129                 s += inlineXmlToNroff(node, r'\fR')
130         else:
131             raise error.Error("unknown node %s in block xml" % node)
132     if s != "" and not s.endswith('\n'):
133         s += '\n'
134     return s
135
136 def typeAndConstraintsToNroff(column):
137     type = column.type.toEnglish(escapeNroffLiteral)
138     constraints = column.type.constraintsToEnglish(escapeNroffLiteral)
139     if constraints:
140         type += ", " + constraints
141     if column.unique:
142         type += " (must be unique within table)"
143     return type
144
145 def columnToNroff(columnName, column, node):
146     type = typeAndConstraintsToNroff(column)
147     s = '.IP "\\fB%s\\fR: %s"\n' % (columnName, type)
148     s += blockXmlToNroff(node.childNodes, '.IP') + "\n"
149     return s
150
151 def columnGroupToNroff(table, groupXml):
152     introNodes = []
153     columnNodes = []
154     for node in groupXml.childNodes:
155         if (node.nodeType == node.ELEMENT_NODE
156             and node.tagName in ('column', 'group')):
157             columnNodes += [node]
158         else:
159             introNodes += [node]
160
161     summary = []
162     intro = blockXmlToNroff(introNodes)
163     body = ''
164     for node in columnNodes:
165         if node.tagName == 'column':
166             columnName = node.attributes['name'].nodeValue
167             column = table.columns[columnName]
168             body += columnToNroff(columnName, column, node)
169             summary += [('column', columnName, column)]
170         elif node.tagName == 'group':
171             title = node.attributes["title"].nodeValue
172             subSummary, subIntro, subBody = columnGroupToNroff(table, node)
173             summary += [('group', title, subSummary)]
174             body += '.ST "%s:"\n' % textToNroff(title)
175             body += subIntro + subBody
176         else:
177             raise error.Error("unknown element %s in <table>" % node.tagName)
178     return summary, intro, body
179
180 def tableSummaryToNroff(summary, level=0):
181     s = ""
182     for type, name, arg in summary:
183         if type == 'column':
184
185             s += "%s\\fB%s\\fR\tT{\n%s\nT}\n" % (
186                 r'\ \ ' * level, name, typeAndConstraintsToNroff(arg))
187         else:
188             if s != "":
189                 s += "_\n"
190             s += """.T&
191 li | s
192 l | l.
193 %s%s
194 _
195 """ % (r'\ \ ' * level, name)
196             s += tableSummaryToNroff(arg, level + 1)
197     return s
198
199 def tableToNroff(schema, tableXml):
200     tableName = tableXml.attributes['name'].nodeValue
201     table = schema.tables[tableName]
202
203     s = """.bp
204 .SS "%s Table"
205 """ % tableName
206     summary, intro, body = columnGroupToNroff(table, tableXml)
207     s += intro
208
209     s += r"""
210 .sp
211 .ce 1
212 \fB%s\fR Table Columns:
213 .TS
214 center box;
215 l | l.
216 Column  Type
217 =
218 """ % tableName
219     s += tableSummaryToNroff(summary)
220     s += ".TE\n"
221
222     s += body
223     return s
224
225 def docsToNroff(schemaFile, xmlFile, erFile, title=None):
226     schema = ovs.db.schema.DbSchema.from_json(ovs.json.from_file(schemaFile))
227     doc = xml.dom.minidom.parse(xmlFile).documentElement
228
229     schemaDate = os.stat(schemaFile).st_mtime
230     xmlDate = os.stat(xmlFile).st_mtime
231     d = date.fromtimestamp(max(schemaDate, xmlDate))
232
233     if title == None:
234         title = schema.name
235
236     # Putting '\" pt as the first line tells "man" that the manpage
237     # needs to be preprocessed by "pic" and "tbl".
238     s = r''''\" pt
239 .TH %s 5 "%s" "Open vSwitch" "Open vSwitch Manual"
240 .\" -*- nroff -*-
241 .de TQ
242 .  br
243 .  ns
244 .  TP
245 \\$1
246 ..
247 .de ST
248 .  PP
249 .  RS -0.15in
250 .  I "\\$1"
251 .  RE
252 ..
253 ''' % (title, d.strftime("%B %Y"))
254
255     s += '.SH "%s DATABASE"\n' % schema.name
256
257     tables = ""
258     introNodes = []
259     tableNodes = []
260     summary = []
261     for dbNode in doc.childNodes:
262         if (dbNode.nodeType == dbNode.ELEMENT_NODE
263             and dbNode.tagName == "table"):
264             tableNodes += [dbNode]
265
266             name = dbNode.attributes['name'].nodeValue
267             if dbNode.hasAttribute("title"):
268                 title = dbNode.attributes['title'].nodeValue
269             else:
270                 title = name + " configuration."
271             summary += [(name, title)]
272         else:
273             introNodes += [dbNode]
274
275     s += blockXmlToNroff(introNodes) + "\n"
276     tableSummary = r"""
277 .sp
278 .ce 1
279 \fB%s\fR Database Tables:
280 .TS
281 center box;
282 l | l
283 lb | l.
284 Table   Purpose
285 =
286 """ % schema.name
287     for name, title in summary:
288         tableSummary += "%s\t%s\n" % (name, textToNroff(title))
289     tableSummary += '.TE\n'
290     s += tableSummary
291
292     if erFile:
293         s += """
294 .if !'\*[.T]'ascii' \{
295 .sp 1
296 .SH "TABLE RELATIONSHIPS"
297 .PP
298 The following diagram shows the relationship among tables in the
299 database.  Each node represents a table.  Tables that are part of the
300 ``root set'' are shown with double borders.  Each edge leads from the
301 table that contains it and points to the table that its value
302 represents.  Edges are labeled with their column names, followed by a
303 constraint on the number of allowed values: \\fB?\\fR for zero or one,
304 \\fB*\\fR for zero or more, \\fB+\\fR for one or more.  Thick lines
305 represent strong references; thin lines represent weak references.
306 .RS -1in
307 """
308         erStream = open(erFile, "r")
309         for line in erStream:
310             s += line + '\n'
311         erStream.close()
312         s += ".RE\\}\n"
313
314     for node in tableNodes:
315         s += tableToNroff(schema, node) + "\n"
316     return s
317
318 def usage():
319     print """\
320 %(argv0)s: ovsdb schema documentation generator
321 Prints documentation for an OVSDB schema as an nroff-formatted manpage.
322 usage: %(argv0)s [OPTIONS] SCHEMA XML
323 where SCHEMA is an OVSDB schema in JSON format
324   and XML is OVSDB documentation in XML format.
325
326 The following options are also available:
327   --er-diagram=DIAGRAM.PIC    include E-R diagram from DIAGRAM.PIC
328   --title=TITLE               use TITLE as title instead of schema name
329   -h, --help                  display this help message
330   -V, --version               display version information\
331 """ % {'argv0': argv0}
332     sys.exit(0)
333
334 if __name__ == "__main__":
335     try:
336         try:
337             options, args = getopt.gnu_getopt(sys.argv[1:], 'hV',
338                                               ['er-diagram=', 'title=',
339                                                'help', 'version'])
340         except getopt.GetoptError, geo:
341             sys.stderr.write("%s: %s\n" % (argv0, geo.msg))
342             sys.exit(1)
343
344         er_diagram = None
345         title = None
346         for key, value in options:
347             if key == '--er-diagram':
348                 er_diagram = value
349             elif key == '--title':
350                 title = value
351             elif key in ['-h', '--help']:
352                 usage()
353             elif key in ['-V', '--version']:
354                 print "ovsdb-doc (Open vSwitch) @VERSION@"
355             else:
356                 sys.exit(0)
357
358         if len(args) != 2:
359             sys.stderr.write("%s: exactly 2 non-option arguments required "
360                              "(use --help for help)\n" % argv0)
361             sys.exit(1)
362
363         # XXX we should warn about undocumented tables or columns
364         s = docsToNroff(args[0], args[1], er_diagram)
365         for line in s.split("\n"):
366             line = line.strip()
367             if len(line):
368                 print line
369
370     except error.Error, e:
371         sys.stderr.write("%s: %s\n" % (argv0, e.msg))
372         sys.exit(1)
373
374 # Local variables:
375 # mode: python
376 # End: