Adding the orphaned options pages to the navigation
[chromium-blink-merge.git] / chrome / common / extensions / docs / server2 / api_schema_graph.py
blob0c90ba1d9afa4ee238316ac9ed8f6447fd8e65c5
1 # Copyright 2013 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 json
6 import logging
8 from api_models import GetNodeCategories
9 from collections import Iterable, Mapping
11 class LookupResult(object):
12 '''Returned from APISchemaGraph.Lookup(), and relays whether or not
13 some element was found and what annotation object was associated with it,
14 if any.
15 '''
17 def __init__(self, found=None, annotation=None):
18 assert found is not None, 'LookupResult was given None value for |found|.'
19 self.found = found
20 self.annotation = annotation
22 def __eq__(self, other):
23 return self.__dict__ == other.__dict__
25 def __ne__(self, other):
26 return not (self == other)
28 def __repr__(self):
29 return '%s%s' % (type(self).__name__, repr(self.__dict__))
31 def __str__(self):
32 return repr(self)
35 class APINodeCursor(object):
36 '''An abstract representation of a node in an APISchemaGraph.
37 The current position in the graph is represented by a path into the
38 underlying dictionary. So if the APISchemaGraph is:
41 'tabs': {
42 'types': {
43 'Tab': {
44 'properties': {
45 'url': {
46 ...
54 then the 'url' property would be represented by:
56 ['tabs', 'types', 'Tab', 'properties', 'url']
57 '''
59 def __init__(self, availability_finder, namespace_name):
60 self._lookup_path = []
61 self._node_availabilities = availability_finder.GetAPINodeAvailability(
62 namespace_name)
63 self._namespace_name = namespace_name
64 self._ignored_categories = []
66 def _AssertIsValidCategory(self, category):
67 assert category in GetNodeCategories(), \
68 '%s is not a valid category. Full path: %s' % (category, str(self))
70 def _GetParentPath(self):
71 '''Returns the path pointing to this node's parent.
72 '''
73 assert len(self._lookup_path) > 1, \
74 'Tried to look up parent for the top-level node.'
76 # lookup_path[-1] is the name of the current node. If this lookup_path
77 # describes a regular node, then lookup_path[-2] will be a node category.
78 # Otherwise, it's an event callback or a function parameter.
79 if self._lookup_path[-2] not in GetNodeCategories():
80 if self._lookup_path[-1] == 'callback':
81 # This is an event callback, so lookup_path[-2] is the event
82 # node name, thus lookup_path[-3] must be 'events'.
83 assert self._lookup_path[-3] == 'events'
84 return self._lookup_path[:-1]
85 # This is a function parameter.
86 assert self._lookup_path[-2] == 'parameters'
87 return self._lookup_path[:-2]
88 # This is a regular node, so lookup_path[-2] should
89 # be a node category.
90 self._AssertIsValidCategory(self._lookup_path[-2])
91 return self._lookup_path[:-2]
93 def _LookupNodeAvailability(self, lookup_path):
94 '''Returns the ChannelInfo object for this node.
95 '''
96 return self._node_availabilities.Lookup(self._namespace_name,
97 *lookup_path).annotation
99 def _CheckNamespacePrefix(self, lookup_path):
100 '''API schemas may prepend the namespace name to top-level types
101 (e.g. declarativeWebRequest > types > declarativeWebRequest.IgnoreRules),
102 but just the base name (here, 'IgnoreRules') will be in the |lookup_path|.
103 Try creating an alternate |lookup_path| by adding the namespace name.
105 # lookup_path[0] is always the node category (e.g. types, functions, etc.).
106 # Thus, lookup_path[1] is always the top-level node name.
107 self._AssertIsValidCategory(lookup_path[0])
108 base_name = lookup_path[1]
109 lookup_path[1] = '%s.%s' % (self._namespace_name, base_name)
110 try:
111 node_availability = self._LookupNodeAvailability(lookup_path)
112 if node_availability is not None:
113 return node_availability
114 finally:
115 # Restore lookup_path.
116 lookup_path[1] = base_name
117 return None
119 def _CheckEventCallback(self, lookup_path):
120 '''Within API schemas, an event has a list of 'properties' that the event's
121 callback expects. The callback itself is not explicitly represented in the
122 schema. However, when creating an event node in JSCView, a callback node
123 is generated and acts as the parent for the event's properties.
124 Modify |lookup_path| to check the original schema format.
126 if 'events' in lookup_path:
127 assert 'callback' in lookup_path, self
128 callback_index = lookup_path.index('callback')
129 try:
130 lookup_path.pop(callback_index)
131 node_availability = self._LookupNodeAvailability(lookup_path)
132 finally:
133 lookup_path.insert(callback_index, 'callback')
134 return node_availability
135 return None
137 def _LookupAvailability(self, lookup_path):
138 '''Runs all the lookup checks on |lookup_path| and
139 returns the node availability if found, None otherwise.
141 for lookup in (self._LookupNodeAvailability,
142 self._CheckEventCallback,
143 self._CheckNamespacePrefix):
144 node_availability = lookup(lookup_path)
145 if node_availability is not None:
146 return node_availability
147 return None
149 def _GetCategory(self):
150 '''Returns the category this node belongs to.
152 if self._lookup_path[-2] in GetNodeCategories():
153 return self._lookup_path[-2]
154 # If lookup_path[-2] is not a valid category and lookup_path[-1] is
155 # 'callback', then we know we have an event callback.
156 if self._lookup_path[-1] == 'callback':
157 return 'events'
158 if self._lookup_path[-2] == 'parameters':
159 # Function parameters are modelled as properties.
160 return 'properties'
161 if (self._lookup_path[-1].endswith('Type') and
162 (self._lookup_path[-1][:-len('Type')] == self._lookup_path[-2] or
163 self._lookup_path[-1][:-len('ReturnType')] == self._lookup_path[-2])):
164 # Array elements and function return objects have 'Type' and 'ReturnType'
165 # appended to their names, respectively, in model.py. This results in
166 # lookup paths like
167 # 'events > types > Rule > properties > tags > tagsType'.
168 # These nodes are treated as properties.
169 return 'properties'
170 if self._lookup_path[0] == 'events':
171 # HACK(ahernandez.miralles): This catches a few edge cases,
172 # such as 'webviewTag > events > consolemessage > level'.
173 return 'properties'
174 raise AssertionError('Could not classify node %s' % self)
176 def GetDeprecated(self):
177 '''Returns when this node became deprecated, or None if it
178 is not deprecated.
180 deprecated_path = self._lookup_path + ['deprecated']
181 for lookup in (self._LookupNodeAvailability,
182 self._CheckNamespacePrefix):
183 node_availability = lookup(deprecated_path)
184 if node_availability is not None:
185 return node_availability
186 if 'callback' in self._lookup_path:
187 return self._CheckEventCallback(deprecated_path)
188 return None
190 def GetAvailability(self):
191 '''Returns availability information for this node.
193 if self._GetCategory() in self._ignored_categories:
194 return None
195 node_availability = self._LookupAvailability(self._lookup_path)
196 if node_availability is None:
197 logging.warning('No availability found for: %s' % self)
198 return None
200 parent_node_availability = self._LookupAvailability(self._GetParentPath())
201 # If the parent node availability couldn't be found, something
202 # is very wrong.
203 assert parent_node_availability is not None
205 # Only render this node's availability if it differs from the parent
206 # node's availability.
207 if node_availability == parent_node_availability:
208 return None
209 return node_availability
211 def Descend(self, *path, **kwargs):
212 '''Moves down the APISchemaGraph, following |path|.
213 |ignore| should be a tuple of category strings (e.g. ('types',))
214 for which nodes should not have availability data generated.
216 ignore = kwargs.get('ignore')
217 class scope(object):
218 def __enter__(self2):
219 if ignore:
220 self._ignored_categories.extend(ignore)
221 if path:
222 self._lookup_path.extend(path)
224 def __exit__(self2, _, __, ___):
225 if ignore:
226 self._ignored_categories[:] = self._ignored_categories[:-len(ignore)]
227 if path:
228 self._lookup_path[:] = self._lookup_path[:-len(path)]
229 return scope()
231 def __str__(self):
232 return repr(self)
234 def __repr__(self):
235 return '%s > %s' % (self._namespace_name, ' > '.join(self._lookup_path))
238 class _GraphNode(dict):
239 '''Represents some element of an API schema, and allows extra information
240 about that element to be stored on the |_annotation| object.
243 def __init__(self, *args, **kwargs):
244 # Use **kwargs here since Python is picky with ordering of default args
245 # and variadic args in the method signature. The only keyword arg we care
246 # about here is 'annotation'. Intentionally don't pass |**kwargs| into the
247 # superclass' __init__().
248 dict.__init__(self, *args)
249 self._annotation = kwargs.get('annotation')
251 def __eq__(self, other):
252 # _GraphNode inherits __eq__() from dict, which will not take annotation
253 # objects into account when comparing.
254 return dict.__eq__(self, other)
256 def __ne__(self, other):
257 return not (self == other)
259 def GetAnnotation(self):
260 return self._annotation
262 def SetAnnotation(self, annotation):
263 self._annotation = annotation
266 def _NameForNode(node):
267 '''Creates a unique id for an object in an API schema, depending on
268 what type of attribute the object is a member of.
270 if 'namespace' in node: return node['namespace']
271 if 'name' in node: return node['name']
272 if 'id' in node: return node['id']
273 if 'type' in node: return node['type']
274 if '$ref' in node: return node['$ref']
275 assert False, 'Problems with naming node: %s' % json.dumps(node, indent=3)
278 def _IsObjectList(value):
279 '''Determines whether or not |value| is a list made up entirely of
280 dict-like objects.
282 return (isinstance(value, Iterable) and
283 all(isinstance(node, Mapping) for node in value))
286 def _CreateGraph(root):
287 '''Recursively moves through an API schema, replacing lists of objects
288 and non-object values with objects.
290 schema_graph = _GraphNode()
291 if _IsObjectList(root):
292 for node in root:
293 name = _NameForNode(node)
294 assert name not in schema_graph, 'Duplicate name in API schema graph.'
295 schema_graph[name] = _GraphNode((key, _CreateGraph(value)) for
296 key, value in node.iteritems())
298 elif isinstance(root, Mapping):
299 for name, node in root.iteritems():
300 if not isinstance(node, Mapping):
301 schema_graph[name] = _GraphNode()
302 else:
303 schema_graph[name] = _GraphNode((key, _CreateGraph(value)) for
304 key, value in node.iteritems())
305 return schema_graph
308 def _Subtract(minuend, subtrahend):
309 ''' A Set Difference adaptation for graphs. Returns a |difference|,
310 which contains key-value pairs found in |minuend| but not in
311 |subtrahend|.
313 difference = _GraphNode()
314 for key in minuend:
315 if key not in subtrahend:
316 # Record all of this key's children as being part of the difference.
317 difference[key] = _Subtract(minuend[key], {})
318 else:
319 # Note that |minuend| and |subtrahend| are assumed to be graphs, and
320 # therefore should have no lists present, only keys and nodes.
321 rest = _Subtract(minuend[key], subtrahend[key])
322 if rest:
323 # Record a difference if children of this key differed at some point.
324 difference[key] = rest
325 return difference
328 class APISchemaGraph(object):
329 '''Provides an interface for interacting with an API schema graph, a
330 nested dict structure that allows for simpler lookups of schema data.
333 def __init__(self, api_schema=None, _graph=None):
334 self._graph = _graph if _graph is not None else _CreateGraph(api_schema)
336 def __eq__(self, other):
337 return self._graph == other._graph
339 def __ne__(self, other):
340 return not (self == other)
342 def Subtract(self, other):
343 '''Returns an APISchemaGraph instance representing keys that are in
344 this graph but not in |other|.
346 return APISchemaGraph(_graph=_Subtract(self._graph, other._graph))
348 def Update(self, other, annotator):
349 '''Modifies this graph by adding keys from |other| that are not
350 already present in this graph.
352 def update(base, addend):
353 '''A Set Union adaptation for graphs. Returns a graph which contains
354 the key-value pairs from |base| combined with any key-value pairs
355 from |addend| that are not present in |base|.
357 for key in addend:
358 if key not in base:
359 # Add this key and the rest of its children.
360 base[key] = update(_GraphNode(annotation=annotator(key)), addend[key])
361 else:
362 # The key is already in |base|, but check its children.
363 update(base[key], addend[key])
364 return base
366 update(self._graph, other._graph)
368 def Lookup(self, *path):
369 '''Given a list of path components, |path|, checks if the
370 APISchemaGraph instance contains |path|.
372 node = self._graph
373 for path_piece in path:
374 node = node.get(path_piece)
375 if node is None:
376 return LookupResult(found=False, annotation=None)
377 return LookupResult(found=True, annotation=node._annotation)
379 def IsEmpty(self):
380 '''Checks for an empty schema graph.
382 return not self._graph