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.
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,
17 def __init__(self
, found
=None, annotation
=None):
18 assert found
is not None, 'LookupResult was given None value for |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
)
29 return '%s%s' % (type(self
).__name
__, repr(self
.__dict
__))
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:
54 then the 'url' property would be represented by:
56 ['tabs', 'types', 'Tab', 'properties', 'url']
59 def __init__(self
, availability_finder
, namespace_name
):
60 self
._lookup
_path
= []
61 self
._node
_availabilities
= availability_finder
.GetAPINodeAvailability(
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.
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
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.
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
)
111 node_availability
= self
._LookupNodeAvailability
(lookup_path
)
112 if node_availability
is not None:
113 return node_availability
115 # Restore lookup_path.
116 lookup_path
[1] = base_name
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')
130 lookup_path
.pop(callback_index
)
131 node_availability
= self
._LookupNodeAvailability
(lookup_path
)
133 lookup_path
.insert(callback_index
, 'callback')
134 return node_availability
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
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':
158 if self
._lookup
_path
[-2] == 'parameters':
159 # Function parameters are modelled as 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
167 # 'events > types > Rule > properties > tags > tagsType'.
168 # These nodes are treated as 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'.
174 raise AssertionError('Could not classify node %s' % self
)
176 def GetDeprecated(self
):
177 '''Returns when this node became deprecated, or None if it
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
)
190 def GetAvailability(self
):
191 '''Returns availability information for this node.
193 if self
._GetCategory
() in self
._ignored
_categories
:
195 node_availability
= self
._LookupAvailability
(self
._lookup
_path
)
196 if node_availability
is None:
197 logging
.warning('No availability found for: %s' % self
)
200 parent_node_availability
= self
._LookupAvailability
(self
._GetParentPath
())
201 # If the parent node availability couldn't be found, something
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
:
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')
218 def __enter__(self2
):
220 self
._ignored
_categories
.extend(ignore
)
222 self
._lookup
_path
.extend(path
)
224 def __exit__(self2
, _
, __
, ___
):
226 self
._ignored
_categories
[:] = self
._ignored
_categories
[:-len(ignore
)]
228 self
._lookup
_path
[:] = self
._lookup
_path
[:-len(path
)]
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
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
):
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()
303 schema_graph
[name
] = _GraphNode((key
, _CreateGraph(value
)) for
304 key
, value
in node
.iteritems())
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
313 difference
= _GraphNode()
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
], {})
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
])
323 # Record a difference if children of this key differed at some point.
324 difference
[key
] = rest
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|.
359 # Add this key and the rest of its children.
360 base
[key
] = update(_GraphNode(annotation
=annotator(key
)), addend
[key
])
362 # The key is already in |base|, but check its children.
363 update(base
[key
], addend
[key
])
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|.
373 for path_piece
in path
:
374 node
= node
.get(path_piece
)
376 return LookupResult(found
=False, annotation
=None)
377 return LookupResult(found
=True, annotation
=node
._annotation
)
380 '''Checks for an empty schema graph.
382 return not self
._graph