From f127adb12b3c5ae2f0416a820251ceeb67b09b09 Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Wed, 27 Jul 2022 00:40:36 -0700 Subject: [PATCH 01/20] Simplify model loading Model loader is now done through a central Consumer that receives "operations" from model files. These operations are things like "addShape", "applyTrait", "addForwardReference", etc. The consumer processes these events and then coordinates the traits loaded in the model, shapes added, and how they are all assembled together. This approach is able to maintain shapes in-tact when they are added to a ModelAssembler up until the point in which the shape might be modified by an applyTrait statement or if another trait is added that conflicts with it. This should improve performance and reduce memory usage when building projections with SmithyBuild since it will cut down on have to deconstruct and the reconstruct previously built shapes. Other things to note: * Ensure a version is emitted from the IDL even if one if not set so that the assembler can know to upgrade 1.0 shapes. * Add an implicit @box trait to members that were nullable in 1.0 so that tooling can know if the member was considered nullable in 1.0 semantics (and this check ignores the @required trait to meet 1.0 expectations). * Now that there are more synthetic traits, the Upgrade12Command needs to not try to rewrite and erase deprecated synthetic traits since they don't actually appear in the model. No new test case was needed. * Ensure that the @default trait is not allowed in 1.0 models. * Ignore invalid doc comments within shapes * Ignore 1.0 deprecation warnings in test runners The Smithy test runner no longer requires 1.0 deprecations or trait deprecations to be listed explicitly. This allows existing model test suites to contine to run without needing to be updated. It also allows trait vendors to deprecated a trait without breaking consumers. To accommodate this, I modified all warnings about 1.0 models to use a "ModelDeprecation" event ID, including set deprecation. --- ...o-not-support-list-set-map-payloads.errors | 2 - .../amazon/smithy/build/SmithyBuildImpl.java | 2 +- .../test-bad-trait-serializer-import.smithy | 2 +- .../cli/commands/Upgrade1to2Command.java | 6 +- .../upgrade/cases/remove-primitive.v1.smithy | 3 - .../upgrade/cases/remove-primitive.v2.smithy | 3 - .../software/amazon/smithy/model/Model.java | 11 + .../loader/AbstractMutableModelFile.java | 283 ------------ .../smithy/model/loader/ApplyMixin.java | 82 ++-- .../loader/ApplyResourceBasedTargets.java | 103 +++++ .../smithy/model/loader/AstModelLoader.java | 314 ++++++------- .../model/loader/CompositeModelFile.java | 195 -------- .../loader/ForwardReferenceModelFile.java | 164 ------- .../model/loader/FullyResolvedModelFile.java | 89 ---- .../smithy/model/loader/IdlModelParser.java | 333 +++++++++----- .../smithy/model/loader/IdlNodeParser.java | 6 +- .../loader/ImmutablePreludeModelFile.java | 104 ----- .../smithy/model/loader/LoadOperation.java | 252 +++++++++++ .../model/loader/LoadOperationProcessor.java | 179 ++++++++ .../smithy/model/loader/LoaderShapeMap.java | 420 ++++++++++++++++++ .../smithy/model/loader/LoaderTraitMap.java | 227 ++++++++++ .../smithy/model/loader/LoaderUtils.java | 19 +- .../model/loader/MetadataContainer.java | 10 +- .../smithy/model/loader/ModelAssembler.java | 223 +++------- .../amazon/smithy/model/loader/ModelFile.java | 143 ------ .../smithy/model/loader/ModelLoader.java | 45 +- .../smithy/model/loader/ModelUpgrader.java | 50 ++- .../smithy/model/loader/ModelValidator.java | 18 +- .../smithy/model/loader/PendingShape.java | 276 ------------ .../model/loader/SetResourceBasedTargets.java | 78 ---- ...gShapeModifier.java => ShapeModifier.java} | 35 +- .../model/loader/TopologicalShapeSort.java | 60 +-- .../smithy/model/loader/TraitContainer.java | 316 ------------- .../amazon/smithy/model/loader/Version.java | 42 +- .../model/shapes/AbstractShapeBuilder.java | 21 +- .../smithy/model/shapes/CollectionShape.java | 6 + .../amazon/smithy/model/shapes/EnumShape.java | 6 +- .../smithy/model/shapes/IntEnumShape.java | 6 +- .../amazon/smithy/model/shapes/MapShape.java | 7 + .../shapes/NamedMembersShapeBuilder.java | 6 +- .../amazon/smithy/model/shapes/SetShape.java | 5 + .../amazon/smithy/model/shapes/Shape.java | 14 +- .../amazon/smithy/model/shapes/ShapeId.java | 17 +- .../model/validation/ValidationUtils.java | 2 +- .../smithy/model/validation/Validator.java | 3 + .../validation/testrunner/SmithyTestCase.java | 14 +- .../testrunner/SmithyTestSuite.java | 6 +- .../validators/ServiceValidator.java | 2 +- .../validation/validators/SetValidator.java | 11 +- .../amazon/smithy/model/loader/prelude.smithy | 2 +- .../model/loader/IdlModelLoaderTest.java | 13 + .../model/loader/IdlTextParserTest.java | 3 +- .../model/loader/ModelAssemblerTest.java | 112 ++++- .../model/loader/ModelUpgraderTest.java | 29 +- .../smithy/model/loader/PendingShapeTest.java | 108 ----- .../ValidSmithyModelLoaderRunnerTest.java | 60 +++ .../testrunner/SmithyTestSuiteTest.java | 2 +- .../loader/default-trait-in-1.0.errors | 1 + .../loader/default-trait-in-1.0.smithy | 7 + .../default-trait-in-implicit-1.0.errors | 1 + .../default-trait-in-implicit-1.0.smithy | 6 + .../detects-default-out-of-range.errors | 22 +- .../detects-duplicates-with-members.errors | 2 +- .../metadata-syntactic-shape-ids.errors | 1 + .../metadata-syntactic-shape-ids.smithy | 5 + .../mixins-detects-invalid-mixin.errors | 2 +- .../loader/sets-are-considered-lists.errors | 4 +- .../use-statement-undefined-shape.errors | 1 + .../use-statement-undefined-shape.smithy | 4 + .../validators/sets/valid-sets.errors | 16 - .../forwardrefs/resource/operation.smithy | 5 + .../forwardrefs/resource/resource.smithy | 7 + .../loader/forwardrefs/resource/result.json | 22 + .../loader/forwardrefs/use/no-use.smithy | 26 ++ .../model/loader/forwardrefs/use/result.json | 39 ++ .../model/loader/forwardrefs/use/use.smithy | 28 ++ .../{ => version}/unsupported-version.smithy | 0 .../version/version-set-more-than-once.smithy | 3 + .../does-not-introduce-conflict/main.smithy | 1 - .../smithy/model/loader/valid/__shared.json | 7 +- .../doc-comments-ignored-after-shape.json | 28 ++ .../doc-comments-ignored-after-shape.smithy | 18 + .../{ => doc-comments}/doc-comments.json | 0 .../{ => doc-comments}/doc-comments.smithy | 0 .../smithy/model/loader/valid/metadata.smithy | 2 - .../model/loader/valid/optional-commas.json | 2 +- .../model/loader/valid/optional-commas.smithy | 1 + .../model/loader/valid/use/use-shapes.json | 8 +- .../model/loader/valid/use/use-shapes.smithy | 7 +- .../valid/can-ignore-1.0-warnings.errors | 0 .../valid/can-ignore-1.0-warnings.smithy | 6 + 91 files changed, 2388 insertions(+), 2444 deletions(-) delete mode 100644 smithy-model/src/main/java/software/amazon/smithy/model/loader/AbstractMutableModelFile.java create mode 100644 smithy-model/src/main/java/software/amazon/smithy/model/loader/ApplyResourceBasedTargets.java delete mode 100644 smithy-model/src/main/java/software/amazon/smithy/model/loader/CompositeModelFile.java delete mode 100644 smithy-model/src/main/java/software/amazon/smithy/model/loader/ForwardReferenceModelFile.java delete mode 100644 smithy-model/src/main/java/software/amazon/smithy/model/loader/FullyResolvedModelFile.java delete mode 100644 smithy-model/src/main/java/software/amazon/smithy/model/loader/ImmutablePreludeModelFile.java create mode 100644 smithy-model/src/main/java/software/amazon/smithy/model/loader/LoadOperation.java create mode 100644 smithy-model/src/main/java/software/amazon/smithy/model/loader/LoadOperationProcessor.java create mode 100644 smithy-model/src/main/java/software/amazon/smithy/model/loader/LoaderShapeMap.java create mode 100644 smithy-model/src/main/java/software/amazon/smithy/model/loader/LoaderTraitMap.java delete mode 100644 smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelFile.java delete mode 100644 smithy-model/src/main/java/software/amazon/smithy/model/loader/PendingShape.java delete mode 100644 smithy-model/src/main/java/software/amazon/smithy/model/loader/SetResourceBasedTargets.java rename smithy-model/src/main/java/software/amazon/smithy/model/loader/{PendingShapeModifier.java => ShapeModifier.java} (69%) delete mode 100644 smithy-model/src/main/java/software/amazon/smithy/model/loader/TraitContainer.java delete mode 100644 smithy-model/src/test/java/software/amazon/smithy/model/loader/PendingShapeTest.java create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/default-trait-in-1.0.errors create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/default-trait-in-1.0.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/default-trait-in-implicit-1.0.errors create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/default-trait-in-implicit-1.0.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/metadata-syntactic-shape-ids.errors create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/metadata-syntactic-shape-ids.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/use-statement-undefined-shape.errors create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/use-statement-undefined-shape.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/forwardrefs/resource/operation.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/forwardrefs/resource/resource.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/forwardrefs/resource/result.json create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/forwardrefs/use/no-use.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/forwardrefs/use/result.json create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/forwardrefs/use/use.smithy rename smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/{ => version}/unsupported-version.smithy (100%) create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/version/version-set-more-than-once.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/doc-comments/doc-comments-ignored-after-shape.json create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/doc-comments/doc-comments-ignored-after-shape.smithy rename smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/{ => doc-comments}/doc-comments.json (100%) rename smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/{ => doc-comments}/doc-comments.smithy (100%) create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/validation/testrunner/testrunner/valid/can-ignore-1.0-warnings.errors create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/validation/testrunner/testrunner/valid/can-ignore-1.0-warnings.smithy diff --git a/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/protocols/aws-protocols-do-not-support-list-set-map-payloads.errors b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/protocols/aws-protocols-do-not-support-list-set-map-payloads.errors index 1c00b318945..7342a67c196 100644 --- a/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/protocols/aws-protocols-do-not-support-list-set-map-payloads.errors +++ b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/protocols/aws-protocols-do-not-support-list-set-map-payloads.errors @@ -1,5 +1,3 @@ [ERROR] smithy.example#InvalidBindingOperationInput$listBinding: AWS Protocols only support binding the following shape types to the payload: string, blob, structure, union, and document | ProtocolHttpPayload [ERROR] smithy.example#InvalidBindingOperationOutput$mapBinding: AWS Protocols only support binding the following shape types to the payload: string, blob, structure, union, and document | ProtocolHttpPayload [ERROR] smithy.example#InvalidBindingError$setBinding: AWS Protocols only support binding the following shape types to the payload: string, blob, structure, union, and document | ProtocolHttpPayload -[WARNING] -: Smithy IDL 1.0 is deprecated. Please upgrade to Smithy IDL 2.0. | Model -[WARNING] smithy.example#StringSet: Set shapes are deprecated and have been removed in Smithy IDL v2. Use a list shape with the @uniqueItems trait instead. | Set diff --git a/smithy-build/src/main/java/software/amazon/smithy/build/SmithyBuildImpl.java b/smithy-build/src/main/java/software/amazon/smithy/build/SmithyBuildImpl.java index f99da43c3d6..a5cc068753b 100644 --- a/smithy-build/src/main/java/software/amazon/smithy/build/SmithyBuildImpl.java +++ b/smithy-build/src/main/java/software/amazon/smithy/build/SmithyBuildImpl.java @@ -284,7 +284,7 @@ private ProjectionResult applyProjection( baseModel = assembler.assemble(); // Fail if the model can't be merged with the imports. - if (!baseModel.getResult().isPresent()) { + if (baseModel.isBroken() || !baseModel.getResult().isPresent()) { LOGGER.severe(String.format( "The model could not be merged with the following imports: [%s]", projection.getImports())); diff --git a/smithy-build/src/test/resources/software/amazon/smithy/build/test-bad-trait-serializer-import.smithy b/smithy-build/src/test/resources/software/amazon/smithy/build/test-bad-trait-serializer-import.smithy index 1515d14dc6e..8de939c4ca7 100644 --- a/smithy-build/src/test/resources/software/amazon/smithy/build/test-bad-trait-serializer-import.smithy +++ b/smithy-build/src/test/resources/software/amazon/smithy/build/test-bad-trait-serializer-import.smithy @@ -1,4 +1,4 @@ -$version: "1.0" +$version: "2.0" namespace smithy.test diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/Upgrade1to2Command.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/Upgrade1to2Command.java index d8a5a2d436e..e47d0e3f887 100644 --- a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/Upgrade1to2Command.java +++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/Upgrade1to2Command.java @@ -478,8 +478,10 @@ private void eraseLine(int lineNumber) { } private void eraseTrait(Shape shape, Trait trait) { - SourceLocation to = findLocationAfterTrait(shape, trait.getClass()); - erase(trait.getSourceLocation(), to); + if (trait.getSourceLocation() != SourceLocation.NONE) { + SourceLocation to = findLocationAfterTrait(shape, trait.getClass()); + erase(trait.getSourceLocation(), to); + } } private SourceLocation findLocationAfterTrait(Shape shape, Class target) { diff --git a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/remove-primitive.v1.smithy b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/remove-primitive.v1.smithy index b74b80278b8..88fa65e7363 100644 --- a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/remove-primitive.v1.smithy +++ b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/remove-primitive.v1.smithy @@ -14,9 +14,6 @@ structure PrimitiveBearer { handlesComments: // Nobody actually does this right? PrimitiveShort, - @default(0) - handlesPreexistingDefault: PrimitiveShort, - @required handlesRequired: PrimitiveLong, diff --git a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/remove-primitive.v2.smithy b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/remove-primitive.v2.smithy index 9c4e41c1c10..317493a4422 100644 --- a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/remove-primitive.v2.smithy +++ b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/remove-primitive.v2.smithy @@ -22,9 +22,6 @@ structure PrimitiveBearer { handlesComments: // Nobody actually does this right? Short, - @default(0) - handlesPreexistingDefault: Short, - @required handlesRequired: Long, diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/Model.java b/smithy-model/src/main/java/software/amazon/smithy/model/Model.java index cd599e3e3c0..112b33fa8ff 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/Model.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/Model.java @@ -999,6 +999,17 @@ public Builder removeShape(ShapeId shapeId) { return this; } + /** + * Gets an immutable view of the current shapes in the builder. + * + *

The returned view may not be updated as shapes are added to the builder. + * + * @return Returns the current shapes in the builder. + */ + public Map getCurrentShapes() { + return shapeMap.peek(); + } + @Override public Model build() { return new Model(this); diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/AbstractMutableModelFile.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/AbstractMutableModelFile.java deleted file mode 100644 index 04b55d4e04c..00000000000 --- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/AbstractMutableModelFile.java +++ /dev/null @@ -1,283 +0,0 @@ -/* - * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.smithy.model.loader; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import software.amazon.smithy.model.SourceException; -import software.amazon.smithy.model.node.Node; -import software.amazon.smithy.model.shapes.AbstractShapeBuilder; -import software.amazon.smithy.model.shapes.MemberShape; -import software.amazon.smithy.model.shapes.Shape; -import software.amazon.smithy.model.shapes.ShapeId; -import software.amazon.smithy.model.shapes.ShapeType; -import software.amazon.smithy.model.traits.Trait; -import software.amazon.smithy.model.traits.TraitFactory; -import software.amazon.smithy.model.validation.Severity; -import software.amazon.smithy.model.validation.ValidationEvent; -import software.amazon.smithy.model.validation.Validator; - -/** - * Base class used for mutable model files. - */ -abstract class AbstractMutableModelFile implements ModelFile { - - protected TraitContainer.VersionAwareTraitContainer traitContainer; - - private final String filename; - private final Set allShapeIds = new HashSet<>(); - private final Map> shapes = new LinkedHashMap<>(); - private final Map> members = new HashMap<>(); - private final Map> pendingModifications = new HashMap<>(); - private final Map> pendingDependencies = new HashMap<>(); - private final List events = new ArrayList<>(); - private final MetadataContainer metadata = new MetadataContainer(events); - private final TraitFactory traitFactory; - - /** - * @param traitFactory Factory used to create traits when merging traits. - */ - AbstractMutableModelFile(String filename, TraitFactory traitFactory) { - this.filename = filename; - this.traitFactory = Objects.requireNonNull(traitFactory, "traitFactory must not be null"); - TraitContainer traitStore = new TraitContainer.TraitHashMap(traitFactory, events); - traitContainer = new TraitContainer.VersionAwareTraitContainer(traitStore); - } - - @Override - public String getFilename() { - return filename; - } - - @Override - public final Version getVersion() { - return traitContainer.getVersion(); - } - - /** - * Adds a shape to the ModelFile, checking for conflicts with other shapes. - * - * @param builder Shape builder to register. - */ - void onShape(AbstractShapeBuilder builder) { - allShapeIds.add(builder.getId()); - - if (!getVersion().isShapeTypeSupported(builder.getShapeType())) { - throw new SourceException(String.format( - "%s shapes cannot be used in Smithy version " + getVersion(), builder.getShapeType().toString()), - builder.getSourceLocation()); - } - - if (builder instanceof MemberShape.Builder) { - String memberName = builder.getId().getMember().get(); - ShapeId containerId = builder.getId().withoutMember(); - if (!members.containsKey(containerId)) { - members.put(containerId, new LinkedHashMap<>()); - } else if (members.get(containerId).containsKey(memberName)) { - throw onConflict(builder, members.get(containerId).get(memberName)); - } - members.get(containerId).put(memberName, (MemberShape.Builder) builder); - } else if (shapes.containsKey(builder.getId())) { - throw onConflict(builder, shapes.get(builder.getId())); - } else { - shapes.put(builder.getId(), builder); - } - } - - void addPendingMixin(ShapeId shape, ShapeId mixin) { - addPendingModification(shape, new ApplyMixin(mixin, events)); - } - - void addPendingModification(ShapeId shape, PendingShapeModifier pendingModification) { - pendingDependencies.computeIfAbsent(shape, id -> new LinkedHashSet<>()) - .addAll(pendingModification.getDependencies()); - pendingModifications.computeIfAbsent(shape, id -> new ArrayList<>()).add(pendingModification); - } - - private SourceException onConflict(AbstractShapeBuilder builder, AbstractShapeBuilder previous) { - // Duplicate shapes in the same model file are not allowed. - ValidationEvent event = LoaderUtils.onShapeConflict(builder.getId(), builder.getSourceLocation(), - previous.getSourceLocation()); - return new SourceException(event.getMessage(), event.getSourceLocation()); - } - - /** - * Adds metadata to be reported by the ModelFile. - * - * @param key Metadata key to set. - * @param value Metadata value to set. - */ - final void putMetadata(String key, Node value) { - metadata.putMetadata(key, value); - } - - /** - * Invoked when a trait is to be reported by the ModelFile. - * - * @param target The shape the trait is applied to. - * @param trait The trait shape ID. - * @param value The node value of the trait. - */ - final void onTrait(ShapeId target, ShapeId trait, Node value) { - traitContainer.onTrait(target, trait, value); - } - - /** - * Invoked when a trait is to be reported by the ModelFile. - * - * @param target The shape the trait is applied to. - * @param trait The trait to apply to the shape. - */ - final void onTrait(ShapeId target, Trait trait) { - traitContainer.onTrait(target, trait); - } - - /** - * Sets the version of the model file being loaded. - * - * @param version Version to set. - */ - final void setVersion(Version version) { - traitContainer.setVersion(version); - } - - @Override - public final List events() { - return events; - } - - @Override - public final Map metadata() { - return metadata.getData(); - } - - @Override - public final Set shapeIds() { - return allShapeIds; - } - - @Override - public final ShapeType getShapeType(ShapeId id) { - return shapes.containsKey(id) ? shapes.get(id).getShapeType() : null; - } - - @Override - public final CreatedShapes createShapes(TraitContainer resolvedTraits) { - List resolvedShapes = new ArrayList<>(shapes.size()); - List pendingMixins = new ArrayList<>(); - - for (Map.Entry> entry : this.pendingModifications.entrySet()) { - ShapeId subject = entry.getKey(); - List pendingModifications = entry.getValue(); - Set dependencies = pendingDependencies.getOrDefault(subject, Collections.emptySet()); - AbstractShapeBuilder builder = shapes.get(entry.getKey()); - Map builderMembers = claimMembersOfContainer(builder.getId()); - shapes.remove(entry.getKey()); - pendingMixins.add(createPendingShape( - subject, builder, builderMembers, dependencies, pendingModifications, traitContainer)); - } - - // Build members and add them to top-level shapes. - for (Map memberBuilders : members.values()) { - for (MemberShape.Builder builder : memberBuilders.values()) { - ShapeId id = builder.getId(); - AbstractShapeBuilder container = shapes.get(id.withoutMember()); - if (container == null) { - throw new RuntimeException("Container shape not found for member: " + id); - } - for (Trait trait : resolvedTraits.getTraitsForShape(id).values()) { - builder.addTrait(trait); - } - container.addMember(builder.build()); - } - } - - // Build top-level shapes that don't use mixins. - for (AbstractShapeBuilder builder : shapes.values()) { - buildShape(builder, resolvedTraits).ifPresent(resolvedShapes::add); - } - - return new CreatedShapes(resolvedShapes, pendingMixins); - } - - private Map claimMembersOfContainer(ShapeId id) { - Map result = members.remove(id); - return result == null ? Collections.emptyMap() : result; - } - - private PendingShape createPendingShape( - ShapeId subject, - AbstractShapeBuilder builder, - Map builderMembers, - Set mixins, - List pendingModifications, - TraitContainer resolvedTraits - ) { - return PendingShape.create(subject, builder, mixins, shapeMap -> { - for (MemberShape.Builder memberBuilder : builderMembers.values()) { - for (PendingShapeModifier pendingModification : pendingModifications) { - pendingModification.modifyMember(builder, memberBuilder, resolvedTraits, shapeMap); - } - buildShape(memberBuilder, resolvedTraits).ifPresent(builder::addMember); - } - - for (PendingShapeModifier pendingModification : pendingModifications) { - pendingModification.modifyShape(builder, builderMembers, resolvedTraits, shapeMap); - } - - buildShape(builder, resolvedTraits).ifPresent(result -> shapeMap.put(result.getId(), result)); - }); - } - - private > Optional buildShape( - B builder, - TraitContainer resolvedTraits - ) { - try { - for (Trait trait : resolvedTraits.getTraitsForShape(builder.getId()).values()) { - builder.addTrait(trait); - } - return Optional.of(builder.build()); - } catch (IllegalStateException e) { - if (builder.getShapeType() == ShapeType.MEMBER && ((MemberShape.Builder) builder).getTarget() == null) { - events.add(ValidationEvent.builder() - .severity(Severity.ERROR) - .id(Validator.MODEL_ERROR) - .shapeId(builder.getId()) - .sourceLocation(builder.getSourceLocation()) - .message("Member target was elided, but no bound resource or mixin contained a matching " - + "identifier or member name.") - .build()); - return Optional.empty(); - } - throw e; - } catch (SourceException e) { - events.add(ValidationEvent.fromSourceException(e, "", builder.getId())); - resolvedTraits.clearTraitsForShape(builder.getId()); - return Optional.empty(); - } - } - -} diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/ApplyMixin.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/ApplyMixin.java index ff093a24be7..2d17c698847 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/ApplyMixin.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/ApplyMixin.java @@ -15,10 +15,12 @@ package software.amazon.smithy.model.loader; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.Set; +import java.util.function.Function; +import software.amazon.smithy.model.SourceException; import software.amazon.smithy.model.shapes.AbstractShapeBuilder; import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.Shape; @@ -31,69 +33,62 @@ /** * Applies mixins to shapes after the mixins have been resolved. */ -final class ApplyMixin implements PendingShapeModifier { +final class ApplyMixin implements ShapeModifier { private final ShapeId mixin; - private final List events; + private List events; - ApplyMixin(ShapeId mixin, List events) { + ApplyMixin(ShapeId mixin) { this.mixin = mixin; - this.events = events; - } - - @Override - public Set getDependencies() { - return Collections.singleton(mixin); } @Override public void modifyMember( AbstractShapeBuilder shapeBuilder, MemberShape.Builder memberBuilder, - TraitContainer resolvedTraits, - Map shapeMap + Function> unclaimedTraits, + Function shapeMap ) { - // Fast-fail the common case. + // The target could have been set by resource based properties. if (memberBuilder.getTarget() != null) { return; } // Members inherited from mixins can have their targets elided, so here we set them // to the target defined in the mixin. - Shape mixinShape = shapeMap.get(mixin); - String name = memberBuilder.getId().getMember().get(); - if (mixinShape.getMember(name).isPresent()) { - memberBuilder.target(mixinShape.getMember(name).get().getTarget()); + Shape mixinShape = shapeMap.apply(mixin); + if (mixinShape == null) { + throw new SourceException("Cannot apply mixin to " + memberBuilder.getId() + ": " + mixin + " not found", + memberBuilder); } + + String name = memberBuilder.getId().getMember().get(); + mixinShape.getMember(name).ifPresent(mixinMember -> memberBuilder.target(mixinMember.getTarget())); } @Override public void modifyShape( AbstractShapeBuilder builder, Map memberBuilders, - TraitContainer resolvedTraits, - Map shapeMap + Function> unclaimedTraits, + Function shapeMap ) { - Shape mixinShape = shapeMap.get(mixin); + Shape mixinShape = shapeMap.apply(mixin); + if (mixinShape == null) { + throw new SourceException("Cannot apply mixin to " + builder.getId() + ": " + mixin + " not found", + builder); + } + for (MemberShape member : mixinShape.members()) { ShapeId targetId = builder.getId().withMember(member.getMemberName()); - Map introducedTraits = resolvedTraits.getTraitsForShape(targetId); + // Claim traits from the trait map that were applied to synthesized shapes. + Map introducedTraits = unclaimedTraits.apply(targetId); MemberShape introducedMember = null; if (memberBuilders.containsKey(member.getMemberName())) { - introducedMember = memberBuilders.get(member.getMemberName()) - .addMixin(member) - .build(); - + MemberShape.Builder original = memberBuilders.get(member.getMemberName()); + introducedMember = original.addMixin(member).build(); if (!introducedMember.getTarget().equals(member.getTarget())) { - // Members cannot be redefined if their targets conflict. - MemberShape.Builder conflict = memberBuilders.get(member.getMemberName()); - events.add(ValidationEvent.builder() - .severity(Severity.ERROR) - .id(Validator.MODEL_ERROR) - .shapeId(conflict.getId()) - .sourceLocation(conflict.getSourceLocation()) - .message("Member conflicts with an inherited mixin member: " + member.getId()) - .build()); + mixinMemberConflict(original, member); } } else if (!introducedTraits.isEmpty()) { // Build local member copies before adding mixins if traits @@ -111,6 +106,25 @@ public void modifyShape( builder.addMember(introducedMember); } } + builder.addMixin(mixinShape); } + + private void mixinMemberConflict(MemberShape.Builder conflict, MemberShape other) { + if (events == null) { + events = new ArrayList<>(); + } + events.add(ValidationEvent.builder() + .severity(Severity.ERROR) + .id(Validator.MODEL_ERROR) + .shapeId(conflict.getId()) + .sourceLocation(conflict.getSourceLocation()) + .message("Member conflicts with an inherited mixin member: " + other.getId()) + .build()); + } + + @Override + public List getEvents() { + return events == null ? Collections.emptyList() : events; + } } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/ApplyResourceBasedTargets.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/ApplyResourceBasedTargets.java new file mode 100644 index 00000000000..415cd304bd3 --- /dev/null +++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/ApplyResourceBasedTargets.java @@ -0,0 +1,103 @@ +/* + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.model.loader; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import software.amazon.smithy.model.SourceException; +import software.amazon.smithy.model.shapes.AbstractShapeBuilder; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.ResourceShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.Trait; +import software.amazon.smithy.model.validation.Severity; +import software.amazon.smithy.model.validation.ValidationEvent; +import software.amazon.smithy.model.validation.Validator; + +/** + * Sets member targets based on referenced resource identifiers. + * + *

Structures can elide the targets of members if they're bound to a resource + * and that resource has an identifier with a matching name. Here we set the + * target based on that information. + */ +final class ApplyResourceBasedTargets implements ShapeModifier { + private final ShapeId resourceId; + private List events; + + ApplyResourceBasedTargets(ShapeId resourceId) { + this.resourceId = resourceId; + } + + @Override + public void modifyMember( + AbstractShapeBuilder shapeBuilder, + MemberShape.Builder memberBuilder, + Function> unclaimedTraits, + Function shapeMap + ) { + // Fast-fail the common case of the target having already been set. + if (memberBuilder.getTarget() != null) { + return; + } + + Shape fromShape = shapeMap.apply(resourceId); + if (fromShape == null) { + throw new SourceException("Cannot apply resource to elided member " + memberBuilder.getId() + ": " + + resourceId + " not found", memberBuilder); + } + + if (!fromShape.isResourceShape()) { + fromShapeIsNotResource(memberBuilder, fromShape); + } else { + ResourceShape resource = fromShape.asResourceShape().get(); + String name = memberBuilder.getId().getMember().get(); + if (resource.getIdentifiers().containsKey(name)) { + memberBuilder.target(resource.getIdentifiers().get(name)); + } + if (resource.getProperties().containsKey(name)) { + memberBuilder.target(resource.getProperties().get(name)); + } + } + } + + private void fromShapeIsNotResource(MemberShape.Builder memberBuilder, Shape fromShape) { + String message = String.format( + "The target of the `for` production must be a resource shape, but found a %s shape: %s", + fromShape.getType(), + resourceId); + ValidationEvent event = ValidationEvent.builder() + .id(Validator.MODEL_ERROR) + .severity(Severity.ERROR) + .shapeId(memberBuilder.getId()) + .sourceLocation(memberBuilder.getSourceLocation()) + .message(message) + .build(); + if (events == null) { + events = new ArrayList<>(1); + } + events.add(event); + } + + @Override + public List getEvents() { + return events == null ? Collections.emptyList() : events; + } +} diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/AstModelLoader.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/AstModelLoader.java index 97f48392fa1..ec0be663496 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/AstModelLoader.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/AstModelLoader.java @@ -21,6 +21,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.function.Consumer; import software.amazon.smithy.model.SourceException; import software.amazon.smithy.model.node.ArrayNode; import software.amazon.smithy.model.node.Node; @@ -53,7 +54,6 @@ import software.amazon.smithy.model.shapes.StructureShape; import software.amazon.smithy.model.shapes.TimestampShape; import software.amazon.smithy.model.shapes.UnionShape; -import software.amazon.smithy.model.traits.TraitFactory; import software.amazon.smithy.model.validation.Severity; import software.amazon.smithy.model.validation.ValidationEvent; import software.amazon.smithy.model.validation.Validator; @@ -63,9 +63,7 @@ /** * A singleton that loads Smithy models from the JSON AST versions 1.0 and 2.0. */ -enum AstModelLoader { - - INSTANCE; +final class AstModelLoader { private static final String METADATA = "metadata"; private static final String MEMBERS = "members"; @@ -92,243 +90,247 @@ enum AstModelLoader { private static final Set SERVICE_PROPERTIES = SetUtils.of( TYPE, "version", "operations", "resources", "rename", ERRORS, TRAITS); - ModelFile load(Version modelVersion, TraitFactory traitFactory, ObjectNode model) { - FullyResolvedModelFile modelFile = new FullyResolvedModelFile(model.getSourceLocation().getFilename(), - traitFactory); - modelFile.setVersion(modelVersion); - LoaderUtils.checkForAdditionalProperties(model, null, TOP_LEVEL_PROPERTIES, modelFile.events()); - loadMetadata(model, modelFile); - loadShapes(model, modelFile); - return modelFile; + private final Version modelVersion; + private final ObjectNode model; + private Consumer operations; + + AstModelLoader(Version modelVersion, ObjectNode model) { + this.modelVersion = modelVersion; + this.model = model; + } + + void parse(Consumer consumer) { + operations = consumer; + LoaderUtils.checkForAdditionalProperties(model, null, TOP_LEVEL_PROPERTIES).ifPresent(this::emit); + StringNode versionNode = model.expectStringMember("smithy"); + consumer.accept(new LoadOperation.ModelVersion(modelVersion, versionNode.getSourceLocation())); + loadMetadata(); + loadShapes(); } - private void loadMetadata(ObjectNode model, FullyResolvedModelFile modelFile) { + private void emit(ValidationEvent event) { + operations.accept(new LoadOperation.Event(event)); + } + + private void loadMetadata() { try { model.getObjectMember(METADATA).ifPresent(metadata -> { for (Map.Entry entry : metadata.getStringMap().entrySet()) { - modelFile.putMetadata(entry.getKey(), entry.getValue()); + operations.accept(new LoadOperation.PutMetadata(modelVersion, entry.getKey(), entry.getValue())); } }); } catch (SourceException e) { - modelFile.events().add(ValidationEvent.fromSourceException(e)); + emit(ValidationEvent.fromSourceException(e)); } } - private void loadShapes(ObjectNode model, FullyResolvedModelFile modelFile) { + private void loadShapes() { model.getObjectMember(SHAPES).ifPresent(shapes -> { for (Map.Entry entry : shapes.getMembers().entrySet()) { ShapeId id = entry.getKey().expectShapeId(); ObjectNode definition = entry.getValue().expectObjectNode(); String type = definition.expectStringMember(TYPE).getValue(); try { - loadShape(id, type, definition, modelFile); + // Note: loadShape() returns null when using apply for traits. + LoadOperation.DefineShape defineShape = loadShape(id, type, definition); + if (defineShape != null) { + operations.accept(defineShape); + } } catch (SourceException e) { ValidationEvent event = ValidationEvent.fromSourceException(e).toBuilder().shapeId(id).build(); - modelFile.events().add(event); + emit(event); } } }); } - private void loadShape(ShapeId id, String type, ObjectNode value, FullyResolvedModelFile modelFile) { + private LoadOperation.DefineShape loadShape(ShapeId id, String type, ObjectNode value) { switch (type) { case "blob": - loadSimpleShape(id, value, BlobShape.builder(), modelFile); - break; + return loadSimpleShape(id, value, BlobShape.builder()); case "boolean": - loadSimpleShape(id, value, BooleanShape.builder(), modelFile); - break; + return loadSimpleShape(id, value, BooleanShape.builder()); case "byte": - loadSimpleShape(id, value, ByteShape.builder(), modelFile); - break; + return loadSimpleShape(id, value, ByteShape.builder()); case "short": - loadSimpleShape(id, value, ShortShape.builder(), modelFile); - break; + return loadSimpleShape(id, value, ShortShape.builder()); case "integer": - loadSimpleShape(id, value, IntegerShape.builder(), modelFile); - break; + return loadSimpleShape(id, value, IntegerShape.builder()); case "intEnum": - loadNamedMemberShape(id, value, IntEnumShape.builder(), modelFile); - break; + return loadNamedMemberShape(id, value, IntEnumShape.builder()); case "long": - loadSimpleShape(id, value, LongShape.builder(), modelFile); - break; + return loadSimpleShape(id, value, LongShape.builder()); case "float": - loadSimpleShape(id, value, FloatShape.builder(), modelFile); - break; + return loadSimpleShape(id, value, FloatShape.builder()); case "double": - loadSimpleShape(id, value, DoubleShape.builder(), modelFile); - break; + return loadSimpleShape(id, value, DoubleShape.builder()); case "document": - loadSimpleShape(id, value, DocumentShape.builder(), modelFile); - break; + return loadSimpleShape(id, value, DocumentShape.builder()); case "bigDecimal": - loadSimpleShape(id, value, BigDecimalShape.builder(), modelFile); - break; + return loadSimpleShape(id, value, BigDecimalShape.builder()); case "bigInteger": - loadSimpleShape(id, value, BigIntegerShape.builder(), modelFile); - break; + return loadSimpleShape(id, value, BigIntegerShape.builder()); case "string": - loadSimpleShape(id, value, StringShape.builder(), modelFile); - break; + return loadSimpleShape(id, value, StringShape.builder()); case "enum": - loadNamedMemberShape(id, value, EnumShape.builder(), modelFile); - break; + return loadNamedMemberShape(id, value, EnumShape.builder()); case "timestamp": - loadSimpleShape(id, value, TimestampShape.builder(), modelFile); - break; + return loadSimpleShape(id, value, TimestampShape.builder()); case "list": - loadCollection(id, value, ListShape.builder(), modelFile); - break; + return loadCollection(id, value, ListShape.builder()); case "set": - loadCollection(id, value, SetShape.builder(), modelFile); - break; + return loadCollection(id, value, SetShape.builder()); case "map": - loadMap(id, value, modelFile); - break; + return loadMap(id, value); case "resource": - loadResource(id, value, modelFile); - break; + return loadResource(id, value); case "service": - loadService(id, value, modelFile); - break; + return loadService(id, value); case "structure": - loadNamedMemberShape(id, value, StructureShape.builder(), modelFile); - break; + return loadNamedMemberShape(id, value, StructureShape.builder()); case "union": - loadNamedMemberShape(id, value, UnionShape.builder(), modelFile); - break; + return loadNamedMemberShape(id, value, UnionShape.builder()); case "operation": - loadOperation(id, value, modelFile); - break; + return loadOperation(id, value); case "apply": - LoaderUtils.checkForAdditionalProperties(value, id, APPLY_PROPERTIES, modelFile.events()); - applyTraits(id, value.expectObjectMember(TRAITS), modelFile); - break; + LoaderUtils.checkForAdditionalProperties(value, id, APPLY_PROPERTIES).ifPresent(this::emit); + applyTraits(id, value.expectObjectMember(TRAITS)); + return null; default: throw new SourceException("Invalid shape `type`: " + type, value); } } - private void applyTraits(ShapeId id, ObjectNode traits, FullyResolvedModelFile modelFile) { + private void applyTraits(ShapeId id, ObjectNode traits) { for (Map.Entry traitNode : traits.getMembers().entrySet()) { ShapeId traitId = traitNode.getKey().expectShapeId(); // JSON AST model traits are never considered annotation traits, meaning // that a null value provided in the AST is not coerced in the same way // as an omitted value in the IDL (e.g., "@foo"). - modelFile.onTrait(id, traitId, traitNode.getValue()); + operations.accept(new LoadOperation.ApplyTrait(modelVersion, traitNode.getKey().getSourceLocation(), + id.getNamespace(), id, traitId, traitNode.getValue())); } } - private void applyShapeTraits(ShapeId id, ObjectNode node, FullyResolvedModelFile modelFile) { - node.getObjectMember(TRAITS).ifPresent(traits -> applyTraits(id, traits, modelFile)); + private void applyShapeTraits(ShapeId id, ObjectNode node) { + node.getObjectMember(TRAITS).ifPresent(traits -> applyTraits(id, traits)); } - private void loadMember(FullyResolvedModelFile modelFile, ShapeId id, ObjectNode targetNode) { - LoaderUtils.checkForAdditionalProperties(targetNode, id, MEMBER_PROPERTIES, modelFile.events()); + private void loadMember(LoadOperation.DefineShape operation, ShapeId id, ObjectNode targetNode) { + LoaderUtils.checkForAdditionalProperties(targetNode, id, MEMBER_PROPERTIES).ifPresent(this::emit); MemberShape.Builder builder = MemberShape.builder().source(targetNode.getSourceLocation()).id(id); ShapeId target = targetNode.expectStringMember(TARGET).expectShapeId(); builder.target(target); - applyShapeTraits(id, targetNode, modelFile); - modelFile.onShape(builder); + applyShapeTraits(id, targetNode); + operation.addMember(builder); } - private void loadOptionalMember(FullyResolvedModelFile modelFile, ShapeId id, ObjectNode node, String member) { - node.getObjectMember(member).ifPresent(targetNode -> loadMember(modelFile, id, targetNode)); + private void loadOptionalMember(LoadOperation.DefineShape operation, ShapeId id, ObjectNode node, String member) { + node.getObjectMember(member).ifPresent(targetNode -> loadMember(operation, id, targetNode)); } - private void loadCollection( + private LoadOperation.DefineShape loadCollection( ShapeId id, ObjectNode node, - CollectionShape.Builder builder, - FullyResolvedModelFile modelFile + CollectionShape.Builder builder ) { - LoaderUtils.checkForAdditionalProperties(node, id, COLLECTION_PROPERTY_NAMES, modelFile.events()); - applyShapeTraits(id, node, modelFile); + LoaderUtils.checkForAdditionalProperties(node, id, COLLECTION_PROPERTY_NAMES).ifPresent(this::emit); + applyShapeTraits(id, node); // Add the container before members to ensure sets are rejected before adding unreferenced members. - modelFile.onShape(builder.id(id).source(node.getSourceLocation())); - loadOptionalMember(modelFile, id.withMember("member"), node, "member"); - addMixins(id, node, modelFile); + builder.id(id).source(node.getSourceLocation()); + LoadOperation.DefineShape operation = createShape(builder); + loadOptionalMember(operation, id.withMember("member"), node, "member"); + addMixins(operation, node); + return operation; + } + + LoadOperation.DefineShape createShape(AbstractShapeBuilder builder) { + return new LoadOperation.DefineShape(modelVersion, builder); } - private void loadMap(ShapeId id, ObjectNode node, FullyResolvedModelFile modelFile) { - LoaderUtils.checkForAdditionalProperties(node, id, MAP_PROPERTY_NAMES, modelFile.events()); - loadOptionalMember(modelFile, id.withMember("key"), node, "key"); - loadOptionalMember(modelFile, id.withMember("value"), node, "value"); - applyShapeTraits(id, node, modelFile); - modelFile.onShape(MapShape.builder().id(id).source(node.getSourceLocation())); - addMixins(id, node, modelFile); + private LoadOperation.DefineShape loadMap(ShapeId id, ObjectNode node) { + LoaderUtils.checkForAdditionalProperties(node, id, MAP_PROPERTY_NAMES).ifPresent(this::emit); + MapShape.Builder builder = MapShape.builder().id(id).source(node.getSourceLocation()); + LoadOperation.DefineShape operation = createShape(builder); + loadOptionalMember(operation, id.withMember("key"), node, "key"); + loadOptionalMember(operation, id.withMember("value"), node, "value"); + addMixins(operation, node); + applyShapeTraits(id, node); + return operation; } - private void loadOperation(ShapeId id, ObjectNode node, FullyResolvedModelFile modelFile) { - LoaderUtils.checkForAdditionalProperties(node, id, OPERATION_PROPERTY_NAMES, modelFile.events()); - applyShapeTraits(id, node, modelFile); + private LoadOperation.DefineShape loadOperation(ShapeId id, ObjectNode node) { + LoaderUtils.checkForAdditionalProperties(node, id, OPERATION_PROPERTY_NAMES).ifPresent(this::emit); + applyShapeTraits(id, node); OperationShape.Builder builder = OperationShape.builder() .id(id) .source(node.getSourceLocation()) - .addErrors(loadOptionalTargetList(modelFile, id, node, ERRORS)); - loadOptionalTarget(modelFile, id, node, "input").ifPresent(builder::input); - loadOptionalTarget(modelFile, id, node, "output").ifPresent(builder::output); - modelFile.onShape(builder); - addMixins(id, node, modelFile); + .addErrors(loadOptionalTargetList(id, node, ERRORS)); + loadOptionalTarget(id, node, "input").ifPresent(builder::input); + loadOptionalTarget(id, node, "output").ifPresent(builder::output); + LoadOperation.DefineShape operation = createShape(builder); + addMixins(operation, node); + return operation; } - private void loadResource(ShapeId id, ObjectNode node, FullyResolvedModelFile modelFile) { - LoaderUtils.checkForAdditionalProperties(node, id, RESOURCE_PROPERTIES, modelFile.events()); - applyShapeTraits(id, node, modelFile); + private LoadOperation.DefineShape loadResource(ShapeId id, ObjectNode node) { + LoaderUtils.checkForAdditionalProperties(node, id, RESOURCE_PROPERTIES).ifPresent(this::emit); + applyShapeTraits(id, node); ResourceShape.Builder builder = ResourceShape.builder().id(id).source(node.getSourceLocation()); - loadOptionalTarget(modelFile, id, node, "put").ifPresent(builder::put); - loadOptionalTarget(modelFile, id, node, "create").ifPresent(builder::create); - loadOptionalTarget(modelFile, id, node, "read").ifPresent(builder::read); - loadOptionalTarget(modelFile, id, node, "update").ifPresent(builder::update); - loadOptionalTarget(modelFile, id, node, "delete").ifPresent(builder::delete); - loadOptionalTarget(modelFile, id, node, "list").ifPresent(builder::list); - builder.operations(loadOptionalTargetList(modelFile, id, node, "operations")); - builder.collectionOperations(loadOptionalTargetList(modelFile, id, node, "collectionOperations")); - builder.resources(loadOptionalTargetList(modelFile, id, node, "resources")); + loadOptionalTarget(id, node, "put").ifPresent(builder::put); + loadOptionalTarget(id, node, "create").ifPresent(builder::create); + loadOptionalTarget(id, node, "read").ifPresent(builder::read); + loadOptionalTarget(id, node, "update").ifPresent(builder::update); + loadOptionalTarget(id, node, "delete").ifPresent(builder::delete); + loadOptionalTarget(id, node, "list").ifPresent(builder::list); + builder.operations(loadOptionalTargetList(id, node, "operations")); + builder.collectionOperations(loadOptionalTargetList(id, node, "collectionOperations")); + builder.resources(loadOptionalTargetList(id, node, "resources")); // Load identifiers and resolve forward references. node.getObjectMember("identifiers").ifPresent(ids -> { for (Map.Entry entry : ids.getMembers().entrySet()) { String name = entry.getKey().getValue(); - ShapeId target = loadReferenceBody(modelFile, id, entry.getValue()); + ShapeId target = loadReferenceBody(id, entry.getValue()); builder.addIdentifier(name, target); } }); // Load properties and resolve forward references. node.getObjectMember("properties").ifPresent(properties -> { - if (!modelFile.getVersion().supportsResourceProperties()) { - modelFile.events().add(ValidationEvent.builder() + if (!modelVersion.supportsResourceProperties()) { + emit(ValidationEvent.builder() .sourceLocation(properties.getSourceLocation()) .id(Validator.MODEL_ERROR) .severity(Severity.ERROR) .message("Resource properties can only be used with Smithy version 2 or later. " - + "Attempted to use resource properties with version `" + modelFile.getVersion() + "`.") + + "Attempted to use resource properties with version `" + modelVersion + "`.") .build()); } for (Map.Entry entry : properties.getMembers().entrySet()) { String name = entry.getKey().getValue(); - ShapeId target = loadReferenceBody(modelFile, id, entry.getValue()); + ShapeId target = loadReferenceBody(id, entry.getValue()); builder.addProperty(name, target); } }); - modelFile.onShape(builder); - addMixins(id, node, modelFile); + LoadOperation.DefineShape operation = createShape(builder); + addMixins(operation, node); + return operation; } - private void loadService(ShapeId id, ObjectNode node, FullyResolvedModelFile modelFile) { - LoaderUtils.checkForAdditionalProperties(node, id, SERVICE_PROPERTIES, modelFile.events()); - applyShapeTraits(id, node, modelFile); + private LoadOperation.DefineShape loadService(ShapeId id, ObjectNode node) { + LoaderUtils.checkForAdditionalProperties(node, id, SERVICE_PROPERTIES).ifPresent(this::emit); + applyShapeTraits(id, node); ServiceShape.Builder builder = new ServiceShape.Builder().id(id).source(node.getSourceLocation()); node.getStringMember("version").map(StringNode::getValue).ifPresent(builder::version); - builder.operations(loadOptionalTargetList(modelFile, id, node, "operations")); - builder.resources(loadOptionalTargetList(modelFile, id, node, "resources")); + builder.operations(loadOptionalTargetList(id, node, "operations")); + builder.resources(loadOptionalTargetList(id, node, "resources")); loadServiceRenameIntoBuilder(builder, node); - builder.addErrors(loadOptionalTargetList(modelFile, id, node, ERRORS)); - modelFile.onShape(builder); - addMixins(id, node, modelFile); + builder.addErrors(loadOptionalTargetList(id, node, ERRORS)); + LoadOperation.DefineShape operation = createShape(builder); + addMixins(operation, node); + return operation; } static void loadServiceRenameIntoBuilder(ServiceShape.Builder builder, ObjectNode node) { @@ -341,58 +343,62 @@ static void loadServiceRenameIntoBuilder(ServiceShape.Builder builder, ObjectNod }); } - private void loadSimpleShape( - ShapeId id, ObjectNode node, AbstractShapeBuilder builder, FullyResolvedModelFile modelFile) { - LoaderUtils.checkForAdditionalProperties(node, id, SIMPLE_PROPERTY_NAMES, modelFile.events()); - applyShapeTraits(id, node, modelFile); - modelFile.onShape(builder.id(id).source(node.getSourceLocation())); - addMixins(id, node, modelFile); + private LoadOperation.DefineShape loadSimpleShape( + ShapeId id, ObjectNode node, AbstractShapeBuilder builder) { + LoaderUtils.checkForAdditionalProperties(node, id, SIMPLE_PROPERTY_NAMES).ifPresent(this::emit); + applyShapeTraits(id, node); + builder.id(id).source(node.getSourceLocation()); + LoadOperation.DefineShape operation = createShape(builder); + addMixins(operation, node); + return operation; } - private void loadNamedMemberShape( + private LoadOperation.DefineShape loadNamedMemberShape( ShapeId id, ObjectNode node, - AbstractShapeBuilder builder, - FullyResolvedModelFile modelFile + AbstractShapeBuilder builder ) { - LoaderUtils.checkForAdditionalProperties(node, id, NAMED_MEMBER_SHAPE_PROPERTY_NAMES, modelFile.events()); - modelFile.onShape(builder.id(id).source(node.getSourceLocation())); - finishLoadingNamedMemberShapeMembers(id, node, modelFile); + LoaderUtils.checkForAdditionalProperties(node, id, NAMED_MEMBER_SHAPE_PROPERTY_NAMES).ifPresent(this::emit); + builder.id(id).source(node.getSourceLocation()); + LoadOperation.DefineShape operation = createShape(builder); + finishLoadingNamedMemberShapeMembers(operation, node); + return operation; } - private void finishLoadingNamedMemberShapeMembers(ShapeId id, ObjectNode node, FullyResolvedModelFile modelFile) { - applyShapeTraits(id, node, modelFile); + private void finishLoadingNamedMemberShapeMembers(LoadOperation.DefineShape operation, ObjectNode node) { + applyShapeTraits(operation.toShapeId(), node); ObjectNode memberObject = node.getObjectMember(MEMBERS).orElse(Node.objectNode()); for (Map.Entry entry : memberObject.getStringMap().entrySet()) { - loadMember(modelFile, id.withMember(entry.getKey()), entry.getValue().expectObjectNode()); + loadMember(operation, operation.toShapeId().withMember(entry.getKey()), + entry.getValue().expectObjectNode()); } - addMixins(id, node, modelFile); + addMixins(operation, node); } - private void addMixins(ShapeId id, ObjectNode node, FullyResolvedModelFile modelFile) { + private void addMixins(LoadOperation.DefineShape operation, ObjectNode node) { ArrayNode mixins = node.getArrayMember(MIXINS).orElse(Node.arrayNode()); for (ObjectNode mixin : mixins.getElementsAs(ObjectNode.class)) { - modelFile.addPendingMixin(id, loadReferenceBody(modelFile, id, mixin)); + ShapeId mixinId = loadReferenceBody(operation.toShapeId(), mixin); + operation.addDependency(mixinId); + operation.addModifier(new ApplyMixin(mixinId)); } } - private Optional loadOptionalTarget( - FullyResolvedModelFile modelFile, ShapeId id, ObjectNode node, String member) { - return node.getObjectMember(member).map(r -> loadReferenceBody(modelFile, id, r)); + private Optional loadOptionalTarget(ShapeId id, ObjectNode node, String member) { + return node.getObjectMember(member).map(r -> loadReferenceBody(id, r)); } - private ShapeId loadReferenceBody(FullyResolvedModelFile modelFile, ShapeId id, Node reference) { + private ShapeId loadReferenceBody(ShapeId id, Node reference) { ObjectNode referenceObject = reference.expectObjectNode(); - LoaderUtils.checkForAdditionalProperties(referenceObject, id, REFERENCE_PROPERTIES, modelFile.events()); + LoaderUtils.checkForAdditionalProperties(referenceObject, id, REFERENCE_PROPERTIES).ifPresent(this::emit); return referenceObject.expectStringMember(TARGET).expectShapeId(); } - private List loadOptionalTargetList( - FullyResolvedModelFile modelFile, ShapeId id, ObjectNode node, String member) { + private List loadOptionalTargetList(ShapeId id, ObjectNode node, String member) { return node.getArrayMember(member).map(array -> { List ids = new ArrayList<>(array.size()); for (Node element : array.getElements()) { - ids.add(loadReferenceBody(modelFile, id, element)); + ids.add(loadReferenceBody(id, element)); } return ids; }).orElseGet(Collections::emptyList); diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/CompositeModelFile.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/CompositeModelFile.java deleted file mode 100644 index aa6a955d99f..00000000000 --- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/CompositeModelFile.java +++ /dev/null @@ -1,195 +0,0 @@ -/* - * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.smithy.model.loader; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.function.Function; -import java.util.logging.Logger; -import software.amazon.smithy.model.SourceLocation; -import software.amazon.smithy.model.node.Node; -import software.amazon.smithy.model.shapes.Shape; -import software.amazon.smithy.model.shapes.ShapeId; -import software.amazon.smithy.model.shapes.ShapeType; -import software.amazon.smithy.model.traits.MixinTrait; -import software.amazon.smithy.model.traits.Trait; -import software.amazon.smithy.model.traits.TraitFactory; -import software.amazon.smithy.model.validation.ValidationEvent; - -/** - * Aggregates together multiple {@code ModelFile}s. - */ -final class CompositeModelFile implements ModelFile { - - private static final Logger LOGGER = Logger.getLogger(CompositeModelFile.class.getName()); - - private final TraitFactory traitFactory; - private final List modelFiles; - private final List events = new ArrayList<>(); - - CompositeModelFile(TraitFactory traitFactory, List modelFiles) { - this.traitFactory = traitFactory; - this.modelFiles = modelFiles; - } - - @Override - public Version getVersion() { - return Version.UNKNOWN; - } - - @Override - public String getFilename() { - return SourceLocation.none().getFilename(); - } - - @Override - public Set shapeIds() { - Set ids = new HashSet<>(); - for (ModelFile modelFile : modelFiles) { - ids.addAll(modelFile.shapeIds()); - } - return ids; - } - - @Override - public ShapeType getShapeType(ShapeId id) { - for (ModelFile modFile : modelFiles) { - ShapeType fileType = modFile.getShapeType(id); - if (fileType != null) { - return fileType; - } - } - return null; - } - - @Override - public Map metadata() { - MetadataContainer metadata = new MetadataContainer(events); - for (ModelFile modelFile : modelFiles) { - for (Map.Entry entry : modelFile.metadata().entrySet()) { - metadata.putMetadata(entry.getKey(), entry.getValue()); - } - } - return metadata.getData(); - } - - @Override - public TraitContainer resolveShapes(Set ids, Function typeProvider) { - TraitContainer.TraitHashMap traitValues = new TraitContainer.TraitHashMap(traitFactory, events); - for (ModelFile modelFile : modelFiles) { - TraitContainer other = modelFile.resolveShapes(ids, typeProvider); - for (Map.Entry> entry : other.traits().entrySet()) { - ShapeId target = entry.getKey(); - for (Map.Entry appliedEntry : entry.getValue().entrySet()) { - traitValues.onTrait(target, appliedEntry.getValue()); - } - } - } - return traitValues; - } - - @Override - public List events() { - // Size the array using the known size of all events. - int size = events.size(); - for (ModelFile modelFile : modelFiles) { - size += modelFile.events().size(); - } - - List newEvents = new ArrayList<>(size); - newEvents.addAll(events); - for (ModelFile modelFile : modelFiles) { - newEvents.addAll(modelFile.events()); - } - - return newEvents; - } - - @Override - public CreatedShapes createShapes(TraitContainer resolvedTraits) { - Map createdShapes = new ResolvedShapeMap(); - Map pendingShapes = new PendingShapeMap(); - TopologicalShapeSort sorter = new TopologicalShapeSort(); - - for (ModelFile modelFile : modelFiles) { - CreatedShapes created = modelFile.createShapes(resolvedTraits); - for (Shape shape : created.getCreatedShapes()) { - createdShapes.put(shape.getId(), shape); - // Optimization: Only add shapes that could be dependencies of - // pending shapes. - if (shape.hasTrait(MixinTrait.class) || shape.isResourceShape()) { - sorter.enqueue(shape); - } - } - for (PendingShape pending : created.getPendingShapes()) { - sorter.enqueue(pending.getId(), pending.getPendingShapes()); - pendingShapes.put(pending.getId(), pending); - } - } - - try { - for (ShapeId id : sorter.dequeueSortedShapes()) { - if (pendingShapes.containsKey(id)) { - // Build pending shapes, which in turn populates the createShapes map. - pendingShapes.get(id).buildShapes(createdShapes); - } - } - } catch (TopologicalShapeSort.CycleException e) { - // Emit useful, per shape, error messages. - for (PendingShape pending : pendingShapes.values()) { - if (e.getUnresolved().contains(pending.getId())) { - events.addAll(pending.unresolved(createdShapes, pendingShapes)); - resolvedTraits.getTraitsForShape(pending.getId()).clear(); - } - } - } - - return new CreatedShapes(createdShapes.values(), Collections.emptyList()); - } - - private final class ResolvedShapeMap extends HashMap { - @Override - public Shape put(ShapeId key, Shape value) { - Shape old = get(key); - if (old == null) { - return super.put(key, value); - } else if (!old.equals(value)) { - events.add(LoaderUtils.onShapeConflict(key, value.getSourceLocation(), old.getSourceLocation())); - } else if (!LoaderUtils.isSameLocation(value, old)) { - LOGGER.warning(() -> "Ignoring duplicate but equivalent shape definition: " + old.getId() - + " defined at " + value.getSourceLocation() + " and " - + old.getSourceLocation()); - } - return old; - } - } - - private static final class PendingShapeMap extends HashMap { - @Override - public PendingShape put(ShapeId key, PendingShape pending) { - PendingShape old = get(key); - if (old != null) { - pending = PendingShape.mergeIntoLeft(old, pending); - } - return super.put(key, pending); - } - } -} diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/ForwardReferenceModelFile.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/ForwardReferenceModelFile.java deleted file mode 100644 index d4895299584..00000000000 --- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/ForwardReferenceModelFile.java +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.smithy.model.loader; - -import java.util.ArrayDeque; -import java.util.Deque; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; -import java.util.function.BiConsumer; -import java.util.function.Consumer; -import java.util.function.Function; -import software.amazon.smithy.model.SourceLocation; -import software.amazon.smithy.model.shapes.AbstractShapeBuilder; -import software.amazon.smithy.model.shapes.ShapeId; -import software.amazon.smithy.model.shapes.ShapeType; -import software.amazon.smithy.model.traits.TraitFactory; -import software.amazon.smithy.utils.Pair; - -/** - * A ModelFile that contains forward references. - * - * @see IdlModelParser - */ -final class ForwardReferenceModelFile extends AbstractMutableModelFile { - - /** The nullable namespace. Is null until it's set. */ - private String namespace; - - /** A queue of forward references. A queue is used since references can be added during resolution. */ - private final Deque>>> forwardReferences - = new ArrayDeque<>(); - - private final Map> useShapes = new HashMap<>(); - - /** - * @param filename Name of the file being parsed. - * @param traitFactory Factory used to create traits when merging traits. - */ - ForwardReferenceModelFile(String filename, TraitFactory traitFactory) { - super(filename, traitFactory); - } - - /** - * Get the currently set namespace. - * - * @return Returns the currently set namespace or {@code null} if not set. - */ - String namespace() { - return namespace; - } - - /** - * Sets the current namespace. - * - * @param namespace Namespace to set. - */ - void setNamespace(String namespace) { - this.namespace = namespace; - } - - /** - * Invoked when a shape is "used" in the model file. - * - * @param id Shape ID to use. - * @param location The source location of where this use occurred. - */ - void useShape(ShapeId id, SourceLocation location) { - // Duplicate use statements. - if (useShapes.containsKey(id.getName())) { - ShapeId previous = useShapes.get(id.getName()).left; - String message = String.format("Cannot use name `%s` because it conflicts with `%s`", id, previous); - throw new ModelSyntaxException(message, location); - } - - useShapes.put(id.getName(), Pair.of(id, location)); - } - - @Override - void onShape(AbstractShapeBuilder builder) { - if (useShapes.containsKey(builder.getId().getName())) { - ShapeId previous = useShapes.get(builder.getId().getName()).left; - String message = String.format("Shape name `%s` conflicts with imported shape `%s`", - builder.getId().getName(), previous); - throw new ModelSyntaxException(message, builder); - } - - super.onShape(builder); - } - - /** - * Adds a forward reference that will be resolved when - * {@link #resolveShapes} is called. - * - * @param name The name of the shape that needs to be resolved. - * @param consumer The consumer that receives the resolved shape ID. - */ - void addForwardReference(String name, Consumer consumer) { - forwardReferences.add(Pair.of(name, (id, typeProvider) -> consumer.accept(id))); - } - - /** - * Adds a forward reference that will be resolved when - * {@link #resolveShapes} is called. - * - *

This variant of {@code addForwardReference} issued when the consumer - * also needs to know the type of shape that is being resolved. For - * example, this is necessary in order to coerce annotation traits - * (traits that define no value) into the expected type for a shape - * (e.g., a list or object). - * - * @param name The name of the shape that needs to be resolved. - * @param consumer The consumer that receives the resolved shape ID. - */ - void addForwardReference(String name, BiConsumer> consumer) { - forwardReferences.add(Pair.of(name, consumer)); - } - - @Override - public TraitContainer resolveShapes(Set ids, Function typeProvider) { - while (!forwardReferences.isEmpty()) { - Pair>> pair = forwardReferences.pop(); - String name = pair.left; - BiConsumer> consumer = pair.right; - - ShapeId resolved; - // Use absolute IDs as-is. - if (name.contains("#")) { - resolved = ShapeId.from(name); - } else if (useShapes.containsKey(name)) { - // Check use statements. - resolved = useShapes.get(name).left; - } else { - // Check if there's a shape with this name in the current namespace. - resolved = ShapeId.from(namespace() + "#" + name); - - // If not defined in the namespace, then check the prelude. - if (!ids.contains(resolved)) { - ShapeId preludeTest = ShapeId.from(Prelude.NAMESPACE + '#' + name); - if (ids.contains(preludeTest)) { - resolved = preludeTest; - } - } - } - - consumer.accept(resolved, typeProvider); - } - - return traitContainer; - } -} diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/FullyResolvedModelFile.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/FullyResolvedModelFile.java deleted file mode 100644 index 5c7a1f19703..00000000000 --- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/FullyResolvedModelFile.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.smithy.model.loader; - -import java.util.Collection; -import java.util.Set; -import java.util.function.Function; -import software.amazon.smithy.model.SourceLocation; -import software.amazon.smithy.model.shapes.AbstractShapeBuilder; -import software.amazon.smithy.model.shapes.Shape; -import software.amazon.smithy.model.shapes.ShapeId; -import software.amazon.smithy.model.shapes.ShapeType; -import software.amazon.smithy.model.traits.Trait; -import software.amazon.smithy.model.traits.TraitFactory; - - -/** - * A model file used for models that do not need forward reference - * resolution (e.g., the JSON AST, manually loaded Nodes, pre-made - * {@link AbstractShapeBuilder} objects, etc). - * - * @see AstModelLoader - */ -final class FullyResolvedModelFile extends AbstractMutableModelFile { - - /** - * @param filename File being parsed. - * @param traitFactory Factory used to create traits when merging traits. - */ - FullyResolvedModelFile(String filename, TraitFactory traitFactory) { - super(filename, traitFactory); - } - - /** - * Create a {@code FullyResolvedModelFile} from already built shapes. - * - * @param traitFactory Factory used to create traits when merging traits. - * @param shapes Shapes to convert into builders and treat as a ModelFile. - * @return Returns the create {@code FullyResolvedModelFile} containing the shapes. - */ - static FullyResolvedModelFile fromShapes(TraitFactory traitFactory, Collection shapes) { - return fromShapes(traitFactory, shapes, Version.UNKNOWN); - } - - /** - * Create a {@code FullyResolvedModelFile} from already built shapes. - * - * @param traitFactory Factory used to create traits when merging traits. - * @param shapes Shapes to convert into builders and treat as a ModelFile. - * @return Returns the create {@code FullyResolvedModelFile} containing the shapes. - */ - static FullyResolvedModelFile fromShapes(TraitFactory traitFactory, Collection shapes, Version version) { - FullyResolvedModelFile modelFile = new FullyResolvedModelFile( - SourceLocation.none().getFilename(), traitFactory); - - modelFile.setVersion(version); - - for (Shape shape : shapes) { - // Convert the shape to a builder and remove all the traits. - // These traits are added to the trait container so that they - // can be merged correctly with any other model. - AbstractShapeBuilder builder = Shape.shapeToBuilder(shape).clearTraits(); - modelFile.onShape(builder); - // Add the traits that were present on the shape. - for (Trait trait : shape.getAllTraits().values()) { - modelFile.onTrait(shape.getId(), trait); - } - } - return modelFile; - } - - @Override - public TraitContainer resolveShapes(Set shapeIds, Function typeProvider) { - return traitContainer; - } -} diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlModelParser.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlModelParser.java index 6e75ed527a5..0261897c0ad 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlModelParser.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlModelParser.java @@ -21,11 +21,13 @@ import java.math.BigInteger; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.StringJoiner; +import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; import software.amazon.smithy.model.SourceLocation; @@ -68,7 +70,6 @@ import software.amazon.smithy.model.traits.InputTrait; import software.amazon.smithy.model.traits.OutputTrait; import software.amazon.smithy.model.traits.Trait; -import software.amazon.smithy.model.traits.TraitFactory; import software.amazon.smithy.model.traits.UnitTypeTrait; import software.amazon.smithy.model.validation.Severity; import software.amazon.smithy.model.validation.ValidationEvent; @@ -104,7 +105,6 @@ final class IdlModelParser extends SimpleParser { IDENTIFIERS_KEY, RESOURCES_KEY, OPERATIONS_KEY, PUT_KEY, PROPERTIES_KEY, COLLECTION_OPERATIONS_KEY); static final List SERVICE_PROPERTY_NAMES = ListUtils.of( TYPE_KEY, VERSION_KEY, OPERATIONS_KEY, RESOURCES_KEY, RENAME_KEY, ERRORS_KEY); - private static final Collection OPERATION_PROPERTY_NAMES = ListUtils.of("input", "output", "errors"); private static final Set SHAPE_TYPES = new HashSet<>(); static { @@ -115,9 +115,13 @@ final class IdlModelParser extends SimpleParser { } } - final ForwardReferenceModelFile modelFile; private final String filename; + private final Map useShapes = new HashMap<>(); + private Consumer operations; + private Version modelVersion = Version.VERSION_1_0; + private String namespace; private TraitEntry pendingDocumentationComment; + private boolean emittedVersion = false; private String operationInputSuffix = "Input"; private String operationOutputSuffix = "Output"; @@ -135,18 +139,60 @@ static final class TraitEntry { } } - IdlModelParser(TraitFactory traitFactory, String filename, String model) { + IdlModelParser(String filename, String model) { super(model, MAX_NESTING_LEVEL); this.filename = filename; - this.modelFile = new ForwardReferenceModelFile(filename, traitFactory); } - ModelFile parse() { + void parse(Consumer operationConsumer) { + operations = operationConsumer; ws(); parseControlSection(); + + // Emit a version from the current location if the assumed 1.0 is used. + if (!emittedVersion) { + operations.accept(new LoadOperation.ModelVersion(modelVersion, currentLocation())); + } + parseMetadataSection(); parseShapeSection(); - return modelFile; + } + + LoadOperation.DefineShape createShape(AbstractShapeBuilder builder) { + return new LoadOperation.DefineShape(modelVersion, builder); + } + + void addOperation(LoadOperation operation) { + operations.accept(operation); + } + + void emit(ValidationEvent event) { + addOperation(new LoadOperation.Event(event)); + } + + void addForwardReference(String id, BiConsumer> consumer) { + int memberPosition = id.indexOf('$'); + + // Check for members by removing the member and checking for the root shape. + if (memberPosition > 0 && memberPosition < id.length() - 1) { + addForwardReference(id.substring(0, memberPosition), (resolved, type) -> { + consumer.accept(resolved.withMember(id.substring(memberPosition + 1)), type); + }); + } else { + String resolved = useShapes.containsKey(id) ? useShapes.get(id).toString() : id; + addOperation(new LoadOperation.ForwardReference(namespace, resolved, consumer)); + } + } + + void addForwardReference(String id, Consumer consumer) { + addForwardReference(id, (resolved, found) -> consumer.accept(resolved)); + } + + String expectNamespace() { + if (namespace == null) { + throw new IllegalStateException("No namespace was set before trying to resolve a forward reference"); + } + return namespace; } /** @@ -207,19 +253,25 @@ private void parseControlSection() { Node value = IdlNodeParser.parseNode(this); - if (key.equals("version")) { - onVersion(value); - } else if (key.equals("operationInputSuffix")) { - operationInputSuffix = value.expectStringNode().getValue(); - } else if (key.equals("operationOutputSuffix")) { - operationOutputSuffix = value.expectStringNode().getValue(); - } else { - modelFile.events().add(ValidationEvent.builder() - .id(Validator.MODEL_ERROR) - .sourceLocation(value) - .severity(Severity.WARNING) - .message(format("Unknown control statement `%s` with value `%s", key, Node.printJson(value))) - .build()); + switch (key) { + case "version": + onVersion(value); + break; + case "operationInputSuffix": + operationInputSuffix = value.expectStringNode().getValue(); + break; + case "operationOutputSuffix": + operationOutputSuffix = value.expectStringNode().getValue(); + break; + default: + emit(ValidationEvent.builder() + .id(Validator.MODEL_ERROR) + .sourceLocation(value) + .severity(Severity.WARNING) + .message(format("Unknown control statement `%s` with value `%s", + key, Node.printJson(value))) + .build()); + break; } ws(); @@ -239,7 +291,8 @@ private void onVersion(Node value) { throw syntax("Unsupported Smithy version number: " + parsedVersion); } - modelFile.setVersion(resolvedVersion); + modelVersion = resolvedVersion; + operations.accept(new LoadOperation.ModelVersion(modelVersion, value.getSourceLocation())); } private void parseMetadataSection() { @@ -257,7 +310,7 @@ private void parseMetadataSection() { ws(); expect('='); ws(); - modelFile.putMetadata(key, IdlNodeParser.parseNode(this)); + operations.accept(new LoadOperation.PutMetadata(modelVersion, key, IdlNodeParser.parseNode(this))); ws(); } } @@ -278,7 +331,7 @@ private void parseShapeSection() { // Parse the namespace. int start = position(); ParserUtils.consumeNamespace(this); - modelFile.setNamespace(sliceFrom(start)); + namespace = sliceFrom(start); // Clear out any erroneous documentation comments. clearPendingDocs(); ws(); @@ -311,10 +364,35 @@ private void parseUseSection() { clearPendingDocs(); ws(); - modelFile.useShape(ShapeId.from(lexeme), location); + ShapeId target = ShapeId.from(lexeme); + + // Validate use statements when the model is fully loaded. + addForwardReference(lexeme, (resolved, typeProvider) -> { + if (typeProvider.apply(resolved) == null) { + ValidationEvent event = ValidationEvent.builder() + .id(Validator.MODEL_ERROR) + .severity(Severity.WARNING) + .sourceLocation(location) + .message("Use statement refers to undefined shape: " + lexeme) + .build(); + emit(event); + } + }); + + useShape(target, location); } } + void useShape(ShapeId id, SourceLocation location) { + if (useShapes.containsKey(id.getName())) { + ShapeId previous = useShapes.get(id.getName()); + String message = String.format("Cannot use name `%s` because it conflicts with `%s`", id, previous); + throw new ModelSyntaxException(message, location); + } + + useShapes.put(id.getName(), id); + } + private void parseShapeStatements() { while (!eof()) { ws(); @@ -455,44 +533,55 @@ private void parseShape(List traits) { } addTraits(id, traits); + clearPendingDocs(); ws(); } private ShapeId parseShapeName() { + SourceLocation currentLocation = currentLocation(); String name = ParserUtils.parseIdentifier(this); - return ShapeId.fromRelative(modelFile.namespace(), name); + ShapeId id = ShapeId.fromRelative(expectNamespace(), name); + + if (useShapes.containsKey(name)) { + ShapeId previous = useShapes.get(name); + String message = String.format("Shape name `%s` conflicts with imported shape `%s`", name, previous); + throw new ModelSyntaxException(message, currentLocation); + } + + return id; } - private void parseSimpleShape(ShapeId id, SourceLocation location, AbstractShapeBuilder builder) { - modelFile.onShape(builder.source(location).id(id)); - parseMixins(id); + private void parseSimpleShape(ShapeId id, SourceLocation location, AbstractShapeBuilder builder) { + LoadOperation.DefineShape operation = createShape(builder.source(location).id(id)); + parseMixins(operation); + operations.accept(operation); } private void parseEnumShape( ShapeId id, SourceLocation location, - AbstractShapeBuilder builder, + AbstractShapeBuilder builder, MemberParsing memberParsing ) { - modelFile.onShape(builder.id(id).source(location)); - parseMixins(id); - parseMembers(id, Collections.emptySet(), memberParsing); + LoadOperation.DefineShape operation = createShape(builder.id(id).source(location)); + parseMixins(operation); + parseMembers(operation, Collections.emptySet(), memberParsing); clearPendingDocs(); + operations.accept(operation); } // See parseMap for information on why members are parsed before the // list/set is registered with the ModelFile. - private void parseCollection(ShapeId id, SourceLocation location, CollectionShape.Builder builder) { - builder.id(id).source(location); - parseMixins(id); - // Add the shape before parsing and adding members to ensure sets are rejected before adding a member. - modelFile.onShape(builder.id(id)); - parseMembers(id, SetUtils.of("member")); + private void parseCollection(ShapeId id, SourceLocation location, CollectionShape.Builder builder) { + LoadOperation.DefineShape operation = createShape(builder.id(id).source(location)); + parseMixins(operation); + parseMembers(operation, SetUtils.of("member")); clearPendingDocs(); + operations.accept(operation); } - private void parseMembers(ShapeId id, Set requiredMembers) { - parseMembers(id, requiredMembers, MemberParsing.PARSING_MEMBER); + private void parseMembers(LoadOperation.DefineShape operation, Set requiredMembers) { + parseMembers(operation, requiredMembers, MemberParsing.PARSING_MEMBER); } private enum MemberParsing { @@ -600,7 +689,7 @@ boolean targetsUnit() { abstract boolean targetsUnit(); } - private void parseMembers(ShapeId id, Set requiredMembers, MemberParsing memberParsing) { + private void parseMembers(LoadOperation.DefineShape op, Set requiredMembers, MemberParsing memberParsing) { Set definedMembers = new HashSet<>(); ws(); @@ -612,7 +701,7 @@ private void parseMembers(ShapeId id, Set requiredMembers, MemberParsing break; } - parseMember(id, requiredMembers, definedMembers, memberParsing); + parseMember(op, requiredMembers, definedMembers, memberParsing); // Clears out any previously captured documentation // comments that may have been found when parsing the member. @@ -628,7 +717,14 @@ private void parseMembers(ShapeId id, Set requiredMembers, MemberParsing expect('}'); } - private void parseMember(ShapeId parent, Set allowed, Set defined, MemberParsing memberParsing) { + private void parseMember( + LoadOperation.DefineShape operation, + Set allowed, + Set defined, + MemberParsing memberParsing + ) { + ShapeId parent = operation.toShapeId(); + // Parse optional member traits. List memberTraits = parseDocsAndTraits(); SourceLocation memberLocation = currentLocation(); @@ -654,24 +750,23 @@ private void parseMember(ShapeId parent, Set allowed, Set define ShapeId memberId = parent.withMember(memberName); - if (isTargetElided && !modelFile.getVersion().supportsTargetElision()) { + if (isTargetElided && !modelVersion.supportsTargetElision()) { throw syntax(memberId, "Members can only elide targets in IDL version 2 or later. " - + "Attempted to elide a target with version `" + modelFile.getVersion() + "`."); + + "Attempted to elide a target with version `" + modelVersion + "`."); } MemberShape.Builder memberBuilder = MemberShape.builder().id(memberId).source(memberLocation); - modelFile.onShape(memberBuilder); // Members whose targets are elided will have those targets resolved later, // for example by SetResourceBasedTargets if (!isTargetElided) { if (memberParsing.targetsUnit()) { - modelFile.addForwardReference(UnitTypeTrait.UNIT.toString(), memberBuilder::target); + addForwardReference(UnitTypeTrait.UNIT.toString(), memberBuilder::target); } else { ws(); expect(':'); ws(); - modelFile.addForwardReference(ParserUtils.parseShapeId(this), memberBuilder::target); + addForwardReference(ParserUtils.parseShapeId(this), memberBuilder::target); } } @@ -679,7 +774,7 @@ private void parseMember(ShapeId parent, Set allowed, Set define sp(); if (memberParsing.supportsAssignment() && peek() == '=') { - if (!modelFile.getVersion().isDefaultSupported()) { + if (!modelVersion.isDefaultSupported()) { throw syntax("@default assignment is only supported in IDL version 2 or later"); } expect('='); @@ -687,21 +782,17 @@ private void parseMember(ShapeId parent, Set allowed, Set define memberBuilder.addTrait(memberParsing.createAssignmentTrait(memberId, IdlNodeParser.parseNode(this))); } - addTraits(parent.withMember(memberName), memberTraits); + // Only add the member once fully parsed. + operation.addMember(memberBuilder); + addTraits(memberBuilder.getId(), memberTraits); } private void parseMapStatement(ShapeId id, SourceLocation location) { - // Parsing members of list/set/map before registering the shape with - // the ModelFile ensures that the shape is only registered if it - // has all of its required members. Otherwise, the validation gives - // a cryptic message with no context about how a "member" wasn't set - // on a builder. This does not suffer the same error messages as - // structures/unions because list/set/map have a fixed and required - // set of members that must be provided. - parseMixins(id); - parseMembers(id, SetUtils.of("key", "value")); - modelFile.onShape(MapShape.builder().id(id).source(location)); + LoadOperation.DefineShape operation = createShape(MapShape.builder().id(id).source(location)); + parseMixins(operation); + parseMembers(operation, SetUtils.of("key", "value")); clearPendingDocs(); + operations.accept(operation); } private void parseStructuredShape( @@ -710,26 +801,22 @@ private void parseStructuredShape( AbstractShapeBuilder builder, MemberParsing memberParsing ) { - // Register the structure/union with the loader before parsing members. - // This will detect shape conflicts with other types (like an operation) - // and still give useful error messages. Trying to parse members first - // would otherwise result in cryptic error messages like: - // "Member `foo.baz#Foo$Baz` cannot be added to software.amazon.smithy.model.shapes.OperationShape$Builder" - modelFile.onShape(builder.id(id).source(location)); + LoadOperation.DefineShape operation = createShape(builder.id(id).source(location)); // If it's a structure, parse the optional "from" statement to enable // eliding member targets for resource identifiers. if (builder.getShapeType() == ShapeType.STRUCTURE) { - parseForResource(id); + parseForResource(operation); } // Parse optional "with" statements to add mixins, but only if it's supported by the version. - parseMixins(id); - parseMembers(id, Collections.emptySet(), memberParsing); + parseMixins(operation); + parseMembers(operation, Collections.emptySet(), memberParsing); clearPendingDocs(); + operations.accept(operation); } - private void parseMixins(ShapeId id) { + private void parseMixins(LoadOperation.DefineShape operation) { sp(); if (peek() != 'w') { return; @@ -740,9 +827,9 @@ private void parseMixins(ShapeId id) { expect('t'); expect('h'); - if (!modelFile.getVersion().supportsMixins()) { - throw syntax(id, "Mixins can only be used with Smithy version 2 or later. " - + "Attempted to use mixins with version `" + modelFile.getVersion() + "`."); + if (!modelVersion.supportsMixins()) { + throw syntax(operation.toShapeId(), "Mixins can only be used with Smithy version 2 or later. " + + "Attempted to use mixins with version `" + modelVersion + "`."); } ws(); @@ -751,16 +838,21 @@ private void parseMixins(ShapeId id) { do { String target = ParserUtils.parseShapeId(this); - modelFile.addForwardReference(target, resolved -> modelFile.addPendingMixin(id, resolved)); + addForwardReference(target, resolved -> { + operation.addDependency(resolved); + operation.addModifier(new ApplyMixin(resolved)); + }); ws(); } while (peek() != ']'); + expect(']'); clearPendingDocs(); } private void parseOperationStatement(ShapeId id, SourceLocation location) { - parseMixins(id); OperationShape.Builder builder = OperationShape.builder().id(id).source(location); + LoadOperation.DefineShape operation = createShape(builder); + parseMixins(operation); parseProperties(id, propertyName -> { switch (propertyName) { case "input": @@ -778,8 +870,8 @@ private void parseOperationStatement(ShapeId id, SourceLocation location) { throw syntax(id, String.format("Unknown property %s for %s", propertyName, id)); } }); - modelFile.onShape(builder); clearPendingDocs(); + operations.accept(operation); } private void parseProperties(ShapeId id, Consumer valueParser) { @@ -811,9 +903,9 @@ private void parseInlineableOperationMember( TraitEntry defaultTrait ) { if (peek() == '=') { - if (!modelFile.getVersion().supportsInlineOperationIO()) { + if (!modelVersion.supportsInlineOperationIO()) { throw syntax(id, "Inlined operation inputs and outputs can only be used with Smithy version 2 or " - + "later. Attempted to use inlined IO with version `" + modelFile.getVersion() + "`."); + + "later. Attempted to use inlined IO with version `" + modelVersion + "`."); } expect('='); clearPendingDocs(); @@ -821,7 +913,7 @@ private void parseInlineableOperationMember( consumer.accept(parseInlineStructure(id.getName() + suffix, defaultTrait)); } else { ws(); - modelFile.addForwardReference(ParserUtils.parseShapeId(this), consumer); + addForwardReference(ParserUtils.parseShapeId(this), consumer); } } @@ -830,21 +922,21 @@ private ShapeId parseInlineStructure(String name, TraitEntry defaultTrait) { if (defaultTrait != null) { traits.add(defaultTrait); } - ShapeId id = ShapeId.fromRelative(modelFile.namespace(), name); + ShapeId id = ShapeId.fromRelative(expectNamespace(), name); SourceLocation location = currentLocation(); - parseMixins(id); - parseForResource(id); StructureShape.Builder builder = StructureShape.builder().id(id).source(location); - - modelFile.onShape(builder); - parseMembers(id, Collections.emptySet(), MemberParsing.PARSING_STRUCTURE_MEMBER); + LoadOperation.DefineShape operation = createShape(builder); + parseMixins(operation); + parseForResource(operation); + parseMembers(operation, Collections.emptySet(), MemberParsing.PARSING_STRUCTURE_MEMBER); addTraits(id, traits); clearPendingDocs(); + operations.accept(operation); ws(); return id; } - private void parseForResource(ShapeId id) { + private void parseForResource(LoadOperation.DefineShape operation) { sp(); if (peek() != 'f') { return; @@ -854,17 +946,18 @@ private void parseForResource(ShapeId id) { expect('o'); expect('r'); - if (!modelFile.getVersion().supportsTargetElision()) { - throw syntax(id, "Structures can only be bound to resources with Smithy version 2 or later. " - + "Attempted to bind a structure to a resource with version `" + modelFile.getVersion() + "`."); + if (!modelVersion.supportsTargetElision()) { + throw syntax(operation.toShapeId(), "Structures can only be bound to resources with Smithy version 2 or " + + "later. Attempted to bind a structure to a resource with version `" + + modelVersion + "`."); } ws(); - modelFile.addForwardReference( - ParserUtils.parseShapeId(this), - shapeId -> modelFile.addPendingModification(id, new SetResourceBasedTargets(shapeId)) - ); + addForwardReference(ParserUtils.parseShapeId(this), shapeId -> { + operation.addDependency(shapeId); + operation.addModifier(new ApplyResourceBasedTargets(shapeId)); + }); } private void parseIdList(Consumer consumer) { @@ -874,7 +967,7 @@ private void parseIdList(Consumer consumer) { ws(); while (!eof() && peek() != ']') { - modelFile.addForwardReference(ParserUtils.parseShapeId(this), consumer); + addForwardReference(ParserUtils.parseShapeId(this), consumer); ws(); } @@ -883,23 +976,24 @@ private void parseIdList(Consumer consumer) { } private void parseServiceStatement(ShapeId id, SourceLocation location) { - parseMixins(id); - ws(); ServiceShape.Builder builder = new ServiceShape.Builder().id(id).source(location); + LoadOperation.DefineShape operation = createShape(builder); + parseMixins(operation); + ws(); ObjectNode shapeNode = IdlNodeParser.parseObjectNode(this, id.toString()); - LoaderUtils.checkForAdditionalProperties(shapeNode, id, SERVICE_PROPERTY_NAMES, modelFile.events()); + LoaderUtils.checkForAdditionalProperties(shapeNode, id, SERVICE_PROPERTY_NAMES).ifPresent(this::emit); shapeNode.getStringMember(VERSION_KEY).map(StringNode::getValue).ifPresent(builder::version); - modelFile.onShape(builder); optionalIdList(shapeNode, OPERATIONS_KEY, builder::addOperation); optionalIdList(shapeNode, RESOURCES_KEY, builder::addResource); optionalIdList(shapeNode, ERRORS_KEY, builder::addError); AstModelLoader.loadServiceRenameIntoBuilder(builder, shapeNode); clearPendingDocs(); + operations.accept(operation); } private void optionalId(ObjectNode node, String name, Consumer consumer) { if (node.getMember(name).isPresent()) { - modelFile.addForwardReference(node.expectStringMember(name).getValue(), consumer); + addForwardReference(node.expectStringMember(name).getValue(), consumer); } } @@ -907,19 +1001,20 @@ private void optionalIdList(ObjectNode node, String name, Consumer cons if (node.getMember(name).isPresent()) { ArrayNode value = node.expectArrayMember(name); for (StringNode element : value.getElementsAs(StringNode.class)) { - modelFile.addForwardReference(element.getValue(), consumer); + addForwardReference(element.getValue(), consumer); } } } private void parseResourceStatement(ShapeId id, SourceLocation location) { - parseMixins(id); - ws(); ResourceShape.Builder builder = ResourceShape.builder().id(id).source(location); - modelFile.onShape(builder); - ObjectNode shapeNode = IdlNodeParser.parseObjectNode(this, id.toString()); + LoadOperation.DefineShape operation = createShape(builder); + + parseMixins(operation); + ws(); - LoaderUtils.checkForAdditionalProperties(shapeNode, id, RESOURCE_PROPERTY_NAMES, modelFile.events()); + ObjectNode shapeNode = IdlNodeParser.parseObjectNode(this, id.toString()); + LoaderUtils.checkForAdditionalProperties(shapeNode, id, RESOURCE_PROPERTY_NAMES).ifPresent(this::emit); optionalId(shapeNode, PUT_KEY, builder::put); optionalId(shapeNode, CREATE_KEY, builder::create); optionalId(shapeNode, READ_KEY, builder::read); @@ -935,22 +1030,23 @@ private void parseResourceStatement(ShapeId id, SourceLocation location) { for (Map.Entry entry : ids.getMembers().entrySet()) { String name = entry.getKey().getValue(); StringNode target = entry.getValue().expectStringNode(); - modelFile.addForwardReference(target.getValue(), targetId -> builder.addIdentifier(name, targetId)); + addForwardReference(target.getValue(), targetId -> builder.addIdentifier(name, targetId)); } }); // Load properties and resolve forward references. shapeNode.getObjectMember(PROPERTIES_KEY).ifPresent(properties -> { - if (!modelFile.getVersion().supportsResourceProperties()) { + if (!modelVersion.supportsResourceProperties()) { throw syntax(id, "Resource properties can only be used with Smithy version 2 or later. " - + "Attempted to use resource properties with version `" + modelFile.getVersion() + "`."); + + "Attempted to use resource properties with version `" + modelVersion + "`."); } for (Map.Entry entry : properties.getMembers().entrySet()) { String name = entry.getKey().getValue(); StringNode target = entry.getValue().expectStringNode(); - modelFile.addForwardReference(target.getValue(), targetId -> builder.addProperty(name, targetId)); + addForwardReference(target.getValue(), targetId -> builder.addProperty(name, targetId)); } }); clearPendingDocs(); + operations.accept(operation); } // "//" *(not_newline) @@ -1012,9 +1108,9 @@ private void parseApplyStatement() { } // First, resolve the targeted shape. - modelFile.addForwardReference(name, id -> { + addForwardReference(name, target -> { for (TraitEntry traitEntry : traitsToApply) { - onDeferredTrait(id, traitEntry.traitName, traitEntry.value, traitEntry.isAnnotation); + onDeferredTrait(target, traitEntry.traitName, traitEntry.value, traitEntry.isAnnotation); } }); @@ -1042,15 +1138,15 @@ private void addTraits(ShapeId id, List traits) { * @param isAnnotation Set to true to indicate that the value for the trait was omitted. */ private void onDeferredTrait(ShapeId target, String traitName, Node traitValue, boolean isAnnotation) { - modelFile.addForwardReference(traitName, (id, typeProvider) -> { - modelFile.onTrait(target, id, coerceTraitValue(id, traitValue, isAnnotation, typeProvider)); + addForwardReference(traitName, (traitId, typeProvider) -> { + Node coerced = coerceTraitValue(traitValue, isAnnotation, typeProvider.apply(traitId)); + operations.accept(new LoadOperation.ApplyTrait( + modelVersion, traitValue.getSourceLocation(), expectNamespace(), target, traitId, coerced)); }); } - private Node coerceTraitValue( - ShapeId traitId, Node value, boolean isAnnotation, Function typeProvider) { + private Node coerceTraitValue(Node value, boolean isAnnotation, ShapeType targetType) { if (isAnnotation && value.isNullNode()) { - ShapeType targetType = typeProvider.apply(traitId); if (targetType == null || targetType == ShapeType.STRUCTURE || targetType == ShapeType.MAP) { // The targetType == null condition helps mitigate a confusing // failure mode where a trait isn't defined in the model, but a @@ -1075,10 +1171,6 @@ SourceLocation currentLocation() { return new SourceLocation(filename, line(), column()); } - NumberNode parseNumberNode() { - return parseNumberNode(currentLocation()); - } - NumberNode parseNumberNode(SourceLocation location) { String lexeme = ParserUtils.parseNumber(this); @@ -1127,5 +1219,4 @@ private String peekDebugMessage() { return result.length() == 0 ? "[EOF]" : result.toString(); } - } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlNodeParser.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlNodeParser.java index 9a984c5355f..f5d43ab7656 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlNodeParser.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlNodeParser.java @@ -86,9 +86,9 @@ static Node parseNodeTextWithKeywords(IdlModelParser parser, SourceLocation loca // not be able to be resolved until after the entire model is loaded. Pair> pair = StringNode.createLazyString(text, location); Consumer consumer = pair.right; - parser.modelFile.addForwardReference(text, (id, typeFunction) -> { - if (typeFunction.apply(id) == null) { - parser.modelFile.events().add(ValidationEvent.builder() + parser.addForwardReference(text, (id, typeProvider) -> { + if (typeProvider.apply(id) == null) { + parser.emit(ValidationEvent.builder() .id(SYNTACTIC_SHAPE_ID_TARGET) .severity(Severity.DANGER) .message(String.format("Syntactic shape ID `%s` does not resolve to a valid shape ID: " diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/ImmutablePreludeModelFile.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/ImmutablePreludeModelFile.java deleted file mode 100644 index 3a92dec29ee..00000000000 --- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/ImmutablePreludeModelFile.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.smithy.model.loader; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.function.Function; -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.shapes.ShapeId; -import software.amazon.smithy.model.shapes.ShapeType; -import software.amazon.smithy.model.traits.Trait; -import software.amazon.smithy.model.validation.Severity; -import software.amazon.smithy.model.validation.ValidationEvent; -import software.amazon.smithy.model.validation.Validator; - -/** - * A model file that contains the immutable prelude. Traits cannot be added - * to the prelude outside of the prelude's definition. - * - * @see Prelude#getPreludeModel() - */ -final class ImmutablePreludeModelFile implements ModelFile { - private final Model prelude; - private final List events = new ArrayList<>(); - - ImmutablePreludeModelFile(Model prelude) { - this.prelude = prelude; - } - - @Override - public Version getVersion() { - return Version.VERSION_2_0; - } - - @Override - public String getFilename() { - return Prelude.class.getResource("prelude.smithy").toString(); - } - - @Override - public Set shapeIds() { - return prelude.getShapeIds(); - } - - @Override - public Map metadata() { - return prelude.getMetadata(); - } - - @Override - public TraitContainer resolveShapes(Set ids, Function typeProvider) { - return TraitContainer.EMPTY; - } - - @Override - public CreatedShapes createShapes(TraitContainer resolvedTraits) { - // Create error events for each trait applied outside of the prelude. - Map> invalidTraits = resolvedTraits.getTraitsAppliedToPrelude(); - - for (Map.Entry> entry : invalidTraits.entrySet()) { - for (Map.Entry trait : entry.getValue().entrySet()) { - String message = String.format( - "Cannot apply `%s` to an immutable prelude shape defined in `smithy.api`.", - trait.getKey()); - events.add(ValidationEvent.builder() - .severity(Severity.ERROR) - .id(Validator.MODEL_ERROR) - .sourceLocation(trait.getValue().getSourceLocation()) - .shapeId(entry.getKey()) - .message(message) - .build()); - } - } - - return new CreatedShapes(prelude.toSet()); - } - - @Override - public List events() { - return events; - } - - @Override - public ShapeType getShapeType(ShapeId id) { - return prelude.getShape(id).map(Shape::getType).orElse(null); - } -} diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/LoadOperation.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/LoadOperation.java new file mode 100644 index 00000000000..d712dd00578 --- /dev/null +++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/LoadOperation.java @@ -0,0 +1,252 @@ +/* + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.model.loader; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Function; +import software.amazon.smithy.model.FromSourceLocation; +import software.amazon.smithy.model.SourceLocation; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.shapes.AbstractShapeBuilder; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.shapes.ShapeType; +import software.amazon.smithy.model.shapes.ToShapeId; +import software.amazon.smithy.model.traits.Trait; +import software.amazon.smithy.model.validation.ValidationEvent; + +abstract class LoadOperation implements FromSourceLocation { + + interface Visitor { + void putMetadata(PutMetadata operation); + + void applyTrait(ApplyTrait operation); + + void defineShape(DefineShape shape); + + void forwardReference(ForwardReference operation); + + void event(Event operation); + + void modelVersion(ModelVersion operation); + } + + final Version version; + + LoadOperation(Version version) { + this.version = version; + } + + abstract void accept(Visitor visitor); + + static final class PutMetadata extends LoadOperation { + final String key; + final Node value; + + PutMetadata(Version version, String key, Node value) { + super(version); + this.key = key; + this.value = value; + } + + @Override + public SourceLocation getSourceLocation() { + return value.getSourceLocation(); + } + + @Override + void accept(Visitor visitor) { + visitor.putMetadata(this); + } + } + + static final class ApplyTrait extends LoadOperation { + final String namespace; + final ShapeId target; + final ShapeId trait; + final Node value; + final SourceLocation location; + + ApplyTrait( + Version version, + SourceLocation location, + String namespace, + ShapeId target, + ShapeId trait, + Node value + ) { + super(version); + this.namespace = namespace; + this.target = target; + this.trait = trait; + this.value = value; + this.location = location; + } + + static ApplyTrait from(ShapeId target, Trait trait) { + return new ApplyTrait(Version.UNKNOWN, trait.getSourceLocation(), target.getNamespace(), + target, trait.toShapeId(), trait.toNode()); + } + + @Override + public SourceLocation getSourceLocation() { + return location; + } + + @Override + void accept(Visitor visitor) { + visitor.applyTrait(this); + } + } + + static final class DefineShape extends LoadOperation implements ToShapeId { + + private final AbstractShapeBuilder builder; + private Set dependencies; + private Map members; + private List modifiers; + + DefineShape(Version version, AbstractShapeBuilder builder) { + super(version); + if (builder.getShapeType() == ShapeType.MEMBER) { + throw new UnsupportedOperationException("Members must be added to top-level DefineShape instances"); + } + this.builder = builder; + } + + @Override + void accept(Visitor visitor) { + visitor.defineShape(this); + } + + @Override + public ShapeId toShapeId() { + return builder.getId(); + } + + @Override + public SourceLocation getSourceLocation() { + return builder.getSourceLocation(); + } + + Set dependencies() { + return dependencies == null ? Collections.emptySet() : dependencies; + } + + void addDependency(ShapeId id) { + if (dependencies == null) { + dependencies = new LinkedHashSet<>(); + } + dependencies.add(id); + } + + ShapeType getShapeType() { + return builder.getShapeType(); + } + + AbstractShapeBuilder builder() { + return builder; + } + + void addMember(MemberShape.Builder member) { + if (members == null) { + members = new LinkedHashMap<>(); + } + members.put(member.getId().getMember().get(), member); + } + + boolean hasMember(String memberName) { + return members != null && members.containsKey(memberName); + } + + Map memberBuilders() { + return members == null ? Collections.emptyMap() : members; + } + + List modifiers() { + return modifiers == null ? Collections.emptyList() : modifiers; + } + + void addModifier(ShapeModifier modifier) { + if (modifiers == null) { + modifiers = new ArrayList<>(); + } + modifiers.add(modifier); + } + } + + static final class ForwardReference extends LoadOperation { + final String namespace; + final String name; + private final BiConsumer> consumer; + + ForwardReference(String namespace, String name, BiConsumer> consumer) { + super(Version.UNKNOWN); + this.namespace = namespace; + this.name = name; + this.consumer = consumer; + } + + @Override + void accept(Visitor visitor) { + visitor.forwardReference(this); + } + + void resolve(ShapeId id, Function typeProvider) { + consumer.accept(id, typeProvider); + } + } + + static final class Event extends LoadOperation { + final ValidationEvent event; + + Event(ValidationEvent event) { + super(Version.UNKNOWN); + this.event = event; + } + + @Override + void accept(Visitor visitor) { + visitor.event(this); + } + } + + static final class ModelVersion extends LoadOperation { + final SourceLocation sourceLocation; + + ModelVersion(Version version, SourceLocation sourceLocation) { + super(version); + this.sourceLocation = sourceLocation; + } + + @Override + void accept(Visitor visitor) { + visitor.modelVersion(this); + } + + @Override + public SourceLocation getSourceLocation() { + return sourceLocation; + } + } +} diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/LoadOperationProcessor.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/LoadOperationProcessor.java new file mode 100644 index 00000000000..7dad827665b --- /dev/null +++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/LoadOperationProcessor.java @@ -0,0 +1,179 @@ +/* + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.model.loader; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Queue; +import java.util.function.Consumer; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.SourceLocation; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.shapes.ShapeType; +import software.amazon.smithy.model.traits.TraitFactory; +import software.amazon.smithy.model.validation.Severity; +import software.amazon.smithy.model.validation.ValidationEvent; +import software.amazon.smithy.model.validation.Validator; + +final class LoadOperationProcessor implements Consumer { + + private final List events; + private final MetadataContainer metadata = new MetadataContainer(); + private final LoaderShapeMap shapeMap; + private final LoaderTraitMap traitMap; + private final Queue forwardReferences = new ArrayDeque<>(); + private final LoadOperation.Visitor visitor; + private final Model prelude; + private final Map modelVersions = new HashMap<>(); + + LoadOperationProcessor( + TraitFactory traitFactory, + Model prelude, + boolean allowUnknownTraits, + Consumer validationEventListener + ) { + // Emit events as the come in. + this.events = new ArrayList() { + @Override + public boolean add(ValidationEvent e) { + validationEventListener.accept(e); + return super.add(e); + } + }; + + this.prelude = prelude; + shapeMap = new LoaderShapeMap(prelude, events); + traitMap = new LoaderTraitMap(traitFactory, events, allowUnknownTraits); + + this.visitor = new LoadOperation.Visitor() { + @Override + public void putMetadata(LoadOperation.PutMetadata operation) { + metadata.putMetadata(operation.key, operation.value, events); + } + + @Override + public void applyTrait(LoadOperation.ApplyTrait operation) { + traitMap.add(operation); + shapeMap.moveCreatedShapeToOperations(operation.target, LoadOperationProcessor.this); + } + + @Override + public void defineShape(LoadOperation.DefineShape operation) { + shapeMap.add(operation); + shapeMap.moveCreatedShapeToOperations(operation.toShapeId(), LoadOperationProcessor.this); + } + + @Override + public void forwardReference(LoadOperation.ForwardReference operation) { + forwardReferences.add(operation); + } + + @Override + public void event(LoadOperation.Event operation) { + events.add(operation.event); + } + + @Override + public void modelVersion(LoadOperation.ModelVersion operation) { + // Don't attempt to assign versions based on N/A or "" source locations. + if (!operation.getSourceLocation().equals(SourceLocation.none()) + && !operation.getSourceLocation().getFilename().isEmpty()) { + modelVersions.put(operation.getSourceLocation().getFilename(), operation.version); + } + + if (operation.version.isDeprecated()) { + events.add(ValidationEvent.builder() + .id(Validator.MODEL_DEPRECATION) + .severity(Severity.WARNING) + .message("Smithy IDL " + operation.version + " is deprecated. Please upgrade to 2.0.") + .sourceLocation(operation.getSourceLocation()) + .build()); + } + } + }; + } + + @Override + public void accept(LoadOperation operation) { + operation.accept(visitor); + } + + void putCreatedShape(Shape shape) { + shapeMap.add(shape, this); + } + + Version getShapeVersion(Shape shape) { + SourceLocation location = shape.getSourceLocation(); + if (location.equals(SourceLocation.none())) { + return shapeMap.getShapeVersion(shape.getId()); + } else { + return modelVersions.getOrDefault(location.getFilename(), Version.UNKNOWN); + } + } + + Model buildModel() { + Model.Builder modelBuilder = Model.builder(); + modelBuilder.metadata(metadata.getData()); + resolveForwardReferences(); + traitMap.applyTraitsToNonMixinsInShapeMap(shapeMap); + shapeMap.buildShapesAndClaimMixinTraits(modelBuilder, traitMap::claimTraitsForShape); + traitMap.emitUnclaimedTraits(); + if (prelude != null) { + modelBuilder.addShapes(prelude); + } + return modelBuilder.build(); + } + + List events() { + return events; + } + + private void resolveForwardReferences() { + while (!forwardReferences.isEmpty()) { + LoadOperation.ForwardReference reference = forwardReferences.poll(); + if (reference.namespace == null) { + // Assume smithy.api if there is no namespace. This can happen in metadata and control sections. + ShapeId absolute = ShapeId.fromOptionalNamespace(Prelude.NAMESPACE, reference.name); + reference.resolve(absolute, shapeMap::getShapeType); + } else { + detectAndEmitForwardReference(reference); + } + } + } + + private void detectAndEmitForwardReference(LoadOperation.ForwardReference reference) { + Objects.requireNonNull(reference.namespace); + ShapeId inNamespace = ShapeId.fromOptionalNamespace(reference.namespace, reference.name); + ShapeType inNamespaceType = shapeMap.getShapeType(inNamespace); + + if (inNamespaceType != null) { + reference.resolve(inNamespace, test -> inNamespaceType); + } else { + // Try to find a prelude shape by ID if no ID exists in the namespace with this name. + ShapeId preludeId = ShapeId.fromOptionalNamespace(Prelude.NAMESPACE, reference.name); + if (prelude.getShapeIds().contains(preludeId)) { + reference.resolve(preludeId, test -> prelude.expectShape(test).getType()); + } else { + reference.resolve(inNamespace, test -> null); + } + } + } +} diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/LoaderShapeMap.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/LoaderShapeMap.java new file mode 100644 index 00000000000..e470e00883e --- /dev/null +++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/LoaderShapeMap.java @@ -0,0 +1,420 @@ +/* + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.model.loader; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.StringJoiner; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.logging.Logger; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.SourceException; +import software.amazon.smithy.model.shapes.AbstractShapeBuilder; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.shapes.ShapeType; +import software.amazon.smithy.model.traits.Trait; +import software.amazon.smithy.model.validation.Severity; +import software.amazon.smithy.model.validation.ValidationEvent; +import software.amazon.smithy.model.validation.Validator; + +final class LoaderShapeMap { + private static final Logger LOGGER = Logger.getLogger(LoaderShapeMap.class.getName()); + + private final Map shapes = new HashMap<>(); + private final Map createdShapes = new HashMap<>(); + private final Model preludeShapes; + private final List events; + + LoaderShapeMap(Model prelude, List events) { + this.preludeShapes = prelude; + this.events = events; + } + + boolean isShapePending(ShapeId id) { + // Check for root-level shapes first. + if (containsShapeId(id)) { + return true; + } + + String member = id.getMember().orElse(null); + if (member == null) { + return false; + } + + ShapeId root = id.withoutMember(); + return containsShapeId(root) && shapes.get(root).hasMember(member); + } + + boolean isRootShapeDefined(ShapeId id) { + return containsPreludeShape(id) || containsShapeId(id) || createdShapes.containsKey(id); + } + + private boolean containsPreludeShape(ShapeId id) { + return preludeShapes != null && preludeShapes.getShapeIds().contains(id); + } + + private boolean containsShapeId(ShapeId id) { + return shapes.containsKey(id); + } + + ShapeType getShapeType(ShapeId id) { + if (id.hasMember()) { + // No need to descend into root shapes since members tell us their type in their shape ID. + return ShapeType.MEMBER; + } else if (shapes.containsKey(id)) { + return shapes.get(id).getFirst().getShapeType(); + } else if (createdShapes.containsKey(id)) { + return createdShapes.get(id).getType(); + } else if (containsPreludeShape(id)) { + return preludeShapes.expectShape(id).getType(); + } else { + return null; + } + } + + Version getShapeVersion(ShapeId shape) { + ShapeId noMember = shape.withoutMember(); + if (shapes.containsKey(noMember)) { + return shapes.get(noMember).getFirst().version; + } else { + return Version.UNKNOWN; + } + } + + ShapeWrapper get(ShapeId id) { + ShapeWrapper result = shapes.get(id); + if (result == null) { + throw new IllegalArgumentException("Shape not found when loading the model: " + id); + } + return result; + } + + void add(LoadOperation.DefineShape operation) { + shapes.computeIfAbsent(operation.toShapeId(), id -> new ShapeWrapper()).add(operation); + } + + void add(Shape shape, Consumer processor) { + if (!shape.isMemberShape() && !Prelude.isPreludeShape(shape)) { + createdShapes.put(shape.getId(), shape); + // If the shape has mixins, then if the mixins are updated, we want those changes reflected in the shape. + if (!shape.getMixins().isEmpty()) { + moveCreatedShapeToOperations(shape.getId(), processor); + } + } + } + + // If a shape was added as a created shape, but then something tries to modify it, then convert it to operations. + void moveCreatedShapeToOperations(ShapeId shapeId, Consumer processor) { + if (createdShapes.containsKey(shapeId)) { + Shape shape = createdShapes.remove(shapeId); + // Convert a created shape to a builder and add its members as builders. + AbstractShapeBuilder builder = Shape.shapeToBuilder(shape); + LoadOperation.DefineShape operation = new LoadOperation.DefineShape(Version.UNKNOWN, builder); + // Remove and deconstruct mixins. + for (ShapeId mixin : shape.getMixins()) { + operation.addDependency(mixin); + operation.addModifier(new ApplyMixin(mixin)); + } + builder.clearMixins(); + // Remove traits from the shape and members and send them through the merging logic of loader. + shape.getIntroducedTraits().values() + .forEach(trait -> processor.accept(LoadOperation.ApplyTrait.from(shape.getId(), trait))); + builder.clearTraits(); + // Clear out member mixins and traits, and register the newly created builders. + for (MemberShape member : shape.members()) { + MemberShape.Builder memberBuilder = member.toBuilder(); + member.getIntroducedTraits().values() + .forEach(trait -> processor.accept(LoadOperation.ApplyTrait.from(member.getId(), trait))); + memberBuilder.clearTraits().clearMixins(); + operation.addMember(memberBuilder); + } + add(operation); + } else if (shapeId.hasMember()) { + // If it was a member that was updated, then move it's root shape out of createdShapes. + moveCreatedShapeToOperations(shapeId.withoutMember(), processor); + } + } + + void buildShapesAndClaimMixinTraits( + Model.Builder modelBuilder, + Function> unclaimedTraits + ) { + Function createdShapeMap = id -> modelBuilder.getCurrentShapes().get(id); + + for (Shape shape : createdShapes.values()) { + modelBuilder.addShapes(shape); + } + + for (ShapeId id : sort()) { + if (!createdShapes.containsKey(id)) { + buildIntoModel(shapes.get(id), modelBuilder, unclaimedTraits, createdShapeMap); + } + } + } + + // Build each pending shape in the wrapper and perform conflict resolution. + private void buildIntoModel( + ShapeWrapper wrapper, + Model.Builder builder, + Function> unclaimedTraits, + Function createdShapeMap + ) { + Shape built = null; + for (LoadOperation.DefineShape shape : wrapper) { + if (validateShapeVersion(shape)) { + Shape newShape = buildShape(shape, unclaimedTraits, createdShapeMap); + if (newShape != null) { + if (validateConflicts(shape.toShapeId(), newShape, built)) { + built = newShape; + } + } + } + } + if (built != null) { + builder.addShape(built); + } + } + + private List sort() { + TopologicalShapeSort sorter = new TopologicalShapeSort(createdShapes.size() + shapes.size()); + + for (Shape shape : createdShapes.values()) { + sorter.enqueue(shape.getId(), Collections.emptyList()); + } + + for (Map.Entry entry : shapes.entrySet()) { + sorter.enqueue(entry.getKey(), entry.getValue().dependencies()); + } + + try { + return sorter.dequeueSortedShapes(); + } catch (TopologicalShapeSort.CycleException e) { + // Emit useful, per shape, error messages. + for (ShapeId unresolved : e.getUnresolved()) { + for (LoadOperation.DefineShape shape : get(unresolved)) { + emitUnresolved(shape, e.getUnresolved(), e.getResolved()); + } + } + return Collections.emptyList(); + } + } + + private void emitUnresolved(LoadOperation.DefineShape shape, Set unresolved, List resolved) { + List notFoundShapes = new ArrayList<>(); + List missingTransitive = new ArrayList<>(); + List cycles = new ArrayList<>(); + + for (ShapeId id : shape.dependencies()) { + if (!unresolved.contains(id)) { + notFoundShapes.add(id); + } else if (anyMissingTransitiveDependencies(id, resolved, unresolved, new HashSet<>())) { + missingTransitive.add(id); + } else { + cycles.add(id); + } + } + + StringJoiner message = new StringJoiner(" "); + message.add("Unable to resolve mixins;"); + + if (!notFoundShapes.isEmpty()) { + message.add("attempted to mixin shapes that are not in the model: " + notFoundShapes); + } + + if (!missingTransitive.isEmpty()) { + message.add("unable to resolve due to missing transitive mixins: " + missingTransitive); + } + + if (!cycles.isEmpty()) { + message.add("cycles detected between this shape and " + cycles); + } + + events.add(ValidationEvent.builder() + .id(Validator.MODEL_ERROR) + .severity(Severity.ERROR) + .shapeId(shape.toShapeId()) + .sourceLocation(shape) + .message(message.toString()) + .build()); + } + + private boolean anyMissingTransitiveDependencies( + ShapeId current, + List resolved, + Set unresolved, + Set visited + ) { + if (resolved.contains(current)) { + return false; + } else if (!unresolved.contains(current)) { + return true; + } else if (visited.contains(current)) { + visited.remove(current); + return false; + } + + visited.add(current); + for (ShapeId next : get(current).dependencies()) { + if (anyMissingTransitiveDependencies(next, resolved, unresolved, visited)) { + return true; + } + } + + return false; + } + + private boolean validateShapeVersion(LoadOperation.DefineShape operation) { + if (!operation.version.isShapeTypeSupported(operation.getShapeType())) { + events.add(ValidationEvent.builder() + .severity(Severity.ERROR) + .id(Validator.MODEL_ERROR) + .shapeId(operation.toShapeId()) + .sourceLocation(operation) + .message(String.format( + "%s shapes cannot be used in Smithy version " + operation.version, + operation.getShapeType())) + .build()); + return false; + } + return true; + } + + private boolean validateConflicts(ShapeId id, Shape built, Shape previous) { + if (previous != null && built != null) { + if (!previous.equals(built)) { + events.add(LoaderUtils.onShapeConflict(id, built.getSourceLocation(), + previous.getSourceLocation())); + return false; + } else if (!LoaderUtils.isSameLocation(built, previous)) { + LOGGER.warning(() -> "Ignoring duplicate but equivalent shape definition: " + id + + " defined at " + built.getSourceLocation() + " and " + + previous.getSourceLocation()); + } + } + return true; + } + + private Shape buildShape( + LoadOperation.DefineShape defineShape, + Function> traitClaimer, + Function createdShapeMap + ) { + try { + AbstractShapeBuilder builder = defineShape.builder(); + for (MemberShape.Builder memberBuilder : defineShape.memberBuilders().values()) { + for (ShapeModifier modifier : defineShape.modifiers()) { + modifier.modifyMember(builder, memberBuilder, traitClaimer, createdShapeMap); + } + MemberShape member = buildMember(memberBuilder); + if (member != null) { + builder.addMember(member); + } + } + + for (ShapeModifier modifier : defineShape.modifiers()) { + modifier.modifyShape(builder, defineShape.memberBuilders(), traitClaimer, createdShapeMap); + events.addAll(modifier.getEvents()); + } + + return builder.build(); + } catch (SourceException e) { + events.add(ValidationEvent.fromSourceException(e, "", defineShape.toShapeId())); + return null; + } + } + + private MemberShape buildMember(MemberShape.Builder builder) { + try { + return builder.build(); + } catch (IllegalStateException e) { + if (builder.getTarget() == null) { + events.add(ValidationEvent.builder() + .severity(Severity.ERROR) + .id(Validator.MODEL_ERROR) + .shapeId(builder.getId()) + .sourceLocation(builder) + .message("Member target was elided, but no bound resource or mixin contained a matching " + + "identifier or member name.") + .build()); + return null; + } + throw e; + } catch (SourceException e) { + events.add(ValidationEvent.fromSourceException(e, "", builder.getId())); + return null; + } + } + + // Aggregates shapes with the same ID before LoaderShapeMap later de-conflicts them as they're built. + static final class ShapeWrapper implements Iterable { + private final List shapes = new ArrayList<>(1); + + @Override + public Iterator iterator() { + return shapes.iterator(); + } + + LoadOperation.DefineShape getFirst() { + return shapes.get(0); + } + + void add(LoadOperation.DefineShape shape) { + shapes.add(shape); + } + + boolean hasMember(String memberName) { + for (LoadOperation.DefineShape shape : this) { + if (shape.hasMember(memberName)) { + return true; + } + } + return false; + } + + Set dependencies() { + // Dependencies have to be computed each time because the deps on a shape can change. + if (shapes.size() == 1) { + return getFirst().dependencies(); + } else if (!hasDependencies()) { + return Collections.emptySet(); + } else { + Set dependencies = new HashSet<>(); + for (LoadOperation.DefineShape shape : shapes) { + dependencies.addAll(shape.dependencies()); + } + return dependencies; + } + } + + private boolean hasDependencies() { + for (LoadOperation.DefineShape shape : shapes) { + if (!shape.dependencies().isEmpty()) { + return true; + } + } + return false; + } + } +} diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/LoaderTraitMap.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/LoaderTraitMap.java new file mode 100644 index 00000000000..b75cecd76be --- /dev/null +++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/LoaderTraitMap.java @@ -0,0 +1,227 @@ +/* + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.model.loader; + +import static java.lang.String.format; + +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; +import software.amazon.smithy.model.SourceException; +import software.amazon.smithy.model.SourceLocation; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.shapes.AbstractShapeBuilder; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.DynamicTrait; +import software.amazon.smithy.model.traits.Trait; +import software.amazon.smithy.model.traits.TraitFactory; +import software.amazon.smithy.model.validation.Severity; +import software.amazon.smithy.model.validation.ValidationEvent; +import software.amazon.smithy.model.validation.Validator; + +final class LoaderTraitMap { + + private static final Logger LOGGER = Logger.getLogger(LoaderTraitMap.class.getName()); + + private final TraitFactory traitFactory; + private final Map> traits = new HashMap<>(); + private final List events; + private final boolean allowUnknownTraits; + private final Map> unclaimed = new HashMap<>(); + + LoaderTraitMap(TraitFactory traitFactory, List events, boolean allowUnknownTraits) { + this.traitFactory = traitFactory; + this.events = events; + this.allowUnknownTraits = allowUnknownTraits; + } + + void applyTraitsToNonMixinsInShapeMap(LoaderShapeMap shapeMap) { + for (Map.Entry> entry : traits.entrySet()) { + ShapeId target = entry.getKey(); + ShapeId root = target.withoutMember(); + + // Check if the actual shape (or member) is found, but grab the member-less shape from the shape map. + // Only pending shapes are checked here. If a trait was added to a built shape, then LoadOperationProcessor + // will have already converted the built shape into a pending shape. + boolean found = shapeMap.isShapePending(target); + Iterable rootShapes = found + ? shapeMap.get(root) + : Collections::emptyIterator; + + for (Map.Entry traitEntry : entry.getValue().entrySet()) { + ShapeId traitId = traitEntry.getKey(); + Node traitNode = traitEntry.getValue(); + Trait created = createTrait(target, traitId, traitNode); + validateTraitIsKnown(target, traitId, created, traitNode.getSourceLocation(), shapeMap); + + if (target.hasMember()) { + // Apply the trait to a member by reaching into the members of each LoadOperation.DefineShape. + String memberName = target.getMember().get(); + boolean foundMember = false; + for (LoadOperation.DefineShape shape : rootShapes) { + if (shape.hasMember(memberName)) { + foundMember = true; + shape.memberBuilders().get(memberName).getAllTraits(); + applyTraitsToShape(shape.memberBuilders().get(memberName), created); + } + } + // If the member wasn't found, then it might be a mixin member that is synthesized later. + if (!foundMember) { + unclaimed.computeIfAbsent(target.withMember(memberName), id -> new LinkedHashMap<>()) + .put(traitId, created); + } + } else if (found) { + // Apply the trait to each shape contained in the shape map for the given target. + for (LoadOperation.DefineShape shape : rootShapes) { + applyTraitsToShape(shape.builder(), created); + } + } else { + unclaimed.computeIfAbsent(target, id -> new LinkedHashMap<>()).put(traitId, created); + } + } + } + } + + private Trait createTrait(ShapeId target, ShapeId traitId, Node traitValue) { + try { + return traitFactory.createTrait(traitId, target, traitValue) + .orElseGet(() -> new DynamicTrait(traitId, traitValue)); + } catch (SourceException e) { + String message = format("Error creating trait `%s`: ", Trait.getIdiomaticTraitName(traitId)); + events.add(ValidationEvent.fromSourceException(e, message, target)); + return null; + } + } + + private void validateTraitIsKnown(ShapeId target, ShapeId traitId, Trait trait, + SourceLocation sourceLocation, LoaderShapeMap shapeMap) { + if (!shapeMap.isRootShapeDefined(traitId) && (trait == null || !trait.isSynthetic())) { + Severity severity = allowUnknownTraits ? Severity.WARNING : Severity.ERROR; + events.add(ValidationEvent.builder() + .id(Validator.MODEL_ERROR) + .severity(severity) + .sourceLocation(sourceLocation) + .shapeId(target) + .message(String.format("Unable to resolve trait `%s`. If this is a custom trait, then it must be " + + "defined before it can be used in a model.", traitId)) + .build()); + } + } + + private void applyTraitsToShape(AbstractShapeBuilder shape, Trait trait) { + if (trait != null) { + shape.addTrait(trait); + } + } + + // Traits can be applied to synthesized members inherited from mixins. Applying these traits is deferred until + // the point in which mixin members are synthesized into shapes. + Map claimTraitsForShape(ShapeId id) { + return unclaimed.containsKey(id) ? unclaimed.remove(id) : Collections.emptyMap(); + } + + // Emit events if any traits were applied to shapes that weren't found in the model. + void emitUnclaimedTraits() { + for (Map.Entry> entry : unclaimed.entrySet()) { + for (Map.Entry traitEntry : entry.getValue().entrySet()) { + events.add(ValidationEvent.builder() + .id(Validator.MODEL_ERROR) + .severity(Severity.ERROR) + .sourceLocation(traitEntry.getValue()) + .message(String.format("Trait `%s` applied to unknown shape `%s`", + Trait.getIdiomaticTraitName(traitEntry.getKey()), entry.getKey())) + .build()); + } + } + } + + void add(LoadOperation.ApplyTrait operation) { + if (validateTraitVersion(operation)) { + if (isAppliedToPreludeOutsidePrelude(operation)) { + String message = String.format( + "Cannot apply `%s` to an immutable prelude shape defined in `smithy.api`.", + operation.trait); + events.add(ValidationEvent.builder() + .severity(Severity.ERROR) + .id(Validator.MODEL_ERROR) + .sourceLocation(operation) + .shapeId(operation.target) + .message(message) + .build()); + } else { + Map current = traits.computeIfAbsent(operation.target, id -> new LinkedHashMap<>()); + Node previous = current.get(operation.trait); + current.put(operation.trait, mergeTraits(operation.target, operation.trait, previous, operation.value)); + } + } + } + + private boolean validateTraitVersion(LoadOperation.ApplyTrait operation) { + try { + operation.version.validateVersionedTrait(operation.target, operation.trait, operation.value); + return true; + } catch (SourceException e) { + events.add(ValidationEvent.fromSourceException(e)); + return false; + } + } + + private boolean isAppliedToPreludeOutsidePrelude(LoadOperation.ApplyTrait operation) { + return !operation.namespace.equals(Prelude.NAMESPACE) + && operation.target.getNamespace().equals(Prelude.NAMESPACE); + } + + private Node mergeTraits(ShapeId target, ShapeId traitId, Node previous, Node updated) { + if (previous == null) { + return updated; + } + + if (LoaderUtils.isSameLocation(previous, updated) && previous.equals(updated)) { + // The assumption here is that if the trait value is exactly the + // same and from the same location, then the same model file was + // included more than once in a way that side-steps file and URL + // de-duplication. For example, this can occur when a Model is assembled + // through a ModelAssembler using model discovery, then the Model is + // added to a subsequent ModelAssembler, and then model discovery is + // performed again using the same classpath. + LOGGER.finest(() -> String.format("Ignoring duplicate %s trait value on %s at same exact location", + traitId, target)); + return previous; + } + + if (previous.isArrayNode() && updated.isArrayNode()) { + // You can merge trait arrays. + return previous.expectArrayNode().merge(updated.expectArrayNode()); + } else if (previous.equals(updated)) { + LOGGER.fine(() -> String.format("Ignoring duplicate %s trait value on %s", traitId, target)); + return previous; + } else { + events.add(ValidationEvent.builder() + .id(Validator.MODEL_ERROR) + .severity(Severity.ERROR) + .sourceLocation(updated) + .shapeId(target) + .message(String.format("Conflicting `%s` trait found on shape `%s`. The previous trait was " + + "defined at `%s`, and a conflicting trait was defined at `%s`.", + traitId, target, previous.getSourceLocation(), updated.getSourceLocation())) + .build()); + return previous; + } + } +} diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/LoaderUtils.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/LoaderUtils.java index b83d361674f..45d5f5242c5 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/LoaderUtils.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/LoaderUtils.java @@ -17,6 +17,7 @@ import java.util.Collection; import java.util.List; +import java.util.Optional; import software.amazon.smithy.model.FromSourceLocation; import software.amazon.smithy.model.SourceLocation; import software.amazon.smithy.model.node.ExpectationNotMetException; @@ -36,21 +37,22 @@ private LoaderUtils() {} * @param node Node to check. * @param shape Shape to associate with the error. * @param properties Properties to allow. + * @return Returns an optionally created event. */ - static void checkForAdditionalProperties( + static Optional checkForAdditionalProperties( ObjectNode node, - ShapeId shape, Collection properties, - List events + ShapeId shape, Collection properties ) { try { node.expectNoAdditionalProperties(properties); + return Optional.empty(); } catch (ExpectationNotMetException e) { ValidationEvent event = ValidationEvent.fromSourceException(e) .toBuilder() .shapeId(shape) .severity(Severity.WARNING) .build(); - events.add(event); + return Optional.of(event); } } @@ -100,13 +102,4 @@ static boolean containsErrorEvents(List events) { } return false; } - - static ValidationEvent onDeprecatedIdlVersion(Version version, String filename) { - return ValidationEvent.builder() - .id(Validator.MODEL_ERROR) - .severity(Severity.WARNING) - .message("Smithy IDL " + version + " is deprecated. Please upgrade to Smithy IDL 2.0.") - .sourceLocation(new SourceLocation(filename, 1, 1)) - .build(); - } } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/MetadataContainer.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/MetadataContainer.java index 7b1049dac15..acb33a3fa44 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/MetadataContainer.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/MetadataContainer.java @@ -35,14 +35,6 @@ final class MetadataContainer { private static final Logger LOGGER = Logger.getLogger(MetadataContainer.class.getName()); private final Map data = new LinkedHashMap<>(); - private final List events; - - /** - * @param events Mutable, by-reference list of validation events. - */ - MetadataContainer(List events) { - this.events = events; - } /** * Put metadata into the map. @@ -55,7 +47,7 @@ final class MetadataContainer { * @param key Metadata key to set. * @param value Value to set. */ - void putMetadata(String key, Node value) { + void putMetadata(String key, Node value, List events) { Node previous = data.putIfAbsent(key, value); if (previous == null) { 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 d3b030a8709..cf9a2db2d5d 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 @@ -40,11 +40,9 @@ import java.util.stream.Stream; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.SourceException; -import software.amazon.smithy.model.SourceLocation; import software.amazon.smithy.model.node.Node; import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.model.shapes.ShapeId; -import software.amazon.smithy.model.shapes.ShapeType; import software.amazon.smithy.model.traits.Trait; import software.amazon.smithy.model.traits.TraitFactory; import software.amazon.smithy.model.validation.Severity; @@ -52,7 +50,6 @@ import software.amazon.smithy.model.validation.ValidationEvent; import software.amazon.smithy.model.validation.Validator; import software.amazon.smithy.model.validation.ValidatorFactory; -import software.amazon.smithy.utils.FunctionalUtils; import software.amazon.smithy.utils.Pair; /** @@ -500,43 +497,6 @@ public ModelAssembler validationEventListener(Consumer eventLis /** * Assembles the model and returns the validated result. * - *

Implementation notes

- * - *

Assembling models is a multi-step process that revolves around - * {@link ModelFile}s. ModelFiles are essentially files that contain - * localized definitions of shapes and metadata. Some model files use - * forward references and can't be fully resolved until all other model - * files have been loaded. To achieve this, the assembler first creates a - * model file that represents manually added shapes, traits, validators, - * etc., and then parses each given import. Parsing an import is used to - * create zero or more ModelFiles by parsing .json, .smithy, and - * .jar files. - * - *

After the parsing phase, each model file returns the metadata - * defined in the file using {@link ModelFile#metadata()} and the set of - * shape IDs that were defined in the file using {@link ModelFile#shapeIds()}. - * The metadata across each file is merged together using the rules - * defined in the Smithy specification. - * - *

Next, the assembler calls {@link ModelFile#resolveShapes} on each - * ModelFile which resolves any forward references, creates traits, and - * returns all of the traits created in the file. The assembler passes in - * the set of found shapes IDs along with a function that can be used to - * get the {@link ShapeType} of any defined shape (this is used to coerce - * annotation trait values into the appropriate type for a trait). - * The assembler aggregates and merges the traits applied across all model - * files using the merge rules defined in the Smithy specification. - * - *

Next, the assembler invokes {@link ModelFile#createShapes}, passing - * in all of the traits defined across every model file. This method - * causes a ModelFile to apply these traits to any shape in the ModelFile, - * build each shape, and return the built shapes. The assembler then - * aggregates all of the created traits, performs conflict resolution, - * and builds a {@link Model} from the shapes and loaded metadata. A shape - * is allowed to be defined in multiple model files if the conflicting - * shapes are equivalent after all traits have been applied to both - * shapes. - * * @return Returns the validated result that optionally contains a Model * and validation events. */ @@ -545,122 +505,84 @@ public ValidatedResult assemble() { traitFactory = LazyTraitFactoryHolder.INSTANCE; } - List files = createModelFiles(); - // Create "model files" for the prelude, manually added shapes, imports, etc. - CompositeModelFile composite = new CompositeModelFile(traitFactory, files); - - try { - TraitContainer traits = composite.resolveShapes(composite.shapeIds(), composite::getShapeType); - Model model = Model.builder() - .metadata(composite.metadata()) - .addShapes(composite.createShapes(traits).getCreatedShapes()) - .build(); + Model prelude = disablePrelude ? null : Prelude.getPreludeModel(); + LoadOperationProcessor processor = new LoadOperationProcessor( + traitFactory, prelude, areUnknownTraitsAllowed(), validationEventListener); + List events = processor.events(); - List compositeEvents = composite.events(); + // Register manually added metadata. + addMetadataToProcessor(metadata, processor); - // Always perform trait validation before 1.0 -> 2.0 transforms. - validateTraits(model.getShapeIds(), traits, compositeEvents); + // Register manually added shapes. Skip members because they are part of aggregate shapes. + shapes.forEach(processor::putCreatedShape); - // If ERROR validation events occur while loading, then performing more - // granular semantic validation will only obscure the root cause of errors. - if (LoaderUtils.containsErrorEvents(compositeEvents)) { - return returnOnlyErrors(model, compositeEvents); - } + // Register manually added traits. + for (Pair entry : pendingTraits) { + processor.accept(LoadOperation.ApplyTrait.from(entry.getKey(), entry.getValue())); + } - // Do the 1.0 -> 2.0 transform before full model validation. - ValidatedResult transformedModel = upgradeModel(model, compositeEvents, files); + // Register manually added Models. + for (Model model : mergeModels) { + // Add manually added metadata from the Model. + addMetadataToProcessor(model.getMetadata(), processor); + model.shapes().forEach(processor::putCreatedShape); + } - if (disableValidation || LoaderUtils.containsErrorEvents(transformedModel.getValidationEvents())) { - // Don't continue to validate the model if the upgrade raised ERROR events. - return transformedModel; + // Load parsed AST nodes and merge them into the processor. + for (Node node : documentNodes) { + try { + ModelLoader.loadParsedNode(node, processor); + } catch (SourceException e) { + processor.accept(new LoadOperation.Event(ValidationEvent.fromSourceException(e))); } - - return validate(transformedModel.getResult().get(), transformedModel.getValidationEvents()); - } catch (SourceException e) { - List events = new ArrayList<>(); - events.add(ValidationEvent.fromSourceException(e)); - events.addAll(composite.events()); - return ValidatedResult.fromErrors(events); } - } - - private ValidatedResult returnOnlyErrors(Model model, List events) { - return new ValidatedResult<>(model, events.stream() - .filter(event -> event.getSeverity() == Severity.ERROR) - .peek(validationEventListener) - .collect(Collectors.toList())); - } - private ValidatedResult upgradeModel(Model model, List events, List files) { - // Create a mapping of filename -> version so that the SourceLocation of each shape and - // trait can be tracked back to a version. Note that the map might contain files that start with - // "file:/", "jar:", etc, and others might not. That doesn't matter because shapes are bound to - // files, and files are defined within a version, so normalizing filenames here would have no effect. - Map modelVersions = new HashMap<>(files.size()); - for (ModelFile file : files) { - modelVersions.put(file.getFilename(), file.getVersion()); - // Warn when any model file is 1.0 and it isn't the default source location. We assume 1.0 by default. - if (file.getVersion() == Version.VERSION_1_0 - && !file.getFilename().equals(SourceLocation.none().getFilename())) { - events.add(LoaderUtils.onDeprecatedIdlVersion(file.getVersion(), file.getFilename())); + // Load model files into the processor. + for (Map.Entry> entry : inputStreamModels.entrySet()) { + try { + ModelLoader.load(traitFactory, properties, entry.getKey(), processor, entry.getValue()); + } catch (SourceException e) { + processor.accept(new LoadOperation.Event(ValidationEvent.fromSourceException(e))); } } - return new ModelUpgrader(model, events, modelVersions).transform(); - } - - private List createModelFiles() { - List modelFiles = new ArrayList<>(); + Model processedModel = processor.buildModel(); - if (!disablePrelude) { - modelFiles.add(new ImmutablePreludeModelFile(Prelude.getPreludeModel())); + // If ERROR validation events occur while loading, then performing more + // granular semantic validation will only obscure the root cause of errors. + if (LoaderUtils.containsErrorEvents(events)) { + return returnOnlyErrors(processedModel, events); } - // A modelFile is created for the assembler to capture anything that was manually added. - FullyResolvedModelFile assemblerModelFile = FullyResolvedModelFile.fromShapes(traitFactory, shapes); - - modelFiles.add(assemblerModelFile); - metadata.forEach(assemblerModelFile::putMetadata); - for (Pair pendingTrait : pendingTraits) { - assemblerModelFile.onTrait(pendingTrait.left, pendingTrait.right); - } + // Do the 1.0 -> 2.0 transform before full-model validation. + ValidatedResult transformed = new ModelUpgrader(processedModel, events, processor::getShapeVersion) + .transform(); - // Merge in fully-built models into the assembler. - for (Model model : mergeModels) { - // Fully resolved models typically contain a prelude. This ensures that the prelude is not included - // in the assembler since it would cause pointless conflicts. - List nonPrelude = model.shapes() - .filter(FunctionalUtils.not(Prelude::isPreludeShape)) - .collect(Collectors.toList()); - FullyResolvedModelFile resolvedFile = FullyResolvedModelFile.fromShapes(traitFactory, nonPrelude); - model.getMetadata().forEach(resolvedFile::putMetadata); - modelFiles.add(resolvedFile); + if (disableValidation + || !transformed.getResult().isPresent() + || LoaderUtils.containsErrorEvents(transformed.getValidationEvents())) { + // Don't continue to validate the model if the upgrade raised ERROR events. + return transformed; } - // Load parsed AST nodes and merge them into the assembler. - for (Node node : documentNodes) { - try { - modelFiles.add(ModelLoader.loadParsedNode(traitFactory, node)); - } catch (SourceException e) { - assemblerModelFile.events().add(ValidationEvent.fromSourceException(e)); - } + try { + return validate(transformed.getResult().get(), transformed.getValidationEvents()); + } catch (SourceException e) { + events.add(ValidationEvent.fromSourceException(e)); + return new ValidatedResult<>(transformed.getResult().get(), events); } + } - // Load model files and merge them into the assembler. - for (Map.Entry> entry : inputStreamModels.entrySet()) { - try { - List loaded = ModelLoader.load(traitFactory, properties, entry.getKey(), entry.getValue()); - if (loaded.isEmpty()) { - LOGGER.warning(() -> "No ModelLoader was able to load " + entry.getKey()); - } else { - modelFiles.addAll(loaded); - } - } catch (SourceException e) { - assemblerModelFile.events().add(ValidationEvent.fromSourceException(e)); - } + private void addMetadataToProcessor(Map metadataMap, LoadOperationProcessor processor) { + for (Map.Entry entry : metadataMap.entrySet()) { + processor.accept(new LoadOperation.PutMetadata(Version.UNKNOWN, entry.getKey(), entry.getValue())); } + } - return modelFiles; + private ValidatedResult returnOnlyErrors(Model model, List events) { + return new ValidatedResult<>(model, events.stream() + .filter(event -> event.getSeverity() == Severity.ERROR) + .collect(Collectors.toList())); } private ValidatedResult validate(Model model, List events) { @@ -677,37 +599,6 @@ private ValidatedResult validate(Model model, List event return new ValidatedResult<>(model, mergedEvents); } - private void validateTraits(Set ids, TraitContainer resolvedTraits, List events) { - Severity severity = areUnknownTraitsAllowed() ? Severity.WARNING : Severity.ERROR; - for (Map.Entry> entry : resolvedTraits.traits().entrySet()) { - ShapeId target = entry.getKey(); - for (Trait trait : entry.getValue().values()) { - // Find trait values that weren't defined, and ignore synthetic traits. - if (!trait.isSynthetic() && !ids.contains(trait.toShapeId())) { - events.add(ValidationEvent.builder() - .id(Validator.MODEL_ERROR) - .severity(severity) - .sourceLocation(trait) - .shapeId(target) - .message(String.format( - "Unable to resolve trait `%s`. If this is a custom trait, then it must be " - + "defined before it can be used in a model.", trait.toShapeId())) - .build()); - } - // Find traits applied to shapes that don't exist. - if (!ids.contains(target)) { - events.add(ValidationEvent.builder() - .id(Validator.MODEL_ERROR) - .severity(Severity.ERROR) - .sourceLocation(trait) - .message(String.format("Trait `%s` applied to unknown shape `%s`", - Trait.getIdiomaticTraitName(trait.toShapeId()), target)) - .build()); - } - } - } - } - private boolean areUnknownTraitsAllowed() { Object allowUnknown = properties.get(ModelAssembler.ALLOW_UNKNOWN_TRAITS); return allowUnknown != null && (boolean) allowUnknown; diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelFile.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelFile.java deleted file mode 100644 index febb36be705..00000000000 --- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelFile.java +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.smithy.model.loader; - -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.function.Function; -import software.amazon.smithy.model.node.Node; -import software.amazon.smithy.model.shapes.Shape; -import software.amazon.smithy.model.shapes.ShapeId; -import software.amazon.smithy.model.shapes.ShapeType; -import software.amazon.smithy.model.validation.ValidationEvent; - -/** - * Represents a model file as defined in the Smithy specification. - * - *

A model file is used as a self-contained scope of shapes and - * metadata. Model files are created in isolation, then merged by - * a {@link ModelAssembler}. - */ -interface ModelFile { - - /** - * Get the filename of the model file. - * - * @return Returns the filename of the model file. - */ - String getFilename(); - - /** - * Gets the Smithy version number used in the file. - * - * @return Returns the version number. - */ - Version getVersion(); - - /** - * Gets the shape IDs that are defined in this ModelFile. - * - *

This is called before any other method in the ModelFile. - * - * @return Returns the shape IDs defined in this file. - */ - Set shapeIds(); - - /** - * Gets the {@link ShapeType} of a shape by ID. - * - *

This is used, for example, to coerce annotation traits into the - * appropriate type when parsing trait node values. - * - * @param id Shape ID to check. - * @return Returns the {@link ShapeType} if known, or {@code null} if not found. - */ - ShapeType getShapeType(ShapeId id); - - /** - * Get the metadata defined in the ModelFile. - * - * @return Returns the defined, non-null metadata. - */ - Map metadata(); - - /** - * Resolves any forward references and returns all of the traits that were - * applied to shapes in this ModelFile. - * - * @param ids All of the shape IDs found across all ModelFiles being assembled. - * @param typeProvider A function that can return type information about shapes. - * @return Returns a container of traits to apply to shapes. - */ - TraitContainer resolveShapes(Set ids, Function typeProvider); - - /** - * Finalizes and creates shapes in the ModelFile. - * - *

This is called after {@link #resolveShapes}. - * - * @param resolvedTraits Traits to apply to the shapes in the ModelFile. - * @return Returns the created shapes. - */ - CreatedShapes createShapes(TraitContainer resolvedTraits); - - /** - * Gets a mutable list of {@link ValidationEvent} objects encountered when - * loading this ModelFile. - * - * @return Returns the list of events. - */ - List events(); - - /** - * Return value of creating shapes from a {@link ModelFile}. - */ - final class CreatedShapes { - - private final Collection shapes; - private final List pending; - - CreatedShapes(Collection shapes, List pending) { - this.shapes = shapes; - this.pending = pending; - } - - CreatedShapes(Collection shapes) { - this(shapes, Collections.emptyList()); - } - - /** - * Gets the shapes that were created. - * - * @return Returns created shapes. - */ - Collection getCreatedShapes() { - return shapes; - } - - /** - * Gets the shapes that are pending other shapes to resolve as mixins. - * - * @return Returns the pending shapes. - */ - List getPendingShapes() { - return pending; - } - } -} diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelLoader.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelLoader.java index 7ee2797bd92..47e7d4fef7f 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelLoader.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelLoader.java @@ -19,10 +19,8 @@ import java.io.InputStream; import java.net.URL; import java.net.URLConnection; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; import java.util.Map; +import java.util.function.Consumer; import java.util.function.Supplier; import java.util.logging.Logger; import software.amazon.smithy.model.SourceException; @@ -32,7 +30,6 @@ import software.amazon.smithy.model.node.StringNode; import software.amazon.smithy.model.traits.TraitFactory; import software.amazon.smithy.utils.IoUtils; -import software.amazon.smithy.utils.ListUtils; /** * Used to load Smithy models from .json, .smithy, and .jar files. @@ -40,12 +37,11 @@ final class ModelLoader { private static final Logger LOGGER = Logger.getLogger(ModelLoader.class.getName()); - private static final String SMITHY = "smithy"; private ModelLoader() {} /** - * Loads the contents of a model into a {@code ModelFile}. + * Parses models and pushes {@link LoadOperation}s to the given consumer. * *

The format contained in the supplied {@code InputStream} is * determined based on the file extension in the provided @@ -54,29 +50,30 @@ private ModelLoader() {} * @param traitFactory Factory used to create traits. * @param properties Bag of loading properties. * @param filename Filename to assign to the model. + * @param operationConsumer Where loader operations are published. * @param contentSupplier The supplier that provides an InputStream. The * supplied {@code InputStream} is automatically closed when the loader * has finished reading from it. - * @return Returns the {@code ModelFile}s if the model could be loaded, or an empty list. * @throws SourceException if there is an error reading from the contents. */ - static List load( + static void load( TraitFactory traitFactory, Map properties, String filename, + Consumer operationConsumer, Supplier contentSupplier ) { try (InputStream inputStream = contentSupplier.get()) { if (filename.endsWith(".smithy")) { String contents = IoUtils.toUtf8String(inputStream); - return ListUtils.of(new IdlModelParser(traitFactory, filename, contents).parse()); + new IdlModelParser(filename, contents).parse(operationConsumer); } else if (filename.endsWith(".jar")) { - return loadJar(traitFactory, properties, filename); + loadJar(traitFactory, properties, filename, operationConsumer); } else if (filename.endsWith(".json") || filename.equals(SourceLocation.NONE.getFilename())) { // Assume it's JSON if there's a N/A filename. - return ListUtils.of(loadParsedNode(traitFactory, Node.parse(inputStream, filename))); + loadParsedNode(Node.parse(inputStream, filename), operationConsumer); } else { - return Collections.emptyList(); + LOGGER.warning(() -> "No ModelLoader was able to load " + filename); } } catch (IOException e) { throw new ModelImportException("Error loading " + filename + ": " + e.getMessage(), e); @@ -89,22 +86,26 @@ static List load( // Smithy JSON AST format. // // This loader supports version 1.0 and 2.0. Support for 0.5 and 0.4 was removed in 0.10. - static ModelFile loadParsedNode(TraitFactory traitFactory, Node node) { + static void loadParsedNode(Node node, Consumer operationConsumer) { ObjectNode model = node.expectObjectNode("Smithy documents must be an object. Found {type}."); - StringNode versionNode = model.expectStringMember(SMITHY); + StringNode versionNode = model.expectStringMember("smithy"); Version version = Version.fromString(versionNode.getValue()); if (version != null) { - return AstModelLoader.INSTANCE.load(version, traitFactory, model); + new AstModelLoader(version, model).parse(operationConsumer); + } else { + throw new ModelSyntaxException("Unsupported Smithy version number: " + versionNode.getValue(), versionNode); } - - throw new ModelSyntaxException("Unsupported Smithy version number: " + versionNode.getValue(), versionNode); } // Allows importing JAR files by discovering models inside of a JAR file. // This is similar to model discovery, but done using an explicit import. - private static List loadJar(TraitFactory traitFactory, Map properties, String filename) { - List modelFiles = new ArrayList<>(); + private static void loadJar( + TraitFactory traitFactory, + Map properties, + String filename, + Consumer operationConsumer + ) { URL manifestUrl = ModelDiscovery.createSmithyJarManifestUrl(filename); LOGGER.fine(() -> "Loading Smithy model imports from JAR: " + manifestUrl); @@ -116,19 +117,17 @@ private static List loadJar(TraitFactory traitFactory, Map { + load(traitFactory, properties, model.toExternalForm(), operationConsumer, () -> { try { return connection.getInputStream(); } catch (IOException e) { throw throwIoJarException(model, e); } - })); + }); } catch (IOException e) { throw throwIoJarException(model, e); } } - - return modelFiles; } private static ModelImportException throwIoJarException(URL model, Throwable e) { diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelUpgrader.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelUpgrader.java index adbf7e02d98..853ec34ed24 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelUpgrader.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelUpgrader.java @@ -23,6 +23,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.function.Function; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.node.BooleanNode; import software.amazon.smithy.model.node.NumberNode; @@ -40,6 +41,7 @@ import software.amazon.smithy.model.validation.Severity; import software.amazon.smithy.model.validation.ValidatedResult; import software.amazon.smithy.model.validation.ValidationEvent; +import software.amazon.smithy.model.validation.Validator; import software.amazon.smithy.utils.MapUtils; /** @@ -50,8 +52,6 @@ */ final class ModelUpgrader { - private static final String UPGRADE_MODEL = "UpgradeModel"; - /** Shape types in Smithy 1.0 that had a default value. */ private static final EnumSet HAD_DEFAULT_VALUE_IN_1_0 = EnumSet.of( ShapeType.BYTE, @@ -77,10 +77,10 @@ final class ModelUpgrader { private final Model model; private final List events; - private final Map fileToVersion; + private final Function fileToVersion; private final List shapeUpgrades = new ArrayList<>(); - ModelUpgrader(Model model, List events, Map fileToVersion) { + ModelUpgrader(Model model, List events, Function fileToVersion) { this.model = model; this.events = events; this.fileToVersion = fileToVersion; @@ -88,10 +88,11 @@ final class ModelUpgrader { ValidatedResult transform() { for (MemberShape member : model.getMemberShapes()) { - // We must assume v2 for manually created shapes. - Version version = fileToVersion.getOrDefault(member.getSourceLocation().getFilename(), - Version.VERSION_2_0); + if (Prelude.isPreludeShape(member)) { + continue; + } + Version version = fileToVersion.apply(member); if (version == Version.VERSION_2_0) { validateV2Member(member); } else { @@ -99,14 +100,14 @@ ValidatedResult transform() { // trying to upgrade 2.0 shapes has no effect. // For v1 shape checks, we need to know the containing shape type to apply the appropriate transform. model.getShape(member.getContainer()) - .ifPresent(container -> upgradeV1Member(container.getType(), member)); + .ifPresent(container -> upgradeV1Member(version, container.getType(), member)); } } return new ValidatedResult<>(ModelTransformer.create().replaceShapes(model, shapeUpgrades), events); } - private void upgradeV1Member(ShapeType containerType, MemberShape member) { + private void upgradeV1Member(Version version, ShapeType containerType, MemberShape member) { // Don't fail here on broken models, and since it's broken, don't try to upgrade it. Shape target = model.getShape(member.getTarget()).orElse(null); if (target == null) { @@ -126,7 +127,7 @@ private void upgradeV1Member(ShapeType containerType, MemberShape member) { // Add the @default trait to structure members when needed. if (shouldV1MemberHaveDefaultTrait(containerType, member, target)) { events.add(ValidationEvent.builder() - .id(UPGRADE_MODEL) + .id(Validator.MODEL_DEPRECATION) .severity(Severity.WARNING) .shape(member) .message("Add the @default trait to this member to make it forward compatible with " @@ -141,6 +142,9 @@ private void upgradeV1Member(ShapeType containerType, MemberShape member) { } else if (isZeroValidDefault(member)) { builder.addTrait(new DefaultTrait(new NumberNode(0, builder.getSourceLocation()))); } + } else if (isMemberImplicitlyBoxed(version, containerType, member, target)) { + // Add a synthetic box trait to the shape. + builder = createOrReuseBuilder(member, builder).addTrait(new BoxTrait()); } if (builder != null) { @@ -148,6 +152,24 @@ private void upgradeV1Member(ShapeType containerType, MemberShape member) { } } + // If it's for sure a v1 shape and was implicitly boxed, then add a synthetic box trait so tooling + // can know that the shape was previously considered nullable. Note that this method does not + // check if the targeted shape is required. It's up to tooling to determine how to handle a 1.0 + // member that is both required and boxed. + private boolean isMemberImplicitlyBoxed( + Version version, + ShapeType containerType, + MemberShape member, + Shape target + ) { + return version == Version.VERSION_1_0 + && containerType == ShapeType.STRUCTURE + && !member.hasTrait(DefaultTrait.class) // don't add box if it has a default trait. + && !member.hasTrait(BoxTrait.class) // don't add box again + && !REMOVED_PRIMITIVE_SHAPES.containsKey(target.getId()) + && (target.hasTrait(BoxTrait.class) || HAD_DEFAULT_VALUE_IN_1_0.contains(target.getType())); + } + private boolean isZeroValidDefault(MemberShape member) { Optional rangeTraitOptional = member.getMemberTrait(model, RangeTrait.class); // No range means 0 is fine. @@ -159,7 +181,7 @@ private boolean isZeroValidDefault(MemberShape member) { // Min is greater than 0. if (rangeTrait.getMin().isPresent() && rangeTrait.getMin().get().compareTo(BigDecimal.ZERO) > 0) { events.add(ValidationEvent.builder() - .id(UPGRADE_MODEL) + .id(Validator.MODEL_DEPRECATION) .severity(Severity.WARNING) .shape(member) .message("Cannot add the @default trait to this member due to a minimum range constraint.") @@ -170,7 +192,7 @@ private boolean isZeroValidDefault(MemberShape member) { // Max is less than 0. if (rangeTrait.getMax().isPresent() && rangeTrait.getMax().get().compareTo(BigDecimal.ZERO) < 0) { events.add(ValidationEvent.builder() - .id(UPGRADE_MODEL) + .id(Validator.MODEL_DEPRECATION) .severity(Severity.WARNING) .shape(member) .message("Cannot add the @default trait to this member due to a maximum range constraint.") @@ -218,7 +240,7 @@ private void validateV2Member(MemberShape member) { if (member.hasTrait(BoxTrait.class)) { events.add(ValidationEvent.builder() - .id(UPGRADE_MODEL) + .id(Validator.MODEL_DEPRECATION) .severity(Severity.ERROR) .shape(member) .sourceLocation(member.expectTrait(BoxTrait.class)) @@ -229,7 +251,7 @@ private void validateV2Member(MemberShape member) { private void emitWhenTargetingRemovedPreludeShape(Severity severity, MemberShape member) { events.add(ValidationEvent.builder() - .id(UPGRADE_MODEL) + .id(Validator.MODEL_DEPRECATION) .severity(severity) .shape(member) .sourceLocation(member) 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 f310d8de2d8..1375242445f 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 @@ -26,6 +26,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.SourceException; import software.amazon.smithy.model.SourceLocation; import software.amazon.smithy.model.node.ObjectNode; import software.amazon.smithy.model.traits.SuppressTrait; @@ -175,7 +176,7 @@ public Validator createValidator() { // Add suppressions found in the model via metadata. List modelSuppressions = new ArrayList<>(suppressions); - loadModelSuppressions(modelSuppressions, model); + loadModelSuppressions(modelSuppressions, model, coreEvents); // Add validators defined in the model through metadata. List modelValidators = new ArrayList<>(staticValidators); @@ -236,7 +237,8 @@ private static void loadModelValidators( List suppressions ) { // Load validators defined in metadata. - ValidatedResult> loaded = ValidationLoader.loadValidators(model.getMetadata()); + ValidatedResult> loaded = ValidationLoader + .loadValidators(model.getMetadata()); events.addAll(loaded.getValidationEvents()); List definitions = loaded.getResult().orElseGet(Collections::emptyList); ValidatorFromDefinitionFactory factory = new ValidatorFromDefinitionFactory(validatorFactory); @@ -264,11 +266,19 @@ private static ValidationEvent unknownValidatorError(String name, SourceLocation .build(); } - private static void loadModelSuppressions(List suppressions, Model model) { + 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) { - suppressions.add(Suppression.fromMetadata(rule)); + try { + suppressions.add(Suppression.fromMetadata(rule)); + } catch (SourceException e) { + events.add(ValidationEvent.fromSourceException(e)); + } } }); } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/PendingShape.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/PendingShape.java deleted file mode 100644 index 78c48ede950..00000000000 --- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/PendingShape.java +++ /dev/null @@ -1,276 +0,0 @@ -/* - * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.smithy.model.loader; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.StringJoiner; -import java.util.function.Consumer; -import software.amazon.smithy.model.FromSourceLocation; -import software.amazon.smithy.model.SourceLocation; -import software.amazon.smithy.model.shapes.Shape; -import software.amazon.smithy.model.shapes.ShapeId; -import software.amazon.smithy.model.validation.Severity; -import software.amazon.smithy.model.validation.ValidationEvent; -import software.amazon.smithy.model.validation.Validator; - -/** - * Represents a shape that is pending other shapes in order to be created. - */ -interface PendingShape { - - /** - * Create a singular pending shape. - * - * @param id ID of the shape. - * @param sourceLocation Where the shape was defined. - * @param dependencies Dependencies the shape is waiting on to resolve. - * @param creator The factory used to create the resolved shape. - * @return Returns the created pending shape. - */ - static PendingShape create( - ShapeId id, - FromSourceLocation sourceLocation, - Set dependencies, - Consumer> creator - ) { - return new Singular(id, sourceLocation, dependencies, creator); - } - - /** - * Merge {@code right} into {@code left} and the updated value. - * - * @param left Left value to merge into. - * @param right Right value to merge into left. - * @return Returns the merged value. - */ - static PendingConflict mergeIntoLeft(PendingShape left, PendingShape right) { - if (!left.getId().equals(right.getId())) { - throw new IllegalArgumentException("Cannot merge conflicting shapes with different IDs"); - } - - PendingConflict result; - if (left instanceof PendingConflict) { - result = (PendingConflict) left; - } else { - result = new PendingConflict(left); - } - - result.pendingDelegates.add(right); - result.pendingShapes.addAll(right.getPendingShapes()); - - return result; - } - - /** - * Gets the shape ID of the shape to create. - * - * @return Returns the shape ID. - */ - ShapeId getId(); - - /** - * Gets the set of shapes that are pending. - * - * @return Returns the set of pending shape IDs. - */ - Set getPendingShapes(); - - /** - * Builds the shape, and populates the built shape and any members into the - * given mutable shapeMap. - * - *

Any conflicts that occurs between shapes is handled implicitly in the - * given {@code shapeMap}. There is no need to account for conflicts when - * implementing {@code buildShapes}. - * - * @param shapeMap Mutable map of shapes to populate with the created shapes. - */ - void buildShapes(Map shapeMap); - - /** - * Creates validation events for any shapes that are still unresolved. - * - * @param resolved The map of shapes that have been resolved. - * @param otherPending The map of other shapes that were not resolved. - * @return Returns the validation events to emit. - */ - List unresolved(Map resolved, Map otherPending); - - /** - * A singular pending shape to resolve. - */ - class Singular implements PendingShape { - private final ShapeId id; - private final SourceLocation sourceLocation; - private final Set dependencies; - private final Set pending; - private final Consumer> creator; - - Singular( - ShapeId id, - FromSourceLocation sourceLocation, - Set dependencies, - Consumer> creator - ) { - this.id = id; - this.sourceLocation = sourceLocation.getSourceLocation(); - this.dependencies = dependencies; - this.pending = new HashSet<>(dependencies); - this.creator = creator; - } - - @Override - public ShapeId getId() { - return id; - } - - @Override - public Set getPendingShapes() { - return pending; - } - - @Override - public void buildShapes(Map shapeMap) { - creator.accept(shapeMap); - } - - @Override - public List unresolved(Map resolved, Map pending) { - // A rare case when there are conflicting shapes, and only some are unresolved. - if (getPendingShapes().isEmpty()) { - return Collections.emptyList(); - } - - List nonMixinDependencies = new ArrayList<>(); - List notFoundShapes = new ArrayList<>(); - List missingTransitive = new ArrayList<>(); - List cycles = new ArrayList<>(); - for (ShapeId id : getPendingShapes()) { - if (resolved.containsKey(id)) { - nonMixinDependencies.add(id); - } else if (!pending.containsKey(id)) { - notFoundShapes.add(id); - } else if (anyMissingTransitiveDependencies(id, resolved, pending, new HashSet<>())) { - missingTransitive.add(id); - } else { - cycles.add(id); - } - } - - StringJoiner message = new StringJoiner(" "); - message.add("Unable to resolve mixins;"); - - if (!nonMixinDependencies.isEmpty()) { - message.add("attempted to mixin shapes with no mixin trait: " + nonMixinDependencies); - } - - if (!notFoundShapes.isEmpty()) { - message.add("attempted to mixin shapes that are not in the model: " + notFoundShapes); - } - - if (!missingTransitive.isEmpty()) { - message.add("unable to resolve due to missing transitive mixins: " + missingTransitive); - } - - if (!cycles.isEmpty()) { - message.add("cycles detected between this shape and " + cycles); - } - - return Collections.singletonList(ValidationEvent.builder() - .id(Validator.MODEL_ERROR) - .severity(Severity.ERROR) - .shapeId(getId()) - .sourceLocation(sourceLocation) - .message(message.toString()) - .build()); - } - - private boolean anyMissingTransitiveDependencies( - ShapeId current, - Map resolved, - Map otherPending, - Set visited - ) { - if (resolved.containsKey(current)) { - return false; - } else if (!otherPending.containsKey(current)) { - return true; - } else if (visited.contains(current)) { - visited.remove(current); - return false; - } - - visited.add(current); - for (ShapeId next : otherPending.get(current).getPendingShapes()) { - if (anyMissingTransitiveDependencies(next, resolved, otherPending, visited)) { - return true; - } - } - - return false; - } - } - - /** - * Aggregates together one or more shapes to implicitly handle aggregating and - * resolving the dependencies of conflicting shapes defined in the model. - * - *

Smithy allows conflicting shapes to be defined, and only emits errors if - * the shapes are not exactly equivalent. It's not possible to know if the - * shapes are equivalent until they are fully built, so that has to be deferred - * until all conflicts are built - hence this class. - */ - class PendingConflict implements PendingShape { - private final List pendingDelegates = new ArrayList<>(); - private final Set pendingShapes; - - PendingConflict(PendingShape pending) { - this.pendingDelegates.add(pending); - this.pendingShapes = new HashSet<>(pending.getPendingShapes()); - } - - @Override - public ShapeId getId() { - return pendingDelegates.get(0).getId(); - } - - @Override - public Set getPendingShapes() { - return pendingShapes; - } - - @Override - public void buildShapes(Map shapeMap) { - for (PendingShape p : pendingDelegates) { - p.buildShapes(shapeMap); - } - } - - @Override - public List unresolved(Map resolved, Map pending) { - List events = new ArrayList<>(); - for (PendingShape p : pendingDelegates) { - events.addAll(p.unresolved(resolved, pending)); - } - return events; - } - } -} diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/SetResourceBasedTargets.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/SetResourceBasedTargets.java deleted file mode 100644 index cee9a8769fb..00000000000 --- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/SetResourceBasedTargets.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.smithy.model.loader; - -import java.util.Collections; -import java.util.Map; -import java.util.Set; -import software.amazon.smithy.model.SourceException; -import software.amazon.smithy.model.shapes.AbstractShapeBuilder; -import software.amazon.smithy.model.shapes.MemberShape; -import software.amazon.smithy.model.shapes.ResourceShape; -import software.amazon.smithy.model.shapes.Shape; -import software.amazon.smithy.model.shapes.ShapeId; - -/** - * Sets member targets based on referenced resource identifiers. - * - *

Structures can elide the targets of members if they're bound to a resource - * and that resource has an identifier with a matching name. Here we set the - * target based on that information. - */ -final class SetResourceBasedTargets implements PendingShapeModifier { - private final ShapeId resourceId; - - SetResourceBasedTargets(ShapeId resourceId) { - this.resourceId = resourceId; - } - - @Override - public Set getDependencies() { - return Collections.singleton(resourceId); - } - - @Override - public void modifyMember( - AbstractShapeBuilder shapeBuilder, - MemberShape.Builder memberBuilder, - TraitContainer resolvedTraits, - Map shapeMap - ) { - // Fast-fail the common case of the target having already been set. - if (memberBuilder.getTarget() != null) { - return; - } - - Shape fromShape = shapeMap.get(resourceId); - if (!fromShape.isResourceShape()) { - String message = String.format( - "The target of the `for` production must be a resource shape, but found a %s shape: %s", - fromShape.getType(), - resourceId - ); - throw new SourceException(message, shapeBuilder.getSourceLocation()); - } - - ResourceShape resource = fromShape.asResourceShape().get(); - String name = memberBuilder.getId().getMember().get(); - if (resource.getIdentifiers().containsKey(name)) { - memberBuilder.target(resource.getIdentifiers().get(name)); - } - if (resource.getProperties().containsKey(name)) { - memberBuilder.target(resource.getProperties().get(name)); - } - } -} diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/PendingShapeModifier.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/ShapeModifier.java similarity index 69% rename from smithy-model/src/main/java/software/amazon/smithy/model/loader/PendingShapeModifier.java rename to smithy-model/src/main/java/software/amazon/smithy/model/loader/ShapeModifier.java index f23998868e0..9b0a5bfd3fe 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/PendingShapeModifier.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/ShapeModifier.java @@ -15,24 +15,20 @@ package software.amazon.smithy.model.loader; +import java.util.List; import java.util.Map; -import java.util.Set; +import java.util.function.Function; import software.amazon.smithy.model.shapes.AbstractShapeBuilder; import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.Trait; +import software.amazon.smithy.model.validation.ValidationEvent; /** * Represents a modification that needs to be done to resolve a pending shape. */ -interface PendingShapeModifier { - - /** - * @return Returns the shapes that need to be resolved before this - * modification can be applied. - */ - Set getDependencies(); - +interface ShapeModifier { /** * Modifies locally defined members on the shape. * @@ -40,14 +36,14 @@ interface PendingShapeModifier { * * @param shapeBuilder The builder for the shape being modified. * @param memberBuilder The builder for the locally defined member. - * @param resolvedTraits A container with all the traits in the model that have been resolved. - * @param shapeMap A map of shape id to resolved shape. + * @param unclaimedTraits Function that provides unclaimed traits for a shape. + * @param shapeMap A function that returns a shape for a given ID, or null. */ default void modifyMember( AbstractShapeBuilder shapeBuilder, MemberShape.Builder memberBuilder, - TraitContainer resolvedTraits, - Map shapeMap + Function> unclaimedTraits, + Function shapeMap ) { } @@ -58,14 +54,19 @@ default void modifyMember( * * @param builder The builder for the shape being modified. * @param memberBuilders The builders for the shape's locally defined members. - * @param resolvedTraits A container with all the traits in the model that have been resolved. - * @param shapeMap A map of shape id to resolved shape. + * @param unclaimedTraits Function that provides unclaimed traits for a shape. + * @param shapeMap A function that returns a shape for a given ID, or null. */ default void modifyShape( AbstractShapeBuilder builder, Map memberBuilders, - TraitContainer resolvedTraits, - Map shapeMap + Function> unclaimedTraits, + Function shapeMap ) { } + + /** + * @return Returns any events emitted by the modifier. + */ + List getEvents(); } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/TopologicalShapeSort.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/TopologicalShapeSort.java index 8b900ad4a8b..4e59d544eaa 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/TopologicalShapeSort.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/TopologicalShapeSort.java @@ -15,16 +15,15 @@ package software.amazon.smithy.model.loader; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.Deque; import java.util.HashMap; import java.util.HashSet; -import java.util.LinkedHashSet; -import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.Queue; import java.util.Set; import java.util.TreeSet; import software.amazon.smithy.model.shapes.Shape; @@ -39,7 +38,16 @@ public final class TopologicalShapeSort { private final Map> forwardDependencies = new HashMap<>(); - private final Queue satisfiedShapes = new LinkedList<>(); + private final Map> reverseDependencies = new HashMap<>(); + private final Deque satisfiedShapes; + + public TopologicalShapeSort() { + this(100); + } + + TopologicalShapeSort(int ensureCapacity) { + satisfiedShapes = new ArrayDeque<>(ensureCapacity); + } /** * Add a shape to the sort queue, and automatically extract dependencies. @@ -57,7 +65,14 @@ public void enqueue(Shape shape) { * @param dependencies Dependencies of the shape. */ public void enqueue(ShapeId shape, Collection dependencies) { - forwardDependencies.put(shape, new LinkedHashSet<>(dependencies)); + if (dependencies.isEmpty()) { + satisfiedShapes.offer(shape); + } else { + for (ShapeId dependent : dependencies) { + reverseDependencies.computeIfAbsent(dependent, unused -> new HashSet<>()).add(shape); + } + forwardDependencies.put(shape, new HashSet<>(dependencies)); + } } /** @@ -67,23 +82,7 @@ public void enqueue(ShapeId shape, Collection dependencies) { * @throws CycleException if cycles exist between shapes. */ public List dequeueSortedShapes() { - Map> reverseDependencies = new HashMap<>(); - - for (Map.Entry> entry : forwardDependencies.entrySet()) { - if (entry.getValue().isEmpty()) { - satisfiedShapes.offer(entry.getKey()); - } else { - for (ShapeId dependent : entry.getValue()) { - reverseDependencies.computeIfAbsent(dependent, unused -> new HashSet<>()).add(entry.getKey()); - } - } - } - - return topologicalSort(reverseDependencies); - } - - private List topologicalSort(Map> reverseDependencies) { - List result = new ArrayList<>(); + List result = new ArrayList<>(satisfiedShapes.size() + forwardDependencies.size()); while (!satisfiedShapes.isEmpty()) { ShapeId current = satisfiedShapes.poll(); @@ -94,13 +93,15 @@ private List topologicalSort(Map> reverseDependen Set dependentDependencies = forwardDependencies.get(dependent); dependentDependencies.remove(current); if (dependentDependencies.isEmpty()) { - satisfiedShapes.add(dependent); + satisfiedShapes.offer(dependent); } } } + reverseDependencies.clear(); + if (!forwardDependencies.isEmpty()) { - throw new CycleException(new TreeSet<>(forwardDependencies.keySet())); + throw new CycleException(new TreeSet<>(forwardDependencies.keySet()), result); } return result; @@ -111,10 +112,12 @@ private List topologicalSort(Map> reverseDependen */ public static final class CycleException extends RuntimeException { private final Set unresolved; + private final List resolved; - public CycleException(Set unresolved) { + public CycleException(Set unresolved, List resolved) { super("Mixin cycles detected among " + unresolved); this.unresolved = unresolved; + this.resolved = resolved; } /** @@ -125,5 +128,12 @@ public CycleException(Set unresolved) { public Set getUnresolved() { return unresolved; } + + /** + * @return Returns the set of resolved shapes. + */ + public List getResolved() { + return resolved; + } } } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/TraitContainer.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/TraitContainer.java deleted file mode 100644 index a69426fa199..00000000000 --- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/TraitContainer.java +++ /dev/null @@ -1,316 +0,0 @@ -/* - * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.smithy.model.loader; - -import static java.lang.String.format; - -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.logging.Logger; -import software.amazon.smithy.model.SourceException; -import software.amazon.smithy.model.node.ArrayNode; -import software.amazon.smithy.model.node.Node; -import software.amazon.smithy.model.shapes.ShapeId; -import software.amazon.smithy.model.traits.DynamicTrait; -import software.amazon.smithy.model.traits.Trait; -import software.amazon.smithy.model.traits.TraitFactory; -import software.amazon.smithy.model.validation.Severity; -import software.amazon.smithy.model.validation.ValidationEvent; -import software.amazon.smithy.model.validation.Validator; - -/** - * Aggregates, merges, and creates traits. - */ -public interface TraitContainer { - - /** Shared empty, immutable instance. */ - TraitContainer EMPTY = new TraitContainer() { - @Override - public Map> traits() { - return Collections.emptyMap(); - } - - @Override - public Map getTraitsForShape(ShapeId shape) { - return Collections.emptyMap(); - } - - @Override - public void clearTraitsForShape(ShapeId shape) { - // Do nothing. - } - - @Override - public Map> getTraitsAppliedToPrelude() { - return Collections.emptyMap(); - } - - @Override - public void onTrait(ShapeId target, Trait value) { - throw new UnsupportedOperationException("Cannot add trait " + value.toShapeId() + " to " + target); - } - - @Override - public void onTrait(ShapeId target, ShapeId traitId, Node value) { - throw new UnsupportedOperationException("Cannot add trait " + traitId + " to " + target); - } - }; - - /** - * @return Gets all traits in the value map. - */ - Map> traits(); - - /** - * Gets the traits applied to a shape. - * - * @param shape Shape to get the traits of. - * @return Returns the traits of the shape. - */ - Map getTraitsForShape(ShapeId shape); - - /** - * Clears the traits applied to a shape. - * - *

This is useful in the event of errors that occur while attempting to - * create a shape so that validation events about traits applied to shapes - * that couldn't be created are not emitted. - * - * @param shape Shape to clear the traits for. - */ - void clearTraitsForShape(ShapeId shape); - - /** - * Gets all traits applied to the prelude. - * - * @return Returns the traits applied to prelude shapes. - */ - Map> getTraitsAppliedToPrelude(); - - /** - * Add a trait. - * - * @param target Shape to add the trait to. - * @param value Trait to add. - */ - void onTrait(ShapeId target, Trait value); - - /** - * Create and add a trait. - * - * @param target Shape to add the trait to. - * @param traitId Trait shape ID to create. - * @param value The value to assign to the trait. - */ - void onTrait(ShapeId target, ShapeId traitId, Node value); - - /** - * The actual, mutable implementation used to aggregate traits. - */ - final class TraitHashMap implements TraitContainer { - private static final Logger LOGGER = Logger.getLogger(TraitContainer.class.getName()); - - private final Map> targetToTraits = new HashMap<>(); - private final Map> traitsAppliedToPrelude = new HashMap<>(); - private final TraitFactory traitFactory; - private final List events; - - /** - * @param traitFactory Factory used to create traits. - * @param events Mutable, by-reference validation event list. - */ - TraitHashMap(TraitFactory traitFactory, List events) { - this.traitFactory = Objects.requireNonNull(traitFactory, "Trait factory must not be null"); - this.events = Objects.requireNonNull(events, "events must not be null"); - } - - @Override - public Map> traits() { - return targetToTraits; - } - - @Override - public Map getTraitsForShape(ShapeId shape) { - return targetToTraits.getOrDefault(shape, Collections.emptyMap()); - } - - @Override - public void clearTraitsForShape(ShapeId shape) { - targetToTraits.remove(shape); - } - - @Override - public Map> getTraitsAppliedToPrelude() { - return traitsAppliedToPrelude; - } - - @Override - public void onTrait(ShapeId target, Trait value) { - ShapeId traitId = value.toShapeId(); - Map traits = targetToTraits.computeIfAbsent(target, id -> new HashMap<>()); - - if (traits.containsKey(traitId)) { - Trait previousTrait = traits.get(traitId); - - if (LoaderUtils.isSameLocation(previousTrait, value) && previousTrait.equals(value)) { - // The assumption here is that if the trait value is exactly the - // same and from the same location, then the same model file was - // included more than once in a way that side-steps file and URL - // de-duplication. For example, this can occur when a Model is assembled - // through a ModelAssembler using model discovery, then the Model is - // added to a subsequent ModelAssembler, and then model discovery is - // performed again using the same classpath. - LOGGER.finest(() -> String.format("Ignoring duplicate %s trait value on %s at same exact location", - traitId, target)); - return; - } - - Node previous = previousTrait.toNode(); - Node updated = value.toNode(); - - if (previous.isArrayNode() && updated.isArrayNode()) { - // You can merge trait arrays. - ArrayNode merged = previous.expectArrayNode().merge(updated.expectArrayNode()); - value = createTrait(target, traitId, merged); - if (value == null) { - return; - } - } else if (previous.equals(updated)) { - LOGGER.fine(() -> String.format("Ignoring duplicate %s trait value on %s", traitId, target)); - return; - } else { - events.add(ValidationEvent.builder() - .id(Validator.MODEL_ERROR) - .severity(Severity.ERROR) - .sourceLocation(value.getSourceLocation()) - .shapeId(target) - .message(String.format( - "Conflicting `%s` trait found on shape `%s`. The previous trait was defined at " - + "`%s`, and a conflicting trait was defined at `%s`.", - traitId, target, previous.getSourceLocation(), value.getSourceLocation())) - .build()); - return; - } - } - - traits.put(traitId, value); - - if (target.getNamespace().equals(Prelude.NAMESPACE)) { - traitsAppliedToPrelude.computeIfAbsent(target, id -> new HashMap<>()).put(traitId, value); - } - } - - @Override - public void onTrait(ShapeId target, ShapeId traitId, Node value) { - Trait trait = createTrait(target, traitId, value); - if (trait != null) { - onTrait(target, trait); - } - } - - /** - * Creates a trait and returns null if it can't be created. - * - * @param target Shape to apply the trait to. - * @param traitId Trait shape ID being created. - * @param traitValue The value to assign to the trait. - * @return Returns the created trait on success, or null on failure. - */ - private Trait createTrait(ShapeId target, ShapeId traitId, Node traitValue) { - try { - return traitFactory.createTrait(traitId, target, traitValue) - .orElseGet(() -> new DynamicTrait(traitId, traitValue)); - } catch (SourceException e) { - String message = format("Error creating trait `%s`: ", Trait.getIdiomaticTraitName(traitId)); - events.add(ValidationEvent.fromSourceException(e, message) - .toBuilder() - .shapeId(target) - .build()); - return null; - } - } - } - - /** - * Performs version-specific validation on traits as they are added. - * - *

For example, this class will throw a {@link ModelSyntaxException} if - * the mixin trait is used in Smithy IDL 1.0. - */ - final class VersionAwareTraitContainer implements TraitContainer { - private final TraitContainer delegate; - - // If a model specifies no version, we by default assume it's for 1.0. - private Version version = Version.VERSION_1_0; - - VersionAwareTraitContainer(TraitContainer delegate) { - this.delegate = delegate; - } - - /** - * Sets the version being tracked. - * - * @param version Version to set. - */ - void setVersion(Version version) { - this.version = version; - } - - /** - * Gets the currently configured version. - * - * @return Returns the configured version. - */ - Version getVersion() { - return version; - } - - @Override - public Map> traits() { - return delegate.traits(); - } - - @Override - public Map getTraitsForShape(ShapeId shape) { - return delegate.getTraitsForShape(shape); - } - - @Override - public void clearTraitsForShape(ShapeId shape) { - delegate.clearTraitsForShape(shape); - } - - @Override - public Map> getTraitsAppliedToPrelude() { - return delegate.getTraitsAppliedToPrelude(); - } - - @Override - public void onTrait(ShapeId target, Trait value) { - version.validateVersionedTrait(target, value.toShapeId(), value.toNode()); - delegate.onTrait(target, value); - } - - @Override - public void onTrait(ShapeId target, ShapeId traitId, Node value) { - version.validateVersionedTrait(target, traitId, value); - delegate.onTrait(target, traitId, value); - } - } -} diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/Version.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/Version.java index 85e4a822cc2..4edf4a103a7 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/Version.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/Version.java @@ -18,20 +18,13 @@ import software.amazon.smithy.model.node.Node; import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.model.shapes.ShapeType; +import software.amazon.smithy.model.traits.DefaultTrait; import software.amazon.smithy.model.traits.MixinTrait; /** * Tracks version-specific features and validation. */ enum Version { - - /** - * Unknown is used for in-memory models that aren't tied to a specific version. - * For these kinds of models, we just assume every feature is supported. - * - *

When loading IDL models with no $version specified, the default assumed - * version is 1.0, not UNKNOWN (see {@link TraitContainer.VersionAwareTraitContainer}). - */ UNKNOWN { @Override public String toString() { @@ -62,6 +55,11 @@ boolean isDefaultSupported() { boolean isShapeTypeSupported(ShapeType shapeType) { return true; } + + @Override + boolean isDeprecated() { + return false; + } }, VERSION_1_0 { @@ -97,16 +95,28 @@ boolean isShapeTypeSupported(ShapeType shapeType) { @Override void validateVersionedTrait(ShapeId target, ShapeId traitId, Node value) { + String errorMessage = null; if (traitId.equals(MixinTrait.ID)) { + errorMessage = String.format("Mixins can only be used in Smithy 2.0 or later. Attempted to apply " + + "a @mixin trait to `%s` in a model file using version `%s`.", + target, this); + } else if (traitId.equals(DefaultTrait.ID)) { + errorMessage = "The @default trait can only be used in Smithy 2.0 or later"; + } + + if (errorMessage != null) { throw ModelSyntaxException.builder() - .message(String.format("Mixins can only be used in Smithy 2.0 or later. Attempted to apply " - + "a @mixin trait to `%s` in a model file using version `%s`.", - target, this)) + .message(errorMessage) .shapeId(target) .sourceLocation(value) .build(); } } + + @Override + boolean isDeprecated() { + return true; + } }, VERSION_2_0 { @@ -139,6 +149,11 @@ boolean isDefaultSupported() { boolean isShapeTypeSupported(ShapeType shapeType) { return shapeType != ShapeType.SET; } + + @Override + boolean isDeprecated() { + return false; + } }; /** @@ -170,6 +185,11 @@ boolean supportsResourceProperties() { return this == VERSION_2_0; } + /** + * @return Return true if deprecated. + */ + abstract boolean isDeprecated(); + /** * Checks if this version of the IDL supports mixins. * diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/AbstractShapeBuilder.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/AbstractShapeBuilder.java index 5a12aa8cfd2..8734b5c4bf1 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/AbstractShapeBuilder.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/AbstractShapeBuilder.java @@ -132,6 +132,15 @@ public B traits(Collection traitsToSet) { return (B) this; } + /** + * Get an immutable view of the traits applied to the builder. + * + * @return Returns the applied traits. + */ + public Map getAllTraits() { + return traits.peek(); + } + /** * Adds traits from an iterator to the shape builder, replacing any * conflicting traits. @@ -202,7 +211,7 @@ public B clearTraits() { * Adds a member to the shape IFF the shape supports members. * * @param member Member to add to the shape. - * @return Returns the model assembler. + * @return Returns the builder. * @throws UnsupportedOperationException if the shape does not support members. */ public B addMember(MemberShape member) { @@ -210,6 +219,16 @@ public B addMember(MemberShape member) { "Member `%s` cannot be added to %s", member.getId(), getClass().getName())); } + /** + * Removes all members from the builder. + * + * @return Returns the builder. + */ + @SuppressWarnings("unchecked") + public B clearMembers() { + return (B) this; + } + /** * Adds a mixin to the shape. * diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/CollectionShape.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/CollectionShape.java index c8d37c25acb..19d71b01c84 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/CollectionShape.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/CollectionShape.java @@ -135,6 +135,12 @@ public B addMember(MemberShape member) { return member(member); } + @Override + public B clearMembers() { + member = null; + return super.clearMembers(); + } + @Override public B flattenMixins() { for (Shape mixin : getMixins().values()) { diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java index ceebb4c2548..644691b2997 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java @@ -378,11 +378,7 @@ public Builder setMembersFromEnumTrait(Collection members) { return this; } - /** - * Removes all members from the shape. - * - * @return Returns the builder. - */ + @Override public Builder clearMembers() { members.clear(); return this; diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/IntEnumShape.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/IntEnumShape.java index add8d6d3520..b7d041d258f 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/IntEnumShape.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/IntEnumShape.java @@ -130,11 +130,7 @@ public Builder members(Collection members) { return this; } - /** - * Removes all members from the shape. - * - * @return Returns the builder. - */ + @Override public Builder clearMembers() { members.clear(); return this; diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/MapShape.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/MapShape.java index ee38f7acf18..87666e376ed 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/MapShape.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/MapShape.java @@ -197,6 +197,13 @@ public Builder addMember(MemberShape member) { } } + @Override + public Builder clearMembers() { + key = null; + value = null; + return this; + } + /** * Sets the key member of the map. * diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/NamedMembersShapeBuilder.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/NamedMembersShapeBuilder.java index eae1d88951d..e68b0b54612 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/NamedMembersShapeBuilder.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/NamedMembersShapeBuilder.java @@ -54,11 +54,7 @@ public B members(Collection members) { return (B) this; } - /** - * Removes all members from the shape. - * - * @return Returns the builder. - */ + @Override @SuppressWarnings("unchecked") public B clearMembers() { members.clear(); diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/SetShape.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/SetShape.java index 3aaf88e906b..402b408673a 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/SetShape.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/SetShape.java @@ -106,6 +106,11 @@ public Builder addMember(MemberShape member) { return (Builder) super.addMember(member); } + @Override + public Builder clearMembers() { + return (Builder) super.clearMembers(); + } + @Override public Builder id(String shapeId) { return (Builder) super.id(shapeId); diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/Shape.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/Shape.java index dc37df42a5c..19e5fdc9bdd 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/Shape.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/Shape.java @@ -143,12 +143,12 @@ protected MemberShape getRequiredMixinMember( */ private void validateShapeId(boolean expectMember) { if (expectMember) { - if (!getId().getMember().isPresent()) { + if (!getId().hasMember()) { throw new SourceException(String.format( "Shapes of type `%s` must contain a member in their shape ID. Found `%s`", getType(), getId()), getSourceLocation()); } - } else if (getId().getMember().isPresent()) { + } else if (getId().hasMember()) { throw new SourceException(String.format( "Shapes of type `%s` cannot contain a member in their shape ID. Found `%s`", getType(), getId()), getSourceLocation()); @@ -785,17 +785,17 @@ public boolean equals(Object o) { return true; } else if (!(o instanceof Shape)) { return false; + } else if (hashCode() != o.hashCode()) { + return false; // take advantage of hashcode caching } Shape other = (Shape) o; return getType() == other.getType() - && hashCode() == other.hashCode() // take advantage of hashcode caching && getId().equals(other.getId()) - && traits.equals(other.traits) - && mixins.equals(other.mixins) - // Ensure members are equal and defined in the same order. + && getMemberNames().equals(other.getMemberNames()) && getAllMembers().equals(other.getAllMembers()) - && getMemberNames().equals(other.getMemberNames()); + && getAllTraits().equals(other.getAllTraits()) + && mixins.equals(other.mixins); } /** diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeId.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeId.java index 68fa4ddc15c..dda2b028293 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeId.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeId.java @@ -276,10 +276,14 @@ public int compareTo(ShapeId other) { /** * Creates a new Shape.Id with no member. * - * @return returns a new Shape.Id + * @return returns a new Shape.Id, or the existing shape if it has no member. */ public ShapeId withoutMember() { - return new ShapeId(namespace, name, null); + if (member == null) { + return this; + } else { + return new ShapeId(namespace, name, null); + } } /** @@ -331,6 +335,15 @@ public Optional getMember() { return Optional.ofNullable(member); } + /** + * Checks if the ID has a member set. + * + * @return Returns true if the ID has a member. + */ + public boolean hasMember() { + return member != null; + } + /** * Creates a string that contains a relative reference to the ID. * diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/ValidationUtils.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/ValidationUtils.java index 3e67db04915..7027aee321b 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/validation/ValidationUtils.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/ValidationUtils.java @@ -94,7 +94,7 @@ public static Map> findDuplicateShap return shapes.stream() .map(ToShapeId::toShapeId) // Exclude IDs with members since these need to be validated separately. - .filter(id -> !id.getMember().isPresent()) + .filter(id -> !id.hasMember()) // Group by the lowercase name of each shape, and collect the shape IDs as strings. .collect(groupingBy(id -> id.getName().toLowerCase(Locale.US))) .entrySet().stream() diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/Validator.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/Validator.java index 229aa235c2b..78ce0274772 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/validation/Validator.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/Validator.java @@ -40,6 +40,9 @@ public interface Validator { /** Event ID used for structural errors encountered when loading a model. */ String MODEL_ERROR = "Model"; + /** Event ID used when something in the model is deprecated. */ + String MODEL_DEPRECATION = "ModelDeprecation"; + /** * Validates a model and returns a list of validation events. * diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/testrunner/SmithyTestCase.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/testrunner/SmithyTestCase.java index 674ef32930e..b06c550bf0b 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/validation/testrunner/SmithyTestCase.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/testrunner/SmithyTestCase.java @@ -30,6 +30,7 @@ import software.amazon.smithy.model.validation.Severity; import software.amazon.smithy.model.validation.ValidatedResult; import software.amazon.smithy.model.validation.ValidationEvent; +import software.amazon.smithy.model.validation.Validator; import software.amazon.smithy.utils.IoUtils; /** @@ -40,8 +41,8 @@ public final class SmithyTestCase { private static final Pattern EVENT_PATTERN = Pattern.compile( "^\\[(?SUPPRESSED|NOTE|WARNING|DANGER|ERROR)] (?[^ ]+): ?(?.*) \\| (?[^)]+)"); - private List expectedEvents; - private String modelLocation; + private final List expectedEvents; + private final String modelLocation; /** * @param modelLocation Location of where the model is stored. @@ -118,6 +119,9 @@ public Result createResult(ValidatedResult validatedResult) { // Exclude suppressed events from needing to be defined as acceptable validation // events. However, these can still be defined as required events. .filter(event -> event.getSeverity() != Severity.SUPPRESSED) + // Exclude ModelDeprecation events and deprecation warnings about traits + // needing to be defined. Without this exclusion, existing 1.0 test cases will fail. + .filter(event -> !isModelDeprecationEvent(event)) .collect(Collectors.toList()); return new SmithyTestCase.Result(getModelLocation(), unmatchedEvents, extraEvents); @@ -137,6 +141,12 @@ private static boolean compareEvents(ValidationEvent expected, ValidationEvent a && normalizedActualMessage.startsWith(comparedMessage); } + private boolean isModelDeprecationEvent(ValidationEvent event) { + return event.getId().equals(Validator.MODEL_DEPRECATION) + // Trait vendors should be free to deprecate a trait without breaking consumers. + || (event.getId().equals("DeprecatedTrait")); + } + private static String inferErrorFileLocation(String modelLocation) { int extensionPosition = modelLocation.lastIndexOf("."); if (extensionPosition == -1) { diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/testrunner/SmithyTestSuite.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/testrunner/SmithyTestSuite.java index 344c40bcfa4..544cc5409d6 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/validation/testrunner/SmithyTestSuite.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/testrunner/SmithyTestSuite.java @@ -42,7 +42,7 @@ public final class SmithyTestSuite { private static final String DEFAULT_TEST_CASE_LOCATION = "errorfiles"; - private List cases = new ArrayList<>(); + private final List cases = new ArrayList<>(); private Supplier modelAssemblerFactory = ModelAssembler::new; private SmithyTestSuite() {} @@ -139,8 +139,8 @@ public SmithyTestSuite addTestCase(SmithyTestCase testCase) { * @see SmithyTestCase#fromModelFile */ public SmithyTestSuite addTestCasesFromDirectory(Path modelDirectory) { - try { - Files.walk(modelDirectory) + try (Stream files = Files.walk(modelDirectory)) { + files .filter(Files::isRegularFile) .filter(file -> { String filename = file.toString(); diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/ServiceValidator.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/ServiceValidator.java index 330f6c185b7..00296706aa9 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/ServiceValidator.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/ServiceValidator.java @@ -77,7 +77,7 @@ private void validateService(Model model, ServiceShape service, List> normalizedNamesToIds = new HashMap<>(); for (ShapeId id : serviceClosure.keySet()) { - if (!id.getMember().isPresent()) { + if (!id.hasMember()) { String possiblyRename = service.getContextualName(id); normalizedNamesToIds .computeIfAbsent(possiblyRename.toLowerCase(Locale.ENGLISH), name -> new TreeSet<>()) diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/SetValidator.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/SetValidator.java index 7f46c1ca836..5bba8732733 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/SetValidator.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/SetValidator.java @@ -20,6 +20,7 @@ import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.SetShape; import software.amazon.smithy.model.validation.AbstractValidator; +import software.amazon.smithy.model.validation.Severity; import software.amazon.smithy.model.validation.ValidationEvent; import software.amazon.smithy.utils.SmithyInternalApi; @@ -29,8 +30,14 @@ public final class SetValidator extends AbstractValidator { public List validate(Model model) { List events = new ArrayList<>(); for (SetShape set : model.getSetShapes()) { - events.add(warning(set, "Set shapes are deprecated and have been removed in Smithy IDL v2. " - + "Use a list shape with the @uniqueItems trait instead.")); + ValidationEvent event = ValidationEvent.builder() + .id(AbstractValidator.MODEL_DEPRECATION) + .severity(Severity.WARNING) + .shape(set) + .message("Set shapes are deprecated and have been removed in Smithy IDL v2. " + + "Use a list shape with the @uniqueItems trait instead.") + .build(); + events.add(event); } return events; } 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 eb9b12a1776..7febc585ef2 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 @@ -533,7 +533,7 @@ string title ] ) @length(min: 1) -@deprecated +@deprecated(messag: "The enum trait is replaced by the enum shape in Smithy 2.0", since: "2.0") list enum { member: EnumDefinition } diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/loader/IdlModelLoaderTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/loader/IdlModelLoaderTest.java index f227e2cfe42..38b4fbd1823 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/loader/IdlModelLoaderTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/loader/IdlModelLoaderTest.java @@ -18,9 +18,12 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Optional; @@ -273,4 +276,14 @@ public void loadsServicesWithNonconflictingUnitTypes() { // Make sure we can find our Unit type assertThat(model.expectShape(ShapeId.from("smithy.example#Unit")), Matchers.notNullValue()); } + + @Test + public void emitsVersionWhenNotSet() { + List operations = new ArrayList<>(); + IdlModelParser parser = new IdlModelParser("foo.smithy", "namespace smithy.example\n"); + parser.parse(operations::add); + + assertThat(operations, hasSize(1)); + assertThat(operations.get(0), instanceOf(LoadOperation.ModelVersion.class)); + } } diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/loader/IdlTextParserTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/loader/IdlTextParserTest.java index fe023b93a02..028e42939c1 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/loader/IdlTextParserTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/loader/IdlTextParserTest.java @@ -8,13 +8,12 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import software.amazon.smithy.model.node.StringNode; -import software.amazon.smithy.model.traits.TraitFactory; public class IdlTextParserTest { @ParameterizedTest @MethodSource("validTextProvider") public void parsesText(String input, String lexeme) { - IdlModelParser parser = new IdlModelParser(TraitFactory.createServiceFactory(), "/foo", input); + IdlModelParser parser = new IdlModelParser("/foo", input); StringNode result = IdlNodeParser.parseNode(parser).expectStringNode(); assertThat(result.getValue(), equalTo(lexeme)); } diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/loader/ModelAssemblerTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/loader/ModelAssemblerTest.java index 97a0dc6e6c5..2550b1da933 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/loader/ModelAssemblerTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/loader/ModelAssemblerTest.java @@ -54,20 +54,26 @@ import software.amazon.smithy.model.node.Node; import software.amazon.smithy.model.node.ObjectNode; import software.amazon.smithy.model.node.StringNode; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.ModelSerializer; +import software.amazon.smithy.model.shapes.ModelSerializerTest; import software.amazon.smithy.model.shapes.SetShape; -import software.amazon.smithy.model.shapes.SetShapeTest; import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.model.shapes.ShapeType; import software.amazon.smithy.model.shapes.StringShape; import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.model.traits.BoxTrait; +import software.amazon.smithy.model.traits.DefaultTrait; import software.amazon.smithy.model.traits.DeprecatedTrait; import software.amazon.smithy.model.traits.DocumentationTrait; import software.amazon.smithy.model.traits.DynamicTrait; import software.amazon.smithy.model.traits.InternalTrait; import software.amazon.smithy.model.traits.MediaTypeTrait; +import software.amazon.smithy.model.traits.MixinTrait; import software.amazon.smithy.model.traits.SensitiveTrait; import software.amazon.smithy.model.traits.SuppressTrait; +import software.amazon.smithy.model.traits.TagsTrait; import software.amazon.smithy.model.traits.synthetic.OriginalShapeIdTrait; import software.amazon.smithy.model.validation.Severity; import software.amazon.smithy.model.validation.ValidatedResult; @@ -126,7 +132,8 @@ public void addsExplicitDocumentNode_1_0_0() { .withMember("type", Node.from("string")))); ValidatedResult result = new ModelAssembler().addDocumentNode(node).assemble(); - assertThat(result.getValidationEvents(), empty()); + assertThat(result.getValidationEvents().stream().anyMatch(e -> e.getMessage().contains("is deprecated")), + is(true)); assertTrue(result.unwrap().getShape(ShapeId.from("ns.foo#String")).isPresent()); } @@ -765,4 +772,105 @@ public void canLoadSetsUsingBuiltModel() { Model.assembler().addModel(model).assemble().unwrap(); } + + @Test + public void canIgnoreTraitConflictsWithBuiltShapes() { + StringShape string1 = StringShape.builder() + .id("smithy.example#String1") + .addTrait(new DocumentationTrait("hi")) + .build(); + ModelAssembler assembler = Model.assembler(); + assembler.addShape(string1); + assembler.addUnparsedModel("foo.smithy", "$version: \"2.0\"\n" + + "namespace smithy.example\n\n" + + "@documentation(\"hi\")\n" + + "string String1\n"); + Model result = assembler.assemble().unwrap(); + + assertThat(result.expectShape(string1.getId()).expectTrait(DocumentationTrait.class).getValue(), equalTo("hi")); + } + + @Test + public void canMergeTraitConflictsWithBuiltShapes() { + StringShape string1 = StringShape.builder() + .id("smithy.example#String1") + .addTrait(TagsTrait.builder().addValue("a").build()) + .build(); + ModelAssembler assembler = Model.assembler(); + assembler.addShape(string1); + assembler.addUnparsedModel("foo.smithy", "$version: \"2.0\"\n" + + "namespace smithy.example\n\n" + + "@tags([\"b\"])\n" + + "string String1\n"); + Model result = assembler.assemble().unwrap(); + + assertThat(result.expectShape(string1.getId()).getTags(), contains("a", "b")); + } + + @Test + public void canRoundTripShapesWithMixinsThroughAssembler() { + StructureShape mixin = StructureShape.builder() + .id("smithy.example#Mixin") + .addTrait(MixinTrait.builder().build()) + .build(); + StructureShape struct = StructureShape.builder() + .id("smithy.example#Foo") + .addMixin(mixin) + .addMember("foo", ShapeId.from("smithy.api#String")) + .build(); + Model model = Model.assembler().addShapes(mixin, struct).assemble().unwrap(); + + assertThat(model.expectShape(struct.getId()), equalTo(struct)); + assertThat(model.expectShape(mixin.getId()), equalTo(mixin)); + } + + @Test + public void mixinShapesNoticeDependencyChanges() { + StructureShape mixin = StructureShape.builder() + .id("smithy.example#Mixin") + .addTrait(MixinTrait.builder().build()) + .build(); + StructureShape struct = StructureShape.builder() + .id("smithy.example#Foo") + .addMixin(mixin) + .addMember("foo", ShapeId.from("smithy.api#String")) + .build(); + Model model = Model.assembler() + .addShapes(mixin, struct) + .addTrait(mixin.getId(), new SensitiveTrait()) + .assemble() + .unwrap(); + + assertThat(model.expectShape(mixin.getId()).getAllTraits(), hasKey(SensitiveTrait.ID)); + assertThat(model.expectShape(mixin.getId()).getIntroducedTraits(), hasKey(SensitiveTrait.ID)); + assertThat(model.expectShape(struct.getId()).getAllTraits(), hasKey(SensitiveTrait.ID)); + assertThat(model.expectShape(struct.getId()).getIntroducedTraits(), not(hasKey(SensitiveTrait.ID))); + } + + @Test + public void nodeModelsDoNotInterfereWithManuallyAddedModels() { + StructureShape struct = StructureShape.builder() + .id("smithy.example#Foo") + .addMember("foo", ShapeId.from("smithy.api#Integer")) + .build(); + // Create an object node with a source location of none. + ObjectNode node = Node.objectNodeBuilder() + .withMember(new StringNode("smithy", SourceLocation.NONE), new StringNode("1.0", SourceLocation.NONE)) + .build(); + Model model = Model.assembler() + .addShape(struct) + // Add a Node with a 1.0 version and a SourceLocation.none() value. + // This source location is the same as the manually given shape, but it should not + // cause the manually given shape to also think it's a 1.0 shape. + .addDocumentNode(node) + .assemble() + .unwrap(); + + // Ensure that the upgrade process did not add a Box trait to the manually created shape + // because it is not assumed to be a 1.0 shape. + ShapeId memberCheck = struct.getMember("foo").get().getId(); + MemberShape createdMember = model.expectShape(memberCheck, MemberShape.class); + + assertThat(createdMember.getAllTraits(), not(hasKey(BoxTrait.ID))); + } } diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/loader/ModelUpgraderTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/loader/ModelUpgraderTest.java index 1e1b5787008..3e3a58e02b3 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/loader/ModelUpgraderTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/loader/ModelUpgraderTest.java @@ -1,6 +1,7 @@ package software.amazon.smithy.model.loader; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import java.io.IOException; @@ -20,9 +21,11 @@ import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.ModelSerializer; import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.BoxTrait; import software.amazon.smithy.model.traits.DefaultTrait; import software.amazon.smithy.model.validation.Severity; import software.amazon.smithy.model.validation.ValidatedResult; +import software.amazon.smithy.model.validation.Validator; public class ModelUpgraderTest { @Test @@ -161,7 +164,7 @@ private static Matcher changedMemberTarget(ValidatedResult resul .description("Changed member to target " + newShapeName + " with a warning") .addAssertion(member -> member.getTarget().equals(ShapeId.fromParts(Prelude.NAMESPACE, newShapeName)), member -> "targeted " + member.getTarget()) - .addEventAssertion("UpgradeModel", Severity.WARNING, newShapeName + " instead") + .addEventAssertion(Validator.MODEL_DEPRECATION, Severity.WARNING, newShapeName + " instead") .build(); } @@ -170,7 +173,7 @@ private static Matcher addedDefaultTrait(ValidatedResult result) .description("member to have a default trait with warning") .addAssertion(member -> member.hasTrait(DefaultTrait.class), member -> "no @default trait") - .addEventAssertion("UpgradeModel", Severity.WARNING, "@default") + .addEventAssertion(Validator.MODEL_DEPRECATION, Severity.WARNING, "@default") .build(); } @@ -180,14 +183,32 @@ private static Matcher shapeTargetsInvalidPrimitive(ValidatedResult member.getTarget().getName().startsWith("Primitive") && member.getTarget().getNamespace().equals(Prelude.NAMESPACE), member -> "member does not target a primitive prelude shape") - .addEventAssertion("UpgradeModel", Severity.ERROR, "removed in Smithy IDL 2.0") + .addEventAssertion(Validator.MODEL_DEPRECATION, Severity.ERROR, "removed in Smithy IDL 2.0") .build(); } private static Matcher v2ShapeUsesBoxTrait(ValidatedResult result) { return ShapeMatcher.builderFor(MemberShape.class, result) .description("v2 shape uses box trait") - .addEventAssertion("UpgradeModel", Severity.ERROR, "@box is not supported") + .addEventAssertion(Validator.MODEL_DEPRECATION, Severity.ERROR, "@box is not supported") .build(); } + + @Test + public void addSyntheticBoxTrait() { + Model model = Model.assembler() + .addImport(getClass().getResource("upgrade/does-not-introduce-conflict/main.smithy")) + .assemble() + .unwrap(); + + assertThat(hasBoxTrait(model, "smithy.example#Foo$alreadyDefault"), is(false)); + assertThat(hasBoxTrait(model, "smithy.example#Foo$alreadyRequired"), is(false)); + assertThat(hasBoxTrait(model, "smithy.example#Foo$boxedMember"), is(true)); + assertThat(hasBoxTrait(model, "smithy.example#Foo$explicitlyBoxedTarget"), is(true)); + assertThat(hasBoxTrait(model, "smithy.example#Foo$previouslyBoxedTarget"), is(true)); + } + + private boolean hasBoxTrait(Model model, String shape) { + return model.expectShape(ShapeId.from(shape)).hasTrait(BoxTrait.class); + } } diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/loader/PendingShapeTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/loader/PendingShapeTest.java deleted file mode 100644 index e2f445708a3..00000000000 --- a/smithy-model/src/test/java/software/amazon/smithy/model/loader/PendingShapeTest.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package software.amazon.smithy.model.loader; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.hamcrest.Matchers.hasSize; - -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import software.amazon.smithy.model.SourceLocation; -import software.amazon.smithy.model.shapes.ShapeId; -import software.amazon.smithy.model.validation.ValidationEvent; -import software.amazon.smithy.utils.SetUtils; - -public class PendingShapeTest { - @Test - public void cannotMergeIncompatibleShapes() { - PendingShape a = PendingShape.create( - ShapeId.from("test#A"), - SourceLocation.NONE, - SetUtils.of(), - (created) -> {}); - PendingShape b = PendingShape.create( - ShapeId.from("test#B"), - SourceLocation.NONE, - SetUtils.of(), - (created) -> {}); - - Assertions.assertThrows(IllegalArgumentException.class, () -> { - PendingShape.mergeIntoLeft(a, b); - }); - } - - @Test - public void mergesIntoExistingPendingConflict() { - PendingShape a = PendingShape.create( - ShapeId.from("test#A"), - SourceLocation.NONE, - SetUtils.of(ShapeId.from("test#X")), - (created) -> {}); - PendingShape b = PendingShape.create( - ShapeId.from("test#A"), - SourceLocation.NONE, - SetUtils.of(ShapeId.from("test#Y")), - (created) -> {}); - - PendingShape merged = PendingShape.mergeIntoLeft(PendingShape.mergeIntoLeft(a, b), a); - - assertThat(merged.getPendingShapes(), containsInAnyOrder(ShapeId.from("test#X"), ShapeId.from("test#Y"))); - } - - @Test - public void callsOnlyTheFirstMergedBuilder() { - Set called = new HashSet<>(); - PendingShape a = PendingShape.create( - ShapeId.from("test#A"), - SourceLocation.NONE, - SetUtils.of(ShapeId.from("test#X")), - (created) -> called.add("a")); - PendingShape b = PendingShape.create( - ShapeId.from("test#A"), - SourceLocation.NONE, - SetUtils.of(ShapeId.from("test#Y")), - (created) -> called.add("b")); - PendingShape merged = PendingShape.mergeIntoLeft(a, b); - merged.buildShapes(Collections.emptyMap()); - - assertThat(called, contains("a", "b")); - } - - @Test - public void findsUnresolvedShapesForEachPendingShape() { - PendingShape a = PendingShape.create( - ShapeId.from("test#A"), - SourceLocation.NONE, - SetUtils.of(ShapeId.from("test#X")), - (created) -> {}); - PendingShape b = PendingShape.create( - ShapeId.from("test#A"), - SourceLocation.NONE, - SetUtils.of(ShapeId.from("test#Y")), - (created) -> {}); - PendingShape merged = PendingShape.mergeIntoLeft(a, b); - - List events = merged.unresolved(Collections.emptyMap(), Collections.emptyMap()); - - assertThat(events, hasSize(2)); - } -} diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/loader/ValidSmithyModelLoaderRunnerTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/loader/ValidSmithyModelLoaderRunnerTest.java index d825731396f..cb20e4d3089 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/loader/ValidSmithyModelLoaderRunnerTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/loader/ValidSmithyModelLoaderRunnerTest.java @@ -23,6 +23,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import software.amazon.smithy.model.Model; @@ -74,6 +75,10 @@ public void parserRunnerTest(String file) { e); } + validateMatch(result, expected, file); + } + + private void validateMatch(Model result, Model expected, String file) { if (!result.equals(expected)) { ModelSerializer serializer = ModelSerializer.builder().build(); throw new IllegalStateException(String.format( @@ -106,4 +111,59 @@ public static Collection data() throws Exception { throw new RuntimeException(e); } } + + @Test + public void canAddTraitsToForwardReferenceMembersWithUseStatements() { + Model result = Model.assembler() + .addImport(ValidSmithyModelLoaderRunnerTest.class.getResource("forwardrefs/use/use.smithy")) + .addModel(shared) + .assemble() + .unwrap(); + Model expected = Model.assembler() + .addImport(ValidSmithyModelLoaderRunnerTest.class.getResource("forwardrefs/use/result.json")) + .addModel(shared) + .assemble() + .unwrap(); + + validateMatch(result, expected, "forwardrefs/use-shapes.smithy"); + } + + @Test + public void canAddTraitsToForwardReferenceMembersWithNoUseStatements() { + Model result = Model.assembler() + .addImport(ValidSmithyModelLoaderRunnerTest.class.getResource("forwardrefs/use/no-use.smithy")) + .addModel(shared) + .assemble() + .unwrap(); + Model expected = Model.assembler() + .addImport(ValidSmithyModelLoaderRunnerTest.class + .getResource("forwardrefs/use/result.json")) + .addModel(shared) + .assemble() + .unwrap(); + + validateMatch(result, expected, "forwardrefs/user/no-use.smithy"); + } + + @Test + public void canHandleForwardRefsInResourceProperties() { + Model modelA = Model.assembler() + .addImport(ValidSmithyModelLoaderRunnerTest.class + .getResource("forwardrefs/resource/operation.smithy")) + .assemble() + .unwrap(); + Model result = Model.assembler() + .addModel(modelA) + .addImport(ValidSmithyModelLoaderRunnerTest.class + .getResource("forwardrefs/resource/resource.smithy")) + .assemble() + .unwrap(); + Model expected = Model.assembler() + .addImport(ValidSmithyModelLoaderRunnerTest.class + .getResource("forwardrefs/resource/result.json")) + .assemble() + .unwrap(); + + validateMatch(result, expected, "forwardrefs/resource/operation.smithy"); + } } diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/validation/testrunner/SmithyTestSuiteTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/validation/testrunner/SmithyTestSuiteTest.java index 52dd48a82c1..cd7efeac862 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/validation/testrunner/SmithyTestSuiteTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/validation/testrunner/SmithyTestSuiteTest.java @@ -47,7 +47,7 @@ public void runsCaseWithFile() { .run(); assertThat(result.getFailedResults().size(), is(0)); - assertThat(result.getSuccessCount(), is(3)); + assertThat(result.getSuccessCount(), is(4)); } @Test diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/default-trait-in-1.0.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/default-trait-in-1.0.errors new file mode 100644 index 00000000000..90ac92e2a97 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/default-trait-in-1.0.errors @@ -0,0 +1 @@ +[ERROR] smithy.example#Foo$bar: The @default trait can only be used in Smithy 2.0 or later | Model diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/default-trait-in-1.0.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/default-trait-in-1.0.smithy new file mode 100644 index 00000000000..c0c24e6f35f --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/default-trait-in-1.0.smithy @@ -0,0 +1,7 @@ +$version: "1.0" +namespace smithy.example + +structure Foo { + @default("Hello") + bar: String +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/default-trait-in-implicit-1.0.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/default-trait-in-implicit-1.0.errors new file mode 100644 index 00000000000..90ac92e2a97 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/default-trait-in-implicit-1.0.errors @@ -0,0 +1 @@ +[ERROR] smithy.example#Foo$bar: The @default trait can only be used in Smithy 2.0 or later | Model diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/default-trait-in-implicit-1.0.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/default-trait-in-implicit-1.0.smithy new file mode 100644 index 00000000000..b35d781adc3 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/default-trait-in-implicit-1.0.smithy @@ -0,0 +1,6 @@ +namespace smithy.example + +structure Foo { + @default("Hello") + bar: String +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/detects-default-out-of-range.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/detects-default-out-of-range.errors index b86996001ce..50995c96aee 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/detects-default-out-of-range.errors +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/detects-default-out-of-range.errors @@ -1,11 +1,11 @@ -[WARNING] -: Smithy IDL 1.0 is deprecated. Please upgrade to Smithy IDL 2.0. | Model -[WARNING] smithy.example#Integers$noRange: Add the @default trait to this member to make it forward compatible with Smithy IDL 2.0 | UpgradeModel -[WARNING] smithy.example#Integers$noRange: This member targets smithy.api#PrimitiveInteger which was removed in Smithy IDL 2.0. Target smithy.api#Integer instead | UpgradeModel -[WARNING] smithy.example#Integers$valid: Add the @default trait to this member to make it forward compatible with Smithy IDL 2.0 | UpgradeModel -[WARNING] smithy.example#Integers$valid: This member targets smithy.api#PrimitiveInteger which was removed in Smithy IDL 2.0. Target smithy.api#Integer instead | UpgradeModel -[WARNING] smithy.example#Integers$invalidMin: Add the @default trait to this member to make it forward compatible with Smithy IDL 2.0 | UpgradeModel -[WARNING] smithy.example#Integers$invalidMin: Cannot add the @default trait to this member due to a minimum range constraint. | UpgradeModel -[WARNING] smithy.example#Integers$invalidMin: This member targets smithy.api#PrimitiveInteger which was removed in Smithy IDL 2.0. Target smithy.api#Integer instead | UpgradeModel -[WARNING] smithy.example#Integers$invalidMax: Add the @default trait to this member to make it forward compatible with Smithy IDL 2.0 | UpgradeModel -[WARNING] smithy.example#Integers$invalidMax: Cannot add the @default trait to this member due to a maximum range constraint. | UpgradeModel -[WARNING] smithy.example#Integers$invalidMax: This member targets smithy.api#PrimitiveInteger which was removed in Smithy IDL 2.0. Target smithy.api#Integer instead | UpgradeModel +[WARNING] -: Smithy IDL 1.0 is deprecated. Please upgrade to 2.0. | ModelDeprecation +[WARNING] smithy.example#Integers$noRange: Add the @default trait to this member to make it forward compatible with Smithy IDL 2.0 | ModelDeprecation +[WARNING] smithy.example#Integers$noRange: This member targets smithy.api#PrimitiveInteger which was removed in Smithy IDL 2.0. Target smithy.api#Integer instead | ModelDeprecation +[WARNING] smithy.example#Integers$valid: Add the @default trait to this member to make it forward compatible with Smithy IDL 2.0 | ModelDeprecation +[WARNING] smithy.example#Integers$valid: This member targets smithy.api#PrimitiveInteger which was removed in Smithy IDL 2.0. Target smithy.api#Integer instead | ModelDeprecation +[WARNING] smithy.example#Integers$invalidMin: Add the @default trait to this member to make it forward compatible with Smithy IDL 2.0 | ModelDeprecation +[WARNING] smithy.example#Integers$invalidMin: Cannot add the @default trait to this member due to a minimum range constraint. | ModelDeprecation +[WARNING] smithy.example#Integers$invalidMin: This member targets smithy.api#PrimitiveInteger which was removed in Smithy IDL 2.0. Target smithy.api#Integer instead | ModelDeprecation +[WARNING] smithy.example#Integers$invalidMax: Add the @default trait to this member to make it forward compatible with Smithy IDL 2.0 | ModelDeprecation +[WARNING] smithy.example#Integers$invalidMax: Cannot add the @default trait to this member due to a maximum range constraint. | ModelDeprecation +[WARNING] smithy.example#Integers$invalidMax: This member targets smithy.api#PrimitiveInteger which was removed in Smithy IDL 2.0. Target smithy.api#Integer instead | ModelDeprecation diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/detects-duplicates-with-members.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/detects-duplicates-with-members.errors index 8f0f08466a8..705f4f95561 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/detects-duplicates-with-members.errors +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/detects-duplicates-with-members.errors @@ -1 +1 @@ -[ERROR] -: Conflicting shape definition for `foo.baz#Foo` found at | Model +[ERROR] foo.baz#Foo: Conflicting shape definition for `foo.baz#Foo` found at | Model diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/metadata-syntactic-shape-ids.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/metadata-syntactic-shape-ids.errors new file mode 100644 index 00000000000..2e21d113311 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/metadata-syntactic-shape-ids.errors @@ -0,0 +1 @@ +[DANGER] -: Syntactic shape ID `smithy.example#enum` does not resolve to a valid shape ID: `smithy.example#enum`. Did you mean to quote this string? Are you missing a model file? | SyntacticShapeIdTarget diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/metadata-syntactic-shape-ids.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/metadata-syntactic-shape-ids.smithy new file mode 100644 index 00000000000..a5383a31462 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/metadata-syntactic-shape-ids.smithy @@ -0,0 +1,5 @@ +$version: "2.0" + +metadata foo = enum +metadata bar = smithy.example#enum +metadata baz = smithy.api#enum diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/mixins-detects-invalid-mixin.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/mixins-detects-invalid-mixin.errors index a7cea9f485a..7376a606f9b 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/mixins-detects-invalid-mixin.errors +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/mixins-detects-invalid-mixin.errors @@ -1 +1 @@ -[ERROR] smithy.example#B: Unable to resolve mixins; attempted to mixin shapes with no mixin trait: [smithy.example#A] | Model +[ERROR] smithy.example#B: Attempted to use smithy.example#A as a mixin, but it is not marked with the mixin trait | Target diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/sets-are-considered-lists.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/sets-are-considered-lists.errors index 3256ba0ac55..c0ad39ea725 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/sets-are-considered-lists.errors +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/sets-are-considered-lists.errors @@ -1,2 +1,2 @@ -[WARNING] smithy.example#someSet: Set shapes are deprecated and have been removed in Smithy IDL v2. Use a list shape with the @uniqueItems trait instead. | Set -[WARNING] -: Smithy IDL 1.0 is deprecated. Please upgrade to Smithy IDL 2.0. | Model +[WARNING] smithy.example#someSet: Set shapes are deprecated and have been removed in Smithy IDL v2. Use a list shape with the @uniqueItems trait instead. | ModelDeprecation +[WARNING] -: Smithy IDL 1.0 is deprecated. Please upgrade to 2.0. | ModelDeprecation diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/use-statement-undefined-shape.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/use-statement-undefined-shape.errors new file mode 100644 index 00000000000..91ddc76a8dc --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/use-statement-undefined-shape.errors @@ -0,0 +1 @@ +[WARNING] -: Use statement refers to undefined shape: smithy.foo.baz#Nope | Model diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/use-statement-undefined-shape.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/use-statement-undefined-shape.smithy new file mode 100644 index 00000000000..90ef22140e2 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/use-statement-undefined-shape.smithy @@ -0,0 +1,4 @@ +$version: "2.0" +namespace smithy.example + +use smithy.foo.baz#Nope diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/sets/valid-sets.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/sets/valid-sets.errors index 3083a2b6fee..e69de29bb2d 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/sets/valid-sets.errors +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/sets/valid-sets.errors @@ -1,16 +0,0 @@ -[WARNING] smithy.example#StringSet: Set shapes are deprecated and have been removed in Smithy IDL v2. Use a list shape with the @uniqueItems trait instead. | Set -[WARNING] smithy.example#BlobSet: Set shapes are deprecated and have been removed in Smithy IDL v2. Use a list shape with the @uniqueItems trait instead. | Set -[WARNING] smithy.example#ByteSet: Set shapes are deprecated and have been removed in Smithy IDL v2. Use a list shape with the @uniqueItems trait instead. | Set -[WARNING] smithy.example#ShortSet: Set shapes are deprecated and have been removed in Smithy IDL v2. Use a list shape with the @uniqueItems trait instead. | Set -[WARNING] smithy.example#IntegerSet: Set shapes are deprecated and have been removed in Smithy IDL v2. Use a list shape with the @uniqueItems trait instead. | Set -[WARNING] smithy.example#LongSet: Set shapes are deprecated and have been removed in Smithy IDL v2. Use a list shape with the @uniqueItems trait instead. | Set -[WARNING] smithy.example#BigIntSet: Set shapes are deprecated and have been removed in Smithy IDL v2. Use a list shape with the @uniqueItems trait instead. | Set -[WARNING] smithy.example#BigDecimalSet: Set shapes are deprecated and have been removed in Smithy IDL v2. Use a list shape with the @uniqueItems trait instead. | Set -[WARNING] smithy.example#TimestampSet: Set shapes are deprecated and have been removed in Smithy IDL v2. Use a list shape with the @uniqueItems trait instead. | Set -[WARNING] smithy.example#BooleanSet: Set shapes are deprecated and have been removed in Smithy IDL v2. Use a list shape with the @uniqueItems trait instead. | Set -[WARNING] smithy.example#ListSet: Set shapes are deprecated and have been removed in Smithy IDL v2. Use a list shape with the @uniqueItems trait instead. | Set -[WARNING] smithy.example#SetSet: Set shapes are deprecated and have been removed in Smithy IDL v2. Use a list shape with the @uniqueItems trait instead. | Set -[WARNING] smithy.example#MapSet: Set shapes are deprecated and have been removed in Smithy IDL v2. Use a list shape with the @uniqueItems trait instead. | Set -[WARNING] smithy.example#StructSet: Set shapes are deprecated and have been removed in Smithy IDL v2. Use a list shape with the @uniqueItems trait instead. | Set -[WARNING] smithy.example#UnionSet: Set shapes are deprecated and have been removed in Smithy IDL v2. Use a list shape with the @uniqueItems trait instead. | Set -[WARNING] -: Smithy IDL 1.0 is deprecated. Please upgrade to Smithy IDL 2.0. | Model diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/forwardrefs/resource/operation.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/forwardrefs/resource/operation.smithy new file mode 100644 index 00000000000..7590fbad92c --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/forwardrefs/resource/operation.smithy @@ -0,0 +1,5 @@ +$version: "1.0" + +namespace smithy.example + +operation GetFoo {} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/forwardrefs/resource/resource.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/forwardrefs/resource/resource.smithy new file mode 100644 index 00000000000..a9873a5951a --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/forwardrefs/resource/resource.smithy @@ -0,0 +1,7 @@ +$version: "1.0" + +namespace smithy.example + +resource Smithy { + operations: [GetFoo] +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/forwardrefs/resource/result.json b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/forwardrefs/resource/result.json new file mode 100644 index 00000000000..aa6d889e60b --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/forwardrefs/resource/result.json @@ -0,0 +1,22 @@ +{ + "smithy": "2.0", + "shapes": { + "smithy.example#GetFoo": { + "type": "operation", + "input": { + "target": "smithy.api#Unit" + }, + "output": { + "target": "smithy.api#Unit" + } + }, + "smithy.example#Smithy": { + "type": "resource", + "operations": [ + { + "target": "smithy.example#GetFoo" + } + ] + } + } +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/forwardrefs/use/no-use.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/forwardrefs/use/no-use.smithy new file mode 100644 index 00000000000..0440afe8d40 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/forwardrefs/use/no-use.smithy @@ -0,0 +1,26 @@ +$version: "2.0" + +namespace smithy.example + +use smithy.api#String +use smithy.api#Integer +use smithy.api#Long +use foo.example#Widget + +list MyList { + member: String, +} + +list Widgets { + member: Widget +} + +structure Struct { + a: Integer, + b: Long, +} + +// This does not have a use statement, so it needs to resolve to +// an absolute member of Widget and then deconstruct the root shape +// to apply the member trait. +apply foo.example#Widget$id @deprecated diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/forwardrefs/use/result.json b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/forwardrefs/use/result.json new file mode 100644 index 00000000000..5987bab01ee --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/forwardrefs/use/result.json @@ -0,0 +1,39 @@ +{ + "smithy": "2.0", + "shapes": { + "smithy.example#MyList": { + "type": "list", + "member": { + "target": "smithy.api#String" + } + }, + "smithy.example#Struct": { + "type": "structure", + "members": { + "a": { + "target": "smithy.api#Integer" + }, + "b": { + "target": "smithy.api#Long" + } + } + }, + "smithy.example#Widgets": { + "type": "list", + "member": { + "target": "foo.example#Widget" + } + }, + "foo.example#Widget": { + "type": "structure", + "members": { + "id": { + "target": "smithy.api#String", + "traits": { + "smithy.api#deprecated": {} + } + } + } + } + } +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/forwardrefs/use/use.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/forwardrefs/use/use.smithy new file mode 100644 index 00000000000..f234c73c151 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/forwardrefs/use/use.smithy @@ -0,0 +1,28 @@ +$version: "2.0" + +namespace smithy.example + +use smithy.api#String +use smithy.api#Integer +use smithy.api#Long +use foo.example#Widget + +list MyList { + member: String, +} + +list Widgets { + member: Widget +} + +structure Struct { + a: Integer, + b: Long, +} + +// This uses a forward reference to a shape in another +// namespace brought in via a use statement, applies a +// trait to its member. Further, Widget was previously +// built and included in the model being built, and updating +// its member requires deconstructing the root built shape. +apply Widget$id @deprecated diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/unsupported-version.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/version/unsupported-version.smithy similarity index 100% rename from smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/unsupported-version.smithy rename to smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/version/unsupported-version.smithy diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/version/version-set-more-than-once.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/version/version-set-more-than-once.smithy new file mode 100644 index 00000000000..9203d46aeba --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/version/version-set-more-than-once.smithy @@ -0,0 +1,3 @@ +// Parse error at line 3, column 11 near `"2`: Duplicate control statement `version` +$version: "2" +$version: "2" diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/upgrade/does-not-introduce-conflict/main.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/upgrade/does-not-introduce-conflict/main.smithy index 90392a314b0..ecf9bd1c95e 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/upgrade/does-not-introduce-conflict/main.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/upgrade/does-not-introduce-conflict/main.smithy @@ -3,7 +3,6 @@ $version: "1.0" namespace smithy.example structure Foo { - @default(0) alreadyDefault: PrimitiveInteger, @required diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/__shared.json b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/__shared.json index ada4ac04e89..19dbe7feaaf 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/__shared.json +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/__shared.json @@ -2,7 +2,12 @@ "smithy": "1.0", "shapes": { "foo.example#Widget": { - "type": "structure" + "type": "structure", + "members": { + "id": { + "target": "smithy.api#String" + } + } } } } diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/doc-comments/doc-comments-ignored-after-shape.json b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/doc-comments/doc-comments-ignored-after-shape.json new file mode 100644 index 00000000000..3c6a38a00c2 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/doc-comments/doc-comments-ignored-after-shape.json @@ -0,0 +1,28 @@ +{ + "smithy": "2.0", + "shapes": { + "smithy.example#Features": { + "type": "string", + "traits": { + "smithy.api#enum": [ + { + "name": "X", + "value": "X" + } + ] + } + }, + "smithy.example#Foo": { + "type": "map", + "key": { + "target": "smithy.example#Features" + }, + "value": { + "target": "smithy.api#Boolean" + }, + "traits": { + "smithy.api#documentation": "X" + } + } + } +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/doc-comments/doc-comments-ignored-after-shape.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/doc-comments/doc-comments-ignored-after-shape.smithy new file mode 100644 index 00000000000..cd63ce414c3 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/doc-comments/doc-comments-ignored-after-shape.smithy @@ -0,0 +1,18 @@ +// The documentation comment in the enum is within the scope of defining a shape. +// Because it's out of position and not before anything that consumes it, it +// should not be applied to the next shape definition, Foo. Applying it to Foo +// would not only be invalid, it would also cause a conflict with its actual +// documentation trait. +$version: "2.0" +namespace smithy.example + +@enum([ + /// Invalid! + { name: "X", value: "X"} +]) string Features + +@documentation("X") +map Foo { + key: Features, + value: Boolean +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/doc-comments.json b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/doc-comments/doc-comments.json similarity index 100% rename from smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/doc-comments.json rename to smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/doc-comments/doc-comments.json diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/doc-comments.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/doc-comments/doc-comments.smithy similarity index 100% rename from smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/doc-comments.smithy rename to smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/doc-comments/doc-comments.smithy diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/metadata.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/metadata.smithy index 848a8bb5ce9..4537ed3ff3a 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/metadata.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/metadata.smithy @@ -63,5 +63,3 @@ metadata trailing_commas2 = ["a", "b",] // Unquoted strings resolve to shape IDs. metadata shape_id = smithy.api#String - -namespace foo.baz diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/optional-commas.json b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/optional-commas.json index 61d7bb38930..5d034f4c063 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/optional-commas.json +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/optional-commas.json @@ -1,5 +1,5 @@ { - "smithy": "1.0", + "smithy": "2.0", "shapes": { "example.weather#Weather": { "type": "service", diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/optional-commas.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/optional-commas.smithy index af770a08455..269ba201af3 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/optional-commas.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/optional-commas.smithy @@ -1,3 +1,4 @@ +$version: "2.0" namespace example.weather // Commas are whitespace. diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/use/use-shapes.json b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/use/use-shapes.json index f11a43ea29b..19734780c9a 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/use/use-shapes.json +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/use/use-shapes.json @@ -1,5 +1,5 @@ { - "smithy": "1.0", + "smithy": "2.0", "shapes": { "smithy.example#MyList": { "type": "list", @@ -17,6 +17,12 @@ "target": "smithy.api#Long" } } + }, + "smithy.example#Widgets": { + "type": "list", + "member": { + "target": "foo.example#Widget" + } } } } diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/use/use-shapes.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/use/use-shapes.smithy index 0348b12d525..820ea9426c1 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/use/use-shapes.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/use/use-shapes.smithy @@ -1,15 +1,20 @@ -$version: "1.0" +$version: "2.0" namespace smithy.example use smithy.api#String use smithy.api#Integer use smithy.api#Long +use foo.example#Widget list MyList { member: String, } +list Widgets { + member: Widget +} + structure Struct { a: Integer, b: Long, diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/validation/testrunner/testrunner/valid/can-ignore-1.0-warnings.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/validation/testrunner/testrunner/valid/can-ignore-1.0-warnings.errors new file mode 100644 index 00000000000..e69de29bb2d diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/validation/testrunner/testrunner/valid/can-ignore-1.0-warnings.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/validation/testrunner/testrunner/valid/can-ignore-1.0-warnings.smithy new file mode 100644 index 00000000000..d7197c24d72 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/validation/testrunner/testrunner/valid/can-ignore-1.0-warnings.smithy @@ -0,0 +1,6 @@ +$version: "1.0" + +namespace example.smithy + +@enum([{name: "FOO", value: "foo"}]) +string Hello From 1765d12bab273f09339f34db73e95db9cb5fd45a Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Wed, 3 Aug 2022 09:25:57 -0700 Subject: [PATCH 02/20] Fix edge case in model upgrader --- .../amazon/smithy/model/loader/ModelUpgrader.java | 2 +- .../amazon/smithy/model/loader/ModelUpgraderTest.java | 1 + .../upgrade/does-not-introduce-conflict/main.smithy | 11 ++++++++++- .../does-not-introduce-conflict/upgraded.smithy | 7 ++++++- 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelUpgrader.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelUpgrader.java index 853ec34ed24..2eca4d94a02 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelUpgrader.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelUpgrader.java @@ -167,7 +167,7 @@ private boolean isMemberImplicitlyBoxed( && !member.hasTrait(DefaultTrait.class) // don't add box if it has a default trait. && !member.hasTrait(BoxTrait.class) // don't add box again && !REMOVED_PRIMITIVE_SHAPES.containsKey(target.getId()) - && (target.hasTrait(BoxTrait.class) || HAD_DEFAULT_VALUE_IN_1_0.contains(target.getType())); + && (target.hasTrait(BoxTrait.class) || PREVIOUSLY_BOXED.contains(target.getId())); } private boolean isZeroValidDefault(MemberShape member) { diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/loader/ModelUpgraderTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/loader/ModelUpgraderTest.java index 3e3a58e02b3..6134a3de69c 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/loader/ModelUpgraderTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/loader/ModelUpgraderTest.java @@ -206,6 +206,7 @@ public void addSyntheticBoxTrait() { assertThat(hasBoxTrait(model, "smithy.example#Foo$boxedMember"), is(true)); assertThat(hasBoxTrait(model, "smithy.example#Foo$explicitlyBoxedTarget"), is(true)); assertThat(hasBoxTrait(model, "smithy.example#Foo$previouslyBoxedTarget"), is(true)); + assertThat(hasBoxTrait(model, "smithy.example#Foo$customPrimitiveLong"), is(false)); } private boolean hasBoxTrait(Model model, String shape) { diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/upgrade/does-not-introduce-conflict/main.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/upgrade/does-not-introduce-conflict/main.smithy index ecf9bd1c95e..d7f54109f18 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/upgrade/does-not-introduce-conflict/main.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/upgrade/does-not-introduce-conflict/main.smithy @@ -11,10 +11,19 @@ structure Foo { @box boxedMember: PrimitiveInteger, + // smithy.api#Integer is not boxed in 2.0 but was in 1.0 so this member is + // considered implicitly boxed. previouslyBoxedTarget: Integer, - explicitlyBoxedTarget: BoxedInteger + explicitlyBoxedTarget: BoxedInteger, + + // This shape cannot be boxed because it targeted a long that was a primitive. + // This shape also cannot be default because it's required. + @required + customPrimitiveLong: MyPrimitiveLong } @box integer BoxedInteger + +long MyPrimitiveLong diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/upgrade/does-not-introduce-conflict/upgraded.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/upgrade/does-not-introduce-conflict/upgraded.smithy index 99126e22512..0ad2855ff13 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/upgrade/does-not-introduce-conflict/upgraded.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/upgrade/does-not-introduce-conflict/upgraded.smithy @@ -13,7 +13,12 @@ structure Foo { previouslyBoxedTarget: Integer, - explicitlyBoxedTarget: BoxedInteger + explicitlyBoxedTarget: BoxedInteger, + + @required + customPrimitiveLong: MyPrimitiveLong } integer BoxedInteger + +long MyPrimitiveLong From 373e85c4d23e3a2ff04ea93751c511ced21e60e4 Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Wed, 3 Aug 2022 16:20:01 -0700 Subject: [PATCH 03/20] Leave Primitive* prelude shapes in 2.0 We previously rewrote models that target smithy.api#Primtive* shapes to target their prelude counterparts that do not have the Primitive prefix. For example, smithy.api#PrimitiveInteger would become smithy.api#Integer. The rationale being that because optionality is now controlled on members rather than based on the shape targeted by a member, there is no longer a reason to use Primitive* shapes in 2.0 models. The problem is that rewriting the shape targets during the loading process can remove information that tooling might need to determine if a shape was considered optional in IDL 1.0 (usually in fringe cases). If a member previously targets PrimitiveInteger and is rewritten to target Integer, but the member is also marked as required, then the ModelUpgrader can't add the default trait to the member. If optionality isn't determined based on the required trait, then a tool has no way to know that a member that targets Integer previously targeted PrimitiveInteger, thereby losing information. Instead of removing Primitive* shapes, we will keep them in 2.0 and mark them as deprecated. To point this out to end users, I added validation (that really should have already existed) to detect when a shape refers to a deprecated shape. However, because we don't want to break previous test cases and because models should be able to deprecate shapes without breaking test cases, DeprecatedShape does not need to be explicitly handled by Smithy errorfile test runners. This matches the special casing added for DeprecatedTrait and ModelDeprecation. --- designs/defaults-and-model-evolution.md | 20 +-- docs/source-2.0/spec/model.rst | 42 ++++++ .../cli/commands/Upgrade1to2Command.java | 26 ---- ...rimitive.v1.smithy => primitive.v1.smithy} | 0 .../upgrade/cases/primitive.v2.smithy | 29 +++++ .../upgrade/cases/remove-primitive.v2.smithy | 29 ----- .../smithy/model/knowledge/NullableIndex.java | 33 +---- .../smithy/model/loader/ModelUpgrader.java | 52 ++------ .../validation/testrunner/SmithyTestCase.java | 3 +- .../validators/TargetValidator.java | 121 ++++++++++++------ .../smithy/model/loader/prelude-1.0.smithy | 23 +--- .../amazon/smithy/model/loader/prelude.smithy | 42 ++++++ .../model/knowledge/NullableIndexTest.java | 15 --- .../model/loader/ModelUpgraderTest.java | 45 ++++--- .../detects-default-out-of-range.errors | 4 - .../deprecated-shape/deprecated-shape.errors | 4 + .../deprecated-shape/deprecated-shape.smithy | 22 ++++ .../loader/upgrade/all-1.0/upgraded.smithy | 14 +- .../upgraded.smithy | 6 +- .../upgrade/mixed-versions/upgraded.smithy | 2 +- 20 files changed, 284 insertions(+), 248 deletions(-) rename smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/{remove-primitive.v1.smithy => primitive.v1.smithy} (100%) create mode 100644 smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/primitive.v2.smithy delete mode 100644 smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/remove-primitive.v2.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/deprecated-shape/deprecated-shape.errors create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/deprecated-shape/deprecated-shape.smithy diff --git a/designs/defaults-and-model-evolution.md b/designs/defaults-and-model-evolution.md index 7f91226be00..f7b42a5c90b 100644 --- a/designs/defaults-and-model-evolution.md +++ b/designs/defaults-and-model-evolution.md @@ -135,7 +135,7 @@ This proposal: 1. Introduces a `@default` trait 2. Introduces a `@clientOptional` trait 3. Removes the `@box` trait -4. Removes the `Primitive*` shapes from the prelude +4. Deprecates the `Primitive*` shapes from the prelude 5. Make the optionality of a structure completely controlled by members rather than the shape targeted by a member @@ -195,19 +195,11 @@ optionality controls from shapes to structure members. Smithy IDL 2.0 will: version bump of the service. 3. Remove the `@box` trait from the Smithy 2.0 prelude and fail to load models that contain the `@box` trait. -4. Remove the `PrimitiveBoolean`, `PrimitiveShort`, `PrimitiveInteger`, - `PrimitiveLong`, `PrimitiveFloat`, and `PrimitiveDouble` shapes from the - Smithy 2.0 prelude. IDL 2.0 models will fail if they use these shapes. -5. Update the Smithy IDL 2.0 model loader implementation to be able to load - Smithy 1.0 models alongside Smithy 2.0 models. - 1. Inject an appropriate `@default` trait on structure members that targeted - shapes with a default zero value and were not marked with the `@box` - trait. - 2. Replace the `@box` trait with `@clientOptional` on structure members. - 3. Remove the `@box` trait from non-members. - 4. Rewrite members that target one of the removed Primitive* shapes to - target the corresponding non-primitive shape in the prelude (for example, - `PrimitiveInteger` is rewritten to target `Integer`). +4. Deprecate the `PrimitiveBoolean`, `PrimitiveShort`, `PrimitiveInteger`, + `PrimitiveLong`, `PrimitiveFloat`, and `PrimitiveDouble` shapes in the + Smithy 2.0 prelude. These shapes are treated exactly the same as their + prelude counterparts not prefixed with "Primitive", so there is no need to + use these shapes. ## `@default` trait diff --git a/docs/source-2.0/spec/model.rst b/docs/source-2.0/spec/model.rst index c75447577e5..1b3489f092f 100644 --- a/docs/source-2.0/spec/model.rst +++ b/docs/source-2.0/spec/model.rst @@ -1288,6 +1288,48 @@ referenced from within any namespace using a relative shape ID. @unitType structure Unit {} + @deprecated( + message: "Use Boolean instead, and add the @default trait to structure members that targets this shape", + since: "2.0" + ) + boolean PrimitiveBoolean + + @deprecated( + message: "Use Byte instead, and add the @default trait to structure members that targets this shape", + since: "2.0" + ) + byte PrimitiveByte + + @deprecated( + message: "Use Short instead, and add the @default trait to structure members that targets this shape", + since: "2.0" + ) + short PrimitiveShort + + @deprecated( + message: "Use Integer instead, and add the @default trait to structure members that targets this shape", + since: "2.0" + ) + integer PrimitiveInteger + + @deprecated( + message: "Use Long instead, and add the @default trait to structure members that targets this shape", + since: "2.0" + ) + long PrimitiveLong + + @deprecated( + message: "Use Float instead, and add the @default trait to structure members that targets this shape", + since: "2.0" + ) + float PrimitiveFloat + + @deprecated( + message: "Use Double instead, and add the @default trait to structure members that targets this shape", + since: "2.0" + ) + double PrimitiveDouble + .. _unit-type: diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/Upgrade1to2Command.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/Upgrade1to2Command.java index e47d0e3f887..66df4acbca5 100644 --- a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/Upgrade1to2Command.java +++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/Upgrade1to2Command.java @@ -46,7 +46,6 @@ import software.amazon.smithy.model.Model; import software.amazon.smithy.model.SourceLocation; import software.amazon.smithy.model.loader.ModelAssembler; -import software.amazon.smithy.model.loader.ParserUtils; import software.amazon.smithy.model.loader.Prelude; import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.NumberShape; @@ -282,8 +281,6 @@ protected Void getDefault(Shape shape) { } private void handleMemberShape(MemberShape shape) { - replacePrimitiveTarget(shape); - if (hasSyntheticDefault(shape)) { SourceLocation memberLocation = shape.getSourceLocation(); String padding = ""; @@ -309,29 +306,6 @@ private void handleMemberShape(MemberShape shape) { } } - private void replacePrimitiveTarget(MemberShape member) { - Shape target = completeModel.expectShape(member.getTarget()); - if (!Prelude.isPreludeShape(target) || !HAD_DEFAULT_VALUE_IN_1_0.contains(target.getType())) { - return; - } - - IdlAwareSimpleParser parser = new IdlAwareSimpleParser(writer.flush()); - parser.rewind(member.getSourceLocation()); - - parser.consumeUntilNoLongerMatches(character -> character != ':'); - parser.skip(); - parser.ws(); - - // Capture the start of the target identifier. - int start = parser.position(); - parser.consumeUntilNoLongerMatches(ParserUtils::isValidIdentifierCharacter); - - // Replace the target with the proper target. Note that we don't - // need to do any sort of mapping because smithy already upgraded - // the target, so we can just use the name of the target it added. - writer.replace(start, parser.position(), target.getId().getName()); - } - private boolean hasSyntheticDefault(MemberShape shape) { Optional defaultLocation = shape.getTrait(DefaultTrait.class) .map(Trait::getSourceLocation); diff --git a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/remove-primitive.v1.smithy b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/primitive.v1.smithy similarity index 100% rename from smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/remove-primitive.v1.smithy rename to smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/primitive.v1.smithy diff --git a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/primitive.v2.smithy b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/primitive.v2.smithy new file mode 100644 index 00000000000..c0b350c06af --- /dev/null +++ b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/primitive.v2.smithy @@ -0,0 +1,29 @@ +$version: "2.0" + +namespace com.example + +structure PrimitiveBearer { + @default(0) + int: PrimitiveInteger, + @default(false) + bool: PrimitiveBoolean, + @default(0) + byte: PrimitiveByte, + @default(0) + double: PrimitiveDouble, + @default(0) + float: PrimitiveFloat, + @default(0) + long: PrimitiveLong, + @default(0) + short: PrimitiveShort, + + @default(0) + handlesComments: // Nobody actually does this right? + PrimitiveShort, + + @required + handlesRequired: PrimitiveLong, + + handlesBox: PrimitiveByte, +} diff --git a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/remove-primitive.v2.smithy b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/remove-primitive.v2.smithy deleted file mode 100644 index 317493a4422..00000000000 --- a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/remove-primitive.v2.smithy +++ /dev/null @@ -1,29 +0,0 @@ -$version: "2.0" - -namespace com.example - -structure PrimitiveBearer { - @default(0) - int: Integer, - @default(false) - bool: Boolean, - @default(0) - byte: Byte, - @default(0) - double: Double, - @default(0) - float: Float, - @default(0) - long: Long, - @default(0) - short: Short, - - @default(0) - handlesComments: // Nobody actually does this right? - Short, - - @required - handlesRequired: Long, - - handlesBox: Byte, -} diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/knowledge/NullableIndex.java b/smithy-model/src/main/java/software/amazon/smithy/model/knowledge/NullableIndex.java index fbf67d888c8..c275f80aedb 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/knowledge/NullableIndex.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/knowledge/NullableIndex.java @@ -22,7 +22,6 @@ import software.amazon.smithy.model.loader.ModelAssembler; import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.Shape; -import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.model.shapes.ShapeType; import software.amazon.smithy.model.shapes.ToShapeId; import software.amazon.smithy.model.traits.BoxTrait; @@ -37,22 +36,13 @@ * An index that checks if a member is nullable. * *

Note: this index assumes Smithy 2.0 nullability semantics. - * There is basic support for detecting 1.0 models by detecting - * when a removed primitive prelude shape is targeted by a member. - * Beyond that, 1.0 models SHOULD be loaded through a {@link ModelAssembler} - * to upgrade them to IDL 2.0. + * 1.0 models SHOULD be loaded through a {@link ModelAssembler} + * to upgrade them in memory to IDL 2.0. Use + * {@link #isMemberNullableInV1(MemberShape)} to check if a shape + * is nullable according to Smithy 1.0 semantics. */ public class NullableIndex implements KnowledgeIndex { - private static final Set V1_REMOVED_PRIMITIVE_SHAPES = SetUtils.of( - ShapeId.from("smithy.api#PrimitiveBoolean"), - ShapeId.from("smithy.api#PrimitiveByte"), - ShapeId.from("smithy.api#PrimitiveShort"), - ShapeId.from("smithy.api#PrimitiveInteger"), - ShapeId.from("smithy.api#PrimitiveLong"), - ShapeId.from("smithy.api#PrimitiveFloat"), - ShapeId.from("smithy.api#PrimitiveDouble")); - private static final Set V1_INHERENTLY_BOXED = SetUtils.of( ShapeType.STRING, ShapeType.BLOB, @@ -148,12 +138,6 @@ public boolean isMemberNullable(MemberShape member) { * {@link ClientOptionalTrait}, while non-authoritative consumers like clients * must honor these traits. * - *

This method will also attempt to detect when a member targets a - * primitive prelude shape that was removed in Smithy IDL 2.0 to account - * for models that were created manually without passing through a - * ModelAssembler. If a member targets a removed primitive prelude shape, - * the member is considered non-null. - * * @param member Member to check. * @param checkMode The mode used when checking if the member is considered nullable. * @return Returns true if the member is optional. @@ -171,14 +155,7 @@ public boolean isMemberNullable(MemberShape member, CheckMode checkMode) { } // Structure members that are @required or @default are not nullable. - if (member.hasTrait(DefaultTrait.class) || member.hasTrait(RequiredTrait.class)) { - return false; - } - - // Detect if the member targets a 1.0 primitive prelude shape and the shape wasn't upgraded. - // These removed prelude shapes are impossible to appear in a 2.0 model, so it's safe to - // detect them and honor 1.0 semantics here. - return !V1_REMOVED_PRIMITIVE_SHAPES.contains(member.getTarget()); + return !member.hasTrait(DefaultTrait.class) && !member.hasTrait(RequiredTrait.class); case UNION: case SET: // Union and set members are never null. diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelUpgrader.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelUpgrader.java index 2eca4d94a02..50880762ffb 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelUpgrader.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelUpgrader.java @@ -18,9 +18,7 @@ import java.math.BigDecimal; import java.util.ArrayList; import java.util.EnumSet; -import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.Function; @@ -42,13 +40,12 @@ import software.amazon.smithy.model.validation.ValidatedResult; import software.amazon.smithy.model.validation.ValidationEvent; import software.amazon.smithy.model.validation.Validator; -import software.amazon.smithy.utils.MapUtils; +import software.amazon.smithy.utils.SetUtils; /** * Upgrades Smithy models from IDL v1 to IDL v2, specifically taking into - * account the removal of the Primitive* shapes from the prelude, the - * removal of the @box trait, the change in default value semantics of - * numbers and booleans, and the @default trait. + * account the removal of the box trait, the change in default value + * semantics of numbers and booleans, and the @default trait. */ final class ModelUpgrader { @@ -62,18 +59,15 @@ final class ModelUpgrader { ShapeType.DOUBLE, ShapeType.BOOLEAN); - /** Provides a mapping of prelude shapes that were removed to the shape to use instead. */ - private static final Map REMOVED_PRIMITIVE_SHAPES = MapUtils.of( - ShapeId.from("smithy.api#PrimitiveBoolean"), ShapeId.from("smithy.api#Boolean"), - ShapeId.from("smithy.api#PrimitiveByte"), ShapeId.from("smithy.api#Byte"), - ShapeId.from("smithy.api#PrimitiveShort"), ShapeId.from("smithy.api#Short"), - ShapeId.from("smithy.api#PrimitiveInteger"), ShapeId.from("smithy.api#Integer"), - ShapeId.from("smithy.api#PrimitiveLong"), ShapeId.from("smithy.api#Long"), - ShapeId.from("smithy.api#PrimitiveFloat"), ShapeId.from("smithy.api#Float"), - ShapeId.from("smithy.api#PrimitiveDouble"), ShapeId.from("smithy.api#Double")); - /** Shapes that were boxed in 1.0, but @box was removed in 2.0. */ - private static final Set PREVIOUSLY_BOXED = new HashSet<>(REMOVED_PRIMITIVE_SHAPES.values()); + private static final Set PREVIOUSLY_BOXED = SetUtils.of( + ShapeId.from("smithy.api#Boolean"), + ShapeId.from("smithy.api#Byte"), + ShapeId.from("smithy.api#Short"), + ShapeId.from("smithy.api#Integer"), + ShapeId.from("smithy.api#Long"), + ShapeId.from("smithy.api#Float"), + ShapeId.from("smithy.api#Double")); private final Model model; private final List events; @@ -117,13 +111,6 @@ private void upgradeV1Member(Version version, ShapeType containerType, MemberSha // This builder will become non-null if/when the member needs to be updated. MemberShape.Builder builder = null; - // Rewrite the member to target the non-removed prelude shape if it's known to be removed. - if (REMOVED_PRIMITIVE_SHAPES.containsKey(target.getId())) { - emitWhenTargetingRemovedPreludeShape(Severity.WARNING, member); - builder = createOrReuseBuilder(member, builder); - builder.target(REMOVED_PRIMITIVE_SHAPES.get(target.getId())); - } - // Add the @default trait to structure members when needed. if (shouldV1MemberHaveDefaultTrait(containerType, member, target)) { events.add(ValidationEvent.builder() @@ -166,7 +153,6 @@ private boolean isMemberImplicitlyBoxed( && containerType == ShapeType.STRUCTURE && !member.hasTrait(DefaultTrait.class) // don't add box if it has a default trait. && !member.hasTrait(BoxTrait.class) // don't add box again - && !REMOVED_PRIMITIVE_SHAPES.containsKey(target.getId()) && (target.hasTrait(BoxTrait.class) || PREVIOUSLY_BOXED.contains(target.getId())); } @@ -234,10 +220,6 @@ private MemberShape.Builder createOrReuseBuilder(MemberShape member, MemberShape @SuppressWarnings("deprecation") private void validateV2Member(MemberShape member) { - if (REMOVED_PRIMITIVE_SHAPES.containsKey(member.getTarget())) { - emitWhenTargetingRemovedPreludeShape(Severity.ERROR, member); - } - if (member.hasTrait(BoxTrait.class)) { events.add(ValidationEvent.builder() .id(Validator.MODEL_DEPRECATION) @@ -248,16 +230,4 @@ private void validateV2Member(MemberShape member) { .build()); } } - - private void emitWhenTargetingRemovedPreludeShape(Severity severity, MemberShape member) { - events.add(ValidationEvent.builder() - .id(Validator.MODEL_DEPRECATION) - .severity(severity) - .shape(member) - .sourceLocation(member) - .message("This member targets " + member.getTarget() + " which was removed in Smithy " - + "IDL " + Model.MODEL_VERSION + ". Target " - + REMOVED_PRIMITIVE_SHAPES.get(member.getTarget()) + " instead ") - .build()); - } } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/testrunner/SmithyTestCase.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/testrunner/SmithyTestCase.java index b06c550bf0b..9b85457da1a 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/validation/testrunner/SmithyTestCase.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/testrunner/SmithyTestCase.java @@ -144,7 +144,8 @@ private static boolean compareEvents(ValidationEvent expected, ValidationEvent a private boolean isModelDeprecationEvent(ValidationEvent event) { return event.getId().equals(Validator.MODEL_DEPRECATION) // Trait vendors should be free to deprecate a trait without breaking consumers. - || (event.getId().equals("DeprecatedTrait")); + || event.getId().equals("DeprecatedTrait") + || event.getId().equals("DeprecatedShape"); } private static String inferErrorFileLocation(String modelLocation) { diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/TargetValidator.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/TargetValidator.java index 5c847dcd1ac..26229574074 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/TargetValidator.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/TargetValidator.java @@ -21,7 +21,7 @@ import java.util.Collection; import java.util.List; import java.util.Locale; -import java.util.Optional; +import java.util.Map; import java.util.Set; import java.util.TreeSet; import software.amazon.smithy.model.Model; @@ -33,11 +33,14 @@ import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.model.shapes.ShapeType; +import software.amazon.smithy.model.traits.DeprecatedTrait; import software.amazon.smithy.model.traits.MixinTrait; import software.amazon.smithy.model.traits.TraitDefinition; import software.amazon.smithy.model.validation.AbstractValidator; +import software.amazon.smithy.model.validation.Severity; import software.amazon.smithy.model.validation.ValidationEvent; import software.amazon.smithy.utils.FunctionalUtils; +import software.amazon.smithy.utils.MapUtils; import software.amazon.smithy.utils.SetUtils; import software.amazon.smithy.utils.StringUtils; @@ -50,6 +53,18 @@ public final class TargetValidator extends AbstractValidator { private static final Set INVALID_MEMBER_TARGETS = SetUtils.of( ShapeType.SERVICE, ShapeType.RESOURCE, ShapeType.OPERATION, ShapeType.MEMBER); + // Relationship types listed here are checked to see if a shape refers to a deprecated shape. + private static final Map RELATIONSHIP_TYPE_DEPRECATION_MAPPINGS = MapUtils.of( + RelationshipType.MEMBER_TARGET, "Member targets a deprecated shape", + RelationshipType.RESOURCE, "Binds a deprecated resource", + RelationshipType.OPERATION, "Binds a deprecated operation", + RelationshipType.IDENTIFIER, "Resource identifier targets a deprecated shape", + RelationshipType.PROPERTY, "Resource property targets a deprecated shape", + RelationshipType.INPUT, "Operation input targets a deprecated shape", + RelationshipType.OUTPUT, "Operation output targets a deprecated shape", + RelationshipType.ERROR, "Operation error targets a deprecated shape", + RelationshipType.MIXIN, "Applies a deprecated mixin"); + @Override public List validate(Model model) { List events = new ArrayList<>(); @@ -68,110 +83,144 @@ private void validateShape( ) { for (Relationship relationship : relationships) { if (relationship.getNeighborShape().isPresent()) { - validateTarget(model, shape, relationship.getNeighborShape().get(), relationship) - .ifPresent(mutableEvents::add); + validateTarget(model, shape, relationship.getNeighborShape().get(), relationship, mutableEvents); } else { mutableEvents.add(unresolvedTarget(model, shape, relationship)); } } } - private Optional validateTarget(Model model, Shape shape, Shape target, Relationship rel) { + private void validateTarget( + Model model, + Shape shape, + Shape target, + Relationship rel, + List events + ) { RelationshipType relType = rel.getRelationshipType(); if (relType != RelationshipType.MIXIN && relType.getDirection() == RelationshipDirection.DIRECTED) { if (target.hasTrait(TraitDefinition.class)) { - return Optional.of(error(shape, format( + events.add(error(shape, format( "Found a %s reference to trait definition `%s`. Trait definitions cannot be targeted by " + "members or referenced by shapes in any other context other than applying them as " + "traits.", relType, rel.getNeighborShapeId()))); + return; } // Ignoring members with the mixin trait, forbid shapes to reference mixins except as mixins. if (!target.isMemberShape() && target.hasTrait(MixinTrait.class)) { - return Optional.of(error(shape, format( + events.add(error(shape, format( "Illegal %s reference to mixin `%s`; shapes marked with the mixin trait can only be " + "referenced to apply them as a mixin.", relType, rel.getNeighborShapeId()))); + return; } } + validateDeprecatedTargets(shape, target, relType, events); + switch (relType) { case PROPERTY: case MEMBER_TARGET: // Members and property cannot target other members, service, operation, or resource shapes. if (INVALID_MEMBER_TARGETS.contains(target.getType())) { - return Optional.of(error(shape, format( + events.add(error(shape, format( "Members cannot target %s shapes, but found %s", target.getType(), target))); } - return Optional.empty(); + break; case MAP_KEY: - return target.asMemberShape().flatMap(m -> validateMapKey(shape, m.getTarget(), model)); + target.asMemberShape().ifPresent(m -> validateMapKey(shape, m.getTarget(), model, events)); + break; case RESOURCE: if (target.getType() != ShapeType.RESOURCE) { - return Optional.of(badType(shape, target, relType, ShapeType.RESOURCE)); + events.add(badType(shape, target, relType, ShapeType.RESOURCE)); } - return Optional.empty(); + break; case OPERATION: if (target.getType() != ShapeType.OPERATION) { - return Optional.of(badType(shape, target, relType, ShapeType.OPERATION)); + events.add(badType(shape, target, relType, ShapeType.OPERATION)); } - return Optional.empty(); + break; case INPUT: case OUTPUT: // Input/output must target structures and cannot have the error trait. if (target.getType() != ShapeType.STRUCTURE) { - return Optional.of(badType(shape, target, relType, ShapeType.STRUCTURE)); + events.add(badType(shape, target, relType, ShapeType.STRUCTURE)); } else if (target.findTrait("error").isPresent()) { - return Optional.of(inputOutputWithErrorTrait(shape, target, rel.getRelationshipType())); - } else { - return Optional.empty(); + events.add(inputOutputWithErrorTrait(shape, target, rel.getRelationshipType())); } + break; case ERROR: // Errors must target a structure with the error trait. if (target.getType() != ShapeType.STRUCTURE) { - return Optional.of(badType(shape, target, relType, ShapeType.STRUCTURE)); + events.add(badType(shape, target, relType, ShapeType.STRUCTURE)); } else if (!target.findTrait("error").isPresent()) { - return Optional.of(errorNoTrait(shape, target.getId())); - } else { - return Optional.empty(); + events.add(errorNoTrait(shape, target.getId())); } + break; case IDENTIFIER: - return validateIdentifier(shape, target); + validateIdentifier(shape, target, events); + break; case CREATE: case READ: case UPDATE: case DELETE: case LIST: if (target.getType() != ShapeType.OPERATION) { - return Optional.of(error(shape, format( + events.add(error(shape, format( "Resource %s lifecycle operation must target an operation, but found %s", relType.toString().toLowerCase(Locale.US), target))); - } else { - return Optional.empty(); } + break; case MIXIN: if (!target.hasTrait(MixinTrait.class)) { - return Optional.of(error(shape, format( + events.add(error(shape, format( "Attempted to use %s as a mixin, but it is not marked with the mixin trait", target.getId()))); } + break; default: - return Optional.empty(); + break; + } + } + + private void validateDeprecatedTargets( + Shape shape, + Shape target, + RelationshipType relType, + List events + ) { + if (!target.hasTrait(DeprecatedTrait.class)) { + return; } + + String relLabel = RELATIONSHIP_TYPE_DEPRECATION_MAPPINGS.get(relType); + if (relLabel == null) { + return; + } + + StringBuilder builder = new StringBuilder(relLabel).append(", ").append(target.getId()); + DeprecatedTrait deprecatedTrait = target.expectTrait(DeprecatedTrait.class); + deprecatedTrait.getMessage().ifPresent(message -> builder.append(". ").append(message)); + deprecatedTrait.getSince().ifPresent(since -> builder.append(" (since ").append(since).append(')')); + events.add(ValidationEvent.builder() + .id("DeprecatedShape") + .severity(Severity.WARNING) + .shape(shape) + .message(builder.toString()) + .build()); } - private Optional validateMapKey(Shape shape, ShapeId target, Model model) { - return model.getShape(target) - .filter(FunctionalUtils.not(Shape::isStringShape)) - .map(resolved -> error(shape, format( - "Map key member targets %s, but is expected to target a string", resolved))); + private void validateMapKey(Shape shape, ShapeId target, Model model, List events) { + model.getShape(target).filter(FunctionalUtils.not(Shape::isStringShape)).ifPresent(resolved -> { + String message = format("Map key member targets %s, but is expected to target a string", resolved); + events.add(error(shape, message)); + }); } - private Optional validateIdentifier(Shape shape, Shape target) { + private void validateIdentifier(Shape shape, Shape target, List events) { if (target.getType() != ShapeType.STRING) { - return Optional.of(badType(shape, target, RelationshipType.IDENTIFIER, ShapeType.STRING)); - } else { - return Optional.empty(); + events.add(badType(shape, target, RelationshipType.IDENTIFIER, ShapeType.STRING)); } } diff --git a/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude-1.0.smithy b/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude-1.0.smithy index cfe2b622596..d71c483a422 100644 --- a/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude-1.0.smithy +++ b/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude-1.0.smithy @@ -2,28 +2,7 @@ $version: "2.0" namespace smithy.api -@deprecated -boolean PrimitiveBoolean - -@deprecated -byte PrimitiveByte - -@deprecated -short PrimitiveShort - -@deprecated -integer PrimitiveInteger - -@deprecated -long PrimitiveLong - -@deprecated -float PrimitiveFloat - -@deprecated -double PrimitiveDouble - -/// Indicates that a shape is boxed. +/// Used in Smithy 1.0 to indicate that a shape is boxed. /// /// When a boxed shape is the target of a member, the member /// may or may not contain a value, and the member has no default value. 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 7febc585ef2..63f9c8050eb 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 @@ -31,6 +31,48 @@ double Double @unitType structure Unit {} +@deprecated( + message: "Use Boolean instead, and add the @default trait to structure members that targets this shape", + since: "2.0" +) +boolean PrimitiveBoolean + +@deprecated( + message: "Use Byte instead, and add the @default trait to structure members that targets this shape", + since: "2.0" +) +byte PrimitiveByte + +@deprecated( + message: "Use Short instead, and add the @default trait to structure members that targets this shape", + since: "2.0" +) +short PrimitiveShort + +@deprecated( + message: "Use Integer instead, and add the @default trait to structure members that targets this shape", + since: "2.0" +) +integer PrimitiveInteger + +@deprecated( + message: "Use Long instead, and add the @default trait to structure members that targets this shape", + since: "2.0" +) +long PrimitiveLong + +@deprecated( + message: "Use Float instead, and add the @default trait to structure members that targets this shape", + since: "2.0" +) +float PrimitiveFloat + +@deprecated( + message: "Use Double instead, and add the @default trait to structure members that targets this shape", + since: "2.0" +) +double PrimitiveDouble + /// Makes a shape a trait. @trait( selector: ":is(simpleType, list, map, structure, union)", diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/NullableIndexTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/NullableIndexTest.java index dbc6c8a8bf1..98aaf37581a 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/NullableIndexTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/NullableIndexTest.java @@ -231,21 +231,6 @@ public static Stream inputTraitTests() { ); } - @Test - public void detectsPreviousPrimitivePreludeShapes() { - IntegerShape integer = IntegerShape.builder() - .id("smithy.api#PrimitiveInteger") - .build(); - StructureShape struct = StructureShape.builder() - .id("smithy.example#Struct") - .addMember("foo", integer.getId()) - .build(); - Model model = Model.builder().addShapes(integer, struct).build(); - NullableIndex index = NullableIndex.of(model); - - assertThat(index.isMemberNullable(struct.getMember("foo").get()), is(false)); - } - @Test public void worksWithV1NullabilityRulesForInteger() { // In Smithy v1, integer was non-nullable by default. diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/loader/ModelUpgraderTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/loader/ModelUpgraderTest.java index 6134a3de69c..b6d77f52b40 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/loader/ModelUpgraderTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/loader/ModelUpgraderTest.java @@ -41,9 +41,9 @@ public void upgradesWhenAllModelsUse1_0() { assertThat(ShapeId.from("smithy.example#Shorts$nullable2"), ShapeMatcher.memberIsNullable(result)); assertThat(ShapeId.from("smithy.example#Integers$nullable2"), ShapeMatcher.memberIsNullable(result)); - assertThat(ShapeId.from("smithy.example#Bytes$nullable2"), changedMemberTarget(result, "Byte")); - assertThat(ShapeId.from("smithy.example#Shorts$nullable2"), changedMemberTarget(result, "Short")); - assertThat(ShapeId.from("smithy.example#Integers$nullable2"), changedMemberTarget(result, "Integer")); + assertThat(ShapeId.from("smithy.example#Bytes$nullable2"), targetsShape(result, "PrimitiveByte")); + assertThat(ShapeId.from("smithy.example#Shorts$nullable2"), targetsShape(result, "PrimitiveShort")); + assertThat(ShapeId.from("smithy.example#Integers$nullable2"), targetsShape(result, "PrimitiveInteger")); assertThat(ShapeId.from("smithy.example#Bytes$nonNull"), not(ShapeMatcher.memberIsNullable(result))); assertThat(ShapeId.from("smithy.example#Shorts$nonNull"), not(ShapeMatcher.memberIsNullable(result))); @@ -57,7 +57,9 @@ public void upgradesWhenModelsMixingVersions() { UpgradeTestCase testCase = UpgradeTestCase.createAndValidate("upgrade/mixed-versions"); ValidatedResult result = testCase.actualModel; - assertThat(ShapeId.from("smithy.example#Foo$number"), changedMemberTarget(result, "Integer")); + // We don't rewrite or mess with Primitive* shape references. (a previous iteration of IDL 2.0 + // attempted to do shape rewrites, but it ended up causing issues with older models). + assertThat(ShapeId.from("smithy.example#Foo$number"), targetsShape(result, "PrimitiveInteger")); assertThat(ShapeId.from("smithy.example#Foo$number"), addedDefaultTrait(result)); } @@ -66,13 +68,13 @@ public void emitsErrorWhenPrimitiveShapeUsedInV2() { UpgradeTestCase testCase = UpgradeTestCase.createAndValidate("upgrade/primitives-in-v2"); ValidatedResult result = testCase.actualModel; - assertThat(ShapeId.from("smithy.example#Bad$boolean"), shapeTargetsInvalidPrimitive(result)); - assertThat(ShapeId.from("smithy.example#Bad$byte"), shapeTargetsInvalidPrimitive(result)); - assertThat(ShapeId.from("smithy.example#Bad$short"), shapeTargetsInvalidPrimitive(result)); - assertThat(ShapeId.from("smithy.example#Bad$integer"), shapeTargetsInvalidPrimitive(result)); - assertThat(ShapeId.from("smithy.example#Bad$long"), shapeTargetsInvalidPrimitive(result)); - assertThat(ShapeId.from("smithy.example#Bad$float"), shapeTargetsInvalidPrimitive(result)); - assertThat(ShapeId.from("smithy.example#Bad$double"), shapeTargetsInvalidPrimitive(result)); + assertThat(ShapeId.from("smithy.example#Bad$boolean"), shapeTargetsDeprecatedPrimitive(result)); + assertThat(ShapeId.from("smithy.example#Bad$byte"), shapeTargetsDeprecatedPrimitive(result)); + assertThat(ShapeId.from("smithy.example#Bad$short"), shapeTargetsDeprecatedPrimitive(result)); + assertThat(ShapeId.from("smithy.example#Bad$integer"), shapeTargetsDeprecatedPrimitive(result)); + assertThat(ShapeId.from("smithy.example#Bad$long"), shapeTargetsDeprecatedPrimitive(result)); + assertThat(ShapeId.from("smithy.example#Bad$float"), shapeTargetsDeprecatedPrimitive(result)); + assertThat(ShapeId.from("smithy.example#Bad$double"), shapeTargetsDeprecatedPrimitive(result)); } @Test @@ -88,9 +90,9 @@ public void doesNotIntroduceConflictsDuringUpgrade() { UpgradeTestCase testCase = UpgradeTestCase.createAndValidate("upgrade/does-not-introduce-conflict"); ValidatedResult result = testCase.actualModel; - assertThat(ShapeId.from("smithy.example#Foo$alreadyDefault"), changedMemberTarget(result, "Integer")); - assertThat(ShapeId.from("smithy.example#Foo$alreadyRequired"), changedMemberTarget(result, "Integer")); - assertThat(ShapeId.from("smithy.example#Foo$boxedMember"), changedMemberTarget(result, "Integer")); + assertThat(ShapeId.from("smithy.example#Foo$alreadyDefault"), targetsShape(result, "PrimitiveInteger")); + assertThat(ShapeId.from("smithy.example#Foo$alreadyRequired"), targetsShape(result, "PrimitiveInteger")); + assertThat(ShapeId.from("smithy.example#Foo$boxedMember"), targetsShape(result, "PrimitiveInteger")); assertThat(ShapeId.from("smithy.example#Foo$boxedMember"), not(addedDefaultTrait(result))); assertThat(ShapeId.from("smithy.example#Foo$previouslyBoxedTarget"), not(addedDefaultTrait(result))); assertThat(ShapeId.from("smithy.example#Foo$explicitlyBoxedTarget"), not(addedDefaultTrait(result))); @@ -159,12 +161,12 @@ private static UpgradeTestCase createFromDirectory(String directory) { } } - private static Matcher changedMemberTarget(ValidatedResult result, String newShapeName) { + private static Matcher targetsShape(ValidatedResult result, String shapeName) { return ShapeMatcher.builderFor(MemberShape.class, result) - .description("Changed member to target " + newShapeName + " with a warning") - .addAssertion(member -> member.getTarget().equals(ShapeId.fromParts(Prelude.NAMESPACE, newShapeName)), + .description("Targets " + shapeName) + .addAssertion(member -> member.getTarget() + .equals(ShapeId.fromOptionalNamespace(Prelude.NAMESPACE, shapeName)), member -> "targeted " + member.getTarget()) - .addEventAssertion(Validator.MODEL_DEPRECATION, Severity.WARNING, newShapeName + " instead") .build(); } @@ -177,13 +179,14 @@ private static Matcher addedDefaultTrait(ValidatedResult result) .build(); } - private static Matcher shapeTargetsInvalidPrimitive(ValidatedResult result) { + private static Matcher shapeTargetsDeprecatedPrimitive(ValidatedResult result) { return ShapeMatcher.builderFor(MemberShape.class, result) - .description("shape targets a removed primitive shape") + .description("shape targets a deprecated primitive shape and emits a warning") .addAssertion(member -> member.getTarget().getName().startsWith("Primitive") && member.getTarget().getNamespace().equals(Prelude.NAMESPACE), member -> "member does not target a primitive prelude shape") - .addEventAssertion(Validator.MODEL_DEPRECATION, Severity.ERROR, "removed in Smithy IDL 2.0") + .addEventAssertion("DeprecatedShape", Severity.WARNING, + "and add the @default trait to structure members that targets this shape") .build(); } diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/detects-default-out-of-range.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/detects-default-out-of-range.errors index 50995c96aee..756aac7535f 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/detects-default-out-of-range.errors +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/detects-default-out-of-range.errors @@ -1,11 +1,7 @@ [WARNING] -: Smithy IDL 1.0 is deprecated. Please upgrade to 2.0. | ModelDeprecation [WARNING] smithy.example#Integers$noRange: Add the @default trait to this member to make it forward compatible with Smithy IDL 2.0 | ModelDeprecation -[WARNING] smithy.example#Integers$noRange: This member targets smithy.api#PrimitiveInteger which was removed in Smithy IDL 2.0. Target smithy.api#Integer instead | ModelDeprecation [WARNING] smithy.example#Integers$valid: Add the @default trait to this member to make it forward compatible with Smithy IDL 2.0 | ModelDeprecation -[WARNING] smithy.example#Integers$valid: This member targets smithy.api#PrimitiveInteger which was removed in Smithy IDL 2.0. Target smithy.api#Integer instead | ModelDeprecation [WARNING] smithy.example#Integers$invalidMin: Add the @default trait to this member to make it forward compatible with Smithy IDL 2.0 | ModelDeprecation [WARNING] smithy.example#Integers$invalidMin: Cannot add the @default trait to this member due to a minimum range constraint. | ModelDeprecation -[WARNING] smithy.example#Integers$invalidMin: This member targets smithy.api#PrimitiveInteger which was removed in Smithy IDL 2.0. Target smithy.api#Integer instead | ModelDeprecation [WARNING] smithy.example#Integers$invalidMax: Add the @default trait to this member to make it forward compatible with Smithy IDL 2.0 | ModelDeprecation [WARNING] smithy.example#Integers$invalidMax: Cannot add the @default trait to this member due to a maximum range constraint. | ModelDeprecation -[WARNING] smithy.example#Integers$invalidMax: This member targets smithy.api#PrimitiveInteger which was removed in Smithy IDL 2.0. Target smithy.api#Integer instead | ModelDeprecation diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/deprecated-shape/deprecated-shape.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/deprecated-shape/deprecated-shape.errors new file mode 100644 index 00000000000..892a168ed4c --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/deprecated-shape/deprecated-shape.errors @@ -0,0 +1,4 @@ +[WARNING] smithy.example#Test$foo1: Member targets a deprecated shape, smithy.example#Foo1 | DeprecatedShape +[WARNING] smithy.example#Test$foo2: Member targets a deprecated shape, smithy.example#Foo2 (since 2.0) | DeprecatedShape +[WARNING] smithy.example#Test$foo3: Member targets a deprecated shape, smithy.example#Foo3. hello (since 2.0) | DeprecatedShape +[WARNING] smithy.example#Test: Applies a deprecated mixin, smithy.example#DeprecatedMixin | DeprecatedShape diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/deprecated-shape/deprecated-shape.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/deprecated-shape/deprecated-shape.smithy new file mode 100644 index 00000000000..41ece04a668 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/deprecated-shape/deprecated-shape.smithy @@ -0,0 +1,22 @@ +$version: "2.0" + +namespace smithy.example + +@deprecated +string Foo1 + +@deprecated(since: "2.0") +string Foo2 + +@deprecated(message: "hello", since: "2.0") +string Foo3 + +@mixin +@deprecated +structure DeprecatedMixin {} + +structure Test with [DeprecatedMixin] { + foo1: Foo1, + foo2: Foo2, + foo3: Foo3 +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/upgrade/all-1.0/upgraded.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/upgrade/all-1.0/upgraded.smithy index 77a1bbef482..349da2e73e0 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/upgrade/all-1.0/upgraded.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/upgrade/all-1.0/upgraded.smithy @@ -6,31 +6,31 @@ structure Bytes { nullable: Byte, @default(0) - nonNull: Byte, + nonNull: PrimitiveByte, - nullable2: Byte, + nullable2: PrimitiveByte, } structure Shorts { nullable: Short, @default(0) - nonNull: Short, + nonNull: PrimitiveShort, - nullable2: Short, + nullable2: PrimitiveShort, } structure Integers { nullable: Integer, @default(0) - nonNull: Integer, + nonNull: PrimitiveInteger, @default(0) @range(min: 0, max: 1) - ranged: Integer, + ranged: PrimitiveInteger, - nullable2: Integer + nullable2: PrimitiveInteger } structure BlobPayload { diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/upgrade/does-not-introduce-conflict/upgraded.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/upgrade/does-not-introduce-conflict/upgraded.smithy index 0ad2855ff13..672b10756bb 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/upgrade/does-not-introduce-conflict/upgraded.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/upgrade/does-not-introduce-conflict/upgraded.smithy @@ -4,12 +4,12 @@ namespace smithy.example structure Foo { @default(0) - alreadyDefault: Integer, + alreadyDefault: PrimitiveInteger, @required - alreadyRequired: Integer, + alreadyRequired: PrimitiveInteger, - boxedMember: Integer, + boxedMember: PrimitiveInteger, previouslyBoxedTarget: Integer, diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/upgrade/mixed-versions/upgraded.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/upgrade/mixed-versions/upgraded.smithy index e261b8be24e..28492b0c43b 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/upgrade/mixed-versions/upgraded.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/upgrade/mixed-versions/upgraded.smithy @@ -4,7 +4,7 @@ namespace smithy.example structure Foo { @default(0) - number: Integer + number: PrimitiveInteger } structure Baz { From 2ff7e00f6a47a4bc9d7a0f43634991d516010680 Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Wed, 3 Aug 2022 22:37:00 -0700 Subject: [PATCH 04/20] Tighten grammar This commit tightens the grammar of the IDL to be a bit more restrictive and less ambiguous. Of note: * Several rules were updated to require spaces between productions. For example `strutureFoo{}` is no longer technically valid. `usesmithy.api#Foo` is no longer valid. * control statements must be on a single line. For example, `$version:\n"1.0"` is no longer valid. Control statements must also be followed by a line break. * metadata statements must be on a single line. `metadata foo\n="bar"` is no longer valid. Metadata statements must be followed by a line break. * use statements must be on a single line too and followed by a line break. * Specify that newlines are allowed in strings * Updated grammar rules to CamelCase to be compatible with ABNF * Adopted RFC 7405 to specify case sensitive strings * Default value assignment and enum value assignment must occur on the same line. * Operation input, output, and errors is now specified more granularly and must be defined in this specific order. * Operation input, output, and error definition must be followed by a line break. * Assigning a default value or an enum value must be followed by a line break. --- .../http-protocol-compliance-tests.rst | 6 +- .../aws/protocols/aws-ec2-query-protocol.rst | 8 +- .../aws/protocols/aws-json-1_1-protocol.rst | 2 +- .../aws/protocols/aws-json.rst.template | 6 +- .../aws/protocols/aws-query-protocol.rst | 6 +- .../aws-query-serialization.rst.template | 2 +- .../aws/protocols/aws-restjson1-protocol.rst | 2 +- .../aws/protocols/aws-restxml-protocol.rst | 4 +- docs/source-2.0/spec/behavior-traits.rst | 2 +- docs/source-2.0/spec/idl.rst | 305 +++++++++--------- docs/source-2.0/spec/json-ast.rst | 4 +- docs/source-2.0/spec/model.rst | 28 +- docs/source-2.0/spec/selectors.rst | 26 +- docs/source-2.0/spec/service-types.rst | 2 +- .../fromsmithy/weather-service-wide.smithy | 12 +- .../schema/fromsmithy/weather.smithy | 11 +- ...nabled-service-no-taggable-resource.smithy | 8 +- .../tagging/invalid-tag-on-create.smithy | 8 +- .../tagging/invalid-tag-types.smithy | 8 +- .../tagging/tagging-warnings.smithy | 11 +- .../tagging/valid-tag-specified-apis.smithy | 8 +- .../smithy/model/loader/IdlModelParser.java | 159 ++++++--- .../loader/dupe-operation-binding.errors | 1 - .../loader/excess-operation-keys.errors | 1 - .../resource-properties-errors.smithy | 2 +- .../apply/apply-missing-newline.smithy | 5 + .../invalid/apply/apply-missing-space.smithy | 5 + .../apply/apply-missing-trait-value.smithy | 4 +- .../invalid/apply/apply-multiple-lines.smithy | 7 + .../control/control-colon-newline.smithy | 3 + .../control/control-missing-value.smithy | 2 + .../invalid/control/control-newline.smithy | 3 + .../control-statement-before-others.smithy | 0 .../control-version-defined-twice.smithy | 0 .../control-with-invalid-key.smithy | 0 .../control-with-invalid-key2.smithy | 0 .../control-with-no-colon.smithy | 0 .../control/no-newline-after-control.smithy | 2 + .../defaults/default-missing-value.smithy | 2 +- .../default-with-newline-before-value.smithy | 9 + .../missing-newline-after-assignment.smithy | 7 + .../enum-with-newline-before-value.smithy | 8 + .../missing-newline-after-assignment.smithy | 7 + .../expected-shape-name-but-eof.smithy | 2 +- .../metadata/metadata-multiple-lines.smithy | 4 + ...metadata-must-come-before-namespace.smithy | 0 .../metadata-with-invalid-key.smithy | 0 .../metadata/no-newline-after-metadata.smithy | 3 + .../not-newline-after-metadata.smithy | 4 + .../space-after-metadata-quoted.smithy | 3 + .../space-after-metadata-unquoted.smithy | 3 + .../space-after-node-array-keyword.smithy | 9 + .../namespace-before-shapes.smithy | 0 .../namespace-before-traits.smithy} | 0 .../namespace-defined-twice.smithy | 0 .../namespace/namespace-multiple-lines.smithy | 4 + .../namespace/namespace-no-newline.smithy | 3 + .../namespace-syntax-error.smithy | 0 .../{ => namespace}/unclosed-string1.smithy | 0 ...closed-string2-invalid-single-quote.smithy | 0 .../{ => namespace}/unclosed-string3.smithy | 0 .../operations/errors-before-input.smithy | 8 + .../operations/input-defined-twice.smithy} | 1 + .../invalid-operation-properties.smithy} | 2 +- .../newline-missing-after-input.smithy | 7 + .../newline-missing-after-output.smithy | 7 + .../operations/output-before-input.smithy | 8 + .../invalid-newline-after-shape-type.smithy | 6 + .../missing-space-after-string.smithy | 5 + .../missing-space-after-structure.smithy | 5 + .../invalid/use/use-missing-newline.smithy | 5 + .../invalid/use/use-missing-space.smithy | 5 + .../use/use-multiple-lines-comment.smithy | 6 + .../invalid/use/use-multiple-lines.smithy | 6 + .../valid/apply/apply-with-whitespace.json | 18 -- .../valid/apply/apply-with-whitespace.smithy | 14 - .../valid/defaults/valid-defaults.smithy | 3 +- .../model/loader/valid/{ => enums}/enums.json | 0 .../loader/valid/{ => enums}/enums.smithy | 0 .../loader/valid/enums/short-form-enum.json | 80 +++++ .../loader/valid/enums/short-form-enum.smithy | 13 + .../valid/newlines-are-not-required.json | 14 - .../valid/newlines-are-not-required.smithy | 1 - .../valid/operations/operation-with-all.json | 28 ++ .../operations/operation-with-all.smithy | 14 + .../operations/operation-with-errors.json | 14 + .../operations/operation-with-errors.smithy | 6 + .../operation-with-inline-input.json | 21 ++ .../operation-with-inline-input.smithy | 6 + .../operation-with-input-and-output.json | 28 ++ .../operation-with-input-and-output.smithy | 13 + .../operations/operation-with-input.json | 21 ++ .../operations/operation-with-input.smithy | 9 + .../operation-with-no-properties.json | 14 + .../operation-with-no-properties.smithy | 4 + .../operations/operation-with-output.json | 21 ++ .../operations/operation-with-output.smithy | 9 + 97 files changed, 810 insertions(+), 343 deletions(-) delete mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/dupe-operation-binding.errors delete mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/excess-operation-keys.errors create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/apply/apply-missing-newline.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/apply/apply-missing-space.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/apply/apply-multiple-lines.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/control/control-colon-newline.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/control/control-missing-value.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/control/control-newline.smithy rename smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/{ => control}/control-statement-before-others.smithy (100%) rename smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/{ => control}/control-version-defined-twice.smithy (100%) rename smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/{ => control}/control-with-invalid-key.smithy (100%) rename smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/{ => control}/control-with-invalid-key2.smithy (100%) rename smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/{ => control}/control-with-no-colon.smithy (100%) create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/control/no-newline-after-control.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/defaults/default-with-newline-before-value.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/defaults/missing-newline-after-assignment.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/enum-values/enum-with-newline-before-value.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/enum-values/missing-newline-after-assignment.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/metadata/metadata-multiple-lines.smithy rename smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/{ => metadata}/metadata-must-come-before-namespace.smithy (100%) rename smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/{ => metadata}/metadata-with-invalid-key.smithy (100%) create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/metadata/no-newline-after-metadata.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/metadata/not-newline-after-metadata.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/metadata/space-after-metadata-quoted.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/metadata/space-after-metadata-unquoted.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/metadata/space-after-node-array-keyword.smithy rename smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/{ => namespace}/namespace-before-shapes.smithy (100%) rename smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/{namespace-before-annotations.smithy => namespace/namespace-before-traits.smithy} (100%) rename smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/{ => namespace}/namespace-defined-twice.smithy (100%) create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/namespace/namespace-multiple-lines.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/namespace/namespace-no-newline.smithy rename smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/{ => namespace}/namespace-syntax-error.smithy (100%) rename smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/{ => namespace}/unclosed-string1.smithy (100%) rename smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/{ => namespace}/unclosed-string2-invalid-single-quote.smithy (100%) rename smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/{ => namespace}/unclosed-string3.smithy (100%) create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/operations/errors-before-input.smithy rename smithy-model/src/test/resources/software/amazon/smithy/model/{errorfiles/loader/dupe-operation-binding.smithy => loader/invalid/operations/input-defined-twice.smithy} (53%) rename smithy-model/src/test/resources/software/amazon/smithy/model/{errorfiles/loader/excess-operation-keys.smithy => loader/invalid/operations/invalid-operation-properties.smithy} (73%) create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/operations/newline-missing-after-input.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/operations/newline-missing-after-output.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/operations/output-before-input.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/parse-errors/invalid-newline-after-shape-type.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/parse-errors/missing-space-after-string.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/parse-errors/missing-space-after-structure.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/use/use-missing-newline.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/use/use-missing-space.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/use/use-multiple-lines-comment.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/use/use-multiple-lines.smithy delete mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/apply/apply-with-whitespace.json delete mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/apply/apply-with-whitespace.smithy rename smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/{ => enums}/enums.json (100%) rename smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/{ => enums}/enums.smithy (100%) create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums/short-form-enum.json create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums/short-form-enum.smithy delete mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/newlines-are-not-required.json delete mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/newlines-are-not-required.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-all.json create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-all.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-errors.json create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-errors.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-inline-input.json create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-inline-input.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-input-and-output.json create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-input-and-output.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-input.json create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-input.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-no-properties.json create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-no-properties.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-output.json create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-output.smithy diff --git a/docs/source-2.0/additional-specs/http-protocol-compliance-tests.rst b/docs/source-2.0/additional-specs/http-protocol-compliance-tests.rst index 2441f77c337..9b90aaa130e 100644 --- a/docs/source-2.0/additional-specs/http-protocol-compliance-tests.rst +++ b/docs/source-2.0/additional-specs/http-protocol-compliance-tests.rst @@ -91,7 +91,7 @@ that support the following members: - **Required**. The identifier of the test case. This identifier can be used by protocol test implementations to filter out unsupported test cases by ID, to generate test case names, etc. The provided - ``id`` MUST match Smithy's :token:`smithy:identifier` ABNF. No two + ``id`` MUST match Smithy's :token:`smithy:Identifier` ABNF. No two ``httpRequestTests`` test cases can share the same ID. * - protocol - shape ID @@ -346,7 +346,7 @@ structures that support the following members: - **Required**. The identifier of the test case. This identifier can be used by protocol test implementations to filter out unsupported test cases by ID, to generate test case names, etc. The provided - ``id`` MUST match Smithy's :token:`smithy:identifier` ABNF. No two + ``id`` MUST match Smithy's :token:`smithy:Identifier` ABNF. No two ``httpResponseTests`` test cases can share the same ID. * - protocol - ``string`` @@ -554,7 +554,7 @@ The ``httpMalformedRequestTests`` trait is a list of - **Required**. The identifier of the test case. This identifier can be used by protocol test implementations to filter out unsupported test cases by ID, to generate test case names, etc. The provided - ``id`` MUST match Smithy's :token:`smithy:identifier` ABNF. No two + ``id`` MUST match Smithy's :token:`smithy:Identifier` ABNF. No two ``httpMalformedRequestTests`` test cases can share the same ID. * - protocol - shape ID diff --git a/docs/source-2.0/aws/protocols/aws-ec2-query-protocol.rst b/docs/source-2.0/aws/protocols/aws-ec2-query-protocol.rst index b7868d17915..f4209e9e142 100644 --- a/docs/source-2.0/aws/protocols/aws-ec2-query-protocol.rst +++ b/docs/source-2.0/aws/protocols/aws-ec2-query-protocol.rst @@ -113,9 +113,9 @@ resolved using the following process: * - Member location - Default value * - ``structure`` member - - The :token:`member name ` capitalized + - The :token:`member name ` capitalized * - ``union`` member - - The :token:`member name ` capitalized + - The :token:`member name ` capitalized ---------------- @@ -317,7 +317,7 @@ of the ``Errors`` tag is an ``Error`` tag which contains the serialized error structure members. Serialized error shapes MUST also contain an additional child element ``Code`` -that contains only the :token:`shape name ` of the error's +that contains only the :token:`shape name ` of the error's :ref:`shape-id`. This can be used to distinguish which specific error has been serialized in the response. @@ -334,7 +334,7 @@ serialized in the response. foo-id -* ``Code``: The :token:`shape name ` of the error's +* ``Code``: The :token:`shape name ` of the error's :ref:`shape-id`. * ``RequestId``: Contains a unique identifier for the associated request. diff --git a/docs/source-2.0/aws/protocols/aws-json-1_1-protocol.rst b/docs/source-2.0/aws/protocols/aws-json-1_1-protocol.rst index 374d3a3ffbc..796ca734720 100644 --- a/docs/source-2.0/aws/protocols/aws-json-1_1-protocol.rst +++ b/docs/source-2.0/aws/protocols/aws-json-1_1-protocol.rst @@ -124,7 +124,7 @@ The following example defines a service that requires the use of .. |quoted shape name| replace:: ``awsJson1_1`` .. |protocol content type| replace:: ``application/x-amz-json-1.1`` -.. |protocol error type contents| replace:: :token:`shape name ` +.. |protocol error type contents| replace:: :token:`shape name ` .. |protocol test link| replace:: https://github.com/awslabs/smithy/tree/main/smithy-aws-protocol-tests/model/awsJson1_1 .. include:: aws-json.rst.template diff --git a/docs/source-2.0/aws/protocols/aws-json.rst.template b/docs/source-2.0/aws/protocols/aws-json.rst.template index 47bfb97a735..8d83232c272 100644 --- a/docs/source-2.0/aws/protocols/aws-json.rst.template +++ b/docs/source-2.0/aws/protocols/aws-json.rst.template @@ -50,9 +50,9 @@ The |quoted shape name| protocol uses the following headers: `RFC 7230 Section 3.3.2`_. * - ``X-Amz-Target`` - true for requests - - The value of this header is the :token:`shape name ` of the + - The value of this header is the :token:`shape name ` of the service's :ref:`shape-id` joined to the - :token:`shape name ` of the operation's :ref:`shape-id`, + :token:`shape name ` of the operation's :ref:`shape-id`, separated by a single period (``.``) character. For example, the value for the operation ``ns.example#MyOp`` of the @@ -191,7 +191,7 @@ There are two difference between :ref:`awsJson1_0 `. However, clients MUST accept either behavior +:token:`shape name `. However, clients MUST accept either behavior for both protocols. See `Operation error serialization`_ for full details on how to deserialize errors for |quoted shape name|. diff --git a/docs/source-2.0/aws/protocols/aws-query-protocol.rst b/docs/source-2.0/aws/protocols/aws-query-protocol.rst index d74e2965ad3..790733bc90e 100644 --- a/docs/source-2.0/aws/protocols/aws-query-protocol.rst +++ b/docs/source-2.0/aws/protocols/aws-query-protocol.rst @@ -141,9 +141,9 @@ resolved using the following process: * - ``map`` value - The string literal "value" * - ``structure`` member - - The :token:`member name ` + - The :token:`member name ` * - ``union`` member - - The :token:`member name ` + - The :token:`member name ` Example requests @@ -436,7 +436,7 @@ following process: 1. Use the value of the ``code`` member of the :ref:`aws.protocols#awsQueryError-trait` applied to the error structure, if present. -2. The :token:`shape name ` of the error's :ref:`shape-id`. +2. The :token:`shape name ` of the error's :ref:`shape-id`. .. smithy-trait:: aws.protocols#awsQuery diff --git a/docs/source-2.0/aws/protocols/aws-query-serialization.rst.template b/docs/source-2.0/aws/protocols/aws-query-serialization.rst.template index e653c66f7de..d7db64b2a3e 100644 --- a/docs/source-2.0/aws/protocols/aws-query-serialization.rst.template +++ b/docs/source-2.0/aws/protocols/aws-query-serialization.rst.template @@ -25,7 +25,7 @@ Requests MUST include the following key value pairs in the serialized body: * - Key - Value * - ``Action`` - - The :token:`shape name ` of the operation's :ref:`shape-id`. + - The :token:`shape name ` of the operation's :ref:`shape-id`. * - ``Version`` - The value of the :ref:`"version" property of the service `. diff --git a/docs/source-2.0/aws/protocols/aws-restjson1-protocol.rst b/docs/source-2.0/aws/protocols/aws-restjson1-protocol.rst index b4d1810e80b..730c8e8cb8a 100644 --- a/docs/source-2.0/aws/protocols/aws-restjson1-protocol.rst +++ b/docs/source-2.0/aws/protocols/aws-restjson1-protocol.rst @@ -313,7 +313,7 @@ is contained. New server-side protocol implementations MUST use a header field named ``X-Amzn-Errortype``. Clients MUST accept any one of the following: an additional header with the name ``X-Amzn-Errortype``, a body field with the name ``__type``, or a body field named ``code``. The value of this component -SHOULD contain only the :token:`shape name ` of the error's +SHOULD contain only the :token:`shape name ` of the error's :ref:`shape-id`. Legacy server-side protocol implementations sometimes include additional diff --git a/docs/source-2.0/aws/protocols/aws-restxml-protocol.rst b/docs/source-2.0/aws/protocols/aws-restxml-protocol.rst index 578b30f49b6..210fb9a78f5 100644 --- a/docs/source-2.0/aws/protocols/aws-restxml-protocol.rst +++ b/docs/source-2.0/aws/protocols/aws-restxml-protocol.rst @@ -259,7 +259,7 @@ contains the serialized error structure members, unless bound to another location with HTTP protocol bindings. Serialized error shapes MUST also contain an additional child element ``Code`` -that contains only the :token:`shape name ` of the error's +that contains only the :token:`shape name ` of the error's :ref:`shape-id`. This can be used to distinguish which specific error has been serialized in the response. @@ -293,7 +293,7 @@ Error responses contain the following nested elements: * ``Error``: A container for the encountered error. * ``Type``: One of "Sender" or "Receiver"; whomever is at fault from the service perspective. -* ``Code``: The :token:`shape name ` of the error's +* ``Code``: The :token:`shape name ` of the error's :ref:`shape-id`. * ``RequestId``: Contains a unique identifier for the associated request. diff --git a/docs/source-2.0/spec/behavior-traits.rst b/docs/source-2.0/spec/behavior-traits.rst index e5795a5fff9..dedb9e1f357 100644 --- a/docs/source-2.0/spec/behavior-traits.rst +++ b/docs/source-2.0/spec/behavior-traits.rst @@ -302,7 +302,7 @@ member in the previously referenced structure. Paths MUST adhere to the following ABNF. .. productionlist:: smithy - path :`identifier` *("." `identifier`) + path :`Identifier` *("." `Identifier`) The following example defines a paginated operation which uses a result wrapper where the output token and items are referenced by paths. diff --git a/docs/source-2.0/spec/idl.rst b/docs/source-2.0/spec/idl.rst index 3f0191c57a4..280b17fd217 100644 --- a/docs/source-2.0/spec/idl.rst +++ b/docs/source-2.0/spec/idl.rst @@ -20,7 +20,7 @@ The Smithy IDL is made up of 3, ordered sections, each of which is optional: 2. **Metadata section**; applies metadata to the entire model. 3. **Shape section**; where shapes and traits are defined. A namespace MUST be defined before any shapes or traits can be defined. - :token:`smithy:use_statement`\s can be defined after a namespace and before shapes + :token:`smithy:UseStatement`\s can be defined after a namespace and before shapes or traits to refer to shapes in other namespaces using a shorter name. The following example defines a model file with each section: @@ -88,136 +88,144 @@ Lexical notes Smithy IDL ABNF --------------- -The Smithy IDL is defined by the following ABNF: +The Smithy IDL is defined by the following ABNF which uses case-sensitive +string support defined in `RFC 5234 `_. .. productionlist:: smithy - idl:`ws` `control_section` `metadata_section` `shape_section` + idl:*`WS` `ControlSection` `MetadataSection` `ShapeSection` .. rubric:: Whitespace .. productionlist:: smithy - ws :*(`sp` / `newline` / `comment` / ",") ; whitespace - sp :*(%x20 / %x09) ; " " and \t - br :`sp` (`comment` / `newline`) `sp` ; break - newline :%x0A / %x0D.0A ; \n and \r\n + WS :1*(`SP` / `NL` / `Comment` / ",") ; whitespace + SP :1*(%x20 / %x09) ; one or more spaces or tabs + NL :%x0A / %x0D.0A ; Newline: \n and \r\n + NotNL: %x09 / %x20-10FFFF ; Any character except newline + BR :*`SP` 1*(`Comment` / `NL`) *`WS`; line break followed by whitespace .. rubric:: Comments .. productionlist:: smithy - comment: `documentation_comment` / `line_comment` - documentation_comment:"///" *`not_newline` `br` - line_comment: "//" *`not_newline` `newline` - not_newline: %x09 / %x20-10FFFF ; Any character except newline + Comment : `DocumentationComment` / `LineComment` + DocumentationComment :"///" *`NotNL` `NL` + LineComment : "//" *`NotNL` `NL` .. rubric:: Control .. productionlist:: smithy - control_section :*(`control_statement`) - control_statement :"$" `ws` `node_object_key` `ws` ":" `ws` `node_value` `ws` + ControlSection :*(`ControlStatement`) + ControlStatement :"$" `NodeObjectKey` *`SP` ":" *`SP` `NodeValue` `BR` .. rubric:: Metadata .. productionlist:: smithy - metadata_section :*(`metadata_statement`) - metadata_statement :"metadata" `ws` `node_object_key` `ws` "=" `ws` `node_value` `ws` + MetadataSection :*(`MetadataStatement`) + MetadataStatement :%s"metadata" `SP` `NodeObjectKey` *`SP` "=" *`SP` `NodeValue` `BR` .. rubric:: Node values .. productionlist:: smithy - node_value :`node_array` - :/ `node_object` - :/ `number` - :/ `node_keywords` - :/ `node_string_value` - node_array :"[" `ws` *(`node_value` `ws`) "]" - node_object :"{" `ws` *(`node_object_kvp` `ws`) "}" - node_object_kvp :`node_object_key` `ws` ":" `ws` `node_value` - node_object_key :`quoted_text` / `identifier` - number :[`minus`] `int` [`frac`] [`exp`] - decimal_point :%x2E ; . - digit1_9 :%x31-39 ; 1-9 - e :%x65 / %x45 ; e E - exp :`e` [`minus` / `plus`] 1*DIGIT - frac :`decimal_point` 1*DIGIT - int :`zero` / (`digit1_9` *DIGIT) - minus :%x2D ; - - plus :%x2B ; + - zero :%x30 ; 0 - node_keywords: "true" / "false" / "null" - node_string_value :`shape_id` / `text_block` / `quoted_text` - quoted_text :DQUOTE *`quoted_char` DQUOTE - quoted_char :%x20-21 ; space - "!" - :/ %x23-5B ; "#" - "[" - :/ %x5D-10FFFF ; "]"+ - :/ `escaped_char` - :/ `preserved_double` - escaped_char :`escape` (`escape` / "'" / DQUOTE / "b" - : / "f" / "n" / "r" / "t" / "/" / `unicode_escape`) - unicode_escape :"u" `hex` `hex` `hex` `hex` - hex : DIGIT / %x41-46 / %x61-66 - preserved_double :`escape` (%x20-21 / %x23-5B / %x5D-10FFFF) - escape :%x5C ; backslash - text_block :`three_dquotes` `br` *`quoted_char` `three_dquotes` - three_dquotes :DQUOTE DQUOTE DQUOTE + NodeValue :`NodeArray` + :/ `NodeObject` + :/ `Number` + :/ `NodeKeywords` + :/ `NodeStringValue` + NodeArray :"[" *`WS` *(`NodeValue` *`WS`) "]" + NodeObject :"{" *`WS` [`NodeObjectKvp` *(`WS` `NodeObjectKvp`)] *`WS` "}" + NodeObjectKvp :`NodeObjectKey` *`WS` ":" *`WS` `NodeValue` + NodeObjectKey :`QuotedText` / `Identifier` + Number :[`Minus`] `Int` [`Frac`] [`Exp`] + DecimalPoint :%x2E ; . + DigitOneToNine :%x31-39 ; 1-9 + E :%x65 / %x45 ; e E + Exp :`E` [`Minus` / `Plus`] 1*DIGIT + Frac :`DecimalPoint` 1*DIGIT + Int :`Zero` / (`DigitOneToNine` *DIGIT) + Minus :%x2D ; - + Plus :%x2B ; + + Zero :%x30 ; 0 + NodeKeywords :%s"true" / %s"false" / %s"null" + NodeStringValue :`ShapeId` / `TextBlock` / `QuotedText` + QuotedText :DQUOTE *`QuotedChar` DQUOTE + QuotedChar :%x20-21 ; space - "!" + :/ %x23-5B ; "#" - "[" + :/ %x5D-10FFFF ; "]"+ + :/ `EscapedChar` + :/ `PreservedDouble` + :/ `NL` + EscapedChar :`Escape` (`Escape` / "'" / DQUOTE / %s"b" + : / %s"f" / %s"n" / %s"r" / %s"t" + : / "/" / `UnicodeEscape`) + UnicodeEscape :%s"u" `Hex` `Hex` `Hex` `Hex` + Hex :DIGIT / %x41-46 / %x61-66 + PreservedDouble :`Escape` (%x20-21 / %x23-5B / %x5D-10FFFF) + Escape :%x5C ; backslash + TextBlock :`ThreeDquotes` *`SP` `NL` *`QuotedChar` `ThreeDquotes` + ThreeDquotes :DQUOTE DQUOTE DQUOTE .. rubric:: Shapes .. productionlist:: smithy - shape_section :[`namespace_statement` [`use_section`] [`shape_statements`]] - namespace_statement :"namespace" `ws` `namespace` `ws` - use_section :*(`use_statement`) - use_statement :"use" `ws` `absolute_root_shape_id` `ws` - shape_statements :*(`shape_statement` / `apply_statement`) - shape_statement :`trait_statements` `shape_body` `ws` - shape_body :`simple_shape_statement` - :/ `enum_shape_statement` - :/ `list_statement` - :/ `set_statement` - :/ `map_statement` - :/ `structure_statement` - :/ `union_statement` - :/ `service_statement` - :/ `operation_statement` - :/ `resource_statement` - mixins :`sp` "with" `ws` "[" 1*(`ws` `shape_id`) `ws` "]" - simple_shape_statement :`simple_type_name` `ws` `identifier` [`mixins`] - simple_type_name :"blob" / "boolean" / "document" / "string" - :/ "byte" / "short" / "integer" / "long" - :/ "float" / "double" / "bigInteger" - :/ "bigDecimal" / "timestamp" - enum_shape_statement :`enum_type_name` `ws` `identifier` [`mixins`] `ws` `enum_shape_members` - enum_type_name :"enum" / "intEnum" - enum_shape_members :"{" `ws` 1*(`trait_statements` `identifier` [`value_assignment`] `ws`) "}" - value_assignment :*`sp` "=" `ws` `node_value` - shape_members :"{" `ws` *(`trait_statements` `shape_member` `ws`) "}" - shape_member :(`shape_member_kvp` / `shape_member_elided`) [`value_assignment`] - shape_member_kvp :`identifier` `ws` ":" `ws` `shape_id` - shape_member_elided :"$" `identifier` - list_statement :"list" `ws` `identifier` [`mixins`] `ws` `shape_members` - set_statement :"set" `ws` `identifier` [`mixins`] `ws` `shape_members` - map_statement :"map" `ws` `identifier` [`mixins`] `ws` `shape_members` - structure_statement :"structure" `ws` `identifier` ["for" `shape_id`] [`mixins`] `ws` `shape_members` - union_statement :"union" `ws` `identifier` [`mixins`] `ws` `shape_members` - service_statement :"service" `ws` `identifier` [`mixins`] `ws` `node_object` - operation_statement :"operation" `ws` `identifier` [`mixins`] `ws` `inlineable_properties` - inlineable_properties :"{" *(`inlineable_property` `ws`) `ws` "}" - inlineable_property :`node_object_kvp` / `inline_structure` - inline_structure :`node_object_key` `ws` ":=" `ws` `inline_structure_value` - inline_structure_value :`trait_statements` [`mixins` ws] shape_members - resource_statement :"resource" `ws` `identifier` [`mixins`] `ws` `node_object` + ShapeSection :[`NamespaceStatement` `UseSection` `ShapeStatements`] + NamespaceStatement :%s"namespace" `SP` `Namespace` `BR` + UseSection :*(`UseStatement`) + UseStatement :%s"use" `SP` `AbsoluteRootShapeId` `BR` + ShapeStatements :*(`ShapeStatement` / `ApplyStatement`) + ShapeStatement :`TraitStatements` `ShapeBody` `BR` + ShapeBody :`SimpleShapeStatement` + :/ `EnumShapeStatement` + :/ `ListStatement` + :/ `MapStatement` + :/ `StructureStatement` + :/ `UnionStatement` + :/ `ServiceStatement` + :/ `OperationStatement` + :/ `ResourceStatement` + SimpleShapeStatement :`SimpleTypeName` `SP` `Identifier` [`Mixins`] + SimpleTypeName :%s"blob" / %s"boolean" / %s"document" / %s"string" + :/ %s"byte" / %s"short" / %s"integer" / %s"long" + :/ %s"float" / %s"double" / %s"bigInteger" + :/ %s"bigDecimal" / %s"timestamp" + Mixins :*`SP` %s"with" *`WS` "[" 1*(*`WS` `ShapeId`) *`WS` "]" + EnumShapeStatement :`EnumTypeName` `SP` `Identifier` [`Mixins`] *`WS` `EnumShapeMembers` + EnumTypeName :%s"enum" / %s"intEnum" + EnumShapeMembers :"{" *`WS` 1*(`TraitStatements` `Identifier` [`ValueAssignment`] `*WS`) "}" + ValueAssignment :*`SP` "=" *`SP` `NodeValue` `BR` + ShapeMembers :"{" *`WS` *(`TraitStatements` `ShapeMember` *`WS`) "}" + ShapeMember :(`ShapeMemberKvp` / `ShapeMemberElided`) [`ValueAssignment`] + ShapeMemberKvp :`Identifier` *`WS` ":" *`WS` `ShapeId` + ShapeMemberElided :"$" `Identifier` + ListStatement :%s"list" `SP` `Identifier` [`Mixins`] *`WS` `ShapeMembers` + MapStatement :%s"map" `SP` `Identifier` [`Mixins`] *`WS` `ShapeMembers` + StructureStatement :%s"structure" `SP` `Identifier` [`StructureResource`] + : [`Mixins`] *`WS` `ShapeMembers` + StructureResource :`SP` %s"for" `SP` `ShapeId` + UnionStatement :%s"union" `SP` `Identifier` [`Mixins`] *`WS` `ShapeMembers` + ServiceStatement :%s"service" `SP` `Identifier` [`Mixins`] *`WS` `NodeObject` + ResourceStatement :%s"resource" `SP` `Identifier` [`Mixins`] *`WS` `NodeObject` + OperationStatement :%s"operation" `SP` `Identifier` [`Mixins`] *`WS` `OperationBody` + OperationBody :"{" *`WS` + : [`OperationInput`] + : [`OperationOutput`] + : [`OperationErrors`] + : *`WS` "}" + OperationInput :%s"input" *WS (`InlineStructure` / `Identifier`) `BR` + OperationOutput :%s"output" *WS (`InlineStructure` / `ShapeId`) `BR` + OperationErrors :%s"errors" *WS ":" *WS "[" *(*`WS` `Identifier`) *`WS` "]" `BR` + InlineStructure :":=" *`WS` `TraitStatements` [`Mixins`] *`WS` `ShapeMembers` .. rubric:: Traits .. productionlist:: smithy - trait_statements : *(`ws` `trait`) `ws` - trait :"@" `shape_id` [`trait_body`] - trait_body :"(" `ws` `trait_body_value` `ws` ")" - trait_body_value :`trait_structure` / `node_value` - trait_structure :`trait_structure_kvp` *(`ws` `trait_structure_kvp`) - trait_structure_kvp :`node_object_key` `ws` ":" `ws` `node_value` - apply_statement :`apply_statement_singular` / `apply_statement_block` - apply_statement_singular: "apply" `ws` `shape_id` `ws` `trait` `ws` - apply_statement_block: "apply" `ws` `shape_id` `ws` "{" `trait_statements` "}" + TraitStatements : *(*`WS` `Trait`) *`WS` + Trait :"@" `ShapeId` [`TraitBody`] + TraitBody :"(" *`WS` [`TraitBodyValue`] *`WS` ")" + TraitBodyValue :`TraitStructure` / `NodeValue` + TraitStructure :`TraitStructureKvp` *(*`WS` `TraitStructureKvp`) + TraitStructureKvp :`NodeObjectKey` *`WS` ":" *`WS` `NodeValue` + ApplyStatement :(`ApplyStatementSingular` / `ApplyStatementBlock`) + ApplyStatementSingular :%s"apply" `SP` `ShapeId` `SP` `Trait` `BR` + ApplyStatementBlock :%s"apply" `SP` `ShapeId` `SP` "{" `TraitStatements` "}" `BR` .. rubric:: Shape ID @@ -232,8 +240,8 @@ The Smithy IDL is defined by the following ABNF: Comments -------- -A :token:`comment ` can appear at any place between tokens where -whitespace (:token:`smithy:ws`) can appear. Comments in Smithy are defined using two +A :token:`comment ` can appear at any place between tokens where +whitespace (:token:`smithy:WS`) can appear. Comments in Smithy are defined using two forward slashes followed by any character. A newline terminates a comment. .. code-block:: smithy @@ -258,8 +266,8 @@ forward slashes followed by any character. A newline terminates a comment. Control section --------------- -The :token:`control section ` of a model contains -:token:`control statements ` that apply parser directives +The :token:`control section ` of a model contains +:token:`control statements ` that apply parser directives to a *specific IDL file*. Because control statements influence parsing, they MUST appear at the beginning of a file before any other statements and have no effect on the :ref:`semantic model ` The following control @@ -295,7 +303,7 @@ Version statement The Smithy specification is versioned using a ``major`` . ``minor`` versioning scheme. A version requirement is specified for a model file using -the ``$version`` control statement. When no version number is specified in +the ``$version`` control statement. When no version Number is specified in the IDL, an implementation SHOULD assume that the model can be loaded. Because this can lead to unexpected parsing errors, models SHOULD always include a version. @@ -356,10 +364,10 @@ supported by the tool loading the models. Metadata section ---------------- -The :token:`metadata section ` is used to apply untyped -:ref:`metadata ` to the entire model. A :token:`smithy:metadata_statement` +The :token:`metadata section ` is used to apply untyped +:ref:`metadata ` to the entire model. A :token:`smithy:MetadataStatement` consists of the metadata key to set, followed by ``=``, followed by the -:token:`node value ` to assign to the key. +:token:`node value ` to assign to the key. The following example defines metadata in the model: @@ -387,7 +395,7 @@ The following example defines metadata in the model: Shape section ------------- -The :token:`shape section ` of the IDL is used to define +The :token:`shape section ` of the IDL is used to define shapes and apply traits to shapes. @@ -397,7 +405,7 @@ Namespaces ========== Shapes can only be defined after a namespace is declared. A namespace is -declared using a :token:`namespace statement `. Only +declared using a :token:`namespace statement `. Only one namespace can appear per file. The following example defines a string shape named ``MyString`` in the @@ -431,9 +439,9 @@ The following example defines a string shape named ``MyString`` in the Referring to shapes =================== -The :token:`use section ` of the IDL is used to import shapes +The :token:`use section ` of the IDL is used to import shapes into the current namespace so that they can be referred to using a -:ref:`relative shape ID `. The :token:`use_statement `\s +:ref:`relative shape ID `. The :token:`UseStatement `\s that make up this section have no effect on the :ref:`semantic model `. The following example uses ``smithy.example#Foo`` and ``smithy.example#Baz`` @@ -483,7 +491,7 @@ Relative shape ID resolution Relative shape IDs are resolved using the following process: -#. If a :token:`smithy:use_statement` has imported a shape with the same name, +#. If a :token:`smithy:UseStatement` has imported a shape with the same name, the shape ID resolves to the imported shape ID. #. If a shape is defined in the same namespace as the shape with the same name, the namespace of the shape resolves to the *current namespace*. @@ -665,7 +673,7 @@ reference can be ignored. Defining shapes =============== -Shapes are defined using a :token:`smithy:shape_statement`. +Shapes are defined using a :token:`smithy:ShapeStatement`. .. _idl-simple: @@ -674,7 +682,7 @@ Simple shapes ------------- :ref:`Simple shapes ` are defined using a -:token:`smithy:simple_shape_statement`. +:token:`smithy:SimpleShapeStatement`. The following example defines a ``string`` shape: @@ -737,7 +745,7 @@ The following example defines an ``integer`` shape with a :ref:`range-trait`: Enum shapes ----------- -The :ref:`enum` shape is defined using an :token:`smithy:enum_shape_statement`. +The :ref:`enum` shape is defined using an :token:`smithy:EnumShapeStatement`. The following example defines an :ref:`enum` shape: @@ -800,7 +808,7 @@ IntEnum shapes -------------- The :ref:`intEnum` shape is defined using an -:token:`smithy:enum_shape_statement`. +:token:`smithy:EnumShapeStatement`. .. note:: The :ref:`enumValue trait ` is required on all @@ -848,7 +856,7 @@ The above intEnum is exactly equivalent to the following intEnum: List shapes ----------- -A :ref:`list ` shape is defined using a :token:`smithy:list_statement`. +A :ref:`list ` shape is defined using a :token:`smithy:ListStatement`. The following example defines a list with a string member from the :ref:`prelude `: @@ -929,7 +937,7 @@ Traits can be applied to the list shape and its member: Map shapes ---------- -A :ref:`map ` shape is defined using a :token:`smithy:map_statement`. +A :ref:`map ` shape is defined using a :token:`smithy:MapStatement`. The following example defines a map of strings to integers: @@ -1026,7 +1034,7 @@ Structure shapes ---------------- A :ref:`structure ` shape is defined using a -:token:`smithy:structure_statement`. +:token:`smithy:StructureStatement`. The following example defines a structure with two members: @@ -1139,7 +1147,7 @@ Is exactly equivalent to: Union shapes ------------ -A :ref:`union ` shape is defined using a :token:`smithy:union_statement`. +A :ref:`union ` shape is defined using a :token:`smithy:UnionStatement`. The following example defines a union shape with several members: @@ -1193,8 +1201,8 @@ The following example defines a union shape with several members: Service shape ------------- -A service shape is defined using a :token:`smithy:service_statement` and the provided -:token:`smithy:node_object` supports the same properties defined in the +A service shape is defined using a :token:`smithy:ServiceStatement` and the provided +:token:`smithy:NodeObject` supports the same properties defined in the :ref:`service specification `. The following example defines a service named ``ModelRepository`` that binds @@ -1242,9 +1250,8 @@ a resource named ``Model`` and an operation named ``PingService``: Operation shape --------------- -An operation shape is defined using an :token:`smithy:operation_statement` and the -provided :token:`smithy:inlineable_properties` supports the same properties defined -in the :ref:`operation specification `. +An operation shape is defined using an :token:`smithy:OperationStatement` and +the same properties defined in the :ref:`operation specification `. The following example defines an operation shape that accepts an input structure named ``Input``, returns an output structure named ``Output``, and @@ -1400,8 +1407,8 @@ The suffixes for the generated names can be customized using the Resource shape -------------- -A resource shape is defined using a :token:`smithy:resource_statement` and the -provided :token:`smithy:node_object` supports the same properties defined in the +A resource shape is defined using a :token:`smithy:ResourceStatement` and the +provided :token:`smithy:NodeObject` supports the same properties defined in the :ref:`resource specification `. The following example defines a resource shape that has a single identifier, @@ -1454,7 +1461,7 @@ Mixins ------ :ref:`Mixins ` can be added to a shape using the optional -:token:`smithy:mixins` clause of a shape definition. +:token:`smithy:Mixins` clause of a shape definition. For example: @@ -1485,7 +1492,7 @@ Target Elision Having to completely redefine a :ref:`resource identifier ` to use it in a structure or redefine a member from a :ref:`mixin ` to add additional traits can be cumbersome and potentially error-prone. The -:token:`type elision syntax ` can be used to cut +:token:`type elision syntax ` can be used to cut down on that repetition by prefixing the member name with a ``$``. If a member is prefixed this way, its target will automatically be set to the target of a mixin member with the same name. The following example shows how to elide the @@ -1580,8 +1587,8 @@ be checked first. The following example is invalid: Documentation comment ===================== -:token:`Documentation comments ` are a -special kind of :token:`smithy:comment` that provide +:token:`Documentation comments ` are a +special kind of :token:`smithy:Comment` that provide :ref:`documentation ` for shapes. A documentation comment is formed when three forward slashes (``"///"``) appear as the first non-whitespace characters on a line. @@ -1673,7 +1680,7 @@ Applying traits =============== Trait values immediately preceding a shape definition are applied to the -shape. The shape ID of a trait is *resolved* against :token:`smithy:use_statement`\s +shape. The shape ID of a trait is *resolved* against :token:`smithy:UseStatement`\s and the current namespace in exactly the same way as :ref:`other shape IDs `. @@ -1813,7 +1820,7 @@ List and set trait values ------------------------- Traits that are a ``list`` or ``set`` shape are defined inside -of brackets (``[``) and (``]``) using a :token:`smithy:node_array` production. +of brackets (``[``) and (``]``) using a :token:`smithy:NodeArray` production. .. code-block:: smithy @@ -1839,7 +1846,7 @@ Apply statement --------------- Traits can be applied to shapes outside of a shape's definition using an -:token:`smithy:apply_statement`. +:token:`smithy:ApplyStatement`. The following example applies the :ref:`documentation-trait` to the ``smithy.example#MyString`` shape: @@ -1961,9 +1968,9 @@ node value: .. rubric:: Array node -An array node is defined like a JSON array. A :token:`smithy:node_array` contains -zero or more heterogeneous :token:`smithy:node_value`\s. A trailing comma is allowed -in a ``node_array``. +An array node is defined like a JSON array. A :token:`smithy:NodeArray` contains +zero or more heterogeneous :token:`smithy:NodeValue`\s. A trailing comma is allowed +in a ``NodeArray``. The following examples define arrays with zero, one, and two values: @@ -1973,10 +1980,10 @@ The following examples define arrays with zero, one, and two values: .. rubric:: Object node -An object node is defined like a JSON object. A :token:`smithy:node_object` contains -zero or more key value pairs of strings (a :token:`smithy:node_object_key`) that map -to heterogeneous :token:`smithy:node_value`\s. A trailing comma is allowed -in a ``node_object``. +An object node is defined like a JSON object. A :token:`smithy:NodeObject` contains +zero or more key value pairs of strings (a :token:`smithy:NodeObjectKey`) that map +to heterogeneous :token:`smithy:NodeValue`\s. A trailing comma is allowed +in a ``NodeObject``. The following examples define objects with zero, one, and two key value pairs: @@ -1986,8 +1993,8 @@ The following examples define objects with zero, one, and two key value pairs: .. rubric:: Number node -A node :token:`smithy:number` contains numeric data. It is defined like a JSON -number. The following examples define several ``number`` values: +A node :token:`smithy:Number` contains numeric data. It is defined like a JSON +Number. The following examples define several ``Number`` values: * ``0`` * ``0.0`` @@ -1998,7 +2005,7 @@ number. The following examples define several ``number`` values: .. rubric:: Node keywords -Several keywords are used when parsing :token:`smithy:node_value`. +Several keywords are used when parsing :token:`smithy:NodeValue`. * ``true``: The value is treated as a boolean ``true`` * ``false``: The value is treated as a boolean ``false`` @@ -2008,7 +2015,7 @@ Several keywords are used when parsing :token:`smithy:node_value`. String values ============= -A ``node_value`` can contain :token:`smithy:node_string_value` productions that all +A ``NodeValue`` can contain :token:`smithy:NodeStringValue` productions that all define strings. .. rubric:: New lines @@ -2020,7 +2027,7 @@ a string value using the Unicode escape ``\u000d``. .. rubric:: String equivalence -The ``node_string_value`` production defines several productions used to +The ``NodeStringValue`` production defines several productions used to define strings, and in order for these productions to work in concert with the :ref:`JSON AST format `, each of these production MUST be treated like equivalent string values when loaded into the @@ -2166,7 +2173,7 @@ incidental whitespace using the following algorithm: [" Foo", " Baz", "", " ", " Bar", " "] 2. Compute the *common whitespace prefix* by iterating over each line, - counting the number of leading spaces (" ") and taking the minimum count. + counting the Number of leading spaces (" ") and taking the minimum count. Except for the last line of content, lines that are empty or consist wholly of whitespace are not considered. If the last line of content (that is, the line that contains the closing delimiter) appears on its own line, then diff --git a/docs/source-2.0/spec/json-ast.rst b/docs/source-2.0/spec/json-ast.rst index 259304c22db..b0b35a3bac3 100644 --- a/docs/source-2.0/spec/json-ast.rst +++ b/docs/source-2.0/spec/json-ast.rst @@ -301,7 +301,7 @@ shape MAY omit the ``members`` property entirely if the structure contains no members. Each shape's member names MUST be case-insensitively unique across the entire -set of members. Each member name MUST adhere to the :token:`smithy:identifier` +set of members. Each member name MUST adhere to the :token:`smithy:Identifier` ABNF grammar. The following example defines a structure with one required and one optional @@ -434,7 +434,7 @@ shapes defined in JSON support the same properties as the Smithy IDL. - map of :ref:`shape ID ` to trait values - Traits to apply to the service * - rename - - map of :ref:`shape ID ` to ``string`` :token:`smithy:identifier` + - map of :ref:`shape ID ` to ``string`` :token:`smithy:Identifier` - Disambiguates shape name conflicts in the :ref:`service closure `. diff --git a/docs/source-2.0/spec/model.rst b/docs/source-2.0/spec/model.rst index 1b3489f092f..0384ab2be22 100644 --- a/docs/source-2.0/spec/model.rst +++ b/docs/source-2.0/spec/model.rst @@ -504,12 +504,12 @@ Shape IDs have the following syntax: Absolute shape ID - An :dfn:`absolute shape ID` starts with a :token:`namespace `, + An :dfn:`absolute shape ID` starts with a :token:`namespace `, followed by "``#``", followed by a *relative shape ID*. For example, ``smithy.example#Foo`` and ``smithy.example#Foo$bar`` are absolute shape IDs. Relative shape ID - A :dfn:`relative shape ID` contains a :token:`shape name ` - and an optional :token:`member name `. The shape name and + A :dfn:`relative shape ID` contains a :token:`shape name ` + and an optional :token:`member name `. The shape name and member name are separated by the "``$``" symbol if a member name is present. For example, ``Foo`` and ``Foo$bar`` are relative shape IDs. Namespace @@ -545,14 +545,14 @@ Shape ID ABNF Shape IDs are formally defined by the following ABNF: .. productionlist:: smithy - shape_id :`root_shape_id` [`shape_id_member`] - root_shape_id :`absolute_root_shape_id` / `identifier` - absolute_root_shape_id :`namespace` "#" `identifier` - namespace :`identifier` *("." `identifier`) - identifier :identifier_start *identifier_chars - identifier_start :*"_" ALPHA - identifier_chars :ALPHA / DIGIT / "_" - shape_id_member :"$" `identifier` + ShapeId :`RootShapeId` [`ShapeIdMember`] + RootShapeId :`AbsoluteRootShapeId` / `Identifier` + AbsoluteRootShapeId :`Namespace` "#" `Identifier` + Namespace :`Identifier` *("." `Identifier`) + Identifier :IdentifierStart *IdentifierChars + IdentifierStart :*"_" ALPHA + IdentifierChars :ALPHA / DIGIT / "_" + ShapeIdMember :"$" `Identifier` .. _shape-id-conflicts: @@ -598,7 +598,7 @@ An instance of a trait applied to a shape is called an *applied trait*. Only a single instance of a trait can be applied to a shape. The way in which a trait is applied to a shape depends on the model file representation. -Traits are applied to shapes in the IDL using :token:`smithy:trait_statements` that +Traits are applied to shapes in the IDL using :token:`smithy:TraitStatements` that immediately precede a shape. The following example applies the :ref:`length-trait` and :ref:`documentation-trait` to ``MyString``: @@ -645,7 +645,7 @@ Applying traits externally Both the IDL and JSON AST model representations allow traits to be applied to shapes outside of a shape's definition. This is done using an -:token:`apply ` statement in the IDL, or the +:token:`apply ` statement in the IDL, or the :ref:`apply ` type in the JSON AST. For example, this can be useful to allow different teams within the same organization to independently own different facets of a model; a service team could own the model that @@ -738,7 +738,7 @@ Trait node values The value provided for a trait MUST be compatible with the ``shape`` of the trait. The following table defines each shape type that is available to target from traits and how their values are defined in -:token:`node ` values. +:token:`node ` values. .. list-table:: :header-rows: 1 diff --git a/docs/source-2.0/spec/selectors.rst b/docs/source-2.0/spec/selectors.rst index 59bc4093b31..3321ddcecb7 100644 --- a/docs/source-2.0/spec/selectors.rst +++ b/docs/source-2.0/spec/selectors.rst @@ -287,7 +287,7 @@ Numeric comparators ------------------- Relative comparators only match if both values being compared contain valid -:token:`smithy:number` productions when converted to a string. +:token:`smithy:Number` productions when converted to a string. .. list-table:: :header-rows: 1 @@ -395,7 +395,7 @@ The ``id`` attribute can be used as an object, and it supports the following properties. ``namespace`` - Gets the :token:`smithy:namespace` part of a shape ID. + Gets the :token:`smithy:Namespace` part of a shape ID. The following selector matches shapes in the ``foo.baz`` namespace: @@ -547,7 +547,7 @@ to a shape. The ``trait`` attribute supports the following properties: [trait|deprecated] - Traits are converted to their serialized :token:`node ` form + Traits are converted to their serialized :token:`node ` form when matching against their values. Only string, boolean, and numeric values can be compared using a :ref:`string comparator `. Boolean values are converted to "true" or "false". Numeric values are @@ -884,7 +884,7 @@ Context values The first part of a scoped attribute selector is the attribute that is scoped for the expression, followed by ``:``. The scoped attribute is accessed using a :token:`context value ` in the form of -``@{`` :token:`smithy:identifier` ``}``. +``@{`` :token:`smithy:Identifier` ``}``. In the following selector, the ``trait|range`` attribute is used as the scoped attribute of the expression, and the selector matches shapes marked with @@ -1615,20 +1615,20 @@ Selectors are defined by the following ABNF_ grammar. :/ `selector_forward_recursive_neighbor` :/ `selector_variable_set` :/ `selector_variable_get` - selector_shape_types :"*" / `smithy:identifier` + selector_shape_types :"*" / `smithy:Identifier` selector_forward_undirected_neighbor :">" selector_reverse_undirected_neighbor :"<" selector_forward_directed_neighbor :"-[" `selector_directed_relationships` "]->" selector_reverse_directed_neighbor :"<-[" selector_directed_relationships "]-" - selector_directed_relationships :`smithy:identifier` *("," `smithy:identifier`) + selector_directed_relationships :`smithy:Identifier` *("," `smithy:Identifier`) selector_forward_recursive_neighbor :"~>" selector_attr :"[" `selector_key` [selector_attr_comparison] "]" selector_attr_comparison :`selector_comparator` `selector_attr_values` ["i"] - selector_key :`smithy:identifier` ["|" `selector_path`] + selector_key :`smithy:Identifier` ["|" `selector_path`] selector_path :`selector_path_segment` *("|" `selector_path_segment`) selector_path_segment :`selector_value` / `selector_function_property` - selector_value :`selector_text` / `smithy:number` / `smithy:root_shape_id` - selector_function_property :"(" `smithy:identifier` ")" + selector_value :`selector_text` / `smithy:Number` / `smithy:RootShapeId` + selector_function_property :"(" `smithy:Identifier` ")" selector_attr_values :`selector_value` *("," `selector_value`) selector_comparator :`selector_string_comparator` :/ `selector_numeric_comparator` @@ -1636,22 +1636,22 @@ Selectors are defined by the following ABNF_ grammar. selector_string_comparator :"^=" / "$=" / "*=" / "!=" / "=" / "?=" selector_numeric_comparator :">=" / ">" / "<=" / "<" selector_projection_comparator :"{=}" / "{!=}" / "{<}" / "{<<}" - selector_absolute_root_shape_id :`smithy:namespace` "#" `smithy:identifier` + selector_AbsoluteRootShapeId :`smithy:Namespace` "#" `smithy:Identifier` selector_scoped_attr :"[@" [`selector_key`] ":" `selector_scoped_assertions` "]" selector_scoped_assertions :`selector_scoped_assertion` *("&&" `selector_scoped_assertion`) selector_scoped_assertion :`selector_scoped_value` `selector_comparator` `selector_scoped_values` ["i"] selector_scoped_value :`selector_value` / `selector_context_value` selector_context_value :"@{" `selector_path` "}" selector_scoped_values :`selector_scoped_value` *("," `selector_scoped_value`) - selector_function :":" `smithy:identifier` "(" `selector_function_args` ")" + selector_function :":" `smithy:Identifier` "(" `selector_function_args` ")" selector_function_args :`selector` *("," `selector`) selector_text :`selector_single_quoted_text` / `selector_double_quoted_text` selector_single_quoted_text :"'" 1*`selector_single_quoted_char` "'" selector_double_quoted_text :DQUOTE 1*`selector_double_quoted_char` DQUOTE selector_single_quoted_char :%x20-26 / %x28-5B / %x5D-10FFFF ; Excludes (') selector_double_quoted_char :%x20-21 / %x23-5B / %x5D-10FFFF ; Excludes (") - selector_variable_set :"$" `smithy:identifier` "(" selector ")" - selector_variable_get :"${" `smithy:identifier` "}" + selector_variable_set :"$" `smithy:Identifier` "(" selector ")" + selector_variable_get :"${" `smithy:Identifier` "}" .. _ABNF: https://tools.ietf.org/html/rfc5234 .. _set: https://en.wikipedia.org/wiki/Set_(abstract_data_type) diff --git a/docs/source-2.0/spec/service-types.rst b/docs/source-2.0/spec/service-types.rst index 014ae4edf14..b26d9780c3e 100644 --- a/docs/source-2.0/spec/service-types.rst +++ b/docs/source-2.0/spec/service-types.rst @@ -55,7 +55,7 @@ The service shape supports the following properties: contained in the service, and map values are the disambiguated shape names to use in the context of the service. Each given shape ID MUST reference a shape contained in the closure of the service. Each given - map value MUST match the :token:`smithy:identifier` production used for + map value MUST match the :token:`smithy:Identifier` production used for shape IDs. Renaming a shape *does not* give the shape a new shape ID. * No renamed shape name can case-insensitively match any other renamed diff --git a/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/weather-service-wide.smithy b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/weather-service-wide.smithy index ef62062eb18..33b04f1d171 100644 --- a/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/weather-service-wide.smithy +++ b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/weather-service-wide.smithy @@ -34,24 +34,23 @@ resource City { } operation CreateCity { - output := { - @required - cityId: CityId - } input := { name: String coordinates: CityCoordinates } -} -operation UpdateCity { output := { + @required + cityId: CityId } +} +operation UpdateCity { input := { @required cityId: CityId name: String coordinates: CityCoordinates } + output := {} } /// @cfnResource @@ -177,4 +176,3 @@ structure GetForecastInput { structure GetForecastOutput { chanceOfRain: Float } - diff --git a/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/weather.smithy b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/weather.smithy index 234b60ba34a..d49df068676 100644 --- a/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/weather.smithy +++ b/smithy-aws-cloudformation/src/test/resources/software/amazon/smithy/aws/cloudformation/schema/fromsmithy/weather.smithy @@ -66,24 +66,23 @@ resource City { } operation CreateCity { - output := { - @required - cityId: CityId - } input := { name: String coordinates: CityCoordinates } -} -operation UpdateCity { output := { + @required + cityId: CityId } +} +operation UpdateCity { input := { @required cityId: CityId name: String coordinates: CityCoordinates } + output := {} } /// @cfnResource diff --git a/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/tagging/invalid-tag-enabled-service-no-taggable-resource.smithy b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/tagging/invalid-tag-enabled-service-no-taggable-resource.smithy index 88567fb6778..23a13e3855e 100644 --- a/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/tagging/invalid-tag-enabled-service-no-taggable-resource.smithy +++ b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/tagging/invalid-tag-enabled-service-no-taggable-resource.smithy @@ -38,15 +38,15 @@ resource City { } operation CreateCity { - output := { - @required - cityId: CityId - } input := { name: String coordinates: CityCoordinates tags: TagList } + output := { + @required + cityId: CityId + } } @pattern("^[A-Za-z0-9 ]+$") diff --git a/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/tagging/invalid-tag-on-create.smithy b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/tagging/invalid-tag-on-create.smithy index bb8189424de..b18fa828584 100644 --- a/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/tagging/invalid-tag-on-create.smithy +++ b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/tagging/invalid-tag-on-create.smithy @@ -40,15 +40,15 @@ resource City { } operation CreateCity { - output := { - @required - cityId: CityId - } input := { name: String coordinates: CityCoordinates tags: TagList } + output := { + @required + cityId: CityId + } } @pattern("^[A-Za-z0-9 ]+$") diff --git a/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/tagging/invalid-tag-types.smithy b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/tagging/invalid-tag-types.smithy index 02c7cdcc2fb..44aeb1e8cb8 100644 --- a/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/tagging/invalid-tag-types.smithy +++ b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/tagging/invalid-tag-types.smithy @@ -78,14 +78,14 @@ resource City { } operation CreateCity { - output := { - @required - cityId: CityId - } input := { name: String coordinates: CityCoordinates } + output := { + @required + cityId: CityId + } } @pattern("^[A-Za-z0-9 ]+$") diff --git a/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/tagging/tagging-warnings.smithy b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/tagging/tagging-warnings.smithy index 1cf8c2d6bb3..b15aeecee53 100644 --- a/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/tagging/tagging-warnings.smithy +++ b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/tagging/tagging-warnings.smithy @@ -81,26 +81,26 @@ resource City { } operation CreateCity { - output := { - @required - cityId: CityId - } input := { name: String coordinates: CityCoordinates } + output := { + @required + cityId: CityId + } } @pattern("^[A-Za-z0-9 ]+$") string CityId operation UpdateCity { - output := {} input := { @required cityId: CityId tagz: TagList } + output := {} } @readonly @@ -160,4 +160,3 @@ structure GetCurrentTimeOutput { @required time: Timestamp } - diff --git a/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/tagging/valid-tag-specified-apis.smithy b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/tagging/valid-tag-specified-apis.smithy index ddeb23fc192..d0b771b48cd 100644 --- a/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/tagging/valid-tag-specified-apis.smithy +++ b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/tagging/valid-tag-specified-apis.smithy @@ -78,14 +78,14 @@ resource City { } operation CreateCity { - output := { - @required - cityId: CityId - } input := { name: String coordinates: CityCoordinates } + output := { + @required + cityId: CityId + } } @pattern("^[A-Za-z0-9 ]+$") diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlModelParser.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlModelParser.java index 0261897c0ad..5f1d0015424 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlModelParser.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlModelParser.java @@ -222,6 +222,35 @@ public void ws() { } } + // required space + public void rsp() { + int cc = column(); + sp(); + if (column() == cc) { + throw syntax("Expected one or more spaces"); + } + } + + @Override + public void sp() { + while (!eof() && isSpaceOrComma(peek())) { + skip(); + } + } + + private boolean isSpaceOrComma(char c) { + return c == ' ' || c == '\t' || c == ','; + } + + @Override + public void br() { + int line = line(); + ws(); + if (line == line() && peek() != Character.MIN_VALUE) { + throw syntax("Expected a line break"); + } + } + @Override public ModelSyntaxException syntax(String message) { return syntax(null, message); @@ -240,11 +269,10 @@ private void parseControlSection() { Set definedKeys = new HashSet<>(); while (peek() == '$') { expect('$'); - ws(); String key = IdlNodeParser.parseNodeObjectKey(this); - ws(); + sp(); expect(':'); - ws(); + sp(); if (definedKeys.contains(key)) { throw syntax(format("Duplicate control statement `%s`", key)); @@ -274,7 +302,7 @@ private void parseControlSection() { break; } - ws(); + br(); } } @@ -291,6 +319,7 @@ private void onVersion(Node value) { throw syntax("Unsupported Smithy version number: " + parsedVersion); } + emittedVersion = true; modelVersion = resolvedVersion; operations.accept(new LoadOperation.ModelVersion(modelVersion, value.getSourceLocation())); } @@ -305,13 +334,13 @@ private void parseMetadataSection() { expect('a'); expect('t'); expect('a'); - ws(); + rsp(); String key = IdlNodeParser.parseNodeObjectKey(this); - ws(); + sp(); expect('='); - ws(); + sp(); operations.accept(new LoadOperation.PutMetadata(modelVersion, key, IdlNodeParser.parseNode(this))); - ws(); + br(); } } @@ -326,7 +355,7 @@ private void parseShapeSection() { expect('a'); expect('c'); expect('e'); - ws(); + rsp(); // Parse the namespace. int start = position(); @@ -334,7 +363,7 @@ private void parseShapeSection() { namespace = sliceFrom(start); // Clear out any erroneous documentation comments. clearPendingDocs(); - ws(); + br(); parseUseSection(); parseShapeStatements(); @@ -352,7 +381,7 @@ private void parseUseSection() { expect('u'); expect('s'); expect('e'); - ws(); + rsp(); int start = position(); SourceLocation location = currentLocation(); @@ -362,7 +391,7 @@ private void parseUseSection() { String lexeme = sliceFrom(start); // Clear out any erroneous documentation comments. clearPendingDocs(); - ws(); + br(); ShapeId target = ShapeId.from(lexeme); @@ -395,7 +424,6 @@ void useShape(ShapeId id, SourceLocation location) { private void parseShapeStatements() { while (!eof()) { - ws(); if (peek() == 'a') { parseApplyStatement(); } else { @@ -454,7 +482,7 @@ private void parseShape(List traits) { } } - ws(); + rsp(); ShapeId id = parseShapeName(); switch (shapeType) { @@ -534,7 +562,7 @@ private void parseShape(List traits) { addTraits(id, traits); clearPendingDocs(); - ws(); + br(); } private ShapeId parseShapeName() { @@ -778,8 +806,9 @@ private void parseMember( throw syntax("@default assignment is only supported in IDL version 2 or later"); } expect('='); - ws(); + sp(); memberBuilder.addTrait(memberParsing.createAssignmentTrait(memberId, IdlNodeParser.parseNode(this))); + br(); } // Only add the member once fully parsed. @@ -853,47 +882,56 @@ private void parseOperationStatement(ShapeId id, SourceLocation location) { OperationShape.Builder builder = OperationShape.builder().id(id).source(location); LoadOperation.DefineShape operation = createShape(builder); parseMixins(operation); - parseProperties(id, propertyName -> { - switch (propertyName) { - case "input": - TraitEntry inputTrait = new TraitEntry(InputTrait.ID.toString(), Node.objectNode(), true); - parseInlineableOperationMember(id, operationInputSuffix, builder::input, inputTrait); - break; - case "output": - TraitEntry outputTrait = new TraitEntry(OutputTrait.ID.toString(), Node.objectNode(), true); - parseInlineableOperationMember(id, operationOutputSuffix, builder::output, outputTrait); - break; - case "errors": - parseIdList(builder::addError); - break; - default: - throw syntax(id, String.format("Unknown property %s for %s", propertyName, id)); - } - }); - clearPendingDocs(); - operations.accept(operation); - } - - private void parseProperties(ShapeId id, Consumer valueParser) { ws(); expect('{'); ws(); - Set defined = new HashSet<>(); - while (!eof() && peek() != '}') { - String key = IdlNodeParser.parseNodeObjectKey(this); - if (defined.contains(key)) { - throw syntax(id, String.format("Duplicate %s binding for %s", key, id)); - } - defined.add(key); + char next = expect('i', 'o', 'e', '}'); + if (next == 'i') { + expect('n'); + expect('p'); + expect('u'); + expect('t'); ws(); expect(':'); - valueParser.accept(key); + TraitEntry inputTrait = new TraitEntry(InputTrait.ID.toString(), Node.objectNode(), true); + parseInlineableOperationMember(id, operationInputSuffix, builder::input, inputTrait); + br(); + next = expect('o', 'e', '}'); + } + + if (next == 'o') { + expect('u'); + expect('t'); + expect('p'); + expect('u'); + expect('t'); ws(); + expect(':'); + TraitEntry outputTrait = new TraitEntry(OutputTrait.ID.toString(), Node.objectNode(), true); + parseInlineableOperationMember(id, operationOutputSuffix, builder::output, outputTrait); + br(); + next = expect('e', '}'); } - expect('}'); + if (next == 'e') { + expect('r'); + expect('r'); + expect('o'); + expect('r'); + expect('s'); + ws(); + expect(':'); + parseIdList(builder::addError); + br(); + expect('}'); + } else if (next != '}') { + expect('}'); + } + + clearPendingDocs(); + operations.accept(operation); } private void parseInlineableOperationMember( @@ -932,7 +970,6 @@ private ShapeId parseInlineStructure(String name, TraitEntry defaultTrait) { addTraits(id, traits); clearPendingDocs(); operations.accept(operation); - ws(); return id; } @@ -952,7 +989,7 @@ private void parseForResource(LoadOperation.DefineShape operation) { + modelVersion + "`."); } - ws(); + rsp(); addForwardReference(ParserUtils.parseShapeId(this), shapeId -> { operation.addDependency(shapeId); @@ -1080,21 +1117,37 @@ private String parseDocCommentLine() { } int start = position(); consumeRemainingCharactersOnLine(); - br(); + nl(); sp(); return StringUtils.stripEnd(sliceFrom(start), " \t\r\n"); } + private void nl() { + switch (peek()) { + case '\n': + skip(); + break; + case '\r': + skip(); + if (peek() == '\n') { + expect('\n'); + } + break; + default: + throw syntax("Expected a newline"); + } + } + private void parseApplyStatement() { expect('a'); expect('p'); expect('p'); expect('l'); expect('y'); - ws(); + rsp(); String name = ParserUtils.parseShapeId(this); - ws(); + rsp(); // Account for singular or block apply statements. List traitsToApply; @@ -1116,7 +1169,7 @@ private void parseApplyStatement() { // Clear out any errantly captured pending docs. clearPendingDocs(); - ws(); + br(); } private void addTraits(ShapeId id, List traits) { diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/dupe-operation-binding.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/dupe-operation-binding.errors deleted file mode 100644 index ad2154ca508..00000000000 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/dupe-operation-binding.errors +++ /dev/null @@ -1 +0,0 @@ -[ERROR] com.foo#GetFoo: Parse error at line 7, column 10 near `: `: Duplicate input binding for com.foo#GetFoo | Model diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/excess-operation-keys.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/excess-operation-keys.errors deleted file mode 100644 index 949b80317ff..00000000000 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/excess-operation-keys.errors +++ /dev/null @@ -1 +0,0 @@ -[ERROR] com.example#Operation: Parse error at line 6, column 25 near ` String`: Unknown property invalidOperationKey for com.example#Operation | Model diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/resource-properties/resource-properties-errors.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/resource-properties/resource-properties-errors.smithy index 7524a70e07b..dc0338cf826 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/resource-properties/resource-properties-errors.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/resource-properties/resource-properties-errors.smithy @@ -11,8 +11,8 @@ resource Forecast { @readonly operation GetForecast { - output: GetForecastOutput input: GetForecastInput + output: GetForecastOutput } structure GetForecastOutput { diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/apply/apply-missing-newline.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/apply/apply-missing-newline.smithy new file mode 100644 index 00000000000..5657545dd42 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/apply/apply-missing-newline.smithy @@ -0,0 +1,5 @@ +// Parse error at line 5, column 29 near `apply`: Expected a line break | Model +$version: "2.0" +namespace com.foo + +apply SomeShape @deprecated apply SomeShape @sensitive diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/apply/apply-missing-space.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/apply/apply-missing-space.smithy new file mode 100644 index 00000000000..e08fe049a04 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/apply/apply-missing-space.smithy @@ -0,0 +1,5 @@ +// Parse error at line 5, column 10 near `{ `: Expected one or more spaces | Model +$version: "2.0" +namespace smithy.example + +apply Foo{ @sensitive } diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/apply/apply-missing-trait-value.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/apply/apply-missing-trait-value.smithy index 61ed0eb7faf..02270631439 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/apply/apply-missing-trait-value.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/apply/apply-missing-trait-value.smithy @@ -1,5 +1,5 @@ -// Parse error at line 5, column 1 near `string`: Expected: '@', but found 's' | Model +// Parse error at line 4, column 17 near `//`: Expected: '@', but found '/' | Model namespace com.foo -apply SomeShape +apply SomeShape // comment so spaces aren't eaten string MyString diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/apply/apply-multiple-lines.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/apply/apply-multiple-lines.smithy new file mode 100644 index 00000000000..8209b34adfb --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/apply/apply-multiple-lines.smithy @@ -0,0 +1,7 @@ +// Parse error at line 6, column 10 near `\n@`: Expected one or more spaces | Model +$version: "2.0" + +namespace smithy.example + +apply Foo +@sensitive diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/control/control-colon-newline.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/control/control-colon-newline.smithy new file mode 100644 index 00000000000..7b0a7fa1c13 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/control/control-colon-newline.smithy @@ -0,0 +1,3 @@ +// Parse error at line 2, column 10 near `\n"`: Expected a valid identifier character, but found '\n' | Model +$version: +"2.0" diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/control/control-missing-value.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/control/control-missing-value.smithy new file mode 100644 index 00000000000..aa20ed498d2 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/control/control-missing-value.smithy @@ -0,0 +1,2 @@ +// Parse error at line 2, column 10 +$version: diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/control/control-newline.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/control/control-newline.smithy new file mode 100644 index 00000000000..96d8ac1808f --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/control/control-newline.smithy @@ -0,0 +1,3 @@ +// Parse error at line 2, column 9 near `\n:`: Expected: ':', but found '\n' | Model +$version +: "2.0" diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/control-statement-before-others.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/control/control-statement-before-others.smithy similarity index 100% rename from smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/control-statement-before-others.smithy rename to smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/control/control-statement-before-others.smithy diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/control-version-defined-twice.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/control/control-version-defined-twice.smithy similarity index 100% rename from smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/control-version-defined-twice.smithy rename to smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/control/control-version-defined-twice.smithy diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/control-with-invalid-key.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/control/control-with-invalid-key.smithy similarity index 100% rename from smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/control-with-invalid-key.smithy rename to smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/control/control-with-invalid-key.smithy diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/control-with-invalid-key2.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/control/control-with-invalid-key2.smithy similarity index 100% rename from smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/control-with-invalid-key2.smithy rename to smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/control/control-with-invalid-key2.smithy diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/control-with-no-colon.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/control/control-with-no-colon.smithy similarity index 100% rename from smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/control-with-no-colon.smithy rename to smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/control/control-with-no-colon.smithy diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/control/no-newline-after-control.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/control/no-newline-after-control.smithy new file mode 100644 index 00000000000..b7d5f6b1388 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/control/no-newline-after-control.smithy @@ -0,0 +1,2 @@ +// Parse error at line 2, column 17 near `$x`: Expected a line break | Model +$version: "2.0" $xyz: "100" diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/defaults/default-missing-value.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/defaults/default-missing-value.smithy index 85a16186ac6..4875761ece0 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/defaults/default-missing-value.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/defaults/default-missing-value.smithy @@ -1,4 +1,4 @@ -// Parse error at line 8, column 1 near `}\n`: Expected a valid identifier character, but found '}' +// Parse error at line 7, column 18 near `\n}`: Expected a valid identifier character, but found '\n' | Model $version: "2.0" namespace com.foo diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/defaults/default-with-newline-before-value.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/defaults/default-with-newline-before-value.smithy new file mode 100644 index 00000000000..059fc58c175 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/defaults/default-with-newline-before-value.smithy @@ -0,0 +1,9 @@ +// Parse error at line 7, column 18 near `\n `: Expected a valid identifier character, but found '\n' | Model +$version: "2.0" + +namespace com.foo + +structure Foo { + bar: String = + "Hi" +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/defaults/missing-newline-after-assignment.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/defaults/missing-newline-after-assignment.smithy new file mode 100644 index 00000000000..ba716c99a15 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/defaults/missing-newline-after-assignment.smithy @@ -0,0 +1,7 @@ +// Parse error at line 6, column 24 near `baz`: Expected a line break | Model +$version: "2.0" +namespace com.foo + +structure Foo { + bar: String = "Hi" baz: String = "Hi" +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/enum-values/enum-with-newline-before-value.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/enum-values/enum-with-newline-before-value.smithy new file mode 100644 index 00000000000..864b2c19e2f --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/enum-values/enum-with-newline-before-value.smithy @@ -0,0 +1,8 @@ +// Parse error at line 6, column 10 near `\n `: Expected a valid identifier character, but found '\n' | Model +$version: "2.0" +namespace smithy.example + +enum Foo { + BAR = + "hi" +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/enum-values/missing-newline-after-assignment.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/enum-values/missing-newline-after-assignment.smithy new file mode 100644 index 00000000000..13b78a67c68 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/enum-values/missing-newline-after-assignment.smithy @@ -0,0 +1,7 @@ +// Parse error at line 6, column 13 near `BAZ`: Expected a line break | Model +$version: "2.0" +namespace smithy.example + +intEnum Foo { + BAR = 1 BAZ = 2 +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/expected-shape-name-but-eof.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/expected-shape-name-but-eof.smithy index f58d35f2d1a..6a66f022631 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/expected-shape-name-but-eof.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/expected-shape-name-but-eof.smithy @@ -1,4 +1,4 @@ -// Parse error at line 5, column 1 near `[EOF]`: Expected a valid identifier character, but found '[EOF]' | Model +// Parse error at line 4, column 7 near `\n`: Expected one or more spaces | Model namespace com.foo string diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/metadata/metadata-multiple-lines.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/metadata/metadata-multiple-lines.smithy new file mode 100644 index 00000000000..799fda5d8d1 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/metadata/metadata-multiple-lines.smithy @@ -0,0 +1,4 @@ +// Parse error at line 3, column 16 near `//`: Expected a valid identifier character, but found '/' | Model +$version: "2.0" +metadata foo = // this is not allows +"bar" diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/metadata-must-come-before-namespace.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/metadata/metadata-must-come-before-namespace.smithy similarity index 100% rename from smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/metadata-must-come-before-namespace.smithy rename to smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/metadata/metadata-must-come-before-namespace.smithy diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/metadata-with-invalid-key.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/metadata/metadata-with-invalid-key.smithy similarity index 100% rename from smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/metadata-with-invalid-key.smithy rename to smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/metadata/metadata-with-invalid-key.smithy diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/metadata/no-newline-after-metadata.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/metadata/no-newline-after-metadata.smithy new file mode 100644 index 00000000000..e7e8df4e133 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/metadata/no-newline-after-metadata.smithy @@ -0,0 +1,3 @@ +// Parse error at line 3, column 22 near `metadata`: Expected a line break | Model +$version: "2.0" +metadata foo = "bar" metadata baz = "bar" diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/metadata/not-newline-after-metadata.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/metadata/not-newline-after-metadata.smithy new file mode 100644 index 00000000000..7b2125e08d3 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/metadata/not-newline-after-metadata.smithy @@ -0,0 +1,4 @@ +// Parse error at line 3, column 9 near `\nf`: Expected one or more spaces | Model +$version: "2.0" +metadata +foo = "bar" diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/metadata/space-after-metadata-quoted.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/metadata/space-after-metadata-quoted.smithy new file mode 100644 index 00000000000..992ebaa7854 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/metadata/space-after-metadata-quoted.smithy @@ -0,0 +1,3 @@ +// Parse error at line 3, column 9 near `"f`: Expected one or more spaces | Model +$version: "2.0" +metadata"foo"="bar" diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/metadata/space-after-metadata-unquoted.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/metadata/space-after-metadata-unquoted.smithy new file mode 100644 index 00000000000..2b3b2a43180 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/metadata/space-after-metadata-unquoted.smithy @@ -0,0 +1,3 @@ +// Parse error at line 3, column 9 near `foo`: Expected one or more spaces | Model +$version: "2.0" +metadatafoo"="bar" diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/metadata/space-after-node-array-keyword.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/metadata/space-after-node-array-keyword.smithy new file mode 100644 index 00000000000..e2340ff68d7 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/metadata/space-after-node-array-keyword.smithy @@ -0,0 +1,9 @@ +// Syntactic shape ID `truefalse` does not resolve to a valid shape ID: `smithy.api#truefalse` +$version: "2.0" +metadata foo = [truefalse] + +// Since NodeValue has keywords and those keywords are used to resolve +// ambiguity in the grammar, this failing with a syntactic shape ID error +// could be considered correct because "truefalse" is not a valid shape ID +// in the smithy.api# namespace and does not exactly match the true or false +// keywords. diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/namespace-before-shapes.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/namespace/namespace-before-shapes.smithy similarity index 100% rename from smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/namespace-before-shapes.smithy rename to smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/namespace/namespace-before-shapes.smithy diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/namespace-before-annotations.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/namespace/namespace-before-traits.smithy similarity index 100% rename from smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/namespace-before-annotations.smithy rename to smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/namespace/namespace-before-traits.smithy diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/namespace-defined-twice.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/namespace/namespace-defined-twice.smithy similarity index 100% rename from smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/namespace-defined-twice.smithy rename to smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/namespace/namespace-defined-twice.smithy diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/namespace/namespace-multiple-lines.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/namespace/namespace-multiple-lines.smithy new file mode 100644 index 00000000000..5f1eb7212d5 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/namespace/namespace-multiple-lines.smithy @@ -0,0 +1,4 @@ +// Parse error at line 3, column 14 near `//`: Expected a valid identifier character, but found '/' | Model +$version: "2.0" +namespace // Spaces are fine. The comment is fine. But the newline is not fine. +com.foo diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/namespace/namespace-no-newline.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/namespace/namespace-no-newline.smithy new file mode 100644 index 00000000000..75938812851 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/namespace/namespace-no-newline.smithy @@ -0,0 +1,3 @@ +// Parse error at line 3, column 19 near `string`: Expected a line break | Model +$version: "2.0" +namespace com.foo string Foo diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/namespace-syntax-error.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/namespace/namespace-syntax-error.smithy similarity index 100% rename from smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/namespace-syntax-error.smithy rename to smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/namespace/namespace-syntax-error.smithy diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/unclosed-string1.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/namespace/unclosed-string1.smithy similarity index 100% rename from smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/unclosed-string1.smithy rename to smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/namespace/unclosed-string1.smithy diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/unclosed-string2-invalid-single-quote.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/namespace/unclosed-string2-invalid-single-quote.smithy similarity index 100% rename from smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/unclosed-string2-invalid-single-quote.smithy rename to smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/namespace/unclosed-string2-invalid-single-quote.smithy diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/unclosed-string3.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/namespace/unclosed-string3.smithy similarity index 100% rename from smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/unclosed-string3.smithy rename to smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/namespace/unclosed-string3.smithy diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/operations/errors-before-input.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/operations/errors-before-input.smithy new file mode 100644 index 00000000000..c0c70dc3b4f --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/operations/errors-before-input.smithy @@ -0,0 +1,8 @@ +// Parse error at line 7, column 5 near `input`: Expected: '}', but found 'i' | Model +$version: "2.0" +namespace smithy.example + +operation GetFoo { + errors: [] + input: GetFooInput +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/dupe-operation-binding.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/operations/input-defined-twice.smithy similarity index 53% rename from smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/dupe-operation-binding.smithy rename to smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/operations/input-defined-twice.smithy index f289e5e2fac..847c243f875 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/dupe-operation-binding.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/operations/input-defined-twice.smithy @@ -1,3 +1,4 @@ +// Parse error at line 8, column 5 near `input`: Found 'i', but expected one of the following tokens: 'o' 'e' '}' | Model $version: "2.0" namespace com.foo diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/excess-operation-keys.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/operations/invalid-operation-properties.smithy similarity index 73% rename from smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/excess-operation-keys.smithy rename to smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/operations/invalid-operation-properties.smithy index bc9193b27e6..e695311499d 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/excess-operation-keys.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/operations/invalid-operation-properties.smithy @@ -1,5 +1,5 @@ +// Parse error at line 6, column 7 $version: "2.0" - namespace com.example operation Operation { diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/operations/newline-missing-after-input.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/operations/newline-missing-after-input.smithy new file mode 100644 index 00000000000..6600dbc1bc0 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/operations/newline-missing-after-input.smithy @@ -0,0 +1,7 @@ +// Parse error at line 6, column 24 near `output`: Expected a line break +$version: "2.0" +namespace smithy.example + +operation GetFoo { + input: GetFooInput output: GetFooOutput +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/operations/newline-missing-after-output.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/operations/newline-missing-after-output.smithy new file mode 100644 index 00000000000..66879552214 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/operations/newline-missing-after-output.smithy @@ -0,0 +1,7 @@ +// Parse error at line 6, column 26 near `errors`: Expected a line break | Model +$version: "2.0" +namespace smithy.example + +operation GetFoo { + output: GetFooOutput errors: [] +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/operations/output-before-input.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/operations/output-before-input.smithy new file mode 100644 index 00000000000..9ca176be0fd --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/operations/output-before-input.smithy @@ -0,0 +1,8 @@ +// Parse error at line 7, column 5 near `input`: Found 'i', but expected one of the following tokens: 'e' '}' | Model +$version: "2.0" +namespace smithy.example + +operation GetFoo { + output: GetFooOutput + input: GetFooInput +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/parse-errors/invalid-newline-after-shape-type.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/parse-errors/invalid-newline-after-shape-type.smithy new file mode 100644 index 00000000000..a6b9d04a8b3 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/parse-errors/invalid-newline-after-shape-type.smithy @@ -0,0 +1,6 @@ +// Parse error at line 5, column 10 near `\nF`: Expected one or more spaces | Model +$version: "2.0" +namespace smithy.example + +structure +Foo {} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/parse-errors/missing-space-after-string.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/parse-errors/missing-space-after-string.smithy new file mode 100644 index 00000000000..82da8cd2c39 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/parse-errors/missing-space-after-string.smithy @@ -0,0 +1,5 @@ +// Parse error at line 5, column 10 near `\n`: Unexpected shape type: stringFoo | Model +$version: "2.0" +namespace smithy.example + +stringFoo diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/parse-errors/missing-space-after-structure.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/parse-errors/missing-space-after-structure.smithy new file mode 100644 index 00000000000..6e700617002 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/parse-errors/missing-space-after-structure.smithy @@ -0,0 +1,5 @@ +// Parse error at line 5, column 13 near `{}`: Unexpected shape type: structureFoo | Model +$version: "2.0" +namespace smithy.example + +structureFoo{} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/use/use-missing-newline.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/use/use-missing-newline.smithy new file mode 100644 index 00000000000..6edf7a7f93d --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/use/use-missing-newline.smithy @@ -0,0 +1,5 @@ +// Parse error at line 5, column 17 near `use`: Expected a line break | Model +$version: "2.0" +namespace smithy.example + +use com.foo#Bar use com.foo#Baz diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/use/use-missing-space.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/use/use-missing-space.smithy new file mode 100644 index 00000000000..53c0f5f292b --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/use/use-missing-space.smithy @@ -0,0 +1,5 @@ +// Parse error at line 5, column 4 near `com` +$version: "2.0" +namespace smithy.example + +usecom.foo#Bar diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/use/use-multiple-lines-comment.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/use/use-multiple-lines-comment.smithy new file mode 100644 index 00000000000..1a90d9ae29b --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/use/use-multiple-lines-comment.smithy @@ -0,0 +1,6 @@ +// Parse error at line 5, column 5 near `//`: Expected a valid identifier character, but found '/' | Model +$version: "2.0" +namespace smithy.example + +use // Invalid +smithy.example#Foo diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/use/use-multiple-lines.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/use/use-multiple-lines.smithy new file mode 100644 index 00000000000..c29ce156f5b --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/use/use-multiple-lines.smithy @@ -0,0 +1,6 @@ +// Parse error at line 5, column 4 near `\ns`: Expected one or more spaces | Model +$version: "2.0" +namespace smithy.example + +use +smithy.example#Foo diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/apply/apply-with-whitespace.json b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/apply/apply-with-whitespace.json deleted file mode 100644 index 12e0ffb752b..00000000000 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/apply/apply-with-whitespace.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "smithy": "2.0", - "shapes": { - "smithy.example#Foo": { - "type": "structure", - "members": { - "baz": { - "target": "smithy.api#String", - "traits": { - "smithy.api#documentation": "Hi", - "smithy.api#internal": {}, - "smithy.api#deprecated": {} - } - } - } - } - } -} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/apply/apply-with-whitespace.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/apply/apply-with-whitespace.smithy deleted file mode 100644 index f3096f49689..00000000000 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/apply/apply-with-whitespace.smithy +++ /dev/null @@ -1,14 +0,0 @@ -$version: "2.0" -namespace smithy.example - -structure Foo { - baz: String, -} - -apply Foo$baz{@documentation("Hi") @internal - - - - - - @deprecated} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/defaults/valid-defaults.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/defaults/valid-defaults.smithy index 55ee02e295e..872e45e99e8 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/defaults/valid-defaults.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/defaults/valid-defaults.smithy @@ -19,8 +19,7 @@ structure Foo { n: Long = 100 o: Float = 0 p: Double= 0 - q: StringMap = - {} + q: StringMap = {} // comment r: BigInteger = 0 s: BigDecimal = 0 } diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums.json b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums/enums.json similarity index 100% rename from smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums.json rename to smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums/enums.json diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums/enums.smithy similarity index 100% rename from smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums.smithy rename to smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums/enums.smithy diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums/short-form-enum.json b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums/short-form-enum.json new file mode 100644 index 00000000000..780fe439fe8 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums/short-form-enum.json @@ -0,0 +1,80 @@ +{ + "smithy": "2.0", + "shapes": { + "smithy.example#A": { + "type": "enum", + "members": { + "FOO": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "FOO" + } + }, + "BAR": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "BAR" + } + }, + "BAZ": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "BAZ" + } + } + } + }, + "smithy.example#B": { + "type": "enum", + "members": { + "FOO": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "FOO" + } + }, + "BAR": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "BAR" + } + }, + "BAZ": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "BAZ" + } + } + } + }, + "smithy.example#C": { + "type": "enum", + "members": { + "FOO": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "FOO" + } + } + } + }, + "smithy.example#D": { + "type": "enum", + "members": { + "FOO": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "FOO" + } + }, + "BAR": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#documentation": "BAR", + "smithy.api#enumValue": "bar" + } + } + } + } + } +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums/short-form-enum.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums/short-form-enum.smithy new file mode 100644 index 00000000000..70a1285cdd9 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums/short-form-enum.smithy @@ -0,0 +1,13 @@ +$version: "2.0" +namespace smithy.example + +enum A { FOO BAR BAZ } +enum B { FOO, BAR, BAZ // Test + } + +enum C { FOO } + +enum D { FOO + /// BAR + BAR = "bar" + } diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/newlines-are-not-required.json b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/newlines-are-not-required.json deleted file mode 100644 index 8c531be6e71..00000000000 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/newlines-are-not-required.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "smithy": "1.0", - "shapes": { - "smithy.example#MyString": { - "type": "string" - }, - "smithy.example#Foo": { - "type": "structure", - "traits": { - "smithy.api#deprecated": {} - } - } - } -} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/newlines-are-not-required.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/newlines-are-not-required.smithy deleted file mode 100644 index cb333abcc07..00000000000 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/newlines-are-not-required.smithy +++ /dev/null @@ -1 +0,0 @@ -namespace smithy.example string MyString @deprecated structure Foo {} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-all.json b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-all.json new file mode 100644 index 00000000000..e75d723fc2b --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-all.json @@ -0,0 +1,28 @@ +{ + "smithy": "2.0", + "shapes": { + "smithy.example#GetFoo": { + "type": "operation", + "input": { + "target": "smithy.example#GetFooInput" + }, + "output": { + "target": "smithy.example#GetFooOutput" + } + }, + "smithy.example#GetFooInput": { + "type": "structure", + "members": {}, + "traits": { + "smithy.api#input": {} + } + }, + "smithy.example#GetFooOutput": { + "type": "structure", + "members": {}, + "traits": { + "smithy.api#output": {} + } + } + } +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-all.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-all.smithy new file mode 100644 index 00000000000..167f7651816 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-all.smithy @@ -0,0 +1,14 @@ +$version: "2.0" +namespace smithy.example + +operation GetFoo { + input: GetFooInput + output: GetFooOutput + errors: [] +} + +@input +structure GetFooInput {} + +@output +structure GetFooOutput {} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-errors.json b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-errors.json new file mode 100644 index 00000000000..2fc497e6ae3 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-errors.json @@ -0,0 +1,14 @@ +{ + "smithy": "2.0", + "shapes": { + "smithy.example#GetFoo": { + "type": "operation", + "input": { + "target": "smithy.api#Unit" + }, + "output": { + "target": "smithy.api#Unit" + } + } + } +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-errors.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-errors.smithy new file mode 100644 index 00000000000..96f805a8fcd --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-errors.smithy @@ -0,0 +1,6 @@ +$version: "2.0" +namespace smithy.example + +operation GetFoo { + errors: [] +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-inline-input.json b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-inline-input.json new file mode 100644 index 00000000000..bcec9e1e17d --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-inline-input.json @@ -0,0 +1,21 @@ +{ + "smithy": "2.0", + "shapes": { + "smithy.example#GetFoo": { + "type": "operation", + "input": { + "target": "smithy.example#GetFooInput" + }, + "output": { + "target": "smithy.api#Unit" + } + }, + "smithy.example#GetFooInput": { + "type": "structure", + "members": {}, + "traits": { + "smithy.api#input": {} + } + } + } +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-inline-input.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-inline-input.smithy new file mode 100644 index 00000000000..df84adb275a --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-inline-input.smithy @@ -0,0 +1,6 @@ +$version: "2.0" +namespace smithy.example + +operation GetFoo { + input := {} +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-input-and-output.json b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-input-and-output.json new file mode 100644 index 00000000000..e75d723fc2b --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-input-and-output.json @@ -0,0 +1,28 @@ +{ + "smithy": "2.0", + "shapes": { + "smithy.example#GetFoo": { + "type": "operation", + "input": { + "target": "smithy.example#GetFooInput" + }, + "output": { + "target": "smithy.example#GetFooOutput" + } + }, + "smithy.example#GetFooInput": { + "type": "structure", + "members": {}, + "traits": { + "smithy.api#input": {} + } + }, + "smithy.example#GetFooOutput": { + "type": "structure", + "members": {}, + "traits": { + "smithy.api#output": {} + } + } + } +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-input-and-output.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-input-and-output.smithy new file mode 100644 index 00000000000..193fbe94c7e --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-input-and-output.smithy @@ -0,0 +1,13 @@ +$version: "2.0" +namespace smithy.example + +operation GetFoo { + input: GetFooInput + output: GetFooOutput +} + +@input +structure GetFooInput {} + +@output +structure GetFooOutput {} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-input.json b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-input.json new file mode 100644 index 00000000000..bcec9e1e17d --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-input.json @@ -0,0 +1,21 @@ +{ + "smithy": "2.0", + "shapes": { + "smithy.example#GetFoo": { + "type": "operation", + "input": { + "target": "smithy.example#GetFooInput" + }, + "output": { + "target": "smithy.api#Unit" + } + }, + "smithy.example#GetFooInput": { + "type": "structure", + "members": {}, + "traits": { + "smithy.api#input": {} + } + } + } +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-input.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-input.smithy new file mode 100644 index 00000000000..cf5231f267b --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-input.smithy @@ -0,0 +1,9 @@ +$version: "2.0" +namespace smithy.example + +operation GetFoo { + input: GetFooInput +} + +@input +structure GetFooInput {} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-no-properties.json b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-no-properties.json new file mode 100644 index 00000000000..2fc497e6ae3 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-no-properties.json @@ -0,0 +1,14 @@ +{ + "smithy": "2.0", + "shapes": { + "smithy.example#GetFoo": { + "type": "operation", + "input": { + "target": "smithy.api#Unit" + }, + "output": { + "target": "smithy.api#Unit" + } + } + } +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-no-properties.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-no-properties.smithy new file mode 100644 index 00000000000..83030521b21 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-no-properties.smithy @@ -0,0 +1,4 @@ +$version: "2.0" +namespace smithy.example + +operation GetFoo {} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-output.json b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-output.json new file mode 100644 index 00000000000..286b9dd065e --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-output.json @@ -0,0 +1,21 @@ +{ + "smithy": "2.0", + "shapes": { + "smithy.example#GetFoo": { + "type": "operation", + "input": { + "target": "smithy.api#Unit" + }, + "output": { + "target": "smithy.example#GetFooOutput" + } + }, + "smithy.example#GetFooOutput": { + "type": "structure", + "members": {}, + "traits": { + "smithy.api#output": {} + } + } + } +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-output.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-output.smithy new file mode 100644 index 00000000000..df28698b01c --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-with-output.smithy @@ -0,0 +1,9 @@ +$version: "2.0" +namespace smithy.example + +operation GetFoo { + output: GetFooOutput +} + +@output +structure GetFooOutput {} From a4c4f46dda796b507d318fbc15373c08e2d5d6bb Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Thu, 4 Aug 2022 12:57:43 -0700 Subject: [PATCH 05/20] Explicitly model and constrain members Members of list, map, union, and structure are now explicitly modeled rather than relying on a generic grammar for all members. Two new constraints: 1. Map keys, if defined, must be defined before map values. 2. Members that are not elided must be on the same line. `foo:\nBar` is no longer valid. --- docs/source-2.0/spec/idl.rst | 122 ++++--- docs/source-2.0/spec/model.rst | 2 +- .../upgrade/cases/primitive.v1.smithy | 3 +- .../upgrade/cases/primitive.v2.smithy | 3 +- .../smithy/model/loader/IdlModelParser.java | 340 ++++++++---------- .../model/loader/LoadOperationProcessor.java | 2 +- .../smithy/model/traits/EnumValueTrait.java | 56 +-- .../validators/EnumShapeValidator.java | 60 +++- .../model/loader/ModelAssemblerTest.java | 13 + .../loader/dupe-list-member-names.errors | 2 +- .../loader/dupe-map-member-names.errors | 2 +- .../loader/dupe-set-member-names.errors | 2 +- .../validators/enum-value-invalid-type.errors | 5 - .../validators/{ => enums}/enum-shapes.errors | 8 +- .../validators/{ => enums}/enum-shapes.smithy | 0 .../{ => enums}/enum-trait-validation.errors | 0 .../{ => enums}/enum-trait-validation.json | 0 .../enums/enum-value-invalid-type.errors | 5 + .../enum-value-invalid-type.smithy | 0 .../invalid/defaults/default-in-v1.smithy | 7 + .../{ => elision}/elide-nonexistent-id.smithy | 0 .../elided-property-type-conflicts.smithy | 0 .../elided-type-conflicts.smithy | 0 .../elided-union-member-from-resource.smithy | 0 .../eliding-with-resources-in-v1.smithy | 0 .../eliding-without-resources-v1.smithy | 2 +- .../elision/list-member-elision-in-v1.smithy | 7 + .../structure-member-elision-in-v1.smithy | 7 + .../enum-values/enum-with-bad-values.smithy | 2 +- .../enum-values/intEnum-with-bad-value.smithy | 2 +- .../enum-values/intEnum-with-float.smithy | 2 +- .../enum-values/intEnum-with-long.smithy | 2 +- .../loader/invalid/enum-without-member.smithy | 2 +- .../loader/invalid/list-invalid-member.smithy | 2 +- .../loader/invalid/map-invalid-member.smithy | 2 +- .../loader/invalid/set-invalid-member.smithy | 2 +- .../model/loader/valid/structures.smithy | 7 +- .../smithy/model/loader/valid/unions.smithy | 7 +- 38 files changed, 345 insertions(+), 333 deletions(-) delete mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-value-invalid-type.errors rename smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/{ => enums}/enum-shapes.errors (67%) rename smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/{ => enums}/enum-shapes.smithy (100%) rename smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/{ => enums}/enum-trait-validation.errors (100%) rename smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/{ => enums}/enum-trait-validation.json (100%) create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enums/enum-value-invalid-type.errors rename smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/{ => enums}/enum-value-invalid-type.smithy (100%) create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/defaults/default-in-v1.smithy rename smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/{ => elision}/elide-nonexistent-id.smithy (100%) rename smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/{ => elision}/elided-property-type-conflicts.smithy (100%) rename smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/{ => elision}/elided-type-conflicts.smithy (100%) rename smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/{ => elision}/elided-union-member-from-resource.smithy (100%) rename smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/{ => elision}/eliding-with-resources-in-v1.smithy (100%) rename smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/{ => elision}/eliding-without-resources-v1.smithy (69%) create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/elision/list-member-elision-in-v1.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/elision/structure-member-elision-in-v1.smithy diff --git a/docs/source-2.0/spec/idl.rst b/docs/source-2.0/spec/idl.rst index 280b17fd217..d285fb3de0a 100644 --- a/docs/source-2.0/spec/idl.rst +++ b/docs/source-2.0/spec/idl.rst @@ -100,7 +100,7 @@ string support defined in `RFC 5234 `_. WS :1*(`SP` / `NL` / `Comment` / ",") ; whitespace SP :1*(%x20 / %x09) ; one or more spaces or tabs NL :%x0A / %x0D.0A ; Newline: \n and \r\n - NotNL: %x09 / %x20-10FFFF ; Any character except newline + NotNL:%x09 / %x20-10FFFF ; Any character except newline BR :*`SP` 1*(`Comment` / `NL`) *`WS`; line break followed by whitespace .. rubric:: Comments @@ -166,58 +166,71 @@ string support defined in `RFC 5234 `_. .. rubric:: Shapes .. productionlist:: smithy - ShapeSection :[`NamespaceStatement` `UseSection` `ShapeStatements`] - NamespaceStatement :%s"namespace" `SP` `Namespace` `BR` - UseSection :*(`UseStatement`) - UseStatement :%s"use" `SP` `AbsoluteRootShapeId` `BR` - ShapeStatements :*(`ShapeStatement` / `ApplyStatement`) - ShapeStatement :`TraitStatements` `ShapeBody` `BR` - ShapeBody :`SimpleShapeStatement` - :/ `EnumShapeStatement` - :/ `ListStatement` - :/ `MapStatement` - :/ `StructureStatement` - :/ `UnionStatement` - :/ `ServiceStatement` - :/ `OperationStatement` - :/ `ResourceStatement` - SimpleShapeStatement :`SimpleTypeName` `SP` `Identifier` [`Mixins`] - SimpleTypeName :%s"blob" / %s"boolean" / %s"document" / %s"string" - :/ %s"byte" / %s"short" / %s"integer" / %s"long" - :/ %s"float" / %s"double" / %s"bigInteger" - :/ %s"bigDecimal" / %s"timestamp" - Mixins :*`SP` %s"with" *`WS` "[" 1*(*`WS` `ShapeId`) *`WS` "]" - EnumShapeStatement :`EnumTypeName` `SP` `Identifier` [`Mixins`] *`WS` `EnumShapeMembers` - EnumTypeName :%s"enum" / %s"intEnum" - EnumShapeMembers :"{" *`WS` 1*(`TraitStatements` `Identifier` [`ValueAssignment`] `*WS`) "}" - ValueAssignment :*`SP` "=" *`SP` `NodeValue` `BR` - ShapeMembers :"{" *`WS` *(`TraitStatements` `ShapeMember` *`WS`) "}" - ShapeMember :(`ShapeMemberKvp` / `ShapeMemberElided`) [`ValueAssignment`] - ShapeMemberKvp :`Identifier` *`WS` ":" *`WS` `ShapeId` - ShapeMemberElided :"$" `Identifier` - ListStatement :%s"list" `SP` `Identifier` [`Mixins`] *`WS` `ShapeMembers` - MapStatement :%s"map" `SP` `Identifier` [`Mixins`] *`WS` `ShapeMembers` - StructureStatement :%s"structure" `SP` `Identifier` [`StructureResource`] - : [`Mixins`] *`WS` `ShapeMembers` - StructureResource :`SP` %s"for" `SP` `ShapeId` - UnionStatement :%s"union" `SP` `Identifier` [`Mixins`] *`WS` `ShapeMembers` - ServiceStatement :%s"service" `SP` `Identifier` [`Mixins`] *`WS` `NodeObject` - ResourceStatement :%s"resource" `SP` `Identifier` [`Mixins`] *`WS` `NodeObject` - OperationStatement :%s"operation" `SP` `Identifier` [`Mixins`] *`WS` `OperationBody` - OperationBody :"{" *`WS` - : [`OperationInput`] - : [`OperationOutput`] - : [`OperationErrors`] - : *`WS` "}" - OperationInput :%s"input" *WS (`InlineStructure` / `Identifier`) `BR` - OperationOutput :%s"output" *WS (`InlineStructure` / `ShapeId`) `BR` - OperationErrors :%s"errors" *WS ":" *WS "[" *(*`WS` `Identifier`) *`WS` "]" `BR` - InlineStructure :":=" *`WS` `TraitStatements` [`Mixins`] *`WS` `ShapeMembers` + ShapeSection :[`NamespaceStatement` `UseSection` `ShapeStatements`] + NamespaceStatement :%s"namespace" `SP` `Namespace` `BR` + UseSection :*(`UseStatement`) + UseStatement :%s"use" `SP` `AbsoluteRootShapeId` `BR` + ShapeStatements :*(`ShapeStatement` / `ApplyStatement`) + ShapeStatement :`TraitStatements` `ShapeBody` `BR` + ShapeBody :`SimpleShapeStatement` + :/ `EnumShapeStatement` + :/ `ListStatement` + :/ `MapStatement` + :/ `StructureStatement` + :/ `UnionStatement` + :/ `ServiceStatement` + :/ `OperationStatement` + :/ `ResourceStatement` + SimpleShapeStatement :`SimpleTypeName` `SP` `Identifier` [`Mixins`] + SimpleTypeName :%s"blob" / %s"boolean" / %s"document" / %s"string" + :/ %s"byte" / %s"short" / %s"integer" / %s"long" + :/ %s"float" / %s"double" / %s"bigInteger" + :/ %s"bigDecimal" / %s"timestamp" + Mixins :*`SP` %s"with" *`WS` "[" 1*(*`WS` `ShapeId`) *`WS` "]" + EnumShapeStatement :`EnumTypeName` `SP` `Identifier` [`Mixins`] *`WS` `EnumShapeMembers` + EnumTypeName :%s"enum" / %s"intEnum" + EnumShapeMembers :"{" *`WS` 1*(`TraitStatements` `Identifier` [`ValueAssignment`] `*WS`) "}" + ValueAssignment :*`SP` "=" *`SP` `NodeValue` `BR` + ListStatement :%s"list" `SP` `Identifier` [`Mixins`] *`WS` `ListMembers` + ListMembers :"{" *`WS` `ListMember` *`WS` "}" + ListMember :[TraitStatements] (`ElidedListMember` / `ExplicitListMember`) + ElidedListMember :%s"$member" + ExplicitListMember :%s"member" *`SP` ":" *`SP` `ShapeId` + MapStatement :%s"map" `SP` `Identifier` [`Mixins`] *`WS` `MapMembers` + MapMembers :"{" *`WS` `MapKey` `BR` `MapValue` *`WS` "}" + MapKey :[TraitStatements] (`ElidedMapKey` / `ExplicitMapKey`) + MapValue :[TraitStatements] (`ElidedMapValue` / `ExplicitMapValue`) + ElidedMapKey :%s"$key" + ExplicitMapKey :%s"key" *`SP` ":" *`SP` `ShapeId` + ElidedMapValue :%s"$value" + ExplicitMapValue :%s"value" *`SP` ":" *`SP` `ShapeId` + StructureStatement :%s"structure" `SP` `Identifier` [`StructureResource`] + : [`Mixins`] *`WS` `StructureMembers` + StructureResource :`SP` %s"for" `SP` `ShapeId` + StructureMembers :"{" *`WS` *(`TraitStatements` `StructureMember` *`WS`) "}" + StructureMember :(`ExplicitStructureMember` / `ElidedStructureMember`) [`ValueAssignment`] + ExplicitStructureMember :`Identifier` *`SP` ":" *`SP` `ShapeId` + ElidedStructureMember :"$" `Identifier` + UnionStatement :%s"union" `SP` `Identifier` [`Mixins`] *`WS` `UnionMembers` + UnionMembers :"{" *`WS` *(`TraitStatements` `UnionMember` *`WS`) "}" + UnionMember :(`ExplicitStructureMember` / `ElidedStructureMember`) + ServiceStatement :%s"service" `SP` `Identifier` [`Mixins`] *`WS` `NodeObject` + ResourceStatement :%s"resource" `SP` `Identifier` [`Mixins`] *`WS` `NodeObject` + OperationStatement :%s"operation" `SP` `Identifier` [`Mixins`] *`WS` `OperationBody` + OperationBody :"{" *`WS` + : [`OperationInput`] + : [`OperationOutput`] + : [`OperationErrors`] + : *`WS` "}" + OperationInput :%s"input" *WS (`InlineStructure` / (":" *`WS` `ShapeId`)) `BR` + OperationOutput :%s"output" *WS (`InlineStructure` / (":" *`WS` `ShapeId`)) `BR` + OperationErrors :%s"errors" *WS ":" *WS "[" *(*`WS` `Identifier`) *`WS` "]" `BR` + InlineStructure :":=" *`WS` `TraitStatements` [`Mixins`] *`WS` `StructureMembers` .. rubric:: Traits .. productionlist:: smithy - TraitStatements : *(*`WS` `Trait`) *`WS` + TraitStatements :*(*`WS` `Trait`) *`WS` Trait :"@" `ShapeId` [`TraitBody`] TraitBody :"(" *`WS` [`TraitBodyValue`] *`WS` ")" TraitBodyValue :`TraitStructure` / `NodeValue` @@ -1491,12 +1504,11 @@ Target Elision Having to completely redefine a :ref:`resource identifier ` to use it in a structure or redefine a member from a :ref:`mixin ` to add -additional traits can be cumbersome and potentially error-prone. The -:token:`type elision syntax ` can be used to cut -down on that repetition by prefixing the member name with a ``$``. If a member -is prefixed this way, its target will automatically be set to the target of a -mixin member with the same name. The following example shows how to elide the -target for a member inherited from a mixin: +additional traits can be cumbersome and potentially error-prone. Target elision +syntax can be used to cut down on that repetition by prefixing the member name +with a ``$``. If a member is prefixed this way, its target will automatically be +set to the target of a mixin member with the same name. The following example +shows how to elide the target for a member inherited from a mixin: .. code-block:: smithy diff --git a/docs/source-2.0/spec/model.rst b/docs/source-2.0/spec/model.rst index 0384ab2be22..f825edacdc3 100644 --- a/docs/source-2.0/spec/model.rst +++ b/docs/source-2.0/spec/model.rst @@ -549,7 +549,7 @@ Shape IDs are formally defined by the following ABNF: RootShapeId :`AbsoluteRootShapeId` / `Identifier` AbsoluteRootShapeId :`Namespace` "#" `Identifier` Namespace :`Identifier` *("." `Identifier`) - Identifier :IdentifierStart *IdentifierChars + Identifier :`IdentifierStart` *`IdentifierChars` IdentifierStart :*"_" ALPHA IdentifierChars :ALPHA / DIGIT / "_" ShapeIdMember :"$" `Identifier` diff --git a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/primitive.v1.smithy b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/primitive.v1.smithy index 88fa65e7363..5b903cf9f2c 100644 --- a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/primitive.v1.smithy +++ b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/primitive.v1.smithy @@ -11,8 +11,7 @@ structure PrimitiveBearer { long: PrimitiveLong, short: PrimitiveShort, - handlesComments: // Nobody actually does this right? - PrimitiveShort, + handlesComments: PrimitiveShort, // comment @required handlesRequired: PrimitiveLong, diff --git a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/primitive.v2.smithy b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/primitive.v2.smithy index c0b350c06af..b5815c3d8f3 100644 --- a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/primitive.v2.smithy +++ b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/primitive.v2.smithy @@ -19,8 +19,7 @@ structure PrimitiveBearer { short: PrimitiveShort, @default(0) - handlesComments: // Nobody actually does this right? - PrimitiveShort, + handlesComments: PrimitiveShort, // comment @required handlesRequired: PrimitiveLong, diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlModelParser.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlModelParser.java index 5f1d0015424..4d3d39f914c 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlModelParser.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlModelParser.java @@ -75,7 +75,6 @@ import software.amazon.smithy.model.validation.ValidationEvent; import software.amazon.smithy.model.validation.Validator; import software.amazon.smithy.utils.ListUtils; -import software.amazon.smithy.utils.SetUtils; import software.amazon.smithy.utils.SimpleParser; import software.amazon.smithy.utils.StringUtils; @@ -233,7 +232,7 @@ public void rsp() { @Override public void sp() { - while (!eof() && isSpaceOrComma(peek())) { + while (isSpaceOrComma(peek())) { skip(); } } @@ -246,7 +245,7 @@ private boolean isSpaceOrComma(char c) { public void br() { int line = line(); ws(); - if (line == line() && peek() != Character.MIN_VALUE) { + if (line == line() && !eof()) { throw syntax("Expected a line break"); } } @@ -517,7 +516,7 @@ private void parseShape(List traits) { parseSimpleShape(id, location, StringShape.builder()); break; case "enum": - parseEnumShape(id, location, EnumShape.builder(), MemberParsing.PARSING_ENUM); + parseEnumShape(id, location, EnumShape.builder()); break; case "blob": parseSimpleShape(id, location, BlobShape.builder()); @@ -532,7 +531,7 @@ private void parseShape(List traits) { parseSimpleShape(id, location, IntegerShape.builder()); break; case "intEnum": - parseEnumShape(id, location, IntEnumShape.builder(), MemberParsing.PARSING_INT_ENUM); + parseEnumShape(id, location, IntEnumShape.builder()); break; case "long": parseSimpleShape(id, location, LongShape.builder()); @@ -585,15 +584,41 @@ private void parseSimpleShape(ShapeId id, SourceLocation location, AbstractShape operations.accept(operation); } - private void parseEnumShape( - ShapeId id, - SourceLocation location, - AbstractShapeBuilder builder, - MemberParsing memberParsing - ) { + private void parseEnumShape(ShapeId id, SourceLocation location, AbstractShapeBuilder builder) { LoadOperation.DefineShape operation = createShape(builder.id(id).source(location)); parseMixins(operation); - parseMembers(operation, Collections.emptySet(), memberParsing); + + ws(); + expect('{'); + clearPendingDocs(); + ws(); + + while (!eof() && peek() != '}') { + List memberTraits = parseDocsAndTraits(); + SourceLocation memberLocation = currentLocation(); + String memberName = ParserUtils.parseIdentifier(this); + MemberShape.Builder memberBuilder = MemberShape.builder() + .id(id.withMember(memberName)) + .source(memberLocation) + .target(UnitTypeTrait.UNIT); + operation.addMember(memberBuilder); + addTraits(memberBuilder.getId(), memberTraits); + + // Check for optional value assignment. + sp(); + if (peek() == '=') { + expect('='); + sp(); + Node value = IdlNodeParser.parseNode(this); + memberBuilder.addTrait(new EnumValueTrait.Provider().createTrait(memberBuilder.getId(), value)); + br(); + } + + clearPendingDocs(); + ws(); + } + + expect('}'); clearPendingDocs(); operations.accept(operation); } @@ -603,80 +628,128 @@ private void parseEnumShape( private void parseCollection(ShapeId id, SourceLocation location, CollectionShape.Builder builder) { LoadOperation.DefineShape operation = createShape(builder.id(id).source(location)); parseMixins(operation); - parseMembers(operation, SetUtils.of("member")); + ws(); + expect('{'); + clearPendingDocs(); + ws(); + parsePossiblyElidedMember(operation, "member"); + ws(); + expect('}'); + clearPendingDocs(); operations.accept(operation); } - private void parseMembers(LoadOperation.DefineShape operation, Set requiredMembers) { - parseMembers(operation, requiredMembers, MemberParsing.PARSING_MEMBER); - } + // Parsed list, set, and map members. + private void parsePossiblyElidedMember(LoadOperation.DefineShape operation, String memberName) { + boolean isElided = false; + List memberTraits = parseDocsAndTraits(); - private enum MemberParsing { - PARSING_INT_ENUM { - @Override - boolean supportsAssignment() { - return true; + if (peek() == '$') { + isElided = true; + if (!modelVersion.supportsTargetElision()) { + throw syntax(operation.toShapeId().withMember(memberName), + "Members can only elide targets in IDL version 2 or later"); } - - @Override - Trait createAssignmentTrait(ShapeId id, Node value) { - NumberNode number = value.asNumberNode().orElseThrow(() -> ModelSyntaxException.builder() - .shapeId(id) - .sourceLocation(value) - .message("intEnum shapes require integer values but found: " + Node.printJson(value)) - .build()); - if (number.isFloatingPointNumber()) { - throw ModelSyntaxException.builder() - .shapeId(id) - .message("intEnum shapes do not support floating point values: " + value) - .sourceLocation(value) - .build(); - } - long longValue = number.getValue().longValue(); - if (longValue > Integer.MAX_VALUE || longValue < Integer.MIN_VALUE) { - throw ModelSyntaxException.builder() - .shapeId(id) - .message("intEnum must fit within an integer, but found: " + longValue) - .sourceLocation(value) - .build(); - } - return EnumValueTrait.builder() - .sourceLocation(value.getSourceLocation()) - .intValue(number.getValue().intValue()) - .build(); + expect('$'); + } else if (peek() != memberName.charAt(0)) { + if (!memberTraits.isEmpty()) { + throw syntax("Expected member definition to follow traits"); } + return; + } - @Override - boolean targetsUnit() { - return true; - } - }, - PARSING_ENUM { - @Override - boolean supportsAssignment() { - return true; - } + MemberShape.Builder memberBuilder = MemberShape.builder() + .id(operation.toShapeId().withMember(memberName)) + .source(currentLocation()); - @Override - Trait createAssignmentTrait(ShapeId id, Node value) { - String stringValue = value.asStringNode().orElseThrow(() -> ModelSyntaxException.builder() - .shapeId(id) - .sourceLocation(value) - .message("enum shapes require string values but found: " + Node.printJson(value)) - .build()) - .getValue(); - return EnumValueTrait.builder() - .sourceLocation(value.getSourceLocation()) - .stringValue(stringValue) - .build(); - } + for (int i = 0; i < memberName.length(); i++) { + expect(memberName.charAt(i)); + } - @Override - boolean targetsUnit() { - return true; - } - }, + if (!isElided) { + sp(); + expect(':'); + sp(); + addForwardReference(ParserUtils.parseShapeId(this), memberBuilder::target); + } + + operation.addMember(memberBuilder); + addTraits(memberBuilder.getId(), memberTraits); + clearPendingDocs(); + } + + private void parseMapStatement(ShapeId id, SourceLocation location) { + LoadOperation.DefineShape operation = createShape(MapShape.builder().id(id).source(location)); + parseMixins(operation); + ws(); + expect('{'); + clearPendingDocs(); + ws(); + parsePossiblyElidedMember(operation, "key"); + ws(); + parsePossiblyElidedMember(operation, "value"); + ws(); + expect('}'); + clearPendingDocs(); + operations.accept(operation); + } + + private void parseStructuredShape( + ShapeId id, + SourceLocation location, + AbstractShapeBuilder builder, + MemberParsing memberParsing + ) { + LoadOperation.DefineShape operation = createShape(builder.id(id).source(location)); + + // If it's a structure, parse the optional "from" statement to enable + // eliding member targets for resource identifiers. + if (builder.getShapeType() == ShapeType.STRUCTURE) { + parseForResource(operation); + } + + // Parse optional "with" statements to add mixins, but only if it's supported by the version. + parseMixins(operation); + parseMembers(operation, memberParsing); + clearPendingDocs(); + operations.accept(operation); + } + + private void parseMixins(LoadOperation.DefineShape operation) { + sp(); + if (peek() != 'w') { + return; + } + + expect('w'); + expect('i'); + expect('t'); + expect('h'); + + if (!modelVersion.supportsMixins()) { + throw syntax(operation.toShapeId(), "Mixins can only be used with Smithy version 2 or later. " + + "Attempted to use mixins with version `" + modelVersion + "`."); + } + + ws(); + expect('['); + ws(); + + do { + String target = ParserUtils.parseShapeId(this); + addForwardReference(target, resolved -> { + operation.addDependency(resolved); + operation.addModifier(new ApplyMixin(resolved)); + }); + ws(); + } while (peek() != ']'); + + expect(']'); + clearPendingDocs(); + } + + private enum MemberParsing { PARSING_STRUCTURE_MEMBER { @Override boolean supportsAssignment() { @@ -687,11 +760,6 @@ boolean supportsAssignment() { Trait createAssignmentTrait(ShapeId id, Node value) { return new DefaultTrait(value); } - - @Override - boolean targetsUnit() { - return false; - } }, PARSING_MEMBER { @Override @@ -703,21 +771,14 @@ boolean supportsAssignment() { Trait createAssignmentTrait(ShapeId id, Node value) { throw new UnsupportedOperationException(); } - - @Override - boolean targetsUnit() { - return false; - } }; abstract boolean supportsAssignment(); abstract Trait createAssignmentTrait(ShapeId id, Node value); - - abstract boolean targetsUnit(); } - private void parseMembers(LoadOperation.DefineShape op, Set requiredMembers, MemberParsing memberParsing) { + private void parseMembers(LoadOperation.DefineShape op, MemberParsing memberParsing) { Set definedMembers = new HashSet<>(); ws(); @@ -729,7 +790,7 @@ private void parseMembers(LoadOperation.DefineShape op, Set requiredMemb break; } - parseMember(op, requiredMembers, definedMembers, memberParsing); + parseMember(op, definedMembers, memberParsing); // Clears out any previously captured documentation // comments that may have been found when parsing the member. @@ -738,26 +799,17 @@ private void parseMembers(LoadOperation.DefineShape op, Set requiredMemb ws(); } - if (eof()) { - expect('}'); - } - expect('}'); } - private void parseMember( - LoadOperation.DefineShape operation, - Set allowed, - Set defined, - MemberParsing memberParsing - ) { + private void parseMember(LoadOperation.DefineShape operation, Set defined, MemberParsing memberParsing) { ShapeId parent = operation.toShapeId(); // Parse optional member traits. List memberTraits = parseDocsAndTraits(); SourceLocation memberLocation = currentLocation(); - boolean isTargetElided = !memberParsing.targetsUnit() && peek() == '$'; + boolean isTargetElided = peek() == '$'; if (isTargetElided) { expect('$'); } @@ -771,16 +823,10 @@ private void parseMember( defined.add(memberName); - // Only enforce "allowedMembers" if it isn't empty. - if (!allowed.isEmpty() && !allowed.contains(memberName)) { - throw syntax(parent, "Unexpected member of " + parent + ": '" + memberName + '\''); - } - ShapeId memberId = parent.withMember(memberName); if (isTargetElided && !modelVersion.supportsTargetElision()) { - throw syntax(memberId, "Members can only elide targets in IDL version 2 or later. " - + "Attempted to elide a target with version `" + modelVersion + "`."); + throw syntax(memberId, "Members can only elide targets in IDL version 2 or later"); } MemberShape.Builder memberBuilder = MemberShape.builder().id(memberId).source(memberLocation); @@ -788,14 +834,10 @@ private void parseMember( // Members whose targets are elided will have those targets resolved later, // for example by SetResourceBasedTargets if (!isTargetElided) { - if (memberParsing.targetsUnit()) { - addForwardReference(UnitTypeTrait.UNIT.toString(), memberBuilder::target); - } else { - ws(); - expect(':'); - ws(); - addForwardReference(ParserUtils.parseShapeId(this), memberBuilder::target); - } + sp(); + expect(':'); + sp(); + addForwardReference(ParserUtils.parseShapeId(this), memberBuilder::target); } // Skip spaces to check if there is default trait sugar. @@ -816,68 +858,6 @@ private void parseMember( addTraits(memberBuilder.getId(), memberTraits); } - private void parseMapStatement(ShapeId id, SourceLocation location) { - LoadOperation.DefineShape operation = createShape(MapShape.builder().id(id).source(location)); - parseMixins(operation); - parseMembers(operation, SetUtils.of("key", "value")); - clearPendingDocs(); - operations.accept(operation); - } - - private void parseStructuredShape( - ShapeId id, - SourceLocation location, - AbstractShapeBuilder builder, - MemberParsing memberParsing - ) { - LoadOperation.DefineShape operation = createShape(builder.id(id).source(location)); - - // If it's a structure, parse the optional "from" statement to enable - // eliding member targets for resource identifiers. - if (builder.getShapeType() == ShapeType.STRUCTURE) { - parseForResource(operation); - } - - // Parse optional "with" statements to add mixins, but only if it's supported by the version. - parseMixins(operation); - parseMembers(operation, Collections.emptySet(), memberParsing); - clearPendingDocs(); - operations.accept(operation); - } - - private void parseMixins(LoadOperation.DefineShape operation) { - sp(); - if (peek() != 'w') { - return; - } - - expect('w'); - expect('i'); - expect('t'); - expect('h'); - - if (!modelVersion.supportsMixins()) { - throw syntax(operation.toShapeId(), "Mixins can only be used with Smithy version 2 or later. " - + "Attempted to use mixins with version `" + modelVersion + "`."); - } - - ws(); - expect('['); - ws(); - - do { - String target = ParserUtils.parseShapeId(this); - addForwardReference(target, resolved -> { - operation.addDependency(resolved); - operation.addModifier(new ApplyMixin(resolved)); - }); - ws(); - } while (peek() != ']'); - - expect(']'); - clearPendingDocs(); - } - private void parseOperationStatement(ShapeId id, SourceLocation location) { OperationShape.Builder builder = OperationShape.builder().id(id).source(location); LoadOperation.DefineShape operation = createShape(builder); @@ -926,8 +906,6 @@ private void parseOperationStatement(ShapeId id, SourceLocation location) { parseIdList(builder::addError); br(); expect('}'); - } else if (next != '}') { - expect('}'); } clearPendingDocs(); @@ -966,7 +944,7 @@ private ShapeId parseInlineStructure(String name, TraitEntry defaultTrait) { LoadOperation.DefineShape operation = createShape(builder); parseMixins(operation); parseForResource(operation); - parseMembers(operation, Collections.emptySet(), MemberParsing.PARSING_STRUCTURE_MEMBER); + parseMembers(operation, MemberParsing.PARSING_STRUCTURE_MEMBER); addTraits(id, traits); clearPendingDocs(); operations.accept(operation); diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/LoadOperationProcessor.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/LoadOperationProcessor.java index 7dad827665b..47faff0d86b 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/LoadOperationProcessor.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/LoadOperationProcessor.java @@ -169,7 +169,7 @@ private void detectAndEmitForwardReference(LoadOperation.ForwardReference refere } else { // Try to find a prelude shape by ID if no ID exists in the namespace with this name. ShapeId preludeId = ShapeId.fromOptionalNamespace(Prelude.NAMESPACE, reference.name); - if (prelude.getShapeIds().contains(preludeId)) { + if (prelude != null && prelude.getShapeIds().contains(preludeId)) { reference.resolve(preludeId, test -> prelude.expectShape(test).getType()); } else { reference.resolve(inNamespace, test -> null); diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumValueTrait.java b/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumValueTrait.java index ffd7230f449..5a851781431 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumValueTrait.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumValueTrait.java @@ -16,7 +16,6 @@ package software.amazon.smithy.model.traits; import java.util.Optional; -import software.amazon.smithy.model.SourceException; import software.amazon.smithy.model.node.ExpectationNotMetException; import software.amazon.smithy.model.node.Node; import software.amazon.smithy.model.node.NumberNode; @@ -31,19 +30,14 @@ public final class EnumValueTrait extends AbstractTrait implements ToSmithyBuilder { public static final ShapeId ID = ShapeId.from("smithy.api#enumValue"); - private final String string; - private final Integer integer; + private final Node value; private EnumValueTrait(Builder builder) { super(ID, builder.sourceLocation); - string = builder.string; - integer = builder.integer; - if (string == null && integer == null) { - throw new SourceException( - "Either a string value or an integer value must be set for the enumValue trait.", - getSourceLocation() - ); + if (builder.value == null) { + throw new IllegalStateException("No integer or string value set on EnumValueTrait"); } + value = builder.value; } /** @@ -52,7 +46,7 @@ private EnumValueTrait(Builder builder) { * @return Optionally returns the string value. */ public Optional getStringValue() { - return Optional.ofNullable(string); + return value.asStringNode().map(StringNode::getValue); } /** @@ -73,7 +67,7 @@ public String expectStringValue() { * @return Returns the set int value. */ public Optional getIntValue() { - return Optional.ofNullable(integer); + return value.asNumberNode().map(NumberNode::getValue).map(Number::intValue); } /** @@ -96,41 +90,20 @@ public Provider() { @Override public Trait createTrait(ShapeId target, Node value) { Builder builder = builder().sourceLocation(value); - value.asStringNode().ifPresent(node -> builder.stringValue(node.getValue())); - value.asNumberNode().ifPresent(node -> { - if (node.isNaturalNumber()) { - builder.intValue(node.getValue().intValue()); - } else { - throw new SourceException( - "Enum values may not use fractional numbers.", - value.getSourceLocation() - ); - } - }); - EnumValueTrait result = builder.build(); - result.setNodeCache(value); - return result; + builder.value = value; + return builder.build(); } } @Override protected Node createNode() { - if (getIntValue().isPresent()) { - return new NumberNode(integer, getSourceLocation()); - } else { - return new StringNode(string, getSourceLocation()); - - } + return value; } @Override public SmithyBuilder toBuilder() { Builder builder = builder().sourceLocation(getSourceLocation()); - if (getIntValue().isPresent()) { - builder.intValue(getIntValue().get()); - } else if (getStringValue().isPresent()) { - builder.stringValue(getStringValue().get()); - } + builder.value = value; return builder; } @@ -139,8 +112,7 @@ public static Builder builder() { } public static final class Builder extends AbstractTraitBuilder { - private String string; - private Integer integer; + private Node value; @Override public EnumValueTrait build() { @@ -148,14 +120,12 @@ public EnumValueTrait build() { } public Builder stringValue(String string) { - this.string = string; - this.integer = null; + this.value = Node.from(string); return this; } public Builder intValue(int integer) { - this.integer = integer; - this.string = null; + this.value = Node.from(integer); return this; } } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/EnumShapeValidator.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/EnumShapeValidator.java index 78c1894f517..24835dfc419 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/EnumShapeValidator.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/EnumShapeValidator.java @@ -23,6 +23,8 @@ import java.util.regex.Pattern; import software.amazon.smithy.model.FromSourceLocation; import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.NumberNode; import software.amazon.smithy.model.shapes.EnumShape; import software.amazon.smithy.model.shapes.IntEnumShape; import software.amazon.smithy.model.shapes.MemberShape; @@ -60,16 +62,16 @@ public List validate(Model model) { private void validateEnumShape(List events, EnumShape shape) { Set values = new HashSet<>(); for (MemberShape member : shape.members()) { - Optional value = member.expectTrait(EnumValueTrait.class).getStringValue(); + EnumValueTrait trait = member.expectTrait(EnumValueTrait.class); + Optional value = trait.getStringValue(); if (!value.isPresent()) { events.add(error(member, member.expectTrait(EnumValueTrait.class), - "The enumValue trait must use the string option when applied to enum shapes.")); + "enum members can only be assigned string values, but found: " + + Node.printJson(trait.toNode()))); } else { if (!values.add(value.get())) { - events.add(error(member, String.format( - "Multiple enum members found with duplicate value `%s`", - value.get() - ))); + events.add(error(member, String.format("Multiple enum members found with duplicate value `%s`", + value.get()))); } if (value.get().equals("")) { events.add(error(member, "enum values may not be empty.")); @@ -82,25 +84,49 @@ private void validateEnumShape(List events, EnumShape shape) { private void validateIntEnumShape(List events, IntEnumShape shape) { Set values = new HashSet<>(); for (MemberShape member : shape.members()) { + // intEnum must all have the EnumValueTrait. if (!member.hasTrait(EnumValueTrait.ID)) { events.add(missingIntEnumValue(member, member)); - } else if (!member.expectTrait(EnumValueTrait.class).getIntValue().isPresent()) { - events.add(missingIntEnumValue(member, member.expectTrait(EnumValueTrait.class))); - } else { - int value = member.expectTrait(EnumValueTrait.class).getIntValue().get(); - if (!values.add(value)) { - events.add(error(member, String.format( - "Multiple enum members found with duplicate value `%s`", - value - ))); - } + continue; } + + EnumValueTrait trait = member.expectTrait(EnumValueTrait.class); + + // The EnumValueTrait must point to a number. + if (!trait.getIntValue().isPresent()) { + ValidationEvent event = error(member, trait, "intEnum members require integer values, but found: " + + Node.printJson(trait.toNode())); + events.add(event); + continue; + } + + NumberNode number = trait.toNode().asNumberNode().get(); + + // Validate the it is an integer. + if (number.isFloatingPointNumber()) { + events.add(error(member, trait, "intEnum members do not support floating point values: " + + number.getValue())); + continue; + } + + long longValue = number.getValue().longValue(); + if (longValue > Integer.MAX_VALUE || longValue < Integer.MIN_VALUE) { + events.add(error(member, trait, "intEnum members must fit within an integer, but found: " + + longValue)); + continue; + } + + if (!values.add(number.getValue().intValue())) { + events.add(error(member, String.format("Multiple intEnum members found with duplicate value `%d`", + number.getValue().intValue()))); + } + validateEnumMemberName(events, member); } } private ValidationEvent missingIntEnumValue(Shape shape, FromSourceLocation sourceLocation) { - return error(shape, sourceLocation, "intEnum members must have the enumValue trait with the `int` member set"); + return error(shape, sourceLocation, "intEnum members must be assigned an integer value"); } private void validateEnumMemberName(List events, MemberShape member) { diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/loader/ModelAssemblerTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/loader/ModelAssemblerTest.java index 2550b1da933..116d8ccd769 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/loader/ModelAssemblerTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/loader/ModelAssemblerTest.java @@ -873,4 +873,17 @@ public void nodeModelsDoNotInterfereWithManuallyAddedModels() { assertThat(createdMember.getAllTraits(), not(hasKey(BoxTrait.ID))); } + + @Test + public void canResolveTargetsWithoutPrelude() { + ValidatedResult model = Model.assembler() + .disablePrelude() + .addUnparsedModel("foo.smithy", "$version: \"2.0\"\n" + + "namespace smithy.example\n" + + "list Foo { member: String }\n") + .assemble(); + + assertThat(model.getValidationEvents(), hasSize(1)); + assertThat(model.getValidationEvents().get(0).getMessage(), containsString("unresolved shape")); + } } diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/dupe-list-member-names.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/dupe-list-member-names.errors index f3ea0edee08..fdc1f145ce8 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/dupe-list-member-names.errors +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/dupe-list-member-names.errors @@ -1 +1 @@ -[ERROR] com.foo#List: Parse error at line 7, column 11 near `: `: Duplicate member of com.foo#List: 'member' | Model +[ERROR] -: Parse error at line 7, column 5 near `member`: Expected: '}', but found 'm' | Model diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/dupe-map-member-names.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/dupe-map-member-names.errors index a94df5b78d2..4bc1d3fc896 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/dupe-map-member-names.errors +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/dupe-map-member-names.errors @@ -1 +1 @@ -[ERROR] com.foo#Map: Parse error at line 7, column 8 near `: `: Duplicate member of com.foo#Map: 'key' | Model +[ERROR] -: Parse error at line 7, column 5 near `key`: Expected: '}', but found 'k' | Model diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/dupe-set-member-names.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/dupe-set-member-names.errors index 480d1e5c716..fdc1f145ce8 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/dupe-set-member-names.errors +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/dupe-set-member-names.errors @@ -1 +1 @@ -[ERROR] com.foo#Set: Parse error at line 7, column 11 near `: `: Duplicate member of com.foo#Set: 'member' | Model +[ERROR] -: Parse error at line 7, column 5 near `member`: Expected: '}', but found 'm' | Model diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-value-invalid-type.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-value-invalid-type.errors deleted file mode 100644 index 49dd9239c0c..00000000000 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-value-invalid-type.errors +++ /dev/null @@ -1,5 +0,0 @@ -[ERROR] smithy.example#IntEnum$FLOAT: Error creating trait `enumValue`: Enum values may not use fractional numbers. | Model -[ERROR] smithy.example#IntEnum$ARRAY: Error creating trait `enumValue`: Either a string value or an integer value must be set for the enumValue trait. | Model -[ERROR] smithy.example#IntEnum$MAP: Error creating trait `enumValue`: Either a string value or an integer value must be set for the enumValue trait. | Model -[ERROR] smithy.example#IntEnum$NULL: Error creating trait `enumValue`: Either a string value or an integer value must be set for the enumValue trait. | Model -[ERROR] smithy.example#IntEnum$BOOLEAN: Error creating trait `enumValue`: Either a string value or an integer value must be set for the enumValue trait. | Model diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-shapes.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enums/enum-shapes.errors similarity index 67% rename from smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-shapes.errors rename to smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enums/enum-shapes.errors index 3d0bdb65dc9..92966ffe0c8 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-shapes.errors +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enums/enum-shapes.errors @@ -1,9 +1,9 @@ -[ERROR] ns.foo#StringEnum$INT_VALUE: The enumValue trait must use the string option when applied to enum shapes. | EnumShape +[ERROR] ns.foo#StringEnum$INT_VALUE: enum members can only be assigned string values, but found: 1 | EnumShape [ERROR] ns.foo#StringEnum$DUPLICATE_VALUE: Multiple enum members found with duplicate value `explicit` | EnumShape [WARNING] ns.foo#StringEnum$undesirableName: The name `undesirableName` does not match the recommended enum name format of beginning with an uppercase letter, followed by any number of uppercase letters, numbers, or underscores. | EnumShape -[ERROR] ns.foo#IntEnum$IMPLICIT_VALUE: intEnum members must have the enumValue trait with the `int` member set | EnumShape -[ERROR] ns.foo#IntEnum$STRING_VALUE: intEnum members must have the enumValue trait with the `int` member set | EnumShape -[ERROR] ns.foo#IntEnum$DUPLICATE_VALUE: Multiple enum members found with duplicate value `1` | EnumShape +[ERROR] ns.foo#IntEnum$IMPLICIT_VALUE: intEnum members must be assigned an integer value | EnumShape +[ERROR] ns.foo#IntEnum$STRING_VALUE: intEnum members require integer values, but found: "foo" | EnumShape +[ERROR] ns.foo#IntEnum$DUPLICATE_VALUE: Multiple intEnum members found with duplicate value `1` | EnumShape [WARNING] ns.foo#IntEnum$undesirableName: The name `undesirableName` does not match the recommended enum name format of beginning with an uppercase letter, followed by any number of uppercase letters, numbers, or underscores. | EnumShape [WARNING] ns.foo#EnumWithEnumTrait: This shape applies a trait that is deprecated: smithy.api#enum | DeprecatedTrait [ERROR] ns.foo#EnumWithEnumTrait: Trait `enum` cannot be applied to `ns.foo#EnumWithEnumTrait`. This trait may only be applied to shapes that match the following selector: string :not(enum) | TraitTarget diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-shapes.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enums/enum-shapes.smithy similarity index 100% rename from smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-shapes.smithy rename to smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enums/enum-shapes.smithy diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-trait-validation.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enums/enum-trait-validation.errors similarity index 100% rename from smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-trait-validation.errors rename to smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enums/enum-trait-validation.errors diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-trait-validation.json b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enums/enum-trait-validation.json similarity index 100% rename from smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-trait-validation.json rename to smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enums/enum-trait-validation.json diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enums/enum-value-invalid-type.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enums/enum-value-invalid-type.errors new file mode 100644 index 00000000000..34ea97ff2b6 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enums/enum-value-invalid-type.errors @@ -0,0 +1,5 @@ +[ERROR] smithy.example#IntEnum$FLOAT: intEnum members do not support floating point values: 1.1 | EnumShape +[ERROR] smithy.example#IntEnum$ARRAY: intEnum members require integer values, but found: [1] | EnumShape +[ERROR] smithy.example#IntEnum$MAP: intEnum members require integer values, but found: {"foo":"bar"} | EnumShape +[ERROR] smithy.example#IntEnum$NULL: intEnum members require integer values, but found: null | EnumShape +[ERROR] smithy.example#IntEnum$BOOLEAN: intEnum members require integer values, but found: true | EnumShape diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-value-invalid-type.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enums/enum-value-invalid-type.smithy similarity index 100% rename from smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-value-invalid-type.smithy rename to smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enums/enum-value-invalid-type.smithy diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/defaults/default-in-v1.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/defaults/default-in-v1.smithy new file mode 100644 index 00000000000..ab12782a73d --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/defaults/default-in-v1.smithy @@ -0,0 +1,7 @@ +// Parse error at line 6, column 17 near `= `: @default assignment is only supported in IDL version 2 or later | Model +$version: "1.0" +namespace smithy.example + +structure Foo { + baz: String = "hello" +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/elide-nonexistent-id.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/elision/elide-nonexistent-id.smithy similarity index 100% rename from smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/elide-nonexistent-id.smithy rename to smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/elision/elide-nonexistent-id.smithy diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/elided-property-type-conflicts.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/elision/elided-property-type-conflicts.smithy similarity index 100% rename from smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/elided-property-type-conflicts.smithy rename to smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/elision/elided-property-type-conflicts.smithy diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/elided-type-conflicts.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/elision/elided-type-conflicts.smithy similarity index 100% rename from smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/elided-type-conflicts.smithy rename to smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/elision/elided-type-conflicts.smithy diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/elided-union-member-from-resource.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/elision/elided-union-member-from-resource.smithy similarity index 100% rename from smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/elided-union-member-from-resource.smithy rename to smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/elision/elided-union-member-from-resource.smithy diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/eliding-with-resources-in-v1.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/elision/eliding-with-resources-in-v1.smithy similarity index 100% rename from smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/eliding-with-resources-in-v1.smithy rename to smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/elision/eliding-with-resources-in-v1.smithy diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/eliding-without-resources-v1.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/elision/eliding-without-resources-v1.smithy similarity index 69% rename from smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/eliding-without-resources-v1.smithy rename to smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/elision/eliding-without-resources-v1.smithy index 963705d0b6c..0210060c07e 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/eliding-without-resources-v1.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/elision/eliding-without-resources-v1.smithy @@ -1,4 +1,4 @@ -// Members can only elide targets in IDL version 2 or later. Attempted to elide a target with version `1.0`. +// Members can only elide targets in IDL version 2 or later $version: "1.0" namespace smithy.example diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/elision/list-member-elision-in-v1.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/elision/list-member-elision-in-v1.smithy new file mode 100644 index 00000000000..5a653fe30b6 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/elision/list-member-elision-in-v1.smithy @@ -0,0 +1,7 @@ +// Parse error at line 6, column 5 near `$m`: Members can only elide targets in IDL version 2 or later +$version: "1.0" +namespace smithy.example + +list Foos { + $member +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/elision/structure-member-elision-in-v1.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/elision/structure-member-elision-in-v1.smithy new file mode 100644 index 00000000000..2e995c6fda1 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/elision/structure-member-elision-in-v1.smithy @@ -0,0 +1,7 @@ +// Parse error at line 6, column 9 near `\n}`: Members can only elide targets in IDL version 2 or later +$version: "1.0" +namespace smithy.example + +structure Foo { + $bar +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/enum-values/enum-with-bad-values.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/enum-values/enum-with-bad-values.smithy index 4e7a1dcb7ee..6da3be630df 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/enum-values/enum-with-bad-values.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/enum-values/enum-with-bad-values.smithy @@ -1,4 +1,4 @@ -// smithy.example#Foo$BAR: enum shapes require string values but found: 10 +// smithy.example#Foo$BAR: enum members can only be assigned string values, but found: 10 | EnumShape $version: "2.0" namespace smithy.example diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/enum-values/intEnum-with-bad-value.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/enum-values/intEnum-with-bad-value.smithy index 6e053230f40..47ef004c4ab 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/enum-values/intEnum-with-bad-value.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/enum-values/intEnum-with-bad-value.smithy @@ -1,4 +1,4 @@ -// [ERROR] smithy.example#Foo$BAR: intEnum shapes require integer values but found: "Abc" +// smithy.example#Foo$BAR: intEnum members require integer values, but found: "Abc" $version: "2.0" namespace smithy.example diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/enum-values/intEnum-with-float.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/enum-values/intEnum-with-float.smithy index 0b3822c3ab9..cd226f17911 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/enum-values/intEnum-with-float.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/enum-values/intEnum-with-float.smithy @@ -1,4 +1,4 @@ -// [ERROR] smithy.example#Foo$BAR: intEnum shapes do not support floating point values +// smithy.example#Foo$BAR: intEnum members do not support floating point values $version: "2.0" namespace smithy.example diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/enum-values/intEnum-with-long.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/enum-values/intEnum-with-long.smithy index 4656c4358c5..0061e8195ab 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/enum-values/intEnum-with-long.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/enum-values/intEnum-with-long.smithy @@ -1,4 +1,4 @@ -// smithy.example#Foo$BAR: intEnum must fit within an integer, but found: 2147483648 +// smithy.example#Foo$BAR: intEnum members must fit within an integer, but found: 2147483648 $version: "2.0" namespace smithy.example diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/enum-without-member.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/enum-without-member.smithy index e77cc3aa05d..b1ede69dea1 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/enum-without-member.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/enum-without-member.smithy @@ -1,4 +1,4 @@ -// enum must have at least one entry +// smithy.example#Enum: enum must have at least one entry | Model $version: "2.0" diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/list-invalid-member.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/list-invalid-member.smithy index f0ce7cef0ea..628749b997e 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/list-invalid-member.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/list-invalid-member.smithy @@ -1,4 +1,4 @@ -// Parse error at line 5, column 6 near `: `: Unexpected member of com.foo#MyList: 'foo' | Model +// Parse error at line 5, column 3 near `foo`: Expected: '}', but found 'f' | Model namespace com.foo list MyList { diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/map-invalid-member.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/map-invalid-member.smithy index 2568d15b244..9836767a472 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/map-invalid-member.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/map-invalid-member.smithy @@ -1,4 +1,4 @@ -// Parse error at line 7, column 7 near `: `: Unexpected member of com.foo#MyMap: 'fuzz' +// Parse error at line 7, column 3 near `fuzz`: Expected: '}', but found 'f' | Model namespace com.foo map MyMap { diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/set-invalid-member.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/set-invalid-member.smithy index 2240277a87e..03dc2c061c2 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/set-invalid-member.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/set-invalid-member.smithy @@ -1,4 +1,4 @@ -// Parse error at line 5, column 6 near `: `: Unexpected member of com.foo#MySet: 'foo' +// Parse error at line 5, column 3 near `foo`: Expected: '}', but found 'f' | Model namespace com.foo set MySet { diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/structures.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/structures.smithy index 6e19f29317d..693acfa95f2 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/structures.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/structures.smithy @@ -51,10 +51,7 @@ structure L { structure M { @deprecated @since("2.0") - foo: - E, + foo:E, @deprecated - baz - : - H + baz:H } diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/unions.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/unions.smithy index 72aad05c1e6..9b89e2e7b1b 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/unions.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/unions.smithy @@ -55,10 +55,7 @@ union L { union M { @deprecated @since("2.0") - foo: - E, + foo:E, @deprecated - baz - : - H + baz:H } From 4258444032f316f24d357539ef9e2daa1effec78 Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Thu, 4 Aug 2022 15:36:46 -0700 Subject: [PATCH 06/20] Emit no rel from enum/intEnum to Unit --- .../model/neighbor/NeighborVisitor.java | 7 ++++ .../amazon/smithy/model/shapes/EnumShape.java | 5 +++ .../model/neighbor/NeighborVisitorTest.java | 34 +++++++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/NeighborVisitor.java b/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/NeighborVisitor.java index a8d26158389..5837be98f91 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/NeighborVisitor.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/NeighborVisitor.java @@ -206,6 +206,13 @@ public List operationShape(OperationShape shape) { @Override public List memberShape(MemberShape shape) { + Shape container = model.getShape(shape.getContainer()).orElse(null); + + // Don't emit relationships from enum/intEnum to the Unit shape. + if (container != null && (container.isEnumShape() || container.isIntEnumShape())) { + return Collections.emptyList(); + } + List result = initializeRelationships(shape, 2); result.add(relationship(shape, RelationshipType.MEMBER_CONTAINER, shape.getContainer())); result.add(relationship(shape, RelationshipType.MEMBER_TARGET, shape.getTarget())); diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java index 644691b2997..fdc1c11e2aa 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java @@ -322,6 +322,11 @@ public Builder id(ShapeId shapeId) { return this; } + @Override + public Builder id(String id) { + return (Builder) super.id(id); + } + /** * Sets enum members from an {@link EnumTrait}. * diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/neighbor/NeighborVisitorTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/neighbor/NeighborVisitorTest.java index 2ca8ec9be73..28d189ec748 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/neighbor/NeighborVisitorTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/neighbor/NeighborVisitorTest.java @@ -19,6 +19,8 @@ import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; import java.util.List; import java.util.stream.Collectors; @@ -41,11 +43,13 @@ import software.amazon.smithy.model.shapes.TimestampShape; import software.amazon.smithy.model.shapes.UnionShape; import software.amazon.smithy.model.traits.DeprecatedTrait; +import software.amazon.smithy.model.traits.EnumValueTrait; import software.amazon.smithy.model.traits.ErrorTrait; import software.amazon.smithy.model.traits.IdempotentTrait; import software.amazon.smithy.model.traits.MixinTrait; import software.amazon.smithy.model.traits.ReadonlyTrait; import software.amazon.smithy.model.traits.SensitiveTrait; +import software.amazon.smithy.model.traits.UnitTypeTrait; public class NeighborVisitorTest { @@ -439,6 +443,36 @@ public void operationShape() { Relationship.create(method, RelationshipType.ERROR, error))); } + @Test + public void operationShapeDoesNotEmitUnitRelationships() { + OperationShape method = OperationShape.builder().id("ns.foo#Foo").build(); + Model model = Model.builder().addShapes(method).build(); + NeighborVisitor neighborVisitor = new NeighborVisitor(model); + List relationships = method.accept(neighborVisitor); + + assertThat(relationships, empty()); + } + + @Test + public void unitTypeRelsNotEmittedFromEnums() { + MemberShape member = MemberShape.builder() + .id("smithy.api#Example$foo") + .target(UnitTypeTrait.UNIT) + .addTrait(EnumValueTrait.builder().stringValue("hi").build()) + .build(); + EnumShape enumShape = EnumShape.builder() + .id("smithy.api#Example") + .addMember(member) + .build(); + + Model model = Model.builder().addShapes(enumShape).build(); + NeighborVisitor neighborVisitor = new NeighborVisitor(model); + List relationships = enumShape.accept(neighborVisitor); + + assertThat(relationships, hasSize(1)); + assertThat(relationships.get(0), equalTo(Relationship.create(enumShape, RelationshipType.ENUM_MEMBER, member))); + } + @Test public void memberShape() { StringShape string = StringShape.builder() From 9c5fbc77c20c78acd86220d3428f08078d0745ce Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Thu, 4 Aug 2022 16:26:57 -0700 Subject: [PATCH 07/20] Relax apply statement grammar to allow ws before trait --- docs/source-2.0/spec/idl.rst | 4 ++-- .../smithy/model/loader/IdlModelParser.java | 16 +++++++++++++--- .../validation/testrunner/SmithyTestCase.java | 2 +- .../invalid/apply/apply-missing-space.smithy | 2 +- .../apply/apply-missing-trait-value.smithy | 2 +- .../invalid/apply/apply-multiple-lines.smithy | 7 ------- .../elision/list-member-elision-in-v1.smithy | 2 +- .../structure-member-elision-in-v1.smithy | 2 +- .../metadata/not-newline-after-metadata.smithy | 2 +- .../invalid-newline-after-shape-type.smithy | 2 +- 10 files changed, 22 insertions(+), 19 deletions(-) delete mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/apply/apply-multiple-lines.smithy diff --git a/docs/source-2.0/spec/idl.rst b/docs/source-2.0/spec/idl.rst index d285fb3de0a..f46a0cf50f2 100644 --- a/docs/source-2.0/spec/idl.rst +++ b/docs/source-2.0/spec/idl.rst @@ -237,8 +237,8 @@ string support defined in `RFC 5234 `_. TraitStructure :`TraitStructureKvp` *(*`WS` `TraitStructureKvp`) TraitStructureKvp :`NodeObjectKey` *`WS` ":" *`WS` `NodeValue` ApplyStatement :(`ApplyStatementSingular` / `ApplyStatementBlock`) - ApplyStatementSingular :%s"apply" `SP` `ShapeId` `SP` `Trait` `BR` - ApplyStatementBlock :%s"apply" `SP` `ShapeId` `SP` "{" `TraitStatements` "}" `BR` + ApplyStatementSingular :%s"apply" `WS` `ShapeId` `WS` `Trait` `BR` + ApplyStatementBlock :%s"apply" `SP` `ShapeId` `WS` "{" `TraitStatements` "}" `BR` .. rubric:: Shape ID diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlModelParser.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlModelParser.java index 4d3d39f914c..a81b2a348c0 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlModelParser.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlModelParser.java @@ -222,7 +222,7 @@ public void ws() { } // required space - public void rsp() { + private void rsp() { int cc = column(); sp(); if (column() == cc) { @@ -230,6 +230,16 @@ public void rsp() { } } + // Required whitespace. + private void rws() { + int line = line(); + int column = column(); + ws(); + if (line() == line && column == column()) { + throw syntax("Expected one or more whitespace characters"); + } + } + @Override public void sp() { while (isSpaceOrComma(peek())) { @@ -1122,10 +1132,10 @@ private void parseApplyStatement() { expect('p'); expect('l'); expect('y'); - rsp(); + sp(); String name = ParserUtils.parseShapeId(this); - rsp(); + rws(); // Account for singular or block apply statements. List traitsToApply; diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/testrunner/SmithyTestCase.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/testrunner/SmithyTestCase.java index 9b85457da1a..82196018cc0 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/validation/testrunner/SmithyTestCase.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/testrunner/SmithyTestCase.java @@ -133,7 +133,7 @@ private static boolean compareEvents(ValidationEvent expected, ValidationEvent a normalizedActualMessage += " (" + actual.getSuppressionReason().get() + ")"; } - String comparedMessage = expected.getMessage().replace("\n", "\\n"); + String comparedMessage = expected.getMessage().replace("\n", "\\n").replace("\r", "\\n"); return expected.getSeverity() == actual.getSeverity() && expected.getId().equals(actual.getId()) && expected.getShapeId().equals(actual.getShapeId()) diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/apply/apply-missing-space.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/apply/apply-missing-space.smithy index e08fe049a04..832b2746ea5 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/apply/apply-missing-space.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/apply/apply-missing-space.smithy @@ -1,4 +1,4 @@ -// Parse error at line 5, column 10 near `{ `: Expected one or more spaces | Model +// Parse error at line 5, column 10 near `{ `: Expected one or more whitespace characters | Model $version: "2.0" namespace smithy.example diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/apply/apply-missing-trait-value.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/apply/apply-missing-trait-value.smithy index 02270631439..38e1baa5fea 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/apply/apply-missing-trait-value.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/apply/apply-missing-trait-value.smithy @@ -1,4 +1,4 @@ -// Parse error at line 4, column 17 near `//`: Expected: '@', but found '/' | Model +// Parse error at line 5, column 1 near `string`: Expected: '@', but found 's' namespace com.foo apply SomeShape // comment so spaces aren't eaten diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/apply/apply-multiple-lines.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/apply/apply-multiple-lines.smithy deleted file mode 100644 index 8209b34adfb..00000000000 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/apply/apply-multiple-lines.smithy +++ /dev/null @@ -1,7 +0,0 @@ -// Parse error at line 6, column 10 near `\n@`: Expected one or more spaces | Model -$version: "2.0" - -namespace smithy.example - -apply Foo -@sensitive diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/elision/list-member-elision-in-v1.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/elision/list-member-elision-in-v1.smithy index 5a653fe30b6..e71c9339941 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/elision/list-member-elision-in-v1.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/elision/list-member-elision-in-v1.smithy @@ -1,4 +1,4 @@ -// Parse error at line 6, column 5 near `$m`: Members can only elide targets in IDL version 2 or later +// Members can only elide targets in IDL version 2 or later $version: "1.0" namespace smithy.example diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/elision/structure-member-elision-in-v1.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/elision/structure-member-elision-in-v1.smithy index 2e995c6fda1..d1f29cf2a52 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/elision/structure-member-elision-in-v1.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/elision/structure-member-elision-in-v1.smithy @@ -1,4 +1,4 @@ -// Parse error at line 6, column 9 near `\n}`: Members can only elide targets in IDL version 2 or later +// Members can only elide targets in IDL version 2 or later $version: "1.0" namespace smithy.example diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/metadata/not-newline-after-metadata.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/metadata/not-newline-after-metadata.smithy index 7b2125e08d3..6501ecb95b4 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/metadata/not-newline-after-metadata.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/metadata/not-newline-after-metadata.smithy @@ -1,4 +1,4 @@ -// Parse error at line 3, column 9 near `\nf`: Expected one or more spaces | Model +// Parse error at line 3, column 9 $version: "2.0" metadata foo = "bar" diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/parse-errors/invalid-newline-after-shape-type.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/parse-errors/invalid-newline-after-shape-type.smithy index a6b9d04a8b3..90ea9d09bff 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/parse-errors/invalid-newline-after-shape-type.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/parse-errors/invalid-newline-after-shape-type.smithy @@ -1,4 +1,4 @@ -// Parse error at line 5, column 10 near `\nF`: Expected one or more spaces | Model +// Parse error at line 5, column 10 $version: "2.0" namespace smithy.example From a6407b16267877f4f0a0e0a32c8cc81f5fa03983 Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Thu, 4 Aug 2022 16:56:43 -0700 Subject: [PATCH 08/20] Avoid newline issues with Windows For some reason Windows tests are failing with anything to do with newlines, event though we attempt to normalize line endings. To unblock us, I just removed anything that caused line ending issues for now. --- .../model/loader/invalid/control/control-colon-newline.smithy | 2 +- .../smithy/model/loader/invalid/control/control-newline.smithy | 2 +- .../model/loader/invalid/defaults/default-missing-value.smithy | 2 +- .../invalid/defaults/default-with-newline-before-value.smithy | 2 +- .../invalid/enum-values/enum-with-newline-before-value.smithy | 2 +- .../smithy/model/loader/invalid/use/use-multiple-lines.smithy | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/control/control-colon-newline.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/control/control-colon-newline.smithy index 7b0a7fa1c13..c0b7cf37cfc 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/control/control-colon-newline.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/control/control-colon-newline.smithy @@ -1,3 +1,3 @@ -// Parse error at line 2, column 10 near `\n"`: Expected a valid identifier character, but found '\n' | Model +// Parse error at line 2, column 10 $version: "2.0" diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/control/control-newline.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/control/control-newline.smithy index 96d8ac1808f..027407e0584 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/control/control-newline.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/control/control-newline.smithy @@ -1,3 +1,3 @@ -// Parse error at line 2, column 9 near `\n:`: Expected: ':', but found '\n' | Model +// Parse error at line 2, column 9 $version : "2.0" diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/defaults/default-missing-value.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/defaults/default-missing-value.smithy index 4875761ece0..eacda9b6d3d 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/defaults/default-missing-value.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/defaults/default-missing-value.smithy @@ -1,4 +1,4 @@ -// Parse error at line 7, column 18 near `\n}`: Expected a valid identifier character, but found '\n' | Model +// Parse error at line 7, column 18 $version: "2.0" namespace com.foo diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/defaults/default-with-newline-before-value.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/defaults/default-with-newline-before-value.smithy index 059fc58c175..ee969789727 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/defaults/default-with-newline-before-value.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/defaults/default-with-newline-before-value.smithy @@ -1,4 +1,4 @@ -// Parse error at line 7, column 18 near `\n `: Expected a valid identifier character, but found '\n' | Model +// Parse error at line 7, column 18 $version: "2.0" namespace com.foo diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/enum-values/enum-with-newline-before-value.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/enum-values/enum-with-newline-before-value.smithy index 864b2c19e2f..af2857d75d2 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/enum-values/enum-with-newline-before-value.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/enum-values/enum-with-newline-before-value.smithy @@ -1,4 +1,4 @@ -// Parse error at line 6, column 10 near `\n `: Expected a valid identifier character, but found '\n' | Model +// Parse error at line 6, column 10 $version: "2.0" namespace smithy.example diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/use/use-multiple-lines.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/use/use-multiple-lines.smithy index c29ce156f5b..1376f723f9d 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/use/use-multiple-lines.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/use/use-multiple-lines.smithy @@ -1,4 +1,4 @@ -// Parse error at line 5, column 4 near `\ns`: Expected one or more spaces | Model +// Parse error at line 5, column 4 $version: "2.0" namespace smithy.example From 4706ce319ed3e221bba69c5f11cb5b738216fea3 Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Fri, 5 Aug 2022 11:38:34 -0700 Subject: [PATCH 09/20] Allow any operation property order There are some teams within Amazon that used operation properties in an order other than input, output, errors (weird!). Rather than break them, we'll just allow any order in the grammar and parser and use post-parsing logic to ensure no duplicates. --- docs/source-2.0/spec/idl.rst | 7 +- .../smithy/model/loader/IdlModelParser.java | 70 +++++++++---------- .../operations/input-defined-twice.smithy | 2 +- .../invalid-operation-properties.smithy | 2 +- .../operations/output-before-input.smithy | 8 --- .../valid/operations/errors-before-input.json | 21 ++++++ .../operations/errors-before-input.smithy | 4 +- .../valid/operations/output-before-input.json | 28 ++++++++ .../operations/output-before-input.smithy | 13 ++++ 9 files changed, 102 insertions(+), 53 deletions(-) delete mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/operations/output-before-input.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/errors-before-input.json rename smithy-model/src/test/resources/software/amazon/smithy/model/loader/{invalid => valid}/operations/errors-before-input.smithy (54%) create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/output-before-input.json create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/output-before-input.smithy diff --git a/docs/source-2.0/spec/idl.rst b/docs/source-2.0/spec/idl.rst index f46a0cf50f2..5a040744da3 100644 --- a/docs/source-2.0/spec/idl.rst +++ b/docs/source-2.0/spec/idl.rst @@ -218,10 +218,9 @@ string support defined in `RFC 5234 `_. ResourceStatement :%s"resource" `SP` `Identifier` [`Mixins`] *`WS` `NodeObject` OperationStatement :%s"operation" `SP` `Identifier` [`Mixins`] *`WS` `OperationBody` OperationBody :"{" *`WS` - : [`OperationInput`] - : [`OperationOutput`] - : [`OperationErrors`] - : *`WS` "}" + : *([`OperationInput`] / [`OperationOutput`] / [`OperationErrors`]) + : *`WS` "}" + : ; only one of each property can be specified. OperationInput :%s"input" *WS (`InlineStructure` / (":" *`WS` `ShapeId`)) `BR` OperationOutput :%s"output" *WS (`InlineStructure` / (":" *`WS` `ShapeId`)) `BR` OperationErrors :%s"errors" *WS ":" *WS "[" *(*`WS` `Identifier`) *`WS` "]" `BR` diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlModelParser.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlModelParser.java index a81b2a348c0..c25ce2e0897 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlModelParser.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlModelParser.java @@ -876,50 +876,44 @@ private void parseOperationStatement(ShapeId id, SourceLocation location) { expect('{'); ws(); - char next = expect('i', 'o', 'e', '}'); - - if (next == 'i') { - expect('n'); - expect('p'); - expect('u'); - expect('t'); - ws(); - expect(':'); - TraitEntry inputTrait = new TraitEntry(InputTrait.ID.toString(), Node.objectNode(), true); - parseInlineableOperationMember(id, operationInputSuffix, builder::input, inputTrait); + parseProperties(id, propertyName -> { + switch (propertyName) { + case "input": + TraitEntry inputTrait = new TraitEntry(InputTrait.ID.toString(), Node.objectNode(), true); + parseInlineableOperationMember(id, operationInputSuffix, builder::input, inputTrait); + break; + case "output": + TraitEntry outputTrait = new TraitEntry(OutputTrait.ID.toString(), Node.objectNode(), true); + parseInlineableOperationMember(id, operationOutputSuffix, builder::output, outputTrait); + break; + case "errors": + parseIdList(builder::addError); + break; + default: + throw syntax(id, String.format("Unknown property %s for %s", propertyName, id)); + } br(); - next = expect('o', 'e', '}'); - } + }); - if (next == 'o') { - expect('u'); - expect('t'); - expect('p'); - expect('u'); - expect('t'); - ws(); - expect(':'); - TraitEntry outputTrait = new TraitEntry(OutputTrait.ID.toString(), Node.objectNode(), true); - parseInlineableOperationMember(id, operationOutputSuffix, builder::output, outputTrait); - br(); - next = expect('e', '}'); - } + expect('}'); + clearPendingDocs(); + operations.accept(operation); + } + + private void parseProperties(ShapeId id, Consumer valueParser) { + Set defined = new HashSet<>(); + while (!eof() && peek() != '}') { + String key = ParserUtils.parseIdentifier(this); + if (defined.contains(key)) { + throw syntax(id, String.format("Duplicate operation %s property for %s", key, id)); + } + defined.add(key); - if (next == 'e') { - expect('r'); - expect('r'); - expect('o'); - expect('r'); - expect('s'); ws(); expect(':'); - parseIdList(builder::addError); - br(); - expect('}'); + valueParser.accept(key); + ws(); } - - clearPendingDocs(); - operations.accept(operation); } private void parseInlineableOperationMember( diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/operations/input-defined-twice.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/operations/input-defined-twice.smithy index 847c243f875..8f8da89df03 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/operations/input-defined-twice.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/operations/input-defined-twice.smithy @@ -1,4 +1,4 @@ -// Parse error at line 8, column 5 near `input`: Found 'i', but expected one of the following tokens: 'o' 'e' '}' | Model +// Parse error at line 8, column 10 near `: `: Duplicate operation input property for com.foo#GetFoo $version: "2.0" namespace com.foo diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/operations/invalid-operation-properties.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/operations/invalid-operation-properties.smithy index e695311499d..c283c5b6097 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/operations/invalid-operation-properties.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/operations/invalid-operation-properties.smithy @@ -1,4 +1,4 @@ -// Parse error at line 6, column 7 +// Unknown property invalidOperationKey for com.example#Operation $version: "2.0" namespace com.example diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/operations/output-before-input.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/operations/output-before-input.smithy deleted file mode 100644 index 9ca176be0fd..00000000000 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/operations/output-before-input.smithy +++ /dev/null @@ -1,8 +0,0 @@ -// Parse error at line 7, column 5 near `input`: Found 'i', but expected one of the following tokens: 'e' '}' | Model -$version: "2.0" -namespace smithy.example - -operation GetFoo { - output: GetFooOutput - input: GetFooInput -} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/errors-before-input.json b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/errors-before-input.json new file mode 100644 index 00000000000..bcec9e1e17d --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/errors-before-input.json @@ -0,0 +1,21 @@ +{ + "smithy": "2.0", + "shapes": { + "smithy.example#GetFoo": { + "type": "operation", + "input": { + "target": "smithy.example#GetFooInput" + }, + "output": { + "target": "smithy.api#Unit" + } + }, + "smithy.example#GetFooInput": { + "type": "structure", + "members": {}, + "traits": { + "smithy.api#input": {} + } + } + } +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/operations/errors-before-input.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/errors-before-input.smithy similarity index 54% rename from smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/operations/errors-before-input.smithy rename to smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/errors-before-input.smithy index c0c70dc3b4f..ca80cfcb094 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/operations/errors-before-input.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/errors-before-input.smithy @@ -1,4 +1,3 @@ -// Parse error at line 7, column 5 near `input`: Expected: '}', but found 'i' | Model $version: "2.0" namespace smithy.example @@ -6,3 +5,6 @@ operation GetFoo { errors: [] input: GetFooInput } + +@input +structure GetFooInput {} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/output-before-input.json b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/output-before-input.json new file mode 100644 index 00000000000..e75d723fc2b --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/output-before-input.json @@ -0,0 +1,28 @@ +{ + "smithy": "2.0", + "shapes": { + "smithy.example#GetFoo": { + "type": "operation", + "input": { + "target": "smithy.example#GetFooInput" + }, + "output": { + "target": "smithy.example#GetFooOutput" + } + }, + "smithy.example#GetFooInput": { + "type": "structure", + "members": {}, + "traits": { + "smithy.api#input": {} + } + }, + "smithy.example#GetFooOutput": { + "type": "structure", + "members": {}, + "traits": { + "smithy.api#output": {} + } + } + } +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/output-before-input.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/output-before-input.smithy new file mode 100644 index 00000000000..84fb8ed8bfe --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/output-before-input.smithy @@ -0,0 +1,13 @@ +$version: "2.0" +namespace smithy.example + +operation GetFoo { + output: GetFooOutput + input: GetFooInput +} + +@input +structure GetFooInput {} + +@output +structure GetFooOutput {} From 51ca2da715eaa34ef247329c3f2ec38db97812ee Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Fri, 5 Aug 2022 12:28:56 -0700 Subject: [PATCH 10/20] Emit rels for MEMBER_CONTAINER for enum members This doesn't impact much, but would allow the reverse crawling of neighbors if given just this relationship. --- .../smithy/model/neighbor/NeighborVisitor.java | 13 +++++++------ .../smithy/model/neighbor/NeighborVisitorTest.java | 13 ++++++++++--- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/NeighborVisitor.java b/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/NeighborVisitor.java index 5837be98f91..afad3188c40 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/NeighborVisitor.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/NeighborVisitor.java @@ -208,14 +208,15 @@ public List operationShape(OperationShape shape) { public List memberShape(MemberShape shape) { Shape container = model.getShape(shape.getContainer()).orElse(null); - // Don't emit relationships from enum/intEnum to the Unit shape. - if (container != null && (container.isEnumShape() || container.isIntEnumShape())) { - return Collections.emptyList(); + // Emit a relationship from a member back to the enum, but not from an enum member to Unit. + boolean isEnumShape = container instanceof EnumShape || container instanceof IntEnumShape; + List result = initializeRelationships(shape, 1 + (isEnumShape ? 0 : 1)); + result.add(relationship(shape, RelationshipType.MEMBER_CONTAINER, shape.getContainer())); + + if (!isEnumShape) { + result.add(relationship(shape, RelationshipType.MEMBER_TARGET, shape.getTarget())); } - List result = initializeRelationships(shape, 2); - result.add(relationship(shape, RelationshipType.MEMBER_CONTAINER, shape.getContainer())); - result.add(relationship(shape, RelationshipType.MEMBER_TARGET, shape.getTarget())); return result; } diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/neighbor/NeighborVisitorTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/neighbor/NeighborVisitorTest.java index 28d189ec748..e125770ed46 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/neighbor/NeighborVisitorTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/neighbor/NeighborVisitorTest.java @@ -467,10 +467,17 @@ public void unitTypeRelsNotEmittedFromEnums() { Model model = Model.builder().addShapes(enumShape).build(); NeighborVisitor neighborVisitor = new NeighborVisitor(model); - List relationships = enumShape.accept(neighborVisitor); - assertThat(relationships, hasSize(1)); - assertThat(relationships.get(0), equalTo(Relationship.create(enumShape, RelationshipType.ENUM_MEMBER, member))); + List enumRelationships = enumShape.accept(neighborVisitor); + List enumMemberRelationships = member.accept(neighborVisitor); + + assertThat(enumRelationships, hasSize(1)); + assertThat(enumRelationships.get(0), + equalTo(Relationship.create(enumShape, RelationshipType.ENUM_MEMBER, member))); + + assertThat(enumMemberRelationships, hasSize(1)); + assertThat(enumMemberRelationships.get(0), + equalTo(Relationship.create(member, RelationshipType.MEMBER_CONTAINER, enumShape))); } @Test From 2c5e3fd92c8424c0686e15b57b6414ff1d1c73ad Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Fri, 5 Aug 2022 12:32:27 -0700 Subject: [PATCH 11/20] Fix enum member trait parsing The clearPendingDocs call was made after consuming traits that follow a previous enum member. Fixed by calling it in the right order. --- .../smithy/model/loader/IdlModelParser.java | 6 ++--- .../model/loader/valid/enums/enum-docs.json | 24 +++++++++++++++++++ .../model/loader/valid/enums/enum-docs.smithy | 10 ++++++++ 3 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums/enum-docs.json create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums/enum-docs.smithy diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlModelParser.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlModelParser.java index c25ce2e0897..670f6855968 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlModelParser.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlModelParser.java @@ -621,11 +621,11 @@ private void parseEnumShape(ShapeId id, SourceLocation location, AbstractShapeBu sp(); Node value = IdlNodeParser.parseNode(this); memberBuilder.addTrait(new EnumValueTrait.Provider().createTrait(memberBuilder.getId(), value)); + clearPendingDocs(); br(); + } else { + ws(); } - - clearPendingDocs(); - ws(); } expect('}'); diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums/enum-docs.json b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums/enum-docs.json new file mode 100644 index 00000000000..6aa6195df64 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums/enum-docs.json @@ -0,0 +1,24 @@ +{ + "smithy": "2.0", + "shapes": { + "smithy.example#Foo": { + "type": "enum", + "members": { + "BAR": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#documentation": "Hello1", + "smithy.api#enumValue": "hi" + } + }, + "BAZ": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#documentation": "Hello2", + "smithy.api#enumValue": "there" + } + } + } + } + } +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums/enum-docs.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums/enum-docs.smithy new file mode 100644 index 00000000000..6b64079681f --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums/enum-docs.smithy @@ -0,0 +1,10 @@ +$version: "2.0" +namespace smithy.example + +enum Foo { + /// Hello1 + BAR = "hi" + + /// Hello2 + BAZ = "there" +} From f31234d9fdf94a8eb5b670389cb3a54ced3cd1bf Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Fri, 5 Aug 2022 13:02:08 -0700 Subject: [PATCH 12/20] Use required WS after operation properties To avoid breaking existing models, require whitespace after operation properties rather than a line break. --- docs/source-2.0/spec/idl.rst | 6 ++-- .../smithy/model/loader/IdlModelParser.java | 2 +- .../operations/missing-ws-after-input.smithy | 7 +++++ .../newline-missing-after-input.smithy | 7 ----- .../newline-missing-after-output.smithy | 7 ----- ...operation-no-newline-after-properties.json | 28 +++++++++++++++++++ ...eration-no-newline-after-properties.smithy | 10 +++++++ 7 files changed, 49 insertions(+), 18 deletions(-) create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/operations/missing-ws-after-input.smithy delete mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/operations/newline-missing-after-input.smithy delete mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/operations/newline-missing-after-output.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-no-newline-after-properties.json create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-no-newline-after-properties.smithy diff --git a/docs/source-2.0/spec/idl.rst b/docs/source-2.0/spec/idl.rst index 5a040744da3..ebd3a23c6df 100644 --- a/docs/source-2.0/spec/idl.rst +++ b/docs/source-2.0/spec/idl.rst @@ -221,9 +221,9 @@ string support defined in `RFC 5234 `_. : *([`OperationInput`] / [`OperationOutput`] / [`OperationErrors`]) : *`WS` "}" : ; only one of each property can be specified. - OperationInput :%s"input" *WS (`InlineStructure` / (":" *`WS` `ShapeId`)) `BR` - OperationOutput :%s"output" *WS (`InlineStructure` / (":" *`WS` `ShapeId`)) `BR` - OperationErrors :%s"errors" *WS ":" *WS "[" *(*`WS` `Identifier`) *`WS` "]" `BR` + OperationInput :%s"input" *WS (`InlineStructure` / (":" *`WS` `ShapeId`)) `WS` + OperationOutput :%s"output" *WS (`InlineStructure` / (":" *`WS` `ShapeId`)) `WS` + OperationErrors :%s"errors" *WS ":" *WS "[" *(*`WS` `Identifier`) *`WS` "]" `WS` InlineStructure :":=" *`WS` `TraitStatements` [`Mixins`] *`WS` `StructureMembers` .. rubric:: Traits diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlModelParser.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlModelParser.java index 670f6855968..c8c60faf7f6 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlModelParser.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlModelParser.java @@ -892,7 +892,7 @@ private void parseOperationStatement(ShapeId id, SourceLocation location) { default: throw syntax(id, String.format("Unknown property %s for %s", propertyName, id)); } - br(); + rws(); }); expect('}'); diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/operations/missing-ws-after-input.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/operations/missing-ws-after-input.smithy new file mode 100644 index 00000000000..4de38001f49 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/operations/missing-ws-after-input.smithy @@ -0,0 +1,7 @@ +// Parse error at line 6, column 29 near `: `: Expected one or more whitespace characters +$version: "2.0" +namespace smithy.example + +operation GetFoo { + input: GetFooInputoutput: GetFooOutput +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/operations/newline-missing-after-input.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/operations/newline-missing-after-input.smithy deleted file mode 100644 index 6600dbc1bc0..00000000000 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/operations/newline-missing-after-input.smithy +++ /dev/null @@ -1,7 +0,0 @@ -// Parse error at line 6, column 24 near `output`: Expected a line break -$version: "2.0" -namespace smithy.example - -operation GetFoo { - input: GetFooInput output: GetFooOutput -} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/operations/newline-missing-after-output.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/operations/newline-missing-after-output.smithy deleted file mode 100644 index 66879552214..00000000000 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/operations/newline-missing-after-output.smithy +++ /dev/null @@ -1,7 +0,0 @@ -// Parse error at line 6, column 26 near `errors`: Expected a line break | Model -$version: "2.0" -namespace smithy.example - -operation GetFoo { - output: GetFooOutput errors: [] -} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-no-newline-after-properties.json b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-no-newline-after-properties.json new file mode 100644 index 00000000000..e75d723fc2b --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-no-newline-after-properties.json @@ -0,0 +1,28 @@ +{ + "smithy": "2.0", + "shapes": { + "smithy.example#GetFoo": { + "type": "operation", + "input": { + "target": "smithy.example#GetFooInput" + }, + "output": { + "target": "smithy.example#GetFooOutput" + } + }, + "smithy.example#GetFooInput": { + "type": "structure", + "members": {}, + "traits": { + "smithy.api#input": {} + } + }, + "smithy.example#GetFooOutput": { + "type": "structure", + "members": {}, + "traits": { + "smithy.api#output": {} + } + } + } +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-no-newline-after-properties.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-no-newline-after-properties.smithy new file mode 100644 index 00000000000..7b8cf76b3a1 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/operations/operation-no-newline-after-properties.smithy @@ -0,0 +1,10 @@ +$version: "2.0" +namespace smithy.example + +operation GetFoo { input: GetFooInput, output: GetFooOutput } + +@input +structure GetFooInput {} + +@output +structure GetFooOutput {} From a02c99fada93ac97c58f98db2f9e6ae83f76ff70 Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Fri, 5 Aug 2022 15:10:03 -0700 Subject: [PATCH 13/20] Add box trait to prelude shapes The box trait is no longer supported in IDL 2.0, so it was removed from the Smithy 2.0 prelude model. However, tooling that is built for Smithy 1.0 might look at shapes targeted by members for the box trait, and their logic would change because the box trait was removed from common shapes like Integer, Boolean, etc. To maintain backward compatibility, this commit adds a box trait to these prelude shapes using the prelude-1.0.smithy model that was previously only used to define the `@box` trait for 1.0 models. It now uses the 1.0 model version so that it can apply the box trait to prelude shapes. This change removes the need for some custom logic in ModelUpgrader too. --- .../smithy/model/loader/ModelUpgrader.java | 19 +--------------- .../validators/DeprecatedTraitValidator.java | 8 +++++-- .../smithy/model/loader/prelude-1.0.smithy | 22 +++++++++++++++++-- .../amazon/smithy/model/loader/prelude.smithy | 1 - .../model/loader/ModelAssemblerTest.java | 16 +++++++++++--- 5 files changed, 40 insertions(+), 26 deletions(-) diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelUpgrader.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelUpgrader.java index 50880762ffb..1eb3980eb72 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelUpgrader.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelUpgrader.java @@ -20,7 +20,6 @@ import java.util.EnumSet; import java.util.List; import java.util.Optional; -import java.util.Set; import java.util.function.Function; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.node.BooleanNode; @@ -28,7 +27,6 @@ import software.amazon.smithy.model.node.StringNode; import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.Shape; -import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.model.shapes.ShapeType; import software.amazon.smithy.model.traits.BoxTrait; import software.amazon.smithy.model.traits.DefaultTrait; @@ -40,7 +38,6 @@ import software.amazon.smithy.model.validation.ValidatedResult; import software.amazon.smithy.model.validation.ValidationEvent; import software.amazon.smithy.model.validation.Validator; -import software.amazon.smithy.utils.SetUtils; /** * Upgrades Smithy models from IDL v1 to IDL v2, specifically taking into @@ -59,16 +56,6 @@ final class ModelUpgrader { ShapeType.DOUBLE, ShapeType.BOOLEAN); - /** Shapes that were boxed in 1.0, but @box was removed in 2.0. */ - private static final Set PREVIOUSLY_BOXED = SetUtils.of( - ShapeId.from("smithy.api#Boolean"), - ShapeId.from("smithy.api#Byte"), - ShapeId.from("smithy.api#Short"), - ShapeId.from("smithy.api#Integer"), - ShapeId.from("smithy.api#Long"), - ShapeId.from("smithy.api#Float"), - ShapeId.from("smithy.api#Double")); - private final Model model; private final List events; private final Function fileToVersion; @@ -153,7 +140,7 @@ private boolean isMemberImplicitlyBoxed( && containerType == ShapeType.STRUCTURE && !member.hasTrait(DefaultTrait.class) // don't add box if it has a default trait. && !member.hasTrait(BoxTrait.class) // don't add box again - && (target.hasTrait(BoxTrait.class) || PREVIOUSLY_BOXED.contains(target.getId())); + && target.hasTrait(BoxTrait.class); } private boolean isZeroValidDefault(MemberShape member) { @@ -196,10 +183,6 @@ private boolean shouldV1MemberHaveDefaultTrait(ShapeType containerType, MemberSh // the member has the http payload trait and targets a streaming blob, which // implies a default in 2.0 && (HAD_DEFAULT_VALUE_IN_1_0.contains(target.getType()) || isDefaultPayload(target)) - // Not when the targeted shape was one of the prelude types with the @box trait. - // This needs special handling here because the @box trait was removed from these - // prelude shapes in v2. - && !PREVIOUSLY_BOXED.contains(target.getId()) // Don't re-add the @default trait && !member.hasTrait(DefaultTrait.class) // Don't add a @default trait if it will conflict with the @required trait. diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/DeprecatedTraitValidator.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/DeprecatedTraitValidator.java index bfb7e0dc416..0936a244698 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/DeprecatedTraitValidator.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/DeprecatedTraitValidator.java @@ -21,6 +21,7 @@ import java.util.List; import java.util.Set; import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.loader.Prelude; import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.model.traits.DeprecatedTrait; import software.amazon.smithy.model.traits.TraitDefinition; @@ -45,8 +46,11 @@ public List validate(Model model) { traitMessage = traitMessage + ", " + deprecatedTrait.getMessage().get(); } for (Shape shape : shapesWithTrait) { - events.add(warning(shape, trait, format( - "This shape applies a trait that is deprecated: %s", traitMessage))); + // Ignore the use of @box on prelude shapes. + if (!Prelude.isPreludeShape(shape)) { + events.add(warning(shape, trait, format( + "This shape applies a trait that is deprecated: %s", traitMessage))); + } } } } diff --git a/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude-1.0.smithy b/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude-1.0.smithy index d71c483a422..9815c8c26dc 100644 --- a/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude-1.0.smithy +++ b/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude-1.0.smithy @@ -1,8 +1,8 @@ -$version: "2.0" - +$version: "1.0" namespace smithy.api /// Used in Smithy 1.0 to indicate that a shape is boxed. +/// This trait has no effect in Smithy IDL 2.0. /// /// When a boxed shape is the target of a member, the member /// may or may not contain a value, and the member has no default value. @@ -13,3 +13,21 @@ namespace smithy.api breakingChanges: [{change: "presence"}] ) structure box {} + +// The box trait was removed in IDL 2.0, so it can't appear on IDL 2.0 prelude shapes +// Apply the box trait in the 1.0 prelude so that previous code written to check for +// the box trait on these shapes continues to function. + +apply Boolean @box + +apply Byte @box + +apply Short @box + +apply Integer @box + +apply Long @box + +apply Float @box + +apply Double @box 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 63f9c8050eb..ecc9095e285 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 @@ -1,5 +1,4 @@ $version: "2.0" - namespace smithy.api string String diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/loader/ModelAssemblerTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/loader/ModelAssemblerTest.java index 116d8ccd769..7e40de19dbb 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/loader/ModelAssemblerTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/loader/ModelAssemblerTest.java @@ -55,8 +55,6 @@ import software.amazon.smithy.model.node.ObjectNode; import software.amazon.smithy.model.node.StringNode; import software.amazon.smithy.model.shapes.MemberShape; -import software.amazon.smithy.model.shapes.ModelSerializer; -import software.amazon.smithy.model.shapes.ModelSerializerTest; import software.amazon.smithy.model.shapes.SetShape; import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.model.shapes.ShapeId; @@ -64,7 +62,6 @@ import software.amazon.smithy.model.shapes.StringShape; import software.amazon.smithy.model.shapes.StructureShape; import software.amazon.smithy.model.traits.BoxTrait; -import software.amazon.smithy.model.traits.DefaultTrait; import software.amazon.smithy.model.traits.DeprecatedTrait; import software.amazon.smithy.model.traits.DocumentationTrait; import software.amazon.smithy.model.traits.DynamicTrait; @@ -886,4 +883,17 @@ public void canResolveTargetsWithoutPrelude() { assertThat(model.getValidationEvents(), hasSize(1)); assertThat(model.getValidationEvents().get(0).getMessage(), containsString("unresolved shape")); } + + @Test + public void findsBoxTraitOnPreludeShapes() { + Model model = Model.assembler().assemble().unwrap(); + + assertThat(model.expectShape(ShapeId.from("smithy.api#Boolean")).hasTrait(BoxTrait.class), is(true)); + assertThat(model.expectShape(ShapeId.from("smithy.api#Byte")).hasTrait(BoxTrait.class), is(true)); + assertThat(model.expectShape(ShapeId.from("smithy.api#Short")).hasTrait(BoxTrait.class), is(true)); + assertThat(model.expectShape(ShapeId.from("smithy.api#Integer")).hasTrait(BoxTrait.class), is(true)); + assertThat(model.expectShape(ShapeId.from("smithy.api#Long")).hasTrait(BoxTrait.class), is(true)); + assertThat(model.expectShape(ShapeId.from("smithy.api#Float")).hasTrait(BoxTrait.class), is(true)); + assertThat(model.expectShape(ShapeId.from("smithy.api#Double")).hasTrait(BoxTrait.class), is(true)); + } } From 8c88e824a28133d3e974d72ecb012284c8d4ff8d Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Fri, 5 Aug 2022 16:02:23 -0700 Subject: [PATCH 14/20] Update NullableIndex to not break v1 semantics Let's keep NullableIndex as-is rather than change it's default behavior to look for v2 semantics. This ensures that no existing code will change behavior unless they want it to. The isNullable method is now marked as deprecated and continues to use v1 semantics, while the other newly introduced methods use v2 semantics. Added various test cases to ensure v1 and v2 compat. --- .../smithy/model/knowledge/NullableIndex.java | 122 ++++++++---------- .../validation/NodeValidationVisitor.java | 7 +- .../model/knowledge/NullableIndexTest.java | 87 ++++++++++++- 3 files changed, 137 insertions(+), 79 deletions(-) diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/knowledge/NullableIndex.java b/smithy-model/src/main/java/software/amazon/smithy/model/knowledge/NullableIndex.java index c275f80aedb..f9605b2747e 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/knowledge/NullableIndex.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/knowledge/NullableIndex.java @@ -17,12 +17,9 @@ import java.lang.ref.WeakReference; import java.util.Objects; -import java.util.Set; import software.amazon.smithy.model.Model; -import software.amazon.smithy.model.loader.ModelAssembler; import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.Shape; -import software.amazon.smithy.model.shapes.ShapeType; import software.amazon.smithy.model.shapes.ToShapeId; import software.amazon.smithy.model.traits.BoxTrait; import software.amazon.smithy.model.traits.ClientOptionalTrait; @@ -30,32 +27,12 @@ import software.amazon.smithy.model.traits.InputTrait; import software.amazon.smithy.model.traits.RequiredTrait; import software.amazon.smithy.model.traits.SparseTrait; -import software.amazon.smithy.utils.SetUtils; /** * An index that checks if a member is nullable. - * - *

Note: this index assumes Smithy 2.0 nullability semantics. - * 1.0 models SHOULD be loaded through a {@link ModelAssembler} - * to upgrade them in memory to IDL 2.0. Use - * {@link #isMemberNullableInV1(MemberShape)} to check if a shape - * is nullable according to Smithy 1.0 semantics. */ public class NullableIndex implements KnowledgeIndex { - private static final Set V1_INHERENTLY_BOXED = SetUtils.of( - ShapeType.STRING, - ShapeType.BLOB, - ShapeType.TIMESTAMP, - ShapeType.BIG_DECIMAL, - ShapeType.BIG_INTEGER, - ShapeType.LIST, - ShapeType.SET, - ShapeType.MAP, - ShapeType.STRUCTURE, - ShapeType.UNION, - ShapeType.DOCUMENT); - private final WeakReference model; public NullableIndex(Model model) { @@ -92,31 +69,6 @@ public enum CheckMode { SERVER } - /** - * Checks if the given shape is optional using {@link CheckMode#CLIENT}. - * - * @param shape Shape or shape ID to check. - * @return Returns true if the shape is nullable. - */ - public final boolean isNullable(ToShapeId shape) { - return isNullable(shape, CheckMode.CLIENT); - } - - /** - * Checks if the given shape is nullable. - * - * @param shape Shape or shape ID to check. - * @param checkMode The mode used when checking if the shape is considered nullable. - * @return Returns true if the shape is nullable. - */ - public final boolean isNullable(ToShapeId shape, CheckMode checkMode) { - Model m = Objects.requireNonNull(model.get()); - Shape s = m.expectShape(shape.toShapeId()); - MemberShape member = s.asMemberShape().orElse(null); - // Non-members should always be considered nullable. - return member == null || isMemberNullable(member, checkMode); - } - /** * Checks if a member is nullable using {@link CheckMode#CLIENT}. * @@ -175,41 +127,69 @@ public boolean isMemberNullable(MemberShape member, CheckMode checkMode) { } /** - * Checks if a member is nullable using v1 nullability rules. + * Checks if the given shape is optional using Smithy IDL 1.0 semantics. * - *

This method matches the previous behavior seen in NullableIndex prior - * to Smithy 1.0. Most models are sent through a ModelAssembler which makes - * using the normal {@link #isMemberNullable(MemberShape)} the best choice. - * However, in some cases, a model might get created directly in code - * using Smithy 1.0 semantics. In those cases, this method can be used to - * detect if the member is nullable or not. + *

This means that the default trait is ignored, the required trait + * is ignored, and only the box trait and sparse traits are used. * - *

This method ignores the default trait, clientOptional trait, - * input trait, and required trait. + *

Use {@link #isMemberNullable(MemberShape)} to check using Smithy + * IDL 2.0 semantics that take required, default, and other traits + * into account. * - * @param member Member to check. - * @return Returns true if the member is nullable using 1.0 resolution rules. + * @param shapeId Shape or shape ID to check. + * @return Returns true if the shape is nullable. */ - public boolean isMemberNullableInV1(MemberShape member) { + @Deprecated + public final boolean isNullable(ToShapeId shapeId) { Model m = Objects.requireNonNull(model.get()); - Shape container = m.getShape(member.getContainer()).orElse(null); - Shape target = m.getShape(member.getTarget()).orElse(null); + Shape shape = m.getShape(shapeId.toShapeId()).orElse(null); - // Ignore broken models in this index. Other validators handle these checks. - if (container == null || target == null) { + if (shape == null) { return false; } - // Defer to 2.0 checks for shapes that aren't structures, since the logic is the same. - if (container.getType() != ShapeType.STRUCTURE) { - return isMemberNullable(member); + switch (shape.getType()) { + case MEMBER: + return isMemberNullableInV1(m, shape.asMemberShape().get()); + case BOOLEAN: + case BYTE: + case SHORT: + case INTEGER: + case LONG: + case FLOAT: + case DOUBLE: + return shape.hasTrait(BoxTrait.class); + default: + return true; } + } + + private boolean isMemberNullableInV1(Model model, MemberShape member) { + Shape container = model.getShape(member.getContainer()).orElse(null); - // Check if the member or the target has the box trait. - if (member.getMemberTrait(m, BoxTrait.class).isPresent()) { - return true; + // Ignore broken models in this index. Other validators handle these checks. + if (container == null) { + return false; } - return V1_INHERENTLY_BOXED.contains(target.getType()); + switch (container.getType()) { + case STRUCTURE: + // Only structure shapes look at the box trait. + if (member.hasTrait(BoxTrait.class)) { + return true; + } else { + return isNullable(member.getTarget()); + } + case MAP: + // Map keys can never be null. + if (member.getMemberName().equals("key")) { + return false; + } // fall-through + case LIST: + // Sparse lists and maps are considered nullable. + return container.hasTrait(SparseTrait.class); + default: + return false; + } } } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/NodeValidationVisitor.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/NodeValidationVisitor.java index 0ed1c5bb51e..ce681135a81 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/validation/NodeValidationVisitor.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/NodeValidationVisitor.java @@ -360,8 +360,11 @@ public List serviceShape(ServiceShape shape) { private List invalidShape(Shape shape, NodeType expectedType) { // Nullable shapes allow null values. - if (allowOptionalNull && value.isNullNode() && nullableIndex.isNullable(shape)) { - return Collections.emptyList(); + if (allowOptionalNull && value.isNullNode()) { + // Non-members are nullable. Members are nullable based on context. + if (!shape.isMemberShape() || shape.asMemberShape().filter(nullableIndex::isMemberNullable).isPresent()) { + return Collections.emptyList(); + } } String message = String.format( diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/NullableIndexTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/NullableIndexTest.java index 98aaf37581a..3275c1bfeb5 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/NullableIndexTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/NullableIndexTest.java @@ -31,7 +31,9 @@ import software.amazon.smithy.model.shapes.IntegerShape; import software.amazon.smithy.model.shapes.ListShape; import software.amazon.smithy.model.shapes.MapShape; +import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.SetShape; +import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.model.shapes.StringShape; import software.amazon.smithy.model.shapes.StructureShape; @@ -50,7 +52,18 @@ public class NullableIndexTest { public void checksIfBoxed(Model model, String shapeId, boolean isBoxed) { NullableIndex index = NullableIndex.of(model); ShapeId targetId = ShapeId.from(shapeId); - boolean actual = index.isNullable(targetId); + Shape shape = model.expectShape(targetId); + + boolean actual; + + if (shape.isMemberShape()) { + // Test member shapes using 2.0 semantics. + MemberShape member = shape.asMemberShape().get(); + actual = index.isMemberNullable(member); + } else { + // Test root shapes using 1.0 semantics. + actual = index.isNullable(targetId); + } if (isBoxed != actual) { if (isBoxed) { @@ -244,7 +257,7 @@ public void worksWithV1NullabilityRulesForInteger() { Model model = Model.builder().addShapes(integer, struct).build(); NullableIndex index = NullableIndex.of(model); - assertThat(index.isMemberNullableInV1(struct.getMember("foo").get()), is(false)); + assertThat(index.isNullable(struct.getMember("foo").get()), is(false)); } @Test @@ -259,7 +272,7 @@ public void worksWithV1NullabilityRulesForString() { Model model = Model.builder().addShapes(string, struct).build(); NullableIndex index = NullableIndex.of(model); - assertThat(index.isMemberNullableInV1(struct.getMember("foo").get()), is(true)); + assertThat(index.isNullable(struct.getMember("foo").get()), is(true)); } @Test @@ -275,7 +288,7 @@ public void worksWithV1NullabilityRulesForBoxedMember() { Model model = Model.builder().addShapes(integer, struct).build(); NullableIndex index = NullableIndex.of(model); - assertThat(index.isMemberNullableInV1(struct.getMember("foo").get()), is(true)); + assertThat(index.isNullable(struct.getMember("foo").get()), is(true)); } @Test @@ -292,7 +305,7 @@ public void worksWithV1NullabilityRulesForBoxedTarget() { Model model = Model.builder().addShapes(integer, struct).build(); NullableIndex index = NullableIndex.of(model); - assertThat(index.isMemberNullableInV1(struct.getMember("foo").get()), is(true)); + assertThat(index.isNullable(struct.getMember("foo").get()), is(true)); } @Test @@ -310,6 +323,68 @@ public void worksWithV1NullabilityRulesIgnoringRequired() { Model model = Model.builder().addShapes(integer, struct).build(); NullableIndex index = NullableIndex.of(model); - assertThat(index.isMemberNullableInV1(struct.getMember("foo").get()), is(true)); + assertThat(index.isNullable(struct.getMember("foo").get()), is(true)); + } + + @Test + @SuppressWarnings("deprecation") + public void worksWithV1NullabilityRulesForDenseMaps() { + MapShape map = MapShape.builder() + .id("smithy.example#Map") + .key(ShapeId.from("smithy.api#String")) + .value(ShapeId.from("smithy.api#String")) + .build(); + Model model = Model.assembler().addShapes(map).assemble().unwrap(); + NullableIndex index = NullableIndex.of(model); + + assertThat(index.isNullable(map), is(true)); + assertThat(index.isNullable(map.getKey()), is(false)); + assertThat(index.isNullable(map.getValue()), is(false)); + } + + @Test + @SuppressWarnings("deprecation") + public void worksWithV1NullabilityRulesForSparseMaps() { + MapShape map = MapShape.builder() + .id("smithy.example#Map") + .key(ShapeId.from("smithy.api#String")) + .value(ShapeId.from("smithy.api#String")) + .addTrait(new SparseTrait()) + .build(); + Model model = Model.assembler().addShapes(map).assemble().unwrap(); + NullableIndex index = NullableIndex.of(model); + + assertThat(index.isNullable(map), is(true)); + assertThat(index.isNullable(map.getKey()), is(false)); + assertThat(index.isNullable(map.getValue()), is(true)); + } + + @Test + @SuppressWarnings("deprecation") + public void worksWithV1NullabilityRulesForDenseLists() { + ListShape list = ListShape.builder() + .id("smithy.example#Map") + .member(ShapeId.from("smithy.api#String")) + .build(); + Model model = Model.assembler().addShapes(list).assemble().unwrap(); + NullableIndex index = NullableIndex.of(model); + + assertThat(index.isNullable(list), is(true)); + assertThat(index.isNullable(list.getMember()), is(false)); + } + + @Test + @SuppressWarnings("deprecation") + public void worksWithV1NullabilityRulesForSparseLists() { + ListShape list = ListShape.builder() + .id("smithy.example#Map") + .member(ShapeId.from("smithy.api#String")) + .addTrait(new SparseTrait()) + .build(); + Model model = Model.assembler().addShapes(list).assemble().unwrap(); + NullableIndex index = NullableIndex.of(model); + + assertThat(index.isNullable(list), is(true)); + assertThat(index.isNullable(list.getMember()), is(true)); } } From a4c0764867b1d58c20ddd1e2721d6a35c8a216b6 Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Fri, 5 Aug 2022 20:18:56 -0700 Subject: [PATCH 15/20] Add CLIENT_CAREFUL mode to NullableIndex --- .../smithy/model/knowledge/NullableIndex.java | 53 ++++++++++++++----- .../model/knowledge/NullableIndexTest.java | 23 ++++++++ 2 files changed, 64 insertions(+), 12 deletions(-) diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/knowledge/NullableIndex.java b/smithy-model/src/main/java/software/amazon/smithy/model/knowledge/NullableIndex.java index f9605b2747e..439b95537a9 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/knowledge/NullableIndex.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/knowledge/NullableIndex.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Copyright 2022 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,9 @@ import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.StructureShape; import software.amazon.smithy.model.shapes.ToShapeId; +import software.amazon.smithy.model.shapes.UnionShape; import software.amazon.smithy.model.traits.BoxTrait; import software.amazon.smithy.model.traits.ClientOptionalTrait; import software.amazon.smithy.model.traits.DefaultTrait; @@ -52,7 +54,32 @@ public enum CheckMode { * A client, or any other kind of non-authoritative model consumer * that must honor the {@link InputTrait} and {@link ClientOptionalTrait}. */ - CLIENT, + CLIENT { + @Override + boolean isStructureMemberOptional(StructureShape container, MemberShape member, Shape target) { + if (member.hasTrait(ClientOptionalTrait.class) || container.hasTrait(InputTrait.class)) { + return true; + } + + return SERVER.isStructureMemberOptional(container, member, target); + } + }, + + /** + * Like {@link #CLIENT} mode, but will treat all members that target + * structures and unions as optional because these members can never + * transition to optional using a default trait. + */ + CLIENT_CAREFUL { + @Override + boolean isStructureMemberOptional(StructureShape container, MemberShape member, Shape target) { + if (target instanceof StructureShape || target instanceof UnionShape) { + return true; + } + + return CLIENT.isStructureMemberOptional(container, member, target); + } + }, /** * A server, or any other kind of authoritative model consumer @@ -66,7 +93,15 @@ public enum CheckMode { * server is required to be updated in order to implement newly added * model components. */ - SERVER + SERVER { + @Override + boolean isStructureMemberOptional(StructureShape container, MemberShape member, Shape target) { + // Structure members that are @required or @default are not nullable. + return !(member.hasTrait(DefaultTrait.class) || member.hasTrait(RequiredTrait.class)); + } + }; + + abstract boolean isStructureMemberOptional(StructureShape container, MemberShape member, Shape target); } /** @@ -74,7 +109,7 @@ public enum CheckMode { * * @param member Member to check. * @return Returns true if the member is optional in - * non-authoritative consumers of the model like clients. + * non-authoritative consumers of the model like clients. * @see #isMemberNullable(MemberShape, CheckMode) */ public boolean isMemberNullable(MemberShape member) { @@ -97,17 +132,11 @@ public boolean isMemberNullable(MemberShape member) { public boolean isMemberNullable(MemberShape member, CheckMode checkMode) { Model m = Objects.requireNonNull(model.get()); Shape container = m.expectShape(member.getContainer()); + Shape target = m.expectShape(member.getTarget()); switch (container.getType()) { case STRUCTURE: - // Client mode honors the nullable and input trait. - if (checkMode == CheckMode.CLIENT - && (member.hasTrait(ClientOptionalTrait.class) || container.hasTrait(InputTrait.class))) { - return true; - } - - // Structure members that are @required or @default are not nullable. - return !member.hasTrait(DefaultTrait.class) && !member.hasTrait(RequiredTrait.class); + return checkMode.isStructureMemberOptional(container.asStructureShape().get(), member, target); case UNION: case SET: // Union and set members are never null. diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/NullableIndexTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/NullableIndexTest.java index 3275c1bfeb5..98bb90ed2a2 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/NullableIndexTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/NullableIndexTest.java @@ -387,4 +387,27 @@ public void worksWithV1NullabilityRulesForSparseLists() { assertThat(index.isNullable(list), is(true)); assertThat(index.isNullable(list.getMember()), is(true)); } + + @Test + public void carefulModeTreatsStructureAndUnionAsOptional() { + StructureShape struct = StructureShape.builder() + .id("smithy.example#Struct") + .build(); + UnionShape union = UnionShape.builder() + .id("smithy.example#Union") + .addMember("a", ShapeId.from("smithy.api#String")) + .build(); + StructureShape outer = StructureShape.builder() + .id("smithy.example#Outer") + .addMember("a", struct.getId(), m -> m.addTrait(new RequiredTrait())) + .addMember("b", union.getId(), m -> m.addTrait(new RequiredTrait())) + .build(); + Model model = Model.assembler().addShapes(struct, union, outer).assemble().unwrap(); + NullableIndex index = NullableIndex.of(model); + + assertThat(index.isMemberNullable(outer.getMember("a").get(), NullableIndex.CheckMode.CLIENT_CAREFUL), + is(true)); + assertThat(index.isMemberNullable(outer.getMember("b").get(), NullableIndex.CheckMode.CLIENT_CAREFUL), + is(true)); + } } From 690a27b9b02231a4435ff2ea856d029e5f853117 Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Sat, 6 Aug 2022 16:24:08 -0700 Subject: [PATCH 16/20] Stop upgrading unknown version shapes If we don't know the version of a shape (like if a Model or a bunch of pre-built shapes are added to an assembler), then we should not attempt to upgrade them. If we do attempt to upgrade them, then we cause issues with things like projections in smithy-build and CLI, where we build a model, then pass that built model into each project, thereby causing the version to be unknown (it's detached from the original source file that contained the version information). If we do attempt to upgrade this kind of model again, then we can get into a situation where the following model: ``` $version: "2.0" namespace smithy.example structure Foo { bar: Bar } integer Bar ``` Is projected into the following, incorrect model: ``` { "smithy": "2.0", "shapes": { "smithy.example#Foo" { "type": "structure", "members": { "bar": { "target": "smithy.example#Bar", "traits": { "smithy.api#default": 0 } } } }, "smithy.example#Bar": { "type": "integer" } } } ``` Notice the incorrectly added `@default` trait. This would happen because when using Smithy 1.0 semantics, `integer Bar` has a default zero value because it isn't marked with the `@box` trait. No `@default` trait is added to the "source" model projection because at that point we know the version is 2.0. But when doing another projection, the loaded model is disconnected from the original version and would cause this issue. In addition to not doing upgrades on shapes with an unknown version, this change also stops adding synthetic box traits to members. That's no longer needed because we now keep a synthetic box trait on prelude shapes. When we removed those traits previously, it meant it was impossible to determine the 1.0 nullability of a shape unless we add the synthetic box trait to the member. Given this change, we no longer need to validate the box trait in the ModelUpgrade and can rather do the validation in the Version enum directly. --- .../services/machinelearning.smithy | 1 - .../restJson1/services/apigateway.smithy | 1 - .../smithy/model/loader/ModelUpgrader.java | 71 ++++--------------- .../amazon/smithy/model/loader/Version.java | 21 +++++- .../model/knowledge/NullableIndexTest.java | 4 +- .../model/loader/ModelUpgraderTest.java | 24 +------ 6 files changed, 35 insertions(+), 87 deletions(-) diff --git a/smithy-aws-protocol-tests/model/awsJson1_1/services/machinelearning.smithy b/smithy-aws-protocol-tests/model/awsJson1_1/services/machinelearning.smithy index e16cc9fbcef..9b952ab9209 100644 --- a/smithy-aws-protocol-tests/model/awsJson1_1/services/machinelearning.smithy +++ b/smithy-aws-protocol-tests/model/awsJson1_1/services/machinelearning.smithy @@ -160,7 +160,6 @@ integer ErrorCode ) string ErrorMessage -@box float floatLabel @length( diff --git a/smithy-aws-protocol-tests/model/restJson1/services/apigateway.smithy b/smithy-aws-protocol-tests/model/restJson1/services/apigateway.smithy index 4366e6c68a3..e4e729b14be 100644 --- a/smithy-aws-protocol-tests/model/restJson1/services/apigateway.smithy +++ b/smithy-aws-protocol-tests/model/restJson1/services/apigateway.smithy @@ -150,7 +150,6 @@ enum EndpointType { PRIVATE } -@box integer NullableInteger string String diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelUpgrader.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelUpgrader.java index 1eb3980eb72..4d4338e6271 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelUpgrader.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelUpgrader.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Copyright 2022 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. @@ -74,32 +74,26 @@ ValidatedResult transform() { } Version version = fileToVersion.apply(member); - if (version == Version.VERSION_2_0) { - validateV2Member(member); - } else { - // Also attempt to upgrade unknown versions since they could be 1.0 and - // trying to upgrade 2.0 shapes has no effect. - // For v1 shape checks, we need to know the containing shape type to apply the appropriate transform. + + // Only update models that are for sure 1.0. Upgrading unknown versions can cause + // things like projections to build a model, feed it back into the assembler, and + // then lose the original context of which version the model was built with, causing + // it to be errantly upgraded. + if (version == Version.VERSION_1_0) { model.getShape(member.getContainer()) - .ifPresent(container -> upgradeV1Member(version, container.getType(), member)); + .ifPresent(container -> upgradeV1Member(container.getType(), member)); } } return new ValidatedResult<>(ModelTransformer.create().replaceShapes(model, shapeUpgrades), events); } - private void upgradeV1Member(Version version, ShapeType containerType, MemberShape member) { + private void upgradeV1Member(ShapeType containerType, MemberShape member) { // Don't fail here on broken models, and since it's broken, don't try to upgrade it. Shape target = model.getShape(member.getTarget()).orElse(null); - if (target == null) { - return; - } - - // This builder will become non-null if/when the member needs to be updated. - MemberShape.Builder builder = null; - // Add the @default trait to structure members when needed. - if (shouldV1MemberHaveDefaultTrait(containerType, member, target)) { + if (target != null && shouldV1MemberHaveDefaultTrait(containerType, member, target)) { + // Add the @default trait to structure members when needed. events.add(ValidationEvent.builder() .id(Validator.MODEL_DEPRECATION) .severity(Severity.WARNING) @@ -107,8 +101,7 @@ private void upgradeV1Member(Version version, ShapeType containerType, MemberSha .message("Add the @default trait to this member to make it forward compatible with " + "Smithy IDL 2.0") .build()); - builder = createOrReuseBuilder(member, builder); - + MemberShape.Builder builder = member.toBuilder(); if (target.isBooleanShape()) { builder.addTrait(new DefaultTrait(new BooleanNode(false, builder.getSourceLocation()))); } else if (target.isBlobShape()) { @@ -116,33 +109,10 @@ private void upgradeV1Member(Version version, ShapeType containerType, MemberSha } else if (isZeroValidDefault(member)) { builder.addTrait(new DefaultTrait(new NumberNode(0, builder.getSourceLocation()))); } - } else if (isMemberImplicitlyBoxed(version, containerType, member, target)) { - // Add a synthetic box trait to the shape. - builder = createOrReuseBuilder(member, builder).addTrait(new BoxTrait()); - } - - if (builder != null) { shapeUpgrades.add(builder.build()); } } - // If it's for sure a v1 shape and was implicitly boxed, then add a synthetic box trait so tooling - // can know that the shape was previously considered nullable. Note that this method does not - // check if the targeted shape is required. It's up to tooling to determine how to handle a 1.0 - // member that is both required and boxed. - private boolean isMemberImplicitlyBoxed( - Version version, - ShapeType containerType, - MemberShape member, - Shape target - ) { - return version == Version.VERSION_1_0 - && containerType == ShapeType.STRUCTURE - && !member.hasTrait(DefaultTrait.class) // don't add box if it has a default trait. - && !member.hasTrait(BoxTrait.class) // don't add box again - && target.hasTrait(BoxTrait.class); - } - private boolean isZeroValidDefault(MemberShape member) { Optional rangeTraitOptional = member.getMemberTrait(model, RangeTrait.class); // No range means 0 is fine. @@ -196,21 +166,4 @@ private boolean shouldV1MemberHaveDefaultTrait(ShapeType containerType, MemberSh private boolean isDefaultPayload(Shape target) { return target.hasTrait(StreamingTrait.class) && target.isBlobShape(); } - - private MemberShape.Builder createOrReuseBuilder(MemberShape member, MemberShape.Builder builder) { - return builder == null ? member.toBuilder() : builder; - } - - @SuppressWarnings("deprecation") - private void validateV2Member(MemberShape member) { - if (member.hasTrait(BoxTrait.class)) { - events.add(ValidationEvent.builder() - .id(Validator.MODEL_DEPRECATION) - .severity(Severity.ERROR) - .shape(member) - .sourceLocation(member.expectTrait(BoxTrait.class)) - .message("@box is not supported in Smithy IDL 2.0") - .build()); - } - } } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/Version.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/Version.java index 4edf4a103a7..5300cf50433 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/Version.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/Version.java @@ -18,6 +18,7 @@ import software.amazon.smithy.model.node.Node; import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.model.shapes.ShapeType; +import software.amazon.smithy.model.traits.BoxTrait; import software.amazon.smithy.model.traits.DefaultTrait; import software.amazon.smithy.model.traits.MixinTrait; @@ -25,6 +26,7 @@ * Tracks version-specific features and validation. */ enum Version { + UNKNOWN { @Override public String toString() { @@ -60,6 +62,10 @@ boolean isShapeTypeSupported(ShapeType shapeType) { boolean isDeprecated() { return false; } + + @Override + void validateVersionedTrait(ShapeId target, ShapeId traitId, Node value) { + } }, VERSION_1_0 { @@ -154,6 +160,18 @@ boolean isShapeTypeSupported(ShapeType shapeType) { boolean isDeprecated() { return false; } + + @Override + @SuppressWarnings("deprecated") + void validateVersionedTrait(ShapeId target, ShapeId traitId, Node value) { + if (traitId.equals(BoxTrait.ID)) { + throw ModelSyntaxException.builder() + .message("@box is not supported in Smithy IDL 2.0") + .shapeId(target) + .sourceLocation(value) + .build(); + } + } }; /** @@ -234,6 +252,5 @@ boolean supportsResourceProperties() { * @param value The Node value of the trait. * @throws ModelSyntaxException if the given trait cannot be used in this version. */ - void validateVersionedTrait(ShapeId target, ShapeId traitId, Node value) { - } + abstract void validateVersionedTrait(ShapeId target, ShapeId traitId, Node value); } diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/NullableIndexTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/NullableIndexTest.java index 98bb90ed2a2..a495b3fb8af 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/NullableIndexTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/NullableIndexTest.java @@ -115,7 +115,7 @@ public static Collection data() { .addMember("b", ShapeId.from("smithy.api#Boolean")) // Nullable because the member is boxed .addMember("c", ShapeId.from("smithy.api#PrimitiveBoolean"), b -> b.addTrait(new BoxTrait())) - // Not nullable. + // Nullable because we don't know if this is a 1.0 or 2.0 model. .addMember("d", ShapeId.from("smithy.api#PrimitiveBoolean")) .addMember("e", ShapeId.from("smithy.api#Document")) .build(); @@ -168,7 +168,7 @@ public static Collection data() { {model, structure.getMember("a").get().getId().toString(), true}, {model, structure.getMember("b").get().getId().toString(), true}, {model, structure.getMember("c").get().getId().toString(), true}, - {model, structure.getMember("d").get().getId().toString(), false}, + {model, structure.getMember("d").get().getId().toString(), true}, // documents are nullable as structure members {model, structure.getMember("e").get().getId().toString(), true}, }); diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/loader/ModelUpgraderTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/loader/ModelUpgraderTest.java index b6d77f52b40..5197312f2ef 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/loader/ModelUpgraderTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/loader/ModelUpgraderTest.java @@ -1,7 +1,6 @@ package software.amazon.smithy.model.loader; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import java.io.IOException; @@ -21,7 +20,6 @@ import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.ModelSerializer; import software.amazon.smithy.model.shapes.ShapeId; -import software.amazon.smithy.model.traits.BoxTrait; import software.amazon.smithy.model.traits.DefaultTrait; import software.amazon.smithy.model.validation.Severity; import software.amazon.smithy.model.validation.ValidatedResult; @@ -193,26 +191,8 @@ private static Matcher shapeTargetsDeprecatedPrimitive(ValidatedResult< private static Matcher v2ShapeUsesBoxTrait(ValidatedResult result) { return ShapeMatcher.builderFor(MemberShape.class, result) .description("v2 shape uses box trait") - .addEventAssertion(Validator.MODEL_DEPRECATION, Severity.ERROR, "@box is not supported") + .addEventAssertion(Validator.MODEL_ERROR, Severity.ERROR, + "@box is not supported in Smithy IDL 2.0") .build(); } - - @Test - public void addSyntheticBoxTrait() { - Model model = Model.assembler() - .addImport(getClass().getResource("upgrade/does-not-introduce-conflict/main.smithy")) - .assemble() - .unwrap(); - - assertThat(hasBoxTrait(model, "smithy.example#Foo$alreadyDefault"), is(false)); - assertThat(hasBoxTrait(model, "smithy.example#Foo$alreadyRequired"), is(false)); - assertThat(hasBoxTrait(model, "smithy.example#Foo$boxedMember"), is(true)); - assertThat(hasBoxTrait(model, "smithy.example#Foo$explicitlyBoxedTarget"), is(true)); - assertThat(hasBoxTrait(model, "smithy.example#Foo$previouslyBoxedTarget"), is(true)); - assertThat(hasBoxTrait(model, "smithy.example#Foo$customPrimitiveLong"), is(false)); - } - - private boolean hasBoxTrait(Model model, String shape) { - return model.expectShape(ShapeId.from(shape)).hasTrait(BoxTrait.class); - } } From 6d729037339583e9b2e9a98746e1cb2cf530c6f7 Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Sat, 6 Aug 2022 23:09:33 -0700 Subject: [PATCH 17/20] Reduce model upgrade checks to only struct members --- .../smithy/model/loader/ModelUpgrader.java | 54 +++++++++---------- 1 file changed, 24 insertions(+), 30 deletions(-) diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelUpgrader.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelUpgrader.java index 4d4338e6271..6793007bd17 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelUpgrader.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelUpgrader.java @@ -28,6 +28,7 @@ import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.model.shapes.ShapeType; +import software.amazon.smithy.model.shapes.StructureShape; import software.amazon.smithy.model.traits.BoxTrait; import software.amazon.smithy.model.traits.DefaultTrait; import software.amazon.smithy.model.traits.RangeTrait; @@ -44,6 +45,7 @@ * account the removal of the box trait, the change in default value * semantics of numbers and booleans, and the @default trait. */ +@SuppressWarnings("deprecation") final class ModelUpgrader { /** Shape types in Smithy 1.0 that had a default value. */ @@ -68,31 +70,24 @@ final class ModelUpgrader { } ValidatedResult transform() { - for (MemberShape member : model.getMemberShapes()) { - if (Prelude.isPreludeShape(member)) { - continue; - } - - Version version = fileToVersion.apply(member); - - // Only update models that are for sure 1.0. Upgrading unknown versions can cause - // things like projections to build a model, feed it back into the assembler, and - // then lose the original context of which version the model was built with, causing - // it to be errantly upgraded. - if (version == Version.VERSION_1_0) { - model.getShape(member.getContainer()) - .ifPresent(container -> upgradeV1Member(container.getType(), member)); + // Upgrade structure members in v1 models to add the default trait if needed. Upgrading unknown versions + // can cause things like projections to build a model, feed it back into the assembler, and then lose the + // original context of which version the model was built with, causing it to be errantly upgraded. + for (StructureShape struct : model.getStructureShapes()) { + if (!Prelude.isPreludeShape(struct) && fileToVersion.apply(struct) == Version.VERSION_1_0) { + for (MemberShape member : struct.getAllMembers().values()) { + model.getShape(member.getTarget()).ifPresent(target -> { + upgradeV1Member(member, target); + }); + } } } return new ValidatedResult<>(ModelTransformer.create().replaceShapes(model, shapeUpgrades), events); } - private void upgradeV1Member(ShapeType containerType, MemberShape member) { - // Don't fail here on broken models, and since it's broken, don't try to upgrade it. - Shape target = model.getShape(member.getTarget()).orElse(null); - - if (target != null && shouldV1MemberHaveDefaultTrait(containerType, member, target)) { + private void upgradeV1Member(MemberShape member, Shape target) { + if (shouldV1MemberHaveDefaultTrait(member, target)) { // Add the @default trait to structure members when needed. events.add(ValidationEvent.builder() .id(Validator.MODEL_DEPRECATION) @@ -147,23 +142,22 @@ private boolean isZeroValidDefault(MemberShape member) { } @SuppressWarnings("deprecation") - private boolean shouldV1MemberHaveDefaultTrait(ShapeType containerType, MemberShape member, Shape target) { - return containerType == ShapeType.STRUCTURE - // Only when the targeted shape had a default value by default in v1 or if - // the member has the http payload trait and targets a streaming blob, which - // implies a default in 2.0 - && (HAD_DEFAULT_VALUE_IN_1_0.contains(target.getType()) || isDefaultPayload(target)) + private boolean shouldV1MemberHaveDefaultTrait(MemberShape member, Shape target) { + // Only when the targeted shape had a default value by default in v1 or if + // the member has the http payload trait and targets a streaming blob, which + // implies a default in 2.0 + return (HAD_DEFAULT_VALUE_IN_1_0.contains(target.getType()) || isDefaultPayload(target)) // Don't re-add the @default trait - && !member.hasTrait(DefaultTrait.class) + && !member.hasTrait(DefaultTrait.ID) // Don't add a @default trait if it will conflict with the @required trait. - && !member.hasTrait(RequiredTrait.class) + && !member.hasTrait(RequiredTrait.ID) // Don't add a @default trait if the member was explicitly boxed in v1. - && !member.hasTrait(BoxTrait.class) + && !member.hasTrait(BoxTrait.ID) // Don't add a @default trait if the targeted shape was explicitly boxed in v1. - && !target.hasTrait(BoxTrait.class); + && !target.hasTrait(BoxTrait.ID); } private boolean isDefaultPayload(Shape target) { - return target.hasTrait(StreamingTrait.class) && target.isBlobShape(); + return target.hasTrait(StreamingTrait.ID) && target.isBlobShape(); } } From 2841778d89291d04580d3e2b67a82e1a015581ad Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Mon, 8 Aug 2022 13:54:27 -0700 Subject: [PATCH 18/20] Update selector ABNF to use CamelCase --- docs/source-2.0/spec/selectors.rst | 114 ++++++++++++++--------------- 1 file changed, 57 insertions(+), 57 deletions(-) diff --git a/docs/source-2.0/spec/selectors.rst b/docs/source-2.0/spec/selectors.rst index 3321ddcecb7..968f340e5ec 100644 --- a/docs/source-2.0/spec/selectors.rst +++ b/docs/source-2.0/spec/selectors.rst @@ -171,7 +171,7 @@ contains a non-empty ``tags`` list. Attribute comparison -------------------- -An attribute selector with a :token:`comparator ` +An attribute selector with a :token:`comparator ` checks for the existence of an attribute and compares the resolved attribute value to a comma separated list of possible values. The resolved attribute value on the left hand side of the comparator MUST @@ -190,7 +190,7 @@ There are three kinds of comparators: String comparators ------------------ -:token:`String comparators ` are used to compare +:token:`String comparators ` are used to compare the string representation of values. Attributes that do not have a string representation are treated as an empty string when these comparisons are performed. @@ -873,7 +873,7 @@ the comparator are projections. Scoped attribute selectors ========================== -A :token:`scoped attribute selector ` is similar to an +A :token:`scoped attribute selector ` is similar to an attribute selector, but it allows multiple complex comparisons to be made against a scoped attribute. @@ -883,7 +883,7 @@ Context values The first part of a scoped attribute selector is the attribute that is scoped for the expression, followed by ``:``. The scoped attribute is accessed using -a :token:`context value ` in the form of +a :token:`context value ` in the form of ``@{`` :token:`smithy:Identifier` ``}``. In the following selector, the ``trait|range`` attribute is used as the scoped @@ -996,7 +996,7 @@ a shape. Forward undirected neighbor ---------------------------- -A :token:`forward undirected neighbor ` +A :token:`forward undirected neighbor ` (``>``) yields every shape that is connected to the current shape. For example, the following selector matches the key and value members of every map: @@ -1025,7 +1025,7 @@ relationships of each operation: operation > * -A forward directed edge traversal is applied using :token:`selectors:selector_forward_directed_neighbor` +A forward directed edge traversal is applied using :token:`selectors:SelectorForwardDirectedNeighbor` (``-[X, Y, Z]->``). The following selector matches all structure shapes referenced as operation ``input`` or ``output``. @@ -1489,7 +1489,7 @@ results that are computed multiples times in a selector or for capturing information about the current shape that is referenced later in a selector after traversing neighbors. -A variable is set using a :token:`selectors:selector_variable_set` expression. +A variable is set using a :token:`selectors:SelectorVariableSet` expression. Variables can be reassigned without error. The following selector defines a variable named ``foo`` that sets the @@ -1499,7 +1499,7 @@ variable to the result of applying the ``*`` selector to the current shape. $foo(*) -A variable is retrieved by name using a :token:`selectors:selector_variable_get` +A variable is retrieved by name using a :token:`selectors:SelectorVariableGet` expression. Retrieving a variable yields the set of shapes stored in the variable. Attempting to get a variable that does not exist yields no shapes. @@ -1603,55 +1603,55 @@ Selectors are defined by the following ABNF_ grammar. changing the semantics of a selector. .. productionlist:: selectors - selector :`selector_expression` *(`selector_expression`) - selector_expression :`selector_shape_types` - :/ `selector_attr` - :/ `selector_scoped_attr` - :/ `selector_function` - :/ `selector_forward_undirected_neighbor` - :/ `selector_reverse_undirected_neighbor` - :/ `selector_forward_directed_neighbor` - :/ `selector_reverse_directed_neighbor` - :/ `selector_forward_recursive_neighbor` - :/ `selector_variable_set` - :/ `selector_variable_get` - selector_shape_types :"*" / `smithy:Identifier` - selector_forward_undirected_neighbor :">" - selector_reverse_undirected_neighbor :"<" - selector_forward_directed_neighbor :"-[" `selector_directed_relationships` "]->" - selector_reverse_directed_neighbor :"<-[" selector_directed_relationships "]-" - selector_directed_relationships :`smithy:Identifier` *("," `smithy:Identifier`) - selector_forward_recursive_neighbor :"~>" - selector_attr :"[" `selector_key` [selector_attr_comparison] "]" - selector_attr_comparison :`selector_comparator` `selector_attr_values` ["i"] - selector_key :`smithy:Identifier` ["|" `selector_path`] - selector_path :`selector_path_segment` *("|" `selector_path_segment`) - selector_path_segment :`selector_value` / `selector_function_property` - selector_value :`selector_text` / `smithy:Number` / `smithy:RootShapeId` - selector_function_property :"(" `smithy:Identifier` ")" - selector_attr_values :`selector_value` *("," `selector_value`) - selector_comparator :`selector_string_comparator` - :/ `selector_numeric_comparator` - :/ `selector_projection_comparator` - selector_string_comparator :"^=" / "$=" / "*=" / "!=" / "=" / "?=" - selector_numeric_comparator :">=" / ">" / "<=" / "<" - selector_projection_comparator :"{=}" / "{!=}" / "{<}" / "{<<}" - selector_AbsoluteRootShapeId :`smithy:Namespace` "#" `smithy:Identifier` - selector_scoped_attr :"[@" [`selector_key`] ":" `selector_scoped_assertions` "]" - selector_scoped_assertions :`selector_scoped_assertion` *("&&" `selector_scoped_assertion`) - selector_scoped_assertion :`selector_scoped_value` `selector_comparator` `selector_scoped_values` ["i"] - selector_scoped_value :`selector_value` / `selector_context_value` - selector_context_value :"@{" `selector_path` "}" - selector_scoped_values :`selector_scoped_value` *("," `selector_scoped_value`) - selector_function :":" `smithy:Identifier` "(" `selector_function_args` ")" - selector_function_args :`selector` *("," `selector`) - selector_text :`selector_single_quoted_text` / `selector_double_quoted_text` - selector_single_quoted_text :"'" 1*`selector_single_quoted_char` "'" - selector_double_quoted_text :DQUOTE 1*`selector_double_quoted_char` DQUOTE - selector_single_quoted_char :%x20-26 / %x28-5B / %x5D-10FFFF ; Excludes (') - selector_double_quoted_char :%x20-21 / %x23-5B / %x5D-10FFFF ; Excludes (") - selector_variable_set :"$" `smithy:Identifier` "(" selector ")" - selector_variable_get :"${" `smithy:Identifier` "}" + Selector :`SelectorExpression` *(`SelectorExpression`) + SelectorExpression :`SelectorShapeTypes` + :/ `SelectorAttr` + :/ `SelectorScopedAttr` + :/ `SelectorFunction` + :/ `SelectorForwardUndirectedNeighbor` + :/ `SelectorReverseUndirectedNeighbor` + :/ `SelectorForwardDirectedNeighbor` + :/ `SelectorForwardRecursiveNeighbor` + :/ `SelectorReverseDirectedNeighbor` + :/ `SelectorVariableSet` + :/ `SelectorVariableGet` + SelectorShapeTypes :"*" / `smithy:Identifier` + SelectorForwardUndirectedNeighbor :">" + SelectorReverseUndirectedNeighbor :"<" + SelectorForwardDirectedNeighbor :"-[" `SelectorDirectedRelationships` "]->" + SelectorReverseDirectedNeighbor :"<-[" SelectorDirectedRelationships "]-" + SelectorDirectedRelationships :`smithy:Identifier` *("," `smithy:Identifier`) + SelectorForwardRecursiveNeighbor :"~>" + SelectorAttr :"[" `SelectorKey` [SelectorAttrComparison] "]" + SelectorAttrComparison :`SelectorComparator` `SelectorAttrValues` ["i"] + SelectorKey :`smithy:Identifier` ["|" `SelectorPath`] + SelectorPath :`SelectorPathSegment` *("|" `SelectorPathSegment`) + SelectorPathSegment :`SelectorValue` / `SelectorFunctionProperty` + SelectorValue :`SelectorText` / `smithy:Number` / `smithy:RootShapeId` + SelectorFunctionProperty :"(" `smithy:Identifier` ")" + SelectorAttrValues :`SelectorValue` *("," `SelectorValue`) + SelectorComparator :`SelectorStringComparator` + :/ `SelectorNumericComparator` + :/ `SelectorProjectionComparator` + SelectorStringComparator :"^=" / "$=" / "*=" / "!=" / "=" / "?=" + SelectorNumericComparator :">=" / ">" / "<=" / "<" + SelectorProjectionComparator :"{=}" / "{!=}" / "{<}" / "{<<}" + SelectorAbsoluteRootShapeId :`smithy:Namespace` "#" `smithy:Identifier` + SelectorScopedAttr :"[@" [`SelectorKey`] ":" `SelectorScopedAssertions` "]" + SelectorScopedAssertions :`SelectorScopedAssertion` *("&&" `SelectorScopedAssertion`) + SelectorScopedAssertion :`SelectorScopedValue` `SelectorComparator` `SelectorScopedValues` ["i"] + SelectorScopedValue :`SelectorValue` / `SelectorContextValue` + SelectorContextValue :"@{" `SelectorPath` "}" + SelectorScopedValues :`SelectorScopedValue` *("," `SelectorScopedValue`) + SelectorFunction :":" `smithy:Identifier` "(" `SelectorFunctionArgs` ")" + SelectorFunctionArgs :`selector` *("," `selector`) + SelectorText :`SelectorSingleQuotedText` / `SelectorDoubleQuotedText` + SelectorSingleQuotedText :"'" 1*`SelectorSingleQuotedChar` "'" + SelectorDoubleQuotedText :DQUOTE 1*`SelectorDoubleQuotedChar` DQUOTE + SelectorSingleQuotedChar :%x20-26 / %x28-5B / %x5D-10FFFF ; Excludes (') + SelectorDoubleQuotedChar :%x20-21 / %x23-5B / %x5D-10FFFF ; Excludes (") + SelectorVariableSet :"$" `smithy:Identifier` "(" selector ")" + SelectorVariableGet :"${" `smithy:Identifier` "}" .. _ABNF: https://tools.ietf.org/html/rfc5234 .. _set: https://en.wikipedia.org/wiki/Set_(abstract_data_type) From 6df8fb25f75d9e7abfb17102a665d30db541fc82 Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Mon, 8 Aug 2022 15:14:19 -0700 Subject: [PATCH 19/20] Add synthetic box trait back to 1.0 members We actually do need synthetic box traits on members so that tooling that attempts to work with 1.0 and 2.0 models can accurately know the intended nullability semantics of a structure member after it has been loaded into memory. This is particularly important for structure members marked as required that target a prelude shape like Integer. Without a synthetic box trait on the member, tooling can't differentiate between a member that was considered nullable in v1 or non-nullable in v2 (the box trait on the prelude shape would be honored in v1 but discarded in v2). The trait on the member itself makes the 1.0 nullability explicit. So explicit in fact that we can even check for this trait and use it in the NullableIndex methods that understand 2.0 semantics. The box trait is only allowed in 1.0 models, so its present doesn't interfere with IDL 2.0 nullability semantics. We will still keep the IDL 1.0 isNullable method around because it will work even with models that weren't loaded through an assembler (such models skip the model upgrade process and have no synthetic box trait). --- .../smithy/model/knowledge/NullableIndex.java | 23 +++++++++++-- .../smithy/model/loader/ModelUpgrader.java | 18 ++++++++++ .../model/knowledge/NullableIndexTest.java | 22 +++++++++++++ .../model/loader/ModelUpgraderTest.java | 22 +++++++++++++ .../model/knowledge/nullable-index-v1.smithy | 33 +++++++++++++++++++ 5 files changed, 115 insertions(+), 3 deletions(-) create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/knowledge/nullable-index-v1.smithy diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/knowledge/NullableIndex.java b/smithy-model/src/main/java/software/amazon/smithy/model/knowledge/NullableIndex.java index 439b95537a9..db49fdbef86 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/knowledge/NullableIndex.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/knowledge/NullableIndex.java @@ -96,8 +96,23 @@ boolean isStructureMemberOptional(StructureShape container, MemberShape member, SERVER { @Override boolean isStructureMemberOptional(StructureShape container, MemberShape member, Shape target) { - // Structure members that are @required or @default are not nullable. - return !(member.hasTrait(DefaultTrait.class) || member.hasTrait(RequiredTrait.class)); + // A member with the default trait is never nullable. Note that even 1.0 models will be + // assigned a default trait when they are "upgraded". + if (member.hasTrait(DefaultTrait.class)) { + return false; + } + + // Detects Smithy IDL 1.0 shapes that were marked as nullable using the box trait. When a structure + // member is loaded from a 1.0 model, the member is "upgraded" from v1 to v2 in the semantic model. + // Any member that was implicitly nullable in v1 gets a synthetic box trait on the member. Box traits + // are not allowed in 2.0 models, so this check is specifically here to ensure that the intended + // nullability semantics of 1.0 models are honored and not to interfere with 2.0 nullability semantics. + if (member.hasTrait(BoxTrait.class)) { + return true; + } + + // A 2.0 member with the required trait is never nullable. + return !member.hasTrait(RequiredTrait.class); } }; @@ -163,7 +178,9 @@ public boolean isMemberNullable(MemberShape member, CheckMode checkMode) { * *

Use {@link #isMemberNullable(MemberShape)} to check using Smithy * IDL 2.0 semantics that take required, default, and other traits - * into account. + * into account. That method also accurately returns the nullability of + * 1.0 members as long as the model it's checking was sent through a + * ModelAssembler. * * @param shapeId Shape or shape ID to check. * @return Returns true if the shape is nullable. diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelUpgrader.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelUpgrader.java index 6793007bd17..237da834efa 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelUpgrader.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelUpgrader.java @@ -105,9 +105,27 @@ private void upgradeV1Member(MemberShape member, Shape target) { builder.addTrait(new DefaultTrait(new NumberNode(0, builder.getSourceLocation()))); } shapeUpgrades.add(builder.build()); + } else if (isMemberImplicitlyBoxed(member, target)) { + // Add a synthetic box trait to the shape. + MemberShape.Builder builder = member.toBuilder(); + builder.addTrait(new BoxTrait()); + shapeUpgrades.add(builder.build()); } } + // If it's for sure a v1 shape and was implicitly boxed, then add a synthetic box trait to the member + // so that tooling that works with both v1 and v2 shapes can know if a shape was considered nullable in 1.0. + // This is particularly important if the member is required. A required member in 2.0 semantics is considered + // non-nullable, but considered nullable in 1.0 if the member targets a primitive shape. However, once a v1 + // model is loaded into memory, tooling no longer can differentiate between required in 1.0 or required in + // 2.0. With these synthetic box traits, tooling can look for the box trait on a member to detect v1 + // nullability semantics. + private boolean isMemberImplicitlyBoxed(MemberShape member, Shape target) { + return !member.hasTrait(DefaultTrait.class) // don't add box if it has a default trait. + && !member.hasTrait(BoxTrait.class) // don't add box again + && target.hasTrait(BoxTrait.class); + } + private boolean isZeroValidDefault(MemberShape member) { Optional rangeTraitOptional = member.getMemberTrait(model, RangeTrait.class); // No range means 0 is fine. diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/NullableIndexTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/NullableIndexTest.java index a495b3fb8af..ca8ebace15e 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/NullableIndexTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/NullableIndexTest.java @@ -410,4 +410,26 @@ public void carefulModeTreatsStructureAndUnionAsOptional() { assertThat(index.isMemberNullable(outer.getMember("b").get(), NullableIndex.CheckMode.CLIENT_CAREFUL), is(true)); } + + @Test + public void correctlyDeterminesNullabilityOfUpgradedV1Models() { + Model model = Model.assembler() + .addImport(getClass().getResource("nullable-index-v1.smithy")) + .assemble() + .unwrap(); + + NullableIndex index = NullableIndex.of(model); + + for (MemberShape shape : model.getMemberShapes()) { + if (shape.getId().getNamespace().equals("smithy.example")) { + if (shape.getMemberName().startsWith("nullable")) { + assertThat(index.isMemberNullable(shape), is(true)); + assertThat(index.isNullable(shape), is(true)); + } else if (shape.getMemberName().startsWith("nonNullable")) { + assertThat(index.isMemberNullable(shape), is(false)); + assertThat(index.isNullable(shape), is(false)); + } + } + } + } } diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/loader/ModelUpgraderTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/loader/ModelUpgraderTest.java index 5197312f2ef..98aab21f288 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/loader/ModelUpgraderTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/loader/ModelUpgraderTest.java @@ -1,6 +1,7 @@ package software.amazon.smithy.model.loader; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import java.io.IOException; @@ -20,6 +21,7 @@ import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.ModelSerializer; import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.BoxTrait; import software.amazon.smithy.model.traits.DefaultTrait; import software.amazon.smithy.model.validation.Severity; import software.amazon.smithy.model.validation.ValidatedResult; @@ -195,4 +197,24 @@ private static Matcher v2ShapeUsesBoxTrait(ValidatedResult resul "@box is not supported in Smithy IDL 2.0") .build(); } + + @Test + public void addSyntheticBoxTrait() { + Model model = Model.assembler() + .addImport(getClass().getResource("upgrade/does-not-introduce-conflict/main.smithy")) + .assemble() + .unwrap(); + + assertThat(hasBoxTrait(model, "smithy.example#Foo$alreadyDefault"), is(false)); + assertThat(hasBoxTrait(model, "smithy.example#Foo$alreadyRequired"), is(false)); + assertThat(hasBoxTrait(model, "smithy.example#Foo$boxedMember"), is(true)); + assertThat(hasBoxTrait(model, "smithy.example#Foo$explicitlyBoxedTarget"), is(true)); + assertThat(hasBoxTrait(model, "smithy.example#Foo$previouslyBoxedTarget"), is(true)); + assertThat(hasBoxTrait(model, "smithy.example#Foo$customPrimitiveLong"), is(false)); + } + + @SuppressWarnings("deprecation") + private boolean hasBoxTrait(Model model, String shape) { + return model.expectShape(ShapeId.from(shape)).hasTrait(BoxTrait.class); + } } diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/knowledge/nullable-index-v1.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/knowledge/nullable-index-v1.smithy new file mode 100644 index 00000000000..9b8b1606425 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/knowledge/nullable-index-v1.smithy @@ -0,0 +1,33 @@ +// When this model is queried by the NullableIndex, it should accurately +// report the intended nullability of each member because members are sent +// through the model upgrade process to add synthetic box traits. +$version: "1.0" + +namespace smithy.example + +structure Foo { + nullable1: Boolean, + nullable2: Integer, + nullable3: MyBoolean, + nullable4: String, + + @box + nullable5: MyBoolean, + nullable6: MyStruct, + + // because this was defined in 1.0, it's still nullable because the required trait wasn't + // use in 1.0 nullability rules. + @required + nullable7: MyBoolean, + + nonNullable1: PrimitiveInteger, + nonNullable2: PrimitiveBoolean, + nonNullable3: MyPrimitiveBoolean +} + +@box +boolean MyBoolean + +boolean MyPrimitiveBoolean + +structure MyStruct {} From faa1bd4ce8c0804ed5ba4ee68fee70f90061930d Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Tue, 9 Aug 2022 11:41:50 -0700 Subject: [PATCH 20/20] Make minor typo and doc fixes * Fix IDL typo for "messag" * Add missing javadoc param * Fix selector ABNF tokens --- docs/source-2.0/spec/model-validation.rst | 2 +- docs/source-2.0/spec/selectors.rst | 2 +- .../software/amazon/smithy/model/loader/MetadataContainer.java | 1 + .../software/amazon/smithy/model/loader/prelude.smithy | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/source-2.0/spec/model-validation.rst b/docs/source-2.0/spec/model-validation.rst index 7df45300108..37dc9e6f450 100644 --- a/docs/source-2.0/spec/model-validation.rst +++ b/docs/source-2.0/spec/model-validation.rst @@ -436,7 +436,7 @@ Message templates ----------------- A ``messageTemplate`` is used to create more granular error messages. The -template consists of literal spans and :token:`selector context value ` +template consists of literal spans and :token:`selector context value ` templates (for example, ``@{id}``). A selector context value MAY be escaped by placing a ``@`` before a ``@`` character (for example, ``@@`` expands to ``@``). ``@`` characters in the message template that are not escaped MUST diff --git a/docs/source-2.0/spec/selectors.rst b/docs/source-2.0/spec/selectors.rst index 968f340e5ec..aa9b745bd63 100644 --- a/docs/source-2.0/spec/selectors.rst +++ b/docs/source-2.0/spec/selectors.rst @@ -1644,7 +1644,7 @@ Selectors are defined by the following ABNF_ grammar. SelectorContextValue :"@{" `SelectorPath` "}" SelectorScopedValues :`SelectorScopedValue` *("," `SelectorScopedValue`) SelectorFunction :":" `smithy:Identifier` "(" `SelectorFunctionArgs` ")" - SelectorFunctionArgs :`selector` *("," `selector`) + SelectorFunctionArgs :`Selector` *("," `Selector`) SelectorText :`SelectorSingleQuotedText` / `SelectorDoubleQuotedText` SelectorSingleQuotedText :"'" 1*`SelectorSingleQuotedChar` "'" SelectorDoubleQuotedText :DQUOTE 1*`SelectorDoubleQuotedChar` DQUOTE diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/MetadataContainer.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/MetadataContainer.java index acb33a3fa44..c1b4f13b208 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/MetadataContainer.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/MetadataContainer.java @@ -46,6 +46,7 @@ final class MetadataContainer { * * @param key Metadata key to set. * @param value Value to set. + * @param events Where to add events as issues are encountered. */ void putMetadata(String key, Node value, List events) { Node previous = data.putIfAbsent(key, value); 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 ecc9095e285..e159f104306 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 @@ -574,7 +574,7 @@ string title ] ) @length(min: 1) -@deprecated(messag: "The enum trait is replaced by the enum shape in Smithy 2.0", since: "2.0") +@deprecated(message: "The enum trait is replaced by the enum shape in Smithy 2.0", since: "2.0") list enum { member: EnumDefinition }