Home | History | Annotate | Download | only in server2
      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 
      5 from copy import copy
      6 import logging
      7 import os
      8 import posixpath
      9 
     10 from data_source import DataSource
     11 from docs_server_utils import StringIdentity
     12 from environment import IsPreviewServer
     13 from extensions_paths import JSON_TEMPLATES, PRIVATE_TEMPLATES
     14 from file_system import FileNotFoundError
     15 from future import Future, Collect
     16 import third_party.json_schema_compiler.json_parse as json_parse
     17 import third_party.json_schema_compiler.model as model
     18 from environment import IsPreviewServer
     19 from third_party.json_schema_compiler.memoize import memoize
     20 
     21 
     22 def _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 
     28 def _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 
     35 def _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 
     55 def _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 
     67 class _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 
    456 class _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 
    469 class 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