blob: f4acfd919f2e1af388af59d9eb5516517750095a [file] [log] [blame]
# Copyright 2013 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
from collections import defaultdict, Mapping
import traceback
from third_party.json_schema_compiler import json_parse, idl_schema, idl_parser
from reference_resolver import ReferenceResolver
from compiled_file_system import CompiledFileSystem
class SchemaProcessorForTest(object):
'''Fake SchemaProcessor class. Returns the original schema, without
processing.
'''
def Process(self, path, file_data):
if path.endswith('.idl'):
idl = idl_schema.IDLSchema(idl_parser.IDLParser().ParseData(file_data))
# Wrap the result in a list so that it behaves like JSON API data.
return [idl.process()[0]]
return json_parse.Parse(file_data)
class SchemaProcessorFactoryForTest(object):
'''Returns a fake SchemaProcessor class to be used for testing.
'''
def Create(self, retain_inlined_types):
return SchemaProcessorForTest()
class SchemaProcessorFactory(object):
'''Factory for creating the schema processing utility.
'''
def __init__(self,
reference_resolver,
api_models,
features_bundle,
compiled_fs_factory,
file_system):
self._reference_resolver = reference_resolver
self._api_models = api_models
self._features_bundle = features_bundle
self._compiled_fs_factory = compiled_fs_factory
self._file_system = file_system
def Create(self, retain_inlined_types):
return SchemaProcessor(self._reference_resolver.Get(),
self._api_models.Get(),
self._features_bundle.Get(),
self._compiled_fs_factory,
self._file_system,
retain_inlined_types)
class SchemaProcessor(object):
'''Helper for parsing the API schema.
'''
def __init__(self,
reference_resolver,
api_models,
features_bundle,
compiled_fs_factory,
file_system,
retain_inlined_types):
self._reference_resolver = reference_resolver
self._api_models = api_models
self._features_bundle = features_bundle
self._retain_inlined_types = retain_inlined_types
self._compiled_file_system = compiled_fs_factory.Create(
file_system, self.Process, SchemaProcessor, category='json-cache')
self._api_stack = []
def _RemoveNoDocs(self, item):
'''Removes nodes that should not be rendered from an API schema.
'''
if json_parse.IsDict(item):
if item.get('nodoc', False):
return True
for key, value in item.items():
if self._RemoveNoDocs(value):
del item[key]
elif type(item) == list:
to_remove = []
for i in item:
if self._RemoveNoDocs(i):
to_remove.append(i)
for i in to_remove:
item.remove(i)
return False
def _DetectInlineableTypes(self, schema):
'''Look for documents that are only referenced once and mark them as inline.
Actual inlining is done by _InlineDocs.
'''
if not schema.get('types'):
return
ignore = frozenset(('value', 'choices'))
refcounts = defaultdict(int)
# Use an explicit stack instead of recursion.
stack = [schema]
while stack:
node = stack.pop()
if isinstance(node, list):
stack.extend(node)
elif isinstance(node, Mapping):
if '$ref' in node:
refcounts[node['$ref']] += 1
stack.extend(v for k, v in node.iteritems() if k not in ignore)
for type_ in schema['types']:
if not 'noinline_doc' in type_:
if refcounts[type_['id']] == 1:
type_['inline_doc'] = True
def _InlineDocs(self, schema):
'''Replace '$ref's that refer to inline_docs with the json for those docs.
If |retain_inlined_types| is False, then the inlined nodes are removed
from the schema.
'''
inline_docs = {}
types_without_inline_doc = []
internal_api = False
api_features = self._features_bundle.GetAPIFeatures().Get()
# We don't want to inline the events API, as it's handled differently
# Also, the webviewTag API is handled differently, as it only exists
# for the purpose of documentation, it's not a true internal api
namespace = schema.get('namespace', '')
if namespace != 'events' and namespace != 'webviewTag':
internal_api = api_features.get(schema.get('namespace', ''), {}).get(
'internal', False)
api_refs = set()
# Gather refs to internal APIs
def gather_api_refs(node):
if isinstance(node, list):
for i in node:
gather_api_refs(i)
elif isinstance(node, Mapping):
ref = node.get('$ref')
if ref:
api_refs.add(ref)
for k, v in node.iteritems():
gather_api_refs(v)
gather_api_refs(schema)
if len(api_refs) > 0:
api_list = self._api_models.GetNames()
api_name = schema.get('namespace', '')
self._api_stack.append(api_name)
for api in self._api_stack:
if api in api_list:
api_list.remove(api)
for ref in api_refs:
model, node_info = self._reference_resolver.GetRefModel(ref, api_list)
if model and api_features.get(model.name, {}).get('internal', False):
category, name = node_info
for ref_schema in self._compiled_file_system.GetFromFile(
model.source_file).Get():
if category == 'type':
for type_json in ref_schema.get('types'):
if type_json['id'] == name:
inline_docs[ref] = type_json
elif category == 'event':
for type_json in ref_schema.get('events'):
if type_json['name'] == name:
inline_docs[ref] = type_json
self._api_stack.remove(api_name)
types = schema.get('types')
if types:
# Gather the types with inline_doc.
for type_ in types:
if type_.get('inline_doc'):
inline_docs[type_['id']] = type_
if not self._retain_inlined_types:
for k in ('description', 'id', 'inline_doc'):
type_.pop(k, None)
elif internal_api:
inline_docs[type_['id']] = type_
# For internal apis that are not inline_doc we want to retain them
# in the schema (i.e. same behaviour as remain_inlined_types)
types_without_inline_doc.append(type_)
else:
types_without_inline_doc.append(type_)
if not self._retain_inlined_types:
schema['types'] = types_without_inline_doc
def apply_inline(node):
if isinstance(node, list):
for i in node:
apply_inline(i)
elif isinstance(node, Mapping):
ref = node.get('$ref')
if ref and ref in inline_docs:
del node['$ref']
for k, v in inline_docs[ref].iteritems():
if k not in node:
node[k] = v
for k, v in node.iteritems():
apply_inline(v)
apply_inline(schema)
def Process(self, path, file_data):
'''Parses |file_data| using a method determined by checking the
extension of the file at the given |path|. Then, trims 'nodoc' and if
|self.retain_inlined_types| is given and False, removes inlineable types
from the parsed schema data.
'''
def trim_and_inline(schema, is_idl=False):
'''Modifies an API schema in place by removing nodes that shouldn't be
documented and inlining schema types that are only referenced once.
'''
if self._RemoveNoDocs(schema):
# A return of True signifies that the entire schema should not be
# documented. Otherwise, only nodes that request 'nodoc' are removed.
return None
if is_idl:
self._DetectInlineableTypes(schema)
self._InlineDocs(schema)
return schema
if path.endswith('.idl'):
idl = idl_schema.IDLSchema(
idl_parser.IDLParser().ParseData(file_data))
# Wrap the result in a list so that it behaves like JSON API data.
return [trim_and_inline(idl.process()[0], is_idl=True)]
try:
schemas = json_parse.Parse(file_data)
except:
raise ValueError('Cannot parse "%s" as JSON:\n%s' %
(path, traceback.format_exc()))
for schema in schemas:
# Schemas could consist of one API schema (data for a specific API file)
# or multiple (data from extension_api.json).
trim_and_inline(schema)
return schemas