Skip to content

Commit 0debd5c

Browse files
committed
Generate correct openapi when returning files from controllers
Fixes #441
1 parent 8b8c43f commit 0debd5c

File tree

3 files changed

+113
-0
lines changed

3 files changed

+113
-0
lines changed

openapi/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ dependencies {
66
compileOnly "io.micronaut:micronaut-inject-java"
77

88
implementation "io.micronaut:micronaut-http"
9+
implementation "io.micronaut:micronaut-http-server"
10+
911
api "io.swagger.core.v3:swagger-core:$swaggerVersion"
1012
api "io.swagger.core.v3:swagger-models:$swaggerVersion"
1113
api "io.swagger.core.v3:swagger-annotations:$swaggerVersion"

openapi/src/main/java/io/micronaut/openapi/visitor/AbstractOpenApiVisitor.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import io.micronaut.core.util.CollectionUtils;
4141
import io.micronaut.core.util.StringUtils;
4242
import io.micronaut.http.MediaType;
43+
import io.micronaut.http.server.types.files.FileCustomizableResponseType;
4344
import io.micronaut.http.uri.UriMatchTemplate;
4445
import io.micronaut.inject.ast.ClassElement;
4546
import io.micronaut.inject.ast.Element;
@@ -79,6 +80,7 @@
7980
import io.swagger.v3.oas.models.media.MapSchema;
8081
import io.swagger.v3.oas.models.media.ObjectSchema;
8182
import io.swagger.v3.oas.models.media.Schema;
83+
import io.swagger.v3.oas.models.media.StringSchema;
8284
import io.swagger.v3.oas.models.security.SecurityRequirement;
8385
import io.swagger.v3.oas.models.security.SecurityScheme;
8486
import org.reactivestreams.Publisher;
@@ -690,6 +692,9 @@ private boolean isTypeNullable(ClassElement type) {
690692
if (schema != null) {
691693
schema = arraySchema(schema);
692694
}
695+
} else if (type.isAssignable(FileCustomizableResponseType.class)) {
696+
schema = new StringSchema();
697+
schema.setFormat("binary");
693698
} else {
694699
schema = getSchemaDefinition(openAPI, context, type, definingElement, mediaTypes);
695700
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package io.micronaut.openapi.visitor
2+
3+
import io.micronaut.annotation.processing.test.AbstractTypeElementSpec
4+
import io.swagger.v3.oas.models.OpenAPI
5+
import io.swagger.v3.oas.models.Operation
6+
7+
class OpenApiFileResponseTypeSpec extends AbstractTypeElementSpec {
8+
def setup() {
9+
System.setProperty(AbstractOpenApiVisitor.ATTR_TEST_MODE, "true")
10+
}
11+
12+
void "test build the OpenAPI for returning files"() {
13+
when:
14+
buildBeanDefinition('test.MyBean', '''
15+
package test;
16+
17+
import io.micronaut.http.HttpResponse;
18+
import io.micronaut.http.annotation.*;
19+
import io.micronaut.http.server.types.files.*;
20+
21+
import java.util.UUID;
22+
23+
@Controller("/upload")
24+
class FooController {
25+
26+
@Produces("application/pdf")
27+
@Get("/response-streamed-file/{documentId}")
28+
public HttpResponse<StreamedFile> action1(UUID documentId) {
29+
return null;
30+
}
31+
32+
@Produces("application/pdf")
33+
@Get("/response-system-file/{documentId}")
34+
public HttpResponse<SystemFile> action2(UUID documentId) {
35+
return null;
36+
}
37+
38+
@Produces("application/pdf")
39+
@Get("/streamed-file/{documentId}")
40+
public StreamedFile action3(UUID documentId) {
41+
return null;
42+
}
43+
44+
@Produces("application/pdf")
45+
@Get("/system-file/{documentId}")
46+
public SystemFile action4(UUID documentId) {
47+
return null;
48+
}
49+
}
50+
51+
@jakarta.inject.Singleton
52+
class MyBean {}
53+
''')
54+
55+
OpenAPI openAPI = AbstractOpenApiVisitor.testReference
56+
57+
then:
58+
openAPI
59+
openAPI.paths.size() == 4
60+
openAPI.paths.each {
61+
assert it.value.get.responses.size() == 1
62+
assert it.value.get.responses['200'].content['application/pdf'].schema.type == 'string'
63+
assert it.value.get.responses['200'].content['application/pdf'].schema.format == 'binary'
64+
assert it.value.get.responses['200'].content['application/pdf'].schema.$ref == null
65+
}
66+
}
67+
68+
void "test build the OpenAPI for returning files and check their annotations takes precedence"() {
69+
when:
70+
buildBeanDefinition('test.MyBean', '''
71+
package test;
72+
73+
import io.micronaut.http.HttpResponse;
74+
import io.micronaut.http.annotation.*;
75+
import io.micronaut.http.server.types.files.*;
76+
import io.swagger.v3.oas.annotations.responses.*;
77+
import io.swagger.v3.oas.annotations.media.*;
78+
79+
import java.util.UUID;
80+
81+
@Controller("/upload")
82+
class FooController {
83+
84+
@Produces("application/octet-stream")
85+
@Get("/{documentId}")
86+
@ApiResponse(responseCode = "200", content = @Content(mediaType = "application/octet-stream", schema = @Schema(type = "string", format = "binary")))
87+
public HttpResponse<StreamedFile> action(UUID documentId) {
88+
return null;
89+
}
90+
}
91+
92+
@jakarta.inject.Singleton
93+
class MyBean {}
94+
''')
95+
96+
OpenAPI openAPI = AbstractOpenApiVisitor.testReference
97+
98+
then: 'The content is the one defined in the @Content annotation'
99+
openAPI
100+
openAPI.paths.size() == 1
101+
Operation operation = openAPI.paths.get('/upload/{documentId}').get
102+
operation.responses['200'].content['application/octet-stream'].schema.type == 'string'
103+
operation.responses['200'].content['application/octet-stream'].schema.format == 'binary'
104+
operation.responses['200'].content['application/octet-stream'].schema.$ref == null
105+
}
106+
}

0 commit comments

Comments
 (0)