This is an automated email from the ASF dual-hosted git repository.
Alanxtl pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/dubbo-go.git
The following commit(s) were added to refs/heads/develop by this push:
new e373d9d66 feat(openapi): support oneof fields for
protoc-gen-triple-openapi (#3637)
e373d9d66 is described below
commit e373d9d6697f858b8983b4086955ab5972db0238
Author: XiaoFei <[email protected]>
AuthorDate: Tue Aug 11 21:25:04 2026 +0800
feat(openapi): support oneof fields for protoc-gen-triple-openapi (#3637)
* feat(openapi): support oneof fields for protoc-gen-triple-openapi
* test(openapi): cover proto3 optional synthetic oneofs
* fix:ci
---
config_center/nacos/listener_concurrency_test.go | 22 +++----
.../protoc-gen-triple-openapi/example/greet.proto | 4 ++
.../example/greet.triple.openapi.json | 45 ++++++++++++++
.../example/greet.triple.openapi.yaml | 23 +++++++
.../internal/converter/convert_test.go | 71 ++++++++++++++++++++++
.../internal/converter/schema/util.go | 46 +++++++++++++-
6 files changed, 196 insertions(+), 15 deletions(-)
diff --git a/config_center/nacos/listener_concurrency_test.go
b/config_center/nacos/listener_concurrency_test.go
index 3b8b030fb..2a6012073 100644
--- a/config_center/nacos/listener_concurrency_test.go
+++ b/config_center/nacos/listener_concurrency_test.go
@@ -155,17 +155,15 @@ func
TestAddRemoveListenerConcurrentChurnKeepsPersistentSubscription(t *testing.
var wg sync.WaitGroup
workers := 32
start := make(chan struct{})
- for w := 0; w < workers; w++ {
- wg.Add(1)
- go func() {
- defer wg.Done()
+ for range workers {
+ wg.Go(func() {
<-start
- for i := 0; i < 200; i++ {
+ for range 200 {
l := newNoopListener()
n.addListener(key, l)
n.removeListener(key, l)
}
- }()
+ })
}
close(start)
wg.Wait()
@@ -239,12 +237,10 @@ func TestRemoveThenAddNoLostSubscriptionDeterministic(t
*testing.T) {
// unfixed code the adder proceeds, stores a fresh set, and ListenConfig
// populates the slot while the remover is still parked.
var adderWG sync.WaitGroup
- adderWG.Add(1)
- go func() {
- defer adderWG.Done()
+ adderWG.Go(func() {
l := newNoopListener()
n.addListener(key, l)
- }()
+ })
// Probe whether the adder could complete before we release the cancel.
With
// the lifecycle lock the adder is still blocked; without it the adder
@@ -287,11 +283,11 @@ func TestListenerConcurrentStress(t *testing.T) {
keyFor := func(k int) string { return "stress-key-" + strconv.Itoa(k) }
var wg sync.WaitGroup
- for g := 0; g < goroutines; g++ {
+ for g := range goroutines {
wg.Add(1)
go func(g int) {
defer wg.Done()
- for i := 0; i < iterations; i++ {
+ for i := range iterations {
k := g % numKeys
key := keyFor(k)
l := newNoopListener()
@@ -310,7 +306,7 @@ func TestListenerConcurrentStress(t *testing.T) {
wg.Wait()
// A live subscription must exist iff at least one listener remains.
- for k := 0; k < numKeys; k++ {
+ for k := range numKeys {
key := keyFor(k)
got := client.hasSubscription(key, "test-group")
want := atomic.LoadInt32(&remaining[k]) > 0
diff --git a/tools/protoc-gen-triple-openapi/example/greet.proto
b/tools/protoc-gen-triple-openapi/example/greet.proto
index 1af03e2b1..f2dd63548 100644
--- a/tools/protoc-gen-triple-openapi/example/greet.proto
+++ b/tools/protoc-gen-triple-openapi/example/greet.proto
@@ -42,6 +42,10 @@ message GreetRequest {
repeated GreetProfile profiles = 7;
map<string, GreetProfile> profile_metadata = 8;
map<string, GreetingType> type_metadata = 9;
+ oneof recipient {
+ string recipient_id = 10;
+ GreetProfile recipient_profile = 11;
+ }
}
message GreetProfile {
diff --git a/tools/protoc-gen-triple-openapi/example/greet.triple.openapi.json
b/tools/protoc-gen-triple-openapi/example/greet.triple.openapi.json
index a2cf154fb..c59dc77a9 100644
--- a/tools/protoc-gen-triple-openapi/example/greet.triple.openapi.json
+++ b/tools/protoc-gen-triple-openapi/example/greet.triple.openapi.json
@@ -128,6 +128,43 @@
},
"greet.GreetRequest": {
"type": "object",
+ "allOf": [
+ {
+ "oneOf": [
+ {
+ "type": "object",
+ "not": {
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "recipientId"
+ ]
+ },
+ {
+ "type": "object",
+ "required": [
+ "recipientProfile"
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "recipientId"
+ ]
+ },
+ {
+ "type": "object",
+ "required": [
+ "recipientProfile"
+ ]
+ }
+ ]
+ }
+ ],
"properties": {
"name": {
"type": "string",
@@ -187,6 +224,14 @@
"additionalProperties": {
"$ref": "#/components/schemas/greet.GreetingType"
}
+ },
+ "recipientId": {
+ "type": "string",
+ "title": "recipient_id"
+ },
+ "recipientProfile": {
+ "title": "recipient_profile",
+ "$ref": "#/components/schemas/greet.GreetProfile"
}
},
"title": "GreetRequest",
diff --git a/tools/protoc-gen-triple-openapi/example/greet.triple.openapi.yaml
b/tools/protoc-gen-triple-openapi/example/greet.triple.openapi.yaml
index 72687d3d7..70c56dab9 100644
--- a/tools/protoc-gen-triple-openapi/example/greet.triple.openapi.yaml
+++ b/tools/protoc-gen-triple-openapi/example/greet.triple.openapi.yaml
@@ -79,6 +79,23 @@ components:
title: GreetProfile
greet.GreetRequest:
type: object
+ allOf:
+ - oneOf:
+ - type: object
+ not:
+ anyOf:
+ - type: object
+ required:
+ - recipientId
+ - type: object
+ required:
+ - recipientProfile
+ - type: object
+ required:
+ - recipientId
+ - type: object
+ required:
+ - recipientProfile
properties:
name:
type: string
@@ -124,6 +141,12 @@ components:
title: type_metadata
additionalProperties:
$ref: '#/components/schemas/greet.GreetingType'
+ recipientId:
+ type: string
+ title: recipient_id
+ recipientProfile:
+ title: recipient_profile
+ $ref: '#/components/schemas/greet.GreetProfile'
title: GreetRequest
description: A request to the Greet RPC.
greet.GreetRequest.MetadataEntry:
diff --git a/tools/protoc-gen-triple-openapi/internal/converter/convert_test.go
b/tools/protoc-gen-triple-openapi/internal/converter/convert_test.go
index 3f5be21fb..35c4529b1 100644
--- a/tools/protoc-gen-triple-openapi/internal/converter/convert_test.go
+++ b/tools/protoc-gen-triple-openapi/internal/converter/convert_test.go
@@ -85,6 +85,61 @@ func TestConvertGolden(t *testing.T) {
}
}
+func TestConvertProto3OptionalSkipsSyntheticOneof(t *testing.T) {
+ request := &pluginpb.CodeGeneratorRequest{
+ FileToGenerate: []string{"optional.proto"},
+ Parameter: proto.String("format=yaml"),
+ ProtoFile: []*descriptorpb.FileDescriptorProto{{
+ Name: proto.String("optional.proto"),
+ Package: proto.String("optional"),
+ Syntax: proto.String("proto3"),
+ MessageType: []*descriptorpb.DescriptorProto{
+ {
+ Name: proto.String("Request"),
+ Field:
[]*descriptorpb.FieldDescriptorProto{
+
proto3OptionalField("display_name", "displayName", 1,
descriptorpb.FieldDescriptorProto_TYPE_STRING, 0),
+
proto3OptionalField("retry_count", "retryCount", 2,
descriptorpb.FieldDescriptorProto_TYPE_INT32, 1),
+ },
+ OneofDecl:
[]*descriptorpb.OneofDescriptorProto{
+ {Name:
proto.String("_display_name")},
+ {Name:
proto.String("_retry_count")},
+ },
+ },
+ {Name: proto.String("Response")},
+ },
+ Service: []*descriptorpb.ServiceDescriptorProto{{
+ Name: proto.String("Service"),
+ Method: []*descriptorpb.MethodDescriptorProto{{
+ Name: proto.String("Call"),
+ InputType:
proto.String(".optional.Request"),
+ OutputType:
proto.String(".optional.Response"),
+ }},
+ }},
+ }},
+ }
+
+ response, err := convert(request)
+ if err != nil {
+ t.Fatalf("convert() error = %v", err)
+ }
+ if len(response.File) != 1 {
+ t.Fatalf("generated %d files, want 1", len(response.File))
+ }
+
+ got := response.File[0].GetContent()
+ for _, want := range []string{
+ "displayName:",
+ "retryCount:",
+ } {
+ if !strings.Contains(got, want) {
+ t.Errorf("generated OpenAPI does not contain %q\n%s",
want, got)
+ }
+ }
+ if strings.Contains(got, "oneOf:") || strings.Contains(got, "allOf:") {
+ t.Errorf("proto3 optional fields generated oneof
constraints\n%s", got)
+ }
+}
+
func examplePath(t *testing.T, name string) string {
t.Helper()
_, sourceFile, _, ok := runtime.Caller(0)
@@ -126,7 +181,10 @@ func greetRequest(format string)
*pluginpb.CodeGeneratorRequest {
field("profiles",
"profiles", 7, descriptorpb.FieldDescriptorProto_LABEL_REPEATED,
descriptorpb.FieldDescriptorProto_TYPE_MESSAGE, ".greet.GreetProfile"),
field("profile_metadata", "profileMetadata", 8,
descriptorpb.FieldDescriptorProto_LABEL_REPEATED,
descriptorpb.FieldDescriptorProto_TYPE_MESSAGE,
".greet.GreetRequest.ProfileMetadataEntry"),
field("type_metadata",
"typeMetadata", 9, descriptorpb.FieldDescriptorProto_LABEL_REPEATED,
descriptorpb.FieldDescriptorProto_TYPE_MESSAGE,
".greet.GreetRequest.TypeMetadataEntry"),
+
oneofField("recipient_id", "recipientId", 10,
descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL,
descriptorpb.FieldDescriptorProto_TYPE_STRING, "", 0),
+
oneofField("recipient_profile", "recipientProfile", 11,
descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL,
descriptorpb.FieldDescriptorProto_TYPE_MESSAGE, ".greet.GreetProfile", 0),
},
+ OneofDecl:
[]*descriptorpb.OneofDescriptorProto{{Name: proto.String("recipient")}},
NestedType:
[]*descriptorpb.DescriptorProto{
{
Name:
proto.String("MetadataEntry"),
@@ -304,6 +362,19 @@ func field(name, jsonName string, number int32, label
descriptorpb.FieldDescript
return fd
}
+func oneofField(name, jsonName string, number int32, label
descriptorpb.FieldDescriptorProto_Label, kind
descriptorpb.FieldDescriptorProto_Type, typeName string, oneofIndex int32)
*descriptorpb.FieldDescriptorProto {
+ fd := field(name, jsonName, number, label, kind, typeName)
+ fd.OneofIndex = proto.Int32(oneofIndex)
+ return fd
+}
+
+func proto3OptionalField(name, jsonName string, number int32, kind
descriptorpb.FieldDescriptorProto_Type, oneofIndex int32)
*descriptorpb.FieldDescriptorProto {
+ fd := field(name, jsonName, number,
descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL, kind, "")
+ fd.OneofIndex = proto.Int32(oneofIndex)
+ fd.Proto3Optional = proto.Bool(true)
+ return fd
+}
+
func sourceComment(path []int32, comment string)
*descriptorpb.SourceCodeInfo_Location {
return &descriptorpb.SourceCodeInfo_Location{
Path: path,
diff --git a/tools/protoc-gen-triple-openapi/internal/converter/schema/util.go
b/tools/protoc-gen-triple-openapi/internal/converter/schema/util.go
index ece1cfe95..8739ac962 100644
--- a/tools/protoc-gen-triple-openapi/internal/converter/schema/util.go
+++ b/tools/protoc-gen-triple-openapi/internal/converter/schema/util.go
@@ -39,20 +39,62 @@ func messageToSchema(tt protoreflect.MessageDescriptor)
(string, *base.Schema) {
}
props := orderedmap.New[string, *base.SchemaProxy]()
+ parent := base.CreateSchemaProxy(s)
fields := tt.Fields()
for i := 0; i < fields.Len(); i++ {
field := fields.Get(i)
- // TODO: handle oneof
- prop := fieldToSchema(base.CreateSchemaProxy(s), field)
+ prop := fieldToSchema(parent, field)
props.Set(field.JSONName(), prop)
}
s.Properties = props
+ appendOneOfSchemas(s, tt.Oneofs())
return string(tt.FullName()), s
}
+func appendOneOfSchemas(s *base.Schema, oneofs protoreflect.OneofDescriptors) {
+ for i := 0; i < oneofs.Len(); i++ {
+ oneof := oneofs.Get(i)
+ if oneof.IsSynthetic() {
+ continue
+ }
+
+ s.AllOf = append(s.AllOf, oneofToSchema(oneof))
+ }
+}
+
+func oneofToSchema(tt protoreflect.OneofDescriptor) *base.SchemaProxy {
+ fields := tt.Fields()
+ choices := make([]*base.SchemaProxy, 0, fields.Len()+1)
+ notAnyOf := make([]*base.SchemaProxy, 0, fields.Len())
+ for i := 0; i < fields.Len(); i++ {
+ notAnyOf = append(notAnyOf,
requiredFieldSchema(fields.Get(i).JSONName()))
+ }
+
+ choices = append(choices, base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Not: base.CreateSchemaProxy(&base.Schema{
+ AnyOf: notAnyOf,
+ }),
+ }))
+ for i := 0; i < fields.Len(); i++ {
+ choices = append(choices,
requiredFieldSchema(fields.Get(i).JSONName()))
+ }
+
+ return base.CreateSchemaProxy(&base.Schema{
+ OneOf: choices,
+ })
+}
+
+func requiredFieldSchema(fieldName string) *base.SchemaProxy {
+ return base.CreateSchemaProxy(&base.Schema{
+ Type: []string{"object"},
+ Required: []string{fieldName},
+ })
+}
+
func fieldToSchema(parent *base.SchemaProxy, tt protoreflect.FieldDescriptor)
*base.SchemaProxy {
if tt.IsMap() {
root := ScalarFieldToSchema(parent, tt, false)