Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add validator to warn on ignored idempotency token trait #2358

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package software.amazon.smithy.model.validation.validators;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
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.NeighborProviderIndex;
import software.amazon.smithy.model.neighbor.NeighborProvider;
import software.amazon.smithy.model.neighbor.Relationship;
import software.amazon.smithy.model.neighbor.RelationshipType;
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.traits.IdempotencyTokenTrait;
import software.amazon.smithy.model.traits.MixinTrait;
import software.amazon.smithy.model.traits.Trait;
import software.amazon.smithy.model.validation.AbstractValidator;
import software.amazon.smithy.model.validation.ValidationEvent;

/**
* Emits warnings when a structure member has an idempotency token trait that will be ignored.
*/
public final class IdempotencyTokenIgnoredValidator extends AbstractValidator {

@Override
public List<ValidationEvent> validate(Model model) {
if (!model.getAppliedTraits().contains(IdempotencyTokenTrait.ID)) {
return Collections.emptyList();
}
List<ValidationEvent> events = new ArrayList<>();
NeighborProvider reverse = NeighborProviderIndex.of(model).getReverseProvider();
for (MemberShape memberShape : model.getMemberShapesWithTrait(IdempotencyTokenTrait.class)) {
Shape container = model.expectShape(memberShape.getContainer());
// Skip non-structures (invalid) and mixins (handled at mixed site).
if (!container.isStructureShape() || container.hasTrait(MixinTrait.class)) {
continue;
}
Trait trait = memberShape.expectTrait(IdempotencyTokenTrait.class);
checkRelationships(container.asStructureShape().get(), memberShape, trait, reverse, events);
}
return events;
}

private void checkRelationships(
StructureShape containerShape,
MemberShape memberShape,
Trait trait,
NeighborProvider reverse,
List<ValidationEvent> events
) {

// Store relationships so we can emit one event per ignored binding.
Map<RelationshipType, List<ShapeId>> ignoredRelationships = new TreeMap<>();
List<Relationship> relationships = reverse.getNeighbors(containerShape);
for (Relationship relationship : relationships) {
// Skip members of the container.
if (relationship.getRelationshipType() == RelationshipType.MEMBER_CONTAINER) {
continue;
}
if (relationship.getRelationshipType() != RelationshipType.INPUT) {
ignoredRelationships.computeIfAbsent(relationship.getRelationshipType(), x -> new ArrayList<>())
.add(relationship.getShape().getId());
}
}

// If we detected invalid relationships, build the right event message.
if (!ignoredRelationships.isEmpty()) {
events.add(emit(memberShape, trait, ignoredRelationships));
}
}

private ValidationEvent emit(
MemberShape memberShape,
Trait trait,
Map<RelationshipType, List<ShapeId>> ignoredRelationships
) {
String message =
"The `idempotencyToken` trait only has an effect when applied to a top-level operation input member, "
+ "but it was applied and ignored in the following contexts: "
+ formatIgnoredRelationships(ignoredRelationships);
return warning(memberShape, trait, message);
}

private String formatIgnoredRelationships(Map<RelationshipType, List<ShapeId>> ignoredRelationships) {
List<String> relationshipTypeBindings = new ArrayList<>();
for (Map.Entry<RelationshipType, List<ShapeId>> ignoredRelationshipType : ignoredRelationships.entrySet()) {
StringBuilder buf = new StringBuilder(ignoredRelationshipType.getKey().toString()
.toLowerCase(Locale.US)
.replace("_", " "));
Set<String> bindings = ignoredRelationshipType.getValue().stream()
.map(ShapeId::toString)
.collect(Collectors.toCollection(TreeSet::new));
buf.append(": [").append(String.join(", ", bindings)).append("]");
relationshipTypeBindings.add(buf.toString());
}
return "{" + String.join(", ", relationshipTypeBindings) + "}";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ software.amazon.smithy.model.validation.validators.HttpResponseCodeSemanticsVali
software.amazon.smithy.model.validation.validators.HttpUriGreedyLabelValidator
software.amazon.smithy.model.validation.validators.HttpUriConflictValidator
software.amazon.smithy.model.validation.validators.HttpUriFormatValidator
software.amazon.smithy.model.validation.validators.IdempotencyTokenIgnoredValidator
software.amazon.smithy.model.validation.validators.JsonNameValidator
software.amazon.smithy.model.validation.validators.LengthTraitValidator
software.amazon.smithy.model.validation.validators.MediaTypeValidator
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[WARNING] io.smithy.example#StructureOne$stringMember: The `idempotencyToken` trait only has an effect when applied to a top-level operation input member, but it was applied and ignored in the following contexts: {member target: [io.smithy.example#StructureTwo$structureOne]} | IdempotencyTokenIgnored
[WARNING] io.smithy.example#StructureThree$stringMember: The `idempotencyToken` trait only has an effect when applied to a top-level operation input member, but it was applied and ignored in the following contexts: {output: [io.smithy.example#OperationFour]} | IdempotencyTokenIgnored

Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
$version: "2"

namespace io.smithy.example

service SmithyExample {
operations: [
OperationOne
OperationTwo
OperationThree
OperationFour
]
}

operation OperationOne {
input := {
@idempotencyToken
stringMember: String
integerMember: Integer
}
output: Unit
}

operation OperationTwo {
input: StructureOne
output: Unit
}

operation OperationThree {
input: StructureTwo
output: Unit
}

operation OperationFour {
input: StructureThree
output: StructureThree
}


structure StructureOne {
@idempotencyToken
stringMember: String
integerMember: Integer
}


structure StructureTwo {
stringMember: String
structureOne: StructureOne
}


@mixin
structure StructureMixin {
@idempotencyToken
stringMember: String
}


structure StructureThree with [StructureMixin] {
integerMember: Integer
}

Loading