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

software.amazon.smithy.model.validation.validators.HttpUriConflictValidator 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.validation.validators;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.knowledge.HttpBinding;
import software.amazon.smithy.model.knowledge.HttpBindingIndex;
import software.amazon.smithy.model.knowledge.OperationIndex;
import software.amazon.smithy.model.knowledge.TopDownIndex;
import software.amazon.smithy.model.pattern.SmithyPattern;
import software.amazon.smithy.model.pattern.SmithyPattern.Segment;
import software.amazon.smithy.model.pattern.UriPattern;
import software.amazon.smithy.model.shapes.MemberShape;
import software.amazon.smithy.model.shapes.OperationShape;
import software.amazon.smithy.model.shapes.ServiceShape;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.model.traits.EndpointTrait;
import software.amazon.smithy.model.traits.HostLabelTrait;
import software.amazon.smithy.model.traits.HttpTrait;
import software.amazon.smithy.model.traits.PatternTrait;
import software.amazon.smithy.model.validation.AbstractValidator;
import software.amazon.smithy.model.validation.ValidationEvent;
import software.amazon.smithy.utils.OptionalUtils;
import software.amazon.smithy.utils.Pair;

/**
 * Validates that no two URIs in a service conflict with each other.
 */
public final class HttpUriConflictValidator extends AbstractValidator {

    @Override
    public List validate(Model model) {
        if (!model.isTraitApplied(HttpTrait.class)) {
            return Collections.emptyList();
        }

        return model.shapes(ServiceShape.class)
                .flatMap(shape -> validateService(model, shape).stream())
                .collect(Collectors.toList());
    }

    private List validateService(Model model, ServiceShape service) {
        List operations = new ArrayList<>();
        for (OperationShape operation : TopDownIndex.of(model).getContainedOperations(service)) {
            if (operation.hasTrait(HttpTrait.class)) {
                operations.add(operation);
            }
        }

        List events = new ArrayList<>();
        for (OperationShape operation : operations) {
            events.addAll(checkConflicts(model, operation, operation.expectTrait(HttpTrait.class), operations));
        }

        return events;
    }

    private List checkConflicts(
            Model model,
            OperationShape operation,
            HttpTrait httpTrait,
            List operations
    ) {
        String method = httpTrait.getMethod();
        UriPattern pattern = httpTrait.getUri();

        // Some conflicts are potentially allowable, so we split them up into to lists.
        List> conflicts = new ArrayList<>();
        List> allowableConflicts = new ArrayList<>();

        for (OperationShape other : operations) {
            if (other != operation && other.hasTrait(HttpTrait.class)) {
                HttpTrait otherHttpTrait = other.expectTrait(HttpTrait.class);
                if (otherHttpTrait.getMethod().equals(method)
                        && otherHttpTrait.getUri().conflictsWith(pattern)
                        && endpointConflicts(model, operation, other)) {
                    // Now that we know we have a conflict, determine whether it is allowable or not.
                    if (isAllowableConflict(model, operation, other)) {
                        allowableConflicts.add(Pair.of(other.getId(), otherHttpTrait.getUri()));
                    } else {
                        conflicts.add(Pair.of(other.getId(), otherHttpTrait.getUri()));
                    }
                }
            }
        }

        // Non-allowable conflicts get turned into ERRORs, they must be resolved to pass validation.
        List events = new ArrayList<>();
        if (!conflicts.isEmpty()) {
            events.add(error(operation, formatConflicts(pattern, conflicts)));
        }

        // Allowable conflicts get turned into DANGERs, which must at least be acknowledged with a suppression.
        if (!allowableConflicts.isEmpty()) {
            String message = formatConflicts(pattern, allowableConflicts)
                    + ". Pattern traits applied to the label members prevent the label value from evaluating to a "
                    + "conflict, but this is still a poor design. If this is acceptable, this can be suppressed.";
            events.add(danger(operation, message));
        }

        return events;
    }

    private boolean endpointConflicts(Model model, OperationShape operation, OperationShape otherOperation) {
        // If neither operation has the endpoint trait, then their endpoints do conflict.
        if (!operation.hasTrait(EndpointTrait.ID) && !otherOperation.hasTrait(EndpointTrait.ID)) {
            return true;
        }

        // If one, but not both, operations have the endpoint trait, then their endpoints can't conflict.
        if (operation.hasTrait(EndpointTrait.ID) ^ otherOperation.hasTrait(EndpointTrait.ID)) {
            return false;
        }

        // At this point we know both operations have the endpoint trait, so we need to check if
        // they conflict.
        SmithyPattern prefix = operation.getTrait(EndpointTrait.class).get().getHostPrefix();
        SmithyPattern otherPrefix = otherOperation.getTrait(EndpointTrait.class).get().getHostPrefix();
        boolean allowable = !isAllowableConflict(
                model, prefix, operation, otherPrefix, otherOperation, this::getHostLabelPatterns);
        return allowable;
    }

    private boolean isAllowableConflict(Model model, OperationShape operation, OperationShape otherOperation) {
        UriPattern uriPattern = operation.getTrait(HttpTrait.class).get().getUri();
        UriPattern otherUriPattern = otherOperation.getTrait(HttpTrait.class).get().getUri();
        return isAllowableConflict(
                model, uriPattern, operation, otherUriPattern, otherOperation, this::getHttpLabelPatterns);
    }

    private boolean isAllowableConflict(
            Model model,
            SmithyPattern pattern,
            OperationShape operation,
            SmithyPattern otherPattern,
            OperationShape otherOperation,
            BiFunction> getLabelPatterns
    ) {

        Map conflictingLabelSegments = pattern.getConflictingLabelSegmentsMap(otherPattern);

        // If there aren't any conflicting label segments that means the uris are identical, which is not allowable.
        if (conflictingLabelSegments.isEmpty()) {
            return false;
        }

        Map labelPatterns = getLabelPatterns.apply(model, operation);
        Map otherLabelPatterns = getLabelPatterns.apply(model, otherOperation);

        return conflictingLabelSegments.entrySet().stream()
                // Only allow conflicts in cases where one of the segments is static and the other is a label.
                .filter(conflict -> conflict.getKey().isLabel() != conflict.getValue().isLabel())
                // Only allow the uris to conflict if every conflicting segment is allowable.
                .allMatch(conflict -> {
                    Pattern p;
                    String staticSegment;

                    if (conflict.getKey().isLabel()) {
                        p = labelPatterns.get(conflict.getKey().getContent());
                        staticSegment = conflict.getValue().getContent();
                    } else {
                        p = otherLabelPatterns.get(conflict.getValue().getContent());
                        staticSegment = conflict.getKey().getContent();
                    }

                    if (p == null) {
                        return false;
                    }

                    // If the pattern on the label segment does not match the static segment, then this segment's
                    // conflict is allowable.
                    return !p.matcher(staticSegment).find();
                });
    }

    private Map getHttpLabelPatterns(Model model, OperationShape operation) {
        return HttpBindingIndex.of(model)
                .getRequestBindings(operation).entrySet().stream()
                .filter(entry -> entry.getValue().getLocation().equals(HttpBinding.Location.LABEL))
                .flatMap(entry -> OptionalUtils.stream(entry.getValue()
                        .getMember().getMemberTrait(model, PatternTrait.class)
                        .map(pattern -> Pair.of(entry.getKey(), pattern.getPattern()))))
                .collect(Collectors.toMap(Pair::getLeft, Pair::getRight));
    }

    private Map getHostLabelPatterns(Model model, OperationShape operation) {
        Map result = new HashMap<>();
        for (MemberShape member : OperationIndex.of(model).expectInputShape(operation).getAllMembers().values()) {
            if (member.hasTrait(HostLabelTrait.class) && member.hasTrait(PatternTrait.class)) {
                result.put(member.getMemberName(), member.expectTrait(PatternTrait.class).getPattern());
            }
        }
        return result;
    }

    private String formatConflicts(UriPattern pattern, List> conflicts) {
        String conflictString = conflicts.stream()
                .map(conflict -> String.format("`%s` (%s)", conflict.getLeft(), conflict.getRight()))
                .sorted()
                .collect(Collectors.joining(", "));

        return String.format(
                "Operation URI, `%s`, conflicts with other operation URIs in the same service: [%s]",
                pattern, conflictString);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy