api_data_source.py revision 6d86b77056ed63eb6871182f42a9fd5f07550f90
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