Skip to content

Commit defdc0d

Browse files
committed
Experimental API generator and example module support
1 parent b58947e commit defdc0d

19 files changed

Lines changed: 1497 additions & 0 deletions

File tree

cmd/README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,21 @@ For structs we generate implementations of:
2020
For enumerated types we generate a `FromString` method that instantiates the enum from either the
2121
old `PascalCase` string we have always supported or the `SCREAMING_SNAKE` string generated by
2222
protojson.
23+
24+
### experimentgen
25+
26+
This tool generates the small experimental `api-go` side modules used to expose either additive
27+
service RPCs or wrappers for new fields on existing stable messages under `experimental/<name>`.
28+
29+
Example:
30+
31+
`go run ./cmd/experimentgen -variant example -api-sha <api-commit>`
32+
33+
The command writes to `experimental/<variant>` and clears that target before regenerating it.
34+
35+
Generated modules can contain:
36+
- `register.go` and `client.go` for additive RPCs such as `Echo`
37+
- `wrapper.go` for accessors that read and write experimental fields on stable messages via
38+
protobuf unknown fields, such as `StartWorkflowExecutionRequest.foo`
39+
- `enum.go` for additive values on existing stable enums, such as
40+
`WORKFLOW_ID_CONFLICT_POLICY_FOO`

cmd/experimentgen/descriptors.go

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
"strings"
8+
9+
"google.golang.org/protobuf/proto"
10+
"google.golang.org/protobuf/types/descriptorpb"
11+
)
12+
13+
type methodInfo struct {
14+
Method string
15+
RequestName string
16+
ResponseName string
17+
}
18+
19+
type protoFieldInfo struct {
20+
Type string
21+
Name string
22+
Number int
23+
}
24+
25+
type messageDef struct {
26+
Name string
27+
Fields []protoFieldInfo
28+
}
29+
30+
type descriptorSnapshot struct {
31+
WorkflowServiceMethods map[string]methodInfo
32+
WorkflowMessages map[string]messageDef
33+
WorkflowDescriptors map[string]*descriptorpb.DescriptorProto
34+
WorkflowEnums map[string]map[string]int32
35+
WorkflowPackage string
36+
WorkflowRequestFile *descriptorpb.FileDescriptorProto
37+
DescriptorFiles []*descriptorpb.FileDescriptorProto
38+
}
39+
40+
func (g generator) loadSnapshot(apiRepo string, sourceSHA string, stableRoot string) (descriptorSnapshot, error) {
41+
worktreeDir, err := os.MkdirTemp("", "experimentgen-api-*")
42+
if err != nil {
43+
return descriptorSnapshot{}, err
44+
}
45+
cleanup := func() {
46+
_ = g.run("", "git", "-C", apiRepo, "worktree", "remove", "--force", worktreeDir)
47+
_ = os.RemoveAll(worktreeDir)
48+
}
49+
defer cleanup()
50+
51+
if err := g.run("", "git", "-C", apiRepo, "worktree", "add", "--detach", worktreeDir, sourceSHA); err != nil {
52+
return descriptorSnapshot{}, err
53+
}
54+
55+
targets := []string{
56+
filepath.ToSlash(filepath.Join(stableRoot, "workflowservice/v1/service.proto")),
57+
filepath.ToSlash(filepath.Join(stableRoot, "workflowservice/v1/request_response.proto")),
58+
filepath.ToSlash(filepath.Join(stableRoot, "enums/v1/workflow.proto")),
59+
}
60+
61+
descPath := filepath.Join(worktreeDir, "experimental-descriptor.pb")
62+
args := append([]string{
63+
"-I", worktreeDir,
64+
"--include_imports",
65+
"--descriptor_set_out=" + descPath,
66+
}, targets...)
67+
if err := g.run(worktreeDir, "protoc", args...); err != nil {
68+
return descriptorSnapshot{}, err
69+
}
70+
71+
blob, err := os.ReadFile(descPath)
72+
if err != nil {
73+
return descriptorSnapshot{}, err
74+
}
75+
var set descriptorpb.FileDescriptorSet
76+
if err := proto.Unmarshal(blob, &set); err != nil {
77+
return descriptorSnapshot{}, err
78+
}
79+
return snapshotFromDescriptorSet(&set, stableRoot)
80+
}
81+
82+
func snapshotFromDescriptorSet(set *descriptorpb.FileDescriptorSet, stableRoot string) (descriptorSnapshot, error) {
83+
snapshot := descriptorSnapshot{
84+
WorkflowServiceMethods: make(map[string]methodInfo),
85+
WorkflowMessages: make(map[string]messageDef),
86+
WorkflowDescriptors: make(map[string]*descriptorpb.DescriptorProto),
87+
WorkflowEnums: make(map[string]map[string]int32),
88+
DescriptorFiles: cloneFileDescriptors(set.File),
89+
}
90+
91+
serviceName := filepath.ToSlash(filepath.Join(stableRoot, "workflowservice/v1/service.proto"))
92+
requestResponseName := filepath.ToSlash(filepath.Join(stableRoot, "workflowservice/v1/request_response.proto"))
93+
enumName := filepath.ToSlash(filepath.Join(stableRoot, "enums/v1/workflow.proto"))
94+
95+
var serviceFile, requestFile, enumFile *descriptorpb.FileDescriptorProto
96+
for _, file := range set.File {
97+
switch file.GetName() {
98+
case serviceName:
99+
serviceFile = file
100+
case requestResponseName:
101+
requestFile = file
102+
case enumName:
103+
enumFile = file
104+
}
105+
}
106+
if serviceFile == nil || requestFile == nil || enumFile == nil {
107+
return descriptorSnapshot{}, fmt.Errorf("descriptor set missing required files")
108+
}
109+
110+
for _, service := range serviceFile.GetService() {
111+
if service.GetName() != "WorkflowService" {
112+
continue
113+
}
114+
for _, method := range service.GetMethod() {
115+
name := method.GetName()
116+
snapshot.WorkflowServiceMethods[name] = methodInfo{
117+
Method: name,
118+
RequestName: trimDescriptorName(method.GetInputType()),
119+
ResponseName: trimDescriptorName(method.GetOutputType()),
120+
}
121+
}
122+
}
123+
124+
for _, msg := range requestFile.GetMessageType() {
125+
snapshot.WorkflowMessages[msg.GetName()] = descriptorMessageDef(msg)
126+
snapshot.WorkflowDescriptors[msg.GetName()] = proto.Clone(msg).(*descriptorpb.DescriptorProto)
127+
}
128+
snapshot.WorkflowPackage = requestFile.GetPackage()
129+
snapshot.WorkflowRequestFile = proto.Clone(requestFile).(*descriptorpb.FileDescriptorProto)
130+
131+
for _, enum := range enumFile.GetEnumType() {
132+
values := make(map[string]int32, len(enum.GetValue()))
133+
for _, value := range enum.GetValue() {
134+
values[value.GetName()] = value.GetNumber()
135+
}
136+
snapshot.WorkflowEnums[enum.GetName()] = values
137+
}
138+
139+
return snapshot, nil
140+
}
141+
142+
func descriptorMessageDef(msg *descriptorpb.DescriptorProto) messageDef {
143+
fields := make([]protoFieldInfo, 0, len(msg.GetField()))
144+
for _, field := range msg.GetField() {
145+
fields = append(fields, protoFieldInfo{
146+
Type: descriptorFieldType(field),
147+
Name: field.GetName(),
148+
Number: int(field.GetNumber()),
149+
})
150+
}
151+
return messageDef{
152+
Name: msg.GetName(),
153+
Fields: fields,
154+
}
155+
}
156+
157+
func descriptorFieldType(field *descriptorpb.FieldDescriptorProto) string {
158+
var base string
159+
switch field.GetType() {
160+
case descriptorpb.FieldDescriptorProto_TYPE_DOUBLE:
161+
base = "double"
162+
case descriptorpb.FieldDescriptorProto_TYPE_FLOAT:
163+
base = "float"
164+
case descriptorpb.FieldDescriptorProto_TYPE_INT64:
165+
base = "int64"
166+
case descriptorpb.FieldDescriptorProto_TYPE_UINT64:
167+
base = "uint64"
168+
case descriptorpb.FieldDescriptorProto_TYPE_INT32:
169+
base = "int32"
170+
case descriptorpb.FieldDescriptorProto_TYPE_FIXED64:
171+
base = "fixed64"
172+
case descriptorpb.FieldDescriptorProto_TYPE_FIXED32:
173+
base = "fixed32"
174+
case descriptorpb.FieldDescriptorProto_TYPE_BOOL:
175+
base = "bool"
176+
case descriptorpb.FieldDescriptorProto_TYPE_STRING:
177+
base = "string"
178+
case descriptorpb.FieldDescriptorProto_TYPE_BYTES:
179+
base = "bytes"
180+
case descriptorpb.FieldDescriptorProto_TYPE_UINT32:
181+
base = "uint32"
182+
case descriptorpb.FieldDescriptorProto_TYPE_SFIXED32:
183+
base = "sfixed32"
184+
case descriptorpb.FieldDescriptorProto_TYPE_SFIXED64:
185+
base = "sfixed64"
186+
case descriptorpb.FieldDescriptorProto_TYPE_SINT32:
187+
base = "sint32"
188+
case descriptorpb.FieldDescriptorProto_TYPE_SINT64:
189+
base = "sint64"
190+
default:
191+
base = trimDescriptorName(field.GetTypeName())
192+
}
193+
if field.GetLabel() == descriptorpb.FieldDescriptorProto_LABEL_REPEATED {
194+
return "repeated " + base
195+
}
196+
return base
197+
}
198+
199+
func trimDescriptorName(name string) string {
200+
name = strings.TrimPrefix(name, ".")
201+
if idx := strings.LastIndex(name, "."); idx >= 0 {
202+
return name[idx+1:]
203+
}
204+
return name
205+
}
206+
207+
func cloneFileDescriptors(files []*descriptorpb.FileDescriptorProto) []*descriptorpb.FileDescriptorProto {
208+
cloned := make([]*descriptorpb.FileDescriptorProto, 0, len(files))
209+
for _, file := range files {
210+
cloned = append(cloned, proto.Clone(file).(*descriptorpb.FileDescriptorProto))
211+
}
212+
return cloned
213+
}

cmd/experimentgen/diff.go

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
package main
2+
3+
import (
4+
"slices"
5+
"strings"
6+
7+
"go.temporal.io/api/internal/strcase"
8+
)
9+
10+
const experimentalNumberStart = 1000
11+
12+
type descriptorChanges struct {
13+
Methods []methodInfo
14+
Overlays []messageOverlay
15+
Enums []enumInfo
16+
}
17+
18+
type messageOverlay struct {
19+
StableMessage string
20+
SourceMessage string
21+
FieldName string
22+
SourceName string
23+
SourceNumber int
24+
FieldNumber int
25+
FieldType string
26+
}
27+
28+
type enumInfo struct {
29+
StableEnum string
30+
SourceEnum string
31+
ValueName string
32+
SourceNumber int
33+
ValueNumber int
34+
}
35+
36+
func collectChanges(base descriptorSnapshot, source descriptorSnapshot) descriptorChanges {
37+
changes := descriptorChanges{
38+
Methods: make([]methodInfo, 0),
39+
Overlays: make([]messageOverlay, 0),
40+
Enums: make([]enumInfo, 0),
41+
}
42+
43+
for name, method := range source.WorkflowServiceMethods {
44+
if _, exists := base.WorkflowServiceMethods[name]; exists {
45+
continue
46+
}
47+
changes.Methods = append(changes.Methods, method)
48+
}
49+
50+
for messageName, sourceMessage := range source.WorkflowMessages {
51+
baseMessage, ok := base.WorkflowMessages[messageName]
52+
if !ok {
53+
continue
54+
}
55+
usedNumbers := make(map[int]struct{}, len(baseMessage.Fields))
56+
baseFields := make(map[string]protoFieldInfo, len(baseMessage.Fields))
57+
for _, field := range baseMessage.Fields {
58+
usedNumbers[field.Number] = struct{}{}
59+
baseFields[field.Name] = field
60+
}
61+
addedFields := make([]protoFieldInfo, 0, len(sourceMessage.Fields))
62+
for _, field := range sourceMessage.Fields {
63+
if _, exists := baseFields[field.Name]; exists {
64+
continue
65+
}
66+
addedFields = append(addedFields, field)
67+
}
68+
slices.SortFunc(addedFields, func(a protoFieldInfo, b protoFieldInfo) int {
69+
return strings.Compare(a.Name, b.Name)
70+
})
71+
nextNumber := nextAvailableNumber(usedNumbers)
72+
for _, field := range addedFields {
73+
changes.Overlays = append(changes.Overlays, messageOverlay{
74+
StableMessage: messageName,
75+
SourceMessage: messageName,
76+
FieldName: strcase.ToCamel(field.Name),
77+
SourceName: field.Name,
78+
SourceNumber: field.Number,
79+
FieldNumber: nextNumber,
80+
FieldType: trimDescriptorName(field.Type),
81+
})
82+
usedNumbers[nextNumber] = struct{}{}
83+
nextNumber = nextAvailableNumber(usedNumbers)
84+
}
85+
}
86+
87+
for enumName, sourceValues := range source.WorkflowEnums {
88+
baseValues, ok := base.WorkflowEnums[enumName]
89+
if !ok {
90+
continue
91+
}
92+
usedNumbers := make(map[int]struct{}, len(baseValues))
93+
for _, number := range baseValues {
94+
usedNumbers[int(number)] = struct{}{}
95+
}
96+
names := make([]string, 0, len(sourceValues))
97+
for name := range sourceValues {
98+
if _, exists := baseValues[name]; exists {
99+
continue
100+
}
101+
names = append(names, name)
102+
}
103+
slices.Sort(names)
104+
nextNumber := nextAvailableNumber(usedNumbers)
105+
for _, name := range names {
106+
changes.Enums = append(changes.Enums, enumInfo{
107+
StableEnum: enumName,
108+
SourceEnum: enumName,
109+
ValueName: name,
110+
SourceNumber: int(sourceValues[name]),
111+
ValueNumber: nextNumber,
112+
})
113+
usedNumbers[nextNumber] = struct{}{}
114+
nextNumber = nextAvailableNumber(usedNumbers)
115+
}
116+
}
117+
118+
slices.SortFunc(changes.Methods, func(a methodInfo, b methodInfo) int {
119+
return strings.Compare(a.Method, b.Method)
120+
})
121+
return changes
122+
}
123+
124+
func nextAvailableNumber(usedNumbers map[int]struct{}) int {
125+
for next := experimentalNumberStart; ; next++ {
126+
if _, exists := usedNumbers[next]; !exists {
127+
return next
128+
}
129+
}
130+
}

0 commit comments

Comments
 (0)