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

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

/*
 * Copyright 2019 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.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.SourceLocation;
import software.amazon.smithy.model.loader.Prelude;
import software.amazon.smithy.model.node.ArrayNode;
import software.amazon.smithy.model.node.Node;
import software.amazon.smithy.model.node.ObjectNode;
import software.amazon.smithy.model.node.StringNode;
import software.amazon.smithy.model.traits.AddedDefaultTrait;
import software.amazon.smithy.model.traits.BoxTrait;
import software.amazon.smithy.model.traits.ClientOptionalTrait;
import software.amazon.smithy.model.traits.DefaultTrait;
import software.amazon.smithy.model.traits.NotPropertyTrait;
import software.amazon.smithy.model.traits.PropertyTrait;
import software.amazon.smithy.model.traits.Trait;
import software.amazon.smithy.model.transform.ModelTransformer;
import software.amazon.smithy.utils.FunctionalUtils;
import software.amazon.smithy.utils.SetUtils;
import software.amazon.smithy.utils.SmithyBuilder;
import software.amazon.smithy.utils.StringUtils;

/**
 * Serializes a {@link Model} to an {@link ObjectNode}.
 *
 * 

The serialized value sorts all map key-value pairs so that they contain * a consistent key ordering, reducing noise in diffs based on * serialized model. * *

After serializing to an ObjectNode, the node can then be serialized * to formats like JSON, YAML, Ion, etc. */ public final class ModelSerializer { // Explicitly filter out these traits. While some of these are automatically removed in the downgradeToV1 // model transformation, calling them out here explicitly is a defense in depth. This also has to remove all // default traits from the output, whereas the downgradeToV1 transform only removes unnecessary default traits // that don't correlate to boxing in V1 models. private static final Set V2_TRAITS_TO_FILTER_FROM_V1 = SetUtils.of( DefaultTrait.ID, AddedDefaultTrait.ID, ClientOptionalTrait.ID, PropertyTrait.ID, NotPropertyTrait.ID); private final Predicate metadataFilter; private final Predicate shapeFilter; private final Predicate traitFilter; private final String version; private ModelSerializer(Builder builder) { metadataFilter = builder.metadataFilter; version = builder.version; if (!builder.includePrelude) { shapeFilter = builder.shapeFilter.and(FunctionalUtils.not(Prelude::isPreludeShape)); } else if (version.equals("1.0")) { throw new UnsupportedOperationException("Cannot serialize prelude and set model version to 1.0"); } else { shapeFilter = builder.shapeFilter; } if (version.equals("1.0")) { traitFilter = builder.traitFilter.and(trait -> { if (trait.toShapeId().equals(BoxTrait.ID)) { // Include the box trait in 1.0 models. return true; } else if (V2_TRAITS_TO_FILTER_FROM_V1.contains(trait.toShapeId())) { // Exclude V2 specific traits. return false; } else { return !trait.isSynthetic(); } }); } else { // 2.0 models just need to filter out synthetic traits, including box. traitFilter = builder.traitFilter.and(FunctionalUtils.not(Trait::isSynthetic)); } } public ObjectNode serialize(Model model) { ShapeSerializer shapeSerializer = new ShapeSerializer(); if (version.equals("1.0")) { model = ModelTransformer.create().downgradeToV1(model); } ObjectNode.Builder builder = Node.objectNodeBuilder() .withMember("smithy", Node.from(version)) .withOptionalMember("metadata", createMetadata(model).map(Node::withDeepSortedKeys)); // Sort shapes by ID. Map shapes = new TreeMap<>(); for (Shape shape : model.toSet()) { // Members are serialized inside of other shapes, so filter them out. if (!shape.isMemberShape() && shapeFilter.test(shape)) { Node value = shape.accept(shapeSerializer); shapes.put(Node.from(shape.getId().toString()), value); // Add any necessary apply statements to inherited mixin members that added traits, but only if there // are actually traits to serialize. Apply statements are used here instead of redefining members on // structures because apply statements are more resilient to change over time if the shapes targeted by // an inherited member changes. if (!shapeSerializer.mixinMemberTraits.isEmpty()) { for (MemberShape member : shapeSerializer.mixinMemberTraits) { Map introducedTraits = createIntroducedTraitsMap( member.getIntroducedTraits().values()); if (!introducedTraits.isEmpty()) { ObjectNode.Builder applyBuilder = Node.objectNodeBuilder(); applyBuilder.withMember("type", "apply"); ObjectNode traits = serializeTraits(applyBuilder, introducedTraits).build(); shapes.put(Node.from(member.getId().toString()), traits); } } } } } builder.withMember("shapes", new ObjectNode(shapes, SourceLocation.NONE)); return builder.build(); } private Optional createMetadata(Model model) { // Grab metadata, filter by key using the predicate. Map metadata = model.getMetadata().entrySet().stream() .filter(entry -> metadataFilter.test(entry.getKey())) .collect(Collectors.toMap(entry -> Node.from(entry.getKey()), Map.Entry::getValue)); return metadata.isEmpty() ? Optional.empty() : Optional.of(new ObjectNode(metadata, SourceLocation.NONE)); } /** * @return Returns a builder used to create a {@link ModelSerializer}. */ public static Builder builder() { return new Builder(); } /** * Builder used to create {@link ModelSerializer}. */ public static final class Builder implements SmithyBuilder { private Predicate metadataFilter = FunctionalUtils.alwaysTrue(); private Predicate shapeFilter = FunctionalUtils.alwaysTrue(); private boolean includePrelude = false; private Predicate traitFilter = FunctionalUtils.alwaysTrue(); private String version = "2.0"; private 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; } /** * Enables or disables including the prelude in the serialized model. * *

By default, the prelude is not included. * *

This should nearly always be left at default, as per the spec the prelude is * inherently part of every model, and so any Smithy implementation must build in * an understanding of the prelude. Disabling this filter can be useful for those * implementations to allow them to build their understanding of it from a JSON * version of the prelude. * * @param includePrelude boolean indicating whether the prelude should be included or not. * @return Returns the builder. */ public Builder includePrelude(boolean includePrelude) { this.includePrelude = includePrelude; 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; } /** * Sets the IDL version to serialize. Defaults to 2.0. * *

Version "1.0" serialization cannot be used with {@link #includePrelude}. * * @param version IDL version to set. Can be "1", "1.0", "2", or "2.0". * "1" and "2" are normalized to "1.0" and "2.0". * @return Returns the builder. */ public Builder version(String version) { switch (version) { case "2": case "2.0": this.version = "2.0"; break; case "1": case "1.0": this.version = "1.0"; break; default: throw new IllegalArgumentException("Unsupported Smithy model version: " + version); } return this; } @Override public ModelSerializer build() { return new ModelSerializer(this); } } private ObjectNode.Builder serializeTraits(ObjectNode.Builder builder, Collection traits) { return serializeTraits(builder, createIntroducedTraitsMap(traits)); } private ObjectNode.Builder serializeTraits(ObjectNode.Builder builder, Map traits) { if (!traits.isEmpty()) { builder.withMember("traits", new ObjectNode(traits, SourceLocation.none())); } return builder; } private Map createIntroducedTraitsMap(Collection traits) { if (traits.isEmpty()) { return Collections.emptyMap(); } else { Map traitsToAdd = new TreeMap<>(); for (Trait trait : traits) { if (traitFilter.test(trait)) { traitsToAdd.put(Node.from(trait.toShapeId().toString()), trait.toNode()); } } return traitsToAdd; } } private final class ShapeSerializer extends ShapeVisitor.Default { private final Set mixinMemberTraits = new TreeSet<>(); private ObjectNode.Builder createTypedBuilder(Shape shape) { ObjectNode.Builder builder = Node.objectNodeBuilder() .withMember("type", Node.from(shape.getType().toString())); if (!shape.getMixins().isEmpty()) { List mixins = new ArrayList<>(shape.getMixins().size()); for (ShapeId mixin : shape.getMixins()) { mixins.add(serializeReference(mixin)); } builder.withMember("mixins", Node.fromNodes(mixins)); } return builder; } private ObjectNode.Builder serializeAllTraits(Shape shape, ObjectNode.Builder builder) { return serializeTraits(builder, shape.getIntroducedTraits().values()); } @Override protected ObjectNode getDefault(Shape shape) { return serializeAllTraits(shape, createTypedBuilder(shape)).build(); } @Override public Node enumShape(EnumShape shape) { return createNamedMemberShape(shape, shape.getAllMembers()); } @Override public Node intEnumShape(IntEnumShape shape) { return createNamedMemberShape(shape, shape.getAllMembers()); } @Override public Node listShape(ListShape shape) { ObjectNode.Builder result = createTypedBuilder(shape); // This visitor covers sets and lists. Sets aren't supported in IDL v2, so convert to list. result.withMember("type", ShapeType.LIST.toString()); mixinMember(result, shape.getMember(), "member"); return serializeAllTraits(shape, result).build(); } private void mixinMember(ObjectNode.Builder builder, MemberShape member, String key) { if (member.getMixins().isEmpty()) { builder.withMember(key, member.accept(this)); } else if (!member.getIntroducedTraits().isEmpty()) { mixinMemberTraits.add(member); } } @Override public Node mapShape(MapShape shape) { ObjectNode.Builder result = createTypedBuilder(shape); mixinMember(result, shape.getKey(), "key"); mixinMember(result, shape.getValue(), "value"); return serializeAllTraits(shape, result).build(); } @Override public Node operationShape(OperationShape shape) { return serializeAllTraits(shape, createTypedBuilder(shape) .withMember("input", serializeReference(shape.getInputShape())) .withMember("output", serializeReference(shape.getOutputShape())) .withOptionalMember("errors", createOptionalIdList(shape.getIntroducedErrors()))) .build(); } @Override public Node resourceShape(ResourceShape shape) { Optional identifiers = Optional.empty(); Optional properties = Optional.empty(); if (shape.hasIdentifiers()) { Stream> ids = shape.getIdentifiers().entrySet().stream(); identifiers = Optional.of(ids.collect(ObjectNode.collectStringKeys( Map.Entry::getKey, entry -> serializeReference(entry.getValue())))); } if (shape.hasProperties()) { Stream> props = shape.getProperties().entrySet().stream(); properties = Optional.of(props.collect(ObjectNode.collectStringKeys( Map.Entry::getKey, entry -> serializeReference(entry.getValue())))); } return serializeAllTraits(shape, createTypedBuilder(shape) .withOptionalMember("identifiers", identifiers) .withOptionalMember("properties", properties) .withOptionalMember("put", shape.getPut().map(this::serializeReference)) .withOptionalMember("create", shape.getCreate().map(this::serializeReference)) .withOptionalMember("read", shape.getRead().map(this::serializeReference)) .withOptionalMember("update", shape.getUpdate().map(this::serializeReference)) .withOptionalMember("delete", shape.getDelete().map(this::serializeReference)) .withOptionalMember("list", shape.getList().map(this::serializeReference)) .withOptionalMember("operations", createOptionalIdList(shape.getIntroducedOperations())) .withOptionalMember("collectionOperations", createOptionalIdList(shape.getCollectionOperations())) .withOptionalMember("resources", createOptionalIdList(shape.getIntroducedResources()))) .build(); } @Override public Node serviceShape(ServiceShape shape) { ObjectNode.Builder serviceBuilder = createTypedBuilder(shape); if (!StringUtils.isBlank(shape.getIntroducedVersion())) { serviceBuilder.withMember("version", Node.from(shape.getIntroducedVersion())); } serviceBuilder.withOptionalMember("operations", createOptionalIdList(shape.getIntroducedOperations())); serviceBuilder.withOptionalMember("resources", createOptionalIdList(shape.getIntroducedResources())); serviceBuilder.withOptionalMember("errors", createOptionalIdList(shape.getIntroducedErrors())); if (!shape.getIntroducedRename().isEmpty()) { ObjectNode.Builder renameBuilder = Node.objectNodeBuilder(); for (Map.Entry entry : shape.getIntroducedRename().entrySet()) { renameBuilder.withMember(entry.getKey().toString(), entry.getValue()); } serviceBuilder.withMember("rename", renameBuilder.build()); } // Serialize traits last, after named structure properties. return serializeAllTraits(shape, serviceBuilder).build(); } private Optional createOptionalIdList(Collection list) { if (list.isEmpty()) { return Optional.empty(); } Node result = list.stream() .sorted() .map(this::serializeReference) .collect(ArrayNode.collect()); return Optional.of(result); } @Override public Node structureShape(StructureShape shape) { return createNamedMemberShape(shape, shape.getAllMembers()); } @Override public Node unionShape(UnionShape shape) { return createNamedMemberShape(shape, shape.getAllMembers()); } private ObjectNode createNamedMemberShape(Shape shape, Map members) { ObjectNode.Builder result = createTypedBuilder(shape); ObjectNode.Builder membersBuilder = ObjectNode.objectNodeBuilder(); for (MemberShape member : members.values()) { mixinMember(membersBuilder, member, member.getMemberName()); } result.withMember("members", membersBuilder.build()); return serializeTraits(result, shape.getIntroducedTraits().values()).build(); } @Override public Node memberShape(MemberShape shape) { // Only serialize traits introduced by the member. Collection introducedTraits = shape.getIntroducedTraits().values(); return serializeTraits(serializeReferenceBuilder(shape.getTarget()), introducedTraits).build(); } private ObjectNode.Builder serializeReferenceBuilder(ShapeId id) { return Node.objectNodeBuilder().withMember("target", id.toString()); } private ObjectNode serializeReference(ShapeId id) { return serializeReferenceBuilder(id).build(); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy