Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
<module>springdoc-openapi-starter-common-mcp</module>
<module>springdoc-openapi-starter-webmvc-mcp</module>
<module>springdoc-openapi-starter-webflux-mcp</module>
<module>springdoc-openapi-generator-worker</module>
<module>springdoc-openapi-bom</module>
<module>springdoc-openapi-tests</module>
</modules>
Expand Down
5 changes: 5 additions & 0 deletions springdoc-openapi-bom/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@
<artifactId>springdoc-openapi-starter-webflux-mcp</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.github.vpelikh</groupId>
<artifactId>springdoc-openapi-generator-worker</artifactId>
<version>${project.version}</version>
</dependency>
<!-- swagger dependencies, so that modules holding only the annotations stay in lockstep -->
<dependency>
<groupId>io.swagger.core.v3</groupId>
Expand Down
144 changes: 144 additions & 0 deletions springdoc-openapi-generator-worker/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
######################
# Project Specific
######################
/target/www/**
/src/test/javascript/coverage/

######################
# Node
######################
/node/
node_tmp/
node_modules/
npm-debug.log.*
/.awcache/*
/.cache-loader/*

######################
# SASS
######################
.sass-cache/

######################
# Eclipse
######################
*.pydevproject
.project
.metadata
tmp/
tmp/**/*
*.tmp
*.bak
*.swp
*~.nib
local.properties
.classpath
.settings/
.loadpath
.factorypath
/src/main/resources/rebel.xml

# External tool builders
.externalToolBuilders/**

# Locally stored "Eclipse launch configurations"
*.launch

# CDT-specific
.cproject

# PDT-specific
.buildpath

######################
# Intellij
######################
.idea/
*.iml
*.iws
*.ipr
*.ids
*.orig
classes/
out/

######################
# Visual Studio Code
######################
.vscode/

######################
# Maven
######################
/log/
/target/

######################
# Gradle
######################
.gradle/
/build/

######################
# Package Files
######################
*.jar
*.war
*.ear
*.db

######################
# Windows
######################
# Windows image file caches
Thumbs.db

# Folder config file
Desktop.ini

######################
# Mac OSX
######################
.DS_Store
.svn

# Thumbnails
._*

# Files that might appear on external disk
.Spotlight-V100
.Trashes

######################
# Directories
######################
/bin/
/deploy/

######################
# Logs
######################
*.log*

######################
# Others
######################
*.class
*.*~
*~
.merge_file*

######################
# Gradle Wrapper
######################
!gradle/wrapper/gradle-wrapper.jar

######################
# Maven Wrapper
######################
!.mvn/wrapper/maven-wrapper.jar

######################
# ESLint
######################
.eslintcache
53 changes: 53 additions & 0 deletions springdoc-openapi-generator-worker/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi</artifactId>
<version>3.1.2-SNAPSHOT</version>
</parent>
<artifactId>springdoc-openapi-generator-worker</artifactId>
<name>${project.artifactId}</name>
<description>Shared forked-JVM worker that boots a Spring Boot reactive or servlet context, runs springdoc-openapi, and writes the OpenAPI document. Used by the Gradle and Maven generator plugins.</description>

<dependencies>
<!-- Thin jar: no classes are bundled. It reuses the target app's own spring/springdoc
classes at runtime, so it must NOT force either web stack onto the fork classpath
(that would flip Spring Boot's deduced web application type). Both springdoc stack
APIs are therefore compile-only; the worker invokes the resource via reflection. -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webflux-api</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-api</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-web-server</artifactId>
<scope>provided</scope>
</dependency>
<!-- Compile-time servlet API for the WebMvc branch. -->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<!-- Runtime: the worker builds mock requests / servlet contexts with spring-test. This is
the only runtime-transitive dependency; it does not influence the web application
type. -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package org.springdoc.generator;

/**
* Entry point for the generator worker, run in a forked JVM by the Gradle and Maven plugins.
* <p>
* It detects the target application's web stack from the fork classpath (the app's own
* dependencies are on it) and delegates to the matching worker:
* <ul>
* <li>WebMvc (servlet)</li>
* <li>WebFlux (reactive)</li>
* </ul>
* The two worker classes are only loaded when selected. Because the JVM resolves constant-pool
* references lazily, the non-selected worker is never loaded on a fork that lacks that stack, so
* this works on WebFlux-only and WebMvc-only classpaths alike.
* <p>
* When both stacks are on the classpath (a mixed application), WebMvc (servlet) wins, matching
* {@code SpringApplication}'s {@code WebApplicationType.deduceFromClasspath}.
* <p>
* Arguments: {@code <mainClass> <outputDir> [outputFileName] [format]}
*/
public final class GeneratorWorkerMain {

private GeneratorWorkerMain() {
}

public static void main(String[] args) throws Exception {
if (args.length < 2) {
throw new IllegalArgumentException(
"Usage: GeneratorWorkerMain <mainClass> <outputDir> [outputFileName] [format]");
}
// Spring Boot prefers servlet when both stacks are present, so check WebMvc first.
if (isOnClasspath("org.springframework.web.servlet.DispatcherServlet")) {
GeneratorWorkerWebMvc.main(args);
}
else if (isOnClasspath("org.springframework.web.reactive.DispatcherHandler")) {
GeneratorWorkerWebFlux.main(args);
}
else {
throw new IllegalStateException(
"Could not detect a WebMvc or WebFlux stack on the application classpath.");
}
}

private static boolean isOnClasspath(String className) {
try {
Class.forName(className, false, GeneratorWorkerMain.class.getClassLoader());
return true;
}
catch (ClassNotFoundException e) {
return false;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package org.springdoc.generator;

import org.springdoc.webflux.api.OpenApiWebfluxResource;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.web.server.reactive.ReactiveWebServerFactory;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;

import java.nio.file.Path;
import java.util.Locale;
import java.util.Map;

/**
* Workers that boot a <b>WebFlux (reactive)</b> Spring Boot application, let springdoc-openapi
* build the OpenAPI document, write it to disk, and shut the context down. Runs in a forked JVM.
* <p>
* A no-op {@link ReactiveWebServerFactory} is registered so springdoc's
* {@code @ConditionalOnWebApplication} activates without ever binding a port.
*/
public class GeneratorWorkerWebFlux {

@Configuration
static class NoServerConfiguration {

@Bean
@ConditionalOnMissingBean(ReactiveWebServerFactory.class)
ReactiveWebServerFactory reactiveWebServerFactory() {
return new NoOpReactiveWebServerFactory();
}
}

public static void main(String[] args) throws Exception {
if (args.length < 2) {
throw new IllegalArgumentException(
"Usage: GeneratorWorkerWebFlux <mainClass> <outputDir> [outputFileName] [format]");
}
String mainClass = args[0];
String outputDir = args[1];
String outputFileName = args.length > 2 ? args[2] : "openapi";
String format = args.length > 3 ? args[3] : "json";
validateFormat(format);
new GeneratorWorkerWebFlux().generate(mainClass, outputDir, outputFileName, format);
}

public void generate(String mainClass, String outputDir, String outputFileName, String format) throws Exception {
SpringApplication app = new SpringApplication(Class.forName(mainClass));
app.setWebApplicationType(WebApplicationType.REACTIVE);
app.addPrimarySources(java.util.List.of(NoServerConfiguration.class));
app.setDefaultProperties(Map.of("spring.main.banner-mode", "off"));

try (ConfigurableApplicationContext context = app.run()) {
OpenApiWebfluxResource resource = context.getBean(OpenApiWebfluxResource.class);
ServerHttpRequest request = MockServerHttpRequest.get("http://localhost/v3/api-docs").build();
String lower = format.toLowerCase(Locale.ROOT);
byte[] bytes;
if (isYaml(lower)) {
bytes = resource.openapiYaml(request, "/v3/api-docs", Locale.ENGLISH).block();
} else {
bytes = resource.openapiJson(request, "/v3/api-docs", Locale.ENGLISH).block();
}
if (bytes == null || bytes.length == 0) {
throw new IllegalStateException("OpenAPI generation returned no content");
}
String ext = isYaml(lower) ? "yaml" : "json";
Path out = Path.of(outputDir).resolve(outputFileName + "." + ext);
WriteUtils.writeAtomic(out, bytes);
System.out.println("Generated OpenAPI spec at " + out.toAbsolutePath());
}
}

private static boolean isYaml(String lower) {
return "yaml".equals(lower) || "yml".equals(lower);
}

/**
* Validates a user-supplied {@code format} argument. Only {@code json}, {@code yaml},
* {@code yml} are supported; anything else is rejected rather than silently falling back
* to JSON output (which would produce a document in a format the user did not ask for).
*/
private static void validateFormat(String format) {
String lower = format.toLowerCase(Locale.ROOT);
boolean valid = "json".equals(lower) || "yaml".equals(lower) || "yml".equals(lower);
if (!valid) {
throw new IllegalArgumentException(
"Unsupported format '" + format + "'. Supported formats: json, yaml, yml.");
}
}
}
Loading