diff --git a/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/trace/ArtifactDefinitions.java b/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/trace/ArtifactDefinitions.java new file mode 100644 index 00000000000..d198fe8edd9 --- /dev/null +++ b/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/trace/ArtifactDefinitions.java @@ -0,0 +1,170 @@ +/* + * 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.codegen.core.trace; + +import java.util.HashMap; +import java.util.Map; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.NodeMapper; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.node.ToNode; +import software.amazon.smithy.utils.MapUtils; +import software.amazon.smithy.utils.SmithyBuilder; +import software.amazon.smithy.utils.ToSmithyBuilder; + +/** + * Class that defines the acceptable values that can be used in {@link ShapeLink} objects. + */ +public final class ArtifactDefinitions implements ToNode, ToSmithyBuilder { + public static final String TYPE_TEXT = "types"; + public static final String TAGS_TEXT = "tags"; + + private Map tags; + private Map types; + + private ArtifactDefinitions(Builder builder) { + if (builder.tags.isEmpty()) { + throw new IllegalStateException("ArtifactDefinition's Tags field must not be empty."); + } + if (builder.types.isEmpty()) { + throw new IllegalStateException("ArtifactDefinition's Types field must not be empty."); + } + tags = MapUtils.copyOf(builder.tags); + types = MapUtils.copyOf(builder.types); + } + + /** + * Converts an ObjectNode that represents the definitions section of the + * trace file into a types maps and tags map. + * + * @param value ObjectNode that contains the JSON data inside the definitions tag of + * the trace file + * @return an ArtifactDefinitions object created from the ObjectNode. + */ + public static ArtifactDefinitions fromNode(Node value) { + NodeMapper mapper = new NodeMapper(); + mapper.disableFromNodeForClass(ArtifactDefinitions.class); + return mapper.deserialize(value, ArtifactDefinitions.class); + } + + public static Builder builder() { + return new Builder(); + } + + /** + * Converts the types and tags Maps into a single ObjectNode. + * + * @return an ObjectNode that contains two ObjectNode children; one contains the tag map structure the + * other contains the type map structure. + */ + @Override + public ObjectNode toNode() { + return ObjectNode.objectNodeBuilder() + .withMember(TAGS_TEXT, ObjectNode.fromStringMap(tags)) + .withMember(TYPE_TEXT, ObjectNode.fromStringMap(types)) + .build(); + } + + /** + * Gets this Definition's Tags Map. + * The tags {@link Map} defines the set of tags that can be used in a {@link ShapeLink} object. + * Each key is the name of the tag, and each value is the description of the tag. + * Tags are used to provide semantics to links. Tools that evaluate trace models + * use these tags to inform their analysis. For example, a tag for an AWS SDK code + * generator could be "requestBuilder" to indicate that a class is used as a builder for a request. + * + * @return this Definition's Tags Map + */ + public Map getTags() { + return tags; + } + + /** + * Gets this Definition's Types Map. + * The types {@link Map} defines the set of types that can be used in a {@link ShapeLink} object. + * Each key is the name of the type, and each value is a description of the type. + * For programming languages, these types represent language-specific components. + * For example, in Java, types should map to the possible values of {@link java.lang.annotation.ElementType}. + * + * @return this Definition's Type's Map + */ + public Map getTypes() { + return types; + } + + /** + * Take this object and create a builder that contains all of the + * current property values of this object. + * + * @return a builder for type T + */ + @Override + public Builder toBuilder() { + return builder() + .tags(tags) + .types(types); + } + + public static final class Builder implements SmithyBuilder { + private final Map tags = new HashMap<>(); + private final Map types = new HashMap<>(); + + /** + * @return Definitions object from this builder. + */ + public ArtifactDefinitions build() { + return new ArtifactDefinitions(this); + } + + public Builder tags(Map tags) { + this.tags.clear(); + this.tags.putAll(tags); + return this; + } + + public Builder types(Map types) { + this.types.clear(); + this.types.putAll(types); + return this; + } + + /** + * Adds the tag's key, value pair to the tags map. + * + * @param name Name of tag. + * @param description Description of tag. + * @return This builder. + */ + public Builder addTag(String name, String description) { + this.tags.put(name, description); + return this; + } + + /** + * Adds the type's key, value pair to the tags map. + * + * @param name Key of type. + * @param description Value of type. + * @return This builder. + */ + public Builder addType(String name, String description) { + this.types.put(name, description); + return this; + } + + } + +} diff --git a/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/trace/ShapeLink.java b/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/trace/ShapeLink.java new file mode 100644 index 00000000000..ccfc0cd3dd0 --- /dev/null +++ b/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/trace/ShapeLink.java @@ -0,0 +1,258 @@ +/* + * 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.codegen.core.trace; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.NodeMapper; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.node.ToNode; +import software.amazon.smithy.utils.ListUtils; +import software.amazon.smithy.utils.SmithyBuilder; +import software.amazon.smithy.utils.ToSmithyBuilder; + +/** + * Class that defines a link between the Smithy {@link software.amazon.smithy.model.shapes.Shape} and + * the artifact that it produced. + */ +public final class ShapeLink implements ToNode, ToSmithyBuilder { + public static final String TYPE_TEXT = "type"; + public static final String ID_TEXT = "id"; + public static final String TAGS_TEXT = "tags"; + public static final String FILE_TEXT = "file"; + public static final String LINE_TEXT = "line"; + public static final String COLUMN_TEXT = "column"; + + private String type; + private String id; + private List tags; //optional + private String file; //optional + private Integer line; //optional + private Integer column; //optional + + private ShapeLink(Builder builder) { + type = SmithyBuilder.requiredState(TYPE_TEXT, builder.type); + id = SmithyBuilder.requiredState(ID_TEXT, builder.id); + tags = ListUtils.copyOf(builder.tags); + file = builder.file; + line = builder.line; + column = builder.column; + } + + /** + * Instantiates ShapeLink instance variables by extracting data from an ObjectNode. + * + * @param value an ObjectNode that represents the a single ShapeLink + * @return a ShapeLink created from the ObjectNode + */ + public static ShapeLink fromNode(Node value) { + NodeMapper mapper = new NodeMapper(); + mapper.disableFromNodeForClass(ShapeLink.class); + return mapper.deserialize(value, ShapeLink.class); + } + + public static Builder builder() { + return new Builder(); + } + + /** + * Converts instance variables into an ObjectNode for writing out a ShapeLink. + * + * @return returns an ObjectNode that contains the StringNodes with the information + * from a ShapeLink + */ + @Override + public ObjectNode toNode() { + ObjectNode.Builder builder = ObjectNode.objectNodeBuilder() + .withMember(ID_TEXT, id) + .withMember(TYPE_TEXT, type) + .withOptionalMember(FILE_TEXT, getFile().map(Node::from)) + .withOptionalMember(LINE_TEXT, getLine().map(Node::from)) + .withOptionalMember(COLUMN_TEXT, getColumn().map(Node::from)); + + if (!tags.isEmpty()) { + builder.withMember(TAGS_TEXT, Node.fromStrings(tags)); + } + + return builder.build(); + } + + /** + * Gets this ShapeLink's type. + * The type is the type of the artifact component. This value MUST correspond to one of the types defined + * in the /definitions/types property of the trace file. + * + * @return this ShapeLink's type + */ + public String getType() { + return type; + } + + /** + * Gets this ShapeLink's id. + * Id is the artifact-specific identifier for the artifact component. For example, in Java a valid id + * would be the fully-qualified name of a class, method, or field as defined in + * Documentation Comment + * Specification for the Standard Doclet + * + * @return this ShapeLink's id + */ + public String getId() { + return id; + } + + /** + * Gets this ShapeLink's tags list. + * Tags defines a list of tags to apply to the trace link. Each tag MUST correspond to a tag defined in + * the /definitions/tags property of the trace file. + * + * @return This ShapeLink's list of tags + */ + public List getTags() { + return tags; + } + + /** + * Gets this ShapeLink's file in an optional container. + * File is a URI that defines the location of the artifact component. Files MUST use the "file" URI scheme, + * and SHOULD be relative. + * + * @return Optional container holding this ShapeLink's file + */ + public Optional getFile() { + return Optional.ofNullable(file); + } + + /** + * Gets this ShapeLink's line number in an optional container. + * Line is the line number in the file that contains the artifact component. + * + * @return Optional container holding this ShapeLink's line number + */ + public Optional getLine() { + return Optional.ofNullable(line); + } + + /** + * Gets this ShapeLink's column number in an optional container. + * Column is the column number in the file that contains the artifact component. + * + * @return Optional container holding this ShapeLink's column number + */ + public Optional getColumn() { + return Optional.ofNullable(column); + } + + /** + * Take this object and create a builder that contains all of the + * current property values of this object. + * + * @return a builder for type T + */ + @Override + public Builder toBuilder() { + return builder() + .id(id) + .column(column) + .type(type) + .tags(tags) + .line(line) + .file(file); + } + + public static final class Builder implements SmithyBuilder { + private final List tags = new ArrayList<>(); + private String type; + private String id; + private String file; + private Integer line; + private Integer column; + + public ShapeLink build() { + return new ShapeLink(this); + } + + public Builder type(String type) { + this.type = type; + return this; + } + + public Builder id(String id) { + this.id = id; + return this; + } + + /** + * Sets tags list of a ShapeLink. + * + * @param tags list of tags. + * @return This builder. + */ + public Builder tags(List tags) { + this.tags.clear(); + this.tags.addAll(tags); + return this; + } + + /** + * Adds a tag to the tags list of a ShapeLink. + * + * @param tag tag to add. + * @return This builder. + */ + public Builder addTag(String tag) { + this.tags.add(tag); + return this; + } + + /** + * Sets File of a ShapeLink. + * + * @param file File. + * @return This builder. + */ + public Builder file(String file) { + this.file = file; + return this; + } + + /** + * Sets line of a ShapeLink. + * + * @param line Line number in artifact file. + * @return This builder. + */ + public Builder line(Integer line) { + this.line = line; + return this; + } + + /** + * Sets tags list of a ShapeLink. + * + * @param column Column number in artifact file. + * @return This builder. + */ + public Builder column(Integer column) { + this.column = column; + return this; + } + + } + +} diff --git a/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/trace/TraceFile.java b/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/trace/TraceFile.java new file mode 100644 index 00000000000..31bcffd7d51 --- /dev/null +++ b/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/trace/TraceFile.java @@ -0,0 +1,374 @@ +/* + * 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.codegen.core.trace; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +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.ExpectationNotMetException; +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.node.ToNode; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.utils.MapUtils; +import software.amazon.smithy.utils.SmithyBuilder; +import software.amazon.smithy.utils.ToSmithyBuilder; + +/** + * Class that represents the contents of a Smithy trace file. + * TraceFile's require a smithyTrace file version number, {@link TraceMetadata}, and + * {@link Map} from {@link ShapeId} to a List of {@link ShapeLink} objects. TraceFile's + * optionally have a {@link ArtifactDefinitions} object. TraceFile handles parsing, serialization + * and deserialization of a Smithy trace file. + */ +public final class TraceFile implements ToNode, ToSmithyBuilder { + public static final String SMITHY_TRACE_TEXT = "smithyTrace"; + public static final String METADATA_TEXT = "metadata"; + public static final String DEFINITIONS_TEXT = "definitions"; + public static final String SHAPES_TEXT = "shapes"; + public static final String SMITHY_TRACE_VERSION = "1.0"; + + private String smithyTrace; + private TraceMetadata metadata; + private ArtifactDefinitions artifactDefinitions; //Optional + private Map> shapes; + + private TraceFile(Builder builder) { + smithyTrace = SmithyBuilder.requiredState(SMITHY_TRACE_TEXT, builder.smithyTrace); + metadata = SmithyBuilder.requiredState(METADATA_TEXT, builder.metadata); + if (builder.shapes.isEmpty()) { + throw new IllegalStateException("TraceFile's shapes field must not be empty to build it."); + } + shapes = MapUtils.copyOf(builder.shapes); + artifactDefinitions = builder.artifactDefinitions; + + //validate we received a TraceFile where ShapeLink types and tags match definitions + validateTypesAndTags(); + } + + /** + * Converts ObjectNode into TraceFile. + * + * @param value an ObjectNode that represents the entire trace file. + */ + public static TraceFile fromNode(Node value) { + ObjectNode node = value.expectObjectNode(); + Builder builder = builder() + .smithyTrace(node.expectStringMember(SMITHY_TRACE_TEXT).getValue()) + .metadata(TraceMetadata.fromNode(node.expectObjectMember(METADATA_TEXT))); + + //parse shapes + Map shapeMap = node.expectObjectMember(SHAPES_TEXT).getMembers(); + for (Map.Entry entry : shapeMap.entrySet()) { + for (Node linkNode : (entry.getValue().expectArrayNode()).getElements()) { + builder.addShapeLink(entry.getKey().getValue(), ShapeLink.fromNode(linkNode)); + } + } + + //parse definitions + if (node.containsMember(DEFINITIONS_TEXT)) { + builder.definitions(ArtifactDefinitions.fromNode(node.expectObjectMember(DEFINITIONS_TEXT))); + } + + return builder.build(); + } + + public static Builder builder() { + return new Builder(); + } + + /** + * Converts TraceFile instance variables into an + * ObjectNode. + * + * @return ObjectNode representation of a TraceFile. + */ + @Override + public ObjectNode toNode() { + //constructing shapes ObjectNode map + ObjectNode.Builder shapesBuilder = ObjectNode.objectNodeBuilder(); + for (Map.Entry> entry : shapes.entrySet()) { + String shapeId = entry.getKey().toString(); + ArrayNode shapeListNode = entry.getValue() //get list of ShapeLinks + .stream() + .map(ShapeLink::toNode) //convert each ShapeLink to an ObjectNode + .collect(ArrayNode.collect()); //collect each ObjectNode in an ArrayNode + shapesBuilder.withMember(shapeId, shapeListNode); + } + + //returning ObjectNode for TraceFile + return ObjectNode.objectNodeBuilder() + .withMember(SMITHY_TRACE_TEXT, smithyTrace) + .withMember(METADATA_TEXT, metadata) + .withOptionalMember(DEFINITIONS_TEXT, getArtifactDefinitions()) + .withMember(SHAPES_TEXT, shapesBuilder.build()) + .build(); + } + + /** + * Throws an error if any ShapeLink object contains a tag or type that is not in artifactDefinition's. + * This method should be called after creating a TraceFile object to verify that all the types and tags + * in shapes have been defined in artifactDefinition's. This TraceFile's ArtifactDefinitions object + * MUST be defined prior to calling this method. + * + * @throws ExpectationNotMetException if a type or tag in shapes is not in artifactDefinitions. + */ + private void validateTypesAndTags() { + //only validate if the optional ArtifactDefinitions is non-null + if (artifactDefinitions != null) { + //for each entry in the shapes map + for (Map.Entry> entry : shapes.entrySet()) { + //for each ShapeLink in entry's List + for (ShapeLink link : entry.getValue()) { + //checking if link's type is in artifactDefinitions + if (!artifactDefinitions.getTypes().containsKey(link.getType())) { + throw new ExpectationNotMetException(entry.getKey().toString() + + " contains types that aren't in definitions.", SourceLocation.none()); + } + + //checking if link's tags are all in artifactDefinitions + List tags = link.getTags(); + for (String tag : tags) { + if (!artifactDefinitions.getTags().containsKey(tag)) { + throw new ExpectationNotMetException(entry.getKey().toString() + " " + tag + + " is a tag that isn't in definitions.", SourceLocation.none()); + } + } + } + } + } + } + + /** + * Parses model and determines whether the trace file object meets the specs of the model by checking if + * the trace file contains all the ShapeIds in the model and the model contains all the ShapeIDs in the trace file. + * + * @param model the Smithy model to validate the trace file against. + * @throws ExpectationNotMetException if model contains a ShapeID not in TraceFile or TraceFile contains a ShapeID + * not in model. + */ + public void validateModel(Model model) { + Set fileShapes = new HashSet<>(shapes.keySet()); + Set fileShapesCopy = new HashSet<>(fileShapes); + + Set modelShapes = model.toSet().stream() + .filter(shape -> !Prelude.isPreludeShape(shape)) //ignore shapes in smithy.api namespace + .map(Shape::getId) //get ShapeId for each shape + .collect(Collectors.toSet()); //collect into a set of ShapeIds + + //get shapes in TraceFile that aren't in model; + fileShapes.removeAll(modelShapes); + //get shapes in model that aren't in TraceFile; + modelShapes.removeAll(fileShapesCopy); + + //if there are shapes in TraceFile not in model or vice versa + if (fileShapes.size() > 0 || modelShapes.size() > 0) { + //building the error message + StringBuilder errorMessageBuilder = new StringBuilder().append("Model validation failed."); + + if (fileShapes.size() > 0) { + errorMessageBuilder.append(" The following shapes are in the TraceFile, but missing from the model: "); + fileShapes.stream().forEach(id -> { + errorMessageBuilder.append(id.toString()).append(", "); + }); + errorMessageBuilder.append(". "); + } + + if (modelShapes.size() > 0) { + errorMessageBuilder.append("The following shapes are in the model, but missing from the TraceFile: "); + modelShapes.stream().forEach(id -> { + errorMessageBuilder.append(id.toString()).append(", "); + }); + errorMessageBuilder.append(". "); + } + + throw new ExpectationNotMetException(errorMessageBuilder.toString(), SourceLocation.none()); + } + } + + /** + * Gets this TraceFile's smithyTrace. + * The smithyTrace {@link String} contains the Smithy trace file version number. + * + * @return a String representing trace file ID. + */ + public String getSmithyTrace() { + return smithyTrace; + } + + /** + * Gets this TraceFile's TraceMetadata. + * + * @return a TraceMetadata object. + */ + public TraceMetadata getMetadata() { + return metadata; + } + + /** + * Gets this TraceFile's Definitions. + * + * @return an Optional Definitions container that contains this TraceFile's Definition + * or isEmpty if Definition's has not been set. + */ + public Optional getArtifactDefinitions() { + return Optional.ofNullable(artifactDefinitions); + } + + /** + * Gets this TraceFile's Shapes map. + * The shapes {@link Map} provides a mapping of absolute Smithy shape IDs to a list + * of shape link objects. A single Smithy shape can be responsible for generating + * multiple components in the target artifact. + * + * @return a Map from ShapeIDs to a list of ShapeLink's that represents the contents of the + * shapes tag in the trace file. + */ + public Map> getShapes() { + return shapes; + } + + /** + * Take this object and create a builder that contains all of the + * current property values of this object. + * + * @return a builder for type T + */ + @Override + public Builder toBuilder() { + return builder() + .metadata(metadata) + .smithyTrace(smithyTrace) + .definitions(artifactDefinitions) + .shapes(shapes); + } + + /** + * Builder for constructing TraceFile's from scratch. + */ + public static final class Builder implements SmithyBuilder { + + private final Map> shapes = new HashMap<>(); + private String smithyTrace = SMITHY_TRACE_VERSION; + private ArtifactDefinitions artifactDefinitions; + private TraceMetadata metadata; + + /** + * @return The TraceFile. + */ + public TraceFile build() { + return new TraceFile(this); + } + + /** + * @param smithyTrace Trace file version number. + * @return This builder. + */ + public Builder smithyTrace(String smithyTrace) { + this.smithyTrace = smithyTrace; + return this; + } + + /** + * @param artifactDefinitions Trace file definitions. + * @return This builder. + */ + public Builder definitions(ArtifactDefinitions artifactDefinitions) { + this.artifactDefinitions = artifactDefinitions; + return this; + } + + /** + * @param metadata Trace file TraceMetadata. + * @return This builder. + */ + public Builder metadata(TraceMetadata metadata) { + this.metadata = metadata; + return this; + } + + /** + * Adds a ShapeLink to this ShapeId in the TraceFile's shapes map. + * + * @param id ShapeId + * @param link ShapeLink corresponding to ShapeId + * @return This builder. + */ + public Builder addShapeLink(ShapeId id, ShapeLink link) { + List list = this.shapes.getOrDefault(id, new ArrayList<>()); + list.add(link); + this.shapes.put(id, list); + return this; + } + + /** + * Adds a ShapeLink to this ShapeId in the TraceFile's shapes map. + * + * @param idString ShapeId represented as a string. + * @param link ShapeLink corresponding to ShapeId + * @return This builder. + */ + public Builder addShapeLink(String idString, ShapeLink link) { + return addShapeLink(ShapeId.from(idString), link); + } + + /** + * Adds a list of ShapeLinks to this ShapeId in the TraceFile's shapes map. + * + * @param id ShapeId. + * @param linkList List of ShapeLinks corresponding to a ShapeId. + * @return This builder. + */ + public Builder addShapeLinks(ShapeId id, List linkList) { + this.shapes.put(id, linkList); + return this; + } + + /** + * Adds a list of ShapeLinks to this ShapeId in the TraceFile's shapes map. + * + * @param idString ShapeId as a String. + * @param linkList List of ShapeLinks corresponding to a ShapeId. + * @return This builder. + */ + public Builder addShapeLinks(String idString, List linkList) { + return addShapeLinks(ShapeId.from(idString), linkList); + } + + /** + * @param shapes shapes map for TraceFile. + * @return This builder. + */ + public Builder shapes(Map> shapes) { + this.shapes.clear(); + this.shapes.putAll(shapes); + return this; + } + + } + +} diff --git a/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/trace/TraceMetadata.java b/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/trace/TraceMetadata.java new file mode 100644 index 00000000000..2a0e7a37030 --- /dev/null +++ b/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/trace/TraceMetadata.java @@ -0,0 +1,243 @@ +/* + * 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.codegen.core.trace; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Optional; +import java.util.TimeZone; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.NodeMapper; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.node.ToNode; +import software.amazon.smithy.utils.SmithyBuilder; +import software.amazon.smithy.utils.ToSmithyBuilder; + +/** + * Class that defines information a code-generated artifact. + */ +public final class TraceMetadata implements ToNode, ToSmithyBuilder { + public static final String ID_TEXT = "id"; + public static final String VERSION_TEXT = "version"; + public static final String TYPE_TEXT = "type"; + public static final String TYPE_VERSION_TEXT = "typeVersion"; + public static final String HOMEPAGE_TEXT = "homepage"; + public static final String TIMESTAMP_TEXT = "timestamp"; + + private String id; + private String version; + private String timestamp; + private String type; + private String typeVersion; //optional + private String homepage; //optional + + private TraceMetadata(Builder builder) { + id = SmithyBuilder.requiredState(ID_TEXT, builder.id); + version = SmithyBuilder.requiredState(VERSION_TEXT, builder.version); + timestamp = SmithyBuilder.requiredState(TIMESTAMP_TEXT, builder.timestamp); + type = SmithyBuilder.requiredState(TYPE_TEXT, builder.type); + typeVersion = builder.typeVersion; + homepage = builder.homepage; + } + + /** + * Instantiates TraceMetadata instance variables using an ObjectNode that contains the artifact section of the + * trace file. + * + * @param value an ObjectNode that contains all children of the artifact tag in the trace file + * @return TraceMetadata produced by deserializing the node. + */ + public static TraceMetadata fromNode(Node value) { + NodeMapper mapper = new NodeMapper(); + mapper.disableFromNodeForClass(TraceMetadata.class); + return mapper.deserialize(value, TraceMetadata.class); + } + + public static Builder builder() { + return new Builder(); + } + + /** + * Converts the metadata contained in TraceMetadata's variables into an ObjectNode. + * + * @return an ObjectNode with that contains StringNodes representing the trace file + * metadata + */ + @Override + public ObjectNode toNode() { + return ObjectNode.objectNodeBuilder() + .withMember(ID_TEXT, id) + .withMember(VERSION_TEXT, version) + .withMember(TYPE_TEXT, type) + .withMember(TIMESTAMP_TEXT, timestamp) + .withOptionalMember(TYPE_VERSION_TEXT, getTypeVersion().map(Node::from)) + .withOptionalMember(HOMEPAGE_TEXT, getHomepage().map(Node::from)) + .build(); + } + + /** + * Gets this TraceMetadata's id. + * The id is the identifier of the artifact. For example, Java packages should use the Maven artifact ID. + * + * @return TraceMetadata's id + */ + public String getId() { + return id; + } + + /** + * Gets this TraceMetadata's version (for example, the AWS SDK release number). + * + * @return TraceMetadata's version + */ + public String getVersion() { + return version; + } + + /** + * Gets this TraceMetadata's timestamp. + * The timestamp is the RFC 3339 date and time that the artifact was created. + * + * @return TraceMetadata's timestamp + */ + public String getTimestamp() { + return timestamp; + } + + /** + * Gets this TraceMetadata's type. For code generation, this is the programming language. + * + * @return TraceMetadata's type + */ + public String getType() { + return type; + } + + /** + * Gets this TraceMetadata's TypeVersion in an Optional container. For example, when defining + * trace files for Java source code, the typeVersion would be the minimum supported JDK version. + * Different artifacts may have different output based on the version targets (for example the ability + * to use more features in a newer version of a language). + * + * @return Optional container with type version or empty container if + * TypeVersion has not been set. + */ + public Optional getTypeVersion() { + return Optional.ofNullable(typeVersion); + } + + /** + * Gets this TraceMetadata's Homepage in an Optional container. + * The homepage is the homepage URL of the artifact. + * + * @return Optional container with homepage or empty container if + * homepage has not been set + */ + public Optional getHomepage() { + return Optional.ofNullable(homepage); + } + + /** + * Take this object and create a builder that contains all of the + * current property values of this object. + * + * @return a builder for type T + */ + @Override + public Builder toBuilder() { + return builder() + .id(id) + .version(version) + .timestamp(timestamp) + .type(type) + .typeVersion(typeVersion) + .homepage(homepage); + } + + public static final class Builder implements SmithyBuilder { + private String id; + private String version; + private String timestamp; + private String type; + private String typeVersion; + private String homepage; + + /** + * @return The TraceMetadata object corresponding to this builder. + */ + public TraceMetadata build() { + return new TraceMetadata(this); + } + + public Builder id(String id) { + this.id = id; + return this; + } + + public Builder version(String version) { + this.version = version; + return this; + } + + public Builder timestamp(String timestamp) { + this.timestamp = timestamp; + return this; + } + + public Builder type(String type) { + this.type = type; + return this; + } + + /** + * Sets the timestamp as the current time in RFC 3339 format. + * + * @return This builder. + */ + public Builder setTimestampAsNow() { + //set timestamp based on current time + DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + this.timestamp = dateFormat.format(new Date()); + return this; + } + + /** + * Sets this builder's typeVersion. + * + * @param typeVersion typeVersion of TraceMetadata. + * @return This builder. + */ + public Builder typeVersion(String typeVersion) { + this.typeVersion = typeVersion; + return this; + } + + /** + * Sets this builder's homepage. + * + * @param homepage homepage of TraceMetadata. + * @return This builder. + */ + public Builder homepage(String homepage) { + this.homepage = homepage; + return this; + } + + } + +} diff --git a/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/trace/TracingSymbolProvider.java b/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/trace/TracingSymbolProvider.java new file mode 100644 index 00000000000..ceb07a2e452 --- /dev/null +++ b/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/trace/TracingSymbolProvider.java @@ -0,0 +1,193 @@ +/* + * 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.codegen.core.trace; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.function.BiFunction; +import software.amazon.smithy.codegen.core.Symbol; +import software.amazon.smithy.codegen.core.SymbolProvider; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.utils.SmithyBuilder; + +/** + * Decorates a {@link SymbolProvider} with a {@link TraceFile.Builder} and adds a {@link ShapeLink} object + * to the builder on each call to toSymbol. + */ +public final class TracingSymbolProvider implements SymbolProvider { + private final TraceFile.Builder traceFileBuilder = new TraceFile.Builder(); + private final Set visitedShapes = new HashSet<>(); + private final SymbolProvider symbolProvider; + private final BiFunction> shapeLinkCreator; + + private TracingSymbolProvider(Builder builder) { + symbolProvider = SmithyBuilder.requiredState("symbolProvider", builder.symbolProvider); + shapeLinkCreator = SmithyBuilder.requiredState("shapeLinkCreator", builder.shapeLinkCreator); + traceFileBuilder.metadata(SmithyBuilder.requiredState("metadata", builder.metadata)) + .definitions(builder.artifactDefinitions); + } + + /** + * Builder to create a TracingSymbolProvider instance. + * + * @return Returns a new Builder. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builds and returns the {@link TracingSymbolProvider}'s {@link TraceFile.Builder}. + * + * @return The {@link TraceFile} built from this {@link TracingSymbolProvider}'s {@link TraceFile.Builder}. + */ + public TraceFile buildTraceFile() { + return traceFileBuilder.build(); + } + + /** + * Converts a shape into a symbol by calling the toSymbol method of the + * SymbolProvider used to construct this TracingSymbolProvider. Adds a + * list of ShapeLinks to the TracingSymbolProvider's TraceFile.Builder. + * + * @param shape Shape to convert to Symbol. + * @return Symbol created from Shape. + */ + @Override + public Symbol toSymbol(Shape shape) { + Symbol symbol = symbolProvider.toSymbol(shape); + ShapeId shapeId = shape.getId(); + if (visitedShapes.add(shapeId)) { + List shapeLinks = shapeLinkCreator.apply(shape, symbol); + if (shapeLinks.size() > 0) { + traceFileBuilder.addShapeLinks(shapeId, shapeLinks); + } + } + return symbol; + } + + @Override + public String toMemberName(MemberShape shape) { + return symbolProvider.toMemberName(shape); + } + + /** + * Builder to create a TracingSymbolProvider instance. + */ + public static final class Builder implements SmithyBuilder { + private SymbolProvider symbolProvider; + private BiFunction> shapeLinkCreator; + private ArtifactDefinitions artifactDefinitions; + private TraceMetadata metadata; + + /** + * Sets this Builder's ArtifactDefinitions. + * + * @param artifactDefinitions ArtifactDefinitions for this TracingSymbolProvider's + * TraceFile. + * @return This Builder. + */ + public Builder artifactDefinitions(ArtifactDefinitions artifactDefinitions) { + this.artifactDefinitions = artifactDefinitions; + return this; + } + + /** + * Sets this Builder's TraceMetadata. + * + * @param metadata TraceMetadata for this TracingSymbolProvider's + * TraceFile. + * @return This Builder. + */ + public Builder metadata(TraceMetadata metadata) { + this.metadata = metadata; + return this; + } + + /** + * Sets the Builder's {@link TraceMetadata} based on the given type and + * default values for other required fields. This method should ONLY be used + * when the version, type, homepage, and typeVersion of the TraceMetadata + * object is unknown at the time of code generation. This method will ONLY set + * the required fields of the {@link TraceMetadata}. + * + *

The type is set to the artifactType that is passed in. The artifactType is + * the code language of the generated artifact, e.g. Java. + * + *

The timestamp in TraceMetadata is set to the current time when the + * method is called. + * + *

The id and version are set to a UUID that should be changed after the + * TraceFile is constructed and the correct id and version are known. + * + * @param artifactType The type, i.e. language, of the TraceMetadata object. + * @return This Builder. + */ + public Builder setTraceMetadataAsDefault(String artifactType) { + String tempIdVersion = UUID.randomUUID().toString(); + this.metadata = TraceMetadata.builder() + .version(tempIdVersion) + .id(tempIdVersion) + .type(artifactType) + .setTimestampAsNow() + .build(); + return this; + } + + /** + * Sets this Builder's shapeLinkCreator. The shapeLinkCreator + * is a function that maps from a Symbol to a List of ShapeLinks. + * Custom Functions should be designed for each code generator + * that map apply the tags and types in the definitions files + * to specific ShapeLinks. + * + * @param shapeLinkCreator A Function that defines a mapping + * from a Symbol to a List of ShapeLinks. + * @return This Builder. + */ + public Builder shapeLinkCreator(BiFunction> shapeLinkCreator) { + this.shapeLinkCreator = shapeLinkCreator; + return this; + } + + /** + * Sets this Builder's SymbolProvider. + * + * @param symbolProvider The SymbolProvider that the + * TracingSymbolProvider will decorate. + * @return This Builder. + */ + public Builder symbolProvider(SymbolProvider symbolProvider) { + this.symbolProvider = symbolProvider; + return this; + } + + /** + * Builds a {@code TracingSymbolProvider} implementation. + * + * @return Built TracingSymbolProvider. + */ + public TracingSymbolProvider build() { + return new TracingSymbolProvider(this); + } + + } + +} diff --git a/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/trace/package-info.java b/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/trace/package-info.java new file mode 100644 index 00000000000..d35623f41b7 --- /dev/null +++ b/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/trace/package-info.java @@ -0,0 +1,22 @@ +/* + * 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. + */ + +/** + * Defines abstractions for implementing Smithy model trace file generation. + */ +@SmithyUnstableApi +package software.amazon.smithy.codegen.core.trace; + +import software.amazon.smithy.utils.SmithyUnstableApi; diff --git a/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/trace/ArtifactDefinitionsTest.java b/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/trace/ArtifactDefinitionsTest.java new file mode 100644 index 00000000000..eb48a95584f --- /dev/null +++ b/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/trace/ArtifactDefinitionsTest.java @@ -0,0 +1,97 @@ +package software.amazon.smithy.codegen.core.trace; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.ObjectNode; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +class ArtifactDefinitionsTest { + @Test + void assertsToNodeWorksWithCorrectFields() { + ArtifactDefinitions artifactDefinitions = ArtifactDefinitions.builder() + .addType("t1", "t1val") + .addType("t2", "t2val") + .addTag("tag1", "tag1val") + .addTag("tag2", "tag2val") + .build(); + + ObjectNode node = artifactDefinitions.toNode(); + + ObjectNode builtNode = ObjectNode.objectNodeBuilder() + .withMember(ArtifactDefinitions.TAGS_TEXT, ObjectNode.fromStringMap(artifactDefinitions.getTags())) + .withMember(ArtifactDefinitions.TYPE_TEXT, ObjectNode.fromStringMap(artifactDefinitions.getTypes())) + .build(); + + Assertions.assertDoesNotThrow(() -> Node.assertEquals(node, builtNode)); + } + + @Test + void assertsFromNodeWorksWithCorrectFields() { + ArtifactDefinitions artifactDefinitions = ArtifactDefinitions.builder() + .addType("t1", "t1val") + .addType("t2", "t2val") + .addTag("tag1", "tag1val") + .addTag("tag2", "tag2val") + .build(); + + ObjectNode node = ObjectNode.objectNodeBuilder() + .withMember(ArtifactDefinitions.TAGS_TEXT, ObjectNode.fromStringMap(artifactDefinitions.getTags())) + .withMember(ArtifactDefinitions.TYPE_TEXT, ObjectNode.fromStringMap(artifactDefinitions.getTypes())) + .build(); + + ArtifactDefinitions artifactDefinitions2 = ArtifactDefinitions.fromNode(node); + + assertThat(artifactDefinitions.getTags(), equalTo(artifactDefinitions2.getTags())); + assertThat(artifactDefinitions.getTypes(), equalTo(artifactDefinitions2.getTypes())); + } + + @Test + void assertsFromDefinitionsFileWorksWithRequiredFields() throws URISyntaxException, FileNotFoundException { + ArtifactDefinitions artifactDefinitions = createFromFileHelper(getClass().getResource("definitions.json").toURI()); + + ArtifactDefinitions artifactDefinitions2 = ArtifactDefinitions.builder() + .addType("t1", "t1val") + .addType("t2", "t2val") + .addTag("tag1", "tag1val") + .addTag("tag2", "tag2val") + .build(); + + assertThat(artifactDefinitions.getTags(), equalTo(artifactDefinitions2.getTags())); + assertThat(artifactDefinitions.getTypes(), equalTo(artifactDefinitions2.getTypes())); + } + + @Test + void assertBuildThrowsWithoutRequiredTypesField() { + Assertions.assertThrows(IllegalStateException.class, () -> { + ArtifactDefinitions.builder() + .addTag("tag1", "tag1val") + .addTag("tag2", "tag2val") + .build(); + }); + } + + @Test + void assertBuildThrowsWithoutRequiredTagsField() { + Assertions.assertThrows(IllegalStateException.class, () -> { + ArtifactDefinitions.builder() + .addType("t1", "t1val") + .addType("t2", "t2val") + .build(); + }); + } + + ArtifactDefinitions createFromFileHelper(URI filename) throws FileNotFoundException { + InputStream stream = new FileInputStream(new File(filename)); + return ArtifactDefinitions.fromNode(Node.parse(stream).expectObjectNode()); + } + +} diff --git a/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/trace/ShapeLinkTest.java b/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/trace/ShapeLinkTest.java new file mode 100644 index 00000000000..d1afc7440dc --- /dev/null +++ b/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/trace/ShapeLinkTest.java @@ -0,0 +1,90 @@ +package software.amazon.smithy.codegen.core.trace; + +import java.util.ArrayList; +import java.util.Optional; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.ObjectNode; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +class ShapeLinkTest { + + @Test + void assertsToNodeWorksWithAllFields() { + ShapeLink shapeLink = ShapeLink.builder() + .addTag("tag") + .file("file") + .id("id") + .type("type") + .line(1) + .column(2) + .build(); + + + ObjectNode node = shapeLink.toNode(); + + assertThat(node.getStringMember(ShapeLink.TYPE_TEXT).get().getValue(), equalTo("type")); + assertThat(node.getNumberMember(ShapeLink.LINE_TEXT).get().getValue(), equalTo(1)); + assertThat(node.getArrayMember(ShapeLink.TAGS_TEXT) + .get() + .get(0) + .get() + .expectStringNode() + .getValue(), + equalTo("tag")); + } + + @Test + void assertsFromNodeWorksWithAllFields() { + ArrayList tags = new ArrayList<>(); + tags.add("tag"); + + Node node = ObjectNode.objectNodeBuilder() + .withMember(ShapeLink.ID_TEXT, "id") + .withMember(ShapeLink.TYPE_TEXT, "type") + .withOptionalMember(ShapeLink.TAGS_TEXT, Optional.of(tags).map(Node::fromStrings)) + .withOptionalMember(ShapeLink.FILE_TEXT, Optional.of("file").map(Node::from)) + .withOptionalMember(ShapeLink.LINE_TEXT, Optional.of(1).map(Node::from)) + .withOptionalMember(ShapeLink.COLUMN_TEXT, Optional.of(2).map(Node::from)) + .build(); + + ShapeLink shapeLink2 = ShapeLink.fromNode(node); + + assertThat(Optional.of(2), equalTo(shapeLink2.getColumn())); + assertThat(Optional.of(1), equalTo(shapeLink2.getLine())); + assertThat("id", equalTo(shapeLink2.getId())); + assertThat(Optional.of("file"), equalTo(shapeLink2.getFile())); + assertThat(tags, equalTo(shapeLink2.getTags())); + assertThat("type", equalTo(shapeLink2.getType())); + } + + @Test + void assertBuildThrowsWithoutRequiredTypesField() { + Assertions.assertThrows(IllegalStateException.class, () -> { + ShapeLink.builder() + .addTag("tag") + .file("file") + .id("id") + .line(1) + .column(2) + .build(); + }); + } + + @Test + void assertBuildThrowsWithoutRequiredIdField() { + Assertions.assertThrows(IllegalStateException.class, () -> { + ShapeLink.builder() + .addTag("tag") + .file("file") + .line(1) + .type("type") + .column(2) + .build(); + }); + } + +} diff --git a/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/trace/TraceFileTest.java b/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/trace/TraceFileTest.java new file mode 100644 index 00000000000..4800bd0bdfa --- /dev/null +++ b/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/trace/TraceFileTest.java @@ -0,0 +1,296 @@ +package software.amazon.smithy.codegen.core.trace; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import software.amazon.smithy.build.MockManifest; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.node.ExpectationNotMetException; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.shapes.StringShape; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.equalTo; + +class TraceFileTest { + /** + * Correct TraceFile use tests + * Test creating, parsing, validating and writing a TraceFile from scratch. + * Test parse and write functionality combined on a correct trace file + * Test parse and write functionality with nodes with single children instead of arrays of children + * Test parse and write functionality combined on a correct trace file without the optional definitions section + */ + + @Test + void assertWriteTraceFileFromScratchWorks() throws IOException, URISyntaxException { + /** + * Building TraceMetadata - this builder uses setTimestampAsNow, but you can also use a different + * builder constructor to specify a custom timestamp. + * The required fields are id, version, type, timestamp. + */ + + String id = "software.amazon.awssdk.services:snowball:2.10.79"; + String version = "2.10.79"; + String type = "Java"; + String typeVersion = "1.8"; + String homepage = "https://github.com/aws/aws-sdk-java-v2/"; + TraceMetadata traceMetadata = TraceMetadata.builder() + .id(id) + .version(version) + .type(type) + .setTimestampAsNow() + .homepage(homepage) + .typeVersion(typeVersion) + .build(); + + /** + * Building Definitions - this example uses addTag and addType to add individual key value pairs. + * There's another builder constructor that allows you to add the entire tags/types Map without + * having to add them individually. + */ + ArtifactDefinitions artifactDefinitions = ArtifactDefinitions.builder() + .addTag("service", "Service client") + .addTag("request", "AWS SDK request type") + .addTag("requestBuilder", "AWS SDK request builder") + .addType("TYPE", "Class, interface (including annotation type), or enum declaration") + .build(); + + /** + * Building TraceFile - build the trace file by passing the different objects to the builder + * You can either construct the shapes map yourself, and add it to TraceFile, or you can add each shape + * individually as shown below. + * SmithyTrace is a constant in my code set to 1.0 for this version, so you don't have to worry about + * setting it. + */ + TraceFile.Builder traceFileBuilder = TraceFile.builder() + .metadata(traceMetadata) + .definitions(artifactDefinitions); + + //adding one ShapeLink to TraceFile + type = "TYPE"; + id = "software.amazon.awssdk.services.snowball.SnowballClient"; + traceFileBuilder.addShapeLink("com.amazonaws.snowball#Snowball", + ShapeLink.builder() + .type(type) + .id(id) + .addTag("service") + .build()); + + //adding multiple ShapeLinks for the same ShapeId; can also add a List + type = "TYPE"; + id = "software.amazon.awssdk.services.snowball.model.ListClusterJobsRequest$Builder"; + traceFileBuilder.addShapeLink("com.amazonaws.snowball#ListClustersRequest", + ShapeLink.builder() + .type(type) + .id(id) + .addTag("requestBuilder") + .build()); + type = "TYPE"; + id = "software.amazon.awssdk.services.snowball.model.ListClusterJobsRequest"; + traceFileBuilder.addShapeLink("com.amazonaws.snowball#ListClustersRequest", + ShapeLink.builder() + .type(type) + .id(id) + .addTag("request") + .build()); + + //finally, build the TraceFile + TraceFile traceFile = traceFileBuilder.build(); + + /** + * After creating a TraceFile, you may want to validate it. You can validate whether all your TraceFile + * matches your model by checking if all the ShapeIds in a model file are in the TraceFile and all the + * ShapeIds in a model file are in the TraceFile. This line is commented out here because there is no + * Smithy model for this TraceFile, so it cannot be validated against a model. See validateModel tests + * below for complete use cases. + */ + //traceFile.validateModel(assembleModelTestHelper(("Your model resource path here"); + + + //Then write the TraceFile, just specify path/filename string. + String filename = "trace-file-output.json"; + MockManifest manifest = writeTraceFileTestHelper(traceFile, filename); + + /** + * Then parse the trace file, specify the files URI for parsing + * Parsing fills all required fields and checks that they're filled + */ + + TraceFile traceFile2 = parseTraceFileFromManifest(manifest, filename); + + //few assorted checks + assertThat(traceFile2.getMetadata().getId(), equalTo("software.amazon.awssdk.services:snowball:2.10.79")); + assertThat(traceFile2.getArtifactDefinitions().get().getTags().keySet(), containsInAnyOrder("service", "request", + "requestBuilder")); + assertThat(traceFile2.getShapes().get(ShapeId.from("com.amazonaws.snowball#Snowball")).get(0).getType(), + equalTo("TYPE")); + assertThat(traceFile2.getSmithyTrace(), equalTo("1.0")); + } + + @Test + void assertsParseTraceFileWorksWithCorrectTraceFile() throws URISyntaxException, FileNotFoundException { + TraceFile traceFile = parseTraceFileFromFile(getClass().getResource("trace-file.json").toURI()); + + assertThat(traceFile.getMetadata().getId(), equalTo("software.amazon.awssdk.services:snowball:2.10.79")); + assertThat(traceFile.getArtifactDefinitions().get().getTags().keySet(), containsInAnyOrder("service", "request", + "response", "requestBuilder", "responseBuilder")); + assertThat(traceFile.getShapes().get(ShapeId.from("com.amazonaws.snowball#Snowball")).get(0).getType(), + equalTo("TYPE")); + assertThat(traceFile.getSmithyTrace(), equalTo("1.0")); + } + + @Test + void assertsWriteTraceFileWorksWithCorrectTraceFile() throws URISyntaxException, IOException { + TraceFile traceFile = parseTraceFileFromFile(getClass().getResource("trace-file.json").toURI()); + MockManifest manifest = writeTraceFileTestHelper(traceFile, "trace-file-output.json"); + TraceFile traceFile2 = parseTraceFileFromManifest(manifest, "trace-file-output.json"); + + assertThat(traceFile2.getMetadata().getId(), equalTo("software.amazon.awssdk.services:snowball:2.10.79")); + assertThat(traceFile2.getArtifactDefinitions().get().getTags().keySet(), containsInAnyOrder("service", "request", + "response", "requestBuilder", "responseBuilder")); + assertThat(traceFile2.getShapes().get(ShapeId.from("com.amazonaws.snowball#Snowball")).get(0).getType(), + equalTo("TYPE")); + assertThat(traceFile2.getSmithyTrace(), equalTo("1.0")); + } + + @Test + void assertParseWriteWorksWithoutDefinitions() throws IOException, URISyntaxException { + TraceFile traceFile = parseTraceFileFromFile(getClass().getResource("trace-file.json").toURI()); + + //set definitions to null before writing and parsing again + traceFile = traceFile.toBuilder().definitions(null).build(); + + MockManifest manifest = writeTraceFileTestHelper(traceFile, "trace-file-output.json"); + TraceFile traceFile2 = parseTraceFileFromManifest(manifest, "trace-file-output.json"); + + assertThat(traceFile.getMetadata().getId(), equalTo("software.amazon.awssdk.services:snowball:2.10.79")); + assertThat(traceFile.getShapes().get(ShapeId.from("com.amazonaws.snowball#Snowball")).get(0).getType(), + equalTo("TYPE")); + assertThat(traceFile.getSmithyTrace(), equalTo("1.0")); + } + + /** + * Validate TraceFile use tests + * validateTypesOrTags throws with invalid type, and throws with invalid tags + * validateModel works with valid input, throws with invalid model/trace file pair + */ + + @Test + void buildThrowsOnInvalidType() throws ExpectationNotMetException { + Assertions.assertThrows(ExpectationNotMetException.class, () -> { + TraceFile traceFile = parseTraceFileFromFile(getClass().getResource("trace-file.json").toURI()); + traceFile.toBuilder() + .addShapeLink("com.amazonaws.snowball#Snowball", ShapeLink + .builder() + .id("id") + .type("fake_type") + .build()) + .build(); + }); + } + + @Test + void buildThrowsOnInvalidTag() throws ExpectationNotMetException { + Assertions.assertThrows(ExpectationNotMetException.class, () -> { + TraceFile traceFile = parseTraceFileFromFile(getClass().getResource("trace-file.json").toURI()); + traceFile.toBuilder() + .addShapeLink("com.amazonaws.snowball#Snowball", ShapeLink + .builder() + .id("id") + .type("TYPE") + .addTag("fake_tag") + .build()) + .build(); + }); + } + + @Test + void validateModelDoesNotThrowOnValidTraceFileModelPair() throws ExpectationNotMetException, URISyntaxException, FileNotFoundException { + Assertions.assertDoesNotThrow(() -> { + TraceFile traceFile = parseTraceFileFromFile(getClass().getResource("trace-for-simple-service-validation.json").toURI()); + traceFile.validateModel(assembleModelTestHelper("simple-service.smithy")); + }); + } + + @Test + void validateModelThrowsOnTraceFileWithoutAllModelShapeIDs() throws ExpectationNotMetException, URISyntaxException, FileNotFoundException { + Assertions.assertThrows(ExpectationNotMetException.class, () -> { + TraceFile traceFile = parseTraceFileFromFile(getClass().getResource("trace-for-model-validation.json").toURI()); + traceFile.validateModel(assembleModelTestHelper("service-with-shapeids.smithy")); + }); + } + + @Test + void validateModelThrowsOnModelWithoutAllTraceFileShapeIds() throws ExpectationNotMetException, URISyntaxException, FileNotFoundException { + Assertions.assertThrows(ExpectationNotMetException.class, () -> { + TraceFile traceFile = parseTraceFileFromFile(getClass().getResource("trace-for-simple-service-validation.json").toURI()); + Model model = assembleModelTestHelper("simple-service.smithy"); + //add a shape to our model that's not in our trace file + model = model.toBuilder().addShape(StringShape.builder().id("ns.foo#bar").build()).build(); + traceFile.validateModel(model); + }); + } + + /** + * Incorrect build tests: + * Test null values for all required variables of TraceFile + */ + + @Test + void buildThrowsWithNoSmithyTrace() { + Assertions.assertThrows(IllegalStateException.class, () -> { + TraceFile traceFile = parseTraceFileFromFile(getClass().getResource("trace-file.json").toURI()); + //set smithyTrace to null before writing and parsing again + traceFile.toBuilder().smithyTrace(null).build(); + }); + } + + @Test + void buildThrowsWithNoTraceMetadata() { + Assertions.assertThrows(IllegalStateException.class, () -> { + TraceFile traceFile = parseTraceFileFromFile(getClass().getResource("trace-file.json").toURI()); + //set to null before writing and parsing again + traceFile.toBuilder().metadata(null).build(); + }); + } + + @Test + void buildThrowsWithNoShapes() { + Assertions.assertThrows(NullPointerException.class, () -> { + TraceFile traceFile = parseTraceFileFromFile(getClass().getResource("trace-file.json").toURI()); + //set to null before writing and parsing again + traceFile.toBuilder().shapes(null).build(); + }); + } + + TraceFile parseTraceFileFromFile(URI filename) throws FileNotFoundException { + InputStream stream = new FileInputStream(new File(filename)); + return TraceFile.fromNode(Node.parse(stream).expectObjectNode()); + } + + TraceFile parseTraceFileFromManifest(MockManifest manifest, String filename) { + return TraceFile.fromNode(Node.parse(manifest.expectFileString(filename))); + } + + MockManifest writeTraceFileTestHelper(TraceFile traceFile, String fileName) throws IOException { + MockManifest manifest = new MockManifest(); + manifest.writeFile(fileName, Node.prettyPrintJson(traceFile.toNode())); + return manifest; + } + + Model assembleModelTestHelper(String modelResourceName) { + return Model.assembler() + .addImport(getClass().getResource(modelResourceName)) + .assemble() + .unwrap(); + } + +} diff --git a/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/trace/TraceMetadataTest.java b/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/trace/TraceMetadataTest.java new file mode 100644 index 00000000000..32c22da1da2 --- /dev/null +++ b/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/trace/TraceMetadataTest.java @@ -0,0 +1,100 @@ +package software.amazon.smithy.codegen.core.trace; + +import java.util.Optional; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.ObjectNode; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +class TraceMetadataTest { + + @Test + void assertsToNodeWorksWithRequiredFields() { + TraceMetadata am = new TraceMetadata.Builder() + .id("a") + .version("b") + .type("c") + .timestamp("d") + .homepage("hp") + .typeVersion("tv") + .build(); + + ObjectNode node = am.toNode(); + + assertThat(node.expectStringMember(TraceMetadata.ID_TEXT).getValue(), equalTo("a")); + assertThat(node.expectStringMember(TraceMetadata.VERSION_TEXT).getValue(), equalTo("b")); + assertThat(node.expectStringMember(TraceMetadata.TYPE_TEXT).getValue(), equalTo("c")); + assertThat(node.expectStringMember(TraceMetadata.TIMESTAMP_TEXT).getValue(), equalTo("d")); + assertThat(node.expectStringMember(TraceMetadata.HOMEPAGE_TEXT).getValue(), equalTo("hp")); + assertThat(node.expectStringMember(TraceMetadata.TYPE_VERSION_TEXT).getValue(), equalTo("tv")); + } + + @Test + void assertsFromNodeWorksWithRequiredFields() { + Node node = ObjectNode.objectNodeBuilder() + .withMember(TraceMetadata.ID_TEXT, "id") + .withMember(TraceMetadata.VERSION_TEXT, "version") + .withMember(TraceMetadata.TYPE_TEXT, "type") + .withMember(TraceMetadata.TIMESTAMP_TEXT, "timestamp") + .withOptionalMember(TraceMetadata.TYPE_VERSION_TEXT, Optional.of("type").map(Node::from)) + .withOptionalMember(TraceMetadata.HOMEPAGE_TEXT, Optional.of("homepage").map(Node::from)) + .build(); + + TraceMetadata am2 = TraceMetadata.fromNode(node); + + assertThat("id", equalTo(am2.getId())); + assertThat("version", equalTo(am2.getVersion())); + assertThat("timestamp", equalTo(am2.getTimestamp())); + assertThat("type", equalTo(am2.getType())); + assertThat("type", equalTo(am2.getTypeVersion().get())); + assertThat("homepage", equalTo(am2.getHomepage().get())); + } + + @Test + void assertBuildThrowsWithoutRequiredId() { + Assertions.assertThrows(IllegalStateException.class, () -> { + TraceMetadata am = new TraceMetadata.Builder() + .version("b") + .type("c") + .setTimestampAsNow() + .build(); + }); + } + + @Test + void assertBuildThrowsWithoutRequiredVersion() { + Assertions.assertThrows(IllegalStateException.class, () -> { + TraceMetadata am = new TraceMetadata.Builder() + .id("a") + .type("c") + .setTimestampAsNow() + .build(); + }); + } + + @Test + void assertBuildThrowsWithoutRequiredType() { + Assertions.assertThrows(IllegalStateException.class, () -> { + TraceMetadata am = new TraceMetadata.Builder() + .id("a") + .version("b") + .setTimestampAsNow() + .build(); + }); + } + + @Test + void assertBuildThrowsWithoutRequiredTimestamp() { + Assertions.assertThrows(IllegalStateException.class, () -> { + TraceMetadata am = new TraceMetadata.Builder() + .id("a") + .version("b") + .type("c") + .build(); + }); + } + +} diff --git a/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/trace/TracingSymbolProviderTest.java b/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/trace/TracingSymbolProviderTest.java new file mode 100644 index 00000000000..e3ee237d61a --- /dev/null +++ b/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/trace/TracingSymbolProviderTest.java @@ -0,0 +1,161 @@ +package software.amazon.smithy.codegen.core.trace; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiFunction; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import software.amazon.smithy.codegen.core.Symbol; +import software.amazon.smithy.codegen.core.SymbolProvider; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.shapes.StringShape; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +class TracingSymbolProviderTest { + + @Test + void assertBuildDoesNotThrowWithAllFields() { + Assertions.assertDoesNotThrow(() -> { + TracingSymbolProvider.builder() + .metadata(constructTraceMetadata()) + .artifactDefinitions(constructArtifactDefinitions()) + .symbolProvider(new TestSymbolProvider()) + .shapeLinkCreator(constructFunction()) + .build(); + }); + } + + @Test + void assertBuildDoesNotThrowWithRequiredFields() { + Assertions.assertDoesNotThrow(() -> { + TracingSymbolProvider.builder() + .metadata(constructTraceMetadata()) + .symbolProvider(new TestSymbolProvider()) + .shapeLinkCreator(constructFunction()) + .build(); + }); + } + + @Test + void assertBuildDoesNotThrowWithDefaultTraceMetadata() { + Assertions.assertDoesNotThrow(() -> { + TracingSymbolProvider.builder() + .setTraceMetadataAsDefault("Java") + .symbolProvider(new TestSymbolProvider()) + .shapeLinkCreator(constructFunction()) + .build(); + }); + } + + @Test + void assertBuildFailsWithoutSymbolProvider() { + Assertions.assertThrows(IllegalStateException.class, () -> TracingSymbolProvider.builder() + .metadata(constructTraceMetadata()) + .shapeLinkCreator(constructFunction()) + .build() + ); + } + + @Test + void assertBuildFailsWithoutTraceMetadata() { + Assertions.assertThrows(IllegalStateException.class, () -> TracingSymbolProvider.builder() + .symbolProvider(new TestSymbolProvider()) + .shapeLinkCreator(constructFunction()) + .build() + ); + } + + @Test + void assertBuildFailsWithoutShapeLinkCreator() { + Assertions.assertThrows(IllegalStateException.class, () -> TracingSymbolProvider.builder() + .metadata(constructTraceMetadata()) + .symbolProvider(new TestSymbolProvider()) + .build() + ); + } + + @Test + void assertToSymbolShapeLinkCreatorCreatesShapeLink() { + TracingSymbolProvider tracingSymbolProvider = TracingSymbolProvider.builder() + .metadata(constructTraceMetadata()) + .artifactDefinitions(constructArtifactDefinitions()) + .symbolProvider(new TestSymbolProvider()) + .shapeLinkCreator(constructFunction()) + .build(); + + Shape shape = StringShape.builder().id("namespace.foo#baz").build(); + + Symbol symbol = tracingSymbolProvider.toSymbol(shape); + + TraceFile traceFile = tracingSymbolProvider.buildTraceFile(); + + ShapeLink createdShapeLink = traceFile.getShapes().get(ShapeId.from("namespace.foo#baz")).get(0); + + assertThat(createdShapeLink.getType(), equalTo("TYPE")); + assertThat(createdShapeLink.getId(), equalTo(symbol.toString())); + } + + @Test + void assertToSymbolDoesNotDuplicateShapeLinks() { + TracingSymbolProvider tracingSymbolProvider = TracingSymbolProvider.builder() + .metadata(constructTraceMetadata()) + .artifactDefinitions(constructArtifactDefinitions()) + .symbolProvider(new TestSymbolProvider()) + .shapeLinkCreator(constructFunction()) + .build(); + + Shape shape = StringShape.builder().id("namespace.foo#baz").build(); + + // Call toSymbol twice to make sure the ShapeId is not added twice. + tracingSymbolProvider.toSymbol(shape); + tracingSymbolProvider.toSymbol(shape); + + TraceFile traceFile = tracingSymbolProvider.buildTraceFile(); + + assertThat(traceFile.getShapes().get(ShapeId.from("namespace.foo#baz")).size(), equalTo(1)); + } + + ArtifactDefinitions constructArtifactDefinitions() { + return ArtifactDefinitions.builder() + .addTag("service", "Service client") + .addType("TYPE", "Class, interface (including annotation type), or enum declaration") + .build(); + } + + TraceMetadata constructTraceMetadata() { + return TraceMetadata.builder() + .id("software.amazon.awssdk.services:snowball:2.10.79") + .version("2.10.79") + .type("Java") + .setTimestampAsNow() + .build(); + } + + BiFunction> constructFunction() { + return (shape, symbol) -> { + List list = new ArrayList<>(); + list.add(ShapeLink.builder() + .id(symbol.toString()) + .type("TYPE") + .build()); + return list; + }; + } + + // Test class that substitutes for language-specific symbol provider. + static class TestSymbolProvider implements SymbolProvider { + @Override + public Symbol toSymbol(Shape shape) { + return Symbol.builder().putProperty("shape", shape) + .name(shape.getId().getName()) + .namespace(shape.getId().getNamespace(), "/") + .definitionFile("file.java") + .build(); + } + + } + +} diff --git a/smithy-codegen-core/src/test/resources/software/amazon/smithy/codegen/core/trace/definitions.json b/smithy-codegen-core/src/test/resources/software/amazon/smithy/codegen/core/trace/definitions.json new file mode 100644 index 00000000000..c6ed333f26c --- /dev/null +++ b/smithy-codegen-core/src/test/resources/software/amazon/smithy/codegen/core/trace/definitions.json @@ -0,0 +1,10 @@ +{ + "types": { + "t1": "t1val", + "t2": "t2val" + }, + "tags": { + "tag1": "tag1val", + "tag2": "tag2val" + } +} diff --git a/smithy-codegen-core/src/test/resources/software/amazon/smithy/codegen/core/trace/service-with-shapeids.smithy b/smithy-codegen-core/src/test/resources/software/amazon/smithy/codegen/core/trace/service-with-shapeids.smithy new file mode 100644 index 00000000000..5971a2f7db8 --- /dev/null +++ b/smithy-codegen-core/src/test/resources/software/amazon/smithy/codegen/core/trace/service-with-shapeids.smithy @@ -0,0 +1,130 @@ +namespace example.weather + +/// Provides weather forecasts. +@paginated(inputToken: "nextToken", outputToken: "nextToken", + pageSize: "pageSize") +service Weather { + version: "2006-03-01", + resources: [City], + operations: [GetCurrentTime] +} + +resource City { + identifiers: { cityId: CityId }, + read: GetCity, + list: ListCities, + resources: [Forecast], +} + +resource Forecast { + identifiers: { cityId: CityId }, + read: GetForecast, +} + +// "pattern" is a trait. +@pattern("^[A-Za-z0-9 ]+$") +string CityId + +@readonly +operation GetCity { + input: GetCityInput, + output: GetCityOutput, + errors: [NoSuchResource] +} + +structure GetCityInput { + // "cityId" provides the identifier for the resource and + // has to be marked as required. + @required + cityId: CityId +} + +structure GetCityOutput { + // "required" is used on output to indicate if the service + // will always provide a value for the member. + @required + name: String, + + @required + coordinates: CityCoordinates, +} + +// This structure is nested within GetCityOutput. +structure CityCoordinates { + @required + latitude: Float, + + @required + longitude: Float, +} + +// "error" is a trait that is used to specialize +// a structure as an error. +@error("client") +structure NoSuchResource { + @required + resourceType: String +} + +// The paginated trait indicates that the operation may +// return truncated results. +@readonly +@paginated(items: "items") +operation ListCities { + input: ListCitiesInput, + output: ListCitiesOutput +} + +structure ListCitiesInput { + nextToken: String, + pageSize: Integer +} + +structure ListCitiesOutput { + nextToken: String, + + @required + items: CitySummaries, +} + +// CitySummaries is a list of CitySummary structures. +list CitySummaries { + member: CitySummary +} + +// CitySummary contains a reference to a City. +@references([{resource: City}]) +structure CitySummary { + @required + cityId: CityId, + + @required + name: String, +} + +@readonly +operation GetCurrentTime { + output: GetCurrentTimeOutput +} + +structure GetCurrentTimeOutput { + @required + time: Timestamp +} + +@readonly +operation GetForecast { + input: GetForecastInput, + output: GetForecastOutput +} + +// "cityId" provides the only identifier for the resource since +// a Forecast doesn't have its own. +structure GetForecastInput { + @required + cityId: CityId, +} + +structure GetForecastOutput { + chanceOfRain: Float +} diff --git a/smithy-codegen-core/src/test/resources/software/amazon/smithy/codegen/core/trace/simple-service.smithy b/smithy-codegen-core/src/test/resources/software/amazon/smithy/codegen/core/trace/simple-service.smithy new file mode 100644 index 00000000000..64843961ac0 --- /dev/null +++ b/smithy-codegen-core/src/test/resources/software/amazon/smithy/codegen/core/trace/simple-service.smithy @@ -0,0 +1,5 @@ +namespace smithy.example + +service Example { + version: "1.0.0" +} diff --git a/smithy-codegen-core/src/test/resources/software/amazon/smithy/codegen/core/trace/trace-file.json b/smithy-codegen-core/src/test/resources/software/amazon/smithy/codegen/core/trace/trace-file.json new file mode 100644 index 00000000000..3a627619f46 --- /dev/null +++ b/smithy-codegen-core/src/test/resources/software/amazon/smithy/codegen/core/trace/trace-file.json @@ -0,0 +1,88 @@ +{ + "smithyTrace": "1.0", + "metadata": { + "id": "software.amazon.awssdk.services:snowball:2.10.79", + "version": "2.10.79", + "type": "Java", + "typeVersion": "1.8", + "homepage": "https://github.com/aws/aws-sdk-java-v2/", + "timestamp": "2020-03-05T23:20:50.52Z" + }, + "definitions": { + "types": { + "TYPE": "Class, interface (including annotation type), or enum declaration", + "FIELD": "Field declaration (includes enum constants)", + "METHOD": "Method declaration" + }, + "tags": { + "service": "Service client", + "request": "AWS SDK request type", + "response": "AWS SDK response type", + "requestBuilder": "AWS SDK request builder", + "responseBuilder": "AWS SDK response builder" + } + }, + "shapes": { + "com.amazonaws.snowball#Snowball": [ + { + "type": "TYPE", + "id": "software.amazon.awssdk.services.snowball.SnowballClient", + "tags": ["service"] + } + ], + "com.amazonaws.snowball#ListClusters": [ + { + "type": "METHOD", + "id": "software.amazon.awssdk.services.snowball.SnowballClient#listClusterJobs" + } + ], + "com.amazonaws.snowball#ListClustersRequest": [ + { + "type": "TYPE", + "id": "software.amazon.awssdk.services.snowball.model.ListClusterJobsRequest$Builder", + "tags": ["requestBuilder"] + }, + { + "type": "TYPE", + "id": "software.amazon.awssdk.services.snowball.model.ListClusterJobsRequest", + "tags": ["request"] + } + ], + "com.amazonaws.snowball#ListClustersRequest$ClusterId": [ + { + "type": "METHOD", + "id": "software.amazon.awssdk.services.snowball.model.ListClusterJobsRequest$Builder#clusterId", + "tags": ["requestBuilder"] + }, + { + "type": "METHOD", + "id": "software.amazon.awssdk.services.snowball.model.ListClusterJobsRequest#clusterId", + "tags": ["request"] + } + ], + "com.amazonaws.snowball#ListClustersRequest$NextToken": [ + { + "type": "METHOD", + "id": "software.amazon.awssdk.services.snowball.model.ListClusterJobsRequest$Builder#nextToken", + "tags": ["requestBuilder"] + }, + { + "type": "METHOD", + "id": "software.amazon.awssdk.services.snowball.model.ListClusterJobsRequest#nextToken", + "tags": ["request"] + } + ], + "com.amazonaws.snowball#ListClustersResponse": [ + { + "type": "TYPE", + "id": "software.amazon.awssdk.services.snowball.model.ListClusterJobsResponse$Builder", + "tags": ["responseBuilder"] + }, + { + "type": "TYPE", + "id": "software.amazon.awssdk.services.snowball.model.ListClusterJobsResponse", + "tags": ["response"] + } + ] + } +} diff --git a/smithy-codegen-core/src/test/resources/software/amazon/smithy/codegen/core/trace/trace-for-model-validation.json b/smithy-codegen-core/src/test/resources/software/amazon/smithy/codegen/core/trace/trace-for-model-validation.json new file mode 100644 index 00000000000..578be43b1ad --- /dev/null +++ b/smithy-codegen-core/src/test/resources/software/amazon/smithy/codegen/core/trace/trace-for-model-validation.json @@ -0,0 +1,42 @@ +{ + "smithyTrace": "1.0", + "metadata": { + "id": "", + "version": "", + "type": "", + "typeVersion": "", + "homepage": "", + "timestamp": "" + }, + "definitions": { + "types": { + "TYPE": "Class, interface (including annotation type), or enum declaration" + }, + "tags": { + "service": "Service client" + } + }, + "shapes": { + "example.weather#CityId": [ + { + "type": "", + "id": "", + "tags": [""] + } + ], + "example.weather#CitySummaries": [ + { + "type": "", + "id": "", + "tags": [""] + } + ], + "example.weather#CitySummary": [ + { + "type": "", + "id": "", + "tags": [""] + } + ] + } +} diff --git a/smithy-codegen-core/src/test/resources/software/amazon/smithy/codegen/core/trace/trace-for-simple-service-validation.json b/smithy-codegen-core/src/test/resources/software/amazon/smithy/codegen/core/trace/trace-for-simple-service-validation.json new file mode 100644 index 00000000000..bacf842dd95 --- /dev/null +++ b/smithy-codegen-core/src/test/resources/software/amazon/smithy/codegen/core/trace/trace-for-simple-service-validation.json @@ -0,0 +1,27 @@ +{ + "smithyTrace": "1.0", + "metadata": { + "id": "", + "version": "", + "type": "", + "typeVersion": "", + "homepage": "", + "timestamp": "" + }, + "definitions": { + "types": { + "TYPE": "Class, interface (including annotation type), or enum declaration" + }, + "tags": { + "service": "Service client" + } + }, + "shapes": { + "smithy.example#Example": [ + { + "type": "TYPE", + "id": "" + } + ] + } +}