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.
10 from data_source import DataSource
11 from docs_server_utils import StringIdentity
12 from environment import IsPreviewServer
13 from extensions_paths import JSON_TEMPLATES, PRIVATE_TEMPLATES
14 from file_system import FileNotFoundError
15 from future import Future, Collect
16 import third_party.json_schema_compiler.json_parse as json_parse
17 import third_party.json_schema_compiler.model as model
18 from environment import IsPreviewServer
19 from third_party.json_schema_compiler.memoize import memoize
22 def _CreateId(node, prefix):
23 if node.parent is not None and not isinstance(node.parent, model.Namespace):
24 return '-'.join([prefix, node.parent.simple_name, node.simple_name])
25 return '-'.join([prefix, node.simple_name])
28 def _FormatValue(value):
29 '''Inserts commas every three digits for integer values. It is magic.
32 return ','.join([s[max(0, i - 3):i] for i in range(len(s), 0, -3)][::-1])
35 def _GetByNameDict(namespace):
36 '''Returns a dictionary mapping names to named items from |namespace|.
38 This lets us render specific API entities rather than the whole thing at once,
39 for example {{apis.manifestTypes.byName.ExternallyConnectable}}.
41 Includes items from namespace['types'], namespace['functions'],
42 namespace['events'], and namespace['properties'].
45 for item_type in ('types', 'functions', 'events', 'properties'):
46 if item_type in namespace:
47 old_size = len(by_name)
49 (item['name'], item) for item in namespace[item_type])
50 assert len(by_name) == old_size + len(namespace[item_type]), (
51 'Duplicate name in %r' % namespace)
55 def _GetEventByNameFromEvents(events):
56 '''Parses the dictionary |events| to find the definitions of members of the
57 type Event. Returns a dictionary mapping the name of a member to that
60 assert 'types' in events, \
61 'The dictionary |events| must contain the key "types".'
62 event_list = [t for t in events['types'] if t.get('name') == 'Event']
63 assert len(event_list) == 1, 'Exactly one type must be called "Event".'
64 return _GetByNameDict(event_list[0])
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.
78 event_byname_function):
79 self._availability_finder = availability_finder
80 self._api_availabilities = json_cache.GetFromFile(
81 posixpath.join(JSON_TEMPLATES, 'api_availabilities.json'))
82 self._intro_tables = json_cache.GetFromFile(
83 posixpath.join(JSON_TEMPLATES, 'intro_tables.json'))
84 self._api_features = features_bundle.GetAPIFeatures()
85 self._template_cache = template_cache
86 self._event_byname_function = event_byname_function
87 self._namespace = namespace
89 def _GetLink(self, link):
90 ref = link if '.' in link else (self._namespace.name + '.' + link)
91 return { 'ref': ref, 'text': link, 'name': link }
94 if self._namespace is None:
96 chrome_dot_name = 'chrome.%s' % self._namespace.name
98 'name': self._namespace.name,
99 'namespace': self._namespace.documentation_options.get('namespace',
101 'title': self._namespace.documentation_options.get('title',
103 'documentationOptions': self._namespace.documentation_options,
104 'types': self._GenerateTypes(self._namespace.types.values()),
105 'functions': self._GenerateFunctions(self._namespace.functions),
106 'events': self._GenerateEvents(self._namespace.events),
107 'domEvents': self._GenerateDomEvents(self._namespace.events),
108 'properties': self._GenerateProperties(self._namespace.properties),
109 'introList': self._GetIntroTableList(),
110 'channelWarning': self._GetChannelWarning(),
112 if self._namespace.deprecated:
113 as_dict['deprecated'] = self._namespace.deprecated
115 as_dict['byName'] = _GetByNameDict(as_dict)
118 def _GetAPIAvailability(self):
119 return self._availability_finder.GetAPIAvailability(self._namespace.name)
121 def _GetChannelWarning(self):
122 if not self._IsExperimental():
123 return { self._GetAPIAvailability().channel_info.channel: True }
126 def _IsExperimental(self):
127 return self._namespace.name.startswith('experimental')
129 def _GenerateTypes(self, types):
130 return [self._GenerateType(t) for t in types]
132 def _GenerateType(self, type_):
134 'name': type_.simple_name,
135 'description': type_.description,
136 'properties': self._GenerateProperties(type_.properties),
137 'functions': self._GenerateFunctions(type_.functions),
138 'events': self._GenerateEvents(type_.events),
139 'id': _CreateId(type_, 'type')
141 self._RenderTypeInformation(type_, type_dict)
144 def _GenerateFunctions(self, functions):
145 return [self._GenerateFunction(f) for f in functions.values()]
147 def _GenerateFunction(self, function):
149 'name': function.simple_name,
150 'description': function.description,
151 'callback': self._GenerateCallback(function.callback),
154 'id': _CreateId(function, 'method')
156 self._AddCommonProperties(function_dict, function)
158 function_dict['returns'] = self._GenerateType(function.returns)
159 for param in function.params:
160 function_dict['parameters'].append(self._GenerateProperty(param))
161 if function.callback is not None:
162 # Show the callback as an extra parameter.
163 function_dict['parameters'].append(
164 self._GenerateCallbackProperty(function.callback))
165 if len(function_dict['parameters']) > 0:
166 function_dict['parameters'][-1]['last'] = True
169 def _GenerateEvents(self, events):
170 return [self._GenerateEvent(e) for e in events.values()
171 if not e.supports_dom]
173 def _GenerateDomEvents(self, events):
174 return [self._GenerateEvent(e) for e in events.values()
177 def _GenerateEvent(self, event):
179 'name': event.simple_name,
180 'description': event.description,
181 'filters': [self._GenerateProperty(f) for f in event.filters],
182 'conditions': [self._GetLink(condition)
183 for condition in event.conditions],
184 'actions': [self._GetLink(action) for action in event.actions],
185 'supportsRules': event.supports_rules,
186 'supportsListeners': event.supports_listeners,
188 'id': _CreateId(event, 'event'),
191 self._AddCommonProperties(event_dict, event)
192 # Add the Event members to each event in this object.
193 if self._event_byname_function:
194 event_dict['byName'].update(self._event_byname_function())
195 # We need to create the method description for addListener based on the
196 # information stored in |event|.
197 if event.supports_listeners:
198 callback_object = model.Function(parent=event,
201 namespace=event.parent,
203 callback_object.params = event.params
205 callback_object.callback = event.callback
206 callback_parameters = self._GenerateCallbackProperty(callback_object)
207 callback_parameters['last'] = True
208 event_dict['byName']['addListener'] = {
209 'name': 'addListener',
210 'callback': self._GenerateFunction(callback_object),
211 'parameters': [callback_parameters]
213 if event.supports_dom:
214 # Treat params as properties of the custom Event object associated with
216 event_dict['properties'] += [self._GenerateProperty(param)
217 for param in event.params]
220 def _GenerateCallback(self, callback):
224 'name': callback.simple_name,
225 'simple_type': {'simple_type': 'function'},
226 'optional': callback.optional,
229 for param in callback.params:
230 callback_dict['parameters'].append(self._GenerateProperty(param))
231 if (len(callback_dict['parameters']) > 0):
232 callback_dict['parameters'][-1]['last'] = True
235 def _GenerateProperties(self, properties):
236 return [self._GenerateProperty(v) for v in properties.values()]
238 def _GenerateProperty(self, property_):
239 if not hasattr(property_, 'type_'):
240 for d in dir(property_):
241 if not d.startswith('_'):
242 print ('%s -> %s' % (d, getattr(property_, d)))
243 type_ = property_.type_
245 # Make sure we generate property info for arrays, too.
246 # TODO(kalman): what about choices?
247 if type_.property_type == model.PropertyType.ARRAY:
248 properties = type_.item_type.properties
250 properties = type_.properties
253 'name': property_.simple_name,
254 'optional': property_.optional,
255 'description': property_.description,
256 'properties': self._GenerateProperties(type_.properties),
257 'functions': self._GenerateFunctions(type_.functions),
260 'id': _CreateId(property_, 'property')
262 self._AddCommonProperties(property_dict, property_)
264 if type_.property_type == model.PropertyType.FUNCTION:
265 function = type_.function
266 for param in function.params:
267 property_dict['parameters'].append(self._GenerateProperty(param))
269 property_dict['returns'] = self._GenerateType(function.returns)
271 value = property_.value
272 if value is not None:
273 if isinstance(value, int):
274 property_dict['value'] = _FormatValue(value)
276 property_dict['value'] = value
278 self._RenderTypeInformation(type_, property_dict)
282 def _GenerateCallbackProperty(self, callback):
284 'name': callback.simple_name,
285 'description': callback.description,
286 'optional': callback.optional,
288 'id': _CreateId(callback, 'property'),
289 'simple_type': 'function',
291 if (callback.parent is not None and
292 not isinstance(callback.parent, model.Namespace)):
293 property_dict['parentName'] = callback.parent.simple_name
296 def _RenderTypeInformation(self, type_, dst_dict):
297 dst_dict['is_object'] = type_.property_type == model.PropertyType.OBJECT
298 if type_.property_type == model.PropertyType.CHOICES:
299 dst_dict['choices'] = self._GenerateTypes(type_.choices)
300 # We keep track of which == last for knowing when to add "or" between
301 # choices in templates.
302 if len(dst_dict['choices']) > 0:
303 dst_dict['choices'][-1]['last'] = True
304 elif type_.property_type == model.PropertyType.REF:
305 dst_dict['link'] = self._GetLink(type_.ref_type)
306 elif type_.property_type == model.PropertyType.ARRAY:
307 dst_dict['array'] = self._GenerateType(type_.item_type)
308 elif type_.property_type == model.PropertyType.ENUM:
309 dst_dict['enum_values'] = [
310 {'name': value.name, 'description': value.description}
311 for value in type_.enum_values]
312 if len(dst_dict['enum_values']) > 0:
313 dst_dict['enum_values'][-1]['last'] = True
314 elif type_.instance_of is not None:
315 dst_dict['simple_type'] = type_.instance_of
317 dst_dict['simple_type'] = type_.property_type.name
319 def _GetIntroTableList(self):
320 '''Create a generic data structure that can be traversed by the templates
321 to create an API intro table.
324 self._GetIntroDescriptionRow(),
325 self._GetIntroAvailabilityRow()
326 ] + self._GetIntroDependencyRows()
328 # Add rows using data from intro_tables.json, overriding any existing rows
329 # if they share the same 'title' attribute.
330 row_titles = [row['title'] for row in intro_rows]
331 for misc_row in self._GetMiscIntroRows():
332 if misc_row['title'] in row_titles:
333 intro_rows[row_titles.index(misc_row['title'])] = misc_row
335 intro_rows.append(misc_row)
339 def _GetIntroDescriptionRow(self):
340 ''' Generates the 'Description' row data for an API intro table.
343 'title': 'Description',
345 { 'text': self._namespace.description }
349 def _GetIntroAvailabilityRow(self):
350 ''' Generates the 'Availability' row data for an API intro table.
352 if self._IsExperimental():
353 status = 'experimental'
357 availability = self._GetAPIAvailability()
358 status = availability.channel_info.channel
359 version = availability.channel_info.version
360 scheduled = availability.scheduled
362 'title': 'Availability',
364 'partial': self._template_cache.GetFromFile(
365 posixpath.join(PRIVATE_TEMPLATES,
367 '%s_message.html' % status)).Get(),
369 'scheduled': scheduled
373 def _GetIntroDependencyRows(self):
374 # Devtools aren't in _api_features. If we're dealing with devtools, bail.
375 if 'devtools' in self._namespace.name:
378 api_feature = self._api_features.Get().get(self._namespace.name)
380 logging.error('"%s" not found in _api_features.json' %
381 self._namespace.name)
384 permissions_content = []
385 manifest_content = []
387 def categorize_dependency(dependency):
388 def make_code_node(text):
389 return { 'class': 'code', 'text': text }
391 context, name = dependency.split(':', 1)
392 if context == 'permission':
393 permissions_content.append(make_code_node('"%s"' % name))
394 elif context == 'manifest':
395 manifest_content.append(make_code_node('"%s": {...}' % name))
396 elif context == 'api':
397 transitive_dependencies = (
398 self._api_features.Get().get(name, {}).get('dependencies', []))
399 for transitive_dependency in transitive_dependencies:
400 categorize_dependency(transitive_dependency)
402 logging.error('Unrecognized dependency for %s: %s' %
403 (self._namespace.name, context))
405 for dependency in api_feature.get('dependencies', ()):
406 categorize_dependency(dependency)
409 if permissions_content:
410 dependency_rows.append({
411 'title': 'Permissions',
412 'content': permissions_content
415 dependency_rows.append({
417 'content': manifest_content
419 return dependency_rows
421 def _GetMiscIntroRows(self):
422 ''' Generates miscellaneous intro table row data, such as 'Permissions',
423 'Samples', and 'Learn More', using intro_tables.json.
426 # Look up the API name in intro_tables.json, which is structured
427 # similarly to the data structure being created. If the name is found, loop
428 # through the attributes and add them to this structure.
429 table_info = self._intro_tables.Get().get(self._namespace.name)
430 if table_info is None:
433 for category in table_info.iterkeys():
435 for node in table_info[category]:
436 # If there is a 'partial' argument and it hasn't already been
437 # converted to a Handlebar object, transform it to a template.
438 if 'partial' in node:
439 # Note: it's enough to copy() not deepcopy() because only a single
440 # top-level key is being modified.
442 node['partial'] = self._template_cache.GetFromFile(
443 posixpath.join(PRIVATE_TEMPLATES, node['partial'])).Get()
445 misc_rows.append({ 'title': category, 'content': content })
448 def _AddCommonProperties(self, target, src):
449 if src.deprecated is not None:
450 target['deprecated'] = src.deprecated
451 if (src.parent is not None and
452 not isinstance(src.parent, model.Namespace)):
453 target['parentName'] = src.parent.simple_name
456 class _LazySamplesGetter(object):
457 '''This class is needed so that an extensions API page does not have to fetch
458 the apps samples page and vice versa.
461 def __init__(self, api_name, samples):
462 self._api_name = api_name
463 self._samples = samples
466 return self._samples.FilterSamples(key, self._api_name)
469 class APIDataSource(DataSource):
470 '''This class fetches and loads JSON APIs from the FileSystem passed in with
471 |compiled_fs_factory|, so the APIs can be plugged into templates.
473 def __init__(self, server_instance, request):
474 file_system = server_instance.host_file_system_provider.GetTrunk()
475 self._json_cache = server_instance.compiled_fs_factory.ForJson(file_system)
476 self._template_cache = server_instance.compiled_fs_factory.ForTemplates(
478 self._availability_finder = server_instance.availability_finder
479 self._api_models = server_instance.api_models
480 self._features_bundle = server_instance.features_bundle
481 self._model_cache = server_instance.object_store_creator.Create(
483 # Update the models when any of templates, APIs, or Features change.
484 category=StringIdentity(self._json_cache.GetIdentity(),
485 self._template_cache.GetIdentity(),
486 self._api_models.GetIdentity(),
487 self._features_bundle.GetIdentity()))
489 # This caches the result of _LoadEventByName.
490 self._event_byname = None
491 self._samples = server_instance.samples_data_source_factory.Create(request)
493 def _LoadEventByName(self):
494 '''All events have some members in common. We source their description
495 from Event in events.json.
497 if self._event_byname is None:
498 self._event_byname = _GetEventByNameFromEvents(
499 self._GetSchemaModel('events').Get())
500 return self._event_byname
502 def _GetSchemaModel(self, api_name):
503 jsc_model_future = self._model_cache.Get(api_name)
504 model_future = self._api_models.GetModel(api_name)
506 jsc_model = jsc_model_future.Get()
507 if jsc_model is None:
508 jsc_model = _JSCModel(
510 self._availability_finder,
512 self._template_cache,
513 self._features_bundle,
514 self._LoadEventByName).ToDict()
515 self._model_cache.Set(api_name, jsc_model)
517 return Future(callback=resolve)
519 def _GetImpl(self, api_name):
520 handlebar_dict_future = self._GetSchemaModel(api_name)
522 handlebar_dict = handlebar_dict_future.Get()
523 # Parsing samples on the preview server takes seconds and doesn't add
524 # anything. Don't do it.
525 if not IsPreviewServer():
526 handlebar_dict['samples'] = _LazySamplesGetter(
527 handlebar_dict['name'],
529 return handlebar_dict
530 return Future(callback=resolve)
532 def get(self, api_name):
533 return self._GetImpl(api_name).Get()
536 futures = [self._GetImpl(name) for name in self._api_models.GetNames()]
537 return Collect(futures, except_pass=FileNotFoundError)