Skip to content

Commit 9af1a03

Browse files
author
Raman Kumar
committed
Implements Swagger request body ai helper
1 parent f8ef708 commit 9af1a03

23 files changed

Lines changed: 1456 additions & 2 deletions

.gitignore

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,14 @@
2323
hs_err_pid*
2424
replay_pid*
2525

26-
.idea
26+
.idea
27+
/ai-swagger-spring-boot-starter/.mvn/
28+
/ai-swagger-spring-boot-starter/mvnw
29+
/ai-swagger-spring-boot-starter/mvnw.cmd
30+
/ai-swagger-spring-boot-starter/target/
31+
/ai-swagger-helper-starter/mvnw
32+
/ai-swagger-helper-starter/target/
33+
/ai-swagger-helper-starter/.mvn/
34+
/ai-swagger-helper-starter/ai-swagger-helper-starter.iml
35+
/ai-swagger-helper-starter/mvnw.cmd
36+
/.idea/

.idea/compiler.xml

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/encodings.xml

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/misc.xml

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<module version="4">
3+
<component name="AdditionalModuleElements">
4+
<content url="file://$MODULE_DIR$/../ai-swagger-helper-starter" dumb="true">
5+
<excludeFolder url="file://$MODULE_DIR$/../ai-swagger-helper-starter/.mvn" />
6+
</content>
7+
</component>
8+
</module>

ai-swagger-helper-starter/pom.xml

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
5+
https://maven.apache.org/xsd/maven-4.0.0.xsd">
6+
7+
<modelVersion>4.0.0</modelVersion>
8+
9+
<parent>
10+
<groupId>io.github.rexrk</groupId>
11+
<artifactId>ai-dev-tools</artifactId>
12+
<version>0.0.1-SNAPSHOT</version>
13+
</parent>
14+
15+
<artifactId>ai-swagger-helper-starter</artifactId>
16+
<packaging>jar</packaging>
17+
18+
<name>AI Swagger Helper Starter</name>
19+
<description>AI-powered helper extensions for Swagger UI</description>
20+
21+
<dependencies>
22+
<!-- REQUIRED for any Spring Boot starter -->
23+
<dependency>
24+
<groupId>org.springframework.boot</groupId>
25+
<artifactId>spring-boot-autoconfigure</artifactId>
26+
</dependency>
27+
28+
<!-- Swagger UI (springdoc) -->
29+
<dependency>
30+
<groupId>org.springdoc</groupId>
31+
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
32+
<version>2.8.14</version>
33+
<optional>true</optional>
34+
</dependency>
35+
36+
<dependency>
37+
<groupId>org.springframework.boot</groupId>
38+
<artifactId>spring-boot-starter-webmvc</artifactId>
39+
<optional>true</optional>
40+
</dependency>
41+
42+
<dependency>
43+
<groupId>org.springframework.ai</groupId>
44+
<artifactId>spring-ai-openai</artifactId>
45+
</dependency>
46+
47+
<dependency>
48+
<groupId>com.fasterxml.jackson.core</groupId>
49+
<artifactId>jackson-databind</artifactId>
50+
</dependency>
51+
52+
</dependencies>
53+
54+
</project>
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
package io.github.rexrk;
2+
3+
import com.fasterxml.jackson.databind.ObjectMapper;
4+
import com.fasterxml.jackson.databind.json.JsonMapper;
5+
import io.github.rexrk.request.AiRequestBodyGeneratorService;
6+
import io.github.rexrk.request.AiSwaggerHelperController;
7+
import jakarta.annotation.PostConstruct;
8+
import org.apache.commons.lang3.BooleanUtils;
9+
import org.slf4j.Logger;
10+
import org.slf4j.LoggerFactory;
11+
import org.springdoc.core.properties.SwaggerUiConfigProperties;
12+
import org.springdoc.core.properties.SwaggerUiOAuthProperties;
13+
import org.springdoc.core.providers.ObjectMapperProvider;
14+
import org.springdoc.webmvc.ui.SwaggerIndexPageTransformer;
15+
import org.springdoc.webmvc.ui.SwaggerIndexTransformer;
16+
import org.springdoc.webmvc.ui.SwaggerWelcomeCommon;
17+
import org.springframework.ai.openai.OpenAiChatModel;
18+
import org.springframework.ai.openai.OpenAiChatOptions;
19+
import org.springframework.ai.openai.api.OpenAiApi;
20+
import org.springframework.beans.factory.ObjectProvider;
21+
import org.springframework.boot.autoconfigure.AutoConfiguration;
22+
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
23+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
24+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
25+
import org.springframework.boot.context.properties.EnableConfigurationProperties;
26+
import org.springframework.context.annotation.Bean;
27+
import org.springframework.core.ParameterizedTypeReference;
28+
import org.springframework.core.io.Resource;
29+
import org.springframework.http.MediaType;
30+
import org.springframework.web.servlet.function.RequestPredicates;
31+
import org.springframework.web.servlet.function.RouterFunction;
32+
import org.springframework.web.servlet.function.RouterFunctions;
33+
import org.springframework.web.servlet.function.ServerResponse;
34+
import org.springframework.web.servlet.resource.ResourceTransformerChain;
35+
import org.springframework.web.servlet.resource.TransformedResource;
36+
import jakarta.servlet.http.HttpServletRequest;
37+
import java.io.IOException;
38+
import java.nio.charset.StandardCharsets;
39+
import java.util.Map;
40+
41+
@AutoConfiguration
42+
@ConditionalOnClass({
43+
org.springdoc.webmvc.ui.SwaggerIndexTransformer.class,
44+
jakarta.servlet.http.HttpServletRequest.class,
45+
})
46+
@ConditionalOnProperty(
47+
prefix = "ai.swagger.helper",
48+
name = "enabled",
49+
havingValue = BooleanUtils.TRUE,
50+
matchIfMissing = true
51+
)
52+
@EnableConfigurationProperties(AiSwaggerHelperProperties.class)
53+
public class AiSwaggerHelperAutoConfiguration {
54+
55+
private static final Logger log = LoggerFactory.getLogger(AiSwaggerHelperAutoConfiguration.class);
56+
private final AiSwaggerHelperProperties properties;
57+
58+
public AiSwaggerHelperAutoConfiguration(AiSwaggerHelperProperties properties) {
59+
this.properties = properties;
60+
}
61+
62+
@Bean
63+
@ConditionalOnMissingBean(ObjectMapper.class)
64+
public ObjectMapper objectMapper() {
65+
return JsonMapper.builder()
66+
.findAndAddModules()
67+
.build();
68+
}
69+
70+
/**
71+
* 1️⃣ Spring-Ai configuration
72+
*/
73+
74+
@Bean
75+
@ConditionalOnMissingBean(OpenAiApi.class)
76+
@ConditionalOnProperty(prefix = "ai.swagger.helper", name = "api-key")
77+
public OpenAiApi openAiApi(AiSwaggerHelperProperties properties) {
78+
return OpenAiApi.builder()
79+
.apiKey(properties.getApiKey())
80+
.baseUrl(properties.getBaseUrl())
81+
.build();
82+
83+
}
84+
85+
@Bean
86+
@ConditionalOnMissingBean
87+
@ConditionalOnProperty(prefix = "ai.swagger.helper", name = "api-key")
88+
public OpenAiChatModel openAiChatModel(
89+
AiSwaggerHelperProperties properties,
90+
OpenAiApi openAiApi) {
91+
OpenAiChatOptions options = OpenAiChatOptions.builder()
92+
.model(properties.getModelName())
93+
.temperature(properties.getTemperature())
94+
.extraBody(properties.getOptions())
95+
.build();
96+
97+
return OpenAiChatModel.builder()
98+
.openAiApi(openAiApi)
99+
.defaultOptions(options)
100+
.build();
101+
}
102+
103+
/**
104+
* 2️⃣ Service
105+
*/
106+
107+
@Bean
108+
public AiRequestBodyGeneratorService aiRequestBodyGeneratorService(
109+
ObjectProvider<OpenAiChatModel> chatModel,
110+
ObjectMapper objectMapper
111+
) {
112+
return new AiRequestBodyGeneratorService(chatModel.getIfAvailable(), objectMapper);
113+
114+
}
115+
116+
/**
117+
* 3️⃣ Controller
118+
*/
119+
@Bean
120+
public AiSwaggerHelperController aiSwaggerHelperController(
121+
AiRequestBodyGeneratorService service,
122+
ObjectMapper objectMapper) {
123+
return new AiSwaggerHelperController(service, objectMapper);
124+
125+
}
126+
127+
@Bean
128+
public RouterFunction<ServerResponse> aiSwaggerRoutes(
129+
AiSwaggerHelperController controller) {
130+
131+
return RouterFunctions.route()
132+
.POST("/ai-swagger/generate",
133+
RequestPredicates.contentType(MediaType.APPLICATION_JSON),
134+
request -> {
135+
try {
136+
// Fetch request body
137+
Map<String, Object> body = request.body(new ParameterizedTypeReference<>() {});
138+
// Generate response for request body
139+
Map<String, Object> response = controller.generateBody(body);
140+
141+
return ServerResponse.ok()
142+
.contentType(MediaType.APPLICATION_JSON)
143+
.body(response);
144+
145+
} catch (IllegalArgumentException e) {
146+
return ServerResponse.badRequest()
147+
.contentType(MediaType.APPLICATION_JSON)
148+
.body(Map.of(
149+
"success", false,
150+
"error", e.getMessage()
151+
));
152+
153+
} catch (Exception e) {
154+
return ServerResponse.status(500)
155+
.contentType(MediaType.APPLICATION_JSON)
156+
.body(Map.of(
157+
"success", false,
158+
"error", e.getMessage()
159+
));
160+
}
161+
})
162+
.build();
163+
}
164+
165+
@Bean
166+
@ConditionalOnMissingBean(SwaggerIndexTransformer.class)
167+
public SwaggerIndexTransformer customSwaggerIndexTransformer(
168+
SwaggerUiConfigProperties swaggerUiConfig,
169+
SwaggerUiOAuthProperties swaggerUiOAuthProperties,
170+
SwaggerWelcomeCommon swaggerWelcomeCommon,
171+
ObjectMapperProvider objectMapperProvider) {
172+
173+
return new SwaggerIndexPageTransformer(swaggerUiConfig, swaggerUiOAuthProperties, swaggerWelcomeCommon, objectMapperProvider) {
174+
175+
@Override
176+
public Resource transform(HttpServletRequest request, Resource resource,
177+
ResourceTransformerChain chain) throws IOException {
178+
Resource transformed = super.transform(request, resource, chain);
179+
180+
// Inject into index.html
181+
if (transformed.getFilename() != null && transformed.getFilename().equals("index.html")) {
182+
String html = new String(transformed.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
183+
184+
// Inject plugin script BEFORE swagger-ui-bundle.js
185+
String pluginScript = "<script src=\"/ai-generate-body-plugin.js\"></script>\n";
186+
html = html.replace("<script src=\"./swagger-ui-bundle.js\"",
187+
pluginScript + " <script src=\"./swagger-ui-bundle.js\"");
188+
189+
byte[] bytes = html.getBytes(StandardCharsets.UTF_8);
190+
return new TransformedResource(transformed, bytes);
191+
}
192+
193+
// Modify swagger-initializer.js to register the plugin
194+
if (transformed.getFilename() != null && transformed.getFilename().equals("swagger-initializer.js")) {
195+
String js = new String(transformed.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
196+
197+
if (js.contains("plugins:")) {
198+
js = js.replaceFirst(
199+
"(plugins:\\s*\\[)",
200+
"$1\n window.AiGenerateBodyPlugin,"
201+
);
202+
} else {
203+
// If no plugins array exists, add it before layout
204+
js = js.replaceFirst(
205+
"(layout:)",
206+
"plugins: [window.AiGenerateBodyPlugin],\n $1"
207+
);
208+
}
209+
210+
byte[] bytes = js.getBytes(StandardCharsets.UTF_8);
211+
return new TransformedResource(transformed, bytes);
212+
}
213+
214+
return transformed;
215+
}
216+
};
217+
}
218+
219+
@PostConstruct
220+
void logProps() {
221+
if (properties.isEnabled()) log.info("AI Swagger Helper enabled");
222+
}
223+
}

0 commit comments

Comments
 (0)