| #!/usr/bin/env python |
| # |
| # Copyright (C) 2018 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 os |
| import re |
| |
| # Matches a JavaDoc comment followed by an @Rpc annotation. |
| import subprocess |
| |
| """A regex that captures the JavaDoc comment and function signature.""" |
| JAVADOC_RPC_REGEX = re.compile( |
| # Capture the entire comment string. |
| r'(?P<comment>/\*\*(?:(?!/\*\*).)*?\*/)(?:(?:(?!\*/).)*?)\s*' |
| # Find at least one @Rpc Annotation |
| r'(?:@\w+\s*)*?(?:@Rpc.*?\s*)(?:@\w+\s*)*?' |
| # Capture the function signature, ignoring the throws statement |
| # (the throws information will be pulled from the comment). |
| r'(?P<function_signature>.*?)(?:throws.*?)?{', |
| flags=re.MULTILINE | re.DOTALL) |
| |
| """ |
| Captures javadoc "frills" like the ones found below: |
| |
| /** |
| * |
| */ |
| |
| /** */ |
| |
| """ |
| CAPTURE_JAVADOC_FRILLS = re.compile( |
| r'(^\s*(/\*\*$|/\*\* |\*($| ))|\s*\*/\s*$)', |
| re.MULTILINE) |
| |
| """A regex to capture the individual pieces of the function signature.""" |
| CAPTURE_FUNCTION_SIGNATURE = re.compile( |
| # Capture any non-static function |
| r'(public|private)' |
| # Allow synchronized and @Annotations() |
| r'[( synchronized)(@\w+\(.*?\)?)]*?' |
| # Return Type (Allow n-number of generics and arrays) |
| r'(?P<return_type>\w+(?:[\[\]<>\w ,]*?)?)\s+' |
| # Capture functionName |
| r'(?P<function_name>\w*)\s*' |
| # Capture anything enclosed in parens |
| r'\((?P<parameters>.*)\)', |
| re.MULTILINE | re.DOTALL) |
| |
| """Matches a parameter and its RPC annotations.""" |
| CAPTURE_PARAMETER = re.compile( |
| r'(?:' |
| r'(?P<optional>@RpcOptional\s+)?' |
| r'(?P<rpc_param>@RpcParameter\(.*?\)\s*)?' |
| r'(?P<default>@RpcDefault\((?P<default_value>.*)\)\s*)?' |
| r')*' |
| r'(?P<param_type>\w+)\s+(?P<param_name>\w+)', |
| flags=re.MULTILINE | re.DOTALL) |
| |
| |
| class Facade(object): |
| """A class representing a Facade. |
| |
| Attributes: |
| path: the path the facade is located at. |
| directory: the |
| """ |
| |
| def __init__(self, path): |
| self.path = path |
| self.directory = os.path.dirname(self.path) |
| # -5 removes the '.java' file extension |
| self.name = path[path.rfind('/') + 1:-5] |
| self.rpcs = list() |
| |
| |
| def main(): |
| basepath = os.path.abspath(os.path.join(os.path.dirname( |
| os.path.realpath(__file__)), '..')) |
| |
| facades = list() |
| |
| for path, dirs, files in os.walk(basepath): |
| for file_name in files: |
| if file_name.endswith('Facade.java'): |
| facades.append(parse_facade_file(os.path.join(path, file_name))) |
| |
| basepath = os.path.abspath(os.path.join(os.path.dirname( |
| os.path.realpath(__file__)), '..')) |
| write_output(facades, os.path.join(basepath, 'Docs/ApiReference.md')) |
| |
| |
| def write_output(facades, output_path): |
| facades = sorted(facades, key=lambda x: x.directory) |
| |
| git_rev = None |
| try: |
| git_rev = subprocess.check_output('git rev-parse HEAD', |
| shell=True).decode('utf-8').strip() |
| except subprocess.CalledProcessError as e: |
| # Getting the commit ID is optional; we continue if we cannot get it |
| pass |
| |
| with open(output_path, 'w') as fd: |
| if git_rev: |
| fd.write('Generated at commit `%s`\n\n' % git_rev) |
| fd.write('# Facade Groups') |
| prev_directory = '' |
| for facade in facades: |
| if facade.directory != prev_directory: |
| fd.write('\n\n## %s\n\n' % facade.directory[ |
| facade.directory.rfind('/') + 1:]) |
| prev_directory = facade.directory |
| fd.write(' * [%s](#%s)\n' % (facade.name, facade.name.lower())) |
| |
| fd.write('\n# Facades\n\n') |
| for facade in facades: |
| fd.write('\n## %s' % facade.name) |
| for rpc in facade.rpcs: |
| fd.write('\n\n### %s\n\n' % rpc.name) |
| fd.write('%s\n' % rpc) |
| |
| |
| def parse_facade_file(file_path): |
| """Parses a .*Facade.java file and represents it as a Facade object""" |
| facade = Facade(file_path) |
| with open(file_path, 'r') as content_file: |
| content = content_file.read() |
| matches = re.findall(JAVADOC_RPC_REGEX, content) |
| for match in matches: |
| rpc_function = DocumentedFunction( |
| match[0].replace('\\n', '\n'), # match[0]: JavaDoc comment |
| match[1].replace('\\n', '\n')) # match[1]: function signature |
| facade.rpcs.append(rpc_function) |
| facade.rpcs.sort(key=lambda rpc: rpc.name) |
| return facade |
| |
| |
| class DefaultValue(object): |
| """An object representation of a default value. |
| |
| Functions as Optional in Java, or a pointer in C++. |
| |
| Attributes: |
| value: the default value |
| """ |
| def __init__(self, default_value=None): |
| self.value = default_value |
| |
| |
| class DocumentedValue(object): |
| def __init__(self): |
| """Creates an empty DocumentedValue object.""" |
| self.type = 'void' |
| self.name = None |
| self.description = None |
| self.default_value = None |
| |
| def set_type(self, param_type): |
| self.type = param_type |
| return self |
| |
| def set_name(self, name): |
| self.name = name |
| return self |
| |
| def set_description(self, description): |
| self.description = description |
| return self |
| |
| def set_default_value(self, default_value): |
| self.default_value = default_value |
| return self |
| |
| def __str__(self): |
| if self.name is None: |
| return self.description |
| if self.default_value is None: |
| return '%s: %s' % (self.name, self.description) |
| else: |
| return '%s: %s (default: %s)' % (self.name, self.description, |
| self.default_value.value) |
| |
| |
| class DocumentedFunction(object): |
| """A combination of all function documentation into a single object. |
| |
| Attributes: |
| _description: A string that describes the function. |
| _parameters: A dictionary of {parameter name: DocumentedValue object} |
| _return: a DocumentedValue with information on the returned value. |
| _throws: A dictionary of {throw type (str): DocumentedValue object} |
| |
| """ |
| def __init__(self, comment, function_signature): |
| self._name = None |
| self._description = None |
| self._parameters = {} |
| self._return = DocumentedValue() |
| self._throws = {} |
| |
| self._parse_comment(comment) |
| self._parse_function_signature(function_signature) |
| |
| @property |
| def name(self): |
| return self._name |
| |
| def _parse_comment(self, comment): |
| """Parses a JavaDoc comment into DocumentedFunction attributes.""" |
| comment = str(re.sub(CAPTURE_JAVADOC_FRILLS, '', comment)) |
| tag = 'description' |
| tag_data = '' |
| for line in comment.split('\n'): |
| line.strip() |
| if line.startswith('@'): |
| self._finalize_tag(tag, tag_data) |
| tag_end_index = line.find(' ') |
| tag = line[1:tag_end_index] |
| tag_data = line[tag_end_index + 1:] |
| else: |
| if not tag_data: |
| whitespace_char = '' |
| elif (line.startswith(' ') |
| or tag_data.endswith('\n') |
| or line == ''): |
| whitespace_char = '\n' |
| else: |
| whitespace_char = ' ' |
| tag_data = '%s%s%s' % (tag_data, whitespace_char, line) |
| self._finalize_tag(tag, tag_data.strip()) |
| |
| def __str__(self): |
| params_signature = ', '.join(['%s %s' % (param.type, param.name) |
| for param in self._parameters.values()]) |
| params_description = '\n '.join(['%s: %s' % (param.name, |
| param.description) |
| for param in |
| self._parameters.values()]) |
| if params_description: |
| params_description = ('\n**Parameters:**\n\n %s\n' % |
| params_description) |
| return_description = '\n' if self._return else '' |
| if self._return: |
| return_description += ('**Returns:**\n\n %s' % |
| self._return.description) |
| return ( |
| # ReturnType functionName(Parameters) |
| '%s %s(%s)\n\n' |
| # Description |
| '%s\n' |
| # Params & Return |
| '%s%s' % (self._return.type, self._name, |
| params_signature, self._description, |
| params_description, return_description)).strip() |
| |
| def _parse_function_signature(self, function_signature): |
| """Parses the function signature into DocumentedFunction attributes.""" |
| header_match = re.search(CAPTURE_FUNCTION_SIGNATURE, function_signature) |
| self._name = header_match.group('function_name') |
| self._return.set_type(header_match.group('return_type')) |
| |
| for match in re.finditer(CAPTURE_PARAMETER, |
| header_match.group('parameters')): |
| param_name = match.group('param_name') |
| param_type = match.group('param_type') |
| if match.group('default_value'): |
| default = DefaultValue(match.group('default_value')) |
| elif match.group('optional'): |
| default = DefaultValue(None) |
| else: |
| default = None |
| |
| if param_name in self._parameters: |
| param = self._parameters[param_name] |
| else: |
| param = DocumentedValue() |
| param.set_type(param_type) |
| param.set_name(param_name) |
| param.set_default_value(default) |
| |
| def _finalize_tag(self, tag, tag_data): |
| """Finalize the JavaDoc @tag by adding it to the correct field.""" |
| name = tag_data[:tag_data.find(' ')] |
| description = tag_data[tag_data.find(' ') + 1:].strip() |
| if tag == 'description': |
| self._description = tag_data |
| elif tag == 'param': |
| if name in self._parameters: |
| param = self._parameters[name] |
| else: |
| param = DocumentedValue().set_name(name) |
| self._parameters[name] = param |
| param.set_description(description) |
| elif tag == 'return': |
| self._return.set_description(tag_data) |
| elif tag == 'throws': |
| new_throws = DocumentedValue().set_name(name) |
| new_throws.set_description(description) |
| self._throws[name] = new_throws |
| |
| |
| if __name__ == '__main__': |
| main() |