-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathmain.go
More file actions
238 lines (197 loc) · 7.31 KB
/
Copy pathmain.go
File metadata and controls
238 lines (197 loc) · 7.31 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
package main
import (
"bytes"
"flag"
"fmt"
"go/format"
"os"
"strings"
temporalv1 "github.com/thomas-maurice/protoc-gen-go-tmprl/gen/temporal/v1"
"github.com/thomas-maurice/protoc-gen-go-tmprl/internal/model"
"github.com/thomas-maurice/protoc-gen-go-tmprl/internal/renderer"
"github.com/thomas-maurice/protoc-gen-go-tmprl/internal/version"
"google.golang.org/protobuf/compiler/protogen"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/pluginpb"
)
var (
genWorkflowPrefix bool
genDocs bool
// Default activity start to close timeout in seconds
defaultActivityScheduleToClose int
)
func main() {
var flags flag.FlagSet
// This is to generate automatically prefixes for the jobs.
flags.BoolVar(&genWorkflowPrefix, "gen-workflow-prefix", false, "Generates a prefix for the jobs like foo.v1.Foo.Method/<workflowID>")
flags.IntVar(&defaultActivityScheduleToClose, "default-activity-schedule-to-close", 3600*24, "Default start to close activity timeout if none is specified anywhere, in seconds")
flags.BoolVar(&genDocs, "gen-docs", false, "Generates documentation for the temporal workflows")
opts := &protogen.Options{
ParamFunc: flags.Set,
}
opts.Run(func(gen *protogen.Plugin) error {
if defaultActivityScheduleToClose <= 0 {
return fmt.Errorf("the default schedule to close activity timeout cannot be 0 nor negative")
}
gen.SupportedFeatures = uint64(pluginpb.CodeGeneratorResponse_FEATURE_PROTO3_OPTIONAL)
config := &model.Config{
GenWorkflowPrefix: genWorkflowPrefix,
DefaultActivityScheduleToClose: defaultActivityScheduleToClose,
}
for _, f := range gen.Files {
if !f.Generate {
continue
}
if err := generateFile(gen, f, config); err != nil {
return err
}
if genDocs {
if err := generateReadme(gen, f, config); err != nil {
return err
}
}
}
return nil
})
}
func generateFile(plugin *protogen.Plugin, file *protogen.File, config *model.Config) error {
filename := file.GeneratedFilenamePrefix + "_tmprl.pb.go"
needsGenerate := false
for _, s := range file.Services {
if so, ok := proto.GetExtension(s.Desc.Options(), temporalv1.E_Service).(*temporalv1.ServiceOptions); ok && so != nil {
needsGenerate = true
}
}
if !needsGenerate {
return nil
}
// A proto package that resolves to the Go package name `temporal` collides
// with the plugin's own temporal/v1 imports and produces broken code; warn
// loudly rather than letting the user hit a cryptic buf resolution error.
if pkg := file.Proto.GetPackage(); pkg == "temporal" || strings.HasPrefix(pkg, "temporal.") {
fmt.Fprintf(os.Stderr, "protoc-gen-go-tmprl: warning: proto package %q collides with the plugin's own temporal/v1 imports and will generate broken code; rename the package\n", pkg)
}
gen := plugin.NewGeneratedFile(filename, file.GoImportPath)
// Build the model for every temporal service in the file up front so we can
// detect cross-service identifier collisions before emitting anything.
var services []*model.Service
for _, s := range file.Services {
if so, ok := proto.GetExtension(s.Desc.Options(), temporalv1.E_Service).(*temporalv1.ServiceOptions); !ok || so == nil {
// not a temporal service if the `temporal.v1.service` option is not set
continue
}
service, err := model.NewService(s, gen, config)
if err != nil {
return fmt.Errorf("failed to create service model: %w", err)
}
services = append(services, service)
}
// Detect package-level identifier collisions between services generated into
// the same file. These identifiers are not service-prefixed, so two services
// sharing a workflow/activity/signal/query/update method name would emit a
// redeclaration and fail the consumer's build with no diagnostic here.
if err := checkServiceCollisions(services, file.Proto.GetName()); err != nil {
return err
}
// Build complete file content in memory first
var fullContent bytes.Buffer
fullContent.WriteString("// Code generated by protoc-gen-go-tmprl. DO NOT EDIT.\n")
fullContent.WriteString("//\n")
fullContent.WriteString("// version:\n")
fullContent.WriteString("// protoc-gen-go-tmprl version: " + version.Version + "\n")
fullContent.WriteString("// protoc-gen-go-tmprl commit: " + version.Commit + "\n")
fullContent.WriteString("//\n")
fullContent.WriteString("// source file: " + file.Proto.GetName() + "\n")
fullContent.WriteString("\n")
fullContent.WriteString("package " + string(file.GoPackageName) + "\n\n")
for _, service := range services {
// Create renderer
r, err := renderer.NewRenderer(gen)
if err != nil {
return fmt.Errorf("failed to create renderer: %w", err)
}
// Render all components
output, err := r.RenderAll(service)
if err != nil {
return fmt.Errorf("failed to render service %s: %w", service.GoName, err)
}
fullContent.WriteString(output)
}
// Format the complete file
formatted, err := format.Source(fullContent.Bytes())
if err != nil {
return fmt.Errorf("failed to format generated code: %w", err)
}
// Write the formatted content
gen.P(string(formatted))
return nil
}
// checkServiceCollisions Returns an error if two temporal services in the same
// file emit the same package-level identifier. Such a collision compiles as
// valid Go syntax (so go/format accepts it) but redeclares an identifier and
// fails the consumer's build; catching it here fails loud at generation time.
func checkServiceCollisions(services []*model.Service, sourceFile string) error {
owner := make(map[string]string)
for _, s := range services {
for _, name := range s.PackageScopedNames() {
if prev, ok := owner[name]; ok {
return fmt.Errorf("identifier %q is generated by both services %s and %s in %s; two services in one proto file cannot share a workflow/activity/signal/query/update method name", name, prev, s.GoName, sourceFile)
}
owner[name] = s.GoName
}
}
return nil
}
func generateReadme(plugin *protogen.Plugin, file *protogen.File, config *model.Config) error {
filename := file.GeneratedFilenamePrefix + "_tmprl_doc.md"
needsGenerate := false
for _, s := range file.Services {
if so, ok := proto.GetExtension(s.Desc.Options(), temporalv1.E_Service).(*temporalv1.ServiceOptions); ok && so != nil {
needsGenerate = true
}
}
if !needsGenerate {
return nil
}
// Use template-based renderer for documentation
gen := plugin.NewGeneratedFile(filename, file.GoImportPath)
gen.P(`<a id="top"></a>`)
gen.P("# Services")
for _, s := range file.Services {
if so, ok := proto.GetExtension(s.Desc.Options(), temporalv1.E_Service).(*temporalv1.ServiceOptions); !ok || so == nil {
// not a temporal service if the `temporal.v1.service` option is not set
continue
}
// Create service model
service, err := model.NewService(s, gen, config)
if err != nil {
return err
}
// Create renderer
renderer, err := renderer.NewRenderer(gen)
if err != nil {
return err
}
// Render documentation
doc, err := renderer.RenderDocumentation(service)
if err != nil {
return err
}
gen.P(doc)
}
gen.P("# Messages")
// Create renderer for messages (reuse from last service)
renderer, err := renderer.NewRenderer(gen)
if err != nil {
return err
}
for _, m := range file.Messages {
msgDoc, err := renderer.RenderMessageDocumentation(m)
if err != nil {
return err
}
gen.P(msgDoc)
}
gen.P("\n\n[Back to top](#top)")
return nil
}