You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
223 lines
7.8 KiB
223 lines
7.8 KiB
#!/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'))
|