Skip to content

Commit 2601b3c

Browse files
author
Anton Tkachenko
committed
Add support for externalizing strings in generated openapi schema via properties that follow conventional naming similar to openapi schema.
1 parent c9cff14 commit 2601b3c

File tree

9 files changed

+385
-0
lines changed

9 files changed

+385
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
*
3+
* *
4+
* * *
5+
* * * *
6+
* * * * * Copyright 2019-2022 the original author or authors.
7+
* * * * *
8+
* * * * * Licensed under the Apache License, Version 2.0 (the "License");
9+
* * * * * you may not use this file except in compliance with the License.
10+
* * * * * You may obtain a copy of the License at
11+
* * * * *
12+
* * * * * https://www.apache.org/licenses/LICENSE-2.0
13+
* * * * *
14+
* * * * * Unless required by applicable law or agreed to in writing, software
15+
* * * * * distributed under the License is distributed on an "AS IS" BASIS,
16+
* * * * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
* * * * * See the License for the specific language governing permissions and
18+
* * * * * limitations under the License.
19+
* * * *
20+
* * *
21+
* *
22+
*
23+
*/
24+
25+
package org.springdoc.core.configuration;
26+
27+
import org.springdoc.core.customizers.SpecificationStringPropertiesCustomizer;
28+
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
29+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
30+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
31+
import org.springframework.context.annotation.Bean;
32+
import org.springframework.context.annotation.Configuration;
33+
import org.springframework.context.annotation.Lazy;
34+
import org.springframework.core.env.PropertyResolver;
35+
36+
/**
37+
* The type Spring doc specification string properties configuration.
38+
*
39+
* @author Anton Tkachenko [email protected]
40+
*/
41+
@Lazy(false)
42+
@Configuration(proxyBeanMethods = false)
43+
@ConditionalOnProperty(name = "springdoc.api-docs.specification-string-properties")
44+
@ConditionalOnBean(SpringDocConfiguration.class)
45+
public class SpringDocSpecificationStringPropertiesConfiguration {
46+
47+
/**
48+
* Springdoc customizer that takes care of the specification string properties customization.
49+
*
50+
* @return the springdoc customizer
51+
*/
52+
@Bean
53+
@ConditionalOnMissingBean
54+
@Lazy(false)
55+
SpecificationStringPropertiesCustomizer specificationStringPropertiesCustomizer(
56+
PropertyResolver propertyResolverUtils
57+
) {
58+
return new SpecificationStringPropertiesCustomizer(propertyResolverUtils);
59+
}
60+
61+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/*
2+
*
3+
* *
4+
* * *
5+
* * * *
6+
* * * * * Copyright 2019-2022 the original author or authors.
7+
* * * * *
8+
* * * * * Licensed under the Apache License, Version 2.0 (the "License");
9+
* * * * * you may not use this file except in compliance with the License.
10+
* * * * * You may obtain a copy of the License at
11+
* * * * *
12+
* * * * * https://www.apache.org/licenses/LICENSE-2.0
13+
* * * * *
14+
* * * * * Unless required by applicable law or agreed to in writing, software
15+
* * * * * distributed under the License is distributed on an "AS IS" BASIS,
16+
* * * * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
* * * * * See the License for the specific language governing permissions and
18+
* * * * * limitations under the License.
19+
* * * *
20+
* * *
21+
* *
22+
*
23+
*/
24+
25+
package org.springdoc.core.customizers;
26+
27+
import io.swagger.v3.oas.models.*;
28+
import io.swagger.v3.oas.models.info.Info;
29+
import io.swagger.v3.oas.models.media.Schema;
30+
import org.apache.commons.lang3.StringUtils;
31+
import org.springframework.core.env.PropertyResolver;
32+
import org.springframework.util.CollectionUtils;
33+
34+
import java.text.MessageFormat;
35+
import java.util.List;
36+
import java.util.Map;
37+
import java.util.function.Consumer;
38+
39+
/**
40+
* Allows externalizing strings in generated openapi schema via properties that follow
41+
* conventional naming similar or identical to <a href="https://swagger.io/docs/specification/basic-structure/">openapi schema</a>
42+
* <p>
43+
* To set value of a string in schema, define an application property that matches the target node
44+
* with springdoc.specification-strings prefix.
45+
* <p>
46+
* Sample supported properties for api-info customization:
47+
* <ul>
48+
* <li>springdoc.specification-strings.info.title - to set title of api-info</li>
49+
* <li>springdoc.specification-strings.info.description - to set description of api-info</li>
50+
* <li>springdoc.specification-strings.info.version - to set version of api-info</li>
51+
* <li>springdoc.specification-strings.info.termsOfService - to set terms of service of api-info</li>
52+
* </ul>
53+
* <p>
54+
* Sample supported properties for components customization:
55+
* <ul>
56+
* <li>springdoc.specification-strings.components.User.description - to set description of User model</li>
57+
* <li>springdoc.specification-strings.components.User.properties.name.description - to set description of 'name' property</li>
58+
* <li>springdoc.specification-strings.components.User.properties.name.example - to set example of 'name' property</li>
59+
* </ul>
60+
* <p>
61+
* Sample supported properties for paths/operationIds customization:
62+
* <ul>
63+
* <li>springdoc.specification-strings.paths.{operationId}.description - to set description of {operationId}</li>
64+
* <li>springdoc.specification-strings.paths.{operationId}.summary - to set summary of {operationId}</li>
65+
* </ul>
66+
*
67+
* @author Anton Tkachenko [email protected]
68+
*/
69+
public class SpecificationStringPropertiesCustomizer implements GlobalOpenApiCustomizer {
70+
71+
private static final String SPECIFICATION_STRINGS_PREFIX = "springdoc.specification-strings.";
72+
73+
private final PropertyResolver propertyResolver;
74+
75+
public SpecificationStringPropertiesCustomizer(PropertyResolver resolverUtils) {
76+
this.propertyResolver = resolverUtils;
77+
}
78+
79+
@Override
80+
public void customise(OpenAPI openApi) {
81+
setOperationInfoProperties(openApi);
82+
setComponentsProperties(openApi);
83+
setPathsProperties(openApi);
84+
}
85+
86+
private void setOperationInfoProperties(OpenAPI openApi) {
87+
if (openApi.getInfo() == null) {
88+
openApi.setInfo(new Info());
89+
}
90+
Info info = openApi.getInfo();
91+
resolveString(info::setTitle, "info.title");
92+
resolveString(info::setDescription, "info.description");
93+
resolveString(info::setVersion, "info.version");
94+
resolveString(info::setTermsOfService, "info.termsOfService");
95+
}
96+
97+
private void setPathsProperties(OpenAPI openApi) {
98+
Paths paths = openApi.getPaths();
99+
if (CollectionUtils.isEmpty(paths.values())) {
100+
return;
101+
}
102+
for (PathItem pathItem : paths.values()) {
103+
List<Operation> operations = pathItem.readOperations();
104+
for (Operation operation : operations) {
105+
String operationId = operation.getOperationId();
106+
String operationNode = MessageFormat.format("paths.{0}", operationId);
107+
resolveString(operation::setDescription, operationNode + ".description");
108+
109+
resolveString(operation::setSummary, operationNode + ".summary");
110+
}
111+
}
112+
}
113+
114+
private void setComponentsProperties(OpenAPI openApi) {
115+
Components components = openApi.getComponents();
116+
if (components == null || CollectionUtils.isEmpty(components.getSchemas())) {
117+
return;
118+
}
119+
120+
for (Schema componentSchema : components.getSchemas().values()) {
121+
// set component description
122+
String schemaPropertyPrefix = MessageFormat.format("components.schemas.{0}", componentSchema.getName());
123+
resolveString(componentSchema::setDescription, schemaPropertyPrefix + ".description");
124+
Map<String, Schema> properties = componentSchema.getProperties();
125+
126+
if (CollectionUtils.isEmpty(properties)) {
127+
continue;
128+
}
129+
130+
for (Schema propSchema : properties.values()) {
131+
String propertyNode = MessageFormat.format("components.schemas.{0}.properties.{1}",
132+
componentSchema.getName(), propSchema.getName());
133+
134+
resolveString(propSchema::setDescription, propertyNode + ".description");
135+
resolveString(propSchema::setExample, propertyNode + ".example");
136+
}
137+
}
138+
}
139+
140+
private void resolveString(
141+
Consumer<String> setter, String node
142+
) {
143+
String nodeWithPrefix = SPECIFICATION_STRINGS_PREFIX + node;
144+
String value = propertyResolver.getProperty(nodeWithPrefix);
145+
if (StringUtils.isNotBlank(value)) {
146+
setter.accept(value);
147+
}
148+
}
149+
150+
}

springdoc-openapi-starter-common/src/main/java/org/springdoc/core/utils/Constants.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,11 @@ public final class Constants {
104104
*/
105105
public static final String SPRINGDOC_SCHEMA_RESOLVE_PROPERTIES = "springdoc.api-docs.resolve-schema-properties";
106106

107+
/**
108+
* The constant SPRINGDOC_SPECIFICATION_STRING_PROPERTIES.
109+
*/
110+
public static final String SPRINGDOC_SPECIFICATION_STRING_PROPERTIES = "springdoc.api-docs.specification-string-properties";
111+
107112
/**
108113
* The constant SPRINGDOC_SHOW_LOGIN_ENDPOINT.
109114
*/

springdoc-openapi-starter-common/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ org.springdoc.core.configuration.SpringDocFunctionCatalogConfiguration
77
org.springdoc.core.configuration.SpringDocHateoasConfiguration
88
org.springdoc.core.configuration.SpringDocPageableConfiguration
99
org.springdoc.core.configuration.SpringDocSortConfiguration
10+
org.springdoc.core.configuration.SpringDocSpecificationStringPropertiesConfiguration
1011
org.springdoc.core.configuration.SpringDocDataRestConfiguration
1112
org.springdoc.core.configuration.SpringDocKotlinConfiguration
1213
org.springdoc.core.configuration.SpringDocKotlinxConfiguration
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
*
3+
* * Copyright 2019-2023 the original author or authors.
4+
* *
5+
* * Licensed under the Apache License, Version 2.0 (the "License");
6+
* * you may not use this file except in compliance with the License.
7+
* * You may obtain a copy of the License at
8+
* *
9+
* * https://www.apache.org/licenses/LICENSE-2.0
10+
* *
11+
* * Unless required by applicable law or agreed to in writing, software
12+
* * distributed under the License is distributed on an "AS IS" BASIS,
13+
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* * See the License for the specific language governing permissions and
15+
* * limitations under the License.
16+
*
17+
*/
18+
19+
package test.org.springdoc.api.app212;
20+
21+
import org.springframework.web.bind.annotation.GetMapping;
22+
import org.springframework.web.bind.annotation.RestController;
23+
24+
@RestController
25+
public class HelloController {
26+
27+
@GetMapping(value = "/persons")
28+
public PersonDTO persons() {
29+
return new PersonDTO("John");
30+
}
31+
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
*
3+
* * Copyright 2019-2023 the original author or authors.
4+
* *
5+
* * Licensed under the Apache License, Version 2.0 (the "License");
6+
* * you may not use this file except in compliance with the License.
7+
* * You may obtain a copy of the License at
8+
* *
9+
* * https://www.apache.org/licenses/LICENSE-2.0
10+
* *
11+
* * Unless required by applicable law or agreed to in writing, software
12+
* * distributed under the License is distributed on an "AS IS" BASIS,
13+
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* * See the License for the specific language governing permissions and
15+
* * limitations under the License.
16+
*
17+
*/
18+
19+
package test.org.springdoc.api.app212;
20+
21+
public record PersonDTO(String name) {
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
*
3+
* * Copyright 2019-2023 the original author or authors.
4+
* *
5+
* * Licensed under the Apache License, Version 2.0 (the "License");
6+
* * you may not use this file except in compliance with the License.
7+
* * You may obtain a copy of the License at
8+
* *
9+
* * https://www.apache.org/licenses/LICENSE-2.0
10+
* *
11+
* * Unless required by applicable law or agreed to in writing, software
12+
* * distributed under the License is distributed on an "AS IS" BASIS,
13+
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* * See the License for the specific language governing permissions and
15+
* * limitations under the License.
16+
*
17+
*/
18+
19+
package test.org.springdoc.api.app212;
20+
21+
import org.springframework.boot.autoconfigure.SpringBootApplication;
22+
import org.springframework.test.context.ActiveProfiles;
23+
import test.org.springdoc.api.AbstractSpringDocTest;
24+
25+
/**
26+
* The type Spring doc app 192 test.
27+
* <p>
28+
* A test for {@link org.springdoc.core.customizers.SpecificationStringPropertiesCustomizer}
29+
*/
30+
@ActiveProfiles("212")
31+
public class SpringDocApp212Test extends AbstractSpringDocTest {
32+
33+
/**
34+
* The type Spring doc test app.
35+
*/
36+
@SpringBootApplication
37+
static class SpringDocTestApp {
38+
}
39+
40+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
springdoc:
2+
api-docs:
3+
specification-string-properties: true
4+
specification-strings:
5+
info:
6+
title: Api info title
7+
description: Api info description
8+
version: Api info version
9+
components:
10+
schemas:
11+
PersonDTO:
12+
description: Description for PersonDTO component
13+
properties:
14+
name:
15+
description: Description for 'name' property
16+
example: Example value for 'name' property
17+
paths:
18+
persons:
19+
description: Description of operationId 'persons'
20+
summary: Summary of operationId 'persons'

0 commit comments

Comments
 (0)