Adds request body content data to allow multiple content types to be sent to servers (#10973)

* Adds CodegenMediaType and CodegenEncoding

* Adds partial new code to set Parameter content data

* Adds content to CodegenParameter

* Sets content for request bodies

* Adds testParameterContent

* Adds testRequestBodyContent
This commit is contained in:
Justin Black
2021-11-29 09:31:38 -08:00
committed by GitHub
parent 8e2e200e18
commit 8702f24f05
10 changed files with 385 additions and 1 deletions

View File

@@ -0,0 +1,67 @@
package org.openapitools.codegen;
import java.util.List;
import java.util.Objects;
public class CodegenEncoding {
private String contentType;
private List<CodegenParameter> headers;
private String style;
private boolean explode;
private boolean allowReserved;
public CodegenEncoding(String contentType, List<CodegenParameter> 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<CodegenParameter> 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);
}
}

View File

@@ -0,0 +1,45 @@
package org.openapitools.codegen;
import java.util.LinkedHashMap;
import java.util.Objects;
public class CodegenMediaType {
private CodegenProperty schema;
private LinkedHashMap<String, CodegenEncoding> encoding;
public CodegenMediaType(CodegenProperty schema, LinkedHashMap<String, CodegenEncoding> encoding) {
this.schema = schema;
this.encoding = encoding;
}
public CodegenProperty getSchema() {
return schema;
}
public LinkedHashMap<String, CodegenEncoding> 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);
}
}

View File

@@ -110,6 +110,7 @@ public class CodegenParameter implements IJsonSchemaValidationProperties {
private boolean hasDiscriminatorWithNonEmptyMapping;
private CodegenComposedSchemas composedSchemas;
private boolean hasMultipleTypes = false;
private LinkedHashMap<String, CodegenMediaType> 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<String, CodegenMediaType> getContent() {
return content;
}
public void setContent(LinkedHashMap<String, CodegenMediaType> content) {
this.content = content;
}
}

View File

@@ -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<String, CodegenMediaType> getContent(Content content, Set<String> imports) {
if (content == null) {
return null;
}
LinkedHashMap<String, CodegenMediaType> cmtContent = new LinkedHashMap<>();
for (Entry<String, MediaType> contentEntry: content.entrySet()) {
MediaType mt = contentEntry.getValue();
LinkedHashMap<String, CodegenEncoding> ceMap = null;
if (mt.getEncoding() != null ) {
ceMap = new LinkedHashMap<>();
Map<String, Encoding> encMap = mt.getEncoding();
for (Entry<String, Encoding> encodingEntry: encMap.entrySet()) {
Encoding enc = encodingEntry.getValue();
List<CodegenParameter> headers = new ArrayList<>();
if (enc.getHeaders() != null) {
Map<String, Header> encHeaders = enc.getHeaders();
for (Entry<String, Header> 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<String> 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());

View File

@@ -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<String, CodegenMediaType> 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<String, CodegenMediaType> 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);
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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