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