diff options
Diffstat (limited to 'doc/sphinx/api2doc.py')
-rwxr-xr-x | doc/sphinx/api2doc.py | 208 |
1 files changed, 208 insertions, 0 deletions
diff --git a/doc/sphinx/api2doc.py b/doc/sphinx/api2doc.py new file mode 100755 index 0000000..4318fd2 --- /dev/null +++ b/doc/sphinx/api2doc.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 + +# Copyright (C) 2019-2021 Internet Systems Consortium, Inc. ("ISC") +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http:#mozilla.org/MPL/2.0/. + +# Produce API Reference +# - reads *.json files (each file describes a single command) +# - produces .rst file suitable for Sphinx as output + +import os +import json +import argparse +import collections + + +def parse_args(): + parser = argparse.ArgumentParser(description='Convert set of *.json files to .rst documentation format') + parser.add_argument('-o', '--output', help='Output file name (default to stdout).') + parser.add_argument('files', help='Input API .json files.', nargs='+') + + args = parser.parse_args() + return args + + +def read_input_files(files): + apis = {} + for f in files: + name = os.path.basename(f)[:-5] + # Skip special names starting with _ (such as _template.json) + if name.startswith('_'): + print("Skipping %s (starts with underscore)" % f) + continue + with open(f) as fp: + print("Processing %s" % f) + # use OrderedDict to preserve order of fields in cmd-syntax + try: + descr = json.load(fp, object_pairs_hook=collections.OrderedDict) + except: + print('\nError while processing %s\n\n' % f) + raise + assert name == descr['name'] + apis[name] = descr + + return apis + + +def generate_rst(apis): + rst = '''.. _api: + +API Reference +============= + +''' + + daemons = {} + hooks = {} + for func in apis.values(): + for dm in func['support']: + if dm not in daemons: + daemons[dm] = [] + daemons[dm].append(func) + + if 'hook' in func: + if func['hook'] not in hooks: + hooks[func['hook']] = [] + hooks[func['hook']].append(func) + + rst += 'Kea currently supports %d commands in %s daemons and %s hook libraries.\n\n' % ( + len(apis), + ", ".join([':ref:`%s <commands-%s>`' % (m, m) for m in sorted(daemons.keys())]), + ", ".join([':ref:`%s <commands-%s>`' % (m, m) for m in sorted(hooks.keys())])) + + for dm, funcs in sorted(daemons.items()): + rst += '.. _commands-%s:\n\n' % dm + rst += 'Commands supported by `%s` daemon: ' % dm + funcs = sorted([ ':ref:`%s <ref-%s>`' % (f['name'], f['name']) for f in funcs]) + rst += ', '.join(funcs) + rst += '.\n\n' + + for h, funcs in sorted(hooks.items()): + rst += '.. _commands-%s:\n\n' % h + rst += 'Commands supported by `%s` hook library: ' % h + funcs = sorted([ ':ref:`%s <ref-%s>`' % (f['name'], f['name']) for f in funcs]) + rst += ', '.join(funcs) + rst += '.\n\n' + + for func in sorted(apis.values(), key=lambda f: f['name']): + # "name" is visible in the ARM. "real_name" is used to provide links + # to commands. Keep both even if they're the same for when you want to + # make changes to "name" to change the way it's seen in the ARM. + name = func['name'] + real_name = func['name'] + + rst += '.. _ref-%s:\n\n' % real_name + rst += name + '\n' + rst += '-' * len(name) + '\n\n' + + # command overview + for brief_line in func['brief']: + rst += '%s\n' % brief_line + rst += '\n' + + # command can be issued to the following daemons + rst += 'Supported by: ' + rst += ', '.join(sorted([':ref:`%s <commands-%s>`' % (dm, dm) for dm in func['support']])) + rst += '\n\n' + + # availability + rst += 'Availability: %s ' % func['avail'] + rst += '(:ref:`%s <commands-%s>` hook library)' % (func['hook'], func['hook']) if 'hook' in func else '(built-in)' + rst += '\n\n' + + # access + try: + access = func['access'] + except: + print('\naccess missing in %s\n\n' % name) + raise + if not access in ['read', 'write']: + print('\nUnknown access %s in %s\n\n' % (access, name)) + raise ValueError('access must be read or write') + rst += 'Access: %s *(parameter ignored in this Kea version)* \n\n' % access + + # description and examples + rst += 'Description and examples: see :ref:`%s command <command-%s>`\n\n' % (name, real_name) + + # command syntax + rst += 'Command syntax:\n\n' + rst += '::\n\n' + if 'cmd-syntax' in func: + cmd_syntaxes = [func['cmd-syntax']] + if isinstance(cmd_syntaxes, dict): + cmd_syntaxes = [cmd_syntax] + for cmd_syntax in cmd_syntaxes: + if 'comment' in cmd_syntax: + rst += cmd_syntax['comment'] + rst += '\n\n' + del cmd_syntax['comment'] + + for line in cmd_syntax: + rst += ' %s\n' % line + else: + rst += ' {\n' + rst += ' "command": \"%s\"\n' % name + rst += ' }' + rst += '\n\n' + + if 'cmd-comment' in func: + for l in func['cmd-comment']: + rst += "%s\n" % l + rst += '\n' + + # response syntax + rst += 'Response syntax:\n\n' + rst += '::\n\n' + if 'resp-syntax' in func: + resp_syntaxes = [func['resp-syntax']] + if isinstance(resp_syntaxes, dict): + resp_syntaxes = [resp_syntax] + for resp_syntax in resp_syntaxes: + + for line in resp_syntax: + rst += ' %s\n' % line + + else: + rst += ' {\n' + rst += ' "result": <integer>,\n' + rst += ' "text": "<string>"\n' + rst += ' }' + rst += '\n\n' + + if 'resp-comment' in func: + for resp_comment_line in func['resp-comment']: + rst += "%s\n" % resp_comment_line + rst += '\n\n' + else: + rst += 'Result is an integer representation of the status. Currently supported statuses are:\n\n' + rst += '- 0 - success\n' + rst += '- 1 - error\n' + rst += '- 2 - unsupported\n' + rst += '- 3 - empty (command was completed successfully, but no data was affected or returned)\n\n' + + return rst + + +def generate(in_files, out_file): + apis = read_input_files(in_files) + + rst = generate_rst(apis) + + if out_file: + with open(out_file, 'w') as f: + f.write(rst) + print('Wrote generated RST content to: %s' % out_file) + else: + print(rst) + + +def main(): + args = parse_args() + generate(args.files, args.output) + + +if __name__ == '__main__': + main() |