blob: 1a9803dc88d99473e893c20e446cde64b71c8f08 [file] [log] [blame]
/*
Copyright 2015 The Kubernetes Authors.
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.
*/
package protobuf
import (
"fmt"
"io"
"log"
"reflect"
"sort"
"strconv"
"strings"
"k8s.io/klog"
"k8s.io/gengo/generator"
"k8s.io/gengo/namer"
"k8s.io/gengo/types"
)
// genProtoIDL produces a .proto IDL.
type genProtoIDL struct {
generator.DefaultGen
localPackage types.Name
localGoPackage types.Name
imports namer.ImportTracker
generateAll bool
omitGogo bool
omitFieldTypes map[types.Name]struct{}
}
func (g *genProtoIDL) PackageVars(c *generator.Context) []string {
if g.omitGogo {
return []string{
fmt.Sprintf("option go_package = %q;", g.localGoPackage.Name),
}
}
return []string{
"option (gogoproto.marshaler_all) = true;",
"option (gogoproto.stable_marshaler_all) = true;",
"option (gogoproto.sizer_all) = true;",
"option (gogoproto.goproto_stringer_all) = false;",
"option (gogoproto.stringer_all) = true;",
"option (gogoproto.unmarshaler_all) = true;",
"option (gogoproto.goproto_unrecognized_all) = false;",
"option (gogoproto.goproto_enum_prefix_all) = false;",
"option (gogoproto.goproto_getters_all) = false;",
fmt.Sprintf("option go_package = %q;", g.localGoPackage.Name),
}
}
func (g *genProtoIDL) Filename() string { return g.OptionalName + ".proto" }
func (g *genProtoIDL) FileType() string { return "protoidl" }
func (g *genProtoIDL) Namers(c *generator.Context) namer.NameSystems {
return namer.NameSystems{
// The local namer returns the correct protobuf name for a proto type
// in the context of a package
"local": localNamer{g.localPackage},
}
}
// Filter ignores types that are identified as not exportable.
func (g *genProtoIDL) Filter(c *generator.Context, t *types.Type) bool {
tagVals := types.ExtractCommentTags("+", t.CommentLines)["protobuf"]
if tagVals != nil {
if tagVals[0] == "false" {
// Type specified "false".
return false
}
if tagVals[0] == "true" {
// Type specified "true".
return true
}
klog.Fatalf(`Comment tag "protobuf" must be true or false, found: %q`, tagVals[0])
}
if !g.generateAll {
// We're not generating everything.
return false
}
seen := map[*types.Type]bool{}
ok := isProtoable(seen, t)
return ok
}
func isProtoable(seen map[*types.Type]bool, t *types.Type) bool {
if seen[t] {
// be optimistic in the case of type cycles.
return true
}
seen[t] = true
switch t.Kind {
case types.Builtin:
return true
case types.Alias:
return isProtoable(seen, t.Underlying)
case types.Slice, types.Pointer:
return isProtoable(seen, t.Elem)
case types.Map:
return isProtoable(seen, t.Key) && isProtoable(seen, t.Elem)
case types.Struct:
if len(t.Members) == 0 {
return true
}
for _, m := range t.Members {
if isProtoable(seen, m.Type) {
return true
}
}
return false
case types.Func, types.Chan:
return false
case types.DeclarationOf, types.Unknown, types.Unsupported:
return false
case types.Interface:
return false
default:
log.Printf("WARNING: type %q is not portable: %s", t.Kind, t.Name)
return false
}
}
// isOptionalAlias should return true if the specified type has an underlying type
// (is an alias) of a map or slice and has the comment tag protobuf.nullable=true,
// indicating that the type should be nullable in protobuf.
func isOptionalAlias(t *types.Type) bool {
if t.Underlying == nil || (t.Underlying.Kind != types.Map && t.Underlying.Kind != types.Slice) {
return false
}
if extractBoolTagOrDie("protobuf.nullable", t.CommentLines) == false {
return false
}
return true
}
func (g *genProtoIDL) Imports(c *generator.Context) (imports []string) {
lines := []string{}
// TODO: this could be expressed more cleanly
for _, line := range g.imports.ImportLines() {
if g.omitGogo && line == "github.com/gogo/protobuf/gogoproto/gogo.proto" {
continue
}
lines = append(lines, line)
}
return lines
}
// GenerateType makes the body of a file implementing a set for type t.
func (g *genProtoIDL) GenerateType(c *generator.Context, t *types.Type, w io.Writer) error {
sw := generator.NewSnippetWriter(w, c, "$", "$")
b := bodyGen{
locator: &protobufLocator{
namer: c.Namers["proto"].(ProtobufFromGoNamer),
tracker: g.imports,
universe: c.Universe,
localGoPackage: g.localGoPackage.Package,
},
localPackage: g.localPackage,
omitGogo: g.omitGogo,
omitFieldTypes: g.omitFieldTypes,
t: t,
}
switch t.Kind {
case types.Alias:
return b.doAlias(sw)
case types.Struct:
return b.doStruct(sw)
default:
return b.unknown(sw)
}
}
// ProtobufFromGoNamer finds the protobuf name of a type (and its package, and
// the package path) from its Go name.
type ProtobufFromGoNamer interface {
GoNameToProtoName(name types.Name) types.Name
}
type ProtobufLocator interface {
ProtoTypeFor(t *types.Type) (*types.Type, error)
GoTypeForName(name types.Name) *types.Type
CastTypeName(name types.Name) string
}
type protobufLocator struct {
namer ProtobufFromGoNamer
tracker namer.ImportTracker
universe types.Universe
localGoPackage string
}
// CastTypeName returns the cast type name of a Go type
// TODO: delegate to a new localgo namer?
func (p protobufLocator) CastTypeName(name types.Name) string {
if name.Package == p.localGoPackage {
return name.Name
}
return name.String()
}
func (p protobufLocator) GoTypeForName(name types.Name) *types.Type {
if len(name.Package) == 0 {
name.Package = p.localGoPackage
}
return p.universe.Type(name)
}
// ProtoTypeFor locates a Protobuf type for the provided Go type (if possible).
func (p protobufLocator) ProtoTypeFor(t *types.Type) (*types.Type, error) {
switch {
// we've already converted the type, or it's a map
case t.Kind == types.Protobuf || t.Kind == types.Map:
p.tracker.AddType(t)
return t, nil
}
// it's a fundamental type
if t, ok := isFundamentalProtoType(t); ok {
p.tracker.AddType(t)
return t, nil
}
// it's a message
if t.Kind == types.Struct || isOptionalAlias(t) {
t := &types.Type{
Name: p.namer.GoNameToProtoName(t.Name),
Kind: types.Protobuf,
CommentLines: t.CommentLines,
}
p.tracker.AddType(t)
return t, nil
}
return nil, errUnrecognizedType
}
type bodyGen struct {
locator ProtobufLocator
localPackage types.Name
omitGogo bool
omitFieldTypes map[types.Name]struct{}
t *types.Type
}
func (b bodyGen) unknown(sw *generator.SnippetWriter) error {
return fmt.Errorf("not sure how to generate: %#v", b.t)
}
func (b bodyGen) doAlias(sw *generator.SnippetWriter) error {
if !isOptionalAlias(b.t) {
return nil
}
var kind string
switch b.t.Underlying.Kind {
case types.Map:
kind = "map"
default:
kind = "slice"
}
optional := &types.Type{
Name: b.t.Name,
Kind: types.Struct,
CommentLines: b.t.CommentLines,
SecondClosestCommentLines: b.t.SecondClosestCommentLines,
Members: []types.Member{
{
Name: "Items",
CommentLines: []string{fmt.Sprintf("items, if empty, will result in an empty %s\n", kind)},
Type: b.t.Underlying,
},
},
}
nested := b
nested.t = optional
return nested.doStruct(sw)
}
func (b bodyGen) doStruct(sw *generator.SnippetWriter) error {
if len(b.t.Name.Name) == 0 {
return nil
}
if namer.IsPrivateGoName(b.t.Name.Name) {
return nil
}
var alias *types.Type
var fields []protoField
options := []string{}
allOptions := types.ExtractCommentTags("+", b.t.CommentLines)
for k, v := range allOptions {
switch {
case strings.HasPrefix(k, "protobuf.options."):
key := strings.TrimPrefix(k, "protobuf.options.")
switch key {
case "marshal":
if v[0] == "false" {
if !b.omitGogo {
options = append(options,
"(gogoproto.marshaler) = false",
"(gogoproto.unmarshaler) = false",
"(gogoproto.sizer) = false",
)
}
}
default:
if !b.omitGogo || !strings.HasPrefix(key, "(gogoproto.") {
if key == "(gogoproto.goproto_stringer)" && v[0] == "false" {
options = append(options, "(gogoproto.stringer) = false")
}
options = append(options, fmt.Sprintf("%s = %s", key, v[0]))
}
}
// protobuf.as allows a type to have the same message contents as another Go type
case k == "protobuf.as":
fields = nil
if alias = b.locator.GoTypeForName(types.Name{Name: v[0]}); alias == nil {
return fmt.Errorf("type %v references alias %q which does not exist", b.t, v[0])
}
// protobuf.embed instructs the generator to use the named type in this package
// as an embedded message.
case k == "protobuf.embed":
fields = []protoField{
{
Tag: 1,
Name: v[0],
Type: &types.Type{
Name: types.Name{
Name: v[0],
Package: b.localPackage.Package,
Path: b.localPackage.Path,
},
},
},
}
}
}
if alias == nil {
alias = b.t
}
// If we don't explicitly embed anything, generate fields by traversing fields.
if fields == nil {
memberFields, err := membersToFields(b.locator, alias, b.localPackage, b.omitFieldTypes)
if err != nil {
return fmt.Errorf("type %v cannot be converted to protobuf: %v", b.t, err)
}
fields = memberFields
}
out := sw.Out()
genComment(out, b.t.CommentLines, "")
sw.Do(`message $.Name.Name$ {
`, b.t)
if len(options) > 0 {
sort.Sort(sort.StringSlice(options))
for _, s := range options {
fmt.Fprintf(out, " option %s;\n", s)
}
fmt.Fprintln(out)
}
for i, field := range fields {
genComment(out, field.CommentLines, " ")
fmt.Fprintf(out, " ")
switch {
case field.Map:
case field.Repeated:
fmt.Fprintf(out, "repeated ")
case field.Required:
fmt.Fprintf(out, "required ")
default:
fmt.Fprintf(out, "optional ")
}
sw.Do(`$.Type|local$ $.Name$ = $.Tag$`, field)
if len(field.Extras) > 0 {
extras := []string{}
for k, v := range field.Extras {
if b.omitGogo && strings.HasPrefix(k, "(gogoproto.") {
continue
}
extras = append(extras, fmt.Sprintf("%s = %s", k, v))
}
sort.Sort(sort.StringSlice(extras))
if len(extras) > 0 {
fmt.Fprintf(out, " [")
fmt.Fprint(out, strings.Join(extras, ", "))
fmt.Fprintf(out, "]")
}
}
fmt.Fprintf(out, ";\n")
if i != len(fields)-1 {
fmt.Fprintf(out, "\n")
}
}
fmt.Fprintf(out, "}\n\n")
return nil
}
type protoField struct {
LocalPackage types.Name
Tag int
Name string
Type *types.Type
Map bool
Repeated bool
Optional bool
Required bool
Nullable bool
Extras map[string]string
CommentLines []string
}
var (
errUnrecognizedType = fmt.Errorf("did not recognize the provided type")
)
func isFundamentalProtoType(t *types.Type) (*types.Type, bool) {
// TODO: when we enable proto3, also include other fundamental types in the google.protobuf package
// switch {
// case t.Kind == types.Struct && t.Name == types.Name{Package: "time", Name: "Time"}:
// return &types.Type{
// Kind: types.Protobuf,
// Name: types.Name{Path: "google/protobuf/timestamp.proto", Package: "google.protobuf", Name: "Timestamp"},
// }, true
// }
switch t.Kind {
case types.Slice:
if t.Elem.Name.Name == "byte" && len(t.Elem.Name.Package) == 0 {
return &types.Type{Name: types.Name{Name: "bytes"}, Kind: types.Protobuf}, true
}
case types.Builtin:
switch t.Name.Name {
case "string", "uint32", "int32", "uint64", "int64", "bool":
return &types.Type{Name: types.Name{Name: t.Name.Name}, Kind: types.Protobuf}, true
case "int":
return &types.Type{Name: types.Name{Name: "int64"}, Kind: types.Protobuf}, true
case "uint":
return &types.Type{Name: types.Name{Name: "uint64"}, Kind: types.Protobuf}, true
case "float64", "float":
return &types.Type{Name: types.Name{Name: "double"}, Kind: types.Protobuf}, true
case "float32":
return &types.Type{Name: types.Name{Name: "float"}, Kind: types.Protobuf}, true
case "uintptr":
return &types.Type{Name: types.Name{Name: "uint64"}, Kind: types.Protobuf}, true
}
// TODO: complex?
}
return t, false
}
func memberTypeToProtobufField(locator ProtobufLocator, field *protoField, t *types.Type) error {
var err error
switch t.Kind {
case types.Protobuf:
field.Type, err = locator.ProtoTypeFor(t)
case types.Builtin:
field.Type, err = locator.ProtoTypeFor(t)
case types.Map:
valueField := &protoField{}
if err := memberTypeToProtobufField(locator, valueField, t.Elem); err != nil {
return err
}
keyField := &protoField{}
if err := memberTypeToProtobufField(locator, keyField, t.Key); err != nil {
return err
}
// All other protobuf types have kind types.Protobuf, so setting types.Map
// here would be very misleading.
field.Type = &types.Type{
Kind: types.Protobuf,
Key: keyField.Type,
Elem: valueField.Type,
}
if !strings.HasPrefix(t.Name.Name, "map[") {
field.Extras["(gogoproto.casttype)"] = strconv.Quote(locator.CastTypeName(t.Name))
}
if k, ok := keyField.Extras["(gogoproto.casttype)"]; ok {
field.Extras["(gogoproto.castkey)"] = k
}
if v, ok := valueField.Extras["(gogoproto.casttype)"]; ok {
field.Extras["(gogoproto.castvalue)"] = v
}
field.Map = true
case types.Pointer:
if err := memberTypeToProtobufField(locator, field, t.Elem); err != nil {
return err
}
field.Nullable = true
case types.Alias:
if isOptionalAlias(t) {
field.Type, err = locator.ProtoTypeFor(t)
field.Nullable = true
} else {
if err := memberTypeToProtobufField(locator, field, t.Underlying); err != nil {
log.Printf("failed to alias: %s %s: err %v", t.Name, t.Underlying.Name, err)
return err
}
// If this is not an alias to a slice, cast to the alias
if !field.Repeated {
if field.Extras == nil {
field.Extras = make(map[string]string)
}
field.Extras["(gogoproto.casttype)"] = strconv.Quote(locator.CastTypeName(t.Name))
}
}
case types.Slice:
if t.Elem.Name.Name == "byte" && len(t.Elem.Name.Package) == 0 {
field.Type = &types.Type{Name: types.Name{Name: "bytes"}, Kind: types.Protobuf}
return nil
}
if err := memberTypeToProtobufField(locator, field, t.Elem); err != nil {
return err
}
field.Repeated = true
case types.Struct:
if len(t.Name.Name) == 0 {
return errUnrecognizedType
}
field.Type, err = locator.ProtoTypeFor(t)
field.Nullable = false
default:
return errUnrecognizedType
}
return err
}
// protobufTagToField extracts information from an existing protobuf tag
func protobufTagToField(tag string, field *protoField, m types.Member, t *types.Type, localPackage types.Name) error {
if len(tag) == 0 || tag == "-" {
return nil
}
// protobuf:"bytes,3,opt,name=Id,customtype=github.com/gogo/protobuf/test.Uuid"
parts := strings.Split(tag, ",")
if len(parts) < 3 {
return fmt.Errorf("member %q of %q malformed 'protobuf' tag, not enough segments\n", m.Name, t.Name)
}
protoTag, err := strconv.Atoi(parts[1])
if err != nil {
return fmt.Errorf("member %q of %q malformed 'protobuf' tag, field ID is %q which is not an integer: %v\n", m.Name, t.Name, parts[1], err)
}
field.Tag = protoTag
// In general there is doesn't make sense to parse the protobuf tags to get the type,
// as all auto-generated once will have wire type "bytes", "varint" or "fixed64".
// However, sometimes we explicitly set them to have a custom serialization, e.g.:
// type Time struct {
// time.Time `protobuf:"Timestamp,1,req,name=time"`
// }
// to force the generator to use a given type (that we manually wrote serialization &
// deserialization methods for).
switch parts[0] {
case "varint", "fixed32", "fixed64", "bytes", "group":
default:
name := types.Name{}
if last := strings.LastIndex(parts[0], "."); last != -1 {
prefix := parts[0][:last]
name = types.Name{
Name: parts[0][last+1:],
Package: prefix,
Path: strings.Replace(prefix, ".", "/", -1),
}
} else {
name = types.Name{
Name: parts[0],
Package: localPackage.Package,
Path: localPackage.Path,
}
}
field.Type = &types.Type{
Name: name,
Kind: types.Protobuf,
}
}
protoExtra := make(map[string]string)
for i, extra := range parts[3:] {
parts := strings.SplitN(extra, "=", 2)
if len(parts) != 2 {
return fmt.Errorf("member %q of %q malformed 'protobuf' tag, tag %d should be key=value, got %q\n", m.Name, t.Name, i+4, extra)
}
switch parts[0] {
case "name":
protoExtra[parts[0]] = parts[1]
case "casttype", "castkey", "castvalue":
parts[0] = fmt.Sprintf("(gogoproto.%s)", parts[0])
protoExtra[parts[0]] = strconv.Quote(parts[1])
}
}
field.Extras = protoExtra
if name, ok := protoExtra["name"]; ok {
field.Name = name
delete(protoExtra, "name")
}
return nil
}
func membersToFields(locator ProtobufLocator, t *types.Type, localPackage types.Name, omitFieldTypes map[types.Name]struct{}) ([]protoField, error) {
fields := []protoField{}
for _, m := range t.Members {
if namer.IsPrivateGoName(m.Name) {
// skip private fields
continue
}
if _, ok := omitFieldTypes[types.Name{Name: m.Type.Name.Name, Package: m.Type.Name.Package}]; ok {
continue
}
tags := reflect.StructTag(m.Tags)
field := protoField{
LocalPackage: localPackage,
Tag: -1,
Extras: make(map[string]string),
}
protobufTag := tags.Get("protobuf")
if protobufTag == "-" {
continue
}
if err := protobufTagToField(protobufTag, &field, m, t, localPackage); err != nil {
return nil, err
}
// extract information from JSON field tag
if tag := tags.Get("json"); len(tag) > 0 {
parts := strings.Split(tag, ",")
if len(field.Name) == 0 && len(parts[0]) != 0 {
field.Name = parts[0]
}
if field.Tag == -1 && field.Name == "-" {
continue
}
}
if field.Type == nil {
if err := memberTypeToProtobufField(locator, &field, m.Type); err != nil {
return nil, fmt.Errorf("unable to embed type %q as field %q in %q: %v", m.Type, field.Name, t.Name, err)
}
}
if len(field.Name) == 0 {
field.Name = namer.IL(m.Name)
}
if field.Map && field.Repeated {
// maps cannot be repeated
field.Repeated = false
field.Nullable = true
}
if !field.Nullable {
field.Extras["(gogoproto.nullable)"] = "false"
}
if (field.Type.Name.Name == "bytes" && field.Type.Name.Package == "") || (field.Repeated && field.Type.Name.Package == "" && namer.IsPrivateGoName(field.Type.Name.Name)) {
delete(field.Extras, "(gogoproto.nullable)")
}
if field.Name != m.Name {
field.Extras["(gogoproto.customname)"] = strconv.Quote(m.Name)
}
field.CommentLines = m.CommentLines
fields = append(fields, field)
}
// assign tags
highest := 0
byTag := make(map[int]*protoField)
// fields are in Go struct order, which we preserve
for i := range fields {
field := &fields[i]
tag := field.Tag
if tag != -1 {
if existing, ok := byTag[tag]; ok {
return nil, fmt.Errorf("field %q and %q both have tag %d", field.Name, existing.Name, tag)
}
byTag[tag] = field
}
if tag > highest {
highest = tag
}
}
// starting from the highest observed tag, assign new field tags
for i := range fields {
field := &fields[i]
if field.Tag != -1 {
continue
}
highest++
field.Tag = highest
byTag[field.Tag] = field
}
return fields, nil
}
func genComment(out io.Writer, lines []string, indent string) {
for {
l := len(lines)
if l == 0 || len(lines[l-1]) != 0 {
break
}
lines = lines[:l-1]
}
for _, c := range lines {
if len(c) == 0 {
fmt.Fprintf(out, "%s//\n", indent) // avoid trailing whitespace
continue
}
fmt.Fprintf(out, "%s// %s\n", indent, c)
}
}
func formatProtoFile(source []byte) ([]byte, error) {
// TODO; Is there any protobuf formatter?
return source, nil
}
func assembleProtoFile(w io.Writer, f *generator.File) {
w.Write(f.Header)
fmt.Fprint(w, "syntax = 'proto2';\n\n")
if len(f.PackageName) > 0 {
fmt.Fprintf(w, "package %s;\n\n", f.PackageName)
}
if len(f.Imports) > 0 {
imports := []string{}
for i := range f.Imports {
imports = append(imports, i)
}
sort.Strings(imports)
for _, s := range imports {
fmt.Fprintf(w, "import %q;\n", s)
}
fmt.Fprint(w, "\n")
}
if f.Vars.Len() > 0 {
fmt.Fprintf(w, "%s\n", f.Vars.String())
}
w.Write(f.Body.Bytes())
}
func NewProtoFile() *generator.DefaultFileType {
return &generator.DefaultFileType{
Format: formatProtoFile,
Assemble: assembleProtoFile,
}
}