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