1# Copyright (c) 2012 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
5from copy import copy
6import logging
7import re
8
9from file_system import FileNotFoundError
10from third_party.json_schema_compiler.model import PropertyType
11
12
13def _ClassifySchemaNode(node_name, node):
14  """Attempt to classify |node_name| in an API, determining whether |node_name|
15  refers to a type, function, event, or property in |api|.
16  """
17  if '.' in node_name:
18    node_name, rest = node_name.split('.', 1)
19  else:
20    rest = None
21  for key, group in [('types', 'type'),
22                     ('functions', 'method'),
23                     ('events', 'event'),
24                     ('properties', 'property')]:
25    for item in getattr(node, key, {}).itervalues():
26      if item.simple_name == node_name:
27        if rest is not None:
28          ret = _ClassifySchemaNode(rest, item)
29          if ret is not None:
30            return ret
31        else:
32          return group, node_name
33  return None
34
35
36def _MakeKey(namespace, ref):
37  key = '%s/%s' % (namespace, ref)
38  # AppEngine doesn't like keys > 500, but there will be some other stuff
39  # that goes into this key, so truncate it earlier.  This shoudn't be
40  # happening anyway unless there's a bug, such as http://crbug.com/314102.
41  max_size = 256
42  if len(key) > max_size:
43    logging.error('Key was >%s characters: %s' % (max_size, key))
44    key = key[:max_size]
45  return key
46
47
48class ReferenceResolver(object):
49  """Resolves references to $ref's by searching through the APIs to find the
50  correct node. See document_renderer.py for more information on $ref syntax.
51  """
52  def __init__(self, api_models, object_store):
53    self._api_models = api_models
54    self._object_store = object_store
55
56  def _GetRefLink(self, ref, api_list, namespace):
57    # Check nodes within each API the ref might refer to.
58    parts = ref.split('.')
59    for i in xrange(1, len(parts)):
60      api_name = '.'.join(parts[:i])
61      if api_name not in api_list:
62        continue
63      try:
64        api_model = self._api_models.GetModel(api_name).Get()
65      except FileNotFoundError:
66        continue
67      name = '.'.join(parts[i:])
68      # Attempt to find |name| in the API.
69      node_info = _ClassifySchemaNode(name, api_model)
70      if node_info is None:
71        # Check to see if this ref is a property. If it is, we want the ref to
72        # the underlying type the property is referencing.
73        for prop in api_model.properties.itervalues():
74          # If the name of this property is in the ref text, replace the
75          # property with its type, and attempt to classify it.
76          if prop.name in name and prop.type_.property_type == PropertyType.REF:
77            name_as_prop_type = name.replace(prop.name, prop.type_.ref_type)
78            node_info = _ClassifySchemaNode(name_as_prop_type, api_model)
79            if node_info is not None:
80              name = name_as_prop_type
81              text = ref.replace(prop.name, prop.type_.ref_type)
82              break
83        if node_info is None:
84          continue
85      else:
86        text = ref
87      category, node_name = node_info
88      if namespace is not None and text.startswith('%s.' % namespace):
89        text = text[len('%s.' % namespace):]
90      api_model = self._api_models.GetModel(api_name).Get()
91      filename = api_model.documentation_options.get('documented_in', api_name)
92      return {
93        'href': '%s#%s-%s' % (filename, category, name.replace('.', '-')),
94        'text': text,
95        'name': node_name
96      }
97
98    # If it's not a reference to an API node it might just be a reference to an
99    # API. Check this last so that links within APIs take precedence over links
100    # to other APIs.
101    if ref in api_list:
102      return {
103        'href': '%s' % ref,
104        'text': ref,
105        'name': ref
106      }
107
108    return None
109
110  def GetRefModel(self, ref, api_list):
111    """Tries to resolve |ref| from the namespaces given in api_list. If ref
112    is found in one of those namespaces, return a tuple (api_model, node_info),
113    where api_model is a model.Namespace class and node info is a tuple
114    (group, name) where group is one of 'type', 'method', 'event', 'property'
115    describing the type of the reference, and name is the name of the reference
116    without the namespace.
117    """
118    # Check nodes within each API the ref might refer to.
119    parts = ref.split('.')
120    for i in xrange(1, len(parts)):
121      api_name = '.'.join(parts[:i])
122      if api_name not in api_list:
123        continue
124      try:
125        api_model = self._api_models.GetModel(api_name).Get()
126      except FileNotFoundError:
127        continue
128      name = '.'.join(parts[i:])
129      # Attempt to find |name| in the API.
130      node_info = _ClassifySchemaNode(name, api_model)
131      if node_info is None:
132        # Check to see if this ref is a property. If it is, we want the ref to
133        # the underlying type the property is referencing.
134        for prop in api_model.properties.itervalues():
135          # If the name of this property is in the ref text, replace the
136          # property with its type, and attempt to classify it.
137          if prop.name in name and prop.type_.property_type == PropertyType.REF:
138            name_as_prop_type = name.replace(prop.name, prop.type_.ref_type)
139            node_info = _ClassifySchemaNode(name_as_prop_type, api_model)
140        if node_info is None:
141          continue
142      return api_model, node_info
143    return None, None
144
145  def GetLink(self, ref, namespace=None, title=None):
146    """Resolve $ref |ref| in namespace |namespace| if not None, returning None
147    if it cannot be resolved.
148    """
149    db_key = _MakeKey(namespace, ref)
150    link = self._object_store.Get(db_key).Get()
151    if link is None:
152      api_list = self._api_models.GetNames()
153      link = self._GetRefLink(ref, api_list, namespace)
154      if link is None and namespace is not None:
155        # Try to resolve the ref in the current namespace if there is one.
156        api_list = self._api_models.GetNames()
157        link = self._GetRefLink('%s.%s' % (namespace, ref),
158                                api_list,
159                                namespace)
160      if link is None:
161        return None
162      self._object_store.Set(db_key, link)
163
164    if title is not None:
165      link = copy(link)
166      link['text'] = title
167
168    return link
169
170  def SafeGetLink(self, ref, namespace=None, title=None, path=None):
171    """Resolve $ref |ref| in namespace |namespace|, or globally if None. If it
172    cannot be resolved, pretend like it is a link to a type.
173    """
174    ref_data = self.GetLink(ref, namespace=namespace, title=title)
175    if ref_data is not None:
176      return ref_data
177    logging.warning('Could not resolve $ref %s in namespace %s on %s.' %
178        (ref, namespace, path))
179    type_name = ref.rsplit('.', 1)[-1]
180    return {
181      'href': '#type-%s' % type_name,
182      'text': title or ref,
183      'name': ref
184    }
185