package parser import ( "errors" "fmt" "go/token" "os" "path/filepath" "strings" "unicode" "unicode/utf8" "github.com/emicklei/proto" ) type ( defaultProtoParser struct{} ) func NewDefaultProtoParser() *defaultProtoParser { return &defaultProtoParser{} } func (p *defaultProtoParser) Parse(src string) (Proto, error) { var ret Proto abs, err := filepath.Abs(src) if err != nil { return Proto{}, err } r, err := os.Open(abs) if err != nil { return ret, err } defer r.Close() parser := proto.NewParser(r) set, err := parser.Parse() if err != nil { return ret, err } var serviceList []Service proto.Walk( set, proto.WithImport(func(i *proto.Import) { ret.Import = append(ret.Import, Import{Import: i}) }), proto.WithMessage(func(message *proto.Message) { ret.Message = append(ret.Message, Message{Message: message}) }), proto.WithPackage(func(p *proto.Package) { ret.Package = Package{Package: p} }), proto.WithService(func(service *proto.Service) { serv := Service{Service: service} elements := service.Elements for _, el := range elements { v, _ := el.(*proto.RPC) if v == nil { continue } serv.RPC = append(serv.RPC, &RPC{RPC: v}) } serviceList = append(serviceList, serv) }), proto.WithOption(func(option *proto.Option) { if option.Name == "go_package" { ret.GoPackage = option.Constant.Source } }), ) if len(serviceList) == 0 { return ret, errors.New("rpc service not found") } if len(serviceList) > 1 { return ret, errors.New("only one service expected") } service := serviceList[0] name := filepath.Base(abs) for _, rpc := range service.RPC { if strings.Contains(rpc.RequestType, ".") { return ret, fmt.Errorf("line %v:%v, request type must defined in %s", rpc.Position.Line, rpc.Position.Column, name) } if strings.Contains(rpc.ReturnsType, ".") { return ret, fmt.Errorf("line %v:%v, returns type must defined in %s", rpc.Position.Line, rpc.Position.Column, name) } } if len(ret.GoPackage) == 0 { ret.GoPackage = ret.Package.Name } ret.PbPackage = GoSanitized(filepath.Base(ret.GoPackage)) ret.Src = abs ret.Name = name ret.Service = service return ret, nil } // see google.golang.org/protobuf@v1.25.0/internal/strs/strings.go:71 func GoSanitized(s string) string { // Sanitize the input to the set of valid characters, // which must be '_' or be in the Unicode L or N categories. s = strings.Map(func(r rune) rune { if unicode.IsLetter(r) || unicode.IsDigit(r) { return r } return '_' }, s) // Prepend '_' in the event of a Go keyword conflict or if // the identifier is invalid (does not start in the Unicode L category). r, _ := utf8.DecodeRuneInString(s) if token.Lookup(s).IsKeyword() || !unicode.IsLetter(r) { return "_" + s } return s } // copy from github.com/golang/protobuf@v1.4.2/protoc-gen-go/generator/generator.go:2648 func CamelCase(s string) string { if s == "" { return "" } t := make([]byte, 0, 32) i := 0 if s[0] == '_' { // Need a capital letter; drop the '_'. t = append(t, 'X') i++ } // Invariant: if the next letter is lower case, it must be converted // to upper case. // That is, we process a word at a time, where words are marked by _ or // upper case letter. Digits are treated as words. for ; i < len(s); i++ { c := s[i] if c == '_' && i+1 < len(s) && isASCIILower(s[i+1]) { continue // Skip the underscore in s. } if isASCIIDigit(c) { t = append(t, c) continue } // Assume we have a letter now - if not, it's a bogus identifier. // The next word is a sequence of characters that must start upper case. if isASCIILower(c) { c ^= ' ' // Make it a capital letter. } t = append(t, c) // Guaranteed not lower case. // Accept lower case sequence that follows. for i+1 < len(s) && isASCIILower(s[i+1]) { i++ t = append(t, s[i]) } } return string(t) } func isASCIILower(c byte) bool { return 'a' <= c && c <= 'z' } // Is c an ASCII digit? func isASCIIDigit(c byte) bool { return '0' <= c && c <= '9' }