├── .github
└── workflows
│ └── maven.yml
├── .gitignore
├── .java-version
├── CHANGELOG.md
├── LICENSE
├── README.md
├── annotations
├── pom.xml
└── src
│ └── main
│ └── java
│ └── com
│ └── github
│ └── jrcodeza
│ ├── Header.java
│ ├── OpenApiExample.java
│ ├── OpenApiExamples.java
│ ├── OpenApiIgnore.java
│ ├── Response.java
│ └── Responses.java
├── client-generator-plugin
├── pom.xml
└── src
│ └── main
│ └── java
│ └── com
│ └── github
│ └── jrcodeza
│ └── client
│ └── generator
│ └── plugin
│ └── GenerateClientFromSchemaMojo.java
├── client-generator
├── pom.xml
└── src
│ ├── main
│ └── java
│ │ └── org
│ │ └── spring
│ │ └── openapi
│ │ └── client
│ │ └── generator
│ │ ├── ClientGeneratorUtils.java
│ │ ├── OpenApiClientGenerator.java
│ │ ├── OperationData.java
│ │ └── ResourceInterfaceGenerator.java
│ └── test
│ ├── java
│ └── org
│ │ └── spring
│ │ └── openapi
│ │ └── client
│ │ └── generator
│ │ └── OpenApiClientGeneratorTest.java
│ └── resources
│ └── input_example.json
├── pom.xml
├── schema-generator-plugin
├── pom.xml
└── src
│ ├── main
│ └── java
│ │ └── com
│ │ └── github
│ │ └── jrcodeza
│ │ └── schema
│ │ └── generator
│ │ └── plugin
│ │ └── GenerateOpenApiSchemaMojo.java
│ └── test
│ ├── java
│ └── com
│ │ └── github
│ │ └── jrcodeza
│ │ └── schema
│ │ └── generator
│ │ └── plugin
│ │ ├── GenerateOpenApiSchemaMojoTest.java
│ │ ├── controller
│ │ └── MojoTestController.java
│ │ ├── example
│ │ └── TestExampleResolver.java
│ │ ├── interceptor
│ │ ├── TestOperationInterceptor.java
│ │ └── TestSchemaFieldInterceptor.java
│ │ └── model
│ │ └── OpenApiTestModel.java
│ └── resources
│ └── unit
│ └── generate-open-api-standard
│ └── pom.xml
├── schema-generator
├── pom.xml
└── src
│ ├── main
│ └── java
│ │ └── com
│ │ └── github
│ │ └── jrcodeza
│ │ └── schema
│ │ └── generator
│ │ ├── ComponentSchemaTransformer.java
│ │ ├── OpenAPIGenerator.java
│ │ ├── OperationsTransformer.java
│ │ ├── config
│ │ ├── OpenApiGeneratorConfig.java
│ │ └── builder
│ │ │ └── OpenApiGeneratorConfigBuilder.java
│ │ ├── filters
│ │ ├── OperationFilter.java
│ │ ├── OperationParameterFilter.java
│ │ └── SchemaFieldFilter.java
│ │ ├── interceptors
│ │ ├── OperationInterceptor.java
│ │ ├── OperationParameterInterceptor.java
│ │ ├── RequestBodyInterceptor.java
│ │ ├── SchemaFieldInterceptor.java
│ │ ├── SchemaInterceptor.java
│ │ └── examples
│ │ │ ├── OpenApiExampleResolver.java
│ │ │ └── OperationParameterExampleInterceptor.java
│ │ ├── model
│ │ ├── CustomComposedSchema.java
│ │ ├── Header.java
│ │ └── InheritanceInfo.java
│ │ └── util
│ │ ├── CommonConstants.java
│ │ ├── GeneratorUtils.java
│ │ └── SchemaGeneratorHelper.java
│ └── test
│ ├── java
│ └── com
│ │ └── github
│ │ └── jrcodeza
│ │ └── schema
│ │ └── generator
│ │ ├── OpenAPIGeneratorTest.java
│ │ ├── base
│ │ └── Entity.java
│ │ ├── controller
│ │ ├── CarController.java
│ │ ├── ControllerToBeIgnored.java
│ │ ├── DummyController.java
│ │ └── ListController.java
│ │ ├── domain
│ │ ├── Car.java
│ │ ├── CarType.java
│ │ ├── Customer.java
│ │ ├── CustomerInventory.java
│ │ ├── Entity.java
│ │ ├── Laptop.java
│ │ ├── OptionsClass.java
│ │ ├── Order.java
│ │ ├── Product.java
│ │ ├── Van.java
│ │ └── dummy
│ │ │ ├── ArrayDummy.java
│ │ │ ├── ListDummy.java
│ │ │ └── ValidationDummy.java
│ │ ├── filters
│ │ ├── TestOperationFilter.java
│ │ ├── TestOperationParameterFilter.java
│ │ └── TestSchemaFieldFilter.java
│ │ ├── interceptors
│ │ ├── TestOperationInterceptor.java
│ │ ├── TestOperationParameterInterceptor.java
│ │ ├── TestRequestBodyInterceptor.java
│ │ ├── TestSchemaFieldInterceptor.java
│ │ └── TestSchemaInterceptor.java
│ │ └── util
│ │ └── TestUtils.java
│ └── resources
│ ├── expected_example_openapi.json
│ ├── expected_filtered_openapi.json
│ └── expected_standard_openapi.json
└── schema-v2-generator
├── pom.xml
└── src
├── main
└── java
│ └── com
│ └── github
│ └── jrcodeza
│ └── schema
│ └── v2
│ └── generator
│ ├── ComponentSchemaTransformer.java
│ ├── OpenAPIV2Generator.java
│ ├── OpenApiTransformer.java
│ ├── OperationsTransformer.java
│ ├── config
│ ├── CompatibilityMode.java
│ ├── OpenApiV2GeneratorConfig.java
│ └── builder
│ │ └── OpenApiV2GeneratorConfigBuilder.java
│ ├── interceptors
│ ├── OperationInterceptor.java
│ ├── OperationParameterInterceptor.java
│ ├── RequestBodyInterceptor.java
│ ├── SchemaFieldInterceptor.java
│ └── SchemaInterceptor.java
│ ├── model
│ ├── CustomBodyParameter.java
│ ├── CustomQueryParameter.java
│ ├── CustomSchema.java
│ ├── GenerationContext.java
│ ├── Header.java
│ └── InheritanceInfo.java
│ └── util
│ ├── CommonConstants.java
│ └── GeneratorUtils.java
└── test
├── java
└── com
│ └── github
│ └── jrcodeza
│ └── schema
│ └── v2
│ └── generator
│ ├── OpenAPIV2GeneratorTest.java
│ ├── base
│ └── Entity.java
│ ├── controller
│ ├── CarController.java
│ ├── ControllerToBeIgnored.java
│ ├── DummyController.java
│ └── ListController.java
│ ├── domain
│ ├── Car.java
│ ├── CarType.java
│ ├── Customer.java
│ ├── CustomerInventory.java
│ ├── Entity.java
│ ├── Laptop.java
│ ├── OptionsClass.java
│ ├── Order.java
│ ├── Product.java
│ ├── Van.java
│ └── dummy
│ │ ├── ArrayDummy.java
│ │ ├── ListDummy.java
│ │ └── ValidationDummy.java
│ ├── interceptors
│ ├── TestOperationInterceptor.java
│ ├── TestOperationParameterInterceptor.java
│ ├── TestRequestBodyInterceptor.java
│ ├── TestSchemaFieldInterceptor.java
│ └── TestSchemaInterceptor.java
│ └── util
│ └── TestUtils.java
└── resources
├── expected_v2_openapi.json
└── expected_v2_openapi_nswag.json
/.github/workflows/maven.yml:
--------------------------------------------------------------------------------
1 | name: Java CI
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 |
8 | runs-on: ubuntu-latest
9 |
10 | steps:
11 | - uses: actions/checkout@v2
12 | - name: Set up JDK 1.8
13 | uses: actions/setup-java@v1
14 | with:
15 | java-version: 1.8
16 | - name: Build with Maven
17 | run: mvn -B package --file pom.xml
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled class file
2 | *.class
3 |
4 | # Log file
5 | *.log
6 |
7 | # BlueJ files
8 | *.ctxt
9 |
10 | # Mobile Tools for Java (J2ME)
11 | .mtj.tmp/
12 |
13 | # Package Files #
14 | *.jar
15 | *.war
16 | *.nar
17 | *.ear
18 | *.zip
19 | *.tar.gz
20 | *.rar
21 |
22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
23 | hs_err_pid*
24 | target/*
25 | /*/target
26 | .idea/*
27 | *.iml
28 | /*/*.iml
29 |
--------------------------------------------------------------------------------
/.java-version:
--------------------------------------------------------------------------------
1 | 1.8
2 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Release notes
2 |
3 | ## 1.4.6
4 |
5 | #44 https://github.com/jrcodeza/spring-openapi/issues/44
6 | #47 https://github.com/jrcodeza/spring-openapi/issues/47
7 |
8 | ## 1.4.5
9 | #41 https://github.com/jrcodeza/spring-openapi/issues/41
10 | #42 https://github.com/jrcodeza/spring-openapi/issues/42
11 | #45 https://github.com/jrcodeza/spring-openapi/issues/45
12 |
13 | ## 1.4.4
14 | OperationId generation improvement
15 | Use latest jackson-databing 2.9.10.3
16 |
17 | ## 1.4.3
18 | #38 https://github.com/jrcodeza/spring-openapi/issues/38
19 |
20 | ## 1.4.2
21 | #34 https://github.com/jrcodeza/spring-openapi/issues/34
22 | #36 https://github.com/jrcodeza/spring-openapi/issues/36
23 |
24 | ## 1.4.1
25 | Bugfixes.
26 |
27 | ## 1.4.0
28 | Added support for OpenAPI version 2 spec generation. For now only
29 | runtime generation (no maven plugin).
30 |
31 | ## 1.3.0
32 | https://github.com/jrcodeza/spring-openapi/issues/22
33 |
34 | https://github.com/jrcodeza/spring-openapi/issues/21
35 |
36 | https://github.com/jrcodeza/spring-openapi/issues/19
37 |
38 | ## 1.2.1
39 |
40 | https://github.com/jrcodeza/spring-openapi/issues/21
41 |
42 | https://github.com/jrcodeza/spring-openapi/issues/19
43 |
44 | Has same features as 1.3.0 but doesn't track (traverse) inheritance hierarchy
45 | over all java 'extends' using allOf. If inheritance hierarchy does not contain
46 | JsonSubTypes annotation (discriminator) then all parent properties are copied
47 | into the current class and no allOf approach is used. AllOf approach is used
48 | only for inheritance hierarchies where JsonSubTypes annotation is present
49 | (on some class).
50 |
51 | This is done because if generated spec has too big inheritance depth, then
52 | swagger ui might have problems to interpret this (follow $refs - I've got
53 | stack overflow and other problems). Oas-ui interpreter
54 | (https://github.com/vahanito/oas-ui) does not have this problem.
55 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 jrcode
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # spring-openapi (OpenAPI 3 generator)
2 | 
3 |
4 | Spring Boot OpenAPI 3 generator. It scans provided packages
5 | (model and controller) and generates based on reflection, javax validation
6 | and spring annotations the OpenAPI 3 json. It is able to handle also
7 | inheritance using OpenAPI 3 discriminator possibilities. The inheritance
8 | generation is achieved using jackson annotations.
9 |
10 | Generator allows you to also intercept schema, schema field, operation
11 | and operation parameter process, so you can include your own behavior
12 | to enrich already mapped sections (e.g. include own annotation behavior
13 | to descriptions etc.).
14 |
15 | So far there is core generator functionality
16 | **spring-openapi-schema-generator** and maven plugin
17 | **spring-openapi-schema-generator-plugin** available.Soon there will be
18 | also java client generator finalized, because current tooling is still
19 | not able to generate client with full OpenAPI 3 discriminator features
20 | (jackson annotations).
21 |
22 | There is also short article describing usage of spring-openapi generator here:
23 | https://medium.com/@remenec.jakub/openapi-3-spec-and-client-generation-on-java-spring-application-38a9ba5a2932.
24 |
25 | ## Current version
26 | **1.4.6**
27 |
28 | Now generator supports also OpenAPI version 2 generation.
29 |
30 | Release notes: https://github.com/jrcodeza/spring-openapi/blob/master/CHANGELOG.md
31 |
32 | 1.2.1 - to ensure compatiblity with swagger ui for bigger inheritance structures.
33 | This version models inheritance using allOf only if discriminator (JsonSubTypes) is found
34 | in inheritance hierarchy. If not, it takes all attributes from parent classes
35 | and adds them to the current one. See tests for more info.
36 |
37 | ## Example project
38 | You can check this repository https://github.com/jrcodeza/spring-openapi-example for an example
39 | usage. It shows spec to code and also code to spec generation using maven plugins. It contains
40 | simple REST resources, model (including inheritance), interceptors and examples resolver.
41 |
42 | ## OpenAPI UI interpreter
43 | If you need to display OpenAPI v3 spec in browser you can have a look also on **oas-ui**
44 | plugin https://github.com/vahanito/oas-ui (which was developed together with spring-openapi) .
45 | It supports searching of components and resources. Correctly displays inheritance also
46 | with discriminator info. You can use it in react as plugin or also as standalone bundle js.
47 |
48 | ## Annotations
49 | ### @OpenApiExample
50 | You can define it on class, method, parameter or field. There are 2 main use cases:
51 |
52 | - Providing the example value **in place**
53 | ```java
54 | @OpenApiExample(name = "StandardExample", value = "standardExampleValue")
55 | ```
56 | - providing **key** to example. You have to manage that key is handled by examples resolver
57 | ```java
58 | @OpenApiExample(name = "KeyExample", key = "KEY")
59 | ```
60 | Example resolver is a simple interface:
61 | ```java
62 | public interface OpenApiExampleResolver {
63 |
64 | String resolveExample(String exampleKey);
65 |
66 | }
67 | ```
68 | The **exampleKey** parameter is a key for which example value should be provided by resolver. It can be
69 | for example a file name with example request - then goal of the resolver is to read file content and provide
70 | it.
71 |
72 | It's possible to define more examples for one element by wrapping @OpenApiExample annotation to **@OpenApiExamples**
73 | one. Name of the example becomes important in this case, because it will be stored to spec as example name.
74 |
75 | ### @OpenApiIgnore
76 | Element annotated with this annotation will not be generated to spec.
77 |
78 | ### @Response
79 | Sometimes you need to specify method (operation) response in your own way. It can
80 | be necessary when you have several return statuses or response types. Also when
81 | you want to specify response headers. For this purpose you can use this annotation.
82 |
83 | It's also possible to wrap multiple @Response annotations to **@Responses** annotations. E.g.:
84 | ```java
85 | @Responses({
86 | @Response(responseCode = 201, description = "Created", responseBody = ValidationDummy.class,
87 | headers = @Header(name = "SomeHeader", description = "TestHeader")),
88 | @Response(responseCode = 200, description = "Replaced", responseBody = ValidationDummy.class)
89 | })
90 | ```
91 | @Header annotation is also custom spring-openapi annotation.
92 |
93 | ## Generate spec from code
94 | ### Runtime usage
95 | Include dependency
96 |
97 | ```java
98 |
99 | com.github.jrcodeza
100 | spring-openapi-schema-generator
101 | 1.4.6
102 |
103 | ```
104 |
105 | Instantiate **OpenAPIGenerator**
106 |
107 | ```java
108 | OpenAPIGenerator openAPIGenerator = new OpenAPIGenerator(
109 | singletonList("org.spring.openapi.schema.generator.plugin.model.*"),
110 | singletonList("org.spring.openapi.schema.generator.plugin.controller.*"),
111 | createTestInfo()
112 | );
113 | ```
114 | The first parameter is model package and the second is controller package. It
115 | is possible to define multiple packages for each. The third parameter
116 | is OpenAPI basic info like project title, description, version etc. It is
117 | required in order to create valid OpenAPI 3 output.
118 | Additionally you can define interceptors
119 | * **OperationInterceptor** - executed after controller method is mapped
120 | * **OperationParameterInterceptor** - executed after each method parameter is mapped
121 | * **RequestBodyInterceptor** - executed after RequestBody annotated parameter of a method is mapped
122 | * **SchemaFieldInterceptor** - executed after class field is mapped
123 | * **SchemaInterceptor** - executed after each class is mapped
124 |
125 | You can add these interceptors either to constructor after info parameter or
126 | by calling add + interceptorName method on OpenAPIGenerator class. E.g.:
127 |
128 | ```java
129 | openAPIGenerator.addOperationInterceptor(operationInterceptor);
130 | openAPIGenerator.addOperationParameterInterceptor(operationParameterInterceptor);
131 | openAPIGenerator.addRequestBodyInterceptor(requestBodyInterceptor);
132 | openAPIGenerator.addSchemaFieldInterceptor(schemaFieldInterceptor);
133 | openAPIGenerator.addSchemaInterceptor(schemaInterceptor);
134 | ```
135 |
136 | You can define your own interceptor as follows:
137 | ```java
138 | package com.github.jrcodeza.schema.generator.plugin.interceptors;
139 |
140 | import java.lang.reflect.Method;
141 |
142 | import com.github.jrcodeza.schema.generator.interceptors.OperationInterceptor;
143 |
144 | import io.swagger.v3.oas.models.Operation;
145 |
146 | public class TestOperationInterceptor implements OperationInterceptor {
147 |
148 | @Override
149 | public void intercept(Method method, Operation transformedOperation) {
150 | transformedOperation.setSummary("Interceptor summary");
151 | }
152 |
153 | }
154 | ```
155 |
156 | It's possible to define also global headers which are applicable for all resources.
157 | ```java
158 | openAPIGenerator.addGlobalHeader("Test-Global-Header", "Some desc", false);
159 | ```
160 |
161 | OpenAPIGenerator method **generate** can also take OpenApiGeneratorConfig as parameter.
162 | In this config you can define if generation of examples is enabled. You can also define
163 | example resolver, which can be useful if for example you have bigger examples of POST body requests
164 | stored in files. You can define it as follows:
165 |
166 | ```java
167 | OpenApiGeneratorConfigBuilder.defaultConfig()
168 | .withGenerateExamples(true)
169 | .withOpenApiExampleResolver(createExampleResolver())
170 | .build()
171 | ```
172 |
173 | Finally when you want to **generate OpenAPI 3 spec** you have to execute
174 | generate method on OpenAPIGenerator instance.
175 | ```java
176 | OpenAPI openAPI = openAPIGenerator.generate();
177 | ```
178 | and you will receive OpenAPI object (from swagger API) which can be serialized
179 | to string like this
180 | ```java
181 | ObjectMapper objectMapper = new ObjectMapper();
182 | objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
183 | try {
184 | String generated = objectMapper.writeValueAsString(openAPI);
185 | JSONAssert.assertEquals(getResourceFileAsString(), generated, false);
186 | } catch (JsonProcessingException | JSONException e) {
187 | e.printStackTrace();
188 | }
189 | ```
190 |
191 | ### Maven plugin usage
192 | Maven plugin wraps before mentioned functionality into maven plugin.
193 |
194 | ```xml
195 |
196 |
197 |
198 | com.github.jrcodeza
199 | spring-openapi-schema-generator-plugin
200 |
201 | Test title
202 | Test description
203 | 1.0.0-TEST
204 |
205 | org.spring.openapi.schema.generator.test.model.*
206 |
207 |
208 | org.spring.openapi.schema.generator.test.controller.*
209 |
210 | target/openapi
211 |
212 | com.github.jrcodeza.schema.generator.plugin.interceptor.TestSchemaFieldInterceptor
213 |
214 |
215 | com.github.jrcodeza.schema.generator.plugin.interceptor.TestOperationInterceptor
216 |
217 | true
218 | com.github.jrcodeza.schema.generator.plugin.example.TestExampleResolver
219 |
220 |
221 |
222 |
223 | ```
224 |
225 | You can see that title, description and version are the OpenAPI info parameters.
226 | Then you have to define also modelPackages and controllerPackages. Additionally
227 | you should define also outputDirectory, in our case target/openapi will be the target folder
228 | where the swagger.json will be saved.
229 |
230 | From 1.2.0 version it is also possible to define all interceptors also in plugin. Additionally there
231 | is also option to turn on/off generateExamples and of course specify openApiExamplesResolver.
232 |
233 | ## Generate client from spec
234 |
235 | ### Runtime usage
236 | Include dependency
237 |
238 | ```java
239 |
240 | com.github.jrcodeza
241 | spring-openapi-client-generator
242 | 1.4.6
243 |
244 | ```
245 |
246 | Use generator in code
247 |
248 | ```java
249 | new OpenApiClientGenerator().generateClient(
250 | "target.package",
251 | "pathToOpenApi3Spec",
252 | "targetFolder", // e.g. /target/generatedSources/oas
253 | true); // should generate interface
254 | ```
255 |
256 | ### Maven plugin usage
257 |
258 | 1. Include dependency
259 | ```java
260 |
261 | com.github.jrcodeza
262 | spring-openapi-client-generator-plugin
263 | 1.4.6
264 |
265 | ```
266 |
267 | 2. Add plugin to maven plugins section
268 | ```xml
269 |
270 | com.github.jrcodeza
271 | spring-openapi-client-generator-plugin
272 |
273 |
274 |
275 | generateClientFromOpenApi
276 |
277 |
278 | test.package
279 | target/generated-sources
280 | src/main/resources/oas3.json
281 | true
282 | true
283 |
284 |
285 |
286 |
287 | ```
288 | **generateResourceInterface** option allows you to turn on/off generation of interface
289 | with methods that represent operations of the API.
290 |
291 | **generateDiscriminatorProperty** allows you to turn on/off functionality where
292 | plugin generates the discriminator property explicitly to class (setting it visible=true in jackson).
293 | This is handy when you need to see the discriminator property inside the class
294 | and being able to access it using setter or getter.
295 |
296 | ## Contributions
297 | Pull requests are welcome. If you would like to collaborate more feel free to contact
298 | me on remenec.jakub@gmail.com .
299 |
300 | ## License
301 | License for this tool is GNU GPL V3.
302 |
--------------------------------------------------------------------------------
/annotations/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 4.0.0
4 |
5 | com.github.jrcodeza
6 | spring-openapi
7 | 1.4.11-SNAPSHOT
8 |
9 |
10 | spring-openapi-annotations
11 |
12 | 1.4.11-SNAPSHOT
13 | jar
14 |
15 | Spring Open API - Annotations
16 |
17 |
18 |
--------------------------------------------------------------------------------
/annotations/src/main/java/com/github/jrcodeza/Header.java:
--------------------------------------------------------------------------------
1 | package com.github.jrcodeza;
2 |
3 | import java.lang.annotation.ElementType;
4 | import java.lang.annotation.Retention;
5 | import java.lang.annotation.RetentionPolicy;
6 | import java.lang.annotation.Target;
7 |
8 | @Target({ElementType.METHOD})
9 | @Retention(RetentionPolicy.RUNTIME)
10 | public @interface Header {
11 |
12 | String name() default "";
13 |
14 | String description() default "";
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/annotations/src/main/java/com/github/jrcodeza/OpenApiExample.java:
--------------------------------------------------------------------------------
1 | package com.github.jrcodeza;
2 |
3 | import java.lang.annotation.ElementType;
4 | import java.lang.annotation.Retention;
5 | import java.lang.annotation.RetentionPolicy;
6 | import java.lang.annotation.Target;
7 |
8 | @Target({ElementType.TYPE, ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD})
9 | @Retention(RetentionPolicy.RUNTIME)
10 | public @interface OpenApiExample {
11 |
12 | String name() default "";
13 | String description() default "";
14 | /**
15 | * If not filled in then key has to be filled in. CustomExampleResolver will be invoked with key
16 | * value as parameter.
17 | * @return
18 | */
19 | String value() default "";
20 | String key() default "";
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/annotations/src/main/java/com/github/jrcodeza/OpenApiExamples.java:
--------------------------------------------------------------------------------
1 | package com.github.jrcodeza;
2 |
3 | import java.lang.annotation.ElementType;
4 | import java.lang.annotation.Retention;
5 | import java.lang.annotation.RetentionPolicy;
6 | import java.lang.annotation.Target;
7 |
8 | @Target({ElementType.TYPE, ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD})
9 | @Retention(RetentionPolicy.RUNTIME)
10 | public @interface OpenApiExamples {
11 |
12 | OpenApiExample[] value();
13 |
14 | }
15 |
--------------------------------------------------------------------------------
/annotations/src/main/java/com/github/jrcodeza/OpenApiIgnore.java:
--------------------------------------------------------------------------------
1 | package com.github.jrcodeza;
2 |
3 | import java.lang.annotation.ElementType;
4 | import java.lang.annotation.Retention;
5 | import java.lang.annotation.RetentionPolicy;
6 | import java.lang.annotation.Target;
7 |
8 | @Target({ElementType.TYPE, ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD})
9 | @Retention(RetentionPolicy.RUNTIME)
10 | public @interface OpenApiIgnore {
11 | }
12 |
--------------------------------------------------------------------------------
/annotations/src/main/java/com/github/jrcodeza/Response.java:
--------------------------------------------------------------------------------
1 | package com.github.jrcodeza;
2 |
3 | import java.lang.annotation.ElementType;
4 | import java.lang.annotation.Retention;
5 | import java.lang.annotation.RetentionPolicy;
6 | import java.lang.annotation.Target;
7 |
8 | @Target({ElementType.METHOD})
9 | @Retention(RetentionPolicy.RUNTIME)
10 | public @interface Response {
11 |
12 | int responseCode();
13 |
14 | String description();
15 |
16 | Class> responseBody() default Void.class;
17 |
18 | Header[] headers() default {};
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/annotations/src/main/java/com/github/jrcodeza/Responses.java:
--------------------------------------------------------------------------------
1 | package com.github.jrcodeza;
2 |
3 | import java.lang.annotation.ElementType;
4 | import java.lang.annotation.Retention;
5 | import java.lang.annotation.RetentionPolicy;
6 | import java.lang.annotation.Target;
7 |
8 | @Target({ElementType.METHOD, ElementType.TYPE})
9 | @Retention(RetentionPolicy.RUNTIME)
10 | public @interface Responses {
11 |
12 | Response[] value();
13 |
14 | }
15 |
--------------------------------------------------------------------------------
/client-generator-plugin/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 4.0.0
4 |
5 | com.github.jrcodeza
6 | spring-openapi
7 | 1.4.11-SNAPSHOT
8 |
9 |
10 | spring-openapi-client-generator-plugin
11 |
12 | 1.4.11-SNAPSHOT
13 | maven-plugin
14 |
15 | Spring Open API - Client generator plugin
16 |
17 |
18 |
19 | com.github.jrcodeza
20 | spring-openapi-client-generator
21 | ${project.version}
22 |
23 |
24 |
25 | org.apache.maven.plugin-tools
26 | maven-plugin-annotations
27 | provided
28 |
29 |
30 | org.apache.maven
31 | maven-plugin-api
32 |
33 |
34 | org.apache.maven
35 | maven-core
36 |
37 |
38 | org.apache.maven
39 | maven-artifact
40 | provided
41 |
42 |
43 | org.apache.maven
44 | maven-compat
45 |
46 |
47 |
48 | org.apache.maven.plugin-testing
49 | maven-plugin-testing-harness
50 | test
51 |
52 |
53 | junit
54 | junit
55 | test
56 |
57 |
58 |
59 |
60 |
61 |
62 | org.apache.maven.plugins
63 | maven-compiler-plugin
64 |
65 | 8
66 | 8
67 |
68 |
69 |
70 | org.apache.maven.plugins
71 | maven-plugin-plugin
72 | 3.5
73 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/client-generator-plugin/src/main/java/com/github/jrcodeza/client/generator/plugin/GenerateClientFromSchemaMojo.java:
--------------------------------------------------------------------------------
1 | package com.github.jrcodeza.client.generator.plugin;
2 |
3 | import org.apache.commons.lang3.BooleanUtils;
4 | import org.apache.maven.plugin.AbstractMojo;
5 | import org.apache.maven.plugins.annotations.LifecyclePhase;
6 | import org.apache.maven.plugins.annotations.Mojo;
7 | import org.apache.maven.plugins.annotations.Parameter;
8 | import org.apache.maven.project.MavenProject;
9 | import org.spring.openapi.client.generator.OpenApiClientGenerator;
10 |
11 | @Mojo(name = "generateClientFromOpenApi", defaultPhase = LifecyclePhase.GENERATE_SOURCES)
12 | public class GenerateClientFromSchemaMojo extends AbstractMojo {
13 |
14 | @Parameter
15 | private String outputPackage;
16 |
17 | @Parameter
18 | private String outputPath;
19 |
20 | @Parameter
21 | private String schemaPath;
22 |
23 | @Parameter
24 | private Boolean generateResourceInterface;
25 |
26 | @Parameter
27 | private Boolean generateDiscriminatorProperty;
28 |
29 | @Parameter(defaultValue = "${project}")
30 | private MavenProject project;
31 |
32 | @Override
33 | public void execute() {
34 | new OpenApiClientGenerator().generateClient(outputPackage, schemaPath, outputPath,
35 | BooleanUtils.isTrue(generateResourceInterface),
36 | BooleanUtils.isTrue(generateDiscriminatorProperty)
37 | );
38 | project.addCompileSourceRoot(outputPath);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/client-generator/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 4.0.0
4 |
5 | com.github.jrcodeza
6 | spring-openapi
7 | 1.4.11-SNAPSHOT
8 |
9 |
10 | spring-openapi-client-generator
11 |
12 | 1.4.11-SNAPSHOT
13 | jar
14 |
15 | Spring Open API - Client generator
16 |
17 |
18 |
19 | org.springframework
20 | spring-core
21 |
22 |
23 | org.springframework
24 | spring-context
25 |
26 |
27 |
28 | org.apache.commons
29 | commons-collections4
30 |
31 |
32 |
33 | io.swagger.core.v3
34 | swagger-models
35 |
36 |
37 | io.swagger.core.v3
38 | swagger-annotations
39 |
40 |
41 | io.swagger.core.v3
42 | swagger-integration
43 |
44 |
45 |
46 | com.squareup
47 | javapoet
48 |
49 |
50 |
51 | com.fasterxml.jackson.core
52 | jackson-databind
53 |
54 |
55 | com.fasterxml.jackson.core
56 | jackson-annotations
57 |
58 |
59 |
60 | javax.validation
61 | validation-api
62 |
63 |
64 |
65 | org.apache.logging.log4j
66 | log4j-api
67 |
68 |
69 | org.apache.logging.log4j
70 | log4j-core
71 |
72 |
73 | org.apache.logging.log4j
74 | log4j-slf4j-impl
75 |
76 |
77 |
78 | org.skyscreamer
79 | jsonassert
80 | test
81 |
82 |
83 | commons-io
84 | commons-io
85 | test
86 |
87 |
88 | junit
89 | junit
90 | 4.12
91 | test
92 |
93 |
94 |
95 |
96 |
97 |
98 | org.apache.maven.plugins
99 | maven-compiler-plugin
100 |
101 | 8
102 | 8
103 |
104 |
105 |
106 |
107 |
108 |
--------------------------------------------------------------------------------
/client-generator/src/main/java/org/spring/openapi/client/generator/ClientGeneratorUtils.java:
--------------------------------------------------------------------------------
1 | package org.spring.openapi.client.generator;
2 |
3 | import java.io.File;
4 | import java.io.IOException;
5 | import java.util.List;
6 | import java.util.Map;
7 | import java.util.Set;
8 |
9 | import com.squareup.javapoet.ClassName;
10 | import com.squareup.javapoet.JavaFile;
11 | import com.squareup.javapoet.TypeSpec;
12 |
13 | import org.apache.commons.collections4.CollectionUtils;
14 | import org.apache.commons.lang3.StringUtils;
15 |
16 | import io.swagger.v3.oas.models.media.ComposedSchema;
17 | import io.swagger.v3.oas.models.media.Schema;
18 |
19 | import static org.apache.commons.lang3.StringUtils.equalsIgnoreCase;
20 |
21 | public final class ClientGeneratorUtils {
22 |
23 | public static final String JAVA_LANG_PKG = "java.lang";
24 | public static final String JAVA_TIME_PKG = "java.time";
25 |
26 | public static void buildTypeSpec(String targetPackage, TypeSpec.Builder typeSpecBuilder, String outputPath) {
27 | try {
28 | JavaFile.builder(targetPackage, typeSpecBuilder.build())
29 | .build()
30 | .writeTo(new File(outputPath));
31 | } catch (IOException e) {
32 | e.printStackTrace(); // TODO
33 | }
34 | }
35 |
36 | private ClientGeneratorUtils() {
37 | throw new AssertionError();
38 | }
39 |
40 | public static String getNameFromRef(String ref) {
41 | return ref.replace("#/components/schemas/", "");
42 | }
43 |
44 | public static ClassName getStringGenericClassName(Schema> genericSchema) {
45 | if (genericSchema.getFormat() == null) {
46 | return ClassName.get(JAVA_LANG_PKG, "String");
47 | } else if (equalsIgnoreCase(genericSchema.getFormat(), "date")) {
48 | return ClassName.get(JAVA_TIME_PKG, "LocalDate");
49 | } else if (equalsIgnoreCase(genericSchema.getFormat(), "date-time")) {
50 | return ClassName.get(JAVA_TIME_PKG, "LocalDateTime");
51 | }
52 | throw new IllegalArgumentException("Error parsing string based property");
53 | }
54 |
55 | public static ClassName getNumberGenericClassName(Schema> genericSchema) {
56 | if (genericSchema.getFormat() == null || StringUtils.equalsIgnoreCase(genericSchema.getFormat(), "int32")) {
57 | return ClassName.get(JAVA_LANG_PKG, "Integer");
58 | } else if (StringUtils.equalsIgnoreCase(genericSchema.getFormat(), "int64")) {
59 | return ClassName.get(JAVA_LANG_PKG, "Long");
60 | } else if (StringUtils.equalsIgnoreCase(genericSchema.getFormat(), "float")) {
61 | return ClassName.get(JAVA_LANG_PKG, "Float");
62 | } else if (StringUtils.equalsIgnoreCase(genericSchema.getFormat(), "double")) {
63 | return ClassName.get(JAVA_LANG_PKG, "Double");
64 | } else {
65 | return ClassName.get(JAVA_LANG_PKG, "Integer");
66 | }
67 | }
68 |
69 | public static String determineParentClassNameUsingDiscriminator(Schema innerSchema, String fieldName, Map allComponents) {
70 | Set> discriminatorEntries = innerSchema.getDiscriminator().getMapping().entrySet();
71 | if (CollectionUtils.isEmpty(discriminatorEntries)) {
72 | throw new IllegalArgumentException("Discriminator needs to have at least one value defined. Field: " + fieldName);
73 | }
74 | return determineParentClassName(discriminatorEntries.iterator().next().getValue(), allComponents);
75 | }
76 |
77 | public static String determineParentClassName(String childClassToFind, Map allComponents) {
78 | Schema childClass = allComponents.get(childClassToFind);
79 | if (childClass instanceof ComposedSchema) {
80 | ComposedSchema childClassComposed = (ComposedSchema) childClass;
81 | if (CollectionUtils.isNotEmpty(childClassComposed.getAllOf())) {
82 | String parentClassRef = childClassComposed.getAllOf().get(0).get$ref();
83 | if (parentClassRef == null) {
84 | throw new IllegalArgumentException("Unsupported inheritance model. AllOf $ref for parent class has to be defined");
85 | }
86 | return getNameFromRef(parentClassRef);
87 | }
88 | }
89 | throw new IllegalArgumentException("Unsupported inheritance model for " + (childClass == null ? "null" : childClass.getName()));
90 | }
91 |
92 | public static String determineParentClassNameUsingOneOf(Schema innerSchema, String fieldName, Map allComponents) {
93 | if (!(innerSchema instanceof ComposedSchema)) {
94 | throw new IllegalArgumentException("To determine class name using allOf schema has to be Composed");
95 | }
96 | List allOf = ((ComposedSchema) innerSchema).getAllOf();
97 | String refToOneOf = allOf.get(0).get$ref();
98 | if (refToOneOf == null) {
99 | throw new IllegalArgumentException("OneOf entry needs to have defined $ref. Field: " + fieldName);
100 | }
101 | return determineParentClassName(getNameFromRef(refToOneOf), allComponents);
102 | }
103 |
104 |
105 | }
106 |
--------------------------------------------------------------------------------
/client-generator/src/main/java/org/spring/openapi/client/generator/OperationData.java:
--------------------------------------------------------------------------------
1 | package org.spring.openapi.client.generator;
2 |
3 | import io.swagger.v3.oas.models.Operation;
4 |
5 | public class OperationData {
6 |
7 | private final Operation operation;
8 | private final String url;
9 |
10 | public OperationData(Operation operation, String url) {
11 | this.operation = operation;
12 | this.url = url;
13 | }
14 |
15 | public Operation getOperation() {
16 | return operation;
17 | }
18 |
19 | public String getUrl() {
20 | return url;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/client-generator/src/main/java/org/spring/openapi/client/generator/ResourceInterfaceGenerator.java:
--------------------------------------------------------------------------------
1 | package org.spring.openapi.client.generator;
2 |
3 | import java.io.File;
4 | import java.util.ArrayList;
5 | import java.util.HashMap;
6 | import java.util.LinkedHashMap;
7 | import java.util.List;
8 | import java.util.Map;
9 | import java.util.Objects;
10 | import java.util.Set;
11 | import java.util.stream.Collectors;
12 | import java.util.stream.Stream;
13 |
14 | import javax.lang.model.element.Modifier;
15 | import javax.validation.constraints.DecimalMax;
16 | import javax.validation.constraints.DecimalMin;
17 |
18 | import com.squareup.javapoet.AnnotationSpec;
19 | import com.squareup.javapoet.ClassName;
20 | import com.squareup.javapoet.MethodSpec;
21 | import com.squareup.javapoet.ParameterSpec;
22 | import com.squareup.javapoet.ParameterizedTypeName;
23 | import com.squareup.javapoet.TypeName;
24 | import com.squareup.javapoet.TypeSpec;
25 |
26 | import org.apache.commons.collections4.CollectionUtils;
27 | import org.apache.commons.lang3.StringUtils;
28 |
29 | import io.swagger.v3.oas.models.Operation;
30 | import io.swagger.v3.oas.models.PathItem;
31 | import io.swagger.v3.oas.models.Paths;
32 | import io.swagger.v3.oas.models.media.ArraySchema;
33 | import io.swagger.v3.oas.models.media.ComposedSchema;
34 | import io.swagger.v3.oas.models.media.MediaType;
35 | import io.swagger.v3.oas.models.media.Schema;
36 | import io.swagger.v3.oas.models.responses.ApiResponse;
37 |
38 | import static org.apache.commons.lang3.StringUtils.equalsIgnoreCase;
39 | import static org.spring.openapi.client.generator.ClientGeneratorUtils.JAVA_LANG_PKG;
40 | import static org.spring.openapi.client.generator.ClientGeneratorUtils.JAVA_TIME_PKG;
41 | import static org.spring.openapi.client.generator.ClientGeneratorUtils.buildTypeSpec;
42 | import static org.spring.openapi.client.generator.ClientGeneratorUtils.determineParentClassName;
43 | import static org.spring.openapi.client.generator.ClientGeneratorUtils.determineParentClassNameUsingOneOf;
44 | import static org.spring.openapi.client.generator.ClientGeneratorUtils.getNameFromRef;
45 | import static org.spring.openapi.client.generator.ClientGeneratorUtils.getNumberGenericClassName;
46 |
47 | public class ResourceInterfaceGenerator {
48 |
49 | private String targetPackage;
50 |
51 | private final Map allComponents;
52 |
53 | public ResourceInterfaceGenerator(Map allComponents) {
54 | this.allComponents = allComponents;
55 | }
56 |
57 | public void generateResourceInterface(Paths paths, String targetPackage, String outputPath) {
58 | this.targetPackage = targetPackage;
59 | Map> resourceMap = new HashMap<>();
60 | paths.entrySet().forEach(pathItemEntry -> addToResourceMap(pathItemEntry, resourceMap));
61 | resourceMap.entrySet().forEach(resource -> createInterface(resource, targetPackage, outputPath));
62 | }
63 |
64 | private void createInterface(Map.Entry> resource, String targetPackage, String outputPath) {
65 | TypeSpec.Builder typeSpecBuilder = TypeSpec.interfaceBuilder(resource.getKey())
66 | .addModifiers(Modifier.PUBLIC);
67 | resource.getValue().forEach(operationData -> typeSpecBuilder.addMethod(createMethod(operationData)));
68 | buildTypeSpec(targetPackage + ".operations", typeSpecBuilder, outputPath);
69 | }
70 |
71 | private MethodSpec createMethod(OperationData operationData) {
72 | Operation operation = operationData.getOperation();
73 | MethodSpec.Builder methodSpecBuilder = MethodSpec.methodBuilder(getMethodName(operation))
74 | .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT);
75 | if (operation.getDescription() != null) {
76 | methodSpecBuilder.addJavadoc(operation.getDescription());
77 | }
78 | if (operation.getParameters() != null) {
79 | operation.getParameters()
80 | .forEach(parameter -> methodSpecBuilder.addParameter(
81 | parseProperties(formatParameterName(parameter.getName()), parameter.getSchema()).build()
82 | ));
83 | }
84 | if (operation.getRequestBody() != null && operation.getRequestBody().getContent() != null) {
85 | LinkedHashMap mediaTypes = operation.getRequestBody().getContent();
86 | methodSpecBuilder.addParameter(parseProperties("requestBody", mediaTypes.entrySet().iterator().next().getValue().getSchema()).build());
87 | }
88 | if (operation.getResponses() == null || CollectionUtils.isEmpty(operation.getResponses().entrySet())) {
89 | methodSpecBuilder.returns(TypeName.VOID);
90 | } else {
91 | ApiResponse apiResponse = operation.getResponses().entrySet().stream()
92 | .filter(responseEntry -> StringUtils.startsWith(responseEntry.getKey(), "2")) // HTTP 20x
93 | .findFirst()
94 | .map(Map.Entry::getValue)
95 | .orElse(null);
96 | if (apiResponse != null && apiResponse.getContent() != null && !apiResponse.getContent().isEmpty()) {
97 | MediaType mediaType = apiResponse.getContent().entrySet().iterator().next().getValue();
98 | if (mediaType.getSchema() != null) {
99 | methodSpecBuilder.returns(determineTypeName(mediaType.getSchema()));
100 | return methodSpecBuilder.build();
101 | }
102 | }
103 | methodSpecBuilder.returns(TypeName.VOID);
104 | }
105 |
106 | return methodSpecBuilder.build();
107 | }
108 |
109 | private String getMethodName(Operation operation) {
110 | String operationIdPrefix = "";
111 | if (CollectionUtils.isNotEmpty(operation.getTags())) {
112 | operationIdPrefix = StringUtils.uncapitalize(toResourceInterfaceName(operation.getTags().get(0)));
113 | }
114 | return StringUtils.uncapitalize(operation.getOperationId()).replaceFirst(operationIdPrefix, "");
115 | }
116 |
117 | private String formatParameterName(String name) {
118 | String result = Stream.of(name.split("-"))
119 | .map(StringUtils::capitalize)
120 | .collect(Collectors.joining());
121 | return StringUtils.uncapitalize(result);
122 | }
123 |
124 | private void addToResourceMap(Map.Entry pathItemEntry, Map> resourceMap) {
125 | String url = pathItemEntry.getKey();
126 | String resourceName = getResourceName(pathItemEntry.getValue());
127 | PathItem pathItem = pathItemEntry.getValue();
128 | Stream.of(pathItem.getPost(), pathItem.getPatch(), pathItem.getPut(), pathItem.getHead(), pathItem.getOptions(), pathItem.getGet(), pathItem.getDelete())
129 | .filter(Objects::nonNull)
130 | .forEach(operation -> {
131 | if (resourceMap.containsKey(resourceName)) {
132 | resourceMap.get(resourceName).add(new OperationData(operation, url));
133 | } else {
134 | List operations = new ArrayList<>();
135 | operations.add(new OperationData(operation, url));
136 | resourceMap.put(resourceName, operations);
137 | }
138 | });
139 | }
140 |
141 | private String getResourceName(PathItem value) {
142 | return Stream.of(value.getPost(), value.getPatch(), value.getPut(), value.getHead(), value.getOptions(), value.getGet(), value.getDelete())
143 | .filter(Objects::nonNull)
144 | .filter(operation -> CollectionUtils.isNotEmpty(operation.getTags()))
145 | .findFirst()
146 | .map(operation -> toResourceInterfaceName(operation.getTags().get(0)))
147 | .orElse(null);
148 | }
149 |
150 | private String toResourceInterfaceName(String operationTag) {
151 | return Stream.of(operationTag.split("-"))
152 | .map(StringUtils::capitalize)
153 | .collect(Collectors.joining());
154 | }
155 |
156 | private ParameterSpec.Builder parseProperties(String parameterName, Schema parameterSchema) {
157 | if (parameterSchema.getType() != null) {
158 | return parseTypeBasedSchema(parameterName, parameterSchema);
159 | } else if (parameterSchema.get$ref() != null) {
160 | // simple no inheritance
161 | return createSimpleParameterSpec(null, getNameFromRef(parameterSchema.get$ref()), parameterName);
162 | } else if (parameterSchema instanceof ComposedSchema && CollectionUtils.isNotEmpty(((ComposedSchema) parameterSchema).getAllOf())) {
163 | return createSimpleParameterSpec(null, determineParentClassNameUsingOneOf(parameterSchema, parameterName, allComponents), parameterName);
164 | } else {
165 | throw new IllegalArgumentException("Incorrect schema. One of [type, $ref, discriminator+oneOf] has to be defined in property schema");
166 | }
167 | }
168 |
169 | private ParameterSpec.Builder parseTypeBasedSchema(String parameterName, Schema innerSchema) {
170 | if (equalsIgnoreCase(innerSchema.getType(), "string")) {
171 | return ParameterSpec.builder(getStringGenericClassName(innerSchema), parameterName);
172 | } else if (equalsIgnoreCase(innerSchema.getType(), "integer") || equalsIgnoreCase(innerSchema.getType(), "number")) {
173 | return getNumberBasedSchemaParameter(parameterName, innerSchema);
174 | } else if (equalsIgnoreCase(innerSchema.getType(), "boolean")) {
175 | return createSimpleParameterSpec(JAVA_LANG_PKG, "Boolean", parameterName);
176 | } else if (equalsIgnoreCase(innerSchema.getType(), "array") && innerSchema instanceof ArraySchema) {
177 | ArraySchema arraySchema = (ArraySchema) innerSchema;
178 | ParameterizedTypeName listParameterizedTypeName = ParameterizedTypeName.get(
179 | ClassName.get("java.util", "List"),
180 | determineTypeName(arraySchema.getItems())
181 | );
182 | return ParameterSpec.builder(listParameterizedTypeName, parameterName);
183 | } else if (equalsIgnoreCase(innerSchema.getType(), "object") && isFile(innerSchema.getProperties())) {
184 | return ParameterSpec.builder(ClassName.get(File.class), parameterName);
185 | }
186 | return createSimpleParameterSpec(JAVA_LANG_PKG, "Object", parameterName);
187 | }
188 |
189 | private ClassName determineTypeName(Schema> schema) {
190 | if (equalsIgnoreCase(schema.getType(), "string") && !StringUtils.equalsIgnoreCase(schema.getFormat(), "binary")) {
191 | return getStringGenericClassName(schema);
192 | } else if (equalsIgnoreCase(schema.getType(), "integer") || equalsIgnoreCase(schema.getType(), "number")) {
193 | return getNumberGenericClassName(schema);
194 | } else if (equalsIgnoreCase(schema.getType(), "boolean")) {
195 | return ClassName.get(JAVA_LANG_PKG, "Boolean");
196 | } else if (schema.get$ref() != null) {
197 | return ClassName.bestGuess(targetPackage + "." + getNameFromRef(schema.get$ref()));
198 | } else if (schema instanceof ComposedSchema && CollectionUtils.isNotEmpty(((ComposedSchema) schema).getAllOf())) {
199 | return ClassName.bestGuess(targetPackage + "." + determineParentClassNameUsingOneOf(schema, "innerArray", allComponents));
200 | } else if (schema.getDiscriminator() != null) {
201 | return ClassName.bestGuess(targetPackage + "." + determineParentClassNameUsingDiscriminator(schema, "innerArray"));
202 | } else if ((equalsIgnoreCase(schema.getType(), "object") || equalsIgnoreCase(schema.getType(), "string")) && isFile(schema.getProperties())) {
203 | return ClassName.get(File.class);
204 | }
205 | return ClassName.get(JAVA_LANG_PKG, "Object");
206 | }
207 |
208 | private boolean isFile(Map properties) {
209 | if (properties == null || properties.isEmpty()) {
210 | return false;
211 | }
212 | return properties.entrySet().stream()
213 | .map(Map.Entry::getValue)
214 | .anyMatch(schema -> equalsIgnoreCase(schema.getFormat(), "binary"));
215 | }
216 |
217 | private ClassName getStringGenericClassName(Schema> genericSchema) {
218 | if (genericSchema.getFormat() == null) {
219 | return ClassName.get(JAVA_LANG_PKG, "String");
220 | } else if (equalsIgnoreCase(genericSchema.getFormat(), "date")) {
221 | return ClassName.get(JAVA_TIME_PKG, "LocalDate");
222 | } else if (equalsIgnoreCase(genericSchema.getFormat(), "date-time")) {
223 | return ClassName.get(JAVA_TIME_PKG, "LocalDateTime");
224 | }
225 | throw new IllegalArgumentException("Error parsing string based property");
226 | }
227 |
228 | private String determineParentClassNameUsingDiscriminator(Schema innerSchema, String fieldName) {
229 | Set> discriminatorEntries = innerSchema.getDiscriminator().getMapping().entrySet();
230 | if (CollectionUtils.isEmpty(discriminatorEntries)) {
231 | throw new IllegalArgumentException("Discriminator needs to have at least one value defined. Field: " + fieldName);
232 | }
233 | return determineParentClassName(discriminatorEntries.iterator().next().getValue(), allComponents);
234 | }
235 |
236 | private ParameterSpec.Builder getNumberBasedSchemaParameter(String fieldName, Schema innerSchema) {
237 | ParameterSpec.Builder fieldBuilder = createNumberBasedParameterWithFormat(fieldName, innerSchema);
238 | if (innerSchema.getMinimum() != null) {
239 | fieldBuilder.addAnnotation(AnnotationSpec.builder(DecimalMin.class)
240 | .addMember("value", "$S", innerSchema.getMinimum().toString())
241 | .build()
242 | );
243 | }
244 | if (innerSchema.getMaximum() != null) {
245 | fieldBuilder.addAnnotation(AnnotationSpec.builder(DecimalMax.class)
246 | .addMember("value", "$S", innerSchema.getMaximum().toString())
247 | .build()
248 | );
249 | }
250 | return fieldBuilder;
251 | }
252 |
253 | private ParameterSpec.Builder createNumberBasedParameterWithFormat(String fieldName, Schema innerSchema) {
254 | if (innerSchema.getFormat() == null || StringUtils.equalsIgnoreCase(innerSchema.getFormat(), "int32")) {
255 | return createSimpleParameterSpec(JAVA_LANG_PKG, "Integer", fieldName);
256 | } else if (StringUtils.equalsIgnoreCase(innerSchema.getFormat(), "int64")) {
257 | return createSimpleParameterSpec(JAVA_LANG_PKG, "Long", fieldName);
258 | } else if (StringUtils.equalsIgnoreCase(innerSchema.getFormat(), "float")) {
259 | return createSimpleParameterSpec(JAVA_LANG_PKG, "Float", fieldName);
260 | } else if (StringUtils.equalsIgnoreCase(innerSchema.getFormat(), "double")) {
261 | return createSimpleParameterSpec(JAVA_LANG_PKG, "Double", fieldName);
262 | } else {
263 | return createSimpleParameterSpec(JAVA_LANG_PKG, "Integer", fieldName);
264 | }
265 | }
266 |
267 | private ParameterSpec.Builder createSimpleParameterSpec(String packageName, String className, String parameterName) {
268 | ClassName simpleFieldClassName = packageName == null ? ClassName.bestGuess(targetPackage + "." + className) : ClassName.get(packageName, className);
269 | return ParameterSpec.builder(simpleFieldClassName, parameterName);
270 | }
271 |
272 | }
273 |
--------------------------------------------------------------------------------
/client-generator/src/test/java/org/spring/openapi/client/generator/OpenApiClientGeneratorTest.java:
--------------------------------------------------------------------------------
1 | package org.spring.openapi.client.generator;
2 |
3 | import org.junit.Test;
4 |
5 | public class OpenApiClientGeneratorTest {
6 |
7 | @Test
8 | public void testClientGenerated() {
9 | new OpenApiClientGenerator().generateClient(
10 | "test.openapi",
11 | "src/test/resources/input_example.json",
12 | "target/openapi",
13 | true,
14 | true);
15 | }
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 4.0.0
4 |
5 | com.github.jrcodeza
6 | spring-openapi
7 | 1.4.11-SNAPSHOT
8 |
9 | pom
10 |
11 | Spring Open API
12 | OpenAPI 3 generator library to generate OpenAPI 3 json spec from Java + Spring annotated classes.
13 | https://github.com/jremenec/spring-openapi
14 |
15 |
16 |
17 | GNU General Public License v3.0
18 | https://www.gnu.org/licenses/gpl-3.0.en.html
19 | repo
20 | GNU General Public License v3.0
21 |
22 |
23 |
24 |
25 |
26 | jremenec
27 | Jakub Remenec
28 | remenec.jakub@gmail.com
29 | https://github.com/jremenec
30 |
31 |
32 |
33 |
34 | annotations
35 | schema-generator
36 | schema-v2-generator
37 | client-generator-plugin
38 | client-generator
39 | schema-generator-plugin
40 |
41 |
42 |
43 | 3.3.9
44 | 3.5.1
45 | 2.0.8
46 | 4.8.43
47 | 2.10.0.pr1
48 | 3.3.0
49 | 4.12
50 | 2.0.1.Final
51 | 2.9.9
52 | 2.6.1
53 | 1.5.0
54 | 2.6
55 | 5.1.9.RELEASE
56 | 1.11.1
57 | 4.4
58 | 1.6.0
59 | 3.9
60 | 2.4.0
61 | 1.7
62 | 1.7
63 |
64 |
65 |
66 |
67 |
68 | org.springframework
69 | spring-core
70 | ${spring-core.version}
71 |
72 |
73 | org.springframework
74 | spring-context
75 | ${spring-core.version}
76 |
77 |
78 | org.springframework
79 | spring-web
80 | ${spring-core.version}
81 |
82 |
83 |
84 | org.apache.commons
85 | commons-collections4
86 | ${commons-collections4.version}
87 |
88 |
89 |
90 | org.apache.maven.plugin-tools
91 | maven-plugin-annotations
92 | 3.5
93 | provided
94 |
95 |
96 | org.apache.maven
97 | maven-plugin-api
98 | ${maven.version}
99 |
100 |
101 | org.apache.maven
102 | maven-core
103 | ${maven.version}
104 |
105 |
106 | org.apache.maven
107 | maven-artifact
108 | ${maven.version}
109 | provided
110 |
111 |
112 | org.apache.maven
113 | maven-compat
114 | ${maven.version}
115 |
116 |
117 |
118 | io.swagger.core.v3
119 | swagger-models
120 | ${swagger-api.version}
121 |
122 |
123 | io.swagger.core.v3
124 | swagger-annotations
125 | ${swagger-api.version}
126 |
127 |
128 | io.swagger.core.v3
129 | swagger-integration
130 | ${swagger-api.version}
131 |
132 |
133 |
134 | io.swagger
135 | swagger-models
136 | ${swagger-v2-api.version}
137 |
138 |
139 | io.swagger
140 | swagger-annotations
141 | ${swagger-v2-api.version}
142 |
143 |
144 |
145 | com.squareup
146 | javapoet
147 | ${javapoet.version}
148 |
149 |
150 |
151 | com.fasterxml.jackson.core
152 | jackson-databind
153 | ${jackson-databind.version}
154 |
155 |
156 |
157 | javax.validation
158 | validation-api
159 | ${validation-api.version}
160 |
161 |
162 |
163 | com.fasterxml.jackson.core
164 | jackson-annotations
165 | ${jackson-annotations.version}
166 |
167 |
168 |
169 | org.apache.logging.log4j
170 | log4j-api
171 | ${log4j-api.version}
172 |
173 |
174 | org.apache.logging.log4j
175 | log4j-core
176 | ${log4j-api.version}
177 |
178 |
179 | org.apache.logging.log4j
180 | log4j-slf4j-impl
181 | ${log4j-api.version}
182 |
183 |
184 |
185 | org.apache.commons
186 | commons-lang3
187 | ${commons-lang3.version}
188 |
189 |
190 |
191 | com.jayway.jsonpath
192 | json-path
193 | ${json-path.version}
194 |
195 |
196 |
197 | org.skyscreamer
198 | jsonassert
199 | ${jsonassert.version}
200 | test
201 |
202 |
203 | commons-io
204 | commons-io
205 | ${commons-io.version}
206 | test
207 |
208 |
209 | org.apache.maven.plugin-testing
210 | maven-plugin-testing-harness
211 | ${maven-plugin-testing-harness.version}
212 | test
213 |
214 |
215 | junit
216 | junit
217 | ${junit.version}
218 | test
219 |
220 |
221 |
222 |
223 |
224 | scm:git:git://github.com/jremenec/spring-openapi
225 | scm:git:git@github.com:jremenec/spring-openapi.git
226 | https://github.com/jremenec/spring-openapi
227 | spring-openapi-1.4.9
228 |
229 |
230 |
231 |
232 | ossrh
233 | https://oss.sonatype.org/content/repositories/snapshots
234 |
235 |
236 | ossrh
237 | https://oss.sonatype.org/service/local/staging/deploy/maven2/
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 | release-sign-artifacts
246 |
247 |
248 | performRelease
249 | true
250 |
251 |
252 |
253 |
254 |
255 | maven-deploy-plugin
256 | 2.8.2
257 |
258 |
259 | default-deploy
260 | deploy
261 |
262 | deploy
263 |
264 |
265 |
266 |
267 |
268 | org.apache.maven.plugins
269 | maven-release-plugin
270 | 2.5.3
271 |
272 | true
273 | false
274 | forked-path
275 | -Dgpg.passphrase=${gpg.passphrase}
276 |
277 |
278 |
279 | org.apache.maven.scm
280 | maven-scm-provider-gitexe
281 | 1.9.5
282 |
283 |
284 |
285 |
286 | org.sonatype.plugins
287 | nexus-staging-maven-plugin
288 | 1.6.7
289 | true
290 |
291 | ossrh
292 | https://oss.sonatype.org/
293 | true
294 |
295 |
296 |
297 | org.apache.maven.plugins
298 | maven-source-plugin
299 | 3.0.1
300 |
301 |
302 | attach-sources
303 |
304 | jar
305 |
306 |
307 |
308 |
309 |
310 | org.apache.maven.plugins
311 | maven-gpg-plugin
312 | 1.6
313 |
314 |
315 | sign-artifacts
316 | verify
317 |
318 | sign
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 |
--------------------------------------------------------------------------------
/schema-generator-plugin/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 4.0.0
4 |
5 | com.github.jrcodeza
6 | spring-openapi
7 | 1.4.11-SNAPSHOT
8 |
9 |
10 | spring-openapi-schema-generator-plugin
11 |
12 | 1.4.11-SNAPSHOT
13 | maven-plugin
14 |
15 | Spring Open API - Schema generator plugin
16 |
17 |
18 |
19 | com.github.jrcodeza
20 | spring-openapi-schema-generator
21 | ${project.version}
22 |
23 |
24 |
25 | org.apache.maven.plugin-tools
26 | maven-plugin-annotations
27 | provided
28 |
29 |
30 | org.apache.maven
31 | maven-plugin-api
32 |
33 |
34 | org.apache.maven
35 | maven-core
36 |
37 |
38 | org.apache.maven
39 | maven-artifact
40 | provided
41 |
42 |
43 | org.apache.maven
44 | maven-compat
45 |
46 |
47 |
48 | com.fasterxml.jackson.core
49 | jackson-databind
50 |
51 |
52 | com.fasterxml.jackson.core
53 | jackson-annotations
54 |
55 |
56 |
57 | javax.validation
58 | validation-api
59 |
60 |
61 |
62 | io.swagger.core.v3
63 | swagger-annotations
64 | test
65 |
66 |
67 | javax.validation
68 | validation-api
69 | test
70 |
71 |
72 | org.apache.maven.plugin-testing
73 | maven-plugin-testing-harness
74 | test
75 |
76 |
77 | junit
78 | junit
79 | test
80 |
81 |
82 |
83 |
84 |
85 |
86 | org.apache.maven.plugins
87 | maven-compiler-plugin
88 |
89 | 8
90 | 8
91 |
92 |
93 |
94 | org.apache.maven.plugins
95 | maven-plugin-plugin
96 | 3.5
97 |
98 |
99 |
100 |
--------------------------------------------------------------------------------
/schema-generator-plugin/src/main/java/com/github/jrcodeza/schema/generator/plugin/GenerateOpenApiSchemaMojo.java:
--------------------------------------------------------------------------------
1 | package com.github.jrcodeza.schema.generator.plugin;
2 |
3 | import java.io.File;
4 | import java.io.IOException;
5 | import java.util.ArrayList;
6 | import java.util.List;
7 |
8 | import com.fasterxml.jackson.annotation.JsonInclude;
9 | import com.fasterxml.jackson.databind.ObjectMapper;
10 | import com.github.jrcodeza.schema.generator.OpenAPIGenerator;
11 | import com.github.jrcodeza.schema.generator.config.OpenApiGeneratorConfig;
12 | import com.github.jrcodeza.schema.generator.config.builder.OpenApiGeneratorConfigBuilder;
13 | import com.github.jrcodeza.schema.generator.filters.OperationFilter;
14 | import com.github.jrcodeza.schema.generator.filters.OperationParameterFilter;
15 | import com.github.jrcodeza.schema.generator.filters.SchemaFieldFilter;
16 | import com.github.jrcodeza.schema.generator.interceptors.OperationInterceptor;
17 | import com.github.jrcodeza.schema.generator.interceptors.OperationParameterInterceptor;
18 | import com.github.jrcodeza.schema.generator.interceptors.RequestBodyInterceptor;
19 | import com.github.jrcodeza.schema.generator.interceptors.SchemaFieldInterceptor;
20 | import com.github.jrcodeza.schema.generator.interceptors.SchemaInterceptor;
21 | import com.github.jrcodeza.schema.generator.interceptors.examples.OpenApiExampleResolver;
22 |
23 | import org.apache.commons.lang3.BooleanUtils;
24 | import org.apache.commons.lang3.StringUtils;
25 | import org.apache.maven.plugin.AbstractMojo;
26 | import org.apache.maven.plugins.annotations.LifecyclePhase;
27 | import org.apache.maven.plugins.annotations.Mojo;
28 | import org.apache.maven.plugins.annotations.Parameter;
29 |
30 | import io.swagger.v3.oas.models.OpenAPI;
31 | import io.swagger.v3.oas.models.info.Info;
32 |
33 | import static java.util.Arrays.asList;
34 |
35 | @Mojo(name = "generateOpenApi", defaultPhase = LifecyclePhase.INSTALL)
36 | public class GenerateOpenApiSchemaMojo extends AbstractMojo {
37 |
38 | @Parameter(required = true)
39 | private String title;
40 |
41 | @Parameter
42 | private String description;
43 |
44 | @Parameter(required = true)
45 | private String version;
46 |
47 | @Parameter(required = true)
48 | private String[] modelPackages;
49 |
50 | @Parameter(required = true)
51 | private String[] controllerBasePackages;
52 |
53 | @Parameter(required = true)
54 | private String outputDirectory;
55 |
56 | @Parameter
57 | private List schemaInterceptors;
58 |
59 | @Parameter
60 | private List schemaFieldInterceptors;
61 |
62 | @Parameter
63 | private List operationParameterInterceptors;
64 |
65 | @Parameter
66 | private List operationInterceptors;
67 |
68 | @Parameter
69 | private List requestBodyInterceptors;
70 |
71 | @Parameter
72 | private String operationFilter;
73 |
74 | @Parameter
75 | private String operationParameterFilter;
76 |
77 | @Parameter
78 | private String schemaFieldFilter;
79 |
80 | @Parameter
81 | private Boolean generateExamples;
82 |
83 | @Parameter
84 | private String openApiExamplesResolver;
85 |
86 | public void execute() {
87 | OpenAPIGenerator openApiGenerator = new OpenAPIGenerator(
88 | asList(modelPackages), asList(controllerBasePackages), createInfoFromParameters(),
89 | parseInputInterceptors(schemaInterceptors, SchemaInterceptor.class),
90 | parseInputInterceptors(schemaFieldInterceptors, SchemaFieldInterceptor.class),
91 | parseInputInterceptors(operationParameterInterceptors, OperationParameterInterceptor.class),
92 | parseInputInterceptors(operationInterceptors, OperationInterceptor.class),
93 | parseInputInterceptors(requestBodyInterceptors, RequestBodyInterceptor.class),
94 | parseInputFilter(operationFilter, OperationFilter.class),
95 | parseInputFilter(operationParameterFilter, OperationParameterFilter.class),
96 | parseInputFilter(schemaFieldFilter, SchemaFieldFilter.class)
97 | );
98 |
99 | OpenApiGeneratorConfig openApiGeneratorConfig = OpenApiGeneratorConfigBuilder.defaultConfig().build();
100 | if (BooleanUtils.isTrue(generateExamples)) {
101 | openApiGeneratorConfig.setGenerateExamples(true);
102 | if (StringUtils.isNotBlank(openApiExamplesResolver)) {
103 | openApiGeneratorConfig.setOpenApiExampleResolver(instantiateClass(openApiExamplesResolver, OpenApiExampleResolver.class));
104 | }
105 | }
106 | OpenAPI openAPI = openApiGenerator.generate(openApiGeneratorConfig);
107 |
108 | ObjectMapper objectMapper = new ObjectMapper();
109 | objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
110 |
111 | try {
112 | if (!new File(outputDirectory).mkdirs()) {
113 | getLog().error(String.format("Error creating directories for path [%s]", outputDirectory));
114 | return;
115 | }
116 | objectMapper.writeValue(new File(outputDirectory + "/swagger.json"), openAPI);
117 | } catch (IOException e) {
118 | getLog().error("Cannot serialize generated OpenAPI spec", e);
119 | }
120 | }
121 |
122 | private T parseInputFilter(String className, Class clazz) {
123 | if (StringUtils.isEmpty(className)) {
124 | return null;
125 | }
126 |
127 | return instantiateClass(className, clazz);
128 | }
129 |
130 | private List parseInputInterceptors(List classNames, Class clazz) {
131 | if (classNames == null || classNames.isEmpty()) {
132 | return new ArrayList<>();
133 | }
134 | List result = new ArrayList<>();
135 | for (String className : classNames) {
136 | result.add(instantiateClass(className, clazz));
137 | }
138 |
139 | return result;
140 | }
141 |
142 | private T instantiateClass(String className, Class superClass) {
143 | try {
144 | Class> inputClass = Class.forName(className);
145 | if (superClass.isAssignableFrom(inputClass)) {
146 | return (T) inputClass.newInstance();
147 | } else {
148 | getLog().error(String.format("Incorrect class type = [%s]. Expected is = [%s]", className, superClass.getName()));
149 | }
150 | } catch (IllegalAccessException | InstantiationException | ClassNotFoundException e) {
151 | getLog().error(e.getMessage());
152 | System.exit(1);
153 | }
154 | return null;
155 | }
156 |
157 | private Info createInfoFromParameters() {
158 | Info info = new Info();
159 | info.setTitle(title);
160 | info.setDescription(description);
161 | info.setVersion(version);
162 | return info;
163 | }
164 |
165 | }
166 |
--------------------------------------------------------------------------------
/schema-generator-plugin/src/test/java/com/github/jrcodeza/schema/generator/plugin/GenerateOpenApiSchemaMojoTest.java:
--------------------------------------------------------------------------------
1 | package com.github.jrcodeza.schema.generator.plugin;
2 |
3 | import java.io.File;
4 |
5 | import org.apache.maven.plugin.testing.AbstractMojoTestCase;
6 | import org.junit.Test;
7 | import com.github.jrcodeza.schema.generator.plugin.GenerateOpenApiSchemaMojo;
8 |
9 | public class GenerateOpenApiSchemaMojoTest extends AbstractMojoTestCase {
10 |
11 | @Override
12 | protected void setUp() throws Exception {
13 | super.setUp();
14 | }
15 |
16 | @Test
17 | public void testStandardScenario() throws Exception {
18 | File testPom = new File(getBasedir(), "src/test/resources/unit/generate-open-api-standard/pom.xml");
19 | GenerateOpenApiSchemaMojo mojo = (GenerateOpenApiSchemaMojo) lookupMojo("generateOpenApi", testPom);
20 | mojo.execute();
21 | }
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/schema-generator-plugin/src/test/java/com/github/jrcodeza/schema/generator/plugin/controller/MojoTestController.java:
--------------------------------------------------------------------------------
1 | package com.github.jrcodeza.schema.generator.plugin.controller;
2 |
3 | import com.github.jrcodeza.schema.generator.plugin.model.OpenApiTestModel;
4 | import org.springframework.web.bind.annotation.GetMapping;
5 | import org.springframework.web.bind.annotation.RequestMapping;
6 | import org.springframework.web.bind.annotation.RequestParam;
7 | import org.springframework.web.bind.annotation.RestController;
8 |
9 | @RestController
10 | @RequestMapping("mojoTest")
11 | public class MojoTestController {
12 |
13 | @GetMapping
14 | private OpenApiTestModel getMojoTest(@RequestParam String mojoTestParam) {
15 | return null;
16 | }
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/schema-generator-plugin/src/test/java/com/github/jrcodeza/schema/generator/plugin/example/TestExampleResolver.java:
--------------------------------------------------------------------------------
1 | package com.github.jrcodeza.schema.generator.plugin.example;
2 |
3 | import com.github.jrcodeza.schema.generator.interceptors.examples.OpenApiExampleResolver;
4 |
5 | public class TestExampleResolver implements OpenApiExampleResolver {
6 |
7 | @Override
8 | public String resolveExample(String exampleKey) {
9 | return "Resolved example";
10 | }
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/schema-generator-plugin/src/test/java/com/github/jrcodeza/schema/generator/plugin/interceptor/TestOperationInterceptor.java:
--------------------------------------------------------------------------------
1 | package com.github.jrcodeza.schema.generator.plugin.interceptor;
2 |
3 | import java.lang.reflect.Method;
4 |
5 | import com.github.jrcodeza.schema.generator.interceptors.OperationInterceptor;
6 |
7 | import io.swagger.v3.oas.models.Operation;
8 |
9 | public class TestOperationInterceptor implements OperationInterceptor {
10 |
11 | @Override
12 | public void intercept(Method method, Operation transformedOperation) {
13 | transformedOperation.setSummary("Interceptor summary");
14 | }
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/schema-generator-plugin/src/test/java/com/github/jrcodeza/schema/generator/plugin/interceptor/TestSchemaFieldInterceptor.java:
--------------------------------------------------------------------------------
1 | package com.github.jrcodeza.schema.generator.plugin.interceptor;
2 |
3 | import java.lang.reflect.Field;
4 |
5 | import com.github.jrcodeza.schema.generator.interceptors.SchemaFieldInterceptor;
6 |
7 | import io.swagger.v3.oas.models.media.Schema;
8 |
9 | public class TestSchemaFieldInterceptor implements SchemaFieldInterceptor {
10 |
11 | @Override
12 | public void intercept(Class> clazz, Field field, Schema> transformedFieldSchema) {
13 | transformedFieldSchema.setDescription("Schema field description");
14 | }
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/schema-generator-plugin/src/test/java/com/github/jrcodeza/schema/generator/plugin/model/OpenApiTestModel.java:
--------------------------------------------------------------------------------
1 | package com.github.jrcodeza.schema.generator.plugin.model;
2 |
3 | import com.github.jrcodeza.OpenApiExample;
4 |
5 | public class OpenApiTestModel {
6 |
7 | @OpenApiExample(name = "StandardExample", value = "standardExampleValue")
8 | private String attribute;
9 |
10 | @OpenApiExample(name = "KeyExample", key = "KEY")
11 | private String keyExample;
12 |
13 | public String getAttribute() {
14 | return attribute;
15 | }
16 |
17 | public void setAttribute(String attribute) {
18 | this.attribute = attribute;
19 | }
20 |
21 | public String getKeyExample() {
22 | return keyExample;
23 | }
24 |
25 | public void setKeyExample(String keyExample) {
26 | this.keyExample = keyExample;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/schema-generator-plugin/src/test/resources/unit/generate-open-api-standard/pom.xml:
--------------------------------------------------------------------------------
1 |
4 | 4.0.0
5 |
6 | org.apache.maven.plugin.my.unit
7 | generate-open-api-schema-mojo-test
8 | 1.0-SNAPSHOT
9 | jar
10 | Test GenerateOpenApiSchemaMojo Standard
11 |
12 |
13 |
14 | junit
15 | junit
16 | 3.8.1
17 | test
18 |
19 |
20 |
21 |
22 |
23 |
24 | com.github.jrcodeza
25 | spring-openapi-schema-generator-plugin
26 |
27 | Test title
28 | Test description
29 | 1.0.0-TEST
30 |
31 | com.github.jrcodeza.schema.generator.plugin.model.*
32 |
33 |
34 | com.github.jrcodeza.schema.generator.plugin.controller.*
35 |
36 | target/openapi
37 |
38 | com.github.jrcodeza.schema.generator.plugin.interceptor.TestSchemaFieldInterceptor
39 |
40 |
41 | com.github.jrcodeza.schema.generator.plugin.interceptor.TestOperationInterceptor
42 |
43 | true
44 | com.github.jrcodeza.schema.generator.plugin.example.TestExampleResolver
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/schema-generator/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 4.0.0
4 |
5 | com.github.jrcodeza
6 | spring-openapi
7 | 1.4.11-SNAPSHOT
8 |
9 |
10 | spring-openapi-schema-generator
11 |
12 | 1.4.11-SNAPSHOT
13 | jar
14 |
15 | Spring Open API - Schema generator
16 |
17 |
18 |
19 | com.github.jrcodeza
20 | spring-openapi-annotations
21 | ${project.version}
22 |
23 |
24 |
25 | org.springframework
26 | spring-core
27 |
28 |
29 | org.springframework
30 | spring-context
31 |
32 |
33 | org.springframework
34 | spring-web
35 |
36 |
37 |
38 | io.swagger.core.v3
39 | swagger-models
40 |
41 |
42 | io.swagger.core.v3
43 | swagger-annotations
44 |
45 |
46 | io.swagger.core.v3
47 | swagger-integration
48 |
49 |
50 |
51 | com.fasterxml.jackson.core
52 | jackson-databind
53 |
54 |
55 | com.fasterxml.jackson.core
56 | jackson-annotations
57 |
58 |
59 |
60 | javax.validation
61 | validation-api
62 |
63 |
64 |
65 | org.apache.logging.log4j
66 | log4j-api
67 |
68 |
69 | org.apache.logging.log4j
70 | log4j-core
71 |
72 |
73 | org.apache.logging.log4j
74 | log4j-slf4j-impl
75 |
76 |
77 |
78 | org.skyscreamer
79 | jsonassert
80 |
81 |
82 | commons-io
83 | commons-io
84 |
85 |
86 | junit
87 | junit
88 |
89 |
90 |
91 |
92 |
93 |
94 | org.apache.maven.plugins
95 | maven-compiler-plugin
96 |
97 | 8
98 | 8
99 |
100 |
101 |
102 |
103 |
104 |
105 |
--------------------------------------------------------------------------------
/schema-generator/src/main/java/com/github/jrcodeza/schema/generator/ComponentSchemaTransformer.java:
--------------------------------------------------------------------------------
1 | package com.github.jrcodeza.schema.generator;
2 |
3 | import com.github.jrcodeza.schema.generator.filters.SchemaFieldFilter;
4 | import com.github.jrcodeza.schema.generator.interceptors.SchemaFieldInterceptor;
5 | import com.github.jrcodeza.schema.generator.model.CustomComposedSchema;
6 | import com.github.jrcodeza.schema.generator.model.InheritanceInfo;
7 | import com.github.jrcodeza.schema.generator.util.SchemaGeneratorHelper;
8 | import io.swagger.v3.oas.models.media.Discriminator;
9 | import io.swagger.v3.oas.models.media.ObjectSchema;
10 | import io.swagger.v3.oas.models.media.Schema;
11 | import io.swagger.v3.oas.models.media.StringSchema;
12 | import org.apache.commons.lang3.StringUtils;
13 | import org.springframework.util.ReflectionUtils;
14 |
15 | import javax.validation.constraints.NotNull;
16 | import java.lang.annotation.Annotation;
17 | import java.lang.reflect.Field;
18 | import java.lang.reflect.ParameterizedType;
19 | import java.util.ArrayList;
20 | import java.util.Arrays;
21 | import java.util.HashMap;
22 | import java.util.List;
23 | import java.util.Map;
24 | import java.util.Optional;
25 | import java.util.concurrent.atomic.AtomicReference;
26 | import java.util.stream.Collectors;
27 | import java.util.stream.Stream;
28 |
29 | import static com.github.jrcodeza.schema.generator.util.CommonConstants.COMPONENT_REF_PREFIX;
30 | import static com.github.jrcodeza.schema.generator.util.GeneratorUtils.shouldBeIgnored;
31 |
32 |
33 | public class ComponentSchemaTransformer {
34 |
35 | private final List schemaFieldInterceptors;
36 | private AtomicReference schemaFieldFilter;
37 | private final SchemaGeneratorHelper schemaGeneratorHelper;
38 |
39 | public ComponentSchemaTransformer(List schemaFieldInterceptors,
40 | AtomicReference schemaFieldFilter,
41 | SchemaGeneratorHelper schemaGeneratorHelper) {
42 | this.schemaFieldInterceptors = schemaFieldInterceptors;
43 | this.schemaFieldFilter = schemaFieldFilter;
44 | this.schemaGeneratorHelper = schemaGeneratorHelper;
45 | }
46 |
47 | public Schema transformSimpleSchema(Class> clazz, Map inheritanceMap) {
48 | if (clazz.isEnum()) {
49 | return schemaGeneratorHelper.createEnumSchema(clazz.getEnumConstants());
50 | }
51 | List requiredFields = new ArrayList<>();
52 |
53 | Schema> schema = new Schema<>();
54 | schema.setType("object");
55 | schema.setProperties(getClassProperties(clazz, requiredFields));
56 | schemaGeneratorHelper.enrichWithTypeAnnotations(schema, clazz.getDeclaredAnnotations());
57 |
58 | updateRequiredFields(schema, requiredFields);
59 |
60 | if (inheritanceMap.containsKey(clazz.getName())) {
61 | Discriminator discriminator = createDiscriminator(inheritanceMap.get(clazz.getName()));
62 | schema.setDiscriminator(discriminator);
63 | enrichWithDiscriminatorProperty(schema, discriminator);
64 | }
65 | if (clazz.getSuperclass() != null) {
66 | return traverseAndAddProperties(schema, inheritanceMap, clazz.getSuperclass(), clazz);
67 | }
68 | return schema;
69 | }
70 |
71 | private Discriminator createDiscriminator(InheritanceInfo inheritanceInfo) {
72 | Map discriminatorTypeMapping = inheritanceInfo.getDiscriminatorClassMap().entrySet()
73 | .stream()
74 | .collect(Collectors.toMap(Map.Entry::getValue, Map.Entry::getKey));
75 |
76 | Discriminator discriminator = new Discriminator();
77 | discriminator.setPropertyName(inheritanceInfo.getDiscriminatorFieldName());
78 | discriminator.setMapping(discriminatorTypeMapping);
79 | return discriminator;
80 | }
81 |
82 | private void updateRequiredFields(Schema schema, List requiredFields) {
83 | if (requiredFields == null || requiredFields.isEmpty()) {
84 | return;
85 | }
86 | if (schema.getRequired() == null) {
87 | schema.setRequired(requiredFields);
88 | return;
89 | }
90 | schema.getRequired().addAll(requiredFields);
91 | }
92 |
93 | private void updateSchemaProperties(Schema schema, String propertyName, Schema propertyValue) {
94 | if (StringUtils.isBlank(propertyName) || propertyValue == null) {
95 | return;
96 | }
97 | if (schema.getProperties() == null) {
98 | schema.setProperties(new HashMap<>());
99 | }
100 | schema.getProperties().put(propertyName, propertyValue);
101 | }
102 |
103 | private void enrichWithDiscriminatorProperty(Schema schema, Discriminator discriminator) {
104 | if (schema != null && !schema.getProperties().containsKey(discriminator.getPropertyName())) {
105 | List discriminatorTypeRequiredProperty = new ArrayList<>();
106 | discriminatorTypeRequiredProperty.add(discriminator.getPropertyName());
107 |
108 | updateSchemaProperties(schema, discriminator.getPropertyName(), new StringSchema());
109 | updateRequiredFields(schema, discriminatorTypeRequiredProperty);
110 | }
111 | }
112 |
113 | private Schema> traverseAndAddProperties(Schema> schema, Map inheritanceMap, Class> superclass, Class> actualClass) {
114 | if (!schemaGeneratorHelper.isInPackagesToBeScanned(superclass)) {
115 | // adding properties from parent classes is present due to swagger ui bug, after using different ui
116 | // this becomes relevant only for third party packages
117 | List requiredFields = new ArrayList<>();
118 | schema.getProperties().putAll(getClassProperties(superclass, requiredFields));
119 | updateRequiredFields(schema, requiredFields);
120 | if (superclass.getSuperclass() != null && !"java.lang".equals(superclass.getSuperclass().getPackage().getName())) {
121 | return traverseAndAddProperties(schema, inheritanceMap, superclass.getSuperclass(), superclass);
122 | }
123 | return schema;
124 | } else {
125 | Schema> parentClassSchema = new Schema<>();
126 | parentClassSchema.set$ref(COMPONENT_REF_PREFIX + superclass.getSimpleName());
127 |
128 | CustomComposedSchema composedSchema = new CustomComposedSchema();
129 | enrichWithAdditionalProperties(composedSchema, inheritanceMap, superclass.getName(), actualClass.getSimpleName());
130 | composedSchema.setAllOf(Arrays.asList(parentClassSchema, schema));
131 | composedSchema.setDescription(schema.getDescription());
132 | return composedSchema;
133 | }
134 | }
135 |
136 | private void enrichWithAdditionalProperties(CustomComposedSchema customComposedSchema, Map inheritanceInfoMap, String superClassName,
137 | String actualClassName) {
138 | if (inheritanceInfoMap.containsKey(superClassName)) {
139 | Map discriminatorClassMap = inheritanceInfoMap.get(superClassName).getDiscriminatorClassMap();
140 | if (discriminatorClassMap.containsKey(actualClassName)) {
141 | customComposedSchema.setDiscriminatorValue(discriminatorClassMap.get(actualClassName));
142 | }
143 | }
144 | }
145 |
146 | private Map getClassProperties(Class> clazz, List requiredFields) {
147 | Map classPropertyMap = new HashMap<>();
148 | ReflectionUtils.doWithLocalFields(clazz,
149 | field -> getFieldSchema(clazz, field, requiredFields).ifPresent(schema -> {
150 | schemaFieldInterceptors.forEach(modelClassFieldInterceptor -> modelClassFieldInterceptor.intercept(clazz, field, schema));
151 | classPropertyMap.put(field.getName(), schema);
152 | })
153 | );
154 | return classPropertyMap;
155 | }
156 |
157 | private Optional getFieldSchema(Class> clazz, Field field, List requiredFields) {
158 | if (shouldIgnoreField(clazz, field)) {
159 | return Optional.empty();
160 | }
161 |
162 | Class> typeSignature = field.getType();
163 | Annotation[] annotations = field.getAnnotations();
164 | if (isRequired(annotations)) {
165 | requiredFields.add(field.getName());
166 | }
167 |
168 | if (typeSignature.isPrimitive()) {
169 | return createBaseTypeSchema(field, requiredFields, annotations);
170 | } else if (typeSignature.isArray()) {
171 | return createArrayTypeSchema(typeSignature, annotations);
172 | } else if (StringUtils.equalsIgnoreCase(typeSignature.getName(), "java.lang.Object")) {
173 | ObjectSchema objectSchema = new ObjectSchema();
174 | objectSchema.setName(field.getName());
175 | return Optional.of(objectSchema);
176 | } else if (typeSignature.isAssignableFrom(List.class)) {
177 | if (field.getGenericType() instanceof ParameterizedType) {
178 | Class> listGenericParameter = (Class>) ((ParameterizedType) field.getGenericType()).getActualTypeArguments()[0];
179 | return Optional.of(schemaGeneratorHelper.parseArraySignature(listGenericParameter, annotations));
180 | }
181 | return Optional.empty();
182 | } else {
183 | return createClassRefSchema(typeSignature, annotations);
184 | }
185 | }
186 |
187 | private boolean shouldIgnoreField(Class> clazz, Field field) {
188 | if (shouldBeIgnored(field)) {
189 | return true;
190 | }
191 |
192 | return schemaFieldFilter.get() != null && schemaFieldFilter.get().shouldIgnore(clazz, field);
193 | }
194 |
195 | private Optional createClassRefSchema(Class> typeClass, Annotation[] annotations) {
196 | Schema> schema = schemaGeneratorHelper.parseClassRefTypeSignature(typeClass, annotations);
197 | schemaGeneratorHelper.enrichWithTypeAnnotations(schema, annotations);
198 | return Optional.ofNullable(schema);
199 | }
200 |
201 | private Optional createArrayTypeSchema(Class> typeSignature, Annotation[] annotations) {
202 | Class> arrayComponentType = typeSignature.getComponentType();
203 | Schema> schema = schemaGeneratorHelper.parseArraySignature(arrayComponentType, annotations);
204 | schemaGeneratorHelper.enrichWithTypeAnnotations(schema, annotations);
205 | return Optional.ofNullable(schema);
206 | }
207 |
208 | private Optional createBaseTypeSchema(Field field, List requiredFields, Annotation[] annotations) {
209 | if (!requiredFields.contains(field.getName())) {
210 | requiredFields.add(field.getName());
211 | }
212 | Schema> schema = schemaGeneratorHelper.parseBaseTypeSignature(field.getType(), annotations);
213 | schemaGeneratorHelper.enrichWithTypeAnnotations(schema, annotations);
214 | return Optional.ofNullable(schema);
215 | }
216 |
217 | private boolean isRequired(Annotation[] annotations) {
218 | return Stream.of(annotations).anyMatch(annotation -> annotation instanceof NotNull);
219 | }
220 |
221 | }
222 |
--------------------------------------------------------------------------------
/schema-generator/src/main/java/com/github/jrcodeza/schema/generator/config/OpenApiGeneratorConfig.java:
--------------------------------------------------------------------------------
1 | package com.github.jrcodeza.schema.generator.config;
2 |
3 | import org.springframework.core.env.Environment;
4 |
5 | import com.github.jrcodeza.schema.generator.interceptors.examples.OpenApiExampleResolver;
6 |
7 | public class OpenApiGeneratorConfig {
8 |
9 | private boolean generateExamples;
10 |
11 | private OpenApiExampleResolver openApiExampleResolver;
12 |
13 | private Environment environment;
14 |
15 | public boolean isGenerateExamples() {
16 | return generateExamples;
17 | }
18 |
19 | public void setGenerateExamples(boolean generateExamples) {
20 | this.generateExamples = generateExamples;
21 | }
22 |
23 | public OpenApiExampleResolver getOpenApiExampleResolver() {
24 | return openApiExampleResolver;
25 | }
26 |
27 | public void setOpenApiExampleResolver(OpenApiExampleResolver openApiExampleResolver) {
28 | this.openApiExampleResolver = openApiExampleResolver;
29 | }
30 |
31 | public Environment getEnvironment() {
32 | return environment;
33 | }
34 |
35 | public void setEnvironment(Environment environment) {
36 | this.environment = environment;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/schema-generator/src/main/java/com/github/jrcodeza/schema/generator/config/builder/OpenApiGeneratorConfigBuilder.java:
--------------------------------------------------------------------------------
1 | package com.github.jrcodeza.schema.generator.config.builder;
2 |
3 | import org.springframework.core.env.Environment;
4 |
5 | import com.github.jrcodeza.schema.generator.config.OpenApiGeneratorConfig;
6 | import com.github.jrcodeza.schema.generator.interceptors.examples.OpenApiExampleResolver;
7 |
8 | public final class OpenApiGeneratorConfigBuilder {
9 |
10 | private OpenApiGeneratorConfig openApiGeneratorConfig;
11 |
12 | private OpenApiGeneratorConfigBuilder() {
13 | openApiGeneratorConfig = new OpenApiGeneratorConfig();
14 | }
15 |
16 | public static OpenApiGeneratorConfigBuilder defaultConfig() {
17 | return new OpenApiGeneratorConfigBuilder();
18 | }
19 |
20 | public OpenApiGeneratorConfigBuilder withGenerateExamples(boolean generateExamples) {
21 | openApiGeneratorConfig.setGenerateExamples(generateExamples);
22 | return this;
23 | }
24 |
25 | public OpenApiGeneratorConfigBuilder withOpenApiExampleResolver(OpenApiExampleResolver openApiExampleResolver) {
26 | openApiGeneratorConfig.setOpenApiExampleResolver(openApiExampleResolver);
27 | return this;
28 | }
29 |
30 | public OpenApiGeneratorConfigBuilder withEnvironment(Environment environment) {
31 | openApiGeneratorConfig.setEnvironment(environment);
32 | return this;
33 | }
34 |
35 | public OpenApiGeneratorConfig build() {
36 | return openApiGeneratorConfig;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/schema-generator/src/main/java/com/github/jrcodeza/schema/generator/filters/OperationFilter.java:
--------------------------------------------------------------------------------
1 | package com.github.jrcodeza.schema.generator.filters;
2 |
3 | import java.lang.reflect.Method;
4 |
5 | public interface OperationFilter {
6 | boolean shouldIgnore(Method method);
7 | }
8 |
--------------------------------------------------------------------------------
/schema-generator/src/main/java/com/github/jrcodeza/schema/generator/filters/OperationParameterFilter.java:
--------------------------------------------------------------------------------
1 | package com.github.jrcodeza.schema.generator.filters;
2 |
3 | import java.lang.reflect.Method;
4 | import java.lang.reflect.Parameter;
5 |
6 | public interface OperationParameterFilter {
7 | boolean shouldIgnore(Method method, Parameter parameter, String parameterName);
8 | }
9 |
--------------------------------------------------------------------------------
/schema-generator/src/main/java/com/github/jrcodeza/schema/generator/filters/SchemaFieldFilter.java:
--------------------------------------------------------------------------------
1 | package com.github.jrcodeza.schema.generator.filters;
2 |
3 | import java.lang.reflect.Field;
4 |
5 | public interface SchemaFieldFilter {
6 | boolean shouldIgnore(Class> clazz, Field field);
7 | }
8 |
--------------------------------------------------------------------------------
/schema-generator/src/main/java/com/github/jrcodeza/schema/generator/interceptors/OperationInterceptor.java:
--------------------------------------------------------------------------------
1 | package com.github.jrcodeza.schema.generator.interceptors;
2 |
3 | import java.lang.reflect.Method;
4 |
5 | import io.swagger.v3.oas.models.Operation;
6 |
7 | public interface OperationInterceptor {
8 |
9 | void intercept(Method method, Operation transformedOperation);
10 |
11 | }
12 |
--------------------------------------------------------------------------------
/schema-generator/src/main/java/com/github/jrcodeza/schema/generator/interceptors/OperationParameterInterceptor.java:
--------------------------------------------------------------------------------
1 | package com.github.jrcodeza.schema.generator.interceptors;
2 |
3 | import java.lang.reflect.Method;
4 | import java.lang.reflect.Parameter;
5 |
6 | public interface OperationParameterInterceptor {
7 |
8 | void intercept(Method method, Parameter parameter, String parameterName,
9 | io.swagger.v3.oas.models.parameters.Parameter transformedParameter);
10 |
11 | }
12 |
--------------------------------------------------------------------------------
/schema-generator/src/main/java/com/github/jrcodeza/schema/generator/interceptors/RequestBodyInterceptor.java:
--------------------------------------------------------------------------------
1 | package com.github.jrcodeza.schema.generator.interceptors;
2 |
3 | import java.lang.reflect.Method;
4 | import java.lang.reflect.Parameter;
5 |
6 | import io.swagger.v3.oas.models.parameters.RequestBody;
7 |
8 | public interface RequestBodyInterceptor {
9 |
10 | void intercept(Method method, Parameter parameter, String parameterName, RequestBody transformedRequestBody);
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/schema-generator/src/main/java/com/github/jrcodeza/schema/generator/interceptors/SchemaFieldInterceptor.java:
--------------------------------------------------------------------------------
1 | package com.github.jrcodeza.schema.generator.interceptors;
2 |
3 | import java.lang.reflect.Field;
4 |
5 | import io.swagger.v3.oas.models.media.Schema;
6 |
7 | public interface SchemaFieldInterceptor {
8 |
9 | void intercept(Class> clazz, Field field, Schema> transformedFieldSchema);
10 |
11 | }
12 |
--------------------------------------------------------------------------------
/schema-generator/src/main/java/com/github/jrcodeza/schema/generator/interceptors/SchemaInterceptor.java:
--------------------------------------------------------------------------------
1 | package com.github.jrcodeza.schema.generator.interceptors;
2 |
3 | import io.swagger.v3.oas.models.media.Schema;
4 |
5 | public interface SchemaInterceptor {
6 |
7 | void intercept(Class> clazz, Schema> transformedSchema);
8 |
9 | }
10 |
--------------------------------------------------------------------------------
/schema-generator/src/main/java/com/github/jrcodeza/schema/generator/interceptors/examples/OpenApiExampleResolver.java:
--------------------------------------------------------------------------------
1 | package com.github.jrcodeza.schema.generator.interceptors.examples;
2 |
3 | public interface OpenApiExampleResolver {
4 |
5 | String resolveExample(String exampleKey);
6 |
7 | }
8 |
--------------------------------------------------------------------------------
/schema-generator/src/main/java/com/github/jrcodeza/schema/generator/interceptors/examples/OperationParameterExampleInterceptor.java:
--------------------------------------------------------------------------------
1 | package com.github.jrcodeza.schema.generator.interceptors.examples;
2 |
3 | import java.lang.annotation.Annotation;
4 | import java.lang.reflect.AnnotatedElement;
5 | import java.lang.reflect.Field;
6 | import java.lang.reflect.Method;
7 | import java.lang.reflect.Parameter;
8 | import java.util.Arrays;
9 | import java.util.Collections;
10 | import java.util.HashMap;
11 | import java.util.LinkedHashMap;
12 | import java.util.List;
13 | import java.util.Map;
14 | import java.util.UUID;
15 | import java.util.function.Consumer;
16 | import java.util.stream.Stream;
17 |
18 | import com.github.jrcodeza.OpenApiExample;
19 | import com.github.jrcodeza.OpenApiExamples;
20 | import com.github.jrcodeza.schema.generator.interceptors.OperationParameterInterceptor;
21 | import com.github.jrcodeza.schema.generator.interceptors.RequestBodyInterceptor;
22 | import com.github.jrcodeza.schema.generator.interceptors.SchemaFieldInterceptor;
23 | import com.github.jrcodeza.schema.generator.interceptors.SchemaInterceptor;
24 |
25 | import org.apache.commons.lang3.StringUtils;
26 | import org.slf4j.Logger;
27 | import org.slf4j.LoggerFactory;
28 |
29 | import io.swagger.v3.oas.models.examples.Example;
30 | import io.swagger.v3.oas.models.media.MediaType;
31 | import io.swagger.v3.oas.models.media.Schema;
32 | import io.swagger.v3.oas.models.parameters.RequestBody;
33 |
34 | import static org.apache.commons.lang3.StringUtils.defaultString;
35 |
36 | public class OperationParameterExampleInterceptor implements OperationParameterInterceptor, RequestBodyInterceptor, SchemaFieldInterceptor, SchemaInterceptor {
37 |
38 | private static Logger logger = LoggerFactory.getLogger(OperationParameterExampleInterceptor.class);
39 |
40 | private final OpenApiExampleResolver openApiExampleResolver;
41 |
42 | public OperationParameterExampleInterceptor(OpenApiExampleResolver openApiExampleResolver) {
43 | this.openApiExampleResolver = openApiExampleResolver;
44 | }
45 |
46 | @Override
47 | public void intercept(Method method, Parameter parameter, String parameterName, io.swagger.v3.oas.models.parameters.Parameter transformedParameter) {
48 | updateExamples(parameter, transformedParameter::setExample, transformedParameter::setExamples);
49 | }
50 |
51 | @Override
52 | public void intercept(Method method, Parameter parameter, String parameterName, RequestBody transformedRequestBody) {
53 | MediaType requestBodyMediaType = getRequestBodyMediaType(transformedRequestBody);
54 | if (requestBodyMediaType == null) {
55 | return;
56 | }
57 | updateExamples(parameter, requestBodyMediaType::setExample, requestBodyMediaType::setExamples);
58 | }
59 |
60 | @Override
61 | public void intercept(Class> clazz, Field field, Schema> transformedFieldSchema) {
62 | updateExamples(field, transformedFieldSchema::setExample, stringExampleMap -> transformedFieldSchema.setExample(getFirstExampleFromMap(stringExampleMap)));
63 | }
64 |
65 | @Override
66 | public void intercept(Class> clazz, Schema> transformedSchema) {
67 | updateExamples(clazz, transformedSchema::setExample, stringExampleMap -> transformedSchema.setExample(getFirstExampleFromMap(stringExampleMap)));
68 | }
69 |
70 | private MediaType getRequestBodyMediaType(RequestBody transformedRequestBody) {
71 | if (transformedRequestBody == null || transformedRequestBody.getContent() == null || transformedRequestBody.getContent().isEmpty()) {
72 | return null;
73 | }
74 | Object object = ((LinkedHashMap) transformedRequestBody.getContent()).values().iterator().next();
75 | if (object instanceof MediaType) {
76 | return (MediaType) object;
77 | }
78 | return null;
79 | }
80 |
81 | private Example getFirstExampleFromMap(Map exampleMap) {
82 | if (exampleMap == null || exampleMap.isEmpty()) {
83 | return null;
84 | }
85 | return exampleMap.entrySet().iterator().next().getValue();
86 | }
87 |
88 | private void updateExamples(AnnotatedElement annotatedElement, Consumer exampleUpdater, Consumer