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

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

/*
 * Copyright 2020 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.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.StringJoiner;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.knowledge.NeighborProviderIndex;
import software.amazon.smithy.model.knowledge.ServiceIndex;
import software.amazon.smithy.model.neighbor.NeighborProvider;
import software.amazon.smithy.model.neighbor.Relationship;
import software.amazon.smithy.model.neighbor.RelationshipDirection;
import software.amazon.smithy.model.neighbor.RelationshipType;
import software.amazon.smithy.model.neighbor.Walker;
import software.amazon.smithy.model.shapes.MemberShape;
import software.amazon.smithy.model.shapes.ServiceShape;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.model.shapes.UnionShape;
import software.amazon.smithy.model.traits.DefaultTrait;
import software.amazon.smithy.model.traits.HttpPayloadTrait;
import software.amazon.smithy.model.traits.ProtocolDefinitionTrait;
import software.amazon.smithy.model.traits.RequiredTrait;
import software.amazon.smithy.model.traits.RequiresLengthTrait;
import software.amazon.smithy.model.traits.StreamingTrait;
import software.amazon.smithy.model.validation.AbstractValidator;
import software.amazon.smithy.model.validation.ValidationEvent;

/**
 * Validates the streaming trait.
 *
 * 

Ensures that the targeted shapes are only referenced by top level input/output structures. * *

If the targeted shape is a union, ensures that all members are structures. * *

If used in the scope of any service that supports {@link software.amazon.smithy.model.traits.HttpPayloadTrait}, * ensures that any blobs targeted also have @httpPayload applied. */ public final class StreamingTraitValidator extends AbstractValidator { @Override public List validate(Model model) { if (!model.isTraitApplied(StreamingTrait.class)) { return Collections.emptyList(); } List events = new ArrayList<>(); validateStreamingTargets(model, events); validateAllEventStreamMembersTargetStructures(model, events); validateBlobTargetsArePayloads(model, events); validateBlobTargetsAreNonOptional(model, events); return events; } private void validateBlobTargetsAreNonOptional(Model model, List events) { for (MemberShape member : model.toSet(MemberShape.class)) { Shape target = model.expectShape(member.getTarget()); if (target.isBlobShape() && target.hasTrait(StreamingTrait.class) && !(member.hasTrait(RequiredTrait.class) || member.hasTrait(DefaultTrait.class))) { events.add(error(member, "Members that target blobs marked with the `streaming` trait MUST also be " + "marked with the `required` or `default` trait.")); } } } private void validateBlobTargetsArePayloads(Model model, List events) { ServiceIndex serviceIndex = ServiceIndex.of(model); Set servicesWithPayloadSupportingProtocols = new HashSet<>(); for (ServiceShape service : model.getServiceShapes()) { for (ShapeId protocolTrait : serviceIndex.getProtocols(service).keySet()) { if (protocolTraitSupportsHttpPayload(model, protocolTrait)) { servicesWithPayloadSupportingProtocols.add(service); break; } } } Walker walker = new Walker(model); for (ServiceShape service : servicesWithPayloadSupportingProtocols) { walker.iterateShapes(service).forEachRemaining(shape -> { if (shape.isMemberShape() && !shape.hasTrait(HttpPayloadTrait.class)) { MemberShape member = shape.asMemberShape().get(); Shape target = model.expectShape(member.getTarget()); if (target.hasTrait(StreamingTrait.class)) { events.add(error(member, String.format( "Member `%s` referencing @streaming shape `%s` must have the @httpPayload trait, " + "as service `%s` has a protocol that supports @httpPayload.", member.toShapeId(), member.getTarget(), service.toShapeId()))); } } }); } } private boolean protocolTraitSupportsHttpPayload(Model model, ShapeId protocolTrait) { return model.expectShape(protocolTrait) .expectTrait(ProtocolDefinitionTrait.class) .getTraits() .contains(HttpPayloadTrait.ID); } private void validateStreamingTargets(Model model, List events) { NeighborProvider provider = NeighborProviderIndex.of(model).getReverseProvider(); // Find members that target streaming shapes and validate things that target their containers. for (Shape shape : model.getShapesWithTrait(StreamingTrait.class)) { for (Relationship rel : provider.getNeighbors(shape)) { if (rel.getRelationshipType() == RelationshipType.MEMBER_TARGET) { MemberShape member = rel.getShape().asMemberShape().get(); validateRef(model, member, provider, events); } } } } private void validateRef(Model model, MemberShape member, NeighborProvider reverse, List events) { Shape target = model.expectShape(member.getTarget()); Shape container = model.expectShape(member.getContainer()); for (Relationship rel : reverse.getNeighbors(container)) { if (rel.getRelationshipType().getDirection() == RelationshipDirection.DIRECTED) { switch (rel.getRelationshipType()) { case INPUT: case MIXIN: break; case OUTPUT: if (target.hasTrait(RequiresLengthTrait.class)) { events.add(error(rel.getNeighborShape().get(), String.format( "Structures that contain a reference to a stream marked with the " + "@requiresLength trait can only be used as operation inputs, but this " + "structure is referenced from `%s` as %s", rel.getShape().getId(), rel.getRelationshipType().toString().toLowerCase(Locale.ENGLISH)))); } break; case MEMBER_TARGET: events.add(error(rel.getShape(), String.format( "Members cannot target structures that contain a stream, but this member targets %s", container.getId()))); break; default: events.add(error(rel.getShape(), String.format( "This shape has an invalid `%s` relationship to a structure, `%s`, that contains " + "a stream", rel.getRelationshipType(), container.getId()))); } } } } private void validateAllEventStreamMembersTargetStructures(Model model, List events) { for (UnionShape union : model.getUnionShapesWithTrait(StreamingTrait.class)) { // Find members that don't reference a structure and combine // these member names into a comma separated list. StringJoiner joiner = new StringJoiner(", "); for (MemberShape member : union.members()) { Shape target = model.expectShape(member.getTarget()); if (!target.isStructureShape()) { joiner.add(member.getMemberName()); } } String invalidMembers = joiner.toString(); if (!invalidMembers.isEmpty()) { events.add(error(union, String.format( "Each member of an event stream union must target a structure shape, but the following union " + "members do not: [%s]", invalidMembers))); } } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy