Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add configurable diff evaluator #2056

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -281,6 +282,8 @@ public Result compare() {

List<DiffEvaluator> 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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
* <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);
}
}
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());
}
}
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;
}
}
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);
}
}
Loading