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

software.amazon.smithy.model.validation.validators.ResourceOperationInputOutputValidator Maven / Gradle / Ivy

/*
 * 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.model.validation.validators;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.stream.Collectors;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.knowledge.IdentifierBindingIndex;
import software.amazon.smithy.model.knowledge.PropertyBindingIndex;
import software.amazon.smithy.model.shapes.MemberShape;
import software.amazon.smithy.model.shapes.OperationShape;
import software.amazon.smithy.model.shapes.ResourceShape;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.model.validation.AbstractValidator;
import software.amazon.smithy.model.validation.ValidationEvent;

/**
 * Validates that resource properties are correctly used in resource-bound operations.
 */
public final class ResourceOperationInputOutputValidator extends AbstractValidator {

    @Override
    public List validate(Model model) {
        List events = new LinkedList<>();
        for (ResourceShape resourceShape : model.getResourceShapes()) {
            if (resourceShape.hasProperties()) {
                events.addAll(validateResource(model, resourceShape));
            }
        }
        return events;
    }

    private List validateResource(Model model, ResourceShape resource) {
        List events = new ArrayList<>();
        Set propertiesInOperations = new TreeSet<>();
        PropertyBindingIndex propertyBindingIndex = PropertyBindingIndex.of(model);

        processLifecycleOperationProperties(model, resource, "put", resource.getPut(),
            propertyBindingIndex, propertiesInOperations, events);
        processLifecycleOperationProperties(model, resource, "create", resource.getCreate(),
            propertyBindingIndex, propertiesInOperations, events);
        processLifecycleOperationProperties(model, resource, "read", resource.getRead(),
            propertyBindingIndex, propertiesInOperations, events);
        processLifecycleOperationProperties(model, resource, "update", resource.getUpdate(),
            propertyBindingIndex, propertiesInOperations, events);
        processLifecycleOperationProperties(model, resource, "delete", resource.getDelete(),
            propertyBindingIndex, propertiesInOperations, events);
        for (ShapeId operationId : resource.getOperations()) {
            processLifecycleOperationProperties(model, resource, operationId.getName(), Optional.of(operationId),
                propertyBindingIndex, propertiesInOperations, events);
        }

        Set definedProperties = new HashSet<>(resource.getProperties().keySet());
        definedProperties.removeAll(propertiesInOperations);
        for (String propertyNotInLifecycleOp : definedProperties) {
            events.add(error(resource, String.format("Resource property `%s` is not used in the input or output"
                    + " of create or an instance operation.", propertyNotInLifecycleOp)));
        }

        return events;
    }

    private void processLifecycleOperationProperties(
        Model model,
        ResourceShape resource,
        String name,
        Optional operationShapeId,
        PropertyBindingIndex propertyBindingIndex,
        Set propertiesInOperations,
        List events
    ) {
        operationShapeId.flatMap(model::getShape).flatMap(Shape::asOperationShape).ifPresent(operation -> {
            propertiesInOperations.addAll(getAllOperationProperties(propertyBindingIndex, operation));
            validateOperationInputOutput(model, propertyBindingIndex, resource, operation,
                    name, events);
        });
    }

    private List getAllOperationProperties(
            PropertyBindingIndex propertyBindingIndex,
            OperationShape operation
    ) {
        List properties = new ArrayList<>();
        for (MemberShape member : propertyBindingIndex.getInputPropertiesShape(operation).members()) {
            if (propertyBindingIndex.isMemberShapeProperty(member)) {
                properties.add(propertyBindingIndex.getPropertyName(member.getId()).get());
            }
        }
        for (MemberShape member : propertyBindingIndex.getOutputPropertiesShape(operation).members()) {
            if (propertyBindingIndex.isMemberShapeProperty(member)) {
                properties.add(propertyBindingIndex.getPropertyName(member.getId()).get());
            }
        }
        return properties;
    }

    private void validateOperationInputOutput(
            Model model,
            PropertyBindingIndex propertyBindingIndex,
            ResourceShape resource,
            OperationShape operation,
            String lifecycleOperationName,
            List events
    ) {
        validateOperationInput(model, propertyBindingIndex, resource, operation, lifecycleOperationName, events);
        validateOperationOutput(model, propertyBindingIndex, resource, operation, lifecycleOperationName, events);
    }

    private void validateOperationOutput(
            Model model,
            PropertyBindingIndex propertyBindingIndex,
            ResourceShape resource,
            OperationShape operation,
            String lifecycleOperationName,
            List events
    ) {
        Map properties = resource.getProperties();
        Map> propertyToMemberMappings = new TreeMap<>();
        IdentifierBindingIndex identifierBindingIndex = IdentifierBindingIndex.of(model);
        Set identifierMembers = new HashSet<>(identifierBindingIndex
            .getOperationOutputBindings(resource, operation).values());

        Shape shape = propertyBindingIndex.getOutputPropertiesShape(operation);
        for (MemberShape member : shape.members()) {
            if (propertyBindingIndex.isMemberShapeProperty(member)) {
                validateMember(events, lifecycleOperationName, propertyBindingIndex, resource, member,
                        identifierMembers, properties, propertyToMemberMappings);
            }
        }
        validateConflictingProperties(events, shape, propertyToMemberMappings);
    }

    private void validateOperationInput(
            Model model,
            PropertyBindingIndex propertyBindingIndex,
            ResourceShape resource,
            OperationShape operation,
            String lifecycleOperationName,
            List events
    ) {
        Map properties = resource.getProperties();
        Map> propertyToMemberMappings = new TreeMap<>();
        IdentifierBindingIndex identifierBindingIndex = IdentifierBindingIndex.of(model);
        Set identifierMembers = new HashSet<>(identifierBindingIndex
            .getOperationOutputBindings(resource, operation).values());

        Shape shape = propertyBindingIndex.getInputPropertiesShape(operation);
        for (MemberShape member : shape.members()) {
            if (propertyBindingIndex.isMemberShapeProperty(member)) {
                validateMember(events, lifecycleOperationName, propertyBindingIndex, resource, member,
                        identifierMembers, properties, propertyToMemberMappings);
            }
        }
        validateConflictingProperties(events, shape, propertyToMemberMappings);
    }

    private void validateConflictingProperties(
            List events,
            Shape shape,
            Map> propertyToMemberMappings
    ) {
        for (Map.Entry> entry : propertyToMemberMappings.entrySet()) {
            if (entry.getValue().size() > 1) {
                events.add(error(shape, String.format(
                        "This shape contains members with conflicting resource property names that resolve to '%s': %s",
                        entry.getKey(),
                        entry.getValue().stream().map(MemberShape::getMemberName)
                                .collect(Collectors.joining(", ")))));
            }
        }
    }

    private void validateMember(
            List events,
            String lifecycleOperationName,
            PropertyBindingIndex propertyBindingIndex,
            ResourceShape resource,
            MemberShape member,
            Set identifierMembers,
            Map properties,
            Map> propertyToMemberMappings
    ) {
        String propertyName = propertyBindingIndex.getPropertyName(member.getId()).get();
        propertyToMemberMappings.computeIfAbsent(propertyName, m -> new TreeSet<>()).add(member);
        if (properties.containsKey(propertyName)) {
            if (!properties.get(propertyName).equals(member.getTarget())) {
                ShapeId expectedTarget = properties.get(propertyName);
                events.add(error(member, String.format(
                        "This member must target `%s`. This member is used as part of the `%s` operation of the `%s` "
                        + "resource and conflicts with its `%s` resource property.",
                        expectedTarget, lifecycleOperationName,
                        resource.getId(), propertyName)));
            }
        } else if (!identifierMembers.contains(member.getMemberName())) {
            events.add(error(member, String.format("Member `%s` does not target a property or identifier"
                    + " for resource `%s`", member.getMemberName(), resource.getId().toString())));
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy