Home | History | Annotate | Download | only in Docs
      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 
     17 import collections
     18 import itertools
     19 import os
     20 import re
     21 import 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
     28 STATE_INITIAL = 1
     29 STATE_RPC_DECORATOR = 2
     30 STATE_FUNCTION_DEFINITION = 3
     31 STATE_COMPLETE = 4
     32 
     33 # RE to match key=value tuples with matching quoting on value.
     34 KEY_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.
     43 FUNC_RE = re.compile(r'.+\s+(\w+)\s*\(.*')
     44 
     45 
     46 class 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 
    105 class 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
    219 basepath = os.path.abspath(os.path.join(os.path.dirname(
    220     os.path.realpath(__file__)), '..'))
    221 g = DocGenerator(basepath)
    222 g.WriteOutput(os.path.join(basepath, 'Docs/ApiReference.md'))
    223