1 # Copyright 2013 The Chromium Authors. All rights reserved. 2 # Use of this source code is governed by a BSD-style license that can be 3 # found in the LICENSE file. 4 5 from collections import defaultdict, Mapping 6 import traceback 7 8 from third_party.json_schema_compiler import json_parse, idl_schema, idl_parser 9 from reference_resolver import ReferenceResolver 10 from compiled_file_system import CompiledFileSystem 11 12 class SchemaProcessorForTest(object): 13 '''Fake SchemaProcessor class. Returns the original schema, without 14 processing. 15 ''' 16 def Process(self, path, file_data): 17 if path.endswith('.idl'): 18 idl = idl_schema.IDLSchema(idl_parser.IDLParser().ParseData(file_data)) 19 # Wrap the result in a list so that it behaves like JSON API data. 20 return [idl.process()[0]] 21 return json_parse.Parse(file_data) 22 23 class SchemaProcessorFactoryForTest(object): 24 '''Returns a fake SchemaProcessor class to be used for testing. 25 ''' 26 def Create(self, retain_inlined_types): 27 return SchemaProcessorForTest() 28 29 30 class SchemaProcessorFactory(object): 31 '''Factory for creating the schema processing utility. 32 ''' 33 def __init__(self, 34 reference_resolver, 35 api_models, 36 features_bundle, 37 compiled_fs_factory, 38 file_system): 39 self._reference_resolver = reference_resolver 40 self._api_models = api_models 41 self._features_bundle = features_bundle 42 self._compiled_fs_factory = compiled_fs_factory 43 self._file_system = file_system 44 45 def Create(self, retain_inlined_types): 46 return SchemaProcessor(self._reference_resolver.Get(), 47 self._api_models.Get(), 48 self._features_bundle.Get(), 49 self._compiled_fs_factory, 50 self._file_system, 51 retain_inlined_types) 52 53 54 class SchemaProcessor(object): 55 '''Helper for parsing the API schema. 56 ''' 57 def __init__(self, 58 reference_resolver, 59 api_models, 60 features_bundle, 61 compiled_fs_factory, 62 file_system, 63 retain_inlined_types): 64 self._reference_resolver = reference_resolver 65 self._api_models = api_models 66 self._features_bundle = features_bundle 67 self._retain_inlined_types = retain_inlined_types 68 self._compiled_file_system = compiled_fs_factory.Create( 69 file_system, self.Process, SchemaProcessor, category='json-cache') 70 self._api_stack = [] 71 72 def _RemoveNoDocs(self, item): 73 '''Removes nodes that should not be rendered from an API schema. 74 ''' 75 if json_parse.IsDict(item): 76 if item.get('nodoc', False): 77 return True 78 for key, value in item.items(): 79 if self._RemoveNoDocs(value): 80 del item[key] 81 elif type(item) == list: 82 to_remove = [] 83 for i in item: 84 if self._RemoveNoDocs(i): 85 to_remove.append(i) 86 for i in to_remove: 87 item.remove(i) 88 return False 89 90 91 def _DetectInlineableTypes(self, schema): 92 '''Look for documents that are only referenced once and mark them as inline. 93 Actual inlining is done by _InlineDocs. 94 ''' 95 if not schema.get('types'): 96 return 97 98 ignore = frozenset(('value', 'choices')) 99 refcounts = defaultdict(int) 100 # Use an explicit stack instead of recursion. 101 stack = [schema] 102 103 while stack: 104 node = stack.pop() 105 if isinstance(node, list): 106 stack.extend(node) 107 elif isinstance(node, Mapping): 108 if '$ref' in node: 109 refcounts[node['$ref']] += 1 110 stack.extend(v for k, v in node.iteritems() if k not in ignore) 111 112 for type_ in schema['types']: 113 if not 'noinline_doc' in type_: 114 if refcounts[type_['id']] == 1: 115 type_['inline_doc'] = True 116 117 118 def _InlineDocs(self, schema): 119 '''Replace '$ref's that refer to inline_docs with the json for those docs. 120 If |retain_inlined_types| is False, then the inlined nodes are removed 121 from the schema. 122 ''' 123 inline_docs = {} 124 types_without_inline_doc = [] 125 internal_api = False 126 127 api_features = self._features_bundle.GetAPIFeatures().Get() 128 # We don't want to inline the events API, as it's handled differently 129 # Also, the webviewTag API is handled differently, as it only exists 130 # for the purpose of documentation, it's not a true internal api 131 namespace = schema.get('namespace', '') 132 if namespace != 'events' and namespace != 'webviewTag': 133 internal_api = api_features.get(schema.get('namespace', ''), {}).get( 134 'internal', False) 135 136 api_refs = set() 137 # Gather refs to internal APIs 138 def gather_api_refs(node): 139 if isinstance(node, list): 140 for i in node: 141 gather_api_refs(i) 142 elif isinstance(node, Mapping): 143 ref = node.get('$ref') 144 if ref: 145 api_refs.add(ref) 146 for k, v in node.iteritems(): 147 gather_api_refs(v) 148 gather_api_refs(schema) 149 150 if len(api_refs) > 0: 151 api_list = self._api_models.GetNames() 152 api_name = schema.get('namespace', '') 153 self._api_stack.append(api_name) 154 for api in self._api_stack: 155 if api in api_list: 156 api_list.remove(api) 157 for ref in api_refs: 158 model, node_info = self._reference_resolver.GetRefModel(ref, api_list) 159 if model and api_features.get(model.name, {}).get('internal', False): 160 category, name = node_info 161 for ref_schema in self._compiled_file_system.GetFromFile( 162 model.source_file).Get(): 163 if category == 'type': 164 for type_json in ref_schema.get('types'): 165 if type_json['id'] == name: 166 inline_docs[ref] = type_json 167 elif category == 'event': 168 for type_json in ref_schema.get('events'): 169 if type_json['name'] == name: 170 inline_docs[ref] = type_json 171 self._api_stack.remove(api_name) 172 173 types = schema.get('types') 174 if types: 175 # Gather the types with inline_doc. 176 for type_ in types: 177 if type_.get('inline_doc'): 178 inline_docs[type_['id']] = type_ 179 if not self._retain_inlined_types: 180 for k in ('description', 'id', 'inline_doc'): 181 type_.pop(k, None) 182 elif internal_api: 183 inline_docs[type_['id']] = type_ 184 # For internal apis that are not inline_doc we want to retain them 185 # in the schema (i.e. same behaviour as remain_inlined_types) 186 types_without_inline_doc.append(type_) 187 else: 188 types_without_inline_doc.append(type_) 189 if not self._retain_inlined_types: 190 schema['types'] = types_without_inline_doc 191 192 def apply_inline(node): 193 if isinstance(node, list): 194 for i in node: 195 apply_inline(i) 196 elif isinstance(node, Mapping): 197 ref = node.get('$ref') 198 if ref and ref in inline_docs: 199 node.update(inline_docs[ref]) 200 del node['$ref'] 201 for k, v in node.iteritems(): 202 apply_inline(v) 203 204 apply_inline(schema) 205 206 207 def Process(self, path, file_data): 208 '''Parses |file_data| using a method determined by checking the 209 extension of the file at the given |path|. Then, trims 'nodoc' and if 210 |self.retain_inlined_types| is given and False, removes inlineable types 211 from the parsed schema data. 212 ''' 213 def trim_and_inline(schema, is_idl=False): 214 '''Modifies an API schema in place by removing nodes that shouldn't be 215 documented and inlining schema types that are only referenced once. 216 ''' 217 if self._RemoveNoDocs(schema): 218 # A return of True signifies that the entire schema should not be 219 # documented. Otherwise, only nodes that request 'nodoc' are removed. 220 return None 221 if is_idl: 222 self._DetectInlineableTypes(schema) 223 self._InlineDocs(schema) 224 return schema 225 226 if path.endswith('.idl'): 227 idl = idl_schema.IDLSchema( 228 idl_parser.IDLParser().ParseData(file_data)) 229 # Wrap the result in a list so that it behaves like JSON API data. 230 return [trim_and_inline(idl.process()[0], is_idl=True)] 231 232 try: 233 schemas = json_parse.Parse(file_data) 234 except: 235 raise ValueError('Cannot parse "%s" as JSON:\n%s' % 236 (path, traceback.format_exc())) 237 for schema in schemas: 238 # Schemas could consist of one API schema (data for a specific API file) 239 # or multiple (data from extension_api.json). 240 trim_and_inline(schema) 241 return schemas 242