api_data_source.py revision 2a99a7e74a7f215066514fe81d2bfa6639d9eddd
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
9import compiled_file_system as compiled_fs
10from file_system import FileNotFoundError
11import third_party.json_schema_compiler.json_parse as json_parse
12import third_party.json_schema_compiler.model as model
13import third_party.json_schema_compiler.idl_schema as idl_schema
14import third_party.json_schema_compiler.idl_parser as idl_parser
15
16# Increment this version when there are changes to the data stored in any of
17# the caches used by APIDataSource. This would include changes to model.py in
18# JSON schema compiler! This allows the cache to be invalidated without having
19# to flush memcache on the production server.
20_VERSION = 15
21
22def _RemoveNoDocs(item):
23  if json_parse.IsDict(item):
24    if item.get('nodoc', False):
25      return True
26    to_remove = []
27    for key, value in item.items():
28      if _RemoveNoDocs(value):
29        to_remove.append(key)
30    for k in to_remove:
31      del item[k]
32  elif type(item) == list:
33    to_remove = []
34    for i in item:
35      if _RemoveNoDocs(i):
36        to_remove.append(i)
37    for i in to_remove:
38      item.remove(i)
39  return False
40
41def _CreateId(node, prefix):
42  if node.parent is not None and not isinstance(node.parent, model.Namespace):
43    return '-'.join([prefix, node.parent.simple_name, node.simple_name])
44  return '-'.join([prefix, node.simple_name])
45
46def _FormatValue(value):
47  """Inserts commas every three digits for integer values. It is magic.
48  """
49  s = str(value)
50  return ','.join([s[max(0, i - 3):i] for i in range(len(s), 0, -3)][::-1])
51
52class _JSCModel(object):
53  """Uses a Model from the JSON Schema Compiler and generates a dict that
54  a Handlebar template can use for a data source.
55  """
56  def __init__(self, json, ref_resolver, disable_refs):
57    self._ref_resolver = ref_resolver
58    self._disable_refs = disable_refs
59    clean_json = copy.deepcopy(json)
60    if _RemoveNoDocs(clean_json):
61      self._namespace = None
62    else:
63      self._namespace = model.Namespace(clean_json, clean_json['namespace'])
64
65  def _FormatDescription(self, description):
66    if self._disable_refs:
67      return description
68    return self._ref_resolver.ResolveAllLinks(description,
69                                              namespace=self._namespace.name)
70
71  def _GetLink(self, link):
72    if self._disable_refs:
73      type_name = link.split('.', 1)[-1]
74      return { 'href': '#type-%s' % type_name, 'text': link, 'name': link }
75    return self._ref_resolver.SafeGetLink(link, namespace=self._namespace.name)
76
77  def ToDict(self):
78    if self._namespace is None:
79      return {}
80    return {
81      'name': self._namespace.name,
82      'types': self._GenerateTypes(self._namespace.types.values()),
83      'functions': self._GenerateFunctions(self._namespace.functions),
84      'events': self._GenerateEvents(self._namespace.events),
85      'properties': self._GenerateProperties(self._namespace.properties)
86    }
87
88  def _GenerateTypes(self, types):
89    return [self._GenerateType(t) for t in types]
90
91  def _GenerateType(self, type_):
92    type_dict = {
93      'name': type_.simple_name,
94      'description': self._FormatDescription(type_.description),
95      'properties': self._GenerateProperties(type_.properties),
96      'functions': self._GenerateFunctions(type_.functions),
97      'events': self._GenerateEvents(type_.events),
98      'id': _CreateId(type_, 'type')
99    }
100    self._RenderTypeInformation(type_, type_dict)
101    return type_dict
102
103  def _GenerateFunctions(self, functions):
104    return [self._GenerateFunction(f) for f in functions.values()]
105
106  def _GenerateFunction(self, function):
107    function_dict = {
108      'name': function.simple_name,
109      'description': self._FormatDescription(function.description),
110      'callback': self._GenerateCallback(function.callback),
111      'parameters': [],
112      'returns': None,
113      'id': _CreateId(function, 'method')
114    }
115    if (function.parent is not None and
116        not isinstance(function.parent, model.Namespace)):
117      function_dict['parent_name'] = function.parent.simple_name
118    if function.returns:
119      function_dict['returns'] = self._GenerateType(function.returns)
120    for param in function.params:
121      function_dict['parameters'].append(self._GenerateProperty(param))
122    if function.callback is not None:
123      # Show the callback as an extra parameter.
124      function_dict['parameters'].append(
125          self._GenerateCallbackProperty(function.callback))
126    if len(function_dict['parameters']) > 0:
127      function_dict['parameters'][-1]['last'] = True
128    return function_dict
129
130  def _GenerateEvents(self, events):
131    return [self._GenerateEvent(e) for e in events.values()]
132
133  def _GenerateEvent(self, event):
134    event_dict = {
135      'name': event.simple_name,
136      'description': self._FormatDescription(event.description),
137      'parameters': [self._GenerateProperty(p) for p in event.params],
138      'callback': self._GenerateCallback(event.callback),
139      'filters': [self._GenerateProperty(f) for f in event.filters],
140      'conditions': [self._GetLink(condition)
141                     for condition in event.conditions],
142      'actions': [self._GetLink(action) for action in event.actions],
143      'supportsRules': event.supports_rules,
144      'id': _CreateId(event, 'event')
145    }
146    if (event.parent is not None and
147        not isinstance(event.parent, model.Namespace)):
148      event_dict['parent_name'] = event.parent.simple_name
149    if event.callback is not None:
150      # Show the callback as an extra parameter.
151      event_dict['parameters'].append(
152          self._GenerateCallbackProperty(event.callback))
153    if len(event_dict['parameters']) > 0:
154      event_dict['parameters'][-1]['last'] = True
155    return event_dict
156
157  def _GenerateCallback(self, callback):
158    if not callback:
159      return None
160    callback_dict = {
161      'name': callback.simple_name,
162      'simple_type': {'simple_type': 'function'},
163      'optional': callback.optional,
164      'parameters': []
165    }
166    for param in callback.params:
167      callback_dict['parameters'].append(self._GenerateProperty(param))
168    if (len(callback_dict['parameters']) > 0):
169      callback_dict['parameters'][-1]['last'] = True
170    return callback_dict
171
172  def _GenerateProperties(self, properties):
173    return [self._GenerateProperty(v) for v in properties.values()]
174
175  def _GenerateProperty(self, property_):
176    if not hasattr(property_, 'type_'):
177      for d in dir(property_):
178        if not d.startswith('_'):
179          print ('%s -> %s' % (d, getattr(property_, d)))
180    type_ = property_.type_
181
182    # Make sure we generate property info for arrays, too.
183    # TODO(kalman): what about choices?
184    if type_.property_type == model.PropertyType.ARRAY:
185      properties = type_.item_type.properties
186    else:
187      properties = type_.properties
188
189    property_dict = {
190      'name': property_.simple_name,
191      'optional': property_.optional,
192      'description': self._FormatDescription(property_.description),
193      'properties': self._GenerateProperties(type_.properties),
194      'functions': self._GenerateFunctions(type_.functions),
195      'parameters': [],
196      'returns': None,
197      'id': _CreateId(property_, 'property')
198    }
199
200    if type_.property_type == model.PropertyType.FUNCTION:
201      function = type_.function
202      for param in function.params:
203        property_dict['parameters'].append(self._GenerateProperty(param))
204      if function.returns:
205        property_dict['returns'] = self._GenerateType(function.returns)
206
207    if (property_.parent is not None and
208        not isinstance(property_.parent, model.Namespace)):
209      property_dict['parent_name'] = property_.parent.simple_name
210
211    value = property_.value
212    if value is not None:
213      if isinstance(value, int):
214        property_dict['value'] = _FormatValue(value)
215      else:
216        property_dict['value'] = value
217    else:
218      self._RenderTypeInformation(type_, property_dict)
219
220    return property_dict
221
222  def _GenerateCallbackProperty(self, callback):
223    property_dict = {
224      'name': callback.simple_name,
225      'description': self._FormatDescription(callback.description),
226      'optional': callback.optional,
227      'id': _CreateId(callback, 'property'),
228      'simple_type': 'function',
229    }
230    if (callback.parent is not None and
231        not isinstance(callback.parent, model.Namespace)):
232      property_dict['parent_name'] = callback.parent.simple_name
233    return property_dict
234
235  def _RenderTypeInformation(self, type_, dst_dict):
236    if type_.property_type == model.PropertyType.CHOICES:
237      dst_dict['choices'] = self._GenerateTypes(type_.choices)
238      # We keep track of which is last for knowing when to add "or" between
239      # choices in templates.
240      if len(dst_dict['choices']) > 0:
241        dst_dict['choices'][-1]['last'] = True
242    elif type_.property_type == model.PropertyType.REF:
243      dst_dict['link'] = self._GetLink(type_.ref_type)
244    elif type_.property_type == model.PropertyType.ARRAY:
245      dst_dict['array'] = self._GenerateType(type_.item_type)
246    elif type_.property_type == model.PropertyType.ENUM:
247      dst_dict['enum_values'] = []
248      for enum_value in type_.enum_values:
249        dst_dict['enum_values'].append({'name': enum_value})
250      if len(dst_dict['enum_values']) > 0:
251        dst_dict['enum_values'][-1]['last'] = True
252    elif type_.instance_of is not None:
253      dst_dict['simple_type'] = type_.instance_of.lower()
254    else:
255      dst_dict['simple_type'] = type_.property_type.name.lower()
256
257class _LazySamplesGetter(object):
258  """This class is needed so that an extensions API page does not have to fetch
259  the apps samples page and vice versa.
260  """
261  def __init__(self, api_name, samples):
262    self._api_name = api_name
263    self._samples = samples
264
265  def get(self, key):
266    return self._samples.FilterSamples(key, self._api_name)
267
268class APIDataSource(object):
269  """This class fetches and loads JSON APIs from the FileSystem passed in with
270  |cache_factory|, so the APIs can be plugged into templates.
271  """
272  class Factory(object):
273    def __init__(self,
274                 cache_factory,
275                 base_path):
276      self._permissions_cache = cache_factory.Create(self._LoadPermissions,
277                                                     compiled_fs.PERMS,
278                                                     version=_VERSION)
279      self._json_cache = cache_factory.Create(
280          lambda api_name, api: self._LoadJsonAPI(api, False),
281          compiled_fs.JSON,
282          version=_VERSION)
283      self._idl_cache = cache_factory.Create(
284          lambda api_name, api: self._LoadIdlAPI(api, False),
285          compiled_fs.IDL,
286          version=_VERSION)
287
288      # These caches are used if an APIDataSource does not want to resolve the
289      # $refs in an API. This is needed to prevent infinite recursion in
290      # ReferenceResolver.
291      self._json_cache_no_refs = cache_factory.Create(
292          lambda api_name, api: self._LoadJsonAPI(api, True),
293          compiled_fs.JSON_NO_REFS,
294          version=_VERSION)
295      self._idl_cache_no_refs = cache_factory.Create(
296          lambda api_name, api: self._LoadIdlAPI(api, True),
297          compiled_fs.IDL_NO_REFS,
298          version=_VERSION)
299      self._idl_names_cache = cache_factory.Create(self._GetIDLNames,
300                                                   compiled_fs.IDL_NAMES,
301                                                   version=_VERSION)
302      self._names_cache = cache_factory.Create(self._GetAllNames,
303                                               compiled_fs.NAMES,
304                                               version=_VERSION)
305      self._base_path = base_path
306
307      # These must be set later via the SetFooDataSourceFactory methods.
308      self._ref_resolver_factory = None
309      self._samples_data_source_factory = None
310
311    def SetSamplesDataSourceFactory(self, samples_data_source_factory):
312      self._samples_data_source_factory = samples_data_source_factory
313
314    def SetReferenceResolverFactory(self, ref_resolver_factory):
315      self._ref_resolver_factory = ref_resolver_factory
316
317    def Create(self, request, disable_refs=False):
318      """Create an APIDataSource. |disable_refs| specifies whether $ref's in
319      APIs being processed by the |ToDict| method of _JSCModel follows $ref's
320      in the API. This prevents endless recursion in ReferenceResolver.
321      """
322      if self._samples_data_source_factory is None:
323        # Only error if there is a request, which means this APIDataSource is
324        # actually being used to render a page.
325        if request is not None:
326          logging.error('SamplesDataSource.Factory was never set in '
327                        'APIDataSource.Factory.')
328        samples = None
329      else:
330        samples = self._samples_data_source_factory.Create(request)
331      if not disable_refs and self._ref_resolver_factory is None:
332        logging.error('ReferenceResolver.Factory was never set in '
333                      'APIDataSource.Factory.')
334      return APIDataSource(self._permissions_cache,
335                           self._json_cache,
336                           self._idl_cache,
337                           self._json_cache_no_refs,
338                           self._idl_cache_no_refs,
339                           self._names_cache,
340                           self._idl_names_cache,
341                           self._base_path,
342                           samples,
343                           disable_refs)
344
345    def _LoadPermissions(self, file_name, json_str):
346      return json_parse.Parse(json_str)
347
348    def _LoadJsonAPI(self, api, disable_refs):
349      return _JSCModel(
350          json_parse.Parse(api)[0],
351          self._ref_resolver_factory.Create() if not disable_refs else None,
352          disable_refs).ToDict()
353
354    def _LoadIdlAPI(self, api, disable_refs):
355      idl = idl_parser.IDLParser().ParseData(api)
356      return _JSCModel(
357          idl_schema.IDLSchema(idl).process()[0],
358          self._ref_resolver_factory.Create() if not disable_refs else None,
359          disable_refs).ToDict()
360
361    def _GetIDLNames(self, base_dir, apis):
362      return [
363        model.UnixName(os.path.splitext(api[len('%s/' % self._base_path):])[0])
364        for api in apis if api.endswith('.idl')
365      ]
366
367    def _GetAllNames(self, base_dir, apis):
368      return [
369        model.UnixName(os.path.splitext(api[len('%s/' % self._base_path):])[0])
370        for api in apis
371      ]
372
373  def __init__(self,
374               permissions_cache,
375               json_cache,
376               idl_cache,
377               json_cache_no_refs,
378               idl_cache_no_refs,
379               names_cache,
380               idl_names_cache,
381               base_path,
382               samples,
383               disable_refs):
384    self._base_path = base_path
385    self._permissions_cache = permissions_cache
386    self._json_cache = json_cache
387    self._idl_cache = idl_cache
388    self._json_cache_no_refs = json_cache_no_refs
389    self._idl_cache_no_refs = idl_cache_no_refs
390    self._names_cache = names_cache
391    self._idl_names_cache = idl_names_cache
392    self._samples = samples
393    self._disable_refs = disable_refs
394
395  def _GetFeatureFile(self, filename):
396    try:
397      perms = self._permissions_cache.GetFromFile('%s/%s' %
398          (self._base_path, filename))
399      return dict((model.UnixName(k), v) for k, v in perms.iteritems())
400    except FileNotFoundError:
401      return {}
402
403  def _GetFeatureData(self, path):
404    # Remove 'experimental_' from path name to match the keys in
405    # _permissions_features.json.
406    path = model.UnixName(path.replace('experimental_', ''))
407
408    for filename in ['_permission_features.json', '_manifest_features.json']:
409      feature_data = self._GetFeatureFile(filename).get(path, None)
410      if feature_data is not None:
411        break
412
413    # There are specific cases in which the feature is actually a list of
414    # features where only one needs to match; but currently these are only
415    # used to whitelist features for specific extension IDs. Filter those out.
416    if isinstance(feature_data, list):
417      feature_list = feature_data
418      feature_data = None
419      for single_feature in feature_list:
420        if 'whitelist' in single_feature:
421          continue
422        if feature_data is not None:
423          # Note: if you are seeing the exception below, add more heuristics as
424          # required to form a single feature.
425          raise ValueError('Multiple potential features match %s. I can\'t '
426                           'decide which one to use. Please help!' % path)
427        feature_data = single_feature
428
429    if feature_data and feature_data['channel'] in ('trunk', 'dev', 'beta'):
430      feature_data[feature_data['channel']] = True
431    return feature_data
432
433  def _GenerateHandlebarContext(self, handlebar_dict, path):
434    handlebar_dict['permissions'] = self._GetFeatureData(path)
435    handlebar_dict['samples'] = _LazySamplesGetter(path, self._samples)
436    return handlebar_dict
437
438  def _GetAsSubdirectory(self, name):
439    if name.startswith('experimental_'):
440      parts = name[len('experimental_'):].split('_', 1)
441      parts[1] = 'experimental_%s' % parts[1]
442      return '/'.join(parts)
443    return name.replace('_', '/', 1)
444
445  def get(self, key):
446    if key.endswith('.html') or key.endswith('.json') or key.endswith('.idl'):
447      path, ext = os.path.splitext(key)
448    else:
449      path = key
450    unix_name = model.UnixName(path)
451    idl_names = self._idl_names_cache.GetFromFileListing(self._base_path)
452    names = self._names_cache.GetFromFileListing(self._base_path)
453    if unix_name not in names and self._GetAsSubdirectory(unix_name) in names:
454      unix_name = self._GetAsSubdirectory(unix_name)
455
456    if self._disable_refs:
457      cache, ext = (
458          (self._idl_cache_no_refs, '.idl') if (unix_name in idl_names) else
459          (self._json_cache_no_refs, '.json'))
460    else:
461      cache, ext = ((self._idl_cache, '.idl') if (unix_name in idl_names) else
462                    (self._json_cache, '.json'))
463    return self._GenerateHandlebarContext(
464        cache.GetFromFile('%s/%s%s' % (self._base_path, unix_name, ext)),
465        path)
466