diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/SmithyIdlComponentOrder.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/SmithyIdlComponentOrder.java new file mode 100644 index 00000000000..a250abf5dde --- /dev/null +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/SmithyIdlComponentOrder.java @@ -0,0 +1,152 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.model.shapes; + +import java.io.Serializable; +import java.util.Comparator; +import java.util.Map; +import software.amazon.smithy.model.FromSourceLocation; +import software.amazon.smithy.model.SourceLocation; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.traits.TraitDefinition; +import software.amazon.smithy.utils.MapUtils; + +/** + * Defines how shapes, traits, and metadata are sorted when serializing a model with {@link SmithyIdlModelSerializer}. + */ +public enum SmithyIdlComponentOrder { + /** + * Sort shapes, traits, and metadata alphabetically. Member order, however, is not sorted. + */ + ALPHA_NUMERIC, + + /** + * Sort shapes, traits, and metadata by source location, persisting their original placement when parsed. + */ + SOURCE_LOCATION, + + /** + * Reorganizes shapes based on a preferred ordering of shapes, and alphanumeric traits and metadata. + * + *

Shapes are ordered as follows: + * + *

+ */ + PREFERRED; + + Comparator shapeComparator() { + return this == PREFERRED ? new PreferredShapeComparator() : toShapeIdComparator(); + } + + Comparator toShapeIdComparator() { + switch (this) { + case PREFERRED: + case ALPHA_NUMERIC: + return Comparator.comparing(ToShapeId::toShapeId); + case SOURCE_LOCATION: + default: + return new SourceComparator<>(); + } + } + + Comparator> metadataComparator() { + switch (this) { + case ALPHA_NUMERIC: + case PREFERRED: + return Map.Entry.comparingByKey(); + case SOURCE_LOCATION: + default: + return new MetadataComparator(); + } + } + + private static final class SourceComparator + implements Comparator, Serializable { + @Override + public int compare(T a, T b) { + SourceLocation left = a.getSourceLocation(); + SourceLocation right = b.getSourceLocation(); + int comparison = left.compareTo(right); + return comparison != 0 ? comparison : a.toShapeId().compareTo(b.toShapeId()); + } + } + + private static final class MetadataComparator implements Comparator>, Serializable { + @Override + public int compare(Map.Entry a, Map.Entry b) { + SourceLocation left = a.getValue().getSourceLocation(); + SourceLocation right = b.getValue().getSourceLocation(); + int comparison = left.compareTo(right); + return comparison != 0 ? comparison : a.getKey().compareTo(b.getKey()); + } + } + + /** + * Comparator used to sort shapes. + */ + private static final class PreferredShapeComparator implements Comparator, Serializable { + private static final Map PRIORITY = MapUtils.of( + ShapeType.SERVICE, 0, + ShapeType.RESOURCE, 1, + ShapeType.OPERATION, 2, + ShapeType.STRUCTURE, 3, + ShapeType.UNION, 4, + ShapeType.LIST, 5, + ShapeType.SET, 6, + ShapeType.MAP, 7); + + @Override + public int compare(Shape s1, Shape s2) { + // Traits go first + if (s1.hasTrait(TraitDefinition.class) || s2.hasTrait(TraitDefinition.class)) { + if (!s1.hasTrait(TraitDefinition.class)) { + return 1; + } + if (!s2.hasTrait(TraitDefinition.class)) { + return -1; + } + // The other sorting rules don't matter for traits. + return s1.compareTo(s2); + } + // If the shapes are the same type, just compare their shape ids. + if (s1.getType().equals(s2.getType())) { + return s1.compareTo(s2); + } + // If one shape is prioritized, compare by priority. + if (PRIORITY.containsKey(s1.getType()) || PRIORITY.containsKey(s2.getType())) { + // If only one shape is prioritized, that shape is "greater". + if (!PRIORITY.containsKey(s1.getType())) { + return 1; + } + if (!PRIORITY.containsKey(s2.getType())) { + return -1; + } + return PRIORITY.get(s1.getType()) - PRIORITY.get(s2.getType()); + } + return s1.compareTo(s2); + } + } +} diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/SmithyIdlModelSerializer.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/SmithyIdlModelSerializer.java index 91ba72c96b7..8404fb9de52 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/SmithyIdlModelSerializer.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/SmithyIdlModelSerializer.java @@ -15,7 +15,6 @@ package software.amazon.smithy.model.shapes; -import java.io.Serializable; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; @@ -50,13 +49,11 @@ 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.TraitDefinition; import software.amazon.smithy.model.traits.UnitTypeTrait; import software.amazon.smithy.utils.AbstractCodeWriter; import software.amazon.smithy.utils.CodeWriter; import software.amazon.smithy.utils.FunctionalUtils; import software.amazon.smithy.utils.ListUtils; -import software.amazon.smithy.utils.MapUtils; import software.amazon.smithy.utils.SmithyBuilder; import software.amazon.smithy.utils.StringUtils; @@ -69,6 +66,7 @@ public final class SmithyIdlModelSerializer { private final Predicate traitFilter; private final Function shapePlacer; private final Path basePath; + private final SmithyIdlComponentOrder componentOrder; /** * Trait serialization features. @@ -114,6 +112,7 @@ private SmithyIdlModelSerializer(Builder builder) { } else { this.shapePlacer = builder.shapePlacer; } + this.componentOrder = builder.componentOrder; } /** @@ -162,11 +161,12 @@ private String serialize(Model fullModel, Collection shapes) { Set inlineableShapes = getInlineableShapes(fullModel, shapes); ShapeSerializer shapeSerializer = new ShapeSerializer( - codeWriter, nodeSerializer, traitFilter, fullModel, inlineableShapes); + codeWriter, nodeSerializer, traitFilter, fullModel, inlineableShapes, componentOrder); + Comparator comparator = componentOrder.shapeComparator(); shapes.stream() .filter(FunctionalUtils.not(Shape::isMemberShape)) .filter(shape -> !inlineableShapes.contains(shape.getId())) - .sorted(new ShapeComparator()) + .sorted(comparator) .forEach(shape -> shape.accept(shapeSerializer)); return serializeHeader(fullModel, namespace) + codeWriter.toString(); @@ -203,11 +203,13 @@ private String serializeHeader(Model fullModel, String namespace) { codeWriter.write("$$version: \"$L\"", Model.MODEL_VERSION).write(""); + Comparator> comparator = componentOrder.metadataComparator(); + // Write the full metadata into every output. When loaded back together the conflicts will be ignored, // but if they're separated out then each file will still have all the context. fullModel.getMetadata().entrySet().stream() .filter(entry -> metadataFilter.test(entry.getKey())) - .sorted(Map.Entry.comparingByKey()) + .sorted(comparator) .forEach(entry -> { codeWriter.trimTrailingSpaces(false) .writeInline("metadata $K = ", entry.getKey()) @@ -247,54 +249,6 @@ public static Path placeShapesByNamespace(Shape shape) { return Paths.get(shape.getId().getNamespace() + ".smithy"); } - /** - * Comparator used to sort shapes. - */ - private static final class ShapeComparator implements Comparator, Serializable { - private static final Map PRIORITY = MapUtils.of( - ShapeType.SERVICE, 0, - ShapeType.RESOURCE, 1, - ShapeType.OPERATION, 2, - ShapeType.STRUCTURE, 3, - ShapeType.UNION, 4, - ShapeType.LIST, 5, - ShapeType.SET, 6, - ShapeType.MAP, 7 - ); - - - @Override - public int compare(Shape s1, Shape s2) { - // Traits go first - if (s1.hasTrait(TraitDefinition.class) || s2.hasTrait(TraitDefinition.class)) { - if (!s1.hasTrait(TraitDefinition.class)) { - return 1; - } - if (!s2.hasTrait(TraitDefinition.class)) { - return -1; - } - // The other sorting rules don't matter for traits. - return s1.compareTo(s2); - } - // If the shapes are the same type, just compare their shape ids. - if (s1.getType().equals(s2.getType())) { - return s1.compareTo(s2); - } - // If one shape is prioritized, compare by priority. - if (PRIORITY.containsKey(s1.getType()) || PRIORITY.containsKey(s2.getType())) { - // If only one shape is prioritized, that shape is "greater". - if (!PRIORITY.containsKey(s1.getType())) { - return 1; - } - if (!PRIORITY.containsKey(s2.getType())) { - return -1; - } - return PRIORITY.get(s1.getType()) - PRIORITY.get(s2.getType()); - } - return s1.compareTo(s2); - } - } - /** * Builder used to create {@link SmithyIdlModelSerializer}. */ @@ -305,6 +259,7 @@ public static final class Builder implements SmithyBuilder shapePlacer = SmithyIdlModelSerializer::placeShapesByNamespace; private Path basePath = null; private boolean serializePrelude = false; + private SmithyIdlComponentOrder componentOrder = SmithyIdlComponentOrder.PREFERRED; public Builder() {} @@ -381,6 +336,20 @@ public Builder serializePrelude() { return this; } + /** + * Defines how components are sorted in the model, changing the default behavior of sorting alphabetically. + * + *

You can serialize metadata, shapes, and traits in the original order they were defined by setting + * this to {@link SmithyIdlComponentOrder#SOURCE_LOCATION}. + * + * @param componentOrder Change how components are sorted. + * @return Returns the builder. + */ + public Builder componentOrder(SmithyIdlComponentOrder componentOrder) { + this.componentOrder = Objects.requireNonNull(componentOrder); + return this; + } + @Override public SmithyIdlModelSerializer build() { return new SmithyIdlModelSerializer(this); @@ -396,19 +365,22 @@ private static final class ShapeSerializer extends ShapeVisitor.Default { private final Predicate traitFilter; private final Model model; private final Set inlineableShapes; + private final SmithyIdlComponentOrder componentOrder; ShapeSerializer( SmithyCodeWriter codeWriter, NodeSerializer nodeSerializer, Predicate traitFilter, Model model, - Set inlineableShapes + Set inlineableShapes, + SmithyIdlComponentOrder componentOrder ) { this.codeWriter = codeWriter; this.nodeSerializer = nodeSerializer; this.traitFilter = traitFilter; this.model = model; this.inlineableShapes = inlineableShapes; + this.componentOrder = componentOrder; } @Override @@ -535,6 +507,8 @@ private void serializeTraits(Map traits, TraitFeature... traitFe } } + Comparator traitComparator = componentOrder.toShapeIdComparator(); + traits.values().stream() .filter(trait -> noSpecialDocsSyntax || !(trait instanceof DocumentationTrait)) // The default and enumValue traits are serialized using the assignment syntactic sugar. @@ -547,7 +521,7 @@ private void serializeTraits(Map traits, TraitFeature... traitFe } }) .filter(traitFilter) - .sorted(Comparator.comparing(Trait::toShapeId)) + .sorted(traitComparator) .forEach(this::serializeTrait); } @@ -560,9 +534,10 @@ private void serializeDocumentation(String documentation) { private void serializeTrait(Trait trait) { Node node = trait.toNode(); - Shape shape = model.expectShape(trait.toShapeId()); + // We won't fail if the trait can't be found. + Shape shape = model.getShape(trait.toShapeId()).orElse(null); - if (trait instanceof AnnotationTrait || isEmptyStructure(node, shape)) { + if (shape != null && (trait instanceof AnnotationTrait || isEmptyStructure(node, shape))) { // Traits that inherit from AnnotationTrait specifically can omit a value. // Traits that are simply boolean shapes which don't implement AnnotationTrait cannot. // Additionally, empty structure traits can omit a value. diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/shapes/SmithyIdlModelSerializerTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/shapes/SmithyIdlModelSerializerTest.java index 32ec26bb34f..8986502963a 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/shapes/SmithyIdlModelSerializerTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/shapes/SmithyIdlModelSerializerTest.java @@ -6,12 +6,12 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasKey; -import static org.hamcrest.Matchers.hasProperty; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import java.io.IOException; import java.net.URISyntaxException; +import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -217,4 +217,30 @@ public void serializesRootLevelDefaults() { is(true)); assertThat(model2, equalTo(model2)); } + + @Test + public void usesOriginalSourceLocation() { + URL resource = getClass().getResource("idl-serialization/out-of-order.smithy"); + Model model = Model.assembler().addImport(resource).assemble().unwrap(); + Map reserialized = SmithyIdlModelSerializer.builder() + .componentOrder(SmithyIdlComponentOrder.SOURCE_LOCATION) + .build() + .serialize(model); + String modelResult = reserialized.values().iterator().next(); + + assertThat(modelResult, equalTo(IoUtils.readUtf8Url(resource))); + } + + @Test + public void sortsAlphabetically() { + URL resource = getClass().getResource("idl-serialization/alphabetical.smithy"); + Model model = Model.assembler().addImport(resource).assemble().unwrap(); + Map reserialized = SmithyIdlModelSerializer.builder() + .componentOrder(SmithyIdlComponentOrder.ALPHA_NUMERIC) + .build() + .serialize(model); + String modelResult = reserialized.values().iterator().next(); + + assertThat(modelResult, equalTo(IoUtils.readUtf8Url(resource))); + } } diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/alphabetical.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/alphabetical.smithy new file mode 100644 index 00000000000..bbf96fd5abd --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/alphabetical.smithy @@ -0,0 +1,21 @@ +$version: "2.0" + +metadata foo = "hi" +metadata zoo = "test" + +namespace com.example + +structure Abc { + bar: String + @length( + min: 1 + ) + @required + baz: String +} + +string Def + +service Hij { + version: "2006-03-01" +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/out-of-order.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/out-of-order.smithy new file mode 100644 index 00000000000..3947bc3f2d3 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/out-of-order.smithy @@ -0,0 +1,21 @@ +$version: "2.0" + +metadata zoo = "test" +metadata foo = "hi" + +namespace com.example + +string MyString + +structure Hello { + bar: String + @required + @length( + min: 1 + ) + baz: MyString +} + +service Foo { + version: "2006-03-01" +}