Skip to content

Commit f24c14e

Browse files
joseegman-idoneeajoseegarciaclaude
authored
Map raw binary request/response bodies to Resource (sngular#394)
* Map raw binary request/response bodies to Resource MapperPathUtil.getObjectOrType typed a top-level `type: string, format: binary` body (e.g. application/octet-stream, not wrapped in a multipart object) as String, because it never checked ApiTool.isBinary. Binary -> file type was only applied to object properties and array items. Add a RESOURCE type (org.springframework.core.io.Resource) and return it from getObjectOrType for binary schemas. Resource (not MultipartFile) is used because a raw @RequestBody binary payload is a streaming resource, not a multipart part; the reactive template wraps it as Mono<Resource>. Both server templates import Resource on demand. Adds testBinaryBodyResource and testReactiveBinaryBodyResource fixtures. Fixes sngular#393 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Bump version to 6.6.4 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: joseegarcia <jose.garcia@disashop.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 23082aa commit f24c14e

14 files changed

Lines changed: 241 additions & 10 deletions

File tree

multiapi-engine/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
<groupId>com.sngular</groupId>
66
<artifactId>multiapi-engine</artifactId>
7-
<version>6.6.3</version>
7+
<version>6.6.4</version>
88
<packaging>jar</packaging>
99

1010
<properties>

multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/model/SchemaFieldObjectType.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ public class SchemaFieldObjectType {
3838
new SimpleImmutableEntry<>(TypeConstants.ZONEDDATETIME, ZONED_DATE_TIME),
3939
new SimpleImmutableEntry<>(TypeConstants.OFFSETDATE, OFFSET_DATE_TIME),
4040
new SimpleImmutableEntry<>(TypeConstants.OFFSETDATETIME, OFFSET_DATE_TIME),
41-
new SimpleImmutableEntry<>(TypeConstants.MULTIPART_FILE, "MultipartFile")
41+
new SimpleImmutableEntry<>(TypeConstants.MULTIPART_FILE, "MultipartFile"),
42+
new SimpleImmutableEntry<>(TypeConstants.RESOURCE, "Resource")
4243
);
4344

4445
private static final Map<String, String> IMPORT_TYPE_MAPPINGS = Map.ofEntries(
@@ -53,7 +54,8 @@ public class SchemaFieldObjectType {
5354
new SimpleImmutableEntry<>(TypeConstants.ZONEDDATETIME, "java.time." + ZONED_DATE_TIME),
5455
new SimpleImmutableEntry<>(TypeConstants.OFFSETDATE, "java.time." + OFFSET_DATE_TIME),
5556
new SimpleImmutableEntry<>(TypeConstants.OFFSETDATETIME, "java.time." + OFFSET_DATE_TIME),
56-
new SimpleImmutableEntry<>(TypeConstants.MULTIPART_FILE, "MultipartFile")
57+
new SimpleImmutableEntry<>(TypeConstants.MULTIPART_FILE, "MultipartFile"),
58+
new SimpleImmutableEntry<>(TypeConstants.RESOURCE, "org.springframework.core.io.Resource")
5759
);
5860

5961
private static final Map<String, String> IMPL_TYPE_MAPPINGS = Map.ofEntries(
@@ -73,7 +75,8 @@ public class SchemaFieldObjectType {
7375
new SimpleImmutableEntry<>(TypeConstants.ZONEDDATETIME, ZONED_DATE_TIME),
7476
new SimpleImmutableEntry<>(TypeConstants.OFFSETDATE, OFFSET_DATE_TIME),
7577
new SimpleImmutableEntry<>(TypeConstants.OFFSETDATETIME, OFFSET_DATE_TIME),
76-
new SimpleImmutableEntry<>(TypeConstants.MULTIPART_FILE, "MultipartFile")
78+
new SimpleImmutableEntry<>(TypeConstants.MULTIPART_FILE, "MultipartFile"),
79+
new SimpleImmutableEntry<>(TypeConstants.RESOURCE, "Resource")
7780
);
7881

7982
private final String baseType;

multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/model/TypeConstants.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ public final class TypeConstants {
5656

5757
public static final String MULTIPART_FILE = "multipartfile";
5858

59+
public static final String RESOURCE = "resource";
60+
5961
public static final Set<String> BASIC_OBJECT_TYPE = Set.of(NUMBER, STRING, BOOLEAN, INTEGER, ARRAY);
6062

6163
public static final Set<String> NO_IMPORT_TYPE = Set.of(STRING, INTEGER, OBJECT);
@@ -94,7 +96,8 @@ public final class TypeConstants {
9496
ZONEDDATETIME,
9597
OFFSETDATE,
9698
OFFSETDATETIME,
97-
MULTIPART_FILE
99+
MULTIPART_FILE,
100+
RESOURCE
98101
);
99102

100103
private TypeConstants() {

multiapi-engine/src/main/java/com/sngular/api/generator/plugin/openapi/utils/MapperPathUtil.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,11 @@ private static JsonNode getRefSchema(JsonNode schema, SpecFile specFile, GlobalO
473473
private static SchemaFieldObjectType getObjectOrType(
474474
final JsonNode schema, final String pojoName, final SpecFile specFile, final GlobalObject globalObject,
475475
final Path baseDir) {
476+
if (ApiTool.isBinary(schema)) {
477+
// A raw binary body (e.g. application/octet-stream, type: string + format: binary,
478+
// not wrapped in a multipart object) maps to a streaming Resource, not String.
479+
return new SchemaFieldObjectType(TypeConstants.RESOURCE);
480+
}
476481
return switch (ApiTool.getType(schema)) {
477482
case TypeConstants.OBJECT -> SchemaFieldObjectType.fromTypeList(TypeConstants.OBJECT, pojoName);
478483
case TypeConstants.INTEGER -> new SchemaFieldObjectType(getIntegerFormat(schema));

multiapi-engine/src/main/resources/templates/openapi/template.ftlh

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package <#if packageApi??>${packageApi}<#elseif package??> ${package}</#if>;
44
<#assign imports=[]>
55
<#assign model_imports=[]>
66
<#assign needsMultipartFile=false>
7+
<#assign needsResource=false>
78
<#list pathObjects as path>
89
<#list path.operationObjects as operation>
910
<#list operation.requestObjects as request>
@@ -18,6 +19,18 @@ package <#if packageApi??>${packageApi}<#elseif package??> ${package}</#if>;
1819
</#if>
1920
</#list>
2021
</#if>
22+
<#list request.contentObjects as content>
23+
<#if content.dataType.containsType("resource")>
24+
<#assign needsResource=true>
25+
</#if>
26+
</#list>
27+
</#list>
28+
<#list operation.responseObjects as response>
29+
<#list response.contentObjects as content>
30+
<#if content.dataType.containsType("resource")>
31+
<#assign needsResource=true>
32+
</#if>
33+
</#list>
2134
</#list>
2235
</#list>
2336
</#list>
@@ -85,6 +98,9 @@ import org.springframework.web.context.request.NativeWebRequest;
8598
<#if needsMultipartFile>
8699
import org.springframework.web.multipart.MultipartFile;
87100
</#if>
101+
<#if needsResource>
102+
import org.springframework.core.io.Resource;
103+
</#if>
88104

89105
<#list model_imports as import>
90106
import <#if packageModel??>${packageModel}<#elseif package??>${package}</#if>.${import};

multiapi-engine/src/main/resources/templates/openapi/templateReactive.ftlh

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import reactor.core.publisher.Mono;
2323
import reactor.core.publisher.Flux;
2424
import springfox.documentation.annotations.ApiIgnore;
2525
<#assign needsFilePart=false>
26+
<#assign needsResource=false>
2627
<#list pathObjects as path>
2728
<#list path.operationObjects as operation>
2829
<#list operation.requestObjects as request>
@@ -37,12 +38,27 @@ import springfox.documentation.annotations.ApiIgnore;
3738
</#if>
3839
</#list>
3940
</#if>
41+
<#list request.contentObjects as content>
42+
<#if content.dataType.containsType("resource")>
43+
<#assign needsResource=true>
44+
</#if>
45+
</#list>
46+
</#list>
47+
<#list operation.responseObjects as response>
48+
<#list response.contentObjects as content>
49+
<#if content.dataType.containsType("resource")>
50+
<#assign needsResource=true>
51+
</#if>
52+
</#list>
4053
</#list>
4154
</#list>
4255
</#list>
4356
<#if needsFilePart>
4457
import org.springframework.http.codec.multipart.FilePart;
4558
</#if>
59+
<#if needsResource>
60+
import org.springframework.core.io.Resource;
61+
</#if>
4662

4763
<#assign imports=[]>
4864
<#list pathObjects as path>

multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorFixtures.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,16 @@ public final class OpenApiGeneratorFixtures {
8686
.apiPackage("com.sngular.multifileplugin.testreactiveformdatamultipart")
8787
.useLombokModelAnnotation(false).isReactive(true).build());
8888

89+
static final List<SpecFile> TEST_BINARY_BODY_RESOURCE = List
90+
.of(SpecFile.builder().filePath("openapigenerator/testBinaryBodyResource/api-test.yml")
91+
.apiPackage("com.sngular.multifileplugin.testbinarybodyresource")
92+
.useLombokModelAnnotation(false).build());
93+
94+
static final List<SpecFile> TEST_REACTIVE_BINARY_BODY_RESOURCE = List
95+
.of(SpecFile.builder().filePath("openapigenerator/testReactiveBinaryBodyResource/api-test.yml")
96+
.apiPackage("com.sngular.multifileplugin.testreactivebinarybodyresource")
97+
.useLombokModelAnnotation(false).isReactive(true).build());
98+
8999
static final List<SpecFile> TEST_API_TAGS_GENERATION = List
90100
.of(SpecFile.builder().filePath("openapigenerator/testApiTagsGeneration/api-tags-test.yml")
91101
.apiPackage("com.sngular.multifileplugin.tagsgeneration")
@@ -607,6 +617,38 @@ static Function<Path, Boolean> validateApiReactiveGeneration(int springBootVersi
607617

608618
}
609619

620+
static Function<Path, Boolean> validateBinaryBodyResource() {
621+
622+
final String DEFAULT_TARGET_API = "generated/com/sngular/multifileplugin/testbinarybodyresource";
623+
624+
final String DEFAULT_MODEL_API = "generated/com/sngular/multifileplugin/testbinarybodyresource/model";
625+
626+
final String COMMON_PATH = "openapigenerator/testBinaryBodyResource/";
627+
628+
final String ASSETS_PATH = COMMON_PATH + "assets/";
629+
630+
final List<String> expectedTestApiFile = List.of(ASSETS_PATH + "UploadApi.java");
631+
632+
return path -> commonTest(path, expectedTestApiFile, Collections.emptyList(), DEFAULT_TARGET_API,
633+
DEFAULT_MODEL_API, Collections.emptyList(), null);
634+
}
635+
636+
static Function<Path, Boolean> validateReactiveBinaryBodyResource() {
637+
638+
final String DEFAULT_TARGET_API = "generated/com/sngular/multifileplugin/testreactivebinarybodyresource";
639+
640+
final String DEFAULT_MODEL_API = "generated/com/sngular/multifileplugin/testreactivebinarybodyresource/model";
641+
642+
final String COMMON_PATH = "openapigenerator/testReactiveBinaryBodyResource/";
643+
644+
final String ASSETS_PATH = COMMON_PATH + "assets/";
645+
646+
final List<String> expectedTestApiFile = List.of(ASSETS_PATH + "UploadApi.java");
647+
648+
return path -> commonTest(path, expectedTestApiFile, Collections.emptyList(), DEFAULT_TARGET_API,
649+
DEFAULT_MODEL_API, Collections.emptyList(), null);
650+
}
651+
610652
static Function<Path, Boolean> validateReactiveFormDataMultipart() {
611653

612654
final String DEFAULT_TARGET_API = "generated/com/sngular/multifileplugin/testreactiveformdatamultipart";

multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorTest.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ static Stream<Arguments> fileSpecToProcess() {
6565
OpenApiGeneratorFixtures.validateApiReactiveGeneration(SPRING_BOOT_VERSION)),
6666
Arguments.of("testReactiveFormDataMultipart", OpenApiGeneratorFixtures.TEST_REACTIVE_FORM_DATA_MULTIPART,
6767
OpenApiGeneratorFixtures.validateReactiveFormDataMultipart()),
68+
Arguments.of("testBinaryBodyResource", OpenApiGeneratorFixtures.TEST_BINARY_BODY_RESOURCE,
69+
OpenApiGeneratorFixtures.validateBinaryBodyResource()),
70+
Arguments.of("testReactiveBinaryBodyResource", OpenApiGeneratorFixtures.TEST_REACTIVE_BINARY_BODY_RESOURCE,
71+
OpenApiGeneratorFixtures.validateReactiveBinaryBodyResource()),
6872
Arguments.of("testApiTagsGeneration", OpenApiGeneratorFixtures.TEST_API_TAGS_GENERATION,
6973
OpenApiGeneratorFixtures.validateTagsGeneration()),
7074
Arguments.of("testMultipleRefGeneration", OpenApiGeneratorFixtures.TEST_MULTIPLE_REF_GENERATION,
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
openapi: 3.0.2
2+
info:
3+
title: Testing raw binary request body
4+
version: 1.0.0
5+
servers:
6+
- url: http://localhost/v1
7+
paths:
8+
/upload:
9+
post:
10+
tags:
11+
- test
12+
operationId: uploadBinary
13+
requestBody:
14+
required: true
15+
content:
16+
application/octet-stream:
17+
schema:
18+
type: string
19+
format: binary
20+
responses:
21+
'200':
22+
description:
23+
OK
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.sngular.multifileplugin.testbinarybodyresource;
2+
3+
import java.util.Optional;
4+
import java.util.List;
5+
import java.util.Map;
6+
import javax.validation.Valid;
7+
8+
import io.swagger.v3.oas.annotations.Operation;
9+
import io.swagger.v3.oas.annotations.Parameter;
10+
import io.swagger.v3.oas.annotations.media.Content;
11+
import io.swagger.v3.oas.annotations.media.Schema;
12+
import io.swagger.v3.oas.annotations.responses.ApiResponse;
13+
import org.springframework.http.MediaType;
14+
import org.springframework.http.HttpStatus;
15+
import org.springframework.http.ResponseEntity;
16+
import org.springframework.web.bind.annotation.*;
17+
import org.springframework.web.context.request.NativeWebRequest;
18+
import org.springframework.core.io.Resource;
19+
20+
21+
public interface UploadApi {
22+
23+
/**
24+
* POST /upload
25+
* @param resource (required)
26+
* @return OK; (status code 200)
27+
*/
28+
29+
@Operation(
30+
operationId = "uploadBinary",
31+
tags = {"test"},
32+
responses = {
33+
@ApiResponse(responseCode = "200", description = "OK")
34+
}
35+
)
36+
@RequestMapping(
37+
method = RequestMethod.POST,
38+
value = "/upload",
39+
produces = {"application/json"}
40+
)
41+
42+
default ResponseEntity<Void> uploadBinary(@Parameter(name = "resource", description = "", required = true, schema = @Schema(description = "")) @Valid @RequestBody Resource resource) {
43+
return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
44+
}
45+
46+
}

0 commit comments

Comments
 (0)