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