Skip to content

Commit 69d1649

Browse files
committed
Merge branch 'specification-string-properties-customizer' of https://github.com/tkachenkoas/springdoc-openapi into tkachenkoas-specification-string-properties-customizer
2 parents 4673b6a + 4618041 commit 69d1649

File tree

10 files changed

+536
-0
lines changed

10 files changed

+536
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
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.springdoc.core.models.GroupedOpenApi;
29+
import org.springframework.beans.factory.config.BeanPostProcessor;
30+
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
31+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
32+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
33+
import org.springframework.context.annotation.Bean;
34+
import org.springframework.context.annotation.Configuration;
35+
import org.springframework.context.annotation.Lazy;
36+
import org.springframework.core.env.PropertyResolver;
37+
38+
import java.util.List;
39+
40+
/**
41+
* The type Spring doc specification string properties configuration.
42+
*
43+
* @author Anton Tkachenko [email protected]
44+
*/
45+
@Lazy(false)
46+
@Configuration(proxyBeanMethods = false)
47+
@ConditionalOnProperty(name = "springdoc.api-docs.specification-string-properties")
48+
@ConditionalOnBean(SpringDocConfiguration.class)
49+
public class SpringDocSpecificationStringPropertiesConfiguration {
50+
51+
/**
52+
* Springdoc customizer that takes care of the specification string properties customization.
53+
* Will be applied to general openapi schema.
54+
*
55+
* @return the springdoc customizer
56+
*/
57+
@Bean
58+
@ConditionalOnMissingBean
59+
@Lazy(false)
60+
SpecificationStringPropertiesCustomizer specificationStringPropertiesCustomizer(
61+
PropertyResolver propertyResolverUtils
62+
) {
63+
return new SpecificationStringPropertiesCustomizer(propertyResolverUtils);
64+
}
65+
66+
/**
67+
* Bean post processor that applies the specification string properties customization to
68+
* grouped openapi schemas by using group name as a prefix for properties.
69+
*
70+
* @return the bean post processor
71+
*/
72+
@Bean
73+
@ConditionalOnMissingBean
74+
@Lazy(false)
75+
SpecificationStringPropertiesCustomizerBeanPostProcessor specificationStringPropertiesCustomizerBeanPostProcessor(
76+
PropertyResolver propertyResolverUtils
77+
) {
78+
return new SpecificationStringPropertiesCustomizerBeanPostProcessor(propertyResolverUtils);
79+
}
80+
81+
82+
private static class SpecificationStringPropertiesCustomizerBeanPostProcessor implements BeanPostProcessor {
83+
84+
private final PropertyResolver propertyResolverUtils;
85+
86+
public SpecificationStringPropertiesCustomizerBeanPostProcessor(
87+
PropertyResolver propertyResolverUtils
88+
) {
89+
this.propertyResolverUtils = propertyResolverUtils;
90+
}
91+
92+
@Override
93+
public Object postProcessAfterInitialization(Object bean, String beanName) {
94+
if (bean instanceof GroupedOpenApi groupedOpenApi) {
95+
groupedOpenApi.addAllOpenApiCustomizer(List.of(new SpecificationStringPropertiesCustomizer(
96+
propertyResolverUtils, groupedOpenApi.getGroup()
97+
)));
98+
}
99+
return bean;
100+
}
101+
}
102+
103+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
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+
* <p>
67+
* Support for groped openapi customization is similar to the above, but with a group name prefix.
68+
* E.g.
69+
* <ul>
70+
* <li>springdoc.specification-strings.{group-name}.info.title - to set title of api-info</li>
71+
* <li>springdoc.specification-strings.{group-name}.components.User.description - to set description of User model</li>
72+
* <li>springdoc.specification-strings.{group-name}.paths.{operationId}.description - to set description of {operationId}</li>
73+
* </ul>
74+
*
75+
* @author Anton Tkachenko [email protected]
76+
*/
77+
public class SpecificationStringPropertiesCustomizer implements GlobalOpenApiCustomizer {
78+
79+
private static final String SPECIFICATION_STRINGS_PREFIX = "springdoc.specification-strings.";
80+
81+
private final PropertyResolver propertyResolver;
82+
private final String propertyPrefix;
83+
84+
public SpecificationStringPropertiesCustomizer(PropertyResolver resolverUtils) {
85+
this.propertyResolver = resolverUtils;
86+
this.propertyPrefix = SPECIFICATION_STRINGS_PREFIX;
87+
}
88+
89+
public SpecificationStringPropertiesCustomizer(PropertyResolver propertyResolver, String groupName) {
90+
this.propertyResolver = propertyResolver;
91+
this.propertyPrefix = SPECIFICATION_STRINGS_PREFIX + groupName + ".";
92+
}
93+
94+
@Override
95+
public void customise(OpenAPI openApi) {
96+
setOperationInfoProperties(openApi);
97+
setComponentsProperties(openApi);
98+
setPathsProperties(openApi);
99+
}
100+
101+
private void setOperationInfoProperties(OpenAPI openApi) {
102+
if (openApi.getInfo() == null) {
103+
openApi.setInfo(new Info());
104+
}
105+
Info info = openApi.getInfo();
106+
resolveString(info::setTitle, "info.title");
107+
resolveString(info::setDescription, "info.description");
108+
resolveString(info::setVersion, "info.version");
109+
resolveString(info::setTermsOfService, "info.termsOfService");
110+
}
111+
112+
private void setPathsProperties(OpenAPI openApi) {
113+
Paths paths = openApi.getPaths();
114+
if (CollectionUtils.isEmpty(paths.values())) {
115+
return;
116+
}
117+
for (PathItem pathItem : paths.values()) {
118+
List<Operation> operations = pathItem.readOperations();
119+
for (Operation operation : operations) {
120+
String operationId = operation.getOperationId();
121+
String operationNode = MessageFormat.format("paths.{0}", operationId);
122+
resolveString(operation::setDescription, operationNode + ".description");
123+
124+
resolveString(operation::setSummary, operationNode + ".summary");
125+
}
126+
}
127+
}
128+
129+
private void setComponentsProperties(OpenAPI openApi) {
130+
Components components = openApi.getComponents();
131+
if (components == null || CollectionUtils.isEmpty(components.getSchemas())) {
132+
return;
133+
}
134+
135+
for (Schema componentSchema : components.getSchemas().values()) {
136+
// set component description
137+
String schemaPropertyPrefix = MessageFormat.format("components.schemas.{0}", componentSchema.getName());
138+
resolveString(componentSchema::setDescription, schemaPropertyPrefix + ".description");
139+
Map<String, Schema> properties = componentSchema.getProperties();
140+
141+
if (CollectionUtils.isEmpty(properties)) {
142+
continue;
143+
}
144+
145+
for (Schema propSchema : properties.values()) {
146+
String propertyNode = MessageFormat.format("components.schemas.{0}.properties.{1}",
147+
componentSchema.getName(), propSchema.getName());
148+
149+
resolveString(propSchema::setDescription, propertyNode + ".description");
150+
resolveString(propSchema::setExample, propertyNode + ".example");
151+
}
152+
}
153+
}
154+
155+
private void resolveString(
156+
Consumer<String> setter, String node
157+
) {
158+
String nodeWithPrefix = propertyPrefix + node;
159+
String value = propertyResolver.getProperty(nodeWithPrefix);
160+
if (StringUtils.isNotBlank(value)) {
161+
setter.accept(value);
162+
}
163+
}
164+
165+
}

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+
}

0 commit comments

Comments
 (0)