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