view yaml/yaml2xml.py @ 2769:16f6fa718be2

Updated TLSv1.3 support notes. Previous notes described some early development snapshot of OpenSSL 1.1.1 with disabled TLSv1.3 by default. It was then enabled in the first alpha. Further, the updated text covers later major releases such as OpenSSL 3.0.
author Sergey Kandaurov <pluknet@nginx.com>
date Thu, 30 Sep 2021 16:29:20 +0300
parents dd3ac7eefeed
children
line wrap: on
line source

#!/usr/bin/env python

# Copyright (C) Nginx, Inc.

import sys, re, datetime

from yaml import load, dump
from collections import OrderedDict

try:
    from yaml import CLoader as Loader, CDumper as Dumper, resolver as resolver

except ImportError:
    from yaml import Loader, Dumper, resolver


# primitive markdown parser and utf encoding for output
def node_description(node):

    text = node.get('description')
    if text == None:
        return ""

    #
    t = re.sub('\<code\>', r'<literal>', text)
    t = re.sub('\</code\>', r'</literal>', t)

    t = re.sub('\<i\>', r'<value>', t)
    t = re.sub('\</i\>', r'</value>', t)

    t = re.sub('\<a href=\"(.*?)\"\>(.*?)\</a\>', r'<link url="\1">\2</link>', t)

    # [desc](url)
    t = re.sub('\[(.*)\]\((.*?)\)', r'<link url="\2">\1</link>', t)

    # ** foo ** is value
    t = re.sub('[*?][*?](\w+)[*?][*?]', r'<value>\1</value>', t)

    # * foo * is literal
    t = re.sub('[*?](\w+)[*?]', r'<literal>\1</literal>', t)


    return t.encode('utf-8').rstrip()


def pretty_endpoint(ep):
    return ep.replace('/slabs/','slabs').replace('/resolvers/','resolvers').replace('/http/','HTTP ').replace('/stream/','stream ').replace('s/','s').replace('_',' ')


# human-readable html element id based on path
def path_to_id(path):
    if path == '/':
        return 'root'

    str = path.replace('/', '_')
    str = str.replace('{', '')
    str = str.replace('}','')

    return uncamelcase(str[1:])


def multiple(str):
    fin2 = str[-2:]
    fin = str[-1:]

    if fin2 == 's' or fin2 == 'sh' or fin2 == 'ch':
        last = 'es'
    elif fin == 'x' or fin == 'z':
        last = 'es'
    else:
        last = 's'

    return str + last

def uncamelcase(name):
    s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
    return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()

def make_defid(str):
    return 'def_' + uncamelcase(str)

# returns name of a referenced object
def get_refname(obj):
    return obj['$ref'][14:] # remove '#/definitions/'


# returns referenced object itself from global definitions table
def node_from_ref(doc, obj):
    return doc['definitions'][get_refname(obj)]


def render_doc(doc):

    out = "<section id=\"endpoints\" name=\"Endpoints\">\n"
    out += render_paths(doc)
    out += "</section>\n"

    # dry run, perform refcount
    render_defs(doc)

    out += "<section id=\"definitions\" name=\"Response Objects\">\n"
    out += render_defs(doc)
    out += "</section>\n"

    return out


def render_paths(doc):

    global curr_endpoint

    paths = doc['paths']

    out = "<para>\n"
    out += "<list type=\"tag\">\n"

    for path_key in paths:
        path_id = path_to_id(path_key)
        curr_endpoint = path_key
        out += render_path(doc, path_key, paths[path_key], path_id)

    curr_endpoint = None

    out += "</list>\n"
    out += "</para>\n"

    return out


def render_defs(doc):

    out = "<para>\n"
    out += "<list type=\"bullet\">\n"

    for d in doc['definitions']:

        if refs.get(d) == None:
            continue

        node = doc['definitions'][d]

        out += "<listitem id=\"%s\">\n" % make_defid(d)
        title = node.get('title', '')

        out += "<para>%s:</para>\n" % title

        out += render_node(doc, d, node, True)

        out += "</listitem>\n"

    out += "</list>\n"
    out += "</para>\n"

    return out


def render_path(doc, path_key, path, path_id):

    out = "<tag-name id=\"%s\" name=\"%s\">\n" % (path_id, path_key)
    out += "<literal>%s</literal>\n" % path_key
    out += "</tag-name>\n"

    out += "<tag-desc>\n"

    # List of common method parameters
    for method_key in path:
        if method_key != 'parameters':
            continue

        out += "Parameters common for all methods:\n"
        out += render_parameters(doc, path[method_key])


    # List of methods for this path
    out += '<para>Supported methods:</para>\n'
    out += '<list type="bullet" compact="yes">\n'


    for method_key in ['get', 'post', 'patch', 'delete']:

        if path.get(method_key) == None:
            continue

        method = path[method_key]

        id = method['operationId']
        summ = method['summary']
        desc = node_description(method)
        name = method_key.upper()

        out += "<listitem id=\"%s\">\n" % id
        out += "<literal>%s</literal> - %s\n" % (name, summ)
        out += "<para>%s</para>\n" % desc

        out += render_method(doc, name, method)

        out += "</listitem>\n"

    out += "</list>\n"
    out += "</tag-desc>\n"

    return out


def render_method(doc, method_name, method):

    out = ""

    if method.get('parameters'):
        out += "<para>\n"
        out += "Request parameters:\n"
        out += render_parameters(doc, method['parameters'])
        out += "</para>\n"

    out += "<para>\n"
    out += "Possible responses:\n"
    out += "</para>\n"

    out += "<list type=\"bullet\">\n"

    for response_key in method['responses']:
        out += "<listitem>"
        out += render_response(doc, response_key, method['responses'][response_key])
        out += "</listitem>\n"

    out += "</list>\n"

    return out


def render_parameters(doc, params):

    out = '<list type="tag">\n'

    for p in params:

        out += "<tag-name><literal>%s</literal>\n" % p['name']
        out += "("

        out += render_node(doc, None, p, True)

        if p.get("required"):

            if p["required"] == True:
                out += ", required"
            else:
                out += ", optional"
        else:
            out += ", optional"

        out += ")"

        out += "</tag-name>\n"
        out += "<tag-desc>\n"

        desc = node_description(p)
        out += desc

        out += "</tag-desc>\n"

    out += "</list>\n"

    return out


def render_response(doc, response_key, response):

    out = ""

    desc = node_description(response)

    out += response_key + " - " + desc

    if response.get('schema'):
        out += ", returns "
        out += render_node(doc, None, response)

    return out



def render_reference(doc, nodename, node):

    global in_array, refs

    out = ""

    ref = get_refname(node)

    refnode = node_from_ref(doc, node)

    if refnode.get('additionalProperties'):
        # in entries

        out += "a collection of "
        ref = get_refname(refnode['additionalProperties'])
        target = node_from_ref(doc, refnode['additionalProperties'])
        label = target.get('title', ref)
        out += "\"<link id=\"%s\">%s</link>\"" % (make_defid(ref), label)
        out += " objects"
        refs[ref] = 1
        if curr_endpoint != None:
            out += " for all %s" % pretty_endpoint(curr_endpoint)

        return out

    # arrays and primitive types are printed immediately
    nt = refnode.get('type', 'object')
    title = refnode.get('title', ref)
    if nt == 'object':
        if in_array == True:
            title = multiple(title)

        out += "<link id=\"%s\">%s</link>" % (make_defid(ref), title)
        refs[ref] = 1

    elif nt  == 'array':

        if nodename == 'peers':
            ref = get_refname(refnode['items'])
            out += "An array of:"
            refnode = node_from_ref(doc, refnode['items'])
            out += render_node(doc, ref, refnode, True)
            return out

        out += "an array of "

        in_array = True
        out += render_node(doc, nodename, refnode['items'], True)
        in_array = False
    else:
        # dead code actually
        out += "<literal>%s</literal>\n" % nt

    return out


# displays object recursively if described inline, or generates links
def render_node(doc, nodename, node, show_type=False):

    if node.get('$ref'):
        return render_reference(doc, nodename, node)

    elif node.get('schema'):
        return render_reference(doc, nodename, node['schema'])

    out = ""

    if node.get('additionalProperties'):
        # in definitions
        ref = get_refname(node['additionalProperties'])
        target = node_from_ref(doc, node['additionalProperties'])
        label = target.get('title', ref)
        desc = node_description(node)
        out += "<para>%s</para><para>A collection of " % desc
        out += "\"<link id=\"%s\">%s</link>\"" % (make_defid(ref), label)
        refs[ref] = 1
        out += " objects</para>\n"

        return out

    nt = node.get('type', 'object')

    if nt == 'object':
        out += render_object(doc, node)

    elif nt == 'array':
        desc = node_description(node)
        out += "<para>%s</para>\n" % desc
        out += "array element type:\n"
        out += render_node(doc, nodename, node['items'], True)

    else:
        if show_type:
            if in_array == True:
                out += "%s" % multiple(node['type'])
            else:
                out += "<literal>%s</literal>" % node['type']

    if node.get('example'):
        out += render_example(node['example'])

    return out

def json_simple_type(obj):

    if isinstance(obj, bool):
        if obj == True:
            return 'true'
        else:
            return 'false'

    elif isinstance(obj, str):
        return '"' + obj + '"'

    elif isinstance(obj,datetime.datetime):
        t = obj.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3]
        z = obj.strftime("Z%Z")
        return '"' + t + z + '"'

    else:
        return str(obj)

def render_example(obj, level = 0):

    out = ""

    if level == 0:
        if isinstance(obj, dict) or isinstance(obj, list):
            out += "<para>Example:</para>\n"
        else:
            out += "Example: <literal>%s</literal>\n" % json_simple_type(obj)
            return out

        out += "<example>\n"

    indent = '  ' * level
    next_indent = '  ' * (level + 1)

    if isinstance(obj, dict):
        out += '{\n'
        i = 0
        last = len(obj)
        for key in obj:
            out += next_indent + '"' + str(key) + '" : '
            out += render_example(obj[key], level + 1)
            if i != last - 1:
                out += ','
            out += '\n'
            i = i + 1
        out += indent + "}"

    elif isinstance(obj, list):
        out += '[\n'
        i = 0
        last = len(obj)
        for item in obj:
            out += next_indent
            out += render_example(item, level + 1)
            if i != last - 1:
                out += ','
            out += '\n'
            i = i + 1
        out += indent + "]"
    else:
        out += json_simple_type(obj)

    if level == 0:
        out += "</example>\n"


    return out


def render_object(doc, obj):

    out = ""

    if obj.get('description'):
        desc = node_description(obj)
        out += desc

    if obj.get('properties') == None:
        return out

    out += '<list type="tag">\n'
    for p in obj['properties']:

        prop = obj['properties'][p]

        out += "<tag-name>\n"
        out += "<literal>%s</literal>" % p

        if prop.get('properties') or prop.get('type') == 'object':
            obj_type = None             # there is nested object
        else:
            if prop.get('type'):
                obj_type = prop['type'] # basic type
            else:
                obj_type = None         # there is a reference

        if obj_type != None:
            out += " (<literal>%s</literal>)\n" % obj_type

        out += "</tag-name>\n"
        out += "<tag-desc>\n"

        if prop.get('description') and obj_type != None:
            desc = node_description(prop)
            out += desc + '\n'

        out += render_node(doc, p, prop)

        out += "</tag-desc>\n"
    out += "</list>\n"

    return out


###############################################################################

if len(sys.argv) < 2:
    print("Usage: %s <nginx_api.yaml>" % sys.argv[0])
    sys.exit(1)

refs = dict()
curr_endpoint = None
in_array = False

def ordered_load(stream, Loader=Loader, object_pairs_hook=OrderedDict):
    class OrderedLoader(Loader):
        pass
    def construct_mapping(loader, node):
        loader.flatten_mapping(node)
        return object_pairs_hook(loader.construct_pairs(node))
    OrderedLoader.add_constructor(
        resolver.BaseResolver.DEFAULT_MAPPING_TAG,construct_mapping)
    return load(stream, OrderedLoader)

with open(sys.argv[1], 'r') as src:
    content = src.read()
    doc = ordered_load(content, Loader=Loader)
    print(render_doc(doc))