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.
9 from collections import defaultdict, Mapping
11 from environment import IsPreviewServer
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
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])
27 def _FormatValue(value):
28 '''Inserts commas every three digits for integer values. It is magic.
31 return ','.join([s[max(0, i - 3):i] for i in range(len(s), 0, -3)][::-1])
34 def _GetByNameDict(namespace):
35 '''Returns a dictionary mapping names to named items from |namespace|.
37 This lets us render specific API entities rather than the whole thing at once,
38 for example {{apis.manifestTypes.byName.ExternallyConnectable}}.
40 Includes items from namespace['types'], namespace['functions'],
41 namespace['events'], and namespace['properties'].
44 for item_type in ('types', 'functions', 'events', 'properties'):
45 if item_type in namespace:
46 old_size = len(by_name)
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)
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
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])
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.
80 event_byname_function,
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
99 DetectInlineableTypes(clean_json)
100 InlineDocs(clean_json)
101 self._namespace = model.Namespace(clean_json, clean_json['namespace'])
103 def _FormatDescription(self, description):
104 if self._disable_refs:
106 return self._ref_resolver.ResolveAllLinks(description,
107 namespace=self._namespace.name)
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)
116 if self._namespace is None:
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),
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:
131 'introList': self._GetIntroTableList(),
132 'channelWarning': self._GetChannelWarning(),
134 as_dict['byName'] = _GetByNameDict(as_dict)
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)
147 def _GetChannelWarning(self):
148 if not self._IsExperimental():
149 return { self._GetApiAvailability().channel: True }
152 def _IsExperimental(self):
153 return self._namespace.name.startswith('experimental')
155 def _GenerateTypes(self, types):
156 return [self._GenerateType(t) for t in types]
158 def _GenerateType(self, type_):
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')
167 self._RenderTypeInformation(type_, type_dict)
170 def _GenerateFunctions(self, functions):
171 return [self._GenerateFunction(f) for f in functions.values()]
173 def _GenerateFunction(self, function):
175 'name': function.simple_name,
176 'description': self._FormatDescription(function.description),
177 'callback': self._GenerateCallback(function.callback),
180 'id': _CreateId(function, 'method')
182 if (function.deprecated is not None):
183 function_dict['deprecated'] = self._FormatDescription(
185 if (function.parent is not None and
186 not isinstance(function.parent, model.Namespace)):
187 function_dict['parentName'] = function.parent.simple_name
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
200 def _GenerateEvents(self, events):
201 return [self._GenerateEvent(e) for e in events.values()
202 if not e.supports_dom]
204 def _GenerateDomEvents(self, events):
205 return [self._GenerateEvent(e) for e in events.values()
208 def _GenerateEvent(self, event):
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,
219 'id': _CreateId(event, 'event'),
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,
236 namespace=event.parent,
238 callback_object.params = event.params
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]
248 if event.supports_dom:
249 # Treat params as properties of the custom Event object associated with
251 event_dict['properties'] += [self._GenerateProperty(param)
252 for param in event.params]
255 def _GenerateCallback(self, callback):
259 'name': callback.simple_name,
260 'simple_type': {'simple_type': 'function'},
261 'optional': callback.optional,
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
270 def _GenerateProperties(self, properties):
271 return [self._GenerateProperty(v) for v in properties.values()]
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_
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
285 properties = type_.properties
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),
295 'id': _CreateId(property_, 'property')
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))
303 property_dict['returns'] = self._GenerateType(function.returns)
305 if (property_.parent is not None and
306 not isinstance(property_.parent, model.Namespace)):
307 property_dict['parentName'] = property_.parent.simple_name
309 value = property_.value
310 if value is not None:
311 if isinstance(value, int):
312 property_dict['value'] = _FormatValue(value)
314 property_dict['value'] = value
316 self._RenderTypeInformation(type_, property_dict)
320 def _GenerateCallbackProperty(self, callback):
322 'name': callback.simple_name,
323 'description': self._FormatDescription(callback.description),
324 'optional': callback.optional,
325 'id': _CreateId(callback, 'property'),
326 'simple_type': 'function',
328 if (callback.parent is not None and
329 not isinstance(callback.parent, model.Namespace)):
330 property_dict['parentName'] = callback.parent.simple_name
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()
354 dst_dict['simple_type'] = type_.property_type.name.lower()
356 def _GetIntroTableList(self):
357 '''Create a generic data structure that can be traversed by the templates
358 to create an API intro table.
361 self._GetIntroDescriptionRow(),
362 self._GetIntroAvailabilityRow()
363 ] + self._GetIntroDependencyRows()
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
372 intro_rows.append(misc_row)
376 def _GetIntroDescriptionRow(self):
377 ''' Generates the 'Description' row data for an API intro table.
380 'title': 'Description',
382 { 'text': self._FormatDescription(self._namespace.description) }
386 def _GetIntroAvailabilityRow(self):
387 ''' Generates the 'Availability' row data for an API intro table.
389 if self._IsExperimental():
390 status = 'experimental'
393 availability = self._GetApiAvailability()
394 status = availability.channel
395 version = availability.version
397 'title': 'Availability',
399 'partial': self._template_cache.GetFromFile(
400 '%s/intro_tables/%s_message.html' %
401 (svn_constants.PRIVATE_TEMPLATE_PATH, status)).Get(),
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:
410 feature = self._api_features.Get().get(self._namespace.name)
411 assert feature, ('"%s" not found in _api_features.json.'
412 % self._namespace.name)
414 dependencies = feature.get('dependencies')
415 if dependencies is None:
418 def make_code_node(text):
419 return { 'class': 'code', 'text': text }
421 permissions_content = []
422 manifest_content = []
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)
436 raise ValueError('Unrecognized dependency for %s: %s' % (
437 self._namespace.name, context))
439 for dependency in dependencies:
440 categorize_dependency(dependency)
443 if permissions_content:
444 dependency_rows.append({
445 'title': 'Permissions',
446 'content': permissions_content
449 dependency_rows.append({
451 'content': manifest_content
453 return dependency_rows
455 def _GetMiscIntroRows(self):
456 ''' Generates miscellaneous intro table row data, such as 'Permissions',
457 'Samples', and 'Learn More', using intro_tables.json.
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:
467 for category in table_info.keys():
468 content = copy.deepcopy(table_info[category])
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 })
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.
484 def __init__(self, api_name, samples):
485 self._api_name = api_name
486 self._samples = samples
489 return self._samples.FilterSamples(key, self._api_name)
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.
497 class Factory(object):
504 def create_compiled_fs(fn, category):
505 return compiled_fs_factory.Create(
506 file_system, fn, APIDataSource, category=category)
508 self._json_cache = create_compiled_fs(
509 lambda api_name, api: self._LoadJsonAPI(api, False),
511 self._idl_cache = create_compiled_fs(
512 lambda api_name, api: self._LoadIdlAPI(api, False),
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
518 self._json_cache_no_refs = create_compiled_fs(
519 lambda api_name, api: self._LoadJsonAPI(api, True),
521 self._idl_cache_no_refs = create_compiled_fs(
522 lambda api_name, api: self._LoadIdlAPI(api, True),
525 self._idl_names_cache = create_compiled_fs(self._GetIDLNames, 'idl-names')
526 self._names_cache = create_compiled_fs(self._GetAllNames, 'names')
528 self._base_path = base_path
529 self._availability_finder = availability_finder
530 self._branch_utility = branch_utility
532 self._parse_cache = compiled_fs_factory.ForJson(file_system)
533 self._template_cache = compiled_fs_factory.ForTemplates(file_system)
535 # These must be set later via the SetFooDataSourceFactory methods.
536 self._ref_resolver_factory = None
537 self._samples_data_source_factory = None
539 # This caches the result of _LoadEventByName.
540 self._event_byname = None
542 def SetSamplesDataSourceFactory(self, samples_data_source_factory):
543 self._samples_data_source_factory = samples_data_source_factory
545 def SetReferenceResolverFactory(self, ref_resolver_factory):
546 self._ref_resolver_factory = ref_resolver_factory
548 def Create(self, request):
549 '''Creates an APIDataSource.
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.')
559 samples = self._samples_data_source_factory.Create(request)
560 return APIDataSource(self._json_cache,
562 self._json_cache_no_refs,
563 self._idl_cache_no_refs,
565 self._idl_names_cache,
569 def _LoadEventByName(self):
570 """ All events have some members in common. We source their description
571 from Event in events.json.
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
579 def _LoadJsonAPI(self, api, disable_refs):
581 json_parse.Parse(api)[0],
582 self._ref_resolver_factory.Create() if not disable_refs else None,
584 self._availability_finder,
585 self._branch_utility,
587 self._template_cache,
588 self._LoadEventByName).ToDict()
590 def _LoadIdlAPI(self, api, disable_refs):
591 idl = idl_parser.IDLParser().ParseData(api)
593 idl_schema.IDLSchema(idl).process()[0],
594 self._ref_resolver_factory.Create() if not disable_refs else None,
596 self._availability_finder,
597 self._branch_utility,
599 self._template_cache,
600 self._LoadEventByName,
603 def _GetIDLNames(self, base_dir, apis):
604 return self._GetExtNames(apis, ['idl'])
606 def _GetAllNames(self, base_dir, apis):
607 return self._GetExtNames(apis, ['json', 'idl'])
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]
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
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'],
638 return handlebar_dict
640 def _GetAsSubdirectory(self, name):
641 if name.startswith('experimental_'):
642 parts = name[len('experimental_'):].split('_', 1)
644 parts[1] = 'experimental_%s' % parts[1]
645 return '/'.join(parts)
646 return '%s/%s' % (parts[0], name)
647 return name.replace('_', '/', 1)
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)
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)
662 (self._idl_cache_no_refs, '.idl') if (unix_name in idl_names) else
663 (self._json_cache_no_refs, '.json'))
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())