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