#! /usr/bin/env python3 # # Generate Sphinx documentation from JSON schema import argparse import sys import json def errprint(*args, **kwargs): print(*args, file=sys.stderr, **kwargs) def find_ref(schema: dict, ref: str) -> dict: parts = ref.split("/") root = parts.pop(0) if root != "#": raise Exception("Unsupported reference: {}".format(ref)) while parts: schema = schema[parts.pop(0)] return schema def get_type(props: dict, name: str) -> str: prop_type = props["type"] if prop_type == "array": try: array_type = props["items"]["type"] except KeyError: errprint("warning: array property without items: {}".format(name)) array_type = "unknown" prop_type = "array of {}s".format(array_type) return prop_type def render_flat(schema: dict): stack = [(schema, [])] while stack: (current, path) = stack.pop(0) for name, props in current["properties"].items(): if "$ref" in props: ref = find_ref(schema, props["$ref"]) if not ref: raise Exception("$ref not found: {}".format(props["$ref"])) props = ref if props["type"] in ["string", "integer", "boolean", "number"]: # End of the line... print("{}: {}".format(".".join(path + [name]), props["type"])) elif props["type"] == "object": print("{}: object".format(".".join(path + [name]))) if "properties" in props: stack.insert(0, (props, path + [name])) else: errprint( "warning: object without properties: {}".format( ".".join(path + [name]) ) ) elif props["type"] == "array": if "items" in props and "type" in props["items"]: print( "{}: {}[]".format( ".".join(path + [name]), props["items"]["type"] ) ) if "properties" in props["items"]: stack.insert( 0, ( props["items"], path + ["{}[]".format(name)], ), ) else: errprint( "warning: undocumented array: {}".format( ".".join(path + [name]) ) ) print("{}: array".format(".".join(path + [name]))) else: raise Exception("Unsupported type: {}".format(props["type"])) def render_rst(schema: dict): stack = [(schema, [], "object")] while stack: (current, path, type) = stack.pop(0) items = [] for name, props in current["properties"].items(): if "$ref" in props: ref = find_ref(schema, props["$ref"]) if not ref: raise Exception( "Reference not found: {}".format(props["$ref"]) ) props = ref prop_type = get_type(props, name) description = props.get("description", "") items.append( {"name": name, "type": prop_type, "description": description} ) if props["type"] == "object" and "properties" in props: stack.insert(0, (props, path + [name], "object")) elif ( props["type"] == "array" and "items" in props and "properties" in props["items"] ): array_type = props["items"]["type"] stack.insert( 0, ( props["items"], path + ["{}".format(name)], "array of {}s".format(array_type), ), ) render_rst_table(items, path, type) def render_rst_table(items: list, path: list, type: str): if not path: title = "Top Level" else: title = ".".join(path) title = "{} ({})".format(title, type) print(title) print("^" * len(title)) name_len = max([len(item["name"]) for item in items] + [len("Name")]) desc_len = max( [len(item["description"]) for item in items] + [len("Description")] ) type_len = max([len(item["type"]) for item in items]) print(".. table::") print(" :width: 100%") print(" :widths: 30 25 45") print("") print(" {} {} {}".format("=" * name_len, "=" * type_len, "=" * desc_len)) print( " {} {} {}".format( "Name".ljust(name_len), "Type".ljust(type_len), "Description".ljust(desc_len), ) ) print(" {} {} {}".format("=" * name_len, "=" * type_len, "=" * desc_len)) for item in items: print( " {} {} {}".format( item["name"].ljust(name_len), item["type"].ljust(type_len), item["description"].ljust(desc_len), ) ) print(" {} {} {}".format("=" * name_len, "=" * type_len, "=" * desc_len)) print("") epilog = """ By default, the EVE schema is rendered as Sphinx documentation. To create "flat" or "dot" separated output, use the --flat option. """ def main(): parser = argparse.ArgumentParser( description="Generate documentation from JSON schema", epilog=epilog, formatter_class=argparse.RawTextHelpFormatter, ) parser.add_argument("--object", help="Object name") parser.add_argument("--output", help="Output file") parser.add_argument("--flat", help="Flatten output", action="store_true") parser.add_argument("filename", help="JSON schema file") args = parser.parse_args() root = json.load(open(args.filename)) schema = root if args.object: schema = schema["properties"][args.object] if args.output: sys.stdout = open(args.output, "w") if args.flat: render_flat(schema) else: render_rst(schema) if __name__ == "__main__": sys.exit(main())