Skip to content

Commit 2391f2a

Browse files
newtorkJonas-Isr
andauthored
feat: (OpenAPI) Fix class name generation for inline-object of //component/response schemas (#835)
Co-authored-by: Jonas-Isr <j.israel.sap@icloud.com>
1 parent e40f689 commit 2391f2a

File tree

9 files changed

+1091
-5
lines changed

9 files changed

+1091
-5
lines changed

datamodel/openapi/openapi-generator/src/main/java/com/sap/cloud/sdk/datamodel/openapi/generator/GenerationConfigurationConverter.java

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.sap.cloud.sdk.datamodel.openapi.generator;
22

3+
import static com.sap.cloud.sdk.datamodel.openapi.generator.GeneratorCustomProperties.FIX_RESPONSE_SCHEMA_TITLES;
4+
35
import java.nio.file.Path;
46
import java.nio.file.Paths;
57
import java.time.Year;
@@ -19,7 +21,11 @@
1921
import com.sap.cloud.sdk.datamodel.openapi.generator.model.GenerationConfiguration;
2022

2123
import io.swagger.parser.OpenAPIParser;
24+
import io.swagger.v3.oas.models.Components;
2225
import io.swagger.v3.oas.models.OpenAPI;
26+
import io.swagger.v3.oas.models.media.Content;
27+
import io.swagger.v3.oas.models.media.Schema;
28+
import io.swagger.v3.oas.models.responses.ApiResponse;
2329
import io.swagger.v3.parser.core.models.AuthorizationValue;
2430
import io.swagger.v3.parser.core.models.ParseOptions;
2531
import lombok.extern.slf4j.Slf4j;
@@ -60,9 +66,11 @@ static ClientOptInput convertGenerationConfiguration(
6066
config.setTemplateDir(TEMPLATE_DIRECTORY);
6167
config.additionalProperties().putAll(getAdditionalProperties(generationConfiguration));
6268

69+
final var openAPI = parseOpenApiSpec(inputSpecFile, generationConfiguration);
70+
6371
final var clientOptInput = new ClientOptInput();
6472
clientOptInput.config(config);
65-
clientOptInput.openAPI(parseOpenApiSpec(inputSpecFile));
73+
clientOptInput.openAPI(openAPI);
6674
return clientOptInput;
6775
}
6876

@@ -90,16 +98,60 @@ private static void setGlobalSettings( @Nonnull final GenerationConfiguration co
9098
GlobalSettings.setProperty(CodegenConstants.HIDE_GENERATION_TIMESTAMP, Boolean.TRUE.toString());
9199
}
92100

93-
private static OpenAPI parseOpenApiSpec( @Nonnull final String inputSpecFile )
101+
@Nonnull
102+
private static
103+
OpenAPI
104+
parseOpenApiSpec( @Nonnull final String inputSpecFile, @Nonnull final GenerationConfiguration config )
94105
{
95-
final List<AuthorizationValue> authorizationValues = List.of();
106+
final var authorizationValues = List.<AuthorizationValue> of();
96107
final var options = new ParseOptions();
97108
options.setResolve(true);
98109
final var spec = new OpenAPIParser().readLocation(inputSpecFile, authorizationValues, options);
99110
if( !spec.getMessages().isEmpty() ) {
100111
log.warn("Parsing the specification yielded the following messages: {}", spec.getMessages());
101112
}
102-
return spec.getOpenAPI();
113+
final var result = spec.getOpenAPI();
114+
preprocessSpecification(result, config);
115+
return result;
116+
}
117+
118+
/**
119+
* Preprocesses the OpenAPI specification to ensure that all inline schemas in "//components/responses" have a
120+
* title. This does not affect regular schema definitions in "//components/schemas"! Without this fix, the OpenAPI
121+
* Generator will generate classes with name format "InlineObject\d*" with high chance of naming conflicts.
122+
*
123+
* @param openAPI
124+
* the OpenAPI specification to preprocess
125+
* @param config
126+
* the generation configuration to extract feature toggles from
127+
*/
128+
private static
129+
void
130+
preprocessSpecification( @Nonnull final OpenAPI openAPI, @Nonnull final GenerationConfiguration config )
131+
{
132+
if( !FIX_RESPONSE_SCHEMA_TITLES.isEnabled(config) ) {
133+
return;
134+
}
135+
final Components components = openAPI.getComponents();
136+
if( components == null ) {
137+
return;
138+
}
139+
final Map<String, ApiResponse> responses = components.getResponses();
140+
if( responses == null ) {
141+
return;
142+
}
143+
responses.forEach(( key, value ) -> {
144+
final Content mediaContent = value.getContent();
145+
if( mediaContent == null ) {
146+
return;
147+
}
148+
mediaContent.forEach(( mediaType, content ) -> {
149+
final Schema<?> schema = content.getSchema();
150+
if( schema != null && schema.getTitle() == null ) {
151+
schema.setTitle(key + " " + (mediaContent.size() > 1 ? mediaType : ""));
152+
}
153+
});
154+
});
103155
}
104156

105157
private static Map<String, Object> getAdditionalProperties( @Nonnull final GenerationConfiguration config )

datamodel/openapi/openapi-generator/src/main/java/com/sap/cloud/sdk/datamodel/openapi/generator/GeneratorCustomProperties.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,12 @@ enum GeneratorCustomProperties
4040
/**
4141
* Remove schema components that are unused, before generating them.
4242
*/
43-
FIX_REMOVE_UNUSED_COMPONENTS("removeUnusedComponents", "false");
43+
FIX_REMOVE_UNUSED_COMPONENTS("removeUnusedComponents", "false"),
44+
45+
/**
46+
* Fix inconsistent "InlineObject\d*" class names for unnamed inline schemas of `//components/responses`.
47+
*/
48+
FIX_RESPONSE_SCHEMA_TITLES("fixResponseSchemaTitles", "false");
4449

4550
private final String key;
4651
private final String defaultValue;

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,16 @@ private enum TestCase
5757
true,
5858
6,
5959
Map.of()),
60+
INLINEOBJECT_SCHEMA_NAME(
61+
"inlineobject-schemas-enabled",
62+
"sodastore.yaml",
63+
"com.sap.cloud.sdk.services.inlineobject.api",
64+
"com.sap.cloud.sdk.services.inlineobject.model",
65+
ApiMaturity.RELEASED,
66+
true,
67+
true,
68+
5,
69+
Map.of("fixResponseSchemaTitles", "true")),
6070
PARTIAL_GENERATION(
6171
"partial-generation",
6272
"sodastore.json",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
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+
/sodas/{sodaId}:
9+
get:
10+
summary: Get details of a specific soda
11+
operationId: getSodaById
12+
parameters:
13+
- name: sodaId
14+
in: path
15+
description: ID of the soda to retrieve
16+
required: true
17+
schema:
18+
type: integer
19+
format: int64
20+
responses:
21+
'200':
22+
description: The requested soda
23+
content:
24+
application/json:
25+
schema:
26+
$ref: '#/components/schemas/Soda'
27+
'404':
28+
$ref: '#/components/responses/NotFound'
29+
'503':
30+
$ref: '#/components/responses/ServiceUnavailable'
31+
32+
components:
33+
schemas:
34+
Soda:
35+
type: object
36+
properties:
37+
id:
38+
type: integer
39+
format: int64
40+
name:
41+
type: string
42+
brand:
43+
type: string
44+
flavor:
45+
type: string
46+
price:
47+
type: number
48+
format: float
49+
required:
50+
- name
51+
- brand
52+
- flavor
53+
- price
54+
responses:
55+
NotFound:
56+
description: The specified resource was not found
57+
content:
58+
application/json:
59+
schema:
60+
type: object
61+
properties:
62+
message:
63+
type: string
64+
example: Resource not found
65+
ServiceUnavailable:
66+
description: The service is currently unavailable
67+
content:
68+
application/json:
69+
schema:
70+
type: object
71+
properties:
72+
message:
73+
type: string
74+
example: Resource not found
75+
application/xml:
76+
schema:
77+
type: object
78+
properties:
79+
message:
80+
type: string
81+
example: Resource not found
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
* Copyright (c) 2025 SAP SE or an SAP affiliate company. All rights reserved.
3+
*/
4+
5+
package com.sap.cloud.sdk.services.inlineobject.api;
6+
7+
import com.sap.cloud.sdk.services.openapi.core.OpenApiRequestException;
8+
import com.sap.cloud.sdk.services.openapi.core.OpenApiResponse;
9+
import com.sap.cloud.sdk.services.openapi.core.AbstractOpenApiService;
10+
import com.sap.cloud.sdk.services.openapi.apiclient.ApiClient;
11+
12+
import com.sap.cloud.sdk.services.inlineobject.model.NotFound;
13+
import com.sap.cloud.sdk.services.inlineobject.model.ServiceUnavailableApplicationJson;
14+
import com.sap.cloud.sdk.services.inlineobject.model.ServiceUnavailableApplicationXml;
15+
import com.sap.cloud.sdk.services.inlineobject.model.Soda;
16+
17+
import java.util.HashMap;
18+
import java.util.List;
19+
import java.util.Locale;
20+
import java.util.Map;
21+
22+
import org.springframework.util.LinkedMultiValueMap;
23+
import org.springframework.util.MultiValueMap;
24+
import org.springframework.web.util.UriComponentsBuilder;
25+
import org.springframework.core.ParameterizedTypeReference;
26+
import org.springframework.core.io.FileSystemResource;
27+
import org.springframework.http.HttpHeaders;
28+
import org.springframework.http.HttpMethod;
29+
import org.springframework.http.MediaType;
30+
31+
import javax.annotation.Nonnull;
32+
import javax.annotation.Nullable;
33+
import com.google.common.annotations.Beta;
34+
35+
import com.sap.cloud.sdk.cloudplatform.connectivity.Destination;
36+
37+
/**
38+
* Soda Store API in version 1.0.0.
39+
*
40+
* API for managing sodas in a soda store
41+
*/
42+
public class DefaultApi extends AbstractOpenApiService {
43+
/**
44+
* Instantiates this API class to invoke operations on the Soda Store API.
45+
*
46+
* @param httpDestination The destination that API should be used with
47+
*/
48+
public DefaultApi( @Nonnull final Destination httpDestination )
49+
{
50+
super(httpDestination);
51+
}
52+
53+
/**
54+
* Instantiates this API class to invoke operations on the Soda Store API based on a given {@link ApiClient}.
55+
*
56+
* @param apiClient
57+
* ApiClient to invoke the API on
58+
*/
59+
@Beta
60+
public DefaultApi( @Nonnull final ApiClient apiClient )
61+
{
62+
super(apiClient);
63+
}
64+
65+
/**
66+
* <p>Get details of a specific soda</p>
67+
* <p></p>
68+
* <p><b>200</b> - The requested soda
69+
* <p><b>404</b> - The specified resource was not found
70+
* <p><b>503</b> - The service is currently unavailable
71+
* @param sodaId
72+
* ID of the soda to retrieve
73+
* @return Soda
74+
* @throws OpenApiRequestException if an error occurs while attempting to invoke the API
75+
*/
76+
@Nonnull
77+
public Soda getSodaById( @Nonnull final Long sodaId) throws OpenApiRequestException {
78+
final Object localVarPostBody = null;
79+
80+
// verify the required parameter 'sodaId' is set
81+
if (sodaId == null) {
82+
throw new OpenApiRequestException("Missing the required parameter 'sodaId' when calling getSodaById");
83+
}
84+
85+
// create path and map variables
86+
final Map<String, Object> localVarPathParams = new HashMap<String, Object>();
87+
localVarPathParams.put("sodaId", sodaId);
88+
final String localVarPath = UriComponentsBuilder.fromPath("/sodas/{sodaId}").buildAndExpand(localVarPathParams).toUriString();
89+
90+
final MultiValueMap<String, String> localVarQueryParams = new LinkedMultiValueMap<String, String>();
91+
final HttpHeaders localVarHeaderParams = new HttpHeaders();
92+
final MultiValueMap<String, Object> localVarFormParams = new LinkedMultiValueMap<String, Object>();
93+
94+
final String[] localVarAccepts = {
95+
"application/json", "application/xml"
96+
};
97+
final List<MediaType> localVarAccept = apiClient.selectHeaderAccept(localVarAccepts);
98+
final String[] localVarContentTypes = { };
99+
final MediaType localVarContentType = apiClient.selectHeaderContentType(localVarContentTypes);
100+
101+
final String[] localVarAuthNames = new String[] { };
102+
103+
final ParameterizedTypeReference<Soda> localVarReturnType = new ParameterizedTypeReference<Soda>() {};
104+
return apiClient.invokeAPI(localVarPath, HttpMethod.GET, localVarQueryParams, localVarPostBody, localVarHeaderParams, localVarFormParams, localVarAccept, localVarContentType, localVarAuthNames, localVarReturnType);
105+
}
106+
}

0 commit comments

Comments
 (0)