#!/usr/bin/env python # # Copyright (C) 2016 Google, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at: # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import collections import itertools import os import re import subprocess # Parsing states: # STATE_INITIAL: looking for rpc or function defintion # STATE_RPC_DECORATOR: in the middle of a multi-line rpc definition # STATE_FUNCTION_DECORATOR: in the middle of a multi-line function definition # STATE_COMPLETE: done parsing a function STATE_INITIAL = 1 STATE_RPC_DECORATOR = 2 STATE_FUNCTION_DEFINITION = 3 STATE_COMPLETE = 4 # RE to match key=value tuples with matching quoting on value. KEY_VAL_RE = re.compile(r''' (?P\w+)\s*=\s* # Key consists of only alphanumerics (?P["']?) # Optional quote character. (?P.*?) # Value is a non greedy match (?P=quote) # Closing quote equals the first. ($|,) # Entry ends with comma or end of string ''', re.VERBOSE) # RE to match a function definition and extract out the function name. FUNC_RE = re.compile(r'.+\s+(\w+)\s*\(.*') class Function(object): """Represents a RPC-exported function.""" def __init__(self, rpc_def, func_def): """Constructs a function object given its RPC and function signature.""" self._function = '' self._signature = '' self._description = '' self._returns = '' self._ParseRpcDefinition(rpc_def) self._ParseFunctionDefinition(func_def) def _ParseRpcDefinition(self, s): """Parse RPC definition.""" # collapse string concatenation s = s.replace('" + "', '') s = s.strip('()') for m in KEY_VAL_RE.finditer(s): if m.group('key') == 'description': self._description = m.group('value') if m.group('key') == 'returns': self._returns = m.group('value') def _ParseFunctionDefinition(self, s): """Parse function definition.""" # Remove some keywords we don't care about. s = s.replace('public ', '') s = s.replace('synchronized ', '') # Remove any throw specifications. s = re.sub('\s+throws.*', '', s) s = s.strip('{') # Remove all the RPC parameter annotations. s = s.replace('@RpcOptional ', '') s = s.replace('@RpcOptional() ', '') s = re.sub('@RpcParameter\s*\(.+?\)\s+', '', s) s = re.sub('@RpcDefault\s*\(.+?\)\s+', '', s) m = FUNC_RE.match(s) if m: self._function = m.group(1) self._signature = s.strip() @property def function(self): return self._function @property def signature(self): return self._signature @property def description(self): return self._description @property def returns(self): return self._returns class DocGenerator(object): """Documentation genereator.""" def __init__(self, basepath): """Construct based on all the *Facade.java files in the given basepath.""" self._functions = collections.defaultdict(list) for path, dirs, files in os.walk(basepath): for f in files: if f.endswith('Facade.java'): self._Parse(os.path.join(path, f)) def _Parse(self, filename): """Parser state machine for a single file.""" state = STATE_INITIAL self._current_rpc = '' self._current_function = '' with open(filename, 'r') as f: for line in f.readlines(): line = line.strip() if state == STATE_INITIAL: state = self._ParseLineInitial(line) elif state == STATE_RPC_DECORATOR: state = self._ParseLineRpcDecorator(line) elif state == STATE_FUNCTION_DEFINITION: state = self._ParseLineFunctionDefinition(line) if state == STATE_COMPLETE: self._EmitFunction(filename) state = STATE_INITIAL def _ParseLineInitial(self, line): """Parse a line while in STATE_INITIAL.""" if line.startswith('@Rpc('): self._current_rpc = line[4:] if not line.endswith(')'): # Multi-line RPC definition return STATE_RPC_DECORATOR elif line.startswith('public'): self._current_function = line if not line.endswith('{'): # Multi-line function definition return STATE_FUNCTION_DEFINITION else: return STATE_COMPLETE return STATE_INITIAL def _ParseLineRpcDecorator(self, line): """Parse a line while in STATE_RPC_DECORATOR.""" self._current_rpc += ' ' + line if line.endswith(')'): # Done with RPC definition return STATE_INITIAL else: # Multi-line RPC definition return STATE_RPC_DECORATOR def _ParseLineFunctionDefinition(self, line): """Parse a line while in STATE_FUNCTION_DEFINITION.""" self._current_function += ' ' + line if line.endswith('{'): # Done with function definition return STATE_COMPLETE else: # Multi-line function definition return STATE_FUNCTION_DEFINITION def _EmitFunction(self, filename): """Store a function definition from the current parse state.""" if self._current_rpc and self._current_function: module = os.path.basename(filename)[0:-5] f = Function(self._current_rpc, self._current_function) if f.function: self._functions[module].append(f) self._current_rpc = None self._current_function = None def WriteOutput(self, filename): git_rev = None try: git_rev = subprocess.check_output('git rev-parse HEAD', shell=True).strip() except subprocess.CalledProcessError as e: # Getting the commit ID is optional; we continue if we cannot get it pass with open(filename, 'w') as f: if git_rev: f.write('Generated at commit `%s`\n\n' % git_rev) # Write table of contents for module in sorted(self._functions.keys()): f.write('**%s**\n\n' % module) for func in self._functions[module]: f.write(' * [%s](#%s)\n' % (func.function, func.function.lower())) f.write('\n') f.write('# Method descriptions\n\n') for func in itertools.chain.from_iterable( self._functions.itervalues()): f.write('## %s\n\n' % func.function) f.write('```\n') f.write('%s\n\n' % func.signature) f.write('%s\n' % func.description) if func.returns: if func.returns.lower().startswith('return'): f.write('\n%s\n' % func.returns) else: f.write('\nReturns %s\n' % func.returns) f.write('```\n\n') # Main basepath = os.path.abspath(os.path.join(os.path.dirname( os.path.realpath(__file__)), '..')) g = DocGenerator(basepath) g.WriteOutput(os.path.join(basepath, 'Docs/ApiReference.md'))