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