1d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)# Copyright 2013 The Chromium Authors. All rights reserved.
2d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)# Use of this source code is governed by a BSD-style license that can be
3d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)# found in the LICENSE file.
4d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)
5d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)import json
65f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)import logging
75f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)
85f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)from api_models import GetNodeCategories
968043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)from collections import Iterable, Mapping
1068043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)
1168043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)class LookupResult(object):
1268043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)  '''Returned from APISchemaGraph.Lookup(), and relays whether or not
1368043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)  some element was found and what annotation object was associated with it,
1468043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)  if any.
1568043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)  '''
1668043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)
1768043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)  def __init__(self, found=None, annotation=None):
1868043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)    assert found is not None, 'LookupResult was given None value for |found|.'
1968043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)    self.found = found
2068043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)    self.annotation = annotation
2168043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)
2268043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)  def __eq__(self, other):
234e180b6a0b4720a9b8e9e959a882386f690f08ffTorne (Richard Coles)    return self.__dict__ == other.__dict__
2468043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)
2568043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)  def __ne__(self, other):
2668043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)    return not (self == other)
2768043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)
284e180b6a0b4720a9b8e9e959a882386f690f08ffTorne (Richard Coles)  def __repr__(self):
294e180b6a0b4720a9b8e9e959a882386f690f08ffTorne (Richard Coles)    return '%s%s' % (type(self).__name__, repr(self.__dict__))
304e180b6a0b4720a9b8e9e959a882386f690f08ffTorne (Richard Coles)
314e180b6a0b4720a9b8e9e959a882386f690f08ffTorne (Richard Coles)  def __str__(self):
324e180b6a0b4720a9b8e9e959a882386f690f08ffTorne (Richard Coles)    return repr(self)
334e180b6a0b4720a9b8e9e959a882386f690f08ffTorne (Richard Coles)
3468043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)
355f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)class APINodeCursor(object):
365f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  '''An abstract representation of a node in an APISchemaGraph.
375f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  The current position in the graph is represented by a path into the
385f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  underlying dictionary. So if the APISchemaGraph is:
395f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)
405f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    {
415f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      'tabs': {
425f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)        'types': {
435f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)          'Tab': {
445f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)            'properties': {
455f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)              'url': {
465f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)                ...
475f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)              }
485f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)            }
495f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)          }
505f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)        }
515f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      }
525f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    }
535f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)
545f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  then the 'url' property would be represented by:
555f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)
565f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    ['tabs', 'types', 'Tab', 'properties', 'url']
575f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  '''
585f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)
595f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  def __init__(self, availability_finder, namespace_name):
605f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    self._lookup_path = []
615f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    self._node_availabilities = availability_finder.GetAPINodeAvailability(
625f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)        namespace_name)
635f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    self._namespace_name = namespace_name
645f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    self._ignored_categories = []
655f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)
665f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  def _AssertIsValidCategory(self, category):
675f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    assert category in GetNodeCategories(), \
685f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)        '%s is not a valid category. Full path: %s' % (category, str(self))
695f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)
705f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  def _GetParentPath(self):
715f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    '''Returns the path pointing to this node's parent.
725f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    '''
735f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    assert len(self._lookup_path) > 1, \
745f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)        'Tried to look up parent for the top-level node.'
755f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)
765f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    # lookup_path[-1] is the name of the current node. If this lookup_path
775f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    # describes a regular node, then lookup_path[-2] will be a node category.
785f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    # Otherwise, it's an event callback or a function parameter.
795f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    if self._lookup_path[-2] not in GetNodeCategories():
805f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      if self._lookup_path[-1] == 'callback':
815f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)        # This is an event callback, so lookup_path[-2] is the event
825f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)        # node name, thus lookup_path[-3] must be 'events'.
835f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)        assert self._lookup_path[-3] == 'events'
845f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)        return self._lookup_path[:-1]
855f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      # This is a function parameter.
865f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      assert self._lookup_path[-2] == 'parameters'
875f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      return self._lookup_path[:-2]
885f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    # This is a regular node, so lookup_path[-2] should
895f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    # be a node category.
905f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    self._AssertIsValidCategory(self._lookup_path[-2])
915f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    return self._lookup_path[:-2]
925f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)
935f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  def _LookupNodeAvailability(self, lookup_path):
945f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    '''Returns the ChannelInfo object for this node.
955f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    '''
965f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    return self._node_availabilities.Lookup(self._namespace_name,
975f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)                                            *lookup_path).annotation
985f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)
995f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  def _CheckNamespacePrefix(self, lookup_path):
1005f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    '''API schemas may prepend the namespace name to top-level types
1015f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    (e.g. declarativeWebRequest > types > declarativeWebRequest.IgnoreRules),
1025f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    but just the base name (here, 'IgnoreRules') will be in the |lookup_path|.
1035f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    Try creating an alternate |lookup_path| by adding the namespace name.
1045f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    '''
1055f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    # lookup_path[0] is always the node category (e.g. types, functions, etc.).
1065f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    # Thus, lookup_path[1] is always the top-level node name.
1075f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    self._AssertIsValidCategory(lookup_path[0])
1085f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    base_name = lookup_path[1]
1095f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    lookup_path[1] = '%s.%s' % (self._namespace_name, base_name)
1105f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    try:
1115f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      node_availability = self._LookupNodeAvailability(lookup_path)
1125f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      if node_availability is not None:
1135f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)        return node_availability
1145f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    finally:
1155f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      # Restore lookup_path.
1165f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      lookup_path[1] = base_name
1175f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    return None
1185f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)
1195f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  def _CheckEventCallback(self, lookup_path):
1205f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    '''Within API schemas, an event has a list of 'properties' that the event's
1215f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    callback expects. The callback itself is not explicitly represented in the
1225f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    schema. However, when creating an event node in JSCView, a callback node
1235f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    is generated and acts as the parent for the event's properties.
1245f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    Modify |lookup_path| to check the original schema format.
1255f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    '''
1265f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    if 'events' in lookup_path:
1275f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      assert 'callback' in lookup_path, self
1285f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      callback_index = lookup_path.index('callback')
1295f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      try:
1305f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)        lookup_path.pop(callback_index)
1315f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)        node_availability = self._LookupNodeAvailability(lookup_path)
1325f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      finally:
1335f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)        lookup_path.insert(callback_index, 'callback')
1345f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      return node_availability
1355f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    return None
1365f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)
1375f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  def _LookupAvailability(self, lookup_path):
1385f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    '''Runs all the lookup checks on |lookup_path| and
1395f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    returns the node availability if found, None otherwise.
1405f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    '''
1415f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    for lookup in (self._LookupNodeAvailability,
1425f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)                   self._CheckEventCallback,
1435f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)                   self._CheckNamespacePrefix):
1445f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      node_availability = lookup(lookup_path)
1455f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      if node_availability is not None:
1465f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)        return node_availability
1475f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    return None
1485f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)
1495f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  def _GetCategory(self):
1505f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    '''Returns the category this node belongs to.
1515f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    '''
1525f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    if self._lookup_path[-2] in GetNodeCategories():
1535f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      return self._lookup_path[-2]
1545f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    # If lookup_path[-2] is not a valid category and lookup_path[-1] is
1555f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    # 'callback', then we know we have an event callback.
1565f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    if self._lookup_path[-1] == 'callback':
1575f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      return 'events'
1585f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    if self._lookup_path[-2] == 'parameters':
1595f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      # Function parameters are modelled as properties.
1605f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      return 'properties'
1615f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    if (self._lookup_path[-1].endswith('Type') and
1625f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)        (self._lookup_path[-1][:-len('Type')] == self._lookup_path[-2] or
1635f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)         self._lookup_path[-1][:-len('ReturnType')] == self._lookup_path[-2])):
1645f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      # Array elements and function return objects have 'Type' and 'ReturnType'
1655f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      # appended to their names, respectively, in model.py. This results in
1665f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      # lookup paths like
1675f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      # 'events > types > Rule > properties > tags > tagsType'.
1685f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      # These nodes are treated as properties.
1695f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      return 'properties'
1705f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    if self._lookup_path[0] == 'events':
1715f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      # HACK(ahernandez.miralles): This catches a few edge cases,
1725f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      # such as 'webviewTag > events > consolemessage > level'.
1735f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      return 'properties'
1745f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    raise AssertionError('Could not classify node %s' % self)
1755f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)
1765f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  def GetDeprecated(self):
1775f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    '''Returns when this node became deprecated, or None if it
1785f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    is not deprecated.
1795f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    '''
1805f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    deprecated_path = self._lookup_path + ['deprecated']
1815f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    for lookup in (self._LookupNodeAvailability,
1825f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)                   self._CheckNamespacePrefix):
1835f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      node_availability = lookup(deprecated_path)
1845f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      if node_availability is not None:
1855f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)        return node_availability
1865f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    if 'callback' in self._lookup_path:
1875f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      return self._CheckEventCallback(deprecated_path)
1885f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    return None
1895f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)
1905f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  def GetAvailability(self):
1915f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    '''Returns availability information for this node.
1925f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    '''
1935f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    if self._GetCategory() in self._ignored_categories:
1945f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      return None
1955f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    node_availability = self._LookupAvailability(self._lookup_path)
1965f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    if node_availability is None:
1975f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      logging.warning('No availability found for: %s' % self)
1985f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      return None
1995f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)
2005f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    parent_node_availability = self._LookupAvailability(self._GetParentPath())
2015f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    # If the parent node availability couldn't be found, something
2025f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    # is very wrong.
2035f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    assert parent_node_availability is not None
2045f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)
2055f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    # Only render this node's availability if it differs from the parent
2065f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    # node's availability.
2075f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    if node_availability == parent_node_availability:
2085f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      return None
2095f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    return node_availability
2105f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)
2115f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  def Descend(self, *path, **kwargs):
2125f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    '''Moves down the APISchemaGraph, following |path|.
2135f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    |ignore| should be a tuple of category strings (e.g. ('types',))
2145f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    for which nodes should not have availability data generated.
2155f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    '''
2165f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    ignore = kwargs.get('ignore')
2175f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    class scope(object):
2185f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      def __enter__(self2):
2195f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)        if ignore:
2205f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)          self._ignored_categories.extend(ignore)
2215f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)        if path:
2225f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)          self._lookup_path.extend(path)
2235f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)
2245f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      def __exit__(self2, _, __, ___):
2255f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)        if ignore:
2265f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)          self._ignored_categories[:] = self._ignored_categories[:-len(ignore)]
2275f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)        if path:
2285f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)          self._lookup_path[:] = self._lookup_path[:-len(path)]
2295f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    return scope()
2305f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)
2315f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  def __str__(self):
2325f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    return repr(self)
2335f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)
2345f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  def __repr__(self):
2355f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    return '%s > %s' % (self._namespace_name, ' > '.join(self._lookup_path))
2365f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)
2375f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)
23868043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)class _GraphNode(dict):
23968043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)  '''Represents some element of an API schema, and allows extra information
24068043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)  about that element to be stored on the |_annotation| object.
24168043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)  '''
24268043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)
24368043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)  def __init__(self, *args, **kwargs):
24468043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)    # Use **kwargs here since Python is picky with ordering of default args
24568043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)    # and variadic args in the method signature. The only keyword arg we care
24668043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)    # about here is 'annotation'. Intentionally don't pass |**kwargs| into the
24768043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)    # superclass' __init__().
24868043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)    dict.__init__(self, *args)
24968043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)    self._annotation = kwargs.get('annotation')
25068043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)
25168043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)  def __eq__(self, other):
25268043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)    # _GraphNode inherits __eq__() from dict, which will not take annotation
25368043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)    # objects into account when comparing.
25468043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)    return dict.__eq__(self, other)
25568043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)
25668043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)  def __ne__(self, other):
25768043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)    return not (self == other)
258d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)
2595f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  def GetAnnotation(self):
2605f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    return self._annotation
2615f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)
2625f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  def SetAnnotation(self, annotation):
2635f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    self._annotation = annotation
2645f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)
265d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)
266d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)def _NameForNode(node):
267d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)  '''Creates a unique id for an object in an API schema, depending on
268d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)  what type of attribute the object is a member of.
269d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)  '''
270d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)  if 'namespace' in node: return node['namespace']
271d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)  if 'name' in node: return node['name']
272d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)  if 'id' in node: return node['id']
273d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)  if 'type' in node: return node['type']
27468043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)  if '$ref' in node: return node['$ref']
275d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)  assert False, 'Problems with naming node: %s' % json.dumps(node, indent=3)
276d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)
277d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)
278d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)def _IsObjectList(value):
279d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)  '''Determines whether or not |value| is a list made up entirely of
280d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)  dict-like objects.
281d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)  '''
28268043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)  return (isinstance(value, Iterable) and
28368043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)          all(isinstance(node, Mapping) for node in value))
284d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)
285d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)
286d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)def _CreateGraph(root):
287d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)  '''Recursively moves through an API schema, replacing lists of objects
288d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)  and non-object values with objects.
289d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)  '''
29068043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)  schema_graph = _GraphNode()
291d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)  if _IsObjectList(root):
292d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)    for node in root:
293d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)      name = _NameForNode(node)
29468043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)      assert name not in schema_graph, 'Duplicate name in API schema graph.'
29568043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)      schema_graph[name] = _GraphNode((key, _CreateGraph(value)) for
29668043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)                                      key, value in node.iteritems())
29768043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)
29868043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)  elif isinstance(root, Mapping):
299d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)    for name, node in root.iteritems():
30068043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)      if not isinstance(node, Mapping):
30168043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)        schema_graph[name] = _GraphNode()
30268043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)      else:
30368043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)        schema_graph[name] = _GraphNode((key, _CreateGraph(value)) for
30468043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)                                        key, value in node.iteritems())
305d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)  return schema_graph
306d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)
307d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)
308d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)def _Subtract(minuend, subtrahend):
309d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)  ''' A Set Difference adaptation for graphs. Returns a |difference|,
310d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)  which contains key-value pairs found in |minuend| but not in
311d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)  |subtrahend|.
312d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)  '''
31368043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)  difference = _GraphNode()
314d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)  for key in minuend:
315d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)    if key not in subtrahend:
316d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)      # Record all of this key's children as being part of the difference.
317d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)      difference[key] = _Subtract(minuend[key], {})
318d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)    else:
319d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)      # Note that |minuend| and |subtrahend| are assumed to be graphs, and
320d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)      # therefore should have no lists present, only keys and nodes.
321d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)      rest = _Subtract(minuend[key], subtrahend[key])
322d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)      if rest:
323d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)        # Record a difference if children of this key differed at some point.
324d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)        difference[key] = rest
325d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)  return difference
326d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)
327d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)
328d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)class APISchemaGraph(object):
329d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)  '''Provides an interface for interacting with an API schema graph, a
330d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)  nested dict structure that allows for simpler lookups of schema data.
331d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)  '''
332d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)
33368043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)  def __init__(self, api_schema=None, _graph=None):
33468043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)    self._graph = _graph if _graph is not None else _CreateGraph(api_schema)
33568043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)
33668043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)  def __eq__(self, other):
33768043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)    return self._graph == other._graph
33868043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)
33968043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)  def __ne__(self, other):
34068043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)    return not (self == other)
341d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)
342d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)  def Subtract(self, other):
343d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)    '''Returns an APISchemaGraph instance representing keys that are in
344d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)    this graph but not in |other|.
345d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)    '''
34668043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)    return APISchemaGraph(_graph=_Subtract(self._graph, other._graph))
34768043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)
3485f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)  def Update(self, other, annotator):
34968043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)    '''Modifies this graph by adding keys from |other| that are not
35068043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)    already present in this graph.
35168043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)    '''
3525f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    def update(base, addend):
3535f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      '''A Set Union adaptation for graphs. Returns a graph which contains
3545f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      the key-value pairs from |base| combined with any key-value pairs
3555f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      from |addend| that are not present in |base|.
3565f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      '''
3575f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      for key in addend:
3585f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)        if key not in base:
3595f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)          # Add this key and the rest of its children.
3605f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)          base[key] = update(_GraphNode(annotation=annotator(key)), addend[key])
3615f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)        else:
3625f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)          # The key is already in |base|, but check its children.
3635f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)           update(base[key], addend[key])
3645f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)      return base
3655f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)
3665f1c94371a64b3196d4be9466099bb892df9b88eTorne (Richard Coles)    update(self._graph, other._graph)
367d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)
368d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)  def Lookup(self, *path):
369d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)    '''Given a list of path components, |path|, checks if the
370d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)    APISchemaGraph instance contains |path|.
371d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)    '''
372d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)    node = self._graph
373d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)    for path_piece in path:
374d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)      node = node.get(path_piece)
375d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)      if node is None:
37668043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)        return LookupResult(found=False, annotation=None)
37768043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)    return LookupResult(found=True, annotation=node._annotation)
378d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)
379d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)  def IsEmpty(self):
380d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)    '''Checks for an empty schema graph.
381d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)    '''
382d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)    return not self._graph
383