Skip to content

Commit

Permalink
Add validator to warn on ignored idempotency token trait (#2358)
Browse files Browse the repository at this point in the history
* Add validator to warn on ignored idempotency token trait

* Address PR comments

* Remove unsued code

---------
  • Loading branch information
sugmanue authored Jul 29, 2024
1 parent 029a1a8 commit 3d76f98
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 0 deletions.
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
}

0 comments on commit 3d76f98

Please sign in to comment.