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