From 5c8f6a9f7833ca3535366788930afbe9e8a10cce Mon Sep 17 00:00:00 2001 From: Miles Ziemer Date: Thu, 30 Nov 2023 10:09:33 -0500 Subject: [PATCH] Add configurable diff evaluator Adds a smithy-diff evaluator which can be configured via metadata property `diffEvaluators` within the model. Unlike the similar EmitEach/EmitNoneSelectorValidators, this diff evaluator is not loaded using SPI, and the ability to create your own custom configurable diff evaluators has not been exposed. Instead, smithy-diff just looks at the new model's metadata and loads any diff evaluators directly. We can expose creating custom configurable diff evaluators later if there is a need, but this diff evaluator should be enough for almost every use case, and we can also extend it easily with more configuration options. This evaluator works as follows: 1. Get a subset of shapes to match based on the `appliesTo` property. Currently either added shapes or removed shapes. 2. Optionally filter this subset of shapes further with a selector configured in the `filter` property, which runs on either the new or old model depending on `appliesTo`. 3. Run the configured `selector` on either the new model or old model depending on `appliesTo`. 4. Match shapes returned by `selector` to the set of shapes from 2, and emit events based on `emitCondition`. This functionality can be extended later by adding more options for `emitCondition` and `appliesTo`. For example, there is currently no configuration option for looking at changed shapes, but you could imaging wanting to see which shapes don't match in the old model, but do in the new model, which would be a new `appliesTo`. Another `emitCondition` could be `IfAnyDontMatch` or something. Also adds a validator to smithy-diff for this metadata property so you'll know when the model is being build if evaluators have been configured incorrectly. This requires having a dependency on smithy-diff in the model package where you're configuring the diff evaluators, but the alternative is stick the validation in smithy-model, creating an implicit dependency on smithy-diff. --- .../diff/DiffEvaluatorsMetadataValidator.java | 23 ++++ .../amazon/smithy/diff/ModelDiff.java | 3 + .../evaluators/configurable/AppliesTo.java | 40 ++++++ .../configurable/ConfigurableEvaluator.java | 89 +++++++++++++ .../ConfigurableEvaluatorDefinition.java | 126 ++++++++++++++++++ .../ConfigurableEvaluatorLoader.java | 58 ++++++++ .../configurable/EmitCondition.java | 44 ++++++ ...e.amazon.smithy.model.validation.Validator | 1 + .../ConfigurableEvaluatorTest.java | 50 +++++++ .../configurable/configurable-a.smithy | 9 ++ .../configurable/configurable-b.smithy | 54 ++++++++ 11 files changed, 497 insertions(+) create mode 100644 smithy-diff/src/main/java/software/amazon/smithy/diff/DiffEvaluatorsMetadataValidator.java create mode 100644 smithy-diff/src/main/java/software/amazon/smithy/diff/evaluators/configurable/AppliesTo.java create mode 100644 smithy-diff/src/main/java/software/amazon/smithy/diff/evaluators/configurable/ConfigurableEvaluator.java create mode 100644 smithy-diff/src/main/java/software/amazon/smithy/diff/evaluators/configurable/ConfigurableEvaluatorDefinition.java create mode 100644 smithy-diff/src/main/java/software/amazon/smithy/diff/evaluators/configurable/ConfigurableEvaluatorLoader.java create mode 100644 smithy-diff/src/main/java/software/amazon/smithy/diff/evaluators/configurable/EmitCondition.java create mode 100644 smithy-diff/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator create mode 100644 smithy-diff/src/test/java/software/amazon/smithy/diff/evaluators/configurable/ConfigurableEvaluatorTest.java create mode 100644 smithy-diff/src/test/resources/software/amazon/smithy/diff/evaluators/configurable/configurable-a.smithy create mode 100644 smithy-diff/src/test/resources/software/amazon/smithy/diff/evaluators/configurable/configurable-b.smithy diff --git a/smithy-diff/src/main/java/software/amazon/smithy/diff/DiffEvaluatorsMetadataValidator.java b/smithy-diff/src/main/java/software/amazon/smithy/diff/DiffEvaluatorsMetadataValidator.java new file mode 100644 index 00000000000..d917ab96980 --- /dev/null +++ b/smithy-diff/src/main/java/software/amazon/smithy/diff/DiffEvaluatorsMetadataValidator.java @@ -0,0 +1,23 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.diff; + +import java.util.List; +import software.amazon.smithy.diff.evaluators.configurable.ConfigurableEvaluatorLoader; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.validation.AbstractValidator; +import software.amazon.smithy.model.validation.ValidationEvent; + +/** + * Validates diff evaluators configured in {@code diffEvaluators} metadata + * property. + */ +public class DiffEvaluatorsMetadataValidator extends AbstractValidator { + @Override + public List validate(Model model) { + return ConfigurableEvaluatorLoader.loadMetadataDiffEvaluators(model).getValidationEvents(); + } +} diff --git a/smithy-diff/src/main/java/software/amazon/smithy/diff/ModelDiff.java b/smithy-diff/src/main/java/software/amazon/smithy/diff/ModelDiff.java index cb5c6b88199..7797128b5e2 100644 --- a/smithy-diff/src/main/java/software/amazon/smithy/diff/ModelDiff.java +++ b/smithy-diff/src/main/java/software/amazon/smithy/diff/ModelDiff.java @@ -23,6 +23,7 @@ import java.util.Set; import java.util.TreeSet; import java.util.stream.Collectors; +import software.amazon.smithy.diff.evaluators.configurable.ConfigurableEvaluatorLoader; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.validation.Severity; import software.amazon.smithy.model.validation.ValidatedResult; @@ -281,6 +282,8 @@ public Result compare() { List evaluators = new ArrayList<>(); ServiceLoader.load(DiffEvaluator.class, classLoader).forEach(evaluators::add); + evaluators.addAll(ConfigurableEvaluatorLoader.loadMetadataDiffEvaluators(newModel).unwrap()); + Differences differences = Differences.detect(oldModel, newModel); // Applies suppressions and elevates event severities. diff --git a/smithy-diff/src/main/java/software/amazon/smithy/diff/evaluators/configurable/AppliesTo.java b/smithy-diff/src/main/java/software/amazon/smithy/diff/evaluators/configurable/AppliesTo.java new file mode 100644 index 00000000000..1a0435d1352 --- /dev/null +++ b/smithy-diff/src/main/java/software/amazon/smithy/diff/evaluators/configurable/AppliesTo.java @@ -0,0 +1,40 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.diff.evaluators.configurable; + +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.NodeMapper; + +/** + * Possible values for the {@code appliesTo} property of configurable diff evaluators. + * + *

This property configures which model the selectors should run on. + *

    + *
  1. Using {@link AppliesTo#ADDED_SHAPES} will configure the evaluator to run selectors on + * the new model, and only consider matches corresponding to added shapes. + *
  2. Using {@link AppliesTo#REMOVED_SHAPES} will configure the evaluator to run selectors on + * the old model, and only consider matches corresponding to removed shapes. + *
+ */ +enum AppliesTo { + ADDED_SHAPES("AddedShapes"), + REMOVED_SHAPES("RemovedShapes"); + + private final String stringValue; + + AppliesTo(String stringValue) { + this.stringValue = stringValue; + } + + @Override + public String toString() { + return stringValue; + } + + static AppliesTo fromNode(Node node) { + return new NodeMapper().deserialize(node, AppliesTo.class); + } +} diff --git a/smithy-diff/src/main/java/software/amazon/smithy/diff/evaluators/configurable/ConfigurableEvaluator.java b/smithy-diff/src/main/java/software/amazon/smithy/diff/evaluators/configurable/ConfigurableEvaluator.java new file mode 100644 index 00000000000..01a97b94db5 --- /dev/null +++ b/smithy-diff/src/main/java/software/amazon/smithy/diff/evaluators/configurable/ConfigurableEvaluator.java @@ -0,0 +1,89 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.diff.evaluators.configurable; + +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import software.amazon.smithy.diff.Differences; +import software.amazon.smithy.diff.evaluators.AbstractDiffEvaluator; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.validation.ValidationEvent; + +/** + * Diff evaluator that is configurable via the `diffEvaluators` metadata property in + * Smithy models. + */ +public final class ConfigurableEvaluator extends AbstractDiffEvaluator { + private final ConfigurableEvaluatorDefinition definition; + + ConfigurableEvaluator(ConfigurableEvaluatorDefinition definition) { + this.definition = definition; + } + + @Override + public String getEventId() { + return definition.getId(); + } + + @Override + public List evaluate(Differences differences) { + Model model = getApplicableModel(differences); + Set shapes = getApplicableShapes(differences); + definition.getFilter().ifPresent(filter -> shapes.retainAll(filter.select(model))); + Set matches = definition.getSelector().shapes(model) + .filter(shapes::contains) + .collect(Collectors.toSet()); + return mapToEvents(shapes, matches); + } + + private Model getApplicableModel(Differences differences) { + return definition.getAppliesTo() == AppliesTo.ADDED_SHAPES + ? differences.getNewModel() + : differences.getOldModel(); + } + + private Set getApplicableShapes(Differences differences) { + return definition.getAppliesTo() == AppliesTo.ADDED_SHAPES + ? differences.addedShapes().collect(Collectors.toSet()) + : differences.removedShapes().collect(Collectors.toSet()); + } + + private List mapToEvents(Set applicableShapes, Set matches) { + switch (definition.getEmitCondition()) { + case IF_ANY_MATCH: + if (!matches.isEmpty()) { + return Collections.singletonList(getBaseEventBuilder().build()); + } + break; + case IF_ALL_MATCH: + if (matches.equals(applicableShapes)) { + return Collections.singletonList(getBaseEventBuilder().build()); + } + break; + case IF_NONE_MATCH: + if (matches.stream().anyMatch(applicableShapes::contains)) { + return Collections.singletonList(getBaseEventBuilder().build()); + } + break; + case FOR_EACH_MATCH: + default: + return matches.stream() + .map(shape -> getBaseEventBuilder().shape(shape).build()) + .collect(Collectors.toList()); + } + return Collections.emptyList(); + } + + private ValidationEvent.Builder getBaseEventBuilder() { + return ValidationEvent.builder() + .id(definition.getId()) + .message(definition.getMessage()) + .severity(definition.getSeverity()); + } +} diff --git a/smithy-diff/src/main/java/software/amazon/smithy/diff/evaluators/configurable/ConfigurableEvaluatorDefinition.java b/smithy-diff/src/main/java/software/amazon/smithy/diff/evaluators/configurable/ConfigurableEvaluatorDefinition.java new file mode 100644 index 00000000000..11a17410aa4 --- /dev/null +++ b/smithy-diff/src/main/java/software/amazon/smithy/diff/evaluators/configurable/ConfigurableEvaluatorDefinition.java @@ -0,0 +1,126 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.diff.evaluators.configurable; + +import java.util.List; +import java.util.Optional; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.selector.Selector; +import software.amazon.smithy.model.validation.Severity; +import software.amazon.smithy.utils.ListUtils; + +/** + * Definition of a diff evaluator which can be configured within a Smithy model. + */ +final class ConfigurableEvaluatorDefinition { + private static final String ID = "id"; + private static final String MESSAGE = "message"; + private static final String EMIT_CONDITION = "emitCondition"; + private static final String APPLIES_TO = "appliesTo"; + private static final String SEVERITY = "severity"; + private static final String FILTER = "filter"; + private static final String SELECTOR = "selector"; + private static final List ALLOWED_PROPERTIES = ListUtils.of( + ID, MESSAGE, EMIT_CONDITION, APPLIES_TO, SEVERITY, FILTER, SELECTOR); + + + private final String id; + private final String message; + private final EmitCondition emitCondition; + private final AppliesTo appliesTo; + private final Severity severity; + private final Selector filter; + private final Selector selector; + + private ConfigurableEvaluatorDefinition( + String id, + String message, + EmitCondition emitCondition, + AppliesTo appliesTo, + Severity severity, + Selector filter, + Selector selector + ) { + this.id = id; + this.message = message; + this.emitCondition = emitCondition; + this.appliesTo = appliesTo; + this.severity = severity; + this.filter = filter; + this.selector = selector; + } + + static ConfigurableEvaluatorDefinition fromNode(Node node) { + ObjectNode objectNode = node.expectObjectNode("Configurable diff evaluators must be objects."); + objectNode.warnIfAdditionalProperties(ALLOWED_PROPERTIES); + String id = objectNode.expectStringMember(ID).getValue(); + String message = objectNode.expectStringMember(MESSAGE).getValue(); + EmitCondition emitCondition = EmitCondition.fromNode(objectNode.expectStringMember(EMIT_CONDITION)); + AppliesTo appliesTo = AppliesTo.fromNode(objectNode.expectStringMember(APPLIES_TO)); + Severity severity = Severity.fromNode(objectNode.expectStringMember(SEVERITY)); + Selector filter = objectNode.getStringMember(FILTER).map(Selector::fromNode).orElse(null); + Selector selector = Selector.fromNode(objectNode.expectStringMember(SELECTOR)); + return new ConfigurableEvaluatorDefinition( + id, + message, + emitCondition, + appliesTo, + severity, + filter, + selector + ); + } + + /** + * @return The id of the event the diff evaluator will emit. + */ + String getId() { + return id; + } + + /** + * @return The message in the event that the diff evaluator will emit. + */ + String getMessage() { + return message; + } + + /** + * @return The condition on which the diff evaluator will emit an event. + */ + EmitCondition getEmitCondition() { + return emitCondition; + } + + /** + * @return What kind of diff the evaluator applies to. + */ + AppliesTo getAppliesTo() { + return appliesTo; + } + + /** + * @return The severity of the events emitted by the diff evaluator. + */ + Severity getSeverity() { + return severity; + } + + /** + * @return An optional selector used to filter the subset of shapes configured by {@link #getAppliesTo()}. + */ + Optional getFilter() { + return Optional.ofNullable(filter); + } + + /** + * @return The selector that chooses which shapes the diff evaluator should apply to. + */ + Selector getSelector() { + return selector; + } +} diff --git a/smithy-diff/src/main/java/software/amazon/smithy/diff/evaluators/configurable/ConfigurableEvaluatorLoader.java b/smithy-diff/src/main/java/software/amazon/smithy/diff/evaluators/configurable/ConfigurableEvaluatorLoader.java new file mode 100644 index 00000000000..95f9445e3e8 --- /dev/null +++ b/smithy-diff/src/main/java/software/amazon/smithy/diff/evaluators/configurable/ConfigurableEvaluatorLoader.java @@ -0,0 +1,58 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.diff.evaluators.configurable; + +import java.util.ArrayList; +import java.util.List; +import software.amazon.smithy.diff.DiffEvaluator; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.SourceException; +import software.amazon.smithy.model.node.ArrayNode; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.validation.ValidatedResult; +import software.amazon.smithy.model.validation.ValidationEvent; + +/** + * Loads configurable diff evaluators defined in the model's metadata. + */ +public final class ConfigurableEvaluatorLoader { + private static final String DIFF_EVALUATORS_KEY = "diffEvaluators"; + + private ConfigurableEvaluatorLoader() {} + + /** + * Loads configurable diff evaluators defined in a model's metadata. + * + *

This returns a ValidatedResult containing validation events that + * occurred when deserializing found diff evaluators. + * + * @param model Model to load diff evaluators from. + * @return The result of loading the diff evaluators, including any validation errors that occurred. + */ + public static ValidatedResult> loadMetadataDiffEvaluators(Model model) { + List events = new ArrayList<>(); + List evaluatorDefinitions = new ArrayList<>(); + + model.getMetadataProperty(DIFF_EVALUATORS_KEY).ifPresent(node -> { + try { + ArrayNode arrayNode = node.expectArrayNode( + String.format("metadata property `%s` must be an array of objects.", DIFF_EVALUATORS_KEY)); + for (Node element : arrayNode.getElements()) { + try { + ConfigurableEvaluatorDefinition definition = ConfigurableEvaluatorDefinition.fromNode(element); + evaluatorDefinitions.add(new ConfigurableEvaluator(definition)); + } catch (SourceException e) { + events.add(ValidationEvent.fromSourceException(e)); + } + } + } catch (SourceException e) { + events.add(ValidationEvent.fromSourceException(e)); + } + }); + + return new ValidatedResult<>(evaluatorDefinitions, events); + } +} diff --git a/smithy-diff/src/main/java/software/amazon/smithy/diff/evaluators/configurable/EmitCondition.java b/smithy-diff/src/main/java/software/amazon/smithy/diff/evaluators/configurable/EmitCondition.java new file mode 100644 index 00000000000..84657f6e7e8 --- /dev/null +++ b/smithy-diff/src/main/java/software/amazon/smithy/diff/evaluators/configurable/EmitCondition.java @@ -0,0 +1,44 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.diff.evaluators.configurable; + +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.NodeMapper; + +/** + * The possible values of the {@code emitCondition} property of configurable diff evaluators. + * + *

This property configures the conditions under which the diff evaluator should emit events, + * based on the shapes which matched the configured {@code selector}. These conditions are applied + * to the set of shapes configured by {@code appliesTo} and {@code filter} (if present). + *

    + *
  1. Using {@link EmitCondition#IF_ANY_MATCH} will emit a single event if any of the shapes match. + *
  2. Using {@link EmitCondition#IF_ALL_MATCH} will emit a single event if all the shapes match. + *
  3. Using {@link EmitCondition#IF_NONE_MATCH} will emit a single event if none of the shapes match. + *
  4. Using {@link EmitCondition#FOR_EACH_MATCH} will emit an event for each shape that matches. + *
+ */ +enum EmitCondition { + IF_ANY_MATCH("IfAnyMatch"), + IF_ALL_MATCH("IfAllMatch"), + IF_NONE_MATCH("IfNoneMatch"), + FOR_EACH_MATCH("ForEachMatch"); + + private final String stringValue; + + EmitCondition(String stringValue) { + this.stringValue = stringValue; + } + + @Override + public String toString() { + return stringValue; + } + + static EmitCondition fromNode(Node node) { + return new NodeMapper().deserialize(node, EmitCondition.class); + } +} diff --git a/smithy-diff/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator b/smithy-diff/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator new file mode 100644 index 00000000000..d9ac3b07c16 --- /dev/null +++ b/smithy-diff/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator @@ -0,0 +1 @@ +software.amazon.smithy.diff.DiffEvaluatorsMetadataValidator diff --git a/smithy-diff/src/test/java/software/amazon/smithy/diff/evaluators/configurable/ConfigurableEvaluatorTest.java b/smithy-diff/src/test/java/software/amazon/smithy/diff/evaluators/configurable/ConfigurableEvaluatorTest.java new file mode 100644 index 00000000000..29f82a14b84 --- /dev/null +++ b/smithy-diff/src/test/java/software/amazon/smithy/diff/evaluators/configurable/ConfigurableEvaluatorTest.java @@ -0,0 +1,50 @@ +package software.amazon.smithy.diff.evaluators.configurable; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.Test; +import software.amazon.smithy.diff.ModelDiff; +import software.amazon.smithy.diff.evaluators.TestHelper; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.validation.Severity; +import software.amazon.smithy.model.validation.ValidationEvent; + +public class ConfigurableEvaluatorTest { + @Test + public void configurableEvaluatorsEmitEvents() { + Model oldModel = Model.assembler() + .addImport(getClass().getResource("configurable-a.smithy")) + .assemble() + .unwrap(); + Model newModel = Model.assembler() + .addImport(getClass().getResource("configurable-b.smithy")) + .assemble() + .unwrap(); + + ModelDiff.Result result = ModelDiff.builder() + .oldModel(oldModel) + .newModel(newModel) + .compare(); + List events = result.getDiffEvents(); + + assertThat(TestHelper.findEvents(events, "AddedInternalOperation").size(), equalTo(1)); + assertThat(TestHelper.findEvents(events, "AddedInternalOperation").get(0).getSeverity(), + equalTo(Severity.NOTE)); + assertThat(TestHelper.findEvents(events, "AddedInternalOperation").get(0).getShapeId(), + equalTo(Optional.of(ShapeId.from("smithy.example#InternalOperation")))); + + assertThat(TestHelper.findEvents(events, "AddedOnlyPrimitiveNumbersAndBools").size(), equalTo(1)); + assertThat(TestHelper.findEvents(events, "AddedOnlyPrimitiveNumbersAndBools").get(0).getShapeId(), + equalTo(Optional.empty())); + + assertThat(TestHelper.findEvents(events, "AddedMemberWithoutClientOptional").size(), equalTo(1)); + assertThat(TestHelper.findEvents(events, "AddedMemberWithoutClientOptional").get(0).getSeverity(), + equalTo(Severity.WARNING)); + + assertThat(TestHelper.findEvents(events, "RemovedRootShape").size(), equalTo(2)); + } +} diff --git a/smithy-diff/src/test/resources/software/amazon/smithy/diff/evaluators/configurable/configurable-a.smithy b/smithy-diff/src/test/resources/software/amazon/smithy/diff/evaluators/configurable/configurable-a.smithy new file mode 100644 index 00000000000..821d2fa0a4c --- /dev/null +++ b/smithy-diff/src/test/resources/software/amazon/smithy/diff/evaluators/configurable/configurable-a.smithy @@ -0,0 +1,9 @@ +$version: "2.0" + +namespace smithy.example + +structure Foo {} + +string RootString + +document RootDocument diff --git a/smithy-diff/src/test/resources/software/amazon/smithy/diff/evaluators/configurable/configurable-b.smithy b/smithy-diff/src/test/resources/software/amazon/smithy/diff/evaluators/configurable/configurable-b.smithy new file mode 100644 index 00000000000..5ab490a69ba --- /dev/null +++ b/smithy-diff/src/test/resources/software/amazon/smithy/diff/evaluators/configurable/configurable-b.smithy @@ -0,0 +1,54 @@ +$version: "2.0" + +metadata diffEvaluators = [ + { + id: "AddedInternalOperation" + message: "Added operation with `@internal` trait." + emitCondition: "ForEachMatch" + appliesTo: "AddedShapes" + severity: "NOTE" + selector: "operation [trait|internal]" + } + { + id: "AddedOnlyPrimitiveNumbersAndBools" + message: "All added numbers and booleans were primitives." + emitCondition: "IfAllMatch" + appliesTo: "AddedShapes" + severity: "NOTE" + filter: ":is(member :test(> number), number)" + selector: "[trait|default = 0]" + } + { + id: "AddedMemberWithoutClientOptional" + message: "One of the added members does not have `@clientOptional` trait." + emitCondition: "IfAnyMatch" + appliesTo: "AddedShapes" + severity: "WARNING" + selector: "member :not([trait|clientOptional])" + } + { + id: "RemovedRootShape" + message: "Removed root shape." + emitCondition: "ForEachMatch" + appliesTo: "RemovedShapes" + severity: "NOTE" + selector: "simpleType" + } +] + +namespace smithy.example + +@internal +operation InternalOperation {} + +structure Foo { + bar: Integer = 0 + baz: String +} + +structure Bar { + baz: Integer = 0 +} + +@default(0) +integer PrimitiveInt