Skip to content

Commit

Permalink
Fix convert Operation annotation with groovy
Browse files Browse the repository at this point in the history
  • Loading branch information
altro3 committed Feb 6, 2024
1 parent 7ff25fb commit 25e213a
Show file tree
Hide file tree
Showing 7 changed files with 234 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@
import static io.micronaut.openapi.visitor.SchemaUtils.isIgnoredHeader;
import static io.micronaut.openapi.visitor.SchemaUtils.setOperationOnPathItem;
import static io.micronaut.openapi.visitor.Utils.DEFAULT_MEDIA_TYPES;
import static io.micronaut.openapi.visitor.Utils.getMediaType;

/**
* A {@link io.micronaut.inject.visitor.TypeElementVisitor} the builds the Swagger model from Micronaut controllers at compile time.
Expand Down Expand Up @@ -486,22 +487,22 @@ private void processExtraBodyParameters(VisitorContext context, HttpMethod httpM
if (HttpMethod.permitsRequestBody(httpMethod) && !extraBodyParameters.isEmpty()) {
if (requestBody == null) {
requestBody = new RequestBody();
Content content = new Content();
var content = new Content();
requestBody.setContent(content);
requestBody.setRequired(true);
swaggerOperation.setRequestBody(requestBody);

consumesMediaTypes = CollectionUtils.isEmpty(consumesMediaTypes) ? DEFAULT_MEDIA_TYPES : consumesMediaTypes;
consumesMediaTypes.forEach(mediaType -> {
io.swagger.v3.oas.models.media.MediaType mt = new io.swagger.v3.oas.models.media.MediaType();
var mt = new io.swagger.v3.oas.models.media.MediaType();
var schema = new Schema<>();
schema.setType(TYPE_OBJECT);
mt.setSchema(schema);
content.addMediaType(mediaType.toString(), mt);
});
}
}
if (requestBody != null && !extraBodyParameters.isEmpty()) {
if (requestBody != null && requestBody.getContent() != null && !extraBodyParameters.isEmpty()) {
requestBody.getContent().forEach((mediaTypeName, mediaType) -> {
var schema = mediaType.getSchema();
if (schema == null) {
Expand All @@ -523,7 +524,7 @@ private void processExtraBodyParameters(VisitorContext context, HttpMethod httpM
}
for (TypedElement parameter : extraBodyParameters) {
if (!isRequestBodySchemaSet) {
processBodyParameter(context, openAPI, javadocDescription, MediaType.of(mediaTypeName), schema, parameter);
processBodyParameter(context, openAPI, javadocDescription, getMediaType(mediaTypeName), schema, parameter);
}
if (mediaTypeName.equals(MediaType.MULTIPART_FORM_DATA)) {
if (CollectionUtils.isNotEmpty(schema.getProperties())) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ <T> Optional<T> toValue(Map<CharSequence, Object> values, VisitorContext context
try {
return Optional.ofNullable(ConvertUtils.treeToValue(node, type, context));
} catch (JsonProcessingException e) {
warn("Error converting [" + node + "]: to " + type + ": " + e.getMessage(), context);
warn("Error converting [" + node + "]: to " + type + ":\n" + Utils.printStackTrace(e), context);
}
return Optional.empty();
}
Expand Down Expand Up @@ -2281,7 +2281,7 @@ private List<Object> getEnumValues(EnumElement type, String schemaType, String s
try {
enumValues.add(ConvertUtils.normalizeValue(jacksonValue, schemaType, schemaFormat, context));
} catch (JsonProcessingException e) {
warn("Error converting jacksonValue " + jacksonValue + " : to " + type + ": " + e.getMessage(), context, element);
warn("Error converting jacksonValue " + jacksonValue + " : to " + type + ":\n" + Utils.printStackTrace(e), context, element);
enumValues.add(element.getSimpleName());
}
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,13 @@
import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.servers.Server;
import io.swagger.v3.oas.annotations.servers.ServerVariable;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.examples.Example;
import io.swagger.v3.oas.models.media.Content;
import io.swagger.v3.oas.models.media.Encoding;
import io.swagger.v3.oas.models.media.MediaType;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.parameters.RequestBody;
import io.swagger.v3.oas.models.responses.ApiResponse;
import io.swagger.v3.oas.models.security.SecurityRequirement;

Expand Down Expand Up @@ -106,7 +110,7 @@ public static <T> T toValue(Map<CharSequence, Object> values, VisitorContext con
try {
return ConvertUtils.treeToValue(node, type, context);
} catch (JsonProcessingException e) {
warn("Error converting [" + node + "]: to " + type + ": " + e.getMessage(), context);
warn("Error converting [" + node + "]: to " + type + ":\n" + Utils.printStackTrace(e), context);
}
return null;
}
Expand Down Expand Up @@ -266,9 +270,11 @@ public static <T> T treeToValue(JsonNode jn, Class<T> clazz, VisitorContext cont
processMediaType(result, contentNode);
}
((ApiResponse) value).setContent(result);
} else {
throw e;
// fix for problem with groovy. Jackson throw exception with Operation class with content and mediaType
// see https://github.com/micronaut-projects/micronaut-openapi/issues/1418
}

value = fixForGroovy(jn, clazz, e);
}

if (value == null) {
Expand Down Expand Up @@ -313,6 +319,77 @@ public static <T> T treeToValue(JsonNode jn, Class<T> clazz, VisitorContext cont
return value;
}

private static <T> Map<String, T> deserMap(String name, JsonNode jn, Class<T> clazz) throws JsonProcessingException {

var mapNode = jn.get(name);
if (mapNode == null) {
return null;
}
((ObjectNode) jn).remove(name);

var iter = mapNode.fieldNames();
var result = new HashMap<String, T>();
while (iter.hasNext()) {
var entryKey = iter.next();
var objectNode = mapNode.get(entryKey);
var object = CONVERT_JSON_MAPPER.treeToValue(objectNode, clazz);
result.put(entryKey, object);
}
return !result.isEmpty() ? result : null;
}

private static <T> T fixForGroovy(JsonNode jn, Class<T> clazz, Exception e) throws JsonProcessingException {

// fix for problem with groovy. Jackson throw exception with Operation class with content and mediaType
// see https://github.com/micronaut-projects/micronaut-openapi/issues/1418
if (clazz == Operation.class && jn.has("requestBody")) {

var requestBodyNode = jn.get("requestBody");
((ObjectNode) jn).remove("requestBody");
T value = CONVERT_JSON_MAPPER.treeToValue(jn, clazz);
var requestBody = fixRequestBodyForGroovy(requestBodyNode);
((Operation) value).setRequestBody(requestBody);
return value;
} else if (clazz == RequestBody.class) {
return (T) fixRequestBodyForGroovy(jn);
} else {
throw new RuntimeException(e);
}
}

private static RequestBody fixRequestBodyForGroovy(JsonNode requestBodyNode) throws JsonProcessingException {
var contentNode = requestBodyNode.get("content");
if (contentNode == null) {
return null;
}

var examples = deserMap("examples", contentNode, Example.class);
var encoding = deserMap("encoding", contentNode, Encoding.class);
var extensions = deserMap("extensions", contentNode, Object.class);
var schemaNode = contentNode.get("schema");
Schema<?> schema = null;
if (schemaNode != null) {
schema = CONVERT_JSON_MAPPER.treeToValue(schemaNode, Schema.class);
}

var mediaTypeNode = contentNode.get("mediaType");
((ObjectNode) contentNode).remove("mediaType");
var requestBody = CONVERT_JSON_MAPPER.treeToValue(requestBodyNode, RequestBody.class);
var content = requestBody.getContent();
var mediaType = content.get("schema");
content.remove("schema");
if (mediaType == null) {
mediaType = new MediaType();
}
content.put(mediaTypeNode.textValue(), mediaType);
mediaType.setExamples(examples);
mediaType.setEncoding(encoding);
mediaType.setExtensions(extensions);
mediaType.setSchema(schema);

return requestBody;
}

private static void processMediaType(Content result, JsonNode content) throws JsonProcessingException {
var mediaType = content.has("mediaType") ? content.get("mediaType").asText() : io.micronaut.http.MediaType.APPLICATION_JSON;
var mediaTypeObj = CONVERT_JSON_MAPPER.treeToValue(content, MediaType.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ private List<MediaType> mediaTypes(MethodElement element, Class<? extends Annota
return DEFAULT_MEDIA_TYPES;
}
return Arrays.stream(values)
.map(MediaType::of)
.map(Utils::getMediaType)
.distinct()
.toList();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ private static List<MediaType> mediaTypes(String... arr) {
return DEFAULT_MEDIA_TYPES;
}
return Arrays.stream(arr)
.map(MediaType::of)
.map(Utils::getMediaType)
.toList();
}

Expand Down
14 changes: 14 additions & 0 deletions openapi/src/main/java/io/micronaut/openapi/visitor/Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,20 @@ public final class Utils {
private Utils() {
}

/**
* Get or create MediaType object by name;
*
* @param mediaTypeName name of mediaType
* @return MediaType object
*/
public static MediaType getMediaType(String mediaTypeName) {
try {
return MediaType.of(mediaTypeName);
} catch (Exception e) {
return new MediaType(mediaTypeName);
}
}

/**
* @return An Instance of default {@link PropertyPlaceholderResolver} to resolve placeholders.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.micronaut.openapi.visitor

import io.micronaut.ast.transform.test.AbstractBeanDefinitionSpec
import io.micronaut.http.MediaType
import io.swagger.v3.oas.models.OpenAPI
import io.swagger.v3.oas.models.media.Schema
import spock.lang.Issue
Expand Down Expand Up @@ -199,4 +200,134 @@ public class MyBean {}
openAPI.paths."/test1".post.requestBody.content."application/json".schema.additionalProperties == true
openAPI.paths."/test1".post.requestBody.content."application/json".schema.default == null
}

void "test operation with content groovy"() {
when:
buildBeanDefinition("test.MyBean", '''
package test;
import groovy.transform.CompileStatic
import io.micronaut.http.HttpStatus
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Post
import io.micronaut.http.annotation.Status
import io.micronaut.http.multipart.CompletedFileUpload
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.media.Content
import io.swagger.v3.oas.annotations.media.Encoding
import io.swagger.v3.oas.annotations.media.ExampleObject
import io.swagger.v3.oas.annotations.media.Schema
import io.swagger.v3.oas.annotations.parameters.RequestBody
import io.swagger.v3.oas.annotations.responses.ApiResponse
@Controller('/file')
@CompileStatic
class FileController {
@Post(value = "/", consumes = MediaType.MULTIPART_FORM_DATA)
@Status(HttpStatus.NO_CONTENT)
@Operation(
operationId = 'UploadFile',
summary = 'Upload a file',
requestBody = @RequestBody(
description = 'File request',
content = @Content(
mediaType = MediaType.MULTIPART_FORM_DATA,
encoding = [
@Encoding(
name = "file",
contentType = MediaType.APPLICATION_OCTET_STREAM
)
],
examples = [
@ExampleObject(
name = "example-1",
summary = "sum",
description = "this is description",
value = "{\\"name\\":\\"Charlie\\"}")
],
schema = @Schema(type = 'object'))
),
responses = [
@ApiResponse(responseCode = '204', description = 'OK'),
]
)
void uploadFile(CompletedFileUpload file) {
assert file.bytes
}
}
@jakarta.inject.Singleton
public class MyBean {}
''')

OpenAPI openAPI = Utils.testReference

then:
openAPI.paths."/file".post.requestBody.content."multipart/form-data"
openAPI.paths."/file".post.requestBody.content."multipart/form-data".schema
openAPI.paths."/file".post.requestBody.content."multipart/form-data".encoding.file.contentType == MediaType.APPLICATION_OCTET_STREAM
}

void "test requestBody groovy"() {
when:
buildBeanDefinition("test.MyBean", '''
package test
import groovy.transform.CompileStatic
import io.micronaut.http.HttpStatus
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Post
import io.micronaut.http.annotation.Status
import io.micronaut.http.multipart.CompletedFileUpload
import io.swagger.v3.oas.annotations.media.Content
import io.swagger.v3.oas.annotations.media.Encoding
import io.swagger.v3.oas.annotations.media.ExampleObject
import io.swagger.v3.oas.annotations.media.Schema
import io.swagger.v3.oas.annotations.parameters.RequestBody
@Controller('/file')
@CompileStatic
class FileController {
@Post(value = "/", consumes = MediaType.MULTIPART_FORM_DATA)
@Status(HttpStatus.NO_CONTENT)
void uploadFile(@RequestBody(
description = 'File request',
content = @Content(
mediaType = MediaType.MULTIPART_FORM_DATA,
encoding = [
@Encoding(
name = "file",
contentType = MediaType.APPLICATION_OCTET_STREAM
)
],
examples = [
@ExampleObject(
name = "example-1",
summary = "sum",
description = "this is description",
value = "{\\"name\\":\\"Charlie\\"}")
],
schema = @Schema(type = 'object'))
) CompletedFileUpload file) {
assert file.bytes
}
}
@jakarta.inject.Singleton
public class MyBean {}
''')

OpenAPI openAPI = Utils.testReference

then:
openAPI.paths."/file".post.requestBody.content."multipart/form-data"
openAPI.paths."/file".post.requestBody.content."multipart/form-data".schema
openAPI.paths."/file".post.requestBody.content."multipart/form-data".encoding.file.contentType == MediaType.APPLICATION_OCTET_STREAM
}
}

0 comments on commit 25e213a

Please sign in to comment.