github.com/ronaksoft/rony@v0.16.26-0.20230807065236-1743dbfe6959/cmd/protoc-gen-gorony/main.go (about)

     1  package main
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"path/filepath"
     7  	"regexp"
     8  	"strings"
     9  	"text/template"
    10  
    11  	"github.com/go-openapi/spec"
    12  	"github.com/ronaksoft/rony/cmd/protoc-gen-gorony/helper"
    13  	"github.com/ronaksoft/rony/cmd/protoc-gen-gorony/repo"
    14  	"github.com/ronaksoft/rony/cmd/protoc-gen-gorony/rpc"
    15  	"github.com/ronaksoft/rony/internal/codegen"
    16  	"google.golang.org/protobuf/compiler/protogen"
    17  	"google.golang.org/protobuf/reflect/protoreflect"
    18  )
    19  
    20  var (
    21  	pluginOpt = &codegen.PluginOptions{}
    22  	pgo       = protogen.Options{
    23  		ParamFunc: pluginOpt.ParamFunc,
    24  		ImportRewriteFunc: func(path protogen.GoImportPath) protogen.GoImportPath {
    25  			// TODO:: this is a hack for bug in Golang/Protobuf which does not support go module versions
    26  			switch path {
    27  			case "github.com/scylladb/gocqlx":
    28  				return "github.com/scylladb/gocqlx/v2"
    29  			case "go.opentelemetry.io/otel/semconv":
    30  				return "go.opentelemetry.io/otel/semconv/v1.7.0"
    31  			}
    32  
    33  			return path
    34  		},
    35  	}
    36  	pathRegEx = regexp.MustCompile(``)
    37  )
    38  
    39  func main() {
    40  	pgo.Run(
    41  		func(plugin *protogen.Plugin) error {
    42  			if pluginOpt.CRC32 {
    43  				codegen.CrcBits = 32
    44  			}
    45  
    46  			switch pluginOpt.ConstructorFormat {
    47  			case codegen.StringJSON:
    48  				return jsonStr(plugin)
    49  			case codegen.Int64JSON:
    50  				return jsonInt(plugin)
    51  			}
    52  
    53  			if pluginOpt.OpenAPI {
    54  				return exportOpenAPI(plugin)
    55  			}
    56  
    57  			if pluginOpt.ExportCleanProto {
    58  				return clearRonyTags(plugin)
    59  			}
    60  
    61  			err := normalMode(plugin)
    62  			if err != nil {
    63  				return err
    64  			}
    65  
    66  			return nil
    67  		},
    68  	)
    69  }
    70  
    71  func normalMode(plugin *protogen.Plugin) error {
    72  	protocVer := plugin.Request.GetCompilerVersion()
    73  	for _, protoFile := range plugin.Files {
    74  		if !protoFile.Generate {
    75  			continue
    76  		}
    77  
    78  		// Create the generator func
    79  		generatedFile := plugin.NewGeneratedFile(
    80  			fmt.Sprintf("%s.rony.go", protoFile.GeneratedFilenamePrefix), protoFile.GoImportPath,
    81  		)
    82  		generatedFile.P("// Code generated by Rony's protoc plugin; DO NOT EDIT.")
    83  		generatedFile.P(
    84  			"// ProtoC ver. v",
    85  			protocVer.GetMajor(), ".", protocVer.GetMinor(), ".", protocVer.GetPatch(),
    86  		)
    87  		generatedFile.P("// Rony ver. ", codegen.Version)
    88  		generatedFile.P("// Source: ", protoFile.Proto.GetName())
    89  		generatedFile.P()
    90  
    91  		// Generate all the helper functions
    92  		_ = helper.GenFunc(generatedFile, pluginOpt, protoFile)
    93  
    94  		// Generate rpc helper functions (Server, Client and CLI)
    95  		_ = rpc.GenFunc(generatedFile, pluginOpt, protoFile)
    96  
    97  		// Generate Repository functionalities
    98  		g3 := repo.New(plugin, protoFile, generatedFile)
    99  		g3.Generate()
   100  	}
   101  
   102  	return nil
   103  }
   104  
   105  func jsonStr(plugin *protogen.Plugin) error {
   106  	var (
   107  		importPath protogen.GoImportPath
   108  		filePrefix string
   109  		cn         = map[string]uint64{}
   110  		cs         = map[uint64]string{}
   111  	)
   112  	for _, f := range plugin.Files {
   113  		if !f.Generate {
   114  			continue
   115  		}
   116  		importPath = f.GoImportPath
   117  		filePrefix = f.GeneratedFilenamePrefix
   118  		// reset the global model and fill with the new data
   119  		for _, mt := range f.Messages {
   120  			constructor := codegen.CrcHash([]byte(mt.Desc.Name()))
   121  			cn[string(mt.Desc.Name())] = constructor
   122  			cs[constructor] = string(mt.Desc.Name())
   123  		}
   124  		for _, s := range f.Services {
   125  			for _, m := range s.Methods {
   126  				methodName := fmt.Sprintf("%s%s", s.Desc.Name(), m.Desc.Name())
   127  				constructor := codegen.CrcHash([]byte(methodName))
   128  				cn[methodName] = constructor
   129  				cs[constructor] = methodName
   130  			}
   131  		}
   132  	}
   133  
   134  	t := template.Must(template.New("t1").Parse(`
   135  	{
   136  	    "ConstructorsByName": {
   137  	    {{range $k,$v := .}}    "{{$k}}": "{{$v}}",
   138  		{{end -}}
   139  		},
   140  		"ConstructorsByValue": {
   141  		{{range $k,$v := .}}    "{{$v}}": "{{$k}}",
   142  		{{end -}}
   143  		}
   144  	}
   145  	`))
   146  
   147  	out := &bytes.Buffer{}
   148  	err := t.Execute(out, cn)
   149  	if err != nil {
   150  		panic(err)
   151  	}
   152  
   153  	gf := plugin.NewGeneratedFile(filepath.Join(filepath.Dir(filePrefix), "constructors.json"), importPath)
   154  	_, err = gf.Write(out.Bytes())
   155  
   156  	return err
   157  }
   158  
   159  func jsonInt(plugin *protogen.Plugin) error {
   160  	var (
   161  		importPath protogen.GoImportPath
   162  		filePrefix string
   163  		cn         = map[string]int64{}
   164  		cs         = map[int64]string{}
   165  	)
   166  	for _, f := range plugin.Files {
   167  		if !f.Generate {
   168  			continue
   169  		}
   170  		importPath = f.GoImportPath
   171  		filePrefix = f.GeneratedFilenamePrefix
   172  		// reset the global model and fill with the new data
   173  		for _, mt := range f.Messages {
   174  			constructor := int64(codegen.CrcHash([]byte(mt.Desc.Name())))
   175  			cn[string(mt.Desc.Name())] = constructor
   176  			cs[constructor] = string(mt.Desc.Name())
   177  		}
   178  		for _, s := range f.Services {
   179  			for _, m := range s.Methods {
   180  				methodName := fmt.Sprintf("%s%s", s.Desc.Name(), m.Desc.Name())
   181  				constructor := int64(codegen.CrcHash([]byte(methodName)))
   182  				cn[methodName] = constructor
   183  				cs[constructor] = methodName
   184  			}
   185  		}
   186  	}
   187  
   188  	t := template.Must(template.New("t1").Parse(`
   189  	{
   190  	    "ConstructorsByName": {
   191  	    {{range $k,$v := .}}    "{{$k}}": "{{$v}}",
   192  		{{end -}}
   193  		},
   194  		"ConstructorsByValue": {
   195  		{{range $k,$v := .}}    "{{$v}}": "{{$k}}",
   196  		{{end -}}
   197  		}
   198  	}
   199  	`))
   200  
   201  	out := &bytes.Buffer{}
   202  	err := t.Execute(out, cn)
   203  	if err != nil {
   204  		panic(err)
   205  	}
   206  
   207  	gf := plugin.NewGeneratedFile(filepath.Join(filepath.Dir(filePrefix), "constructors.json"), importPath)
   208  	_, err = gf.Write(out.Bytes())
   209  
   210  	return err
   211  }
   212  
   213  func exportOpenAPI(plugin *protogen.Plugin) error {
   214  	var (
   215  		importPath protogen.GoImportPath
   216  		filePrefix string
   217  	)
   218  	swag := &spec.Swagger{}
   219  	swag.Info = &spec.Info{
   220  		InfoProps: spec.InfoProps{
   221  			Description: "Rony Swagger Service Description",
   222  			Title:       "Rony",
   223  			Version:     "0.0.1",
   224  		},
   225  	}
   226  	swag.Schemes = []string{"http", "https"}
   227  	swag.Swagger = "2.0"
   228  
   229  	for _, protoFile := range plugin.Files {
   230  		if !protoFile.Generate || protoFile.Proto.GetPackage() == "google.protobuf" {
   231  			continue
   232  		}
   233  		importPath = protoFile.GoImportPath
   234  		filePrefix = protoFile.GeneratedFilenamePrefix
   235  
   236  		arg := codegen.GenTemplateArg(protoFile)
   237  		for _, s := range arg.Services {
   238  			addTag(swag, s)
   239  			for _, m := range s.Methods {
   240  				if !m.RestEnabled {
   241  					continue
   242  				}
   243  				addOperation(swag, s, m)
   244  			}
   245  		}
   246  		for _, m := range arg.Messages {
   247  			addDefinition(swag, m, true)
   248  		}
   249  	}
   250  
   251  	out, err := swag.MarshalJSON()
   252  	if err != nil {
   253  		return err
   254  	}
   255  
   256  	gf := plugin.NewGeneratedFile(filepath.Join(filepath.Dir(filePrefix), "swagger.json"), importPath)
   257  
   258  	_, err = gf.Write(out)
   259  
   260  	return err
   261  }
   262  func addTag(swag *spec.Swagger, s codegen.ServiceArg) {
   263  	swag.Tags = append(
   264  		swag.Tags,
   265  		spec.NewTag(s.Name(), s.Comments, nil),
   266  	)
   267  }
   268  func addDefinition(swag *spec.Swagger, m codegen.MessageArg, jsonEncode bool) {
   269  	if swag.Definitions == nil {
   270  		swag.Definitions = map[string]spec.Schema{}
   271  	}
   272  
   273  	def := spec.Schema{}
   274  	def.Description = m.Comments
   275  	def.Typed("object", "")
   276  	for _, f := range m.Fields {
   277  		fName := f.DescName()
   278  		if jsonEncode {
   279  			fName = f.JSONName()
   280  		}
   281  
   282  		var wrapFunc func(schema *spec.Schema) spec.Schema
   283  		switch f.ProtoCardinality {
   284  		case protoreflect.Repeated:
   285  			wrapFunc = func(item *spec.Schema) spec.Schema {
   286  				return *spec.ArrayProperty(item)
   287  			}
   288  		default:
   289  			wrapFunc = func(schema *spec.Schema) spec.Schema {
   290  				return *schema
   291  			}
   292  		}
   293  		switch f.ProtoKind {
   294  		case protoreflect.StringKind:
   295  			def.SetProperty(fName, wrapFunc(spec.StringProperty()))
   296  		case protoreflect.BytesKind:
   297  			def.SetProperty(fName, wrapFunc(spec.ArrayProperty(spec.Int8Property())))
   298  		case protoreflect.Int32Kind, protoreflect.Uint32Kind,
   299  			protoreflect.Sint32Kind, protoreflect.Fixed32Kind:
   300  			def.SetProperty(fName, wrapFunc(spec.Int32Property()))
   301  		case protoreflect.Int64Kind, protoreflect.Uint64Kind,
   302  			protoreflect.Sint64Kind, protoreflect.Fixed64Kind:
   303  			def.SetProperty(fName, wrapFunc(spec.Int64Property()))
   304  		case protoreflect.FloatKind:
   305  			def.SetProperty(fName, wrapFunc(spec.Float32Property()))
   306  		case protoreflect.DoubleKind:
   307  			def.SetProperty(fName, wrapFunc(spec.Float64Property()))
   308  		case protoreflect.EnumKind:
   309  			def.SetProperty(fName, wrapFunc(spec.Int32Property()))
   310  		case protoreflect.MessageKind:
   311  			def.SetProperty(fName, wrapFunc(spec.RefProperty(fmt.Sprintf("#/definitions/%s", f.Type()))))
   312  		default:
   313  			def.SetProperty(fName, wrapFunc(spec.StringProperty()))
   314  		}
   315  	}
   316  
   317  	swag.Definitions[m.Name()] = def
   318  }
   319  func addOperation(swag *spec.Swagger, s codegen.ServiceArg, m codegen.MethodArg) {
   320  	if swag.Paths == nil {
   321  		swag.Paths = &spec.Paths{
   322  			Paths: map[string]spec.PathItem{},
   323  		}
   324  	}
   325  
   326  	opID := fmt.Sprintf("%s%s", s.NameCC(), m.Name())
   327  	op := spec.NewOperation(opID).
   328  		RespondsWith(200,
   329  			spec.NewResponse().
   330  				WithSchema(
   331  					spec.RefProperty(fmt.Sprintf("#/definitions/%s", m.Output.Name())),
   332  				),
   333  		).
   334  		WithTags(s.Name())
   335  
   336  	if m.Rest.Json {
   337  		op.WithProduces("application/json").
   338  			WithConsumes("application/json")
   339  	} else {
   340  		op.WithProduces("application/protobuf").
   341  			WithConsumes("application/protobuf")
   342  	}
   343  
   344  	for name, kind := range m.Rest.PathParams {
   345  		op.AddParam(
   346  			setParamType(
   347  				spec.PathParam(name).
   348  					AsRequired().
   349  					NoEmptyValues(),
   350  				kind,
   351  			),
   352  		)
   353  	}
   354  
   355  	for name, kind := range m.Rest.QueryParams {
   356  		op.AddParam(
   357  			setParamType(
   358  				spec.QueryParam(name).
   359  					AsRequired().
   360  					NoEmptyValues(),
   361  				kind,
   362  			),
   363  		)
   364  	}
   365  
   366  	if m.Rest.Unmarshal {
   367  		op.AddParam(
   368  			spec.BodyParam(
   369  				m.Input.Name(),
   370  				spec.RefProperty(fmt.Sprintf("#/definitions/%s", m.Input.Name())),
   371  			),
   372  		)
   373  	}
   374  
   375  	restPath := replacePath(m.Rest.Path)
   376  	pathItem := swag.Paths.Paths[restPath]
   377  	switch strings.ToLower(m.Rest.Method) {
   378  	case "get":
   379  		pathItem.Get = op
   380  	case "post":
   381  		pathItem.Post = op
   382  	case "put":
   383  		pathItem.Put = op
   384  	case "delete":
   385  		pathItem.Delete = op
   386  	case "patch":
   387  		pathItem.Patch = op
   388  	}
   389  	swag.Paths.Paths[restPath] = pathItem
   390  }
   391  func setParamType(p *spec.Parameter, kind protoreflect.Kind) *spec.Parameter {
   392  	switch kind {
   393  	case protoreflect.StringKind:
   394  		p.Typed("string", kind.String())
   395  	case protoreflect.BytesKind:
   396  		p.Typed("array", "int8")
   397  	case protoreflect.DoubleKind, protoreflect.FloatKind:
   398  		p.Typed("number", kind.String())
   399  	case protoreflect.Int32Kind, protoreflect.Sint32Kind,
   400  		protoreflect.Uint32Kind, protoreflect.Fixed32Kind:
   401  		p.Typed("integer", "int32")
   402  	case protoreflect.Int64Kind, protoreflect.Sint64Kind,
   403  		protoreflect.Uint64Kind, protoreflect.Fixed64Kind:
   404  		p.Typed("integer", "int64")
   405  	default:
   406  		p.Typed("integer", kind.String())
   407  	}
   408  
   409  	return p
   410  }
   411  
   412  func replacePath(path string) string {
   413  	sb := strings.Builder{}
   414  	for idx, p := range strings.Split(path, "/") {
   415  		if idx > 0 {
   416  			sb.WriteRune('/')
   417  		}
   418  		if strings.HasPrefix(p, ":") {
   419  			sb.WriteRune('{')
   420  			sb.WriteString(p[1:])
   421  			sb.WriteRune('}')
   422  		} else {
   423  			sb.WriteString(p)
   424  		}
   425  	}
   426  
   427  	return sb.String()
   428  }
   429  func clearRonyTags(plugin *protogen.Plugin) error {
   430  	for _, protoFile := range plugin.Files {
   431  		if !protoFile.Generate || protoFile.Proto.GetPackage() == "google.protobuf" {
   432  			continue
   433  		}
   434  
   435  		// Create the generator func
   436  		gFile := plugin.NewGeneratedFile(
   437  			fmt.Sprintf(
   438  				"%s.clean.proto",
   439  				protoFile.GeneratedFilenamePrefix,
   440  			),
   441  			protoFile.GoImportPath,
   442  		)
   443  		gFile.P("syntax = \"", protoFile.Proto.GetSyntax(), "\";")
   444  		gFile.P()
   445  		gFile.P("package ", protoFile.Proto.GetPackage(), ";")
   446  		gFile.P()
   447  		for _, dep := range protoFile.Proto.Dependency {
   448  			for _, f := range plugin.Request.FileToGenerate {
   449  				if f == dep {
   450  					gFile.P("import \"", dep, "\";")
   451  				}
   452  			}
   453  		}
   454  		for _, s := range protoFile.Services {
   455  			gFile.P()
   456  			for _, c := range s.Comments.LeadingDetached {
   457  				gFile.P(s.Comments.Leading, " ", c, " ", s.Comments.Trailing)
   458  			}
   459  			gFile.P("service ", s.Desc.Name(), "{")
   460  			for _, m := range s.Methods {
   461  				for _, c := range m.Comments.LeadingDetached {
   462  					gFile.P(m.Comments.Leading, " ", c, " ", m.Comments.Trailing)
   463  				}
   464  				gFile.P("\t rpc ", m.Desc.Name(), "(", m.Desc.Input().Name(), ") returns (", m.Desc.Output().Name(), ");")
   465  			}
   466  			gFile.P("}")
   467  		}
   468  		for _, m := range protoFile.Messages {
   469  			gFile.P()
   470  			for _, c := range m.Comments.LeadingDetached {
   471  				gFile.P(m.Comments.Leading, " ", c, " ", m.Comments.Trailing)
   472  			}
   473  			gFile.P("message ", m.Desc.Name(), "{")
   474  			for _, f := range m.Fields {
   475  				for _, c := range f.Comments.LeadingDetached {
   476  					gFile.P(f.Comments.Leading, " ", c, " ", f.Comments.Trailing)
   477  				}
   478  				switch protoFile.Proto.GetSyntax() {
   479  				case "proto3":
   480  					switch f.Desc.Cardinality() {
   481  					case protoreflect.Optional, protoreflect.Required:
   482  						switch f.Desc.Kind() {
   483  						case protoreflect.MessageKind:
   484  							gFile.P("\t", f.Desc.Message().Name(), " ", f.Desc.Name(), " = ", f.Desc.Number(), ";")
   485  						case protoreflect.EnumKind:
   486  							gFile.P("\t", f.Desc.Enum().Name(), " ", f.Desc.Name(), " = ", f.Desc.Number(), ";")
   487  						default:
   488  							gFile.P("\t", f.Desc.Kind(), " ", f.Desc.Name(), " = ", f.Desc.Number(), ";")
   489  						}
   490  					case protoreflect.Repeated:
   491  						switch f.Desc.Kind() {
   492  						case protoreflect.MessageKind:
   493  							gFile.P(
   494  								"\t",
   495  								f.Desc.Cardinality().String(), " ", f.Desc.Message().Name(), " ",
   496  								f.Desc.Name(), " = ", f.Desc.Number(), ";",
   497  							)
   498  						case protoreflect.EnumKind:
   499  							gFile.P(
   500  								"\t",
   501  								f.Desc.Cardinality().String(), " ", f.Desc.Enum().Name(), " ",
   502  								f.Desc.Name(), " = ", f.Desc.Number(), ";",
   503  							)
   504  						default:
   505  							gFile.P(
   506  								"\t",
   507  								f.Desc.Cardinality().String(), " ", f.Desc.Kind(), " ",
   508  								f.Desc.Name(), " = ", f.Desc.Number(), ";",
   509  							)
   510  						}
   511  					}
   512  				case "proto2":
   513  					switch f.Desc.Kind() {
   514  					case protoreflect.MessageKind:
   515  						gFile.P(
   516  							"\t",
   517  							f.Desc.Cardinality().String(), " ", f.Desc.Message().Name(), " ",
   518  							f.Desc.Name(), " = ", f.Desc.Number(), ";",
   519  						)
   520  					case protoreflect.EnumKind:
   521  						gFile.P(
   522  							"\t",
   523  							f.Desc.Cardinality().String(), " ", f.Desc.Enum().Name(), " ",
   524  							f.Desc.Name(), " = ", f.Desc.Number(), ";",
   525  						)
   526  					default:
   527  						gFile.P(
   528  							"\t",
   529  							f.Desc.Cardinality().String(), " ", f.Desc.Kind(), " ",
   530  							f.Desc.Name(), " = ", f.Desc.Number(), ";",
   531  						)
   532  					}
   533  				}
   534  			}
   535  			gFile.P("}")
   536  		}
   537  		for _, m := range protoFile.Enums {
   538  			gFile.P()
   539  			for _, c := range m.Comments.LeadingDetached {
   540  				gFile.P(m.Comments.Leading, " ", c, " ", m.Comments.Trailing)
   541  			}
   542  			gFile.P("enum ", m.Desc.Name(), "{")
   543  			for _, f := range m.Values {
   544  				gFile.P("\t", f.Desc.Name(), " = ", f.Desc.Number(), ";")
   545  			}
   546  			gFile.P("}")
   547  		}
   548  	}
   549  
   550  	return nil
   551  }