All Downloads are FREE. Search and download functionalities are using the official Maven repository.

software.amazon.smithy.diff.evaluators.TraitBreakingChange Maven / Gradle / Ivy

Go to download

This module detects differences between two Smithy models, identifying changes that are safe and changes that are backward incompatible.

There is a newer version: 1.53.0
Show newest version
/*
 * Copyright 2022 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.diff.evaluators;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import software.amazon.smithy.diff.Differences;
import software.amazon.smithy.model.FromSourceLocation;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.node.Node;
import software.amazon.smithy.model.shapes.CollectionShape;
import software.amazon.smithy.model.shapes.MapShape;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.traits.Trait;
import software.amazon.smithy.model.traits.TraitDefinition;
import software.amazon.smithy.model.validation.ValidationEvent;
import software.amazon.smithy.utils.StringUtils;

/**
 * Finds breaking changes related to when a trait is added, removed, or
 * updated based on the breakingChanges property of traits.
 */
public final class TraitBreakingChange extends AbstractDiffEvaluator {

    private static final List ANY_TYPES = Arrays.asList(
            TraitDefinition.ChangeType.ADD,
            TraitDefinition.ChangeType.REMOVE,
            TraitDefinition.ChangeType.UPDATE);

    private static final List PRESENCE_TYPES = Arrays.asList(
            TraitDefinition.ChangeType.ADD,
            TraitDefinition.ChangeType.REMOVE);

    @Override
    public List evaluate(Differences differences) {
        List events = new ArrayList<>();

        differences.changedShapes().forEach(changedShape -> {
            changedShape.getTraitDifferences().forEach((traitId, oldTraitNewTraitPair) -> {
                Trait oldTrait = oldTraitNewTraitPair.left;
                Trait newTrait = oldTraitNewTraitPair.right;
                // Use the breaking changes rules of the new trait.
                differences.getNewModel().getShape(traitId).ifPresent(traitShape -> {
                    List rules = traitShape
                            .expectTrait(TraitDefinition.class)
                            .getBreakingChanges();
                    for (TraitDefinition.BreakingChangeRule rule : rules) {
                        PathChecker checker = new PathChecker(differences.getNewModel(), traitShape,
                                                              changedShape.getNewShape(), rule, events);
                        checker.check(Node.from(oldTrait), Node.from(newTrait));
                    }
                });
            });
        });

        return events;
    }

    private static final class PathChecker {
        private final Model model;
        private final Shape trait;
        private final Shape targetShape;
        private final TraitDefinition.BreakingChangeRule rule;
        private final List events;
        private final List segements;

        PathChecker(
                Model model,
                Shape trait,
                Shape targetShape,
                TraitDefinition.BreakingChangeRule rule,
                List events
        ) {
            this.model = model;
            this.trait = trait;
            this.targetShape = targetShape;
            this.rule = rule;
            this.events = events;
            this.segements = rule.getDefaultedPath().getParts();
        }

        private void check(Node left, Node right) {
            // Only perform nested diffs if the right node was found.
            if (right.isNullNode() && !segements.isEmpty()) {
                return;
            }

            Map leftValues = new TreeMap<>();
            Map rightValues = new TreeMap<>();
            extract(leftValues, trait, 0, left, "");
            extract(rightValues, trait, 0, right, "");

            // Compare values that exist only in left or in both.
            for (Map.Entry entry : leftValues.entrySet()) {
                Node rightValue = rightValues.getOrDefault(entry.getKey(), Node.nullNode());
                compareResult(entry.getKey(), entry.getValue(), rightValue);
            }

            // Find newly added values.
            for (Map.Entry entry : rightValues.entrySet()) {
                if (!leftValues.containsKey(entry.getKey())) {
                    compareResult(entry.getKey(), Node.nullNode(), entry.getValue());
                }
            }
        }

        private void extract(
                Map result,
                Shape currentShape,
                int segmentPosition,
                Node currentValue,
                String path
        ) {
            // Don't keep crawling when a "" segment is hit or the last segment is hit.
            if (segmentPosition >= segements.size() || segements.get(segmentPosition).isEmpty()) {
                result.put(path, currentValue);
                return;
            }

            String segment = segements.get(segmentPosition);
            currentShape.getMember(segment).flatMap(m -> model.getShape(m.getTarget())).ifPresent(nextShape -> {
                if (currentShape instanceof CollectionShape) {
                    currentValue.asArrayNode().ifPresent(v -> {
                        for (int i = 0; i < v.size(); i++) {
                            Node value = v.get(i).get();
                            extract(result, nextShape, segmentPosition + 1, value, path + "/" + i);
                        }
                    });
                } else if (currentShape instanceof MapShape) {
                    currentValue.asObjectNode().ifPresent(v -> {
                        for (Map.Entry entry : v.getStringMap().entrySet()) {
                            extract(result, nextShape, segmentPosition + 1,
                                    entry.getValue(), path + "/" + entry.getKey());
                        }
                    });
                } else if (currentShape.isStructureShape() || currentShape.isUnionShape()) {
                    currentValue.asObjectNode().ifPresent(v -> {
                        extract(result, nextShape, segmentPosition + 1, v.getMember(segment).orElse(Node.nullNode()),
                                path + "/" + segment);
                    });
                }
            });
        }

        private void compareResult(String path, Node left, Node right) {
            if (!left.isNullNode() || !right.isNullNode()) {
                TraitDefinition.ChangeType type = isChangeBreaking(rule.getChange(), left, right);
                if (type != null) {
                    String message = createBreakingMessage(type, path, left, right);
                    if (rule.getMessage().isPresent()) {
                        if (!message.endsWith(".")) {
                            message = message + "; ";
                        }
                        message = message + rule.getMessage().get();
                    }
                    FromSourceLocation location = !right.isNullNode() ? right : targetShape;
                    events.add(ValidationEvent.builder()
                                       .id(getValidationEventId(type))
                                       .severity(rule.getDefaultedSeverity())
                                       .shape(targetShape)
                                       .sourceLocation(location)
                                       .message(message)
                                       .build());
                }
            }
        }

        private String getValidationEventId(TraitDefinition.ChangeType type) {
            return String.format("%s.%s.%s", TraitBreakingChange.class.getSimpleName(),
                    StringUtils.capitalize(type.toString()), trait.getId());
        }

        // Check if a breaking change was encountered, and return the type of breaking change.
        private TraitDefinition.ChangeType isChangeBreaking(TraitDefinition.ChangeType type, Node left, Node right) {
            switch (type) {
                case ADD:
                    return left.isNullNode() && !right.isNullNode() ? type : null;
                case REMOVE:
                    return right.isNullNode() && !left.isNullNode() ? type : null;
                case UPDATE:
                    return !left.isNullNode() && !right.isNullNode() && !left.equals(right) ? type : null;
                case ANY:
                    for (TraitDefinition.ChangeType checkType : ANY_TYPES) {
                        if (isChangeBreaking(checkType, left, right) != null) {
                            return checkType;
                        }
                    }
                    return null;
                case PRESENCE:
                    for (TraitDefinition.ChangeType checkType : PRESENCE_TYPES) {
                        if (isChangeBreaking(checkType, left, right) != null) {
                            return checkType;
                        }
                    }
                default:
                    return null;
            }
        }

        private String createBreakingMessage(TraitDefinition.ChangeType type, String path, Node left, Node right) {
            String leftPretty = Node.prettyPrintJson(left.toNode());
            String rightPretty = Node.prettyPrintJson(right.toNode());

            switch (type) {
                case ADD:
                    if (!path.isEmpty()) {
                        return String.format("Added trait contents to `%s` at path `%s` with value %s",
                                             trait.getId(), path, rightPretty);
                    } else if (rightPretty.equals("{}")) {
                        return String.format("Added trait `%s`", trait.getId());
                    } else {
                        return String.format("Added trait `%s` with value %s", trait.getId(), rightPretty);
                    }
                case REMOVE:
                    if (!path.isEmpty()) {
                        return String.format("Removed trait contents from `%s` at path `%s`. Removed value: %s",
                                             trait.getId(), path, leftPretty);
                    } else if (leftPretty.equals("{}")) {
                        return String.format("Removed trait `%s`", trait.getId());
                    } else {
                        return String.format("Removed trait `%s`. Previous trait value: %s",
                                             trait.getId(), leftPretty);
                    }
                case UPDATE:
                    if (!path.isEmpty()) {
                        return String.format("Changed trait contents of `%s` at path `%s` from %s to %s",
                                             trait.getId(), path, leftPretty, rightPretty);
                    } else {
                        return String.format("Changed trait `%s` from %s to %s",
                                             trait.getId(), leftPretty, rightPretty);
                    }
                default:
                    throw new UnsupportedOperationException("Expected add, remove, update: " + type);
            }
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy