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