api_data_source.py revision 2a99a7e74a7f215066514fe81d2bfa6639d9eddd
1# Copyright (c) 2012 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 5import copy 6import logging 7import os 8 9import compiled_file_system as compiled_fs 10from file_system import FileNotFoundError 11import third_party.json_schema_compiler.json_parse as json_parse 12import third_party.json_schema_compiler.model as model 13import third_party.json_schema_compiler.idl_schema as idl_schema 14import third_party.json_schema_compiler.idl_parser as idl_parser 15 16# Increment this version when there are changes to the data stored in any of 17# the caches used by APIDataSource. This would include changes to model.py in 18# JSON schema compiler! This allows the cache to be invalidated without having 19# to flush memcache on the production server. 20_VERSION = 15 21 22def _RemoveNoDocs(item): 23 if json_parse.IsDict(item): 24 if item.get('nodoc', False): 25 return True 26 to_remove = [] 27 for key, value in item.items(): 28 if _RemoveNoDocs(value): 29 to_remove.append(key) 30 for k in to_remove: 31 del item[k] 32 elif type(item) == list: 33 to_remove = [] 34 for i in item: 35 if _RemoveNoDocs(i): 36 to_remove.append(i) 37 for i in to_remove: 38 item.remove(i) 39 return False 40 41def _CreateId(node, prefix): 42 if node.parent is not None and not isinstance(node.parent, model.Namespace): 43 return '-'.join([prefix, node.parent.simple_name, node.simple_name]) 44 return '-'.join([prefix, node.simple_name]) 45 46def _FormatValue(value): 47 """Inserts commas every three digits for integer values. It is magic. 48 """ 49 s = str(value) 50 return ','.join([s[max(0, i - 3):i] for i in range(len(s), 0, -3)][::-1]) 51 52class _JSCModel(object): 53 """Uses a Model from the JSON Schema Compiler and generates a dict that 54 a Handlebar template can use for a data source. 55 """ 56 def __init__(self, json, ref_resolver, disable_refs): 57 self._ref_resolver = ref_resolver 58 self._disable_refs = disable_refs 59 clean_json = copy.deepcopy(json) 60 if _RemoveNoDocs(clean_json): 61 self._namespace = None 62 else: 63 self._namespace = model.Namespace(clean_json, clean_json['namespace']) 64 65 def _FormatDescription(self, description): 66 if self._disable_refs: 67 return description 68 return self._ref_resolver.ResolveAllLinks(description, 69 namespace=self._namespace.name) 70 71 def _GetLink(self, link): 72 if self._disable_refs: 73 type_name = link.split('.', 1)[-1] 74 return { 'href': '#type-%s' % type_name, 'text': link, 'name': link } 75 return self._ref_resolver.SafeGetLink(link, namespace=self._namespace.name) 76 77 def ToDict(self): 78 if self._namespace is None: 79 return {} 80 return { 81 'name': self._namespace.name, 82 'types': self._GenerateTypes(self._namespace.types.values()), 83 'functions': self._GenerateFunctions(self._namespace.functions), 84 'events': self._GenerateEvents(self._namespace.events), 85 'properties': self._GenerateProperties(self._namespace.properties) 86 } 87 88 def _GenerateTypes(self, types): 89 return [self._GenerateType(t) for t in types] 90 91 def _GenerateType(self, type_): 92 type_dict = { 93 'name': type_.simple_name, 94 'description': self._FormatDescription(type_.description), 95 'properties': self._GenerateProperties(type_.properties), 96 'functions': self._GenerateFunctions(type_.functions), 97 'events': self._GenerateEvents(type_.events), 98 'id': _CreateId(type_, 'type') 99 } 100 self._RenderTypeInformation(type_, type_dict) 101 return type_dict 102 103 def _GenerateFunctions(self, functions): 104 return [self._GenerateFunction(f) for f in functions.values()] 105 106 def _GenerateFunction(self, function): 107 function_dict = { 108 'name': function.simple_name, 109 'description': self._FormatDescription(function.description), 110 'callback': self._GenerateCallback(function.callback), 111 'parameters': [], 112 'returns': None, 113 'id': _CreateId(function, 'method') 114 } 115 if (function.parent is not None and 116 not isinstance(function.parent, model.Namespace)): 117 function_dict['parent_name'] = function.parent.simple_name 118 if function.returns: 119 function_dict['returns'] = self._GenerateType(function.returns) 120 for param in function.params: 121 function_dict['parameters'].append(self._GenerateProperty(param)) 122 if function.callback is not None: 123 # Show the callback as an extra parameter. 124 function_dict['parameters'].append( 125 self._GenerateCallbackProperty(function.callback)) 126 if len(function_dict['parameters']) > 0: 127 function_dict['parameters'][-1]['last'] = True 128 return function_dict 129 130 def _GenerateEvents(self, events): 131 return [self._GenerateEvent(e) for e in events.values()] 132 133 def _GenerateEvent(self, event): 134 event_dict = { 135 'name': event.simple_name, 136 'description': self._FormatDescription(event.description), 137 'parameters': [self._GenerateProperty(p) for p in event.params], 138 'callback': self._GenerateCallback(event.callback), 139 'filters': [self._GenerateProperty(f) for f in event.filters], 140 'conditions': [self._GetLink(condition) 141 for condition in event.conditions], 142 'actions': [self._GetLink(action) for action in event.actions], 143 'supportsRules': event.supports_rules, 144 'id': _CreateId(event, 'event') 145 } 146 if (event.parent is not None and 147 not isinstance(event.parent, model.Namespace)): 148 event_dict['parent_name'] = event.parent.simple_name 149 if event.callback is not None: 150 # Show the callback as an extra parameter. 151 event_dict['parameters'].append( 152 self._GenerateCallbackProperty(event.callback)) 153 if len(event_dict['parameters']) > 0: 154 event_dict['parameters'][-1]['last'] = True 155 return event_dict 156 157 def _GenerateCallback(self, callback): 158 if not callback: 159 return None 160 callback_dict = { 161 'name': callback.simple_name, 162 'simple_type': {'simple_type': 'function'}, 163 'optional': callback.optional, 164 'parameters': [] 165 } 166 for param in callback.params: 167 callback_dict['parameters'].append(self._GenerateProperty(param)) 168 if (len(callback_dict['parameters']) > 0): 169 callback_dict['parameters'][-1]['last'] = True 170 return callback_dict 171 172 def _GenerateProperties(self, properties): 173 return [self._GenerateProperty(v) for v in properties.values()] 174 175 def _GenerateProperty(self, property_): 176 if not hasattr(property_, 'type_'): 177 for d in dir(property_): 178 if not d.startswith('_'): 179 print ('%s -> %s' % (d, getattr(property_, d))) 180 type_ = property_.type_ 181 182 # Make sure we generate property info for arrays, too. 183 # TODO(kalman): what about choices? 184 if type_.property_type == model.PropertyType.ARRAY: 185 properties = type_.item_type.properties 186 else: 187 properties = type_.properties 188 189 property_dict = { 190 'name': property_.simple_name, 191 'optional': property_.optional, 192 'description': self._FormatDescription(property_.description), 193 'properties': self._GenerateProperties(type_.properties), 194 'functions': self._GenerateFunctions(type_.functions), 195 'parameters': [], 196 'returns': None, 197 'id': _CreateId(property_, 'property') 198 } 199 200 if type_.property_type == model.PropertyType.FUNCTION: 201 function = type_.function 202 for param in function.params: 203 property_dict['parameters'].append(self._GenerateProperty(param)) 204 if function.returns: 205 property_dict['returns'] = self._GenerateType(function.returns) 206 207 if (property_.parent is not None and 208 not isinstance(property_.parent, model.Namespace)): 209 property_dict['parent_name'] = property_.parent.simple_name 210 211 value = property_.value 212 if value is not None: 213 if isinstance(value, int): 214 property_dict['value'] = _FormatValue(value) 215 else: 216 property_dict['value'] = value 217 else: 218 self._RenderTypeInformation(type_, property_dict) 219 220 return property_dict 221 222 def _GenerateCallbackProperty(self, callback): 223 property_dict = { 224 'name': callback.simple_name, 225 'description': self._FormatDescription(callback.description), 226 'optional': callback.optional, 227 'id': _CreateId(callback, 'property'), 228 'simple_type': 'function', 229 } 230 if (callback.parent is not None and 231 not isinstance(callback.parent, model.Namespace)): 232 property_dict['parent_name'] = callback.parent.simple_name 233 return property_dict 234 235 def _RenderTypeInformation(self, type_, dst_dict): 236 if type_.property_type == model.PropertyType.CHOICES: 237 dst_dict['choices'] = self._GenerateTypes(type_.choices) 238 # We keep track of which is last for knowing when to add "or" between 239 # choices in templates. 240 if len(dst_dict['choices']) > 0: 241 dst_dict['choices'][-1]['last'] = True 242 elif type_.property_type == model.PropertyType.REF: 243 dst_dict['link'] = self._GetLink(type_.ref_type) 244 elif type_.property_type == model.PropertyType.ARRAY: 245 dst_dict['array'] = self._GenerateType(type_.item_type) 246 elif type_.property_type == model.PropertyType.ENUM: 247 dst_dict['enum_values'] = [] 248 for enum_value in type_.enum_values: 249 dst_dict['enum_values'].append({'name': enum_value}) 250 if len(dst_dict['enum_values']) > 0: 251 dst_dict['enum_values'][-1]['last'] = True 252 elif type_.instance_of is not None: 253 dst_dict['simple_type'] = type_.instance_of.lower() 254 else: 255 dst_dict['simple_type'] = type_.property_type.name.lower() 256 257class _LazySamplesGetter(object): 258 """This class is needed so that an extensions API page does not have to fetch 259 the apps samples page and vice versa. 260 """ 261 def __init__(self, api_name, samples): 262 self._api_name = api_name 263 self._samples = samples 264 265 def get(self, key): 266 return self._samples.FilterSamples(key, self._api_name) 267 268class APIDataSource(object): 269 """This class fetches and loads JSON APIs from the FileSystem passed in with 270 |cache_factory|, so the APIs can be plugged into templates. 271 """ 272 class Factory(object): 273 def __init__(self, 274 cache_factory, 275 base_path): 276 self._permissions_cache = cache_factory.Create(self._LoadPermissions, 277 compiled_fs.PERMS, 278 version=_VERSION) 279 self._json_cache = cache_factory.Create( 280 lambda api_name, api: self._LoadJsonAPI(api, False), 281 compiled_fs.JSON, 282 version=_VERSION) 283 self._idl_cache = cache_factory.Create( 284 lambda api_name, api: self._LoadIdlAPI(api, False), 285 compiled_fs.IDL, 286 version=_VERSION) 287 288 # These caches are used if an APIDataSource does not want to resolve the 289 # $refs in an API. This is needed to prevent infinite recursion in 290 # ReferenceResolver. 291 self._json_cache_no_refs = cache_factory.Create( 292 lambda api_name, api: self._LoadJsonAPI(api, True), 293 compiled_fs.JSON_NO_REFS, 294 version=_VERSION) 295 self._idl_cache_no_refs = cache_factory.Create( 296 lambda api_name, api: self._LoadIdlAPI(api, True), 297 compiled_fs.IDL_NO_REFS, 298 version=_VERSION) 299 self._idl_names_cache = cache_factory.Create(self._GetIDLNames, 300 compiled_fs.IDL_NAMES, 301 version=_VERSION) 302 self._names_cache = cache_factory.Create(self._GetAllNames, 303 compiled_fs.NAMES, 304 version=_VERSION) 305 self._base_path = base_path 306 307 # These must be set later via the SetFooDataSourceFactory methods. 308 self._ref_resolver_factory = None 309 self._samples_data_source_factory = None 310 311 def SetSamplesDataSourceFactory(self, samples_data_source_factory): 312 self._samples_data_source_factory = samples_data_source_factory 313 314 def SetReferenceResolverFactory(self, ref_resolver_factory): 315 self._ref_resolver_factory = ref_resolver_factory 316 317 def Create(self, request, disable_refs=False): 318 """Create an APIDataSource. |disable_refs| specifies whether $ref's in 319 APIs being processed by the |ToDict| method of _JSCModel follows $ref's 320 in the API. This prevents endless recursion in ReferenceResolver. 321 """ 322 if self._samples_data_source_factory is None: 323 # Only error if there is a request, which means this APIDataSource is 324 # actually being used to render a page. 325 if request is not None: 326 logging.error('SamplesDataSource.Factory was never set in ' 327 'APIDataSource.Factory.') 328 samples = None 329 else: 330 samples = self._samples_data_source_factory.Create(request) 331 if not disable_refs and self._ref_resolver_factory is None: 332 logging.error('ReferenceResolver.Factory was never set in ' 333 'APIDataSource.Factory.') 334 return APIDataSource(self._permissions_cache, 335 self._json_cache, 336 self._idl_cache, 337 self._json_cache_no_refs, 338 self._idl_cache_no_refs, 339 self._names_cache, 340 self._idl_names_cache, 341 self._base_path, 342 samples, 343 disable_refs) 344 345 def _LoadPermissions(self, file_name, json_str): 346 return json_parse.Parse(json_str) 347 348 def _LoadJsonAPI(self, api, disable_refs): 349 return _JSCModel( 350 json_parse.Parse(api)[0], 351 self._ref_resolver_factory.Create() if not disable_refs else None, 352 disable_refs).ToDict() 353 354 def _LoadIdlAPI(self, api, disable_refs): 355 idl = idl_parser.IDLParser().ParseData(api) 356 return _JSCModel( 357 idl_schema.IDLSchema(idl).process()[0], 358 self._ref_resolver_factory.Create() if not disable_refs else None, 359 disable_refs).ToDict() 360 361 def _GetIDLNames(self, base_dir, apis): 362 return [ 363 model.UnixName(os.path.splitext(api[len('%s/' % self._base_path):])[0]) 364 for api in apis if api.endswith('.idl') 365 ] 366 367 def _GetAllNames(self, base_dir, apis): 368 return [ 369 model.UnixName(os.path.splitext(api[len('%s/' % self._base_path):])[0]) 370 for api in apis 371 ] 372 373 def __init__(self, 374 permissions_cache, 375 json_cache, 376 idl_cache, 377 json_cache_no_refs, 378 idl_cache_no_refs, 379 names_cache, 380 idl_names_cache, 381 base_path, 382 samples, 383 disable_refs): 384 self._base_path = base_path 385 self._permissions_cache = permissions_cache 386 self._json_cache = json_cache 387 self._idl_cache = idl_cache 388 self._json_cache_no_refs = json_cache_no_refs 389 self._idl_cache_no_refs = idl_cache_no_refs 390 self._names_cache = names_cache 391 self._idl_names_cache = idl_names_cache 392 self._samples = samples 393 self._disable_refs = disable_refs 394 395 def _GetFeatureFile(self, filename): 396 try: 397 perms = self._permissions_cache.GetFromFile('%s/%s' % 398 (self._base_path, filename)) 399 return dict((model.UnixName(k), v) for k, v in perms.iteritems()) 400 except FileNotFoundError: 401 return {} 402 403 def _GetFeatureData(self, path): 404 # Remove 'experimental_' from path name to match the keys in 405 # _permissions_features.json. 406 path = model.UnixName(path.replace('experimental_', '')) 407 408 for filename in ['_permission_features.json', '_manifest_features.json']: 409 feature_data = self._GetFeatureFile(filename).get(path, None) 410 if feature_data is not None: 411 break 412 413 # There are specific cases in which the feature is actually a list of 414 # features where only one needs to match; but currently these are only 415 # used to whitelist features for specific extension IDs. Filter those out. 416 if isinstance(feature_data, list): 417 feature_list = feature_data 418 feature_data = None 419 for single_feature in feature_list: 420 if 'whitelist' in single_feature: 421 continue 422 if feature_data is not None: 423 # Note: if you are seeing the exception below, add more heuristics as 424 # required to form a single feature. 425 raise ValueError('Multiple potential features match %s. I can\'t ' 426 'decide which one to use. Please help!' % path) 427 feature_data = single_feature 428 429 if feature_data and feature_data['channel'] in ('trunk', 'dev', 'beta'): 430 feature_data[feature_data['channel']] = True 431 return feature_data 432 433 def _GenerateHandlebarContext(self, handlebar_dict, path): 434 handlebar_dict['permissions'] = self._GetFeatureData(path) 435 handlebar_dict['samples'] = _LazySamplesGetter(path, self._samples) 436 return handlebar_dict 437 438 def _GetAsSubdirectory(self, name): 439 if name.startswith('experimental_'): 440 parts = name[len('experimental_'):].split('_', 1) 441 parts[1] = 'experimental_%s' % parts[1] 442 return '/'.join(parts) 443 return name.replace('_', '/', 1) 444 445 def get(self, key): 446 if key.endswith('.html') or key.endswith('.json') or key.endswith('.idl'): 447 path, ext = os.path.splitext(key) 448 else: 449 path = key 450 unix_name = model.UnixName(path) 451 idl_names = self._idl_names_cache.GetFromFileListing(self._base_path) 452 names = self._names_cache.GetFromFileListing(self._base_path) 453 if unix_name not in names and self._GetAsSubdirectory(unix_name) in names: 454 unix_name = self._GetAsSubdirectory(unix_name) 455 456 if self._disable_refs: 457 cache, ext = ( 458 (self._idl_cache_no_refs, '.idl') if (unix_name in idl_names) else 459 (self._json_cache_no_refs, '.json')) 460 else: 461 cache, ext = ((self._idl_cache, '.idl') if (unix_name in idl_names) else 462 (self._json_cache, '.json')) 463 return self._GenerateHandlebarContext( 464 cache.GetFromFile('%s/%s%s' % (self._base_path, unix_name, ext)), 465 path) 466