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