Skip to content

Commit 265bb3d

Browse files
authored
fix: [OpenAPI] Pin file upload parameter to File type (#1160)
1 parent d5d3dfd commit 265bb3d

9 files changed

Lines changed: 370 additions & 19 deletions

File tree

datamodel/openapi/openapi-api-apache-sample/src/main/java/com/sap/cloud/sdk/datamodel/openapi/apache/sodastore/api/SodasApi.java

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
package com.sap.cloud.sdk.datamodel.openapi.apache.sodastore.api;
66

7+
import java.io.File;
78
import java.util.ArrayList;
89
import java.util.HashMap;
910
import java.util.List;
@@ -21,6 +22,7 @@
2122
import com.sap.cloud.sdk.services.openapi.apache.apiclient.BaseApi;
2223
import com.sap.cloud.sdk.services.openapi.apache.apiclient.Pair;
2324
import com.sap.cloud.sdk.services.openapi.apache.core.OpenApiRequestException;
25+
import com.sap.cloud.sdk.services.openapi.apache.core.OpenApiResponse;
2426

2527
/**
2628
* SodaStore API in version 1.0.0.
@@ -243,6 +245,68 @@ public SodaWithId sodasIdGet( @Nonnull final Long id )
243245
localVarReturnType);
244246
}
245247

248+
/**
249+
* <p>
250+
* Import soda data from a file
251+
* <p>
252+
* <p>
253+
* <b>200</b> - Imported successfully
254+
*
255+
* @param _file
256+
* The value for the parameter _file
257+
* @return An OpenApiResponse containing the status code of the HttpResponse.
258+
* @throws OpenApiRequestException
259+
* if an error occurs while attempting to invoke the API
260+
*/
261+
@Nonnull
262+
public OpenApiResponse sodasImportPost( @Nonnull final File _file )
263+
throws OpenApiRequestException
264+
{
265+
266+
// verify the required parameter '_file' is set
267+
if( _file == null ) {
268+
throw new OpenApiRequestException("Missing the required parameter '_file' when calling sodasImportPost")
269+
.statusCode(400);
270+
}
271+
272+
// create path and map variables
273+
final String localVarPath = "/sodas/upload";
274+
275+
final StringJoiner localVarQueryStringJoiner = new StringJoiner("&");
276+
final List<Pair> localVarQueryParams = new ArrayList<Pair>();
277+
final List<Pair> localVarCollectionQueryParams = new ArrayList<Pair>();
278+
final Map<String, String> localVarHeaderParams = new HashMap<String, String>(defaultHeaders);
279+
final Map<String, Object> localVarFormParams = new HashMap<String, Object>();
280+
281+
if( _file != null )
282+
localVarFormParams.put("file", _file);
283+
284+
final String[] localVarAccepts = {
285+
286+
};
287+
final String localVarAccept = ApiClient.selectHeaderAccept(localVarAccepts);
288+
final String[] localVarContentTypes = { "multipart/form-data" };
289+
final String localVarContentType = ApiClient.selectHeaderContentType(localVarContentTypes);
290+
291+
final TypeReference<OpenApiResponse> localVarReturnType = new TypeReference<OpenApiResponse>()
292+
{
293+
};
294+
295+
return apiClient
296+
.invokeAPI(
297+
localVarPath,
298+
"POST",
299+
localVarQueryParams,
300+
localVarCollectionQueryParams,
301+
localVarQueryStringJoiner.toString(),
302+
null,
303+
localVarHeaderParams,
304+
localVarFormParams,
305+
localVarAccept,
306+
localVarContentType,
307+
localVarReturnType);
308+
}
309+
246310
/**
247311
* <p>
248312
* Update a specific soda product by ID

datamodel/openapi/openapi-api-apache-sample/src/main/resources/sodastore.yaml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,27 @@ paths:
281281
format: binary
282282
'404':
283283
description: Soda product not found
284+
/sodas/upload:
285+
post:
286+
summary: Import soda data from a file
287+
operationId: sodasImportPost
288+
tags:
289+
- Sodas
290+
requestBody:
291+
required: true
292+
content:
293+
multipart/form-data:
294+
schema:
295+
type: object
296+
required:
297+
- file
298+
properties:
299+
file:
300+
type: string
301+
format: binary
302+
responses:
303+
'200':
304+
description: Imported successfully
284305
/orders:
285306
post:
286307
summary: Create a new order

datamodel/openapi/openapi-api-apache-sample/src/test/java/com/sap/cloud/sdk/services/openapi/apache/SerializationTest.java

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
package com.sap.cloud.sdk.services.openapi.apache;
22

3+
import static com.github.tomakehurst.wiremock.client.WireMock.aMultipart;
4+
import static com.github.tomakehurst.wiremock.client.WireMock.containing;
5+
import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor;
6+
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
37
import static org.assertj.core.api.Assertions.assertThat;
48

9+
import java.io.File;
10+
import java.io.IOException;
11+
512
import org.junit.jupiter.api.BeforeEach;
613
import org.junit.jupiter.api.Test;
714

@@ -98,12 +105,28 @@ void testJacksonSerializeOrder()
98105
assertThat(new ObjectMapper().readValue(expected, Order.class)).isEqualTo(order);
99106
}
100107

108+
@Test
109+
void testFileUploadHasFilenameInContentDisposition()
110+
throws IOException
111+
{
112+
WireMock.stubFor(WireMock.post(urlEqualTo("/sodas/upload")).willReturn(WireMock.ok()));
113+
114+
final var tempFile = File.createTempFile("test-soda-import", ".csv");
115+
tempFile.deleteOnExit();
116+
117+
sut.sodasImportPost(tempFile);
118+
119+
final var filePart =
120+
aMultipart("file").withHeader("Content-Disposition", containing(tempFile.getName())).build();
121+
WireMock.verify(postRequestedFor(urlEqualTo("/sodas/upload")).withRequestBodyPart(filePart));
122+
}
123+
101124
private void verify( String requestBody )
102125
{
103126
WireMock
104127
.verify(
105128
WireMock
106-
.putRequestedFor(WireMock.urlEqualTo("/sodas"))
129+
.putRequestedFor(urlEqualTo("/sodas"))
107130
.withHeader("Content-Type", WireMock.equalTo("application/json; charset=UTF-8"))
108131
.withRequestBody(WireMock.equalToJson(requestBody)));
109132
}

datamodel/openapi/openapi-generator/src/main/resources/openapi-generator/mustache-templates/libraries/apache-httpclient/api.mustache

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ public class {{classname}} extends BaseApi {
146146
@Deprecated
147147
{{/isDeprecated}}
148148
{{#vendorExtensions.x-return-nullable}}@Nullable{{/vendorExtensions.x-return-nullable}}{{^vendorExtensions.x-return-nullable}}@Nonnull{{/vendorExtensions.x-return-nullable}}
149-
public {{#returnType}}{{{returnType}}} {{/returnType}}{{^returnType}}OpenApiResponse {{/returnType}}{{#vendorExtensions.x-sap-cloud-sdk-operation-name}}{{vendorExtensions.x-sap-cloud-sdk-operation-name}}{{/vendorExtensions.x-sap-cloud-sdk-operation-name}}{{^vendorExtensions.x-sap-cloud-sdk-operation-name}}{{operationId}}{{/vendorExtensions.x-sap-cloud-sdk-operation-name}}({{#allParams}}{{>nullable_var_annotations}} final {{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}) throws OpenApiRequestException {
149+
public {{#returnType}}{{{returnType}}} {{/returnType}}{{^returnType}}OpenApiResponse {{/returnType}}{{#vendorExtensions.x-sap-cloud-sdk-operation-name}}{{vendorExtensions.x-sap-cloud-sdk-operation-name}}{{/vendorExtensions.x-sap-cloud-sdk-operation-name}}{{^vendorExtensions.x-sap-cloud-sdk-operation-name}}{{operationId}}{{/vendorExtensions.x-sap-cloud-sdk-operation-name}}({{#allParams}}{{>nullable_var_annotations}} final {{#isFile}}File{{/isFile}}{{^isFile}}{{{dataType}}}{{/isFile}} {{paramName}} {{^-last}}, {{/-last}}{{/allParams}}) throws OpenApiRequestException {
150150
{{>operationBody}}
151151
}
152152
{{/hasOptionalParams}}
@@ -185,7 +185,7 @@ public class {{classname}} extends BaseApi {
185185
@Deprecated
186186
{{/isDeprecated}}
187187
{{#vendorExtensions.x-return-nullable}}@Nullable{{/vendorExtensions.x-return-nullable}}{{^vendorExtensions.x-return-nullable}}@Nonnull{{/vendorExtensions.x-return-nullable}}
188-
public {{#returnType}}{{{returnType}}} {{/returnType}}{{^returnType}}OpenApiResponse {{/returnType}}{{#vendorExtensions.x-sap-cloud-sdk-operation-name}}{{vendorExtensions.x-sap-cloud-sdk-operation-name}}{{/vendorExtensions.x-sap-cloud-sdk-operation-name}}{{^vendorExtensions.x-sap-cloud-sdk-operation-name}}{{operationId}}{{/vendorExtensions.x-sap-cloud-sdk-operation-name}}({{#requiredParams}}{{>nullable_var_annotations}} final {{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/requiredParams}}) throws OpenApiRequestException {
188+
public {{#returnType}}{{{returnType}}} {{/returnType}}{{^returnType}}OpenApiResponse {{/returnType}}{{#vendorExtensions.x-sap-cloud-sdk-operation-name}}{{vendorExtensions.x-sap-cloud-sdk-operation-name}}{{/vendorExtensions.x-sap-cloud-sdk-operation-name}}{{^vendorExtensions.x-sap-cloud-sdk-operation-name}}{{operationId}}{{/vendorExtensions.x-sap-cloud-sdk-operation-name}}({{#requiredParams}}{{>nullable_var_annotations}} final {{#isFile}}File{{/isFile}}{{^isFile}}{{{dataType}}}{{/isFile}} {{paramName}}{{^-last}}, {{/-last}}{{/requiredParams}}) throws OpenApiRequestException {
189189
{{#hasOptionalParams}}
190190
return {{#vendorExtensions.x-sap-cloud-sdk-operation-name}}{{vendorExtensions.x-sap-cloud-sdk-operation-name}}{{/vendorExtensions.x-sap-cloud-sdk-operation-name}}{{^vendorExtensions.x-sap-cloud-sdk-operation-name}}{{operationId}}{{/vendorExtensions.x-sap-cloud-sdk-operation-name}}({{#hasRequiredParams}}{{#requiredParams}}{{paramName}}{{^-last}}, {{/-last}}{{/requiredParams}}, {{/hasRequiredParams}}{{#optionalParams}}null{{^-last}}, {{/-last}}{{/optionalParams}});
191191
{{/hasOptionalParams}}

datamodel/openapi/openapi-generator/src/test/java/com/sap/cloud/sdk/datamodel/openapi/generator/DataModelGeneratorApacheIntegrationTest.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ void integrationTests( final TestCase testCase, @TempDir final Path path )
5757
.withSapCopyrightHeader(true)
5858
.oneOfAnyOfGenerationEnabled(testCase.anyOfOneOfGenerationEnabled)
5959
.additionalProperty("useAbstractionForFiles", "true")
60-
.additionalProperty("library", LIBRARY);
60+
.additionalProperty("library", LIBRARY)
61+
.typeMappings(testCase.typeMappings);
6162

6263
testCase.additionalProperties.forEach(generationConfiguration::additionalProperty);
6364

@@ -103,7 +104,8 @@ void generateDataModelForComparison( final TestCase testCase )
103104
.withSapCopyrightHeader(true)
104105
.oneOfAnyOfGenerationEnabled(testCase.anyOfOneOfGenerationEnabled)
105106
.additionalProperty("useAbstractionForFiles", "true")
106-
.additionalProperty("library", LIBRARY);
107+
.additionalProperty("library", LIBRARY)
108+
.typeMappings(testCase.typeMappings);
107109
testCase.additionalProperties.forEach(generationConfiguration::additionalProperty);
108110

109111
GenerationConfiguration build = generationConfiguration.build();

datamodel/openapi/openapi-generator/src/test/java/com/sap/cloud/sdk/datamodel/openapi/generator/DataModelGeneratorIntegrationTest.java

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ enum TestCase
3636
true,
3737
true,
3838
6,
39-
Map.of("aiSdkConstructor", "true", "fixRedundantIsBooleanPrefix", "true", "useFloatArrays", "true")),
39+
Map.of("aiSdkConstructor", "true", "fixRedundantIsBooleanPrefix", "true", "useFloatArrays", "true"),
40+
Map.of()),
4041
API_CLASS_VENDOR_EXTENSION_YAML(
4142
"api-class-vendor-extension-yaml",
4243
"sodastore.yaml",
@@ -46,6 +47,7 @@ enum TestCase
4647
false,
4748
true,
4849
4,
50+
Map.of(),
4951
Map.of()),
5052
API_CLASS_VENDOR_EXTENSION_JSON(
5153
"api-class-vendor-extension-json",
@@ -56,6 +58,7 @@ enum TestCase
5658
false,
5759
true,
5860
6,
61+
Map.of(),
5962
Map.of()),
6063
INLINEOBJECT_SCHEMA_NAME(
6164
"inlineobject-schemas-enabled",
@@ -66,7 +69,8 @@ enum TestCase
6669
true,
6770
true,
6871
5,
69-
Map.of("fixResponseSchemaTitles", "true")),
72+
Map.of("fixResponseSchemaTitles", "true"),
73+
Map.of()),
7074
PARTIAL_GENERATION(
7175
"partial-generation",
7276
"sodastore.json",
@@ -80,7 +84,8 @@ enum TestCase
8084
.ofEntries(
8185
entry("excludePaths", "/sodas,/foobar/{baz}"),
8286
entry("excludeProperties", "Foo.bar,Soda.embedding,Soda.flavor,UpdateSoda.flavor,SodaWithFoo.foo"),
83-
entry("removeUnusedComponents", "true"))),
87+
entry("removeUnusedComponents", "true")),
88+
Map.of()),
8489
INPUT_SPEC_WITH_UPPERCASE_FILE_EXTENSION(
8590
"input-spec-with-uppercase-file-extension",
8691
"sodastore.JSON",
@@ -90,6 +95,7 @@ enum TestCase
9095
false,
9196
true,
9297
6,
98+
Map.of(),
9399
Map.of()),
94100
ONE_OF_INTERFACES_DISABLED(
95101
"oneof-interfaces-disabled",
@@ -100,6 +106,7 @@ enum TestCase
100106
false,
101107
true,
102108
9,
109+
Map.of(),
103110
Map.of()),
104111
ONE_OF_INTERFACES_ENABLED(
105112
"oneof-interfaces-enabled",
@@ -110,7 +117,8 @@ enum TestCase
110117
true,
111118
true,
112119
11,
113-
Map.of("useOneOfInterfaces", "true", "useOneOfCreators", "true", "useFloatArrays", "true")),
120+
Map.of("useOneOfInterfaces", "true", "useOneOfCreators", "true", "useFloatArrays", "true"),
121+
Map.of()),
114122
INPUT_SPEC_WITH_BUILDER(
115123
"input-spec-with-builder",
116124
"sodastore.JSON",
@@ -127,7 +135,8 @@ enum TestCase
127135
"pojoBuildMethodName",
128136
"build",
129137
"pojoConstructorVisibility",
130-
"private")),
138+
"private"),
139+
Map.of()),
131140
REMOVE_OPERATION_ID_PREFIX(
132141
"remove-operation-id-prefix",
133142
"sodastore.json",
@@ -144,7 +153,8 @@ enum TestCase
144153
"removeOperationIdPrefixDelimiter",
145154
"\\.",
146155
"removeOperationIdPrefixCount",
147-
"3")),
156+
"3"),
157+
Map.of()),
148158
GENERATE_APIS(
149159
"generate-apis",
150160
"sodastore.yaml",
@@ -154,7 +164,19 @@ enum TestCase
154164
true,
155165
false,
156166
7,
157-
Map.of());
167+
Map.of(),
168+
Map.of()),
169+
FILE_HANDLING(
170+
"file-handling",
171+
"file-handling.yaml",
172+
"com.sap.cloud.sdk.services.filehandling.api",
173+
"com.sap.cloud.sdk.services.filehandling.model",
174+
ApiMaturity.RELEASED,
175+
false,
176+
true,
177+
1,
178+
Map.of(),
179+
Map.of("File", "byte[]"));
158180

159181
final String testCaseName;
160182
final String inputSpecFileName;
@@ -165,10 +187,11 @@ enum TestCase
165187
final boolean generateApis;
166188
final int expectedNumberOfGeneratedFiles;
167189
final Map<String, String> additionalProperties;
190+
final Map<String, String> typeMappings;
168191
}
169192

170193
@ParameterizedTest
171-
@EnumSource( TestCase.class )
194+
@EnumSource( value = TestCase.class, mode = EnumSource.Mode.EXCLUDE, names = { "FILE_HANDLING" } )
172195
void integrationTests( final TestCase testCase, @TempDir final Path path )
173196
throws Throwable
174197
{
@@ -194,7 +217,8 @@ void integrationTests( final TestCase testCase, @TempDir final Path path )
194217
.outputDirectory(tempOutputDirectory.toAbsolutePath().toString())
195218
.withSapCopyrightHeader(true)
196219
.oneOfAnyOfGenerationEnabled(testCase.anyOfOneOfGenerationEnabled)
197-
.additionalProperty("useAbstractionForFiles", "true");
220+
.additionalProperty("useAbstractionForFiles", "true")
221+
.typeMappings(testCase.typeMappings);
198222
testCase.additionalProperties.forEach(generationConfiguration::additionalProperty);
199223

200224
final Try<GenerationResult> maybeGenerationResult =
@@ -229,7 +253,8 @@ void generateDataModelForComparison( final TestCase testCase )
229253
.deleteOutputDirectory(true)
230254
.withSapCopyrightHeader(true)
231255
.oneOfAnyOfGenerationEnabled(testCase.anyOfOneOfGenerationEnabled)
232-
.additionalProperty("useAbstractionForFiles", "true");
256+
.additionalProperty("useAbstractionForFiles", "true")
257+
.typeMappings(testCase.typeMappings);
233258
testCase.additionalProperties.forEach(generationConfiguration::additionalProperty);
234259

235260
GenerationConfiguration build = generationConfiguration.build();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
openapi: 3.0.0
2+
info:
3+
title: Soda Store API
4+
version: 1.0.0
5+
description: API for managing sodas in a soda store
6+
7+
paths:
8+
/files/import:
9+
post:
10+
operationId: importFile
11+
tags:
12+
- Files
13+
summary: Upload a file
14+
requestBody:
15+
content:
16+
multipart/form-data:
17+
schema:
18+
type: object
19+
required:
20+
- file
21+
properties:
22+
file:
23+
type: string
24+
format: binary
25+
responses:
26+
'200':
27+
description: File imported successfully
28+
/files/export:
29+
get:
30+
operationId: exportFile
31+
tags:
32+
- Files
33+
summary: Download a file
34+
responses:
35+
'200':
36+
description: File content as binary
37+
content:
38+
application/octet-stream:
39+
schema:
40+
type: string
41+
format: binary

0 commit comments

Comments
 (0)