Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ public class KotlinSpringServerCodegen extends AbstractKotlinCodegen
public static final String SKIP_DEFAULT_API_INTERFACE = "skipDefaultApiInterface";
public static final String SKIP_DEFAULT_DELEGATE_INTERFACE = "skipDefaultDelegateInterface";
public static final String REACTIVE = "reactive";
private static final String REACTIVE_MULTIPART = "reactiveMultipart";
public static final String INTERFACE_ONLY = "interfaceOnly";
public static final String USE_FEIGN_CLIENT_URL = "useFeignClientUrl";
public static final String USE_FEIGN_CLIENT = "useFeignClient";
Expand Down Expand Up @@ -725,6 +726,7 @@ public void processOpts() {
writePropertyBack(SKIP_DEFAULT_INTERFACE, skipDefaultInterface);
}
writePropertyBack(REACTIVE, reactive);
writePropertyBack(REACTIVE_MULTIPART, reactive && SPRING_BOOT.equals(library));
writePropertyBack(EXCEPTION_HANDLER, exceptionHandler);
writePropertyBack(USE_FLOW_FOR_ARRAY_RETURN_TYPE, useFlowForArrayReturnType);

Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{{^isFile}}{{{dataType}}}{{^required}}{{^defaultValue}}?{{/defaultValue}}{{/required}}{{/isFile}}{{#isFile}}{{#isArray}}Array<{{/isArray}}org.springframework.web.multipart.MultipartFile{{#isArray}}>{{/isArray}}{{#isNullable}}?{{/isNullable}}{{/isFile}}
{{^isFile}}{{{dataType}}}{{^required}}{{^defaultValue}}?{{/defaultValue}}{{/required}}{{/isFile}}{{#isFile}}{{#reactiveMultipart}}{{#isArray}}reactor.core.publisher.Flux<{{/isArray}}org.springframework.http.codec.multipart.Part{{#isArray}}>{{/isArray}}{{/reactiveMultipart}}{{^reactiveMultipart}}{{#isArray}}Array<{{/isArray}}org.springframework.web.multipart.MultipartFile{{#isArray}}>{{/isArray}}{{/reactiveMultipart}}{{#isNullable}}?{{/isNullable}}{{/isFile}}
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@

public class KotlinSpringServerCodegenTest {

private static final String MULTIPART_SPEC = "src/test/resources/3_0/form-multipart-binary-array.yaml";

@Test(description = "test embedded enum array")
public void embeddedEnumArrayTest() throws Exception {
String baseModelPackage = "zz";
Expand Down Expand Up @@ -432,6 +434,26 @@ public void testNullableMultipartFile() throws IOException {
+ " )");
}

@Test
public void testNullableMultipartFileReactive() throws IOException {
Map<String, File> files = generateFromContract(
"src/test/resources/3_0/kotlin/feat-multipartfile_nullable.yaml",
Map.of(KotlinSpringServerCodegen.REACTIVE, true));

assertFileContains(files.get("NullableMultipartfileApiController.kt").toPath(),
"file: org.springframework.http.codec.multipart.Part?"
+ " )");
assertFileContains(files.get("NullableMultipartfileArrayApiController.kt").toPath(),
"files: reactor.core.publisher.Flux<org.springframework.http.codec.multipart.Part>?"
+ " )");
assertFileContains(files.get("NonNullableMultipartfileApiController.kt").toPath(),
"file: org.springframework.http.codec.multipart.Part"
+ " )");
assertFileContains(files.get("NonNullableMultipartfileArrayApiController.kt").toPath(),
"files: reactor.core.publisher.Flux<org.springframework.http.codec.multipart.Part>"
+ " )");
}

@Test
public void arrayItemsCanBeNullable() throws IOException {
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
Expand Down Expand Up @@ -984,35 +1006,98 @@ public void givenOctetStreamResponseType_whenGenerateServer_thenReturnTypeIsReso

@Test
public void givenMultipartForm_whenGenerateReactiveServer_thenParameterAreCreatedAsRequestPart() throws IOException {
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
output.deleteOnExit();
String outputPath = output.getAbsolutePath().replace('\\', '/');
Map<String, File> files = generateFromContract(
MULTIPART_SPEC,
Map.of(
KotlinSpringServerCodegen.REACTIVE, true,
KotlinSpringServerCodegen.SERVICE_IMPLEMENTATION, true));

final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/kotlin/petstore-with-tags.yaml");
final KotlinSpringServerCodegen codegen = new KotlinSpringServerCodegen();
codegen.setOpenAPI(openAPI);
codegen.setOutputDir(output.getAbsolutePath());
assertReactiveMultipartParameters(files, "Controller.kt");
assertReactiveMultipartParameters(files, "Service.kt");
assertReactiveMultipartParameters(files, "ServiceImpl.kt");
assertReactiveMultipartParameters(files, "Test.kt");
}

ClientOptInput input = new ClientOptInput();
input.openAPI(openAPI);
input.config(codegen);
@Test
public void givenMultipartForm_whenGenerateReactiveDelegate_thenParametersUseRequestPart() throws IOException {
Map<String, File> files = generateFromContract(
MULTIPART_SPEC,
Map.of(
KotlinSpringServerCodegen.REACTIVE, true,
KotlinSpringServerCodegen.DELEGATE_PATTERN, true));

DefaultGenerator generator = new DefaultGenerator();
assertReactiveMultipartParameters(files, ".kt");
assertReactiveMultipartParameters(files, "Delegate.kt");
}

generator.setGeneratorPropertyDefault(CodegenConstants.MODELS, "false");
generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_TESTS, "false");
generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_DOCS, "false");
generator.setGeneratorPropertyDefault(CodegenConstants.APIS, "true");
generator.setGeneratorPropertyDefault(CodegenConstants.SUPPORTING_FILES, "false");
@Test
public void givenMultipartForm_whenGenerateBlockingServer_thenMultipartFileTypesRemain() throws IOException {
Map<String, File> files = generateFromContract(
MULTIPART_SPEC,
Map.of(KotlinSpringServerCodegen.SERVICE_IMPLEMENTATION, true));

generator.opts(input).generate();
assertBlockingMultipartParameters(files, "Controller.kt");
assertBlockingMultipartParameters(files, "Service.kt");
assertBlockingMultipartParameters(files, "ServiceImpl.kt");
assertBlockingMultipartParameters(files, "Test.kt");
}

Path outputFilepath = Paths.get(outputPath + "/src/main/kotlin/org/openapitools/api/PetApiController.kt");
@Test
public void givenMultipartForm_whenGenerateReactiveDeclarativeHttpInterface_thenMultipartFileTypesRemain() throws IOException {
Map<String, File> files = generateFromContract(
MULTIPART_SPEC,
Map.of(
KotlinSpringServerCodegen.REACTIVE, true,
KotlinSpringServerCodegen.USE_FLOW_FOR_ARRAY_RETURN_TYPE, false),
new HashMap<>(),
configurator -> configurator.setLibrary(SPRING_DECLARATIVE_HTTP_INTERFACE_LIBRARY));

assertFileContains(outputFilepath,
"@Parameter(description = \"Additional data to pass to server\") @Valid @RequestParam(value = \"additionalMetadata\", required = false) additionalMetadata: kotlin.String?");
assertFileContains(outputFilepath,
"@Parameter(description = \"image to upload\") @Valid @RequestPart(\"image\", required = false) image: org.springframework.web.multipart.MultipartFile");
Path apiFile = files.get("MultipartApi.kt").toPath();
assertFileContains(apiFile,
"files: Array<org.springframework.web.multipart.MultipartFile>",
"file: org.springframework.web.multipart.MultipartFile",
"status: MultipartMixedStatus",
"marker: MultipartMixedRequestMarker?",
"statusArray: kotlin.collections.List<MultipartMixedStatus>?");
assertFileNotContains(apiFile, "org.springframework.http.codec.multipart.Part");
}

private void assertReactiveMultipartParameters(Map<String, File> files, String fileSuffix) {
Path arrayFile = files.get("MultipartArrayApi" + fileSuffix).toPath();
Path singleFile = files.get("MultipartSingleApi" + fileSuffix).toPath();
Path mixedFile = files.get("MultipartMixedApi" + fileSuffix).toPath();

assertFileContains(arrayFile,
"files: reactor.core.publisher.Flux<org.springframework.http.codec.multipart.Part>");
assertFileContains(singleFile,
"file: org.springframework.http.codec.multipart.Part");
assertFileContains(mixedFile,
"status: MultipartMixedStatus",
"file: org.springframework.http.codec.multipart.Part",
"marker: MultipartMixedRequestMarker?",
"statusArray: kotlin.collections.List<MultipartMixedStatus>?");
assertFileNotContains(arrayFile, "org.springframework.web.multipart.MultipartFile");
assertFileNotContains(singleFile, "org.springframework.web.multipart.MultipartFile");
assertFileNotContains(mixedFile, "org.springframework.web.multipart.MultipartFile");
}

private void assertBlockingMultipartParameters(Map<String, File> files, String fileSuffix) {
Path arrayFile = files.get("MultipartArrayApi" + fileSuffix).toPath();
Path singleFile = files.get("MultipartSingleApi" + fileSuffix).toPath();
Path mixedFile = files.get("MultipartMixedApi" + fileSuffix).toPath();

assertFileContains(arrayFile,
"files: Array<org.springframework.web.multipart.MultipartFile>");
assertFileContains(singleFile,
"file: org.springframework.web.multipart.MultipartFile");
assertFileContains(mixedFile,
"status: MultipartMixedStatus",
"file: org.springframework.web.multipart.MultipartFile",
"marker: MultipartMixedRequestMarker?",
"statusArray: kotlin.collections.List<MultipartMixedStatus>?");
assertFileNotContains(arrayFile, "org.springframework.http.codec.multipart.Part");
assertFileNotContains(singleFile, "org.springframework.http.codec.multipart.Part");
assertFileNotContains(mixedFile, "org.springframework.http.codec.multipart.Part");

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ class PetApiController(@Autowired(required = true) val service: PetApiService) {
suspend fun uploadFile(
@Parameter(description = "ID of pet to update", required = true) @PathVariable("petId") petId: kotlin.Long,
@Parameter(description = "Additional data to pass to server") @Valid @RequestParam(value = "additionalMetadata", required = false) additionalMetadata: kotlin.String?,
@Parameter(description = "file to upload") @Valid @RequestPart("file", required = false) file: org.springframework.web.multipart.MultipartFile
@Parameter(description = "file to upload") @Valid @RequestPart("file", required = false) file: org.springframework.http.codec.multipart.Part
): ResponseEntity<ModelApiResponse> {
return ResponseEntity(service.uploadFile(petId, additionalMetadata, file), HttpStatus.valueOf(200))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,5 +101,5 @@ interface PetApiService {
* @return successful operation (status code 200)
* @see PetApi#uploadFile
*/
suspend fun uploadFile(petId: kotlin.Long, additionalMetadata: kotlin.String?, file: org.springframework.web.multipart.MultipartFile): ModelApiResponse
suspend fun uploadFile(petId: kotlin.Long, additionalMetadata: kotlin.String?, file: org.springframework.http.codec.multipart.Part): ModelApiResponse
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class PetApiServiceImpl : PetApiService {
TODO("Implement me")
}

override suspend fun uploadFile(petId: kotlin.Long, additionalMetadata: kotlin.String?, file: org.springframework.web.multipart.MultipartFile): ModelApiResponse {
override suspend fun uploadFile(petId: kotlin.Long, additionalMetadata: kotlin.String?, file: org.springframework.http.codec.multipart.Part): ModelApiResponse {
TODO("Implement me")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ class PetApiTest {
fun uploadFileTest() = runBlockingTest {
val petId: kotlin.Long = TODO()
val additionalMetadata: kotlin.String? = TODO()
val file: org.springframework.web.multipart.MultipartFile? = TODO()
val file: org.springframework.http.codec.multipart.Part = TODO()
val response: ResponseEntity<ModelApiResponse> = api.uploadFile(petId, additionalMetadata, file)

// TODO: test validations
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ class PetApiController(@Autowired(required = true) val service: PetApiService) {
suspend fun uploadFile(
@Parameter(description = "ID of pet to update", required = true) @PathVariable("petId") petId: kotlin.Long,
@Parameter(description = "Additional data to pass to server") @Valid @RequestParam(value = "additionalMetadata", required = false) additionalMetadata: kotlin.String?,
@Parameter(description = "file to upload") @Valid @RequestPart("file", required = false) file: org.springframework.web.multipart.MultipartFile
@Parameter(description = "file to upload") @Valid @RequestPart("file", required = false) file: org.springframework.http.codec.multipart.Part
): ResponseEntity<ModelApiResponse> {
return ResponseEntity(service.uploadFile(petId, additionalMetadata, file), HttpStatus.valueOf(200))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,5 +101,5 @@ interface PetApiService {
* @return successful operation (status code 200)
* @see PetApi#uploadFile
*/
suspend fun uploadFile(petId: kotlin.Long, additionalMetadata: kotlin.String?, file: org.springframework.web.multipart.MultipartFile): ModelApiResponse
suspend fun uploadFile(petId: kotlin.Long, additionalMetadata: kotlin.String?, file: org.springframework.http.codec.multipart.Part): ModelApiResponse
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class PetApiServiceImpl : PetApiService {
TODO("Implement me")
}

override suspend fun uploadFile(petId: kotlin.Long, additionalMetadata: kotlin.String?, file: org.springframework.web.multipart.MultipartFile): ModelApiResponse {
override suspend fun uploadFile(petId: kotlin.Long, additionalMetadata: kotlin.String?, file: org.springframework.http.codec.multipart.Part): ModelApiResponse {
TODO("Implement me")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ class PetApiTest {
fun uploadFileTest() = runBlockingTest {
val petId: kotlin.Long = TODO()
val additionalMetadata: kotlin.String? = TODO()
val file: org.springframework.web.multipart.MultipartFile? = TODO()
val file: org.springframework.http.codec.multipart.Part = TODO()
val response: ResponseEntity<ModelApiResponse> = api.uploadFile(petId, additionalMetadata, file)

// TODO: test validations
Expand Down