software.amazon.smithy.model.transform.plugins.CleanStructureAndUnionMembers Maven / Gradle / Ivy
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package software.amazon.smithy.model.transform.plugins;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.mapping;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.shapes.EnumShape;
import software.amazon.smithy.model.shapes.IntEnumShape;
import software.amazon.smithy.model.shapes.MemberShape;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.model.shapes.StructureShape;
import software.amazon.smithy.model.shapes.UnionShape;
import software.amazon.smithy.model.transform.ModelTransformer;
import software.amazon.smithy.model.transform.ModelTransformerPlugin;
import software.amazon.smithy.utils.OptionalUtils;
import software.amazon.smithy.utils.Pair;
/**
* Cleans up structure, union, enum, and intEnum shapes after shapes are removed.
*
*
* - Ensures that structure and union shapes are updated to no
* longer reference any removed members.
* - Ensures that structure/union members that reference shapes
* that have been removed are also removed.
*
*/
public final class CleanStructureAndUnionMembers implements ModelTransformerPlugin {
@Override
public Model onRemove(ModelTransformer transformer, Collection removed, Model model) {
// Remove members from containers that have been removed from the index.
Model result = removeMembersFromContainers(transformer, removed, model);
// Remove members from containers when the member targets a removed shape.
return transformer.removeShapes(result, findMembersThatNeedRemoval(result, removed));
}
private Model removeMembersFromContainers(ModelTransformer transformer, Collection removed, Model model) {
List replacements = new ArrayList<>(getStructureReplacements(model, removed));
replacements.addAll(getUnionReplacements(model, removed));
replacements.addAll(getEnumReplacements(model, removed));
replacements.addAll(getIntEnumReplacements(model, removed));
return transformer.replaceShapes(model, replacements);
}
private Collection getEnumReplacements(Model model, Collection removed) {
return createUpdatedShapes(model, removed, Shape::asEnumShape, entry -> {
EnumShape.Builder builder = entry.getKey().toBuilder();
entry.getValue().forEach(member -> builder.removeMember(member.getMemberName()));
return builder.build();
});
}
private Collection getIntEnumReplacements(Model model, Collection removed) {
return createUpdatedShapes(model, removed, Shape::asIntEnumShape, entry -> {
IntEnumShape.Builder builder = entry.getKey().toBuilder();
entry.getValue().forEach(member -> builder.removeMember(member.getMemberName()));
return builder.build();
});
}
private Collection getStructureReplacements(Model model, Collection removed) {
return createUpdatedShapes(model, removed, Shape::asStructureShape, entry -> {
StructureShape.Builder builder = entry.getKey().toBuilder();
entry.getValue().forEach(member -> builder.removeMember(member.getMemberName()));
return builder.build();
});
}
private Collection getUnionReplacements(Model model, Collection removed) {
return createUpdatedShapes(model, removed, Shape::asUnionShape, entry -> {
UnionShape.Builder builder = entry.getKey().toBuilder();
entry.getValue().forEach(member -> builder.removeMember(member.getMemberName()));
return builder.build();
});
}
/**
* Finds structure and union shapes that have had members removed,
* converts them to a builder, and then returns new versions of those
* shapes without the members.
*
* The removal of members is done using a single transformation of a
* shape. For example, if multiple members are removed from a single
* structure shape, the structure shape is rebuilt a single time. This is
* done by first grouping the members into a map of {@code S} to a list
* of members that were removed, allowing the grouped members to be
* removed in a single pass.
*
* @param model Model used to get the container.
* @param removed The collection of shapes that were removed.
* @param containerShapeMapper A function that accepts a shape and tries
* to convert it to {@code S}.
* @param entryMapperAndFactory A function that takes {@code S} and create
* a new version without the members.
* @param The shape type being transformed.
* @return Returns a list of shapes that need to be modified in the model.
*/
private Collection createUpdatedShapes(
Model model,
Collection removed,
Function> containerShapeMapper,
Function>, S> entryMapperAndFactory
) {
return removed.stream()
.flatMap(shape -> OptionalUtils.stream(shape.asMemberShape()))
.flatMap(member -> OptionalUtils.stream(model.getShape(member.getContainer())
.flatMap(containerShapeMapper)
.map(container -> Pair.of(container, member))))
.collect(groupingBy(Pair::getLeft, mapping(Pair::getRight, Collectors.toList())))
.entrySet()
.stream()
.map(entryMapperAndFactory)
.collect(Collectors.toList());
}
/**
* Find members that target a shape that was removed, and ensure
* that the member is removed.
*
* @param model Model to check.
* @param removed The shapes that were removed.
* @return Returns the member shapes that need to be removed because
* their target was removed.
*/
private Collection findMembersThatNeedRemoval(Model model, Collection removed) {
Set removedIds = removed.stream().map(Shape::getId).collect(Collectors.toSet());
Collection removeMembers = new HashSet<>();
model.shapes(StructureShape.class)
.flatMap(shape -> shape.getAllMembers().values().stream())
.filter(value -> removedIds.contains(value.getTarget()))
.forEach(removeMembers::add);
model.shapes(UnionShape.class)
.flatMap(shape -> shape.getAllMembers().values().stream())
.filter(value -> removedIds.contains(value.getTarget()))
.forEach(removeMembers::add);
return removeMembers;
}
}