api_data_source.py revision 3240926e260ce088908e02ac07a6cf7b0c0cbf44
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
8from collections import defaultdict, Mapping
9
10from branch_utility import BranchUtility
11import svn_constants
12from third_party.handlebar import Handlebar
13import third_party.json_schema_compiler.json_parse as json_parse
14import third_party.json_schema_compiler.model as model
15import third_party.json_schema_compiler.idl_schema as idl_schema
16import third_party.json_schema_compiler.idl_parser as idl_parser
17
18def _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
34def _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
60def _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
94def _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
99def _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
105class _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['parent_name'] = 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['parent_name'] = 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['parent_name'] = 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['parent_name'] = 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
460class _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
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  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