reference_resolver.py revision c5cede9ae108bb15f6b7a8aea21c7e1fefa2834c
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.
51
52  $ref's have two forms:
53
54    $ref:api.node - Replaces the $ref with a link to node on the API page. The
55                    title is set to the name of the node.
56
57    $ref:[api.node The Title] - Same as the previous form but title is set to
58                                "The Title".
59  """
60
61  # Matches after a $ref: that doesn't have []s.
62  _bare_ref = re.compile('\w+(\.\w+)*')
63
64  def __init__(self, api_models, object_store):
65    self._api_models = api_models
66    self._object_store = object_store
67
68  def _GetRefLink(self, ref, api_list, namespace):
69    # Check nodes within each API the ref might refer to.
70    parts = ref.split('.')
71    for i, part in enumerate(parts):
72      api_name = '.'.join(parts[:i])
73      if api_name not in api_list:
74        continue
75      try:
76        api_model = self._api_models.GetModel(api_name).Get()
77      except FileNotFoundError:
78        continue
79      name = '.'.join(parts[i:])
80      # Attempt to find |name| in the API.
81      node_info = _ClassifySchemaNode(name, api_model)
82      if node_info is None:
83        # Check to see if this ref is a property. If it is, we want the ref to
84        # the underlying type the property is referencing.
85        for prop in api_model.properties.itervalues():
86          # If the name of this property is in the ref text, replace the
87          # property with its type, and attempt to classify it.
88          if prop.name in name and prop.type_.property_type == PropertyType.REF:
89            name_as_prop_type = name.replace(prop.name, prop.type_.ref_type)
90            node_info = _ClassifySchemaNode(name_as_prop_type, api_model)
91            if node_info is not None:
92              name = name_as_prop_type
93              text = ref.replace(prop.name, prop.type_.ref_type)
94              break
95        if node_info is None:
96          continue
97      else:
98        text = ref
99      category, node_name = node_info
100      if namespace is not None and text.startswith('%s.' % namespace):
101        text = text[len('%s.' % namespace):]
102      api_model = self._api_models.GetModel(api_name).Get()
103      filename = api_model.documentation_options.get('documented_in', api_name)
104      return {
105        'href': '%s#%s-%s' % (filename, category, name.replace('.', '-')),
106        'text': text,
107        'name': node_name
108      }
109
110    # If it's not a reference to an API node it might just be a reference to an
111    # API. Check this last so that links within APIs take precedence over links
112    # to other APIs.
113    if ref in api_list:
114      return {
115        'href': '%s' % ref,
116        'text': ref,
117        'name': ref
118      }
119
120    return None
121
122  def GetLink(self, ref, namespace=None, title=None):
123    """Resolve $ref |ref| in namespace |namespace| if not None, returning None
124    if it cannot be resolved.
125    """
126    db_key = _MakeKey(namespace, ref)
127    link = self._object_store.Get(db_key).Get()
128    if link is None:
129      api_list = self._api_models.GetNames()
130      link = self._GetRefLink(ref, api_list, namespace)
131      if link is None and namespace is not None:
132        # Try to resolve the ref in the current namespace if there is one.
133        link = self._GetRefLink('%s.%s' % (namespace, ref), api_list, namespace)
134      if link is None:
135        return None
136      self._object_store.Set(db_key, link)
137
138    if title is not None:
139      link = copy(link)
140      link['text'] = title
141
142    return link
143
144  def SafeGetLink(self, ref, namespace=None, title=None):
145    """Resolve $ref |ref| in namespace |namespace|, or globally if None. If it
146    cannot be resolved, pretend like it is a link to a type.
147    """
148    ref_data = self.GetLink(ref, namespace=namespace, title=title)
149    if ref_data is not None:
150      return ref_data
151    logging.error('$ref %s could not be resolved in namespace %s.' %
152        (ref, namespace))
153    type_name = ref.rsplit('.', 1)[-1]
154    return {
155      'href': '#type-%s' % type_name,
156      'text': title or ref,
157      'name': ref
158    }
159
160  # TODO(ahernandez.miralles): This function is no longer needed,
161  # and uses a deprecated style of ref
162  def ResolveAllLinks(self, text, relative_to='', namespace=None):
163    """This method will resolve all $ref links in |text| using namespace
164    |namespace| if not None. Any links that cannot be resolved will be replaced
165    using the default link format that |SafeGetLink| uses.
166    The links will be generated relative to |relative_to|.
167    """
168    if text is None or '$ref:' not in text:
169      return text
170
171    # requestPath should be of the form (apps|extensions)/...../page.html.
172    # link_prefix should  that the target will point to
173    # (apps|extensions)/target.html. Note multiplying a string by a negative
174    # number gives the empty string.
175    link_prefix = '../' * (relative_to.count('/') - 1)
176    split_text = text.split('$ref:')
177    # |split_text| is an array of text chunks that all start with the
178    # argument to '$ref:'.
179    formatted_text = [split_text[0]]
180    for ref_and_rest in split_text[1:]:
181      title = None
182      if ref_and_rest.startswith('[') and ']' in ref_and_rest:
183        # Text was '$ref:[foo.bar maybe title] other stuff'.
184        ref_with_title, rest = ref_and_rest[1:].split(']', 1)
185        ref_with_title = ref_with_title.split(None, 1)
186        if len(ref_with_title) == 1:
187          # Text was '$ref:[foo.bar] other stuff'.
188          ref = ref_with_title[0]
189        else:
190          # Text was '$ref:[foo.bar title] other stuff'.
191          ref, title = ref_with_title
192      else:
193        # Text was '$ref:foo.bar other stuff'.
194        match = self._bare_ref.match(ref_and_rest)
195        if match is None:
196          ref = ''
197          rest = ref_and_rest
198        else:
199          ref = match.group()
200          rest = ref_and_rest[match.end():]
201
202      ref_dict = self.SafeGetLink(ref, namespace=namespace, title=title)
203      formatted_text.append('<a href="%s%s">%s</a>%s' %
204          (link_prefix, ref_dict['href'], ref_dict['text'], rest))
205    return ''.join(formatted_text)
206