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