diff --git a/golden-file-tests/apidocs_golden.swagger.json b/golden-file-tests/apidocs_golden.swagger.json index f4df434f4f0..97e73f4bdf8 100644 --- a/golden-file-tests/apidocs_golden.swagger.json +++ b/golden-file-tests/apidocs_golden.swagger.json @@ -6,53 +6,34 @@ }, "paths": { "/v1/things": { - "get": { + "post": { "externalDocs": { "url": "" }, - "operationId": "ThingService_ListThings", - "parameters": [ - { - "in": "query", - "name": "filter", - "required": false, - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "page_size", - "required": false, - "schema": { - "format": "int32", - "type": "integer" - } - }, - { - "in": "query", - "name": "skip", - "required": false, - "schema": { - "format": "int32", - "type": "integer" - } - }, - { - "in": "query", - "name": "page_token", - "required": false, - "schema": { - "type": "string" + "operationId": "ThingService_CreateThing", + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/CreateThingRequestVariantB" + }, + { + "$ref": "#/components/schemas/CreateThingRequestVariantA" + } + ], + "type": "object" + } } } - ], + }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ListThingsResponse" + "$ref": "#/components/schemas/CreateThingResponse" } } }, @@ -77,7 +58,7 @@ "description": "Internal server error" } }, - "summary": "List Things", + "summary": "Create Thing", "tags": [ "Things Service" ] @@ -135,68 +116,54 @@ ] } }, - "/v1/things/{thing.id}": { - "put": { + "/v1/things/{page_size}": { + "get": { "externalDocs": { "url": "" }, - "operationId": "ThingService_ReplaceThing", + "operationId": "ThingService_ListThings", "parameters": [ { "in": "path", - "name": "thing.id", + "name": "page_size", "required": true, "schema": { - "example": "123e4567-e89b-12d3-a456-426614174000", + "format": "int32", + "type": "integer" + } + }, + { + "in": "query", + "name": "filter", + "required": false, + "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "additionalProperties": false, - "properties": { - "thing": { - "additionalProperties": false, - "properties": { - "createdAt": { - "example": "2021-01-01T00:00:00Z", - "format": "date-time", - "type": "string" - }, - "name": { - "example": "A Thing's Name", - "type": "string" - }, - "nestedField": { - "properties": { - "numericField": { - "format": "int32", - "type": "integer" - }, - "stringField": { - "type": "string" - } - }, - "type": "object" - } - }, - "type": "object" - } - }, - "type": "object" - } + }, + { + "in": "query", + "name": "skip", + "required": false, + "schema": { + "format": "int32", + "type": "integer" + } + }, + { + "in": "query", + "name": "page_token", + "required": false, + "schema": { + "type": "string" } } - }, + ], "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ReplaceThingResponse" + "$ref": "#/components/schemas/ListThingsResponse" } } }, @@ -221,53 +188,23 @@ "description": "Internal server error" } }, - "summary": "Replace Thing", + "summary": "List Things", "tags": [ "Things Service" ] } }, - "/v1/things/{thing.nested_field.numeric_field}": { - "post": { + "/v1/things/{thing_id}": { + "put": { "externalDocs": { "url": "" }, - "operationId": "ThingService_CreateThing", + "operationId": "ThingService_ReplaceThing", "parameters": [ { "in": "path", - "name": "thing.nested_field.numeric_field", + "name": "thing_id", "required": true, - "schema": { - "format": "int32", - "type": "integer" - } - }, - { - "in": "query", - "name": "thing", - "required": false, - "schema": { - "additionalProperties": false, - "properties": { - "createdAt": { - "example": "2021-01-01T00:00:00Z", - "format": "date-time", - "type": "string" - }, - "id": { - "example": "123e4567-e89b-12d3-a456-426614174000", - "type": "string" - }, - "nestedField": {} - }, - "type": "object" - } - }, - { - "in": "query", - "name": "a_query_parameter", - "required": false, "schema": { "type": "string" } @@ -277,13 +214,14 @@ "content": { "application/json": { "schema": { - "additionalProperties": false, - "properties": { - "name": { - "example": "A Thing's Name", - "type": "string" + "oneOf": [ + { + "$ref": "#/components/schemas/ThingVariantA" + }, + { + "$ref": "#/components/schemas/ThingVariantB" } - }, + ], "type": "object" } } @@ -294,7 +232,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateThingResponse" + "$ref": "#/components/schemas/ReplaceThingResponse" } } }, @@ -319,13 +257,11 @@ "description": "Internal server error" } }, - "summary": "Create Thing", + "summary": "Replace Thing", "tags": [ "Things Service" ] - } - }, - "/v1/things/{thing_id}": { + }, "delete": { "externalDocs": { "url": "" @@ -411,13 +347,66 @@ "type": "string" }, "CreateThingRequest": { + "oneOf": [ + { + "$ref": "#/components/schemas/CreateThingRequestVariantB" + }, + { + "$ref": "#/components/schemas/CreateThingRequestVariantA" + } + ] + }, + "CreateThingRequestVariantA": { "type": "object", "properties": { - "aQueryParameter": { - "type": "string" + "createdAt": { + "type": "string", + "format": "date-time", + "example": "2021-01-01T00:00:00Z" }, - "thing": { - "$ref": "#/components/schemas/Thing" + "id": { + "type": "string", + "example": "123e4567-e89b-12d3-a456-426614174000" + }, + "name": { + "type": "string", + "example": "A Thing's Name" + }, + "nestedField": { + "$ref": "#/components/schemas/NestedStruct" + }, + "thingType": { + "$ref": "#/components/schemas/ThingType" + }, + "variantA": { + "$ref": "#/components/schemas/PolymorphicVariantA" + } + } + }, + "CreateThingRequestVariantB": { + "type": "object", + "properties": { + "createdAt": { + "type": "string", + "format": "date-time", + "example": "2021-01-01T00:00:00Z" + }, + "id": { + "type": "string", + "example": "123e4567-e89b-12d3-a456-426614174000" + }, + "name": { + "type": "string", + "example": "A Thing's Name" + }, + "nestedField": { + "$ref": "#/components/schemas/NestedStruct" + }, + "thingType": { + "$ref": "#/components/schemas/ThingType" + }, + "variantB": { + "$ref": "#/components/schemas/PolymorphicVariantB" } } }, @@ -456,6 +445,144 @@ } } }, + "HttpRuleCustom": { + "type": "object", + "properties": { + "additionalBindings": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HttpRule" + } + }, + "body": { + "type": "string" + }, + "custom": { + "$ref": "#/components/schemas/CustomHttpPattern" + }, + "responseBody": { + "type": "string" + }, + "selector": { + "type": "string" + } + } + }, + "HttpRuleDelete": { + "type": "object", + "properties": { + "additionalBindings": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HttpRule" + } + }, + "body": { + "type": "string" + }, + "delete": { + "type": "string" + }, + "responseBody": { + "type": "string" + }, + "selector": { + "type": "string" + } + } + }, + "HttpRuleGet": { + "type": "object", + "properties": { + "additionalBindings": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HttpRule" + } + }, + "body": { + "type": "string" + }, + "get": { + "type": "string" + }, + "responseBody": { + "type": "string" + }, + "selector": { + "type": "string" + } + } + }, + "HttpRulePatch": { + "type": "object", + "properties": { + "additionalBindings": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HttpRule" + } + }, + "body": { + "type": "string" + }, + "patch": { + "type": "string" + }, + "responseBody": { + "type": "string" + }, + "selector": { + "type": "string" + } + } + }, + "HttpRulePost": { + "type": "object", + "properties": { + "additionalBindings": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HttpRule" + } + }, + "body": { + "type": "string" + }, + "post": { + "type": "string" + }, + "responseBody": { + "type": "string" + }, + "selector": { + "type": "string" + } + } + }, + "HttpRulePut": { + "type": "object", + "properties": { + "additionalBindings": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HttpRule" + } + }, + "body": { + "type": "string" + }, + "put": { + "type": "string" + }, + "responseBody": { + "type": "string" + }, + "selector": { + "type": "string" + } + } + }, "ListThingsRequest": { "type": "object", "properties": { @@ -473,8 +600,7 @@ "type": "integer", "format": "int32" } - }, - "additionalProperties": false + } }, "ListThingsResponse": { "type": "object", @@ -492,8 +618,7 @@ "type": "integer", "format": "int32" } - }, - "additionalProperties": false + } }, "NestedStruct": { "type": "object", @@ -507,11 +632,31 @@ } } }, + "PolymorphicVariantA": { + "type": "object", + "properties": { + "fieldA": { + "type": "string" + } + } + }, + "PolymorphicVariantB": { + "type": "object", + "properties": { + "fieldB": { + "type": "integer", + "format": "int32" + } + } + }, "ReplaceThingRequest": { "type": "object", "properties": { "thing": { "$ref": "#/components/schemas/Thing" + }, + "thingId": { + "type": "string" } } }, @@ -542,6 +687,26 @@ } }, "Thing": { + "oneOf": [ + { + "$ref": "#/components/schemas/ThingVariantA" + }, + { + "$ref": "#/components/schemas/ThingVariantB" + } + ] + }, + "ThingType": { + "title": "ThingType", + "enum": [ + "THING_TYPE_UNSPECIFIED", + "THING_TYPE_STANDARD", + "THING_TYPE_PREMIUM" + ], + "type": "string", + "description": "The type of the thing." + }, + "ThingVariantA": { "type": "object", "properties": { "createdAt": { @@ -559,19 +724,41 @@ }, "nestedField": { "$ref": "#/components/schemas/NestedStruct" + }, + "thingType": { + "$ref": "#/components/schemas/ThingType" + }, + "variantA": { + "$ref": "#/components/schemas/PolymorphicVariantA" } - }, - "additionalProperties": false + } }, - "ThingType": { - "title": "ThingType", - "enum": [ - "THING_TYPE_UNSPECIFIED", - "THING_TYPE_STANDARD", - "THING_TYPE_PREMIUM" - ], - "type": "string", - "description": "The type of the thing." + "ThingVariantB": { + "type": "object", + "properties": { + "createdAt": { + "type": "string", + "format": "date-time", + "example": "2021-01-01T00:00:00Z" + }, + "id": { + "type": "string", + "example": "123e4567-e89b-12d3-a456-426614174000" + }, + "name": { + "type": "string", + "example": "A Thing's Name" + }, + "nestedField": { + "$ref": "#/components/schemas/NestedStruct" + }, + "thingType": { + "$ref": "#/components/schemas/ThingType" + }, + "variantB": { + "$ref": "#/components/schemas/PolymorphicVariantB" + } + } } } }, diff --git a/golden-file-tests/apidocs_golden_preview_visibility.swagger.json b/golden-file-tests/apidocs_golden_preview_visibility.swagger.json index bde6d3bf2ab..be0f0b820d4 100644 --- a/golden-file-tests/apidocs_golden_preview_visibility.swagger.json +++ b/golden-file-tests/apidocs_golden_preview_visibility.swagger.json @@ -6,53 +6,34 @@ }, "paths": { "/v1/things": { - "get": { + "post": { "externalDocs": { "url": "" }, - "operationId": "ThingService_ListThings", - "parameters": [ - { - "in": "query", - "name": "filter", - "required": false, - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "page_size", - "required": false, - "schema": { - "format": "int32", - "type": "integer" - } - }, - { - "in": "query", - "name": "skip", - "required": false, - "schema": { - "format": "int32", - "type": "integer" - } - }, - { - "in": "query", - "name": "page_token", - "required": false, - "schema": { - "type": "string" + "operationId": "ThingService_CreateThing", + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/CreateThingRequestVariantB" + }, + { + "$ref": "#/components/schemas/CreateThingRequestVariantA" + } + ], + "type": "object" + } } } - ], + }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ListThingsResponse" + "$ref": "#/components/schemas/CreateThingResponse" } } }, @@ -77,7 +58,7 @@ "description": "Internal server error" } }, - "summary": "List Things", + "summary": "Create Thing", "tags": [ "Things Service" ] @@ -89,51 +70,18 @@ "url": "" }, "operationId": "ThingService_PreviewMethod", - "parameters": [ - { - "in": "query", - "name": "thing", - "required": false, - "schema": { - "additionalProperties": false, - "properties": { - "createdAt": { - "example": "2021-01-01T00:00:00Z", - "format": "date-time", - "type": "string" - }, - "id": { - "example": "123e4567-e89b-12d3-a456-426614174000", - "type": "string" - }, - "nestedField": {}, - "previewField": { - "type": "string" - } - }, - "type": "object" - } - }, - { - "in": "query", - "name": "a_query_parameter", - "required": false, - "schema": { - "type": "string" - } - } - ], "requestBody": { "content": { "application/json": { "schema": { - "additionalProperties": false, - "properties": { - "name": { - "example": "A Thing's Name", - "type": "string" + "oneOf": [ + { + "$ref": "#/components/schemas/CreateThingRequestVariantA" + }, + { + "$ref": "#/components/schemas/CreateThingRequestVariantB" } - }, + ], "type": "object" } } @@ -227,71 +175,54 @@ ] } }, - "/v1/things/{thing.id}": { - "put": { + "/v1/things/{page_size}": { + "get": { "externalDocs": { "url": "" }, - "operationId": "ThingService_ReplaceThing", + "operationId": "ThingService_ListThings", "parameters": [ { "in": "path", - "name": "thing.id", + "name": "page_size", "required": true, "schema": { - "example": "123e4567-e89b-12d3-a456-426614174000", + "format": "int32", + "type": "integer" + } + }, + { + "in": "query", + "name": "filter", + "required": false, + "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "additionalProperties": false, - "properties": { - "thing": { - "additionalProperties": false, - "properties": { - "createdAt": { - "example": "2021-01-01T00:00:00Z", - "format": "date-time", - "type": "string" - }, - "name": { - "example": "A Thing's Name", - "type": "string" - }, - "nestedField": { - "properties": { - "numericField": { - "format": "int32", - "type": "integer" - }, - "stringField": { - "type": "string" - } - }, - "type": "object" - }, - "previewField": { - "type": "string" - } - }, - "type": "object" - } - }, - "type": "object" - } + }, + { + "in": "query", + "name": "skip", + "required": false, + "schema": { + "format": "int32", + "type": "integer" + } + }, + { + "in": "query", + "name": "page_token", + "required": false, + "schema": { + "type": "string" } } - }, + ], "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ReplaceThingResponse" + "$ref": "#/components/schemas/ListThingsResponse" } } }, @@ -316,56 +247,23 @@ "description": "Internal server error" } }, - "summary": "Replace Thing", + "summary": "List Things", "tags": [ "Things Service" ] } }, - "/v1/things/{thing.nested_field.numeric_field}": { - "post": { + "/v1/things/{thing_id}": { + "put": { "externalDocs": { "url": "" }, - "operationId": "ThingService_CreateThing", + "operationId": "ThingService_ReplaceThing", "parameters": [ { "in": "path", - "name": "thing.nested_field.numeric_field", + "name": "thing_id", "required": true, - "schema": { - "format": "int32", - "type": "integer" - } - }, - { - "in": "query", - "name": "thing", - "required": false, - "schema": { - "additionalProperties": false, - "properties": { - "createdAt": { - "example": "2021-01-01T00:00:00Z", - "format": "date-time", - "type": "string" - }, - "id": { - "example": "123e4567-e89b-12d3-a456-426614174000", - "type": "string" - }, - "nestedField": {}, - "previewField": { - "type": "string" - } - }, - "type": "object" - } - }, - { - "in": "query", - "name": "a_query_parameter", - "required": false, "schema": { "type": "string" } @@ -375,13 +273,14 @@ "content": { "application/json": { "schema": { - "additionalProperties": false, - "properties": { - "name": { - "example": "A Thing's Name", - "type": "string" + "oneOf": [ + { + "$ref": "#/components/schemas/ThingVariantA" + }, + { + "$ref": "#/components/schemas/ThingVariantB" } - }, + ], "type": "object" } } @@ -392,7 +291,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateThingResponse" + "$ref": "#/components/schemas/ReplaceThingResponse" } } }, @@ -417,13 +316,11 @@ "description": "Internal server error" } }, - "summary": "Create Thing", + "summary": "Replace Thing", "tags": [ "Things Service" ] - } - }, - "/v1/things/{thing_id}": { + }, "delete": { "externalDocs": { "url": "" @@ -509,13 +406,72 @@ "type": "string" }, "CreateThingRequest": { + "oneOf": [ + { + "$ref": "#/components/schemas/CreateThingRequestVariantA" + }, + { + "$ref": "#/components/schemas/CreateThingRequestVariantB" + } + ] + }, + "CreateThingRequestVariantA": { "type": "object", "properties": { - "aQueryParameter": { + "createdAt": { + "type": "string", + "format": "date-time", + "example": "2021-01-01T00:00:00Z" + }, + "id": { + "type": "string", + "example": "123e4567-e89b-12d3-a456-426614174000" + }, + "name": { + "type": "string", + "example": "A Thing's Name" + }, + "nestedField": { + "$ref": "#/components/schemas/NestedStruct" + }, + "previewField": { "type": "string" }, - "thing": { - "$ref": "#/components/schemas/Thing" + "thingType": { + "$ref": "#/components/schemas/ThingType" + }, + "variantA": { + "$ref": "#/components/schemas/PolymorphicVariantA" + } + } + }, + "CreateThingRequestVariantB": { + "type": "object", + "properties": { + "createdAt": { + "type": "string", + "format": "date-time", + "example": "2021-01-01T00:00:00Z" + }, + "id": { + "type": "string", + "example": "123e4567-e89b-12d3-a456-426614174000" + }, + "name": { + "type": "string", + "example": "A Thing's Name" + }, + "nestedField": { + "$ref": "#/components/schemas/NestedStruct" + }, + "previewField": { + "type": "string" + }, + "thingType": { + "$ref": "#/components/schemas/ThingType" + }, + "variantB": { + "$ref": "#/components/schemas/PolymorphicVariantB" } } }, @@ -554,6 +510,144 @@ } } }, + "HttpRuleCustom": { + "type": "object", + "properties": { + "additionalBindings": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HttpRule" + } + }, + "body": { + "type": "string" + }, + "custom": { + "$ref": "#/components/schemas/CustomHttpPattern" + }, + "responseBody": { + "type": "string" + }, + "selector": { + "type": "string" + } + } + }, + "HttpRuleDelete": { + "type": "object", + "properties": { + "additionalBindings": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HttpRule" + } + }, + "body": { + "type": "string" + }, + "delete": { + "type": "string" + }, + "responseBody": { + "type": "string" + }, + "selector": { + "type": "string" + } + } + }, + "HttpRuleGet": { + "type": "object", + "properties": { + "additionalBindings": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HttpRule" + } + }, + "body": { + "type": "string" + }, + "get": { + "type": "string" + }, + "responseBody": { + "type": "string" + }, + "selector": { + "type": "string" + } + } + }, + "HttpRulePatch": { + "type": "object", + "properties": { + "additionalBindings": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HttpRule" + } + }, + "body": { + "type": "string" + }, + "patch": { + "type": "string" + }, + "responseBody": { + "type": "string" + }, + "selector": { + "type": "string" + } + } + }, + "HttpRulePost": { + "type": "object", + "properties": { + "additionalBindings": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HttpRule" + } + }, + "body": { + "type": "string" + }, + "post": { + "type": "string" + }, + "responseBody": { + "type": "string" + }, + "selector": { + "type": "string" + } + } + }, + "HttpRulePut": { + "type": "object", + "properties": { + "additionalBindings": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HttpRule" + } + }, + "body": { + "type": "string" + }, + "put": { + "type": "string" + }, + "responseBody": { + "type": "string" + }, + "selector": { + "type": "string" + } + } + }, "ListThingsRequest": { "type": "object", "properties": { @@ -571,8 +665,7 @@ "type": "integer", "format": "int32" } - }, - "additionalProperties": false + } }, "ListThingsResponse": { "type": "object", @@ -590,8 +683,7 @@ "type": "integer", "format": "int32" } - }, - "additionalProperties": false + } }, "NestedStruct": { "type": "object", @@ -605,11 +697,31 @@ } } }, + "PolymorphicVariantA": { + "type": "object", + "properties": { + "fieldA": { + "type": "string" + } + } + }, + "PolymorphicVariantB": { + "type": "object", + "properties": { + "fieldB": { + "type": "integer", + "format": "int32" + } + } + }, "ReplaceThingRequest": { "type": "object", "properties": { "thing": { "$ref": "#/components/schemas/Thing" + }, + "thingId": { + "type": "string" } } }, @@ -640,6 +752,26 @@ } }, "Thing": { + "oneOf": [ + { + "$ref": "#/components/schemas/ThingVariantA" + }, + { + "$ref": "#/components/schemas/ThingVariantB" + } + ] + }, + "ThingType": { + "title": "ThingType", + "enum": [ + "THING_TYPE_UNSPECIFIED", + "THING_TYPE_STANDARD", + "THING_TYPE_PREMIUM" + ], + "type": "string", + "description": "The type of the thing." + }, + "ThingVariantA": { "type": "object", "properties": { "createdAt": { @@ -660,19 +792,44 @@ }, "previewField": { "type": "string" + }, + "thingType": { + "$ref": "#/components/schemas/ThingType" + }, + "variantA": { + "$ref": "#/components/schemas/PolymorphicVariantA" } - }, - "additionalProperties": false + } }, - "ThingType": { - "title": "ThingType", - "enum": [ - "THING_TYPE_UNSPECIFIED", - "THING_TYPE_STANDARD", - "THING_TYPE_PREMIUM" - ], - "type": "string", - "description": "The type of the thing." + "ThingVariantB": { + "type": "object", + "properties": { + "createdAt": { + "type": "string", + "format": "date-time", + "example": "2021-01-01T00:00:00Z" + }, + "id": { + "type": "string", + "example": "123e4567-e89b-12d3-a456-426614174000" + }, + "name": { + "type": "string", + "example": "A Thing's Name" + }, + "nestedField": { + "$ref": "#/components/schemas/NestedStruct" + }, + "previewField": { + "type": "string" + }, + "thingType": { + "$ref": "#/components/schemas/ThingType" + }, + "variantB": { + "$ref": "#/components/schemas/PolymorphicVariantB" + } + } } } }, diff --git a/golden-file-tests/cx-api/build_facade_files_with_preview_visibility.sh b/golden-file-tests/cx-api/build_facade_files_with_preview_visibility.sh new file mode 100755 index 00000000000..8cd281edf48 --- /dev/null +++ b/golden-file-tests/cx-api/build_facade_files_with_preview_visibility.sh @@ -0,0 +1,31 @@ +# Directory containing the protobuf files +mkdir -p proto +cp -a src/** proto/ +cp -a deps/** proto/ +proto_dir="proto" +go_out_dir="internal" +mod_prefix="github.com/coralogix" +mod_name="$mod_prefix/openapi-facade/go" +proto_files=($(find "$proto_dir" -name "*.proto" -print)) +openapi_args="" + +# Build arguments for import paths of all modules +for proto_file in "${proto_files[@]}" +do + out_module=$(dirname $proto_file) + + if [[ $out_module == *"coralogix"* ]]; then + mod_path="${out_module##*/com/}" + # For all other protos, the package path is the same as the directory path + openapi_args+="--openapiv3_opt=M${proto_file##*$proto_dir/}=${mod_name}/${go_out_dir}/${mod_path} " + fi +done + +protofile_list="" + +for proto_file in "${proto_files[@]}" +do + protofile_list+="${proto_file} " +done + +protoc --proto_path=$proto_dir --openapiv3_out=.. --openapiv3_opt=allow_merge=true,ignore_additional_bindings=true,openapi_naming_strategy=simple,visibility_restriction_selectors=PREVIEW $openapi_args $protofile_list diff --git a/golden-file-tests/cx-api/output.txt b/golden-file-tests/cx-api/output.txt deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/golden-file-tests/cx-api/src/com/coralogixapis/thing/v1/thing.proto b/golden-file-tests/cx-api/src/com/coralogixapis/thing/v1/thing.proto index 5667f967dce..9d46a1cff8e 100644 --- a/golden-file-tests/cx-api/src/com/coralogixapis/thing/v1/thing.proto +++ b/golden-file-tests/cx-api/src/com/coralogixapis/thing/v1/thing.proto @@ -19,6 +19,11 @@ message Thing { optional google.protobuf.Timestamp created_at = 4 [(grpc.gateway.protoc_gen_openapiv3.options.openapiv3_field) = {example: "\"2021-01-01T00:00:00Z\""}]; NestedStruct nested_field = 5; string preview_field = 6 [(google.api.field_visibility).restriction = "PREVIEW"]; + ThingType thing_type = 7 [(grpc.gateway.protoc_gen_openapiv3.options.openapiv3_field) = {example: "\"THING_TYPE_STANDARD\""}]; + oneof polymorphic_field { + PolymorphicVariantA variant_a = 8; + PolymorphicVariantB variant_b = 9; + } } message NestedStruct { @@ -26,7 +31,13 @@ message NestedStruct { string string_field = 2; } +message PolymorphicVariantA { + string field_a = 1; +} +message PolymorphicVariantB { + int32 field_b = 1; +} enum ThingType { option (grpc.gateway.protoc_gen_openapiv3.options.openapiv3_enum) = { diff --git a/golden-file-tests/cx-api/src/com/coralogixapis/thing/v1/thing_service.proto b/golden-file-tests/cx-api/src/com/coralogixapis/thing/v1/thing_service.proto index 6deae9a5742..416962ecf4f 100644 --- a/golden-file-tests/cx-api/src/com/coralogixapis/thing/v1/thing_service.proto +++ b/golden-file-tests/cx-api/src/com/coralogixapis/thing/v1/thing_service.proto @@ -6,6 +6,7 @@ import "com/coralogixapis/thing/v1/thing.proto"; import "google/api/annotations.proto"; import "google/api/http.proto"; import "google/api/visibility.proto"; +import "google/protobuf/timestamp.proto"; import "protoc-gen-openapiv3/options/annotations.proto"; // ThingService is the service for managing things. @@ -18,8 +19,8 @@ service ThingService { // RPC for creating a thing. rpc CreateThing(CreateThingRequest) returns (CreateThingResponse) { option (google.api.http) = { - post: "/v1/things/{thing.nested_field.numeric_field}" - body: "thing.name" + post: "/v1/things" + body: "*" }; option (grpc.gateway.protoc_gen_openapiv3.options.openapiv3_operation) = { @@ -44,7 +45,7 @@ service ThingService { rpc PreviewMethod(CreateThingRequest) returns (CreateThingResponse) { option (google.api.http) = { post: "/v1/things/preview" - body: "thing.name" + body: "*" }; option (google.api.method_visibility).restriction = "PREVIEW"; @@ -71,8 +72,8 @@ service ThingService { // RPC for replacing a thing. rpc ReplaceThing(ReplaceThingRequest) returns (ReplaceThingResponse) { option (google.api.http) = { - put: "/v1/things/{thing.id}" - body: "*" + put: "/v1/things/{thing_id}" + body: "thing" }; option (grpc.gateway.protoc_gen_openapiv3.options.openapiv3_operation) = { @@ -139,7 +140,7 @@ service ThingService { // RPC for listing things with pagination. rpc ListThings(ListThingsRequest) returns (ListThingsResponse) { - option (google.api.http) = {get: "/v1/things"}; + option (google.api.http) = {get: "/v1/things/{page_size}"}; option (grpc.gateway.protoc_gen_openapiv3.options.openapiv3_operation) = { responses: { @@ -162,9 +163,21 @@ service ThingService { // CreateThingRequest is the request for the CreateThing RPC. message CreateThingRequest { - // The thing to create. - Thing thing = 1; - string a_query_parameter = 2; + // ID is the unique identifier of the thing. + string id = 1 [(grpc.gateway.protoc_gen_openapiv3.options.openapiv3_field) = {example: "\"123e4567-e89b-12d3-a456-426614174000\""}]; + + // Name of the thing. + string name = 2 [(grpc.gateway.protoc_gen_openapiv3.options.openapiv3_field) = {example: "\"A Thing's Name\""}]; + + // Creation timestamp of the thing. + optional google.protobuf.Timestamp created_at = 4 [(grpc.gateway.protoc_gen_openapiv3.options.openapiv3_field) = {example: "\"2021-01-01T00:00:00Z\""}]; + NestedStruct nested_field = 5; + string preview_field = 6 [(google.api.field_visibility).restriction = "PREVIEW"]; + ThingType thing_type = 7 [(grpc.gateway.protoc_gen_openapiv3.options.openapiv3_field) = {example: "\"THING_TYPE_STANDARD\""}]; + oneof polymorphic_field { + PolymorphicVariantA variant_a = 8; + PolymorphicVariantB variant_b = 9; + } } // CreateThingResponse is the response for the CreateThing RPC. @@ -176,16 +189,16 @@ message CreateThingResponse { // ListThingsRequest is the request for the ListThings RPC. message ListThingsRequest { // The filter. - optional string filter = 1; // use whatever type is actually required + string filter = 1; // use whatever type is actually required // The page size. - optional int32 page_size = 2; + int32 page_size = 2; // The skip count. - optional int32 skip = 3; + int32 skip = 3; // The page token. - optional string page_token = 4; + string page_token = 4; } // ListThingsResponse is the response for the ListThings RPC. @@ -203,6 +216,7 @@ message ListThingsResponse { // ReplaceThingRequest is the request for the ReplaceThing RPC. message ReplaceThingRequest { // The thing to replace. + string thing_id = 2; Thing thing = 1; } diff --git a/golden-file-tests/run_golden_file_test.sh b/golden-file-tests/run_golden_file_test.sh index 102b420bf6f..b0af80971ae 100644 --- a/golden-file-tests/run_golden_file_test.sh +++ b/golden-file-tests/run_golden_file_test.sh @@ -2,10 +2,24 @@ cd cx-api && bash build_facade_files.sh cd .. -if diff <(jq --sort-keys . apidocs_golden.swagger.json) <(jq --sort-keys . apidocs.swagger.json) > /dev/null; then - echo "Plugin output matches golden file." +if diff <(jq --sort-keys . apidocs_golden.swagger.json | jq 'walk(if type == "object" and has("oneOf") and (.oneOf | type) == "array" then .oneOf |= sort_by(."$ref") else . end)') \ + <(jq --sort-keys . apidocs.swagger.json | jq 'walk(if type == "object" and has("oneOf") and (.oneOf | type) == "array" then .oneOf |= sort_by(."$ref") else . end)') > /dev/null; then + echo "Plugin output matches golden file without preview visibility." else - echo "Plugin output does not match golden file." + echo "Plugin output does not match golden file without preview visibility." + exit 1 +fi + +rm -r apidocs.swagger.json + +cd cx-api && bash build_facade_files_with_preview_visibility.sh +cd .. + +if diff <(jq --sort-keys . apidocs_golden_preview_visibility.swagger.json | jq 'walk(if type == "object" and has("oneOf") and (.oneOf | type) == "array" then .oneOf |= sort_by(."$ref") else . end)') \ + <(jq --sort-keys . apidocs.swagger.json | jq 'walk(if type == "object" and has("oneOf") and (.oneOf | type) == "array" then .oneOf |= sort_by(."$ref") else . end)') > /dev/null; then + echo "Plugin output matches golden file with preview visibility." +else + echo "Plugin output does not match golden file with preview visibility." exit 1 fi diff --git a/protoc-gen-openapiv3/internal/genopenapi/helpers.go b/protoc-gen-openapiv3/internal/genopenapi/helpers.go index 2fc15f415bc..265563e5119 100644 --- a/protoc-gen-openapiv3/internal/genopenapi/helpers.go +++ b/protoc-gen-openapiv3/internal/genopenapi/helpers.go @@ -3,6 +3,11 @@ package genopenapi +import ( + "strings" + "unicode" +) + // this method will filter the same fields and return the unique one func getUniqueFields(schemaFieldsRequired []string, fieldsRequired []string) []string { var unique []string @@ -23,3 +28,28 @@ func getUniqueFields(schemaFieldsRequired []string, fieldsRequired []string) []s } return unique } + +func toPascalCase(s string) string { + if s == "" { + return "" + } + + var builder strings.Builder + capitalizeNext := true + + for _, r := range s { + if r == '_' { + capitalizeNext = true + continue // Skip the underscore itself + } + + if capitalizeNext { + builder.WriteRune(unicode.ToUpper(r)) + capitalizeNext = false // Reset the flag + } else { + builder.WriteRune(r) + } + } + + return builder.String() +} diff --git a/protoc-gen-openapiv3/internal/genopenapi/template_v3.go b/protoc-gen-openapiv3/internal/genopenapi/template_v3.go index 34da8999b7f..26485597e34 100644 --- a/protoc-gen-openapiv3/internal/genopenapi/template_v3.go +++ b/protoc-gen-openapiv3/internal/genopenapi/template_v3.go @@ -4,6 +4,7 @@ import ( "fmt" "log" "maps" + "sort" "strings" "slices" @@ -115,7 +116,8 @@ func applyTemplateV3(param param) (OpenAPIV3Document, error) { for _, schema := range schemas { schema.OpenAPIV3Schema.CamelCase() } - paths, err := buildOpenAPIV3Paths(param, resolvedNames) + paths, schemasToAddToComponents, err := buildOpenAPIV3Paths(param, resolvedNames) + maps.Copy(schemas, schemasToAddToComponents) if err != nil { return OpenAPIV3Document{}, err } @@ -161,8 +163,9 @@ func resolveNames(param param) map[string]string { } } -func buildOpenAPIV3Paths(param param, resolvedNames map[string]string) (OpenAPIV3Paths, error) { +func buildOpenAPIV3Paths(param param, resolvedNames map[string]string) (OpenAPIV3Paths, map[string]*OpenAPIV3SchemaRef, error) { paths := OpenAPIV3Paths{} + schemasToAddToComponents := map[string]*OpenAPIV3SchemaRef{} for _, svc := range param.Services { if !isVisible(getServiceVisibilityOption(svc), param.reg) { continue @@ -231,11 +234,11 @@ func buildOpenAPIV3Paths(param param, resolvedNames map[string]string) (OpenAPIV paths[path] = pathItem } - schemaMap := buildMessageSchemas(param) + schemaMap, messageOneOfSchemas := buildMessageSchemas(param, resolvedNames) + requestBody, bodyOneOfSchemas := buildRequestBody(b, schemaMap, param.reg, resolvedNames) pathParameters := buildPathParameters(b, param.reg, resolvedNames) - queryParameters := buildQueryParameters(b, schemaMap, param.reg) + queryParameters := buildQueryParameters(b, schemaMap, resolvedNames, param.reg) parameters := append(pathParameters, queryParameters...) - requestBody := buildRequestBody(b, schemaMap, param.reg, resolvedNames) if requestBody != nil { requestBody.OpenAPIV3RequestBody.Content["application/json"].Schema.OpenAPIV3Schema.CamelCase() } @@ -275,10 +278,12 @@ func buildOpenAPIV3Paths(param param, resolvedNames map[string]string) (OpenAPIV case "TRACE": pathItem.Trace = op } + maps.Copy(schemasToAddToComponents, bodyOneOfSchemas) + maps.Copy(schemasToAddToComponents, messageOneOfSchemas) } } } - return paths, nil + return paths, schemasToAddToComponents, nil } func extractOpenAPIV3ResponsesFromProtoExtension(operation *options.Operation) OpenAPIV3Responses { @@ -334,7 +339,7 @@ func extractOpenAPIV3ResponsesFromProtoExtension(operation *options.Operation) O } func buildTags(param param) ([]OpenAPIV3Tag, error) { - openApiV3TagSet := map[OpenAPIV3Tag]struct{}{} + openApiV3TagSet := map[string]OpenAPIV3Tag{} for _, svc := range param.Services { if !proto.HasExtension(svc.Options, options.E_Openapiv3Tag) { continue @@ -352,11 +357,11 @@ func buildTags(param param) ([]OpenAPIV3Tag, error) { URL: tag.GetExternalDocs().GetUrl(), }, } - openApiV3TagSet[openapiV3Tag] = struct{}{} + openApiV3TagSet[tag.GetName()] = openapiV3Tag } } openapiV3Tags := make([]OpenAPIV3Tag, 0, len(openApiV3TagSet)) - for tag := range openApiV3TagSet { + for _, tag := range openApiV3TagSet { openapiV3Tags = append(openapiV3Tags, tag) } return openapiV3Tags, nil @@ -425,7 +430,7 @@ func buildPathParameters(binding *descriptor.Binding, registry *descriptor.Regis return parameterRefs } -func buildQueryParameters(binding *descriptor.Binding, schemaMap map[string]*OpenAPIV3SchemaRef, registry *descriptor.Registry) []OpenAPIV3ParameterRef { +func buildQueryParameters(binding *descriptor.Binding, schemaMap map[string]*OpenAPIV3SchemaRef, resolvedNames map[string]string, registry *descriptor.Registry) []OpenAPIV3ParameterRef { if binding.Body != nil && len(binding.Body.FieldPath) == 0 { return []OpenAPIV3ParameterRef{} } @@ -471,10 +476,23 @@ func buildQueryParameters(binding *descriptor.Binding, schemaMap map[string]*Ope continue } - queryParameterSchema := buildPropertySchemaFromField(field, schemaMap, registry) + queryParameterSchema := buildPropertySchemaFromField(field, schemaMap, resolvedNames, registry) if queryParameterSchema == nil { continue } + // This means we're dealing with an enum, so we can just create a reference parameter. + if queryParameterSchema.Ref != "" { + parameterRef := OpenAPIV3ParameterRef{ + OpenAPIV3Parameter: &OpenAPIV3Parameter{ + Name: *field.Name, + In: "query", + Required: false, + Schema: queryParameterSchema, + }, + } + parameterRefs = append(parameterRefs, parameterRef) + continue + } // Follow the path of the field to remove, and remove it from the body schema if len(queryParameterSchema.Properties) > 0 { properties := &queryParameterSchema.Properties @@ -516,14 +534,15 @@ func buildQueryParameters(binding *descriptor.Binding, schemaMap map[string]*Ope return parameterRefs } -func buildRequestBody(binding *descriptor.Binding, schemaMap map[string]*OpenAPIV3SchemaRef, registry *descriptor.Registry, resolvedNames map[string]string) *OpenAPIV3RequestBodyRef { +func buildRequestBody(binding *descriptor.Binding, schemaMap map[string]*OpenAPIV3SchemaRef, registry *descriptor.Registry, resolvedNames map[string]string) (*OpenAPIV3RequestBodyRef, map[string]*OpenAPIV3SchemaRef) { if binding.Body == nil { - return nil + return nil, map[string]*OpenAPIV3SchemaRef{} } + schemasToAddToComponents := map[string]*OpenAPIV3SchemaRef{} bodyRepresentation := extractRequestBodyFieldCombinations(binding, registry) parameterFields := extractParameterFields(binding) - oneOfSchemas := make([]*OpenAPIV3SchemaRef, 0, len(bodyRepresentation.fieldCombinations)) - for _, bodyFields := range bodyRepresentation.fieldCombinations { + oneOfSchemas := map[string]*OpenAPIV3SchemaRef{} + for combinationName, bodyFields := range bodyRepresentation.fieldCombinations { bodyProperties := make(map[string]*OpenAPIV3SchemaRef) for _, bodyField := range bodyFields { if !isVisible(getFieldVisibilityOption(bodyField.Field), registry) { @@ -545,9 +564,10 @@ func buildRequestBody(binding *descriptor.Binding, schemaMap map[string]*OpenAPI fieldMessage, err := registry.LookupMsg(*bodyField.Field.TypeName, *bodyField.Field.TypeName) if err != nil || fieldMessage == nil { log.Printf("Warning: field %s has no message type", *bodyField.Field.Name) - return nil + return nil, map[string]*OpenAPIV3SchemaRef{} } - fieldSchema := buildOpenAPIV3SchemaFromMessage(fieldMessage, schemaMap, registry) + fieldSchema, messageOneOfSchemas := buildOpenAPIV3SchemaFromMessage(fieldMessage, schemaMap, resolvedNames, registry) + maps.Copy(schemasToAddToComponents, messageOneOfSchemas) // Follow the path of the field to remove, and remove it from the body schema if len(fieldSchema.Properties) > 0 { properties := &fieldSchema.Properties @@ -595,22 +615,33 @@ func buildRequestBody(binding *descriptor.Binding, schemaMap map[string]*OpenAPI AdditionalProperties: false, OpenAPIV3Extensions: bodyRepresentation.extensions, } - oneOfSchemas = append(oneOfSchemas, &OpenAPIV3SchemaRef{ + oneOfSchemas[combinationName] = &OpenAPIV3SchemaRef{ OpenAPIV3Schema: &schema, - }) + } } } + oneOfSchemaRefs := []*OpenAPIV3SchemaRef{} + for combinationName := range oneOfSchemas { + schemaRef := OpenAPIV3SchemaRef{ + Ref: "#/components/schemas/" + combinationName, + } + oneOfSchemaRefs = append(oneOfSchemaRefs, &schemaRef) + } var bodySchema *OpenAPIV3Schema if len(oneOfSchemas) == 0 { - return nil + return nil, map[string]*OpenAPIV3SchemaRef{} } if len(oneOfSchemas) > 1 { bodySchema = &OpenAPIV3Schema{ Type: "object", - OneOf: oneOfSchemas, + OneOf: oneOfSchemaRefs, } + schemasToAddToComponents = oneOfSchemas } else { - bodySchema = oneOfSchemas[0].OpenAPIV3Schema + for _, schema := range oneOfSchemas { + bodySchema = schema.OpenAPIV3Schema + break + } } bodyContent := make(map[string]OpenAPIV3MediaType) @@ -619,15 +650,18 @@ func buildRequestBody(binding *descriptor.Binding, schemaMap map[string]*OpenAPI OpenAPIV3Schema: bodySchema, }, } + if len(oneOfSchemas) > 1 { + schemasToAddToComponents = oneOfSchemas + } return &OpenAPIV3RequestBodyRef{ OpenAPIV3RequestBody: &OpenAPIV3RequestBody{ Content: bodyContent, }, - } + }, schemasToAddToComponents } type openAPIV3BodyRepresentation struct { - fieldCombinations [][]protoField + fieldCombinations map[string][]protoField requiredFields []string title string description string @@ -650,7 +684,7 @@ func extractRequestBodyFieldCombinations(binding *descriptor.Binding, registry * // and therefore we just return the field as is. if *fieldPathComponent.Target.Type != descriptorpb.FieldDescriptorProto_TYPE_MESSAGE { return openAPIV3BodyRepresentation{ - fieldCombinations: [][]protoField{{ + fieldCombinations: map[string][]protoField{"": { { FullPathToField: prefix, Field: fieldPathComponent.Target, @@ -703,6 +737,13 @@ func extractRequestBodyFieldCombinations(binding *descriptor.Binding, registry * } oneofGroups[*oneofDecl.Name] = append(oneofGroups[*oneofDecl.Name], field) } + for group := range oneofGroups { + numberOfFieldsInGroup := len(oneofGroups[group]) + if numberOfFieldsInGroup <= 1 { + fieldsNotPartOfOneofGroup = append(fieldsNotPartOfOneofGroup, oneofGroups[group]...) + delete(oneofGroups, group) + } + } if len(oneofGroups) == 0 { for _, field := range fieldsNotPartOfOneofGroup { @@ -713,7 +754,7 @@ func extractRequestBodyFieldCombinations(binding *descriptor.Binding, registry * bodyFields = append(bodyFields, bodyField) } return openAPIV3BodyRepresentation{ - fieldCombinations: [][]protoField{bodyFields}, + fieldCombinations: map[string][]protoField{*fieldMessage.Name: bodyFields}, requiredFields: requiredFields, title: title, description: description, @@ -722,9 +763,9 @@ func extractRequestBodyFieldCombinations(binding *descriptor.Binding, registry * } } - combinationsOfFieldsPartOfOneofGroups := generateOneOfCombinations(oneofGroups) - protoFields := make([][]protoField, 0, len(combinationsOfFieldsPartOfOneofGroups)) - for _, combination := range combinationsOfFieldsPartOfOneofGroups { + combinationsOfFieldsPartOfOneofGroups := generateOneOfCombinations(oneofGroups, *fieldMessage.Name) + protoFields := make(map[string][]protoField) + for combinationName, combination := range combinationsOfFieldsPartOfOneofGroups { fields := make([]protoField, 0, len(combination)+len(fieldsNotPartOfOneofGroup)) for _, field := range fieldsNotPartOfOneofGroup { bodyField := protoField{ @@ -741,7 +782,7 @@ func extractRequestBodyFieldCombinations(binding *descriptor.Binding, registry * } fields = append(fields, bodyField) } - protoFields = append(protoFields, fields) + protoFields[combinationName] = fields } return openAPIV3BodyRepresentation{ @@ -797,8 +838,9 @@ func buildMessageSchemasWithReferences(param param, resolvedNames map[string]str return schemas } -func buildMessageSchemas(param param) map[string]*OpenAPIV3SchemaRef { +func buildMessageSchemas(param param, resolvedNames map[string]string) (map[string]*OpenAPIV3SchemaRef, map[string]*OpenAPIV3SchemaRef) { schemaMap := make(map[string]*OpenAPIV3SchemaRef) + oneOfSchemas := make(map[string]*OpenAPIV3SchemaRef) for _, message := range param.Messages { schemaMap[message.FQMN()] = &OpenAPIV3SchemaRef{ @@ -808,12 +850,12 @@ func buildMessageSchemas(param param) map[string]*OpenAPIV3SchemaRef { for _, message := range param.Messages { schemaRefPtr := schemaMap[message.FQMN()] - schema := buildOpenAPIV3SchemaFromMessage(message, schemaMap, param.reg) + schema, messageOneOfSchemas := buildOpenAPIV3SchemaFromMessage(message, schemaMap, resolvedNames, param.reg) schemaRefPtr.OpenAPIV3Schema = schema - + maps.Copy(oneOfSchemas, messageOneOfSchemas) } - return schemaMap + return schemaMap, oneOfSchemas } func buildEnumSchemas(param param, resolvedNames map[string]string) map[string]*OpenAPIV3SchemaRef { @@ -932,46 +974,30 @@ func buildOpenAPIV3SchemaFromMessageWithReferences(message *descriptor.Message, oneofGroups[*oneofDecl.Name] = append(oneofGroups[*oneofDecl.Name], field) } + for group := range oneofGroups { + numberOfFieldsInGroup := len(oneofGroups[group]) + if numberOfFieldsInGroup <= 1 { + fieldsNotPartOfOneofGroup = append(fieldsNotPartOfOneofGroup, oneofGroups[group]...) + delete(oneofGroups, group) + } + } + if len(oneofGroups) == 0 { return buildSchemaFromFieldsWithReferences(fieldsNotPartOfOneofGroup, registry, requiredFields, title, description, externalDocs, extensions, resolvedNames) } - combinationsOfFieldsPartOfOneofGroups := generateOneOfCombinations(oneofGroups) - - oneOfSchemas := make([]*OpenAPIV3SchemaRef, 0, len(combinationsOfFieldsPartOfOneofGroups)) - for _, combination := range combinationsOfFieldsPartOfOneofGroups { - properties := make(map[string]*OpenAPIV3SchemaRef) + combinationsOfFieldsPartOfOneofGroups := generateOneOfCombinations(oneofGroups, *message.Name) - for _, field := range fieldsNotPartOfOneofGroup { - propertySchema := buildPropertySchemaWithReferencesFromField(field, registry, resolvedNames) - if propertySchema != nil { - properties[*field.Name] = propertySchema - } - } - - for _, field := range combination { - propertySchema := buildPropertySchemaWithReferencesFromField(field, registry, resolvedNames) - if propertySchema != nil { - properties[*field.Name] = propertySchema - } - } - - schema := &OpenAPIV3Schema{ - Type: "object", - Title: title, - Description: description, - ExternalDocs: externalDocs, - OpenAPIV3Extensions: extensions, - Properties: properties, - Required: requiredFields, - AdditionalProperties: false, - } + oneOfSchemas := []*OpenAPIV3SchemaRef{} + for combinationName := range combinationsOfFieldsPartOfOneofGroups { oneOfSchemas = append(oneOfSchemas, &OpenAPIV3SchemaRef{ - OpenAPIV3Schema: schema, + Ref: "#/components/schemas/" + combinationName, }) } if len(oneOfSchemas) == 1 { - return oneOfSchemas[0].OpenAPIV3Schema + for _, schema := range oneOfSchemas { + return schema.OpenAPIV3Schema + } } return &OpenAPIV3Schema{ @@ -979,7 +1005,7 @@ func buildOpenAPIV3SchemaFromMessageWithReferences(message *descriptor.Message, } } -func buildOpenAPIV3SchemaFromMessage(message *descriptor.Message, schemaMap map[string]*OpenAPIV3SchemaRef, registry *descriptor.Registry) *OpenAPIV3Schema { +func buildOpenAPIV3SchemaFromMessage(message *descriptor.Message, schemaMap map[string]*OpenAPIV3SchemaRef, resolvedNames map[string]string, registry *descriptor.Registry) (*OpenAPIV3Schema, map[string]*OpenAPIV3SchemaRef) { var fieldsNotPartOfOneofGroup []*descriptor.Field oneofGroups := make(map[string][]*descriptor.Field) var title string @@ -1018,70 +1044,68 @@ func buildOpenAPIV3SchemaFromMessage(message *descriptor.Message, schemaMap map[ } oneofGroups[*oneofDecl.Name] = append(oneofGroups[*oneofDecl.Name], field) } + for group := range oneofGroups { + numberOfFieldsInGroup := len(oneofGroups[group]) + if numberOfFieldsInGroup <= 1 { + fieldsNotPartOfOneofGroup = append(fieldsNotPartOfOneofGroup, oneofGroups[group]...) + delete(oneofGroups, group) + } + } if len(oneofGroups) == 0 { - return buildSchemaFromFields(fieldsNotPartOfOneofGroup, schemaMap, requiredFields, title, description, externalDocs, extensions, registry) + return buildSchemaFromFields(fieldsNotPartOfOneofGroup, schemaMap, requiredFields, title, description, externalDocs, extensions, resolvedNames, registry), map[string]*OpenAPIV3SchemaRef{} } - combinationsOfFieldsPartOfOneofGroups := generateOneOfCombinations(oneofGroups) - - oneOfSchemas := make([]*OpenAPIV3SchemaRef, 0, len(combinationsOfFieldsPartOfOneofGroups)) - for _, combination := range combinationsOfFieldsPartOfOneofGroups { - properties := make(map[string]*OpenAPIV3SchemaRef) - for _, field := range fieldsNotPartOfOneofGroup { - propertySchema := buildPropertySchemaFromField(field, schemaMap, registry) - if propertySchema != nil { - properties[*field.Name] = propertySchema - } - } + combinationsOfFieldsPartOfOneofGroups := generateOneOfCombinations(oneofGroups, *message.Name) + oneOfSchemas := map[string]*OpenAPIV3SchemaRef{} + for combinationName, combination := range combinationsOfFieldsPartOfOneofGroups { + combinationFields := []*descriptor.Field{} for _, field := range combination { - propertySchema := buildPropertySchemaFromField(field, schemaMap, registry) - if propertySchema != nil { - properties[*field.Name] = propertySchema - } + combinationFields = append(combinationFields, field) + } + allSchemaFields := append(fieldsNotPartOfOneofGroup, combinationFields...) + schema := buildSchemaFromFieldsWithReferences(allSchemaFields, registry, requiredFields, title, description, externalDocs, extensions, resolvedNames) + oneOfSchemas[combinationName] = &OpenAPIV3SchemaRef{ + OpenAPIV3Schema: schema, } - - oneOfSchemas = append(oneOfSchemas, &OpenAPIV3SchemaRef{ - OpenAPIV3Schema: &OpenAPIV3Schema{ - Type: "object", - Title: title, - Description: description, - ExternalDocs: externalDocs, - Properties: properties, - Required: requiredFields, - OpenAPIV3Extensions: extensions, - AdditionalProperties: false, - }, - }) } if len(oneOfSchemas) == 1 { - return oneOfSchemas[0].OpenAPIV3Schema + for _, schema := range oneOfSchemas { + return schema.OpenAPIV3Schema, map[string]*OpenAPIV3SchemaRef{} + } } - return &OpenAPIV3Schema{ - OneOf: oneOfSchemas, + oneOfSchemaRefs := []*OpenAPIV3SchemaRef{} + for combinationName := range oneOfSchemas { + schemaRef := OpenAPIV3SchemaRef{ + Ref: "#/components/schemas/" + combinationName, + } + oneOfSchemaRefs = append(oneOfSchemaRefs, &schemaRef) } + + return &OpenAPIV3Schema{ + OneOf: oneOfSchemaRefs, + }, oneOfSchemas } -// I made this function generic for ease of testing. Concretely, F is really a *descriptor.Field. -// This could have been much clearer with recursion, but an iterative approach is safer in production code. -func generateOneOfCombinations[F any](oneofGroups map[string][]F) []map[string]F { - allCombinations := []map[string]F{{}} +func generateOneOfCombinations(oneofGroups map[string][]*descriptor.Field, messageName string) map[string]map[string]*descriptor.Field { + allCombinations := []map[string]*descriptor.Field{{}} oneofGroupNames := make([]string, 0, len(oneofGroups)) for name := range oneofGroups { oneofGroupNames = append(oneofGroupNames, name) } + sort.Strings(oneofGroupNames) for _, groupName := range oneofGroupNames { variants := oneofGroups[groupName] - newCombinations := []map[string]F{} + newCombinations := []map[string]*descriptor.Field{} for _, existingCombination := range allCombinations { for _, variant := range variants { - newCombination := make(map[string]F) + newCombination := make(map[string]*descriptor.Field) maps.Copy(newCombination, existingCombination) newCombination[groupName] = variant @@ -1089,11 +1113,30 @@ func generateOneOfCombinations[F any](oneofGroups map[string][]F) []map[string]F newCombinations = append(newCombinations, newCombination) } } - allCombinations = newCombinations } - return allCombinations + namedCombinations := make(map[string]map[string]*descriptor.Field, len(allCombinations)) + + for _, combination := range allCombinations { + keyParts := make([]string, 0, len(oneofGroupNames)) + + for _, groupName := range oneofGroupNames { + variant, ok := combination[groupName] + if !ok { + continue + } + keyPart := fmt.Sprintf("%v", variant.GetName()) + keyParts = append(keyParts, keyPart) + } + + combinationName := strings.Join(keyParts, "_") + combinationName = messageName + "_" + combinationName + combinationName = toPascalCase(combinationName) + namedCombinations[combinationName] = combination + } + + return namedCombinations } // Helper function to build a single OpenAPI schema from a list of fields. @@ -1135,11 +1178,12 @@ func buildSchemaFromFields( description string, externalDocs *OpenAPIV3ExternalDocs, extensions OpenAPIV3Extensions, + resolvedNames map[string]string, registry *descriptor.Registry, ) *OpenAPIV3Schema { properties := make(map[string]*OpenAPIV3SchemaRef) for _, field := range fields { - propertySchema := buildPropertySchemaFromField(field, schemaMap, registry) + propertySchema := buildPropertySchemaFromField(field, schemaMap, resolvedNames, registry) if propertySchema == nil { continue } @@ -1416,7 +1460,7 @@ func buildPropertySchemaWithReferencesFromFieldType(field *descriptor.Field, reg return &OpenAPIV3SchemaRef{OpenAPIV3Schema: &OpenAPIV3Schema{Type: "string"}} } -func buildPropertySchemaFromField(field *descriptor.Field, schemaMap map[string]*OpenAPIV3SchemaRef, registry *descriptor.Registry) *OpenAPIV3SchemaRef { +func buildPropertySchemaFromField(field *descriptor.Field, schemaMap map[string]*OpenAPIV3SchemaRef, resolvedNames map[string]string, registry *descriptor.Registry) *OpenAPIV3SchemaRef { if !isVisible(getFieldVisibilityOption(field), registry) { return nil } @@ -1431,15 +1475,15 @@ func buildPropertySchemaFromField(field *descriptor.Field, schemaMap map[string] if field.Label != nil && *field.Label == descriptorpb.FieldDescriptorProto_LABEL_REPEATED && (opts == nil || opts.MapEntry == nil || !*opts.MapEntry) { schema := &OpenAPIV3Schema{ Type: "array", - Items: buildPropertySchemaFromFieldType(field, schemaMap, registry), + Items: buildPropertySchemaFromFieldType(field, schemaMap, resolvedNames, registry), } return &OpenAPIV3SchemaRef{ OpenAPIV3Schema: schema, } } - return buildPropertySchemaFromFieldType(field, schemaMap, registry) + return buildPropertySchemaFromFieldType(field, schemaMap, resolvedNames, registry) } -func buildPropertySchemaFromFieldType(field *descriptor.Field, schemaMap map[string]*OpenAPIV3SchemaRef, registry *descriptor.Registry) *OpenAPIV3SchemaRef { +func buildPropertySchemaFromFieldType(field *descriptor.Field, schemaMap map[string]*OpenAPIV3SchemaRef, resolvedNames map[string]string, registry *descriptor.Registry) *OpenAPIV3SchemaRef { // This function handles the logic from your original code, mapping protobuf types to OpenAPI types. var title string var maximum float64 @@ -1610,48 +1654,7 @@ func buildPropertySchemaFromFieldType(field *descriptor.Field, schemaMap map[str OpenAPIV3Extensions: extensions, }} } else if *field.Type == descriptorpb.FieldDescriptorProto_TYPE_ENUM { - var enumDefaultValue interface{} - if proto.HasExtension(field.Options, options.E_Openapiv3Enum) { - enumExtension, ok := proto.GetExtension(field.Options, options.E_Openapiv3Enum).(*options.EnumSchema) - enumOpenAPIV3Extensions := OpenAPIV3Extensions{} - for k, v := range enumExtension.Extensions { - (enumOpenAPIV3Extensions)[k] = v - } - if ok { - if enumExtension.GetDefault() != "" { - enumDefaultValue = enumExtension.GetDefault() - } else { - enumDefaultValue = nil - } - title = enumExtension.Title - description = enumExtension.Description - readOnly = enumExtension.ReadOnly - extensions = enumOpenAPIV3Extensions - example = RawExample(enumExtension.Example) - } - } - enumVariants := make([]string, 0) - enum, err := registry.LookupEnum(*field.TypeName, *field.TypeName) - if err != nil || enum == nil { - return &OpenAPIV3SchemaRef{OpenAPIV3Schema: &OpenAPIV3Schema{Type: "string"}} - } - for _, enumValue := range enum.Value { - if !isVisible(getEnumValueVisibilityOption(enumValue), registry) { - continue - } - enumVariants = append(enumVariants, *enumValue.Name) - } - return &OpenAPIV3SchemaRef{OpenAPIV3Schema: &OpenAPIV3Schema{ - Type: "string", - Enum: enumVariants, - Default: enumDefaultValue, - Title: title, - Description: description, - Deprecated: deprecated, - ReadOnly: readOnly, - Example: example, - OpenAPIV3Extensions: extensions, - }} + return &OpenAPIV3SchemaRef{Ref: "#/components/schemas/" + resolvedNames[*field.TypeName]} } else if field.TypeName != nil { if schema, ok := wellKnownTypesToOpenAPIV3SchemaMapping[*field.TypeName]; ok && schema != nil { schemaCopy := *schema // Create a copy to avoid modifying the original schema @@ -1693,7 +1696,7 @@ func buildPropertySchemaFromFieldType(field *descriptor.Field, schemaMap map[str } return &OpenAPIV3SchemaRef{OpenAPIV3Schema: &OpenAPIV3Schema{ Type: "object", - AdditionalProperties: buildPropertySchemaFromFieldType(valueField, schemaMap, registry), + AdditionalProperties: buildPropertySchemaFromFieldType(valueField, schemaMap, resolvedNames, registry), Title: title, Description: description, Deprecated: deprecated, diff --git a/protoc-gen-openapiv3/internal/genopenapi/template_v3_test.go b/protoc-gen-openapiv3/internal/genopenapi/template_v3_test.go index cb20ade9a57..6ce5654ddef 100644 --- a/protoc-gen-openapiv3/internal/genopenapi/template_v3_test.go +++ b/protoc-gen-openapiv3/internal/genopenapi/template_v3_test.go @@ -1,9 +1,12 @@ package genopenapi import ( - "reflect" - "sort" + "log" "testing" + + "github.com/grpc-ecosystem/grpc-gateway/v2/internal/descriptor" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/descriptorpb" ) // Mock descriptor.Field type to simulate a protobuf field @@ -15,112 +18,28 @@ func (f *MockField) GetName() string { return f.Name } -func Test_generateOneOfCombinations(t *testing.T) { - t.Run("NoOneOfGroups", func(t *testing.T) { - oneofGroups := map[string][]MockField{} - result := generateOneOfCombinations(oneofGroups) - - if len(result) != 1 { - t.Fatalf("Expected 1 combination, got %d", len(result)) - } - if len(result[0]) != 0 { - t.Fatalf("Expected an empty map, got a map with %d elements", len(result[0])) - } - }) - - t.Run("SingleOneOfGroup", func(t *testing.T) { - oneofGroups := map[string][]MockField{ - "oneof_group_A": { - {Name: "field_A1"}, - {Name: "field_A2"}, - }, - } - - result := generateOneOfCombinations(oneofGroups) - if len(result) != 2 { - t.Fatalf("Expected 2 combinations, got %d", len(result)) - } - - var foundFieldNames []string - for _, combination := range result { - foundFieldNames = append(foundFieldNames, combination["oneof_group_A"].Name) - } - sort.Strings(foundFieldNames) - expectedFieldNames := []string{"field_A1", "field_A2"} - - if !reflect.DeepEqual(foundFieldNames, expectedFieldNames) { - t.Errorf("Field names do not match. Expected %+v, got %+v", expectedFieldNames, foundFieldNames) - } - }) - // This tests the Cartesian product logic. - t.Run("MultipleOneOfGroups", func(t *testing.T) { - oneofGroups := map[string][]MockField{ - "oneof_group_A": { - {Name: "field_A1"}, - {Name: "field_A2"}, - }, - "oneof_group_B": { - {Name: "field_B1"}, - {Name: "field_B2"}, - }, - } - - result := generateOneOfCombinations(oneofGroups) - // 2 variants * 2 variants = 4 combinations expected - if len(result) != 4 { - t.Fatalf("Expected 4 combinations, got %d", len(result)) - } - - // Check the specific combinations - expectedCombinations := []map[string]string{ - {"oneof_group_A": "field_A1", "oneof_group_B": "field_B1"}, - {"oneof_group_A": "field_A1", "oneof_group_B": "field_B2"}, - {"oneof_group_A": "field_A2", "oneof_group_B": "field_B1"}, - {"oneof_group_A": "field_A2", "oneof_group_B": "field_B2"}, - } - - // Convert the result to a comparable format and sort for stable comparison. - foundCombinations := make([]map[string]string, len(result)) - for i, combination := range result { - foundCombinations[i] = make(map[string]string) - for k, v := range combination { - foundCombinations[i][k] = v.Name - } - } - - // Sort both slices for consistent comparison - sort.Slice(foundCombinations, func(i, j int) bool { - if foundCombinations[i]["oneof_group_A"] != foundCombinations[j]["oneof_group_A"] { - return foundCombinations[i]["oneof_group_A"] < foundCombinations[j]["oneof_group_A"] - } - return foundCombinations[i]["oneof_group_B"] < foundCombinations[j]["oneof_group_B"] - }) - - if !reflect.DeepEqual(foundCombinations, expectedCombinations) { - t.Errorf("Combinations do not match expected result.\nExpected: %+v\nGot: %+v", expectedCombinations, foundCombinations) - } - }) - +func Test_generateOneOfCombinations2(t *testing.T) { t.Run("MultipleOneOfGroupsWithDifferentVariantNumbers", func(t *testing.T) { - oneofGroups := map[string][]MockField{ + oneofGroups := map[string][]*descriptor.Field{ "oneof_group_A": { - {Name: "field_A1"}, - {Name: "field_A2"}, - {Name: "field_A3"}, + {FieldDescriptorProto: &descriptorpb.FieldDescriptorProto{Name: proto.String("field_A1")}}, + {FieldDescriptorProto: &descriptorpb.FieldDescriptorProto{Name: proto.String("field_A2")}}, + {FieldDescriptorProto: &descriptorpb.FieldDescriptorProto{Name: proto.String("field_A3")}}, + {FieldDescriptorProto: &descriptorpb.FieldDescriptorProto{Name: proto.String("field_A4")}}, }, "oneof_group_B": { - {Name: "field_B1"}, - {Name: "field_B2"}, + {FieldDescriptorProto: &descriptorpb.FieldDescriptorProto{Name: proto.String("field_B1")}}, + {FieldDescriptorProto: &descriptorpb.FieldDescriptorProto{Name: proto.String("field_B2")}}, }, "oneof_group_C": { - {Name: "field_C1"}, - {Name: "field_C2"}, - {Name: "field_C3"}, - {Name: "field_C4"}, + {FieldDescriptorProto: &descriptorpb.FieldDescriptorProto{Name: proto.String("field_C1")}}, + {FieldDescriptorProto: &descriptorpb.FieldDescriptorProto{Name: proto.String("field_C2")}}, + {FieldDescriptorProto: &descriptorpb.FieldDescriptorProto{Name: proto.String("field_C3")}}, }, } - result := generateOneOfCombinations(oneofGroups) + result := generateOneOfCombinations(oneofGroups, "TestMessage") + log.Printf("Result: %+v", result) if len(result) != 24 { t.Fatalf("Expected 4 combinations, got %d", len(result))