-
Notifications
You must be signed in to change notification settings - Fork 218
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
- Loading branch information
1 parent
d457aab
commit 273a14c
Showing
11 changed files
with
498 additions
and
0 deletions.
There are no files selected for viewing
23 changes: 23 additions & 0 deletions
23
smithy-diff/src/main/java/software/amazon/smithy/diff/DiffEvaluatorsMetadataValidator.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<ValidationEvent> validate(Model model) { | ||
return ConfigurableEvaluatorLoader.loadMetadataDiffEvaluators(model).getValidationEvents(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
41 changes: 41 additions & 0 deletions
41
smithy-diff/src/main/java/software/amazon/smithy/diff/evaluators/configurable/AppliesTo.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
/* | ||
* 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. | ||
* | ||
* <p>This property configures which model the selectors should run on. | ||
* <ol> | ||
* <li> Using {@link AppliesTo#ADDED_SHAPES} will configure the evaluator to run selectors on | ||
* the new model, and only consider matches corresponding to added shapes. | ||
* <li> Using {@link AppliesTo#REMOVED_SHAPES} will configure the evaluator to run selectors on | ||
* the old model, and only consider matches corresponding to removed shapes. | ||
* </ol> | ||
* | ||
*/ | ||
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); | ||
} | ||
} |
89 changes: 89 additions & 0 deletions
89
.../main/java/software/amazon/smithy/diff/evaluators/configurable/ConfigurableEvaluator.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<ValidationEvent> evaluate(Differences differences) { | ||
Model model = getApplicableModel(differences); | ||
Set<Shape> shapes = getApplicableShapes(differences); | ||
definition.getFilter().ifPresent(filter -> shapes.retainAll(filter.select(model))); | ||
Set<Shape> 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<Shape> getApplicableShapes(Differences differences) { | ||
return definition.getAppliesTo() == AppliesTo.ADDED_SHAPES | ||
? differences.addedShapes().collect(Collectors.toSet()) | ||
: differences.removedShapes().collect(Collectors.toSet()); | ||
} | ||
|
||
private List<ValidationEvent> mapToEvents(Set<Shape> applicableShapes, Set<Shape> 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()); | ||
} | ||
} |
126 changes: 126 additions & 0 deletions
126
.../software/amazon/smithy/diff/evaluators/configurable/ConfigurableEvaluatorDefinition.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<String> 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<Selector> getFilter() { | ||
return Optional.ofNullable(filter); | ||
} | ||
|
||
/** | ||
* @return The selector that chooses which shapes the diff evaluator should apply to. | ||
*/ | ||
Selector getSelector() { | ||
return selector; | ||
} | ||
} |
58 changes: 58 additions & 0 deletions
58
...java/software/amazon/smithy/diff/evaluators/configurable/ConfigurableEvaluatorLoader.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. | ||
* | ||
* <p>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<List<DiffEvaluator>> loadMetadataDiffEvaluators(Model model) { | ||
List<ValidationEvent> events = new ArrayList<>(); | ||
List<DiffEvaluator> 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); | ||
} | ||
} |
Oops, something went wrong.