• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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