Repository: smithy-lang/smithy
Pull request #1954 (2023-08-25 20:38:40)

Re-add validator to check consistency of resource names 
### Description of changes
Re-add validator to check consistency of resource name used for IamResource

Validator was previously reverted in 859e82dc9eee405f6416eecefb16885f1509a6a6.

The validator now ignores arns marked as `absolute`.

See original PR: https://github.com/smithy-lang/smithy/pull/1819

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.



Patch:
diff --git a/build.gradle b/build.gradle
index dd79739ce..2e7c14a3f 100644
--- a/build.gradle
+++ b/build.gradle
@@ -53,10 +53,11 @@ subprojects {
     }
 
     dependencies {
-        testImplementation "org.junit.jupiter:junit-jupiter-api:5.4.0"
-        testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.4.0"
-        testImplementation "org.junit.jupiter:junit-jupiter-params:5.4.0"
+        testImplementation "org.junit.jupiter:junit-jupiter-api:5.10.0"
+        testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.10.0"
+        testImplementation "org.junit.jupiter:junit-jupiter-params:5.10.0"
         testImplementation "org.hamcrest:hamcrest:2.1"
+        testCompileOnly "org.apiguardian:apiguardian-api:1.1.2"
     }
 
     // Reusable license copySpec for building JARs
diff --git a/smithy-aws-iam-traits/src/main/java/software/amazon/smithy/aws/iam/traits/IamResourceTraitValidator.java b/smithy-aws-iam-traits/src/main/java/software/amazon/smithy/aws/iam/traits/IamResourceTraitValidator.java
new file mode 100644
index 000000000..2b35343c4
--- /dev/null
+++ b/smithy-aws-iam-traits/src/main/java/software/amazon/smithy/aws/iam/traits/IamResourceTraitValidator.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 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.aws.iam.traits;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import software.amazon.smithy.aws.traits.ArnTrait;
+import software.amazon.smithy.model.Model;
+import software.amazon.smithy.model.shapes.ResourceShape;
+import software.amazon.smithy.model.validation.AbstractValidator;
+import software.amazon.smithy.model.validation.ValidationEvent;
+import software.amazon.smithy.utils.SmithyInternalApi;
+import software.amazon.smithy.utils.StringUtils;
+
+/**
+ * Ensures that any resource name defined in the {@link IamResourceTrait} is
+ * consistent with the resource name used in any {@link ArnTrait} definition
+ * applied to the resource.
+ */
+@SmithyInternalApi
+public class IamResourceTraitValidator extends AbstractValidator {
+    @Override
+    public List<ValidationEvent> validate(Model model) {
+        List<ValidationEvent> results = new ArrayList<>();
+        for (ResourceShape resource : model.getResourceShapesWithTrait(IamResourceTrait.class)) {
+            // If the resource has both the IamResourceTrait and Arn trait,
+            // check that the resource name is consistent between the two traits
+            if (resource.hasTrait(ArnTrait.class)) {
+                String resourceName = resource.expectTrait(IamResourceTrait.class).getName()
+                    .orElseGet(() -> StringUtils.lowerCase(resource.getId().getName()));
+                ArnTrait arnTrait = resource.expectTrait(ArnTrait.class);
+                List<String> arnComponents = parseArnComponents(arnTrait.getTemplate());
+
+                // Do not check for a matching resource name when the arn is marked as absolute
+                if (!arnComponents.contains(resourceName) && !arnTrait.isAbsolute()) {
+                    results.add(danger(resource, String.format(
+                            "The `@aws.iam#iamResource` trait applied to the resource "
+                            + "defines an IAM resource name, `%s`, that does not match the `@arn` template, "
+                            + "`%s`, of the resource.",
+                            resourceName, arnTrait.getTemplate())));
+                }
+            }
+        }
+        return results;
+    }
+
+    private List<String> parseArnComponents(String arnTemplate) {
+        return Arrays.asList(arnTemplate.split("/"));
+    }
+}
diff --git a/smithy-aws-iam-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator b/smithy-aws-iam-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator
index c77a40ad5..d03291f51 100644
--- a/smithy-aws-iam-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator
+++ b/smithy-aws-iam-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator
@@ -1,1 +1,2 @@
 software.amazon.smithy.aws.iam.traits.ConditionKeysValidator
+software.amazon.smithy.aws.iam.traits.IamResourceTraitValidator
diff --git a/smithy-cli/build.gradle b/smithy-cli/build.gradle
index df4bd77d0..dcfb805ad 100644
--- a/smithy-cli/build.gradle
+++ b/smithy-cli/build.gradle
@@ -57,6 +57,7 @@ dependencies {
     implementation "org.slf4j:slf4j-jdk14:1.7.36" // Route slf4j used by Maven through JUL like the rest of Smithy.
 
     testImplementation "org.mock-server:mockserver-netty:3.10.8"
+
 }
 
 // ------ Shade Maven dependency resolvers into the JAR. -------
@@ -223,7 +224,7 @@ tasks["runtime"].dependsOn("shadowJar")
 
 sourceSets {
     create("it") {
-        compileClasspath += sourceSets["main"].output + configurations["testRuntimeClasspath"]
+        compileClasspath += sourceSets["main"].output + configurations["testRuntimeClasspath"] + configurations["testCompileClasspath"]
         runtimeClasspath += output + compileClasspath + sourceSets["test"].runtimeClasspath
     }
 }
@@ -233,7 +234,11 @@ task integ(type: Test) {
     systemProperty "SMITHY_BINARY", "${smithyBinary}"
     testClassesDirs = sourceSets["it"].output.classesDirs
     classpath = sourceSets["it"].runtimeClasspath
-    maxParallelForks = Runtime.getRuntime().availableProcessors() / 2
+
+    // Configuration parameters to execute top-level classes in parallel but methods in same thread
+    systemProperties["junit.jupiter.execution.parallel.enabled"] = "true"
+    systemProperties["junit.jupiter.execution.parallel.mode.default"] = "same_thread"
+    systemProperties["junit.jupiter.execution.parallel.mode.classes.default"] = "concurrent"
 
     testLogging {
         events = ["passed", "skipped", "failed"]
diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/SelectCommand.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/SelectCommand.java
index ae44f7729..3f2548cb9 100644
--- a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/SelectCommand.java
+++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/SelectCommand.java
@@ -21,6 +21,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.TreeMap;
+import java.util.TreeSet;
 import java.util.function.Consumer;
 import java.util.logging.Level;
 import java.util.logging.Logger;
@@ -86,30 +87,88 @@ final class SelectCommand implements Command {
     }
 
     private String getDocumentation(ColorFormatter colors) {
-        return "By default, each matching shape ID is printed to stdout on a new line. Pass --show-vars to print out "
-               + "a JSON array that contains a 'shape' and 'vars' property, where the 'vars' property is a map of "
-               + "each variable that was captured when the shape was matched.";
+        return "By default, each matching shape ID is printed to stdout on a new line. Pass --show or --show-traits "
+               + "to get JSON array output.";
     }
 
     private static final class Options implements ArgumentReceiver {
-        private boolean showVars;
         private Selector selector;
         private final List<ShapeId> showTraits = new ArrayList<>();
+        private final Set<Show> show = new TreeSet<>();
+
+        private enum Show {
+            TYPE("type") {
+                @Override
+                protected void inject(Selector.ShapeMatch match, ObjectNode.Builder builder) {
+                    builder.withMember("type", match.getShape().getType().toString());
+                }
+            },
+
+            FILE("file") {
+                @Override
+                protected void inject(Selector.ShapeMatch match, ObjectNode.Builder builder) {
+                    SourceLocation source = match.getShape().getSourceLocation();
+                    // Only shapes with a real source location add a file.
+                    if (!source.getFilename().equals(SourceLocation.NONE.getFilename())) {
+                        builder.withMember("file", source.getFilename()
+                                                   + ':' + source.getLine()
+                                                   + ':' + source.getColumn());
+                    }
+                }
+            },
+
+            VARS("vars") {
+                @Override
+                protected void inject(Selector.ShapeMatch match, ObjectNode.Builder builder) {
+                    if (!match.isEmpty()) {
+                        ObjectNode.Builder varBuilder = Node.objectNodeBuilder();
+                        for (Map.Entry<String, Set<Shape>> varEntry : match.entrySet()) {
+                            varBuilder.withMember(
+                                varEntry.getKey(),
+                                sortShapeIds(varEntry.getValue()).map(Node::from).collect(ArrayNode.collect())
+                            );
+                        }
+                        ObjectNode collectedVars = varBuilder.build();
+                        builder.withMember("vars", collectedVars);
+                    }
+                }
+            };
+
+            private final String value;
+
+            Show(String value) {
+                this.value = value;
+            }
+
+            protected abstract void inject(Selector.ShapeMatch match, ObjectNode.Builder builder);
+
+            private static Show from(String value) {
+                for (Show variant : values()) {
+                    if (variant.value.equals(value)) {
+                        return variant;
+                    }
+                }
+                throw new CliError("Invalid value given to --show: `" + value + "`");
+            }
+        }
 
         @Override
         public boolean testOption(String name) {
             switch (name) {
                 case "--vars":
-                    LOGGER.warning("--vars is deprecated. Use --show-vars instead.");
-                    // fall-through
                 case "--show-vars":
-                    showVars = true;
-                    return true;
+                    return deprecatedVars(name);
                 default:
                     return false;
             }
         }
 
+        private boolean deprecatedVars(String name) {
+            LOGGER.warning(name + " is deprecated. Use `--show vars` instead.");
+            show.add(Show.VARS);
+            return true;
+        }
+
         @Override
         public Consumer<String> testParameter(String name) {
             switch (name) {
@@ -128,6 +187,13 @@ final class SelectCommand implements Command {
                             throw new CliError("--show-traits must contain traits");
                         }
                     };
+                case "--show":
+                    return value -> {
+                        String[] parts = value.split("\\s*,\\s*");
+                        for (String part : parts) {
+                            show.add(Show.from(part));
+                        }
+                    };
                 default:
                     return null;
             }
@@ -137,16 +203,17 @@ final class SelectCommand implements Command {
         public void registerHelp(HelpPrinter printer) {
             printer.param("--selector", null, "SELECTOR",
                           "The Smithy selector to execute. Reads from STDIN when not provided.");
+            printer.param("--show", null, "DATA",
+                          "Displays additional top-level members in each match and forces JSON output. This parameter "
+                          + "accepts a comma-separated list of values, including 'type', 'file', and 'vars'. 'type' "
+                          + "adds a string member containing the shape type of each match. 'file' adds a string "
+                          + "member containing the absolute path to where the shape is defined followed by the line "
+                          + "number then column (e.g., '/path/example.smithy:10:1'). 'vars' adds an object containing "
+                          + "the variables that were captured when a shape was matched.");
             printer.param("--show-traits", null, "TRAITS",
                           "Returns JSON output that includes the values of specific traits applied to matched shapes, "
                           + "stored in a 'traits' property. Provide a comma-separated list of trait shape IDs. "
                           + "Prelude traits may omit a namespace (e.g., 'required' or 'smithy.api#required').");
-            printer.option("--show-vars", null, "Returns JSON output that includes the variables that were captured "
-                                                + "when a shape was matched, stored in a 'vars' property.");
-        }
-
-        public boolean showVars() {
-            return showVars;
         }
 
         public Selector selector() {
@@ -199,8 +266,8 @@ final class SelectCommand implements Command {
                     ObjectNode.Builder builder = Node.objectNodeBuilder()
                             .withMember("shape", Node.from(match.getShape().getId().toString()));
 
-                    if (!match.isEmpty()) {
-                        builder.withMember("vars", collectVars(match));
+                    for (Options.Show showData : options.show) {
+                        showData.inject(match, builder);
                     }
 
                     if (!options.showTraits.isEmpty()) {
@@ -223,21 +290,11 @@ final class SelectCommand implements Command {
         abstract void dumpResults(Selector selector, Model model, Options options, CliPrinter stdout);
 
         static OutputFormat determineFormat(Options options) {
-            // If --show-vars isn't provided and --show-traits is empty, then use the SHAPE_ID_LINES output.
-            return !options.showVars() && options.showTraits.isEmpty() ? SHAPE_ID_LINES : JSON;
-        }
-
-        private static Stream<String> sortShapeIds(Collection<Shape> shapes) {
-            return shapes.stream().map(Shape::getId).map(ShapeId::toString).sorted();
+            return options.showTraits.isEmpty() && options.show.isEmpty() ? SHAPE_ID_LINES : JSON;
         }
+    }
 
-        private static ObjectNode collectVars(Map<String, Set<Shape>> vars) {
-            ObjectNode.Builder varBuilder = Node.objectNodeBuilder();
-            for (Map.Entry<String, Set<Shape>> varEntry : vars.entrySet()) {
-                ArrayNode value = sortShapeIds(varEntry.getValue()).map(Node::from).collect(ArrayNode.collect());
-                varBuilder.withMember(varEntry.getKey(), value);
-            }
-            return varBuilder.build();
-        }
+    private static Stream<String> sortShapeIds(Collection<Shape> shapes) {
+        return shapes.stream().map(Shape::getId).map(ShapeId::toString).sorted();
     }
 }
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/node/NumberNode.java b/smithy-model/src/main/java/software/amazon/smithy/model/node/NumberNode.java
index 10346dbd6..a52349cb5 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/node/NumberNode.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/node/NumberNode.java
@@ -33,11 +33,16 @@ public final class NumberNode extends Node {
 
     private final Number value;
     private final String stringCache;
+    private volatile BigDecimal equality;
 
     public NumberNode(Number value, SourceLocation sourceLocation) {
         super(sourceLocation);
         this.value = Objects.requireNonNull(value);
         stringCache = value.toString();
+
+        if (value instanceof BigDecimal) {
+            equality = (BigDecimal) value;
+        }
     }
 
     /**
@@ -129,7 +134,47 @@ public final class NumberNode extends Node {
 
     @Override
     public boolean equals(Object other) {
-        return other instanceof NumberNode && stringCache.equals(((NumberNode) other).stringCache);
+        if (!(other instanceof NumberNode)) {
+            return false;
+        } else if (other == this) {
+            return true;
+        } else {
+            NumberNode otherNode = (NumberNode) other;
+
+            // This only works if both values are the same type.
+            if (value.equals(otherNode.value)) {
+                return true;
+            }
+
+            // Attempt a cheap check based on the string cache.
+            if (stringCache.equals(otherNode.stringCache)) {
+                return true;
+            }
+
+            // Convert both to BigDecimal and compare equality.
+            return getEquality().equals(otherNode.getEquality());
+        }
+    }
+
+    private BigDecimal getEquality() {
+        BigDecimal e = equality;
+
+        if (e == null) {
+            if (value instanceof Integer) {
+                e = BigDecimal.valueOf(value.intValue());
+            } else if (value instanceof Short) {
+                e = BigDecimal.valueOf(value.shortValue());
+            } else if (value instanceof Byte) {
+                e = BigDecimal.valueOf(value.byteValue());
+            } else if (value instanceof Long) {
+                e = BigDecimal.valueOf(value.longValue());
+            } else {
+                e = new BigDecimal(stringCache);
+            }
+            equality = e;
+        }
+
+        return e;
     }
 
     @Override


Test patch:
diff --git a/smithy-aws-iam-traits/src/test/java/software/amazon/smithy/aws/iam/traits/ConditionKeysIndexTest.java b/smithy-aws-iam-traits/src/test/java/software/amazon/smithy/aws/iam/traits/ConditionKeysIndexTest.java
index 4e9281059..a2047f153 100644
--- a/smithy-aws-iam-traits/src/test/java/software/amazon/smithy/aws/iam/traits/ConditionKeysIndexTest.java
+++ b/smithy-aws-iam-traits/src/test/java/software/amazon/smithy/aws/iam/traits/ConditionKeysIndexTest.java
@@ -65,18 +65,4 @@ public class ConditionKeysIndexTest {
         assertThat(index.getDefinedConditionKeys(service, ShapeId.from("smithy.example#GetResource2")).keySet(),
                 is(empty()));
     }
-
-    @Test
-    public void detectsUnknownConditionKeys() {
-        ValidatedResult<Model> result = Model.assembler()
-                .addImport(getClass().getResource("invalid-condition-keys.smithy"))
-                .discoverModels(getClass().getClassLoader())
-                .assemble();
-
-        assertTrue(result.isBroken());
-        assertThat(result.getValidationEvents(Severity.ERROR).stream()
-                        .map(ValidationEvent::getId)
-                        .collect(Collectors.toSet()),
-                contains("ConditionKeys"));
-    }
 }
diff --git a/smithy-aws-iam-traits/src/test/java/software/amazon/smithy/aws/iam/traits/TestRunnerTest.java b/smithy-aws-iam-traits/src/test/java/software/amazon/smithy/aws/iam/traits/TestRunnerTest.java
new file mode 100644
index 000000000..aa2c66b47
--- /dev/null
+++ b/smithy-aws-iam-traits/src/test/java/software/amazon/smithy/aws/iam/traits/TestRunnerTest.java
@@ -0,0 +1,21 @@
+package software.amazon.smithy.aws.iam.traits;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import software.amazon.smithy.model.validation.testrunner.SmithyTestCase;
+import software.amazon.smithy.model.validation.testrunner.SmithyTestSuite;
+
+import java.util.concurrent.Callable;
+import java.util.stream.Stream;
+
+public class TestRunnerTest {
+    @ParameterizedTest(name = "{0}")
+    @MethodSource("source")
+    public void testRunner(String filename, Callable<SmithyTestCase.Result> callable) throws Exception {
+        callable.call();
+    }
+
+    public static Stream<?> source() {
+        return SmithyTestSuite.defaultParameterizedTestSource(TestRunnerTest.class);
+    }
+}
diff --git a/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/errorfiles/condition-keys/invalid-condition-keys.errors b/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/errorfiles/condition-keys/invalid-condition-keys.errors
new file mode 100644
index 000000000..a347362d0
--- /dev/null
+++ b/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/errorfiles/condition-keys/invalid-condition-keys.errors
@@ -0,0 +1,1 @@
+[ERROR] smithy.example#Operation: This operation scoped within the `smithy.example#MyService` service refers to an undefined condition key `foo:qux`. Expected one of the following defined condition keys: [`foo:baz`] | ConditionKeys
diff --git a/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/invalid-condition-keys.smithy b/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/errorfiles/condition-keys/invalid-condition-keys.smithy
similarity index 100%
rename from smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/invalid-condition-keys.smithy
rename to smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/errorfiles/condition-keys/invalid-condition-keys.smithy
diff --git a/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/errorfiles/iam-resources/invalid-iam-resources.errors b/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/errorfiles/iam-resources/invalid-iam-resources.errors
new file mode 100644
index 000000000..4278821c8
--- /dev/null
+++ b/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/errorfiles/iam-resources/invalid-iam-resources.errors
@@ -0,0 +1,3 @@
+[DANGER] smithy.example#BadIamResourceName: The `@aws.iam#iamResource` trait applied to the resource defines an IAM resource name, `bad-iam-resourceName`, that does not match the `@arn` template, `bad-iam-resource-name/{id}`, of the resource. | IamResourceTrait
+[DANGER] smithy.example#IncompatibleResourceName: The `@aws.iam#iamResource` trait applied to the resource defines an IAM resource name, `IncompatibleResourceName`, that does not match the `@arn` template, `beer/{beerId}/incompatible-resource-name`, of the resource. | IamResourceTrait
+[DANGER] smithy.example#InvalidResource: The `@aws.iam#iamResource` trait applied to the resource defines an IAM resource name, `invalidResource`, that does not match the `@arn` template, `invalid-resource`, of the resource. | IamResourceTrait
diff --git a/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/errorfiles/iam-resources/invalid-iam-resources.smithy b/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/errorfiles/iam-resources/invalid-iam-resources.smithy
new file mode 100644
index 000000000..26cbc3b96
--- /dev/null
+++ b/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/errorfiles/iam-resources/invalid-iam-resources.smithy
@@ -0,0 +1,53 @@
+$version: "2"
+namespace smithy.example
+
+use aws.api#arn
+
+@aws.api#service(sdkId: "My")
+@aws.iam#defineConditionKeys("foo:baz": {type: "String", documentation: "Foo baz"})
+service MyService {
+    version: "2019-02-20",
+    resources: [
+        BadIamResourceName,
+        Beer,
+        InvalidResource,
+        ShouldNotThrowAnError
+    ]
+}
+
+@aws.iam#iamResource(name: "bad-iam-resourceName")
+@arn(template: "bad-iam-resource-name/{id}")
+resource BadIamResourceName {
+    identifiers: {
+        id: String
+    }
+}
+
+@aws.iam#iamResource(name: "beer")
+@arn(template: "beer/{beerId}")
+resource Beer {
+    identifiers: {
+        beerId: String
+    }
+    resources: [IncompatibleResourceName]
+}
+
+@arn(template: "beer/{beerId}/incompatible-resource-name")
+@aws.iam#iamResource(name: "IncompatibleResourceName")
+resource IncompatibleResourceName {
+    identifiers: {
+        beerId: String
+    }
+}
+
+@aws.iam#iamResource(name: "invalidResource")
+@arn(template: "invalid-resource")
+resource InvalidResource {}
+
+@aws.iam#iamResource(name: "shouldNotThrowError")
+@arn(template: "{arn}", absolute: true)
+resource ShouldNotThrowAnError {
+    identifiers: {
+        arn: String
+    }
+}
diff --git a/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/iam-resource.smithy b/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/iam-resource.smithy
index b7fb78baa..76bfb1e2d 100644
--- a/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/iam-resource.smithy
+++ b/smithy-aws-iam-traits/src/test/resources/software/amazon/smithy/aws/iam/traits/iam-resource.smithy
@@ -2,6 +2,8 @@ $version: "1.0"
 
 namespace smithy.example
 
+use aws.api#arn
+
 @aws.api#service(sdkId: "My")
 service MyService {
     version: "2020-07-02",
@@ -9,6 +11,7 @@ service MyService {
 }
 
 @aws.iam#iamResource(name: "super")
+@arn(template: "super/{id1}")
 resource SuperResource {
     identifiers: {
         id1: String,
diff --git a/smithy-cli/src/it/java/software/amazon/smithy/cli/CleanCommandTest.java b/smithy-cli/src/it/java/software/amazon/smithy/cli/CleanCommandTest.java
index 29fb1da6e..a2ef93611 100644
--- a/smithy-cli/src/it/java/software/amazon/smithy/cli/CleanCommandTest.java
+++ b/smithy-cli/src/it/java/software/amazon/smithy/cli/CleanCommandTest.java
@@ -11,13 +11,13 @@ import java.io.IOException;
 import java.io.UncheckedIOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.nio.file.Paths;
 
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.parallel.Isolated;
 import software.amazon.smithy.utils.IoUtils;
 import software.amazon.smithy.utils.ListUtils;
 
-
+@Isolated
 public class CleanCommandTest {
     private static final String PROJECT_NAME = "simple-config-sources";
     @Test
diff --git a/smithy-cli/src/it/java/software/amazon/smithy/cli/InitCommandTest.java b/smithy-cli/src/it/java/software/amazon/smithy/cli/InitCommandTest.java
index 102a97993..ae8aef636 100644
--- a/smithy-cli/src/it/java/software/amazon/smithy/cli/InitCommandTest.java
+++ b/smithy-cli/src/it/java/software/amazon/smithy/cli/InitCommandTest.java
@@ -10,6 +10,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.parallel.Isolated;
 import software.amazon.smithy.utils.IoUtils;
 import software.amazon.smithy.utils.ListUtils;
 
@@ -20,6 +21,7 @@ import java.nio.file.Paths;
 import java.util.Collections;
 import java.util.List;
 
+@Isolated
 public class InitCommandTest {
     private static final String PROJECT_NAME = "smithy-templates";
 
diff --git a/smithy-cli/src/it/java/software/amazon/smithy/cli/SelectTest.java b/smithy-cli/src/it/java/software/amazon/smithy/cli/SelectTest.java
index 14dc8562a..8b6673fe6 100644
--- a/smithy-cli/src/it/java/software/amazon/smithy/cli/SelectTest.java
+++ b/smithy-cli/src/it/java/software/amazon/smithy/cli/SelectTest.java
@@ -31,7 +31,7 @@ public class SelectTest {
 
     @Test
     public void selectsVariables() {
-        List<String> args = Arrays.asList("select", "--show-vars", "--selector", "list $list(*) > member > string");
+        List<String> args = Arrays.asList("select", "--show", "vars", "--selector", "list $list(*) > member > string");
         IntegUtils.run("simple-config-sources", args, result -> {
             assertThat(result.getExitCode(), equalTo(0));
             String content = result.getOutput().trim();
@@ -79,4 +79,80 @@ public class SelectTest {
             assertThat(result.getExitCode(), not(equalTo(0)));
         });
     }
+
+    @Test
+    public void showCannotBeEmpty() {
+        List<String> args = Arrays.asList("select", "--show", "", "--selector", "string");
+        IntegUtils.run("simple-config-sources", args, result -> {
+            assertThat(result.getExitCode(), equalTo(1));
+        });
+    }
+
+    @Test
+    public void showCannotContainInvalidValues() {
+        List<String> args = Arrays.asList("select", "--show", "foo", "--selector", "string");
+        IntegUtils.run("simple-config-sources", args, result -> {
+            assertThat(result.getExitCode(), equalTo(1));
+        });
+    }
+
+    @Test
+    public void showCannotContainInvalidValuesInCsv() {
+        List<String> args = Arrays.asList("select", "--show", "vars,foo", "--selector", "string");
+        IntegUtils.run("simple-config-sources", args, result -> {
+            assertThat(result.getExitCode(), equalTo(1));
+        });
+    }
+
+    @Test
+    public void includesType() {
+        List<String> args = Arrays.asList("select", "--show", "type", "--selector", "string");
+        IntegUtils.run("simple-config-sources", args, result -> {
+            assertThat(result.getExitCode(), equalTo(0));
+            String content = result.getOutput().trim();
+            // Ensure it's valid JSON
+            Node.parse(content);
+            assertThat(content, containsString("\"type\": \"string\""));
+        });
+    }
+
+    @Test
+    public void includesFile() {
+        List<String> args = Arrays.asList("select", "--show", "file", "--selector", "string");
+        IntegUtils.run("simple-config-sources", args, result -> {
+            assertThat(result.getExitCode(), equalTo(0));
+            String content = result.getOutput().trim();
+            // Ensure it's valid JSON
+            Node.parse(content);
+            assertThat(content, containsString("\"file\": "));
+        });
+    }
+
+    @Test
+    public void includesFileAndType() {
+        List<String> args = Arrays.asList("select", "--show", "file, type", "--selector", "string");
+        IntegUtils.run("simple-config-sources", args, result -> {
+            assertThat(result.getExitCode(), equalTo(0));
+            String content = result.getOutput().trim();
+            // Ensure it's valid JSON
+            Node.parse(content);
+            assertThat(content, containsString("\"type\": \"string\""));
+            assertThat(content, containsString("\"file\": "));
+        });
+    }
+
+    @Test
+    public void includesFileAndTypeAndVars() {
+        List<String> args = Arrays.asList("select", "--show", "file, type,vars", "--selector", "string $hi(*)");
+        IntegUtils.run("simple-config-sources", args, result -> {
+            assertThat(result.getExitCode(), equalTo(0));
+            String content = result.getOutput().trim();
+            // Ensure it's valid JSON
+            Node.parse(content);
+            assertThat(content, containsString("\"type\": \"string\""));
+            assertThat(content, containsString("\"file\": "));
+            assertThat(content, containsString("\"vars\": "));
+            assertThat(content, containsString("\"hi\": "));
+        });
+    }
 }
diff --git a/smithy-cli/src/test/java/software/amazon/smithy/cli/commands/SelectCommandTest.java b/smithy-cli/src/test/java/software/amazon/smithy/cli/commands/SelectCommandTest.java
index 11fb44566..69bbf930d 100644
--- a/smithy-cli/src/test/java/software/amazon/smithy/cli/commands/SelectCommandTest.java
+++ b/smithy-cli/src/test/java/software/amazon/smithy/cli/commands/SelectCommandTest.java
@@ -57,7 +57,7 @@ public class SelectCommandTest {
     public void printsJsonVarsToStdout() throws Exception {
         String model = Paths.get(getClass().getResource("valid-model.smithy").toURI()).toString();
         CliUtils.Result result = CliUtils.runSmithy("select", "--selector", "string $referenceMe(<)",
-                                                    "--show-vars", model);
+                                                    "--show", "vars", model);
 
         assertThat(result.code(), equalTo(0));
         validateSelectorOutput(result.stdout());
@@ -81,7 +81,7 @@ public class SelectCommandTest {
         try {
             // Send the selector through input stream.
             System.setIn(new ByteArrayInputStream("string $referenceMe(<)".getBytes()));
-            CliUtils.Result result = CliUtils.runSmithy("select", "--show-vars", model);
+            CliUtils.Result result = CliUtils.runSmithy("select", "--show", "vars", model);
 
             assertThat(result.code(), equalTo(0));
             validateSelectorOutput(result.stdout());
diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/node/NumberNodeTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/node/NumberNodeTest.java
index 25d9e2ea1..910860574 100644
--- a/smithy-model/src/test/java/software/amazon/smithy/model/node/NumberNodeTest.java
+++ b/smithy-model/src/test/java/software/amazon/smithy/model/node/NumberNodeTest.java
@@ -15,7 +15,9 @@
 
 package software.amazon.smithy.model.node;
 
+import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -241,4 +243,44 @@ public class NumberNodeTest {
             assertEquals(v, result, "Expected " + k + " to be " + v);
         });
     }
+
+    @Test
+    public void compareIntAndShort() {
+        Node left = Node.from(1);
+        Node right = Node.from((short) 1);
+
+        assertThat(left, equalTo(right));
+    }
+
+    @Test
+    public void compareShortAndFloat() {
+        Node left = Node.from((float) 1.0);
+        Node right = Node.from((short) 1);
+
+        assertThat(left, not(equalTo(right)));
+    }
+
+    @Test
+    public void compareByteAndFloat() {
+        Node left = Node.from((float) 1.0);
+        Node right = Node.from((byte) 1);
+
+        assertThat(left, not(equalTo(right)));
+    }
+
+    @Test
+    public void compareDoubleAndBigDecimal() {
+        Node left = Node.from(new BigDecimal("1.0e+10"));
+        Node right = Node.from(1.0e+10);
+
+        assertThat(left, equalTo(right));
+    }
+
+    @Test
+    public void compareDoubleAndFloat() {
+        Node left = Node.from((float) 1.0e+10);
+        Node right = Node.from((double) 1.0e+10);
+
+        assertThat(left, equalTo(right));
+    }
 }