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 import copy
      6 import logging
      7 import os
      8 from collections import defaultdict, Mapping
      9 
     10 from branch_utility import BranchUtility
     11 import svn_constants
     12 from third_party.handlebar import Handlebar
     13 import third_party.json_schema_compiler.json_parse as json_parse
     14 import third_party.json_schema_compiler.model as model
     15 import third_party.json_schema_compiler.idl_schema as idl_schema
     16 import third_party.json_schema_compiler.idl_parser as idl_parser
     17 
     18 def _RemoveNoDocs(item):
     19   if json_parse.IsDict(item):
     20     if item.get('nodoc', False):
     21       return True
     22     for key, value in item.items():
     23       if _RemoveNoDocs(value):
     24         del item[key]
     25   elif type(item) == list:
     26     to_remove = []
     27     for i in item:
     28       if _RemoveNoDocs(i):
     29         to_remove.append(i)
     30     for i in to_remove:
     31       item.remove(i)
     32   return False
     33 
     34 def _DetectInlineableTypes(schema):
     35   """Look for documents that are only referenced once and mark them as inline.
     36   Actual inlining is done by _InlineDocs.
     37   """
     38   if not schema.get('types'):
     39     return
     40 
     41   ignore = frozenset(('value', 'choices'))
     42   refcounts = defaultdict(int)
     43   # Use an explicit stack instead of recursion.
     44   stack = [schema]
     45 
     46   while stack:
     47     node = stack.pop()
     48     if isinstance(node, list):
     49       stack.extend(node)
     50     elif isinstance(node, Mapping):
     51       if '$ref' in node:
     52         refcounts[node['$ref']] += 1
     53       stack.extend(v for k, v in node.iteritems() if k not in ignore)
     54 
     55   for type_ in schema['types']:
     56     if not 'noinline_doc' in type_:
     57       if refcounts[type_['id']] == 1:
     58         type_['inline_doc'] = True
     59 
     60 def _InlineDocs(schema):
     61   """Replace '$ref's that refer to inline_docs with the json for those docs.
     62   """
     63   types = schema.get('types')
     64   if types is None:
     65     return
     66 
     67   inline_docs = {}
     68   types_without_inline_doc = []
     69 
     70   # Gather the types with inline_doc.
     71   for type_ in types:
     72     if type_.get('inline_doc'):
     73       inline_docs[type_['id']] = type_
     74       for k in ('description', 'id', 'inline_doc'):
     75         type_.pop(k, None)
     76     else:
     77       types_without_inline_doc.append(type_)
     78   schema['types'] = types_without_inline_doc
     79 
     80   def apply_inline(node):
     81     if isinstance(node, list):
     82       for i in node:
     83         apply_inline(i)
     84     elif isinstance(node, Mapping):
     85       ref = node.get('$ref')
     86       if ref and ref in inline_docs:
     87         node.update(inline_docs[ref])
     88         del node['$ref']
     89       for k, v in node.iteritems():
     90         apply_inline(v)
     91 
     92   apply_inline(schema)
     93 
     94 def _CreateId(node, prefix):
     95   if node.parent is not None and not isinstance(node.parent, model.Namespace):
     96     return '-'.join([prefix, node.parent.simple_name, node.simple_name])
     97   return '-'.join([prefix, node.simple_name])
     98 
     99 def _FormatValue(value):
    100   """Inserts commas every three digits for integer values. It is magic.
    101   """
    102   s = str(value)
    103   return ','.join([s[max(0, i - 3):i] for i in range(len(s), 0, -3)][::-1])
    104 
    105 class _JSCModel(object):
    106   """Uses a Model from the JSON Schema Compiler and generates a dict that
    107   a Handlebar template can use for a data source.
    108   """
    109   def __init__(self,
    110                json,
    111                ref_resolver,
    112                disable_refs,
    113                availability_finder,
    114                parse_cache,
    115                template_data_source,
    116                idl=False):
    117     self._ref_resolver = ref_resolver
    118     self._disable_refs = disable_refs
    119     self._availability_finder = availability_finder
    120     self._intro_tables = parse_cache.GetFromFile(
    121         '%s/intro_tables.json' % svn_constants.JSON_PATH)
    122     self._api_features = parse_cache.GetFromFile(
    123         '%s/_api_features.json' % svn_constants.API_PATH)
    124     self._template_data_source = template_data_source
    125     clean_json = copy.deepcopy(json)
    126     if _RemoveNoDocs(clean_json):
    127       self._namespace = None
    128     else:
    129       if idl:
    130         _DetectInlineableTypes(clean_json)
    131       _InlineDocs(clean_json)
    132       self._namespace = model.Namespace(clean_json, clean_json['namespace'])
    133 
    134   def _FormatDescription(self, description):
    135     if self._disable_refs:
    136       return description
    137     return self._ref_resolver.ResolveAllLinks(description,
    138                                               namespace=self._namespace.name)
    139 
    140   def _GetLink(self, link):
    141     if self._disable_refs:
    142       type_name = link.split('.', 1)[-1]
    143       return { 'href': '#type-%s' % type_name, 'text': link, 'name': link }
    144     return self._ref_resolver.SafeGetLink(link, namespace=self._namespace.name)
    145 
    146   def ToDict(self):
    147     if self._namespace is None:
    148       return {}
    149     return {
    150       'name': self._namespace.name,
    151       'types': self._GenerateTypes(self._namespace.types.values()),
    152       'functions': self._GenerateFunctions(self._namespace.functions),
    153       'events': self._GenerateEvents(self._namespace.events),
    154       'properties': self._GenerateProperties(self._namespace.properties),
    155       'intro_list': self._GetIntroTableList(),
    156       'channel_warning': self._GetChannelWarning()
    157     }
    158 
    159   def _GetIntroTableList(self):
    160     """Create a generic data structure that can be traversed by the templates
    161     to create an API intro table.
    162     """
    163     intro_rows = [
    164       self._GetIntroDescriptionRow(),
    165       self._GetIntroAvailabilityRow()
    166     ] + self._GetIntroDependencyRows()
    167 
    168     # Add rows using data from intro_tables.json, overriding any existing rows
    169     # if they share the same 'title' attribute.
    170     row_titles = [row['title'] for row in intro_rows]
    171     for misc_row in self._GetMiscIntroRows():
    172       if misc_row['title'] in row_titles:
    173         intro_rows[row_titles.index(misc_row['title'])] = misc_row
    174       else:
    175         intro_rows.append(misc_row)
    176 
    177     return intro_rows
    178 
    179   def _GetIntroDescriptionRow(self):
    180     """ Generates the 'Description' row data for an API intro table.
    181     """
    182     return {
    183       'title': 'Description',
    184       'content': [
    185         { 'text': self._FormatDescription(self._namespace.description) }
    186       ]
    187     }
    188 
    189   def _GetIntroAvailabilityRow(self):
    190     """ Generates the 'Availability' row data for an API intro table.
    191     """
    192     if self._IsExperimental():
    193       status = 'experimental'
    194       version = None
    195     else:
    196       availability = self._GetApiAvailability()
    197       status = availability.channel
    198       version = availability.version
    199     return {
    200       'title': 'Availability',
    201       'content': [{
    202         'partial': self._template_data_source.get(
    203             'intro_tables/%s_message.html' % status),
    204         'version': version
    205       }]
    206     }
    207 
    208   def _GetIntroDependencyRows(self):
    209     # Devtools aren't in _api_features. If we're dealing with devtools, bail.
    210     if 'devtools' in self._namespace.name:
    211       return []
    212     feature = self._api_features.get(self._namespace.name)
    213     assert feature, ('"%s" not found in _api_features.json.'
    214                      % self._namespace.name)
    215 
    216     dependencies = feature.get('dependencies')
    217     if dependencies is None:
    218       return []
    219 
    220     def make_code_node(text):
    221       return { 'class': 'code', 'text': text }
    222 
    223     permissions_content = []
    224     manifest_content = []
    225 
    226     def categorize_dependency(dependency):
    227       context, name = dependency.split(':', 1)
    228       if context == 'permission':
    229         permissions_content.append(make_code_node('"%s"' % name))
    230       elif context == 'manifest':
    231         manifest_content.append(make_code_node('"%s": {...}' % name))
    232       elif context == 'api':
    233         transitive_dependencies = (
    234             self._api_features.get(context, {}).get('dependencies', []))
    235         for transitive_dependency in transitive_dependencies:
    236           categorize_dependency(transitive_dependency)
    237       else:
    238         raise ValueError('Unrecognized dependency for %s: %s' % (
    239             self._namespace.name, context))
    240 
    241     for dependency in dependencies:
    242       categorize_dependency(dependency)
    243 
    244     dependency_rows = []
    245     if permissions_content:
    246       dependency_rows.append({
    247         'title': 'Permissions',
    248         'content': permissions_content
    249       })
    250     if manifest_content:
    251       dependency_rows.append({
    252         'title': 'Manifest',
    253         'content': manifest_content
    254       })
    255     return dependency_rows
    256 
    257   def _GetMiscIntroRows(self):
    258     """ Generates miscellaneous intro table row data, such as 'Permissions',
    259     'Samples', and 'Learn More', using intro_tables.json.
    260     """
    261     misc_rows = []
    262     # Look up the API name in intro_tables.json, which is structured
    263     # similarly to the data structure being created. If the name is found, loop
    264     # through the attributes and add them to this structure.
    265     table_info = self._intro_tables.get(self._namespace.name)
    266     if table_info is None:
    267       return misc_rows
    268 
    269     for category in table_info.keys():
    270       content = copy.deepcopy(table_info[category])
    271       for node in content:
    272         # If there is a 'partial' argument and it hasn't already been
    273         # converted to a Handlebar object, transform it to a template.
    274         if 'partial' in node:
    275           node['partial'] = self._template_data_source.get(node['partial'])
    276       misc_rows.append({ 'title': category, 'content': content })
    277     return misc_rows
    278 
    279   def _GetApiAvailability(self):
    280     return self._availability_finder.GetApiAvailability(self._namespace.name)
    281 
    282   def _GetChannelWarning(self):
    283     if not self._IsExperimental():
    284       return { self._GetApiAvailability().channel: True }
    285     return None
    286 
    287   def _IsExperimental(self):
    288      return self._namespace.name.startswith('experimental')
    289 
    290   def _GenerateTypes(self, types):
    291     return [self._GenerateType(t) for t in types]
    292 
    293   def _GenerateType(self, type_):
    294     type_dict = {
    295       'name': type_.simple_name,
    296       'description': self._FormatDescription(type_.description),
    297       'properties': self._GenerateProperties(type_.properties),
    298       'functions': self._GenerateFunctions(type_.functions),
    299       'events': self._GenerateEvents(type_.events),
    300       'id': _CreateId(type_, 'type')
    301     }
    302     self._RenderTypeInformation(type_, type_dict)
    303     return type_dict
    304 
    305   def _GenerateFunctions(self, functions):
    306     return [self._GenerateFunction(f) for f in functions.values()]
    307 
    308   def _GenerateFunction(self, function):
    309     function_dict = {
    310       'name': function.simple_name,
    311       'description': self._FormatDescription(function.description),
    312       'callback': self._GenerateCallback(function.callback),
    313       'parameters': [],
    314       'returns': None,
    315       'id': _CreateId(function, 'method')
    316     }
    317     if (function.parent is not None and
    318         not isinstance(function.parent, model.Namespace)):
    319       function_dict['parentName'] = function.parent.simple_name
    320     if function.returns:
    321       function_dict['returns'] = self._GenerateType(function.returns)
    322     for param in function.params:
    323       function_dict['parameters'].append(self._GenerateProperty(param))
    324     if function.callback is not None:
    325       # Show the callback as an extra parameter.
    326       function_dict['parameters'].append(
    327           self._GenerateCallbackProperty(function.callback))
    328     if len(function_dict['parameters']) > 0:
    329       function_dict['parameters'][-1]['last'] = True
    330     return function_dict
    331 
    332   def _GenerateEvents(self, events):
    333     return [self._GenerateEvent(e) for e in events.values()]
    334 
    335   def _GenerateEvent(self, event):
    336     event_dict = {
    337       'name': event.simple_name,
    338       'description': self._FormatDescription(event.description),
    339       'parameters': [self._GenerateProperty(p) for p in event.params],
    340       'callback': self._GenerateCallback(event.callback),
    341       'filters': [self._GenerateProperty(f) for f in event.filters],
    342       'conditions': [self._GetLink(condition)
    343                      for condition in event.conditions],
    344       'actions': [self._GetLink(action) for action in event.actions],
    345       'supportsRules': event.supports_rules,
    346       'id': _CreateId(event, 'event')
    347     }
    348     if (event.parent is not None and
    349         not isinstance(event.parent, model.Namespace)):
    350       event_dict['parentName'] = event.parent.simple_name
    351     if event.callback is not None:
    352       # Show the callback as an extra parameter.
    353       event_dict['parameters'].append(
    354           self._GenerateCallbackProperty(event.callback))
    355     if len(event_dict['parameters']) > 0:
    356       event_dict['parameters'][-1]['last'] = True
    357     return event_dict
    358 
    359   def _GenerateCallback(self, callback):
    360     if not callback:
    361       return None
    362     callback_dict = {
    363       'name': callback.simple_name,
    364       'simple_type': {'simple_type': 'function'},
    365       'optional': callback.optional,
    366       'parameters': []
    367     }
    368     for param in callback.params:
    369       callback_dict['parameters'].append(self._GenerateProperty(param))
    370     if (len(callback_dict['parameters']) > 0):
    371       callback_dict['parameters'][-1]['last'] = True
    372     return callback_dict
    373 
    374   def _GenerateProperties(self, properties):
    375     return [self._GenerateProperty(v) for v in properties.values()]
    376 
    377   def _GenerateProperty(self, property_):
    378     if not hasattr(property_, 'type_'):
    379       for d in dir(property_):
    380         if not d.startswith('_'):
    381           print ('%s -> %s' % (d, getattr(property_, d)))
    382     type_ = property_.type_
    383 
    384     # Make sure we generate property info for arrays, too.
    385     # TODO(kalman): what about choices?
    386     if type_.property_type == model.PropertyType.ARRAY:
    387       properties = type_.item_type.properties
    388     else:
    389       properties = type_.properties
    390 
    391     property_dict = {
    392       'name': property_.simple_name,
    393       'optional': property_.optional,
    394       'description': self._FormatDescription(property_.description),
    395       'properties': self._GenerateProperties(type_.properties),
    396       'functions': self._GenerateFunctions(type_.functions),
    397       'parameters': [],
    398       'returns': None,
    399       'id': _CreateId(property_, 'property')
    400     }
    401 
    402     if type_.property_type == model.PropertyType.FUNCTION:
    403       function = type_.function
    404       for param in function.params:
    405         property_dict['parameters'].append(self._GenerateProperty(param))
    406       if function.returns:
    407         property_dict['returns'] = self._GenerateType(function.returns)
    408 
    409     if (property_.parent is not None and
    410         not isinstance(property_.parent, model.Namespace)):
    411       property_dict['parentName'] = property_.parent.simple_name
    412 
    413     value = property_.value
    414     if value is not None:
    415       if isinstance(value, int):
    416         property_dict['value'] = _FormatValue(value)
    417       else:
    418         property_dict['value'] = value
    419     else:
    420       self._RenderTypeInformation(type_, property_dict)
    421 
    422     return property_dict
    423 
    424   def _GenerateCallbackProperty(self, callback):
    425     property_dict = {
    426       'name': callback.simple_name,
    427       'description': self._FormatDescription(callback.description),
    428       'optional': callback.optional,
    429       'id': _CreateId(callback, 'property'),
    430       'simple_type': 'function',
    431     }
    432     if (callback.parent is not None and
    433         not isinstance(callback.parent, model.Namespace)):
    434       property_dict['parentName'] = callback.parent.simple_name
    435     return property_dict
    436 
    437   def _RenderTypeInformation(self, type_, dst_dict):
    438     dst_dict['is_object'] = type_.property_type == model.PropertyType.OBJECT
    439     if type_.property_type == model.PropertyType.CHOICES:
    440       dst_dict['choices'] = self._GenerateTypes(type_.choices)
    441       # We keep track of which == last for knowing when to add "or" between
    442       # choices in templates.
    443       if len(dst_dict['choices']) > 0:
    444         dst_dict['choices'][-1]['last'] = True
    445     elif type_.property_type == model.PropertyType.REF:
    446       dst_dict['link'] = self._GetLink(type_.ref_type)
    447     elif type_.property_type == model.PropertyType.ARRAY:
    448       dst_dict['array'] = self._GenerateType(type_.item_type)
    449     elif type_.property_type == model.PropertyType.ENUM:
    450       dst_dict['enum_values'] = []
    451       for enum_value in type_.enum_values:
    452         dst_dict['enum_values'].append({'name': enum_value})
    453       if len(dst_dict['enum_values']) > 0:
    454         dst_dict['enum_values'][-1]['last'] = True
    455     elif type_.instance_of is not None:
    456       dst_dict['simple_type'] = type_.instance_of.lower()
    457     else:
    458       dst_dict['simple_type'] = type_.property_type.name.lower()
    459 
    460 class _LazySamplesGetter(object):
    461   """This class is needed so that an extensions API page does not have to fetch
    462   the apps samples page and vice versa.
    463   """
    464   def __init__(self, api_name, samples):
    465     self._api_name = api_name
    466     self._samples = samples
    467 
    468   def get(self, key):
    469     return self._samples.FilterSamples(key, self._api_name)
    470 
    471 class 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   class Factory(object):
    476     def __init__(self,
    477                  compiled_fs_factory,
    478                  base_path,
    479                  availability_finder_factory):
    480       def create_compiled_fs(fn, category):
    481         return compiled_fs_factory.Create(fn, APIDataSource, category=category)
    482 
    483       self._json_cache = create_compiled_fs(
    484           lambda api_name, api: self._LoadJsonAPI(api, False),
    485           'json')
    486       self._idl_cache = create_compiled_fs(
    487           lambda api_name, api: self._LoadIdlAPI(api, False),
    488           'idl')
    489 
    490       # These caches are used if an APIDataSource does not want to resolve the
    491       # $refs in an API. This is needed to prevent infinite recursion in
    492       # ReferenceResolver.
    493       self._json_cache_no_refs = create_compiled_fs(
    494           lambda api_name, api: self._LoadJsonAPI(api, True),
    495           'json-no-refs')
    496       self._idl_cache_no_refs = create_compiled_fs(
    497           lambda api_name, api: self._LoadIdlAPI(api, True),
    498           'idl-no-refs')
    499 
    500       self._idl_names_cache = create_compiled_fs(self._GetIDLNames, 'idl-names')
    501       self._names_cache = create_compiled_fs(self._GetAllNames, 'names')
    502 
    503       self._base_path = base_path
    504       self._availability_finder = availability_finder_factory.Create()
    505       self._parse_cache = create_compiled_fs(
    506           lambda _, json: json_parse.Parse(json),
    507           'intro-cache')
    508       # These must be set later via the SetFooDataSourceFactory methods.
    509       self._ref_resolver_factory = None
    510       self._samples_data_source_factory = None
    511 
    512     def SetSamplesDataSourceFactory(self, samples_data_source_factory):
    513       self._samples_data_source_factory = samples_data_source_factory
    514 
    515     def SetReferenceResolverFactory(self, ref_resolver_factory):
    516       self._ref_resolver_factory = ref_resolver_factory
    517 
    518     def SetTemplateDataSource(self, template_data_source_factory):
    519       # This TemplateDataSource is only being used for fetching template data.
    520       self._template_data_source = template_data_source_factory.Create(None, '')
    521 
    522     def Create(self, request, disable_refs=False):
    523       """Create an APIDataSource. |disable_refs| specifies whether $ref's in
    524       APIs being processed by the |ToDict| method of _JSCModel follows $ref's
    525       in the API. This prevents endless recursion in ReferenceResolver.
    526       """
    527       if self._samples_data_source_factory is None:
    528         # Only error if there is a request, which means this APIDataSource is
    529         # actually being used to render a page.
    530         if request is not None:
    531           logging.error('SamplesDataSource.Factory was never set in '
    532                         'APIDataSource.Factory.')
    533         samples = None
    534       else:
    535         samples = self._samples_data_source_factory.Create(request)
    536       if not disable_refs and self._ref_resolver_factory is None:
    537         logging.error('ReferenceResolver.Factory was never set in '
    538                       'APIDataSource.Factory.')
    539       return APIDataSource(self._json_cache,
    540                            self._idl_cache,
    541                            self._json_cache_no_refs,
    542                            self._idl_cache_no_refs,
    543                            self._names_cache,
    544                            self._idl_names_cache,
    545                            self._base_path,
    546                            samples,
    547                            disable_refs)
    548 
    549     def _LoadJsonAPI(self, api, disable_refs):
    550       return _JSCModel(
    551           json_parse.Parse(api)[0],
    552           self._ref_resolver_factory.Create() if not disable_refs else None,
    553           disable_refs,
    554           self._availability_finder,
    555           self._parse_cache,
    556           self._template_data_source).ToDict()
    557 
    558     def _LoadIdlAPI(self, api, disable_refs):
    559       idl = idl_parser.IDLParser().ParseData(api)
    560       return _JSCModel(
    561           idl_schema.IDLSchema(idl).process()[0],
    562           self._ref_resolver_factory.Create() if not disable_refs else None,
    563           disable_refs,
    564           self._availability_finder,
    565           self._parse_cache,
    566           self._template_data_source,
    567           idl=True).ToDict()
    568 
    569     def _GetIDLNames(self, base_dir, apis):
    570       return self._GetExtNames(apis, ['idl'])
    571 
    572     def _GetAllNames(self, base_dir, apis):
    573       return self._GetExtNames(apis, ['json', 'idl'])
    574 
    575     def _GetExtNames(self, apis, exts):
    576       return [model.UnixName(os.path.splitext(api)[0]) for api in apis
    577               if os.path.splitext(api)[1][1:] in exts]
    578 
    579   def __init__(self,
    580                json_cache,
    581                idl_cache,
    582                json_cache_no_refs,
    583                idl_cache_no_refs,
    584                names_cache,
    585                idl_names_cache,
    586                base_path,
    587                samples,
    588                disable_refs):
    589     self._base_path = base_path
    590     self._json_cache = json_cache
    591     self._idl_cache = idl_cache
    592     self._json_cache_no_refs = json_cache_no_refs
    593     self._idl_cache_no_refs = idl_cache_no_refs
    594     self._names_cache = names_cache
    595     self._idl_names_cache = idl_names_cache
    596     self._samples = samples
    597     self._disable_refs = disable_refs
    598 
    599   def _GenerateHandlebarContext(self, handlebar_dict, path):
    600     handlebar_dict['samples'] = _LazySamplesGetter(path, self._samples)
    601     return handlebar_dict
    602 
    603   def _GetAsSubdirectory(self, name):
    604     if name.startswith('experimental_'):
    605       parts = name[len('experimental_'):].split('_', 1)
    606       if len(parts) > 1:
    607         parts[1] = 'experimental_%s' % parts[1]
    608         return '/'.join(parts)
    609       return '%s/%s' % (parts[0], name)
    610     return name.replace('_', '/', 1)
    611 
    612   def get(self, key):
    613     if key.endswith('.html') or key.endswith('.json') or key.endswith('.idl'):
    614       path, ext = os.path.splitext(key)
    615     else:
    616       path = key
    617     unix_name = model.UnixName(path)
    618     idl_names = self._idl_names_cache.GetFromFileListing(self._base_path)
    619     names = self._names_cache.GetFromFileListing(self._base_path)
    620     if unix_name not in names and self._GetAsSubdirectory(unix_name) in names:
    621       unix_name = self._GetAsSubdirectory(unix_name)
    622 
    623     if self._disable_refs:
    624       cache, ext = (
    625           (self._idl_cache_no_refs, '.idl') if (unix_name in idl_names) else
    626           (self._json_cache_no_refs, '.json'))
    627     else:
    628       cache, ext = ((self._idl_cache, '.idl') if (unix_name in idl_names) else
    629                     (self._json_cache, '.json'))
    630     return self._GenerateHandlebarContext(
    631         cache.GetFromFile('%s/%s%s' % (self._base_path, unix_name, ext)),
    632         path)
    633