From c3bff4acc36adb7e79a43e025db367e3222819c6 Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Wed, 20 Nov 2019 16:14:32 -0800 Subject: [PATCH] Update smithy-build to be streaming smithy-build used to load every model from every projection into memory at once. This was not ideal since some use cases utilize hundreds of projections. For example, in AWS SDKs, we create a projection for every AWS service, which is hundreds of them with tens-of-thousands of shapes and operations. This commit updates smithy-build to support two modes: in-memory and streaming. The in-memory mode is useful for testing or one-off integrations. The streaming mode is useful for actually executing smithy-build from the CLI so that results are outputted to stdout as they happen and not all models are loaded into memory. The CLI was updated to significantly change its output to show more information about things that go wrong and print more statistics. Every projection and plugin is attempted to be executed -- the CLI previously failed on the first exception. Finally, executing tasks is now done with an ExecutorService rather than parallel streams to have more control over task execution. --- .../amazon/smithy/build/SmithyBuild.java | 63 +++++++- .../amazon/smithy/build/SmithyBuildImpl.java | 134 +++++++++++++----- .../smithy/build/SmithyBuildResult.java | 6 +- .../amazon/smithy/build/SmithyBuildTest.java | 42 ++++++ .../smithy/build/trigger-plugin-error.json | 10 ++ .../smithy/cli/commands/BuildCommand.java | 90 ++++++++++-- .../smithy/cli/commands/BuildCommandTest.java | 64 +++++++++ .../commands/projection-build-failure.json | 10 ++ .../smithy/cli/commands/valid-model.smithy | 18 +++ 9 files changed, 387 insertions(+), 50 deletions(-) create mode 100644 smithy-build/src/test/resources/software/amazon/smithy/build/trigger-plugin-error.json create mode 100644 smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/projection-build-failure.json create mode 100644 smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/valid-model.smithy diff --git a/smithy-build/src/main/java/software/amazon/smithy/build/SmithyBuild.java b/smithy-build/src/main/java/software/amazon/smithy/build/SmithyBuild.java index 900f0199943..c82e258ea16 100644 --- a/smithy-build/src/main/java/software/amazon/smithy/build/SmithyBuild.java +++ b/smithy-build/src/main/java/software/amazon/smithy/build/SmithyBuild.java @@ -19,9 +19,13 @@ import java.nio.file.Paths; import java.util.Collections; import java.util.HashSet; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.TreeMap; +import java.util.function.BiConsumer; +import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; @@ -89,16 +93,69 @@ public static SmithyBuild create(ClassLoader classLoader, SupplierThis method loads all projections, projected models, and their + * results into memory so that a {@link SmithyBuildResult} can be + * returned. See {@link #build(Consumer, BiConsumer)} for a streaming + * approach that uses callbacks and does not load all projections into + * memory at once. + * + *

Errors are aggregated together into a single + * {@link SmithyBuildException} that contains an aggregated error + * message and each encountered exception is registered to the aggregate + * exception through {@link Throwable#addSuppressed(Throwable)}. + * + * @return Returns the result of building the model. + * @throws IllegalStateException if a {@link SmithyBuildConfig} is not set. + * @throws SmithyBuildException if the build fails. + * @see #build(Consumer, BiConsumer) + */ + public SmithyBuildResult build() { + SmithyBuildResult.Builder resultBuilder = SmithyBuildResult.builder(); + Map errors = Collections.synchronizedMap(new TreeMap<>()); + build(resultBuilder::addProjectionResult, errors::put); + + if (!errors.isEmpty()) { + StringBuilder message = new StringBuilder(); + message.append(errors.size()).append(" Smithy build projections failed."); + message.append(System.lineSeparator()).append(System.lineSeparator()); + + for (Map.Entry e : errors.entrySet()) { + message.append("(").append(e.getKey()).append("): ") + .append(e.getValue()) + .append(System.lineSeparator()); + } + + SmithyBuildException buildException = new SmithyBuildException(message.toString()); + errors.values().forEach(buildException::addSuppressed); + throw buildException; + } + + return resultBuilder.build(); + } + + /** + * Builds the model and applies all projections, passing each + * {@link ProjectionResult} to the provided callback as they are + * completed and each encountered exception to the provided + * {@code exceptionCallback} as they are encountered. + * + *

This method differs from {@link #build()} in that it does not + * require every projection and projection result to be loaded into + * memory. + * *

The result each projection is placed in the outputDirectory. * A {@code [projection]-build-info.json} file is created in the output * directory. A directory is created for each projection using the * projection name, and a file named model.json is place in each directory. * - * @return Returns the result of building the model. + * @param resultCallback A thread-safe callback that receives projection + * results as they complete. + * @param exceptionCallback A thread-safe callback that receives the name + * of each failed projection and the exception that occurred. * @throws IllegalStateException if a {@link SmithyBuildConfig} is not set. */ - public SmithyBuildResult build() { - return new SmithyBuildImpl(this).applyAllProjections(); + public void build(Consumer resultCallback, BiConsumer exceptionCallback) { + new SmithyBuildImpl(this).applyAllProjections(resultCallback, exceptionCallback); } /** diff --git a/smithy-build/src/main/java/software/amazon/smithy/build/SmithyBuildImpl.java b/smithy-build/src/main/java/software/amazon/smithy/build/SmithyBuildImpl.java index 68d8d448ab0..31c1754e63c 100644 --- a/smithy-build/src/main/java/software/amazon/smithy/build/SmithyBuildImpl.java +++ b/smithy-build/src/main/java/software/amazon/smithy/build/SmithyBuildImpl.java @@ -17,20 +17,27 @@ import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Comparator; +import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.TreeMap; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.function.BiConsumer; import java.util.function.BiFunction; +import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; import java.util.logging.Logger; import java.util.regex.Pattern; -import java.util.stream.Collectors; import java.util.stream.Stream; import software.amazon.smithy.build.model.ProjectionConfig; import software.amazon.smithy.build.model.SmithyBuildConfig; @@ -150,46 +157,99 @@ private static void validatePluginName(String projection, String plugin) { } } - SmithyBuildResult applyAllProjections() { + void applyAllProjections( + Consumer projectionResultConsumer, + BiConsumer projectionExceptionConsumer + ) { Model resolvedModel = createBaseModel(); - SmithyBuildResult.Builder builder = SmithyBuildResult.builder(); - - // The projections are being split up here because we need to be able to break out non-parallelizeable plugins. - // Right now the only parallelization that occurs is at the projection level though, which is why the split is - // here instead of somewhere else. - // TODO: Run all parallelizeable plugins across all projections in parallel, followed by all serial plugins - Map serialProjections = new TreeMap<>(); - Map parallelProjections = new TreeMap<>(); - config.getProjections().entrySet().stream() - .filter(e -> !e.getValue().isAbstract()) - .filter(e -> projectionFilter.test(e.getKey())) - .sorted(Comparator.comparing(Map.Entry::getKey)) - .forEach(e -> { - // Check to see if any of the plugins in the projection require the projection be run serially - boolean isSerial = resolvePlugins(e.getValue()).keySet().stream().anyMatch(pluginName -> { - Optional plugin = pluginFactory.apply(pluginName); - return plugin.isPresent() && plugin.get().isSerial(); - }); - // Only run a projection in parallel if all its plugins are parallelizeable. - if (isSerial) { - serialProjections.put(e.getKey(), e.getValue()); - } else { - parallelProjections.put(e.getKey(), e.getValue()); - } + + // The projections are being split up here because we need to be able + // to break out non-parallelizeable plugins. Right now the only + // parallelization that occurs is at the projection level. + List> parallelProjections = new ArrayList<>(); + List parallelProjectionNameOrder = new ArrayList<>(); + + for (Map.Entry entry : config.getProjections().entrySet()) { + String name = entry.getKey(); + ProjectionConfig config = entry.getValue(); + + if (config.isAbstract() || !projectionFilter.test(name)) { + continue; + } + + // Check to see if any of the plugins in the projection require the projection be run serially + boolean isSerial = resolvePlugins(config).keySet().stream().anyMatch(pluginName -> { + Optional plugin = pluginFactory.apply(pluginName); + return plugin.isPresent() && plugin.get().isSerial(); + }); + + if (isSerial) { + executeSerialProjection(resolvedModel, name, config, + projectionResultConsumer, projectionExceptionConsumer); + } else { + parallelProjectionNameOrder.add(name); + parallelProjections.add(() -> { + ProjectionResult projectionResult = applyProjection(name, config, resolvedModel); + projectionResultConsumer.accept(projectionResult); + return null; }); + } + } - serialProjections.entrySet().stream() - .map(e -> applyProjection(e.getKey(), e.getValue(), resolvedModel)) - .collect(Collectors.toList()) - .forEach(builder::addProjectionResult); + if (!parallelProjections.isEmpty()) { + executeParallelProjections(parallelProjections, parallelProjectionNameOrder, projectionExceptionConsumer); + } + } - parallelProjections.entrySet().stream() - .parallel() - .map(e -> applyProjection(e.getKey(), e.getValue(), resolvedModel)) - .collect(Collectors.toList()) - .forEach(builder::addProjectionResult); + private void executeSerialProjection( + Model resolvedModel, + String name, + ProjectionConfig config, + Consumer projectionResultConsumer, + BiConsumer projectionExceptionConsumer + ) { + // Errors that occur while invoking the result callback must not + // cause the exception callback to be invoked. + ProjectionResult result = null; + + try { + result = applyProjection(name, config, resolvedModel); + } catch (Throwable e) { + projectionExceptionConsumer.accept(name, e); + } - return builder.build(); + if (result != null) { + projectionResultConsumer.accept(result); + } + } + + private void executeParallelProjections( + List> parallelProjections, + List parallelProjectionNameOrder, + BiConsumer projectionExceptionConsumer + ) { + // Except for writing files to disk, projections are mostly CPU bound. + int numberOfCores = Runtime.getRuntime().availableProcessors(); + ExecutorService executor = Executors.newFixedThreadPool(numberOfCores); + + try { + List> futures = executor.invokeAll(parallelProjections); + executor.shutdown(); + // Futures are returned in the same order they were added, so + // use the list of ordered names to know which projections failed. + for (int i = 0; i < futures.size(); i++) { + try { + futures.get(i).get(); + } catch (ExecutionException e) { + Throwable cause = e.getCause() != null ? e.getCause() : e; + String failedProjectionName = parallelProjectionNameOrder.get(i); + projectionExceptionConsumer.accept(failedProjectionName, cause); + } + } + } catch (InterruptedException e) { + executor.shutdownNow(); + throw new SmithyBuildException(e.getMessage(), e); + } } private Model createBaseModel() { diff --git a/smithy-build/src/main/java/software/amazon/smithy/build/SmithyBuildResult.java b/smithy-build/src/main/java/software/amazon/smithy/build/SmithyBuildResult.java index 0c657b30a38..534a2e0f955 100644 --- a/smithy-build/src/main/java/software/amazon/smithy/build/SmithyBuildResult.java +++ b/smithy-build/src/main/java/software/amazon/smithy/build/SmithyBuildResult.java @@ -17,6 +17,7 @@ import java.nio.file.Path; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; @@ -123,7 +124,7 @@ public boolean isEmpty() { * Creates a SmithyBuildResult. */ public static final class Builder implements SmithyBuilder { - private final List results = new ArrayList<>(); + private final List results = Collections.synchronizedList(new ArrayList<>()); private Builder() {} @@ -135,6 +136,9 @@ public SmithyBuildResult build() { /** * Adds a projection result to the builder. * + *

This method is thread-safe as a synchronized list is updated each + * time this is called. + * * @param result Result to add. * @return Returns the builder. */ diff --git a/smithy-build/src/test/java/software/amazon/smithy/build/SmithyBuildTest.java b/smithy-build/src/test/java/software/amazon/smithy/build/SmithyBuildTest.java index c662021fc8c..95583f4ec7e 100644 --- a/smithy-build/src/test/java/software/amazon/smithy/build/SmithyBuildTest.java +++ b/smithy-build/src/test/java/software/amazon/smithy/build/SmithyBuildTest.java @@ -16,12 +16,15 @@ package software.amazon.smithy.build; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.File; @@ -31,6 +34,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.Comparator; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -482,4 +486,42 @@ public void canFilterPlugins() throws URISyntaxException { assertFalse(a.getPluginManifest("model").isPresent()); assertTrue(a.getPluginManifest("build-info").isPresent()); } + + @Test + public void throwsWhenErrorsOccur() throws Exception { + Path badConfig = Paths.get(getClass().getResource("trigger-plugin-error.json").toURI()); + Model model = Model.assembler() + .addImport(getClass().getResource("simple-model.json")) + .assemble() + .unwrap(); + + RuntimeException canned = new RuntimeException("Hi"); + Map plugins = new HashMap<>(); + plugins.put("foo", new SmithyBuildPlugin() { + @Override + public String getName() { + return "foo"; + } + + @Override + public void execute(PluginContext context) { + throw canned; + } + }); + + Function> factory = SmithyBuildPlugin.createServiceFactory(); + Function> composed = name -> OptionalUtils.or( + Optional.ofNullable(plugins.get(name)), () -> factory.apply(name)); + + SmithyBuild builder = new SmithyBuild() + .model(model) + .fileManifestFactory(MockManifest::new) + .pluginFactory(composed) + .config(SmithyBuildConfig.load(badConfig)); + + SmithyBuildException e = Assertions.assertThrows(SmithyBuildException.class, builder::build); + assertThat(e.getMessage(), containsString("1 Smithy build projections failed")); + assertThat(e.getMessage(), containsString("(exampleProjection): java.lang.RuntimeException: Hi")); + assertThat(e.getSuppressed(), equalTo(new Throwable[]{canned})); + } } diff --git a/smithy-build/src/test/resources/software/amazon/smithy/build/trigger-plugin-error.json b/smithy-build/src/test/resources/software/amazon/smithy/build/trigger-plugin-error.json new file mode 100644 index 00000000000..cea64649a6d --- /dev/null +++ b/smithy-build/src/test/resources/software/amazon/smithy/build/trigger-plugin-error.json @@ -0,0 +1,10 @@ +{ + "version": "1.0", + "projections": { + "exampleProjection": { + "plugins": { + "foo": {} + } + } + } +} diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/BuildCommand.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/BuildCommand.java index e40ae9d50d6..4acf84f6ca9 100644 --- a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/BuildCommand.java +++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/BuildCommand.java @@ -19,11 +19,17 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.ArrayList; import java.util.Collections; +import java.util.Iterator; import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiConsumer; +import java.util.function.Consumer; import java.util.logging.Logger; +import software.amazon.smithy.build.FileManifest; +import software.amazon.smithy.build.ProjectionResult; import software.amazon.smithy.build.SmithyBuild; -import software.amazon.smithy.build.SmithyBuildResult; import software.amazon.smithy.build.model.SmithyBuildConfig; import software.amazon.smithy.cli.Arguments; import software.amazon.smithy.cli.CliError; @@ -33,6 +39,7 @@ import software.amazon.smithy.cli.SmithyCli; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.loader.ModelAssembler; +import software.amazon.smithy.model.validation.Severity; import software.amazon.smithy.model.validation.ValidatedResult; public final class BuildCommand implements Command { @@ -112,15 +119,27 @@ public void execute(Arguments arguments, ClassLoader classLoader) { // Register sources with the builder. models.forEach(path -> smithyBuild.registerSources(Paths.get(path))); - SmithyBuildResult smithyBuildResult = smithyBuild.build(); - - // Fail if any projections failed to build, but build all projections. - if (smithyBuildResult.anyBroken()) { - throw new CliError("One or more projections contained ERROR or unsuppressed DANGER events"); + ResultConsumer resultConsumer = new ResultConsumer(); + smithyBuild.build(resultConsumer, resultConsumer); + + // Always print out the status of the successful projections. + Colors color = resultConsumer.failedProjections.isEmpty() + ? Colors.BRIGHT_BOLD_GREEN + : Colors.BRIGHT_BOLD_YELLOW; + Colors.out(color, String.format( + "Smithy built %s projection(s), %s plugin(s), and %s artifacts", + resultConsumer.projectionCount, + resultConsumer.pluginCount, + resultConsumer.artifactCount)); + + // Throw an exception if any errors occurred. + if (!resultConsumer.failedProjections.isEmpty()) { + resultConsumer.failedProjections.sort(String::compareTo); + throw new CliError(String.format( + "The following %d Smithy build projection(s) failed: %s", + resultConsumer.failedProjections.size(), + resultConsumer.failedProjections)); } - - Colors.out(Colors.BRIGHT_BOLD_GREEN, "Smithy build successfully generated the following artifacts"); - smithyBuildResult.allArtifacts().map(Path::toString).sorted().forEach(System.out::println); } private ValidatedResult buildModel(ClassLoader classLoader, List models, Arguments arguments) { @@ -132,4 +151,57 @@ private ValidatedResult buildModel(ClassLoader classLoader, List Validator.validate(result, true); return result; } + + private static final class ResultConsumer implements Consumer, BiConsumer { + List failedProjections = Collections.synchronizedList(new ArrayList<>()); + AtomicInteger artifactCount = new AtomicInteger(); + AtomicInteger pluginCount = new AtomicInteger(); + AtomicInteger projectionCount = new AtomicInteger(); + + @Override + public void accept(String name, Throwable exception) { + failedProjections.add(name); + StringBuilder message = new StringBuilder( + String.format("%nProjection %s failed: %s%n", name, exception.toString())); + + for (StackTraceElement element : exception.getStackTrace()) { + message.append(element).append(System.lineSeparator()); + } + + System.out.println(message); + } + + @Override + public void accept(ProjectionResult result) { + if (result.isBroken()) { + // Write out validation errors as they occur. + failedProjections.add(result.getProjectionName()); + StringBuilder message = new StringBuilder(System.lineSeparator()); + message.append(result.getProjectionName()) + .append(" has a model that failed validation") + .append(System.lineSeparator()); + result.getEvents().forEach(event -> { + if (event.getSeverity() == Severity.DANGER || event.getSeverity() == Severity.ERROR) { + message.append(event).append(System.lineSeparator()); + } + }); + Colors.out(Colors.RED, message.toString()); + } else { + // Only increment the projection count if it succeeded. + projectionCount.incrementAndGet(); + } + + pluginCount.addAndGet(result.getPluginManifests().size()); + + // Get the base directory of the projection. + Iterator manifestIterator = result.getPluginManifests().values().iterator(); + Path root = manifestIterator.hasNext() ? manifestIterator.next().getBaseDir().getParent() : null; + Colors.out(Colors.GREEN, String.format("Completed projection %s: %s", result.getProjectionName(), root)); + + // Increment the total number of artifacts written. + for (FileManifest manifest : result.getPluginManifests().values()) { + artifactCount.addAndGet(manifest.getFiles().size()); + } + } + } } diff --git a/smithy-cli/src/test/java/software/amazon/smithy/cli/commands/BuildCommandTest.java b/smithy-cli/src/test/java/software/amazon/smithy/cli/commands/BuildCommandTest.java index 10b0139cf64..de8e333ba3a 100644 --- a/smithy-cli/src/test/java/software/amazon/smithy/cli/commands/BuildCommandTest.java +++ b/smithy-cli/src/test/java/software/amazon/smithy/cli/commands/BuildCommandTest.java @@ -20,7 +20,9 @@ import java.io.ByteArrayOutputStream; import java.io.PrintStream; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import software.amazon.smithy.cli.CliError; import software.amazon.smithy.cli.SmithyCli; public class BuildCommandTest { @@ -36,4 +38,66 @@ public void hasBuildCommand() throws Exception { assertThat(help, containsString("Builds")); } + + @Test + public void dumpsOutValidationErrorsAndFails() throws Exception { + PrintStream out = System.out; + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + PrintStream printStream = new PrintStream(outputStream); + System.setOut(printStream); + + CliError e = Assertions.assertThrows(CliError.class, () -> { + String model = getClass().getResource("unknown-trait.smithy").getPath(); + SmithyCli.create().configureLogging(true).run("build", model); + }); + + System.setOut(out); + String output = outputStream.toString("UTF-8"); + + assertThat(output, containsString("smithy.example#MyString")); + assertThat(output, containsString("1 ERROR")); + assertThat(e.getMessage(), containsString("The model is invalid")); + } + + @Test + public void printsSuccessfulProjections() throws Exception { + String model = getClass().getResource("valid-model.smithy").getPath(); + + PrintStream out = System.out; + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + PrintStream printStream = new PrintStream(outputStream); + System.setOut(printStream); + SmithyCli.create().configureLogging(true).run("build", model); + System.setOut(out); + String output = outputStream.toString("UTF-8"); + + assertThat(output, containsString("Completed projection source")); + assertThat(output, containsString("Smithy built ")); + } + + @Test + public void validationFailuresCausedByProjectionsAreDetected() throws Exception { + PrintStream out = System.out; + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + PrintStream printStream = new PrintStream(outputStream); + System.setOut(printStream); + + CliError e = Assertions.assertThrows(CliError.class, () -> { + String model = getClass().getResource("valid-model.smithy").getPath(); + String config = getClass().getResource("projection-build-failure.json").getPath(); + SmithyCli.create().configureLogging(true).run("build", "--debug", "--config", config, model); + }); + + System.setOut(out); + String output = outputStream.toString("UTF-8"); + + assertThat(output, containsString("ResourceLifecycle")); + assertThat(e.getMessage(), + containsString("The following 1 Smithy build projection(s) failed: [exampleProjection]")); + } + + @Test + public void exceptionsThrownByProjectionsAreDetected() { + // TODO: need to make a plugin throw an exception + } } diff --git a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/projection-build-failure.json b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/projection-build-failure.json new file mode 100644 index 00000000000..13de1c13203 --- /dev/null +++ b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/projection-build-failure.json @@ -0,0 +1,10 @@ +{ + "version": "1.0", + "projections": { + "exampleProjection": { + "transforms": [ + {"name": "excludeTraits", "args": ["readonly"]} + ] + } + } +} diff --git a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/valid-model.smithy b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/valid-model.smithy new file mode 100644 index 00000000000..f06701bfe89 --- /dev/null +++ b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/valid-model.smithy @@ -0,0 +1,18 @@ +namespace smithy.example + +resource Foo { + identifier: {id: FooId}, + read: GetFoo, +} + +string FooId + +@readonly +operation GetFoo(GetFooInput) -> GetFooOutput + +structure GetFooInput { + @required + id: FooId, +} + +structure GetFooOutput {}