Skip to content

Commit 88232ef

Browse files
committed
feat: add proto generation and mapper functionality
- Introduced `gen.go` for generating protobuf files from entity schemas. - Implemented `graph.go` to manage entity graph loading and annotations. - Created `converter.go` for converting between entity and protobuf types. - Developed `generator.go` to handle the generation of mapper code. - Added `mapper.tmpl` for templating the generated mapper code. - Enhanced support for various field types and conversions in protobuf. - Implemented utility functions for handling protobuf packages and imports.
1 parent 6f15447 commit 88232ef

File tree

6 files changed

+909
-77
lines changed

6 files changed

+909
-77
lines changed

cmd/entproto.go

Lines changed: 83 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,103 @@
11
package cmd
22

33
import (
4-
"github.com/go-sphere/sphere-cli/internal/entproto"
4+
"github.com/go-sphere/sphere-cli/internal/entity"
5+
"github.com/go-sphere/sphere-cli/internal/entity/graph"
56
"github.com/spf13/cobra"
7+
"github.com/spf13/pflag"
68
)
79

10+
type EntSharedOptions struct {
11+
SchemaPath string
12+
13+
AllFieldsRequired bool
14+
AutoAddAnnotation bool
15+
EnumUseRawType bool
16+
SkipUnsupported bool
17+
18+
TimeProtoType string
19+
UUIDProtoType string
20+
UnsupportedProtoType string
21+
22+
ProtoPackages string
23+
}
24+
25+
func (o *EntSharedOptions) AddFlags(fs *pflag.FlagSet) {
26+
fs.StringVar(&o.SchemaPath, "path", "./schema", "path to schema directory")
27+
28+
fs.StringVar(&o.TimeProtoType, "time_proto_type", "int64", "use proto type for time.Time, one of int64, string, google.protobuf.Timestamp")
29+
fs.StringVar(&o.UUIDProtoType, "uuid_proto_type", "string", "use proto type for uuid.UUID, one of string, bytes")
30+
fs.StringVar(&o.UnsupportedProtoType, "unsupported_proto_type", "google.protobuf.Any", "use proto type for unsupported types, one of google.protobuf.Any, google.protobuf.Struct, bytes")
31+
32+
fs.BoolVar(&o.AllFieldsRequired, "all_fields_required", true, "ignore optional, use zero value instead")
33+
fs.BoolVar(&o.AutoAddAnnotation, "auto_annotation", true, "auto add annotation to the schema")
34+
fs.BoolVar(&o.EnumUseRawType, "enum_raw_type", true, "use string for enum")
35+
fs.BoolVar(&o.SkipUnsupported, "skip_unsupported", true, "skip unsupported types, when unsupportedProtoType is not set")
36+
37+
fs.StringVar(&o.ProtoPackages, "import_proto", "google/protobuf/any.proto,google.protobuf,Any;", "import proto, format: path1,package1,type1,type2;path2,package2,type3,type4;")
38+
}
39+
40+
func (o *EntSharedOptions) ToGraphOptions() *graph.Options {
41+
return &graph.Options{
42+
SchemaPath: o.SchemaPath,
43+
AllFieldsRequired: o.AllFieldsRequired,
44+
AutoAddAnnotation: o.AutoAddAnnotation,
45+
EnumUseRawType: o.EnumUseRawType,
46+
SkipUnsupported: o.SkipUnsupported,
47+
TimeProtoType: o.TimeProtoType,
48+
UUIDProtoType: o.UUIDProtoType,
49+
UnsupportedProtoType: o.UnsupportedProtoType,
50+
}
51+
}
52+
853
var ent2protoCmd = &cobra.Command{
954
Use: "entproto",
1055
Aliases: []string{"ent2proto"},
1156
Short: "Convert Ent schema to Protobuf definitions",
1257
Long: `Convert Ent schema to Protobuf definitions, generating .proto files from Ent schema definitions.`,
1358
}
1459

60+
var ent2mapperCmd = &cobra.Command{
61+
Use: "entmapper",
62+
Aliases: []string{"ent2mapper"},
63+
Short: "Convert Ent schema to Ent mapper",
64+
Long: `Convert Ent schema to Ent mapper, generating mapper files from Ent schema definitions.`,
65+
}
66+
1567
func init() {
68+
shareOptions := &EntSharedOptions{}
69+
shareOptions.AddFlags(ent2protoCmd.Flags())
70+
shareOptions.AddFlags(ent2mapperCmd.Flags())
1671
rootCmd.AddCommand(ent2protoCmd)
72+
rootCmd.AddCommand(ent2mapperCmd)
1773

18-
flag := ent2protoCmd.Flags()
19-
schemaPath := flag.String("path", "./schema", "path to schema directory")
20-
protoDir := flag.String("proto", "./proto", "path to proto directory")
21-
22-
timeProtoType := flag.String("time_proto_type", "int64", "use proto type for time.Time, one of int64, string, google.protobuf.Timestamp")
23-
uuidProtoType := flag.String("uuid_proto_type", "string", "use proto type for uuid.UUID, one of string, bytes")
24-
unsupportedProtoType := flag.String("unsupported_proto_type", "google.protobuf.Any", "use proto type for unsupported types, one of google.protobuf.Any, google.protobuf.Struct, bytes")
25-
26-
allFieldsRequired := flag.Bool("all_fields_required", true, "ignore optional, use zero value instead")
27-
autoAddAnnotation := flag.Bool("auto_annotation", true, "auto add annotation to the schema")
28-
enumUseRawType := flag.Bool("enum_raw_type", true, "use string for enum")
29-
skipUnsupported := flag.Bool("skip_unsupported", true, "skip unsupported types, when unsupportedProtoType is not set")
30-
31-
importProto := flag.String("import_proto", "google/protobuf/any.proto,google.protobuf,Any;", "import proto, format: path1,package1,type1,type2;path2,package2,type3,type4;")
32-
33-
ent2protoCmd.RunE = func(cmd *cobra.Command, args []string) error {
34-
options := entproto.Options{
35-
SchemaPath: *schemaPath,
36-
ProtoDir: *protoDir,
37-
38-
TimeProtoType: *timeProtoType,
39-
UUIDProtoType: *uuidProtoType,
40-
UnsupportedProtoType: *unsupportedProtoType,
41-
SkipUnsupported: *skipUnsupported,
42-
43-
AllFieldsRequired: *allFieldsRequired,
44-
AutoAddAnnotation: *autoAddAnnotation,
45-
EnumUseRawType: *enumUseRawType,
74+
{
75+
flag := ent2protoCmd.Flags()
76+
protoDir := flag.String("proto", "./proto", "path to proto directory")
77+
ent2protoCmd.RunE = func(cmd *cobra.Command, args []string) error {
78+
return entity.GenerateProto(&entity.ProtoOptions{
79+
Graph: shareOptions.ToGraphOptions(),
80+
ProtoDir: *protoDir,
81+
})
82+
}
4683

47-
ProtoPackages: entproto.ParseProtoPackages(*importProto),
84+
}
85+
{
86+
flag := ent2mapperCmd.Flags()
87+
mapperDir := flag.String("mapper", "./mapper", "path to mapper directory")
88+
mapperPackage := flag.String("mapper_package", "mapper", "package name for the generated mapper code")
89+
entPackage := flag.String("ent_package", "ent", "package name for the ent code")
90+
protoPkgPath := flag.String("proto_pkg_path", "github.com/go-sphere/sphere-layout/proto", "go module path for the generated proto code")
91+
protoPkgName := flag.String("proto_pkg_name", "proto", "package name for the generated proto code")
92+
ent2mapperCmd.RunE = func(cmd *cobra.Command, args []string) error {
93+
return entity.GenerateMapper(&entity.MapperOptions{
94+
Graph: shareOptions.ToGraphOptions(),
95+
MapperDir: *mapperDir,
96+
MapperPackage: *mapperPackage,
97+
EntPackage: *entPackage,
98+
ProtoPkgPath: *protoPkgPath,
99+
ProtoPkgName: *protoPkgName,
100+
})
48101
}
49-
entproto.Generate(&options)
50-
return nil
51102
}
52103
}

internal/entity/gen.go

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package entity
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"go/format"
7+
"log"
8+
"os"
9+
"path"
10+
"path/filepath"
11+
12+
"entgo.io/contrib/entproto"
13+
"entgo.io/ent/entc/gen"
14+
"github.com/go-sphere/sphere-cli/internal/entity/graph"
15+
"github.com/go-sphere/sphere-cli/internal/entity/mapper"
16+
"github.com/jhump/protoreflect/desc"
17+
"github.com/jhump/protoreflect/desc/protoprint"
18+
)
19+
20+
type ProtoOptions struct {
21+
Graph *graph.Options
22+
ProtoDir string
23+
}
24+
25+
func GenerateProto(options *ProtoOptions) error {
26+
gh, err := graph.LoadGraph(options.Graph)
27+
if err != nil {
28+
return err
29+
}
30+
err = generateProto(gh, options)
31+
if err != nil {
32+
return err
33+
}
34+
return nil
35+
}
36+
37+
func generateProto(g *gen.Graph, options *ProtoOptions) error {
38+
entProtoDir := path.Join(g.Target, "proto")
39+
if options.ProtoDir != "" {
40+
entProtoDir = options.ProtoDir
41+
}
42+
adapter, err := entproto.LoadAdapter(g)
43+
if err != nil {
44+
return fmt.Errorf("entproto: failed parsing entity graph: %w", err)
45+
}
46+
var errs []error
47+
for _, schema := range g.Schemas {
48+
name := schema.Name
49+
_, sErr := adapter.GetFileDescriptor(name)
50+
if sErr != nil && !errors.Is(sErr, entproto.ErrSchemaSkipped) {
51+
errs = append(errs, sErr)
52+
}
53+
}
54+
if len(errs) > 0 {
55+
return fmt.Errorf("entproto: failed parsing some schemas: %w", errors.Join(errs...))
56+
}
57+
allDescriptors := make([]*desc.FileDescriptor, 0, len(adapter.AllFileDescriptors()))
58+
for _, fDesc := range adapter.AllFileDescriptors() {
59+
graph.FixProto3Optional(g, fDesc)
60+
allDescriptors = append(allDescriptors, fDesc)
61+
}
62+
var printer protoprint.Printer
63+
printer.Compact = true
64+
if err = printer.PrintProtosToFileSystem(allDescriptors, entProtoDir); err != nil {
65+
return fmt.Errorf("entproto: failed writing .proto files: %w", err)
66+
}
67+
return nil
68+
}
69+
70+
type MapperOptions struct {
71+
Graph *graph.Options
72+
MapperDir string
73+
MapperPackage string
74+
EntPackage string
75+
ProtoPkgPath string
76+
ProtoPkgName string
77+
}
78+
79+
func GenerateMapper(options *MapperOptions) error {
80+
gh, err := graph.LoadGraph(options.Graph)
81+
if err != nil {
82+
return err
83+
}
84+
err = generateMappers(gh, options)
85+
if err != nil {
86+
return err
87+
}
88+
return nil
89+
}
90+
91+
func generateMappers(graph *gen.Graph, options *MapperOptions) error {
92+
adapter, err := entproto.LoadAdapter(graph)
93+
if err != nil {
94+
return fmt.Errorf("entproto: failed loading adapter: %w", err)
95+
}
96+
_ = os.RemoveAll(options.MapperDir)
97+
err = os.MkdirAll(options.MapperDir, 0755)
98+
if err != nil {
99+
return fmt.Errorf("entproto: failed creating entmapper dir: %w", err)
100+
}
101+
for _, node := range graph.Nodes {
102+
msgDesc, nErr := adapter.GetMessageDescriptor(node.Name)
103+
if nErr != nil {
104+
continue
105+
}
106+
fileDesc := msgDesc.GetFile()
107+
g, nErr := mapper.NewGenerator(fileDesc, graph, adapter, node, msgDesc)
108+
if nErr != nil {
109+
return nErr
110+
}
111+
112+
if options.EntPackage != "" {
113+
g.EntPackage = mapper.GoImportPath(options.EntPackage)
114+
}
115+
if options.ProtoPkgPath != "" {
116+
g.ProtoImportPath = mapper.GoImportPath(options.ProtoPkgPath)
117+
}
118+
if options.MapperPackage != "" {
119+
g.PackageName = options.MapperPackage
120+
}
121+
if options.ProtoPkgName != "" {
122+
g.ProtoPackageName = options.ProtoPkgName
123+
}
124+
content, nErr := g.Generate()
125+
if nErr != nil {
126+
return nErr
127+
}
128+
formatted, fmtErr := format.Source(content)
129+
if fmtErr != nil {
130+
return fmt.Errorf("entproto: format entmapper for %s: %w", node.Name, fmtErr)
131+
}
132+
fileName := gen.Funcs["snake"].(func(string) string)(node.Name) + ".go"
133+
outPath := filepath.Join(options.MapperDir, fileName)
134+
log.Printf("entproto: generating entmapper for %s to %s", node.Name, outPath)
135+
nErr = os.WriteFile(outPath, formatted, 0644)
136+
if nErr != nil {
137+
return nErr
138+
}
139+
}
140+
return nil
141+
}
Lines changed: 5 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
package entproto
1+
package graph
22

33
import (
4-
"errors"
54
"fmt"
65
"log"
7-
"path"
86
"reflect"
97
"sort"
108
"strings"
@@ -16,13 +14,11 @@ import (
1614
"entgo.io/ent/schema/field"
1715
"github.com/go-viper/mapstructure/v2"
1816
"github.com/jhump/protoreflect/desc" //nolint
19-
"github.com/jhump/protoreflect/desc/protoprint"
2017
"google.golang.org/protobuf/types/descriptorpb"
2118
)
2219

2320
type Options struct {
2421
SchemaPath string
25-
ProtoDir string
2622

2723
AllFieldsRequired bool
2824
AutoAddAnnotation bool
@@ -42,56 +38,20 @@ type ProtoPackage struct {
4238
Types []string
4339
}
4440

45-
func Generate(options *Options) {
41+
func LoadGraph(options *Options) (*gen.Graph, error) {
4642
injectProtoPackages(options.ProtoPackages)
4743
graph, err := entc.LoadGraph(options.SchemaPath, &gen.Config{
4844
Target: options.SchemaPath,
4945
})
5046
if err != nil {
51-
log.Fatalf("entproto: failed loading ent graph: %v", err)
47+
log.Fatalf("entproto: failed loading entity graph: %v", err)
5248
}
5349
if options.AutoAddAnnotation {
5450
for i := 0; i < len(graph.Nodes); i++ {
5551
addAnnotationForNode(graph.Nodes[i], options)
5652
}
5753
}
58-
err = generate(graph, options)
59-
if err != nil {
60-
log.Fatalf("entproto: failed generating protos: %s", err)
61-
}
62-
}
63-
64-
func generate(g *gen.Graph, options *Options) error {
65-
entProtoDir := path.Join(g.Target, "proto")
66-
if options.ProtoDir != "" {
67-
entProtoDir = options.ProtoDir
68-
}
69-
adapter, err := entproto.LoadAdapter(g)
70-
if err != nil {
71-
return fmt.Errorf("entproto: failed parsing ent graph: %w", err)
72-
}
73-
var errs []error
74-
for _, schema := range g.Schemas {
75-
name := schema.Name
76-
_, sErr := adapter.GetFileDescriptor(name)
77-
if sErr != nil && !errors.Is(sErr, entproto.ErrSchemaSkipped) {
78-
errs = append(errs, sErr)
79-
}
80-
}
81-
if len(errs) > 0 {
82-
return fmt.Errorf("entproto: failed parsing some schemas: %w", errors.Join(errs...))
83-
}
84-
allDescriptors := make([]*desc.FileDescriptor, 0, len(adapter.AllFileDescriptors()))
85-
for _, fDesc := range adapter.AllFileDescriptors() {
86-
fixProto3Optional(g, fDesc)
87-
allDescriptors = append(allDescriptors, fDesc)
88-
}
89-
var printer protoprint.Printer
90-
printer.Compact = true
91-
if err = printer.PrintProtosToFileSystem(allDescriptors, entProtoDir); err != nil {
92-
return fmt.Errorf("entproto: failed writing .proto files: %w", err)
93-
}
94-
return nil
54+
return graph, err
9555
}
9656

9757
func addAnnotationForNode(node *gen.Type, options *Options) {
@@ -239,7 +199,7 @@ var (
239199
optionalFieldLabel = descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL
240200
)
241201

242-
func fixProto3Optional(g *gen.Graph, fDesc *desc.FileDescriptor) {
202+
func FixProto3Optional(g *gen.Graph, fDesc *desc.FileDescriptor) {
243203
messageMap := make(map[string]*desc.MessageDescriptor)
244204
for _, message := range fDesc.GetMessageTypes() {
245205
messageMap[message.GetName()] = message

0 commit comments

Comments
 (0)