blob: 1ec58334bceaa482055ab8625f48b12ca89a9916 [file] [log] [blame]
# Copyright 2013 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import json
import logging
from api_models import GetNodeCategories
from collections import Iterable, Mapping
class LookupResult(object):
'''Returned from APISchemaGraph.Lookup(), and relays whether or not
some element was found and what annotation object was associated with it,
if any.
'''
def __init__(self, found=None, annotation=None):
assert found is not None, 'LookupResult was given None value for |found|.'
self.found = found
self.annotation = annotation
def __eq__(self, other):
return self.__dict__ == other.__dict__
def __ne__(self, other):
return not (self == other)
def __repr__(self):
return '%s%s' % (type(self).__name__, repr(self.__dict__))
def __str__(self):
return repr(self)
class APINodeCursor(object):
'''An abstract representation of a node in an APISchemaGraph.
The current position in the graph is represented by a path into the
underlying dictionary. So if the APISchemaGraph is:
{
'tabs': {
'types': {
'Tab': {
'properties': {
'url': {
...
}
}
}
}
}
}
then the 'url' property would be represented by:
['tabs', 'types', 'Tab', 'properties', 'url']
'''
def __init__(self, availability_finder, namespace_name):
self._lookup_path = []
self._node_availabilities = availability_finder.GetAPINodeAvailability(
namespace_name)
self._namespace_name = namespace_name
self._ignored_categories = []
def _AssertIsValidCategory(self, category):
assert category in GetNodeCategories(), \
'%s is not a valid category. Full path: %s' % (category, str(self))
def _GetParentPath(self):
'''Returns the path pointing to this node's parent.
'''
assert len(self._lookup_path) > 1, \
'Tried to look up parent for the top-level node.'
# lookup_path[-1] is the name of the current node. If this lookup_path
# describes a regular node, then lookup_path[-2] will be a node category.
# Otherwise, it's an event callback or a function parameter.
if self._lookup_path[-2] not in GetNodeCategories():
if self._lookup_path[-1] == 'callback':
# This is an event callback, so lookup_path[-2] is the event
# node name, thus lookup_path[-3] must be 'events'.
assert self._lookup_path[-3] == 'events' , \
'Invalid lookup path: %s' % (self._lookup_path)
return self._lookup_path[:-1]
# This is a function parameter.
assert self._lookup_path[-2] == 'parameters'
return self._lookup_path[:-2]
# This is a regular node, so lookup_path[-2] should
# be a node category.
self._AssertIsValidCategory(self._lookup_path[-2])
return self._lookup_path[:-2]
def _LookupNodeAvailability(self, lookup_path):
'''Returns the ChannelInfo object for this node.
'''
return self._node_availabilities.Lookup(self._namespace_name,
*lookup_path).annotation
def _CheckNamespacePrefix(self, lookup_path):
'''API schemas may prepend the namespace name to top-level types
(e.g. declarativeWebRequest > types > declarativeWebRequest.IgnoreRules),
but just the base name (here, 'IgnoreRules') will be in the |lookup_path|.
Try creating an alternate |lookup_path| by adding the namespace name.
'''
# lookup_path[0] is always the node category (e.g. types, functions, etc.).
# Thus, lookup_path[1] is always the top-level node name.
self._AssertIsValidCategory(lookup_path[0])
base_name = lookup_path[1]
lookup_path[1] = '%s.%s' % (self._namespace_name, base_name)
try:
node_availability = self._LookupNodeAvailability(lookup_path)
if node_availability is not None:
return node_availability
finally:
# Restore lookup_path.
lookup_path[1] = base_name
return None
def _CheckEventCallback(self, lookup_path):
'''Within API schemas, an event has a list of 'properties' that the event's
callback expects. The callback itself is not explicitly represented in the
schema. However, when creating an event node in JSCView, a callback node
is generated and acts as the parent for the event's properties.
Modify |lookup_path| to check the original schema format.
'''
if 'events' in lookup_path:
assert 'callback' in lookup_path, self
callback_index = lookup_path.index('callback')
try:
lookup_path.pop(callback_index)
node_availability = self._LookupNodeAvailability(lookup_path)
finally:
lookup_path.insert(callback_index, 'callback')
return node_availability
return None
def _LookupAvailability(self, lookup_path):
'''Runs all the lookup checks on |lookup_path| and
returns the node availability if found, None otherwise.
'''
for lookup in (self._LookupNodeAvailability,
self._CheckEventCallback,
self._CheckNamespacePrefix):
node_availability = lookup(lookup_path)
if node_availability is not None:
return node_availability
return None
def _GetCategory(self):
'''Returns the category this node belongs to.
'''
if self._lookup_path[-2] in GetNodeCategories():
return self._lookup_path[-2]
# If lookup_path[-2] is not a valid category and lookup_path[-1] is
# 'callback', then we know we have an event callback.
if self._lookup_path[-1] == 'callback':
return 'events'
if self._lookup_path[-2] == 'parameters':
# Function parameters are modelled as properties.
return 'properties'
if (self._lookup_path[-1].endswith('Type') and
(self._lookup_path[-1][:-len('Type')] == self._lookup_path[-2] or
self._lookup_path[-1][:-len('ReturnType')] == self._lookup_path[-2])):
# Array elements and function return objects have 'Type' and 'ReturnType'
# appended to their names, respectively, in model.py. This results in
# lookup paths like
# 'events > types > Rule > properties > tags > tagsType'.
# These nodes are treated as properties.
return 'properties'
if self._lookup_path[0] == 'events':
# HACK(ahernandez.miralles): This catches a few edge cases,
# such as 'webviewTag > events > consolemessage > level'.
return 'properties'
raise AssertionError('Could not classify node %s' % self)
def GetDeprecated(self):
'''Returns when this node became deprecated, or None if it
is not deprecated.
'''
deprecated_path = self._lookup_path + ['deprecated']
for lookup in (self._LookupNodeAvailability,
self._CheckNamespacePrefix):
node_availability = lookup(deprecated_path)
if node_availability is not None:
return node_availability
if 'callback' in self._lookup_path:
return self._CheckEventCallback(deprecated_path)
return None
def GetAvailability(self):
'''Returns availability information for this node.
'''
if self._GetCategory() in self._ignored_categories:
return None
node_availability = self._LookupAvailability(self._lookup_path)
if node_availability is None:
logging.warning('No availability found for: %s' % self)
return None
parent_node_availability = self._LookupAvailability(self._GetParentPath())
# If the parent node availability couldn't be found, something
# is very wrong.
assert parent_node_availability is not None
# Only render this node's availability if it differs from the parent
# node's availability.
if node_availability == parent_node_availability:
return None
return node_availability
def Descend(self, *path, **kwargs):
'''Moves down the APISchemaGraph, following |path|.
|ignore| should be a tuple of category strings (e.g. ('types',))
for which nodes should not have availability data generated.
'''
ignore = kwargs.get('ignore')
class scope(object):
def __enter__(self2):
if ignore:
self._ignored_categories.extend(ignore)
if path:
self._lookup_path.extend(path)
def __exit__(self2, _, __, ___):
if ignore:
self._ignored_categories[:] = self._ignored_categories[:-len(ignore)]
if path:
self._lookup_path[:] = self._lookup_path[:-len(path)]
return scope()
def __str__(self):
return repr(self)
def __repr__(self):
return '%s > %s' % (self._namespace_name, ' > '.join(self._lookup_path))
class _GraphNode(dict):
'''Represents some element of an API schema, and allows extra information
about that element to be stored on the |_annotation| object.
'''
def __init__(self, *args, **kwargs):
# Use **kwargs here since Python is picky with ordering of default args
# and variadic args in the method signature. The only keyword arg we care
# about here is 'annotation'. Intentionally don't pass |**kwargs| into the
# superclass' __init__().
dict.__init__(self, *args)
self._annotation = kwargs.get('annotation')
def __eq__(self, other):
# _GraphNode inherits __eq__() from dict, which will not take annotation
# objects into account when comparing.
return dict.__eq__(self, other)
def __ne__(self, other):
return not (self == other)
def GetAnnotation(self):
return self._annotation
def SetAnnotation(self, annotation):
self._annotation = annotation
def _NameForNode(node):
'''Creates a unique id for an object in an API schema, depending on
what type of attribute the object is a member of.
'''
if 'namespace' in node: return node['namespace']
if 'name' in node: return node['name']
if 'id' in node: return node['id']
if 'type' in node: return node['type']
if '$ref' in node: return node['$ref']
assert False, 'Problems with naming node: %s' % json.dumps(node, indent=3)
def _IsObjectList(value):
'''Determines whether or not |value| is a list made up entirely of
dict-like objects.
'''
return (isinstance(value, Iterable) and
all(isinstance(node, Mapping) for node in value))
def _CreateGraph(root):
'''Recursively moves through an API schema, replacing lists of objects
and non-object values with objects.
'''
schema_graph = _GraphNode()
if _IsObjectList(root):
for node in root:
name = _NameForNode(node)
assert name not in schema_graph, 'Duplicate name in API schema graph.'
schema_graph[name] = _GraphNode((key, _CreateGraph(value)) for
key, value in node.iteritems())
elif isinstance(root, Mapping):
for name, node in root.iteritems():
if not isinstance(node, Mapping):
schema_graph[name] = _GraphNode()
else:
schema_graph[name] = _GraphNode((key, _CreateGraph(value)) for
key, value in node.iteritems())
return schema_graph
def _Subtract(minuend, subtrahend):
''' A Set Difference adaptation for graphs. Returns a |difference|,
which contains key-value pairs found in |minuend| but not in
|subtrahend|.
'''
difference = _GraphNode()
for key in minuend:
if key not in subtrahend:
# Record all of this key's children as being part of the difference.
difference[key] = _Subtract(minuend[key], {})
else:
# Note that |minuend| and |subtrahend| are assumed to be graphs, and
# therefore should have no lists present, only keys and nodes.
rest = _Subtract(minuend[key], subtrahend[key])
if rest:
# Record a difference if children of this key differed at some point.
difference[key] = rest
return difference
class APISchemaGraph(object):
'''Provides an interface for interacting with an API schema graph, a
nested dict structure that allows for simpler lookups of schema data.
'''
def __init__(self, api_schema=None, _graph=None):
self._graph = _graph if _graph is not None else _CreateGraph(api_schema)
def __eq__(self, other):
return self._graph == other._graph
def __ne__(self, other):
return not (self == other)
def Subtract(self, other):
'''Returns an APISchemaGraph instance representing keys that are in
this graph but not in |other|.
'''
return APISchemaGraph(_graph=_Subtract(self._graph, other._graph))
def Update(self, other, annotator):
'''Modifies this graph by adding keys from |other| that are not
already present in this graph.
'''
def update(base, addend):
'''A Set Union adaptation for graphs. Returns a graph which contains
the key-value pairs from |base| combined with any key-value pairs
from |addend| that are not present in |base|.
'''
for key in addend:
if key not in base:
# Add this key and the rest of its children.
base[key] = update(_GraphNode(annotation=annotator(key)), addend[key])
else:
# The key is already in |base|, but check its children.
update(base[key], addend[key])
return base
update(self._graph, other._graph)
def Lookup(self, *path):
'''Given a list of path components, |path|, checks if the
APISchemaGraph instance contains |path|.
'''
node = self._graph
for path_piece in path:
node = node.get(path_piece)
if node is None:
return LookupResult(found=False, annotation=None)
return LookupResult(found=True, annotation=node._annotation)
def IsEmpty(self):
'''Checks for an empty schema graph.
'''
return not self._graph