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)

Reply via email to