All Downloads are FREE. Search and download functionalities are using the official Maven repository.

software.amazon.smithy.model.shapes.SmithyIdlModelSerializer Maven / Gradle / Ivy

/*
 * 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.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
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;
import software.amazon.smithy.model.knowledge.OperationIndex;
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.AnnotationTrait;
import software.amazon.smithy.model.traits.DefaultTrait;
import software.amazon.smithy.model.traits.DocumentationTrait;
import software.amazon.smithy.model.traits.EnumValueTrait;
import software.amazon.smithy.model.traits.IdRefTrait;
import software.amazon.smithy.model.traits.InputTrait;
import software.amazon.smithy.model.traits.OutputTrait;
import software.amazon.smithy.model.traits.Trait;
import software.amazon.smithy.model.traits.UnitTypeTrait;
import software.amazon.smithy.utils.AbstractCodeWriter;
import software.amazon.smithy.utils.CodeWriter;
import software.amazon.smithy.utils.FunctionalUtils;
import software.amazon.smithy.utils.ListUtils;
import software.amazon.smithy.utils.Pair;
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 static final String DEFAULT_INLINE_INPUT_SUFFIX = "Input";
    private static final String DEFAULT_INLINE_OUTPUT_SUFFIX = "Output";

    private final Predicate metadataFilter;
    private final Predicate shapeFilter;
    private final Predicate traitFilter;
    private final Function shapePlacer;
    private final Path basePath;
    private final SmithyIdlComponentOrder componentOrder;
    private final String inlineInputSuffix;
    private final String inlineOutputSuffix;
    private final boolean inferInlineIoSuffixes;
    private final boolean shouldCoerceInlineIo;

    /**
     * Trait serialization features.
     */
    private enum TraitFeature {
        /** Inline documentation traits with other traits as opposed to using /// syntax. */
        NO_SPECIAL_DOCS_SYNTAX,

        /** Serializing a member, so special default syntax can be used. */
        MEMBER;

        /**
         * Checks if the current enum value is present in an array of enum values.
         *
         * @param haystack Array of enums to check.
         * @return Returns true if this enum is found in the array.
         */
        boolean hasFeature(TraitFeature[] haystack) {
            for (TraitFeature test : haystack) {
                if (test == this) {
                    return true;
                }
            }
            return false;
        }
    }

    private SmithyIdlModelSerializer(Builder builder) {
        metadataFilter = builder.metadataFilter;
        // If prelude serializing has been enabled, only use the given shape filter.
        if (builder.serializePrelude) {
            shapeFilter = builder.shapeFilter;
        // Default to using the given shape filter and filtering prelude shapes.
        } else {
            shapeFilter = builder.shapeFilter.and(FunctionalUtils.not(Prelude::isPreludeShape));
        }
        // Never serialize synthetic traits.
        traitFilter = builder.traitFilter.and(FunctionalUtils.not(Trait::isSynthetic));
        basePath = builder.basePath;
        if (basePath != null) {
            Function placer = builder.shapePlacer;
            shapePlacer = shape -> basePath.resolve(placer.apply(shape));
        } else {
            shapePlacer = builder.shapePlacer;
        }
        componentOrder = builder.componentOrder;
        inlineInputSuffix = builder.inlineInputSuffix;
        inlineOutputSuffix = builder.inlineOutputSuffix;
        shouldCoerceInlineIo = builder.shouldCoerceInlineIo;
        // Force inferring suffixes on if coercion is on.
        if (shouldCoerceInlineIo) {
            inferInlineIoSuffixes = true;
        } else {
            inferInlineIoSuffixes = builder.inferInlineIoSuffixes;
        }
    }

    /**
     * Serializes a {@link Model} into a set of Smithy IDL files.
     *
     * 

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. */ public Map serialize(Model model) { Map result = model.shapes() .filter(FunctionalUtils.not(Shape::isMemberShape)) .filter(shapeFilter) .collect(Collectors.groupingBy(shapePlacer)).entrySet().stream() .collect(Collectors.toMap(Map.Entry::getKey, entry -> serialize(model, entry.getValue()))); if (result.isEmpty()) { Path path = Paths.get("metadata.smithy"); if (basePath != null) { path = basePath.resolve(path); } return Collections.singletonMap(path, serializeHeader( model, null, Collections.emptySet(), inlineInputSuffix, inlineOutputSuffix)); } return result; } 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); Pair inlineSuffixes = determineInlineSuffixes(fullModel, shapes); Set inlineableShapes = getInlineableShapes( fullModel, shapes, inlineSuffixes.getLeft(), inlineSuffixes.getRight()); ShapeSerializer shapeSerializer = new ShapeSerializer( codeWriter, nodeSerializer, traitFilter, fullModel, inlineableShapes, componentOrder); Comparator comparator = componentOrder.shapeComparator(); shapes.stream() .filter(FunctionalUtils.not(Shape::isMemberShape)) .filter(shape -> !inlineableShapes.contains(shape.getId())) .sorted(comparator) .forEach(shape -> shape.accept(shapeSerializer)); String header = serializeHeader( fullModel, namespace, shapes, inlineSuffixes.getLeft(), inlineSuffixes.getRight()); return header + codeWriter.toString(); } private Set getInlineableShapes( Model fullModel, Collection shapes, String inputSuffix, String outputSuffix ) { OperationIndex operationIndex = OperationIndex.of(fullModel); Set inlineableShapes = new HashSet<>(); for (Shape shape : shapes) { if (!shape.isOperationShape()) { continue; } OperationShape operation = shape.asOperationShape().get(); if (!operation.getInputShape().equals(UnitTypeTrait.UNIT)) { Shape inputShape = fullModel.expectShape(operation.getInputShape()); if (shapes.contains(inputShape) && shouldInlineInputShape(operationIndex, inputShape) && inputShape.getId().getName().equals(operation.getId().getName() + inputSuffix)) { inlineableShapes.add(operation.getInputShape()); } } if (!operation.getOutputShape().equals(UnitTypeTrait.UNIT)) { Shape outputShape = fullModel.expectShape(operation.getOutputShape()); if (shapes.contains(outputShape) && shouldInlineOutputShape(operationIndex, outputShape) && outputShape.getId().getName().equals(operation.getId().getName() + outputSuffix)) { inlineableShapes.add(operation.getOutputShape()); } } } return inlineableShapes; } private boolean shouldInlineInputShape(OperationIndex operationIndex, Shape target) { // Only allow coercion if the shape is only used as one input. return target.hasTrait(InputTrait.ID) || (shouldCoerceInlineIo && operationIndex.getInputBindings(target).size() == 1); } private boolean shouldInlineOutputShape(OperationIndex operationIndex, Shape target) { // Only allow coercion if the shape is only used as one output. return target.hasTrait(OutputTrait.ID) || (shouldCoerceInlineIo && operationIndex.getOutputBindings(target).size() == 1); } private Pair determineInlineSuffixes(Model fullModel, Collection shapes) { if (!inferInlineIoSuffixes) { return Pair.of(inlineInputSuffix, inlineOutputSuffix); } Map inputSuffixes = new LinkedHashMap<>(); Map outputSuffixes = new LinkedHashMap<>(); for (Shape shape : shapes) { if (!shape.isOperationShape()) { continue; } OperationShape operation = shape.asOperationShape().get(); StructureShape input = fullModel.expectShape(operation.getInputShape(), StructureShape.class); StructureShape output = fullModel.expectShape(operation.getOutputShape(), StructureShape.class); if (shapes.contains(input) && input.getId().getName().startsWith(operation.getId().getName())) { String inputSuffix = input.getId().getName().substring(operation.getId().getName().length()); int inputCount = inputSuffixes.getOrDefault(inputSuffix, 0); inputSuffixes.put(inputSuffix, ++inputCount); } if (shapes.contains(output) && output.getId().getName().startsWith(operation.getId().getName())) { String outputSuffix = output.getId().getName().substring(operation.getId().getName().length()); int outputCount = outputSuffixes.getOrDefault(outputSuffix, 0); outputSuffixes.put(outputSuffix, ++outputCount); } } String inputSuffix = inputSuffixes.entrySet() .stream() .max(Map.Entry.comparingByValue()) .map(Map.Entry::getKey) .orElse(inlineInputSuffix); String outputSuffix = outputSuffixes.entrySet() .stream() .max(Map.Entry.comparingByValue()) .map(Map.Entry::getKey) .orElse(inlineOutputSuffix); return Pair.of(inputSuffix, outputSuffix); } private String serializeHeader( Model fullModel, String namespace, Collection shapes, String inputSuffix, String outputSuffix ) { SmithyCodeWriter codeWriter = new SmithyCodeWriter(null, fullModel); NodeSerializer nodeSerializer = new NodeSerializer(codeWriter, fullModel); codeWriter.write("$$version: \"$L\"", Model.MODEL_VERSION); if (shapes.stream().anyMatch(Shape::isOperationShape)) { if (!inputSuffix.equals(DEFAULT_INLINE_INPUT_SUFFIX)) { codeWriter.write("$$operationInputSuffix: $S", inputSuffix); } if (!outputSuffix.equals(DEFAULT_INLINE_OUTPUT_SUFFIX)) { codeWriter.write("$$operationOutputSuffix: $S", outputSuffix); } } codeWriter.write(""); Comparator> comparator = componentOrder.metadataComparator(); // Write the full metadata into every output. When loaded back together the conflicts will be ignored, // but if they're separated out then each file will still have all the context. fullModel.getMetadata().entrySet().stream() .filter(entry -> metadataFilter.test(entry.getKey())) .sorted(comparator) .forEach(entry -> { codeWriter.trimTrailingSpaces(false) .writeInline("metadata $K = ", entry.getKey()) .trimTrailingSpaces(); nodeSerializer.serialize(entry.getValue()); codeWriter.write(""); }); if (!fullModel.getMetadata().isEmpty()) { codeWriter.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(); } /** * @return Returns a builder used to create a {@link SmithyIdlModelSerializer} */ public static Builder builder() { return new Builder(); } /** * Sorts shapes into files based on their namespace, where each file is named {namespace}.smithy. * * @param shape The shape to assign a file to. * @return Returns the file the given shape should be placed in. */ public static Path placeShapesByNamespace(Shape shape) { return Paths.get(shape.getId().getNamespace() + ".smithy"); } /** * Builder used to create {@link SmithyIdlModelSerializer}. */ public static final class Builder implements SmithyBuilder { private Predicate metadataFilter = FunctionalUtils.alwaysTrue(); private Predicate shapeFilter = FunctionalUtils.alwaysTrue(); private Predicate traitFilter = FunctionalUtils.alwaysTrue(); private Function shapePlacer = SmithyIdlModelSerializer::placeShapesByNamespace; private Path basePath = null; private boolean serializePrelude = false; private SmithyIdlComponentOrder componentOrder = SmithyIdlComponentOrder.PREFERRED; private String inlineInputSuffix = DEFAULT_INLINE_INPUT_SUFFIX; private String inlineOutputSuffix = DEFAULT_INLINE_OUTPUT_SUFFIX; private boolean inferInlineIoSuffixes = false; private boolean shouldCoerceInlineIo = false; 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. * *

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. */ public Builder shapePlacer(Function shapePlacer) { this.shapePlacer = Objects.requireNonNull(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; } /** * Enables serializing shapes in the Smithy prelude. * Defaults to false. * @return Returns the builder. */ public Builder serializePrelude() { this.serializePrelude = true; return this; } /** * Defines how components are sorted in the model, changing the default behavior of sorting alphabetically. * *

You can serialize metadata, shapes, and traits in the original order they were defined by setting * this to {@link SmithyIdlComponentOrder#SOURCE_LOCATION}. * * @param componentOrder Change how components are sorted. * @return Returns the builder. */ public Builder componentOrder(SmithyIdlComponentOrder componentOrder) { this.componentOrder = Objects.requireNonNull(componentOrder); return this; } /** * Defines what suffixes are checked on operation input shapes to determine whether * inline syntax should be used. * *

This will also set the "operationInputSuffix" control statement. * * @param inlineInputSuffix The suffix to use for inline operation input. * @return Returns the builder. */ public Builder inlineInputSuffix(String inlineInputSuffix) { this.inlineInputSuffix = inlineInputSuffix; return this; } /** * Defines what suffixes are checked on operation output shapes to determine whether * inline syntax should be used. * *

This will also set the "operationOutputSuffix" control statement. * * @param inlineOutputSuffix The suffix to use for inline operation output. * @return Returns the builder. */ public Builder inlineOutputSuffix(String inlineOutputSuffix) { this.inlineOutputSuffix = inlineOutputSuffix; return this; } /** * Determines whether the inline IO suffixes should be automatically determined. * *

If true, this will determine any shared IO suffixes for each file. Only the * shapes present within each file will impact what that file's suffixes will be. * *

The suffixes set by {@link #inlineInputSuffix(String)} and {@link #inlineOutputSuffix(String)} * will be the default values. * * @param shouldInferInlineIoSuffixes Whether inline IO suffixes should be inferred for each file. * @return Returns the builder. */ public Builder inferInlineIoSuffixes(boolean shouldInferInlineIoSuffixes) { this.inferInlineIoSuffixes = shouldInferInlineIoSuffixes; return this; } /** * Determines whether inline IO should be coerced for shapes operation input * and output that does not have the {@code @input} or {@code @output} trait, * respectively. * *

If true, this will determine any shared IO suffixes for each file. Only the * shapes present within each file will impact what that file's suffixes will be. * *

The suffixes set by {@link #inlineInputSuffix(String)} and {@link #inlineOutputSuffix(String)} * will be the default values. * * @param shouldCoerceInlineIo Whether inline IO should be coerced for each file. * @return Returns the builder. */ public Builder coerceInlineIo(boolean shouldCoerceInlineIo) { this.shouldCoerceInlineIo = shouldCoerceInlineIo; 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 Predicate traitFilter; private final Model model; private final Set inlineableShapes; private final SmithyIdlComponentOrder componentOrder; ShapeSerializer( SmithyCodeWriter codeWriter, NodeSerializer nodeSerializer, Predicate traitFilter, Model model, Set inlineableShapes, SmithyIdlComponentOrder componentOrder ) { this.codeWriter = codeWriter; this.nodeSerializer = nodeSerializer; this.traitFilter = traitFilter; this.model = model; this.inlineableShapes = inlineableShapes; this.componentOrder = componentOrder; } @Override protected Void getDefault(Shape shape) { serializeTraits(shape); codeWriter.writeInline("$L $L ", shape.getType(), shape.getId().getName()); writeMixins(shape); codeWriter.write("").write(""); return null; } private void shapeWithMembers(Shape shape, Collection members) { shapeWithMembers(shape, members, false); } private void shapeWithMembers(Shape shape, Collection members, boolean isEnum) { List nonMixinMembers = new ArrayList<>(); List mixinMembers = new ArrayList<>(); for (MemberShape member : members) { if (member.getMixins().isEmpty()) { nonMixinMembers.add(member); } else if (!member.getIntroducedTraits().isEmpty()) { mixinMembers.add(member); } } serializeTraits(shape); // IDL V2 does not support sets, so convert set to list when serializing. String v2Type = shape.getType() == ShapeType.SET ? ShapeType.LIST.toString() : shape.getType().toString(); codeWriter.writeInline("$L $L ", v2Type, shape.getId().getName()); writeMixins(shape); if (isEnum) { writeEnumMembers(nonMixinMembers); } else { writeShapeMembers(nonMixinMembers); } codeWriter.write(""); applyIntroducedTraits(mixinMembers); } private void writeMixins(Shape shape) { if (shape.getMixins().size() == 1) { codeWriter.writeInline("with [$I] ", shape.getMixins().iterator().next()); } else if (shape.getMixins().size() > 1) { codeWriter.write("with [").indent(); for (ShapeId id : shape.getMixins()) { // Trailing spaces are trimmed. codeWriter.write("$I", id); } codeWriter.dedent().writeInline("] "); } } private void writeShapeMembers(Collection members) { if (members.isEmpty()) { // When the are no members to write, put "{}" on the same line. codeWriter.writeInline("{}").write(""); } else { codeWriter.openBlock("{", "}", () -> { for (MemberShape member : members) { serializeTraits(member.getAllTraits(), TraitFeature.MEMBER); String assignment = ""; if (member.hasTrait(DefaultTrait.class)) { assignment = " = " + Node.printJson(member.expectTrait(DefaultTrait.class).toNode()); } codeWriter.write("$L: $I$L", member.getMemberName(), member.getTarget(), assignment); } }); } } private void writeEnumMembers(Collection members) { if (members.isEmpty()) { codeWriter.writeInline("{}").write(""); return; } codeWriter.openBlock("{", "}", () -> { for (MemberShape member : members) { Map traits = new LinkedHashMap<>(member.getAllTraits()); Optional stringValue = member.expectTrait(EnumValueTrait.class).getStringValue(); boolean hasNormalName = stringValue.isPresent() && member.getMemberName().equals(stringValue.get()); String assignment = ""; if (!hasNormalName) { assignment = " = " + Node.printJson(member.expectTrait(EnumValueTrait.class).toNode()); } traits.remove(EnumValueTrait.ID); serializeTraits(traits); codeWriter.write("$L$L", member.getMemberName(), assignment); } }); } private void applyIntroducedTraits(Collection members) { for (MemberShape member : members) { Map introducedTraits = new LinkedHashMap<>(member.getIntroducedTraits()); // The @enumValue trait is serialized using the `=` IDL syntax, so remove it here. introducedTraits.remove(EnumValueTrait.ID); // Use short form for a single trait, and block form for multiple traits. if (introducedTraits.size() == 1) { codeWriter.writeInline("apply $I ", member.getId()); serializeTraits(member.getIntroducedTraits(), TraitFeature.NO_SPECIAL_DOCS_SYNTAX); codeWriter.write(""); } else if (!introducedTraits.isEmpty()) { codeWriter.openBlock("apply $I {", "}", member.getId(), () -> { // Only serialize local traits, and don't use special documentation syntax here. serializeTraits(member.getIntroducedTraits(), TraitFeature.NO_SPECIAL_DOCS_SYNTAX); }).write(""); } } } private void serializeTraits(Shape shape) { serializeTraits(shape.getIntroducedTraits()); } private void serializeTraits(Map traits, TraitFeature... traitFeatures) { boolean noSpecialDocsSyntax = TraitFeature.NO_SPECIAL_DOCS_SYNTAX.hasFeature(traitFeatures); boolean isMember = TraitFeature.MEMBER.hasFeature(traitFeatures); // The documentation trait always needs to be serialized first since it uses special syntax. if (!noSpecialDocsSyntax && traits.containsKey(DocumentationTrait.ID)) { Trait documentation = traits.get(DocumentationTrait.ID); if (traitFilter.test(documentation)) { serializeDocumentation(documentation.toNode().expectStringNode().getValue()); } } Comparator traitComparator = componentOrder.toShapeIdComparator(); traits.values().stream() .filter(trait -> noSpecialDocsSyntax || !(trait instanceof DocumentationTrait)) // The default and enumValue traits are serialized using the assignment syntactic sugar. .filter(trait -> { if (trait instanceof EnumValueTrait) { return false; } else { // Default traits are serialized normally for non-members, but omitted for members. return !isMember || !(trait instanceof DefaultTrait); } }) .filter(traitFilter) .sorted(traitComparator) .forEach(this::serializeTrait); } private void serializeDocumentation(String documentation) { // The documentation trait has a special syntax, which we always want to use. codeWriter .pushState() // See https://github.com/smithy-lang/smithy/issues/2115 .trimTrailingSpaces(false) .setNewlinePrefix("/// ") .write(documentation.replace("$", "$$")) .popState(); } private void serializeTrait(Trait trait) { Node node = trait.toNode(); // We won't fail if the trait can't be found. Shape shape = model.getShape(trait.toShapeId()).orElse(null); if (shape != null && (trait instanceof AnnotationTrait || isEmptyStructure(node, shape))) { // Traits that inherit from AnnotationTrait specifically can omit a value. // Traits that are simply boolean shapes which don't implement AnnotationTrait cannot. // Additionally, empty structure traits can omit a value. 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 enumShape(EnumShape shape) { shapeWithMembers(shape, shape.members(), true); return null; } @Override public Void intEnumShape(IntEnumShape shape) { shapeWithMembers(shape, shape.members(), true); return null; } @Override public Void listShape(ListShape 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, shape.members()); return null; } @Override public Void unionShape(UnionShape shape) { shapeWithMembers(shape, shape.members()); return null; } @Override public Void serviceShape(ServiceShape shape) { serializeTraits(shape); codeWriter.writeInline("service $L ", shape.getId().getName()); writeMixins(shape); codeWriter.openBlock("{"); if (!StringUtils.isBlank(shape.getIntroducedVersion())) { codeWriter.write("version: $S", shape.getIntroducedVersion()); } codeWriter.writeOptionalIdList("operations", shape.getIntroducedOperations()); codeWriter.writeOptionalIdList("resources", shape.getIntroducedResources()); codeWriter.writeOptionalIdList("errors", shape.getIntroducedErrors()); if (!shape.getIntroducedRename().isEmpty()) { codeWriter.openBlock("rename: {", "}", () -> { for (Map.Entry entry : shape.getIntroducedRename().entrySet()) { codeWriter.write("$S: $S", entry.getKey(), entry.getValue()); } }); } codeWriter.closeBlock("}").write(""); return null; } @Override public Void resourceShape(ResourceShape shape) { serializeTraits(shape); codeWriter.writeInline("resource $L ", shape.getId().getName()); writeMixins(shape); codeWriter.openBlock("{"); if (!shape.getIdentifiers().isEmpty()) { codeWriter.openBlock("identifiers: {"); shape.getIdentifiers().entrySet().stream() .sorted(Map.Entry.comparingByKey()) .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.getIntroducedOperations()); codeWriter.writeOptionalIdList("collectionOperations", shape.getCollectionOperations()); codeWriter.writeOptionalIdList("resources", shape.getIntroducedResources()); if (shape.hasProperties()) { codeWriter.openBlock("properties: {"); shape.getProperties().forEach((name, shapeId) -> codeWriter.write("$L: $I", name, shapeId)); codeWriter.closeBlock("}"); } codeWriter.closeBlock("}"); codeWriter.write(""); return null; } @Override public Void operationShape(OperationShape shape) { serializeTraits(shape); codeWriter.writeInline("operation $L ", shape.getId().getName()); writeMixins(shape); codeWriter.openBlock("{"); List mixinMembers = new ArrayList<>(); mixinMembers.addAll(writeInlineableProperty("input", shape.getInputShape(), InputTrait.ID)); mixinMembers.addAll(writeInlineableProperty("output", shape.getOutputShape(), OutputTrait.ID)); codeWriter.writeOptionalIdList("errors", shape.getIntroducedErrors()); codeWriter.closeBlock("}"); codeWriter.write(""); applyIntroducedTraits(mixinMembers); return null; } private Collection writeInlineableProperty(String key, ShapeId shapeId, ShapeId defaultTrait) { if (!inlineableShapes.contains(shapeId)) { codeWriter.write("$L: $I", key, shapeId); return Collections.emptyList(); } StructureShape structure = model.expectShape(shapeId, StructureShape.class); if (hasOnlyDefaultTrait(structure, defaultTrait)) { codeWriter.writeInline("$L := ", key); } else { codeWriter.write("$L := ", key); codeWriter.indent(); Map traits = structure.getAllTraits(); if (defaultTrait != null) { traits = new HashMap<>(traits); traits.remove(defaultTrait); } serializeTraits(traits); } List nonMixinMembers = new ArrayList<>(); List mixinMembers = new ArrayList<>(); for (MemberShape member : structure.members()) { if (member.getMixins().isEmpty()) { nonMixinMembers.add(member); } else if (!member.getIntroducedTraits().isEmpty()) { mixinMembers.add(member); } } writeMixins(structure); writeShapeMembers(nonMixinMembers); if (!hasOnlyDefaultTrait(structure, defaultTrait)) { codeWriter.dedent(); } return mixinMembers; } private boolean hasOnlyDefaultTrait(Shape shape, ShapeId defaultTrait) { return shape.getAllTraits().isEmpty() || (shape.getAllTraits().size() == 1 && shape.hasTrait(defaultTrait)); } } /** * 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. */ private 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. */ 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)) { 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; } return shape.getMemberTrait(model, IdRefTrait.class).isPresent(); } 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.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. */ private 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() .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 StructureShape || shape instanceof UnionShape) { member = members.get(name.getValue()); } else { // At this point the shape is either null or a document shape. member = shape; } codeWriter.writeInline("\n$K: ", name.getValue()); serialize(value, member); }); 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_KEY_STRING = Pattern.compile("[a-zA-Z_][a-zA-Z_0-9]*"); 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('K', this::optionallyQuoteKey); } /** * Opens a block without writing indentation whitespace or inserting a newline. */ private SmithyCodeWriter openBlockInline(String content, Object... args) { writeInline(content, args).indent(); return this; } /** * Closes a block without inserting a newline. */ private 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. */ private 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. */ private SmithyCodeWriter writeOptionalIdList(String textBeforeList, Collection shapeIds) { if (shapeIds.isEmpty()) { return this; } openBlock("$L: [", textBeforeList); shapeIds.stream().sorted().forEach(shapeId -> write("$I", shapeId)); closeBlock("]"); return this; } /** * Formatter that quotes (and escapes) a string unless it's a valid object key string. */ private String optionallyQuoteKey(Object key, String indent) { String formatted = AbstractCodeWriter.formatLiteral(key); if (UNQUOTED_KEY_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() .map(shapeId -> String.format("use %s", shapeId.toString())) .collect(Collectors.joining("\n")); return importString + "\n\n" + contents; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy