From c2287349788ac47e9713b8c1aa5acdda025b031c Mon Sep 17 00:00:00 2001 From: IvanCherepov Date: Wed, 24 Jan 2018 14:09:29 -0500 Subject: [PATCH] Generates HTML documentation explaining all of configuration fields (#2952) * create HTML documentation explaining all of schema's configuration fields --- util/config/configdocs/configdoc.py | 43 + util/config/configdocs/docsmodel.py | 93 +++ util/config/configdocs/html_output.py | 63 ++ util/config/configdocs/schema.html | 1043 +++++++++++++++++++++++++ util/config/configdocs/style.css | 78 ++ 5 files changed, 1320 insertions(+) create mode 100644 util/config/configdocs/configdoc.py create mode 100644 util/config/configdocs/docsmodel.py create mode 100644 util/config/configdocs/html_output.py create mode 100644 util/config/configdocs/schema.html create mode 100644 util/config/configdocs/style.css diff --git a/util/config/configdocs/configdoc.py b/util/config/configdocs/configdoc.py new file mode 100644 index 000000000..d73a8b5cd --- /dev/null +++ b/util/config/configdocs/configdoc.py @@ -0,0 +1,43 @@ +""" Generates html documentation from JSON Schema """ + + +import json +from collections import OrderedDict + +import docsmodel +import html_output + +from util.config.schema import CONFIG_SCHEMA + + +def make_custom_sort(orders): + """ Sort in a specified order any dictionary nested in a complex structure """ + + orders = [{k: -i for (i, k) in enumerate(reversed(order), 1)} for order in orders] + def process(stuff): + if isinstance(stuff, dict): + l = [(k, process(v)) for (k, v) in stuff.iteritems()] + keys = set(stuff) + for order in orders: + if keys.issubset(order) or keys.issuperset(order): + return OrderedDict(sorted(l, key=lambda x: order.get(x[0], 0))) + return OrderedDict(sorted(l)) + if isinstance(stuff, list): + return [process(x) for x in stuff] + return stuff + return process + +SCHEMA_HTML_FILE = "schema.html" + +schema = json.dumps(CONFIG_SCHEMA, sort_keys = True) +schema = json.loads(schema, object_pairs_hook = OrderedDict) + +req = sorted(schema["required"]) +custom_sort = make_custom_sort([req]) +schema = custom_sort(schema) + +parsed_items = docsmodel.DocsModel().parse(schema)[1:] +output = html_output.HtmlOutput().generate_output(parsed_items) + +with open(SCHEMA_HTML_FILE, 'wt') as f: + f.write(output) diff --git a/util/config/configdocs/docsmodel.py b/util/config/configdocs/docsmodel.py new file mode 100644 index 000000000..ebf8e0264 --- /dev/null +++ b/util/config/configdocs/docsmodel.py @@ -0,0 +1,93 @@ +import json +import collections + +class ParsedItem(dict): + """ Parsed Schema item """ + + def __init__(self, json_object, name, required, level): + """Fills dict with basic item information""" + super(ParsedItem, self).__init__() + self['name'] = name + self['title'] = json_object.get('title', '') + self['type'] = json_object.get('type') + self['description'] = json_object.get('description', '') + self['level'] = level + self['required'] = required + self['x-reference'] = json_object.get('x-reference', '') + self['x-example'] = json_object.get('x-example', '') + self['pattern'] = json_object.get('pattern', '') + self['enum'] = json_object.get('enum', '') + +class DocsModel: + """ Documentation model and Schema Parser """ + + def __init__(self): + self.__parsed_items = None + + def parse(self, json_object): + """ Returns multi-level list of recursively parsed items """ + + self.__parsed_items = list() + self.__parse_schema(json_object, 'root', True, 0) + return self.__parsed_items + + def __parse_schema(self, schema, name, required, level): + """ Parses schema, which type is object, array or leaf. + Appends new ParsedItem to self.__parsed_items lis """ + parsed_item = ParsedItem(schema, name, required, level) + self.__parsed_items.append(parsed_item) + required = schema.get('required', []) + + if 'enum' in schema: + parsed_item['item'] = schema.get('enum') + item_type = schema.get('type') + if item_type == 'object' and name != 'DISTRIBUTED_STORAGE_CONFIG': + self.__parse_object(parsed_item, schema, required, level) + elif item_type == 'array': + self.__parse_array(parsed_item, schema, required, level) + else: + parse_leaf(parsed_item, schema) + + def __parse_object(self, parsed_item, schema, required, level): + """ Parses schema of type object """ + for key, value in schema.get('properties', {}).items(): + self.__parse_schema(value, key, key in required, level + 1) + + def __parse_array(self, parsed_item, schema, required, level): + """ Parses schema of type array """ + items = schema.get('items') + parsed_item['minItems'] = schema.get('minItems', None) + parsed_item['maxItems'] = schema.get('maxItems', None) + parsed_item['uniqueItems'] = schema.get('uniqueItems', False) + if isinstance(items, dict): + # item is single schema describing all elements in an array + self.__parse_schema( + items, + 'array item', + required, + level + 1) + + elif isinstance(items, list): + # item is a list of schemas + for index, list_item in enumerate(items): + self.__parse_schema( + list_item, + 'array item {}'.format(index), + index in required, + level + 1) + +def parse_leaf(parsed_item, schema): + """ Parses schema of a number and a string """ + if parsed_item['name'] != 'root': + parsed_item['description'] = schema.get('description','') + parsed_item['x-reference'] = schema.get('x-reference','') + parsed_item['pattern'] = schema.get('pattern','') + parsed_item['enum'] = ", ".join(schema.get('enum','')).encode() + + ex = schema.get('x-example', '') + if isinstance(ex, list): + parsed_item['x-example'] = ", ".join(ex).encode() + elif isinstance(ex, collections.OrderedDict): + parsed_item['x-example'] = json.dumps(ex) + else: + parsed_item['x-example'] = ex diff --git a/util/config/configdocs/html_output.py b/util/config/configdocs/html_output.py new file mode 100644 index 000000000..35ccf792b --- /dev/null +++ b/util/config/configdocs/html_output.py @@ -0,0 +1,63 @@ +class HtmlOutput: + """ Generates HTML from documentation model """ + + def __init__(self): + pass + + def generate_output(self, parsed_items): + """Returns generated HTML strin""" + return self.__get_html_begin() + \ + self.__get_html_middle(parsed_items) + \ + self.__get_html_end() + + def __get_html_begin(self): + return '\n\n\n\n\n\n' + + def __get_html_end(self): + return '\n' + + def __get_html_middle(self, parsed_items): + output = '' + root_item = parsed_items[0] + + #output += '

{}

\n'.format(root_item['title']) + #output += '

{}

\n'.format(root_item['title']) + output += "Schema for Quay Enterprise" + + output += '\n' + return output + + def __get_required_field(self, parsed_item): + return 'required' if parsed_item['required'] else '' + + def __get_html_item(self, parsed_item, is_root): + item = '
  • \n' + item += '
    {}
    \n'.format(parsed_item['name']) + item += '
    [{}]
    \n'.format(parsed_item['type']) + item += '
    {}
    \n'.format(self.__get_required_field(parsed_item)) + item += '
    \n' if not is_root else '
    \n' + item += '
    {}
    \n'.format(parsed_item['title']) + item += ': ' if parsed_item['title'] != '' and parsed_item['description'] != '' else '' + item += '
    {}
    \n'.format(parsed_item['description']) + item += '
    enum: {}
    \n'.format(parsed_item['enum']) if parsed_item['enum'] != '' else '' + item += '
    Min Items: {}
    \n'.format(parsed_item['minItems']) if parsed_item['type'] == "array" and parsed_item['minItems'] != "None" else '' + item += '
    Unique Items: {}
    \n'.format(parsed_item['uniqueItems']) if parsed_item['type'] == "array" and parsed_item['uniqueItems'] else '' + item += '
    Pattern: {}
    \n'.format(parsed_item['pattern']) if parsed_item['pattern'] != 'None' and parsed_item['pattern'] != '' else '' + item += '\n'.format(parsed_item['x-reference'],parsed_item['x-reference']) if parsed_item['x-reference'] != '' else '' + item += '
    Example: {}
    \n'.format(parsed_item['x-example']) if parsed_item['x-example'] != '' else '' + item += '
    \n' + item += '
  • \n' + return item + diff --git a/util/config/configdocs/schema.html b/util/config/configdocs/schema.html new file mode 100644 index 000000000..f0dc6041a --- /dev/null +++ b/util/config/configdocs/schema.html @@ -0,0 +1,1043 @@ + + + + + + +Schema for Quay Enterprise + + diff --git a/util/config/configdocs/style.css b/util/config/configdocs/style.css new file mode 100644 index 000000000..4b0bdb6f6 --- /dev/null +++ b/util/config/configdocs/style.css @@ -0,0 +1,78 @@ +body { + font-family: sans-serif; +} + +pre, code{ + white-space: normal; +} + +div.root docs{ + display: none; +} + +div.name { + display: inline; +} + +div.type { + display: inline; + font-weight: bold; + color: blue; +} + +div.required { + display: inline; + font-weight: bold; +} + +div.docs { + display: block; +} + +div.title { + display: block; + font-weight: bold; +} + +div.description { + display: block; + font-family: serif; + font-style: italic; +} + +div.enum { + display: block; + font-family: serif; + font-style: italic; +} + +div.x-example { + display: block; + font-family: serif; + font-style: italic; + margin-bottom: 10px; +} + +div.pattern { + display: block; + font-family: serif; + font-style: italic; +} + +div.x-reference { + display: block; + font-family: serif; + font-style: italic; +} + +div.uniqueItems { + display: block; +} + +div.minItems { + display: block; +} + +div.maxItems { + display: block; +}