diff --git a/.changes/next-release/feature-5c018acaef9147e7499e70c6d8919d6cf0b496c5.json b/.changes/next-release/feature-5c018acaef9147e7499e70c6d8919d6cf0b496c5.json new file mode 100644 index 00000000000..890bc66174a --- /dev/null +++ b/.changes/next-release/feature-5c018acaef9147e7499e70c6d8919d6cf0b496c5.json @@ -0,0 +1,7 @@ +{ + "type": "feature", + "description": "Added a new `metadata` trait that allows model authors to declare types for metadata keys that will be automatically validated when building models.", + "pull_requests": [ + "[#3078](https://github.com/smithy-lang/smithy/pull/3078)" + ] +} diff --git a/designs/typed-metadata.md b/designs/typed-metadata.md new file mode 100644 index 00000000000..d56b3f04992 --- /dev/null +++ b/designs/typed-metadata.md @@ -0,0 +1,172 @@ +# Typed Metadata + +Metadata is a schema-less extensibility mechanism used to associate metadata to +an entire model. For example, metadata is used to define validators and +model-wide suppressions. + +This document describes a way to define typing information for metadata that is +automatically validated by Smithy. + +## Motivation + +The schema-less nature of metadata has allowed it to be used for any purpose +without much hassle. However, that has come at the cost of increased validation +complexity. Any tool, including Smithy itself, that uses metadata in a +structured way has to perform validation itself. By allowing opt-in validation, +we can centralize and deduplicate that effort. + +## Proposal + +Model authors may globally declare the type of a metadata key by targeting a +shape with the `@metadata` trait. + +```smithy +/// Defines a type for a metadata key. +/// +/// If a matching key is defined in the model, its value will be validated +/// according to the targeted shape. +/// +/// The type for any metadata key MUST only be defined once. +@trait(selector: "dataType :not([trait|input]) :not([trait|output])") +structure metadata { + /// The metadata key to validate. Each key MUST only be defined once. + @required + @length(min: 1) + key: String +} +``` + +For example, the +[suppressions metadata](https://smithy.io/2.0/spec/model-validation.html#suppressions-metadata) +could be defined as: + +```smithy +$version: "2.0" + +namespace smithy.api + +@metadata(key: "suppressions") +list MetadataSuppressions { + member: MetadataSuppression +} + +structure MetadataSuppression { + @required + id: String + + @required + @pattern("^(\*|[_a-zA-Z]\w*(\.[_a-zA-Z]\w*)*)$") + namespace: String + + reason: String +} +``` + +### Validation + +A metadata key MUST NOT be defined more than once with the `@metadata` trait. If +a metadata key is defined more than once with the `@metadata` trait, an +`ERROR`-level validation event will be emitted. + +Validation of metadata values will behave no differently than validation +anywhere else, with severity levels being determined by each individual +validator. + +Shapes targeted by the metadata shape and shapes transitively referenced by +those shapes will not trigger the unreferenced shapes validator. + +Adding or removing this trait is considered backwards-compatible because +metadata does not inherently change any interface. It may cause a build to break +if the new validation rejects existing metadata, but that is intended behavior. + +## Alternatives + +### Inline definitions + +The metadata trait globally defines the shape of a metadata key, but we could +allow local, inline definitions additionally or instead. This can be achieved by +using a special `$type` key in metadata's node value. + +```smithy +$version: "2.0" + +metadata foo = { + "$type": "com.example#Foo" + "bar": "baz" +} + +namespace com.example + +structure Foo { + bar: String +} +``` + +The problem with this strategy is that it can only be applied to structure and +union shapes, because other shape types have nowhere to add the `$type` key or +it would potentially shadow a valid value key. + +Other shapes could instead be defined via a `__type__` metadata key: + +```smithy +$version: "2.0" + +metadata "__type__": [ + ["foo", "com.example#Foo"] +] + +metadata foo = "bar" + +namespace com.example + +string Foo + +@metadata(key: "__type__") +list MetdataTypeDeclarations { + member: MetadataTypeDeclaration +} + +@length(min: 2, max: 2) +list MetadataTypeDeclaration { + member: String +} +``` + +Unfortunately, this is inherently a global declaration that is not inline with +the value, defeating the intent of an inline definition. The metadata trait is +better suited to this purpose, as it is clearer, easier to validate, more easily +discoverable, and more easily trackable. + +Smithy 2.1 could introduce new syntax to declare inline metadata types that isn't +tied to the value, but the AST could not support it without either exposing the +same problem or making a breaking change. + +## FAQ + +### Does this interact with metadata merging semantics? + +When a given metadata key is defined more than once, it usually results in a +conflict that fails the build. If both definitions are arrays, however, they are +instead merged. If a metadata key is defined as a list, the merged array will be +validated. + +This trait does not change merging semantics. + +### Will the existing defined metadata keys get metadata-trait-based definitions? + +Smithy currently has definitions for three metadata keys: `severityOverrides`, +`suppressions`, and `validators`. All three could be defined with the metadata +trait. + +However, these three keys are currently parsed and validated early on when +loading a model, before validation generally is run. They will need to be +special-cased to either run early or to be skipped in favor of existing +validation. + +These keys will initially be reserved so they can't be defined. As the keys get +full definitions, their reserve list entries will be removed. + +### What happens if a defined metadata key is not used? + +Nothing. Metadata keys are inherently optional. A `required` member may be added +later to enforce presence if there is a need. diff --git a/docs/source-2.0/spec/model-validation.rst b/docs/source-2.0/spec/model-validation.rst index 7c20a53ce66..102c07897aa 100644 --- a/docs/source-2.0/spec/model-validation.rst +++ b/docs/source-2.0/spec/model-validation.rst @@ -867,3 +867,86 @@ Validator definition - ``string`` - The :ref:`severity ` to use when an incompatible shape is found. Defaults to ``ERROR`` if not set. + + +.. smithy-trait:: smithy.api#metadata +.. _metadata-trait: + +------------------ +``metadata`` trait +------------------ + +:ref:`Metadata ` is schema-less by default. Any tool that consumes +metadata, including Smithy itself, must validate it independently. The +``metadata`` trait lets model authors define a type for a metadata key. +When the type for a metadata key is defined this way, Smithy will automatically +validate any value for that metadata key against its defined type. + +Summary + Defines a type for a metadata key by targeting a shape. When a metadata + entry with a matching key is defined in the model, its value is + validated against the targeted shape. +Trait selector + ``dataType :not([trait|input]) :not([trait|output])`` + + *A :ref:`simple shape ` or + :ref:`aggregate shape ` that is not marked with the + :ref:`input trait ` or :ref:`output trait `.* +Value type + ``structure`` + +The ``metadata`` trait is a structure that contains the following members: + +.. list-table:: + :header-rows: 1 + :widths: 10 10 80 + + * - Property + - Type + - Description + * - key + - ``string`` + - **Required**. The metadata key whose type is being defined. The + type of any metadata key may only be defined once across the entire + model. This value must be non-empty. + +.. rubric:: Example + +Consider a service that configures an ``auditLevel`` metadata entry. Defining +a type for the key gives Smithy enough information to validate each usage: + +.. code-block:: smithy + + $version: "2" + namespace smithy.example + + @metadata(key: "auditLevel") + enum AuditLevel { + LOW + MEDIUM + HIGH + } + +With that declaration in place, any ``auditLevel`` metadata entry in the +model is automatically validated against the ``AuditLevel`` shape. For example, +the following model would produce a validation event because an invalid enum +value is provided: + +.. code-block:: smithy + + $version: "2" + + metadata auditLevel = "unknown" + +Building the above model results in the following validation event: + +.. code-block:: + + ── ERROR ──────────────────────────────────────────── TypedMetadata.auditLevel + File: example.smithy:3:23 + + 3| metadata auditLevel = "unknown" + | ^ + + metadata.auditLevel: String value provided for smithy.example#AuditLevel must + be one of the following values: HIGH, LOW, MEDIUM diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/UnreferencedShapes.java b/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/UnreferencedShapes.java index 9ca4566d726..4874f304c18 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/UnreferencedShapes.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/UnreferencedShapes.java @@ -13,13 +13,19 @@ import software.amazon.smithy.model.selector.Selector; import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.MetadataTrait; import software.amazon.smithy.model.traits.TraitDefinition; import software.amazon.smithy.utils.FunctionalUtils; /** - * Finds shapes that are not connected to a "root" shape, are not trait definitions, are not referenced by trait - * definitions, and are not referenced in trait values through - * {@link software.amazon.smithy.model.traits.IdRefTrait}. + * Finds shapes that do not meet any of the following criteria: + * + *
    + *
  • The shape is connected to a "root" shape. + *
  • The shape is a trait definition or is connected to a trait definition. + *
  • The shape is referenced in a trait value through {@link software.amazon.smithy.model.traits.IdRefTrait}. + *
  • The shape is a metadata definition or is connected to a metadata definition. + *
* *

The "root" shapes defaults to all service shapes in the model. You can customize this by providing a selector * that considers every matching shape a root shape. For example, a model might consider all shapes marked with @@ -82,6 +88,11 @@ public Set compute(Model model) { shapeWalker.iterateShapes(trait, traversed).forEachRemaining(shape -> connected.add(shape.getId())); } + // Don't remove shapes that are metadata definitions or connected to metadata definitions. + for (Shape metadata : model.getShapesWithTrait(MetadataTrait.class)) { + shapeWalker.iterateShapes(metadata, traversed).forEachRemaining(shape -> connected.add(shape.getId())); + } + // Any shape that wasn't identified as connected to a root is considered unreferenced. Set result = new HashSet<>(); for (Shape shape : model.toSet()) { diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/traits/MetadataTrait.java b/smithy-model/src/main/java/software/amazon/smithy/model/traits/MetadataTrait.java new file mode 100644 index 00000000000..0a1a3c6cac4 --- /dev/null +++ b/smithy-model/src/main/java/software/amazon/smithy/model/traits/MetadataTrait.java @@ -0,0 +1,92 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package software.amazon.smithy.model.traits; + +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.utils.SmithyBuilder; +import software.amazon.smithy.utils.ToSmithyBuilder; + +/** + * Globally declares the type of a metadata key. + * + *

When this trait is applied to a shape, any metadata entry in the model + * with the same key is validated against the targeted shape. A given + * metadata key may only have its type declared once across the entire model. + */ +public final class MetadataTrait extends AbstractTrait implements ToSmithyBuilder { + public static final ShapeId ID = ShapeId.from("smithy.api#metadata"); + + private final String key; + + private MetadataTrait(Builder builder) { + super(ID, builder.getSourceLocation()); + this.key = SmithyBuilder.requiredState("key", builder.key); + } + + /** + * @return Creates a builder for a {@link MetadataTrait}. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * @return The metadata key that this trait defines a type for. + */ + public String getKey() { + return key; + } + + @Override + public Builder toBuilder() { + return builder().sourceLocation(getSourceLocation()).key(key); + } + + @Override + protected Node createNode() { + return Node.objectNodeBuilder() + .sourceLocation(getSourceLocation()) + .withMember("key", key) + .build(); + } + + public static final class Provider extends AbstractTrait.Provider { + public Provider() { + super(ID); + } + + @Override + public Trait createTrait(ShapeId target, Node value) { + Builder builder = builder().sourceLocation(value.getSourceLocation()); + builder.key(value.expectObjectNode().expectStringMember("key").getValue()); + MetadataTrait result = builder.build(); + result.setNodeCache(value); + return result; + } + } + + public static final class Builder extends AbstractTraitBuilder { + private String key; + + private Builder() {} + + /** + * Sets the metadata key that this trait defines a type for. + * + * @param key The metadata key. Required. + * @return Returns the builder. + */ + public Builder key(String key) { + this.key = key; + return this; + } + + @Override + public MetadataTrait build() { + return new MetadataTrait(this); + } + } +} diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/MetadataTraitValidator.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/MetadataTraitValidator.java new file mode 100644 index 00000000000..af0d7032346 --- /dev/null +++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/MetadataTraitValidator.java @@ -0,0 +1,86 @@ +/* + * 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.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.stream.Collectors; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.MetadataTrait; +import software.amazon.smithy.model.validation.AbstractValidator; +import software.amazon.smithy.model.validation.ValidationEvent; +import software.amazon.smithy.utils.SetUtils; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Validates applications of the {@code metadata} trait itself. + * + *

A metadata key MUST NOT have a type defined by more than one shape + * carrying the {@code metadata} trait. Each shape participating in a + * conflict receives an {@code ERROR} event. + * + *

Validation of the metadata values themselves is handled separately by + * {@link TypedMetadataValidator}. + */ +@SmithyInternalApi +public final class MetadataTraitValidator extends AbstractValidator { + // We will eventually add trait definitions for these, but for now + // this ensures that nobody else can. + private static final Set RESERVED = SetUtils.of("severityOverrides", "suppressions", "validators"); + + @Override + public List validate(Model model) { + Map> shapesByKey = new LinkedHashMap<>(); + for (Shape shape : model.getShapesWithTrait(MetadataTrait.class)) { + String key = shape.expectTrait(MetadataTrait.class).getKey(); + shapesByKey.computeIfAbsent(key, k -> new ArrayList<>()).add(shape); + } + + List events = new ArrayList<>(); + for (Map.Entry> entry : shapesByKey.entrySet()) { + if (RESERVED.contains(entry.getKey())) { + events.addAll(emitReservedEvents(entry.getKey(), entry.getValue())); + } else if (entry.getValue().size() > 1) { + events.addAll(emitDuplicateEvents(entry.getKey(), entry.getValue())); + } + } + return events; + } + + private List emitReservedEvents(String key, List shapes) { + List events = new ArrayList<>(); + for (Shape shape : shapes) { + events.add(error(shape, + shape.expectTrait(MetadataTrait.class), + String.format( + "The metadata key `%s` has been reserved by Smithy and may not be defined elsewhere.", + key))); + } + return events; + } + + private List emitDuplicateEvents(String key, List shapes) { + Set allIds = shapes.stream().map(Shape::toShapeId).map(ShapeId::toString).collect(Collectors.toSet()); + List events = new ArrayList<>(); + for (Shape shape : shapes) { + Set others = new TreeSet<>(allIds); + others.remove(shape.getId().toString()); + events.add(error(shape, + shape.expectTrait(MetadataTrait.class), + String.format( + "A type has already been defined for the metadata key `%s`. " + + "Conflicts with: [%s]", + key, + String.join(", ", others)))); + } + return events; + } +} diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/TypedMetadataValidator.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/TypedMetadataValidator.java new file mode 100644 index 00000000000..d01c8e995dc --- /dev/null +++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/TypedMetadataValidator.java @@ -0,0 +1,71 @@ +/* + * 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.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.traits.MetadataTrait; +import software.amazon.smithy.model.validation.AbstractValidator; +import software.amazon.smithy.model.validation.NodeValidationVisitor; +import software.amazon.smithy.model.validation.ValidationEvent; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Validates metadata entries against their declared types. + */ +@SmithyInternalApi +public final class TypedMetadataValidator extends AbstractValidator { + + @Override + public List validate(Model model) { + Map metadataTypes = collectMetadataTypes(model); + + List events = new ArrayList<>(); + for (Map.Entry entry : metadataTypes.entrySet()) { + String key = entry.getKey(); + Node value = model.getMetadata().get(key); + if (value == null) { + continue; + } + + Shape shape = entry.getValue(); + NodeValidationVisitor visitor = NodeValidationVisitor.builder() + .model(model) + .value(value) + .eventId("TypedMetadata." + key) + .startingContext("metadata." + key) + .addFeature(NodeValidationVisitor.Feature.REQUIRE_BASE_64_BLOB_VALUES) + .addFeature(NodeValidationVisitor.Feature.RANGE_TRAIT_ZERO_VALUE_WARNING) + .build(); + events.addAll(shape.accept(visitor)); + } + return events; + } + + private Map collectMetadataTypes(Model model) { + Map metadataTypes = new HashMap<>(); + Set duplicates = new HashSet<>(); + for (Shape shape : model.getShapesWithTrait(MetadataTrait.class)) { + String key = shape.expectTrait(MetadataTrait.class).getKey(); + if (metadataTypes.containsKey(key)) { + duplicates.add(key); + } else { + metadataTypes.put(key, shape); + } + } + // If the type for the key is defined multiple times, don't try to + // validate it. This invalid state will produce a validation event + // in the validator for the metadata trait itself. + duplicates.forEach(metadataTypes::remove); + return metadataTypes; + } +} diff --git a/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService b/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService index c02d03c9717..5a25f244a2a 100644 --- a/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService +++ b/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService @@ -38,6 +38,7 @@ software.amazon.smithy.model.traits.JsonNameTrait$Provider software.amazon.smithy.model.traits.LengthTrait$Provider software.amazon.smithy.model.traits.LongPollTrait$Provider software.amazon.smithy.model.traits.MediaTypeTrait$Provider +software.amazon.smithy.model.traits.MetadataTrait$Provider software.amazon.smithy.model.traits.MixinTrait$Provider software.amazon.smithy.model.traits.NestedPropertiesTrait$Provider software.amazon.smithy.model.traits.NoReplaceTrait$Provider diff --git a/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator b/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator index 563056a3e60..d687eea65cb 100644 --- a/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator +++ b/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator @@ -28,6 +28,7 @@ software.amazon.smithy.model.validation.validators.JsonNameValidator software.amazon.smithy.model.validation.validators.LengthTraitValidator software.amazon.smithy.model.validation.validators.MediaTypeValidator software.amazon.smithy.model.validation.validators.MemberShouldReferenceResourceValidator +software.amazon.smithy.model.validation.validators.MetadataTraitValidator software.amazon.smithy.model.validation.validators.NoInlineDocumentSupportValidator software.amazon.smithy.model.validation.validators.OperationValidator software.amazon.smithy.model.validation.validators.PaginatedTraitValidator @@ -55,6 +56,7 @@ software.amazon.smithy.model.validation.validators.TraitBreakingChangesValidator software.amazon.smithy.model.validation.validators.TraitConflictValidator software.amazon.smithy.model.validation.validators.TraitTargetValidator software.amazon.smithy.model.validation.validators.TraitValueValidator +software.amazon.smithy.model.validation.validators.TypedMetadataValidator software.amazon.smithy.model.validation.validators.UnionValidator software.amazon.smithy.model.validation.validators.UnitTypeValidator software.amazon.smithy.model.validation.validators.UnstableTraitValidator diff --git a/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy b/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy index 4adc6d79e83..bd6675cb232 100644 --- a/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy +++ b/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy @@ -350,6 +350,20 @@ structure TraitValidator { severity: Severity = "ERROR" } +/// Defines a type for a metadata key. +/// +/// If a matching key is defined in the model, its value will be validated +/// according to the targeted shape. +/// +/// The type for any metadata key MUST only be defined once. +@trait(selector: "dataType :not([trait|input]) :not([trait|output])") +structure metadata { + /// The metadata key to validate. Each key MUST only be defined once. + @required + @length(min: 1) + key: String +} + /// Provides a structure member with a default value. When added to root /// level shapes, requires that every targeting structure member defines the /// same default value on the member or sets a default of null. diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/neighbor/UnreferencedShapesTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/neighbor/UnreferencedShapesTest.java index 7479b9ac077..f80080d7798 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/neighbor/UnreferencedShapesTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/neighbor/UnreferencedShapesTest.java @@ -7,6 +7,8 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.hasItems; +import static org.hamcrest.Matchers.not; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.Set; @@ -27,7 +29,7 @@ public class UnreferencedShapesTest { @BeforeAll public static void before() { model = Model.assembler() - .addImport(UnreferencedShapesTest.class.getResource("unreferenced-test.json")) + .addImport(UnreferencedShapesTest.class.getResource("unreferenced-test.smithy")) .assemble() .unwrap(); } @@ -54,6 +56,13 @@ public void doesNotCheckShapesThatAreTraitShapes() { ShapeId.from("ns.foo#Exclude2"))); } + @Test + public void doesNotCheckShapesThatAreMetadataDefinitions() { + UnreferencedShapes unref = new UnreferencedShapes(); + Set result = unref.compute(model).stream().map(Shape::getId).collect(Collectors.toSet()); + assertThat(result, not(hasItems(ShapeId.from("ns.foo#MetadataConfig"), ShapeId.from("ns.foo#AuditLevel")))); + } + @Test public void doesNotIgnorePrivateShapes() { ShapeId id = ShapeId.from("foo.baz#Bar"); diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/neighbor/UnreferencedTraitDefinitionsTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/neighbor/UnreferencedTraitDefinitionsTest.java index c82899a3f51..d513b7a9c9b 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/neighbor/UnreferencedTraitDefinitionsTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/neighbor/UnreferencedTraitDefinitionsTest.java @@ -15,7 +15,7 @@ public class UnreferencedTraitDefinitionsTest { @Test public void shouldReportDefinitionsForTraitsThatAreNotUsed() { Model model = Model.assembler() - .addImport(UnreferencedTraitDefinitionsTest.class.getResource("unreferenced-test.json")) + .addImport(UnreferencedTraitDefinitionsTest.class.getResource("unreferenced-test.smithy")) .assemble() .unwrap(); UnreferencedTraitDefinitions unreferencedTraitDefinitions = new UnreferencedTraitDefinitions(); diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/traits/MetadataTraitTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/traits/MetadataTraitTest.java new file mode 100644 index 00000000000..166ea66b612 --- /dev/null +++ b/smithy-model/src/test/java/software/amazon/smithy/model/traits/MetadataTraitTest.java @@ -0,0 +1,75 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package software.amazon.smithy.model.traits; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Optional; +import org.junit.jupiter.api.Test; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.shapes.StringShape; + +public class MetadataTraitTest { + @Test + public void loadsTrait() { + TraitFactory provider = TraitFactory.createServiceFactory(); + ObjectNode node = Node.objectNodeBuilder().withMember("key", "suppressions").build(); + Optional trait = provider.createTrait(ShapeId.from("smithy.api#metadata"), + ShapeId.from("ns.qux#foo"), + node); + + assertTrue(trait.isPresent()); + assertThat(trait.get(), instanceOf(MetadataTrait.class)); + MetadataTrait metadataTrait = (MetadataTrait) trait.get(); + + assertEquals("suppressions", metadataTrait.getKey()); + assertEquals(MetadataTrait.ID, metadataTrait.toShapeId()); + assertThat(metadataTrait.toNode(), equalTo(node)); + assertThat(metadataTrait.toBuilder().build(), equalTo(metadataTrait)); + } + + @Test + public void requiresKey() { + assertThrows(IllegalStateException.class, () -> MetadataTrait.builder().build()); + } + + @Test + public void equalityIsBasedOnKey() { + MetadataTrait a = MetadataTrait.builder().key("suppressions").build(); + MetadataTrait b = MetadataTrait.builder().key("suppressions").build(); + MetadataTrait c = MetadataTrait.builder().key("other").build(); + + assertEquals(a, b); + assertEquals(a.hashCode(), b.hashCode()); + assertNotEquals(a, c); + } + + @Test + public void loadsFromModel() { + // End-to-end: a .smithy model assembles, the trait is applied via SPI, and + // the declared type is reachable through the standard Shape API. + Model model = Model.assembler() + .addUnparsedModel("test.smithy", + "$version: \"2.0\"\n" + + "namespace smithy.example\n" + + "@metadata(key: \"auditLevel\")\n" + + "string AuditLevel\n") + .assemble() + .unwrap(); + + StringShape shape = model.expectShape(ShapeId.from("smithy.example#AuditLevel"), StringShape.class); + MetadataTrait trait = shape.expectTrait(MetadataTrait.class); + assertEquals("auditLevel", trait.getKey()); + } +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/duplicate-key.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/duplicate-key.errors new file mode 100644 index 00000000000..748d6719ca3 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/duplicate-key.errors @@ -0,0 +1,2 @@ +[ERROR] smithy.example#AuditLevelA: A type has already been defined for the metadata key `auditLevel`. Conflicts with: [smithy.example#AuditLevelB] | MetadataTrait +[ERROR] smithy.example#AuditLevelB: A type has already been defined for the metadata key `auditLevel`. Conflicts with: [smithy.example#AuditLevelA] | MetadataTrait diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/duplicate-key.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/duplicate-key.smithy new file mode 100644 index 00000000000..b070feabb14 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/duplicate-key.smithy @@ -0,0 +1,9 @@ +$version: "2.0" + +namespace smithy.example + +@metadata(key: "auditLevel") +string AuditLevelA + +@metadata(key: "auditLevel") +string AuditLevelB diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/invalid-value.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/invalid-value.errors new file mode 100644 index 00000000000..e003f4583a1 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/invalid-value.errors @@ -0,0 +1 @@ +[ERROR] -: metadata.auditLevel: String value provided for `smithy.example#AuditLevel` must be one of the following values: `HIGH`, `LOW`, `MEDIUM` | TypedMetadata.auditLevel diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/invalid-value.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/invalid-value.smithy new file mode 100644 index 00000000000..5a27823febb --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/invalid-value.smithy @@ -0,0 +1,12 @@ +$version: "2.0" + +metadata auditLevel = "unknown" + +namespace smithy.example + +@metadata(key: "auditLevel") +enum AuditLevel { + LOW + MEDIUM + HIGH +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/map-typed-metadata.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/map-typed-metadata.errors new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/map-typed-metadata.errors @@ -0,0 +1 @@ + diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/map-typed-metadata.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/map-typed-metadata.smithy new file mode 100644 index 00000000000..0e4cfb50ad3 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/map-typed-metadata.smithy @@ -0,0 +1,14 @@ +$version: "2.0" + +metadata tags = { + "env": "prod" + "team": "core" +} + +namespace smithy.example + +@metadata(key: "tags") +map Tags { + key: String + value: String +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/missing-required-member.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/missing-required-member.errors new file mode 100644 index 00000000000..d390e9e8f5e --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/missing-required-member.errors @@ -0,0 +1 @@ +[ERROR] -: metadata.auditLevel: Missing required structure member `level` for `smithy.example#AuditLevelMetadata` | TypedMetadata.auditLevel diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/missing-required-member.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/missing-required-member.smithy new file mode 100644 index 00000000000..4b6eac9115e --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/missing-required-member.smithy @@ -0,0 +1,19 @@ +$version: "2.0" + +metadata auditLevel = {} + +namespace smithy.example + +@metadata(key: "auditLevel") +structure AuditLevelMetadata { + @required + level: AuditLevel + + reason: String +} + +enum AuditLevel { + LOW + MEDIUM + HIGH +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/missing-required-nested-member.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/missing-required-nested-member.errors new file mode 100644 index 00000000000..f8891b324fa --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/missing-required-nested-member.errors @@ -0,0 +1 @@ +[ERROR] -: metadata.auditLevel.0: Missing required structure member `namespace` for `smithy.example#NamespaceAuditLevel` | TypedMetadata.auditLevel diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/missing-required-nested-member.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/missing-required-nested-member.smithy new file mode 100644 index 00000000000..4a8ca205804 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/missing-required-nested-member.smithy @@ -0,0 +1,28 @@ +$version: "2.0" + +metadata auditLevel = [ + { + level: "HIGH" + } +] + +namespace smithy.example + +@metadata(key: "auditLevel") +list AuditLevelMetadata { + member: NamespaceAuditLevel +} + +structure NamespaceAuditLevel { + @required + namespace: String + + @required + level: AuditLevel +} + +enum AuditLevel { + LOW + MEDIUM + HIGH +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/no-metadata-value.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/no-metadata-value.errors new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/no-metadata-value.errors @@ -0,0 +1 @@ + diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/no-metadata-value.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/no-metadata-value.smithy new file mode 100644 index 00000000000..1fc6f46fd83 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/no-metadata-value.smithy @@ -0,0 +1,10 @@ +$version: "2.0" + +namespace smithy.example + +@metadata(key: "auditLevel") +enum AuditLevel { + LOW + MEDIUM + HIGH +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/operation-io-metadata.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/operation-io-metadata.errors new file mode 100644 index 00000000000..1bbf90d9c4b --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/operation-io-metadata.errors @@ -0,0 +1,2 @@ +[ERROR] smithy.example#FooInput: Trait `metadata` cannot be applied to `smithy.example#FooInput`. This trait may only be applied to shapes that match the following selector: dataType :not([trait|input]) :not([trait|output]) | TraitTarget +[ERROR] smithy.example#FooOutput: Trait `metadata` cannot be applied to `smithy.example#FooOutput`. This trait may only be applied to shapes that match the following selector: dataType :not([trait|input]) :not([trait|output]) | TraitTarget diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/operation-io-metadata.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/operation-io-metadata.smithy new file mode 100644 index 00000000000..cdd3fd3871c --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/operation-io-metadata.smithy @@ -0,0 +1,10 @@ +$version: "2.0" + +namespace smithy.example + +operation Foo { + input := @metadata(key: "bar") { + } + output := @metadata(key: "baz") { + } +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/reserved-keys.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/reserved-keys.errors new file mode 100644 index 00000000000..406c5ab7ec3 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/reserved-keys.errors @@ -0,0 +1,3 @@ +[ERROR] smithy.example#Suppressions: The metadata key `suppressions` has been reserved by Smithy and may not be defined elsewhere. | MetadataTrait +[ERROR] smithy.example#Validators: The metadata key `validators` has been reserved by Smithy and may not be defined elsewhere. | MetadataTrait +[ERROR] smithy.example#SeverityOverrides: The metadata key `severityOverrides` has been reserved by Smithy and may not be defined elsewhere. | MetadataTrait diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/reserved-keys.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/reserved-keys.smithy new file mode 100644 index 00000000000..136408d3b62 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/reserved-keys.smithy @@ -0,0 +1,12 @@ +$version: "2.0" + +namespace smithy.example + +@metadata(key: "suppressions") +string Suppressions + +@metadata(key: "validators") +string Validators + +@metadata(key: "severityOverrides") +string SeverityOverrides diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/triple-duplicate-key.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/triple-duplicate-key.errors new file mode 100644 index 00000000000..312a2fb5f22 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/triple-duplicate-key.errors @@ -0,0 +1,3 @@ +[ERROR] smithy.example#AuditLevelA: A type has already been defined for the metadata key `auditLevel`. Conflicts with: [smithy.example#AuditLevelB, smithy.example#AuditLevelC] | MetadataTrait +[ERROR] smithy.example#AuditLevelB: A type has already been defined for the metadata key `auditLevel`. Conflicts with: [smithy.example#AuditLevelA, smithy.example#AuditLevelC] | MetadataTrait +[ERROR] smithy.example#AuditLevelC: A type has already been defined for the metadata key `auditLevel`. Conflicts with: [smithy.example#AuditLevelA, smithy.example#AuditLevelB] | MetadataTrait diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/triple-duplicate-key.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/triple-duplicate-key.smithy new file mode 100644 index 00000000000..5317d4cf652 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/triple-duplicate-key.smithy @@ -0,0 +1,14 @@ +$version: "2.0" + +metadata auditLevel = "anything" + +namespace smithy.example + +@metadata(key: "auditLevel") +string AuditLevelA + +@metadata(key: "auditLevel") +string AuditLevelB + +@metadata(key: "auditLevel") +string AuditLevelC diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/valid-typed-metadata.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/valid-typed-metadata.errors new file mode 100644 index 00000000000..e69de29bb2d diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/valid-typed-metadata.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/valid-typed-metadata.smithy new file mode 100644 index 00000000000..15d1e4c3bb1 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/metadata/valid-typed-metadata.smithy @@ -0,0 +1,12 @@ +$version: "2.0" + +metadata auditLevel = "HIGH" + +namespace smithy.example + +@metadata(key: "auditLevel") +enum AuditLevel { + LOW + MEDIUM + HIGH +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/neighbor/unreferenced-test.json b/smithy-model/src/test/resources/software/amazon/smithy/model/neighbor/unreferenced-test.json deleted file mode 100644 index 3226109ec47..00000000000 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/neighbor/unreferenced-test.json +++ /dev/null @@ -1,118 +0,0 @@ -{ - "smithy": "2.0", - "shapes": { - "ns.foo#bar": { - "type": "structure", - "members": { - "member": { - "target": "ns.foo#BarTraitShapeMember" - } - }, - "traits": { - "smithy.api#trait": {}, - "ns.foo#meta": {}, - "ns.foo#threeMeta": {} - } - }, - "ns.foo#quux": { - "type": "structure", - "members": { - "member": { - "target": "ns.foo#QuuxTraitShapeMember" - } - }, - "traits": { - "smithy.api#trait": {} - } - }, - "ns.foo#meta": { - "type": "structure", - "members": { - "member": { - "target": "smithy.api#String" - } - }, - "traits": { - "smithy.api#trait": {}, - "ns.foo#tooMeta": {}, - "ns.foo#threeMeta": {} - } - }, - "ns.foo#tooMeta": { - "type": "structure", - "members": { - "member": { - "target": "smithy.api#String" - } - }, - "traits": { - "smithy.api#trait": {} - } - }, - "ns.foo#threeMeta": { - "type": "structure", - "members": { - "member": { - "target": "smithy.api#String" - } - }, - "traits": { - "smithy.api#trait": {} - } - }, - "ns.foo#MyService": { - "type": "service", - "version": "2017-01-19", - "operations": [ - { - "target": "ns.foo#MyOperation" - } - ] - }, - "ns.foo#MyOperation": { - "type": "operation", - "input": { - "target": "ns.foo#MyOperationInput" - } - }, - "ns.foo#MyOperationInput": { - "type": "structure", - "members": { - "fizz": { - "target": "ns.foo#Include1" - }, - "buzz": { - "target": "ns.foo#Include2" - } - } - }, - "ns.foo#Exclude1": { - "type": "string", - "traits": { - "ns.foo#quux": { - "member": "pop" - } - } - }, - "ns.foo#Exclude2": { - "type": "string" - }, - "ns.foo#Include1": { - "type": "string", - "traits": { - "ns.foo#bar": { - "member": "baz" - } - } - }, - "ns.foo#Include2": { - "type": "string" - }, - "ns.foo#BarTraitShapeMember": { - "type": "string" - }, - "ns.foo#QuuxTraitShapeMember": { - "type": "string" - } - } -} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/neighbor/unreferenced-test.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/neighbor/unreferenced-test.smithy new file mode 100644 index 00000000000..e867034a3b8 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/neighbor/unreferenced-test.smithy @@ -0,0 +1,78 @@ +$version: "2.0" + +namespace ns.foo + +@meta +@threeMeta +@trait +structure bar { + member: BarTraitShapeMember +} + +@threeMeta +@tooMeta +@trait +structure meta { + member: String +} + +@trait +structure quux { + member: QuuxTraitShapeMember +} + +@trait +structure threeMeta { + member: String +} + +@trait +structure tooMeta { + member: String +} + +@metadata(key: "config") +structure MetadataConfig { + auditLevel: AuditLevel +} + +enum AuditLevel { + LOW + MEDIUM + HIGH +} + +service MyService { + version: "2017-01-19" + operations: [ + MyOperation + ] +} + +operation MyOperation { + input: MyOperationInput + output: Unit +} + +structure MyOperationInput { + fizz: Include1 + buzz: Include2 +} + +string BarTraitShapeMember + +@quux( + member: "pop" +) +string Exclude1 + +string Exclude2 + +@bar( + member: "baz" +) +string Include1 + +string Include2 + +string QuuxTraitShapeMember