From b6c1361cb7f35633e6f8b718ab95f11cf2a1c9f9 Mon Sep 17 00:00:00 2001 From: Martin Panzer Date: Mon, 15 Nov 2021 17:14:30 +0100 Subject: [PATCH] maven-plugin make it easier to scan dependend schemas Added new options scan.profiles and scan.exclude.profiles to filter the operations that are kept in the schema. Only one of the configured profiles is needed to either include, or exclude, the operation. Exclusions are checked first, then inclusions. Extensions containing profiles have to start with "x-smallrye-profile", and will not be included in the openapi document. Naming of these new options are kept in line with the openapi scan.package and scan.external.package options. --- .../smallrye/openapi/api/OpenApiConfig.java | 8 ++ .../openapi/api/OpenApiConfigImpl.java | 26 +++++ .../api/constants/OpenApiConstants.java | 4 + .../spi/AbstractAnnotationScanner.java | 55 ++++++++++- .../spi/AbstractAnnotationScannerTest.java | 96 +++++++++++++++++++ .../runtime/scanner/spi/PathMakerTest.java | 36 ------- .../openapi/jaxrs/JaxRsAnnotationScanner.java | 4 + .../scanner/JaxRsAnnotationScannerTest.java | 51 ++++++++++ .../spring/SpringAnnotationScanner.java | 4 + .../openapi/vertx/VertxAnnotationScanner.java | 4 + tools/maven-plugin/README.adoc | 4 +- .../mavenplugin/GenerateSchemaMojo.java | 14 +++ .../openapi/mavenplugin/MavenConfig.java | 10 ++ 13 files changed, 277 insertions(+), 39 deletions(-) create mode 100644 core/src/test/java/io/smallrye/openapi/runtime/scanner/spi/AbstractAnnotationScannerTest.java delete mode 100644 core/src/test/java/io/smallrye/openapi/runtime/scanner/spi/PathMakerTest.java diff --git a/core/src/main/java/io/smallrye/openapi/api/OpenApiConfig.java b/core/src/main/java/io/smallrye/openapi/api/OpenApiConfig.java index 38e78e83b..4ebd2e052 100644 --- a/core/src/main/java/io/smallrye/openapi/api/OpenApiConfig.java +++ b/core/src/main/java/io/smallrye/openapi/api/OpenApiConfig.java @@ -156,6 +156,14 @@ default Optional allowNakedPathParameter() { return Optional.empty(); } + default Set getScanProfiles() { + return new HashSet<>(); + } + + default Set getScanExcludeProfiles() { + return new HashSet<>(); + } + default void doAllowNakedPathParameter() { } diff --git a/core/src/main/java/io/smallrye/openapi/api/OpenApiConfigImpl.java b/core/src/main/java/io/smallrye/openapi/api/OpenApiConfigImpl.java index 9c83fca7b..4ff9e8950 100644 --- a/core/src/main/java/io/smallrye/openapi/api/OpenApiConfigImpl.java +++ b/core/src/main/java/io/smallrye/openapi/api/OpenApiConfigImpl.java @@ -51,6 +51,8 @@ public class OpenApiConfigImpl implements OpenApiConfig { private String infoLicenseName; private String infoLicenseUrl; private OperationIdStrategy operationIdStrategy; + private Set scanProfiles; + private Set scanExcludeProfiles; private Optional defaultProduces = UNSET; private Optional defaultConsumes = UNSET; private Optional allowNakedPathParameter = Optional.empty(); @@ -408,6 +410,30 @@ public Optional getDefaultConsumes() { return defaultConsumes; } + @Override + public Set getScanProfiles() { + if (scanProfiles == null) { + String classes = getStringConfigValue(OpenApiConstants.SCAN_PROFILES); + if (classes == null) { + classes = getStringConfigValue(OpenApiConstants.SCAN_PROFILES); + } + scanProfiles = asCsvSet(classes); + } + return scanProfiles; + } + + @Override + public Set getScanExcludeProfiles() { + if (scanExcludeProfiles == null) { + String classes = getStringConfigValue(OpenApiConstants.SCAN_EXCLUDE_PROFILES); + if (classes == null) { + classes = getStringConfigValue(OpenApiConstants.SCAN_EXCLUDE_PROFILES); + } + scanExcludeProfiles = asCsvSet(classes); + } + return scanExcludeProfiles; + } + /** * getConfig().getOptionalValue(key) can return "" if optional {@link Converter}s are used. Enforce a null value if * we get an empty string back. diff --git a/core/src/main/java/io/smallrye/openapi/api/constants/OpenApiConstants.java b/core/src/main/java/io/smallrye/openapi/api/constants/OpenApiConstants.java index ec59cd06e..a45699b02 100644 --- a/core/src/main/java/io/smallrye/openapi/api/constants/OpenApiConstants.java +++ b/core/src/main/java/io/smallrye/openapi/api/constants/OpenApiConstants.java @@ -42,6 +42,8 @@ public final class OpenApiConstants { public static final String SMALLRYE_PRIVATE_PROPERTIES_ENABLE = SMALLRYE_PREFIX + SUFFIX_PRIVATE_PROPERTIES_ENABLE; public static final String SMALLRYE_PROPERTY_NAMING_STRATEGY = SMALLRYE_PREFIX + SUFFIX_PROPERTY_NAMING_STRATEGY; public static final String SMALLRYE_SORTED_PROPERTIES_ENABLE = SMALLRYE_PREFIX + SUFFIX_SORTED_PROPERTIES_ENABLE; + public static final String SCAN_PROFILES = SMALLRYE_PREFIX + "scan.profiles"; + public static final String SCAN_EXCLUDE_PROFILES = SMALLRYE_PREFIX + "scan.exclude.profiles"; public static final String VERSION = SMALLRYE_PREFIX + "openapi"; public static final String INFO_TITLE = SMALLRYE_PREFIX + "info.title"; @@ -108,6 +110,8 @@ public final class OpenApiConstants { // Used by both Jax-rs and openapi public static final String REF = "ref"; + public static final String EXTENSION_PROFILE_PREFIX = "x-smallrye-profile-"; + /** * Constructor. */ diff --git a/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AbstractAnnotationScanner.java b/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AbstractAnnotationScanner.java index 8e6ad035f..b549827e5 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AbstractAnnotationScanner.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AbstractAnnotationScanner.java @@ -1,11 +1,22 @@ package io.smallrye.openapi.runtime.scanner.spi; +import io.smallrye.openapi.api.OpenApiConfig; +import io.smallrye.openapi.api.constants.OpenApiConstants; +import org.eclipse.microprofile.openapi.models.Extensible; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + /** * Abstract base class for annotation scanners * * @author Phillip Kruger (phillip.kruger@redhat.com) */ public abstract class AbstractAnnotationScanner implements AnnotationScanner { + private static final String EMPTY = ""; + protected String currentAppPath = EMPTY; private String contextRoot = EMPTY; @@ -20,7 +31,7 @@ protected String makePath(String operationPath) { /** * Make a path out of a number of path segments. - * + * * @param segments String paths * @return Path built from the segments */ @@ -46,5 +57,45 @@ protected static String createPathFromSegments(String... segments) { return rval; } - private static final String EMPTY = ""; + /** + * Checks if the given extensible contains profiles, and if the extensible should be included in the final openapi document. + * Any extension containing a profile is removed from the extensible. + * inclusion is then calculated based on all collected profiles. + * + * @param config current config + * @param extensible the extensible to check for profiles + * @return true, if the given extensible should be included in the final openapi document, otherwise false + */ + protected static boolean processProfiles(OpenApiConfig config, Extensible extensible) { + + Set profiles = new HashSet<>(); + Map extensions = extensible.getExtensions(); + if (extensions != null && !extensions.isEmpty()) { + extensions = new HashMap<>(extensions); + + for (String name : extensions.keySet()) { + if (!name.startsWith(OpenApiConstants.EXTENSION_PROFILE_PREFIX)) { + continue; + } + + String profile = name.substring(OpenApiConstants.EXTENSION_PROFILE_PREFIX.length()); + profiles.add(profile); + extensible.removeExtension(name); + } + } + + return profileIncluded(config, profiles); + } + + private static boolean profileIncluded(OpenApiConfig config, Set profiles) { + if (!config.getScanExcludeProfiles().isEmpty()) { + return config.getScanExcludeProfiles().stream().noneMatch(profiles::contains); + } + + if (config.getScanProfiles().isEmpty()) { + return true; + } + + return config.getScanProfiles().stream().anyMatch(profiles::contains); + } } diff --git a/core/src/test/java/io/smallrye/openapi/runtime/scanner/spi/AbstractAnnotationScannerTest.java b/core/src/test/java/io/smallrye/openapi/runtime/scanner/spi/AbstractAnnotationScannerTest.java new file mode 100644 index 000000000..addad0260 --- /dev/null +++ b/core/src/test/java/io/smallrye/openapi/runtime/scanner/spi/AbstractAnnotationScannerTest.java @@ -0,0 +1,96 @@ +package io.smallrye.openapi.runtime.scanner.spi; + +import io.smallrye.openapi.api.OpenApiConfig; +import io.smallrye.openapi.api.models.OperationImpl; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class AbstractAnnotationScannerTest { + /** + * Test method for {@link AbstractAnnotationScanner#makePath(String)}. + */ + @Test + void testMakePath() { + + String path = AbstractAnnotationScanner.createPathFromSegments("", "", ""); + Assertions.assertEquals("/", path); + + path = AbstractAnnotationScanner.createPathFromSegments("/", "/"); + Assertions.assertEquals("/", path); + + path = AbstractAnnotationScanner.createPathFromSegments("", "/bookings"); + Assertions.assertEquals("/bookings", path); + + path = AbstractAnnotationScanner.createPathFromSegments("/api", "/bookings"); + Assertions.assertEquals("/api/bookings", path); + + path = AbstractAnnotationScanner.createPathFromSegments("api", "bookings"); + Assertions.assertEquals("/api/bookings", path); + + path = AbstractAnnotationScanner.createPathFromSegments("/", "/bookings", "{id}"); + Assertions.assertEquals("/bookings/{id}", path); + } + + @Test + void testNoConfiguredProfile() { + OpenApiConfig config = new OpenApiConfig() { + }; + + OperationImpl operation = new OperationImpl(); + operation.setExtensions(Collections.singletonMap("x-smallrye-profile-external", "")); + + boolean result = AbstractAnnotationScanner.processProfiles(config, operation); + + assertTrue(result); + assertEquals(0, operation.getExtensions().size()); + } + + @Test + void testConfiguredIncludeProfile() { + OpenApiConfig config = new OpenApiConfig() { + @Override + public Set getScanProfiles() { + return Collections.singleton("external"); + } + }; + + OperationImpl operation = new OperationImpl(); + + boolean result = AbstractAnnotationScanner.processProfiles(config, operation); + assertFalse(result); + + operation.setExtensions(Collections.singletonMap("x-smallrye-profile-external", "")); + result = AbstractAnnotationScanner.processProfiles(config, operation); + + assertTrue(result); + assertEquals(0, operation.getExtensions().size()); + } + + @Test + void testConfiguredExcludeProfile() { + OpenApiConfig config = new OpenApiConfig() { + @Override + public Set getScanExcludeProfiles() { + return Collections.singleton("external"); + } + }; + + OperationImpl operation = new OperationImpl(); + + boolean result = AbstractAnnotationScanner.processProfiles(config, operation); + assertTrue(result); + + operation.setExtensions(Collections.singletonMap("x-smallrye-profile-external", "")); + result = AbstractAnnotationScanner.processProfiles(config, operation); + + assertFalse(result); + assertEquals(0, operation.getExtensions().size()); + } +} diff --git a/core/src/test/java/io/smallrye/openapi/runtime/scanner/spi/PathMakerTest.java b/core/src/test/java/io/smallrye/openapi/runtime/scanner/spi/PathMakerTest.java deleted file mode 100644 index 0aa71681a..000000000 --- a/core/src/test/java/io/smallrye/openapi/runtime/scanner/spi/PathMakerTest.java +++ /dev/null @@ -1,36 +0,0 @@ -package io.smallrye.openapi.runtime.scanner.spi; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -/** - * Test that the path is created correctly - * - * @author Phillip Kruger (phillip.kruger@redhat.com) - */ -class PathMakerTest { - /** - * Test method for {@link PathMaker#makePath(java.lang.String[])}. - */ - @Test - void testMakePath() { - - String path = AbstractAnnotationScanner.createPathFromSegments("", "", ""); - Assertions.assertEquals("/", path); - - path = AbstractAnnotationScanner.createPathFromSegments("/", "/"); - Assertions.assertEquals("/", path); - - path = AbstractAnnotationScanner.createPathFromSegments("", "/bookings"); - Assertions.assertEquals("/bookings", path); - - path = AbstractAnnotationScanner.createPathFromSegments("/api", "/bookings"); - Assertions.assertEquals("/api/bookings", path); - - path = AbstractAnnotationScanner.createPathFromSegments("api", "bookings"); - Assertions.assertEquals("/api/bookings", path); - - path = AbstractAnnotationScanner.createPathFromSegments("/", "/bookings", "{id}"); - Assertions.assertEquals("/bookings/{id}", path); - } -} diff --git a/extension-jaxrs/src/main/java/io/smallrye/openapi/jaxrs/JaxRsAnnotationScanner.java b/extension-jaxrs/src/main/java/io/smallrye/openapi/jaxrs/JaxRsAnnotationScanner.java index cf17a2ef0..017f12a8c 100644 --- a/extension-jaxrs/src/main/java/io/smallrye/openapi/jaxrs/JaxRsAnnotationScanner.java +++ b/extension-jaxrs/src/main/java/io/smallrye/openapi/jaxrs/JaxRsAnnotationScanner.java @@ -458,6 +458,10 @@ private void processResourceMethod(final AnnotationScannerContext context, // Now set the operation on the PathItem as appropriate based on the Http method type setOperationOnPathItem(methodType, pathItem, operation); + if (!processProfiles(context.getConfig(), operation)) { + return; + } + // Figure out the path for the operation. This is a combination of the App, Resource, and Method @Path annotations final String path; diff --git a/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/JaxRsAnnotationScannerTest.java b/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/JaxRsAnnotationScannerTest.java index a6b6c1090..c4808d1cc 100644 --- a/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/JaxRsAnnotationScannerTest.java +++ b/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/JaxRsAnnotationScannerTest.java @@ -6,11 +6,14 @@ import java.util.UUID; import javax.ws.rs.GET; +import javax.ws.rs.POST; import javax.ws.rs.Path; +import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import org.eclipse.microprofile.openapi.OASConfig; +import org.eclipse.microprofile.openapi.annotations.extensions.Extension; import org.eclipse.microprofile.openapi.models.OpenAPI; import org.eclipse.microprofile.openapi.models.media.Schema; import org.jboss.jandex.Index; @@ -18,6 +21,7 @@ import org.jboss.jandex.Type; import org.jboss.jandex.Type.Kind; import org.json.JSONException; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; @@ -392,4 +396,51 @@ public void registerCustomSchemas(SchemaRegistry schemaRegistry) { } } + + /**************************************************************************/ + + @Test + void testIncludeProfile() { + Index index = indexOf(ProfileResource.class); + OpenApiConfig config = dynamicConfig(OpenApiConstants.SCAN_PROFILES, + "external"); + + OpenApiAnnotationScanner scanner = new OpenApiAnnotationScanner(config, index); + + OpenAPI result = scanner.scan(); + + Assertions.assertEquals(1, result.getPaths().getPathItems().size()); + Assertions.assertTrue(result.getPaths().getPathItems().containsKey("/profile/{id}")); + } + + @Test + void testExcludeProfile() { + Index index = indexOf(ProfileResource.class); + OpenApiConfig config = dynamicConfig(OpenApiConstants.SCAN_EXCLUDE_PROFILES, + "external"); + + OpenApiAnnotationScanner scanner = new OpenApiAnnotationScanner(config, index); + + OpenAPI result = scanner.scan(); + + Assertions.assertEquals(1, result.getPaths().getPathItems().size()); + Assertions.assertTrue(result.getPaths().getPathItems().containsKey("/profile")); + } + + @Path("/profile") + static class ProfileResource { + @GET + @Produces(MediaType.APPLICATION_JSON) + public String read() { + return ""; + } + + @Path("{id}") + @POST + @Produces(MediaType.APPLICATION_JSON) + @Extension(name = "x-smallrye-profile-external", value = "") + public String create(@PathParam("id") Long id) { + return ""; + } + } } diff --git a/extension-spring/src/main/java/io/smallrye/openapi/spring/SpringAnnotationScanner.java b/extension-spring/src/main/java/io/smallrye/openapi/spring/SpringAnnotationScanner.java index a5bf5aaf4..341504675 100644 --- a/extension-spring/src/main/java/io/smallrye/openapi/spring/SpringAnnotationScanner.java +++ b/extension-spring/src/main/java/io/smallrye/openapi/spring/SpringAnnotationScanner.java @@ -344,6 +344,10 @@ private void processControllerMethod(final AnnotationScannerContext context, // Now set the operation on the PathItem as appropriate based on the Http method type setOperationOnPathItem(methodType, pathItem, operation); + if (!processProfiles(context.getConfig(), operation)) { + return; + } + // Figure out the path for the operation. This is a combination of the App, Resource, and Method @Path annotations String path = super.makePath(params.getOperationPath()); diff --git a/extension-vertx/src/main/java/io/smallrye/openapi/vertx/VertxAnnotationScanner.java b/extension-vertx/src/main/java/io/smallrye/openapi/vertx/VertxAnnotationScanner.java index 74143a5d3..c9e14238d 100644 --- a/extension-vertx/src/main/java/io/smallrye/openapi/vertx/VertxAnnotationScanner.java +++ b/extension-vertx/src/main/java/io/smallrye/openapi/vertx/VertxAnnotationScanner.java @@ -306,6 +306,10 @@ private void processRouteMethod(final AnnotationScannerContext context, // Now set the operation on the PathItem as appropriate based on the Http method type setOperationOnPathItem(methodType, pathItem, operation); + if (!processProfiles(context.getConfig(), operation)) { + return; + } + // Figure out the path for the operation. This is a combination of the App, Resource, and Method @Path annotations String path = super.makePath(params.getOperationPath()); diff --git a/tools/maven-plugin/README.adoc b/tools/maven-plugin/README.adoc index a8c072c6a..9c827bcc7 100644 --- a/tools/maven-plugin/README.adoc +++ b/tools/maven-plugin/README.adoc @@ -84,4 +84,6 @@ All properties from the SmallRye OpenAPI Implementation is supported. Properties - `operationIdStrategy` (METHOD/CLASS_METHOD/PACKAGE_CLASS_METHOD) - Configuration property to specify how the operationid is generated. Can be used to minimize risk of collisions between operations. - `METHOD` - The method name is used as operationId. - `CLASS_METHOD` - The class name and method name is used as operationId. - - `PACKAGE_CLASS_METHOD` - The fully qualified class name and method name is used as operationId. \ No newline at end of file + - `PACKAGE_CLASS_METHOD` - The fully qualified class name and method name is used as operationId. +- `scanProfiles` (List) - Profiles which explicitly include operations. Any operation without a matching profile is excluded. +- `scanExcludeProfiles` (List) - Profiles which explicitly exclude operations. Any operation without a matching profile is included. \ No newline at end of file diff --git a/tools/maven-plugin/src/main/java/io/smallrye/openapi/mavenplugin/GenerateSchemaMojo.java b/tools/maven-plugin/src/main/java/io/smallrye/openapi/mavenplugin/GenerateSchemaMojo.java index eea42a01d..4ac5e457f 100644 --- a/tools/maven-plugin/src/main/java/io/smallrye/openapi/mavenplugin/GenerateSchemaMojo.java +++ b/tools/maven-plugin/src/main/java/io/smallrye/openapi/mavenplugin/GenerateSchemaMojo.java @@ -211,6 +211,18 @@ public class GenerateSchemaMojo extends AbstractMojo { @Parameter(property = "operationIdStrategy") private String operationIdStrategy; + /** + * Profiles which explicitly include operations. Any operation without a matching profile is excluded. + */ + @Parameter(property = "scanProfiles") + private List scanProfiles; + + /** + * Profiles which explicitly exclude operations. Any operation without a matching profile is included. + */ + @Parameter(property = "scanExcludeProfiles") + private List scanExcludeProfiles; + @Component private MavenDependencyIndexCreator mavenDependencyIndexCreator; @@ -369,6 +381,8 @@ private Map getProperties() throws IOException { addToPropertyMap(cp, OpenApiConstants.INFO_LICENSE_NAME, infoLicenseName); addToPropertyMap(cp, OpenApiConstants.INFO_LICENSE_URL, infoLicenseUrl); addToPropertyMap(cp, OpenApiConstants.OPERATION_ID_STRAGEGY, operationIdStrategy); + addToPropertyMap(cp, OpenApiConstants.SCAN_PROFILES, scanProfiles); + addToPropertyMap(cp, OpenApiConstants.SCAN_EXCLUDE_PROFILES, scanExcludeProfiles); return cp; } diff --git a/tools/maven-plugin/src/main/java/io/smallrye/openapi/mavenplugin/MavenConfig.java b/tools/maven-plugin/src/main/java/io/smallrye/openapi/mavenplugin/MavenConfig.java index e280bde5f..1522bc870 100644 --- a/tools/maven-plugin/src/main/java/io/smallrye/openapi/mavenplugin/MavenConfig.java +++ b/tools/maven-plugin/src/main/java/io/smallrye/openapi/mavenplugin/MavenConfig.java @@ -145,4 +145,14 @@ public OperationIdStrategy getOperationIdStrategy() { } return null; } + + @Override + public Set getScanProfiles() { + return asCsvSet(properties.getOrDefault(OpenApiConstants.SCAN_PROFILES, null)); + } + + @Override + public Set getScanExcludeProfiles() { + return asCsvSet(properties.getOrDefault(OpenApiConstants.SCAN_EXCLUDE_PROFILES, null)); + } }