Skip to content

Commit 9278f28

Browse files
authored
Add support for custom OperationIdStrategy implementations (#2346)
Signed-off-by: Michael Edgar <michael@xlate.io>
1 parent ca433b7 commit 9278f28

File tree

10 files changed

+208
-32
lines changed

10 files changed

+208
-32
lines changed

README.adoc

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,15 @@ Set this boolean value to enable or disable the sorting of parameter array entri
111111
mp.openapi.extensions.smallrye.generic-response-use-default
112112
----
113113
Set this boolean value to enable the automatic use of the https://spec.openapis.org/oas/v3.1.0.html#responses-object[`default` response code] for framework response types (e.g. Jakarta REST's `Response` type) when no `@APIResponse` annotations have been used and the HTTP response code for an operation cannot be determined. When unset or `false`, the response code will be set to `200`.
114+
115+
* Operation ID Strategy
116+
+
117+
[source%nowrap]
118+
----
119+
mp.openapi.extensions.smallrye.operationIdStrategy
120+
----
121+
Specify a strategy to be used globally for generating operation IDs when not specified via an annotation or static OpenAPI input. If not set, operation IDs will not be generated. Set to one of the following:
122+
** `METHOD` (may result in duplicate operation IDs in an application where REST endpoint method names are not unique)
123+
** `CLASS_METHOD` (may result in duplicate operation IDs in an application where REST endpoint simple class name + method names are not unique)
124+
** `PACKAGE_CLASS_METHOD`
125+
** A fully-qualified class name of an implementation of `io.smallrye.openapi.api.OperationIdGenerator` (as of 4.1.2, https://javadoc.io/doc/io.smallrye/smallrye-open-api-core/latest/io/smallrye/openapi/api/OperationIdGenerator.html[see JavaDoc])

core/src/main/java/io/smallrye/openapi/api/ApiMessages.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package io.smallrye.openapi.api;
22

33
import org.jboss.logging.Messages;
4+
import org.jboss.logging.annotations.Cause;
45
import org.jboss.logging.annotations.Message;
56
import org.jboss.logging.annotations.MessageBundle;
67

8+
import io.smallrye.openapi.runtime.OpenApiRuntimeException;
9+
710
@MessageBundle(projectCode = "SROAP", length = 5)
811
interface ApiMessages {
912
ApiMessages msg = Messages.getBundle(ApiMessages.class);
@@ -16,4 +19,7 @@ interface ApiMessages {
1619

1720
@Message(id = 2, value = "OpenApiConfig must be set before init")
1821
IllegalStateException configMustBeSet();
22+
23+
@Message(id = 3, value = "Exception accessing operationIdStrategy: %s")
24+
OpenApiRuntimeException invalidOperationIdStrategyWithCause(String strategyName, @Cause Throwable cause);
1925
}

core/src/main/java/io/smallrye/openapi/api/OpenApiConfig.java

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,44 @@ public interface OpenApiConfig {
6060
"org.eclipse.microprofile.openapi",
6161
"org.jetbrains.annotations")));
6262

63-
enum OperationIdStrategy {
64-
METHOD,
65-
CLASS_METHOD,
66-
PACKAGE_CLASS_METHOD
63+
class OperationIdStrategy {
64+
private OperationIdStrategy() {
65+
}
66+
67+
/**
68+
* Strategy name to generate an operationId with the resource method's
69+
* name
70+
*/
71+
public static final String METHOD = "METHOD";
72+
/**
73+
* Strategy name to generate an operationId with the resource class's
74+
* simple name and the resource method's name
75+
*/
76+
public static final String CLASS_METHOD = "CLASS_METHOD";
77+
/**
78+
* Strategy name to generate an operationId with the resource class's
79+
* fully-qualified name and the resource method's name
80+
*/
81+
public static final String PACKAGE_CLASS_METHOD = "PACKAGE_CLASS_METHOD";
82+
83+
/**
84+
* Support compatibility with Quarkus configuration mapping. May be
85+
* removed once Quarkus is updated to no longer treat the
86+
* operation-id-strategy as an enum.
87+
*
88+
* @deprecated not for general purpose use. See
89+
* {@link OperationIdGenerator#load(String, ClassLoader)}
90+
* instead
91+
*/
92+
@Deprecated(since = "4.1.2")
93+
public static OperationIdStrategy valueOf(String value) { // NOSONAR
94+
return new OperationIdStrategy() {
95+
@Override
96+
public String toString() {
97+
return value;
98+
}
99+
};
100+
}
67101
}
68102

69103
enum DuplicateOperationIdBehavior {
@@ -241,8 +275,8 @@ default String getInfoLicenseUrl() {
241275
return getConfigValue(SmallRyeOASConfig.INFO_LICENSE_URL, String.class, () -> null);
242276
}
243277

244-
default OperationIdStrategy getOperationIdStrategy() {
245-
return getConfigValue(SmallRyeOASConfig.OPERATION_ID_STRAGEGY, String.class, OperationIdStrategy::valueOf, () -> null);
278+
default String getOperationIdStrategy() {
279+
return getConfigValue(SmallRyeOASConfig.OPERATION_ID_STRAGEGY, String.class, () -> null);
246280
}
247281

248282
default DuplicateOperationIdBehavior getDuplicateOperationIdBehavior() {
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package io.smallrye.openapi.api;
2+
3+
import org.jboss.jandex.ClassInfo;
4+
import org.jboss.jandex.MethodInfo;
5+
6+
import io.smallrye.openapi.api.OpenApiConfig.OperationIdStrategy;
7+
8+
/**
9+
* Interface that may be implemented to generate custom operationId values.
10+
*
11+
* Three built-in generators may be used by specifying an operationIdStrategy
12+
* configuration property value of METHOD, CLASS_METHOD, or
13+
* PACKAGE_CLASS_METHOD. Otherwise, an fully-qualified implementation name may
14+
* be given for a custom generator.
15+
*
16+
* @since 4.1.2
17+
*/
18+
@FunctionalInterface
19+
public interface OperationIdGenerator {
20+
21+
/**
22+
* Derive an operationId given the Jandex resource ClassInfo and MethodInfo.
23+
*
24+
* @param resourceClass
25+
* the resource class. E.g. a Jakarta REST end-point class
26+
* @param method
27+
* the resource method. E.g. a Jakarta REST end-point method.
28+
* This may be declared in a class other than the resourceClass
29+
* such as a parent class or interface.
30+
* @return value to be used for the OpenAPI operationId
31+
*/
32+
String generateOperationId(ClassInfo resourceClass, MethodInfo method);
33+
34+
/**
35+
* Loads an OperationIdGenerator instance by name. If providing a
36+
* fully-qualified implementation class name, the class must be loadable
37+
* using the provided ClassLoader. The loader is not required or used
38+
* when one of the built-in strategy names is provided.
39+
*
40+
* @param strategyName
41+
* name of the OperationIdGenerator to load
42+
* @param loader
43+
* ClassLoader to load a custom implementation, if necessary
44+
* @return the requested OperationIdGenerator
45+
*/
46+
public static OperationIdGenerator load(String strategyName, ClassLoader loader) {
47+
final OperationIdGenerator strategy;
48+
49+
switch (strategyName) {
50+
case OperationIdStrategy.METHOD:
51+
strategy = (c, m) -> m.name();
52+
break;
53+
case OperationIdStrategy.CLASS_METHOD:
54+
strategy = (c, m) -> c.name().withoutPackagePrefix() + "_" + m.name();
55+
break;
56+
case OperationIdStrategy.PACKAGE_CLASS_METHOD:
57+
strategy = (c, m) -> c.name() + "_" + m.name();
58+
break;
59+
default:
60+
final Class<?> strategyType;
61+
62+
try {
63+
strategyType = loader.loadClass(strategyName);
64+
strategy = (OperationIdGenerator) strategyType.getConstructor().newInstance();
65+
} catch (Exception e) {
66+
throw ApiMessages.msg.invalidOperationIdStrategyWithCause(strategyName, e);
67+
}
68+
break;
69+
}
70+
71+
return strategy;
72+
}
73+
}

core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AnnotationScanner.java

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
import org.jboss.jandex.Type.Kind;
4141

4242
import io.smallrye.openapi.api.OpenApiConfig.DuplicateOperationIdBehavior;
43-
import io.smallrye.openapi.api.OpenApiConfig.OperationIdStrategy;
43+
import io.smallrye.openapi.api.OperationIdGenerator;
4444
import io.smallrye.openapi.api.SmallRyeOASConfig;
4545
import io.smallrye.openapi.api.constants.JacksonConstants;
4646
import io.smallrye.openapi.api.constants.KotlinConstants;
@@ -296,26 +296,11 @@ default Optional<Operation> processOperation(final AnnotationScannerContext cont
296296
TypeUtil.mapDeprecated(context, method, operation::getDeprecated, operation::setDeprecated);
297297
TypeUtil.mapDeprecated(context, resourceClass, operation::getDeprecated, operation::setDeprecated);
298298

299-
OperationIdStrategy operationIdStrategy = context.getConfig().getOperationIdStrategy();
299+
String operationIdStrategy = context.getConfig().getOperationIdStrategy();
300300

301301
if (operationIdStrategy != null && operation.getOperationId() == null) {
302-
String operationId = null;
303-
304-
switch (operationIdStrategy) {
305-
case METHOD:
306-
operationId = method.name();
307-
break;
308-
case CLASS_METHOD:
309-
operationId = resourceClass.name().withoutPackagePrefix() + "_" + method.name();
310-
break;
311-
case PACKAGE_CLASS_METHOD:
312-
operationId = resourceClass.name() + "_" + method.name();
313-
break;
314-
default:
315-
break;
316-
}
317-
318-
operation.setOperationId(operationId);
302+
OperationIdGenerator instance = OperationIdGenerator.load(operationIdStrategy, context.getClassLoader());
303+
operation.setOperationId(instance.generateOperationId(resourceClass, method));
319304
}
320305

321306
context.getOperationHandler().handleOperation(operation, resourceClass, method);

extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/JaxRsAnnotationScannerTest.java

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
package io.smallrye.openapi.runtime.scanner;
22

3+
import static org.hamcrest.MatcherAssert.assertThat;
4+
import static org.hamcrest.Matchers.containsString;
5+
import static org.junit.jupiter.api.Assertions.assertEquals;
6+
import static org.junit.jupiter.api.Assertions.assertThrows;
7+
38
import java.io.ByteArrayInputStream;
49
import java.io.IOException;
510
import java.util.Collections;
@@ -14,6 +19,7 @@
1419
import jakarta.ws.rs.Produces;
1520
import jakarta.ws.rs.core.MediaType;
1621

22+
import org.eclipse.microprofile.config.Config;
1723
import org.eclipse.microprofile.openapi.OASConfig;
1824
import org.eclipse.microprofile.openapi.OASFactory;
1925
import org.eclipse.microprofile.openapi.annotations.Components;
@@ -29,8 +35,10 @@
2935
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
3036
import org.eclipse.microprofile.openapi.models.OpenAPI;
3137
import org.eclipse.microprofile.openapi.models.media.Schema;
38+
import org.jboss.jandex.ClassInfo;
3239
import org.jboss.jandex.Index;
3340
import org.jboss.jandex.Indexer;
41+
import org.jboss.jandex.MethodInfo;
3442
import org.jboss.jandex.Type;
3543
import org.jboss.jandex.Type.Kind;
3644
import org.json.JSONException;
@@ -42,8 +50,11 @@
4250

4351
import io.smallrye.mutiny.Uni;
4452
import io.smallrye.openapi.api.OpenApiConfig;
53+
import io.smallrye.openapi.api.OpenApiConfig.OperationIdStrategy;
4554
import io.smallrye.openapi.api.OpenApiDocument;
55+
import io.smallrye.openapi.api.OperationIdGenerator;
4656
import io.smallrye.openapi.api.SmallRyeOASConfig;
57+
import io.smallrye.openapi.runtime.OpenApiRuntimeException;
4758
import io.smallrye.openapi.runtime.io.Format;
4859
import io.smallrye.openapi.runtime.io.OpenApiParser;
4960

@@ -91,7 +102,7 @@ void testMixedJakartaAndJavaxAnnotations() throws IOException {
91102
// fail hard to trigger the failure in this test
92103
cfg.put(SmallRyeOASConfig.DUPLICATE_OPERATION_ID_BEHAVIOR, OpenApiConfig.DuplicateOperationIdBehavior.FAIL.toString());
93104
// method-strategy needed for this test
94-
cfg.put(SmallRyeOASConfig.OPERATION_ID_STRAGEGY, OpenApiConfig.OperationIdStrategy.METHOD.toString());
105+
cfg.put(SmallRyeOASConfig.OPERATION_ID_STRAGEGY, OpenApiConfig.OperationIdStrategy.METHOD);
95106
OpenApiConfig config = dynamicConfig(cfg);
96107

97108
OpenApiAnnotationScanner s = new OpenApiAnnotationScanner(config, index);
@@ -570,4 +581,58 @@ public Uni<jakarta.ws.rs.core.Response> post(
570581
UserResponse.class,
571582
UserEndpoint.class);
572583
}
584+
585+
public static class CustomStrategy implements OperationIdGenerator {
586+
public CustomStrategy() {
587+
// Create by OperationIdGenerator#load
588+
}
589+
590+
@Override
591+
public String generateOperationId(ClassInfo resourceClass, MethodInfo method) {
592+
return "Custom:" + method.name();
593+
}
594+
}
595+
596+
@ParameterizedTest
597+
@CsvSource({
598+
OperationIdStrategy.METHOD + ", get",
599+
OperationIdStrategy.CLASS_METHOD + ", JaxRsAnnotationScannerTest$1StrategyResource_get",
600+
OperationIdStrategy.PACKAGE_CLASS_METHOD
601+
+ ", io.smallrye.openapi.runtime.scanner.JaxRsAnnotationScannerTest$1StrategyResource_get",
602+
"io.smallrye.openapi.runtime.scanner.JaxRsAnnotationScannerTest$CustomStrategy, Custom:get",
603+
"'', ",
604+
})
605+
void testOperationIdStrategies(String strategyName, String expectedOperationId) {
606+
@Path("/resource")
607+
class StrategyResource {
608+
@GET
609+
@Produces(MediaType.APPLICATION_JSON)
610+
public String get() {
611+
return "";
612+
}
613+
}
614+
615+
OpenAPI result = scan(config(SmallRyeOASConfig.OPERATION_ID_STRAGEGY, strategyName), StrategyResource.class);
616+
assertEquals(expectedOperationId, result.getPaths().getPathItem("/resource").getGET().getOperationId());
617+
}
618+
619+
@Test
620+
void testOperationIdStrategyInvalid() {
621+
@Path("/resource")
622+
class StrategyResource {
623+
@GET
624+
@Produces(MediaType.APPLICATION_JSON)
625+
public String get() {
626+
return "";
627+
}
628+
}
629+
630+
String strategyName = "com.example.MissingStrategy";
631+
Config config = config(SmallRyeOASConfig.OPERATION_ID_STRAGEGY, strategyName);
632+
OpenApiRuntimeException thrown = assertThrows(OpenApiRuntimeException.class,
633+
() -> scan(config, StrategyResource.class));
634+
String message = thrown.getMessage();
635+
assertThat(message, containsString("SROAP00003"));
636+
assertThat(message, containsString(strategyName));
637+
}
573638
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
mp.openapi.extensions.smallrye.operationIdStrategy=METHOD

tools/gradle-plugin/src/main/java/io/smallrye/openapi/gradleplugin/Configs.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ class Configs implements SmallryeOpenApiProperties {
5757
final Property<String> infoContactUrl;
5858
final Property<String> infoLicenseName;
5959
final Property<String> infoLicenseUrl;
60-
final Property<OpenApiConfig.OperationIdStrategy> operationIdStrategy;
60+
final Property<String> operationIdStrategy;
6161
final Property<OpenApiConfig.DuplicateOperationIdBehavior> duplicateOperationIdBehavior;
6262
final ListProperty<String> scanProfiles;
6363
final ListProperty<String> scanExcludeProfiles;
@@ -93,7 +93,7 @@ class Configs implements SmallryeOpenApiProperties {
9393
infoContactUrl = objects.property(String.class);
9494
infoLicenseName = objects.property(String.class);
9595
infoLicenseUrl = objects.property(String.class);
96-
operationIdStrategy = objects.property(OpenApiConfig.OperationIdStrategy.class);
96+
operationIdStrategy = objects.property(String.class);
9797
duplicateOperationIdBehavior = objects.property(OpenApiConfig.DuplicateOperationIdBehavior.class);
9898
scanProfiles = objects.listProperty(String.class);
9999
scanExcludeProfiles = objects.listProperty(String.class);
@@ -130,7 +130,7 @@ class Configs implements SmallryeOpenApiProperties {
130130
infoContactUrl = objects.property(String.class).convention(ext.getInfoContactUrl());
131131
infoLicenseName = objects.property(String.class).convention(ext.getInfoLicenseName());
132132
infoLicenseUrl = objects.property(String.class).convention(ext.getInfoLicenseUrl());
133-
operationIdStrategy = objects.property(OpenApiConfig.OperationIdStrategy.class)
133+
operationIdStrategy = objects.property(String.class)
134134
.convention(ext.getOperationIdStrategy());
135135
duplicateOperationIdBehavior = objects.property(OpenApiConfig.DuplicateOperationIdBehavior.class)
136136
.convention(ext.getDuplicateOperationIdBehavior());
@@ -325,7 +325,7 @@ public Property<String> getInfoLicenseUrl() {
325325
return infoLicenseUrl;
326326
}
327327

328-
public Property<OpenApiConfig.OperationIdStrategy> getOperationIdStrategy() {
328+
public Property<String> getOperationIdStrategy() {
329329
return operationIdStrategy;
330330
}
331331

tools/gradle-plugin/src/main/java/io/smallrye/openapi/gradleplugin/SmallryeOpenApiProperties.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ public interface SmallryeOpenApiProperties {
121121
* Configuration property to specify how the operationid is generated. Can be used to minimize
122122
* risk of collisions between operations.
123123
*/
124-
Property<OpenApiConfig.OperationIdStrategy> getOperationIdStrategy();
124+
Property<String> getOperationIdStrategy();
125125

126126
/**
127127
* Configuration property to specify what should happen if duplicate operationIds occur.

tools/gradle-plugin/src/main/java/io/smallrye/openapi/gradleplugin/SmallryeOpenApiTask.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -418,7 +418,7 @@ public Property<String> getInfoLicenseUrl() {
418418
@Input
419419
@Optional
420420
@Override
421-
public Property<OpenApiConfig.OperationIdStrategy> getOperationIdStrategy() {
421+
public Property<String> getOperationIdStrategy() {
422422
return properties.operationIdStrategy;
423423
}
424424

0 commit comments

Comments
 (0)