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. 4 5 import json 6 import logging 7 8 from api_models import GetNodeCategories 9 from collections import Iterable, Mapping 10 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 ''' 16 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 21 22 def __eq__(self, other): 23 return self.__dict__ == other.__dict__ 24 25 def __ne__(self, other): 26 return not (self == other) 27 28 def __repr__(self): 29 return '%s%s' % (type(self).__name__, repr(self.__dict__)) 30 31 def __str__(self): 32 return repr(self) 33 34 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: 39 40 { 41 'tabs': { 42 'types': { 43 'Tab': { 44 'properties': { 45 'url': { 46 ... 47 } 48 } 49 } 50 } 51 } 52 } 53 54 then the 'url' property would be represented by: 55 56 ['tabs', 'types', 'Tab', 'properties', 'url'] 57 ''' 58 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 = [] 65 66 def _AssertIsValidCategory(self, category): 67 assert category in GetNodeCategories(), \ 68 '%s is not a valid category. Full path: %s' % (category, str(self)) 69 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.' 75 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] 92 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 98 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. 104 ''' 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 118 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. 125 ''' 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 136 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. 140 ''' 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 148 149 def _GetCategory(self): 150 '''Returns the category this node belongs to. 151 ''' 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) 175 176 def GetDeprecated(self): 177 '''Returns when this node became deprecated, or None if it 178 is not deprecated. 179 ''' 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 189 190 def GetAvailability(self): 191 '''Returns availability information for this node. 192 ''' 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 199 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 204 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 210 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. 215 ''' 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) 223 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() 230 231 def __str__(self): 232 return repr(self) 233 234 def __repr__(self): 235 return '%s > %s' % (self._namespace_name, ' > '.join(self._lookup_path)) 236 237 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. 241 ''' 242 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') 250 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) 255 256 def __ne__(self, other): 257 return not (self == other) 258 259 def GetAnnotation(self): 260 return self._annotation 261 262 def SetAnnotation(self, annotation): 263 self._annotation = annotation 264 265 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. 269 ''' 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) 276 277 278 def _IsObjectList(value): 279 '''Determines whether or not |value| is a list made up entirely of 280 dict-like objects. 281 ''' 282 return (isinstance(value, Iterable) and 283 all(isinstance(node, Mapping) for node in value)) 284 285 286 def _CreateGraph(root): 287 '''Recursively moves through an API schema, replacing lists of objects 288 and non-object values with objects. 289 ''' 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()) 297 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 306 307 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|. 312 ''' 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 326 327 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. 331 ''' 332 333 def __init__(self, api_schema=None, _graph=None): 334 self._graph = _graph if _graph is not None else _CreateGraph(api_schema) 335 336 def __eq__(self, other): 337 return self._graph == other._graph 338 339 def __ne__(self, other): 340 return not (self == other) 341 342 def Subtract(self, other): 343 '''Returns an APISchemaGraph instance representing keys that are in 344 this graph but not in |other|. 345 ''' 346 return APISchemaGraph(_graph=_Subtract(self._graph, other._graph)) 347 348 def Update(self, other, annotator): 349 '''Modifies this graph by adding keys from |other| that are not 350 already present in this graph. 351 ''' 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|. 356 ''' 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 365 366 update(self._graph, other._graph) 367 368 def Lookup(self, *path): 369 '''Given a list of path components, |path|, checks if the 370 APISchemaGraph instance contains |path|. 371 ''' 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) 378 379 def IsEmpty(self): 380 '''Checks for an empty schema graph. 381 ''' 382 return not self._graph 383