[READ ONLY MIRROR] Envoy REST/proto API definitions and documentation. (grpc依赖)
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.

292 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())