Skip to content

Commit 356a360

Browse files
committed
feat: add command to generate code based on custom templates and protobuf
1 parent 47bb7b5 commit 356a360

File tree

2 files changed

+282
-0
lines changed

2 files changed

+282
-0
lines changed

cmd/sponge/commands/generate/common.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,20 @@ func parseProtobufFiles(protobufFile string) ([]string, bool, error) {
347347
return protobufFiles, countImportTypes > 0, nil
348348
}
349349

350+
// ParseFuzzyProtobufFiles parse fuzzy protobuf files
351+
func ParseFuzzyProtobufFiles(protobufFile string) ([]string, error) {
352+
var protoFiles []string
353+
ss := strings.Split(protobufFile, ",")
354+
for _, s := range ss {
355+
files, _, err := parseProtobufFiles(s)
356+
if err != nil {
357+
return nil, err
358+
}
359+
protoFiles = append(protoFiles, files...)
360+
}
361+
return protoFiles, nil
362+
}
363+
350364
// save the moduleName and serverName to the specified file for external use
351365
func saveGenInfo(moduleName string, serverName string, suitedMonoRepo bool, outputDir string) error {
352366
genInfo := moduleName + "," + serverName + "," + strconv.FormatBool(suitedMonoRepo)
Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
package template
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"fmt"
7+
"os"
8+
"strings"
9+
"sync"
10+
"time"
11+
12+
"github.com/fatih/color"
13+
"github.com/spf13/cobra"
14+
15+
"github.com/zhufuyi/sponge/pkg/gobash"
16+
"github.com/zhufuyi/sponge/pkg/gofile"
17+
"github.com/zhufuyi/sponge/pkg/krand"
18+
"github.com/zhufuyi/sponge/pkg/replacer"
19+
20+
"github.com/zhufuyi/sponge/cmd/sponge/commands/generate"
21+
)
22+
23+
var (
24+
printProtoOnce sync.Once
25+
printProtoContent *strings.Builder
26+
)
27+
28+
// ProtobufCommand generate code based on protobuf and custom template
29+
func ProtobufCommand() *cobra.Command {
30+
var (
31+
protobufFile string // protobuf file, support * matching
32+
depProtoDir string // dependency protobuf files directory
33+
34+
tplDir = "" // template directory
35+
fieldsFile = "" // fields defined in json
36+
37+
outPath string // output directory
38+
onlyPrint bool // only print template code and all fields
39+
)
40+
printProtoOnce = sync.Once{}
41+
printProtoContent = new(strings.Builder)
42+
43+
cmd := &cobra.Command{
44+
Use: "protobuf",
45+
Short: "Generate code based on protobuf and custom template",
46+
Long: "Generate code based on protobuf and custom template.",
47+
Example: color.HiBlackString(` # Generate code.
48+
sponge template protobuf --protobuf-file=./test.proto --tpl-dir=yourTemplateDir
49+
50+
# Generate code and specify fields defined in json file.
51+
sponge template protobuf --protobuf-file=./test.proto --tpl-dir=yourTemplateDir --fields=yourDefineFields.json
52+
53+
# Print template code and all fields, do not generate code.
54+
sponge template protobuf --protobuf-file=./test.proto --tpl-dir=yourTemplateDir --fields=yourDefineFields.json --only-print
55+
56+
# Generate code with dependency protobuf files, if your proto file import other proto files, you must specify them in the command.
57+
sponge template protobuf --protobuf-file=./test.proto --dep-proto-dir=./depProtoDir --tpl-dir=yourTemplateDir
58+
59+
# Generate code and specify output directory. Note: code generation will be canceled when the latest generated file already exists.
60+
sponge template protobuf --protobuf-file=./test.proto --tpl-dir=yourTemplateDir --out=./yourDir`),
61+
SilenceErrors: true,
62+
SilenceUsage: true,
63+
RunE: func(cmd *cobra.Command, args []string) error {
64+
if files, err := gofile.ListFiles(tplDir); err != nil {
65+
return err
66+
} else if len(files) == 0 {
67+
return fmt.Errorf("no template files found in directory '%s'", tplDir)
68+
}
69+
70+
m := make(map[string]interface{})
71+
if fieldsFile != "" {
72+
var err error
73+
m, err = parseFields(fieldsFile)
74+
if err != nil {
75+
return err
76+
}
77+
}
78+
79+
thirdPartyDir, err := copyThirdPartyProtoFiles(depProtoDir)
80+
if err != nil {
81+
return err
82+
}
83+
defer deleteFileOrDir(thirdPartyDir)
84+
85+
isSucceed := false
86+
pbfs, err := generate.ParseFuzzyProtobufFiles(protobufFile)
87+
if err != nil {
88+
return err
89+
}
90+
l := len(pbfs)
91+
for i, file := range pbfs {
92+
if !gofile.IsExists(file) || gofile.GetFileSuffixName(file) != ".proto" {
93+
continue
94+
}
95+
96+
jsonFile, err := convertProtoToJSON(file, thirdPartyDir)
97+
if err != nil {
98+
return err
99+
}
100+
101+
protoData, err := getProtoDataFromJSON(jsonFile)
102+
deleteFileOrDir(jsonFile)
103+
if err != nil {
104+
return err
105+
}
106+
protoMap := map[string]interface{}{"Proto": protoData}
107+
fields, err := mergeFields(protoMap, m)
108+
if err != nil {
109+
return err
110+
}
111+
112+
g := protoGenerator{
113+
tplDir: tplDir,
114+
fields: fields,
115+
onlyPrint: onlyPrint,
116+
outPath: outPath,
117+
}
118+
outPath, err = g.generateCode()
119+
if err != nil {
120+
return err
121+
}
122+
isSucceed = true
123+
124+
if i != l-1 {
125+
printProtoContent.WriteString("\n " +
126+
"------------------------------------------------------------------\n\n\n")
127+
}
128+
}
129+
130+
if !isSucceed {
131+
return errors.New("no proto file found")
132+
}
133+
134+
if onlyPrint {
135+
fmt.Println(printProtoContent.String())
136+
} else {
137+
fmt.Printf("generate custom code successfully, out = %s\n", outPath)
138+
}
139+
return nil
140+
},
141+
}
142+
143+
cmd.Flags().StringVarP(&protobufFile, "protobuf-file", "p", "", "proto file")
144+
_ = cmd.MarkFlagRequired("protobuf-file")
145+
cmd.Flags().StringVarP(&depProtoDir, "dep-proto-dir", "d", "", "directory where the dependent proto files are located, example: ./depProtoDir")
146+
cmd.Flags().StringVarP(&tplDir, "tpl-dir", "i", "", "directory where your template code is located")
147+
_ = cmd.MarkFlagRequired("tpl-dir")
148+
cmd.Flags().StringVarP(&fieldsFile, "fields", "f", "", "fields defined in json file")
149+
cmd.Flags().BoolVarP(&onlyPrint, "only-print", "n", false, "only print template code and all fields, do not generate code")
150+
cmd.Flags().StringVarP(&outPath, "out", "o", "", "output directory, default is ./protobuf_to_template_<time>")
151+
152+
return cmd
153+
}
154+
155+
type protoGenerator struct {
156+
tplDir string
157+
fields map[string]interface{}
158+
onlyPrint bool
159+
outPath string
160+
}
161+
162+
func (g *protoGenerator) generateCode() (string, error) {
163+
subTplName := "protobuf_to_template"
164+
r, _ := replacer.New(g.tplDir)
165+
if r == nil {
166+
return "", errors.New("replacer is nil")
167+
}
168+
169+
files := r.GetFiles()
170+
if len(files) == 0 {
171+
return "", errors.New("no template files found")
172+
}
173+
174+
if g.onlyPrint {
175+
printProtoOnce.Do(func() {
176+
listTemplateFiles(printProtoContent, files)
177+
printProtoContent.WriteString("\n\nAll fields name and value:\n")
178+
})
179+
listFields(printProtoContent, g.fields)
180+
return "", nil
181+
}
182+
183+
_ = r.SetOutputDir(g.outPath, subTplName)
184+
if err := r.SaveTemplateFiles(g.fields, gofile.GetSuffixDir(g.tplDir)); err != nil {
185+
return "", err
186+
}
187+
188+
return r.GetOutputDir(), nil
189+
}
190+
191+
func copyThirdPartyProtoFiles(depProtoDir string) (string, error) {
192+
thirdPartyDir := "third_party"
193+
if !gofile.IsExists(thirdPartyDir + "/google") {
194+
r := generate.Replacers[generate.TplNameSponge]
195+
if r == nil {
196+
return "", errors.New("replacer is nil")
197+
}
198+
199+
subDirs := []string{"sponge/" + thirdPartyDir}
200+
subFiles := []string{}
201+
r.SetSubDirsAndFiles(subDirs, subFiles...)
202+
_ = r.SetOutputDir(".") // out dir is third_party
203+
err := r.SaveFiles()
204+
if err != nil {
205+
return "", err
206+
}
207+
}
208+
209+
var err error
210+
var protoFiles []string
211+
if depProtoDir != "" {
212+
protoFiles, err = gofile.ListFiles(depProtoDir, gofile.WithSuffix(".proto"))
213+
if err != nil {
214+
return "", err
215+
}
216+
}
217+
for _, file := range protoFiles {
218+
err = copyProtoFileToDir(file, thirdPartyDir)
219+
if err != nil {
220+
return "", err
221+
}
222+
}
223+
224+
return thirdPartyDir, nil
225+
}
226+
227+
func convertProtoToJSON(protoFile string, thirdPartyDir string) (string, error) {
228+
dir := thirdPartyDir + "/" + krand.String(krand.R_All, 8)
229+
err := os.Mkdir(dir, 0755)
230+
if err != nil {
231+
return "", err
232+
}
233+
currentProtoFile := dir + "/" + gofile.GetFilename(protoFile)
234+
_, err = gobash.Exec("cp", "-f", protoFile, currentProtoFile)
235+
if err != nil {
236+
return "", err
237+
}
238+
239+
protocArgs := []string{"--proto_path=.", fmt.Sprintf("--proto_path=%s", thirdPartyDir),
240+
"--json-field_out=.", "--json-field_opt=paths=source_relative", currentProtoFile}
241+
_, err = gobash.Exec("protoc", protocArgs...)
242+
if err != nil {
243+
return "", err
244+
}
245+
return strings.TrimSuffix(currentProtoFile, ".proto") + ".json", nil
246+
}
247+
248+
func getProtoDataFromJSON(jsonFile string) (map[string]interface{}, error) {
249+
protoData := make(map[string]interface{})
250+
data, err := os.ReadFile(jsonFile)
251+
if err != nil {
252+
return nil, err
253+
}
254+
err = json.Unmarshal(data, &protoData)
255+
return protoData, err
256+
}
257+
258+
func deleteFileOrDir(path string) {
259+
if strings.Contains(path, "third_party") || gofile.GetFileSuffixName(path) == ".json" || gofile.GetFileSuffixName(path) == ".proto" {
260+
for i := 0; i < 10; i++ {
261+
err := os.RemoveAll(path)
262+
if err == nil {
263+
return
264+
}
265+
time.Sleep(200 * time.Millisecond)
266+
}
267+
}
268+
}

0 commit comments

Comments
 (0)