ovsdb-doc: Factor out nroff formatting into a separate Python module.
[cascardo/ovs.git] / ovsdb / ovsdb-doc
1 #! /usr/bin/python
2
3 from datetime import date
4 import getopt
5 import os
6 import sys
7 import xml.dom.minidom
8
9 import ovs.json
10 from ovs.db import error
11 import ovs.db.schema
12
13 from build.nroff import *
14
15 argv0 = sys.argv[0]
16
17 def typeAndConstraintsToNroff(column):
18     type = column.type.toEnglish(escapeNroffLiteral)
19     constraints = column.type.constraintsToEnglish(escapeNroffLiteral,
20                                                    textToNroff)
21     if constraints:
22         type += ", " + constraints
23     if column.unique:
24         type += " (must be unique within table)"
25     return type
26
27 def columnGroupToNroff(table, groupXml, documented_columns):
28     introNodes = []
29     columnNodes = []
30     for node in groupXml.childNodes:
31         if (node.nodeType == node.ELEMENT_NODE
32             and node.tagName in ('column', 'group')):
33             columnNodes += [node]
34         else:
35             if (columnNodes
36                 and not (node.nodeType == node.TEXT_NODE
37                          and node.data.isspace())):
38                 raise error.Error("text follows <column> or <group> inside <group>: %s" % node)
39             introNodes += [node]
40
41     summary = []
42     intro = blockXmlToNroff(introNodes)
43     body = ''
44     for node in columnNodes:
45         if node.tagName == 'column':
46             name = node.attributes['name'].nodeValue
47             documented_columns.add(name)
48             column = table.columns[name]
49             if node.hasAttribute('key'):
50                 key = node.attributes['key'].nodeValue
51                 if node.hasAttribute('type'):
52                     type_string = node.attributes['type'].nodeValue
53                     type_json = ovs.json.from_string(str(type_string))
54                     if type(type_json) in (str, unicode):
55                         raise error.Error("%s %s:%s has invalid 'type': %s" 
56                                           % (table.name, name, key, type_json))
57                     type_ = ovs.db.types.BaseType.from_json(type_json)
58                 else:
59                     type_ = column.type.value
60
61                 nameNroff = "%s : %s" % (name, key)
62
63                 if column.type.value:
64                     typeNroff = "optional %s" % column.type.value.toEnglish(
65                         escapeNroffLiteral)
66                     if (column.type.value.type == ovs.db.types.StringType and
67                         type_.type == ovs.db.types.BooleanType):
68                         # This is a little more explicit and helpful than
69                         # "containing a boolean"
70                         typeNroff += r", either \fBtrue\fR or \fBfalse\fR"
71                     else:
72                         if type_.type != column.type.value.type:
73                             type_english = type_.toEnglish()
74                             if type_english[0] in 'aeiou':
75                                 typeNroff += ", containing an %s" % type_english
76                             else:
77                                 typeNroff += ", containing a %s" % type_english
78                         constraints = (
79                             type_.constraintsToEnglish(escapeNroffLiteral,
80                                                        textToNroff))
81                         if constraints:
82                             typeNroff += ", %s" % constraints
83                 else:
84                     typeNroff = "none"
85             else:
86                 nameNroff = name
87                 typeNroff = typeAndConstraintsToNroff(column)
88             if not column.mutable:
89                 typeNroff = "immutable %s" % typeNroff
90             body += '.IP "\\fB%s\\fR: %s"\n' % (nameNroff, typeNroff)
91             body += blockXmlToNroff(node.childNodes, '.IP') + "\n"
92             summary += [('column', nameNroff, typeNroff)]
93         elif node.tagName == 'group':
94             title = node.attributes["title"].nodeValue
95             subSummary, subIntro, subBody = columnGroupToNroff(
96                 table, node, documented_columns)
97             summary += [('group', title, subSummary)]
98             body += '.ST "%s:"\n' % textToNroff(title)
99             body += subIntro + subBody
100         else:
101             raise error.Error("unknown element %s in <table>" % node.tagName)
102     return summary, intro, body
103
104 def tableSummaryToNroff(summary, level=0):
105     s = ""
106     for type, name, arg in summary:
107         if type == 'column':
108             s += ".TQ %.2fin\n\\fB%s\\fR\n%s\n" % (3 - level * .25, name, arg)
109         else:
110             s += ".TQ .25in\n\\fI%s:\\fR\n.RS .25in\n" % name
111             s += tableSummaryToNroff(arg, level + 1)
112             s += ".RE\n"
113     return s
114
115 def tableToNroff(schema, tableXml):
116     tableName = tableXml.attributes['name'].nodeValue
117     table = schema.tables[tableName]
118
119     documented_columns = set()
120     s = """.bp
121 .SH "%s TABLE"
122 """ % tableName
123     summary, intro, body = columnGroupToNroff(table, tableXml,
124                                               documented_columns)
125     s += intro
126     s += '.SS "Summary:\n'
127     s += tableSummaryToNroff(summary)
128     s += '.SS "Details:\n'
129     s += body
130
131     schema_columns = set(table.columns.keys())
132     undocumented_columns = schema_columns - documented_columns
133     for column in undocumented_columns:
134         raise error.Error("table %s has undocumented column %s"
135                           % (tableName, column))
136
137     return s
138
139 def docsToNroff(schemaFile, xmlFile, erFile, version=None):
140     schema = ovs.db.schema.DbSchema.from_json(ovs.json.from_file(schemaFile))
141     doc = xml.dom.minidom.parse(xmlFile).documentElement
142
143     schemaDate = os.stat(schemaFile).st_mtime
144     xmlDate = os.stat(xmlFile).st_mtime
145     d = date.fromtimestamp(max(schemaDate, xmlDate))
146
147     if doc.hasAttribute('name'):
148         manpage = doc.attributes['name'].nodeValue
149     else:
150         manpage = schema.name
151
152     if version == None:
153         version = "UNKNOWN"
154
155     # Putting '\" p as the first line tells "man" that the manpage
156     # needs to be preprocessed by "pic".
157     s = r''''\" p
158 .\" -*- nroff -*-
159 .TH "%s" 5 " DB Schema %s" "Open vSwitch %s" "Open vSwitch Manual"
160 .fp 5 L CR              \\" Make fixed-width font available as \\fL.
161 .de TQ
162 .  br
163 .  ns
164 .  TP "\\$1"
165 ..
166 .de ST
167 .  PP
168 .  RS -0.15in
169 .  I "\\$1"
170 .  RE
171 ..
172 .SH NAME
173 %s \- %s database schema
174 .PP
175 ''' % (manpage, schema.version, version, textToNroff(manpage), schema.name)
176
177     tables = ""
178     introNodes = []
179     tableNodes = []
180     summary = []
181     for dbNode in doc.childNodes:
182         if (dbNode.nodeType == dbNode.ELEMENT_NODE
183             and dbNode.tagName == "table"):
184             tableNodes += [dbNode]
185
186             name = dbNode.attributes['name'].nodeValue
187             if dbNode.hasAttribute("title"):
188                 title = dbNode.attributes['title'].nodeValue
189             else:
190                 title = name + " configuration."
191             summary += [(name, title)]
192         else:
193             introNodes += [dbNode]
194
195     documented_tables = set((name for (name, title) in summary))
196     schema_tables = set(schema.tables.keys())
197     undocumented_tables = schema_tables - documented_tables
198     for table in undocumented_tables:
199         raise error.Error("undocumented table %s" % table)
200
201     s += blockXmlToNroff(introNodes) + "\n"
202
203     s += r"""
204 .SH "TABLE SUMMARY"
205 .PP
206 The following list summarizes the purpose of each of the tables in the
207 \fB%s\fR database.  Each table is described in more detail on a later
208 page.
209 .IP "Table" 1in
210 Purpose
211 """ % schema.name
212     for name, title in summary:
213         s += r"""
214 .TQ 1in
215 \fB%s\fR
216 %s
217 """ % (name, textToNroff(title))
218
219     if erFile:
220         s += """
221 .\\" check if in troff mode (TTY)
222 .if t \{
223 .bp
224 .SH "TABLE RELATIONSHIPS"
225 .PP
226 The following diagram shows the relationship among tables in the
227 database.  Each node represents a table.  Tables that are part of the
228 ``root set'' are shown with double borders.  Each edge leads from the
229 table that contains it and points to the table that its value
230 represents.  Edges are labeled with their column names, followed by a
231 constraint on the number of allowed values: \\fB?\\fR for zero or one,
232 \\fB*\\fR for zero or more, \\fB+\\fR for one or more.  Thick lines
233 represent strong references; thin lines represent weak references.
234 .RS -1in
235 """
236         erStream = open(erFile, "r")
237         for line in erStream:
238             s += line + '\n'
239         erStream.close()
240         s += ".RE\\}\n"
241
242     for node in tableNodes:
243         s += tableToNroff(schema, node) + "\n"
244     return s
245
246 def usage():
247     print """\
248 %(argv0)s: ovsdb schema documentation generator
249 Prints documentation for an OVSDB schema as an nroff-formatted manpage.
250 usage: %(argv0)s [OPTIONS] SCHEMA XML
251 where SCHEMA is an OVSDB schema in JSON format
252   and XML is OVSDB documentation in XML format.
253
254 The following options are also available:
255   --er-diagram=DIAGRAM.PIC    include E-R diagram from DIAGRAM.PIC
256   --version=VERSION           use VERSION to display on document footer
257   -h, --help                  display this help message\
258 """ % {'argv0': argv0}
259     sys.exit(0)
260
261 if __name__ == "__main__":
262     try:
263         try:
264             options, args = getopt.gnu_getopt(sys.argv[1:], 'hV',
265                                               ['er-diagram=',
266                                                'version=', 'help'])
267         except getopt.GetoptError, geo:
268             sys.stderr.write("%s: %s\n" % (argv0, geo.msg))
269             sys.exit(1)
270
271         er_diagram = None
272         version = None
273         for key, value in options:
274             if key == '--er-diagram':
275                 er_diagram = value
276             elif key == '--version':
277                 version = value
278             elif key in ['-h', '--help']:
279                 usage()
280             else:
281                 sys.exit(0)
282
283         if len(args) != 2:
284             sys.stderr.write("%s: exactly 2 non-option arguments required "
285                              "(use --help for help)\n" % argv0)
286             sys.exit(1)
287
288         # XXX we should warn about undocumented tables or columns
289         s = docsToNroff(args[0], args[1], er_diagram, version)
290         for line in s.split("\n"):
291             line = line.strip()
292             if len(line):
293                 print line
294
295     except error.Error, e:
296         sys.stderr.write("%s: %s\n" % (argv0, e.msg))
297         sys.exit(1)
298
299 # Local variables:
300 # mode: python
301 # End: