Skip to content

Commit

Permalink
Add support for removing unused models
Browse files Browse the repository at this point in the history
Fixes smallrye#1183

Signed-off-by: Michael Edgar <michael@xlate.io>
  • Loading branch information
MikeEdgar committed Aug 8, 2022
1 parent 2c1c6c6 commit 32055a8
Show file tree
Hide file tree
Showing 9 changed files with 232 additions and 1 deletion.
1 change: 1 addition & 0 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,4 @@ mvn clean install
** A standard JSON-B naming strategy (listed in `jakarta.json.bind.config.PropertyNamingStrategy`/`javax.json.bind.config.PropertyNamingStrategy`)
** A fully-qualified class name of an implementation of a JSON-B property naming strategy (`jakarta.json.bind.config.PropertyNamingStrategy` or `javax.json.bind.config.PropertyNamingStrategy`)
** A fully-qualified class name of an implementation of a Jackson property naming strategy base class (`com.fasterxml.jackson.databind.PropertyNamingStrategies.NamingBase`). Only the `translate` method is utilized.
* `mp.openapi.extensions.smallrye.remove-unused-schemas.enable` - Set to `true` enable automatic removal of unused schemas from `components/schemas` in the OpenAPI model. Unused schemas will be removed following annotation scanning but prior to running any `OASFilter` that may be configured. Default value is `false`.
4 changes: 4 additions & 0 deletions core/src/main/java/io/smallrye/openapi/api/OpenApiConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,10 @@ default Set<String> getScanExcludeProfiles() {
return new HashSet<>();
}

default boolean removeUnusedSchemas() {
return false;
}

default void doAllowNakedPathParameter() {
}

Expand Down
11 changes: 11 additions & 0 deletions core/src/main/java/io/smallrye/openapi/api/OpenApiConfigImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public class OpenApiConfigImpl implements OpenApiConfig {
private OperationIdStrategy operationIdStrategy;
private Set<String> scanProfiles;
private Set<String> scanExcludeProfiles;
private Boolean removeUnusedSchemas;
private Optional<String[]> defaultProduces = UNSET;
private Optional<String[]> defaultConsumes = UNSET;
private Optional<Boolean> allowNakedPathParameter = Optional.empty();
Expand Down Expand Up @@ -438,6 +439,16 @@ public Set<String> getScanExcludeProfiles() {
return scanExcludeProfiles;
}

@Override
public boolean removeUnusedSchemas() {
if (removeUnusedSchemas == null) {
removeUnusedSchemas = getConfig()
.getOptionalValue(OpenApiConstants.SMALLRYE_REMOVE_UNUSED_SCHEMAS, Boolean.class)
.orElse(OpenApiConfig.super.removeUnusedSchemas());
}
return removeUnusedSchemas;
}

/**
* getConfig().getOptionalValue(key) can return "" if optional {@link Converter}s are used. Enforce a null value if
* we get an empty string back.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import io.smallrye.openapi.api.util.ConfigUtil;
import io.smallrye.openapi.api.util.FilterUtil;
import io.smallrye.openapi.api.util.MergeUtil;
import io.smallrye.openapi.api.util.UnusedSchemaFilter;

/**
* Holds the final OpenAPI document produced during the startup of the app.
Expand Down Expand Up @@ -166,9 +167,12 @@ public synchronized void initialize() {
* @param model
*/
private OpenAPI filterModel(OpenAPI model) {
if (model == null || filters.isEmpty()) {
if (model == null) {
return model;
}
if (config.removeUnusedSchemas()) {
model = FilterUtil.applyFilter(new UnusedSchemaFilter(), model);
}
for (OASFilter filter : filters.values()) {
model = FilterUtil.applyFilter(filter, model);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public final class OpenApiConstants {
public static final String SUFFIX_PRIVATE_PROPERTIES_ENABLE = "private-properties.enable";
public static final String SUFFIX_PROPERTY_NAMING_STRATEGY = "property-naming-strategy";
public static final String SUFFIX_SORTED_PROPERTIES_ENABLE = "sorted-properties.enable";
public static final String SUFFIX_REMOVE_UNUSED_SCHEMAS_ENABLE = "remove-unused-schemas.enable";

public static final String SCAN_DEPENDENCIES_DISABLE = OASConfig.EXTENSIONS_PREFIX + SUFFIX_SCAN_DEPENDENCIES_DISABLE;
public static final String SCAN_DEPENDENCIES_JARS = OASConfig.EXTENSIONS_PREFIX + SUFFIX_SCAN_DEPENDENCIES_JARS;
Expand All @@ -42,6 +43,7 @@ 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 SMALLRYE_REMOVE_UNUSED_SCHEMAS = SMALLRYE_PREFIX + SUFFIX_REMOVE_UNUSED_SCHEMAS_ENABLE;
public static final String SCAN_PROFILES = SMALLRYE_PREFIX + "scan.profiles";
public static final String SCAN_EXCLUDE_PROFILES = SMALLRYE_PREFIX + "scan.exclude.profiles";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,7 @@ private static void filterSchema(OASFilter filter, Schema model) {
model::setAdditionalPropertiesSchema);
filter(filter, model.getAllOf(), FilterUtil::filterSchema, filter::filterSchema, model::removeAllOf);
filter(filter, model.getAnyOf(), FilterUtil::filterSchema, filter::filterSchema, model::removeAnyOf);
filter(filter, model.getOneOf(), FilterUtil::filterSchema, filter::filterSchema, model::removeOneOf);
filter(filter, model.getItems(), FilterUtil::filterSchema, filter::filterSchema, model::setItems);
filter(filter, model.getNot(), FilterUtil::filterSchema, filter::filterSchema, model::setNot);
filter(filter, model.getProperties(), FilterUtil::filterSchema, filter::filterSchema, model::removeProperty);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package io.smallrye.openapi.api.util;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

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.media.Schema;

import io.smallrye.openapi.runtime.util.ModelUtil;

public class UnusedSchemaFilter implements OASFilter {

/**
* Map of schemas present in {@code /components/schemas} with a list of the
* schemas that refer to them.
*/
Map<String, List<Schema>> references = new HashMap<>();

@Override
public Schema filterSchema(Schema schema) {
String name = referencedName(schema);

if (name != null) {
references.computeIfAbsent(name, k -> new ArrayList<>()).add(schema);
}

return schema;
}

@Override
public void filterOpenAPI(OpenAPI openAPI) {
final Components components = openAPI.getComponents();

Optional.ofNullable(components)
.map(Components::getSchemas)
.map(Map::keySet)
.ifPresent(schemaNames -> {
Set<String> unusedNames = unusedSchemaNames(schemaNames);

while (!unusedNames.isEmpty()) {
unusedNames.forEach(name -> remove(name, components));
unusedNames = unusedSchemaNames(schemaNames);
}
});
}

String referencedName(Schema schema) {
final String ref = schema.getRef();

if (ref != null && ref.startsWith("#/components/schemas/")) {
return ModelUtil.nameFromRef(ref);
}

return null;
}

boolean notUsed(String schemaName) {
return !references.containsKey(schemaName);
}

Set<String> unusedSchemaNames(Set<String> allSchemaNames) {
return allSchemaNames.stream().filter(this::notUsed).collect(Collectors.toSet());
}

void remove(String schemaName, Components components) {
Schema unusedSchema = components.getSchemas().get(schemaName);
removeReference(unusedSchema.getAdditionalPropertiesSchema());
removeReferences(unusedSchema.getAllOf());
removeReferences(unusedSchema.getAnyOf());
removeReferences(unusedSchema.getOneOf());
removeReference(unusedSchema.getItems());
removeReference(unusedSchema.getNot());
removeReferences(unusedSchema.getProperties());
components.removeSchema(schemaName);
UtilLogging.logger.unusedSchemaRemoved(schemaName);
}

void removeReference(Schema schema) {
if (schema != null) {
String name = referencedName(schema);

if (name != null) {
references.computeIfPresent(name, (k, v) -> {
v.remove(schema);
return v.isEmpty() ? null : v;
});
}
}
}

void removeReferences(Map<String, Schema> schemas) {
if (schemas != null) {
schemas.values().forEach(this::removeReference);
}
}

void removeReferences(List<Schema> schemas) {
if (schemas != null) {
schemas.forEach(this::removeReference);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,9 @@ interface UtilLogging extends BasicLogger {
@LogMessage(level = Logger.Level.ERROR)
@Message(id = 1000, value = "Failed to introspect BeanInfo for: %s")
void failedToIntrospectBeanInfo(Class<?> clazz, @Cause Throwable cause);

@LogMessage(level = Logger.Level.INFO)
@Message(id = 1001, value = "Schema with zero references removed from #/components/schemas: %s")
void unusedSchemaRemoved(String name);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package io.smallrye.openapi.api.util;

import static org.eclipse.microprofile.openapi.OASFactory.createAPIResponse;
import static org.eclipse.microprofile.openapi.OASFactory.createAPIResponses;
import static org.eclipse.microprofile.openapi.OASFactory.createComponents;
import static org.eclipse.microprofile.openapi.OASFactory.createContent;
import static org.eclipse.microprofile.openapi.OASFactory.createMediaType;
import static org.eclipse.microprofile.openapi.OASFactory.createOpenAPI;
import static org.eclipse.microprofile.openapi.OASFactory.createOperation;
import static org.eclipse.microprofile.openapi.OASFactory.createPathItem;
import static org.eclipse.microprofile.openapi.OASFactory.createPaths;
import static org.eclipse.microprofile.openapi.OASFactory.createSchema;
import static org.junit.jupiter.api.Assertions.assertEquals;

import org.eclipse.microprofile.openapi.models.OpenAPI;
import org.eclipse.microprofile.openapi.models.media.Schema.SchemaType;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

class UnusedSchemaFilterTest {

UnusedSchemaFilter target;
OpenAPI openAPI;

@BeforeEach
void setUp() throws Exception {
target = new UnusedSchemaFilter();

openAPI = createOpenAPI();
openAPI.paths(createPaths()
.addPathItem("/data", createPathItem()
.GET(createOperation()
.responses(createAPIResponses()
.addAPIResponse("200", createAPIResponse()
.content(createContent()
.addMediaType("text/plain", createMediaType()
.schema(createSchema().ref("#/components/schemas/Data")))
.addMediaType("text/html", createMediaType()
.schema(createSchema().ref(
"http://example.com/schemas/Data?type=html")))))))))
.components(createComponents()
.addSchema("Data", createSchema()
.type(SchemaType.STRING)
.description("The data returned by the API")));
}

@Test
void testUnusedSchemaPropertyRemoved() {
openAPI.getComponents()
.addSchema("RemovedSchema", createSchema()
.type(SchemaType.OBJECT)
.description("Schema to be removed, pass 1")
.addProperty("prop1", createSchema()
.ref("#/components/schemas/RemovedPropertySchema"))
.addProperty("prop2", createSchema()
.ref("#/components/schemas/RemovedPropertySchema")))
.addSchema("RemovedPropertySchema", createSchema()
.type(SchemaType.STRING)
.description("Schema to be removed, pass 2"));

assertEquals(3, openAPI.getComponents().getSchemas().size());

openAPI = FilterUtil.applyFilter(target, openAPI);
assertEquals(1, openAPI.getComponents().getSchemas().size());
assertEquals("Data", openAPI.getComponents().getSchemas().keySet().iterator().next());
}

@Test
void testUnusedOneOfSchemasRemoved() {
openAPI.getComponents()
.addSchema("RemovedSchema", createSchema()
.type(SchemaType.OBJECT)
.description("Schema to be removed, pass 1")
.addOneOf(createSchema()
.ref("#/components/schemas/RemovedAllOfSchema1"))
.addOneOf(createSchema()
.ref("#/components/schemas/RemovedAllOfSchema2"))
.addOneOf(createSchema()
.type(SchemaType.BOOLEAN)))
.addSchema("RemovedAllOfSchema1", createSchema()
.type(SchemaType.INTEGER)
.description("Schema to be removed, pass 2"))
.addSchema("RemovedAllOfSchema2", createSchema()
.type(SchemaType.STRING)
.description("Schema to be removed, pass 2"));

assertEquals(4, openAPI.getComponents().getSchemas().size());

openAPI = FilterUtil.applyFilter(target, openAPI);
assertEquals(1, openAPI.getComponents().getSchemas().size());
assertEquals("Data", openAPI.getComponents().getSchemas().keySet().iterator().next());
}

}

0 comments on commit 32055a8

Please sign in to comment.