diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/api/AbstractOpenApiResource.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/api/AbstractOpenApiResource.java index 2dda6e1c2..353f86cf5 100644 --- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/api/AbstractOpenApiResource.java +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/api/AbstractOpenApiResource.java @@ -69,6 +69,7 @@ import io.swagger.v3.oas.models.PathItem.HttpMethod; import io.swagger.v3.oas.models.Paths; import io.swagger.v3.oas.models.SpecVersion; +import io.swagger.v3.oas.models.media.Schema; import io.swagger.v3.oas.models.media.StringSchema; import io.swagger.v3.oas.models.parameters.Parameter; import io.swagger.v3.oas.models.responses.ApiResponses; @@ -100,6 +101,7 @@ import org.springdoc.core.service.GenericResponseService; import org.springdoc.core.service.OpenAPIService; import org.springdoc.core.service.OperationService; +import org.springdoc.core.utils.PropertyResolverUtils; import org.springdoc.core.utils.SpringDocUtils; import org.springframework.aop.support.AopUtils; @@ -352,6 +354,9 @@ protected OpenAPI getOpenApi(Locale locale) { } getPaths(mappingsMap, finalLocale, openAPI); + if (springDocConfigProperties.isTrimKotlinIndent()) + this.trimIndent(openAPI); + Optional cloudFunctionProviderOptional = springDocProviders.getSpringCloudFunctionProvider(); cloudFunctionProviderOptional.ifPresent(cloudFunctionProvider -> { List routerOperationList = cloudFunctionProvider.getRouterOperations(openAPI); @@ -384,7 +389,6 @@ protected OpenAPI getOpenApi(Locale locale) { if (!CollectionUtils.isEmpty(openAPI.getServers()) && !openAPI.getServers().equals(serversCopy)) openAPIService.setServersPresent(true); - openAPIService.setCachedOpenAPI(openAPI, finalLocale); LOGGER.info("Init duration for springdoc-openapi is: {} ms", @@ -396,12 +400,78 @@ protected OpenAPI getOpenApi(Locale locale) { openAPIService.updateServers(openAPI); } openAPIService.updateServers(openAPI); - return openAPI; } - finally { + return openAPI; + } finally { this.reentrantLock.unlock(); } } + /** + * Indents are removed for properties that are mainly used as “explanations” using Open API. + * @param openAPI the open api + */ + private void trimIndent(OpenAPI openAPI) { + trimComponents(openAPI); + trimPaths(openAPI); + } + + /** + * Trim the indent for descriptions in the 'components' of open api. + * @param openAPI the open api + */ + private void trimComponents(OpenAPI openAPI) { + final PropertyResolverUtils propertyResolverUtils = operationParser.getPropertyResolverUtils(); + if (openAPI.getComponents() == null || openAPI.getComponents().getSchemas() == null) { + return; + } + for (Schema schema : openAPI.getComponents().getSchemas().values()) { + schema.description(propertyResolverUtils.trimIndent(schema.getDescription())); + if (schema.getProperties() == null) { + continue; + } + for (Object prop : schema.getProperties().values()) { + if (prop instanceof Schema schemaProp) { + schemaProp.setDescription(propertyResolverUtils.trimIndent(schemaProp.getDescription())); + } + } + } + } + + /** + * Trim the indent for descriptions in the 'paths' of open api. + * @param openAPI the open api + */ + private void trimPaths(OpenAPI openAPI) { + final PropertyResolverUtils propertyResolverUtils = operationParser.getPropertyResolverUtils(); + if (openAPI.getPaths() == null) { + return; + } + for (PathItem value : openAPI.getPaths().values()) { + value.setDescription(propertyResolverUtils.trimIndent(value.getDescription())); + trimIndentOperation(value.getGet()); + trimIndentOperation(value.getPut()); + trimIndentOperation(value.getPost()); + trimIndentOperation(value.getDelete()); + trimIndentOperation(value.getOptions()); + trimIndentOperation(value.getHead()); + trimIndentOperation(value.getPatch()); + trimIndentOperation(value.getTrace()); + } + } + + /** + * Trim the indent for 'operation' + * @param operation the operation + */ + private void trimIndentOperation(Operation operation) { + final PropertyResolverUtils propertyResolverUtils = operationParser.getPropertyResolverUtils(); + if (operation == null) { + return; + } + operation.setSummary(propertyResolverUtils.trimIndent(operation.getSummary())); + operation.setDescription(propertyResolverUtils.trimIndent(operation.getDescription())); + } + /** * Gets paths. * diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/OperationService.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/OperationService.java index 5a539b6b8..9f91dd18c 100644 --- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/OperationService.java +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/OperationService.java @@ -646,4 +646,12 @@ public Operation mergeOperation(Operation operation, Operation operationModel) { public JavadocProvider getJavadocProvider() { return parameterBuilder.getJavadocProvider(); } + + /** + * Gets propertyResolverUtils + * @return propertyResolverUtils + */ + public PropertyResolverUtils getPropertyResolverUtils(){ + return parameterBuilder.getPropertyResolverUtils(); + } } diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/utils/PropertyResolverUtils.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/utils/PropertyResolverUtils.java index dfa282e82..88993014c 100644 --- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/utils/PropertyResolverUtils.java +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/utils/PropertyResolverUtils.java @@ -99,9 +99,6 @@ public String resolve(String parameterProperty, Locale locale) { } if (parameterProperty.equals(result)) try { - if (springDocConfigProperties.isTrimKotlinIndent()) { - parameterProperty = trimIndent(parameterProperty); - } result = factory.resolveEmbeddedValue(parameterProperty); } catch (IllegalArgumentException ex) { @@ -119,18 +116,23 @@ public String resolve(String parameterProperty, Locale locale) { * @param text The original string with possible leading indentation. * @return The string with leading indentation removed from each line. */ - private String trimIndent(String text) { - if (text == null) { - return null; + public String trimIndent(String text) { + try { + if (text == null) { + return null; + } + final String newLine = "\n"; + String[] lines = text.split(newLine); + int minIndent = resolveMinIndent(lines); + return Arrays.stream(lines) + .map(line -> line.substring(Math.min(line.length(), minIndent))) + .reduce((a, b) -> a + newLine + b) + .orElse(StringUtils.EMPTY); + } catch (Exception ex){ + LOGGER.warn(ex.getMessage()); + return text; } - final String newLine = "\n"; - String[] lines = text.split(newLine); - int minIndent = resolveMinIndent(lines); - return Arrays.stream(lines) - .map(line -> line.substring(Math.min(line.length(), minIndent))) - .reduce((a, b) -> a + newLine + b) - .orElse(StringUtils.EMPTY); - } + } private int resolveMinIndent(String[] lines) { return Arrays.stream(lines) @@ -222,4 +224,4 @@ public Map resolveExtensions(Locale locale, Map else return extensions; } -} +} \ No newline at end of file diff --git a/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/kotlin/test/org/springdoc/api/app11/ExampleController.kt b/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/kotlin/test/org/springdoc/api/app11/ExampleController.kt index c6cc0a45d..79116e0b4 100644 --- a/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/kotlin/test/org/springdoc/api/app11/ExampleController.kt +++ b/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/kotlin/test/org/springdoc/api/app11/ExampleController.kt @@ -1,7 +1,9 @@ package test.org.springdoc.api.app11 import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.media.Schema import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController @@ -20,11 +22,75 @@ class ExampleController { | 400 | STORE_NOT_FOUND |Store not found. | | | """ ) - @GetMapping("/foo/remove-kotlin-indent") + @GetMapping("/foo/trim-kotlin-indent") fun readFoo(@RequestParam name: String?) = FooResponse("Hello ${name ?: "world"}") } data class FooResponse( - private val name: String, -) \ No newline at end of file + val name: String, +) + +@RestController +class ExampleController2 { + + @GetMapping("/foo/trim-kotlin-indent/schema") + fun readFoo( + @RequestBody request: FooRequestWithSchema, + ) = FooResponseWithSchema( + name = "Hello ${request.age ?: "world"}", + subFoo = SubFooResponseWithSchema(subName = "sub foo name"), + ) +} + +@Schema( + name = "foo request", + description = """ + foo request class description + with kotlin indent + """ +) +data class FooRequestWithSchema( + @Schema( + name = "age", + description = """ + foo request field with kotlin indent + """ + ) + val age: Int, +) + +@Schema( + name = "foo response", + description = """ + foo response class description + with kotlin indent + """ +) +data class FooResponseWithSchema( + @Schema( + name = "name", + description = """ + foo response fields with kotlin indent + """ + ) + val name: String, + val subFoo: SubFooResponseWithSchema +) + +@Schema( + name = "sub foo response", + description = """ + sub foo response class description + with kotlin indent + """ +) +data class SubFooResponseWithSchema( + @Schema( + name = "subName", + description = """ + sub foo response fields with kotlin indent + """ + ) + val subName: String, +) diff --git a/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/resources/results/app11.json b/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/resources/results/app11.json index 13425c05a..a19d2fca6 100644 --- a/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/resources/results/app11.json +++ b/springdoc-openapi-tests/springdoc-openapi-kotlin-webmvc-tests/src/test/resources/results/app11.json @@ -11,7 +11,7 @@ } ], "paths": { - "/foo/remove-kotlin-indent": { + "/foo/trim-kotlin-indent": { "get": { "tags": [ "example-controller" @@ -42,18 +42,94 @@ } } } + }, + "/foo/trim-kotlin-indent/schema": { + "get": { + "tags": [ + "example-controller-2" + ], + "operationId": "readFoo_1", + "parameters": [ + { + "name": "request", + "in": "query", + "required": true, + "schema": { + "$ref": "#/components/schemas/foo request" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/foo response" + } + } + } + } + } + } } }, "components": { "schemas": { "FooResponse": { + "required": [ + "name" + ], "type": "object", "properties": { "name": { - "type": "string", - "writeOnly": true + "type": "string" } } + }, + "foo request": { + "required": [ + "age" + ], + "type": "object", + "properties": { + "age": { + "type": "integer", + "description": "\nfoo request field with kotlin indent\n", + "format": "int32" + } + }, + "description": "\nfoo request class description\nwith kotlin indent\n" + }, + "foo response": { + "required": [ + "name", + "subFoo" + ], + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "\nfoo response fields with kotlin indent\n" + }, + "subFoo": { + "$ref": "#/components/schemas/sub foo response" + } + }, + "description": "\nfoo response class description\nwith kotlin indent\n" + }, + "sub foo response": { + "required": [ + "subName" + ], + "type": "object", + "properties": { + "subName": { + "type": "string", + "description": "\nsub foo response fields with kotlin indent\n" + } + }, + "description": "\nsub foo response class description\nwith kotlin indent\n" } } }