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.
291 lines
8.8 KiB
291 lines
8.8 KiB
# protoc plugin to map from FileDescriptorProtos to Envoy doc style RST. |
|
# See https://github.com/google/protobuf/blob/master/src/google/protobuf/descriptor.proto |
|
# for the underlying protos mentioned in this file. |
|
|
|
import functools |
|
import sys |
|
|
|
from google.protobuf.compiler import plugin_pb2 |
|
|
|
# Namespace prefix for Envoy APIs. |
|
ENVOY_API_NAMESPACE_PREFIX = '.envoy.api.v2.' |
|
|
|
|
|
class ProtodocError(Exception): |
|
"""Base error class for the protodoc module.""" |
|
|
|
|
|
class SourceCodeInfo(object): |
|
"""Wrapper for SourceCodeInfo proto.""" |
|
|
|
def __init__(self, source_code_info): |
|
self._proto = source_code_info |
|
|
|
@property |
|
def file_level_comment(self): |
|
"""Obtain inferred file level comment.""" |
|
comment = '' |
|
earliest_detached_comment = max( |
|
max(location.span) for location in self._proto.location) |
|
for location in self._proto.location: |
|
if location.leading_detached_comments and location.span[0] < earliest_detached_comment: |
|
comment = StripLeadingSpace(''.join( |
|
location.leading_detached_comments)) + '\n' |
|
earliest_detached_comment = location.span[0] |
|
return comment |
|
|
|
def LeadingCommentPathLookup(self, path): |
|
"""Lookup leading comment by path in SourceCodeInfo. |
|
|
|
Args: |
|
path: a list of path indexes as per |
|
https://github.com/google/protobuf/blob/a08b03d4c00a5793b88b494f672513f6ad46a681/src/google/protobuf/descriptor.proto#L717. |
|
Returns: |
|
Attached leading comment if it exists, otherwise empty space. |
|
""" |
|
for location in self._proto.location: |
|
if location.path == path: |
|
return StripLeadingSpace(location.leading_comments) + '\n' |
|
return '' |
|
|
|
|
|
def MapLines(f, s): |
|
"""Apply a function across each line in a flat string. |
|
|
|
Args: |
|
f: A string transform function for a line. |
|
s: A string consisting of potentially multiple lines. |
|
Returns: |
|
A flat string with f applied to each line. |
|
""" |
|
return '\n'.join(f(line) for line in s.split('\n')) |
|
|
|
|
|
def Indent(spaces, line): |
|
"""Indent a string.""" |
|
return ' ' * spaces + line |
|
|
|
|
|
def IndentLines(spaces, lines): |
|
"""Indent a list of strings.""" |
|
return map(functools.partial(Indent, spaces), lines) |
|
|
|
|
|
def FormatHeader(style, text): |
|
"""Format RST header. |
|
|
|
Args: |
|
style: underline style, e.g. '=', '-'. |
|
text: header text |
|
Returns: |
|
RST formatted header. |
|
""" |
|
return '%s\n%s\n\n' % (text, style * len(text)) |
|
|
|
|
|
def FormatFieldTypeAsJson(field): |
|
"""Format FieldDescriptorProto.Type as a pseudo-JSON string. |
|
|
|
Args: |
|
field: FieldDescriptor proto. |
|
Return: |
|
RST formatted pseudo-JSON string representation of field type. |
|
""" |
|
if field.label == field.LABEL_REPEATED: |
|
return '[]' |
|
if field.type == field.TYPE_MESSAGE: |
|
return '"{...}"' |
|
return '"..."' |
|
|
|
|
|
def FormatMessageAsJson(msg): |
|
"""Format a message definition DescriptorProto as a pseudo-JSON block. |
|
|
|
Args: |
|
msg: message definition DescriptorProto. |
|
Return: |
|
RST formatted pseudo-JSON string representation of message definition. |
|
""" |
|
lines = ['"%s": %s' % (f.name, FormatFieldTypeAsJson(f)) for f in msg.field] |
|
return '.. code-block:: json\n\n {\n' + ',\n'.join(IndentLines( |
|
4, lines)) + '\n }\n\n' |
|
|
|
|
|
def NormalizeFQN(fqn): |
|
"""Normalize a fully qualified field type name. |
|
|
|
Strips leading ENVOY_API_NAMESPACE_PREFIX and makes pretty wrapped type names. |
|
|
|
Args: |
|
fqn: a fully qualified type name from FieldDescriptorProto.type_name. |
|
Return: |
|
Normalized type name. |
|
""" |
|
if fqn.startswith(ENVOY_API_NAMESPACE_PREFIX): |
|
return fqn[len(ENVOY_API_NAMESPACE_PREFIX):] |
|
|
|
def Wrapped(s): |
|
return '{%s}' % s |
|
|
|
remap_fqn = { |
|
'.google.protobuf.UInt32Value': Wrapped('uint32'), |
|
'.google.protobuf.UInt64Value': Wrapped('uint64'), |
|
'.google.protobuf.BoolValue': Wrapped('bool'), |
|
} |
|
if fqn in remap_fqn: |
|
return remap_fqn[fqn] |
|
|
|
return fqn |
|
|
|
|
|
def FormatEmph(s): |
|
"""RST format a string for emphasis.""" |
|
return '*%s*' % s |
|
|
|
|
|
def FormatFieldType(field): |
|
"""Format a FieldDescriptorProto type description. |
|
|
|
Adds cross-refs for message types. |
|
TODO(htuch): Add cross-refs for enums as well. |
|
|
|
Args: |
|
field: FieldDescriptor proto. |
|
Return: |
|
RST formatted field type. |
|
""" |
|
if field.type == field.TYPE_MESSAGE and field.type_name.startswith( |
|
ENVOY_API_NAMESPACE_PREFIX): |
|
type_name = NormalizeFQN(field.type_name) |
|
return ':ref:`%s <%s>`' % (type_name, MessageCrossRefLabel(type_name)) |
|
# TODO(htuch): Replace with enum handling. |
|
if field.type_name: |
|
return FormatEmph(NormalizeFQN(field.type_name)) |
|
pretty_type_names = { |
|
field.TYPE_DOUBLE: 'double', |
|
field.TYPE_FLOAT: 'float', |
|
field.TYPE_INT32: 'int32', |
|
field.TYPE_UINT32: 'uint32', |
|
field.TYPE_INT64: 'int64', |
|
field.TYPE_UINT64: 'uint64', |
|
field.TYPE_BOOL: 'bool', |
|
field.TYPE_STRING: 'string', |
|
field.TYPE_BYTES: 'bytes', |
|
} |
|
if field.type in pretty_type_names: |
|
return FormatEmph(pretty_type_names[field.type]) |
|
raise ProtodocError('Unknown field type ' + str(field.type)) |
|
|
|
|
|
def StripLeadingSpace(s): |
|
"""Remove leading space in flat comment strings.""" |
|
return MapLines(lambda s: s[1:], s) |
|
|
|
|
|
def MessageCrossRefLabel(msg_name): |
|
"""Message cross reference label.""" |
|
return 'envoy_api_%s' % msg_name |
|
|
|
|
|
def FieldCrossRefLabel(msg_name, field_name): |
|
"""Field cross reference label.""" |
|
return 'envoy_api_%s_%s' % (msg_name, field_name) |
|
|
|
|
|
def FormatAnchor(label): |
|
"""Format a label as an Envoy API RST anchor.""" |
|
return '.. _%s:\n\n' % label |
|
|
|
|
|
def FormatFieldAsDefinitionListItem(source_code_info, msg, path, field): |
|
"""Format a FieldDescriptorProto as RST definition list item. |
|
|
|
Args: |
|
source_code_info: SourceCodeInfo object. |
|
msg: MessageDescriptorProto. |
|
path: a list of path indexes as per |
|
https://github.com/google/protobuf/blob/a08b03d4c00a5793b88b494f672513f6ad46a681/src/google/protobuf/descriptor.proto#L717. |
|
field: FieldDescriptorProto. |
|
Returns: |
|
RST formatted definition list item. |
|
""" |
|
anchor = FormatAnchor(FieldCrossRefLabel(msg.name, field.name)) |
|
comment = '(%s) ' % FormatFieldType( |
|
field) + source_code_info.LeadingCommentPathLookup(path) |
|
return anchor + field.name + '\n' + MapLines( |
|
functools.partial(Indent, 2), comment) |
|
|
|
|
|
def FormatMessageAsDefinitionList(source_code_info, path, msg): |
|
"""Format a MessageDescriptorProto as RST definition list. |
|
|
|
Args: |
|
source_code_info: SourceCodeInfo object. |
|
path: a list of path indexes as per |
|
https://github.com/google/protobuf/blob/a08b03d4c00a5793b88b494f672513f6ad46a681/src/google/protobuf/descriptor.proto#L717. |
|
msg: MessageDescriptorProto. |
|
Returns: |
|
RST formatted definition list item. |
|
""" |
|
return '\n\n'.join( |
|
FormatFieldAsDefinitionListItem(source_code_info, msg, path + [2, index], |
|
field) |
|
for index, field in enumerate(msg.field)) + '\n' |
|
|
|
|
|
def FormatMessage(source_code_info, path, msg): |
|
"""Format a MessageDescriptorProto as RST section. |
|
|
|
Args: |
|
source_code_info: SourceCodeInfo object. |
|
path: a list of path indexes as per |
|
https://github.com/google/protobuf/blob/a08b03d4c00a5793b88b494f672513f6ad46a681/src/google/protobuf/descriptor.proto#L717. |
|
msg: MessageDescriptorProto. |
|
Returns: |
|
RST formatted section. |
|
""" |
|
anchor = FormatAnchor(MessageCrossRefLabel(msg.name)) |
|
header = FormatHeader('-', msg.name) |
|
comment = source_code_info.LeadingCommentPathLookup(path) |
|
return anchor + header + comment + FormatMessageAsJson( |
|
msg) + FormatMessageAsDefinitionList(source_code_info, path, msg) |
|
|
|
|
|
def FormatProtoAsBlockComment(proto): |
|
"""Format as RST a proto as a block comment. |
|
|
|
Useful in debugging, not usually referenced. |
|
""" |
|
return '\n\nproto::\n\n' + MapLines(functools.partial(Indent, 2), |
|
str(proto)) + '\n' |
|
|
|
|
|
def GenerateRst(proto_file): |
|
"""Generate a RST representation from a FileDescriptor proto. |
|
|
|
""" |
|
header = FormatHeader('=', proto_file.name) |
|
source_code_info = SourceCodeInfo(proto_file.source_code_info) |
|
# Find the earliest detached comment, attribute it to file level. |
|
comment = source_code_info.file_level_comment |
|
msgs = '\n'.join( |
|
FormatMessage(source_code_info, [4, index], msg) |
|
for index, msg in enumerate(proto_file.message_type)) |
|
#debug_proto = FormatProtoAsBlockComment(proto_file.source_code_info) |
|
return header + comment + msgs #+ debug_proto |
|
|
|
|
|
if __name__ == '__main__': |
|
# http://www.expobrain.net/2015/09/13/create-a-plugin-for-google-protocol-buffer/ |
|
request = plugin_pb2.CodeGeneratorRequest() |
|
request.ParseFromString(sys.stdin.read()) |
|
response = plugin_pb2.CodeGeneratorResponse() |
|
|
|
for proto_file in request.proto_file: |
|
f = response.file.add() |
|
f.name = proto_file.name + '.rst' |
|
# We don't actually generate any RST right now, we just string dump the |
|
# input proto file descriptor into the output file. |
|
f.content = GenerateRst(proto_file) |
|
|
|
sys.stdout.write(response.SerializeToString())
|
|
|