diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/SmithyCli.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/SmithyCli.java index d7e353e693a..2f857b08b61 100644 --- a/smithy-cli/src/main/java/software/amazon/smithy/cli/SmithyCli.java +++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/SmithyCli.java @@ -20,6 +20,7 @@ import software.amazon.smithy.cli.commands.BuildCommand; import software.amazon.smithy.cli.commands.DiffCommand; import software.amazon.smithy.cli.commands.SelectCommand; +import software.amazon.smithy.cli.commands.Upgrade1to2Command; import software.amazon.smithy.cli.commands.ValidateCommand; /** @@ -98,6 +99,7 @@ private Cli createCliRunner() { cli.addCommand(new DiffCommand()); cli.addCommand(new SelectCommand()); cli.addCommand(new AstCommand()); + cli.addCommand(new Upgrade1to2Command()); cli.setConfigureLogging(configureLogging); return cli; } diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/CommandUtils.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/CommandUtils.java index 6271828f296..bc01cfe22c7 100644 --- a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/CommandUtils.java +++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/CommandUtils.java @@ -22,6 +22,7 @@ import java.security.AccessController; import java.security.PrivilegedAction; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Set; import java.util.function.Consumer; @@ -44,7 +45,41 @@ final class CommandUtils { private CommandUtils() {} static Model buildModel(Arguments arguments, ClassLoader classLoader, Set features) { - List models = arguments.positionalArguments(); + Severity minSeverity = arguments.has(SmithyCli.SEVERITY) + ? parseSeverity(arguments.parameter(SmithyCli.SEVERITY)) + : Severity.NOTE; + return buildModel( + arguments.positionalArguments(), + classLoader, + minSeverity, + arguments.has(SmithyCli.DISCOVER), + arguments.parameter(SmithyCli.DISCOVER_CLASSPATH, null), + arguments.has(SmithyCli.ALLOW_UNKNOWN_TRAITS), + features + ); + } + + static Model buildModel( + List models, + ClassLoader classLoader, + Severity minSeverity, + boolean discover, + String discoverPath, + boolean allowUnknownTraits + ) { + return buildModel( + models, classLoader, minSeverity, discover, discoverPath, allowUnknownTraits, Collections.emptySet()); + } + + static Model buildModel( + List models, + ClassLoader classLoader, + Severity minSeverity, + boolean discover, + String discoverPath, + boolean allowUnknownTraits, + Set features + ) { ModelAssembler assembler = CommandUtils.createModelAssembler(classLoader); ContextualValidationEventFormatter formatter = new ContextualValidationEventFormatter(); @@ -52,11 +87,6 @@ static Model buildModel(Arguments arguments, ClassLoader classLoader, Set writer = stdout ? Cli.getStdout() : Cli.getStderr(); - // --severity defaults to NOTE. - Severity minSeverity = arguments.has(SmithyCli.SEVERITY) - ? parseSeverity(arguments.parameter(SmithyCli.SEVERITY)) - : Severity.NOTE; - assembler.validationEventListener(event -> { // Only log events that are >= --severity. if (event.getSeverity().ordinal() >= minSeverity.ordinal()) { @@ -72,8 +102,17 @@ static Model buildModel(Arguments arguments, ClassLoader classLoader, Set result = assembler.assemble(); Validator.validate(result, features); @@ -89,23 +128,7 @@ static ModelAssembler createModelAssembler(ClassLoader classLoader) { return Model.assembler(classLoader).putProperty(ModelAssembler.DISABLE_JAR_CACHE, true); } - private static void handleUnknownTraitsOption(Arguments arguments, ModelAssembler assembler) { - if (arguments.has(SmithyCli.ALLOW_UNKNOWN_TRAITS)) { - LOGGER.fine("Ignoring unknown traits"); - assembler.putProperty(ModelAssembler.ALLOW_UNKNOWN_TRAITS, true); - } - } - - private static void handleModelDiscovery(Arguments arguments, ModelAssembler assembler, ClassLoader baseLoader) { - if (arguments.has(SmithyCli.DISCOVER_CLASSPATH)) { - discoverModelsWithClasspath(arguments, assembler); - } else if (arguments.has(SmithyCli.DISCOVER)) { - assembler.discoverModels(baseLoader); - } - } - - private static void discoverModelsWithClasspath(Arguments arguments, ModelAssembler assembler) { - String rawClasspath = arguments.parameter(SmithyCli.DISCOVER_CLASSPATH); + private static void discoverModelsWithClasspath(String rawClasspath, ModelAssembler assembler) { LOGGER.finer("Discovering models with classpath: " + rawClasspath); // Use System.getProperty here each time since it allows the value to be changed. diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/Upgrade1to2Command.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/Upgrade1to2Command.java new file mode 100644 index 00000000000..57a341177fb --- /dev/null +++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/Upgrade1to2Command.java @@ -0,0 +1,494 @@ +/* + * Copyright 2022 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.cli.commands; + +import static java.lang.String.format; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import software.amazon.smithy.build.ProjectionResult; +import software.amazon.smithy.build.SmithyBuild; +import software.amazon.smithy.build.model.SmithyBuildConfig; +import software.amazon.smithy.cli.Arguments; +import software.amazon.smithy.cli.CliError; +import software.amazon.smithy.cli.Command; +import software.amazon.smithy.cli.Parser; +import software.amazon.smithy.cli.SmithyCli; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.SourceLocation; +import software.amazon.smithy.model.loader.ModelAssembler; +import software.amazon.smithy.model.loader.ParserUtils; +import software.amazon.smithy.model.loader.Prelude; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeType; +import software.amazon.smithy.model.shapes.ShapeVisitor; +import software.amazon.smithy.model.shapes.SmithyIdlModelSerializer; +import software.amazon.smithy.model.shapes.StringShape; +import software.amazon.smithy.model.traits.BoxTrait; +import software.amazon.smithy.model.traits.DefaultTrait; +import software.amazon.smithy.model.traits.EnumTrait; +import software.amazon.smithy.model.traits.Trait; +import software.amazon.smithy.model.transform.ModelTransformer; +import software.amazon.smithy.model.validation.Severity; +import software.amazon.smithy.utils.IoUtils; +import software.amazon.smithy.utils.SimpleParser; +import software.amazon.smithy.utils.SmithyInternalApi; +import software.amazon.smithy.utils.StringUtils; + +@SmithyInternalApi +public final class Upgrade1to2Command implements Command { + private static final Logger LOGGER = Logger.getLogger(Upgrade1to2Command.class.getName()); + private static final Pattern VERSION_1 = Pattern.compile("(?m)^\\s*\\$\\s*version:\\s*\"1\\.0\"\\s*$"); + private static final Pattern VERSION_2 = Pattern.compile("(?m)^\\s*\\$\\s*version:\\s*\"2\\.0\"\\s*$"); + private static final EnumSet HAD_DEFAULT_VALUE_IN_1_0 = EnumSet.of( + ShapeType.BYTE, + ShapeType.SHORT, + ShapeType.INTEGER, + ShapeType.LONG, + ShapeType.FLOAT, + ShapeType.DOUBLE, + ShapeType.BOOLEAN); + + @Override + public String getName() { + return "upgrade-1-to-2"; + } + + @Override + public String getSummary() { + return "Upgrades Smithy IDL model files from 1.0 to 2.0 in place."; + } + + @Override + public Parser getParser() { + return Parser.builder() + .repeatedParameter("--config", "-c", + "Path to smithy-build.json configuration. Defaults to 'smithy-build.json'.") + .option(SmithyCli.DISCOVER, "-d", "Enables model discovery, merging in models found inside of jars") + .parameter(SmithyCli.DISCOVER_CLASSPATH, "Enables model discovery using a custom classpath for models") + .positional("", "Path to Smithy models or directories") + .build(); + } + + @Override + public void execute(Arguments arguments, ClassLoader classLoader) { + upgradeFiles( + arguments.positionalArguments(), + arguments.parameter(SmithyCli.DISCOVER_CLASSPATH, null), + arguments.parameter("--config"), + classLoader + ); + } + + private void upgradeFiles( + List modelFilesOrDirectories, + String discoverPath, + String config, + ClassLoader classLoader + ) { + // Use the provided smithy-build.json file + SmithyBuildConfig.Builder configBuilder = SmithyBuildConfig.builder(); + configBuilder.load(Paths.get(config).toAbsolutePath()); + + // Set an output into a temporary directory - we don't actually care about + // the serialized output. + Path tempDir; + try { + tempDir = Files.createTempDirectory("smithyUpgrade"); + } catch (IOException e) { + throw new CliError("Unable to create temporary working directory: " + e); + } + configBuilder.outputDirectory(tempDir.toString()); + + Model initialModel = CommandUtils.buildModel( + modelFilesOrDirectories, + classLoader, + Severity.DANGER, + false, + discoverPath, + true + ); + + SmithyBuild smithyBuild = SmithyBuild.create(classLoader) + .config(configBuilder.build()) + // Only build the source projection + .projectionFilter(name -> name.equals("source")) + // The only traits we care about looking at are in the prelude, + // so we can safely ignore any that are unknown. + .modelAssemblerSupplier(() -> { + ModelAssembler assembler = Model.assembler(); + assembler.putProperty(ModelAssembler.ALLOW_UNKNOWN_TRAITS, true); + return assembler; + }) + .model(initialModel); + + // Run SmithyBuild to get the finalized model + ResultConsumer resultConsumer = new ResultConsumer(); + smithyBuild.build(resultConsumer, resultConsumer); + Model finalizedModel = resultConsumer.getResult().getModel(); + + for (Path modelFile : resolveModelFiles(finalizedModel, modelFilesOrDirectories)) { + writeUpgradedFile(finalizedModel, modelFile); + } + } + + private List resolveModelFiles(Model model, List modelFilesOrDirectories) { + Set absoluteModelFilesOrDirectories = modelFilesOrDirectories.stream() + .map(path -> Paths.get(path).toAbsolutePath()) + .collect(Collectors.toSet()); + return model.shapes() + .filter(shape -> !Prelude.isPreludeShape(shape)) + .filter(shape -> !shape.getSourceLocation().getFilename().startsWith("jar:")) + .map(shape -> Paths.get(shape.getSourceLocation().getFilename()).toAbsolutePath()) + .distinct() + .filter(locationPath -> { + for (Path inputPath : absoluteModelFilesOrDirectories) { + if (!locationPath.startsWith(inputPath)) { + LOGGER.finest("Skipping non-target model file: " + locationPath); + return false; + } + } + if (!locationPath.toString().endsWith(".smithy")) { + LOGGER.info("Skipping non-IDL model file: " + locationPath); + return false; + } + return true; + }) + .sorted() + .collect(Collectors.toList()); + } + + private void writeUpgradedFile(Model completeModel, Path filePath) { + try { + Files.write(filePath, upgradeFile(completeModel, filePath).getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new CliError(format("Unable to write upgraded model file to %s: %s", filePath, e)); + } + } + + String upgradeFile(Model completeModel, Path filePath) { + String contents = IoUtils.readUtf8File(filePath); + if (VERSION_2.matcher(contents).find()) { + return contents; + } + + ShapeUpgradeVisitor visitor = new ShapeUpgradeVisitor(completeModel, contents); + + completeModel.shapes() + .filter(shape -> shape.getSourceLocation().getFilename().equals(filePath.toString())) + // Apply updates to the shapes at the bottom of the file first. + // This lets us modify the file without invalidating the existing + // source locations. + .sorted(Comparator.comparing(Shape::getSourceLocation).reversed()) + .forEach(shape -> shape.accept(visitor)); + + return updateVersion(visitor.getModelString()); + } + + private String updateVersion(String modelString) { + Matcher matcher = VERSION_1.matcher(modelString); + if (matcher.find()) { + return matcher.replaceFirst(format("\\$version: \"2.0\"%n")); + } + return format("$version: \"2.0\"%n%n") + modelString; + } + + private static final class ResultConsumer implements Consumer, BiConsumer { + private Throwable error; + private ProjectionResult result; + + @Override + public void accept(String name, Throwable throwable) { + this.error = throwable; + } + + @Override + public void accept(ProjectionResult projectionResult) { + // We only expect one result because we're only building one + // projection - the source projection. + this.result = projectionResult; + } + + ProjectionResult getResult() { + if (error != null) { + throw new RuntimeException(error); + } + return result; + } + } + + private static class ShapeUpgradeVisitor extends ShapeVisitor.Default { + private final Model completeModel; + private final ModelWriter writer; + + ShapeUpgradeVisitor(Model completeModel, String modelString) { + this.completeModel = completeModel; + this.writer = new ModelWriter(modelString); + } + + String getModelString() { + return writer.flush(); + } + + @Override + protected Void getDefault(Shape shape) { + if (shape.hasTrait(BoxTrait.class)) { + writer.eraseTrait(shape, shape.expectTrait(BoxTrait.class)); + } + // Handle members in reverse definition order. + shape.members().stream() + .sorted(Comparator.comparing(Shape::getSourceLocation).reversed()) + .forEach(this::handleMemberShape); + return null; + } + + private void handleMemberShape(MemberShape shape) { + replacePrimitiveTarget(shape); + + if (hasSyntheticDefault(shape)) { + SourceLocation memberLocation = shape.getSourceLocation(); + String padding = ""; + if (memberLocation.getColumn() > 1) { + padding = StringUtils.repeat(' ', memberLocation.getColumn() - 1); + } + writer.insertLine(shape.getSourceLocation().getLine(), padding + "@default"); + } + + if (shape.hasTrait(BoxTrait.class)) { + writer.eraseTrait(shape, shape.expectTrait(BoxTrait.class)); + } + } + + private void replacePrimitiveTarget(MemberShape member) { + Shape target = completeModel.expectShape(member.getTarget()); + if (!Prelude.isPreludeShape(target) || !HAD_DEFAULT_VALUE_IN_1_0.contains(target.getType())) { + return; + } + + IdlAwareSimpleParser parser = new IdlAwareSimpleParser(writer.flush()); + parser.rewind(member.getSourceLocation()); + + parser.consumeUntilNoLongerMatches(character -> character != ':'); + parser.skip(); + parser.ws(); + + // Capture the start of the target identifier. + int start = parser.position(); + parser.consumeUntilNoLongerMatches(ParserUtils::isValidIdentifierCharacter); + + // Replace the target with the proper target. Note that we don't + // need to do any sort of mapping because smithy already upgraded + // the target, so we can just use the name of the target it added. + writer.replace(start, parser.position(), target.getId().getName()); + } + + private boolean hasSyntheticDefault(MemberShape shape) { + Shape target = completeModel.expectShape(shape.getTarget()); + if (!(HAD_DEFAULT_VALUE_IN_1_0.contains(target.getType()) && shape.hasTrait(DefaultTrait.class))) { + return false; + } + // When Smithy injects the default trait, it sets the source + // location equal to the shape's source location. This is + // impossible in any other scenario, so we can use this info + // to know whether it was injected or not. + return shape.getSourceLocation().equals(shape.expectTrait(DefaultTrait.class).getSourceLocation()); + } + + @Override + public Void memberShape(MemberShape shape) { + // members are handled from their containers so that they can + // be properly sorted. + return null; + } + + @Override + public Void stringShape(StringShape shape) { + if (!shape.hasTrait(EnumTrait.class)) { + return null; + } + + EnumTrait enumTrait = shape.expectTrait(EnumTrait.class); + if (!enumTrait.getValues().iterator().next().getName().isPresent()) { + return null; + } + + writer.insertLine(shape.getSourceLocation().getLine() + 1, serializeEnum(shape)); + writer.eraseLine(shape.getSourceLocation().getLine()); + writer.eraseTrait(shape, enumTrait); + + return null; + } + + private String serializeEnum(StringShape shape) { + // Strip all the traits from the shape except the enum trait. + // We're leaving the other traits where they are in the model + // string to preserve things like comments as much as is possible. + StringShape stripped = shape.toBuilder() + .clearTraits() + .addTrait(shape.expectTrait(EnumTrait.class)) + .build(); + + // Build a faux model that only contains the enum we want to write. + Model model = Model.assembler() + .addShapes(stripped) + .assemble().unwrap(); + + // Use existing conversion tools to convert it to an enum shape, + // then serialize it using the idl serializer. + model = ModelTransformer.create().changeStringEnumsToEnumShapes(model); + Map files = SmithyIdlModelSerializer.builder().build().serialize(model); + + // There's only one shape, so there should only be one file. + String serialized = files.values().iterator().next(); + + // The serialized file will contain things we don't want, like the + // namespace and version statements, so here we strip everything + // we find before the enum statement. + ArrayList lines = new ArrayList<>(); + boolean foundEnum = false; + for (String line : serialized.split("\\r?\\n")) { + if (foundEnum) { + lines.add(line); + } else if (line.startsWith("enum")) { + lines.add(line); + foundEnum = true; + } + } + + return String.join(System.lineSeparator(), lines); + } + } + + private static class IdlAwareSimpleParser extends SimpleParser { + IdlAwareSimpleParser(String expression) { + super(expression); + } + + public void rewind(SourceLocation location) { + rewind(0, 1, 1); + while (!eof()) { + if (line() == location.getLine() && column() == location.getColumn()) { + break; + } + skip(); + } + if (eof()) { + throw syntax("Expected a source location, but was EOF"); + } + } + + @Override + public void ws() { + while (!eof()) { + switch (peek()) { + case '/': + // If we see a comment, advance to the next line. + if (peek(1) == '/') { + consumeRemainingCharactersOnLine(); + break; + } else { + return; + } + case ' ': + case '\t': + case '\r': + case '\n': + case ',': + skip(); + break; + default: + return; + } + } + } + } + + private static class ModelWriter { + private String contents; + + ModelWriter(String contents) { + this.contents = contents; + } + + public String flush() { + if (!contents.endsWith(System.lineSeparator())) { + contents = contents + System.lineSeparator(); + } + return contents; + } + + private void insertLine(int lineNumber, String line) { + List lines = new ArrayList<>(Arrays.asList(contents.split("\\r?\\n"))); + lines.add(lineNumber - 1, line); + contents = String.join(System.lineSeparator(), lines); + } + + private void eraseLine(int lineNumber) { + List lines = new ArrayList<>(Arrays.asList(contents.split("\\r?\\n"))); + lines.remove(lineNumber - 1); + contents = String.join(System.lineSeparator(), lines); + } + + private void eraseTrait(Shape shape, Trait trait) { + SourceLocation to = findLocationAfterTrait(shape, trait.getClass()); + erase(trait.getSourceLocation(), to); + } + + private SourceLocation findLocationAfterTrait(Shape shape, Class target) { + boolean haveSeenTarget = false; + List traits = new ArrayList<>(shape.getIntroducedTraits().values()); + traits.sort(Comparator.comparing(Trait::getSourceLocation)); + for (Trait trait : traits) { + if (target.isInstance(trait)) { + haveSeenTarget = true; + } else if (haveSeenTarget && !trait.getSourceLocation().equals(SourceLocation.NONE)) { + return trait.getSourceLocation(); + } + } + return shape.getSourceLocation(); + } + + private void erase(SourceLocation from, SourceLocation to) { + IdlAwareSimpleParser parser = new IdlAwareSimpleParser(contents); + parser.rewind(from); + int fromPosition = parser.position(); + parser.rewind(to); + int toPosition = parser.position(); + contents = contents.substring(0, fromPosition) + contents.substring(toPosition); + } + + private void replace(int from, int to, String with) { + contents = contents.substring(0, from) + with + contents.substring(to); + } + } +} diff --git a/smithy-cli/src/test/java/software/amazon/smithy/cli/commands/Upgrade1to2CommandTest.java b/smithy-cli/src/test/java/software/amazon/smithy/cli/commands/Upgrade1to2CommandTest.java new file mode 100644 index 00000000000..645ce20add8 --- /dev/null +++ b/smithy-cli/src/test/java/software/amazon/smithy/cli/commands/Upgrade1to2CommandTest.java @@ -0,0 +1,182 @@ +/* + * Copyright 2022 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.cli.commands; + +import static java.nio.file.FileVisitResult.CONTINUE; +import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.FileVisitor; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileTime; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import software.amazon.smithy.cli.SmithyCli; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.utils.IoUtils; + +public class Upgrade1to2CommandTest { + + @ParameterizedTest(name = "{1}") + @MethodSource("source") + public void testUpgrade(Path initialPath, String name) { + Path parentDir = initialPath.getParent(); + String expectedFileName = initialPath.getFileName().toString().replace(".v1.smithy", ".v2.smithy"); + Path expectedPath = parentDir.resolve(expectedFileName); + + Model model = Model.assembler().addImport(initialPath).assemble().unwrap(); + String actual = new Upgrade1to2Command().upgradeFile(model, initialPath); + String expected = IoUtils.readUtf8File(expectedPath); + assertThat(actual, equalTo(expected)); + } + + public static Stream source() throws Exception { + Path start = Paths.get(Upgrade1to2CommandTest.class.getResource("upgrade/cases").toURI()); + return Files.walk(start) + .filter(path -> path.getFileName().toString().endsWith("enum-with-traits.v1.smithy")) + .map(path -> Arguments.of(path, path.getFileName().toString().replace(".v1.smithy", ""))); + } + + @Test + public void testUpgradeDirectory() throws Exception { + Path baseDir = Paths.get(Upgrade1to2CommandTest.class.getResource( + "upgrade/directory-cases/all-local/v1").toURI()).toAbsolutePath(); + + Path tempDir = Files.createTempDirectory("testUpgradeDirectory"); + copyDir(baseDir, tempDir); + + Path modelsDir = tempDir.resolve("model"); + Path config = tempDir.resolve("smithy-build.json"); + + SmithyCli.create().run("upgrade-1-to-2", "--config", config.toString(), modelsDir.toString()); + assertDirEqual(baseDir.getParent().resolve("v2"), tempDir); + } + + @Test + public void testUpgradeDirectoryWithProjection() throws Exception { + Path baseDir = Paths.get(Upgrade1to2CommandTest.class.getResource( + "upgrade/directory-cases/ignores-projections/v1").toURI()); + + Path tempDir = Files.createTempDirectory("testUpgradeDirectory"); + copyDir(baseDir, tempDir); + + Path modelsDir = tempDir.resolve("model"); + Path config = tempDir.resolve("smithy-build.json"); + SmithyCli.create().run("upgrade-1-to-2", "--config", config.toString(), modelsDir.toString()); + assertDirEqual(baseDir.getParent().resolve("v2"), tempDir); + } + + @Test + public void testUpgradeDirectoryWithJar() throws Exception { + Path baseDir = Paths.get(Upgrade1to2CommandTest.class.getResource( + "upgrade/directory-cases/with-jar/v1").toURI()); + + Path tempDir = Files.createTempDirectory("testUpgradeDirectory"); + copyDir(baseDir, tempDir); + + Path modelsDir = tempDir.resolve("model"); + Path config = tempDir.resolve("smithy-build.json"); + SmithyCli.create().run( + "upgrade-1-to-2", + "--config", config.toString(), + "--discover-classpath", tempDir.toAbsolutePath().resolve("jar-import.jar").toString(), + modelsDir.toString() + ); + assertDirEqual(baseDir.getParent().resolve("v2"), tempDir); + + } + + private void assertDirEqual(Path actualDir, Path excpectedDir) throws Exception { + Set files = Files.walk(actualDir) + .filter(Files::isRegularFile) + .filter(path -> path.toString().endsWith(".smithy")) + .collect(Collectors.toSet()); + for (Path actual : files) { + Path expected = excpectedDir.resolve(actualDir.relativize(actual)); + assertThat(IoUtils.readUtf8File(actual), equalTo(IoUtils.readUtf8File(expected))); + } + } + + // Why does Java make this so hard + private void copyDir(Path source, Path target) throws IOException { + Files.walkFileTree(source, new DirectoryCopier(source, target)); + } + + static class DirectoryCopier implements FileVisitor { + private final Path source; + private final Path target; + + DirectoryCopier(Path source, Path target) { + this.source = source; + this.target = target; + } + + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { + if (dir.toAbsolutePath().equals(source.toAbsolutePath())) { + return CONTINUE; + } + try { + Files.copy(dir, target.resolve(source.relativize(dir)), REPLACE_EXISTING); + } catch (Exception e) { + throw new RuntimeException(e); + } + return CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { + try { + Files.copy(file, target.resolve(source.relativize(file))); + } catch (Exception e) { + throw new RuntimeException(e); + } + return CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException e) { + if (e != null) { + throw new RuntimeException(e); + } + + try { + FileTime time = Files.getLastModifiedTime(dir); + Files.setLastModifiedTime(target.resolve(source.relativize(dir)), time); + } catch (Exception exc) { + throw new RuntimeException(exc); + } + + return CONTINUE; + } + + @Override + public FileVisitResult visitFileFailed(Path file, IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/box.v1.smithy b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/box.v1.smithy new file mode 100644 index 00000000000..6442e831784 --- /dev/null +++ b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/box.v1.smithy @@ -0,0 +1,39 @@ +$version: "1.0" + +namespace com.example + +@box +integer BoxedInteger + +integer NonBoxedInteger + +structure StructureWithOptionalString { + boxedTarget: BoxedInteger, + + @box + boxedMember: NonBoxedInteger, +} + +union BoxyUnion { + boxedTarget: BoxedInteger, + + @box + boxedMember: NonBoxedInteger, +} + +list BadSparseList { + @box + member: NonBoxedInteger, +} + +set BadSparseSet { + @box + member: NonBoxedInteger, +} + +map BadSparseMap { + key: String, + + @box + value: NonBoxedInteger, +} diff --git a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/box.v2.smithy b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/box.v2.smithy new file mode 100644 index 00000000000..4068a53b54e --- /dev/null +++ b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/box.v2.smithy @@ -0,0 +1,33 @@ +$version: "2.0" + +namespace com.example + +integer BoxedInteger + +integer NonBoxedInteger + +structure StructureWithOptionalString { + boxedTarget: BoxedInteger, + + boxedMember: NonBoxedInteger, +} + +union BoxyUnion { + boxedTarget: BoxedInteger, + + boxedMember: NonBoxedInteger, +} + +list BadSparseList { + member: NonBoxedInteger, +} + +set BadSparseSet { + member: NonBoxedInteger, +} + +map BadSparseMap { + key: String, + + value: NonBoxedInteger, +} diff --git a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/enum-with-traits.v1.smithy b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/enum-with-traits.v1.smithy new file mode 100644 index 00000000000..56e2f4b3e3f --- /dev/null +++ b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/enum-with-traits.v1.smithy @@ -0,0 +1,42 @@ +$version: "1.0" + +namespace com.example + +@enum([ + { + name: "FOO", + value: "foo", + }, + { + name: "BAR", + value: "bar", + } +]) +@internal +string TraitAfterEnum + +@internal +@enum([ + { + name: "FOO", + value: "foo", + }, + { + name: "BAR", + value: "bar", + } +]) +string TraitBeforeEnum + +@enum([ + { + name: "FOO", + value: "foo", + }, + { + name: "BAR", + value: "bar", + } +]) +@internal() +string AnnotationTraitWithParens diff --git a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/enum-with-traits.v2.smithy b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/enum-with-traits.v2.smithy new file mode 100644 index 00000000000..f702d63af4c --- /dev/null +++ b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/enum-with-traits.v2.smithy @@ -0,0 +1,27 @@ +$version: "2.0" + +namespace com.example + +@internal +enum TraitAfterEnum { + @enumValue("foo") + FOO + @enumValue("bar") + BAR +} + +@internal +enum TraitBeforeEnum { + @enumValue("foo") + FOO + @enumValue("bar") + BAR +} + +@internal() +enum AnnotationTraitWithParens { + @enumValue("foo") + FOO + @enumValue("bar") + BAR +} diff --git a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/enum.v1.smithy b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/enum.v1.smithy new file mode 100644 index 00000000000..3c5a605c809 --- /dev/null +++ b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/enum.v1.smithy @@ -0,0 +1,15 @@ +$version: "1.0" + +namespace com.example + +@enum([ + { + name: "FOO", + value: "foo", + }, + { + name: "BAR", + value: "bar", + } +]) +string EnumString diff --git a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/enum.v2.smithy b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/enum.v2.smithy new file mode 100644 index 00000000000..56dbe6205be --- /dev/null +++ b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/enum.v2.smithy @@ -0,0 +1,10 @@ +$version: "2.0" + +namespace com.example + +enum EnumString { + @enumValue("foo") + FOO + @enumValue("bar") + BAR +} diff --git a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/multiple-enums.v1.smithy b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/multiple-enums.v1.smithy new file mode 100644 index 00000000000..8760642f7ee --- /dev/null +++ b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/multiple-enums.v1.smithy @@ -0,0 +1,27 @@ +$version: "1.0" + +namespace com.example + +@enum([ + { + name: "FOO", + value: "foo", + }, + { + name: "BAR", + value: "bar", + } +]) +string First + +@enum([ + { + name: "FOO", + value: "foo", + }, + { + name: "BAR", + value: "bar", + } +]) +string Second diff --git a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/multiple-enums.v2.smithy b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/multiple-enums.v2.smithy new file mode 100644 index 00000000000..aaad6ad6368 --- /dev/null +++ b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/multiple-enums.v2.smithy @@ -0,0 +1,17 @@ +$version: "2.0" + +namespace com.example + +enum First { + @enumValue("foo") + FOO + @enumValue("bar") + BAR +} + +enum Second { + @enumValue("foo") + FOO + @enumValue("bar") + BAR +} diff --git a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/non-convertible-enum.v1.smithy b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/non-convertible-enum.v1.smithy new file mode 100644 index 00000000000..5374a298242 --- /dev/null +++ b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/non-convertible-enum.v1.smithy @@ -0,0 +1,13 @@ +$version: "1.0" + +namespace com.example + +@enum([ + { + value: "\tfoo", + }, + { + value: "\tbar", + } +]) +string EnumString diff --git a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/non-convertible-enum.v2.smithy b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/non-convertible-enum.v2.smithy new file mode 100644 index 00000000000..6a342d60698 --- /dev/null +++ b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/non-convertible-enum.v2.smithy @@ -0,0 +1,13 @@ +$version: "2.0" + +namespace com.example + +@enum([ + { + value: "\tfoo", + }, + { + value: "\tbar", + } +]) +string EnumString diff --git a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/remove-primitive.v1.smithy b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/remove-primitive.v1.smithy new file mode 100644 index 00000000000..b792cfdaabe --- /dev/null +++ b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/remove-primitive.v1.smithy @@ -0,0 +1,25 @@ +$version: "1.0" + +namespace com.example + +structure PrimitiveBearer { + int: PrimitiveInteger, + bool: PrimitiveBoolean, + byte: PrimitiveByte, + double: PrimitiveDouble, + float: PrimitiveFloat, + long: PrimitiveLong, + short: PrimitiveShort, + + handlesComments: // Nobody actually does this right? + PrimitiveShort, + + @default + handlesPreexistingDefault: PrimitiveShort, + + @required + handlesRequired: PrimitiveLong, + + @box + handlesBox: PrimitiveByte, +} diff --git a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/remove-primitive.v2.smithy b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/remove-primitive.v2.smithy new file mode 100644 index 00000000000..a71cb70c565 --- /dev/null +++ b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/remove-primitive.v2.smithy @@ -0,0 +1,32 @@ +$version: "2.0" + +namespace com.example + +structure PrimitiveBearer { + @default + int: Integer, + @default + bool: Boolean, + @default + byte: Byte, + @default + double: Double, + @default + float: Float, + @default + long: Long, + @default + short: Short, + + @default + handlesComments: // Nobody actually does this right? + Short, + + @default + handlesPreexistingDefault: Short, + + @required + handlesRequired: Long, + + handlesBox: Byte, +} diff --git a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/version.v1.smithy b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/version.v1.smithy new file mode 100644 index 00000000000..2b3efdd3447 --- /dev/null +++ b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/version.v1.smithy @@ -0,0 +1,3 @@ +namespace com.example + +string Foo diff --git a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/version.v2.smithy b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/version.v2.smithy new file mode 100644 index 00000000000..1c4e1bafe8a --- /dev/null +++ b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/cases/version.v2.smithy @@ -0,0 +1,5 @@ +$version: "2.0" + +namespace com.example + +string Foo diff --git a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/directory-cases/all-local/v1/model/A.smithy b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/directory-cases/all-local/v1/model/A.smithy new file mode 100644 index 00000000000..4c0515d37cf --- /dev/null +++ b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/directory-cases/all-local/v1/model/A.smithy @@ -0,0 +1,5 @@ +$version: "1.0" + +namespace com.example + +string A diff --git a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/directory-cases/all-local/v1/model/B.smithy b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/directory-cases/all-local/v1/model/B.smithy new file mode 100644 index 00000000000..7ec3566d85b --- /dev/null +++ b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/directory-cases/all-local/v1/model/B.smithy @@ -0,0 +1,5 @@ +$version: "1.0" + +namespace com.example + +string B diff --git a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/directory-cases/all-local/v1/smithy-build.json b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/directory-cases/all-local/v1/smithy-build.json new file mode 100644 index 00000000000..04450271687 --- /dev/null +++ b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/directory-cases/all-local/v1/smithy-build.json @@ -0,0 +1,3 @@ +{ + "version": "1.0" +} diff --git a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/directory-cases/all-local/v2/model/A.smithy b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/directory-cases/all-local/v2/model/A.smithy new file mode 100644 index 00000000000..ee76322774c --- /dev/null +++ b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/directory-cases/all-local/v2/model/A.smithy @@ -0,0 +1,5 @@ +$version: "2.0" + +namespace com.example + +string A diff --git a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/directory-cases/all-local/v2/model/B.smithy b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/directory-cases/all-local/v2/model/B.smithy new file mode 100644 index 00000000000..3ed3daf9463 --- /dev/null +++ b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/directory-cases/all-local/v2/model/B.smithy @@ -0,0 +1,5 @@ +$version: "2.0" + +namespace com.example + +string B diff --git a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/directory-cases/all-local/v2/smithy-build.json b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/directory-cases/all-local/v2/smithy-build.json new file mode 100644 index 00000000000..04450271687 --- /dev/null +++ b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/directory-cases/all-local/v2/smithy-build.json @@ -0,0 +1,3 @@ +{ + "version": "1.0" +} diff --git a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/directory-cases/ignores-projections/v1/model/main.smithy b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/directory-cases/ignores-projections/v1/model/main.smithy new file mode 100644 index 00000000000..4c0515d37cf --- /dev/null +++ b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/directory-cases/ignores-projections/v1/model/main.smithy @@ -0,0 +1,5 @@ +$version: "1.0" + +namespace com.example + +string A diff --git a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/directory-cases/ignores-projections/v1/smithy-build.json b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/directory-cases/ignores-projections/v1/smithy-build.json new file mode 100644 index 00000000000..dc368fbc3e7 --- /dev/null +++ b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/directory-cases/ignores-projections/v1/smithy-build.json @@ -0,0 +1,13 @@ +{ + "version": "1.0", + "projections": { + "strips-internal": { + "transforms": [{ + "name": "includeNamespaces", + "args": { + "namespaces": ["does.not.exist"] + } + }] + } + } +} diff --git a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/directory-cases/ignores-projections/v2/model/main.smithy b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/directory-cases/ignores-projections/v2/model/main.smithy new file mode 100644 index 00000000000..ee76322774c --- /dev/null +++ b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/directory-cases/ignores-projections/v2/model/main.smithy @@ -0,0 +1,5 @@ +$version: "2.0" + +namespace com.example + +string A diff --git a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/directory-cases/ignores-projections/v2/smithy-build.json b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/directory-cases/ignores-projections/v2/smithy-build.json new file mode 100644 index 00000000000..dc368fbc3e7 --- /dev/null +++ b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/directory-cases/ignores-projections/v2/smithy-build.json @@ -0,0 +1,13 @@ +{ + "version": "1.0", + "projections": { + "strips-internal": { + "transforms": [{ + "name": "includeNamespaces", + "args": { + "namespaces": ["does.not.exist"] + } + }] + } + } +} diff --git a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/directory-cases/with-jar/v1/jar-import.jar b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/directory-cases/with-jar/v1/jar-import.jar new file mode 100644 index 00000000000..675f4c88033 Binary files /dev/null and b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/directory-cases/with-jar/v1/jar-import.jar differ diff --git a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/directory-cases/with-jar/v1/model/main.smithy b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/directory-cases/with-jar/v1/model/main.smithy new file mode 100644 index 00000000000..3a6f8ef247a --- /dev/null +++ b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/directory-cases/with-jar/v1/model/main.smithy @@ -0,0 +1,14 @@ +$version: "1.0" + +namespace com.example + +use foo.baz#A +use foo.baz#B +use foo.baz#C + +structure Dependant { + a: A, + b: B, + c: C, +} + diff --git a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/directory-cases/with-jar/v1/smithy-build.json b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/directory-cases/with-jar/v1/smithy-build.json new file mode 100644 index 00000000000..04450271687 --- /dev/null +++ b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/directory-cases/with-jar/v1/smithy-build.json @@ -0,0 +1,3 @@ +{ + "version": "1.0" +} diff --git a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/directory-cases/with-jar/v2/model/main.smithy b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/directory-cases/with-jar/v2/model/main.smithy new file mode 100644 index 00000000000..f6631376611 --- /dev/null +++ b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/directory-cases/with-jar/v2/model/main.smithy @@ -0,0 +1,14 @@ +$version: "2.0" + +namespace com.example + +use foo.baz#A +use foo.baz#B +use foo.baz#C + +structure Dependant { + a: A, + b: B, + c: C, +} + diff --git a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/directory-cases/with-jar/v2/smithy-build.json b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/directory-cases/with-jar/v2/smithy-build.json new file mode 100644 index 00000000000..04450271687 --- /dev/null +++ b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/upgrade/directory-cases/with-jar/v2/smithy-build.json @@ -0,0 +1,3 @@ +{ + "version": "1.0" +}