api_data_source.py revision e5d81f57cb97b3b6b7fccc9c5610d21eb81db09d
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 5from copy import copy 6import logging 7import os 8import posixpath 9 10from environment import IsPreviewServer 11from extensions_paths import JSON_TEMPLATES, PRIVATE_TEMPLATES 12import third_party.json_schema_compiler.json_parse as json_parse 13import third_party.json_schema_compiler.model as model 14from environment import IsPreviewServer 15from third_party.json_schema_compiler.memoize import memoize 16 17 18def _CreateId(node, prefix): 19 if node.parent is not None and not isinstance(node.parent, model.Namespace): 20 return '-'.join([prefix, node.parent.simple_name, node.simple_name]) 21 return '-'.join([prefix, node.simple_name]) 22 23 24def _FormatValue(value): 25 '''Inserts commas every three digits for integer values. It is magic. 26 ''' 27 s = str(value) 28 return ','.join([s[max(0, i - 3):i] for i in range(len(s), 0, -3)][::-1]) 29 30 31def _GetByNameDict(namespace): 32 '''Returns a dictionary mapping names to named items from |namespace|. 33 34 This lets us render specific API entities rather than the whole thing at once, 35 for example {{apis.manifestTypes.byName.ExternallyConnectable}}. 36 37 Includes items from namespace['types'], namespace['functions'], 38 namespace['events'], and namespace['properties']. 39 ''' 40 by_name = {} 41 for item_type in ('types', 'functions', 'events', 'properties'): 42 if item_type in namespace: 43 old_size = len(by_name) 44 by_name.update( 45 (item['name'], item) for item in namespace[item_type]) 46 assert len(by_name) == old_size + len(namespace[item_type]), ( 47 'Duplicate name in %r' % namespace) 48 return by_name 49 50 51def _GetEventByNameFromEvents(events): 52 '''Parses the dictionary |events| to find the definitions of members of the 53 type Event. Returns a dictionary mapping the name of a member to that 54 member's definition. 55 ''' 56 assert 'types' in events, \ 57 'The dictionary |events| must contain the key "types".' 58 event_list = [t for t in events['types'] if t.get('name') == 'Event'] 59 assert len(event_list) == 1, 'Exactly one type must be called "Event".' 60 return _GetByNameDict(event_list[0]) 61 62 63class _JSCModel(object): 64 '''Uses a Model from the JSON Schema Compiler and generates a dict that 65 a Handlebar template can use for a data source. 66 ''' 67 68 def __init__(self, 69 api_name, 70 api_models, 71 disable_refs, 72 availability_finder, 73 json_cache, 74 template_cache, 75 features_bundle, 76 event_byname_function): 77 self._disable_refs = disable_refs 78 self._availability_finder = availability_finder 79 self._api_availabilities = json_cache.GetFromFile( 80 posixpath.join(JSON_TEMPLATES, 'api_availabilities.json')) 81 self._intro_tables = json_cache.GetFromFile( 82 posixpath.join(JSON_TEMPLATES, 'intro_tables.json')) 83 self._api_features = features_bundle.GetAPIFeatures() 84 self._template_cache = template_cache 85 self._event_byname_function = event_byname_function 86 self._namespace = api_models.GetModel(api_name).Get() 87 88 def _GetLink(self, link): 89 ref = link if '.' in link else (self._namespace.name + '.' + link) 90 return { 'ref': ref, 'text': link, 'name': link } 91 92 def ToDict(self): 93 if self._namespace is None: 94 return {} 95 chrome_dot_name = 'chrome.%s' % self._namespace.name 96 as_dict = { 97 'name': self._namespace.name, 98 'namespace': self._namespace.documentation_options.get('namespace', 99 chrome_dot_name), 100 'title': self._namespace.documentation_options.get('title', 101 chrome_dot_name), 102 'documentationOptions': self._namespace.documentation_options, 103 'types': self._GenerateTypes(self._namespace.types.values()), 104 'functions': self._GenerateFunctions(self._namespace.functions), 105 'events': self._GenerateEvents(self._namespace.events), 106 'domEvents': self._GenerateDomEvents(self._namespace.events), 107 'properties': self._GenerateProperties(self._namespace.properties), 108 } 109 if self._namespace.deprecated: 110 as_dict['deprecated'] = self._namespace.deprecated 111 # Rendering the intro list is really expensive and there's no point doing it 112 # unless we're rending the page - and disable_refs=True implies we're not. 113 if not self._disable_refs: 114 as_dict.update({ 115 'introList': self._GetIntroTableList(), 116 'channelWarning': self._GetChannelWarning(), 117 }) 118 as_dict['byName'] = _GetByNameDict(as_dict) 119 return as_dict 120 121 def _GetApiAvailability(self): 122 return self._availability_finder.GetApiAvailability(self._namespace.name) 123 124 def _GetChannelWarning(self): 125 if not self._IsExperimental(): 126 return { self._GetApiAvailability().channel: True } 127 return None 128 129 def _IsExperimental(self): 130 return self._namespace.name.startswith('experimental') 131 132 def _GenerateTypes(self, types): 133 return [self._GenerateType(t) for t in types] 134 135 def _GenerateType(self, type_): 136 type_dict = { 137 'name': type_.simple_name, 138 'description': type_.description, 139 'properties': self._GenerateProperties(type_.properties), 140 'functions': self._GenerateFunctions(type_.functions), 141 'events': self._GenerateEvents(type_.events), 142 'id': _CreateId(type_, 'type') 143 } 144 self._RenderTypeInformation(type_, type_dict) 145 return type_dict 146 147 def _GenerateFunctions(self, functions): 148 return [self._GenerateFunction(f) for f in functions.values()] 149 150 def _GenerateFunction(self, function): 151 function_dict = { 152 'name': function.simple_name, 153 'description': function.description, 154 'callback': self._GenerateCallback(function.callback), 155 'parameters': [], 156 'returns': None, 157 'id': _CreateId(function, 'method') 158 } 159 self._AddCommonProperties(function_dict, function) 160 if function.returns: 161 function_dict['returns'] = self._GenerateType(function.returns) 162 for param in function.params: 163 function_dict['parameters'].append(self._GenerateProperty(param)) 164 if function.callback is not None: 165 # Show the callback as an extra parameter. 166 function_dict['parameters'].append( 167 self._GenerateCallbackProperty(function.callback)) 168 if len(function_dict['parameters']) > 0: 169 function_dict['parameters'][-1]['last'] = True 170 return function_dict 171 172 def _GenerateEvents(self, events): 173 return [self._GenerateEvent(e) for e in events.values() 174 if not e.supports_dom] 175 176 def _GenerateDomEvents(self, events): 177 return [self._GenerateEvent(e) for e in events.values() 178 if e.supports_dom] 179 180 def _GenerateEvent(self, event): 181 event_dict = { 182 'name': event.simple_name, 183 'description': event.description, 184 'filters': [self._GenerateProperty(f) for f in event.filters], 185 'conditions': [self._GetLink(condition) 186 for condition in event.conditions], 187 'actions': [self._GetLink(action) for action in event.actions], 188 'supportsRules': event.supports_rules, 189 'supportsListeners': event.supports_listeners, 190 'properties': [], 191 'id': _CreateId(event, 'event'), 192 'byName': {}, 193 } 194 self._AddCommonProperties(event_dict, event) 195 # Add the Event members to each event in this object. 196 # If refs are disabled then don't worry about this, since it's only needed 197 # for rendering, and disable_refs=True implies we're not rendering. 198 if self._event_byname_function and not self._disable_refs: 199 event_dict['byName'].update(self._event_byname_function()) 200 # We need to create the method description for addListener based on the 201 # information stored in |event|. 202 if event.supports_listeners: 203 callback_object = model.Function(parent=event, 204 name='callback', 205 json={}, 206 namespace=event.parent, 207 origin='') 208 callback_object.params = event.params 209 if event.callback: 210 callback_object.callback = event.callback 211 callback_parameters = self._GenerateCallbackProperty(callback_object) 212 callback_parameters['last'] = True 213 event_dict['byName']['addListener'] = { 214 'name': 'addListener', 215 'callback': self._GenerateFunction(callback_object), 216 'parameters': [callback_parameters] 217 } 218 if event.supports_dom: 219 # Treat params as properties of the custom Event object associated with 220 # this DOM Event. 221 event_dict['properties'] += [self._GenerateProperty(param) 222 for param in event.params] 223 return event_dict 224 225 def _GenerateCallback(self, callback): 226 if not callback: 227 return None 228 callback_dict = { 229 'name': callback.simple_name, 230 'simple_type': {'simple_type': 'function'}, 231 'optional': callback.optional, 232 'parameters': [] 233 } 234 for param in callback.params: 235 callback_dict['parameters'].append(self._GenerateProperty(param)) 236 if (len(callback_dict['parameters']) > 0): 237 callback_dict['parameters'][-1]['last'] = True 238 return callback_dict 239 240 def _GenerateProperties(self, properties): 241 return [self._GenerateProperty(v) for v in properties.values()] 242 243 def _GenerateProperty(self, property_): 244 if not hasattr(property_, 'type_'): 245 for d in dir(property_): 246 if not d.startswith('_'): 247 print ('%s -> %s' % (d, getattr(property_, d))) 248 type_ = property_.type_ 249 250 # Make sure we generate property info for arrays, too. 251 # TODO(kalman): what about choices? 252 if type_.property_type == model.PropertyType.ARRAY: 253 properties = type_.item_type.properties 254 else: 255 properties = type_.properties 256 257 property_dict = { 258 'name': property_.simple_name, 259 'optional': property_.optional, 260 'description': property_.description, 261 'properties': self._GenerateProperties(type_.properties), 262 'functions': self._GenerateFunctions(type_.functions), 263 'parameters': [], 264 'returns': None, 265 'id': _CreateId(property_, 'property') 266 } 267 self._AddCommonProperties(property_dict, property_) 268 269 if type_.property_type == model.PropertyType.FUNCTION: 270 function = type_.function 271 for param in function.params: 272 property_dict['parameters'].append(self._GenerateProperty(param)) 273 if function.returns: 274 property_dict['returns'] = self._GenerateType(function.returns) 275 276 value = property_.value 277 if value is not None: 278 if isinstance(value, int): 279 property_dict['value'] = _FormatValue(value) 280 else: 281 property_dict['value'] = value 282 else: 283 self._RenderTypeInformation(type_, property_dict) 284 285 return property_dict 286 287 def _GenerateCallbackProperty(self, callback): 288 property_dict = { 289 'name': callback.simple_name, 290 'description': callback.description, 291 'optional': callback.optional, 292 'is_callback': True, 293 'id': _CreateId(callback, 'property'), 294 'simple_type': 'function', 295 } 296 if (callback.parent is not None and 297 not isinstance(callback.parent, model.Namespace)): 298 property_dict['parentName'] = callback.parent.simple_name 299 return property_dict 300 301 def _RenderTypeInformation(self, type_, dst_dict): 302 dst_dict['is_object'] = type_.property_type == model.PropertyType.OBJECT 303 if type_.property_type == model.PropertyType.CHOICES: 304 dst_dict['choices'] = self._GenerateTypes(type_.choices) 305 # We keep track of which == last for knowing when to add "or" between 306 # choices in templates. 307 if len(dst_dict['choices']) > 0: 308 dst_dict['choices'][-1]['last'] = True 309 elif type_.property_type == model.PropertyType.REF: 310 dst_dict['link'] = self._GetLink(type_.ref_type) 311 elif type_.property_type == model.PropertyType.ARRAY: 312 dst_dict['array'] = self._GenerateType(type_.item_type) 313 elif type_.property_type == model.PropertyType.ENUM: 314 dst_dict['enum_values'] = [ 315 {'name': value.name, 'description': value.description} 316 for value in type_.enum_values] 317 if len(dst_dict['enum_values']) > 0: 318 dst_dict['enum_values'][-1]['last'] = True 319 elif type_.instance_of is not None: 320 dst_dict['simple_type'] = type_.instance_of 321 else: 322 dst_dict['simple_type'] = type_.property_type.name 323 324 def _GetIntroTableList(self): 325 '''Create a generic data structure that can be traversed by the templates 326 to create an API intro table. 327 ''' 328 intro_rows = [ 329 self._GetIntroDescriptionRow(), 330 self._GetIntroAvailabilityRow() 331 ] + self._GetIntroDependencyRows() 332 333 # Add rows using data from intro_tables.json, overriding any existing rows 334 # if they share the same 'title' attribute. 335 row_titles = [row['title'] for row in intro_rows] 336 for misc_row in self._GetMiscIntroRows(): 337 if misc_row['title'] in row_titles: 338 intro_rows[row_titles.index(misc_row['title'])] = misc_row 339 else: 340 intro_rows.append(misc_row) 341 342 return intro_rows 343 344 def _GetIntroDescriptionRow(self): 345 ''' Generates the 'Description' row data for an API intro table. 346 ''' 347 return { 348 'title': 'Description', 349 'content': [ 350 { 'text': self._namespace.description } 351 ] 352 } 353 354 def _GetIntroAvailabilityRow(self): 355 ''' Generates the 'Availability' row data for an API intro table. 356 ''' 357 if self._IsExperimental(): 358 status = 'experimental' 359 version = None 360 else: 361 availability = self._GetApiAvailability() 362 status = availability.channel 363 version = availability.version 364 return { 365 'title': 'Availability', 366 'content': [{ 367 'partial': self._template_cache.GetFromFile( 368 posixpath.join(PRIVATE_TEMPLATES, 369 'intro_tables', 370 '%s_message.html' % status)).Get(), 371 'version': version 372 }] 373 } 374 375 def _GetIntroDependencyRows(self): 376 # Devtools aren't in _api_features. If we're dealing with devtools, bail. 377 if 'devtools' in self._namespace.name: 378 return [] 379 380 api_feature = self._api_features.Get().get(self._namespace.name) 381 if not api_feature: 382 logging.error('"%s" not found in _api_features.json' % 383 self._namespace.name) 384 return [] 385 386 permissions_content = [] 387 manifest_content = [] 388 389 def categorize_dependency(dependency): 390 def make_code_node(text): 391 return { 'class': 'code', 'text': text } 392 393 context, name = dependency.split(':', 1) 394 if context == 'permission': 395 permissions_content.append(make_code_node('"%s"' % name)) 396 elif context == 'manifest': 397 manifest_content.append(make_code_node('"%s": {...}' % name)) 398 elif context == 'api': 399 transitive_dependencies = ( 400 self._api_features.Get().get(name, {}).get('dependencies', [])) 401 for transitive_dependency in transitive_dependencies: 402 categorize_dependency(transitive_dependency) 403 else: 404 logging.error('Unrecognized dependency for %s: %s' % 405 (self._namespace.name, context)) 406 407 for dependency in api_feature.get('dependencies', ()): 408 categorize_dependency(dependency) 409 410 dependency_rows = [] 411 if permissions_content: 412 dependency_rows.append({ 413 'title': 'Permissions', 414 'content': permissions_content 415 }) 416 if manifest_content: 417 dependency_rows.append({ 418 'title': 'Manifest', 419 'content': manifest_content 420 }) 421 return dependency_rows 422 423 def _GetMiscIntroRows(self): 424 ''' Generates miscellaneous intro table row data, such as 'Permissions', 425 'Samples', and 'Learn More', using intro_tables.json. 426 ''' 427 misc_rows = [] 428 # Look up the API name in intro_tables.json, which is structured 429 # similarly to the data structure being created. If the name is found, loop 430 # through the attributes and add them to this structure. 431 table_info = self._intro_tables.Get().get(self._namespace.name) 432 if table_info is None: 433 return misc_rows 434 435 for category in table_info.iterkeys(): 436 content = [] 437 for node in table_info[category]: 438 # If there is a 'partial' argument and it hasn't already been 439 # converted to a Handlebar object, transform it to a template. 440 if 'partial' in node: 441 # Note: it's enough to copy() not deepcopy() because only a single 442 # top-level key is being modified. 443 node = copy(node) 444 node['partial'] = self._template_cache.GetFromFile( 445 posixpath.join(PRIVATE_TEMPLATES, node['partial'])).Get() 446 content.append(node) 447 misc_rows.append({ 'title': category, 'content': content }) 448 return misc_rows 449 450 def _AddCommonProperties(self, target, src): 451 if src.deprecated is not None: 452 target['deprecated'] = src.deprecated 453 if (src.parent is not None and 454 not isinstance(src.parent, model.Namespace)): 455 target['parentName'] = src.parent.simple_name 456 457 458class _LazySamplesGetter(object): 459 '''This class is needed so that an extensions API page does not have to fetch 460 the apps samples page and vice versa. 461 ''' 462 463 def __init__(self, api_name, samples): 464 self._api_name = api_name 465 self._samples = samples 466 467 def get(self, key): 468 return self._samples.FilterSamples(key, self._api_name) 469 470 471class APIDataSource(object): 472 '''This class fetches and loads JSON APIs from the FileSystem passed in with 473 |compiled_fs_factory|, so the APIs can be plugged into templates. 474 ''' 475 476 class Factory(object): 477 def __init__(self, 478 compiled_fs_factory, 479 file_system, 480 availability_finder, 481 api_models, 482 features_bundle, 483 object_store_creator): 484 self._json_cache = compiled_fs_factory.ForJson(file_system) 485 self._template_cache = compiled_fs_factory.ForTemplates(file_system) 486 self._availability_finder = availability_finder 487 self._api_models = api_models 488 self._features_bundle = features_bundle 489 self._model_cache_refs = object_store_creator.Create( 490 APIDataSource, 'model-cache-refs') 491 self._model_cache_no_refs = object_store_creator.Create( 492 APIDataSource, 'model-cache-no-refs') 493 494 # These must be set later via the SetFooDataSourceFactory methods. 495 self._samples_data_source_factory = None 496 497 # This caches the result of _LoadEventByName. 498 self._event_byname = None 499 500 def SetSamplesDataSourceFactory(self, samples_data_source_factory): 501 self._samples_data_source_factory = samples_data_source_factory 502 503 def Create(self, request): 504 '''Creates an APIDataSource. 505 ''' 506 if self._samples_data_source_factory is None: 507 # Only error if there is a request, which means this APIDataSource is 508 # actually being used to render a page. 509 if request is not None: 510 logging.error('SamplesDataSource.Factory was never set in ' 511 'APIDataSource.Factory.') 512 samples = None 513 else: 514 samples = self._samples_data_source_factory.Create(request) 515 return APIDataSource(self._GetSchemaModel, samples) 516 517 def _LoadEventByName(self): 518 '''All events have some members in common. We source their description 519 from Event in events.json. 520 ''' 521 if self._event_byname is None: 522 self._event_byname = _GetEventByNameFromEvents( 523 self._GetSchemaModel('events', True)) 524 return self._event_byname 525 526 def _GetModelCache(self, disable_refs): 527 if disable_refs: 528 return self._model_cache_no_refs 529 return self._model_cache_refs 530 531 def _GetSchemaModel(self, api_name, disable_refs): 532 jsc_model = self._GetModelCache(disable_refs).Get(api_name).Get() 533 if jsc_model is not None: 534 return jsc_model 535 536 jsc_model = _JSCModel( 537 api_name, 538 self._api_models, 539 disable_refs, 540 self._availability_finder, 541 self._json_cache, 542 self._template_cache, 543 self._features_bundle, 544 self._LoadEventByName).ToDict() 545 546 self._GetModelCache(disable_refs).Set(api_name, jsc_model) 547 return jsc_model 548 549 def __init__(self, get_schema_model, samples): 550 self._get_schema_model = get_schema_model 551 self._samples = samples 552 553 def _GenerateHandlebarContext(self, handlebar_dict): 554 # Parsing samples on the preview server takes seconds and doesn't add 555 # anything. Don't do it. 556 if not IsPreviewServer(): 557 handlebar_dict['samples'] = _LazySamplesGetter( 558 handlebar_dict['name'], 559 self._samples) 560 return handlebar_dict 561 562 def get(self, api_name, disable_refs=False): 563 return self._GenerateHandlebarContext( 564 self._get_schema_model(api_name, disable_refs)) 565