Home | History | Annotate | Download | only in server2
      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