vswitchd: Document map members as separate columns
[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                 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                 nameNroff = "%s : %s" % (name, key)
178                 typeNroff = "optional %s" % column.type.value.toEnglish()
179             else:
180                 nameNroff = name
181                 typeNroff = typeAndConstraintsToNroff(column)
182             body += '.IP "\\fB%s\\fR: %s"\n' % (nameNroff, typeNroff)
183             body += blockXmlToNroff(node.childNodes, '.IP') + "\n"
184             summary += [('column', nameNroff, typeNroff)]
185         elif node.tagName == 'group':
186             title = node.attributes["title"].nodeValue
187             subSummary, subIntro, subBody = columnGroupToNroff(table, node)
188             summary += [('group', title, subSummary)]
189             body += '.ST "%s:"\n' % textToNroff(title)
190             body += subIntro + subBody
191         else:
192             raise error.Error("unknown element %s in <table>" % node.tagName)
193     return summary, intro, body
194
195 def tableSummaryToNroff(summary, level=0):
196     s = ""
197     for type, name, arg in summary:
198         if type == 'column':
199             s += ".TQ %.2fin\n\\fB%s\\fR\n%s\n" % (3 - level * .25, name, arg)
200         else:
201             s += ".TQ .25in\n\\fI%s:\\fR\n.RS .25in\n" % name
202             s += tableSummaryToNroff(arg, level + 1)
203             s += ".RE\n"
204     return s
205
206 def tableToNroff(schema, tableXml):
207     tableName = tableXml.attributes['name'].nodeValue
208     table = schema.tables[tableName]
209
210     s = """.bp
211 .SH "%s TABLE"
212 """ % tableName
213     summary, intro, body = columnGroupToNroff(table, tableXml)
214     s += intro
215     s += '.SS "Summary:\n'
216     s += tableSummaryToNroff(summary)
217     s += '.SS "Details:\n'
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 '\" p as the first line tells "man" that the manpage
233     # needs to be preprocessed by "pic".
234     s = r''''\" p
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
272     s += r"""
273 .SH "TABLE SUMMARY"
274 .PP
275 The following list summarizes the purpose of each of the tables in the
276 \fB%s\fR database.  Each table is described in more detail on a later
277 page.
278 .IP "Table" 1in
279 Purpose
280 """ % schema.name
281     for name, title in summary:
282         s += r"""
283 .TQ 1in
284 \fB%s\fR
285 %s
286 """ % (name, textToNroff(title))
287
288     if erFile:
289         s += """
290 .if !'\*[.T]'ascii' \{
291 .bp
292 .SH "TABLE RELATIONSHIPS"
293 .PP
294 The following diagram shows the relationship among tables in the
295 database.  Each node represents a table.  Tables that are part of the
296 ``root set'' are shown with double borders.  Each edge leads from the
297 table that contains it and points to the table that its value
298 represents.  Edges are labeled with their column names, followed by a
299 constraint on the number of allowed values: \\fB?\\fR for zero or one,
300 \\fB*\\fR for zero or more, \\fB+\\fR for one or more.  Thick lines
301 represent strong references; thin lines represent weak references.
302 .RS -1in
303 """
304         erStream = open(erFile, "r")
305         for line in erStream:
306             s += line + '\n'
307         erStream.close()
308         s += ".RE\\}\n"
309
310     for node in tableNodes:
311         s += tableToNroff(schema, node) + "\n"
312     return s
313
314 def usage():
315     print """\
316 %(argv0)s: ovsdb schema documentation generator
317 Prints documentation for an OVSDB schema as an nroff-formatted manpage.
318 usage: %(argv0)s [OPTIONS] SCHEMA XML
319 where SCHEMA is an OVSDB schema in JSON format
320   and XML is OVSDB documentation in XML format.
321
322 The following options are also available:
323   --er-diagram=DIAGRAM.PIC    include E-R diagram from DIAGRAM.PIC
324   --title=TITLE               use TITLE as title instead of schema name
325   -h, --help                  display this help message
326   -V, --version               display version information\
327 """ % {'argv0': argv0}
328     sys.exit(0)
329
330 if __name__ == "__main__":
331     try:
332         try:
333             options, args = getopt.gnu_getopt(sys.argv[1:], 'hV',
334                                               ['er-diagram=', 'title=',
335                                                'help', 'version'])
336         except getopt.GetoptError, geo:
337             sys.stderr.write("%s: %s\n" % (argv0, geo.msg))
338             sys.exit(1)
339
340         er_diagram = None
341         title = None
342         for key, value in options:
343             if key == '--er-diagram':
344                 er_diagram = value
345             elif key == '--title':
346                 title = value
347             elif key in ['-h', '--help']:
348                 usage()
349             elif key in ['-V', '--version']:
350                 print "ovsdb-doc (Open vSwitch) @VERSION@"
351             else:
352                 sys.exit(0)
353
354         if len(args) != 2:
355             sys.stderr.write("%s: exactly 2 non-option arguments required "
356                              "(use --help for help)\n" % argv0)
357             sys.exit(1)
358
359         # XXX we should warn about undocumented tables or columns
360         s = docsToNroff(args[0], args[1], er_diagram)
361         for line in s.split("\n"):
362             line = line.strip()
363             if len(line):
364                 print line
365
366     except error.Error, e:
367         sys.stderr.write("%s: %s\n" % (argv0, e.msg))
368         sys.exit(1)
369
370 # Local variables:
371 # mode: python
372 # End: