software.amazon.smithy.model.loader.ModelValidator Maven / Gradle / Ivy
/*
* Copyright 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.loader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.SourceLocation;
import software.amazon.smithy.model.validation.Severity;
import software.amazon.smithy.model.validation.ValidatedResult;
import software.amazon.smithy.model.validation.ValidationEvent;
import software.amazon.smithy.model.validation.ValidationEventDecorator;
import software.amazon.smithy.model.validation.ValidationUtils;
import software.amazon.smithy.model.validation.Validator;
import software.amazon.smithy.model.validation.ValidatorFactory;
import software.amazon.smithy.model.validation.suppressions.ModelBasedEventDecorator;
import software.amazon.smithy.model.validation.validators.ResourceCycleValidator;
import software.amazon.smithy.model.validation.validators.TargetValidator;
import software.amazon.smithy.utils.BuilderRef;
import software.amazon.smithy.utils.ListUtils;
import software.amazon.smithy.utils.MapUtils;
import software.amazon.smithy.utils.SmithyBuilder;
/**
* Validates a model, including validators and suppressions loaded from traits and metadata.
*
* Validators found in metadata and suppressions found in traits are automatically created and applied to the
* model. Explicitly provided validators are merged with the validators and suppressions loaded from metadata.
*
*
The internal implementation of this class is broken into three parts: a builder, a validator, and a loaded
* validator.
*
*
* ModelValidator.builder().build().validate(model);
* // ^ creates Builder
* // ^ creates ModelValidator
* // ^ creates a LoadedModelValidator
*
*
* The builder is used to build up the customized context of the validator. ModelValidator is the created
* {@link Validator} implementation isolated from the builder. LoadedModelValidator loads metadata from a Model and
* performs the actual validation in an isolated context from the ModelValidator.
*/
final class ModelValidator implements Validator {
// Lazy initialization holder class idiom to hold a default validator factory.
private static final class LazyValidatorFactoryHolder {
static final ValidatorFactory INSTANCE = ValidatorFactory.createServiceFactory(
ModelAssembler.class.getClassLoader());
}
/** If these validators fail, then many others will too. Validate these first. */
private static final Map, Validator> CORRECTNESS_VALIDATORS = MapUtils.of(
TargetValidator.class, new TargetValidator(),
ResourceCycleValidator.class, new ResourceCycleValidator()
);
private final ValidatorFactory validatorFactory;
private final List events;
private final List validators;
private final List criticalValidators;
private final ValidationEventDecorator validationEventDecorator;
private final Consumer eventListener;
private final boolean legacyValidationMode;
ModelValidator(Builder builder) {
this.validatorFactory = builder.validatorFactory;
this.eventListener = builder.eventListener;
this.validationEventDecorator = builder.validationEventDecorator;
this.events = builder.includeEvents.copy();
this.validators = builder.validators.copy();
this.criticalValidators = builder.criticalValidators.copy();
this.legacyValidationMode = builder.legacyValidationMode;
}
@Override
public List validate(Model model) {
return new LoadedModelValidator(model, this).validate();
}
static Builder builder() {
return new Builder();
}
static ValidatorFactory defaultValidationFactory() {
return LazyValidatorFactoryHolder.INSTANCE;
}
static final class Builder implements SmithyBuilder {
private final BuilderRef> validators = BuilderRef.forList();
private final BuilderRef> criticalValidators = BuilderRef.forList();
private final BuilderRef> includeEvents = BuilderRef.forList();
private ValidatorFactory validatorFactory = LazyValidatorFactoryHolder.INSTANCE;
private Consumer eventListener = event -> { };
private ValidationEventDecorator validationEventDecorator;
private boolean legacyValidationMode = false;
private Builder() {}
/**
* Adds an array of {@link Validator}s to use when running the ModelValidator.
*
* @param validators Validators to add.
* @return Returns the builder.
*/
public Builder addValidators(Collection extends Validator> validators) {
for (Validator validator : validators) {
addValidator(validator);
}
return this;
}
/**
* Adds a {@link Validator}.
*
* @param validator Validator to add.
* @return Returns the builder.
*/
public Builder addValidator(Validator validator) {
if (!CORRECTNESS_VALIDATORS.containsKey(validator.getClass())) {
if (ValidationUtils.isCriticalValidator(validator.getClass())) {
criticalValidators.get().add(validator);
} else {
validators.get().add(validator);
}
}
return this;
}
/**
* Sets the factory used to find built-in {@link Validator}s and to load validators found in model metadata.
*
* @param validatorFactory Factory to use to load {@code Validator}s.
* @param validationEventDecorator Provide a previously loaded and composed decorator.
* @return Returns the builder.
*/
public Builder validatorFactory(
ValidatorFactory validatorFactory,
ValidationEventDecorator validationEventDecorator
) {
this.validatorFactory = Objects.requireNonNull(validatorFactory);
this.validationEventDecorator = validationEventDecorator;
return this;
}
/**
* Sets a custom event listener that receives each {@link ValidationEvent} as it is emitted.
*
* @param eventListener Event listener that consumes each event.
* @return Returns the builder.
*/
public Builder eventListener(Consumer eventListener) {
this.eventListener = Objects.requireNonNull(eventListener);
return this;
}
/**
* Includes a set of events that were already encountered in the result.
*
* Suppressions and severity overrides will be applied to the given {@code events}. However, the validator
* assumes that the event has already been decorated and the event listener has already seen the event.
*
* @param events Events to include.
* @return Returns the builder.
*/
public Builder includeEvents(List events) {
this.includeEvents.get().addAll(events);
return this;
}
/**
* Enables legacy validation mode that does not fail if critical Validators emit an ERROR.
*
* @param legacyValidationMode Set to true to enable legacy validation mode.
* @return Returns the builder.
*/
public Builder legacyValidationMode(boolean legacyValidationMode) {
this.legacyValidationMode = legacyValidationMode;
return this;
}
@Override
public ModelValidator build() {
// Adding built-in validators is deferred to allow for a custom factory to be set on the builder.
addValidators(validatorFactory.loadBuiltinValidators());
return new ModelValidator(this);
}
}
private static final class LoadedModelValidator {
private final Model model;
private final List validators;
private final List criticalValidators;
private final List events = new ArrayList<>();
private final ValidationEventDecorator validationEventDecorator;
private final Consumer eventListener;
private final boolean legacyValidationMode;
private LoadedModelValidator(Model model, ModelValidator validator) {
this.model = model;
this.eventListener = validator.eventListener;
this.validators = new ArrayList<>(validator.validators);
this.criticalValidators = Collections.unmodifiableList(validator.criticalValidators);
this.legacyValidationMode = validator.legacyValidationMode;
// Suppressing and elevating events is handled by composing a given decorator with a
// ModelBasedEventDecorator.
ModelBasedEventDecorator modelBasedEventDecorator = new ModelBasedEventDecorator();
ValidatedResult result = modelBasedEventDecorator.createDecorator(model);
this.validationEventDecorator = result.getResult()
.map(decorator -> ValidationEventDecorator.compose(
ListUtils.of(decorator, validator.validationEventDecorator)))
.orElse(validator.validationEventDecorator);
// Events encountered while loading suppressions and overrides have been modified by everything the
// modelBasedEventDecorator knows about, but has not been modified by any custom decorator (if any).
for (ValidationEvent event : result.getValidationEvents()) {
if (validationEventDecorator.canDecorate(event)) {
event = validationEventDecorator.decorate(event);
}
events.add(event);
}
// Now that the decorator is available, emit/decorate/suppress/collect explicitly provided events.
for (ValidationEvent event : validator.events) {
pushEvent(event);
}
// The decorator itself doesn't handle loading and applying validators, just modifying events.
loadModelValidators(validator.validatorFactory);
}
private void loadModelValidators(ValidatorFactory validatorFactory) {
// Load validators defined in metadata.
ValidatedResult> loaded = ValidationLoader
.loadValidators(model.getMetadata());
pushEvents(loaded.getValidationEvents());
List definitions = loaded.getResult().orElseGet(Collections::emptyList);
ValidatorFromDefinitionFactory factory = new ValidatorFromDefinitionFactory(validatorFactory);
// Attempt to create the Validator instances and collect errors along the way.
for (ValidatorDefinition val : definitions) {
ValidatedResult result = factory.loadValidator(val);
result.getResult().ifPresent(validators::add);
pushEvents(result.getValidationEvents());
if (result.getValidationEvents().isEmpty() && !result.getResult().isPresent()) {
ValidationEvent event = unknownValidatorError(val.name, val.sourceLocation);
pushEvent(event);
}
}
}
// Unknown validators don't fail the build!
private static ValidationEvent unknownValidatorError(String name, SourceLocation location) {
return ValidationEvent.builder()
// Per the spec, the eventID is "UnknownValidator_".
.id("UnknownValidator_" + name)
.severity(Severity.WARNING)
.sourceLocation(location)
.message("Unable to locate a validator named `" + name + "`")
.build();
}
private void pushEvents(List source) {
for (ValidationEvent event : source) {
pushEvent(event);
}
}
private void pushEvent(ValidationEvent event) {
events.add(updateAndEmitEvent(event));
}
private ValidationEvent updateAndEmitEvent(ValidationEvent event) {
if (validationEventDecorator.canDecorate(event)) {
event = validationEventDecorator.decorate(event);
}
eventListener.accept(event);
return event;
}
private List validate() {
// Perform critical correctness validation before other critical validators.
events.addAll(streamEvents(CORRECTNESS_VALIDATORS.values().stream()));
if (LoaderUtils.containsErrorEvents(events)) {
return events;
}
// Same thing, but for other critical validators.
events.addAll(streamEvents(criticalValidators.parallelStream()));
// Only fail early here if legacy validation mode is enabled.
if (!legacyValidationMode && LoaderUtils.containsErrorEvents(events)) {
return events;
}
events.addAll(streamEvents(validators.parallelStream()));
return events;
}
private List streamEvents(Stream validators) {
return validators
.flatMap(validator -> validator.validate(model).stream())
.filter(this::filterPrelude)
.map(this::updateAndEmitEvent)
.collect(Collectors.toList());
}
private boolean filterPrelude(ValidationEvent event) {
// Don't emit any non-error events for prelude shapes and traits.
// This prevents custom validators from unnecessarily needing to worry about prelude shapes and trait
// definitions, but still allows for validation events when the prelude is broken.
return event.getSeverity() == Severity.ERROR || !event.getShapeId()
.filter(Prelude::isPreludeShape)
.isPresent();
}
}
}