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