Skip to content

Commit b025bc3

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

19 files changed

Lines changed: 1545 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: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
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+
Name string
21+
Number int
22+
}
23+
24+
type messageDef struct {
25+
Name string
26+
Fields []protoFieldInfo
27+
}
28+
29+
type descriptorSnapshot struct {
30+
WorkflowServiceMethods map[string]methodInfo
31+
WorkflowMessages map[string]messageDef
32+
WorkflowDescriptors map[string]*descriptorpb.DescriptorProto
33+
WorkflowEnums map[string]map[string]int32
34+
WorkflowPackage string
35+
WorkflowRequestFile *descriptorpb.FileDescriptorProto
36+
DescriptorFiles []*descriptorpb.FileDescriptorProto
37+
}
38+
39+
func (g generator) loadSnapshot(apiRepo string, sourceSHA string, stableRoot string) (descriptorSnapshot, error) {
40+
worktreeDir, err := os.MkdirTemp("", "experimentgen-api-*")
41+
if err != nil {
42+
return descriptorSnapshot{}, err
43+
}
44+
cleanup := func() {
45+
_ = g.run("", "git", "-C", apiRepo, "worktree", "remove", "--force", worktreeDir)
46+
_ = os.RemoveAll(worktreeDir)
47+
}
48+
defer cleanup()
49+
50+
if err := g.run("", "git", "-C", apiRepo, "worktree", "add", "--detach", worktreeDir, sourceSHA); err != nil {
51+
return descriptorSnapshot{}, err
52+
}
53+
54+
targets := []string{
55+
filepath.ToSlash(filepath.Join(stableRoot, "workflowservice/v1/service.proto")),
56+
filepath.ToSlash(filepath.Join(stableRoot, "workflowservice/v1/request_response.proto")),
57+
filepath.ToSlash(filepath.Join(stableRoot, "enums/v1/workflow.proto")),
58+
}
59+
60+
descPath := filepath.Join(worktreeDir, "experimental-descriptor.pb")
61+
args := append([]string{
62+
"-I", worktreeDir,
63+
"--include_imports",
64+
"--descriptor_set_out=" + descPath,
65+
}, targets...)
66+
if err := g.run(worktreeDir, "protoc", args...); err != nil {
67+
return descriptorSnapshot{}, err
68+
}
69+
70+
blob, err := os.ReadFile(descPath)
71+
if err != nil {
72+
return descriptorSnapshot{}, err
73+
}
74+
var set descriptorpb.FileDescriptorSet
75+
if err := proto.Unmarshal(blob, &set); err != nil {
76+
return descriptorSnapshot{}, err
77+
}
78+
return snapshotFromDescriptorSet(&set, stableRoot)
79+
}
80+
81+
func snapshotFromDescriptorSet(set *descriptorpb.FileDescriptorSet, stableRoot string) (descriptorSnapshot, error) {
82+
snapshot := descriptorSnapshot{
83+
WorkflowServiceMethods: make(map[string]methodInfo),
84+
WorkflowMessages: make(map[string]messageDef),
85+
WorkflowDescriptors: make(map[string]*descriptorpb.DescriptorProto),
86+
WorkflowEnums: make(map[string]map[string]int32),
87+
DescriptorFiles: cloneFileDescriptors(set.File),
88+
}
89+
90+
serviceName := filepath.ToSlash(filepath.Join(stableRoot, "workflowservice/v1/service.proto"))
91+
requestResponseName := filepath.ToSlash(filepath.Join(stableRoot, "workflowservice/v1/request_response.proto"))
92+
enumName := filepath.ToSlash(filepath.Join(stableRoot, "enums/v1/workflow.proto"))
93+
94+
var serviceFile, requestFile, enumFile *descriptorpb.FileDescriptorProto
95+
for _, file := range set.File {
96+
switch file.GetName() {
97+
case serviceName:
98+
serviceFile = file
99+
case requestResponseName:
100+
requestFile = file
101+
case enumName:
102+
enumFile = file
103+
}
104+
}
105+
if serviceFile == nil || requestFile == nil || enumFile == nil {
106+
return descriptorSnapshot{}, fmt.Errorf("descriptor set missing required files")
107+
}
108+
109+
for _, service := range serviceFile.GetService() {
110+
if service.GetName() != "WorkflowService" {
111+
continue
112+
}
113+
for _, method := range service.GetMethod() {
114+
name := method.GetName()
115+
snapshot.WorkflowServiceMethods[name] = methodInfo{
116+
Method: name,
117+
RequestName: trimDescriptorName(method.GetInputType()),
118+
ResponseName: trimDescriptorName(method.GetOutputType()),
119+
}
120+
}
121+
}
122+
123+
for _, msg := range requestFile.GetMessageType() {
124+
snapshot.WorkflowMessages[msg.GetName()] = descriptorMessageDef(msg)
125+
snapshot.WorkflowDescriptors[msg.GetName()] = proto.Clone(msg).(*descriptorpb.DescriptorProto)
126+
}
127+
snapshot.WorkflowPackage = requestFile.GetPackage()
128+
snapshot.WorkflowRequestFile = proto.Clone(requestFile).(*descriptorpb.FileDescriptorProto)
129+
130+
for _, enum := range enumFile.GetEnumType() {
131+
values := make(map[string]int32, len(enum.GetValue()))
132+
for _, value := range enum.GetValue() {
133+
values[value.GetName()] = value.GetNumber()
134+
}
135+
snapshot.WorkflowEnums[enum.GetName()] = values
136+
}
137+
138+
return snapshot, nil
139+
}
140+
141+
func descriptorMessageDef(msg *descriptorpb.DescriptorProto) messageDef {
142+
fields := make([]protoFieldInfo, 0, len(msg.GetField()))
143+
for _, field := range msg.GetField() {
144+
fields = append(fields, protoFieldInfo{
145+
Name: field.GetName(),
146+
Number: int(field.GetNumber()),
147+
})
148+
}
149+
return messageDef{
150+
Name: msg.GetName(),
151+
Fields: fields,
152+
}
153+
}
154+
155+
func trimDescriptorName(name string) string {
156+
name = strings.TrimPrefix(name, ".")
157+
if idx := strings.LastIndex(name, "."); idx >= 0 {
158+
return name[idx+1:]
159+
}
160+
return name
161+
}
162+
163+
func cloneFileDescriptors(files []*descriptorpb.FileDescriptorProto) []*descriptorpb.FileDescriptorProto {
164+
cloned := make([]*descriptorpb.FileDescriptorProto, 0, len(files))
165+
for _, file := range files {
166+
cloned = append(cloned, proto.Clone(file).(*descriptorpb.FileDescriptorProto))
167+
}
168+
return cloned
169+
}

cmd/experimentgen/diff.go

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
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+
OverlayMessage string
21+
SourceMessage string
22+
FieldName string
23+
SourceName string
24+
SourceNumber int
25+
FieldNumber int
26+
}
27+
28+
type messageOverlayGroup struct {
29+
StableMessage string
30+
OverlayMessage string
31+
VariableName string
32+
Fields []messageOverlay
33+
}
34+
35+
type enumInfo struct {
36+
StableEnum string
37+
SourceEnum string
38+
ValueName string
39+
SourceNumber int
40+
ValueNumber int
41+
}
42+
43+
func collectChanges(base descriptorSnapshot, source descriptorSnapshot) descriptorChanges {
44+
changes := descriptorChanges{
45+
Methods: make([]methodInfo, 0),
46+
Overlays: make([]messageOverlay, 0),
47+
Enums: make([]enumInfo, 0),
48+
}
49+
50+
for name, method := range source.WorkflowServiceMethods {
51+
if _, exists := base.WorkflowServiceMethods[name]; exists {
52+
continue
53+
}
54+
changes.Methods = append(changes.Methods, method)
55+
}
56+
57+
for messageName, sourceMessage := range source.WorkflowMessages {
58+
baseMessage, ok := base.WorkflowMessages[messageName]
59+
if !ok {
60+
continue
61+
}
62+
usedNumbers := make(map[int]struct{}, len(baseMessage.Fields))
63+
baseFields := make(map[string]protoFieldInfo, len(baseMessage.Fields))
64+
for _, field := range baseMessage.Fields {
65+
usedNumbers[field.Number] = struct{}{}
66+
baseFields[field.Name] = field
67+
}
68+
addedFields := make([]protoFieldInfo, 0, len(sourceMessage.Fields))
69+
for _, field := range sourceMessage.Fields {
70+
if _, exists := baseFields[field.Name]; exists {
71+
continue
72+
}
73+
addedFields = append(addedFields, field)
74+
}
75+
slices.SortFunc(addedFields, func(a protoFieldInfo, b protoFieldInfo) int {
76+
return a.Number - b.Number
77+
})
78+
nextNumber := nextAvailableNumber(usedNumbers)
79+
for _, field := range addedFields {
80+
changes.Overlays = append(changes.Overlays, messageOverlay{
81+
StableMessage: messageName,
82+
OverlayMessage: messageName + "Overlay",
83+
SourceMessage: messageName,
84+
FieldName: strcase.ToCamel(field.Name),
85+
SourceName: field.Name,
86+
SourceNumber: field.Number,
87+
FieldNumber: nextNumber,
88+
})
89+
usedNumbers[nextNumber] = struct{}{}
90+
nextNumber = nextAvailableNumber(usedNumbers)
91+
}
92+
}
93+
94+
for enumName, sourceValues := range source.WorkflowEnums {
95+
baseValues, ok := base.WorkflowEnums[enumName]
96+
if !ok {
97+
continue
98+
}
99+
usedNumbers := make(map[int]struct{}, len(baseValues))
100+
for _, number := range baseValues {
101+
usedNumbers[int(number)] = struct{}{}
102+
}
103+
names := make([]string, 0, len(sourceValues))
104+
for name := range sourceValues {
105+
if _, exists := baseValues[name]; exists {
106+
continue
107+
}
108+
names = append(names, name)
109+
}
110+
slices.Sort(names)
111+
nextNumber := nextAvailableNumber(usedNumbers)
112+
for _, name := range names {
113+
changes.Enums = append(changes.Enums, enumInfo{
114+
StableEnum: enumName,
115+
SourceEnum: enumName,
116+
ValueName: name,
117+
SourceNumber: int(sourceValues[name]),
118+
ValueNumber: nextNumber,
119+
})
120+
usedNumbers[nextNumber] = struct{}{}
121+
nextNumber = nextAvailableNumber(usedNumbers)
122+
}
123+
}
124+
125+
slices.SortFunc(changes.Methods, func(a methodInfo, b methodInfo) int {
126+
return strings.Compare(a.Method, b.Method)
127+
})
128+
return changes
129+
}
130+
131+
func nextAvailableNumber(usedNumbers map[int]struct{}) int {
132+
for next := experimentalNumberStart; ; next++ {
133+
if _, exists := usedNumbers[next]; !exists {
134+
return next
135+
}
136+
}
137+
}
138+
139+
func groupMessageOverlays(overlays []messageOverlay) []messageOverlayGroup {
140+
groups := make([]messageOverlayGroup, 0)
141+
groupByMessage := make(map[string]int)
142+
for _, overlay := range overlays {
143+
idx, ok := groupByMessage[overlay.StableMessage]
144+
if !ok {
145+
idx = len(groups)
146+
groupByMessage[overlay.StableMessage] = idx
147+
groups = append(groups, messageOverlayGroup{
148+
StableMessage: overlay.StableMessage,
149+
OverlayMessage: overlay.OverlayMessage,
150+
VariableName: lowerFirst(overlay.OverlayMessage),
151+
})
152+
}
153+
groups[idx].Fields = append(groups[idx].Fields, overlay)
154+
}
155+
return groups
156+
}
157+
158+
func lowerFirst(s string) string {
159+
if s == "" {
160+
return ""
161+
}
162+
return strings.ToLower(s[:1]) + s[1:]
163+
}

0 commit comments

Comments
 (0)