# 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: node.update(inline_docs[ref]) del node['$ref'] 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