diff --git a/bin/configs/protobuf-schema-config.yaml b/bin/configs/protobuf-schema-config.yaml index 5000698d79f..7a57d7fdaba 100644 --- a/bin/configs/protobuf-schema-config.yaml +++ b/bin/configs/protobuf-schema-config.yaml @@ -8,6 +8,7 @@ additionalProperties: numberedFieldNumberList: true startEnumsWithUnspecified: true wrapComplexType: false + supportMultipleResponses: false aggregateModelsName: data typeMappings: object: "google.protobuf.Struct" diff --git a/docs/generators/protobuf-schema.md b/docs/generators/protobuf-schema.md index f05d48eabd8..d3ddc215aee 100644 --- a/docs/generators/protobuf-schema.md +++ b/docs/generators/protobuf-schema.md @@ -22,6 +22,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |aggregateModelsName|Aggregated model filename. If set, all generated models will be combined into this single file.| |null| |numberedFieldNumberList|Field numbers in order.| |false| |startEnumsWithUnspecified|Introduces "UNSPECIFIED" as the first element of enumerations.| |false| +|supportMultipleResponses|Support multiple responses| |true| |wrapComplexType|Generate Additional message for complex type| |true| ## IMPORT MAPPING diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ProtobufSchemaCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ProtobufSchemaCodegen.java index 362249ab3be..f22cda8c54e 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ProtobufSchemaCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ProtobufSchemaCodegen.java @@ -68,6 +68,8 @@ public class ProtobufSchemaCodegen extends DefaultCodegen implements CodegenConf public static final String AGGREGATE_MODELS_NAME = "aggregateModelsName"; + public static final String SUPPORT_MULTIPLE_RESPONSES = "supportMultipleResponses"; + private final Logger LOGGER = LoggerFactory.getLogger(ProtobufSchemaCodegen.class); @Setter protected String packageName = "openapitools"; @@ -82,6 +84,8 @@ public class ProtobufSchemaCodegen extends DefaultCodegen implements CodegenConf private boolean wrapComplexType = true; + private boolean supportMultipleResponses = true; + @Override public CodegenType getTag() { return CodegenType.SCHEMA; @@ -192,6 +196,7 @@ public class ProtobufSchemaCodegen extends DefaultCodegen implements CodegenConf addSwitch(START_ENUMS_WITH_UNSPECIFIED, "Introduces \"UNSPECIFIED\" as the first element of enumerations.", startEnumsWithUnspecified); addSwitch(ADD_JSON_NAME_ANNOTATION, "Append \"json_name\" annotation to message field when the specification name differs from the protobuf field name", addJsonNameAnnotation); addSwitch(WRAP_COMPLEX_TYPE, "Generate Additional message for complex type", wrapComplexType); + addSwitch(SUPPORT_MULTIPLE_RESPONSES, "Support multiple responses", supportMultipleResponses); addOption(AGGREGATE_MODELS_NAME, "Aggregated model filename. If set, all generated models will be combined into this single file.", null); } @@ -239,6 +244,12 @@ public class ProtobufSchemaCodegen extends DefaultCodegen implements CodegenConf this.setAggregateModelsName((String) additionalProperties.get(AGGREGATE_MODELS_NAME)); } + if (additionalProperties.containsKey(this.SUPPORT_MULTIPLE_RESPONSES)) { + this.supportMultipleResponses = convertPropertyToBooleanAndWriteBack(SUPPORT_MULTIPLE_RESPONSES); + } else { + additionalProperties.put(this.SUPPORT_MULTIPLE_RESPONSES, this.supportMultipleResponses); + } + supportingFiles.add(new SupportingFile("README.mustache", "", "README.md")); } @@ -442,7 +453,6 @@ public class ProtobufSchemaCodegen extends DefaultCodegen implements CodegenConf } else if (ModelUtils.isAnyOf(schema)) { wrapComposedChildren(schema.getAnyOf(), visitedSchema); } - } } @@ -475,7 +485,6 @@ public class ProtobufSchemaCodegen extends DefaultCodegen implements CodegenConf } } - /** * Adds prefix to the enum allowable values * NOTE: Enum values use C++ scoping rules, meaning that enum values are siblings of their type, not children of it. Therefore, enum value must be unique @@ -522,7 +531,9 @@ public class ProtobufSchemaCodegen extends DefaultCodegen implements CodegenConf if (allowableValues.containsKey("values")) { List values = (List) allowableValues.get("values"); - values.add(0, "UNSPECIFIED"); + List modifiableValues = new ArrayList<>(values); + modifiableValues.add(0, "UNSPECIFIED"); + allowableValues.put("values", modifiableValues); } } } @@ -542,24 +553,28 @@ public class ProtobufSchemaCodegen extends DefaultCodegen implements CodegenConf public List processOneOfAnyOfItems(List composedSchemasProperty) { for(CodegenProperty cd: composedSchemasProperty) { - if (cd.getTitle() != null) { - cd.name = cd.getTitle(); - cd.baseName = cd.getTitle(); - } else{ - cd.name = getNameFromDataType(cd); - cd.baseName = getNameFromDataType(cd); - } + cd.name = resolveVarName(cd); + cd.baseName = resolveVarName(cd); } return composedSchemasProperty; } + + private String resolveVarName(CodegenProperty property) { + if(property.getTitle() != null) { + return toVarName(property.getTitle()); + } else { + return getNameFromDataType(property); + } + } + public String getNameFromDataType(CodegenProperty property) { if (Boolean.TRUE.equals(property.getIsArray())){ - return underscore(property.mostInnerItems.dataType + ARRAY_SUFFIX); + return toVarName(property.mostInnerItems.dataType + ARRAY_SUFFIX); } else if (Boolean.TRUE.equals(property.getIsMap())) { - return underscore(property.mostInnerItems.dataType + MAP_SUFFIX); + return toVarName(property.mostInnerItems.dataType + MAP_SUFFIX); } else { - return underscore(property.dataType); + return toVarName(property.dataType); } } @@ -944,12 +959,41 @@ public class ProtobufSchemaCodegen extends DefaultCodegen implements CodegenConf } } } + + if(this.supportMultipleResponses) { + int responseIdx = 1; + op.vendorExtensions.put("x-grpc-response", op.operationId+"Response"); + for (CodegenResponse r : op.responses) { + if (r.returnProperty == null) { + r.vendorExtensions.put("x-oneOf-response-type", "google.protobuf.Empty"); + r.vendorExtensions.put("x-oneOf-response-name", "empty"); + } else if (r.isMap && r.additionalProperties != null) { + r.vendorExtensions.put("x-oneOf-response-type", r.returnProperty.additionalProperties.dataType); + r.vendorExtensions.put("x-oneOf-response-name", resolveVarName(r.returnProperty.additionalProperties)); + LOGGER.warn("Mapping responses for operations with supportMultipleResponses flag (operation ID: {}) is not currently supported.", op.operationId); + } else if (r.isArray && r.items != null) { + r.vendorExtensions.put("x-oneOf-response-type", r.returnProperty.items.dataType); + r.vendorExtensions.put("x-oneOf-response-name", resolveVarName(r.returnProperty.items)); + LOGGER.warn("Array responses for operations with supportMultipleResponses flag (operation ID: {}) is not currently supported.", op.operationId); + } + else { + r.vendorExtensions.put("x-oneOf-response-type", r.returnProperty.dataType); + r.vendorExtensions.put("x-oneOf-response-name", resolveVarName(r.returnProperty)); + } + r.vendorExtensions.put("x-oneOf-response-index", responseIdx++); + } + } } if (this.aggregateModelsName != null) { + List> imports = objs.getImports().stream() + .filter(importMap -> !importMap.get("import").startsWith("models/")) + .collect(Collectors.toList()); + List> aggregate_imports = Collections.singletonList(Collections .singletonMap(IMPORT, toModelImport(this.aggregateModelsName))); - objs.setImports(aggregate_imports); + imports.addAll(aggregate_imports); + objs.setImports(imports); } return objs; } diff --git a/modules/openapi-generator/src/main/resources/protobuf-schema/api.mustache b/modules/openapi-generator/src/main/resources/protobuf-schema/api.mustache index cab1a8ac794..8f804af4b6c 100644 --- a/modules/openapi-generator/src/main/resources/protobuf-schema/api.mustache +++ b/modules/openapi-generator/src/main/resources/protobuf-schema/api.mustache @@ -42,5 +42,14 @@ message {{operationId}}Response { } {{/vendorExtensions.x-grpc-response}} +{{#supportMultipleResponses}} +message {{operationId}}Response { + oneof response { + {{#responses}} + {{{vendorExtensions.x-oneOf-response-type}}} {{vendorExtensions.x-oneOf-response-name}}_{{code}} = {{vendorExtensions.x-oneOf-response-index}}; + {{/responses}} + } +} +{{/supportMultipleResponses}} {{/operation}} {{/operations}} diff --git a/modules/openapi-generator/src/test/resources/3_0/protobuf/petstore.yaml b/modules/openapi-generator/src/test/resources/3_0/protobuf/petstore.yaml index 01b109bdad1..1173ca9591b 100644 --- a/modules/openapi-generator/src/test/resources/3_0/protobuf/petstore.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/protobuf/petstore.yaml @@ -59,7 +59,7 @@ paths: schema: $ref: '#/components/schemas/Pet' '405': - description: Invalid input + $ref: '#/components/responses/ErrorResponse' security: - petstore_auth: - 'write:pets' @@ -83,11 +83,11 @@ paths: schema: $ref: '#/components/schemas/Pet' '400': - description: Invalid ID supplied + $ref: '#/components/responses/ErrorResponse' '404': - description: Pet not found + $ref: '#/components/responses/ErrorResponse' '405': - description: Validation exception + $ref: '#/components/responses/ErrorResponse' security: - petstore_auth: - 'write:pets' @@ -593,6 +593,13 @@ externalDocs: description: Find out more about Swagger url: 'http://swagger.io' components: + responses: + ErrorResponse: + description: An error response. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' requestBodies: UserArray: content: @@ -786,3 +793,16 @@ components: uniqueItems: true items: type: string + Error: + type: object + required: + - code + - message + properties: + code: + type: integer + format: int32 + description: Error code. + message: + type: string + description: Detailed error message. diff --git a/samples/config/petstore/protobuf-schema-config-complex/services/default_service.proto b/samples/config/petstore/protobuf-schema-config-complex/services/default_service.proto index 58b52c6724c..86c13ed3514 100644 --- a/samples/config/petstore/protobuf-schema-config-complex/services/default_service.proto +++ b/samples/config/petstore/protobuf-schema-config-complex/services/default_service.proto @@ -16,7 +16,7 @@ import "google/protobuf/empty.proto"; import public "models/cat.proto"; service DefaultService { - rpc PetsGet (PetsGetRequest) returns (google.protobuf.Empty); + rpc PetsGet (PetsGetRequest) returns (PetsGetResponse); } @@ -25,3 +25,8 @@ message PetsGetRequest { } +message PetsGetResponse { + oneof response { + google.protobuf.Empty empty_200 = 1; + } +} diff --git a/samples/config/petstore/protobuf-schema-config/models/data.proto b/samples/config/petstore/protobuf-schema-config/models/data.proto index 0a3854cdef4..946e1d2c109 100644 --- a/samples/config/petstore/protobuf-schema-config/models/data.proto +++ b/samples/config/petstore/protobuf-schema-config/models/data.proto @@ -56,6 +56,16 @@ message Dog { } +message Error { + + // Error code. + int32 code = 1; + + // Detailed error message. + string message = 2; + +} + message Order { int64 id = 1; diff --git a/samples/config/petstore/protobuf-schema/.openapi-generator/FILES b/samples/config/petstore/protobuf-schema/.openapi-generator/FILES index f4da595c7e4..0508069e665 100644 --- a/samples/config/petstore/protobuf-schema/.openapi-generator/FILES +++ b/samples/config/petstore/protobuf-schema/.openapi-generator/FILES @@ -3,6 +3,7 @@ models/api_response.proto models/cat.proto models/category.proto models/dog.proto +models/error.proto models/order.proto models/other_test.proto models/pet.proto diff --git a/samples/config/petstore/protobuf-schema/models/error.proto b/samples/config/petstore/protobuf-schema/models/error.proto new file mode 100644 index 00000000000..9eda174c958 --- /dev/null +++ b/samples/config/petstore/protobuf-schema/models/error.proto @@ -0,0 +1,25 @@ +/* + OpenAPI Petstore + + This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. + + The version of the OpenAPI document: 1.0.0 + + Generated by OpenAPI Generator: https://openapi-generator.tech +*/ + +syntax = "proto3"; + +package petstore; + + +message Error { + + // Error code. + int32 code = 3059181; + + // Detailed error message. + string message = 418054152; + +} + diff --git a/samples/config/petstore/protobuf-schema/services/default_service.proto b/samples/config/petstore/protobuf-schema/services/default_service.proto index d04c2d4cedd..3d991d871a9 100644 --- a/samples/config/petstore/protobuf-schema/services/default_service.proto +++ b/samples/config/petstore/protobuf-schema/services/default_service.proto @@ -17,9 +17,9 @@ import public "models/pets_get_request.proto"; import public "models/pets_post_request.proto"; service DefaultService { - rpc PetsGet (PetsGetRequest) returns (google.protobuf.Empty); + rpc PetsGet (PetsGetRequest) returns (PetsGetResponse); - rpc PetsPost (PetsPostRequest) returns (google.protobuf.Empty); + rpc PetsPost (PetsPostRequest) returns (PetsPostResponse); } @@ -28,8 +28,18 @@ message PetsGetRequest { } +message PetsGetResponse { + oneof response { + google.protobuf.Empty empty_200 = 1; + } +} message PetsPostRequest { PetsPostRequest pets_post_request = 1; } +message PetsPostResponse { + oneof response { + google.protobuf.Empty empty_200 = 1; + } +} diff --git a/samples/config/petstore/protobuf-schema/services/pet_service.proto b/samples/config/petstore/protobuf-schema/services/pet_service.proto index f8938c757d0..370307fde14 100644 --- a/samples/config/petstore/protobuf-schema/services/pet_service.proto +++ b/samples/config/petstore/protobuf-schema/services/pet_service.proto @@ -14,24 +14,25 @@ package petstore.services.petservice; import "google/protobuf/empty.proto"; import public "models/api_response.proto"; +import public "models/error.proto"; import public "models/pet.proto"; service PetService { - rpc AddPet (AddPetRequest) returns (Pet); + rpc AddPet (AddPetRequest) returns (AddPetResponse); - rpc DeletePet (DeletePetRequest) returns (google.protobuf.Empty); + rpc DeletePet (DeletePetRequest) returns (DeletePetResponse); rpc FindPetsByStatus (FindPetsByStatusRequest) returns (FindPetsByStatusResponse); rpc FindPetsByTags (FindPetsByTagsRequest) returns (FindPetsByTagsResponse); - rpc GetPetById (GetPetByIdRequest) returns (Pet); + rpc GetPetById (GetPetByIdRequest) returns (GetPetByIdResponse); - rpc UpdatePet (UpdatePetRequest) returns (Pet); + rpc UpdatePet (UpdatePetRequest) returns (UpdatePetResponse); - rpc UpdatePetWithForm (UpdatePetWithFormRequest) returns (google.protobuf.Empty); + rpc UpdatePetWithForm (UpdatePetWithFormRequest) returns (UpdatePetWithFormResponse); - rpc UploadFile (UploadFileRequest) returns (ApiResponse); + rpc UploadFile (UploadFileRequest) returns (UploadFileResponse); } @@ -41,6 +42,12 @@ message AddPetRequest { } +message AddPetResponse { + oneof response { + Pet pet_200 = 1; + Error error_405 = 2; + } +} message DeletePetRequest { // Pet id to delete int64 pet_id = 1; @@ -48,6 +55,11 @@ message DeletePetRequest { } +message DeletePetResponse { + oneof response { + google.protobuf.Empty empty_400 = 1; + } +} message FindPetsByStatusRequest { // Status values that need to be considered for filter repeated string status = 1; @@ -55,9 +67,11 @@ message FindPetsByStatusRequest { } message FindPetsByStatusResponse { - repeated Pet data = 1; + oneof response { + Pet pet_200 = 1; + google.protobuf.Empty empty_400 = 2; + } } - message FindPetsByTagsRequest { // Tags to filter by repeated string tags = 1; @@ -65,21 +79,38 @@ message FindPetsByTagsRequest { } message FindPetsByTagsResponse { - repeated Pet data = 1; + oneof response { + Pet pet_200 = 1; + google.protobuf.Empty empty_400 = 2; + } } - message GetPetByIdRequest { // ID of pet to return int64 pet_id = 1; } +message GetPetByIdResponse { + oneof response { + Pet pet_200 = 1; + google.protobuf.Empty empty_400 = 2; + google.protobuf.Empty empty_404 = 3; + } +} message UpdatePetRequest { // Pet object that needs to be added to the store Pet pet = 1; } +message UpdatePetResponse { + oneof response { + Pet pet_200 = 1; + Error error_400 = 2; + Error error_404 = 3; + Error error_405 = 4; + } +} message UpdatePetWithFormRequest { // ID of pet that needs to be updated int64 pet_id = 1; @@ -90,6 +121,11 @@ message UpdatePetWithFormRequest { } +message UpdatePetWithFormResponse { + oneof response { + google.protobuf.Empty empty_405 = 1; + } +} message UploadFileRequest { // ID of pet to update int64 pet_id = 1; @@ -100,3 +136,8 @@ message UploadFileRequest { } +message UploadFileResponse { + oneof response { + ApiResponse api_response_200 = 1; + } +} diff --git a/samples/config/petstore/protobuf-schema/services/store_service.proto b/samples/config/petstore/protobuf-schema/services/store_service.proto index 4fc08377623..29632ba3908 100644 --- a/samples/config/petstore/protobuf-schema/services/store_service.proto +++ b/samples/config/petstore/protobuf-schema/services/store_service.proto @@ -16,13 +16,13 @@ import "google/protobuf/empty.proto"; import public "models/order.proto"; service StoreService { - rpc DeleteOrder (DeleteOrderRequest) returns (google.protobuf.Empty); + rpc DeleteOrder (DeleteOrderRequest) returns (DeleteOrderResponse); rpc GetInventory (google.protobuf.Empty) returns (GetInventoryResponse); - rpc GetOrderById (GetOrderByIdRequest) returns (Order); + rpc GetOrderById (GetOrderByIdRequest) returns (GetOrderByIdResponse); - rpc PlaceOrder (PlaceOrderRequest) returns (Order); + rpc PlaceOrder (PlaceOrderRequest) returns (PlaceOrderResponse); } @@ -32,19 +32,39 @@ message DeleteOrderRequest { } -message GetInventoryResponse { - int32 data = 1; +message DeleteOrderResponse { + oneof response { + google.protobuf.Empty empty_400 = 1; + google.protobuf.Empty empty_404 = 2; + } +} +message GetInventoryResponse { + oneof response { + int32 int32_200 = 1; + } } - message GetOrderByIdRequest { // ID of pet that needs to be fetched int64 order_id = 1; } +message GetOrderByIdResponse { + oneof response { + Order order_200 = 1; + google.protobuf.Empty empty_400 = 2; + google.protobuf.Empty empty_404 = 3; + } +} message PlaceOrderRequest { // order placed for purchasing the pet Order order = 1; } +message PlaceOrderResponse { + oneof response { + Order order_200 = 1; + google.protobuf.Empty empty_400 = 2; + } +} diff --git a/samples/config/petstore/protobuf-schema/services/user_service.proto b/samples/config/petstore/protobuf-schema/services/user_service.proto index 119219d6fc8..688de9d4896 100644 --- a/samples/config/petstore/protobuf-schema/services/user_service.proto +++ b/samples/config/petstore/protobuf-schema/services/user_service.proto @@ -16,21 +16,21 @@ import "google/protobuf/empty.proto"; import public "models/user.proto"; service UserService { - rpc CreateUser (CreateUserRequest) returns (google.protobuf.Empty); + rpc CreateUser (CreateUserRequest) returns (CreateUserResponse); - rpc CreateUsersWithArrayInput (CreateUsersWithArrayInputRequest) returns (google.protobuf.Empty); + rpc CreateUsersWithArrayInput (CreateUsersWithArrayInputRequest) returns (CreateUsersWithArrayInputResponse); - rpc CreateUsersWithListInput (CreateUsersWithListInputRequest) returns (google.protobuf.Empty); + rpc CreateUsersWithListInput (CreateUsersWithListInputRequest) returns (CreateUsersWithListInputResponse); - rpc DeleteUser (DeleteUserRequest) returns (google.protobuf.Empty); + rpc DeleteUser (DeleteUserRequest) returns (DeleteUserResponse); - rpc GetUserByName (GetUserByNameRequest) returns (User); + rpc GetUserByName (GetUserByNameRequest) returns (GetUserByNameResponse); rpc LoginUser (LoginUserRequest) returns (LoginUserResponse); - rpc LogoutUser (google.protobuf.Empty) returns (google.protobuf.Empty); + rpc LogoutUser (google.protobuf.Empty) returns (LogoutUserResponse); - rpc UpdateUser (UpdateUserRequest) returns (google.protobuf.Empty); + rpc UpdateUser (UpdateUserRequest) returns (UpdateUserResponse); } @@ -40,30 +40,58 @@ message CreateUserRequest { } +message CreateUserResponse { + oneof response { + google.protobuf.Empty empty_0 = 1; + } +} message CreateUsersWithArrayInputRequest { // List of user object repeated User user = 1; } +message CreateUsersWithArrayInputResponse { + oneof response { + google.protobuf.Empty empty_0 = 1; + } +} message CreateUsersWithListInputRequest { // List of user object repeated User user = 1; } +message CreateUsersWithListInputResponse { + oneof response { + google.protobuf.Empty empty_0 = 1; + } +} message DeleteUserRequest { // The name that needs to be deleted string username = 1; } +message DeleteUserResponse { + oneof response { + google.protobuf.Empty empty_400 = 1; + google.protobuf.Empty empty_404 = 2; + } +} message GetUserByNameRequest { // The name that needs to be fetched. Use user1 for testing. string username = 1; } +message GetUserByNameResponse { + oneof response { + User user_200 = 1; + google.protobuf.Empty empty_400 = 2; + google.protobuf.Empty empty_404 = 3; + } +} message LoginUserRequest { // The user name for login string username = 1; @@ -73,9 +101,16 @@ message LoginUserRequest { } message LoginUserResponse { - string data = 1; + oneof response { + string string_200 = 1; + google.protobuf.Empty empty_400 = 2; + } +} +message LogoutUserResponse { + oneof response { + google.protobuf.Empty empty_0 = 1; + } } - message UpdateUserRequest { // name that need to be deleted string username = 1; @@ -84,3 +119,9 @@ message UpdateUserRequest { } +message UpdateUserResponse { + oneof response { + google.protobuf.Empty empty_400 = 1; + google.protobuf.Empty empty_404 = 2; + } +}