diff --git a/.gitignore b/.gitignore index 50fd311..fcfd7cf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ vendor -./tuugen +/tuugen +example_project/internal diff --git a/cmd.go b/cmd.go index 5c61832..933d8ed 100644 --- a/cmd.go +++ b/cmd.go @@ -14,3 +14,26 @@ func runCommand(cmd string, args []string) error { } return nil } + +func GenerateProtos() error { + cmd := "protoc" + args := []string{"-I", ".:${GOPATH}/src", "--go_out=.", "--go-grpc_out=.", "service.proto"} + return runCommand(cmd, args) +} + +func GoFmt() error { + cmd := "gofmt" + return runCommand(cmd, []string{}) +} + +func GoImports() error { + if err := runCommand("goimports", []string{"-w", "internal/service/service.go"}); err != nil { + return err + } + return runCommand("goimports", []string{"-w", "internal/interactors/interactors.go"}) +} + +func GoModInit(importPath string) error { + runCommand("go", []string{"mod", "init", importPath}) + return runCommand("go", []string{"mod", "tidy"}) +} diff --git a/cmd/tuugen/main.go b/cmd/tuugen/main.go index 5d8233f..e58fbad 100644 --- a/cmd/tuugen/main.go +++ b/cmd/tuugen/main.go @@ -1,19 +1,53 @@ package main import ( + "embed" + _ "embed" "log" "github.com/cubixle/tuugen" ) +//go:embed templates/* +var FS embed.FS + func main() { + // idea is to read a yaml file and create the project. err := tuugen.GenerateProtos() if err != nil { log.Fatal(err) } - err = tuugen.GenerateService("./internal/pb/service_grpc.pb.go") + importPath := "github.com/cubixle/tuugen/example_project" + + err = tuugen.GenerateService( + FS, + "Service", + importPath, + "./internal/pb/service/service_grpc.pb.go", + "./internal/service/service.go", + ) if err != nil { - log.Fatal(err) + log.Fatalf("failed to generate service: %v", err) + } + err = tuugen.GenerateInteractor( + FS, + "Service", + importPath, + "./internal/pb/service/service_grpc.pb.go", + "./internal/interactors/interactors.go", + ) + if err != nil { + log.Fatalf("failed to generate interactor: %v", err) + } + + if err := tuugen.GoFmt(); err != nil { + log.Fatalf("failed to run gofmt: %v", err) + } + if err := tuugen.GoModInit(importPath); err != nil { + log.Fatalf("failed to run 'go mod init': %v", err) + } + if err := tuugen.GoImports(); err != nil { + log.Fatalf("failed to run 'goimports': %v", err) } } diff --git a/templates/data_obj.go.tmpl b/cmd/tuugen/templates/data_obj.go.tmpl similarity index 100% rename from templates/data_obj.go.tmpl rename to cmd/tuugen/templates/data_obj.go.tmpl diff --git a/cmd/tuugen/templates/interactors.go.tmpl b/cmd/tuugen/templates/interactors.go.tmpl new file mode 100644 index 0000000..548c7ab --- /dev/null +++ b/cmd/tuugen/templates/interactors.go.tmpl @@ -0,0 +1,15 @@ +package interactors + +import ( + "fmt"{{ range .Imports }} + "{{ . }}"{{ end }} +) + +type Interactor struct { + +} +{{ range .Funcs }} +func (i *Interactor) {{ .Name }}({{.ArgsToStr}}) {{ if gt (len .Returns) 0 }}({{.ListToStr .Returns}}){{end}} { + return nil, fmt.Errorf("unimplemented") +} +{{ end }} \ No newline at end of file diff --git a/templates/main.go.tmpl b/cmd/tuugen/templates/main.go.tmpl similarity index 100% rename from templates/main.go.tmpl rename to cmd/tuugen/templates/main.go.tmpl diff --git a/cmd/tuugen/templates/service.go.tmpl b/cmd/tuugen/templates/service.go.tmpl new file mode 100644 index 0000000..3c8d765 --- /dev/null +++ b/cmd/tuugen/templates/service.go.tmpl @@ -0,0 +1,22 @@ +package service + +import ({{ range .Imports }} + "{{ . }}"{{ end }} +) + +type Service struct { + interactor *interactors.Interactor +} +{{ range .Funcs }} +func (s *Service) {{ .Name }}({{.ArgsToStr}}) {{ if gt (len .Returns) 0 }}({{.ListToStr .Returns}}){{end}} { + res, err := s.interactor.{{.Name}}(ctx, req) + if err != nil { + return nil, errorToGRPCError(err) + } + return res, nil +} +{{ end }} + +func errorToGRPCError(err error) error { + return err +} \ No newline at end of file diff --git a/example_project/.gitignore b/example_project/.gitignore new file mode 100644 index 0000000..634f0cc --- /dev/null +++ b/example_project/.gitignore @@ -0,0 +1,3 @@ +internal +go.mod +go.sum \ No newline at end of file diff --git a/example_project/tuugen.yml b/example_project/tuugen.yml index 2ecb858..e1d0f91 100644 --- a/example_project/tuugen.yml +++ b/example_project/tuugen.yml @@ -1,4 +1,5 @@ project: tuugen_test_project +import_path: github.com/cubixle/tuugen/example_project proto: service.proto db_models: - name: User diff --git a/go.mod b/go.mod index 4cb0dbb..8cfdfb2 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module github.com/cubixle/tuugen go 1.16 + +require golang.org/x/tools v0.1.3 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e652f3e --- /dev/null +++ b/go.sum @@ -0,0 +1,27 @@ +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.3 h1:L69ShwSZEyCsLKoAxDKeMvLDZkumEe8gXUZAjab0tX8= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/protos.go b/protos.go index de29fb4..fda308e 100644 --- a/protos.go +++ b/protos.go @@ -1,7 +1 @@ package tuugen - -func GenerateProtos() error { - cmd := "protoc" - args := []string{"-I", ".:${GOPATH}/src", "--go_out=.", "--go-grpc_out=.", "service.proto"} - return runCommand(cmd, args) -} diff --git a/service.go b/service.go index 43a13d4..9b3885d 100644 --- a/service.go +++ b/service.go @@ -1,34 +1,158 @@ package tuugen import ( + "embed" + "fmt" "go/ast" "go/parser" "go/token" + "html/template" + "os" + "path/filepath" + "strings" ) -type serviceEndpoint struct { - name string - args []string -} - -func GenerateService(filepath string) error { - - // load service template - // t, err := template.ParseFiles("templates/service.go.tmpl") - // if err != nil { - // return err - // } - +func GenerateService(FS embed.FS, serviceName, importPath, protoFilepath, outputFile string) error { // read interface from project built protos. - fset := token.NewFileSet() - f, err := parser.ParseFile(fset, filepath, nil, 0) - if err != nil { - return err - } - // generate the service template and store it to a file. - err = ast.Print(fset, f) - if err != nil { + if err := createFileFromProto(FS, serviceName, "templates/service.go.tmpl", importPath, protoFilepath, outputFile); err != nil { return err } return nil } + +func GenerateInteractor(FS embed.FS, serviceName, importPath, protoFilepath, outputFile string) error { + // read interface from project built protos. + if err := createFileFromProto(FS, serviceName, "templates/interactors.go.tmpl", importPath, protoFilepath, outputFile); err != nil { + return err + } + return nil +} + +type serviceDef struct { + Imports []string + Funcs []serviceFuncDef +} + +type serviceFuncDef struct { + Name string + Args []string + Returns []string +} + +func (s serviceFuncDef) ListToStr(list []string) string { + return strings.Join(list, ", ") +} + +func (s serviceFuncDef) ArgsToStr() string { + args := []string{} + for _, a := range s.Args { + switch a { + case "context.Context": + args = append(args, fmt.Sprintf("ctx %s", a)) + default: + args = append(args, fmt.Sprintf("req %s", a)) + } + } + return s.ListToStr(args) +} + +// generate the service/interactor template and store it to a file. +func createFileFromProto(FS embed.FS, serviceName, templateFile, importPath, protoFilePath, outputFile string) error { + // load service template + t, err := template.ParseFS(FS, templateFile) + if err != nil { + return err + } + + // check/create file path + fp := filepath.Dir(outputFile) + + if err := os.MkdirAll(fp, 0777); err != nil { + return err + } + + f, err := os.Create(outputFile) + if err != nil { + return nil + } + funcs, err := methodsFromProto(protoFilePath, importPath, serviceName) + if err != nil { + return err + } + + if err := t.Execute(f, funcs); err != nil { + return err + } + + return f.Close() +} + +func methodsFromProto(filepath, importPath, serviceName string) (serviceDef, error) { + funcs := []serviceFuncDef{} + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, filepath, nil, 0) + if err != nil { + return serviceDef{}, fmt.Errorf("failed to parse proto go file: %w", err) + } + + imports := f.Imports + importStrs := []string{} + // ast.Print(fset, imports) + // return serviceDef{}, nil + for _, i := range imports { + importStrs = append(importStrs, i.Name.Name) + } + importStrs = append(importStrs, importPath+"/internal/pb/service", importPath+"/internal/interactors") + + for _, m := range f.Scope.Objects { + if m.Name != serviceName+"Server" { + continue + } + decl := m.Decl.(*ast.TypeSpec) + t := decl.Type.(*ast.InterfaceType) + + for _, m := range t.Methods.List { + name := m.Names[0].Name + if strings.Contains(name, "mustEmbed") { + continue + } + f := serviceFuncDef{Name: name} + ft := m.Type.(*ast.FuncType) + if ft.Params != nil { + f.Args = parseFieldList(ft.Params) + } + + if ft.Results != nil { + f.Returns = parseFieldList(ft.Results) + } + funcs = append(funcs, f) + // err = ast.Print(fset, ft.Params) + // if err != nil { + // return serviceDef{}, err + // } + } + } + return serviceDef{Funcs: funcs, Imports: importStrs}, nil +} + +func parseFieldList(fl *ast.FieldList) []string { + args := []string{} + if fl.List == nil { + return args + } + for _, a := range fl.List { + argStr := "" + switch a.Type.(type) { + case *ast.SelectorExpr: + as := a.Type.(*ast.SelectorExpr) + argStr = as.X.(*ast.Ident).Name + "." + as.Sel.Name + case *ast.StarExpr: + as := a.Type.(*ast.StarExpr) + argStr = "*service." + as.X.(*ast.Ident).Name + case *ast.Ident: + argStr = a.Type.(*ast.Ident).Name + } + args = append(args, argStr) + } + return args +} diff --git a/templates.go b/templates.go deleted file mode 100644 index 1e192a3..0000000 --- a/templates.go +++ /dev/null @@ -1,4 +0,0 @@ -package tuugen - -type Templates interface { -} diff --git a/templates/service.go.tmpl b/templates/service.go.tmpl deleted file mode 100644 index c69584c..0000000 --- a/templates/service.go.tmpl +++ /dev/null @@ -1,5 +0,0 @@ -type Service struct { - -} - -func {{ name }}() \ No newline at end of file diff --git a/test.sh b/test.sh new file mode 100644 index 0000000..49daa56 --- /dev/null +++ b/test.sh @@ -0,0 +1,3 @@ +make build +cd example_project +../tuugen \ No newline at end of file diff --git a/tuugen b/tuugen deleted file mode 100755 index b85848f..0000000 Binary files a/tuugen and /dev/null differ