blob: 8c776696e262d8c6f71409efa01a594c7a9d443a [file] [log] [blame]
#!/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'))