diff --git a/docs/source/1.0/guides/building-models/build-config.rst b/docs/source/1.0/guides/building-models/build-config.rst index 5b59ee5abfa..0e9b9a814b2 100644 --- a/docs/source/1.0/guides/building-models/build-config.rst +++ b/docs/source/1.0/guides/building-models/build-config.rst @@ -1021,6 +1021,51 @@ but keeps the shape if it has any of the provided tags: } } +.. _renameShapes-transform: + +renameShapes +------------ + +Renames shapes within the model, including updating any references to the +shapes that are being renamed. + +.. list-table:: + :header-rows: 1 + :widths: 10 20 70 + + * - Property + - Type + - Description + * - renamed + - ``Map`` + - The map of :ref:`shape IDs ` to rename. Each key ``shapeId`` will be + renamed to the value ``shapeId``. Each :ref:`shape ID ` must be + be an absolute shape ID. + +The following example renames the ``ns.foo#Bar`` shape to ``ns.foo#Baz``. +Any references to ``ns.foo#Bar`` on other shapes will also be updated. + +.. tabs:: + + .. code-tab:: json + + { + "version": "1.0", + "projections": { + "exampleProjection": { + "transforms": [ + { + "name": "renameShapes", + "args": { + "renamed": { + "ns.foo#Bar": "ns.foo#Baz" + } + } + } + ] + } + } + } .. _build_envars: diff --git a/smithy-build/src/main/java/software/amazon/smithy/build/transforms/RenameShapes.java b/smithy-build/src/main/java/software/amazon/smithy/build/transforms/RenameShapes.java new file mode 100644 index 00000000000..6ea3e88369d --- /dev/null +++ b/smithy-build/src/main/java/software/amazon/smithy/build/transforms/RenameShapes.java @@ -0,0 +1,105 @@ +/* + * Copyright 2021 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.build.transforms; + +import java.util.HashMap; +import java.util.Map; +import software.amazon.smithy.build.SmithyBuildException; +import software.amazon.smithy.build.TransformContext; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.shapes.ShapeIdSyntaxException; +import software.amazon.smithy.model.transform.ModelTransformer; + +/** + * {@code renameShapes} updates a model by renaming shapes. When + * configuring the transformer, a `renamed` property must be set as a + * map with the keys as the `from` shape ids that will be renamed `to` + * the shape id values. Any references to a renamed shape will also be + * updated. + */ +public class RenameShapes extends ConfigurableProjectionTransformer { + + /** + * {@code renameShapes} configuration settings. + */ + public static final class Config { + + private Map renamed; + + /** + * Sets the map of `from` shape ids to the `to` shape id values that they shapes + * will be renamed to. + * + * @param renamed The map of shapes to rename. + */ + public void setRenamed(Map renamed) { + this.renamed = renamed; + } + + /** + * Gets the map of shape ids to be renamed. + * + * @return The map of shapes to rename. + */ + public Map getRenamed() { + return renamed; + } + } + + @Override + public Class getConfigType() { + return Config.class; + } + + @Override + protected Model transformWithConfig(TransformContext context, Config config) { + if (config.getRenamed() == null || config.getRenamed().isEmpty()) { + throw new SmithyBuildException( + "'renamed' property must be set and non-empty on renameShapes transformer."); + } + Model model = context.getModel(); + + return ModelTransformer.create().renameShapes(model, getShapeIdsToRename(config, model)); + } + + @Override + public String getName() { + return "renameShapes"; + } + + private Map getShapeIdsToRename(Config config, Model model) { + Map shapeIdMap = new HashMap<>(); + for (String fromShape : config.getRenamed().keySet()) { + ShapeId fromShapeId = getShapeIdFromString(fromShape); + if (!model.getShape(fromShapeId.toShapeId()).isPresent()) { + throw new SmithyBuildException( + String.format("'%s' to be renamed does not exist in model.", fromShapeId) + ); + } + shapeIdMap.put(fromShapeId, getShapeIdFromString(config.getRenamed().get(fromShape))); + } + return shapeIdMap; + } + + private ShapeId getShapeIdFromString(String id) { + try { + return ShapeId.from(id); + } catch (ShapeIdSyntaxException e) { + throw new SmithyBuildException(String.format("'%s' must be a valid, absolute shape ID", id)); + } + } +} diff --git a/smithy-build/src/main/resources/META-INF/services/software.amazon.smithy.build.ProjectionTransformer b/smithy-build/src/main/resources/META-INF/services/software.amazon.smithy.build.ProjectionTransformer index 9cc23290cb4..9203083422e 100644 --- a/smithy-build/src/main/resources/META-INF/services/software.amazon.smithy.build.ProjectionTransformer +++ b/smithy-build/src/main/resources/META-INF/services/software.amazon.smithy.build.ProjectionTransformer @@ -14,3 +14,4 @@ software.amazon.smithy.build.transforms.IncludeTraits software.amazon.smithy.build.transforms.IncludeTraitsByTag software.amazon.smithy.build.transforms.RemoveTraitDefinitions software.amazon.smithy.build.transforms.RemoveUnusedShapes +software.amazon.smithy.build.transforms.RenameShapes diff --git a/smithy-build/src/test/java/software/amazon/smithy/build/transforms/RenameShapesTest.java b/smithy-build/src/test/java/software/amazon/smithy/build/transforms/RenameShapesTest.java new file mode 100644 index 00000000000..f0300483811 --- /dev/null +++ b/smithy-build/src/test/java/software/amazon/smithy/build/transforms/RenameShapesTest.java @@ -0,0 +1,140 @@ +/* + * Copyright 2021 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.build.transforms; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.not; + +import java.nio.file.Paths; +import java.util.List; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import software.amazon.smithy.build.SmithyBuildException; +import software.amazon.smithy.build.TransformContext; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.loader.Prelude; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.utils.FunctionalUtils; + +public class RenameShapesTest { + + @Test + public void renameShapes() throws Exception { + Model model = Model.assembler() + .addImport(Paths.get(getClass().getResource("rename-shapes.json").toURI())) + .assemble() + .unwrap(); + ObjectNode renamed = Node.objectNode() + .withMember("ns.foo#Bar", "ns.foo#Baz") + .withMember("ns.foo#Qux", "ns.foo#Corge"); + ObjectNode config = Node.objectNode() + .withMember("renamed", renamed); + TransformContext context = TransformContext.builder() + .model(model) + .settings(config) + .build(); + Model result = new RenameShapes().transform(context); + List ids = result.shapes() + .filter(FunctionalUtils.not(Prelude::isPreludeShape)) + .map(Shape::getId) + .map(Object::toString) + .collect(Collectors.toList()); + + assertThat(ids, containsInAnyOrder("ns.foo#MyService", "ns.foo#Baz", "ns.foo#Corge", "ns.foo#Corge$foo")); + assertThat(ids, not(containsInAnyOrder("ns.foo#Bar", "ns.foo#Qux"))); + } + + @Test + public void throwsWhenRenamedPropertyIsNotConfigured() { + Model model = Model.assembler() + .addUnparsedModel("N/A", "{ \"smithy\": \"1.0\" }") + .assemble() + .unwrap(); + TransformContext context = TransformContext.builder() + .model(model) + .settings(Node.objectNode().withMember("badConfig", Node.from("foo"))) + .build(); + Assertions.assertThrows(SmithyBuildException.class, () -> new RenameShapes().transform(context)); + } + + @Test + public void throwsWhenRenamedPropertyMapIsEmpty() { + Model model = Model.assembler() + .addUnparsedModel("N/A", "{ \"smithy\": \"1.0\" }") + .assemble() + .unwrap(); + TransformContext context = TransformContext.builder() + .model(model) + .settings(Node.objectNode().withMember("renamed", Node.nullNode())) + .build(); + Assertions.assertThrows(SmithyBuildException.class, () -> new RenameShapes().transform(context)); + } + + @Test + public void throwsWhenShapeToBeRenamedIsNotFound() { + Model model = Model.assembler() + .addUnparsedModel("N/A", "{ \"smithy\": \"1.0\" }") + .assemble() + .unwrap(); + ObjectNode renamed = Node.objectNode() + .withMember("ns.foo#Bar", "ns.foo#Baz"); + ObjectNode config = Node.objectNode() + .withMember("renamed", renamed); + TransformContext context = TransformContext.builder() + .model(model) + .settings(config) + .build(); + Assertions.assertThrows(SmithyBuildException.class, () -> new RenameShapes().transform(context)); + } + + @Test + public void throwsWhenFromShapeIsInvalid() { + Model model = Model.assembler() + .addUnparsedModel("N/A", "{ \"smithy\": \"1.0\" }") + .assemble() + .unwrap(); + ObjectNode renamed = Node.objectNode() + .withMember("Bar", "ns.foo#Baz"); + ObjectNode config = Node.objectNode() + .withMember("renamed", renamed); + TransformContext context = TransformContext.builder() + .model(model) + .settings(config) + .build(); + Assertions.assertThrows(SmithyBuildException.class, () -> new RenameShapes().transform(context)); + } + + @Test + public void throwsWhenToShapeIsInvalid() { + Model model = Model.assembler() + .addUnparsedModel("N/A", "{ \"smithy\": \"1.0\" }") + .assemble() + .unwrap(); + ObjectNode renamed = Node.objectNode() + .withMember("ns.foo#Bar", "Baz"); + ObjectNode config = Node.objectNode() + .withMember("renamed", renamed); + TransformContext context = TransformContext.builder() + .model(model) + .settings(config) + .build(); + Assertions.assertThrows(SmithyBuildException.class, () -> new RenameShapes().transform(context)); + } +} diff --git a/smithy-build/src/test/resources/software/amazon/smithy/build/transforms/rename-shapes.json b/smithy-build/src/test/resources/software/amazon/smithy/build/transforms/rename-shapes.json new file mode 100644 index 00000000000..bc9097d4d0b --- /dev/null +++ b/smithy-build/src/test/resources/software/amazon/smithy/build/transforms/rename-shapes.json @@ -0,0 +1,28 @@ +{ + "smithy": "1.0", + "shapes": { + "ns.foo#MyService": { + "type": "service", + "version": "2017-01-19", + "operations": [ + { + "target": "ns.foo#Bar" + } + ] + }, + "ns.foo#Bar": { + "type": "operation", + "output": { + "target": "ns.foo#Qux" + } + }, + "ns.foo#Qux": { + "type": "structure", + "members": { + "foo": { + "target": "smithy.api#String" + } + } + } + } +}