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