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