diff --git a/docs/source-2.0/spec/model-validation.rst b/docs/source-2.0/spec/model-validation.rst index bd4a532bf87..444964d2653 100644 --- a/docs/source-2.0/spec/model-validation.rst +++ b/docs/source-2.0/spec/model-validation.rst @@ -229,7 +229,7 @@ following properties: - Description * - id - ``string`` - - **Required**. The validation event ID to suppress. + - **Required**. The hierarchical validation event ID to suppress. * - namespace - ``string`` - **Required**. The validation event is only suppressed if it matches the @@ -315,6 +315,57 @@ specific. Further, a suppression ID of "ABC" does not match an event ID of - No +------------------ +Severity overrides +------------------ + +The ``severityOverrides`` metadata property is used to elevate the severity +of validation events. This property contains an array of severity override +objects that support the following properties: + +.. list-table:: + :header-rows: 1 + :widths: 20 20 60 + + * - Property + - Type + - Description + * - id + - ``string`` + - **Required**. The hierarchical validation event ID to elevate. + * - namespace + - ``string`` + - **Required**. The validation event is only elevated if it matches the + supplied namespace. A value of ``*`` can be provided to match any namespace. + * - severity + - ``string`` + - Defines the :ref:`severity ` to elevate matching + events to. This value can only be set to ``WARNING`` or ``DANGER``. + +The following example elevates the events of ``SomeValidator`` to ``DANGER`` +in any namespace, and ``OtherValidator`` is elevated to ``WARNING`` but only +for events emitted for shapes in the ``smithy.example`` namespace: + +.. code-block:: smithy + + $version: "2" + + metadata severityOverrides = [ + { + namespace: "*" + id: "SomeValidator" + severity: "DANGER" + } + { + namespace: "smithy.example" + id: "OtherValidator" + severity: "WARNING" + } + ] + + namespace smithy.example + + ------------------- Built-in validators ------------------- diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelAssembler.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelAssembler.java index 365ffe1f94f..d042acc42cb 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelAssembler.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelAssembler.java @@ -580,15 +580,18 @@ public ValidatedResult assemble() { } if (disableValidation) { - List decorators = validatorFactory.loadDecorators(); - return new ValidatedResult<>(transformed, ModelValidator.decorateEvents(decorators, events)); - } - - try { - return validate(transformed, events); - } catch (SourceException e) { - events.add(ValidationEvent.fromSourceException(e)); + ValidationEventDecorator decorator = ValidationEventDecorator.compose(validatorFactory.loadDecorators()); + for (int idx = 0; idx < events.size(); idx++) { + events.set(idx, decorator.decorate(events.get(idx))); + } return new ValidatedResult<>(transformed, events); + } else { + try { + return validate(transformed, events); + } catch (SourceException e) { + events.add(ValidationEvent.fromSourceException(e)); + return new ValidatedResult<>(transformed, events); + } } } @@ -599,22 +602,22 @@ private void addMetadataToProcessor(Map metadataMap, LoadOperation } private ValidatedResult returnOnlyErrors(Model model, List events) { - List decorators = validatorFactory.loadDecorators(); + ValidationEventDecorator decorator = ValidationEventDecorator.compose(validatorFactory.loadDecorators()); return new ValidatedResult<>(model, events.stream() .filter(event -> event.getSeverity() == Severity.ERROR) - .map(event -> ModelValidator.decorateEvent(decorators, event)) + .map(decorator::decorate) .collect(Collectors.toList())); } private ValidatedResult validate(Model model, List events) { // Validate the model based on the explicit validators and model metadata. // Note the ModelValidator handles emitting events to the validationEventListener. - List mergedEvents = new ModelValidator() + List mergedEvents = ModelValidator.builder() .validators(validators) .validatorFactory(validatorFactory) .eventListener(validationEventListener) .includeEvents(events) - .createValidator() + .build() .validate(model); return new ValidatedResult<>(model, mergedEvents); diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelValidator.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelValidator.java index 27f540cb77e..3d957c1015a 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelValidator.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelValidator.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * 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. @@ -20,7 +20,6 @@ import java.util.Collections; import java.util.List; import java.util.Objects; -import java.util.Optional; import java.util.Set; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -28,6 +27,8 @@ import software.amazon.smithy.model.SourceException; import software.amazon.smithy.model.SourceLocation; import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.model.traits.SuppressTrait; import software.amazon.smithy.model.validation.Severity; import software.amazon.smithy.model.validation.ValidatedResult; @@ -35,22 +36,37 @@ import software.amazon.smithy.model.validation.ValidationEventDecorator; import software.amazon.smithy.model.validation.Validator; import software.amazon.smithy.model.validation.ValidatorFactory; +import software.amazon.smithy.model.validation.suppressions.SeverityOverride; import software.amazon.smithy.model.validation.suppressions.Suppression; import software.amazon.smithy.model.validation.validators.ResourceCycleValidator; import software.amazon.smithy.model.validation.validators.TargetValidator; import software.amazon.smithy.utils.SetUtils; +import software.amazon.smithy.utils.SmithyBuilder; /** - * Validates a model, including validators and suppressions loaded from - * traits and metadata. + * 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 together with the validators and suppressions - * loaded from 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 { +final class ModelValidator implements Validator { + private static final String SUPPRESSIONS = "suppressions"; + private static final String SEVERITY_OVERRIDES = "severityOverrides"; // Lazy initialization holder class idiom to hold a default validator factory. private static final class LazyValidatorFactoryHolder { @@ -64,307 +80,309 @@ private static final class LazyValidatorFactoryHolder { ResourceCycleValidator.class ); + private final ValidatorFactory validatorFactory; + private final List events = new ArrayList<>(); private final List validators = new ArrayList<>(); + private final List severityOverrides = new ArrayList<>(); private final List suppressions = new ArrayList<>(); - private final List includeEvents = new ArrayList<>(); - private ValidatorFactory validatorFactory; - private Consumer eventListener; - - /** - * Sets the custom {@link Validator}s to use when running the ModelValidator. - * - * @param validators Validators to set. - * @return Returns the ModelValidator. - */ - public ModelValidator validators(Collection validators) { - this.validators.clear(); - validators.forEach(this::addValidator); - return this; + private final ValidationEventDecorator validationEventDecorator; + private final Consumer eventListener; + + ModelValidator(Builder builder) { + this.validatorFactory = builder.validatorFactory; + this.eventListener = builder.eventListener; + this.validationEventDecorator = ValidationEventDecorator.compose(validatorFactory.loadDecorators()); + this.events.addAll(builder.includeEvents); + this.suppressions.addAll(builder.suppressions); + this.validators.addAll(builder.validators); } - /** - * Adds a custom {@link Validator} to the ModelValidator. - * - * @param validator Validator to add. - * @return Returns the ModelValidator. - */ - public ModelValidator addValidator(Validator validator) { - validators.add(Objects.requireNonNull(validator)); - return this; + @Override + public List validate(Model model) { + return new LoadedModelValidator(model, this).validate(); } - /** - * Sets the {@link Suppression}s to use with the validator. - * - * @param suppressions Suppressions to set. - * @return Returns the ModelValidator. - */ - public ModelValidator suppressions(Collection suppressions) { - this.suppressions.clear(); - suppressions.forEach(this::addSuppression); - return this; + static Builder builder() { + return new Builder(); } - /** - * Adds a custom {@link Suppression} to the validator. - * - * @param suppression Suppression to add. - * @return Returns the ModelValidator. - */ - public ModelValidator addSuppression(Suppression suppression) { - suppressions.add(Objects.requireNonNull(suppression)); - return this; + static ValidatorFactory defaultValidationFactory() { + return LazyValidatorFactoryHolder.INSTANCE; } - /** - * 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. - * - * @return Returns the ModelValidator. - */ - public ModelValidator validatorFactory(ValidatorFactory validatorFactory) { - this.validatorFactory = validatorFactory; - return this; - } + static final class Builder implements SmithyBuilder { + + private final List validators = new ArrayList<>(); + private final List suppressions = new ArrayList<>(); + private final List includeEvents = new ArrayList<>(); + private ValidatorFactory validatorFactory = LazyValidatorFactoryHolder.INSTANCE; + private Consumer eventListener = event -> { }; + + private Builder() {} + + /** + * Sets the custom {@link Validator}s to use when running the ModelValidator. + * + * @param validators Validators to set. + * @return Returns the ModelValidator. + */ + public Builder validators(Collection validators) { + this.validators.clear(); + validators.forEach(this::addValidator); + 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 ModelValidator. - */ - public ModelValidator eventListener(Consumer eventListener) { - this.eventListener = eventListener; - return this; - } + /** + * Adds a custom {@link Validator} to the ModelValidator. + * + * @param validator Validator to add. + * @return Returns the ModelValidator. + */ + public Builder addValidator(Validator validator) { + validators.add(Objects.requireNonNull(validator)); + return this; + } - /** - * Includes a set of events that were already encountered in the result. - * - *

The included events may be suppressed if they match any registered - * suppressions or suppressions loaded from the model. - * - * @param events Events to include. - * @return Returns the ModelValidator. - */ - public ModelValidator includeEvents(List events) { - this.includeEvents.clear(); - this.includeEvents.addAll(events); - return this; - } + /** + * Sets the {@link Suppression}s to use with the validator. + * + * @param suppressions Suppressions to set. + * @return Returns the ModelValidator. + */ + public Builder suppressions(Collection suppressions) { + this.suppressions.clear(); + suppressions.forEach(this::addSuppression); + return this; + } - /** - * Creates a reusable Model Validator that uses every registered validator, - * suppression, and extracts validators and suppressions from each - * provided model. - * - * @return Returns the created {@link Validator}. - */ - public Validator createValidator() { - if (validatorFactory == null) { - validatorFactory = LazyValidatorFactoryHolder.INSTANCE; + /** + * Adds a custom {@link Suppression} to the validator. + * + * @param suppression Suppression to add. + * @return Returns the ModelValidator. + */ + public Builder addSuppression(Suppression suppression) { + suppressions.add(Objects.requireNonNull(suppression)); + return this; } - List staticValidators = resolveStaticValidators(); - List staticDecorators = validatorFactory.loadDecorators(); + /** + * 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. + * @return Returns the ModelValidator. + */ + public Builder validatorFactory(ValidatorFactory validatorFactory) { + this.validatorFactory = Objects.requireNonNull(validatorFactory); + return this; + } - return model -> { - List coreEvents = new ArrayList<>(); + /** + * 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 ModelValidator. + */ + public Builder eventListener(Consumer eventListener) { + this.eventListener = Objects.requireNonNull(eventListener); + return this; + } - // Add suppressions found in the model via metadata. - List modelSuppressions = new ArrayList<>(suppressions); - loadModelSuppressions(modelSuppressions, model, coreEvents); + /** + * Includes a set of events that were already encountered in the result. + * + *

Suppressions and severity overrides will be applied to the given {@code events}. + * + * @param events Events to include. + * @return Returns the ModelValidator. + */ + public Builder includeEvents(List events) { + this.includeEvents.clear(); + this.includeEvents.addAll(events); + return this; + } - // Add validators defined in the model through metadata. - List modelValidators = new ArrayList<>(staticValidators); - loadModelValidators(validatorFactory, modelValidators, model, coreEvents, modelSuppressions); + @Override + public ModelValidator build() { + validators.addAll(validatorFactory.loadBuiltinValidators()); + validators.removeIf(v -> CORE_VALIDATORS.contains(v.getClass())); + return new ModelValidator(this); + } + } - // Perform critical validation before other more granular semantic validators. - // If these validators fail, then many other validators will fail as well, - // which will only obscure the root cause. - coreEvents.addAll(suppressEvents(model, new TargetValidator().validate(model), modelSuppressions)); - coreEvents.addAll(suppressEvents(model, new ResourceCycleValidator().validate(model), modelSuppressions)); - // Decorate all the events - coreEvents = decorateEvents(staticDecorators, coreEvents); - // Emit any events that have already occurred. - coreEvents.forEach(eventListener); - - if (LoaderUtils.containsErrorEvents(coreEvents)) { - return coreEvents; - } + private static final class LoadedModelValidator { + + private final Model model; + private final List suppressions; + private final List severityOverrides; + private final List validators; + private final List events = new ArrayList<>(); + private final ValidationEventDecorator validationEventDecorator; + private final Consumer eventListener; + + private LoadedModelValidator(Model model, ModelValidator validator) { + this.model = model; + this.validationEventDecorator = validator.validationEventDecorator; + this.eventListener = validator.eventListener; + this.suppressions = new ArrayList<>(validator.suppressions); + this.severityOverrides = new ArrayList<>(validator.severityOverrides); + this.validators = new ArrayList<>(validator.validators); + + pushEvents(validator.events); + loadMetadataSuppressions(); + loadMetadataSeverityOverrides(); + loadModelValidators(validator.validatorFactory); + } - List result = modelValidators.parallelStream() - .map(validator -> validator.validate(model)) - .flatMap(Collection::stream) - .filter(ModelValidator::filterPrelude) - .map(event -> suppressEvent(model, event, modelSuppressions)) - .map(event -> decorateEvent(staticDecorators, event)) - // Emit events as they occur during validation. - .peek(eventListener) - .collect(Collectors.toList()); + private void loadMetadataSeverityOverrides() { + model.getMetadataProperty(SEVERITY_OVERRIDES).ifPresent(value -> { + List values = value.expectArrayNode().getElementsAs(ObjectNode.class); + for (ObjectNode rule : values) { + try { + severityOverrides.add(SeverityOverride.fromMetadata(rule)); + } catch (SourceException e) { + pushEvent(ValidationEvent.fromSourceException(e)); + } + } + }); + } - for (ValidationEvent event : includeEvents) { - if (ModelValidator.filterPrelude(event)) { - result.add(decorateEvent(staticDecorators, suppressEvent(model, event, modelSuppressions))); + private void loadMetadataSuppressions() { + model.getMetadataProperty(SUPPRESSIONS).ifPresent(value -> { + List values = value.expectArrayNode().getElementsAs(ObjectNode.class); + for (ObjectNode rule : values) { + try { + suppressions.add(Suppression.fromMetadata(rule)); + } catch (SourceException e) { + pushEvent(ValidationEvent.fromSourceException(e)); + } } - } + }); + } - // Add in events encountered while building up validators and suppressions. - result.addAll(coreEvents); + 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); + } + } + } - return result; - }; - } + // 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(); + } - static List decorateEvents( - List decorators, - List events - ) { - if (!decorators.isEmpty()) { - for (int idx = 0; idx < events.size(); idx++) { - events.set(idx, decorateEvent(decorators, events.get(idx))); + private void pushEvents(List source) { + for (ValidationEvent event : source) { + pushEvent(event); } } - return events; - } - static ValidationEvent decorateEvent(List decorators, ValidationEvent event) { - ValidationEvent decoratedEvent = event; - for (ValidationEventDecorator decorator : decorators) { - if (decorator.canDecorate(event)) { - decoratedEvent = decorator.decorate(decoratedEvent); - } + private void pushEvent(ValidationEvent event) { + event = modifyEventSeverity(event); + event = validationEventDecorator.decorate(event); + events.add(event); + eventListener.accept(event); } - return decoratedEvent; - } - static ValidatorFactory defaultValidationFactory() { - return LazyValidatorFactoryHolder.INSTANCE; - } + private List validate() { + // Perform critical validation before other more granular semantic validators. + pushEvents(new TargetValidator().validate(model)); + pushEvents(new ResourceCycleValidator().validate(model)); - private List resolveStaticValidators() { - List resolvedValidators = new ArrayList<>(validatorFactory.loadBuiltinValidators()); - resolvedValidators.addAll(validators); - // These core validators are applied first, so don't run them again. - resolvedValidators.removeIf(v -> CORE_VALIDATORS.contains(v.getClass())); - return resolvedValidators; - } + // Fail early if errors were detected since further validation will just obscure the root cause. + if (LoaderUtils.containsErrorEvents(events)) { + return events; + } - private static 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(); - } + List result = validators.parallelStream() + .flatMap(validator -> validator.validate(model).stream()) + .filter(this::filterPrelude) + .map(this::modifyEventSeverity) + .map(validationEventDecorator::decorate) + // Emit events as they occur during validation. + .peek(eventListener) + .collect(Collectors.toList()); - private static void loadModelValidators( - ValidatorFactory validatorFactory, - List validators, - Model model, - List events, - List suppressions - ) { - // Load validators defined in metadata. - ValidatedResult> loaded = ValidationLoader - .loadValidators(model.getMetadata()); - events.addAll(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); - events.addAll(result.getValidationEvents()); - if (result.getValidationEvents().isEmpty() && !result.getResult().isPresent()) { - ValidationEvent event = unknownValidatorError(val.name, val.sourceLocation); - events.add(suppressEvent(model, event, suppressions)); - } + // Add in events encountered while building up validators and suppressions. + result.addAll(events); + + return result; } - } - // 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 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(); + } + + private ValidationEvent modifyEventSeverity(ValidationEvent event) { + // Use a suppress trait if present. + if (event.getShapeId().isPresent()) { + ShapeId target = event.getShapeId().get(); + Shape shape = model.getShape(target).orElse(null); + if (shape != null) { + if (shape.hasTrait(SuppressTrait.class)) { + Suppression suppression = Suppression.fromSuppressTrait(shape); + if (suppression.test(event)) { + return changeSeverity(event, Severity.SUPPRESSED, suppression.getReason().orElse(null)); + } + } + } + } - private static void loadModelSuppressions( - List suppressions, - Model model, - List events - ) { - model.getMetadataProperty(SUPPRESSIONS).ifPresent(value -> { - List values = value.expectArrayNode().getElementsAs(ObjectNode.class); - for (ObjectNode rule : values) { - try { - suppressions.add(Suppression.fromMetadata(rule)); - } catch (SourceException e) { - events.add(ValidationEvent.fromSourceException(e)); + // Check metadata and manual suppressions. + for (Suppression suppression : suppressions) { + if (suppression.test(event)) { + return changeSeverity(event, Severity.SUPPRESSED, suppression.getReason().orElse(null)); } } - }); - } - private static List suppressEvents( - Model model, - List events, - List suppressions) { - return events.stream().map(event -> suppressEvent(model, event, suppressions)).collect(Collectors.toList()); - } + Severity appliedSeverity = event.getSeverity(); + for (SeverityOverride override : severityOverrides) { + Severity overrideResult = override.apply(event); + if (overrideResult.ordinal() > appliedSeverity.ordinal()) { + appliedSeverity = overrideResult; + } + } - private static ValidationEvent suppressEvent(Model model, ValidationEvent event, List suppressions) { - // ERROR and SUPPRESSED events cannot be suppressed. - if (!event.getSeverity().canSuppress()) { - return event; + return changeSeverity(event, appliedSeverity, null); } - Suppression matchedSuppression = findMatchingSuppression(model, event, suppressions); - - if (matchedSuppression == null) { - return event; + private static ValidationEvent changeSeverity(ValidationEvent event, Severity severity, String reason) { + if (event.getSeverity() == severity) { + return event; + } else { + // The event was suppressed so change the severity and reason. + ValidationEvent.Builder builder = event.toBuilder(); + builder.severity(severity); + if (reason != null) { + builder.suppressionReason(reason); + } + return builder.build(); + } } - - // The event was suppressed so change the severity and reason. - ValidationEvent.Builder builder = event.toBuilder(); - builder.severity(Severity.SUPPRESSED); - matchedSuppression.getReason().ifPresent(builder::suppressionReason); - - return builder.build(); - } - - private static Suppression findMatchingSuppression( - Model model, - ValidationEvent event, - List suppressions - ) { - return event.getShapeId() - .flatMap(model::getShape) - // First check for trait based suppressions. - .flatMap(shape -> shape.hasTrait(SuppressTrait.class) - ? Optional.of(Suppression.fromSuppressTrait(shape)) - : Optional.empty()) - // Try to suppress it. - .flatMap(suppression -> suppression.test(event) ? Optional.of(suppression) : Optional.empty()) - // If it wasn't suppressed, then try the rules loaded from metadata. - .orElseGet(() -> { - for (Suppression suppression : suppressions) { - if (suppression.test(event)) { - return suppression; - } - } - return null; - }); } } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/ValidationEventDecorator.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/ValidationEventDecorator.java index a18560cbd7f..8cfba31d16a 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/validation/ValidationEventDecorator.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/ValidationEventDecorator.java @@ -15,6 +15,8 @@ package software.amazon.smithy.model.validation; +import java.util.List; + /** * Validation event decorators take validation events and transform them by adding more contextual information, * usually adding a hint to let the user know what can it be done to solve the issue. This might add context specific @@ -37,4 +39,49 @@ public interface ValidationEventDecorator { * @return The decorated event or the original one if no decoration took place. */ ValidationEvent decorate(ValidationEvent ev); + + /** + * Creates a decorator composed of one or more decorators. + * + * @param decorators Decorators to compose. + * @return Returns the composed decorator. + */ + static ValidationEventDecorator compose(List decorators) { + if (decorators.isEmpty()) { + return new ValidationEventDecorator() { + @Override + public boolean canDecorate(ValidationEvent ev) { + return false; + } + + @Override + public ValidationEvent decorate(ValidationEvent ev) { + return ev; + } + }; + } else { + return new ValidationEventDecorator() { + @Override + public boolean canDecorate(ValidationEvent ev) { + for (ValidationEventDecorator decorator : decorators) { + if (decorator.canDecorate(ev)) { + return true; + } + } + return false; + } + + @Override + public ValidationEvent decorate(ValidationEvent ev) { + ValidationEvent decoratedEvent = ev; + for (ValidationEventDecorator decorator : decorators) { + if (decorator.canDecorate(ev)) { + decoratedEvent = decorator.decorate(decoratedEvent); + } + } + return decoratedEvent; + } + }; + } + } } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/suppressions/MetadataSeverityOverride.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/suppressions/MetadataSeverityOverride.java new file mode 100644 index 00000000000..1a1cb4791b3 --- /dev/null +++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/suppressions/MetadataSeverityOverride.java @@ -0,0 +1,60 @@ +/* + * 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.validation.suppressions; + +import java.util.Collection; +import java.util.function.Predicate; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.validation.Severity; +import software.amazon.smithy.model.validation.ValidationEvent; +import software.amazon.smithy.utils.ListUtils; + +final class MetadataSeverityOverride implements SeverityOverride { + + private static final String ID = "id"; + private static final String NAMESPACE = "namespace"; + private static final String SEVERITY = "severity"; + private static final Collection KEYS = ListUtils.of(ID, NAMESPACE, SEVERITY); + private static final Predicate STAR_MATCHER = event -> true; + private static final String[] SEVERITIES = {"WARNING", "DANGER"}; + + private final String id; + private final String namespace; + private final Severity severity; + private final Predicate namespaceMatcher; + + MetadataSeverityOverride(String id, String namespace, Severity severity) { + this.id = id; + this.namespace = namespace; + this.severity = severity; + this.namespaceMatcher = namespace.equals("*") ? STAR_MATCHER : new NamespacePredicate(this.namespace); + } + + static MetadataSeverityOverride fromNode(Node node) { + ObjectNode rule = node.expectObjectNode(); + rule.warnIfAdditionalProperties(KEYS); + String id = rule.expectStringMember(ID).getValue(); + String namespace = rule.expectStringMember(NAMESPACE).getValue(); + String severity = rule.expectStringMember(SEVERITY).expectOneOf(SEVERITIES); + return new MetadataSeverityOverride(id, namespace, Severity.valueOf(severity)); + } + + @Override + public Severity apply(ValidationEvent event) { + return namespaceMatcher.test(event) ? severity : event.getSeverity(); + } +} diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/suppressions/MetadataSuppression.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/suppressions/MetadataSuppression.java index db3af4835b4..a48da21cca2 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/validation/suppressions/MetadataSuppression.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/suppressions/MetadataSuppression.java @@ -17,6 +17,7 @@ import java.util.Collection; import java.util.Optional; +import java.util.function.Predicate; import software.amazon.smithy.model.node.Node; import software.amazon.smithy.model.node.ObjectNode; import software.amazon.smithy.model.validation.ValidationEvent; @@ -33,15 +34,18 @@ final class MetadataSuppression implements Suppression { private static final String NAMESPACE = "namespace"; private static final String REASON = "reason"; private static final Collection SUPPRESSION_KEYS = ListUtils.of(ID, NAMESPACE, REASON); + private static final Predicate STAR_MATCHER = event -> true; private final String id; private final String namespace; private final String reason; + private final Predicate namespaceMatcher; MetadataSuppression(String id, String namespace, String reason) { this.id = id; this.namespace = namespace; this.reason = reason; + this.namespaceMatcher = namespace.equals("*") ? STAR_MATCHER : new NamespacePredicate(namespace); } static MetadataSuppression fromNode(Node node) { @@ -55,16 +59,11 @@ static MetadataSuppression fromNode(Node node) { @Override public boolean test(ValidationEvent event) { - return event.containsId(id) && matchesNamespace(event); + return event.containsId(id) && namespaceMatcher.test(event); } @Override public Optional getReason() { return Optional.ofNullable(reason); } - - private boolean matchesNamespace(ValidationEvent event) { - return namespace.equals("*") - || event.getShapeId().filter(id -> id.getNamespace().equals(namespace)).isPresent(); - } } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/suppressions/NamespacePredicate.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/suppressions/NamespacePredicate.java new file mode 100644 index 00000000000..02eab6e7775 --- /dev/null +++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/suppressions/NamespacePredicate.java @@ -0,0 +1,39 @@ +/* + * 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.validation.suppressions; + +import java.util.function.Predicate; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.validation.ValidationEvent; + +final class NamespacePredicate implements Predicate { + + private final String namespace; + + NamespacePredicate(String namespace) { + this.namespace = namespace; + } + + @Override + public boolean test(ValidationEvent event) { + ShapeId id = event.getShapeId().orElse(null); + if (id == null) { + return false; + } + + return id.getNamespace().equals(namespace); + } +} diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/suppressions/SeverityOverride.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/suppressions/SeverityOverride.java new file mode 100644 index 00000000000..caca968c5b9 --- /dev/null +++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/suppressions/SeverityOverride.java @@ -0,0 +1,47 @@ +/* + * 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.validation.suppressions; + +import software.amazon.smithy.model.node.ExpectationNotMetException; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.validation.Severity; +import software.amazon.smithy.model.validation.ValidationEvent; + +/** + * Changes the severity of a validation event. + */ +@FunctionalInterface +public interface SeverityOverride { + + /** + * Returns the severity to apply to the validation event. + * + * @param event Event to modify. + * @return Returns the severity to use with the event. + */ + Severity apply(ValidationEvent event); + + /** + * Creates a severity override from a {@link Node} found in the "severityOverrides" metadata of a Smithy model. + * + * @param node Node to parse. + * @return Returns the loaded override. + * @throws ExpectationNotMetException if the override node is malformed. + */ + static SeverityOverride fromMetadata(Node node) { + return MetadataSeverityOverride.fromNode(node); + } +} diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/PropertyBindingIndexTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/PropertyBindingIndexTest.java index d11da1713f4..b51c20ac8a6 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/PropertyBindingIndexTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/PropertyBindingIndexTest.java @@ -15,18 +15,20 @@ package software.amazon.smithy.model.knowledge; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.OperationShape; import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.validation.Severity; import software.amazon.smithy.model.validation.ValidatedResult; -import software.amazon.smithy.model.validation.ValidatedResultException; public class PropertyBindingIndexTest { @@ -68,6 +70,9 @@ public void testIndex() { assertTrue(index.doesMemberShapeRequireProperty(model.expectShape( ShapeId.from("com.example#ChangeResourceOutput$id"), MemberShape.class))); - Assertions.assertThrows(ValidatedResultException.class, () -> vrmodel.unwrap()); + + assertThat(vrmodel.getValidationEvents(Severity.SUPPRESSED), hasSize(1)); + assertThat(vrmodel.getValidationEvents(Severity.SUPPRESSED).get(0).getId(), + equalTo("ResourceOperationInputOutput")); } } diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/resource-properties/invalid-output-identifier.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/resource-properties/invalid-output-identifier.smithy index 649e961bf9e..aaf228cb343 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/resource-properties/invalid-output-identifier.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/resource-properties/invalid-output-identifier.smithy @@ -1,12 +1,5 @@ $version: "2.0" -metadata suppressions = [ - { - id: "ResourceOperationInputOutput", - namespace: "com.example" - } -] - namespace com.example resource Resource1 { diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/cannot-change-severity-of-error.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/cannot-change-severity-of-error.errors new file mode 100644 index 00000000000..418bdc06da9 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/cannot-change-severity-of-error.errors @@ -0,0 +1 @@ +[ERROR] smithy.example#Invalid: A length trait is applied with a negative `min` value. | LengthTrait diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/cannot-change-severity-of-error.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/cannot-change-severity-of-error.smithy new file mode 100644 index 00000000000..d6521386266 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/cannot-change-severity-of-error.smithy @@ -0,0 +1,15 @@ +$version: "2.0" + +metadata severityOverrides = [ + { + namespace: "*" + id: "LengthTrait" + severity: "WARNING" + } +] + +namespace smithy.example + +// This emits an error which the above severityOverride attempts, but fails, to lower the severity of. +@length(min: -1) +string Invalid diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/cannot-elevate-to-error.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/cannot-elevate-to-error.errors new file mode 100644 index 00000000000..e27c6bf9628 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/cannot-elevate-to-error.errors @@ -0,0 +1 @@ +[ERROR] -: Expected one of `DANGER`, `WARNING`; got `ERROR`. | Model diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/cannot-elevate-to-error.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/cannot-elevate-to-error.smithy new file mode 100644 index 00000000000..89694ebee8a --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/cannot-elevate-to-error.smithy @@ -0,0 +1,10 @@ +$version: "2.0" + +// Cannot override a severity to ERROR. +metadata severityOverrides = [ + { + namespace: "*" + id: "Foo" + severity: "ERROR" + } +] diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/elevate-severity.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/elevate-severity.errors new file mode 100644 index 00000000000..3b489e045a9 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/elevate-severity.errors @@ -0,0 +1,2 @@ +[DANGER] smithy.example#IntegerTarget: Note the integer please | NoteTheInteger +[DANGER] smithy.example#ListTarget: Note the list please | NoteTheList diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/elevate-severity.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/elevate-severity.smithy new file mode 100644 index 00000000000..50b9ef36f70 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/elevate-severity.smithy @@ -0,0 +1,49 @@ +$version: "2.0" + +metadata validators = [ + { + name: "EmitEachSelector" + id: "NoteTheList" + message: "Note the list please" + severity: "NOTE" + configuration: { + selector: "[id|namespace = smithy.example] list" + } + } + { + name: "EmitEachSelector" + id: "NoteTheInteger" + message: "Note the integer please" + severity: "WARNING" + configuration: { + selector: "[id|namespace = smithy.example] integer" + } + } +] + +metadata severityOverrides = [ + { + namespace: "*" + id: "NoteTheList" + severity: "DANGER" + } + { + namespace: "smithy.example" + id: "NoteTheList" + severity: "DANGER" + } + // This one is ignored because it has a severity < DANGER. + { + namespace: "smithy.example" + id: "NoteTheList" + severity: "WARNING" + } +] + +namespace smithy.example + +integer IntegerTarget + +list ListTarget { + member: String +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/missing-id.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/missing-id.errors new file mode 100644 index 00000000000..7aff59c0183 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/missing-id.errors @@ -0,0 +1 @@ +[ERROR] -: Missing expected member `id`. | Model diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/missing-id.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/missing-id.smithy new file mode 100644 index 00000000000..4f96adba4a5 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/missing-id.smithy @@ -0,0 +1,8 @@ +$version: "2.0" + +// Each element requires an ID. +metadata severityOverrides = [ + { + namespace: "*" + } +] diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/missing-namespace.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/missing-namespace.errors new file mode 100644 index 00000000000..03ba62a88a8 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/missing-namespace.errors @@ -0,0 +1 @@ +[ERROR] -: Missing expected member `namespace`. | Model diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/missing-namespace.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/missing-namespace.smithy new file mode 100644 index 00000000000..5aaa063af23 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/missing-namespace.smithy @@ -0,0 +1,8 @@ +$version: "2.0" + +// Each element requires a namespace. +metadata severityOverrides = [ + { + id: "Foo" + } +] diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/missing-severity.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/missing-severity.errors new file mode 100644 index 00000000000..5669c6b7972 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/missing-severity.errors @@ -0,0 +1 @@ +[ERROR] -: Missing expected member `severity`. | Model diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/missing-severity.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/missing-severity.smithy new file mode 100644 index 00000000000..e0d73293cf9 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/missing-severity.smithy @@ -0,0 +1,9 @@ +$version: "2.0" + +// Each element requires a severity. +metadata severityOverrides = [ + { + namespace: "*" + id: "Foo" + } +] diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/override-hierarchy.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/override-hierarchy.errors new file mode 100644 index 00000000000..4badff1a9e3 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/override-hierarchy.errors @@ -0,0 +1,2 @@ +[WARNING] smithy.example#IntegerTarget: Note the shape | NoteTheShape.Integer +[WARNING] smithy.example#ListTarget: Note the list please | NoteTheShape.List diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/override-hierarchy.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/override-hierarchy.smithy new file mode 100644 index 00000000000..b836a142f09 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/override-hierarchy.smithy @@ -0,0 +1,38 @@ +$version: "2.0" + +metadata validators = [ + { + name: "EmitEachSelector" + id: "NoteTheShape.Integer" + message: "Note the shape" + severity: "NOTE" + configuration: { + selector: "[id|namespace = smithy.example] integer" + } + } + { + name: "EmitEachSelector" + id: "NoteTheShape.List" + message: "Note the list please" + severity: "NOTE" + configuration: { + selector: "[id|namespace = smithy.example] list" + } + } +] + +metadata severityOverrides = [ + { + namespace: "*" + id: "NoteTheShape" + severity: "WARNING" + } +] + +namespace smithy.example + +integer IntegerTarget + +list ListTarget { + member: String +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/should-be-array-of-objects.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/should-be-array-of-objects.errors new file mode 100644 index 00000000000..dbe12a4ac1e --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/should-be-array-of-objects.errors @@ -0,0 +1 @@ +[ERROR] -: Expected array element 0 to be a object but found boolean. | Model diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/should-be-array-of-objects.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/should-be-array-of-objects.smithy new file mode 100644 index 00000000000..0ca3e83d856 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/should-be-array-of-objects.smithy @@ -0,0 +1,6 @@ +$version: "2.0" + +// Should be array of objects +metadata severityOverrides = [ + true +] diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/should-be-array.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/should-be-array.errors new file mode 100644 index 00000000000..bb163d0b05e --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/should-be-array.errors @@ -0,0 +1 @@ +[ERROR] -: Expected array, but found object. | Model diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/should-be-array.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/should-be-array.smithy new file mode 100644 index 00000000000..48c1a73a5ef --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/severityOverrides/should-be-array.smithy @@ -0,0 +1,4 @@ +$version: "2.0" + +// Should be array +metadata severityOverrides = {}