
software.amazon.smithy.model.validation.validators.PaginatedTraitValidator Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of smithy-model Show documentation
Show all versions of smithy-model Show documentation
This module provides the core implementation of loading, validating, traversing, mutating, and serializing a Smithy model.
/*
* 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.List;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Pattern;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.knowledge.OperationIndex;
import software.amazon.smithy.model.knowledge.TopDownIndex;
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.Shape;
import software.amazon.smithy.model.shapes.ShapeType;
import software.amazon.smithy.model.shapes.StructureShape;
import software.amazon.smithy.model.traits.PaginatedTrait;
import software.amazon.smithy.model.validation.AbstractValidator;
import software.amazon.smithy.model.validation.ValidationEvent;
import software.amazon.smithy.model.validation.ValidationUtils;
import software.amazon.smithy.utils.SetUtils;
/**
* Validates paginated traits.
*
*
* - The inputToken and outputToken properties must be set when an
* operation's properties are optionally merged with a service's.
* - The items property, if set, must reference a list or map
* output member.
* - The pageSize property, if set, should reference an optional integer
* input member. It may, but should not reference an optional byte, short,
* or long.
* - The inputToken property should reference an optional string input
* member. It may, but should not reference an optional map.
* - The outputToken property should reference an optional string output
* member. It may, but should not reference an optional map.
*
*/
public final class PaginatedTraitValidator extends AbstractValidator {
private static final Set ITEM_SHAPES = SetUtils.of(ShapeType.LIST, ShapeType.MAP);
private static final Set PAGE_SHAPES = SetUtils.of(ShapeType.BYTE, ShapeType.INTEGER,
ShapeType.LONG, ShapeType.SHORT);
private static final Set DANGER_PAGE_SHAPES = SetUtils.of(ShapeType.BYTE, ShapeType.LONG,
ShapeType.SHORT);
private static final Set TOKEN_SHAPES = SetUtils.of(ShapeType.STRING, ShapeType.MAP);
private static final Set DANGER_TOKEN_SHAPES = SetUtils.of(ShapeType.MAP);
private static final Pattern PATH_PATTERN = Pattern.compile("\\.");
private static final String DEEPLY_NESTED = "DeeplyNested";
private static final String SHOULD_NOT_BE_REQUIRED = "ShouldNotBeRequired";
private static final String WRONG_SHAPE_TYPE = "WrongShapeType";
@Override
public List validate(Model model) {
OperationIndex opIndex = OperationIndex.of(model);
TopDownIndex topDown = TopDownIndex.of(model);
List events = new ArrayList<>();
for (OperationShape operation : model.getOperationShapesWithTrait(PaginatedTrait.class)) {
PaginatedTrait paginatedTrait = operation.expectTrait(PaginatedTrait.class);
events.addAll(validateOperation(model, topDown, opIndex, operation, paginatedTrait));
}
return events;
}
private List validateOperation(
Model model,
TopDownIndex topDownIndex,
OperationIndex opIndex,
OperationShape operation,
PaginatedTrait trait
) {
List events = new ArrayList<>();
// Validate input.
events.addAll(validateMember(opIndex, model, null, operation, trait, new InputTokenValidator()));
PageSizeValidator pageSizeValidator = new PageSizeValidator();
events.addAll(validateMember(opIndex, model, null, operation, trait, pageSizeValidator));
pageSizeValidator.getMember(model, opIndex, operation, trait)
.filter(MemberShape::isRequired)
.ifPresent(member -> events.add(warning(
operation,
trait,
String.format(
"paginated trait `%s` member `%s` should not be required",
pageSizeValidator.propertyName(), member.getMemberName()),
SHOULD_NOT_BE_REQUIRED, pageSizeValidator.propertyName())
));
// Validate output.
events.addAll(validateMember(opIndex, model, null, operation, trait, new OutputTokenValidator()));
events.addAll(validateMember(opIndex, model, null, operation, trait, new ItemValidator()));
if (events.isEmpty()) {
model.shapes(ServiceShape.class).forEach(svc -> {
if (topDownIndex.getContainedOperations(svc).contains(operation)) {
// Create a merged trait if one is present on the service.
PaginatedTrait merged = svc.getTrait(PaginatedTrait.class).map(trait::merge).orElse(trait);
events.addAll(validateMember(opIndex, model, svc, operation, merged, new InputTokenValidator()));
events.addAll(validateMember(opIndex, model, svc, operation, merged, new PageSizeValidator()));
events.addAll(validateMember(opIndex, model, svc, operation, merged, new OutputTokenValidator()));
events.addAll(validateMember(opIndex, model, svc, operation, merged, new ItemValidator()));
}
});
}
return events;
}
private List validateMember(
OperationIndex opIndex,
Model model,
ServiceShape service,
OperationShape operation,
PaginatedTrait trait,
PropertyValidator validator
) {
String prefix = service != null ? "When bound within the `" + service.getId() + "` service, " : "";
String memberPath = validator.getMemberPath(opIndex, operation, trait).orElse(null);
if (memberPath == null) {
return service != null && validator.isRequiredToBePresent()
? Collections.singletonList(error(operation, trait, String.format(
"%spaginated trait `%s` is not configured", prefix, validator.propertyName())))
: Collections.emptyList();
}
if (!validator.pathsAllowed() && memberPath.contains(".")) {
return Collections.singletonList(error(operation, trait, String.format(
"%spaginated trait `%s` does not allow path values", prefix, validator.propertyName()
)));
}
MemberShape member = validator.getMember(model, opIndex, operation, trait).orElse(null);
if (member == null) {
return Collections.singletonList(error(operation, trait, String.format(
"%spaginated trait `%s` targets a member `%s` that does not exist",
prefix, validator.propertyName(), memberPath)));
}
List events = new ArrayList<>();
if (validator.mustBeOptional() && member.isRequired()) {
events.add(error(operation, trait, String.format(
"%spaginated trait `%s` member `%s` must not be required",
prefix, validator.propertyName(), member.getMemberName())));
}
Shape target = model.getShape(member.getTarget()).orElse(null);
if (target != null) {
if (!validator.validTargets().contains(target.getType())) {
events.add(error(operation, trait, String.format(
"%spaginated trait `%s` member `%s` targets a %s shape, but must target one of "
+ "the following: [%s]",
prefix, validator.propertyName(), member.getId().getMember().get(), target.getType(),
ValidationUtils.tickedList(validator.validTargets()))));
}
if (validator.dangerTargets().contains(target.getType())) {
Set preferredTargets = new TreeSet<>(validator.validTargets());
preferredTargets.removeAll(validator.dangerTargets());
String traitName = validator.propertyName();
String memberName = member.getId().getMember().get();
String targetType = target.getType().toString();
events.add(danger(operation, trait, String.format(
"%spaginated trait `%s` member `%s` targets a %s shape, but this is not recommended. "
+ "One of [%s] SHOULD be targeted.",
prefix, traitName, memberName, targetType, ValidationUtils.tickedList(preferredTargets)),
WRONG_SHAPE_TYPE, traitName
));
}
}
if (validator.pathsAllowed() && PATH_PATTERN.split(memberPath).length > 2) {
events.add(warning(operation, trait, String.format(
"%spaginated trait `%s` contains a path with more than two parts, which can make your API "
+ "cumbersome to use",
prefix, validator.propertyName()),
DEEPLY_NESTED, validator.propertyName()
));
}
return events;
}
private abstract static class PropertyValidator {
abstract boolean mustBeOptional();
abstract boolean isRequiredToBePresent();
abstract String propertyName();
abstract Set validTargets();
Set dangerTargets() {
return Collections.emptySet();
}
abstract Optional getMemberPath(OperationIndex opIndex, OperationShape operation, PaginatedTrait trait);
abstract Optional getMember(
Model model, OperationIndex opIndex, OperationShape operation, PaginatedTrait trait
);
boolean pathsAllowed() {
return false;
}
}
private abstract static class OutputPropertyValidator extends PropertyValidator {
@Override
boolean pathsAllowed() {
return true;
}
Optional getMember(
Model model, OperationIndex opIndex, OperationShape operation, PaginatedTrait trait
) {
StructureShape outputShape = opIndex.expectOutputShape(operation);
return getMemberPath(opIndex, operation, trait)
.map(path -> PaginatedTrait.resolveFullPath(path, model, outputShape))
.flatMap(memberShapes -> {
if (memberShapes.size() == 0) {
return Optional.empty();
}
return Optional.of(memberShapes.get(memberShapes.size() - 1));
});
}
}
private static final class InputTokenValidator extends PropertyValidator {
boolean mustBeOptional() {
return true;
}
boolean isRequiredToBePresent() {
return true;
}
String propertyName() {
return "inputToken";
}
Set validTargets() {
return TOKEN_SHAPES;
}
Set dangerTargets() {
return DANGER_TOKEN_SHAPES;
}
Optional getMemberPath(OperationIndex opIndex, OperationShape operation, PaginatedTrait trait) {
return trait.getInputToken();
}
Optional getMember(
Model model, OperationIndex opIndex, OperationShape operation, PaginatedTrait trait
) {
StructureShape input = opIndex.expectInputShape(operation);
return getMemberPath(opIndex, operation, trait).flatMap(input::getMember);
}
}
private static final class OutputTokenValidator extends OutputPropertyValidator {
boolean mustBeOptional() {
return true;
}
boolean isRequiredToBePresent() {
return true;
}
String propertyName() {
return "outputToken";
}
Set validTargets() {
return TOKEN_SHAPES;
}
Set dangerTargets() {
return DANGER_TOKEN_SHAPES;
}
Optional getMemberPath(OperationIndex opIndex, OperationShape operation, PaginatedTrait trait) {
return trait.getOutputToken();
}
}
private static final class PageSizeValidator extends PropertyValidator {
boolean mustBeOptional() {
return false;
}
boolean isRequiredToBePresent() {
return false;
}
String propertyName() {
return "pageSize";
}
Set validTargets() {
return PAGE_SHAPES;
}
Set dangerTargets() {
return DANGER_PAGE_SHAPES;
}
Optional getMemberPath(OperationIndex opIndex, OperationShape operation, PaginatedTrait trait) {
return trait.getPageSize();
}
Optional getMember(
Model model, OperationIndex opIndex, OperationShape operation, PaginatedTrait trait
) {
StructureShape input = opIndex.expectInputShape(operation);
return getMemberPath(opIndex, operation, trait).flatMap(input::getMember);
}
}
private static final class ItemValidator extends OutputPropertyValidator {
boolean mustBeOptional() {
return false;
}
boolean isRequiredToBePresent() {
return false;
}
String propertyName() {
return "items";
}
Set validTargets() {
return ITEM_SHAPES;
}
Optional getMemberPath(OperationIndex opIndex, OperationShape operation, PaginatedTrait trait) {
return trait.getItems();
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy