blob: caa2f354eb13dd7502f7d0aaee8dffd20c8cfe61 [file] [log] [blame]
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright 2017 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Generates markdown from the JSON schema."""
from __future__ import print_function
import argparse
import collections
import itertools
import os.path
import re
import sys
import yaml # pylint: disable=import-error
import libcros_schema
def ParseArgs(argv):
"""Parse the available arguments.
Invalid arguments or -h cause this function to print a message and exit.
Args:
argv: List of string arguments (excluding program name / argv[0])
Returns:
argparse.Namespace object containing the attributes.
"""
parser = argparse.ArgumentParser(
description='Generates Markdown documentation based on the config schema')
parser.add_argument(
'-s',
'--schema',
default='cros_config_host/cros_config_schema.yaml',
type=str,
help='Schema file that is processed')
parser.add_argument(
'-o', '--output', type=str, help='Output file that will be generated')
return parser.parse_args(argv)
def PopulateTypeDef(
name,
type_def,
ref_types,
output,
inherited_build_only=False):
"""Populates type definitions in the output (recursive)
Args:
name: Name of the type
type_def: Dict containing all of the type def attributes
ref_types: Shared type definitions using the #ref attribute
output: Running array of markdown string output lines
inherited_build_only: Boolean, whether this element is the child of a
build-only element.
"""
child_types = collections.OrderedDict()
output.append('### %s' % name)
output.append('| Attribute | Type | RegEx | Required | Oneof Group '
'| Build-only | Description |')
output.append('| --------- | ------ | --------- | -------- | ----------- '
'| ---------- | ----------- |')
attrs_by_group = {'': collections.OrderedDict(
sorted(type_def.get('properties', {}).items()))}
group_index = 0
for group in itertools.chain(type_def.get('oneOf', []),
type_def.get('anyOf', [])):
group_attrs = collections.OrderedDict(
sorted(group.get('properties', {}).items()))
group_name = 'GROUP(%s)' % group_index
for group_attr in group_attrs.values():
match = re.match(r'\[(.*)\] .*', group_attr.get('description', ''))
if match:
group_name = match.group(1)
attrs_by_group[group_name] = group_attrs
group_index = group_index + 1
additional_props = type_def.get('additionalProperties', False)
if additional_props:
output.append(
'| [ANY] | N/A | N/A | N/A | N/A | N/A | '
'This type allows additional properties not governed by the schema. '
'See the type description for details on these additional properties.|')
for attr_group_name, attrs in attrs_by_group.items():
for attr in attrs:
attr_name = attr
# https://github.com/google/gitiles/blob/HEAD/Documentation/markdown.md#named-anchors
attr_anchor = ''
for c in attr_name:
if c.isalnum():
attr_anchor += c
elif c.isspace():
attr_anchor += '-'
else:
attr_anchor += '_'
attr_anchor = re.sub('-+', '-', attr_anchor)
attr_anchor = re.sub('_+', '_', attr_anchor)
type_attrs = attrs[attr]
if '$ref' in type_attrs:
type_attrs = ref_types[type_attrs['$ref']]
attr_type = type_attrs['type']
regex = type_attrs.get('pattern', '')
if regex:
# Regex need escaping for markdown
regex = '```%s```' % regex
description = type_attrs.get('description', '')
description = description.replace('\n', ' ')
required_list = type_def.get('required', [])
required = attr in required_list
build_only = (inherited_build_only
or type_attrs.get('build-only-element', False))
if type_attrs['type'] == 'object':
child_types[attr_name] = type_attrs
if build_only:
child_types[attr_name]['build-only-element'] = True
attr_type = '[%s](#%s)' % (attr_name, attr_anchor)
elif type_attrs['type'] == 'array':
description = type_attrs['items'].get('description', '')
if type_attrs['items']['type'] == 'object':
child_types[attr_name] = type_attrs['items']
if build_only:
child_types[attr_name]['build-only-element'] = True
attr_type = 'array - [%s](#%s)' % (attr_name, attr_anchor)
else:
attr_type = 'array - %s' % type_attrs['items']['type']
elif type_attrs['type'] == 'integer':
if 'minimum' in type_attrs:
description += ' Minimum value: %s.' % hex(type_attrs['minimum'])
if 'maximum' in type_attrs:
description += ' Maximum value: %s.' % hex(type_attrs['maximum'])
output_tuple = (attr_name,
attr_type,
regex,
required,
attr_group_name,
build_only,
description)
output.append('| %s | %s | %s | %s | %s | %s | %s |' % output_tuple)
output.append('')
for child_type in child_types:
child_is_build_only = (inherited_build_only
or child_types[child_type].get('build-only-element',
False))
PopulateTypeDef(child_type, child_types[child_type], ref_types, output,
inherited_build_only=child_is_build_only)
def Main(schema, output):
"""Generates markdown documentation based on the JSON schema.
Args:
schema: Schema file.
output: Output file.
"""
schema_yaml = yaml.load(
libcros_schema.ApplyImports(schema), Loader=yaml.SafeLoader)
ref_types = {}
for type_def in schema_yaml.get('typeDefs', []):
ref_types['#/typeDefs/%s' % type_def] = schema_yaml['typeDefs'][type_def]
type_def_outputs = []
type_def_outputs.append('[](begin_definitions)')
type_def_outputs.append('')
PopulateTypeDef(
'model',
schema_yaml['properties']['chromeos']['properties']['configs']['items'],
ref_types, type_def_outputs)
type_def_outputs.append('')
type_def_outputs.append('[](end_definitions)')
type_def_outputs.append('')
if output:
pre_lines = []
post_lines = []
if os.path.isfile(output):
with open(output) as output_stream:
output_lines = output_stream.readlines()
pre_section = True
post_section = False
for line in output_lines:
if 'begin_definitions' in line:
pre_section = False
if pre_section:
pre_lines.append(line)
if post_section:
post_lines.append(line)
if 'end_definitions' in line:
post_section = True
with open(output, 'w') as output_stream:
if pre_lines:
output_stream.writelines(pre_lines)
output_stream.write('\n'.join(type_def_outputs))
if post_lines:
output_stream.writelines(post_lines)
else:
print('\n'.join(type_def_outputs))
if __name__ == '__main__':
args = ParseArgs(sys.argv[1:])
Main(args.schema, args.output)