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