Skip to content

Commit

Permalink
Allow IDL serialization order configuration
Browse files Browse the repository at this point in the history
This commit adds the ability to order the output of the IDL
serializer so that shapes/traits/metadata can be written out in the
order they were originally defined. You can also use alphabetical
ordering or a "preferred" order (the default, and previous behavior).

This change will allow us to potentially use the IDL serializer to
more faithfully serialize transformed Smithy models.
  • Loading branch information
mtdowling committed Apr 8, 2023
1 parent 37170e3 commit 81a155b
Show file tree
Hide file tree
Showing 5 changed files with 254 additions and 59 deletions.
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>Shapes are ordered as follows:
*
* <ul>
* <li>Trait definitions</li>
* <li>Services</li>
* <li>Resources</li>
* <li>Operations</li>
* <li>Structures</li>
* <li>Unions</li>
* <li>Lists</li>
* <li>Maps</li>
* <li>Finally, alphabetically by shape type.</li>
* </ul>
*/
PREFERRED;

Comparator<Shape> shapeComparator() {
return this == PREFERRED ? new PreferredShapeComparator() : toShapeIdComparator();
}

<T extends FromSourceLocation & ToShapeId> Comparator<T> toShapeIdComparator() {
switch (this) {
case PREFERRED:
case ALPHA_NUMERIC:
return Comparator.comparing(ToShapeId::toShapeId);
case SOURCE_LOCATION:
default:
return new SourceComparator<>();
}
}

Comparator<Map.Entry<String, Node>> metadataComparator() {
switch (this) {
case ALPHA_NUMERIC:
case PREFERRED:
return Map.Entry.comparingByKey();
case SOURCE_LOCATION:
default:
return new MetadataComparator();
}
}

private static final class SourceComparator<T extends FromSourceLocation & ToShapeId>
implements Comparator<T>, 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<Map.Entry<String, Node>>, Serializable {
@Override
public int compare(Map.Entry<String, Node> a, Map.Entry<String, Node> 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<Shape>, Serializable {
private static final Map<ShapeType, Integer> 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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand All @@ -69,6 +66,7 @@ public final class SmithyIdlModelSerializer {
private final Predicate<Trait> traitFilter;
private final Function<Shape, Path> shapePlacer;
private final Path basePath;
private final SmithyIdlComponentOrder componentOrder;

/**
* Trait serialization features.
Expand Down Expand Up @@ -114,6 +112,7 @@ private SmithyIdlModelSerializer(Builder builder) {
} else {
this.shapePlacer = builder.shapePlacer;
}
this.componentOrder = builder.componentOrder;
}

/**
Expand Down Expand Up @@ -162,11 +161,12 @@ private String serialize(Model fullModel, Collection<Shape> shapes) {

Set<ShapeId> inlineableShapes = getInlineableShapes(fullModel, shapes);
ShapeSerializer shapeSerializer = new ShapeSerializer(
codeWriter, nodeSerializer, traitFilter, fullModel, inlineableShapes);
codeWriter, nodeSerializer, traitFilter, fullModel, inlineableShapes, componentOrder);
Comparator<Shape> 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();
Expand Down Expand Up @@ -203,11 +203,13 @@ private String serializeHeader(Model fullModel, String namespace) {

codeWriter.write("$$version: \"$L\"", Model.MODEL_VERSION).write("");

Comparator<Map.Entry<String, Node>> 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())
Expand Down Expand Up @@ -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<Shape>, Serializable {
private static final Map<ShapeType, Integer> 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}.
*/
Expand All @@ -305,6 +259,7 @@ public static final class Builder implements SmithyBuilder<SmithyIdlModelSeriali
private Function<Shape, Path> shapePlacer = SmithyIdlModelSerializer::placeShapesByNamespace;
private Path basePath = null;
private boolean serializePrelude = false;
private SmithyIdlComponentOrder componentOrder = SmithyIdlComponentOrder.PREFERRED;

public Builder() {}

Expand Down Expand Up @@ -381,6 +336,20 @@ public Builder serializePrelude() {
return this;
}

/**
* Defines how components are sorted in the model, changing the default behavior of sorting alphabetically.
*
* <p>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);
Expand All @@ -396,19 +365,22 @@ private static final class ShapeSerializer extends ShapeVisitor.Default<Void> {
private final Predicate<Trait> traitFilter;
private final Model model;
private final Set<ShapeId> inlineableShapes;
private final SmithyIdlComponentOrder componentOrder;

ShapeSerializer(
SmithyCodeWriter codeWriter,
NodeSerializer nodeSerializer,
Predicate<Trait> traitFilter,
Model model,
Set<ShapeId> inlineableShapes
Set<ShapeId> inlineableShapes,
SmithyIdlComponentOrder componentOrder
) {
this.codeWriter = codeWriter;
this.nodeSerializer = nodeSerializer;
this.traitFilter = traitFilter;
this.model = model;
this.inlineableShapes = inlineableShapes;
this.componentOrder = componentOrder;
}

@Override
Expand Down Expand Up @@ -535,6 +507,8 @@ private void serializeTraits(Map<ShapeId, Trait> traits, TraitFeature... traitFe
}
}

Comparator<Trait> traitComparator = componentOrder.toShapeIdComparator();

traits.values().stream()
.filter(trait -> noSpecialDocsSyntax || !(trait instanceof DocumentationTrait))
// The default and enumValue traits are serialized using the assignment syntactic sugar.
Expand All @@ -547,7 +521,7 @@ private void serializeTraits(Map<ShapeId, Trait> traits, TraitFeature... traitFe
}
})
.filter(traitFilter)
.sorted(Comparator.comparing(Trait::toShapeId))
.sorted(traitComparator)
.forEach(this::serializeTrait);
}

Expand All @@ -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.
Expand Down
Loading

0 comments on commit 81a155b

Please sign in to comment.