package protoanalysis import ( "regexp" "strings" "golang.org/x/mod/semver" "git.cw.tr/mukan-network/mukan-ignite/ignite/pkg/errors" ) type ( // Packages represents slice of Package. Packages []Package PkgName string // Package represents a proto pkg. Package struct { // Name of the proto pkg. Name string `json:"name,omitempty"` // Path of the package in the fs. Path string `json:"path,omitempty"` // Files is a list of .proto files in the package. Files Files `json:"files,omitempty"` // GoImportName is the go package name of proto package. GoImportName string `json:"go_import_name,omitempty"` // Messages is a list of proto messages defined in the package. Messages []Message `json:"messages,omitempty"` // Services is a list of RPC services. Services []Service `json:"services,omitempty"` } ) var regexBetaVersion = regexp.MustCompile("^v[0-9]+(beta|alpha)[0-9]+") // ErrMessageNotFound is returned when a proto message cannot be found in a package. var ErrMessageNotFound = errors.New("no message found") // ModuleName retrieves the single module name of the package. func (p Package) ModuleName() (name string) { names := strings.Split(p.Name, ".") for i := len(names) - 1; i >= 0; i-- { name = names[i] if !semver.IsValid(name) && !regexBetaVersion.MatchString(name) { break } } return } // MessageByName finds a message by its name inside Package. func (p Package) MessageByName(name string) (Message, error) { message, ok := p.FindMessageByName(name) if !ok { return Message{}, ErrMessageNotFound } return message, nil } // FindMessageByName finds a message by its name inside Package. // It accepts plain message names, current-package qualified names and nested message names. func (p Package) FindMessageByName(name string) (Message, bool) { for _, candidate := range candidateMessageNames(p.Name, name) { for _, message := range p.Messages { if message.Name == candidate { return message, true } } } if !strings.Contains(strings.TrimPrefix(name, "."), ".") { leafName := leafMessageName(name) var leafMatches []Message for _, message := range p.Messages { if leafMessageName(message.Name) == leafName { leafMatches = append(leafMatches, message) } } if len(leafMatches) == 1 { return leafMatches[0], true } } return Message{}, false } // GoImportPath retrieves the Go import path. func (p Package) GoImportPath() string { return strings.Split(p.GoImportName, ";")[0] } // Files retrieves the files from the package list. func (p Packages) Files() Files { var files []File for _, pkg := range p { files = append(files, pkg.Files...) } return files } type ( Files []File File struct { // Path of the file. Path string `json:"path,omitempty"` // Dependencies is a list of imported proto packages. Dependencies []string `json:"dependencies,omitempty"` } ) func candidateMessageNames(pkgName, name string) []string { candidates := []string{name} canonical := canonicalMessageName(pkgName, name) if canonical != "" && canonical != name { candidates = append(candidates, canonical) } return candidates } func canonicalMessageName(pkgName, name string) string { name = strings.TrimPrefix(name, ".") if pkgName != "" { name = strings.TrimPrefix(name, pkgName+".") } return strings.ReplaceAll(name, ".", "_") } func leafMessageName(name string) string { if index := strings.LastIndex(name, "_"); index >= 0 { return name[index+1:] } if index := strings.LastIndex(name, "."); index >= 0 { return name[index+1:] } return name } // Paths retrieves the list of paths from the files. func (f Files) Paths() []string { var paths []string for _, ff := range f { paths = append(paths, ff.Path) } return paths } type ( // Message represents a proto message. Message struct { // Name of the message. Name string `json:"name,omitempty"` // Path of the proto file where the message is defined. Path string `json:"path,omitempty"` // HighestFieldNumber is the highest field number among fields of the message. // This allows to determine new field number when writing to proto message. HighestFieldNumber int `json:"highest_field_number,omitempty"` // Fields contains message's field names and types. Fields map[string]string `json:"fields,omitempty"` } // Service is an RPC service. Service struct { // Name of the services. Name string `json:"name,omitempty"` // RPCFuncs is a list of RPC funcs of the service. RPCFuncs []RPCFunc `json:"functions,omitempty"` } // RPCFunc is an RPC func. RPCFunc struct { // Name of the RPC func. Name string `json:"name,omitempty"` // RequestType is the request type of RPC func. RequestType string `json:"request_type,omitempty"` // ReturnsType is the response type of RPC func. ReturnsType string `json:"return_type,omitempty"` // HTTPRules keeps info about http rules of an RPC func. // spec: // https://github.com/googleapis/googleapis/blob/master/google/api/http.proto. HTTPRules []HTTPRule `json:"http_rules,omitempty"` } // HTTPRule keeps info about a configured http rule of an RPC func. HTTPRule struct { // Endpoint is the HTTP endpoint path pattern. Endpoint string `json:"endpoint,omitempty"` // Params is a list of parameters defined in the HTTP endpoint itself. Params []string `json:"params,omitempty"` // HasQuery indicates if there is a request query. HasQuery bool `json:"has_query,omitempty"` // QueryFields is a list of query fields defined in the HTTP endpoint. QueryFields map[string]string `json:"query_fields,omitempty"` // HasBody indicates if there is a request payload. HasBody bool `json:"has_body,omitempty"` // BodyFields is a list of body fields defined in the HTTP endpoint. BodyFields map[string]string `json:"body_fields,omitempty"` } ) // IsPaginated checks if the HTTPRule is paginated based on its QueryFields. func (hr HTTPRule) IsPaginated() bool { if len(hr.QueryFields) == 0 { return false } for _, fieldType := range hr.QueryFields { // Message field type suffix check to match common pagination types: // cosmos.base.query.v1beta1.PageRequest // cosmos.base.query.v1beta1.PageResponse if strings.HasSuffix(fieldType, "PageRequest") || strings.HasSuffix(fieldType, "PageResponse") { return true } } return false }