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 14dc8562ab7..8b6673fe6b7 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 void doesNotShowWarnings() { @Test public void selectsVariables() { - List args = Arrays.asList("select", "--show-vars", "--selector", "list $list(*) > member > string"); + List 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 void showTraitsCannotHaveEmptyValues() { assertThat(result.getExitCode(), not(equalTo(0))); }); } + + @Test + public void showCannotBeEmpty() { + List args = Arrays.asList("select", "--show", "", "--selector", "string"); + IntegUtils.run("simple-config-sources", args, result -> { + assertThat(result.getExitCode(), equalTo(1)); + }); + } + + @Test + public void showCannotContainInvalidValues() { + List 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 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 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 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 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 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/main/java/software/amazon/smithy/cli/commands/SelectCommand.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/SelectCommand.java index ae44f7729f1..3f2548cb9f4 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.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 @@ public int execute(Arguments arguments, Env env) { } 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 showTraits = new ArrayList<>(); + private final Set 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> 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 testParameter(String name) { switch (name) { @@ -128,6 +187,13 @@ public Consumer testParameter(String name) { 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 @@ public Consumer testParameter(String name) { 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 @@ void dumpResults(Selector selector, Model model, Options options, CliPrinter std 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 @@ void dumpResults(Selector selector, Model model, Options options, CliPrinter std 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 sortShapeIds(Collection 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> vars) { - ObjectNode.Builder varBuilder = Node.objectNodeBuilder(); - for (Map.Entry> 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 sortShapeIds(Collection shapes) { + return shapes.stream().map(Shape::getId).map(ShapeId::toString).sorted(); } } 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 11fb44566d8..69bbf930d57 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 void printsSuccessfulMatchesToStdout() throws Exception { 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 void readsSelectorFromStdinToo() throws Exception { 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());