diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenEncoding.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenEncoding.java new file mode 100644 index 000000000000..efd5b0f16abc --- /dev/null +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenEncoding.java @@ -0,0 +1,67 @@ +package org.openapitools.codegen; + +import java.util.List; +import java.util.Objects; + +public class CodegenEncoding { + private String contentType; + private List headers; + private String style; + private boolean explode; + private boolean allowReserved; + + public CodegenEncoding(String contentType, List headers, String style, boolean explode, boolean allowReserved) { + this.contentType = contentType; + this.headers = headers; + this.style = style; + this.explode = explode; + this.allowReserved = allowReserved; + } + + public String getContentType() { + return contentType; + } + + public List getHeaders() { + return headers; + } + + public String getStyle() { + return style; + } + + public boolean getExplode() { + return explode; + } + + public boolean getAllowReserved() { + return allowReserved; + } + + public String toString() { + final StringBuilder sb = new StringBuilder("CodegenEncoding{"); + sb.append("contentType=").append(contentType); + sb.append(", headers=").append(headers); + sb.append(", style=").append(style); + sb.append(", explode=").append(explode); + sb.append(", allowReserved=").append(allowReserved); + sb.append('}'); + return sb.toString(); + } + + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CodegenEncoding that = (CodegenEncoding) o; + return contentType == that.getContentType() && + Objects.equals(headers, that.getHeaders()) && + style == that.getStyle() && + explode == that.getExplode() && + allowReserved == that.getAllowReserved(); + } + + @Override + public int hashCode() { + return Objects.hash(contentType, headers, style, explode, allowReserved); + } +} diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenMediaType.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenMediaType.java new file mode 100644 index 000000000000..965e50319427 --- /dev/null +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenMediaType.java @@ -0,0 +1,45 @@ +package org.openapitools.codegen; + +import java.util.LinkedHashMap; +import java.util.Objects; + +public class CodegenMediaType { + private CodegenProperty schema; + private LinkedHashMap encoding; + + public CodegenMediaType(CodegenProperty schema, LinkedHashMap encoding) { + this.schema = schema; + this.encoding = encoding; + } + + public CodegenProperty getSchema() { + return schema; + } + + public LinkedHashMap getEncoding() { + return encoding; + } + + public String toString() { + final StringBuilder sb = new StringBuilder("CodegenMediaType{"); + sb.append("schema=").append(schema); + sb.append(", encoding=").append(encoding); + sb.append('}'); + return sb.toString(); + } + + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CodegenMediaType that = (CodegenMediaType) o; + return Objects.equals(schema,that.getSchema()) && + Objects.equals(encoding, that.getEncoding()); + } + + @Override + public int hashCode() { + return Objects.hash(schema, encoding); + } +} + + diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenParameter.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenParameter.java index 5bd8a369f0d3..dba1721fbca2 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenParameter.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenParameter.java @@ -110,6 +110,7 @@ public class CodegenParameter implements IJsonSchemaValidationProperties { private boolean hasDiscriminatorWithNonEmptyMapping; private CodegenComposedSchemas composedSchemas; private boolean hasMultipleTypes = false; + private LinkedHashMap content; public CodegenParameter copy() { CodegenParameter output = new CodegenParameter(); @@ -163,6 +164,9 @@ public class CodegenParameter implements IJsonSchemaValidationProperties { output.setHasDiscriminatorWithNonEmptyMapping(this.hasDiscriminatorWithNonEmptyMapping); output.setHasMultipleTypes(this.hasMultipleTypes); + if (this.content != null) { + output.setContent(this.content); + } if (this.schema != null) { output.setSchema(this.schema); } @@ -226,7 +230,7 @@ public class CodegenParameter implements IJsonSchemaValidationProperties { @Override public int hashCode() { - return Objects.hash(isFormParam, isQueryParam, isPathParam, isHeaderParam, isCookieParam, isBodyParam, isContainer, isCollectionFormatMulti, isPrimitiveType, isModel, isExplode, baseName, paramName, dataType, datatypeWithEnum, dataFormat, collectionFormat, description, unescapedDescription, baseType, defaultValue, enumName, style, isDeepObject, isAllowEmptyValue, example, jsonSchema, isString, isNumeric, isInteger, isLong, isNumber, isFloat, isDouble, isDecimal, isByteArray, isBinary, isBoolean, isDate, isDateTime, isUuid, isUri, isEmail, isFreeFormObject, isAnyType, isArray, isMap, isFile, isEnum, _enum, allowableValues, items, mostInnerItems, additionalProperties, vars, requiredVars, vendorExtensions, hasValidation, getMaxProperties(), getMinProperties(), isNullable, isDeprecated, required, getMaximum(), getExclusiveMaximum(), getMinimum(), getExclusiveMinimum(), getMaxLength(), getMinLength(), getPattern(), getMaxItems(), getMinItems(), getUniqueItems(), contentType, multipleOf, isNull, additionalPropertiesIsAnyType, hasVars, hasRequired, isShort, isUnboundedInteger, hasDiscriminatorWithNonEmptyMapping, composedSchemas, hasMultipleTypes, schema); + return Objects.hash(isFormParam, isQueryParam, isPathParam, isHeaderParam, isCookieParam, isBodyParam, isContainer, isCollectionFormatMulti, isPrimitiveType, isModel, isExplode, baseName, paramName, dataType, datatypeWithEnum, dataFormat, collectionFormat, description, unescapedDescription, baseType, defaultValue, enumName, style, isDeepObject, isAllowEmptyValue, example, jsonSchema, isString, isNumeric, isInteger, isLong, isNumber, isFloat, isDouble, isDecimal, isByteArray, isBinary, isBoolean, isDate, isDateTime, isUuid, isUri, isEmail, isFreeFormObject, isAnyType, isArray, isMap, isFile, isEnum, _enum, allowableValues, items, mostInnerItems, additionalProperties, vars, requiredVars, vendorExtensions, hasValidation, getMaxProperties(), getMinProperties(), isNullable, isDeprecated, required, getMaximum(), getExclusiveMaximum(), getMinimum(), getExclusiveMinimum(), getMaxLength(), getMinLength(), getPattern(), getMaxItems(), getMinItems(), getUniqueItems(), contentType, multipleOf, isNull, additionalPropertiesIsAnyType, hasVars, hasRequired, isShort, isUnboundedInteger, hasDiscriminatorWithNonEmptyMapping, composedSchemas, hasMultipleTypes, schema, content); } @Override @@ -282,6 +286,7 @@ public class CodegenParameter implements IJsonSchemaValidationProperties { getExclusiveMaximum() == that.getExclusiveMaximum() && getExclusiveMinimum() == that.getExclusiveMinimum() && getUniqueItems() == that.getUniqueItems() && + Objects.equals(content, that.getContent()) && Objects.equals(schema, that.getSchema()) && Objects.equals(composedSchemas, that.getComposedSchemas()) && Objects.equals(baseName, that.baseName) && @@ -409,6 +414,7 @@ public class CodegenParameter implements IJsonSchemaValidationProperties { sb.append(", composedSchemas=").append(composedSchemas); sb.append(", hasMultipleTypes=").append(hasMultipleTypes); sb.append(", schema=").append(schema); + sb.append(", content=").append(content); sb.append('}'); return sb.toString(); } @@ -743,5 +749,12 @@ public class CodegenParameter implements IJsonSchemaValidationProperties { public void setSchema(CodegenProperty schema) { this.schema = schema; } + public LinkedHashMap getContent() { + return content; + } + + public void setContent(LinkedHashMap content) { + this.content = content; + } } diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java index 563dc10ebc94..046d8609d3ce 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java @@ -4486,6 +4486,7 @@ public class DefaultCodegen implements CodegenConfig { codegenParameter.isDeprecated = parameter.getDeprecated(); } codegenParameter.jsonSchema = Json.pretty(parameter); + codegenParameter.setContent(getContent(parameter.getContent(), imports)); if (GlobalSettings.getProperty("debugParser") != null) { LOGGER.info("working on Parameter {}", parameter.getName()); @@ -6557,6 +6558,61 @@ public class DefaultCodegen implements CodegenConfig { codegenParameter.pattern = toRegularExpression(schema.getPattern()); } + protected LinkedHashMap getContent(Content content, Set imports) { + if (content == null) { + return null; + } + LinkedHashMap cmtContent = new LinkedHashMap<>(); + for (Entry contentEntry: content.entrySet()) { + MediaType mt = contentEntry.getValue(); + LinkedHashMap ceMap = null; + if (mt.getEncoding() != null ) { + ceMap = new LinkedHashMap<>(); + Map encMap = mt.getEncoding(); + for (Entry encodingEntry: encMap.entrySet()) { + Encoding enc = encodingEntry.getValue(); + List headers = new ArrayList<>(); + if (enc.getHeaders() != null) { + Map encHeaders = enc.getHeaders(); + for (Entry headerEntry: encHeaders.entrySet()) { + String headerName = headerEntry.getKey(); + Header header = ModelUtils.getReferencedHeader(this.openAPI, headerEntry.getValue()); + Parameter headerParam = new Parameter(); + headerParam.setName(headerName); + headerParam.setIn("header"); + headerParam.setDescription(header.getDescription()); + headerParam.setRequired(header.getRequired()); + headerParam.setDeprecated(header.getDeprecated()); + headerParam.setStyle(Parameter.StyleEnum.valueOf(header.getStyle().name())); + headerParam.setExplode(header.getExplode()); + headerParam.setSchema(header.getSchema()); + headerParam.setExamples(header.getExamples()); + headerParam.setExample(header.getExample()); + headerParam.setContent(header.getContent()); + headerParam.setExtensions(header.getExtensions()); + CodegenParameter param = fromParameter(headerParam, imports); + headers.add(param); + } + } + CodegenEncoding ce = new CodegenEncoding( + enc.getContentType(), + headers, + enc.getStyle().toString(), + enc.getExplode().booleanValue(), + enc.getAllowReserved().booleanValue() + ); + String propName = encodingEntry.getKey(); + ceMap.put(propName, ce); + } + } + CodegenProperty schemaProp = fromProperty("schema", mt.getSchema()); + CodegenMediaType codegenMt = new CodegenMediaType(schemaProp, ceMap); + String contentType = contentEntry.getKey(); + cmtContent.put(contentType, codegenMt); + } + return cmtContent; + } + public CodegenParameter fromRequestBody(RequestBody body, Set imports, String bodyParameterName) { if (body == null) { LOGGER.error("body in fromRequestBody cannot be null!"); @@ -6578,6 +6634,7 @@ public class DefaultCodegen implements CodegenConfig { if (schema == null) { throw new RuntimeException("Request body cannot be null. Possible cause: missing schema in body parameter (OAS v2): " + body); } + codegenParameter.setContent(getContent(body.getContent(), imports)); if (StringUtils.isNotBlank(schema.get$ref())) { name = ModelUtils.getSimpleRef(schema.get$ref()); diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java index aaede097e3d8..28748850f1bd 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java @@ -3875,4 +3875,76 @@ public class DefaultCodegenTest { assertTrue(pr.isByteArray); assertFalse(pr.getIsString()); } + + @Test + public void testParameterContent() { + DefaultCodegen codegen = new DefaultCodegen(); + final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/content-data.yaml"); + codegen.setOpenAPI(openAPI); + String path; + CodegenOperation co; + + path = "/jsonQueryParams"; + co = codegen.fromOperation(path, "GET", openAPI.getPaths().get(path).getGet(), null); + CodegenParameter coordinatesInlineSchema = co.queryParams.get(0); + LinkedHashMap content = coordinatesInlineSchema.getContent(); + assertNotNull(content); + assertEquals(content.keySet(), new HashSet<>(Arrays.asList("application/json"))); + CodegenMediaType mt = content.get("application/json"); + assertNull(mt.getEncoding()); + CodegenProperty cp = mt.getSchema(); + assertTrue(cp.isMap); + assertEquals(cp.complexType, "object"); + + CodegenParameter coordinatesReferencedSchema = co.queryParams.get(1); + content = coordinatesReferencedSchema.getContent(); + mt = content.get("application/json"); + assertNull(mt.getEncoding()); + cp = mt.getSchema(); + assertFalse(cp.isMap); // because it is a referenced schema + assertEquals(cp.complexType, "coordinates"); + } + + @Test + public void testRequestBodyContent() { + DefaultCodegen codegen = new DefaultCodegen(); + final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/content-data.yaml"); + codegen.setOpenAPI(openAPI); + String path; + CodegenOperation co; + + path = "/inlineRequestBodySchemasDifferingByContentType"; + co = codegen.fromOperation(path, "POST", openAPI.getPaths().get(path).getPost(), null); + CodegenParameter bodyParameter = co.bodyParam; + LinkedHashMap content = bodyParameter.getContent(); + assertNotNull(content); + assertEquals(content.keySet(), new HashSet<>(Arrays.asList("application/json", "text/plain"))); + CodegenMediaType mt = content.get("application/json"); + assertNull(mt.getEncoding()); + CodegenProperty cp = mt.getSchema(); + assertNotNull(cp); + + mt = content.get("text/plain"); + assertNull(mt.getEncoding()); + cp = mt.getSchema(); + assertNotNull(cp); + // Note: the inline model resolver has a bug for this use case; it extracts an inline request body into a component + // but the schema it references is not string type + + path = "/refRequestBodySchemasDifferingByContentType"; + co = codegen.fromOperation(path, "POST", openAPI.getPaths().get(path).getPost(), null); + bodyParameter = co.bodyParam; + content = bodyParameter.getContent(); + assertNotNull(content); + assertEquals(content.keySet(), new HashSet<>(Arrays.asList("application/json", "text/plain"))); + mt = content.get("application/json"); + assertNull(mt.getEncoding()); + cp = mt.getSchema(); + assertEquals(cp.complexType, "coordinates"); + + mt = content.get("text/plain"); + assertNull(mt.getEncoding()); + cp = mt.getSchema(); + assertTrue(cp.isString); + } } diff --git a/modules/openapi-generator/src/test/resources/3_0/content-data.yaml b/modules/openapi-generator/src/test/resources/3_0/content-data.yaml new file mode 100644 index 000000000000..56228cd30543 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/content-data.yaml @@ -0,0 +1,83 @@ +openapi: 3.0.0 +info: + title: Tests content data in an endpoint parameter and a request body + description: blah +paths: + /jsonQueryParams: + get: + parameters: + - name: coordinatesInlineSchema + in: query + content: + application/json: + schema: + type: object + required: + - lat + - long + properties: + lat: + type: number + long: + type: number + - name: coordinatesReferencedSchema + in: query + content: + application/json: + schema: + $ref: '#/components/schemas/coordinates' + responses: + '201': + description: 'OK' + /inlineRequestBodySchemasDifferingByContentType: + post: + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - lat + - long + properties: + lat: + type: number + long: + type: number + text/plain: + schema: + type: string + minLength: 5 + responses: + 200: + description: OK + /refRequestBodySchemasDifferingByContentType: + post: + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/coordinates' + text/plain: + schema: + $ref: '#/components/schemas/stringWithMinLength' + responses: + 200: + description: OK +components: + schemas: + stringWithMinLength: + type: string + minLength: 5 + coordinates: + type: object + required: + - lat + - long + properties: + lat: + type: number + long: + type: number diff --git a/samples/server/petstore/python-aiohttp-srclayout/src/openapi_server/openapi/openapi.yaml b/samples/server/petstore/python-aiohttp-srclayout/src/openapi_server/openapi/openapi.yaml index 8b70dbaa5fb1..416d4ab0b6d6 100644 --- a/samples/server/petstore/python-aiohttp-srclayout/src/openapi_server/openapi/openapi.yaml +++ b/samples/server/petstore/python-aiohttp-srclayout/src/openapi_server/openapi/openapi.yaml @@ -680,22 +680,30 @@ components: properties: id: format: int64 + title: id type: integer username: + title: username type: string firstName: + title: firstName type: string lastName: + title: lastName type: string email: + title: email type: string password: + title: password type: string phone: + title: phone type: string userStatus: description: User Status format: int32 + title: userStatus type: integer title: a User type: object @@ -738,15 +746,18 @@ components: properties: id: format: int64 + title: id type: integer category: $ref: '#/components/schemas/Category' name: example: doggie + title: name type: string photoUrls: items: type: string + title: photoUrls type: array xml: name: photoUrl @@ -754,6 +765,7 @@ components: tags: items: $ref: '#/components/schemas/Tag' + title: tags type: array xml: name: tag @@ -764,6 +776,7 @@ components: - available - pending - sold + title: status type: string required: - name diff --git a/samples/server/petstore/python-aiohttp/openapi_server/openapi/openapi.yaml b/samples/server/petstore/python-aiohttp/openapi_server/openapi/openapi.yaml index 8b70dbaa5fb1..416d4ab0b6d6 100644 --- a/samples/server/petstore/python-aiohttp/openapi_server/openapi/openapi.yaml +++ b/samples/server/petstore/python-aiohttp/openapi_server/openapi/openapi.yaml @@ -680,22 +680,30 @@ components: properties: id: format: int64 + title: id type: integer username: + title: username type: string firstName: + title: firstName type: string lastName: + title: lastName type: string email: + title: email type: string password: + title: password type: string phone: + title: phone type: string userStatus: description: User Status format: int32 + title: userStatus type: integer title: a User type: object @@ -738,15 +746,18 @@ components: properties: id: format: int64 + title: id type: integer category: $ref: '#/components/schemas/Category' name: example: doggie + title: name type: string photoUrls: items: type: string + title: photoUrls type: array xml: name: photoUrl @@ -754,6 +765,7 @@ components: tags: items: $ref: '#/components/schemas/Tag' + title: tags type: array xml: name: tag @@ -764,6 +776,7 @@ components: - available - pending - sold + title: status type: string required: - name diff --git a/samples/server/petstore/python-fastapi/openapi.yaml b/samples/server/petstore/python-fastapi/openapi.yaml index 133f2f16cfcf..94256ed0a0f2 100644 --- a/samples/server/petstore/python-fastapi/openapi.yaml +++ b/samples/server/petstore/python-fastapi/openapi.yaml @@ -691,22 +691,30 @@ components: properties: id: format: int64 + title: id type: integer username: + title: username type: string firstName: + title: firstName type: string lastName: + title: lastName type: string email: + title: email type: string password: + title: password type: string phone: + title: phone type: string userStatus: description: User Status format: int32 + title: userStatus type: integer title: a User type: object diff --git a/samples/server/petstore/python-flask/openapi_server/openapi/openapi.yaml b/samples/server/petstore/python-flask/openapi_server/openapi/openapi.yaml index 8b402c5f7bbd..f94133dcf463 100644 --- a/samples/server/petstore/python-flask/openapi_server/openapi/openapi.yaml +++ b/samples/server/petstore/python-flask/openapi_server/openapi/openapi.yaml @@ -669,22 +669,30 @@ components: properties: id: format: int64 + title: id type: integer username: + title: username type: string firstName: + title: firstName type: string lastName: + title: lastName type: string email: + title: email type: string password: + title: password type: string phone: + title: phone type: string userStatus: description: User Status format: int32 + title: userStatus type: integer title: a User type: object @@ -727,15 +735,18 @@ components: properties: id: format: int64 + title: id type: integer category: $ref: '#/components/schemas/Category' name: example: doggie + title: name type: string photoUrls: items: type: string + title: photoUrls type: array xml: name: photoUrl @@ -743,6 +754,7 @@ components: tags: items: $ref: '#/components/schemas/Tag' + title: tags type: array xml: name: tag @@ -753,6 +765,7 @@ components: - available - pending - sold + title: status type: string required: - name