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 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.
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'].
41 for item_type
in ('types', 'functions', 'events', 'properties'):
42 if item_type
in namespace
:
43 old_size
= len(by_name
)
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
)
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
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.
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
:
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
)
102 if self
._namespace
is None:
104 chrome_dot_name
= 'chrome.%s' % self
._namespace
.name
106 'name': self
._namespace
.name
,
107 'namespace': self
._namespace
.documentation_options
.get('namespace',
109 'title': self
._namespace
.documentation_options
.get('title',
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
:
122 'introList': self
._GetIntroTableList
(),
123 'channelWarning': self
._GetChannelWarning
(),
125 as_dict
['byName'] = _GetByNameDict(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 }
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_
):
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
)
154 def _GenerateFunctions(self
, functions
):
155 return [self
._GenerateFunction
(f
) for f
in functions
.values()]
157 def _GenerateFunction(self
, function
):
159 'name': function
.simple_name
,
160 'description': self
._FormatDescription
(function
.description
),
161 'callback': self
._GenerateCallback
(function
.callback
),
164 'id': _CreateId(function
, 'method')
166 self
._AddCommonProperties
(function_dict
, function
)
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
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()
187 def _GenerateEvent(self
, event
):
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
,
198 'id': _CreateId(event
, 'event'),
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
,
213 namespace
=event
.parent
,
215 callback_object
.params
= event
.params
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
228 event_dict
['properties'] += [self
._GenerateProperty
(param
)
229 for param
in event
.params
]
232 def _GenerateCallback(self
, callback
):
236 'name': callback
.simple_name
,
237 'simple_type': {'simple_type': 'function'},
238 'optional': callback
.optional
,
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
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
262 properties
= type_
.properties
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
),
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
))
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
)
288 property_dict
['value'] = value
290 self
._RenderTypeInformation
(type_
, property_dict
)
294 def _GenerateCallbackProperty(self
, callback
):
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
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
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.
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
346 intro_rows
.append(misc_row
)
350 def _GetIntroDescriptionRow(self
):
351 ''' Generates the 'Description' row data for an API intro table.
354 'title': 'Description',
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'
367 availability
= self
._GetApiAvailability
()
368 status
= availability
.channel
369 version
= availability
.version
371 'title': 'Availability',
373 'partial': self
._template
_cache
.GetFromFile(
374 '%s/intro_tables/%s_message.html' %
375 (PRIVATE_TEMPLATES
, status
)).Get(),
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
:
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:
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
)
410 raise ValueError('Unrecognized dependency for %s: %s' % (
411 self
._namespace
.name
, context
))
413 for dependency
in dependencies
:
414 categorize_dependency(dependency
)
417 if permissions_content
:
418 dependency_rows
.append({
419 'title': 'Permissions',
420 'content': permissions_content
423 dependency_rows
.append({
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.
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:
441 for category
in table_info
.keys():
442 content
= copy
.deepcopy(table_info
[category
])
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
})
452 def _AddCommonProperties(self
, target
, src
):
453 if src
.deprecated
is not None:
454 target
['deprecated'] = self
._FormatDescription
(
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
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):
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.')
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
):
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:
541 jsc_model
= _JSCModel(
544 self
._ref
_resolver
_factory
.Create() if not disable_refs
else None,
546 self
._availability
_finder
,
548 self
._template
_cache
,
549 self
._LoadEventByName
).ToDict()
551 self
._GetModelCache
(disable_refs
).Set(api_name
, 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'],
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
))