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

smithy4s.meta.validation.RefinementTraitValidator Maven / Gradle / Ivy

There is a newer version: 0.19.0-41-91762fb
Show newest version
/*
 *  Copyright 2021-2024 Disney Streaming
 *
 *  Licensed under the Tomorrow Open Source Technology License, Version 1.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *     https://disneystreaming.github.io/TOST-1.0.txt
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License 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 smithy4s.meta.validation;

import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.shapes.ShapeType;
import software.amazon.smithy.model.traits.*;
import software.amazon.smithy.model.validation.AbstractValidator;
import software.amazon.smithy.model.validation.ValidationEvent;
import software.amazon.smithy.model.SourceLocation;
import smithy4s.meta.RefinementTrait;

import java.util.List;
import java.util.Optional;
import java.util.Collections;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.Set;

public final class RefinementTraitValidator extends AbstractValidator {

	boolean isSimple(Model model, Shape shape) {
		return shape.getType().getCategory() == ShapeType.Category.SIMPLE
				|| shape.getType().getCategory() == ShapeType.Category.MEMBER && shape.asMemberShape()
						.map(ms -> model.expectShape(ms.getTarget())).map(s -> isSimple(model, s)).orElse(false);
	}

	boolean isAllowedType(Model model, Shape shape) {
		boolean notConstrained = shape.getAllTraits().values().stream().allMatch(t -> {
			@SuppressWarnings("deprecation") // EnumTrait
			boolean isConstrained = t instanceof EnumTrait || t instanceof LengthTrait || t instanceof RangeTrait
					|| t instanceof PatternTrait;
			return !isConstrained;
		});
		boolean _isSimple = isSimple(model, shape) && notConstrained;
		@SuppressWarnings("deprecation") // isSetShape
		boolean isCollection = shape.isListShape() || shape.isMapShape() || shape.isSetShape();
		return _isSimple || isCollection;
	}

	// inspired from:
	// https://github.com/awslabs/smithy/blob/7f7669e0ee1f563488d111e9ee9c8df9179533d3/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/Upgrade1to2Command.java#L256
	private boolean isSyntheticDefault(Shape shape, DefaultTrait defTrait) {
		Optional defaultLocation = shape.getTrait(DefaultTrait.class).map(Trait::getSourceLocation);
		// When Smithy injects the default trait, it sets the source
		// location equal to the shape's source location. This is
		// impossible in any other scenario, so we can use this info
		// to know whether it was injected or not.
		return shape.getSourceLocation().equals(defTrait.getSourceLocation());
	}

	// if there is a default trait, and it is not a synthetic one
	// then we can assume the user added it
	boolean hasUserDefault(Shape shape) {
		Optional defaultTrait = shape.getTrait(DefaultTrait.class);
		return defaultTrait.filter(d -> !isSyntheticDefault(shape, d)).isPresent();
	}

	Stream refinedTraitsNoDefault(Model model) {
		return model.getShapesWithTrait(RefinementTrait.class).stream().flatMap(refinedTrait -> {
			return model.getShapesWithTrait(refinedTrait.getId()).stream().filter(shape -> hasUserDefault(shape))
					.map(shape -> warning(shape, refinedTrait.getId() + " is a refinement trait. It is applied to "
							+ shape.getId() + " along with a @default trait. You should avoid mixing the two."));
		});
	}

	@Override
	public List validate(Model model) {
		Stream countErrors = model.shapes().flatMap(s -> {
			long numRefinedTraits = s.getAllTraits().values().stream().flatMap(t -> {
				return OptionHelper.toStream(model.getShape(t.toShapeId())).flatMap(traitShape -> {
					return OptionHelper.toStream(traitShape.getTrait(RefinementTrait.class));
				});
			}).count();
			if (numRefinedTraits > 1) {
				return Stream.of(error(s, "Shapes may only be annotated with one refinement trait"));
			} else if (numRefinedTraits == 1 && !isAllowedType(model, s)) {
				return Stream.of(error(s,
						"refinements can only be used on simpleShapes, list, set, and map. Simple shapes must not be constrained by enum, length, range, or pattern traits"));
			} else {
				return Stream.empty();
			}
		});
		Stream defaultUsageWarnings = refinedTraitsNoDefault(model);
		return Stream.of(countErrors, defaultUsageWarnings).flatMap(s -> s).collect(Collectors.toList());
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy