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