| #!/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<key>\w+)\s*=\s* # Key consists of only alphanumerics |
| (?P<quote>["']?) # Optional quote character. |
| (?P<value>.*?) # 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')) |