1#!/usr/bin/env python 2# 3# Copyright (C) 2016 Google, Inc. 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at: 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17import collections 18import itertools 19import os 20import re 21import subprocess 22 23# Parsing states: 24# STATE_INITIAL: looking for rpc or function defintion 25# STATE_RPC_DECORATOR: in the middle of a multi-line rpc definition 26# STATE_FUNCTION_DECORATOR: in the middle of a multi-line function definition 27# STATE_COMPLETE: done parsing a function 28STATE_INITIAL = 1 29STATE_RPC_DECORATOR = 2 30STATE_FUNCTION_DEFINITION = 3 31STATE_COMPLETE = 4 32 33# RE to match key=value tuples with matching quoting on value. 34KEY_VAL_RE = re.compile(r''' 35 (?P<key>\w+)\s*=\s* # Key consists of only alphanumerics 36 (?P<quote>["']?) # Optional quote character. 37 (?P<value>.*?) # Value is a non greedy match 38 (?P=quote) # Closing quote equals the first. 39 ($|,) # Entry ends with comma or end of string 40 ''', re.VERBOSE) 41 42# RE to match a function definition and extract out the function name. 43FUNC_RE = re.compile(r'.+\s+(\w+)\s*\(.*') 44 45 46class Function(object): 47 """Represents a RPC-exported function.""" 48 49 def __init__(self, rpc_def, func_def): 50 """Constructs a function object given its RPC and function signature.""" 51 self._function = '' 52 self._signature = '' 53 self._description = '' 54 self._returns = '' 55 56 self._ParseRpcDefinition(rpc_def) 57 self._ParseFunctionDefinition(func_def) 58 59 def _ParseRpcDefinition(self, s): 60 """Parse RPC definition.""" 61 # collapse string concatenation 62 s = s.replace('" + "', '') 63 s = s.strip('()') 64 for m in KEY_VAL_RE.finditer(s): 65 if m.group('key') == 'description': 66 self._description = m.group('value') 67 if m.group('key') == 'returns': 68 self._returns = m.group('value') 69 70 def _ParseFunctionDefinition(self, s): 71 """Parse function definition.""" 72 # Remove some keywords we don't care about. 73 s = s.replace('public ', '') 74 s = s.replace('synchronized ', '') 75 # Remove any throw specifications. 76 s = re.sub('\s+throws.*', '', s) 77 s = s.strip('{') 78 # Remove all the RPC parameter annotations. 79 s = s.replace('@RpcOptional ', '') 80 s = s.replace('@RpcOptional() ', '') 81 s = re.sub('@RpcParameter\s*\(.+?\)\s+', '', s) 82 s = re.sub('@RpcDefault\s*\(.+?\)\s+', '', s) 83 m = FUNC_RE.match(s) 84 if m: 85 self._function = m.group(1) 86 self._signature = s.strip() 87 88 @property 89 def function(self): 90 return self._function 91 92 @property 93 def signature(self): 94 return self._signature 95 96 @property 97 def description(self): 98 return self._description 99 100 @property 101 def returns(self): 102 return self._returns 103 104 105class DocGenerator(object): 106 """Documentation genereator.""" 107 108 def __init__(self, basepath): 109 """Construct based on all the *Facade.java files in the given basepath.""" 110 self._functions = collections.defaultdict(list) 111 112 for path, dirs, files in os.walk(basepath): 113 for f in files: 114 if f.endswith('Facade.java'): 115 self._Parse(os.path.join(path, f)) 116 117 def _Parse(self, filename): 118 """Parser state machine for a single file.""" 119 state = STATE_INITIAL 120 self._current_rpc = '' 121 self._current_function = '' 122 123 with open(filename, 'r') as f: 124 for line in f.readlines(): 125 line = line.strip() 126 if state == STATE_INITIAL: 127 state = self._ParseLineInitial(line) 128 elif state == STATE_RPC_DECORATOR: 129 state = self._ParseLineRpcDecorator(line) 130 elif state == STATE_FUNCTION_DEFINITION: 131 state = self._ParseLineFunctionDefinition(line) 132 133 if state == STATE_COMPLETE: 134 self._EmitFunction(filename) 135 state = STATE_INITIAL 136 137 def _ParseLineInitial(self, line): 138 """Parse a line while in STATE_INITIAL.""" 139 if line.startswith('@Rpc('): 140 self._current_rpc = line[4:] 141 if not line.endswith(')'): 142 # Multi-line RPC definition 143 return STATE_RPC_DECORATOR 144 elif line.startswith('public'): 145 self._current_function = line 146 if not line.endswith('{'): 147 # Multi-line function definition 148 return STATE_FUNCTION_DEFINITION 149 else: 150 return STATE_COMPLETE 151 return STATE_INITIAL 152 153 def _ParseLineRpcDecorator(self, line): 154 """Parse a line while in STATE_RPC_DECORATOR.""" 155 self._current_rpc += ' ' + line 156 if line.endswith(')'): 157 # Done with RPC definition 158 return STATE_INITIAL 159 else: 160 # Multi-line RPC definition 161 return STATE_RPC_DECORATOR 162 163 def _ParseLineFunctionDefinition(self, line): 164 """Parse a line while in STATE_FUNCTION_DEFINITION.""" 165 self._current_function += ' ' + line 166 if line.endswith('{'): 167 # Done with function definition 168 return STATE_COMPLETE 169 else: 170 # Multi-line function definition 171 return STATE_FUNCTION_DEFINITION 172 173 def _EmitFunction(self, filename): 174 """Store a function definition from the current parse state.""" 175 if self._current_rpc and self._current_function: 176 module = os.path.basename(filename)[0:-5] 177 f = Function(self._current_rpc, self._current_function) 178 if f.function: 179 self._functions[module].append(f) 180 181 self._current_rpc = None 182 self._current_function = None 183 184 def WriteOutput(self, filename): 185 git_rev = None 186 try: 187 git_rev = subprocess.check_output('git rev-parse HEAD', 188 shell=True).strip() 189 except subprocess.CalledProcessError as e: 190 # Getting the commit ID is optional; we continue if we cannot get it 191 pass 192 193 with open(filename, 'w') as f: 194 if git_rev: 195 f.write('Generated at commit `%s`\n\n' % git_rev) 196 # Write table of contents 197 for module in sorted(self._functions.keys()): 198 f.write('**%s**\n\n' % module) 199 for func in self._functions[module]: 200 f.write(' * [%s](#%s)\n' % 201 (func.function, func.function.lower())) 202 f.write('\n') 203 204 f.write('# Method descriptions\n\n') 205 for func in itertools.chain.from_iterable( 206 self._functions.itervalues()): 207 f.write('## %s\n\n' % func.function) 208 f.write('```\n') 209 f.write('%s\n\n' % func.signature) 210 f.write('%s\n' % func.description) 211 if func.returns: 212 if func.returns.lower().startswith('return'): 213 f.write('\n%s\n' % func.returns) 214 else: 215 f.write('\nReturns %s\n' % func.returns) 216 f.write('```\n\n') 217 218# Main 219basepath = os.path.abspath(os.path.join(os.path.dirname( 220 os.path.realpath(__file__)), '..')) 221g = DocGenerator(basepath) 222g.WriteOutput(os.path.join(basepath, 'Docs/ApiReference.md')) 223