docs: better docs, mv doxygen to sphinx
[vpp.git] / src / scripts / fts.py
1 #!/usr/bin/env python3
2
3 import sys
4 import os
5 import os.path
6 import ipaddress
7 import yaml
8 from pprint import pprint
9 import re
10 from jsonschema import validate, exceptions
11 import argparse
12 from subprocess import run, PIPE
13 from io import StringIO
14 import urllib.parse
15
16 # VPP feature JSON schema
17 schema = {
18     "$schema": "http://json-schema.org/schema#",
19     "type": "object",
20     "properties": {
21         "name": {"type": "string"},
22         "description": {"type": "string"},
23         "maintainer": {"$ref": "#/definitions/maintainers"},
24         "state": {"type": "string",
25                   "enum": ["production", "experimental", "development"]},
26         "features": {"$ref": "#/definitions/features"},
27         "missing": {"$ref": "#/definitions/features"},
28         "properties": {"type": "array",
29                        "items": {"type": "string",
30                                  "enum": ["API", "CLI", "STATS",
31                                           "MULTITHREAD"]},
32                        },
33     },
34     "additionalProperties": False,
35     "definitions": {
36         "maintainers": {
37             "anyof": [{
38                 "type": "array",
39                 "items": {"type": "string"},
40                 "minItems": 1,
41             },
42                 {"type": "string"}],
43         },
44         "featureobject": {
45             "type": "object",
46             "patternProperties": {
47                 "^.*$": {"$ref": "#/definitions/features"},
48             },
49         },
50         "features": {
51             "type": "array",
52             "items": {"anyOf": [{"$ref": "#/definitions/featureobject"},
53                                 {"type": "string"},
54                                 ]},
55             "minItems": 1,
56         },
57     },
58 }
59
60 DEFAULT_REPO_LINK = "https://github.com/FDio/vpp/blob/master/"
61
62 def filelist_from_git_status():
63     filelist = []
64     git_status = 'git status --porcelain */FEATURE*.yaml'
65     rv = run(git_status.split(), stdout=PIPE, stderr=PIPE)
66     if rv.returncode != 0:
67         sys.exit(rv.returncode)
68
69     for l in rv.stdout.decode('ascii').split('\n'):
70         if len(l):
71             filelist.append(l.split()[1])
72     return filelist
73
74
75 def filelist_from_git_ls():
76     filelist = []
77     git_ls = 'git ls-files :(top)*/FEATURE*.yaml'
78     rv = run(git_ls.split(), stdout=PIPE, stderr=PIPE)
79     if rv.returncode != 0:
80         sys.exit(rv.returncode)
81
82     for l in rv.stdout.decode('ascii').split('\n'):
83         if len(l):
84             filelist.append(l)
85     return filelist
86
87 def version_from_git():
88     git_describe = 'git describe'
89     rv = run(git_describe.split(), stdout=PIPE, stderr=PIPE)
90     if rv.returncode != 0:
91         sys.exit(rv.returncode)
92     return rv.stdout.decode('ascii').split('\n')[0]
93
94 class MarkDown():
95     _dispatch = {}
96
97     def __init__(self, stream):
98         self.stream = stream
99         self.toc = []
100
101     def print_maintainer(self, o):
102         write = self.stream.write
103         if type(o) is list:
104             write('Maintainers: ' +
105                   ', '.join('{m}'.format(m=m) for m in
106                             o) + '  \n')
107         else:
108             write('Maintainer: {o}  \n'.format(o=o))
109
110     _dispatch['maintainer'] = print_maintainer
111
112     def print_features(self, o, indent=0):
113         write = self.stream.write
114         for f in o:
115             indentstr = ' ' * indent
116             if type(f) is dict:
117                 for k, v in f.items():
118                     write('{indentstr}- {k}\n'.format(indentstr=indentstr, k=k))
119                     self.print_features(v, indent + 2)
120             else:
121                 write('{indentstr}- {f}\n'.format(indentstr=indentstr, f=f))
122         write('\n')
123     _dispatch['features'] = print_features
124
125     def print_markdown_header(self, o):
126         write = self.stream.write
127         write('## {o}\n'.format(o=o))
128     _dispatch['markdown_header'] = print_markdown_header
129
130     def print_name(self, o):
131         write = self.stream.write
132         write('### {o}\n'.format(o=o))
133         self.toc.append(o)
134     _dispatch['name'] = print_name
135
136     def print_description(self, o):
137         write = self.stream.write
138         write('\n{o}\n\n'.format(o=o))
139     _dispatch['description'] = print_description
140
141     def print_state(self, o):
142         write = self.stream.write
143         write('Feature maturity level: {o}  \n'.format(o=o))
144     _dispatch['state'] = print_state
145
146     def print_properties(self, o):
147         write = self.stream.write
148         write('Supports: {s}  \n'.format(s=" ".join(o)))
149     _dispatch['properties'] = print_properties
150
151     def print_missing(self, o):
152         write = self.stream.write
153         write('\nNot yet implemented:  \n')
154         self.print_features(o)
155     _dispatch['missing'] = print_missing
156
157     def print_code(self, o):
158         write = self.stream.write
159         write('Source Code: [{o}]({o}) \n'.format(o=o))
160     _dispatch['code'] = print_code
161
162     def print(self, t, o):
163         write = self.stream.write
164         if t in self._dispatch:
165             self._dispatch[t](self, o,)
166         else:
167             write('NOT IMPLEMENTED: {t}\n')
168
169 def output_toc(toc, stream):
170     write = stream.write
171     write('# VPP Supported Features\n')
172
173     for t in toc:
174         ref = t.lower().replace(' ', '-')
175         write('[{t}](#{ref})  \n'.format(t=t, ref=ref))
176
177 def featuresort(k):
178     return k[1]['name']
179
180 def featurelistsort(k):
181     orderedfields = {
182         'name': 0,
183         'maintainer': 1,
184         'description': 2,
185         'features': 3,
186         'state': 4,
187         'properties': 5,
188         'missing': 6,
189         'code': 7,
190     }
191     return orderedfields[k[0]]
192
193 def output_markdown(features, fields, notfields, repository_url):
194     stream = StringIO()
195     m = MarkDown(stream)
196     m.print('markdown_header', 'Feature Details:')
197     for path, featuredef in sorted(features.items(), key=featuresort):
198         codeurl = urllib.parse.urljoin(repository_url, os.path.dirname(path))
199         featuredef['code'] = codeurl
200         for k, v in sorted(featuredef.items(), key=featurelistsort):
201             if notfields:
202                 if k not in notfields:
203                     m.print(k, v)
204             elif fields:
205                 if k in fields:
206                     m.print(k, v)
207             else:
208                 m.print(k, v)
209
210     tocstream = StringIO()
211     output_toc(m.toc, tocstream)
212     return tocstream, stream
213
214 def main():
215     parser = argparse.ArgumentParser(description='VPP Feature List.')
216     parser.add_argument('--validate', dest='validate', action='store_true',
217                         help='validate the FEATURE.yaml file')
218     parser.add_argument("--repolink", metavar="repolink", default=DEFAULT_REPO_LINK,
219                 help="Link to public repository [%s]" %
220                      DEFAULT_REPO_LINK)
221     parser.add_argument('--git-status', dest='git_status', action='store_true',
222                         help='Get filelist from git status')
223     parser.add_argument('--all', dest='all', action='store_true',
224                         help='Validate all files in repository')
225     parser.add_argument('--markdown', dest='markdown', action='store_true',
226                         help='Output feature table in markdown')
227     parser.add_argument('infile', nargs='?', type=argparse.FileType('r'),
228                         default=sys.stdin)
229     group = parser.add_mutually_exclusive_group()
230     group.add_argument('--include', help='List of fields to include')
231     group.add_argument('--exclude', help='List of fields to exclude')
232     args = parser.parse_args()
233     features = {}
234
235     if args.git_status:
236         filelist = filelist_from_git_status()
237     elif args.all:
238         filelist = filelist_from_git_ls()
239     else:
240         filelist = args.infile
241
242     if args.include:
243         fields = args.include.split(',')
244     else:
245         fields = []
246     if args.exclude:
247         notfields = args.exclude.split(',')
248     else:
249         notfields = []
250
251     for featurefile in filelist:
252         featurefile = featurefile.rstrip()
253
254         # Load configuration file
255         with open(featurefile, encoding='utf-8') as f:
256             cfg = yaml.load(f, Loader=yaml.SafeLoader)
257         try:
258             validate(instance=cfg, schema=schema)
259         except exceptions.ValidationError:
260             print('File does not validate: {featurefile}' \
261                   .format(featurefile=featurefile), file=sys.stderr)
262             raise
263         features[featurefile] = cfg
264
265     if args.markdown:
266         stream = StringIO()
267         tocstream, stream = output_markdown(features, fields, notfields, args.repolink)
268         print(tocstream.getvalue())
269         print(stream.getvalue())
270         stream.close()
271
272
273 if __name__ == '__main__':
274     main()