Skip to content

Commit 0a4c35b

Browse files
committed
Process profiles in filter phase
Signed-off-by: Michael Edgar <michael@xlate.io>
1 parent b696ab5 commit 0a4c35b

9 files changed

Lines changed: 263 additions & 41 deletions

File tree

README.adoc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,9 @@ Set this boolean value to enable or disable the expansion of YAML aliases when l
169169
mp.openapi.extensions.smallrye.scan.profiles
170170
mp.openapi.extensions.smallrye.scan.exclude.profiles
171171
----
172-
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.
172+
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.
173+
174+
Note: Profiles will be processed after the OpenAPI model is fully merged from the static YAML/JSON file, the `OASReader` 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.
173175

174176
=== Extensions
175177

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import io.smallrye.openapi.api.util.UnusedSchemaFilter;
1616
import io.smallrye.openapi.model.Extensions;
1717
import io.smallrye.openapi.model.ReferenceType;
18+
import io.smallrye.openapi.runtime.util.ProfileFilter;
1819

1920
/**
2021
* Holds the final OpenAPI document produced during the startup of the app.
@@ -190,6 +191,10 @@ private OpenAPI filterModel(OpenAPI model) {
190191
return model;
191192
}
192193

194+
if (!config.getScanProfiles().isEmpty() || !config.getScanExcludeProfiles().isEmpty()) {
195+
model = FilterUtil.applyFilter(new ProfileFilter(model, config), model);
196+
}
197+
193198
if (config.removeUnusedSchemas()) {
194199
model = FilterUtil.applyFilter(new UnusedSchemaFilter(), model);
195200
}

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

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -104,30 +104,15 @@ protected static String createPathFromSegments(String... segments) {
104104
}
105105

106106
/**
107-
* Checks if the given extensible contains profiles, and if the extensible should be included in the final openapi document.
108-
* Any extension containing a profile is removed from the extensible.
109-
* inclusion is then calculated based on all collected profiles.
107+
* Checks if the given extensible contains profiles, and if the extensible should be included in the final OpenAPI document.
108+
* Inclusion is calculated based on all collected profiles.
110109
*
111110
* @param config current config
112111
* @param extensible the extensible to check for profiles
113-
* @return true, if the given extensible should be included in the final openapi document, otherwise false
112+
* @return true, if the given extensible should be included in the final OpenAPI document, otherwise false
114113
*/
115114
protected static boolean processProfiles(OpenApiConfig config, Extensible<?> extensible) {
116-
Set<String> profiles = Extensions.getProfiles(extensible);
117-
Extensions.removeProfiles(extensible);
118-
return profileIncluded(config, profiles);
119-
}
120-
121-
private static boolean profileIncluded(OpenApiConfig config, Set<String> profiles) {
122-
if (!config.getScanExcludeProfiles().isEmpty()) {
123-
return config.getScanExcludeProfiles().stream().noneMatch(profiles::contains);
124-
}
125-
126-
if (config.getScanProfiles().isEmpty()) {
127-
return true;
128-
}
129-
130-
return config.getScanProfiles().stream().anyMatch(profiles::contains);
115+
return Extensions.includedProfile(extensible, config.getScanProfiles(), config.getScanExcludeProfiles());
131116
}
132117

133118
@Override
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package io.smallrye.openapi.runtime.util;
2+
3+
import java.util.Collections;
4+
import java.util.Map;
5+
import java.util.Optional;
6+
import java.util.Set;
7+
8+
import org.eclipse.microprofile.openapi.OASFilter;
9+
import org.eclipse.microprofile.openapi.models.Components;
10+
import org.eclipse.microprofile.openapi.models.OpenAPI;
11+
import org.eclipse.microprofile.openapi.models.Operation;
12+
import org.eclipse.microprofile.openapi.models.PathItem;
13+
import org.eclipse.microprofile.openapi.models.PathItem.HttpMethod;
14+
15+
import io.smallrye.openapi.api.OpenApiConfig;
16+
import io.smallrye.openapi.model.Extensions;
17+
18+
/**
19+
* Not intended for use outside of smallrye-open-api. Interface and functionality
20+
* may not be stable for general use.
21+
*
22+
* Removes operations and path items from the model if they are not included
23+
* based on configuration. Note that path items will be removed if all operations
24+
* have been removed by the filter and if the path item is not specified in the
25+
* OpenAPI components section.
26+
*/
27+
public class ProfileFilter implements OASFilter {
28+
29+
private final Map<String, PathItem> pathItemComponents;
30+
private final Set<String> included;
31+
private final Set<String> excluded;
32+
33+
public ProfileFilter(OpenAPI model, OpenApiConfig config) {
34+
pathItemComponents = Optional.ofNullable(model.getComponents())
35+
.map(Components::getPathItems)
36+
.orElseGet(Collections::emptyMap);
37+
38+
included = config.getScanProfiles();
39+
excluded = config.getScanExcludeProfiles();
40+
}
41+
42+
@Override
43+
public PathItem filterPathItem(PathItem pathItem) {
44+
boolean operationExcluded = false;
45+
46+
for (HttpMethod method : Set.copyOf(pathItem.getOperations().keySet())) {
47+
Operation o = pathItem.getOperations().get(method);
48+
49+
if (!Extensions.includedProfile(o, included, excluded)) {
50+
operationExcluded = true;
51+
pathItem.setOperation(method, null);
52+
}
53+
}
54+
55+
if (operationExcluded && pathItem.getOperations().isEmpty() && nonComponent(pathItem)) {
56+
// Only remove the path item if it is not a component that may be referenced elsewhere.
57+
return null;
58+
}
59+
60+
return pathItem;
61+
}
62+
63+
private boolean nonComponent(PathItem pathItem) {
64+
for (PathItem component : pathItemComponents.values()) {
65+
if (pathItem == component) {
66+
// If it's the same object, the given pathItem is in components
67+
return false;
68+
}
69+
}
70+
return true;
71+
}
72+
}

core/src/test/java/io/smallrye/openapi/runtime/scanner/spi/AbstractAnnotationScannerTest.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,6 @@ void testNoConfiguredProfile() {
121121
boolean result = AbstractAnnotationScanner.processProfiles(config, operation);
122122

123123
assertTrue(result);
124-
assertEquals(0, operation.getExtensions().size());
125124
}
126125

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

144143
assertTrue(result);
145-
assertEquals(0, operation.getExtensions().size());
146144
}
147145

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

165163
assertFalse(result);
166-
assertEquals(0, operation.getExtensions().size());
167164
}
168165

169166
@ParameterizedTest

extension-jaxrs/src/main/java/io/smallrye/openapi/jaxrs/JaxRsAnnotationScanner.java

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,19 @@ private void processResourceMethod(final ClassInfo resourceClass,
496496
// Process tags - @Tag and @Tags annotations combines with the resource tags we've already found (passed in)
497497
processOperationTags(context, method, context.getOpenApi(), resourceTags, operation);
498498

499+
// Process @Extension annotations
500+
processExtensions(context, method, operation);
501+
502+
if (!processProfiles(context.getConfig(), operation)) {
503+
// Any operations not included by a profile (if configured) will also be filtered out
504+
// later during the filter stage so that profile included/exclusion takes into account
505+
// the static file and the OASReader models. This short-circuit is left in as an
506+
// optimization to avoid additional scanning when the operation would have been
507+
// excluded anyway. As a result, profile configurations are built-time only for Quarkus
508+
// applications.
509+
return;
510+
}
511+
499512
// Process @Parameter annotations.
500513
List<Parameter> operationParams = params.getOperationParameters();
501514
operation.setParameters(operationParams);
@@ -524,19 +537,12 @@ private void processResourceMethod(final ClassInfo resourceClass,
524537
// Process @Server annotations
525538
processServerAnnotation(context, method, operation);
526539

527-
// Process @Extension annotations
528-
processExtensions(context, method, operation);
529-
530540
// Process Security Roles
531541
context.getJavaSecurityProcessor().processSecurityRoles(method, operation);
532542

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

536-
if (!processProfiles(context.getConfig(), operation)) {
537-
return;
538-
}
539-
540546
// When processing a sub-resource tree, ignore any @Path information from the current class
541547
List<String> operationPaths = this.subResourceStack.isEmpty() ? params.getFullOperationPaths()
542548
: params.getOperationPaths();
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package io.smallrye.openapi.runtime.scanner;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertTrue;
5+
6+
import java.util.Collections;
7+
import java.util.List;
8+
9+
import jakarta.ws.rs.GET;
10+
import jakarta.ws.rs.Path;
11+
12+
import org.eclipse.microprofile.openapi.OASConfig;
13+
import org.eclipse.microprofile.openapi.OASFactory;
14+
import org.eclipse.microprofile.openapi.OASModelReader;
15+
import org.eclipse.microprofile.openapi.annotations.extensions.Extension;
16+
import org.eclipse.microprofile.openapi.models.OpenAPI;
17+
import org.eclipse.microprofile.openapi.models.media.Schema.SchemaType;
18+
import org.jboss.jandex.Index;
19+
import org.junit.jupiter.api.Test;
20+
21+
import io.smallrye.openapi.api.SmallRyeOASConfig;
22+
import io.smallrye.openapi.api.SmallRyeOpenAPI;
23+
24+
class ProfileSelectionWithStaticModelTest {
25+
26+
public static class MyReader implements OASModelReader {
27+
@Override
28+
public OpenAPI buildModel() {
29+
return OASFactory.createOpenAPI()
30+
.components(OASFactory.createComponents()
31+
.addPathItem("Orders", OASFactory.createPathItem()
32+
.GET(OASFactory.createOperation()
33+
.responses(OASFactory.createAPIResponses()
34+
.addAPIResponse("default", OASFactory.createAPIResponse()
35+
.content(OASFactory.createContent()
36+
.addMediaType("text/plain", OASFactory.createMediaType()
37+
.schema(OASFactory.createSchema()
38+
.type(List.of(SchemaType.STRING))))))))));
39+
}
40+
}
41+
42+
@Test
43+
void testStaticModelOperationExcluded() throws Exception {
44+
@Path("/api")
45+
class MyResource {
46+
@Path("/users")
47+
@GET
48+
@Extension(name = "x-smallrye-profile-public", value = "")
49+
public List<String> listUsers() {
50+
return Collections.emptyList();
51+
}
52+
}
53+
54+
SmallRyeOpenAPI result;
55+
56+
try {
57+
System.setProperty(SmallRyeOASConfig.SCAN_PROFILES, "public");
58+
System.setProperty(OASConfig.MODEL_READER, MyReader.class.getName());
59+
60+
result = SmallRyeOpenAPI.builder()
61+
.enableStandardFilter(false)
62+
.enableStandardStaticFiles(false)
63+
.withCustomStaticFile(() -> {
64+
return getClass()
65+
.getClassLoader()
66+
.getResourceAsStream(
67+
"io/smallrye/openapi/runtime/scanner/static/profile-selection-static-model.yaml");
68+
})
69+
.enableAnnotationScan(true)
70+
.withIndex(Index.of(MyResource.class))
71+
.build();
72+
} finally {
73+
System.clearProperty(SmallRyeOASConfig.SCAN_PROFILES);
74+
System.clearProperty(OASConfig.MODEL_READER);
75+
}
76+
77+
OpenAPI model = result.model();
78+
79+
var paths = model.getPaths().getPathItems();
80+
assertEquals(2, paths.size(), () -> paths.keySet().toString());
81+
assertTrue(paths.containsKey("/api/users"), () -> paths.keySet().toString());
82+
assertTrue(paths.containsKey("/api/public/echo"), () -> paths.keySet().toString());
83+
84+
var componentPaths = model.getComponents().getPathItems();
85+
assertEquals(1, componentPaths.size());
86+
var orderOperations = componentPaths.get("Orders").getOperations();
87+
// removed by filter
88+
assertTrue(orderOperations.isEmpty());
89+
}
90+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
openapi: 3.0.3
2+
info:
3+
title: echo
4+
version: '1.0.0'
5+
description: ""
6+
paths:
7+
/api/public/echo:
8+
get:
9+
summary: Echo GET
10+
operationId: get-echo
11+
x-smallrye-profile-public: ""
12+
parameters:
13+
- name: Message
14+
in: query
15+
schema:
16+
type: string
17+
options:
18+
summary: Echo OPTIONS
19+
operationId: options-echo
20+
description: Echo OPTIONS not for public use!
21+
22+
/api/private/echo:
23+
post:
24+
summary: Echo POST
25+
operationId: post-echo
26+
requestBody:
27+
content:
28+
application/json:
29+
schema:
30+
$ref: "#/components/schemas/Message"
31+
responses:
32+
"200":
33+
description: OK
34+
content:
35+
application/json:
36+
schema:
37+
$ref: '#/components/schemas/Echo'
38+
components:
39+
schemas:
40+
Echo:
41+
type: object
42+
properties:
43+
echo:
44+
type: string
45+
Message:
46+
type: object
47+
properties:
48+
message:
49+
type: string

0 commit comments

Comments
 (0)