Skip to content

Commit 59339a9

Browse files
authored
[kotlin-spring] Add clientRegistrationId option for declarative HTTP interface (#25013)
Mirror the Java spring generator's clientRegistrationId option (#22726) for kotlin-spring with library=spring-declarative-http-interface. When set, the generated interfaces are annotated with @ClientRegistrationId and the spring-boot-starter-oauth2-client dependency is added to the Spring Boot 4 build files even if the spec declares no security schemes. The option requires useSpringBoot4=true and is rejected for other libraries, matching the Java generator. Output is unchanged when it is not set. Fixes #24936
1 parent a01a87b commit 59339a9

6 files changed

Lines changed: 97 additions & 1 deletion

File tree

‎docs/generators/kotlin-spring.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
2727
|autoXSpringPaginated|Automatically add x-spring-paginated to operations that have the given set of pagination query parameters. 'page-size-sort' requires 'page', 'size', and 'sort'; 'page-size' requires only 'page' and 'size' (sort may be absent). Operations with x-spring-paginated explicitly set to false will not be auto-detected. The legacy values 'true' (alias for 'page-size-sort') and 'false' (alias for 'none') are deprecated and will be removed in a future release.|<dl><dt>**none**</dt><dd>Disable auto-detection.</dd><dt>**page-size-sort**</dt><dd>Require 'page', 'size', and 'sort' query parameters.</dd><dt>**page-size**</dt><dd>Require only 'page' and 'size' query parameters.</dd><dt>**true**</dt><dd>(deprecated) alias for 'page-size-sort'.</dd><dt>**false**</dt><dd>(deprecated) alias for 'none'.</dd></dl>|none|
2828
|basePackage|base package (invokerPackage) for generated code| |org.openapitools|
2929
|beanQualifiers|Whether to add fully-qualifier class names as bean qualifiers in @Component and @RestController annotations. May be used to prevent bean names clash if multiple generated libraries (contexts) added to single project.| |false|
30+
|clientRegistrationId|Client registration ID for OAuth2 in Spring HTTP Interface (@ClientRegistrationId annotation). Requires library=spring-declarative-http-interface and useSpringBoot4=true (Spring Security 7).| |null|
3031
|companionObject|Whether to generate companion objects in data classes, enabling companion extensions.| |false|
3132
|configPackage|configuration package for generated code| |org.openapitools.configuration|
3233
|declarativeInterfaceReactiveMode|What type of reactive style to use in Spring Http declarative interface|<dl><dt>**coroutines**</dt><dd>Use kotlin-idiomatic 'suspend' functions</dd><dt>**reactor**</dt><dd>Use reactor return wrappers 'Mono' and 'Flux'</dd></dl>|coroutines|

‎modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ public class KotlinSpringServerCodegen extends AbstractKotlinCodegen
101101
public static final String BEAN_QUALIFIERS = "beanQualifiers";
102102
public static final String USE_RESPONSE_ENTITY = "useResponseEntity";
103103
public static final String DECLARATIVE_INTERFACE_REACTIVE_MODE = "declarativeInterfaceReactiveMode";
104+
public static final String CLIENT_REGISTRATION_ID = "clientRegistrationId";
104105

105106
public static final String USE_SPRING_BOOT3 = "useSpringBoot3";
106107
public static final String USE_SPRING_BOOT4 = "useSpringBoot4";
@@ -223,6 +224,8 @@ public void setAutoXSpringPaginated(boolean autoXSpringPaginated) {
223224
protected boolean useSpringBoot4 = false;
224225
@Getter @Setter
225226
protected boolean useSpringBuiltInValidation = false;
227+
@Getter @Setter
228+
protected String clientRegistrationId = null;
226229
@Setter
227230
@Getter
228231
protected RequestMappingMode requestMappingMode = RequestMappingMode.controller;
@@ -426,6 +429,8 @@ public KotlinSpringServerCodegen() {
426429
}
427430
cliOptions.add(declarativeInterfaceReactiveModeOpt);
428431

432+
cliOptions.add(CliOption.newString(CLIENT_REGISTRATION_ID, "Client registration ID for OAuth2 in Spring HTTP Interface (@ClientRegistrationId annotation). Requires library=" + SPRING_DECLARATIVE_HTTP_INTERFACE_LIBRARY + " and useSpringBoot4=true (Spring Security 7)."));
433+
429434
if (null != defaultDocumentationProvider()) {
430435
CliOption documentationProviderCliOption = new CliOption(DOCUMENTATION_PROVIDER,
431436
"Select the OpenAPI documentation provider.")
@@ -601,6 +606,9 @@ public void processOpts() {
601606
if (additionalProperties.containsKey(USE_SPRING_BOOT4)) {
602607
this.setUseSpringBoot4(convertPropertyToBoolean(USE_SPRING_BOOT4));
603608
}
609+
if (additionalProperties.containsKey(CLIENT_REGISTRATION_ID)) {
610+
this.setClientRegistrationId(additionalProperties.get(CLIENT_REGISTRATION_ID).toString());
611+
}
604612
if (additionalProperties.containsKey(USE_SPRING_BUILT_IN_VALIDATION)) {
605613
this.setUseSpringBuiltInValidation(convertPropertyToBoolean(USE_SPRING_BUILT_IN_VALIDATION));
606614
writePropertyBack(USE_SPRING_BUILT_IN_VALIDATION, useSpringBuiltInValidation);
@@ -805,6 +813,17 @@ public void processOpts() {
805813
writePropertyBack(USE_FEIGN_CLIENT, useFeignClient);
806814
writePropertyBack(SKIP_DEFAULT_INTERFACE, skipDefaultInterface);
807815
}
816+
if (clientRegistrationId != null && !clientRegistrationId.isEmpty()) {
817+
if (!SPRING_DECLARATIVE_HTTP_INTERFACE_LIBRARY.equals(library)) {
818+
throw new IllegalArgumentException(CLIENT_REGISTRATION_ID + " is only supported with the " + SPRING_DECLARATIVE_HTTP_INTERFACE_LIBRARY + " library");
819+
}
820+
if (!isUseSpringBoot4()) {
821+
throw new IllegalArgumentException(CLIENT_REGISTRATION_ID + " requires " + USE_SPRING_BOOT4 + "=true because @ClientRegistrationId is provided by Spring Security 7");
822+
}
823+
additionalProperties.put(CLIENT_REGISTRATION_ID, clientRegistrationId);
824+
} else {
825+
additionalProperties.remove(CLIENT_REGISTRATION_ID);
826+
}
808827
writePropertyBack(REACTIVE, reactive);
809828
writePropertyBack(REACTIVE_MULTIPART, reactive && SPRING_BOOT.equals(library));
810829
writePropertyBack(EXCEPTION_HANDLER, exceptionHandler);

‎modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-declarative-http-interface/apiInterface.mustache‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ import io.swagger.annotations.AuthorizationScope
2929

3030
import org.springframework.web.service.annotation.*
3131
import org.springframework.web.bind.annotation.*
32+
{{#clientRegistrationId}}
33+
import org.springframework.security.oauth2.client.annotation.ClientRegistrationId
34+
{{/clientRegistrationId}}
3235
{{^useResponseEntity}}
3336
import org.springframework.http.HttpStatus
3437
{{/useResponseEntity}}
@@ -55,6 +58,9 @@ import kotlin.collections.Map
5558
{{#useBeanValidation}}
5659
@Validated
5760
{{/useBeanValidation}}
61+
{{#clientRegistrationId}}
62+
@ClientRegistrationId("{{{clientRegistrationId}}}")
63+
{{/clientRegistrationId}}
5864
{{#operations}}
5965
interface {{classname}} {
6066

‎modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-declarative-http-interface/buildGradle-sb4-Kts.mustache‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ dependencies {
6767
{{/useJackson3}}
6868

6969
{{#hasAuthMethods}}
70-
implementation("org.springframework.boot:spring-boot-starter-oauth2-client"){{/hasAuthMethods}}
70+
implementation("org.springframework.boot:spring-boot-starter-oauth2-client"){{/hasAuthMethods}}{{^hasAuthMethods}}{{#clientRegistrationId}}
71+
implementation("org.springframework.boot:spring-boot-starter-oauth2-client"){{/clientRegistrationId}}{{/hasAuthMethods}}
7172

7273
{{#useBeanValidation}}
7374
implementation("jakarta.validation:jakarta.validation-api"){{/useBeanValidation}}

‎modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-declarative-http-interface/pom-sb4.mustache‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,14 @@
180180
<artifactId>spring-boot-starter-oauth2-client</artifactId>
181181
</dependency>
182182
{{/hasAuthMethods}}
183+
{{^hasAuthMethods}}
184+
{{#clientRegistrationId}}
185+
<dependency>
186+
<groupId>org.springframework.boot</groupId>
187+
<artifactId>spring-boot-starter-oauth2-client</artifactId>
188+
</dependency>
189+
{{/clientRegistrationId}}
190+
{{/hasAuthMethods}}
183191
{{#useJackson3}}
184192
<dependency>
185193
<groupId>{{jacksonPackage}}.dataformat</groupId>

‎modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1510,6 +1510,67 @@ public void generateHttpInterfaceRendersOperationExtraAnnotation() throws Except
15101510
);
15111511
}
15121512

1513+
@Test
1514+
public void generateHttpInterfaceWithClientRegistrationId() throws Exception {
1515+
Map<String, Object> props = new HashMap<>();
1516+
props.put(USE_SPRING_BOOT4, "true");
1517+
props.put(CLIENT_REGISTRATION_ID, "my-oauth-client");
1518+
1519+
Map<String, File> files = generateFromContract("src/test/resources/3_0/kotlin/petstore.yaml", props, new HashMap<>(),
1520+
configurator -> configurator.setLibrary(SPRING_DECLARATIVE_HTTP_INTERFACE_LIBRARY));
1521+
1522+
Path petApi = files.get("PetApi.kt").toPath();
1523+
assertFileContains(petApi,
1524+
"import org.springframework.security.oauth2.client.annotation.ClientRegistrationId",
1525+
"@ClientRegistrationId(\"my-oauth-client\")\ninterface PetApi {");
1526+
}
1527+
1528+
@Test
1529+
public void generateHttpInterfaceWithClientRegistrationIdAddsOAuth2ClientDependencyWithoutAuthMethods() throws Exception {
1530+
Map<String, Object> props = new HashMap<>();
1531+
props.put(USE_SPRING_BOOT4, "true");
1532+
props.put(CLIENT_REGISTRATION_ID, "my-oauth-client");
1533+
1534+
Map<String, File> files = generateFromContract("src/test/resources/3_0/kotlin/bean-qualifiers.yaml", props, new HashMap<>(),
1535+
configurator -> configurator.setLibrary(SPRING_DECLARATIVE_HTTP_INTERFACE_LIBRARY));
1536+
1537+
assertFileContains(files.get("build.gradle.kts").toPath(),
1538+
"implementation(\"org.springframework.boot:spring-boot-starter-oauth2-client\")");
1539+
}
1540+
1541+
@Test
1542+
public void generateHttpInterfaceWithoutClientRegistrationId() throws Exception {
1543+
Map<String, Object> props = new HashMap<>();
1544+
props.put(USE_SPRING_BOOT4, "true");
1545+
1546+
Map<String, File> files = generateFromContract("src/test/resources/3_0/kotlin/petstore.yaml", props, new HashMap<>(),
1547+
configurator -> configurator.setLibrary(SPRING_DECLARATIVE_HTTP_INTERFACE_LIBRARY));
1548+
1549+
assertFileNotContains(files.get("PetApi.kt").toPath(), "ClientRegistrationId");
1550+
}
1551+
1552+
@Test
1553+
public void shouldRefuseClientRegistrationIdWithoutSpringBoot4() {
1554+
Map<String, Object> props = new HashMap<>();
1555+
props.put(CLIENT_REGISTRATION_ID, "my-oauth-client");
1556+
1557+
Assertions.assertThatExceptionOfType(IllegalArgumentException.class)
1558+
.isThrownBy(() -> generateFromContract("src/test/resources/3_0/kotlin/petstore.yaml", props, new HashMap<>(),
1559+
configurator -> configurator.setLibrary(SPRING_DECLARATIVE_HTTP_INTERFACE_LIBRARY)))
1560+
.withMessageContaining(USE_SPRING_BOOT4);
1561+
}
1562+
1563+
@Test
1564+
public void shouldRefuseClientRegistrationIdOutsideDeclarativeHttpInterface() {
1565+
Map<String, Object> props = new HashMap<>();
1566+
props.put(USE_SPRING_BOOT4, "true");
1567+
props.put(CLIENT_REGISTRATION_ID, "my-oauth-client");
1568+
1569+
Assertions.assertThatExceptionOfType(IllegalArgumentException.class)
1570+
.isThrownBy(() -> generateFromContract("src/test/resources/3_0/kotlin/petstore.yaml", props))
1571+
.withMessageContaining(SPRING_DECLARATIVE_HTTP_INTERFACE_LIBRARY);
1572+
}
1573+
15131574
@Test
15141575
public void generateHttpInterfaceReactiveWithCoroutinesResponseEntity() throws Exception {
15151576
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();

0 commit comments

Comments
 (0)