From 543857c269ee78779635a2eec7e414d2de694cdb Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Fri, 21 Feb 2020 12:45:38 -0800 Subject: [PATCH 01/16] Add writeInline method to CodeWriter This adds a method to CodeWriter which allow for writing without having a trailing newline or leading indentation inserted. This can be very useful when you want to write part of a line without knowing what the whole line will contain. This does preserve some safety in that it will intercept newlines to treat them as normal, but it is still potentially dangerous if not used carefully. --- .../amazon/smithy/utils/CodeWriter.java | 82 ++++++++++++++++--- .../amazon/smithy/utils/CodeWriterTest.java | 42 ++++++++++ 2 files changed, 111 insertions(+), 13 deletions(-) diff --git a/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeWriter.java b/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeWriter.java index 33e82d85744..de69c967de0 100644 --- a/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeWriter.java +++ b/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeWriter.java @@ -17,6 +17,7 @@ import java.util.ArrayDeque; import java.util.ArrayList; +import java.util.Arrays; import java.util.Deque; import java.util.HashMap; import java.util.List; @@ -944,6 +945,43 @@ public final CodeWriter write(Object content, Object... args) { return this; } + /** + * Writes text to the CodeWriter without appending a newline or prefixing indentation. + * + *

If newlines are present in the given string, each of those lines will receive proper indentation. + * + * @param content Content to write. + * @param args String arguments to use for formatting. + * @return Returns the CodeWriter. + */ + public final CodeWriter writeInline(Object content, Object... args) { + String value = formatter.format(content, currentState.indentText, this, args); + ArrayList lines = new ArrayList<>(Arrays.asList(value.split(newlineRegexQuoted, -1))); + + // The first line is written directly, with no added indentation or newline + currentState.write(lines.remove(0)); + + // If there aren't any additional lines, return. + if (lines.isEmpty()) { + return this; + } + + // If there are additional lines, they need to be handled properly. So insert a newline. + currentState.write(newline); + + // We don't want to append a newline, so remove the last line for handling later. + String lastLine = lines.remove(lines.size() - 1); + + // Write all the intermediate lines as normal. + for (String line : lines) { + currentState.writeLine(line + newline); + } + + // Write the final line with proper indentation, but without an appended newline. + currentState.writeLine(lastLine); + return this; + } + /** * Optionally writes text to the CodeWriter and appends a newline * if a value is present. @@ -1115,6 +1153,14 @@ void putInterceptor(String section, Consumer interceptor) { interceptors.computeIfAbsent(section, s -> new ArrayList<>()).add(interceptor); } + void write(String contents) { + if (builder == null) { + builder = new StringBuilder(); + } + builder.append(contents); + trimSpaces(false); + } + void writeLine(String line) { if (builder == null) { builder = new StringBuilder(); @@ -1125,21 +1171,31 @@ void writeLine(String line) { builder.append(line); // Trim all trailing spaces before the trailing (customizable) newline. - if (trimTrailingSpaces) { - int newlineLength = newline.length(); - int toRemove = 0; - for (int i = builder.length() - 1 - newlineLength; i > 0; i--) { - if (builder.charAt(i) == ' ') { - toRemove++; - } else { - break; - } - } - // Remove the slice of the string that is made up of whitespace before the newline. - if (toRemove > 0) { - builder.delete(builder.length() - newlineLength - toRemove, builder.length() - newlineLength); + trimSpaces(true); + } + + private void trimSpaces(boolean skipNewline) { + if (!trimTrailingSpaces) { + return; + } + + int skipLength = 0; + if (skipNewline) { + skipLength = newline.length(); + } + + int toRemove = 0; + for (int i = builder.length() - 1 - skipLength; i > 0; i--) { + if (builder.charAt(i) == ' ') { + toRemove++; + } else { + break; } } + // Remove the slice of the string that is made up of whitespace before the newline. + if (toRemove > 0) { + builder.delete(builder.length() - skipLength - toRemove, builder.length() - skipLength); + } } private void indent(int levels, String indentText) { diff --git a/smithy-utils/src/test/java/software/amazon/smithy/utils/CodeWriterTest.java b/smithy-utils/src/test/java/software/amazon/smithy/utils/CodeWriterTest.java index 04539ff989b..d93f07b6dea 100644 --- a/smithy-utils/src/test/java/software/amazon/smithy/utils/CodeWriterTest.java +++ b/smithy-utils/src/test/java/software/amazon/smithy/utils/CodeWriterTest.java @@ -505,4 +505,46 @@ public void poppedSectionsEscapeDollars() { assertThat(result, equalTo("$Hello\n")); } + + @Test + public void canWriteInline() { + String result = CodeWriter.createDefault() + .writeInline("foo") + .writeInline(", bar") + .toString(); + + assertThat(result, equalTo("foo, bar")); + } + + @Test + public void writeInlineHandlesSingleNewline() { + String result = CodeWriter.createDefault() + .writeInline("foo").indent() + .writeInline(":\nbar") + .toString(); + + assertThat(result, equalTo("foo:\n bar")); + } + + @Test + public void writeInlineHandlesMultipleNewlines() { + String result = CodeWriter.createDefault() + .writeInline("foo:") + .writeInline(" [").indent() + .writeInline("\nbar,\nbaz,\nbam,") + .dedent().writeInline("\n]") + .toString(); + + assertThat(result, equalTo("foo: [\n bar,\n baz,\n bam,\n]")); + } + + @Test + public void writeInlineStripsSpaces() { + String result = CodeWriter.createDefault() + .trimTrailingSpaces() + .writeInline("foo ") + .toString(); + + assertThat(result, equalTo("foo")); + } } From c0b8ce5ceccfbe17c6a10c7e028f1e48d2282846 Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Fri, 21 Feb 2020 12:46:01 -0800 Subject: [PATCH 02/16] Support Smithy IDL serialization This adds support for serializing a Smithy model into the IDL format. --- .../shapes/SmithyIdlModelSerializer.java | 723 ++++++++++++++++++ .../shapes/SmithyIdlModelSerializerTest.java | 54 ++ .../cases/boolean-traits.smithy | 11 + .../cases/collection-traits.smithy | 27 + .../cases/document-traits.smithy | 28 + .../cases/documentation-trait.smithy | 7 + .../idl-serialization/cases/metadata.smithy | 20 + .../cases/number-traits.smithy | 37 + .../cases/object-traits.smithy | 66 ++ .../optional-namespaces-are-stripped.smithy | 11 + .../cases/service-shapes.smithy | 75 ++ .../cases/simple-shapes.smithy | 52 ++ .../cases/string-traits.smithy | 9 + .../multiple-namespaces/input.json | 28 + .../output/ns.primitives.smithy | 11 + .../output/ns.structures.smithy | 12 + 16 files changed, 1171 insertions(+) create mode 100644 smithy-model/src/main/java/software/amazon/smithy/model/shapes/SmithyIdlModelSerializer.java create mode 100644 smithy-model/src/test/java/software/amazon/smithy/model/shapes/SmithyIdlModelSerializerTest.java create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/boolean-traits.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/collection-traits.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/document-traits.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/documentation-trait.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/metadata.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/number-traits.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/object-traits.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/optional-namespaces-are-stripped.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/service-shapes.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/simple-shapes.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/string-traits.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/multiple-namespaces/input.json create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/multiple-namespaces/output/ns.primitives.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/multiple-namespaces/output/ns.structures.smithy 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 new file mode 100644 index 00000000000..2342bdee077 --- /dev/null +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/SmithyIdlModelSerializer.java @@ -0,0 +1,723 @@ +/* + * 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.shapes; + +import java.io.Serializable; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.loader.Prelude; +import software.amazon.smithy.model.node.ArrayNode; +import software.amazon.smithy.model.node.BooleanNode; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.NumberNode; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.node.StringNode; +import software.amazon.smithy.model.traits.BooleanTrait; +import software.amazon.smithy.model.traits.DocumentationTrait; +import software.amazon.smithy.model.traits.IdRefTrait; +import software.amazon.smithy.model.traits.Trait; +import software.amazon.smithy.model.traits.TraitDefinition; +import software.amazon.smithy.utils.CodeWriter; +import software.amazon.smithy.utils.FunctionalUtils; +import software.amazon.smithy.utils.ListUtils; +import software.amazon.smithy.utils.SmithyBuilder; +import software.amazon.smithy.utils.StringUtils; + +/** + * Serializes a {@link Model} into a set of Smithy IDL files. + */ +public final class SmithyIdlModelSerializer { + private final Function shapePlacer; + + private SmithyIdlModelSerializer(Builder builder) { + shapePlacer = builder.shapePlacer; + } + + /** + * Serializes a {@link Model} into a set of Smithy IDL files. + * + *

This does not write the models to disk. + * @param model The model to serialize. + * @return A map of (possibly relative) file paths to Smithy IDL strings. + */ + public Map serialize(Model model) { + return model.shapes() + .filter(FunctionalUtils.not(Prelude::isPreludeShape)) + .collect(Collectors.groupingBy(shapePlacer)).entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, entry -> serialize(model, entry.getValue()))); + } + + private String serialize(Model fullModel, Collection shapes) { + Set namespaces = shapes.stream() + .map(shape -> shape.getId().getNamespace()) + .collect(Collectors.toSet()); + if (namespaces.size() != 1) { + throw new RuntimeException("All shapes in a single file must share a namespace."); + } + + // There should only be one namespace at this point, so grab it. + String namespace = namespaces.iterator().next(); + SmithyCodeWriter codeWriter = new SmithyCodeWriter(namespace, fullModel); + NodeSerializer nodeSerializer = new NodeSerializer(codeWriter, fullModel); + + ShapeSerializer shapeSerializer = new ShapeSerializer(codeWriter, nodeSerializer, fullModel); + shapes.stream() + .filter(FunctionalUtils.not(Shape::isMemberShape)) + .sorted(new ShapeComparator()) + .forEach(shape -> shape.accept(shapeSerializer)); + + return serializeHeader(namespace, fullModel) + codeWriter.toString(); + } + + private String serializeHeader(String namespace, Model fullModel) { + SmithyCodeWriter codeWriter = new SmithyCodeWriter(namespace, fullModel); + NodeSerializer nodeSerializer = new NodeSerializer(codeWriter, fullModel); + + codeWriter.write("$$version: \"$L\"", Model.MODEL_VERSION).write(""); + + // 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() + .sorted(Map.Entry.comparingByKey(String.CASE_INSENSITIVE_ORDER)) + .forEach(entry -> { + codeWriter.trimTrailingSpaces(false) + .writeInline("metadata $M = ", entry.getKey()) + .trimTrailingSpaces(); + nodeSerializer.serialize(entry.getValue()); + codeWriter.write(""); + }); + if (!fullModel.getMetadata().isEmpty()) { + codeWriter.write(""); + } + + codeWriter.write("namespace " + namespace) + .trimBlankLines(-1) + .write(""); + return codeWriter.toString(); + } + + /** + * @return Returns a builder used to create a {@link SmithyIdlModelSerializer} + */ + public static Builder builder() { + return new Builder(); + } + + public static Path placeShapesByNamespace(Shape shape) { + return Paths.get(shape.getId().getNamespace() + ".smithy"); + } + + /** + * Comparator used to sort shapes. + */ + public static final class ShapeComparator implements Comparator, Serializable { + private static final List PRIORITY = ListUtils.of( + ShapeType.SERVICE, + ShapeType.RESOURCE, + ShapeType.OPERATION, + ShapeType.UNION, + ShapeType.STRUCTURE, + ShapeType.LIST, + ShapeType.SET, + ShapeType.MAP + ); + + @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 compareCaseInsensitive(s1, s2); + } + // If the shapes are the same type, just compare their shape ids. + if (s1.getType().equals(s2.getType())) { + return compareCaseInsensitive(s1, s2); + } + // If one shape is prioritized, compare by priority. + if (PRIORITY.contains(s1.getType()) || PRIORITY.contains(s2.getType())) { + // If only one shape is prioritized, that shape is "greater". + if (!PRIORITY.contains(s1.getType())) { + return 1; + } + if (!PRIORITY.contains(s2.getType())) { + return -1; + } + return PRIORITY.indexOf(s1.getType()) - PRIORITY.indexOf(s2.getType()); + } + return compareCaseInsensitive(s1, s2); + } + + /** + * Compare two shapes by id case-insensitively. + */ + private int compareCaseInsensitive(Shape s1, Shape s2) { + return s1.toShapeId().toString().toLowerCase().compareTo(s2.toShapeId().toString().toLowerCase()); + } + } + + /** + * Builder used to create {@link SmithyIdlModelSerializer}. + */ + public static final class Builder implements SmithyBuilder { + private Function shapePlacer = SmithyIdlModelSerializer::placeShapesByNamespace; + + public Builder() {} + + /** + * Function that determines what output file a shape should go in. + * + *

NOTE: the Smithy IDL only supports one namespace per file. + * @param shapePlacer Function that accepts a shape and returns file path. + * @return Returns the builder. + */ + Builder shapePlacer(Function shapePlacer) { + this.shapePlacer = Objects.requireNonNull(shapePlacer); + return this; + } + + @Override + public SmithyIdlModelSerializer build() { + return new SmithyIdlModelSerializer(this); + } + } + + /** + * Serializes shapes in the IDL format. + */ + private static final class ShapeSerializer extends ShapeVisitor.Default { + private final SmithyCodeWriter codeWriter; + private final NodeSerializer nodeSerializer; + private final Model model; + + ShapeSerializer(SmithyCodeWriter codeWriter, NodeSerializer nodeSerializer, Model model) { + this.codeWriter = codeWriter; + this.nodeSerializer = nodeSerializer; + this.model = model; + } + + @Override + protected Void getDefault(Shape shape) { + serializeTraits(shape); + codeWriter.write("$L $L", shape.getType(), shape.getId().getName()).write(""); + return null; + } + + private void shapeWithMembers(Shape shape, List members) { + serializeTraits(shape); + if (members.isEmpty()) { + // If there are no members then we don't want to introduce an unnecessary newline by opening a block. + codeWriter.write("$L $L {}", shape.getType(), shape.getId().getName()).write(""); + return; + } + + codeWriter.openBlock("$L $L {", shape.getType(), shape.getId().getName()); + for (MemberShape member : members) { + serializeTraits(member); + codeWriter.write("$L: $I,", member.getMemberName(), member.getTarget()); + } + codeWriter.closeBlock("}").write(""); + } + + private void serializeTraits(Shape shape) { + // The documentation trait always needs to be serialized first since it uses special syntax. + shape.getTrait(DocumentationTrait.class).ifPresent(this::serializeDocumentationTrait); + shape.getAllTraits().values().stream() + .filter(trait -> !(trait instanceof DocumentationTrait)) + .sorted(Comparator.comparing(trait -> trait.toShapeId().toString(), String.CASE_INSENSITIVE_ORDER)) + .forEach(this::serializeTrait); + } + + private void serializeDocumentationTrait(DocumentationTrait trait) { + // The documentation trait has a special syntax, which we always want to use. + codeWriter.setNewlinePrefix("/// ") + .write(trait.getValue().replace("$", "$$")) + .setNewlinePrefix(""); + } + + private void serializeTrait(Trait trait) { + Node node = trait.toNode(); + Shape shape = model.expectShape(trait.toShapeId()); + + if (trait instanceof BooleanTrait || isEmptyStructure(node, shape)) { + // Traits that inherit from BooleanTrait specifically can omit a value. + // Traits that are simply boolean shapes which don't implement BooleanTrait cannot. + // Additionally, empty structure traits can omit a value. + codeWriter.write("@$I", trait.toShapeId()); + } else if (node.isObjectNode()) { + codeWriter.writeIndent().openBlockInline("@$I(", trait.toShapeId()); + nodeSerializer.serializeKeyValuePairs(node.expectObjectNode(), shape); + codeWriter.closeBlock(")"); + } else { + codeWriter.writeIndent().writeInline("@$I(", trait.toShapeId()); + nodeSerializer.serialize(node, shape); + codeWriter.write(")"); + } + } + + private boolean isEmptyStructure(Node node, Shape shape) { + return !shape.isDocumentShape() && node.asObjectNode().map(ObjectNode::isEmpty).orElse(false); + } + + @Override + public Void listShape(ListShape shape) { + shapeWithMembers(shape, Collections.singletonList(shape.getMember())); + return null; + } + + @Override + public Void setShape(SetShape shape) { + shapeWithMembers(shape, Collections.singletonList(shape.getMember())); + return null; + } + + @Override + public Void mapShape(MapShape shape) { + shapeWithMembers(shape, ListUtils.of(shape.getKey(), shape.getValue())); + return null; + } + + @Override + public Void structureShape(StructureShape shape) { + shapeWithMembers(shape, sortMembers(shape.getAllMembers().values())); + return null; + } + + @Override + public Void unionShape(UnionShape shape) { + shapeWithMembers(shape, sortMembers(shape.getAllMembers().values())); + return null; + } + + private List sortMembers(Collection members) { + return members.stream() + .sorted(Comparator.comparing(MemberShape::getMemberName, String.CASE_INSENSITIVE_ORDER)) + .collect(Collectors.toList()); + } + + @Override + public Void serviceShape(ServiceShape shape) { + serializeTraits(shape); + codeWriter.openBlock("service $L {", shape.getId().getName()) + .write("version: $S,", shape.getVersion()); + codeWriter.writeOptionalIdList("operations", shape.getOperations()); + codeWriter.writeOptionalIdList("resources", shape.getResources()); + codeWriter.closeBlock("}").write(""); + return null; + } + + @Override + public Void resourceShape(ResourceShape shape) { + serializeTraits(shape); + if (isEmptyResource(shape)) { + codeWriter.write("resource $L {}", shape.getId().getName()).write(""); + return null; + } + + codeWriter.openBlock("resource $L {", shape.getId().getName()); + if (!shape.getIdentifiers().isEmpty()) { + codeWriter.openBlock("identifiers: {"); + shape.getIdentifiers().entrySet().stream() + .sorted(Map.Entry.comparingByKey(String.CASE_INSENSITIVE_ORDER)) + .forEach(entry -> codeWriter.write( + "$L: $I,", entry.getKey(), entry.getValue())); + codeWriter.closeBlock("},"); + } + + shape.getPut().ifPresent(shapeId -> codeWriter.write("put: $I,", shapeId)); + shape.getCreate().ifPresent(shapeId -> codeWriter.write("create: $I,", shapeId)); + shape.getRead().ifPresent(shapeId -> codeWriter.write("read: $I,", shapeId)); + shape.getUpdate().ifPresent(shapeId -> codeWriter.write("update: $I,", shapeId)); + shape.getDelete().ifPresent(shapeId -> codeWriter.write("delete: $I,", shapeId)); + shape.getList().ifPresent(shapeId -> codeWriter.write("list: $I,", shapeId)); + codeWriter.writeOptionalIdList("operations", shape.getOperations()); + codeWriter.writeOptionalIdList("collectionOperations", shape.getCollectionOperations()); + codeWriter.writeOptionalIdList("resources", shape.getResources()); + + codeWriter.closeBlock("}"); + codeWriter.write(""); + return null; + } + + private boolean isEmptyResource(ResourceShape shape) { + return !(!shape.getIdentifiers().isEmpty() + || !shape.getOperations().isEmpty() + || !shape.getCollectionOperations().isEmpty() + || !shape.getResources().isEmpty() + || shape.getPut().isPresent() + || shape.getCreate().isPresent() + || shape.getRead().isPresent() + || shape.getUpdate().isPresent() + || shape.getDelete().isPresent() + || shape.getList().isPresent()); + } + + @Override + public Void operationShape(OperationShape shape) { + serializeTraits(shape); + if (isEmptyOperation(shape)) { + codeWriter.write("operation $L {}", shape.getId().getName()).write(""); + return null; + } + + codeWriter.openBlock("operation $L {", shape.getId().getName()); + shape.getInput().ifPresent(shapeId -> codeWriter.write("input: $I,", shapeId)); + shape.getOutput().ifPresent(shapeId -> codeWriter.write("output: $I,", shapeId)); + codeWriter.writeOptionalIdList("errors", shape.getErrors()); + codeWriter.closeBlock("}"); + codeWriter.write(""); + return null; + } + + private boolean isEmptyOperation(OperationShape shape) { + return !(shape.getInput().isPresent() || shape.getOutput().isPresent() || !shape.getErrors().isEmpty()); + } + } + + /** + * Serializes nodes into the Smithy IDL format. + */ + private static final class NodeSerializer { + private final SmithyCodeWriter codeWriter; + private final Model model; + + NodeSerializer(SmithyCodeWriter codeWriter, Model model) { + this.codeWriter = codeWriter; + this.model = model; + } + + /** + * Serialize a node into the Smithy IDL format. + * + * @param node The node to serialize. + */ + public void serialize(Node node) { + serialize(node, null); + } + + /** + * Serialize a node into the Smithy IDL format. + * + *

This uses the given shape to influence serialization. For example, a string shape marked with the idRef + * trait will be serialized as a shape id rather than a string. + * + * @param node The node to serialize. + * @param shape The shape of the node. + */ + public void serialize(Node node, Shape shape) { + // ShapeIds are represented differently than strings, so if a shape looks like it's + // representing a shapeId we need to serialize it without quotes. + if (isShapeId(shape)) { + serializeShapeId(node.expectStringNode()); + return; + } + + if (shape != null && shape.isMemberShape()) { + shape = model.expectShape(shape.asMemberShape().get().getTarget()); + } + + if (node.isStringNode()) { + serializeString(node.expectStringNode()); + } else if (node.isNumberNode()) { + serializeNumber(node.expectNumberNode()); + } else if (node.isBooleanNode()) { + serializeBoolean(node.expectBooleanNode()); + } else if (node.isNullNode()) { + serializeNull(); + } else if (node.isArrayNode()) { + serializeArray(node.expectArrayNode(), shape); + } else if (node.isObjectNode()) { + serializeObject(node.expectObjectNode(), shape); + } + } + + private boolean isShapeId(Shape shape) { + if (shape == null) { + return false; + } + if (shape.isMemberShape()) { + return shape.asMemberShape() + .flatMap(member -> member.getMemberTrait(model, IdRefTrait.class)).isPresent(); + } + return shape.hasTrait(IdRefTrait.class); + } + + private void serializeString(StringNode node) { + codeWriter.writeInline("$S", node.getValue()); + } + + private void serializeShapeId(StringNode node) { + codeWriter.writeInline("$I", node.getValue()); + } + + private void serializeNumber(NumberNode node) { + codeWriter.writeInline("$L", node.getValue()); + } + + private void serializeBoolean(BooleanNode node) { + codeWriter.writeInline(String.valueOf(node.getValue())); + } + + private void serializeNull() { + codeWriter.writeInline("null"); + } + + private void serializeArray(ArrayNode node, Shape shape) { + if (node.isEmpty()) { + codeWriter.writeInline("[]"); + return; + } + + codeWriter.openBlockInline("["); + + // If it's not a collection shape, it'll be a document shape or null + Shape member = shape; + if (shape instanceof CollectionShape) { + member = ((CollectionShape) shape).getMember(); + } + + for (Node element : node.getElements()) { + // Elements will be written inline to enable them being written as values. + // So here we need to ensure that they're written on a new line that's properly indented. + codeWriter.write(""); + codeWriter.writeIndent(); + serialize(element, member); + codeWriter.writeInline(","); + } + codeWriter.write(""); + + // We want to make sure to close without inserting a newline, as this could itself be a list element + //or an object value. + codeWriter.closeBlockWithoutNewline("]"); + } + + private void serializeObject(ObjectNode node, Shape shape) { + if (node.isEmpty()) { + codeWriter.writeInline("{}"); + return; + } + + codeWriter.openBlockInline("{"); + serializeKeyValuePairs(node, shape); + codeWriter.closeBlockWithoutNewline("}"); + } + + /** + * Serialize an object node without the surrounding brackets. + * + *

This is mainly useful for serializing trait value nodes. + * + * @param node The node to serialize. + * @param shape The shape of the node. + */ + public void serializeKeyValuePairs(ObjectNode node, Shape shape) { + if (node.isEmpty()) { + return; + } + + // If we're looking at a structure or union shape, we'll need to get the member shape based on the + // node key. Here we pre-compute a mapping so we don't have to traverse the member list every time. + Map members; + if (shape == null) { + members = Collections.emptyMap(); + } else { + members = shape.members().stream().distinct() + .collect(Collectors.toMap(MemberShape::getMemberName, Function.identity())); + } + + node.getMembers().forEach((name, value) -> { + // Try to find the member shape. + Shape member; + if (shape != null && shape.isMapShape()) { + // For maps the value member will always be the same. + member = shape.asMapShape().get().getValue(); + } else if (shape instanceof NamedMembersShape) { + member = members.get(name.getValue()); + } else { + // At this point the shape is either null or a document shape. + member = shape; + } + + codeWriter.writeInline("\n$M: ", name.getValue()); + serialize(value, member); + codeWriter.writeInline(","); + }); + codeWriter.write(""); + } + } + + /** + * Extension of {@link CodeWriter} that provides additional convenience methods. + * + *

Provides a built in $I formatter that formats shape ids, automatically trimming namespace where possible. + */ + private static final class SmithyCodeWriter extends CodeWriter { + private static final Pattern UNQUOTED_STRING = Pattern.compile("[a-zA-Z_][\\w$.#]*"); + private final String namespace; + private final Model model; + private final Set imports; + + SmithyCodeWriter(String namespace, Model model) { + super(); + this.namespace = namespace; + this.model = model; + this.imports = new HashSet<>(); + trimTrailingSpaces(); + trimBlankLines(); + putFormatter('I', (s, i) -> formatShapeId(s)); + putFormatter('M', this::optionallyQuoteString); + } + + /** + * Opens a block without writing indentation whitespace or inserting a newline. + */ + public SmithyCodeWriter openBlockInline(String content, Object... args) { + writeInline(content, args).indent(); + return this; + } + + /** + * Closes a block without inserting a newline. + */ + public SmithyCodeWriter closeBlockWithoutNewline(String content, Object... args) { + setNewline(""); + closeBlock(content, args); + setNewline("\n"); + return this; + } + + /** + * Writes an empty line that contains only indentation appropriate to the current indentation level. + * + *

This does not insert a trailing newline. + */ + public SmithyCodeWriter writeIndent() { + setNewline(""); + // We explicitly want the trailing spaces, so disable trimming for this call. + trimTrailingSpaces(false); + write(""); + trimTrailingSpaces(); + setNewline("\n"); + return this; + } + + private String formatShapeId(Object value) { + if (value == null) { + return ""; + } + ShapeId shapeId = ShapeId.from(String.valueOf(value)); + if (!shouldWriteNamespace(shapeId)) { + return shapeId.asRelativeReference(); + } + return shapeId.toString(); + } + + private boolean shouldWriteNamespace(ShapeId shapeId) { + if (shapeId.getNamespace().equals(namespace)) { + return false; + } + if (Prelude.isPublicPreludeShape(shapeId)) { + return conflictsWithLocalNamespace(shapeId); + } + if (shouldImport(shapeId)) { + imports.add(shapeId.withoutMember()); + } + return !imports.contains(shapeId); + } + + private boolean conflictsWithLocalNamespace(ShapeId shapeId) { + return model.getShape(ShapeId.fromParts(namespace, shapeId.getName())).isPresent(); + } + + private boolean shouldImport(ShapeId shapeId) { + return !conflictsWithLocalNamespace(shapeId) + // It's easier to simply never import something that conflicts with the prelude, because + // if we did then we'd have to somehow handle rewriting all existing references to the + // prelude shape that it conflicts with. + && !conflictsWithPreludeNamespace(shapeId) + && !conflictsWithImports(shapeId); + + } + + private boolean conflictsWithPreludeNamespace(ShapeId shapeId) { + return Prelude.isPublicPreludeShape(ShapeId.fromParts(Prelude.NAMESPACE, shapeId.getName())); + } + + private boolean conflictsWithImports(ShapeId shapeId) { + return imports.stream().anyMatch(importId -> importId.getName().equals(shapeId.getName())); + } + + /** + * Writes a possibly-empty list where each element is a shape id. + * + *

If the list is empty, nothing is written. + */ + public SmithyCodeWriter writeOptionalIdList(String textBeforeList, Collection shapeIds) { + if (shapeIds.isEmpty()) { + return this; + } + + openBlock("$L: [", textBeforeList); + shapeIds.stream() + .sorted(Comparator.comparing(ShapeId::toString, String.CASE_INSENSITIVE_ORDER)) + .forEach(shapeId -> write("$I,", shapeId)); + closeBlock("],"); + + return this; + } + + /** + * Formatter that quotes (and escapes) a string unless it's a valid unquoted string. + */ + private String optionallyQuoteString(Object key, String indent) { + String formatted = CodeWriter.formatLiteral(key); + if (UNQUOTED_STRING.matcher(formatted).matches()) { + return formatted; + } + return StringUtils.escapeJavaString(formatted, indent); + } + + @Override + public String toString() { + String contents = StringUtils.stripStart(super.toString(), null); + if (imports.isEmpty()) { + return contents; + } + String importString = imports.stream() + .sorted(Comparator.comparing(ShapeId::toString, String.CASE_INSENSITIVE_ORDER)) + .map(shapeId -> String.format("use %s", shapeId.toString())) + .collect(Collectors.joining("\n")); + return importString + "\n\n" + contents; + } + } +} 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 new file mode 100644 index 00000000000..18faf28195c --- /dev/null +++ b/smithy-model/src/test/java/software/amazon/smithy/model/shapes/SmithyIdlModelSerializerTest.java @@ -0,0 +1,54 @@ +package software.amazon.smithy.model.shapes; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Map; +import java.util.stream.Stream; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestFactory; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.utils.IoUtils; + +public class SmithyIdlModelSerializerTest { + @TestFactory + public Stream generateTests() throws IOException { + return Files.list(Paths.get( + SmithyIdlModelSerializer.class.getResource("idl-serialization/cases").getPath())) + .map(path -> DynamicTest.dynamicTest(path.getFileName().toString(), () -> testConversion(path))); + } + + public void testConversion(Path path) { + Model model = Model.assembler().addImport(path).assemble().unwrap(); + SmithyIdlModelSerializer serializer = SmithyIdlModelSerializer.builder().build(); + Map serialized = serializer.serialize(model); + + if (serialized.size() != 1) { + throw new RuntimeException("Exactly one smithy file should be output for generated tests."); + } + + String serializedString = serialized.entrySet().iterator().next().getValue(); + assertThat(serializedString, equalTo(IoUtils.readUtf8File(path))); + } + + @Test + public void multipleNamespacesGenerateMultipleFiles() { + Model model = Model.assembler() + .addImport(getClass().getResource("idl-serialization/multiple-namespaces/input.json")) + .assemble() + .unwrap(); + SmithyIdlModelSerializer serializer = SmithyIdlModelSerializer.builder().build(); + Map serialized = serializer.serialize(model); + + Path outputDir = Paths.get(getClass().getResource("idl-serialization/multiple-namespaces/output").getFile()); + serialized.forEach((path, generated) -> { + Path expectedPath = outputDir.resolve(path); + assertThat(generated, equalTo(IoUtils.readUtf8File(expectedPath))); + }); + } +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/boolean-traits.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/boolean-traits.smithy new file mode 100644 index 00000000000..c26ba1d30e2 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/boolean-traits.smithy @@ -0,0 +1,11 @@ +$version: "0.5.0" + +namespace ns.foo + +/// This trait isn't an annotation trait since it doesn't extend BooleanTrait +@trait +boolean FalseBooleanTrait + +@FalseBooleanTrait(true) +@private +string Foo diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/collection-traits.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/collection-traits.smithy new file mode 100644 index 00000000000..ea700339470 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/collection-traits.smithy @@ -0,0 +1,27 @@ +$version: "0.5.0" + +namespace ns.foo + +@trait +list ListTrait { + member: String, +} + +@trait +set SetTrait { + member: String, +} + +@ListTrait([]) +@SetTrait([]) +string Bar + +@ListTrait([ + "first", + "second", +]) +@SetTrait([ + "first", + "second", +]) +string Foo diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/document-traits.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/document-traits.smithy new file mode 100644 index 00000000000..95833a75d17 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/document-traits.smithy @@ -0,0 +1,28 @@ +$version: "0.5.0" + +namespace ns.foo + +@trait +document DocumentTrait + +@DocumentTrait(false) +string Boolean + +@DocumentTrait([ + "foo", +]) +string List + +@DocumentTrait( + foo: "bar", +) +string Map + +@DocumentTrait(null) +string Null + +@DocumentTrait(123) +string Number + +@DocumentTrait("foo") +string String diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/documentation-trait.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/documentation-trait.smithy new file mode 100644 index 00000000000..e67fad99147 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/documentation-trait.smithy @@ -0,0 +1,7 @@ +$version: "0.5.0" + +namespace ns.foo + +/// Documentation comments are used. +/// $ is escaped +string Foo diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/metadata.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/metadata.smithy new file mode 100644 index 00000000000..69eac0008bf --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/metadata.smithy @@ -0,0 +1,20 @@ +$version: "0.5.0" + +metadata example.array = [ + 10, + true, + "hello", +] +metadata example.bool1 = true +metadata example.bool2 = false +metadata example.null = null +metadata example.number = 10 +metadata example.object = { + foo: "baz", +} +metadata example.string = "hello there" +metadata "key must be quoted" = true + +namespace ns.foo + +string Placeholder diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/number-traits.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/number-traits.smithy new file mode 100644 index 00000000000..4cfeffdc5fb --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/number-traits.smithy @@ -0,0 +1,37 @@ +$version: "0.5.0" + +namespace ns.foo + +@trait +bigDecimal BigDecimalTrait + +@trait +bigInteger BigIntegerTrait + +@trait +byte ByteTrait + +@trait +double DoubleTrait + +@trait +float FloatTrait + +@trait +integer IntegerTrait + +@trait +long LongTrait + +@trait +short ShortTrait + +@BigDecimalTrait(3.14) +@BigIntegerTrait(123) +@ByteTrait(123) +@DoubleTrait(1.234) +@FloatTrait(-1.234) +@IntegerTrait(-123) +@LongTrait(321) +@ShortTrait(12321) +string Foo diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/object-traits.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/object-traits.smithy new file mode 100644 index 00000000000..cd569a22264 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/object-traits.smithy @@ -0,0 +1,66 @@ +$version: "0.5.0" + +namespace ns.foo + +@trait +map MapTrait { + key: String, + value: String, +} + +@trait +structure StructureTrait { + collectionMember: StringList, + nestedMember: NestedMember, + shapeIdMember: ShapeId, + staticMember: String, +} + +@trait +union UnionTrait { + boolean: Boolean, + string: String, +} + +structure NestedMember { + shapeIds: ShapeIdList, +} + +list ShapeIdList { + member: ShapeId, +} + +list StringList { + member: String, +} + +@MapTrait +@StructureTrait +string EmptyBody + +@MapTrait( + bar: "baz", + foo: "bar", + "must be quoted": "bam", +) +@StructureTrait( + collectionMember: [ + "foo", + "bar", + ], + nestedMember: { + shapeIds: [ + String, + EmptyBody, + ], + }, + shapeIdMember: UnionTrait, + staticMember: "Foo", +) +@UnionTrait( + boolean: false, +) +string NonEmptyBody + +@idRef +string ShapeId diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/optional-namespaces-are-stripped.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/optional-namespaces-are-stripped.smithy new file mode 100644 index 00000000000..d3b79f49cd7 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/optional-namespaces-are-stripped.smithy @@ -0,0 +1,11 @@ +$version: "0.5.0" + +namespace ns.foo + +structure Structure { + localWithConflict: String, + prelude: Blob, + preludeWithConflict: smithy.api#String, +} + +string String diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/service-shapes.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/service-shapes.smithy new file mode 100644 index 00000000000..a7f8602b376 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/service-shapes.smithy @@ -0,0 +1,75 @@ +$version: "0.5.0" + +namespace ns.foo + +service EmptyService { + version: "2020-02-18", +} + +service MyService { + version: "2020-02-18", + operations: [ + MyOperation, + ], + resources: [ + MyResource, + ], +} + +resource EmptyResource {} + +resource MyResource { + identifiers: { + id: String, + }, + put: ResourceOperation, + create: ResourceOperation, + read: ReadonlyResourceOperation, + update: ResourceOperation, + delete: ResourceOperation, + list: ReadonlyResourceOperation, + operations: [ + ResourceOperation, + ], + collectionOperations: [ + ResourceOperation, + ], + resources: [ + SubResource, + ], +} + +resource SubResource { + identifiers: { + id: String, + }, +} + +operation EmptyOperation {} + +operation MyOperation { + input: InputOutput, + output: InputOutput, + errors: [ + Error, + ], +} + +@readonly +operation ReadonlyResourceOperation { + input: ResourceOperationInput, +} + +@idempotent +operation ResourceOperation { + input: ResourceOperationInput, +} + +@error("client") +structure Error {} + +structure InputOutput {} + +structure ResourceOperationInput { + id: String, +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/simple-shapes.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/simple-shapes.smithy new file mode 100644 index 00000000000..af07b8b6279 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/simple-shapes.smithy @@ -0,0 +1,52 @@ +$version: "0.5.0" + +namespace ns.foo + +union Union { + byte: Byte, + double: Double, +} + +structure StructureWithMembers { + a: String, + b: String, +} + +structure StructureWithoutMembers {} + +list List { + member: String, +} + +set Set { + member: String, +} + +map Map { + key: String, + value: String, +} + +bigDecimal BigDecimal + +bigInteger BigInteger + +blob Blob + +boolean Boolean + +byte Byte + +document Document + +double Double + +float Float + +integer Integer + +long Long + +short Short + +string String diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/string-traits.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/string-traits.smithy new file mode 100644 index 00000000000..8945274376c --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/string-traits.smithy @@ -0,0 +1,9 @@ +$version: "0.5.0" + +namespace ns.foo + +@trait +string StringTrait + +@StringTrait("foo") +string Foo diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/multiple-namespaces/input.json b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/multiple-namespaces/input.json new file mode 100644 index 00000000000..46987a88fc3 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/multiple-namespaces/input.json @@ -0,0 +1,28 @@ +{ + "smithy": "0.5.0", + "metadata": { + "shared": true + }, + "shapes": { + "ns.structures#Structure": { + "type": "structure", + "members": { + "listMember": { + "target": "ns.primitives#StringList" + }, + "stringMember": { + "target": "ns.primitives#String" + } + } + }, + "ns.primitives#String": { + "type": "string" + }, + "ns.primitives#StringList": { + "type": "list", + "member": { + "target": "ns.primitives#String" + } + } + } +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/multiple-namespaces/output/ns.primitives.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/multiple-namespaces/output/ns.primitives.smithy new file mode 100644 index 00000000000..9d20979e21c --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/multiple-namespaces/output/ns.primitives.smithy @@ -0,0 +1,11 @@ +$version: "0.5.0" + +metadata shared = true + +namespace ns.primitives + +list StringList { + member: String, +} + +string String diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/multiple-namespaces/output/ns.structures.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/multiple-namespaces/output/ns.structures.smithy new file mode 100644 index 00000000000..e05139f9488 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/multiple-namespaces/output/ns.structures.smithy @@ -0,0 +1,12 @@ +$version: "0.5.0" + +metadata shared = true + +namespace ns.structures + +use ns.primitives#StringList + +structure Structure { + listMember: StringList, + stringMember: ns.primitives#String, +} From e8097964862335d775b6628fef9c4b7fbdbec746 Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Mon, 24 Feb 2020 10:14:43 -0800 Subject: [PATCH 03/16] Add filtering predicates to idl serializer --- .../shapes/SmithyIdlModelSerializer.java | 63 +++++++++++++++-- .../shapes/SmithyIdlModelSerializerTest.java | 68 +++++++++++++++++++ .../shapes/idl-serialization/test-model.json | 44 ++++++++++++ 3 files changed, 171 insertions(+), 4 deletions(-) create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/test-model.json 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 2342bdee077..3ea2783ab3f 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 @@ -27,6 +27,7 @@ import java.util.Objects; import java.util.Set; import java.util.function.Function; +import java.util.function.Predicate; import java.util.regex.Pattern; import java.util.stream.Collectors; import software.amazon.smithy.model.Model; @@ -52,9 +53,15 @@ * Serializes a {@link Model} into a set of Smithy IDL files. */ public final class SmithyIdlModelSerializer { + private final Predicate metadataFilter; + private final Predicate shapeFilter; + private final Predicate traitFilter; private final Function shapePlacer; private SmithyIdlModelSerializer(Builder builder) { + metadataFilter = builder.metadataFilter; + shapeFilter = builder.shapeFilter.and(FunctionalUtils.not(Prelude::isPreludeShape)); + traitFilter = builder.traitFilter; shapePlacer = builder.shapePlacer; } @@ -67,7 +74,8 @@ private SmithyIdlModelSerializer(Builder builder) { */ public Map serialize(Model model) { return model.shapes() - .filter(FunctionalUtils.not(Prelude::isPreludeShape)) + .filter(FunctionalUtils.not(Shape::isMemberShape)) + .filter(shapeFilter) .collect(Collectors.groupingBy(shapePlacer)).entrySet().stream() .collect(Collectors.toMap(Map.Entry::getKey, entry -> serialize(model, entry.getValue()))); } @@ -85,7 +93,7 @@ private String serialize(Model fullModel, Collection shapes) { SmithyCodeWriter codeWriter = new SmithyCodeWriter(namespace, fullModel); NodeSerializer nodeSerializer = new NodeSerializer(codeWriter, fullModel); - ShapeSerializer shapeSerializer = new ShapeSerializer(codeWriter, nodeSerializer, fullModel); + ShapeSerializer shapeSerializer = new ShapeSerializer(codeWriter, nodeSerializer, traitFilter, fullModel); shapes.stream() .filter(FunctionalUtils.not(Shape::isMemberShape)) .sorted(new ShapeComparator()) @@ -104,6 +112,7 @@ private String serializeHeader(String namespace, Model fullModel) { // but if they're separated out then each file will still have all the context. fullModel.getMetadata().entrySet().stream() .sorted(Map.Entry.comparingByKey(String.CASE_INSENSITIVE_ORDER)) + .filter(entry -> metadataFilter.test(entry.getKey())) .forEach(entry -> { codeWriter.trimTrailingSpaces(false) .writeInline("metadata $M = ", entry.getKey()) @@ -190,10 +199,48 @@ private int compareCaseInsensitive(Shape s1, Shape s2) { * Builder used to create {@link SmithyIdlModelSerializer}. */ public static final class Builder implements SmithyBuilder { + private Predicate metadataFilter = pair -> true; + private Predicate shapeFilter = shape -> true; + private Predicate traitFilter = trait -> true; private Function shapePlacer = SmithyIdlModelSerializer::placeShapesByNamespace; public Builder() {} + /** + * Predicate that determines if a metadata is serialized. + * @param metadataFilter Predicate that accepts a metadata key. + * @return Returns the builder. + */ + public Builder metadataFilter(Predicate metadataFilter) { + this.metadataFilter = Objects.requireNonNull(metadataFilter); + return this; + } + + /** + * Predicate that determines if a shape and its traits are serialized. + * @param shapeFilter Predicate that accepts a shape. + * @return Returns the builder. + */ + public Builder shapeFilter(Predicate shapeFilter) { + this.shapeFilter = Objects.requireNonNull(shapeFilter); + return this; + } + + /** + * Sets a predicate that can be used to filter trait values from + * appearing in the serialized model. + * + *

Note that this does not filter out trait definitions. It only filters + * out instances of traits from being serialized on shapes. + * + * @param traitFilter Predicate that filters out trait definitions. + * @return Returns the builder. + */ + public Builder traitFilter(Predicate traitFilter) { + this.traitFilter = traitFilter; + return this; + } + /** * Function that determines what output file a shape should go in. * @@ -218,11 +265,18 @@ public SmithyIdlModelSerializer build() { private static final class ShapeSerializer extends ShapeVisitor.Default { private final SmithyCodeWriter codeWriter; private final NodeSerializer nodeSerializer; + private final Predicate traitFilter; private final Model model; - ShapeSerializer(SmithyCodeWriter codeWriter, NodeSerializer nodeSerializer, Model model) { + ShapeSerializer( + SmithyCodeWriter codeWriter, + NodeSerializer nodeSerializer, + Predicate traitFilter, + Model model + ) { this.codeWriter = codeWriter; this.nodeSerializer = nodeSerializer; + this.traitFilter = traitFilter; this.model = model; } @@ -251,9 +305,10 @@ private void shapeWithMembers(Shape shape, List members) { private void serializeTraits(Shape shape) { // The documentation trait always needs to be serialized first since it uses special syntax. - shape.getTrait(DocumentationTrait.class).ifPresent(this::serializeDocumentationTrait); + shape.getTrait(DocumentationTrait.class).filter(traitFilter).ifPresent(this::serializeDocumentationTrait); shape.getAllTraits().values().stream() .filter(trait -> !(trait instanceof DocumentationTrait)) + .filter(traitFilter) .sorted(Comparator.comparing(trait -> trait.toShapeId().toString(), String.CASE_INSENSITIVE_ORDER)) .forEach(this::serializeTrait); } 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 18faf28195c..9123ac6c28b 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 @@ -1,7 +1,11 @@ package software.amazon.smithy.model.shapes; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.aMapWithSize; +import static org.hamcrest.Matchers.hasKey; +import static org.hamcrest.Matchers.not; import java.io.IOException; import java.nio.file.Files; @@ -13,6 +17,8 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestFactory; import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.traits.DocumentationTrait; +import software.amazon.smithy.model.traits.RequiredTrait; import software.amazon.smithy.utils.IoUtils; public class SmithyIdlModelSerializerTest { @@ -51,4 +57,66 @@ public void multipleNamespacesGenerateMultipleFiles() { assertThat(generated, equalTo(IoUtils.readUtf8File(expectedPath))); }); } + + @Test + public void filtersShapes() { + Model model = Model.assembler() + .addImport(getClass().getResource("idl-serialization/test-model.json")) + .assemble() + .unwrap(); + SmithyIdlModelSerializer serializer = SmithyIdlModelSerializer.builder() + .shapeFilter(shape -> shape.getId().getNamespace().equals("ns.structures")) + .build(); + Map serialized = serializer.serialize(model); + + assertThat(serialized, aMapWithSize(1)); + assertThat(serialized, hasKey(Paths.get("ns.structures.smithy"))); + assertThat(serialized.get(Paths.get("ns.structures.smithy")), + containsString("namespace ns.structures")); + } + + @Test + public void filtersMetadata() { + Model model = Model.assembler() + .addImport(getClass().getResource("idl-serialization/test-model.json")) + .assemble() + .unwrap(); + SmithyIdlModelSerializer serializer = SmithyIdlModelSerializer.builder() + .metadataFilter(key -> false) + .build(); + Map serialized = serializer.serialize(model); + for (String output : serialized.values()) { + assertThat(output, not(containsString("metadata"))); + } + } + + @Test + public void filtersTraits() { + Model model = Model.assembler() + .addImport(getClass().getResource("idl-serialization/test-model.json")) + .assemble() + .unwrap(); + SmithyIdlModelSerializer serializer = SmithyIdlModelSerializer.builder() + .traitFilter(trait -> !(trait instanceof RequiredTrait)) + .build(); + Map serialized = serializer.serialize(model); + for (String output : serialized.values()) { + assertThat(output, not(containsString("@required"))); + } + } + + @Test + public void filtersDocumentationTrait() { + Model model = Model.assembler() + .addImport(getClass().getResource("idl-serialization/test-model.json")) + .assemble() + .unwrap(); + SmithyIdlModelSerializer serializer = SmithyIdlModelSerializer.builder() + .traitFilter(trait -> !(trait instanceof DocumentationTrait)) + .build(); + Map serialized = serializer.serialize(model); + for (String output : serialized.values()) { + assertThat(output, not(containsString("/// "))); + } + } } diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/test-model.json b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/test-model.json new file mode 100644 index 00000000000..670f608db42 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/test-model.json @@ -0,0 +1,44 @@ +{ + "smithy": "0.5.0", + "metadata": { + "shared": true + }, + "shapes": { + "ns.structures#Structure": { + "type": "structure", + "members": { + "listMember": { + "target": "ns.primitives#StringList", + "traits": { + "smithy.api#documentation": "A list member" + } + }, + "stringMember": { + "target": "ns.primitives#String", + "traits": { + "smithy.api#required": true, + "smithy.api#documentation": "A string member" + } + } + }, + "traits": { + "smithy.api#documentation": "A structure shape" + } + }, + "ns.primitives#String": { + "type": "string", + "traits": { + "smithy.api#documentation": "A string shape" + } + }, + "ns.primitives#StringList": { + "type": "list", + "member": { + "target": "ns.primitives#String" + }, + "traits": { + "smithy.api#documentation": "A list shape" + } + } + } +} From 675b7d12d1a2ec9da32c5f2fb74f4c951ae341d9 Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Mon, 24 Feb 2020 10:37:13 -0800 Subject: [PATCH 04/16] Don't filter metadata case-insensitively Shapes can be filtered case-insensitively because they are required to be case-insensitively unique. Metadata doesn't have that same constraint though, so filtering them as such would lead to a non- deterministic ordering. --- .../amazon/smithy/model/shapes/SmithyIdlModelSerializer.java | 2 +- .../smithy/model/shapes/idl-serialization/cases/metadata.smithy | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) 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 3ea2783ab3f..8ec52e5f7d2 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 @@ -111,7 +111,7 @@ private String serializeHeader(String namespace, Model fullModel) { // 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() - .sorted(Map.Entry.comparingByKey(String.CASE_INSENSITIVE_ORDER)) + .sorted(Map.Entry.comparingByKey()) .filter(entry -> metadataFilter.test(entry.getKey())) .forEach(entry -> { codeWriter.trimTrailingSpaces(false) diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/metadata.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/metadata.smithy index 69eac0008bf..83948a83c9c 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/metadata.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/metadata.smithy @@ -1,5 +1,7 @@ $version: "0.5.0" +metadata CaseSensitive = true +metadata caseSensitive = true metadata example.array = [ 10, true, From 0f8205a2f5da508ed4ab106afd25772aaf38aae1 Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Mon, 24 Feb 2020 11:00:34 -0800 Subject: [PATCH 05/16] Serialize metadata-only models --- .../shapes/SmithyIdlModelSerializer.java | 24 +++++++++++++------ .../idl-serialization/cases/metadata.smithy | 4 ---- 2 files changed, 17 insertions(+), 11 deletions(-) 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 8ec52e5f7d2..94183fd3a2e 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 @@ -73,9 +73,14 @@ private SmithyIdlModelSerializer(Builder builder) { * @return A map of (possibly relative) file paths to Smithy IDL strings. */ public Map serialize(Model model) { - return model.shapes() + List shapes = model.shapes() .filter(FunctionalUtils.not(Shape::isMemberShape)) .filter(shapeFilter) + .collect(Collectors.toList()); + if (shapes.isEmpty()) { + return Collections.singletonMap(Paths.get("metadata.smithy"), serializeHeader(model, null)); + } + return shapes.stream() .collect(Collectors.groupingBy(shapePlacer)).entrySet().stream() .collect(Collectors.toMap(Map.Entry::getKey, entry -> serialize(model, entry.getValue()))); } @@ -99,11 +104,11 @@ private String serialize(Model fullModel, Collection shapes) { .sorted(new ShapeComparator()) .forEach(shape -> shape.accept(shapeSerializer)); - return serializeHeader(namespace, fullModel) + codeWriter.toString(); + return serializeHeader(fullModel, namespace) + codeWriter.toString(); } - private String serializeHeader(String namespace, Model fullModel) { - SmithyCodeWriter codeWriter = new SmithyCodeWriter(namespace, fullModel); + private String serializeHeader(Model fullModel, String namespace) { + SmithyCodeWriter codeWriter = new SmithyCodeWriter(null, fullModel); NodeSerializer nodeSerializer = new NodeSerializer(codeWriter, fullModel); codeWriter.write("$$version: \"$L\"", Model.MODEL_VERSION).write(""); @@ -120,13 +125,18 @@ private String serializeHeader(String namespace, Model fullModel) { nodeSerializer.serialize(entry.getValue()); codeWriter.write(""); }); + if (!fullModel.getMetadata().isEmpty()) { codeWriter.write(""); } - codeWriter.write("namespace " + namespace) - .trimBlankLines(-1) - .write(""); + if (namespace != null) { + codeWriter.write("namespace $L", namespace) + .write("") + // We want the extra blank line to separate the header and the model contents. + .trimBlankLines(-1); + } + return codeWriter.toString(); } diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/metadata.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/metadata.smithy index 83948a83c9c..23934670c8c 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/metadata.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/metadata.smithy @@ -16,7 +16,3 @@ metadata example.object = { } metadata example.string = "hello there" metadata "key must be quoted" = true - -namespace ns.foo - -string Placeholder From 6fca073114eb353a7a859cc3b960bd40e3e634ce Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Mon, 24 Feb 2020 12:06:59 -0800 Subject: [PATCH 06/16] Clarify documentation on IDL serializer outputs --- .../model/shapes/SmithyIdlModelSerializer.java | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) 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 94183fd3a2e..dfb1ddf1573 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 @@ -68,7 +68,15 @@ private SmithyIdlModelSerializer(Builder builder) { /** * Serializes a {@link Model} into a set of Smithy IDL files. * - *

This does not write the models to disk. + *

The output is a mapping + * + *

By default the paths are relative paths where each namespace is given its own file in the form + * "namespace.smithy". This is configurable via the shape placer, which can place shapes into absolute + * paths. + * + *

If the model contains no shapes, or all shapes are filtered out, then a single path "metadata.smithy" + * will be present. This will contain only any defined metadata. + * * @param model The model to serialize. * @return A map of (possibly relative) file paths to Smithy IDL strings. */ @@ -147,6 +155,9 @@ public static Builder builder() { return new Builder(); } + /** + * Sorts shapes into files based on their namespace, where each file is named {namespace}.smithy. + */ public static Path placeShapesByNamespace(Shape shape) { return Paths.get(shape.getId().getNamespace() + ".smithy"); } @@ -254,6 +265,8 @@ public Builder traitFilter(Predicate traitFilter) { /** * Function that determines what output file a shape should go in. * + *

The returned paths may be absolute or relative. + * *

NOTE: the Smithy IDL only supports one namespace per file. * @param shapePlacer Function that accepts a shape and returns file path. * @return Returns the builder. From 5555a4e01f96d5006cc5ef7172da4ade3d5bf921 Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Mon, 24 Feb 2020 12:09:17 -0800 Subject: [PATCH 07/16] Make IDL shape comparator private --- .../amazon/smithy/model/shapes/SmithyIdlModelSerializer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 dfb1ddf1573..566ad478fea 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 @@ -165,7 +165,7 @@ public static Path placeShapesByNamespace(Shape shape) { /** * Comparator used to sort shapes. */ - public static final class ShapeComparator implements Comparator, Serializable { + private static final class ShapeComparator implements Comparator, Serializable { private static final List PRIORITY = ListUtils.of( ShapeType.SERVICE, ShapeType.RESOURCE, From 9397ca41929ced18c2c8e94fa2fae56ddd11d3f7 Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Mon, 24 Feb 2020 12:13:04 -0800 Subject: [PATCH 08/16] Sort structures before unions in serialized IDL --- .../smithy/model/shapes/SmithyIdlModelSerializer.java | 2 +- .../idl-serialization/cases/simple-shapes.smithy | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) 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 566ad478fea..b574150a3af 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 @@ -170,8 +170,8 @@ private static final class ShapeComparator implements Comparator, Seriali ShapeType.SERVICE, ShapeType.RESOURCE, ShapeType.OPERATION, - ShapeType.UNION, ShapeType.STRUCTURE, + ShapeType.UNION, ShapeType.LIST, ShapeType.SET, ShapeType.MAP diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/simple-shapes.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/simple-shapes.smithy index af07b8b6279..f7e48c23821 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/simple-shapes.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/simple-shapes.smithy @@ -2,11 +2,6 @@ $version: "0.5.0" namespace ns.foo -union Union { - byte: Byte, - double: Double, -} - structure StructureWithMembers { a: String, b: String, @@ -14,6 +9,11 @@ structure StructureWithMembers { structure StructureWithoutMembers {} +union Union { + byte: Byte, + double: Double, +} + list List { member: String, } From 496f43add49ad6c5631fd05a4aa017d57f86236e Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Mon, 24 Feb 2020 12:22:05 -0800 Subject: [PATCH 09/16] touch up docs --- .../amazon/smithy/model/shapes/SmithyIdlModelSerializer.java | 3 +++ 1 file changed, 3 insertions(+) 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 b574150a3af..836bd84e3da 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 @@ -229,6 +229,7 @@ public Builder() {} /** * Predicate that determines if a metadata is serialized. + * * @param metadataFilter Predicate that accepts a metadata key. * @return Returns the builder. */ @@ -239,6 +240,7 @@ public Builder metadataFilter(Predicate metadataFilter) { /** * Predicate that determines if a shape and its traits are serialized. + * * @param shapeFilter Predicate that accepts a shape. * @return Returns the builder. */ @@ -268,6 +270,7 @@ public Builder traitFilter(Predicate traitFilter) { *

The returned paths may be absolute or relative. * *

NOTE: the Smithy IDL only supports one namespace per file. + * * @param shapePlacer Function that accepts a shape and returns file path. * @return Returns the builder. */ From a96403acfe997bb20d864a7c755f5cc5984a0afa Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Mon, 24 Feb 2020 12:22:45 -0800 Subject: [PATCH 10/16] Make shapeplacer public --- .../amazon/smithy/model/shapes/SmithyIdlModelSerializer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 836bd84e3da..d9b34b882a4 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 @@ -274,7 +274,7 @@ public Builder traitFilter(Predicate traitFilter) { * @param shapePlacer Function that accepts a shape and returns file path. * @return Returns the builder. */ - Builder shapePlacer(Function shapePlacer) { + public Builder shapePlacer(Function shapePlacer) { this.shapePlacer = Objects.requireNonNull(shapePlacer); return this; } From 9ef38f40b941a7029d36c403f889d0a60aa5a645 Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Mon, 24 Feb 2020 14:02:22 -0800 Subject: [PATCH 11/16] Privatize everything that can be --- .../model/shapes/SmithyIdlModelSerializer.java | 14 +++++++------- .../cases/documentation-trait.smithy | 1 + 2 files changed, 8 insertions(+), 7 deletions(-) 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 d9b34b882a4..63dae9ae76d 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 @@ -502,7 +502,7 @@ private static final class NodeSerializer { * * @param node The node to serialize. */ - public void serialize(Node node) { + private void serialize(Node node) { serialize(node, null); } @@ -515,7 +515,7 @@ public void serialize(Node node) { * @param node The node to serialize. * @param shape The shape of the node. */ - public void serialize(Node node, Shape shape) { + private void serialize(Node node, Shape shape) { // ShapeIds are represented differently than strings, so if a shape looks like it's // representing a shapeId we need to serialize it without quotes. if (isShapeId(shape)) { @@ -621,7 +621,7 @@ private void serializeObject(ObjectNode node, Shape shape) { * @param node The node to serialize. * @param shape The shape of the node. */ - public void serializeKeyValuePairs(ObjectNode node, Shape shape) { + private void serializeKeyValuePairs(ObjectNode node, Shape shape) { if (node.isEmpty()) { return; } @@ -682,7 +682,7 @@ private static final class SmithyCodeWriter extends CodeWriter { /** * Opens a block without writing indentation whitespace or inserting a newline. */ - public SmithyCodeWriter openBlockInline(String content, Object... args) { + private SmithyCodeWriter openBlockInline(String content, Object... args) { writeInline(content, args).indent(); return this; } @@ -690,7 +690,7 @@ public SmithyCodeWriter openBlockInline(String content, Object... args) { /** * Closes a block without inserting a newline. */ - public SmithyCodeWriter closeBlockWithoutNewline(String content, Object... args) { + private SmithyCodeWriter closeBlockWithoutNewline(String content, Object... args) { setNewline(""); closeBlock(content, args); setNewline("\n"); @@ -702,7 +702,7 @@ public SmithyCodeWriter closeBlockWithoutNewline(String content, Object... args) * *

This does not insert a trailing newline. */ - public SmithyCodeWriter writeIndent() { + private SmithyCodeWriter writeIndent() { setNewline(""); // We explicitly want the trailing spaces, so disable trimming for this call. trimTrailingSpaces(false); @@ -763,7 +763,7 @@ private boolean conflictsWithImports(ShapeId shapeId) { * *

If the list is empty, nothing is written. */ - public SmithyCodeWriter writeOptionalIdList(String textBeforeList, Collection shapeIds) { + private SmithyCodeWriter writeOptionalIdList(String textBeforeList, Collection shapeIds) { if (shapeIds.isEmpty()) { return this; } diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/documentation-trait.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/documentation-trait.smithy index e67fad99147..54a9622bb13 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/documentation-trait.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/documentation-trait.smithy @@ -4,4 +4,5 @@ namespace ns.foo /// Documentation comments are used. /// $ is escaped +/// /// doesn't need to be escaped string Foo From dd4396b7932d324e93c9318e0f26e58cef0317e1 Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Mon, 24 Feb 2020 14:12:29 -0800 Subject: [PATCH 12/16] Use string array for writeInline --- .../software/amazon/smithy/utils/CodeWriter.java | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeWriter.java b/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeWriter.java index de69c967de0..13bfd9673ab 100644 --- a/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeWriter.java +++ b/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeWriter.java @@ -956,29 +956,26 @@ public final CodeWriter write(Object content, Object... args) { */ public final CodeWriter writeInline(Object content, Object... args) { String value = formatter.format(content, currentState.indentText, this, args); - ArrayList lines = new ArrayList<>(Arrays.asList(value.split(newlineRegexQuoted, -1))); + String[] lines = value.split(newlineRegexQuoted, -1); // The first line is written directly, with no added indentation or newline - currentState.write(lines.remove(0)); + currentState.write(lines[0]); // If there aren't any additional lines, return. - if (lines.isEmpty()) { + if (lines.length == 1) { return this; } // If there are additional lines, they need to be handled properly. So insert a newline. currentState.write(newline); - // We don't want to append a newline, so remove the last line for handling later. - String lastLine = lines.remove(lines.size() - 1); - // Write all the intermediate lines as normal. - for (String line : lines) { - currentState.writeLine(line + newline); + for (int i = 1; i <= lines.length - 2; i++) { + currentState.writeLine(lines[i] + newline); } // Write the final line with proper indentation, but without an appended newline. - currentState.writeLine(lastLine); + currentState.writeLine(lines[lines.length - 1]); return this; } From b57193f74cf3dad4ad982569af0d4e1b26e954be Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Mon, 24 Feb 2020 16:10:27 -0800 Subject: [PATCH 13/16] remove unused import --- .../src/main/java/software/amazon/smithy/utils/CodeWriter.java | 1 - 1 file changed, 1 deletion(-) diff --git a/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeWriter.java b/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeWriter.java index 13bfd9673ab..d43432f30e3 100644 --- a/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeWriter.java +++ b/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeWriter.java @@ -17,7 +17,6 @@ import java.util.ArrayDeque; import java.util.ArrayList; -import java.util.Arrays; import java.util.Deque; import java.util.HashMap; import java.util.List; From 8ea2e2f6476a052f9e8d9af455eaf36ba24576fb Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Wed, 26 Feb 2020 16:02:42 -0800 Subject: [PATCH 14/16] Simplify IDL serializer This simplifies various bits of the internals, for instance by discarding much manual tinkering of sorting. --- .../shapes/SmithyIdlModelSerializer.java | 102 ++++++------------ .../cases/service-shapes.smithy | 3 +- 2 files changed, 35 insertions(+), 70 deletions(-) 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 63dae9ae76d..25e9978ef57 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 @@ -18,6 +18,7 @@ import java.io.Serializable; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; @@ -46,6 +47,7 @@ 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; @@ -81,16 +83,15 @@ private SmithyIdlModelSerializer(Builder builder) { * @return A map of (possibly relative) file paths to Smithy IDL strings. */ public Map serialize(Model model) { - List shapes = model.shapes() + Map result = model.shapes() .filter(FunctionalUtils.not(Shape::isMemberShape)) .filter(shapeFilter) - .collect(Collectors.toList()); - if (shapes.isEmpty()) { - return Collections.singletonMap(Paths.get("metadata.smithy"), serializeHeader(model, null)); - } - return shapes.stream() .collect(Collectors.groupingBy(shapePlacer)).entrySet().stream() .collect(Collectors.toMap(Map.Entry::getKey, entry -> serialize(model, entry.getValue()))); + if (result.isEmpty()) { + return Collections.singletonMap(Paths.get("metadata.smithy"), serializeHeader(model, null)); + } + return result; } private String serialize(Model fullModel, Collection shapes) { @@ -124,8 +125,8 @@ private String serializeHeader(Model fullModel, String namespace) { // 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() - .sorted(Map.Entry.comparingByKey()) .filter(entry -> metadataFilter.test(entry.getKey())) + .sorted(Map.Entry.comparingByKey()) .forEach(entry -> { codeWriter.trimTrailingSpaces(false) .writeInline("metadata $M = ", entry.getKey()) @@ -166,17 +167,18 @@ public static Path placeShapesByNamespace(Shape shape) { * Comparator used to sort shapes. */ private static final class ShapeComparator implements Comparator, Serializable { - private static final List PRIORITY = ListUtils.of( - ShapeType.SERVICE, - ShapeType.RESOURCE, - ShapeType.OPERATION, - ShapeType.STRUCTURE, - ShapeType.UNION, - ShapeType.LIST, - ShapeType.SET, - ShapeType.MAP + 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 @@ -188,31 +190,24 @@ public int compare(Shape s1, Shape s2) { return -1; } // The other sorting rules don't matter for traits. - return compareCaseInsensitive(s1, s2); + return s1.compareTo(s2); } // If the shapes are the same type, just compare their shape ids. if (s1.getType().equals(s2.getType())) { - return compareCaseInsensitive(s1, s2); + return s1.compareTo(s2); } // If one shape is prioritized, compare by priority. - if (PRIORITY.contains(s1.getType()) || PRIORITY.contains(s2.getType())) { + if (PRIORITY.containsKey(s1.getType()) || PRIORITY.containsKey(s2.getType())) { // If only one shape is prioritized, that shape is "greater". - if (!PRIORITY.contains(s1.getType())) { + if (!PRIORITY.containsKey(s1.getType())) { return 1; } - if (!PRIORITY.contains(s2.getType())) { + if (!PRIORITY.containsKey(s2.getType())) { return -1; } - return PRIORITY.indexOf(s1.getType()) - PRIORITY.indexOf(s2.getType()); + return PRIORITY.get(s1.getType()) - PRIORITY.get(s2.getType()); } - return compareCaseInsensitive(s1, s2); - } - - /** - * Compare two shapes by id case-insensitively. - */ - private int compareCaseInsensitive(Shape s1, Shape s2) { - return s1.toShapeId().toString().toLowerCase().compareTo(s2.toShapeId().toString().toLowerCase()); + return s1.compareTo(s2); } } @@ -335,7 +330,7 @@ private void serializeTraits(Shape shape) { shape.getAllTraits().values().stream() .filter(trait -> !(trait instanceof DocumentationTrait)) .filter(traitFilter) - .sorted(Comparator.comparing(trait -> trait.toShapeId().toString(), String.CASE_INSENSITIVE_ORDER)) + .sorted(Comparator.comparing(Trait::toShapeId)) .forEach(this::serializeTrait); } @@ -390,22 +385,16 @@ public Void mapShape(MapShape shape) { @Override public Void structureShape(StructureShape shape) { - shapeWithMembers(shape, sortMembers(shape.getAllMembers().values())); + shapeWithMembers(shape, new ArrayList<>(shape.getAllMembers().values())); return null; } @Override public Void unionShape(UnionShape shape) { - shapeWithMembers(shape, sortMembers(shape.getAllMembers().values())); + shapeWithMembers(shape, new ArrayList<>(shape.getAllMembers().values())); return null; } - private List sortMembers(Collection members) { - return members.stream() - .sorted(Comparator.comparing(MemberShape::getMemberName, String.CASE_INSENSITIVE_ORDER)) - .collect(Collectors.toList()); - } - @Override public Void serviceShape(ServiceShape shape) { serializeTraits(shape); @@ -420,16 +409,11 @@ public Void serviceShape(ServiceShape shape) { @Override public Void resourceShape(ResourceShape shape) { serializeTraits(shape); - if (isEmptyResource(shape)) { - codeWriter.write("resource $L {}", shape.getId().getName()).write(""); - return null; - } - codeWriter.openBlock("resource $L {", shape.getId().getName()); if (!shape.getIdentifiers().isEmpty()) { codeWriter.openBlock("identifiers: {"); shape.getIdentifiers().entrySet().stream() - .sorted(Map.Entry.comparingByKey(String.CASE_INSENSITIVE_ORDER)) + .sorted(Map.Entry.comparingByKey()) .forEach(entry -> codeWriter.write( "$L: $I,", entry.getKey(), entry.getValue())); codeWriter.closeBlock("},"); @@ -450,19 +434,6 @@ public Void resourceShape(ResourceShape shape) { return null; } - private boolean isEmptyResource(ResourceShape shape) { - return !(!shape.getIdentifiers().isEmpty() - || !shape.getOperations().isEmpty() - || !shape.getCollectionOperations().isEmpty() - || !shape.getResources().isEmpty() - || shape.getPut().isPresent() - || shape.getCreate().isPresent() - || shape.getRead().isPresent() - || shape.getUpdate().isPresent() - || shape.getDelete().isPresent() - || shape.getList().isPresent()); - } - @Override public Void operationShape(OperationShape shape) { serializeTraits(shape); @@ -546,11 +517,7 @@ private boolean isShapeId(Shape shape) { if (shape == null) { return false; } - if (shape.isMemberShape()) { - return shape.asMemberShape() - .flatMap(member -> member.getMemberTrait(model, IdRefTrait.class)).isPresent(); - } - return shape.hasTrait(IdRefTrait.class); + return shape.getMemberTrait(model, IdRefTrait.class).isPresent(); } private void serializeString(StringNode node) { @@ -632,7 +599,7 @@ private void serializeKeyValuePairs(ObjectNode node, Shape shape) { if (shape == null) { members = Collections.emptyMap(); } else { - members = shape.members().stream().distinct() + members = shape.members().stream() .collect(Collectors.toMap(MemberShape::getMemberName, Function.identity())); } @@ -769,9 +736,7 @@ private SmithyCodeWriter writeOptionalIdList(String textBeforeList, Collection write("$I,", shapeId)); + shapeIds.stream().sorted().forEach(shapeId -> write("$I,", shapeId)); closeBlock("],"); return this; @@ -794,8 +759,7 @@ public String toString() { if (imports.isEmpty()) { return contents; } - String importString = imports.stream() - .sorted(Comparator.comparing(ShapeId::toString, String.CASE_INSENSITIVE_ORDER)) + String importString = imports.stream().sorted() .map(shapeId -> String.format("use %s", shapeId.toString())) .collect(Collectors.joining("\n")); return importString + "\n\n" + contents; diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/service-shapes.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/service-shapes.smithy index a7f8602b376..61385cd0a91 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/service-shapes.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/service-shapes.smithy @@ -16,7 +16,8 @@ service MyService { ], } -resource EmptyResource {} +resource EmptyResource { +} resource MyResource { identifiers: { From 92a4eaa1a1cb59e08b17059e32e5a659715ef95a Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Wed, 26 Feb 2020 16:20:28 -0800 Subject: [PATCH 15/16] Add a base path to the idl serializer --- .../shapes/SmithyIdlModelSerializer.java | 27 ++++++++++++++++-- .../shapes/SmithyIdlModelSerializerTest.java | 28 ++++++++++++++----- 2 files changed, 46 insertions(+), 9 deletions(-) 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 25e9978ef57..95b57b77090 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 @@ -59,12 +59,19 @@ public final class SmithyIdlModelSerializer { private final Predicate shapeFilter; private final Predicate traitFilter; private final Function shapePlacer; + private final Path basePath; private SmithyIdlModelSerializer(Builder builder) { metadataFilter = builder.metadataFilter; shapeFilter = builder.shapeFilter.and(FunctionalUtils.not(Prelude::isPreludeShape)); traitFilter = builder.traitFilter; - shapePlacer = builder.shapePlacer; + basePath = builder.basePath; + if (basePath != null) { + Function shapePlacer = builder.shapePlacer; + this.shapePlacer = shape -> this.basePath.resolve(shapePlacer.apply(shape)); + } else { + this.shapePlacer = builder.shapePlacer; + } } /** @@ -89,7 +96,11 @@ public Map serialize(Model model) { .collect(Collectors.groupingBy(shapePlacer)).entrySet().stream() .collect(Collectors.toMap(Map.Entry::getKey, entry -> serialize(model, entry.getValue()))); if (result.isEmpty()) { - return Collections.singletonMap(Paths.get("metadata.smithy"), serializeHeader(model, null)); + Path path = Paths.get("metadata.smithy"); + if (basePath != null) { + path = basePath.resolve(path); + } + return Collections.singletonMap(path, serializeHeader(model, null)); } return result; } @@ -219,6 +230,7 @@ public static final class Builder implements SmithyBuilder shapeFilter = shape -> true; private Predicate traitFilter = trait -> true; private Function shapePlacer = SmithyIdlModelSerializer::placeShapesByNamespace; + private Path basePath = null; public Builder() {} @@ -274,6 +286,17 @@ public Builder shapePlacer(Function shapePlacer) { return this; } + /** + * A base path to use for any created models. + * + * @param basePath The base directory to assign models to. + * @return Returns the builder. + */ + public Builder basePath(Path basePath) { + this.basePath = basePath; + return this; + } + @Override public SmithyIdlModelSerializer build() { return new SmithyIdlModelSerializer(this); 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 9123ac6c28b..11224d49591 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 @@ -1,6 +1,7 @@ package software.amazon.smithy.model.shapes; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.aMapWithSize; @@ -17,6 +18,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestFactory; import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.node.StringNode; import software.amazon.smithy.model.traits.DocumentationTrait; import software.amazon.smithy.model.traits.RequiredTrait; import software.amazon.smithy.utils.IoUtils; @@ -48,14 +50,12 @@ public void multipleNamespacesGenerateMultipleFiles() { .addImport(getClass().getResource("idl-serialization/multiple-namespaces/input.json")) .assemble() .unwrap(); - SmithyIdlModelSerializer serializer = SmithyIdlModelSerializer.builder().build(); - Map serialized = serializer.serialize(model); - Path outputDir = Paths.get(getClass().getResource("idl-serialization/multiple-namespaces/output").getFile()); - serialized.forEach((path, generated) -> { - Path expectedPath = outputDir.resolve(path); - assertThat(generated, equalTo(IoUtils.readUtf8File(expectedPath))); - }); + SmithyIdlModelSerializer serializer = SmithyIdlModelSerializer.builder() + .basePath(outputDir) + .build(); + Map serialized = serializer.serialize(model); + serialized.forEach((path, generated) -> assertThat(generated, equalTo(IoUtils.readUtf8File(path)))); } @Test @@ -119,4 +119,18 @@ public void filtersDocumentationTrait() { assertThat(output, not(containsString("/// "))); } } + + @Test + public void basePathAppliesToMetadataOnlyModel() { + Path basePath = Paths.get("/tmp/smithytest"); + Model model = Model.assembler() + .putMetadata("foo", StringNode.from("bar")) + .assemble() + .unwrap(); + SmithyIdlModelSerializer serializer = SmithyIdlModelSerializer.builder() + .basePath(basePath) + .build(); + Map serialized = serializer.serialize(model); + assertThat(serialized.keySet(), contains(basePath.resolve("metadata.smithy"))); + } } From 1811d0a36f7add497a15d0be73836a7436057a33 Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Wed, 26 Feb 2020 17:23:42 -0800 Subject: [PATCH 16/16] Compare ShapeId's case-insensitively This updates the `compareTo` on shapeIds to be case-insensitve by default, with a case-sensitive tie-breaker. Though technically that means we could be comparing a given shape twice, practically the likelihood is low since ShapeId's in a model must be case-insensitively unique. --- .../amazon/smithy/model/shapes/ShapeId.java | 7 ++++- .../smithy/model/shapes/ShapeIdTest.java | 29 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) 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 2214a0a9342..820cd005c3f 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 @@ -258,7 +258,12 @@ public ShapeId toShapeId() { @Override public int compareTo(ShapeId other) { - return toString().compareTo(other.toString()); + int outcome = toString().compareToIgnoreCase(other.toString()); + if (outcome == 0) { + // If they're case-insensitively equal, use a case-sensitive comparison as a tie-breaker. + return toString().compareTo(other.toString()); + } + return outcome; } /** diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/shapes/ShapeIdTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/shapes/ShapeIdTest.java index 5435474d792..508f62fc8cb 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/shapes/ShapeIdTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/shapes/ShapeIdTest.java @@ -22,6 +22,7 @@ import java.util.Arrays; import java.util.Collection; +import java.util.List; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -238,6 +239,34 @@ public static Collection toStringData() { }); } + @Test + public void compareToTest() { + List given = Arrays.asList( + ShapeId.fromParts("ns.foo", "foo"), + ShapeId.fromParts("ns.foo", "Foo"), + ShapeId.fromParts("ns.foo", "bar"), + ShapeId.fromParts("ns.foo", "bar", "member"), + ShapeId.fromParts("ns.foo", "bar", "Member"), + ShapeId.fromParts("ns.foo", "bar", "AMember"), + ShapeId.fromParts("ns.Foo", "foo"), + ShapeId.fromParts("ns.baz", "foo") + ); + given.sort(ShapeId::compareTo); + + List expected = Arrays.asList( + ShapeId.fromParts("ns.baz", "foo"), + ShapeId.fromParts("ns.foo", "bar"), + ShapeId.fromParts("ns.foo", "bar", "AMember"), + ShapeId.fromParts("ns.foo", "bar", "Member"), + ShapeId.fromParts("ns.foo", "bar", "member"), + ShapeId.fromParts("ns.Foo", "foo"), + ShapeId.fromParts("ns.foo", "Foo"), + ShapeId.fromParts("ns.foo", "foo") + ); + + assertEquals(expected, given); + } + @ParameterizedTest @MethodSource("equalsData") public void equalsTest(final ShapeId lhs, final Object rhs, final boolean expected) {