11e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)# Copyright 2013 The Chromium Authors. All rights reserved.
21e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)# Use of this source code is governed by a BSD-style license that can be
31e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)# found in the LICENSE file.
41e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)
51e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)from collections import defaultdict, Mapping
65d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)import traceback
71e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)
81e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)from third_party.json_schema_compiler import json_parse, idl_schema, idl_parser
91e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)
101e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)
111e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)def RemoveNoDocs(item):
121e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)  '''Removes nodes that should not be rendered from an API schema.
131e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)  '''
141e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)  if json_parse.IsDict(item):
151e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)    if item.get('nodoc', False):
161e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)      return True
171e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)    for key, value in item.items():
181e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)      if RemoveNoDocs(value):
191e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)        del item[key]
201e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)  elif type(item) == list:
211e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)    to_remove = []
221e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)    for i in item:
231e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)      if RemoveNoDocs(i):
241e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)        to_remove.append(i)
251e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)    for i in to_remove:
261e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)      item.remove(i)
271e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)  return False
281e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)
291e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)
301e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)def DetectInlineableTypes(schema):
311e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)  '''Look for documents that are only referenced once and mark them as inline.
321e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)  Actual inlining is done by _InlineDocs.
331e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)  '''
341e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)  if not schema.get('types'):
351e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)    return
361e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)
371e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)  ignore = frozenset(('value', 'choices'))
381e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)  refcounts = defaultdict(int)
391e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)  # Use an explicit stack instead of recursion.
401e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)  stack = [schema]
411e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)
421e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)  while stack:
431e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)    node = stack.pop()
441e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)    if isinstance(node, list):
451e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)      stack.extend(node)
461e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)    elif isinstance(node, Mapping):
471e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)      if '$ref' in node:
481e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)        refcounts[node['$ref']] += 1
491e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)      stack.extend(v for k, v in node.iteritems() if k not in ignore)
501e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)
511e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)  for type_ in schema['types']:
521e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)    if not 'noinline_doc' in type_:
531e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)      if refcounts[type_['id']] == 1:
541e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)        type_['inline_doc'] = True
551e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)
561e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)
571e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)def InlineDocs(schema):
581e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)  '''Replace '$ref's that refer to inline_docs with the json for those docs.
591e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)  '''
601e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)  types = schema.get('types')
611e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)  if types is None:
621e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)    return
631e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)
641e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)  inline_docs = {}
651e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)  types_without_inline_doc = []
661e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)
671e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)  # Gather the types with inline_doc.
681e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)  for type_ in types:
691e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)    if type_.get('inline_doc'):
701e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)      inline_docs[type_['id']] = type_
711e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)      for k in ('description', 'id', 'inline_doc'):
721e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)        type_.pop(k, None)
731e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)    else:
741e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)      types_without_inline_doc.append(type_)
751e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)  schema['types'] = types_without_inline_doc
761e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)
771e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)  def apply_inline(node):
781e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)    if isinstance(node, list):
791e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)      for i in node:
801e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)        apply_inline(i)
811e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)    elif isinstance(node, Mapping):
821e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)      ref = node.get('$ref')
831e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)      if ref and ref in inline_docs:
841e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)        node.update(inline_docs[ref])
851e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)        del node['$ref']
861e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)      for k, v in node.iteritems():
871e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)        apply_inline(v)
881e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)
891e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)  apply_inline(schema)
901e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)
911e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)
921e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)def ProcessSchema(path, file_data):
931e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)  '''Parses |file_data| using a method determined by checking the
941e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)  extension of the file at the given |path|. Then, trims 'nodoc' and handles
951e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)  inlineable types from the parsed schema data.
961e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)  '''
971e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)  def trim_and_inline(schema, is_idl=False):
981e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)    '''Modifies an API schema in place by removing nodes that shouldn't be
991e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)    documented and inlining schema types that are only referenced once.
1001e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)    '''
1011e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)    if RemoveNoDocs(schema):
1021e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)      # A return of True signifies that the entire schema should not be
1031e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)      # documented. Otherwise, only nodes that request 'nodoc' are removed.
1041e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)      return None
1051e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)    if is_idl:
1061e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)      DetectInlineableTypes(schema)
1071e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)    InlineDocs(schema)
1081e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)    return schema
1091e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)
1101e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)  if path.endswith('.idl'):
1111e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)    idl = idl_schema.IDLSchema(idl_parser.IDLParser().ParseData(file_data))
112a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)    # Wrap the result in a list so that it behaves like JSON API data.
113a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles)    return [trim_and_inline(idl.process()[0], is_idl=True)]
1141e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)
1155d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)  try:
1165d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    schemas = json_parse.Parse(file_data)
1175d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)  except:
1185d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)    raise ValueError('Cannot parse "%s" as JSON:\n%s' %
1195d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)                     (path, traceback.format_exc()))
1201e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)  for schema in schemas:
1211e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)    # Schemas could consist of one API schema (data for a specific API file)
1221e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)    # or multiple (data from extension_api.json).
1231e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)    trim_and_inline(schema)
1241e9bf3e0803691d0a228da41fc608347b6db4340Torne (Richard Coles)  return schemas
125