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
4 changes: 3 additions & 1 deletion README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,9 @@ Set this boolean value to enable or disable the expansion of YAML aliases when l
mp.openapi.extensions.smallrye.scan.profiles
mp.openapi.extensions.smallrye.scan.exclude.profiles
----
These properties are used together with the <<user-content-x-smallrye-profile,x-smallrye-profile>> extension to provide a way to limit which REST endpoints and associated resources are included in the OpenAPI model.
These properties are used together with the <<user-content-x-smallrye-profile,x-smallrye-profile>> extension to provide a way to limit which REST endpoints and associated resources are included in the OpenAPI model. Path items where all operations have been excluded will also be removed from the model unless the path item is in the model's components section.

Note: Profiles will be processed after the OpenAPI model is fully merged from the static YAML/JSON file, the `OASModelReader` output, and annotation scanning but prior to any user-provided filters. This means that user filters will receive the model after non-included profiles have been removed.

=== Extensions

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import io.smallrye.openapi.api.util.UnusedSchemaFilter;
import io.smallrye.openapi.model.Extensions;
import io.smallrye.openapi.model.ReferenceType;
import io.smallrye.openapi.runtime.util.ProfileFilter;

/**
* Holds the final OpenAPI document produced during the startup of the app.
Expand Down Expand Up @@ -190,6 +191,10 @@ private OpenAPI filterModel(OpenAPI model) {
return model;
}

if (!config.getScanProfiles().isEmpty() || !config.getScanExcludeProfiles().isEmpty()) {
model = FilterUtil.applyFilter(new ProfileFilter(model, config), model);
}

if (config.removeUnusedSchemas()) {
model = FilterUtil.applyFilter(new UnusedSchemaFilter(), model);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,30 +104,15 @@ protected static String createPathFromSegments(String... segments) {
}

/**
* 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.
* Checks if the given extensible contains profiles, and if the extensible should be included in the final OpenAPI document.
* Inclusion is 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
* @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<String> profiles = Extensions.getProfiles(extensible);
Extensions.removeProfiles(extensible);
return profileIncluded(config, profiles);
}

private static boolean profileIncluded(OpenApiConfig config, Set<String> 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);
return Extensions.includedProfile(extensible, config.getScanProfiles(), config.getScanExcludeProfiles());
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package io.smallrye.openapi.runtime.util;

import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

import org.eclipse.microprofile.openapi.OASFilter;
import org.eclipse.microprofile.openapi.models.Components;
import org.eclipse.microprofile.openapi.models.OpenAPI;
import org.eclipse.microprofile.openapi.models.Operation;
import org.eclipse.microprofile.openapi.models.PathItem;
import org.eclipse.microprofile.openapi.models.PathItem.HttpMethod;

import io.smallrye.openapi.api.OpenApiConfig;
import io.smallrye.openapi.model.Extensions;

/**
* Not intended for use outside of smallrye-open-api. Interface and functionality
* may not be stable for general use.
*
* Removes operations and path items from the model if they are not included
* based on configuration. Note that path items will be removed if all operations
* have been removed by the filter and if the path item is not specified in the
* OpenAPI components section.
*/
public class ProfileFilter implements OASFilter {

private final Map<String, PathItem> pathItemComponents;
private final Set<String> included;
private final Set<String> excluded;

public ProfileFilter(OpenAPI model, OpenApiConfig config) {
pathItemComponents = Optional.ofNullable(model.getComponents())
.map(Components::getPathItems)
.orElseGet(Collections::emptyMap);

included = config.getScanProfiles();
excluded = config.getScanExcludeProfiles();
}

@Override
public PathItem filterPathItem(PathItem pathItem) {
boolean operationExcluded = false;

for (HttpMethod method : Set.copyOf(pathItem.getOperations().keySet())) {
Operation o = pathItem.getOperations().get(method);

if (!Extensions.includedProfile(o, included, excluded)) {
operationExcluded = true;
pathItem.setOperation(method, null);
}
}

if (operationExcluded && pathItem.getOperations().isEmpty() && nonComponent(pathItem)) {
// Only remove the path item if it is not a component that may be referenced elsewhere.
return null;
}

return pathItem;
}

private boolean nonComponent(PathItem pathItem) {
for (PathItem component : pathItemComponents.values()) {
if (pathItem == component) {
// If it's the same object, the given pathItem is in components
return false;
}
}
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,6 @@ void testNoConfiguredProfile() {
boolean result = AbstractAnnotationScanner.processProfiles(config, operation);

assertTrue(result);
assertEquals(0, operation.getExtensions().size());
}

@Test
Expand All @@ -142,7 +141,6 @@ public Set<String> getScanProfiles() {
result = AbstractAnnotationScanner.processProfiles(config, operation);

assertTrue(result);
assertEquals(0, operation.getExtensions().size());
}

@Test
Expand All @@ -163,7 +161,6 @@ public Set<String> getScanExcludeProfiles() {
result = AbstractAnnotationScanner.processProfiles(config, operation);

assertFalse(result);
assertEquals(0, operation.getExtensions().size());
}

@ParameterizedTest
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,19 @@ private void processResourceMethod(final ClassInfo resourceClass,
// Process tags - @Tag and @Tags annotations combines with the resource tags we've already found (passed in)
processOperationTags(context, method, context.getOpenApi(), resourceTags, operation);

// Process @Extension annotations
processExtensions(context, method, operation);

if (!processProfiles(context.getConfig(), operation)) {
// Any operations not included by a profile (if configured) will also be filtered out
// later during the filter stage so that profile included/exclusion takes into account
// the static file and the OASReader models. This short-circuit is left in as an
// optimization to avoid additional scanning when the operation would have been
// excluded anyway. As a result, profile configurations are built-time only for Quarkus
// applications.
return;
}

// Process @Parameter annotations.
List<Parameter> operationParams = params.getOperationParameters();
operation.setParameters(operationParams);
Expand Down Expand Up @@ -524,19 +537,12 @@ private void processResourceMethod(final ClassInfo resourceClass,
// Process @Server annotations
processServerAnnotation(context, method, operation);

// Process @Extension annotations
processExtensions(context, method, operation);

// Process Security Roles
context.getJavaSecurityProcessor().processSecurityRoles(method, operation);

// Now set the operation on the PathItem as appropriate based on the Http method type
pathItem.setOperation(methodType, operation);

if (!processProfiles(context.getConfig(), operation)) {
return;
}

// When processing a sub-resource tree, ignore any @Path information from the current class
List<String> operationPaths = this.subResourceStack.isEmpty() ? params.getFullOperationPaths()
: params.getOperationPaths();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package io.smallrye.openapi.runtime.scanner;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.Collections;
import java.util.List;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

import org.eclipse.microprofile.openapi.OASConfig;
import org.eclipse.microprofile.openapi.OASFactory;
import org.eclipse.microprofile.openapi.OASModelReader;
import org.eclipse.microprofile.openapi.annotations.extensions.Extension;
import org.eclipse.microprofile.openapi.models.OpenAPI;
import org.eclipse.microprofile.openapi.models.media.Schema.SchemaType;
import org.jboss.jandex.Index;
import org.junit.jupiter.api.Test;

import io.smallrye.openapi.api.SmallRyeOASConfig;
import io.smallrye.openapi.api.SmallRyeOpenAPI;

class ProfileSelectionWithStaticModelTest {

public static class MyReader implements OASModelReader {
@Override
public OpenAPI buildModel() {
return OASFactory.createOpenAPI()
.components(OASFactory.createComponents()
.addPathItem("Orders", OASFactory.createPathItem()
.GET(OASFactory.createOperation()
.responses(OASFactory.createAPIResponses()
.addAPIResponse("default", OASFactory.createAPIResponse()
.content(OASFactory.createContent()
.addMediaType("text/plain", OASFactory.createMediaType()
.schema(OASFactory.createSchema()
.type(List.of(SchemaType.STRING))))))))));
}
}

@Test
void testStaticModelOperationExcluded() throws Exception {
@Path("/api")
class MyResource {
@Path("/users")
@GET
@Extension(name = "x-smallrye-profile-public", value = "")
public List<String> listUsers() {
return Collections.emptyList();
}
}

SmallRyeOpenAPI result;

try {
System.setProperty(SmallRyeOASConfig.SCAN_PROFILES, "public");
System.setProperty(OASConfig.MODEL_READER, MyReader.class.getName());

result = SmallRyeOpenAPI.builder()
.enableStandardFilter(false)
.enableStandardStaticFiles(false)
.withCustomStaticFile(() -> {
return getClass()
.getClassLoader()
.getResourceAsStream(
"io/smallrye/openapi/runtime/scanner/static/profile-selection-static-model.yaml");
})
.enableAnnotationScan(true)
.withIndex(Index.of(MyResource.class))
.build();
} finally {
System.clearProperty(SmallRyeOASConfig.SCAN_PROFILES);
System.clearProperty(OASConfig.MODEL_READER);
}

OpenAPI model = result.model();

var paths = model.getPaths().getPathItems();
assertEquals(2, paths.size(), () -> paths.keySet().toString());
assertTrue(paths.containsKey("/api/users"), () -> paths.keySet().toString());
assertTrue(paths.containsKey("/api/public/echo"), () -> paths.keySet().toString());

var componentPaths = model.getComponents().getPathItems();
assertEquals(1, componentPaths.size());
var orderOperations = componentPaths.get("Orders").getOperations();
// removed by filter
assertTrue(orderOperations.isEmpty());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
openapi: 3.0.3
info:
title: echo
version: '1.0.0'
description: ""
paths:
/api/public/echo:
get:
summary: Echo GET
operationId: get-echo
x-smallrye-profile-public: ""
parameters:
- name: Message
in: query
schema:
type: string
options:
summary: Echo OPTIONS
operationId: options-echo
description: Echo OPTIONS not for public use!

/api/private/echo:
post:
summary: Echo POST
operationId: post-echo
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/Message"
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/Echo'
components:
schemas:
Echo:
type: object
properties:
echo:
type: string
Message:
type: object
properties:
message:
type: string
Loading