Skip to content

Commit 2a59a5e

Browse files
committed
feat: add OpenAPI generation infrastructure
Add reusable components for automated OpenAPI spec generation: - @EnableOpenApiGen meta-annotation combining all required auto-configs - AutoMockMissingBeansConfig for mocking controller dependencies at build time - application-openapi-gen.yaml profile config - mockito-core as optional compile dependency
1 parent af7d23a commit 2a59a5e

5 files changed

Lines changed: 264 additions & 2 deletions

File tree

README.md

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,53 @@ firefly:
110110
log-body: false
111111
```
112112
113-
## Documentation
113+
## OpenAPI Generation Infrastructure
114+
115+
This module provides the reusable infrastructure for automated OpenAPI spec generation and SDK creation at build time. Microservices use these components to generate an OpenAPI spec from their controller annotations without loading any production dependencies.
116+
117+
### Components
118+
119+
| Component | Purpose |
120+
|-----------|---------|
121+
| `@EnableOpenApiGen` | Meta-annotation that combines `@SpringBootConfiguration`, `@EnableWebFlux`, and all required auto-configurations (Springdoc, WebFlux, Jackson) into a single annotation |
122+
| `AutoMockMissingBeansConfig` | `BeanDefinitionRegistryPostProcessor` that automatically creates Mockito mocks for any `@Autowired` dependency not present in the context. Only active under the `openapi-gen` Spring profile |
123+
| `application-openapi-gen.yaml` | Profile-specific config that enables Springdoc and allows bean definition overriding. Auto-discovered from the classpath by Spring Boot |
124+
125+
### How It Works
114126

115-
No additional documentation available for this project.
127+
1. A lightweight Spring Boot app (`OpenApiGenApplication`) starts during the Maven build using the test classpath
128+
2. `@EnableOpenApiGen` imports the minimal set of auto-configurations needed for Springdoc + WebFlux
129+
3. `AutoMockMissingBeansConfig` scans all `@RestController` beans and registers Mockito mocks for their `@Autowired` dependencies — this allows controllers to load without their real service implementations
130+
4. Springdoc reads the controller annotations and exposes the OpenAPI spec at `/v3/api-docs.yaml`
131+
5. The `springdoc-openapi-maven-plugin` fetches the spec and writes it to `target/openapi/openapi.yml`
132+
6. The SDK module uses `openapi-generator-maven-plugin` to generate typed API clients from the spec
133+
134+
### Usage in a Microservice
135+
136+
**1. Create `OpenApiGenApplication` in `src/test/java`:**
137+
138+
```java
139+
@EnableOpenApiGen
140+
@ComponentScan(basePackages = "com.firefly.your.module.web.controllers")
141+
public class OpenApiGenApplication {
142+
public static void main(String[] args) {
143+
SpringApplication.run(OpenApiGenApplication.class, args);
144+
}
145+
}
146+
```
147+
148+
**2. Add properties to the `-web` module's `pom.xml`:**
149+
150+
```xml
151+
<properties>
152+
<openapi.gen.skip>false</openapi.gen.skip>
153+
<openapi.gen.mainClass>com.firefly.your.module.web.openapi.OpenApiGenApplication</openapi.gen.mainClass>
154+
</properties>
155+
```
156+
157+
The Maven profile and plugin configuration are inherited from `firefly-parent`. See the [firefly-parent README](https://github.com/firefly-oss/firefly-parent) for details.
158+
159+
## Documentation
116160

117161
## Contributing
118162

pom.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,13 @@
4343
<artifactId>swagger-annotations</artifactId>
4444
</dependency>
4545

46+
<!-- Mockito (optional — only needed at compile time for AutoMockMissingBeansConfig) -->
47+
<dependency>
48+
<groupId>org.mockito</groupId>
49+
<artifactId>mockito-core</artifactId>
50+
<optional>true</optional>
51+
</dependency>
52+
4653
<!-- Utils -->
4754
<dependency>
4855
<groupId>org.projectlombok</groupId>
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/*
2+
* Copyright 2024-2026 Firefly Software Solutions Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.fireflyframework.web.openapi;
18+
19+
import org.mockito.Mockito;
20+
import org.slf4j.Logger;
21+
import org.slf4j.LoggerFactory;
22+
import org.springframework.beans.BeansException;
23+
import org.springframework.beans.factory.annotation.Autowired;
24+
import org.springframework.beans.factory.config.BeanDefinition;
25+
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
26+
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
27+
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
28+
import org.springframework.context.annotation.Configuration;
29+
import org.springframework.context.annotation.Profile;
30+
import org.springframework.web.bind.annotation.RestController;
31+
32+
import java.lang.reflect.Field;
33+
import java.util.LinkedHashMap;
34+
import java.util.Map;
35+
36+
/**
37+
* Automatically registers Mockito mock beans for every {@code @Autowired}
38+
* dependency of {@code @RestController} classes that is not already present
39+
* in the bean registry.
40+
*
41+
* <p>Mocks are registered as pre-existing singletons via
42+
* {@link ConfigurableListableBeanFactory#registerSingleton} so that Spring
43+
* does not attempt to process {@code @Autowired} annotations on the mock
44+
* objects themselves (which would fail for concrete service classes that
45+
* have their own unsatisfied dependencies).
46+
*
47+
* <p>Activated only under the {@code openapi-gen} Spring profile.
48+
*/
49+
@Configuration
50+
@Profile("openapi-gen")
51+
public class AutoMockMissingBeansConfig implements BeanDefinitionRegistryPostProcessor {
52+
53+
private static final Logger logger = LoggerFactory.getLogger(AutoMockMissingBeansConfig.class);
54+
55+
private final Map<String, Class<?>> mocksToRegister = new LinkedHashMap<>();
56+
57+
@Override
58+
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
59+
for (String beanName : registry.getBeanDefinitionNames()) {
60+
BeanDefinition bd = registry.getBeanDefinition(beanName);
61+
String beanClassName = bd.getBeanClassName();
62+
if (beanClassName == null) {
63+
continue;
64+
}
65+
66+
Class<?> beanClass;
67+
try {
68+
beanClass = Class.forName(beanClassName);
69+
} catch (ClassNotFoundException e) {
70+
continue;
71+
}
72+
73+
if (!beanClass.isAnnotationPresent(RestController.class)) {
74+
continue;
75+
}
76+
77+
for (Field field : beanClass.getDeclaredFields()) {
78+
if (!field.isAnnotationPresent(Autowired.class)) {
79+
continue;
80+
}
81+
82+
Class<?> fieldType = field.getType();
83+
String mockBeanName = "mock_" + fieldType.getSimpleName();
84+
85+
if (mocksToRegister.containsKey(fieldType.getName())) {
86+
continue;
87+
}
88+
89+
if (isBeanTypeRegistered(registry, fieldType)) {
90+
continue;
91+
}
92+
93+
logger.info("Will register Mockito mock for missing bean: {} (type: {})",
94+
mockBeanName, fieldType.getName());
95+
mocksToRegister.put(fieldType.getName(), fieldType);
96+
}
97+
}
98+
}
99+
100+
@Override
101+
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
102+
for (Map.Entry<String, Class<?>> entry : mocksToRegister.entrySet()) {
103+
Class<?> fieldType = entry.getValue();
104+
String mockBeanName = "mock_" + fieldType.getSimpleName();
105+
106+
logger.info("Registering Mockito mock singleton: {} (type: {})",
107+
mockBeanName, fieldType.getName());
108+
beanFactory.registerSingleton(mockBeanName, Mockito.mock(fieldType));
109+
}
110+
}
111+
112+
private boolean isBeanTypeRegistered(BeanDefinitionRegistry registry, Class<?> type) {
113+
for (String name : registry.getBeanDefinitionNames()) {
114+
BeanDefinition bd = registry.getBeanDefinition(name);
115+
String className = bd.getBeanClassName();
116+
if (className == null) {
117+
continue;
118+
}
119+
try {
120+
Class<?> clazz = Class.forName(className);
121+
if (type.isAssignableFrom(clazz)) {
122+
return true;
123+
}
124+
} catch (ClassNotFoundException e) {
125+
// Skip
126+
}
127+
}
128+
return false;
129+
}
130+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright 2024-2026 Firefly Software Solutions Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.fireflyframework.web.openapi;
18+
19+
import org.springdoc.core.configuration.SpringDocConfiguration;
20+
import org.springdoc.core.properties.SpringDocConfigProperties;
21+
import org.springdoc.webflux.core.configuration.SpringDocWebFluxConfiguration;
22+
import org.springframework.boot.SpringBootConfiguration;
23+
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
24+
import org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration;
25+
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
26+
import org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration;
27+
import org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryAutoConfiguration;
28+
import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration;
29+
import org.springframework.web.reactive.config.EnableWebFlux;
30+
31+
import java.lang.annotation.ElementType;
32+
import java.lang.annotation.Inherited;
33+
import java.lang.annotation.Retention;
34+
import java.lang.annotation.RetentionPolicy;
35+
import java.lang.annotation.Target;
36+
37+
/**
38+
* Meta-annotation that configures a minimal Spring Boot application for
39+
* OpenAPI spec generation. Combines {@code @SpringBootConfiguration},
40+
* {@code @EnableWebFlux}, and all required auto-configurations for Springdoc
41+
* and WebFlux into a single annotation.
42+
*
43+
* <p>Usage in each microservice's {@code src/test/java}:
44+
* <pre>{@code
45+
* @EnableOpenApiGen
46+
* @ComponentScan(basePackages = "com.example.web.controllers")
47+
* public class OpenApiGenApplication {
48+
* public static void main(String[] args) {
49+
* SpringApplication.run(OpenApiGenApplication.class, args);
50+
* }
51+
* }
52+
* }</pre>
53+
*
54+
* <p>The annotated class only needs to add {@code @ComponentScan} pointing at
55+
* its controller package — everything else is handled by this annotation.
56+
*/
57+
@Target(ElementType.TYPE)
58+
@Retention(RetentionPolicy.RUNTIME)
59+
@Inherited
60+
@SpringBootConfiguration
61+
@EnableWebFlux
62+
@ImportAutoConfiguration({
63+
AutoMockMissingBeansConfig.class,
64+
SpringDocConfiguration.class,
65+
SpringDocConfigProperties.class,
66+
SpringDocWebFluxConfiguration.class,
67+
ReactiveWebServerFactoryAutoConfiguration.class,
68+
HttpHandlerAutoConfiguration.class,
69+
WebFluxAutoConfiguration.class,
70+
JacksonAutoConfiguration.class,
71+
SpringApplicationAdminJmxAutoConfiguration.class
72+
})
73+
public @interface EnableOpenApiGen {
74+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
spring:
2+
main:
3+
allow-bean-definition-overriding: true
4+
springdoc:
5+
api-docs:
6+
enabled: true
7+
version: openapi_3_0

0 commit comments

Comments
 (0)