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