reference_resolver.py revision 23730a6e56a168d1879203e4b3819bb36e3d8f1f
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 deepcopy
6import logging
7import re
8
9from file_system import FileNotFoundError
10
11
12def _ClassifySchemaNode(node_name, api):
13  """Attempt to classify |node_name| in an API, determining whether |node_name|
14  refers to a type, function, event, or property in |api|.
15  """
16  if '.' in node_name:
17    node_name, rest = node_name.split('.', 1)
18  else:
19    rest = None
20  for key, group in [('types', 'type'),
21                     ('functions', 'method'),
22                     ('events', 'event'),
23                     ('properties', 'property')]:
24    for item in api.get(key, []):
25      if item['name'] == node_name:
26        if rest is not None:
27          ret = _ClassifySchemaNode(rest, item)
28          if ret is not None:
29            return ret
30        else:
31          return group, node_name
32  return None
33
34
35def _MakeKey(namespace, ref):
36  key = '%s/%s' % (namespace, ref)
37  # AppEngine doesn't like keys > 500, but there will be some other stuff
38  # that goes into this key, so truncate it earlier.  This shoudn't be
39  # happening anyway unless there's a bug, such as http://crbug.com/314102.
40  max_size = 256
41  if len(key) > max_size:
42    logging.error('Key was >%s characters: %s' % (max_size, key))
43    key = key[:max_size]
44  return key
45
46
47class ReferenceResolver(object):
48  """Resolves references to $ref's by searching through the APIs to find the
49  correct node.
50
51  $ref's have two forms:
52
53    $ref:api.node - Replaces the $ref with a link to node on the API page. The
54                    title is set to the name of the node.
55
56    $ref:[api.node The Title] - Same as the previous form but title is set to
57                                "The Title".
58  """
59
60  # Matches after a $ref: that doesn't have []s.
61  _bare_ref = re.compile('\w+(\.\w+)*')
62
63  class Factory(object):
64    def __init__(self,
65                 api_data_source_factory,
66                 api_models,
67                 object_store_creator):
68      self._api_data_source_factory = api_data_source_factory
69      self._api_models = api_models
70      self._object_store_creator = object_store_creator
71
72    def Create(self):
73      return ReferenceResolver(
74          self._api_data_source_factory.Create(None),
75          self._api_models,
76          self._object_store_creator.Create(ReferenceResolver))
77
78  def __init__(self, api_data_source, api_models, object_store):
79    self._api_data_source = api_data_source
80    self._api_models = api_models
81    self._object_store = object_store
82
83  def _GetRefLink(self, ref, api_list, namespace):
84    # Check nodes within each API the ref might refer to.
85    parts = ref.split('.')
86    for i, part in enumerate(parts):
87      api_name = '.'.join(parts[:i])
88      if api_name not in api_list:
89        continue
90      try:
91        api = self._api_data_source.get(api_name, disable_refs=True)
92      except FileNotFoundError:
93        continue
94      name = '.'.join(parts[i:])
95      # Attempt to find |name| in the API.
96      node_info = _ClassifySchemaNode(name, api)
97      if node_info is None:
98        # Check to see if this ref is a property. If it is, we want the ref to
99        # the underlying type the property is referencing.
100        for prop in api.get('properties', []):
101          # If the name of this property is in the ref text, replace the
102          # property with its type, and attempt to classify it.
103          if prop['name'] in name and 'link' in prop:
104            name_as_prop_type = name.replace(prop['name'], prop['link']['name'])
105            node_info = _ClassifySchemaNode(name_as_prop_type, api)
106            if node_info is not None:
107              name = name_as_prop_type
108              text = ref.replace(prop['name'], prop['link']['name'])
109              break
110        if node_info is None:
111          continue
112      else:
113        text = ref
114      category, node_name = node_info
115      if namespace is not None and text.startswith('%s.' % namespace):
116        text = text[len('%s.' % namespace):]
117      return {
118        'href': '%s.html#%s-%s' % (api_name, category, name.replace('.', '-')),
119        'text': text,
120        'name': node_name
121      }
122
123    # If it's not a reference to an API node it might just be a reference to an
124    # API. Check this last so that links within APIs take precedence over links
125    # to other APIs.
126    if ref in api_list:
127      return {
128        'href': '%s.html' % ref,
129        'text': ref,
130        'name': ref
131      }
132
133    return None
134
135  def GetLink(self, ref, namespace=None, title=None):
136    """Resolve $ref |ref| in namespace |namespace| if not None, returning None
137    if it cannot be resolved.
138    """
139    db_key = _MakeKey(namespace, ref)
140    link = self._object_store.Get(db_key).Get()
141    if link is None:
142      api_list = self._api_models.GetNames()
143      link = self._GetRefLink(ref, api_list, namespace)
144      if link is None and namespace is not None:
145        # Try to resolve the ref in the current namespace if there is one.
146        link = self._GetRefLink('%s.%s' % (namespace, ref), api_list, namespace)
147      if link is None:
148        return None
149      self._object_store.Set(db_key, link)
150    else:
151      link = deepcopy(link)
152    if title is not None:
153      link['text'] = title
154    return link
155
156  def SafeGetLink(self, ref, namespace=None, title=None):
157    """Resolve $ref |ref| in namespace |namespace|, or globally if None. If it
158    cannot be resolved, pretend like it is a link to a type.
159    """
160    ref_data = self.GetLink(ref, namespace=namespace, title=title)
161    if ref_data is not None:
162      return ref_data
163    logging.error('$ref %s could not be resolved in namespace %s.' %
164        (ref, namespace))
165    type_name = ref.rsplit('.', 1)[-1]
166    return {
167      'href': '#type-%s' % type_name,
168      'text': title or ref,
169      'name': ref
170    }
171
172  # TODO(ahernandez.miralles): This function is no longer needed,
173  # and uses a deprecated style of ref
174  def ResolveAllLinks(self, text, relative_to='', namespace=None):
175    """This method will resolve all $ref links in |text| using namespace
176    |namespace| if not None. Any links that cannot be resolved will be replaced
177    using the default link format that |SafeGetLink| uses.
178    The links will be generated relative to |relative_to|.
179    """
180    if text is None or '$ref:' not in text:
181      return text
182
183    # requestPath should be of the form (apps|extensions)/...../page.html.
184    # link_prefix should  that the target will point to
185    # (apps|extensions)/target.html. Note multiplying a string by a negative
186    # number gives the empty string.
187    link_prefix = '../' * (relative_to.count('/') - 1)
188    split_text = text.split('$ref:')
189    # |split_text| is an array of text chunks that all start with the
190    # argument to '$ref:'.
191    formatted_text = [split_text[0]]
192    for ref_and_rest in split_text[1:]:
193      title = None
194      if ref_and_rest.startswith('[') and ']' in ref_and_rest:
195        # Text was '$ref:[foo.bar maybe title] other stuff'.
196        ref_with_title, rest = ref_and_rest[1:].split(']', 1)
197        ref_with_title = ref_with_title.split(None, 1)
198        if len(ref_with_title) == 1:
199          # Text was '$ref:[foo.bar] other stuff'.
200          ref = ref_with_title[0]
201        else:
202          # Text was '$ref:[foo.bar title] other stuff'.
203          ref, title = ref_with_title
204      else:
205        # Text was '$ref:foo.bar other stuff'.
206        match = self._bare_ref.match(ref_and_rest)
207        if match is None:
208          ref = ''
209          rest = ref_and_rest
210        else:
211          ref = match.group()
212          rest = ref_and_rest[match.end():]
213
214      ref_dict = self.SafeGetLink(ref, namespace=namespace, title=title)
215      formatted_text.append('<a href="%s%s">%s</a>%s' %
216          (link_prefix, ref_dict['href'], ref_dict['text'], rest))
217    return ''.join(formatted_text)
218