Skip to content

Commit 7bc1e26

Browse files
Mark plugin as supporting edition 2024 (#125)
Signed-off-by: Stefan VanBuren <svanburen@buf.build>
1 parent 6bb0f5a commit 7bc1e26

3 files changed

Lines changed: 110 additions & 78 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -383,10 +383,10 @@ When exceeded, returns `RESOURCE_EXHAUSTED` error.
383383

384384
### Proto Editions Support
385385

386-
connect-python supports Proto Editions 2023:
386+
`protoc-gen-connect-python` supports up to [Protobuf Editions](https://protobuf.dev/editions/overview/) 2024:
387387

388388
```proto
389-
edition = "2023";
389+
edition = "2024";
390390
391391
package your.service;
392392

protoc-gen-connect-python/generator/generator.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ func Handle(ctx context.Context, _ protoplugin.PluginEnv, responseWriter protopl
1818
responseWriter.SetFeatureProto3Optional()
1919
responseWriter.SetFeatureSupportsEditions(
2020
descriptorpb.Edition_EDITION_PROTO3,
21-
descriptorpb.Edition_EDITION_2023,
21+
descriptorpb.Edition_EDITION_2024,
2222
)
2323

2424
conf := parseConfig(request.Parameter())

protoc-gen-connect-python/generator/generator_test.go

Lines changed: 107 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ func TestGenerateConnectFile(t *testing.T) {
9595

9696
for _, tt := range tests {
9797
t.Run(tt.name, func(t *testing.T) {
98+
t.Parallel()
9899
fd, err := protodesc.NewFile(tt.input, nil)
99100
if err != nil {
100101
t.Fatalf("Failed to create FileDescriptorProto: %v", err)
@@ -200,6 +201,7 @@ func TestGenerate(t *testing.T) {
200201

201202
for _, tt := range tests {
202203
t.Run(tt.name, func(t *testing.T) {
204+
t.Parallel()
203205
resp := generate(t, tt.req)
204206
if tt.wantErr {
205207
if resp.GetError() == "" {
@@ -222,101 +224,131 @@ func TestGenerate(t *testing.T) {
222224
}
223225
}
224226

225-
func TestEdition2023Support(t *testing.T) {
227+
func TestEditionSupport(t *testing.T) {
226228
t.Parallel()
227229

228-
// Create a request with an Edition 2023 proto file
229-
edition2023 := descriptorpb.Edition_EDITION_2023
230+
tests := []struct {
231+
name string
232+
edition descriptorpb.Edition
233+
protoFileName string
234+
packageName string
235+
serviceName string
236+
wantMinEdition descriptorpb.Edition
237+
wantMaxEdition descriptorpb.Edition
238+
wantGeneratedFile string
239+
wantServiceClass string
240+
}{
241+
{
242+
name: "edition 2023",
243+
edition: descriptorpb.Edition_EDITION_2023,
244+
protoFileName: "test_edition2023.proto",
245+
packageName: "test.edition2023",
246+
serviceName: "Edition2023Service",
247+
wantMinEdition: descriptorpb.Edition_EDITION_PROTO3,
248+
wantMaxEdition: descriptorpb.Edition_EDITION_2024,
249+
wantGeneratedFile: "test_edition2023_connect.py",
250+
wantServiceClass: "class Edition2023Service",
251+
},
252+
{
253+
name: "edition 2024",
254+
edition: descriptorpb.Edition_EDITION_2024,
255+
protoFileName: "test_edition2024.proto",
256+
packageName: "test.edition2024",
257+
serviceName: "Edition2024Service",
258+
wantMinEdition: descriptorpb.Edition_EDITION_PROTO3,
259+
wantMaxEdition: descriptorpb.Edition_EDITION_2024,
260+
wantGeneratedFile: "test_edition2024_connect.py",
261+
wantServiceClass: "class Edition2024Service",
262+
},
263+
}
230264

231-
req := &pluginpb.CodeGeneratorRequest{
232-
FileToGenerate: []string{"test_edition2023.proto"},
233-
ProtoFile: []*descriptorpb.FileDescriptorProto{
234-
{
235-
Name: proto.String("test_edition2023.proto"),
236-
Package: proto.String("test.edition2023"),
237-
Edition: edition2023.Enum(),
238-
// Edition 2023 default: field_presence = EXPLICIT
239-
Options: &descriptorpb.FileOptions{
240-
Features: &descriptorpb.FeatureSet{
241-
FieldPresence: descriptorpb.FeatureSet_EXPLICIT.Enum(),
242-
},
243-
},
244-
Service: []*descriptorpb.ServiceDescriptorProto{
265+
for _, tt := range tests {
266+
t.Run(tt.name, func(t *testing.T) {
267+
t.Parallel()
268+
req := &pluginpb.CodeGeneratorRequest{
269+
FileToGenerate: []string{tt.protoFileName},
270+
ProtoFile: []*descriptorpb.FileDescriptorProto{
245271
{
246-
Name: proto.String("Edition2023Service"),
247-
Method: []*descriptorpb.MethodDescriptorProto{
248-
{
249-
Name: proto.String("TestMethod"),
250-
InputType: proto.String(".test.edition2023.TestRequest"),
251-
OutputType: proto.String(".test.edition2023.TestResponse"),
272+
Name: proto.String(tt.protoFileName),
273+
Package: proto.String(tt.packageName),
274+
Edition: tt.edition.Enum(),
275+
Options: &descriptorpb.FileOptions{
276+
Features: &descriptorpb.FeatureSet{
277+
FieldPresence: descriptorpb.FeatureSet_EXPLICIT.Enum(),
252278
},
253279
},
254-
},
255-
},
256-
MessageType: []*descriptorpb.DescriptorProto{
257-
{
258-
Name: proto.String("TestRequest"),
259-
Field: []*descriptorpb.FieldDescriptorProto{
280+
Service: []*descriptorpb.ServiceDescriptorProto{
260281
{
261-
Name: proto.String("message"),
262-
Number: proto.Int32(1),
263-
Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(),
264-
Type: descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum(),
265-
// In Edition 2023, field presence is controlled by features
282+
Name: proto.String(tt.serviceName),
283+
Method: []*descriptorpb.MethodDescriptorProto{
284+
{
285+
Name: proto.String("TestMethod"),
286+
InputType: proto.String("." + tt.packageName + ".TestRequest"),
287+
OutputType: proto.String("." + tt.packageName + ".TestResponse"),
288+
},
289+
},
266290
},
267291
},
268-
},
269-
{
270-
Name: proto.String("TestResponse"),
271-
Field: []*descriptorpb.FieldDescriptorProto{
292+
MessageType: []*descriptorpb.DescriptorProto{
272293
{
273-
Name: proto.String("result"),
274-
Number: proto.Int32(1),
275-
Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(),
276-
Type: descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum(),
294+
Name: proto.String("TestRequest"),
295+
Field: []*descriptorpb.FieldDescriptorProto{
296+
{
297+
Name: proto.String("message"),
298+
Number: proto.Int32(1),
299+
Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(),
300+
Type: descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum(),
301+
},
302+
},
303+
},
304+
{
305+
Name: proto.String("TestResponse"),
306+
Field: []*descriptorpb.FieldDescriptorProto{
307+
{
308+
Name: proto.String("result"),
309+
Number: proto.Int32(1),
310+
Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(),
311+
Type: descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum(),
312+
},
313+
},
277314
},
278315
},
279316
},
280317
},
281-
},
282-
},
283-
}
318+
}
284319

285-
// Call Generate
286-
resp := generate(t, req)
320+
resp := generate(t, req)
287321

288-
// Verify no error occurred
289-
if resp.GetError() != "" {
290-
t.Fatalf("generate() failed for Edition 2023 proto: %v", resp.GetError())
291-
}
322+
if resp.GetError() != "" {
323+
t.Fatalf("generate() failed for %s proto: %v", tt.name, resp.GetError())
324+
}
292325

293-
// Verify the generator declared Edition support
294-
if resp.GetSupportedFeatures()&uint64(pluginpb.CodeGeneratorResponse_FEATURE_SUPPORTS_EDITIONS) == 0 {
295-
t.Error("Generator should declare FEATURE_SUPPORTS_EDITIONS")
296-
}
326+
if resp.GetSupportedFeatures()&uint64(pluginpb.CodeGeneratorResponse_FEATURE_SUPPORTS_EDITIONS) == 0 {
327+
t.Error("Generator should declare FEATURE_SUPPORTS_EDITIONS")
328+
}
297329

298-
// Verify minimum and maximum editions are set
299-
if resp.GetMinimumEdition() != int32(descriptorpb.Edition_EDITION_PROTO3) {
300-
t.Errorf("Expected minimum edition PROTO3, got %v", resp.GetMinimumEdition())
301-
}
302-
if resp.GetMaximumEdition() != int32(descriptorpb.Edition_EDITION_2023) {
303-
t.Errorf("Expected maximum edition 2023, got %v", resp.GetMaximumEdition())
304-
}
330+
if resp.GetMinimumEdition() != int32(tt.wantMinEdition) {
331+
t.Errorf("Expected minimum edition %v, got %v", tt.wantMinEdition, resp.GetMinimumEdition())
332+
}
333+
if resp.GetMaximumEdition() != int32(tt.wantMaxEdition) {
334+
t.Errorf("Expected maximum edition %v, got %v", tt.wantMaxEdition, resp.GetMaximumEdition())
335+
}
305336

306-
// Verify a file was generated
307-
if len(resp.GetFile()) == 0 {
308-
t.Error("No files generated for Edition 2023 proto")
309-
} else {
310-
generatedFile := resp.GetFile()[0]
311-
if generatedFile.GetName() != "test_edition2023_connect.py" {
312-
t.Errorf("Expected filename test_edition2023_connect.py, got %v", generatedFile.GetName())
313-
}
337+
if len(resp.GetFile()) == 0 {
338+
t.Errorf("No files generated for %s proto", tt.name)
339+
return
340+
}
314341

315-
// Verify the generated content includes the service
316-
content := generatedFile.GetContent()
317-
if !strings.Contains(content, "class Edition2023Service") {
318-
t.Error("Generated code missing Edition2023Service class")
319-
}
342+
generatedFile := resp.GetFile()[0]
343+
if generatedFile.GetName() != tt.wantGeneratedFile {
344+
t.Errorf("Expected filename %s, got %v", tt.wantGeneratedFile, generatedFile.GetName())
345+
}
346+
347+
content := generatedFile.GetContent()
348+
if !strings.Contains(content, tt.wantServiceClass) {
349+
t.Errorf("Generated code missing %s", tt.wantServiceClass)
350+
}
351+
})
320352
}
321353
}
322354

0 commit comments

Comments
 (0)