Skip to content

Commit 9e6361d

Browse files
authored
Add custom IDL serialization ordering support
A new `SmithyIdlSerializationOrder` interface extracts the comparator methods from `SmithyIdlComponentOrder` into a public contract that the enum now implements. Users can provide custom implementations to control shape, trait, and metadata ordering during IDL serialization.
1 parent 524ebb7 commit 9e6361d

5 files changed

Lines changed: 219 additions & 15 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "feature",
3+
"description": "Add support for custom comparators for sorting IDL serialization output",
4+
"pull_requests": [
5+
"[#3058](https://github.com/smithy-lang/smithy/pull/3058)"
6+
]
7+
}

smithy-model/src/main/java/software/amazon/smithy/model/shapes/SmithyIdlComponentOrder.java

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,18 @@
1010
import software.amazon.smithy.model.FromSourceLocation;
1111
import software.amazon.smithy.model.SourceLocation;
1212
import software.amazon.smithy.model.node.Node;
13+
import software.amazon.smithy.model.traits.Trait;
1314
import software.amazon.smithy.model.traits.TraitDefinition;
1415
import software.amazon.smithy.utils.MapUtils;
1516

1617
/**
1718
* Defines how shapes, traits, and metadata are sorted when serializing a model with {@link SmithyIdlModelSerializer}.
19+
*
20+
* <p>This enum provides the built-in orderings. For custom ordering logic, implement
21+
* {@link SmithyIdlSerializationOrder} directly and pass it to
22+
* {@link SmithyIdlModelSerializer.Builder#componentOrder(SmithyIdlSerializationOrder)}.
1823
*/
19-
public enum SmithyIdlComponentOrder {
24+
public enum SmithyIdlComponentOrder implements SmithyIdlSerializationOrder {
2025
/**
2126
* Sort shapes, traits, and metadata alphabetically. Member order, however, is not sorted.
2227
*/
@@ -46,29 +51,36 @@ public enum SmithyIdlComponentOrder {
4651
*/
4752
PREFERRED;
4853

49-
Comparator<Shape> shapeComparator() {
54+
@Override
55+
public Comparator<Shape> shapeComparator() {
5056
return this == PREFERRED ? new PreferredShapeComparator() : toShapeIdComparator();
5157
}
5258

53-
<T extends FromSourceLocation & ToShapeId> Comparator<T> toShapeIdComparator() {
59+
@Override
60+
public Comparator<Trait> traitComparator() {
61+
return toShapeIdComparator();
62+
}
63+
64+
@Override
65+
public Comparator<Map.Entry<String, Node>> metadataComparator() {
5466
switch (this) {
55-
case PREFERRED:
5667
case ALPHA_NUMERIC:
57-
return Comparator.comparing(ToShapeId::toShapeId);
68+
case PREFERRED:
69+
return Map.Entry.comparingByKey();
5870
case SOURCE_LOCATION:
5971
default:
60-
return new SourceComparator<>();
72+
return new MetadataComparator();
6173
}
6274
}
6375

64-
Comparator<Map.Entry<String, Node>> metadataComparator() {
76+
private <T extends FromSourceLocation & ToShapeId> Comparator<T> toShapeIdComparator() {
6577
switch (this) {
66-
case ALPHA_NUMERIC:
6778
case PREFERRED:
68-
return Map.Entry.comparingByKey();
79+
case ALPHA_NUMERIC:
80+
return Comparator.comparing(ToShapeId::toShapeId);
6981
case SOURCE_LOCATION:
7082
default:
71-
return new MetadataComparator();
83+
return new SourceComparator<>();
7284
}
7385
}
7486

smithy-model/src/main/java/software/amazon/smithy/model/shapes/SmithyIdlModelSerializer.java

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ public final class SmithyIdlModelSerializer {
6060
private final Predicate<Trait> traitFilter;
6161
private final Function<Shape, Path> shapePlacer;
6262
private final Path basePath;
63-
private final SmithyIdlComponentOrder componentOrder;
63+
private final SmithyIdlSerializationOrder componentOrder;
6464
private final String inlineInputSuffix;
6565
private final String inlineOutputSuffix;
6666
private final boolean inferInlineIoSuffixes;
@@ -382,7 +382,7 @@ public static final class Builder implements SmithyBuilder<SmithyIdlModelSeriali
382382
private Function<Shape, Path> shapePlacer = SmithyIdlModelSerializer::placeShapesByNamespace;
383383
private Path basePath = null;
384384
private boolean serializePrelude = false;
385-
private SmithyIdlComponentOrder componentOrder = SmithyIdlComponentOrder.PREFERRED;
385+
private SmithyIdlSerializationOrder componentOrder = SmithyIdlComponentOrder.PREFERRED;
386386
private String inlineInputSuffix = DEFAULT_INLINE_INPUT_SUFFIX;
387387
private String inlineOutputSuffix = DEFAULT_INLINE_OUTPUT_SUFFIX;
388388
private boolean inferInlineIoSuffixes = false;
@@ -477,6 +477,21 @@ public Builder componentOrder(SmithyIdlComponentOrder componentOrder) {
477477
return this;
478478
}
479479

480+
/**
481+
* Defines how components are sorted in the model using a custom ordering configuration.
482+
*
483+
* <p>Implement {@link SmithyIdlSerializationOrder} to provide custom comparators for shapes,
484+
* traits, and metadata. The built-in orderings are available as constants on
485+
* {@link SmithyIdlComponentOrder}.
486+
*
487+
* @param componentOrder Custom component ordering configuration.
488+
* @return Returns the builder.
489+
*/
490+
public Builder componentOrder(SmithyIdlSerializationOrder componentOrder) {
491+
this.componentOrder = Objects.requireNonNull(componentOrder);
492+
return this;
493+
}
494+
480495
/**
481496
* Defines what suffixes are checked on operation input shapes to determine whether
482497
* inline syntax should be used.
@@ -556,15 +571,15 @@ private static final class ShapeSerializer extends ShapeVisitor.Default<Void> {
556571
private final Predicate<Trait> traitFilter;
557572
private final Model model;
558573
private final Set<ShapeId> inlineableShapes;
559-
private final SmithyIdlComponentOrder componentOrder;
574+
private final SmithyIdlSerializationOrder componentOrder;
560575

561576
ShapeSerializer(
562577
SmithyCodeWriter codeWriter,
563578
NodeSerializer nodeSerializer,
564579
Predicate<Trait> traitFilter,
565580
Model model,
566581
Set<ShapeId> inlineableShapes,
567-
SmithyIdlComponentOrder componentOrder
582+
SmithyIdlSerializationOrder componentOrder
568583
) {
569584
this.codeWriter = codeWriter;
570585
this.nodeSerializer = nodeSerializer;
@@ -703,7 +718,7 @@ private void serializeTraits(Map<ShapeId, Trait> traits, TraitFeature... traitFe
703718
}
704719
}
705720

706-
Comparator<Trait> traitComparator = componentOrder.toShapeIdComparator();
721+
Comparator<Trait> traitComparator = componentOrder.traitComparator();
707722

708723
traits.values()
709724
.stream()
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package software.amazon.smithy.model.shapes;
6+
7+
import java.util.Comparator;
8+
import java.util.Map;
9+
import software.amazon.smithy.model.node.Node;
10+
import software.amazon.smithy.model.traits.Trait;
11+
12+
/**
13+
* Configures how shapes, traits, and metadata are ordered when serializing a model
14+
* with {@link SmithyIdlModelSerializer}.
15+
*
16+
* <p>The built-in orderings are available as constants on {@link SmithyIdlComponentOrder}.
17+
* Implement this interface directly to provide custom ordering logic.
18+
*
19+
* <p>Only {@link #shapeComparator()} is required. The default implementations of
20+
* {@link #traitComparator()} and {@link #metadataComparator()} sort alphabetically,
21+
* which is suitable for most use cases.
22+
*
23+
* @see SmithyIdlComponentOrder
24+
* @see SmithyIdlModelSerializer.Builder#componentOrder(SmithyIdlSerializationOrder)
25+
*/
26+
public interface SmithyIdlSerializationOrder {
27+
28+
/**
29+
* A shape comparator that sorts alphabetically by shape ID.
30+
*/
31+
Comparator<Shape> ALPHABETICAL = Comparator.comparing(Shape::toShapeId);
32+
33+
/**
34+
* Returns a comparator used to sort shapes within a namespace file.
35+
*
36+
* @return Comparator for ordering shapes.
37+
*/
38+
Comparator<Shape> shapeComparator();
39+
40+
/**
41+
* Returns a comparator used to sort traits applied to a shape.
42+
*
43+
* <p>The default implementation sorts traits alphabetically by their shape ID. Custom
44+
* implementations that use source-location-based ordering should consider overriding
45+
* this method to also sort traits by source location for consistency.
46+
*
47+
* @return Comparator for ordering traits.
48+
*/
49+
default Comparator<Trait> traitComparator() {
50+
return Comparator.comparing(Trait::toShapeId);
51+
}
52+
53+
/**
54+
* Returns a comparator used to sort metadata entries.
55+
*
56+
* <p>The default implementation sorts metadata alphabetically by key.
57+
*
58+
* @return Comparator for ordering metadata entries.
59+
*/
60+
default Comparator<Map.Entry<String, Node>> metadataComparator() {
61+
return Map.Entry.comparingByKey();
62+
}
63+
}

smithy-model/src/test/java/software/amazon/smithy/model/shapes/SmithyIdlModelSerializerTest.java

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.nio.file.Files;
2323
import java.nio.file.Path;
2424
import java.nio.file.Paths;
25+
import java.util.Comparator;
2526
import java.util.Map;
2627
import java.util.Objects;
2728
import java.util.stream.Stream;
@@ -37,6 +38,8 @@
3738
import software.amazon.smithy.model.traits.DocumentationTrait;
3839
import software.amazon.smithy.model.traits.DynamicTrait;
3940
import software.amazon.smithy.model.traits.RequiredTrait;
41+
import software.amazon.smithy.model.traits.SensitiveTrait;
42+
import software.amazon.smithy.model.traits.Trait;
4043
import software.amazon.smithy.model.traits.synthetic.OriginalShapeIdTrait;
4144
import software.amazon.smithy.utils.IoUtils;
4245
import software.amazon.smithy.utils.MapUtils;
@@ -392,4 +395,108 @@ public void unresolvableIdRefSerializedAsQuotedString() {
392395
Model model2 = Model.assembler().addUnparsedModel("test.smithy", modelResult).assemble().unwrap();
393396
assertThat(model2, equalTo(model));
394397
}
398+
399+
@Test
400+
public void usesCustomComponentOrderConfig() {
401+
// Build a simple model with multiple shapes to verify custom ordering.
402+
Model model = Model.builder()
403+
.addShape(StringShape.builder().id("com.example#Zebra").build())
404+
.addShape(StringShape.builder().id("com.example#Alpha").build())
405+
.addShape(StructureShape.builder().id("com.example#Middle").build())
406+
.build();
407+
408+
// Custom config that reverses the default alphabetical shape order.
409+
SmithyIdlSerializationOrder reverseOrder = new SmithyIdlSerializationOrder() {
410+
@Override
411+
public Comparator<Shape> shapeComparator() {
412+
return Comparator.comparing(Shape::toShapeId).reversed();
413+
}
414+
};
415+
416+
Map<Path, String> serialized = SmithyIdlModelSerializer.builder()
417+
.componentOrder(reverseOrder)
418+
.build()
419+
.serialize(model);
420+
String result = serialized.values().iterator().next();
421+
422+
// Verify shapes appear in reverse alphabetical order: Zebra, Middle, Alpha.
423+
int zebraPos = result.indexOf("Zebra");
424+
int middlePos = result.indexOf("Middle");
425+
int alphaPos = result.indexOf("Alpha");
426+
assertTrue(zebraPos < middlePos, "Zebra should appear before Middle in reverse order");
427+
assertTrue(middlePos < alphaPos, "Middle should appear before Alpha in reverse order");
428+
}
429+
430+
@Test
431+
public void usesCustomTraitComparator() {
432+
// Use two annotation traits (not documentation) whose relative order is observable.
433+
Model model = Model.builder()
434+
.addShape(StructureShape.builder()
435+
.id("com.example#MyStruct")
436+
.addTrait(new RequiredTrait())
437+
.addTrait(new SensitiveTrait())
438+
.build())
439+
.build();
440+
441+
// Reversed trait comparator: sensitive (s) should appear before required (r) alphabetically,
442+
// but reversed means required comes first.
443+
SmithyIdlSerializationOrder customOrder = new SmithyIdlSerializationOrder() {
444+
@Override
445+
public Comparator<Shape> shapeComparator() {
446+
return SmithyIdlSerializationOrder.ALPHABETICAL;
447+
}
448+
449+
@Override
450+
public Comparator<Trait> traitComparator() {
451+
return Comparator.comparing(Trait::toShapeId).reversed();
452+
}
453+
};
454+
455+
Map<Path, String> serialized = SmithyIdlModelSerializer.builder()
456+
.componentOrder(customOrder)
457+
.build()
458+
.serialize(model);
459+
String result = serialized.get(Paths.get("com.example.smithy"));
460+
461+
// In reversed order, sensitive (s) comes before required (r).
462+
int sensitivePos = result.indexOf("@sensitive");
463+
int requiredPos = result.indexOf("@required");
464+
assertTrue(sensitivePos < requiredPos,
465+
"@sensitive should appear before @required in reversed trait order");
466+
}
467+
468+
@Test
469+
public void usesCustomMetadataComparator() {
470+
// Use two metadata keys whose relative order is observable.
471+
Model model = Model.builder()
472+
.addShape(StringShape.builder().id("com.example#Placeholder").build())
473+
.putMetadataProperty("zeta", Node.from("z"))
474+
.putMetadataProperty("alpha", Node.from("a"))
475+
.build();
476+
477+
// Reversed metadata comparator: alpha (a) should appear before zeta (z) alphabetically,
478+
// but reversed means zeta comes first.
479+
SmithyIdlSerializationOrder customOrder = new SmithyIdlSerializationOrder() {
480+
@Override
481+
public Comparator<Shape> shapeComparator() {
482+
return SmithyIdlSerializationOrder.ALPHABETICAL;
483+
}
484+
485+
@Override
486+
public Comparator<Map.Entry<String, Node>> metadataComparator() {
487+
return Map.Entry.<String, Node>comparingByKey().reversed();
488+
}
489+
};
490+
491+
Map<Path, String> serialized = SmithyIdlModelSerializer.builder()
492+
.componentOrder(customOrder)
493+
.build()
494+
.serialize(model);
495+
String result = serialized.get(Paths.get("metadata.smithy"));
496+
497+
// In reversed order, zeta (z) comes before alpha (a).
498+
int zetaPos = result.indexOf("zeta");
499+
int alphaPos = result.indexOf("alpha");
500+
assertTrue(zetaPos < alphaPos, "zeta should appear before alpha in reversed metadata order");
501+
}
395502
}

0 commit comments

Comments
 (0)