Make various perf improvements

This commit makes various performance improvements that particularly help
when validating models with 100k+ shapes.

* Fixed JMH benchmarks so they work with the updated Gradle version.
* Model now uses a synchronized IdentityHashMap now to back the
  blackboard cache rather than a ConcurrentHashMap. This will actually
  now prevent duplicate work in creating KnowledgeIndexes. An
  IdentityHashMap was used because it works well for the classes
  used to cache knowledge indexes.
* HttpBindingIndex now uses a WeakReference to Model. This was
  previously an unnecessary cyclical reference.
* HttpBindingIndex no longer throws when attempting to access the
  HTTP bindings of shapes that don't exist or aren't operations.
  This prevents it from having to store a List entry for every
  single operation.
* Model#getShapesWithTraits was used everywhere possible rather
  than streaming over all shapes and looking for traits.
* Made optimizations to NullableIndex to no longer need to
  traverse every shape.
* Removed unnecessary streaming from OperationIndex.
* Removed unnecessary streaming from PaginatedIndex.
* Removed unnecessary streaming from NeighborVisitor.
* Updated Node expect methods to also accept a Supplier to create
  error messages if the expectation fails. This prevents needing to
  evaluate String.format even for valid node assertions.
* AttributeSelector no longer uses a BiFunction key supplier, and instead
  the attribute path is just passed in. This allows for the selector
  to also perform optimizations when determining if a shape has a trait
  by leveraging Model#getShapesWithTraits.
* InternalSelectors used to implement Selectors now support more general
  optimizations. This was previously hardcoded to only support an optimization
  for selecting shapes by type, but now selecting shapes by trait is
  optimized too.
* Minor optimization to structure and union loading so that when validating
  that members have a shape ID compatible with the container, an intermediate
  shape ID no longer is constructed.
* The ShapeId cache was increased from 1024 to 8192. This helps significantly
  with large models. The ShapeId cache was also updated to implement the
  LRA cache inside of the computeIfAbsent method.
* NodeValidationVisitor now has a Context object that supports caching
  selectors evaluated against a model. This helps significantly with IdRef
  validation.  To make this caching reusable, the visitor is now mutable
  after it is constructed.
* NodeValidationVisitor idRef now special cases "*" and uses the context
  cache.
* TraitTargetValidator has been simplified, special cases "*", and now
  uses a cache to speed up evaluating traits that use the same selectors.
* TraitValueValidator now reuses the same NodeValidationVisitor in order
  to reuse the same selector cache.
* The `Model#toSet(Class)` method is now public, so getting a set of
  shapes of a specific type no longer has to always go through a
  `Stream`.
This commit is contained in:
Michael Dowling 2021-05-16 23:56:28 -07:00 committed by Michael Dowling
parent 7ec2b66b63
commit 31b0ada4d8
52 changed files with 686 additions and 443 deletions

View File

@ -23,7 +23,7 @@ plugins {
id "jacoco"
id "com.github.spotbugs" version "4.6.0"
id "io.codearte.nexus-staging" version "0.21.0"
id "me.champeau.gradle.jmh" version "0.5.3"
id "me.champeau.jmh" version "0.6.4"
}
ext {

View File

@ -104,4 +104,11 @@
<Class name="software.amazon.smithy.model.knowledge.NeighborProviderIndex"/>
<Bug pattern="NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE"/>
</Match>
<!-- Spotbugs for some reason isn't seeing that Objects.requireNonNull prevents a null return.
this is used when dereferencing a WeakReference to the Model. -->
<Match>
<Class name="software.amazon.smithy.model.knowledge.HttpBindingIndex"/>
<Bug pattern="NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE"/>
</Match>
</FindBugsFilter>

View File

@ -21,7 +21,7 @@ ext {
moduleName = "software.amazon.smithy.model"
}
apply plugin: "me.champeau.gradle.jmh"
apply plugin: "me.champeau.jmh"
dependencies {
api project(":smithy-utils")

View File

@ -18,7 +18,6 @@ operation Resource1Operation {}
resource Resource1_2 {}
resource Resource1_1 {
type: resource,
operations: [Resource1_1_Operation],
resources: [Resource1_1_1, Resource1_1_2]
}

View File

@ -20,6 +20,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;
@ -64,7 +65,8 @@ public final class Model implements ToSmithyBuilder<Model> {
private final Map<Class<? extends Shape>, Set<? extends Shape>> cachedTypes = new ConcurrentHashMap<>();
/** Cache of computed {@link KnowledgeIndex} instances. */
private final Map<Class<? extends KnowledgeIndex>, KnowledgeIndex> blackboard = new ConcurrentHashMap<>();
private final Map<Class<? extends KnowledgeIndex>, KnowledgeIndex> blackboard
= Collections.synchronizedMap(new IdentityHashMap<>());
/** Lazily computed trait mappings. */
private volatile TraitCache traitCache;
@ -283,7 +285,7 @@ public final class Model implements ToSmithyBuilder<Model> {
* @return Returns an unmodifiable set of shapes.
*/
@SuppressWarnings("unchecked")
private <T extends Shape> Set<T> toSet(Class<T> shapeType) {
public <T extends Shape> Set<T> toSet(Class<T> shapeType) {
return (Set<T>) cachedTypes.computeIfAbsent(shapeType, t -> {
Set<T> result = new HashSet<>();
for (Shape shape : shapeMap.values()) {
@ -394,22 +396,7 @@ public final class Model implements ToSmithyBuilder<Model> {
*/
@SuppressWarnings("unchecked")
public <T extends KnowledgeIndex> T getKnowledge(Class<T> type, Function<Model, T> constructor) {
// This method intentionally does not use putIfAbsent to avoid
// deadlocks in the case where a knowledge index needs to access
// other knowledge indexes from a Model. While this *can* cause
// duplicate computations, it's preferable over *always* requiring
// duplicate computations, deadlocks of computeIfAbsent, or
// spreading out the cache state associated with previously
// computed indexes through WeakHashMaps on each KnowledgeIndex
// (a previous approach we used for caching).
T value = (T) blackboard.get(type);
if (value == null) {
value = constructor.apply(this);
blackboard.put(type, value);
}
return value;
return (T) blackboard.computeIfAbsent(type, t -> constructor.apply(this));
}
/**

View File

@ -40,7 +40,8 @@ public final class BottomUpIndex implements KnowledgeIndex {
public BottomUpIndex(Model model) {
PathFinder pathFinder = PathFinder.create(model);
model.shapes(ServiceShape.class).forEach(service -> {
for (ServiceShape service : model.toSet(ServiceShape.class)) {
Map<ShapeId, List<EntityShape>> serviceBindings = new HashMap<>();
parentBindings.put(service.getId(), serviceBindings);
for (PathFinder.Path path : pathFinder.search(service, SELECTOR)) {
@ -56,7 +57,7 @@ public final class BottomUpIndex implements KnowledgeIndex {
// Add the end shape (a resource or operation) to the service bindings.
serviceBindings.put(path.getEndShape().getId(), shapes);
}
});
}
}
public static BottomUpIndex of(Model model) {

View File

@ -45,14 +45,14 @@ public final class EventStreamIndex implements KnowledgeIndex {
public EventStreamIndex(Model model) {
OperationIndex operationIndex = OperationIndex.of(model);
model.shapes(OperationShape.class).forEach(operation -> {
for (OperationShape operation : model.toSet(OperationShape.class)) {
operationIndex.getInput(operation).ifPresent(input -> {
computeEvents(model, operation, input, inputInfo);
});
operationIndex.getOutput(operation).ifPresent(output -> {
computeEvents(model, operation, output, outputInfo);
});
});
}
}
public static EventStreamIndex of(Model model) {

View File

@ -15,12 +15,14 @@
package software.amazon.smithy.model.knowledge;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
@ -44,8 +46,6 @@ import software.amazon.smithy.model.traits.HttpTrait;
import software.amazon.smithy.model.traits.MediaTypeTrait;
import software.amazon.smithy.model.traits.StreamingTrait;
import software.amazon.smithy.model.traits.TimestampFormatTrait;
import software.amazon.smithy.model.traits.Trait;
import software.amazon.smithy.utils.ListUtils;
/**
* Computes and indexes the explicit and implicit HTTP bindings of a model.
@ -59,29 +59,27 @@ import software.amazon.smithy.utils.ListUtils;
* <p>This index does not perform validation of the underlying model.
*/
public final class HttpBindingIndex implements KnowledgeIndex {
private final Model model;
private final WeakReference<Model> model;
private final Map<ShapeId, List<HttpBinding>> requestBindings = new HashMap<>();
private final Map<ShapeId, List<HttpBinding>> responseBindings = new HashMap<>();
public HttpBindingIndex(Model model) {
this.model = model;
this.model = new WeakReference<>(model);
OperationIndex opIndex = OperationIndex.of(model);
model.shapes(OperationShape.class).forEach(shape -> {
if (shape.getTrait(HttpTrait.class).isPresent()) {
requestBindings.put(shape.getId(), computeRequestBindings(opIndex, shape));
responseBindings.put(shape.getId(), computeResponseBindings(opIndex, shape));
} else {
requestBindings.put(shape.getId(), ListUtils.of());
responseBindings.put(shape.getId(), ListUtils.of());
}
});
for (Shape shape : model.getShapesWithTrait(HttpTrait.class)) {
shape.asOperationShape().ifPresent(operation -> {
requestBindings.put(operation.getId(), computeRequestBindings(opIndex, operation));
responseBindings.put(operation.getId(), computeResponseBindings(opIndex, operation));
});
}
// Add error structure bindings.
model.shapes(StructureShape.class)
.flatMap(shape -> Trait.flatMapStream(shape, ErrorTrait.class))
.forEach(pair -> responseBindings.put(
pair.getLeft().getId(),
createStructureBindings(pair.getLeft(), false)));
for (Shape shape : model.getShapesWithTrait(ErrorTrait.class)) {
shape.asStructureShape().ifPresent(structure -> {
responseBindings.put(structure.getId(), createStructureBindings(structure, false));
});
}
}
public static HttpBindingIndex of(Model model) {
@ -120,7 +118,7 @@ public final class HttpBindingIndex implements KnowledgeIndex {
private HttpTrait getHttpTrait(ToShapeId operation) {
ShapeId id = operation.toShapeId();
return model.getShape(id)
return getModel().getShape(id)
.orElseThrow(() -> new IllegalArgumentException(id + " is not a valid shape"))
.asOperationShape()
.orElseThrow(() -> new IllegalArgumentException(id + " is not an operation shape"))
@ -128,6 +126,10 @@ public final class HttpBindingIndex implements KnowledgeIndex {
.orElseThrow(() -> new IllegalArgumentException(id + " has no http binding trait"));
}
private Model getModel() {
return Objects.requireNonNull(model.get(), "The dereferenced WeakReference<Model> is null");
}
/**
* Gets the computed status code of an operation or error structure.
*
@ -138,7 +140,7 @@ public final class HttpBindingIndex implements KnowledgeIndex {
*/
public int getResponseCode(ToShapeId shapeOrId) {
ShapeId id = shapeOrId.toShapeId();
Shape shape = model.getShape(id).orElseThrow(() -> new IllegalArgumentException("Shape not found " + id));
Shape shape = getModel().getShape(id).orElseThrow(() -> new IllegalArgumentException("Shape not found " + id));
if (shape.isOperationShape()) {
return getHttpTrait(id).getCode();
@ -157,21 +159,14 @@ public final class HttpBindingIndex implements KnowledgeIndex {
*
* @param operationShapeOrId Operation to get the request bindings for.
* @return Map of unmodifiable bindings.
* @throws IllegalArgumentException if the given shape is not an operation.
*/
public Map<String, HttpBinding> getRequestBindings(ToShapeId operationShapeOrId) {
ShapeId id = operationShapeOrId.toShapeId();
validateRequestBindingShapeId(id);
return requestBindings.get(id).stream()
return requestBindings.getOrDefault(id, Collections.emptyList())
.stream()
.collect(Collectors.toMap(HttpBinding::getMemberName, Function.identity()));
}
private void validateRequestBindingShapeId(ShapeId id) {
if (!requestBindings.containsKey(id)) {
throw new IllegalArgumentException(id + " does not reference an operation with http bindings");
}
}
/**
* Gets the request bindings of an operation as a map of member name to
* the binding for a specific location type.
@ -179,12 +174,11 @@ public final class HttpBindingIndex implements KnowledgeIndex {
* @param operationShapeOrId Operation to get the request bindings for.
* @param requestLocation Location of the binding.
* @return Map of unmodifiable bindings.
* @throws IllegalArgumentException if the given shape is not an operation.
*/
public List<HttpBinding> getRequestBindings(ToShapeId operationShapeOrId, HttpBinding.Location requestLocation) {
ShapeId id = operationShapeOrId.toShapeId();
validateRequestBindingShapeId(id);
return requestBindings.get(id).stream()
return requestBindings.getOrDefault(id, Collections.emptyList())
.stream()
.filter(binding -> binding.getLocation() == requestLocation)
.collect(Collectors.toList());
}
@ -195,22 +189,14 @@ public final class HttpBindingIndex implements KnowledgeIndex {
*
* @param shapeOrId Operation or error structure shape or ID.
* @return Map of unmodifiable bindings.
* @throws IllegalArgumentException if the given shape is not an operation
* or error structure.
*/
public Map<String, HttpBinding> getResponseBindings(ToShapeId shapeOrId) {
ShapeId id = shapeOrId.toShapeId();
validateResponseBindingShapeId(id);
return responseBindings.get(id).stream()
return responseBindings.getOrDefault(id, Collections.emptyList())
.stream()
.collect(Collectors.toMap(HttpBinding::getMemberName, Function.identity()));
}
private void validateResponseBindingShapeId(ShapeId id) {
if (!responseBindings.containsKey(id)) {
throw new IllegalArgumentException(id + " does not reference an operation or error structure");
}
}
/**
* Gets the computed HTTP message response bindings for an operation
* or structure with an error trait for a specific binding type.
@ -223,8 +209,8 @@ public final class HttpBindingIndex implements KnowledgeIndex {
*/
public List<HttpBinding> getResponseBindings(ToShapeId shapeOrId, HttpBinding.Location bindingLocation) {
ShapeId id = shapeOrId.toShapeId();
validateResponseBindingShapeId(id);
return responseBindings.get(id).stream()
return responseBindings.getOrDefault(id, Collections.emptyList())
.stream()
.filter(binding -> binding.getLocation() == bindingLocation)
.collect(Collectors.toList());
}
@ -243,6 +229,7 @@ public final class HttpBindingIndex implements KnowledgeIndex {
HttpBinding.Location location,
TimestampFormatTrait.Format defaultFormat
) {
Model model = getModel();
return model.getShape(member.toShapeId())
// Use the timestampFormat trait on the member or target if present.
.flatMap(shape -> shape.getMemberTrait(model, TimestampFormatTrait.class))
@ -371,6 +358,8 @@ public final class HttpBindingIndex implements KnowledgeIndex {
String documentContentType,
String eventStreamContentType
) {
Model model = getModel();
for (HttpBinding binding : bindings) {
if (binding.getLocation() == HttpBinding.Location.DOCUMENT) {
return documentContentType;

View File

@ -53,7 +53,9 @@ public final class IdentifierBindingIndex implements KnowledgeIndex {
public IdentifierBindingIndex(Model model) {
OperationIndex operationIndex = OperationIndex.of(model);
model.shapes(ResourceShape.class).forEach(resource -> processResource(resource, operationIndex, model));
for (ResourceShape resource : model.toSet(ResourceShape.class)) {
processResource(resource, operationIndex, model);
}
}
public static IdentifierBindingIndex of(Model model) {

View File

@ -48,15 +48,23 @@ public class NullableIndex implements KnowledgeIndex {
private final Set<ShapeId> nullableShapes = new HashSet<>();
public NullableIndex(Model model) {
for (Shape shape : model.toSet()) {
if (shape.asMemberShape().isPresent()) {
if (isMemberNullable(model, shape.asMemberShape().get())) {
nullableShapes.add(shape.getId());
}
} else if (isShapeBoxed(shape)) {
for (ShapeType type : INHERENTLY_BOXED) {
model.shapes(type.getShapeClass()).forEach(shape -> nullableShapes.add(shape.getId()));
}
for (Shape shape : model.getShapesWithTrait(BoxTrait.class)) {
// Only structure members honor the box trait, so defer to the
// isMemberNullable method to determine member nullability.
if (!shape.isMemberShape()) {
nullableShapes.add(shape.getId());
}
}
for (MemberShape member : model.toSet(MemberShape.class)) {
if (isMemberNullable(model, member)) {
nullableShapes.add(member.getId());
}
}
}
public static NullableIndex of(Model model) {

View File

@ -15,12 +15,12 @@
package software.amazon.smithy.model.knowledge;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.shapes.MemberShape;
import software.amazon.smithy.model.shapes.OperationShape;
@ -43,21 +43,20 @@ public final class OperationIndex implements KnowledgeIndex {
private final Map<ShapeId, List<StructureShape>> errors = new HashMap<>();
public OperationIndex(Model model) {
model.shapes(OperationShape.class).forEach(operation -> {
for (OperationShape operation : model.toSet(OperationShape.class)) {
operation.getInput()
.flatMap(id -> getStructure(model, id))
.ifPresent(shape -> inputs.put(operation.getId(), shape));
operation.getOutput()
.flatMap(id -> getStructure(model, id))
.ifPresent(shape -> outputs.put(operation.getId(), shape));
errors.put(operation.getId(),
operation.getErrors()
.stream()
.map(e -> getStructure(model, e))
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toList()));
});
List<StructureShape> errorShapes = new ArrayList<>(operation.getErrors().size());
for (ShapeId target : operation.getErrors()) {
model.getShape(target).flatMap(Shape::asStructureShape).ifPresent(errorShapes::add);
}
errors.put(operation.getId(), errorShapes);
}
}
public static OperationIndex of(Model model) {

View File

@ -20,8 +20,6 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.shapes.MemberShape;
import software.amazon.smithy.model.shapes.OperationShape;
@ -30,10 +28,8 @@ import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.model.shapes.StructureShape;
import software.amazon.smithy.model.shapes.ToShapeId;
import software.amazon.smithy.model.traits.PaginatedTrait;
import software.amazon.smithy.model.traits.Trait;
import software.amazon.smithy.model.validation.validators.PaginatedTraitValidator;
import software.amazon.smithy.utils.ListUtils;
import software.amazon.smithy.utils.OptionalUtils;
/**
* Index of operation shapes to paginated trait information.
@ -53,15 +49,19 @@ public final class PaginatedIndex implements KnowledgeIndex {
TopDownIndex topDownIndex = TopDownIndex.of(model);
OperationIndex opIndex = OperationIndex.of(model);
model.shapes(ServiceShape.class).forEach(service -> {
for (ServiceShape service : model.toSet(ServiceShape.class)) {
PaginatedTrait serviceTrait = service.getTrait(PaginatedTrait.class).orElse(null);
Map<ShapeId, PaginationInfo> mappings = topDownIndex.getContainedOperations(service).stream()
.flatMap(operation -> Trait.flatMapStream(operation, PaginatedTrait.class))
.flatMap(p -> OptionalUtils.stream(create(
model, service, opIndex, p.left, p.right.merge(serviceTrait))))
.collect(Collectors.toMap(i -> i.getOperation().getId(), Function.identity()));
Map<ShapeId, PaginationInfo> mappings = new HashMap<>();
for (OperationShape operation : topDownIndex.getContainedOperations(service)) {
if (operation.hasTrait(PaginatedTrait.class)) {
PaginatedTrait merged = operation.expectTrait(PaginatedTrait.class).merge(serviceTrait);
create(model, service, opIndex, operation, merged).ifPresent(info -> {
mappings.put(info.getOperation().getId(), info);
});
}
}
paginationInfo.put(service.getId(), Collections.unmodifiableMap(mappings));
});
}
}
public static PaginatedIndex of(Model model) {

View File

@ -17,13 +17,12 @@ package software.amazon.smithy.model.knowledge;
import java.lang.ref.WeakReference;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.function.Function;
import java.util.stream.Collectors;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.shapes.ShapeId;
@ -45,21 +44,19 @@ import software.amazon.smithy.model.traits.Trait;
public final class ServiceIndex implements KnowledgeIndex {
private final WeakReference<Model> model;
private final Set<ShapeId> protocolTraits;
private final Set<ShapeId> authTraits;
private final Set<ShapeId> protocolTraits = new HashSet<>();
private final Set<ShapeId> authTraits = new HashSet<>();
public ServiceIndex(Model model) {
this.model = new WeakReference<>(model);
protocolTraits = model.shapes()
.filter(shape -> shape.hasTrait(ProtocolDefinitionTrait.class))
.map(Shape::getId)
.collect(Collectors.toSet());
for (Shape shape : model.getShapesWithTrait(ProtocolDefinitionTrait.class)) {
protocolTraits.add(shape.getId());
}
authTraits = model.shapes()
.filter(shape -> shape.hasTrait(AuthDefinitionTrait.class))
.map(Shape::getId)
.collect(Collectors.toSet());
for (Shape shape : model.getShapesWithTrait(AuthDefinitionTrait.class)) {
authTraits.add(shape.getId());
}
}
public static ServiceIndex of(Model model) {
@ -86,9 +83,15 @@ public final class ServiceIndex implements KnowledgeIndex {
return getModel()
.getShape(service.toShapeId())
.flatMap(Shape::asServiceShape)
.<Map<ShapeId, Trait>>map(shape -> shape.getAllTraits().values().stream()
.filter(trait -> haystack.contains(trait.toShapeId()))
.collect(Collectors.toMap(Trait::toShapeId, Function.identity(), (a, b) -> b, TreeMap::new)))
.map(shape -> {
Map<ShapeId, Trait> result = new TreeMap<>();
for (Trait trait : shape.getAllTraits().values()) {
if (haystack.contains(trait.toShapeId())) {
result.put(trait.toShapeId(), trait);
}
}
return result;
})
.orElse(Collections.emptyMap());
}

View File

@ -62,10 +62,13 @@ public final class TopDownIndex implements KnowledgeIndex {
}
};
model.shapes(ResourceShape.class).forEach(resource -> findContained(
resource.getId(), walker.walkShapes(resource, filter)));
model.shapes(ServiceShape.class).forEach(resource -> findContained(
resource.getId(), walker.walkShapes(resource, filter)));
for (ResourceShape resource : model.toSet(ResourceShape.class)) {
findContained(resource.getId(), walker.walkShapes(resource, filter));
}
for (ServiceShape service : model.toSet(ServiceShape.class)) {
findContained(service.getId(), walker.walkShapes(service, filter));
}
}
public static TopDownIndex of(Model model) {
@ -76,7 +79,7 @@ public final class TopDownIndex implements KnowledgeIndex {
Set<ResourceShape> containedResources = new TreeSet<>();
Set<OperationShape> containedOperations = new TreeSet<>();
shapes.forEach(shape -> {
for (Shape shape : shapes) {
if (!shape.getId().equals(container)) {
if (shape instanceof ResourceShape) {
containedResources.add((ResourceShape) shape);
@ -84,7 +87,7 @@ public final class TopDownIndex implements KnowledgeIndex {
containedOperations.add((OperationShape) shape);
}
}
});
}
operations.put(container, Collections.unmodifiableSet(containedOperations));
resources.put(container, Collections.unmodifiableSet(containedResources));

View File

@ -201,8 +201,8 @@ final class IdlModelParser extends SimpleParser {
private void onVersion(Node value) {
if (!value.isStringNode()) {
value.expectStringNode("The $version control statement must have a string value, but found "
+ Node.printJson(value));
value.expectStringNode(() -> "The $version control statement must have a string value, but found "
+ Node.printJson(value));
}
String parsedVersion = value.expectStringNode().getValue();

View File

@ -99,8 +99,11 @@ public interface NeighborProvider {
* @return Returns the created neighbor provider.
*/
static NeighborProvider precomputed(Model model, NeighborProvider provider) {
Map<Shape, List<Relationship>> relationships = new HashMap<>();
model.shapes().forEach(shape -> relationships.put(shape, provider.getNeighbors(shape)));
Set<Shape> shapes = model.toSet();
Map<Shape, List<Relationship>> relationships = new HashMap<>(shapes.size());
for (Shape shape : shapes) {
relationships.put(shape, provider.getNeighbors(shape));
}
return shape -> relationships.getOrDefault(shape, ListUtils.of());
}

View File

@ -17,9 +17,8 @@ package software.amazon.smithy.model.neighbor;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.shapes.EntityShape;
import software.amazon.smithy.model.shapes.ListShape;
import software.amazon.smithy.model.shapes.MapShape;
import software.amazon.smithy.model.shapes.MemberShape;
@ -65,9 +64,13 @@ final class NeighborVisitor extends ShapeVisitor.Default<List<Relationship>> imp
public List<Relationship> serviceShape(ServiceShape shape) {
List<Relationship> result = new ArrayList<>();
// Add OPERATION from service -> operation. Add BINDING from operation -> service.
shape.getOperations().forEach(id -> addBinding(result, shape, id, RelationshipType.OPERATION));
for (ShapeId operation : shape.getOperations()) {
addBinding(result, shape, operation, RelationshipType.OPERATION);
}
// Add RESOURCE from service -> resource. Add BINDING from resource -> service.
shape.getResources().forEach(id -> addBinding(result, shape, id, RelationshipType.RESOURCE));
for (ShapeId resource : shape.getResources()) {
addBinding(result, shape, resource, RelationshipType.RESOURCE);
}
return result;
}
@ -123,27 +126,40 @@ final class NeighborVisitor extends ShapeVisitor.Default<List<Relationship>> imp
// Add in all the other instance operations
shape.getOperations().forEach(id -> result.add(
relationship(shape, RelationshipType.INSTANCE_OPERATION, id)));
// Find shapes that bind this resource to it.
Stream.concat(model.shapes(ResourceShape.class), model.shapes(ServiceShape.class))
.filter(parent -> parent.getResources().contains(shape.getId()))
.forEach(parent -> addBinding(result, parent, shape.getId(), RelationshipType.RESOURCE));
// Find resource shapes that bind this resource to it.
for (ResourceShape resource : model.toSet(ResourceShape.class)) {
addServiceAndResourceBindings(result, shape, resource);
}
// Find service shapes that bind this resource to it.
for (ServiceShape service : model.toSet(ServiceShape.class)) {
addServiceAndResourceBindings(result, shape, service);
}
return result;
}
private void addServiceAndResourceBindings(List<Relationship> result, ResourceShape resource, EntityShape entity) {
if (entity.getResources().contains(resource.getId())) {
addBinding(result, entity, resource.getId(), RelationshipType.RESOURCE);
}
}
@Override
public List<Relationship> operationShape(OperationShape shape) {
List<Relationship> result = new ArrayList<>();
shape.getInput().ifPresent(id -> result.add(relationship(shape, RelationshipType.INPUT, id)));
shape.getOutput().ifPresent(id -> result.add(relationship(shape, RelationshipType.OUTPUT, id)));
result.addAll(shape.getErrors().stream()
.map(errorId -> relationship(shape, RelationshipType.ERROR, errorId))
.collect(Collectors.toList()));
for (ShapeId errorId : shape.getErrors()) {
result.add(relationship(shape, RelationshipType.ERROR, errorId));
}
return result;
}
@Override
public List<Relationship> memberShape(MemberShape shape) {
List<Relationship> result = new ArrayList<>();
List<Relationship> result = new ArrayList<>(2);
result.add(relationship(shape, RelationshipType.MEMBER_CONTAINER, shape.getContainer()));
result.add(relationship(shape, RelationshipType.MEMBER_TARGET, shape.getTarget()));
return result;
@ -161,7 +177,7 @@ final class NeighborVisitor extends ShapeVisitor.Default<List<Relationship>> imp
@Override
public List<Relationship> mapShape(MapShape shape) {
List<Relationship> result = new ArrayList<>();
List<Relationship> result = new ArrayList<>(2);
result.add(relationship(shape, RelationshipType.MAP_KEY, shape.getKey()));
result.add(relationship(shape, RelationshipType.MAP_VALUE, shape.getValue()));
return result;
@ -169,16 +185,20 @@ final class NeighborVisitor extends ShapeVisitor.Default<List<Relationship>> imp
@Override
public List<Relationship> structureShape(StructureShape shape) {
return shape.getAllMembers().values().stream()
.map(member -> Relationship.create(shape, RelationshipType.STRUCTURE_MEMBER, member))
.collect(Collectors.toList());
List<Relationship> result = new ArrayList<>();
for (MemberShape member : shape.getAllMembers().values()) {
result.add(Relationship.create(shape, RelationshipType.STRUCTURE_MEMBER, member));
}
return result;
}
@Override
public List<Relationship> unionShape(UnionShape shape) {
return shape.getAllMembers().values().stream()
.map(member -> Relationship.create(shape, RelationshipType.UNION_MEMBER, member))
.collect(Collectors.toList());
List<Relationship> result = new ArrayList<>();
for (MemberShape member : shape.getAllMembers().values()) {
result.add(Relationship.create(shape, RelationshipType.UNION_MEMBER, member));
}
return result;
}
private Relationship relationship(Shape shape, RelationshipType type, MemberShape memberShape) {

View File

@ -22,6 +22,7 @@ import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collector;
@ -78,6 +79,11 @@ public final class ArrayNode extends Node implements Iterable<Node> {
return this;
}
@Override
public ArrayNode expectArrayNode(Supplier<String> errorMessage) {
return this;
}
@Override
public Optional<ArrayNode> asArrayNode() {
return Optional.of(this);

View File

@ -16,6 +16,7 @@
package software.amazon.smithy.model.node;
import java.util.Optional;
import java.util.function.Supplier;
import software.amazon.smithy.model.SourceLocation;
/**
@ -53,6 +54,11 @@ public final class BooleanNode extends Node {
return this;
}
@Override
public BooleanNode expectBooleanNode(Supplier<String> errorMessage) {
return this;
}
@Override
public Optional<BooleanNode> asBooleanNode() {
return Optional.of(this);

View File

@ -26,6 +26,7 @@ import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.TreeMap;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import software.amazon.smithy.model.FromSourceLocation;
import software.amazon.smithy.model.SourceException;
@ -465,7 +466,7 @@ public abstract class Node implements FromSourceLocation, ToNode {
* @throws ExpectationNotMetException when the node is not an {@code ObjectNode}.
*/
public final ObjectNode expectObjectNode() {
return expectObjectNode(null);
return expectObjectNode((String) null);
}
/**
@ -480,6 +481,18 @@ public abstract class Node implements FromSourceLocation, ToNode {
throw new ExpectationNotMetException(expandMessage(message, NodeType.OBJECT.toString()), this);
}
/**
* Casts the current node to an {@code ObjectNode}, throwing
* {@link ExpectationNotMetException} when the node is the wrong type.
*
* @param message Error message supplier.
* @return Returns an object node.
* @throws ExpectationNotMetException when the node is not an {@code ObjectNode}.
*/
public ObjectNode expectObjectNode(Supplier<String> message) {
return expectObjectNode(message.get());
}
/**
* Casts the current node to an {@code ArrayNode}.
*
@ -487,7 +500,7 @@ public abstract class Node implements FromSourceLocation, ToNode {
* @throws ExpectationNotMetException when the node is not an {@code ArrayNode}.
*/
public final ArrayNode expectArrayNode() {
return expectArrayNode(null);
return expectArrayNode((String) null);
}
/**
@ -502,6 +515,18 @@ public abstract class Node implements FromSourceLocation, ToNode {
throw new ExpectationNotMetException(expandMessage(message, NodeType.ARRAY.toString()), this);
}
/**
* Casts the current node to an {@code ArrayNode}, throwing
* {@link ExpectationNotMetException} when the node is the wrong type.
*
* @param message Error message supplier.
* @return Returns an array node.
* @throws ExpectationNotMetException when the node is the wrong type.
*/
public ArrayNode expectArrayNode(Supplier<String> message) {
return expectArrayNode(message.get());
}
/**
* Casts the current node to a {@code StringNode}.
*
@ -509,7 +534,7 @@ public abstract class Node implements FromSourceLocation, ToNode {
* @throws ExpectationNotMetException when the node is the wrong type.
*/
public final StringNode expectStringNode() {
return expectStringNode(null);
return expectStringNode((String) null);
}
/**
@ -524,6 +549,18 @@ public abstract class Node implements FromSourceLocation, ToNode {
throw new ExpectationNotMetException(expandMessage(message, NodeType.STRING.toString()), this);
}
/**
* Casts the current node to a {@code StringNode}, throwing
* {@link ExpectationNotMetException} when the node is the wrong type.
*
* @param message Error message supplier.
* @return Returns a string node.
* @throws ExpectationNotMetException when the node is the wrong type.
*/
public StringNode expectStringNode(Supplier<String> message) {
return expectStringNode(message.get());
}
/**
* Casts the current node to a {@code NumberNode}.
*
@ -531,7 +568,7 @@ public abstract class Node implements FromSourceLocation, ToNode {
* @throws ExpectationNotMetException when the node is the wrong type.
*/
public final NumberNode expectNumberNode() {
return expectNumberNode(null);
return expectNumberNode((String) null);
}
/**
@ -546,6 +583,18 @@ public abstract class Node implements FromSourceLocation, ToNode {
throw new ExpectationNotMetException(expandMessage(message, NodeType.NUMBER.toString()), this);
}
/**
* Casts the current node to a {@code NumberNode}, throwing
* {@link ExpectationNotMetException} when the node is the wrong type.
*
* @param message Error message supplier.
* @return Returns a number node.
* @throws ExpectationNotMetException when the node is the wrong type.
*/
public NumberNode expectNumberNode(Supplier<String> message) {
return expectNumberNode(message.get());
}
/**
* Casts the current node to a {@code BooleanNode}.
*
@ -553,7 +602,7 @@ public abstract class Node implements FromSourceLocation, ToNode {
* @throws ExpectationNotMetException when the node is the wrong type.
*/
public final BooleanNode expectBooleanNode() {
return expectBooleanNode(null);
return expectBooleanNode((String) null);
}
/**
@ -568,6 +617,18 @@ public abstract class Node implements FromSourceLocation, ToNode {
throw new ExpectationNotMetException(expandMessage(message, NodeType.BOOLEAN.toString()), this);
}
/**
* Casts the current node to a {@code BooleanNode}, throwing
* {@link ExpectationNotMetException} when the node is the wrong type.
*
* @param message Error message supplier.
* @return Returns a boolean node.
* @throws ExpectationNotMetException when the node is the wrong type.
*/
public BooleanNode expectBooleanNode(Supplier<String> message) {
return expectBooleanNode(message.get());
}
/**
* Casts the current node to a {@code NullNode}.
*
@ -575,7 +636,7 @@ public abstract class Node implements FromSourceLocation, ToNode {
* @throws ExpectationNotMetException when the node is the wrong type.
*/
public final NullNode expectNullNode() {
return expectNullNode(null);
return expectNullNode((String) null);
}
/**
@ -590,6 +651,18 @@ public abstract class Node implements FromSourceLocation, ToNode {
throw new ExpectationNotMetException(expandMessage(message, NodeType.NULL.toString()), this);
}
/**
* Casts the current node to a {@code NullNode}, throwing
* {@link ExpectationNotMetException} when the node is the wrong type.
*
* @param message Error message supplier.
* @return Returns a null node.
* @throws ExpectationNotMetException when the node is the wrong type.
*/
public NullNode expectNullNode(Supplier<String> message) {
return expectNullNode(message.get());
}
@Override
public final SourceLocation getSourceLocation() {
return sourceLocation;

View File

@ -16,6 +16,7 @@
package software.amazon.smithy.model.node;
import java.util.Optional;
import java.util.function.Supplier;
import software.amazon.smithy.model.SourceLocation;
/**
@ -42,6 +43,11 @@ public final class NullNode extends Node {
return this;
}
@Override
public NullNode expectNullNode(Supplier<String> errorMessage) {
return this;
}
@Override
public Optional<NullNode> asNullNode() {
return Optional.of(this);

View File

@ -18,6 +18,7 @@ package software.amazon.smithy.model.node;
import java.math.BigDecimal;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Supplier;
import software.amazon.smithy.model.SourceLocation;
/**
@ -83,6 +84,11 @@ public final class NumberNode extends Node {
return this;
}
@Override
public NumberNode expectNumberNode(Supplier<String> errorMessage) {
return this;
}
@Override
public Optional<NumberNode> asNumberNode() {
return Optional.of(this);

View File

@ -26,6 +26,7 @@ import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.logging.Logger;
import java.util.stream.Collector;
import java.util.stream.Collectors;
@ -77,6 +78,11 @@ public final class ObjectNode extends Node implements ToSmithyBuilder<ObjectNode
return this;
}
@Override
public ObjectNode expectObjectNode(Supplier<String> errorMessage) {
return this;
}
@Override
public Optional<ObjectNode> asObjectNode() {
return Optional.of(this);
@ -240,7 +246,7 @@ public final class ObjectNode extends Node implements ToSmithyBuilder<ObjectNode
*/
public Optional<StringNode> getStringMember(String memberName) {
return getMember(memberName)
.map(n -> n.expectStringNode(format("Expected `%s` to be a string; found {type}", memberName)));
.map(n -> n.expectStringNode(() -> format("Expected `%s` to be a string; found {type}", memberName)));
}
/**
@ -266,7 +272,7 @@ public final class ObjectNode extends Node implements ToSmithyBuilder<ObjectNode
*/
public Optional<NumberNode> getNumberMember(String memberName) {
return getMember(memberName)
.map(n -> n.expectNumberNode(format("Expected `%s` to be a number; found {type}", memberName)));
.map(n -> n.expectNumberNode(() -> format("Expected `%s` to be a number; found {type}", memberName)));
}
/**
@ -292,7 +298,7 @@ public final class ObjectNode extends Node implements ToSmithyBuilder<ObjectNode
*/
public Optional<ArrayNode> getArrayMember(String memberName) {
return getMember(memberName)
.map(n -> n.expectArrayNode(format("Expected `%s` to be an array; found {type}", memberName)));
.map(n -> n.expectArrayNode(() -> format("Expected `%s` to be an array; found {type}", memberName)));
}
/**
@ -305,7 +311,7 @@ public final class ObjectNode extends Node implements ToSmithyBuilder<ObjectNode
*/
public Optional<ObjectNode> getObjectMember(String memberName) {
return getMember(memberName)
.map(n -> n.expectObjectNode(format("Expected `%s` to be an object; found {type}", memberName)));
.map(n -> n.expectObjectNode(() -> format("Expected `%s` to be an object; found {type}", memberName)));
}
/**
@ -318,7 +324,7 @@ public final class ObjectNode extends Node implements ToSmithyBuilder<ObjectNode
*/
public Optional<BooleanNode> getBooleanMember(String memberName) {
return getMember(memberName)
.map(n -> n.expectBooleanNode(format("Expected `%s` to be a boolean; found {type}", memberName)));
.map(n -> n.expectBooleanNode(() -> format("Expected `%s` to be a boolean; found {type}", memberName)));
}
/**
@ -356,7 +362,7 @@ public final class ObjectNode extends Node implements ToSmithyBuilder<ObjectNode
* present in the members map.
*/
public Node expectMember(String name) {
return expectMember(name, format("Missing expected member `%s`.", name));
return expectMember(name, () -> format("Missing expected member `%s`.", name));
}
/**
@ -373,6 +379,20 @@ public final class ObjectNode extends Node implements ToSmithyBuilder<ObjectNode
return getMember(name).orElseThrow(() -> new ExpectationNotMetException(errorMessage, this));
}
/**
* Gets the member with the given name, throwing
* {@link ExpectationNotMetException} when the member is not present.
*
* @param name Name of the member to get.
* @param errorMessage Error message supplier.
* @return Returns the node with the given member name.
* @throws ExpectationNotMetException when {@code memberName} is is not
* present in the members map.
*/
public Node expectMember(String name, Supplier<String> errorMessage) {
return getMember(name).orElseThrow(() -> new ExpectationNotMetException(errorMessage.get(), this));
}
/**
* Gets a member and requires it to be an array.
*
@ -382,7 +402,7 @@ public final class ObjectNode extends Node implements ToSmithyBuilder<ObjectNode
*/
public ArrayNode expectArrayMember(String name) {
return expectMember(name)
.expectArrayNode(format("Expected `%s` member to be an array, but found {type}.", name));
.expectArrayNode(() -> format("Expected `%s` member to be an array, but found {type}.", name));
}
/**
@ -394,7 +414,7 @@ public final class ObjectNode extends Node implements ToSmithyBuilder<ObjectNode
*/
public BooleanNode expectBooleanMember(String name) {
return expectMember(name)
.expectBooleanNode(format("Expected `%s` member to be a boolean, but found {type}.", name));
.expectBooleanNode(() -> format("Expected `%s` member to be a boolean, but found {type}.", name));
}
/**
@ -406,7 +426,7 @@ public final class ObjectNode extends Node implements ToSmithyBuilder<ObjectNode
*/
public NullNode expectNullMember(String name) {
return expectMember(name)
.expectNullNode(format("Expected `%s` member to be null, but found {type}.", name));
.expectNullNode(() -> format("Expected `%s` member to be null, but found {type}.", name));
}
/**
@ -418,7 +438,7 @@ public final class ObjectNode extends Node implements ToSmithyBuilder<ObjectNode
*/
public NumberNode expectNumberMember(String name) {
return expectMember(name)
.expectNumberNode(format("Expected `%s` member to be a number, but found {type}.", name));
.expectNumberNode(() -> format("Expected `%s` member to be a number, but found {type}.", name));
}
/**
@ -430,7 +450,7 @@ public final class ObjectNode extends Node implements ToSmithyBuilder<ObjectNode
*/
public ObjectNode expectObjectMember(String name) {
return expectMember(name)
.expectObjectNode(format("Expected `%s` member to be an object, but found {type}.", name));
.expectObjectNode(() -> format("Expected `%s` member to be an object, but found {type}.", name));
}
/**
@ -442,7 +462,7 @@ public final class ObjectNode extends Node implements ToSmithyBuilder<ObjectNode
*/
public StringNode expectStringMember(String name) {
return expectMember(name)
.expectStringNode(format("Expected `%s` member to be a string, but found {type}.", name));
.expectStringNode(() -> format("Expected `%s` member to be a string, but found {type}.", name));
}
/**

View File

@ -20,6 +20,7 @@ import java.util.Collection;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Supplier;
import software.amazon.smithy.model.SourceException;
import software.amazon.smithy.model.SourceLocation;
import software.amazon.smithy.model.shapes.ShapeId;
@ -84,6 +85,11 @@ public final class StringNode extends Node {
return this;
}
@Override
public StringNode expectStringNode(Supplier<String> errorMessage) {
return this;
}
@Override
public Optional<StringNode> asStringNode() {
return Optional.of(this);

View File

@ -16,30 +16,32 @@
package software.amazon.smithy.model.selector;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Function;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.model.traits.Trait;
/**
* Matches shapes with a specific attribute or that matches an attribute comparator.
*/
final class AttributeSelector implements InternalSelector {
private final BiFunction<Shape, Map<String, Set<Shape>>, AttributeValue> key;
private final List<String> path;
private final List<AttributeValue> expected;
private final AttributeComparator comparator;
private final boolean caseInsensitive;
AttributeSelector(
BiFunction<Shape, Map<String, Set<Shape>>, AttributeValue> key,
List<String> path,
List<String> expected,
AttributeComparator comparator,
boolean caseInsensitive
) {
this.key = key;
this.path = path;
this.caseInsensitive = caseInsensitive;
this.comparator = comparator;
@ -54,8 +56,28 @@ final class AttributeSelector implements InternalSelector {
}
}
static AttributeSelector existence(BiFunction<Shape, Map<String, Set<Shape>>, AttributeValue> key) {
return new AttributeSelector(key, null, null, false);
static AttributeSelector existence(List<String> path) {
return new AttributeSelector(path, null, null, false);
}
@Override
public Function<Model, Collection<? extends Shape>> optimize() {
// Optimization for loading shapes with a specific trait.
// This optimization can only be applied when there's no comparator,
// and it doesn't matter how deep into the trait the selector descends.
if (comparator == null
&& path.size() >= 2
&& path.get(0).equals("trait") // only match on traits
&& !path.get(1).startsWith("(")) { // don't match projections
return model -> {
// The trait name might be relative to the prelude, so ensure it's absolute.
String absoluteShapeId = Trait.makeAbsoluteName(path.get(1));
ShapeId trait = ShapeId.from(absoluteShapeId);
return model.getShapesWithTrait(trait);
};
} else {
return null;
}
}
@Override
@ -68,9 +90,9 @@ final class AttributeSelector implements InternalSelector {
}
private boolean matchesAttribute(Shape shape, Context stack) {
AttributeValue lhs = key.apply(shape, stack.getVars());
AttributeValue lhs = AttributeValue.shape(shape, stack.getVars()).getPath(path);
if (expected.isEmpty()) {
if (comparator == null) {
return lhs.isPresent();
}

View File

@ -15,6 +15,9 @@
package software.amazon.smithy.model.selector;
import java.util.Collection;
import java.util.function.Function;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.shapes.Shape;
/**
@ -52,6 +55,21 @@ interface InternalSelector {
*/
boolean push(Context ctx, Shape shape, Receiver next);
/**
* Returns a function that is used to optimize which shapes in a model
* need to be evaluated.
*
* <p>For example, when selecting "structure", it is far less work
* to leverage {@link Model#toSet(Class)} than it is to send every shape
* through every selector.
*
* @return Returns a function that returns null if no optimization can
* be made, or a Collection of Shapes if an optimization was made.
*/
default Function<Model, Collection<? extends Shape>> optimize() {
return null;
}
/**
* Receives shapes from an InternalSelector.
*/

View File

@ -18,7 +18,6 @@ package software.amazon.smithy.model.selector;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiFunction;
import software.amazon.smithy.model.shapes.Shape;
/**
@ -56,15 +55,11 @@ final class ScopedAttributeSelector implements InternalSelector {
AttributeValue create(AttributeValue value);
}
private final BiFunction<Shape, Map<String, Set<Shape>>, AttributeValue> keyScope;
private final List<String> path;
private final List<Assertion> assertions;
ScopedAttributeSelector(
BiFunction<Shape, Map<String, Set<Shape>>,
AttributeValue> keyScope,
List<Assertion> assertions
) {
this.keyScope = keyScope;
ScopedAttributeSelector(List<String> path, List<Assertion> assertions) {
this.path = path;
this.assertions = assertions;
}
@ -79,7 +74,7 @@ final class ScopedAttributeSelector implements InternalSelector {
private boolean matchesAssertions(Shape shape, Map<String, Set<Shape>> vars) {
// First resolve the scope of the assertions.
AttributeValue scope = keyScope.apply(shape, vars);
AttributeValue scope = AttributeValue.shape(shape, vars).getPath(path);
// If it's not present, then nothing could ever match.
if (!scope.isPresent()) {

View File

@ -19,15 +19,12 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.logging.Logger;
import software.amazon.smithy.model.loader.ParserUtils;
import software.amazon.smithy.model.neighbor.RelationshipType;
import software.amazon.smithy.model.shapes.CollectionShape;
import software.amazon.smithy.model.shapes.NumberShape;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.shapes.ShapeType;
import software.amazon.smithy.model.shapes.SimpleShape;
import software.amazon.smithy.utils.ListUtils;
@ -264,19 +261,19 @@ final class SelectorParser extends SimpleParser {
private InternalSelector parseAttribute() {
ws();
BiFunction<Shape, Map<String, Set<Shape>>, AttributeValue> keyFactory = parseAttributePath();
List<String> path = parseAttributePath();
ws();
char next = expect(']', '=', '!', '^', '$', '*', '?', '>', '<');
if (next == ']') {
return AttributeSelector.existence(keyFactory);
return AttributeSelector.existence(path);
}
AttributeComparator comparator = parseComparator(next);
List<String> values = parseAttributeValues();
boolean insensitive = parseCaseInsensitiveToken();
expect(']');
return new AttributeSelector(keyFactory, values, comparator, insensitive);
return new AttributeSelector(path, values, comparator, insensitive);
}
private boolean parseCaseInsensitiveToken() {
@ -360,11 +357,11 @@ final class SelectorParser extends SimpleParser {
// "[@" selector_key ":" selector_scoped_comparisons "]"
private InternalSelector parseScopedAttribute() {
ws();
BiFunction<Shape, Map<String, Set<Shape>>, AttributeValue> keyScope = parseAttributePath();
List<String> path = parseAttributePath();
ws();
expect(':');
ws();
return new ScopedAttributeSelector(keyScope, parseScopedAssertions());
return new ScopedAttributeSelector(path, parseScopedAssertions());
}
// selector_scoped_comparison *("&&" selector_scoped_comparison)
@ -415,12 +412,12 @@ final class SelectorParser extends SimpleParser {
}
}
private BiFunction<Shape, Map<String, Set<Shape>>, AttributeValue> parseAttributePath() {
private List<String> parseAttributePath() {
ws();
// '[@:' binds the current shape as the context.
if (peek() == ':') {
return AttributeValue::shape;
return Collections.emptyList();
}
List<String> path = new ArrayList<>();
@ -430,7 +427,7 @@ final class SelectorParser extends SimpleParser {
// It is optionally followed by "|" delimited path keys.
path.addAll(parseSelectorPath(this));
return (shape, variables) -> AttributeValue.shape(shape, variables).getPath(path);
return path;
}
private List<String> parseAttributeValues() {

View File

@ -15,6 +15,9 @@
package software.amazon.smithy.model.selector;
import java.util.Collection;
import java.util.function.Function;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.shapes.ShapeType;
@ -34,4 +37,9 @@ final class ShapeTypeSelector implements InternalSelector {
return true;
}
@Override
public Function<Model, Collection<? extends Shape>> optimize() {
return model -> model.toSet(shapeType.getShapeClass());
}
}

View File

@ -16,10 +16,12 @@
package software.amazon.smithy.model.selector;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Stream;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.knowledge.NeighborProviderIndex;
@ -31,24 +33,12 @@ import software.amazon.smithy.model.shapes.Shape;
final class WrappedSelector implements Selector {
private final String expression;
private final InternalSelector delegate;
private final Class<? extends Shape> startingShapeType;
private final Function<Model, Collection<? extends Shape>> optimizer;
WrappedSelector(String expression, List<InternalSelector> selectors) {
this.expression = expression;
if (selectors.get(0) instanceof ShapeTypeSelector) {
// If the starting selector filters based on type, then that can be
// done before sending all shapes in a model through the selector
// since querying models based on type is cached.
//
// This optimization significantly reduces the number of shapes
// that need to be sent through a selector.
startingShapeType = ((ShapeTypeSelector) selectors.get(0)).shapeType.getShapeClass();
delegate = AndSelector.of(selectors.subList(1, selectors.size()));
} else {
startingShapeType = null;
delegate = AndSelector.of(selectors);
}
delegate = AndSelector.of(selectors);
optimizer = selectors.get(0).optimize();
}
@Override
@ -122,20 +112,15 @@ final class WrappedSelector implements Selector {
private void pushShapes(Model model, InternalSelector.Receiver acceptor) {
Context context = createContext(model);
if (startingShapeType != null) {
model.shapes(startingShapeType).forEach(shape -> {
delegate.push(context.clearVars(), shape, acceptor);
});
} else {
for (Shape shape : model.toSet()) {
delegate.push(context.clearVars(), shape, acceptor);
}
Collection<? extends Shape> shapes = optimizer == null
? model.toSet()
: optimizer.apply(model);
for (Shape shape : shapes) {
delegate.push(context.clearVars(), shape, acceptor);
}
}
private Stream<? extends Shape> streamStartingShape(Model model) {
// Optimization for selectors that start with a type.
return startingShapeType != null ? model.shapes(startingShapeType) : model.shapes();
return optimizer != null ? optimizer.apply(model).stream() : model.shapes();
}
}

View File

@ -43,14 +43,14 @@ abstract class NamedMembersShape extends Shape {
// member has a valid ID that is prefixed with the shape ID.
members = MapUtils.orderedCopyOf(builder.members);
members.forEach((key, value) -> {
ShapeId expected = getId().withMember(key);
if (!value.getId().equals(expected)) {
for (MemberShape member : members.values()) {
if (!member.getId().toString().startsWith(getId().toString())) {
ShapeId expected = getId().withMember(member.getMemberName());
throw new IllegalArgumentException(String.format(
"Expected the `%s` member of `%s` to have an ID of `%s` but found `%s`",
key, getId(), expected, value.getId()));
member.getMemberName(), getId(), expected, member.getId()));
}
});
}
}
/**

View File

@ -15,12 +15,14 @@
package software.amazon.smithy.model.shapes;
import java.util.LinkedHashMap;
import java.util.Objects;
import java.util.Optional;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicInteger;
import software.amazon.smithy.model.loader.ParserUtils;
import software.amazon.smithy.model.loader.Prelude;
@ -46,7 +48,7 @@ import software.amazon.smithy.model.loader.Prelude;
public final class ShapeId implements ToShapeId, Comparable<ShapeId> {
/** LRA (least recently added) cache of parsed shape IDs. */
private static final ShapeIdFactory FACTORY = new ShapeIdFactory(1024);
private static final ShapeIdFactory FACTORY = new ShapeIdFactory(8192);
private final String namespace;
private final String name;
@ -405,12 +407,6 @@ public final class ShapeId implements ToShapeId, Comparable<ShapeId> {
* draining the prioritized queue. Draining the prioritized queue should
* only be necessary to account for misbehaving inputs.
*
* <p>The number of entries in the cache is an estimate because the result
* of calling {@link ConcurrentHashMap#size()} is an estimate. This is
* acceptable for a cache though, and any time the number of entries
* exceeds {@code maxSize}, the cache will likely get truncated on
* subsequent cache-misses.
*
* <p>While not an optimal cache, this cache is the way it is because it's
* meant to be concurrent, non-blocking, lightweight, dependency-free, and
* should improve the performance of normal workflows of using Smithy.
@ -418,12 +414,12 @@ public final class ShapeId implements ToShapeId, Comparable<ShapeId> {
* <p>Other alternatives considered were:
*
* <ol>
* <li>Pull in Caffeine. While appealing since you basically can't do
* better in terms of performance, it is a dependency that we wouldn't
* otherwise need to take, and we've drawn a pretty hard line in
* Smithy to be dependency-free. We could potentially "vendor" parts
* of Caffeine, but it's a large library that doesn't lend itself well
* towards that use case.</li>
* <li>Pull in Caffeine. While appealing, Caffeine's cache didn't
* perform noticeably better than this cache _and_ it is a dependency
* that we wouldn't otherwise need to take, and we've drawn a pretty
* hard line in Smithy to be dependency-free. We could potentially
* "vendor" parts of Caffeine, but it's a large library that doesn't
* lend itself well towards that use case.</li>
* <li>Just use an unbounded ConcurrentHashMap. While simple, this
* approach is not good for long running processes where you can't
* control the input</li>
@ -432,6 +428,8 @@ public final class ShapeId implements ToShapeId, Comparable<ShapeId> {
* entries. This approach would work for most normal use cases but
* would not work well for long running processes that potentially
* load multiple models.</li>
* <li>Use a synchronized {@link LinkedHashMap}. This approach is
* just a bit slower than the chosen approach.</li>
* </ol>
*/
private static final class ShapeIdFactory {
@ -439,6 +437,7 @@ public final class ShapeId implements ToShapeId, Comparable<ShapeId> {
private final Queue<String> priorityQueue = new ConcurrentLinkedQueue<>();
private final Queue<String> deprioritizedQueue = new ConcurrentLinkedQueue<>();
private final ConcurrentMap<String, ShapeId> map;
private final AtomicInteger size = new AtomicInteger();
ShapeIdFactory(final int maxSize) {
this.maxSize = maxSize;
@ -446,22 +445,30 @@ public final class ShapeId implements ToShapeId, Comparable<ShapeId> {
}
ShapeId create(final String key) {
ShapeId value = map.get(key);
return map.computeIfAbsent(key, id -> {
ShapeId value = buildShapeId(key);
if (value.getNamespace().equals(Prelude.NAMESPACE)) {
priorityQueue.offer(key);
} else {
deprioritizedQueue.offer(key);
}
// Evict items when the cache gets too big.
if (size.incrementAndGet() > maxSize) {
// Remove an element from the deprioritized queue if it isn't empty,
// and if it is, then try to remove an element from the priority queue.
String item = deprioritizedQueue.poll();
if (item == null) {
item = priorityQueue.poll();
}
size.decrementAndGet();
map.remove(item);
}
// Return the value if it exists in the cache.
if (value != null) {
return value;
}
value = buildShapeId(key);
// Only update the queue if the value is new to the map.
if (map.put(key, value) == null) {
offer(key, value);
evict();
}
return value;
});
}
private ShapeId buildShapeId(String absoluteShapeId) {
@ -487,36 +494,5 @@ public final class ShapeId implements ToShapeId, Comparable<ShapeId> {
validateParts(absoluteShapeId, namespace, name, memberName);
return new ShapeId(absoluteShapeId, namespace, name, memberName);
}
// Enqueue a key into the appropriate queue, putting more often used
// IDs in the smithy.api# namespace into a priority queue.
private void offer(String key, ShapeId value) {
if (value.getNamespace().equals(Prelude.NAMESPACE)) {
priorityQueue.offer(key);
} else {
deprioritizedQueue.offer(key);
}
}
// Purge least recently added items from the cache.
private void evict() {
// Note: the result of map.size is an estimate, but that's ok for a cache.
while (map.size() > maxSize) {
String item = poll();
// Stop trying to remove items from the queue if neither
// the priority queue or deprioritized queues have entries,
// or if attempting to remove an item from the map fails.
if (item == null || map.remove(item) == null) {
break;
}
}
}
// Remove an element from the deprioritized queue if it isn't empty,
// and if it is, then try to remove an element from the priority queue.
private String poll() {
String item = deprioritizedQueue.poll();
return item != null ? item : priorityQueue.poll();
}
}
}

View File

@ -87,7 +87,9 @@ public interface Trait extends FromSourceLocation, ToNode, ToShapeId {
* @return Returns the idiomatic trait name.
*/
static String getIdiomaticTraitName(String traitName) {
return traitName.replace("smithy.api#", "");
return traitName.startsWith("smithy.api#")
? traitName.substring("smithy.api#".length())
: traitName;
}
/**

View File

@ -71,38 +71,80 @@ public final class NodeValidationVisitor implements ShapeVisitor<List<Validation
private static final List<NodeValidatorPlugin> BUILTIN = NodeValidatorPlugin.getBuiltins();
private final String eventId;
private final Node value;
private final Model model;
private final String context;
private final ShapeId eventShapeId;
private final TimestampValidationStrategy timestampValidationStrategy;
private final boolean allowBoxedNull;
private String eventId;
private Node value;
private ShapeId eventShapeId;
private String startingContext;
private NodeValidatorPlugin.Context validationContext;
private NodeValidationVisitor(Builder builder) {
this.value = SmithyBuilder.requiredState("value", builder.value);
this.model = SmithyBuilder.requiredState("model", builder.model);
this.context = builder.context;
this.eventId = builder.eventId;
this.eventShapeId = builder.eventShapeId;
this.validationContext = new NodeValidatorPlugin.Context(model);
this.timestampValidationStrategy = builder.timestampValidationStrategy;
this.allowBoxedNull = builder.allowBoxedNull;
setValue(SmithyBuilder.requiredState("value", builder.value));
setStartingContext(builder.contextText);
setValue(builder.value);
setEventShapeId(builder.eventShapeId);
setEventId(builder.eventId);
}
public static Builder builder() {
return new Builder();
}
private NodeValidationVisitor withNode(String segment, Node node) {
/**
* Changes the Node value the the visitor will evaluate.
*
* @param value Value to set.
*/
public void setValue(Node value) {
this.value = Objects.requireNonNull(value);
}
/**
* Changes the shape ID that emitted events are associated with.
*
* @param eventShapeId Shape ID to set.
*/
public void setEventShapeId(ShapeId eventShapeId) {
this.eventShapeId = eventShapeId;
}
/**
* Changes the starting context of the messages emitted by events.
*
* @param startingContext Starting context message to set.
*/
public void setStartingContext(String startingContext) {
this.startingContext = startingContext == null ? "" : startingContext;
}
/**
* Changes the event ID emitted for events created by this validator.
*
* @param eventId Event ID to set.
*/
public void setEventId(String eventId) {
this.eventId = eventId == null ? Validator.MODEL_ERROR : eventId;
}
private NodeValidationVisitor traverse(String segment, Node node) {
Builder builder = builder();
builder.eventShapeId(eventShapeId);
builder.eventId(eventId);
builder.value(node);
builder.model(model);
builder.startingContext(context.isEmpty() ? segment : (context + "." + segment));
builder.startingContext(startingContext.isEmpty() ? segment : (startingContext + "." + segment));
builder.timestampValidationStrategy(timestampValidationStrategy);
builder.allowBoxedNull(allowBoxedNull);
return new NodeValidationVisitor(builder);
NodeValidationVisitor visitor = new NodeValidationVisitor(builder);
// Use the same validation context.
visitor.validationContext = this.validationContext;
return visitor;
}
@Override
@ -222,7 +264,7 @@ public final class NodeValidationVisitor implements ShapeVisitor<List<Validation
List<ValidationEvent> events = applyPlugins(shape);
// Each element creates a context with a numeric index (e.g., "foo.0.baz", "foo.1.baz", etc.).
for (int i = 0; i < array.getElements().size(); i++) {
events.addAll(member.accept(withNode(String.valueOf(i), array.getElements().get(i))));
events.addAll(member.accept(traverse(String.valueOf(i), array.getElements().get(i))));
}
return events;
})
@ -236,8 +278,8 @@ public final class NodeValidationVisitor implements ShapeVisitor<List<Validation
List<ValidationEvent> events = applyPlugins(shape);
for (Map.Entry<StringNode, Node> entry : object.getMembers().entrySet()) {
String key = entry.getKey().getValue();
events.addAll(withNode(key + " (map-key)", entry.getKey()).memberShape(shape.getKey()));
events.addAll(withNode(key, entry.getValue()).memberShape(shape.getValue()));
events.addAll(traverse(key + " (map-key)", entry.getKey()).memberShape(shape.getKey()));
events.addAll(traverse(key, entry.getValue()).memberShape(shape.getValue()));
}
return events;
})
@ -250,26 +292,29 @@ public final class NodeValidationVisitor implements ShapeVisitor<List<Validation
.map(object -> {
List<ValidationEvent> events = applyPlugins(shape);
Map<String, MemberShape> members = shape.getAllMembers();
object.getMembers().forEach((keyNode, value) -> {
String key = keyNode.getValue();
if (!members.containsKey(key)) {
for (Map.Entry<String, Node> entry : object.getStringMap().entrySet()) {
String entryKey = entry.getKey();
Node entryValue = entry.getValue();
if (!members.containsKey(entryKey)) {
String message = String.format(
"Invalid structure member `%s` found for `%s`", key, shape.getId());
"Invalid structure member `%s` found for `%s`", entryKey, shape.getId());
events.add(event(message, Severity.WARNING));
} else {
events.addAll(withNode(key, value).memberShape(members.get(key)));
events.addAll(traverse(entryKey, entryValue).memberShape(members.get(entryKey)));
}
});
}
members.forEach((memberName, member) -> {
for (MemberShape member : members.values()) {
if (member.isRequired()
&& !object.getMember(memberName).isPresent()
&& !object.getMember(member.getMemberName()).isPresent()
// Ignore missing required primitive members because they have a default value.
&& !isMemberPrimitive(member)) {
events.add(event(String.format(
"Missing required structure member `%s` for `%s`", memberName, shape.getId())));
"Missing required structure member `%s` for `%s`",
member.getMemberName(), shape.getId())));
}
});
}
return events;
})
.orElseGet(() -> invalidShape(shape, NodeType.OBJECT));
@ -288,15 +333,16 @@ public final class NodeValidationVisitor implements ShapeVisitor<List<Validation
events.add(event("union values can contain a value for only a single member"));
} else {
Map<String, MemberShape> members = shape.getAllMembers();
object.getMembers().forEach((keyNode, value) -> {
String key = keyNode.getValue();
if (!members.containsKey(key)) {
for (Map.Entry<String, Node> entry : object.getStringMap().entrySet()) {
String entryKey = entry.getKey();
Node entryValue = entry.getValue();
if (!members.containsKey(entryKey)) {
events.add(event(String.format(
"Invalid union member `%s` found for `%s`", key, shape.getId())));
"Invalid union member `%s` found for `%s`", entryKey, shape.getId())));
} else {
events.addAll(withNode(key, value).memberShape(members.get(key)));
events.addAll(traverse(entryKey, entryValue).memberShape(members.get(entryKey)));
}
});
}
}
return events;
})
@ -364,18 +410,18 @@ public final class NodeValidationVisitor implements ShapeVisitor<List<Validation
.severity(severity)
.sourceLocation(sourceLocation)
.shapeId(eventShapeId)
.message(context.isEmpty() ? message : context + ": " + message)
.message(startingContext.isEmpty() ? message : startingContext + ": " + message)
.build();
}
private List<ValidationEvent> applyPlugins(Shape shape) {
List<ValidationEvent> events = new ArrayList<>();
timestampValidationStrategy.apply(shape, value, model, (location, message) -> {
timestampValidationStrategy.apply(shape, value, validationContext, (location, message) -> {
events.add(event(message, Severity.ERROR, location.getSourceLocation()));
});
for (NodeValidatorPlugin plugin : BUILTIN) {
plugin.apply(shape, value, model, (location, message) -> {
plugin.apply(shape, value, validationContext, (location, message) -> {
events.add(event(message, Severity.ERROR, location.getSourceLocation()));
});
}
@ -387,8 +433,8 @@ public final class NodeValidationVisitor implements ShapeVisitor<List<Validation
* Builds a {@link NodeValidationVisitor}.
*/
public static final class Builder implements SmithyBuilder<NodeValidationVisitor> {
private String eventId = Validator.MODEL_ERROR;
private String context = "";
private String eventId;
private String contextText;
private ShapeId eventShapeId;
private Node value;
private Model model;
@ -435,11 +481,11 @@ public final class NodeValidationVisitor implements ShapeVisitor<List<Validation
* Sets an optional starting context of the validator that is prepended
* to each emitted validation event message.
*
* @param context Starting event message content.
* @param contextText Starting event message content.
* @return Returns the builder.
*/
public Builder startingContext(String context) {
this.context = Objects.requireNonNull(context);
public Builder startingContext(String contextText) {
this.contextText = Objects.requireNonNull(contextText);
return this;
}

View File

@ -15,10 +15,9 @@
package software.amazon.smithy.model.validation.node;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.function.BiConsumer;
import software.amazon.smithy.model.FromSourceLocation;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.node.StringNode;
import software.amazon.smithy.model.shapes.BlobShape;
import software.amazon.smithy.model.shapes.Shape;
@ -40,11 +39,11 @@ final class BlobLengthPlugin extends MemberAndShapeTraitPlugin<BlobShape, String
Shape shape,
LengthTrait trait,
StringNode node,
Model model,
Context context,
BiConsumer<FromSourceLocation, String> emitter
) {
String value = node.getValue();
int size = value.getBytes(Charset.forName("UTF-8")).length;
int size = value.getBytes(StandardCharsets.UTF_8).length;
trait.getMin().ifPresent(min -> {
if (size < min) {
@ -54,7 +53,7 @@ final class BlobLengthPlugin extends MemberAndShapeTraitPlugin<BlobShape, String
});
trait.getMax().ifPresent(max -> {
if (value.getBytes(Charset.forName("UTF-8")).length > max) {
if (value.getBytes(StandardCharsets.UTF_8).length > max) {
emitter.accept(node, "Value provided for `" + shape.getId() + "` must have no more than "
+ max + " bytes, but the provided value has " + size + " bytes");
}

View File

@ -17,7 +17,6 @@ package software.amazon.smithy.model.validation.node;
import java.util.function.BiConsumer;
import software.amazon.smithy.model.FromSourceLocation;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.node.ArrayNode;
import software.amazon.smithy.model.shapes.CollectionShape;
import software.amazon.smithy.model.shapes.Shape;
@ -40,7 +39,7 @@ final class CollectionLengthPlugin extends MemberAndShapeTraitPlugin<CollectionS
Shape shape,
LengthTrait trait,
ArrayNode node,
Model model,
Context context,
BiConsumer<FromSourceLocation, String> emitter
) {
trait.getMin().ifPresent(min -> {

View File

@ -17,7 +17,6 @@ package software.amazon.smithy.model.validation.node;
import java.util.function.BiConsumer;
import software.amazon.smithy.model.FromSourceLocation;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.node.Node;
import software.amazon.smithy.model.shapes.Shape;
@ -32,11 +31,11 @@ abstract class FilteredPlugin<S extends Shape, N extends Node> implements NodeVa
@Override
@SuppressWarnings("unchecked")
public final void apply(Shape shape, Node value, Model model, BiConsumer<FromSourceLocation, String> emitter) {
public final void apply(Shape shape, Node value, Context context, BiConsumer<FromSourceLocation, String> emitter) {
if (shapeClass.isInstance(shape) && nodeClass.isInstance(value)) {
check((S) shape, (N) value, model, emitter);
check((S) shape, (N) value, context, emitter);
}
}
abstract void check(S shape, N node, Model model, BiConsumer<FromSourceLocation, String> emitter);
abstract void check(S shape, N node, Context context, BiConsumer<FromSourceLocation, String> emitter);
}

View File

@ -17,7 +17,6 @@ package software.amazon.smithy.model.validation.node;
import java.util.function.BiConsumer;
import software.amazon.smithy.model.FromSourceLocation;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.SourceException;
import software.amazon.smithy.model.node.StringNode;
import software.amazon.smithy.model.shapes.Shape;
@ -43,32 +42,36 @@ final class IdRefPlugin extends MemberAndShapeTraitPlugin<StringShape, StringNod
Shape shape,
IdRefTrait trait,
StringNode node,
Model model,
Context context,
BiConsumer<FromSourceLocation, String> emitter
) {
try {
ShapeId target = node.expectShapeId();
Shape resolved = model.getShape(target).orElse(null);
Shape resolved = context.model().getShape(target).orElse(null);
if (resolved == null) {
if (trait.failWhenMissing()) {
failWhenNoMatch(node, trait, emitter, String.format(
"Shape ID `%s` was not found in the model", target));
}
} else if (!matchesSelector(trait, resolved.getId(), model)) {
failWhenNoMatch(node, trait, emitter, String.format(
"Shape ID `%s` does not match selector `%s`",
resolved.getId(), trait.getSelector()));
} else {
if (!matchesSelector(trait, resolved, context)) {
failWhenNoMatch(node, trait, emitter, String.format(
"Shape ID `%s` does not match selector `%s`",
resolved.getId(), trait.getSelector()));
}
}
} catch (SourceException e) {
emitter.accept(node, e.getMessageWithoutLocation());
}
}
private boolean matchesSelector(IdRefTrait trait, ShapeId needle, Model haystack) {
return trait.getSelector().select(haystack).stream()
.map(Shape::getId)
.anyMatch(shapeId -> shapeId.equals(needle));
private boolean matchesSelector(IdRefTrait trait, Shape needle, Context context) {
if (trait.getSelector().toString().equals("*")) {
return true;
} else {
return context.select(trait.getSelector()).contains(needle);
}
}
private void failWhenNoMatch(

View File

@ -17,7 +17,6 @@ package software.amazon.smithy.model.validation.node;
import java.util.function.BiConsumer;
import software.amazon.smithy.model.FromSourceLocation;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.node.ObjectNode;
import software.amazon.smithy.model.shapes.MapShape;
import software.amazon.smithy.model.shapes.Shape;
@ -39,7 +38,7 @@ final class MapLengthPlugin extends MemberAndShapeTraitPlugin<MapShape, ObjectNo
Shape shape,
LengthTrait trait,
ObjectNode node,
Model model,
Context context,
BiConsumer<FromSourceLocation, String> emitter
) {
trait.getMin().ifPresent(min -> {

View File

@ -37,11 +37,11 @@ abstract class MemberAndShapeTraitPlugin<S extends Shape, N extends Node, T exte
@Override
@SuppressWarnings("unchecked")
public final void apply(Shape shape, Node value, Model model, BiConsumer<FromSourceLocation, String> emitter) {
public final void apply(Shape shape, Node value, Context context, BiConsumer<FromSourceLocation, String> emitter) {
if (nodeClass.isInstance(value)
&& shape.getTrait(traitClass).isPresent()
&& isMatchingShape(shape, model)) {
check(shape, shape.getTrait(traitClass).get(), (N) value, model, emitter);
&& isMatchingShape(shape, context.model())) {
check(shape, shape.getTrait(traitClass).get(), (N) value, context, emitter);
}
}
@ -62,6 +62,6 @@ abstract class MemberAndShapeTraitPlugin<S extends Shape, N extends Node, T exte
Shape shape,
T trait,
N value,
Model model,
Context context,
BiConsumer<FromSourceLocation, String> emitter);
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
* Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
@ -15,11 +15,15 @@
package software.amazon.smithy.model.validation.node;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import software.amazon.smithy.model.FromSourceLocation;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.node.Node;
import software.amazon.smithy.model.selector.Selector;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.utils.ListUtils;
import software.amazon.smithy.utils.SmithyInternalApi;
@ -37,10 +41,10 @@ public interface NodeValidatorPlugin {
*
* @param shape Shape being checked.
* @param value Value being evaluated.
* @param model Model to traverse.
* @param context Evaluation context.
* @param emitter Consumer to notify of validation event locations and messages.
*/
void apply(Shape shape, Node value, Model model, BiConsumer<FromSourceLocation, String> emitter);
void apply(Shape shape, Node value, Context context, BiConsumer<FromSourceLocation, String> emitter);
/**
* @return Gets the built-in Node validation plugins.
@ -56,4 +60,52 @@ public interface NodeValidatorPlugin {
new StringEnumPlugin(),
new StringLengthPlugin());
}
/**
* Validation context to pass to each NodeValidatorPlugin.
*/
@SmithyInternalApi
final class Context {
private final Model model;
// Use an LRU cache to ensure the Selector cache doesn't grow too large
// when given bad inputs.
private final Map<Selector, Set<Shape>> selectorResults = new LinkedHashMap<Selector, Set<Shape>>(
50 + 1, .75F, true) {
@Override
public boolean removeEldestEntry(Map.Entry<Selector, Set<Shape>> eldest) {
return size() > 50;
}
};
/**
* @param model Model being evaluated.
*/
public Context(Model model) {
this.model = model;
}
/**
* Get the model being evaluated.
*
* @return Returns the model.
*/
public Model model() {
return model;
}
/**
* Select and memoize shapes from the model using a Selector.
*
* <p>The cache used by this method is not thread-safe, though that's
* fine because NodeValidatorPlugins using the same Context all run
* within the same thread.
*
* @param selector Selector to evaluate.
* @return Returns the matching shapes.
*/
public Set<Shape> select(Selector selector) {
return selectorResults.computeIfAbsent(selector, s -> s.select(model));
}
}
}

View File

@ -17,7 +17,6 @@ package software.amazon.smithy.model.validation.node;
import java.util.function.BiConsumer;
import software.amazon.smithy.model.FromSourceLocation;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.node.StringNode;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.shapes.StringShape;
@ -37,7 +36,7 @@ final class PatternTraitPlugin extends MemberAndShapeTraitPlugin<StringShape, St
Shape shape,
PatternTrait trait,
StringNode node,
Model model,
Context context,
BiConsumer<FromSourceLocation, String> emitter
) {
if (!trait.getPattern().matcher(node.getValue()).find()) {

View File

@ -18,7 +18,6 @@ package software.amazon.smithy.model.validation.node;
import java.math.BigDecimal;
import java.util.function.BiConsumer;
import software.amazon.smithy.model.FromSourceLocation;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.node.NumberNode;
import software.amazon.smithy.model.shapes.NumberShape;
import software.amazon.smithy.model.shapes.Shape;
@ -38,7 +37,7 @@ final class RangeTraitPlugin extends MemberAndShapeTraitPlugin<NumberShape, Numb
Shape shape,
RangeTrait trait,
NumberNode node,
Model model,
Context context,
BiConsumer<FromSourceLocation, String> emitter
) {
Number number = node.getValue();

View File

@ -18,7 +18,6 @@ package software.amazon.smithy.model.validation.node;
import java.util.List;
import java.util.function.BiConsumer;
import software.amazon.smithy.model.FromSourceLocation;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.node.StringNode;
import software.amazon.smithy.model.shapes.StringShape;
import software.amazon.smithy.model.traits.EnumTrait;
@ -37,7 +36,7 @@ final class StringEnumPlugin extends FilteredPlugin<StringShape, StringNode> {
protected void check(
StringShape shape,
StringNode node,
Model model,
Context context,
BiConsumer<FromSourceLocation, String> emitter
) {
shape.getTrait(EnumTrait.class).ifPresent(trait -> {

View File

@ -17,7 +17,6 @@ package software.amazon.smithy.model.validation.node;
import java.util.function.BiConsumer;
import software.amazon.smithy.model.FromSourceLocation;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.node.StringNode;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.shapes.StringShape;
@ -37,7 +36,7 @@ final class StringLengthPlugin extends MemberAndShapeTraitPlugin<StringShape, St
Shape shape,
LengthTrait trait,
StringNode node,
Model model,
Context context,
BiConsumer<FromSourceLocation, String> emitter
) {
trait.getMin().ifPresent(min -> {

View File

@ -20,7 +20,6 @@ import java.time.format.DateTimeParseException;
import java.util.function.BiConsumer;
import java.util.logging.Logger;
import software.amazon.smithy.model.FromSourceLocation;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.node.Node;
import software.amazon.smithy.model.shapes.MemberShape;
import software.amazon.smithy.model.shapes.Shape;
@ -39,7 +38,7 @@ final class TimestampFormatPlugin implements NodeValidatorPlugin {
private static final Logger LOGGER = Logger.getLogger(TimestampFormatPlugin.class.getName());
@Override
public void apply(Shape shape, Node value, Model model, BiConsumer<FromSourceLocation, String> emitter) {
public void apply(Shape shape, Node value, Context context, BiConsumer<FromSourceLocation, String> emitter) {
if (shape instanceof TimestampShape) {
validate(shape, shape.getTrait(TimestampFormatTrait.class).orElse(null), value, emitter);
} else if (shape instanceof MemberShape && shape.getTrait(TimestampFormatTrait.class).isPresent()) {

View File

@ -35,8 +35,8 @@ public enum TimestampValidationStrategy implements NodeValidatorPlugin {
*/
FORMAT {
@Override
public void apply(Shape shape, Node value, Model model, BiConsumer<FromSourceLocation, String> emitter) {
new TimestampFormatPlugin().apply(shape, value, model, emitter);
public void apply(Shape shape, Node value, Context context, BiConsumer<FromSourceLocation, String> emitter) {
new TimestampFormatPlugin().apply(shape, value, context, emitter);
}
},
@ -46,8 +46,8 @@ public enum TimestampValidationStrategy implements NodeValidatorPlugin {
*/
EPOCH_SECONDS {
@Override
public void apply(Shape shape, Node value, Model model, BiConsumer<FromSourceLocation, String> emitter) {
if (isTimestampMember(model, shape) && !value.isNumberNode()) {
public void apply(Shape shape, Node value, Context context, BiConsumer<FromSourceLocation, String> emitter) {
if (isTimestampMember(context.model(), shape) && !value.isNumberNode()) {
emitter.accept(shape, "Invalid " + value.getType() + " value provided for timestamp, `"
+ shape.getId() + "`. Expected a number that contains epoch "
+ "seconds with optional millisecond precision");

View File

@ -16,9 +16,9 @@
package software.amazon.smithy.model.validation.validators;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import software.amazon.smithy.model.Model;
@ -45,54 +45,51 @@ public final class TraitTargetValidator extends AbstractValidator {
@Override
public List<ValidationEvent> validate(Model model) {
List<ValidationEvent> events = new ArrayList<>();
Collection<SelectorTest> tests = createTests(model);
Map<Selector, Set<Shape>> selectorCache = new HashMap<>();
for (SelectorTest test : tests) {
// Find the shapes that this trait can be applied to.
Set<Shape> matches = test.selector.select(model);
// Only validate trait targets for traits that are actually used.
for (ShapeId traitId : model.getAppliedTraits()) {
model.getTraitDefinition(traitId).ifPresent(definition -> {
// Find all shapes that have the used trait applied to it.
Set<Shape> shapes = model.getShapesWithTrait(traitId);
validateTraitTargets(model, events, traitId, definition.getSelector(), shapes, selectorCache);
});
}
// Remove the allowed locations from the real locations, leaving only
// the shapes in the set that are invalid.
test.appliedTo.removeAll(matches);
selectorCache.clear();
return events;
}
for (Shape shape : test.appliedTo) {
private void validateTraitTargets(
Model model,
List<ValidationEvent> events,
ShapeId trait,
Selector selector,
Set<Shape> appliedTo,
Map<Selector, Set<Shape>> selectorCache
) {
// Short circuit for shapes that match everything.
if (selector.toString().equals("*")) {
return;
}
// Find the shapes that this trait can be applied to.
// Many selectors are identical to other selectors, so use a cache
// to reduce the number of times the entire model is traversed.
Set<Shape> matches = selectorCache.computeIfAbsent(selector, s -> s.select(model));
for (Shape shape : appliedTo) {
// Emit events when a shape is applied to something that didn't match the selector.
if (!matches.contains(shape)) {
// Strip out newlines with successive spaces.
String sanitized = SANITIZE.matcher(test.selector.toString()).replaceAll(" ");
events.add(error(shape, shape.findTrait(test.trait).get(), String.format(
String sanitized = SANITIZE.matcher(selector.toString()).replaceAll(" ");
events.add(error(shape, shape.findTrait(trait).get(), String.format(
"Trait `%s` cannot be applied to `%s`. This trait may only be applied "
+ "to shapes that match the following selector: %s",
Trait.getIdiomaticTraitName(test.trait.toShapeId()),
Trait.getIdiomaticTraitName(trait.toShapeId()),
shape.getId(),
sanitized)));
}
}
return events;
}
private Collection<SelectorTest> createTests(Model model) {
List<SelectorTest> tests = new ArrayList<>(model.getAppliedTraits().size());
for (ShapeId traitId : model.getAppliedTraits()) {
// This set is mutated later, so make a copy.
Set<Shape> shapes = new HashSet<>(model.getShapesWithTrait(traitId));
model.getTraitDefinition(traitId).ifPresent(definition -> {
tests.add(new SelectorTest(traitId, definition.getSelector(), shapes));
});
}
return tests;
}
private static final class SelectorTest {
final ShapeId trait;
final Selector selector;
final Set<Shape> appliedTo;
SelectorTest(ShapeId trait, Selector selector, Set<Shape> appliedTo) {
this.trait = trait;
this.selector = selector;
this.appliedTo = appliedTo;
}
}
}

View File

@ -19,6 +19,7 @@ import java.util.ArrayList;
import java.util.List;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.loader.Prelude;
import software.amazon.smithy.model.node.Node;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.model.traits.Trait;
@ -37,11 +38,19 @@ public final class TraitValueValidator implements Validator {
@Override
public List<ValidationEvent> validate(Model model) {
// Create a reusable validation visitor so that the
// selector cache is shared for each trait.
NodeValidationVisitor validator = NodeValidationVisitor.builder()
.eventId(NAME)
.model(model)
.value(Node.nullNode())
.build();
List<ValidationEvent> events = new ArrayList<>();
boolean validatePrelude = model.getMetadataProperty(VALIDATE_PRELUDE).isPresent();
for (Shape shape : model.toSet()) {
for (Trait trait : shape.getAllTraits().values()) {
events.addAll(validateTrait(model, shape, trait, validatePrelude));
events.addAll(validateTrait(model, validator, shape, trait, validatePrelude));
}
}
@ -50,6 +59,7 @@ public final class TraitValueValidator implements Validator {
private List<ValidationEvent> validateTrait(
Model model,
NodeValidationVisitor validator,
Shape targetShape,
Trait trait,
boolean validatePrelude
@ -68,15 +78,9 @@ public final class TraitValueValidator implements Validator {
return ListUtils.of();
}
Shape schema = model.getShape(shape).get();
NodeValidationVisitor cases = NodeValidationVisitor.builder()
.model(model)
.value(trait.toNode())
.eventShapeId(targetShape.getId())
.eventId(NAME)
.startingContext("Error validating trait `" + Trait.getIdiomaticTraitName(trait) + "`")
.build();
return schema.accept(cases);
validator.setValue(trait.toNode());
validator.setEventShapeId(targetShape.getId());
validator.setStartingContext("Error validating trait `" + Trait.getIdiomaticTraitName(trait) + "`");
return model.getShape(shape).get().accept(validator);
}
}

View File

@ -33,6 +33,8 @@ import java.util.Set;
import java.util.stream.Collectors;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import software.amazon.smithy.model.knowledge.HttpBindingIndex;
import software.amazon.smithy.model.knowledge.KnowledgeIndex;
import software.amazon.smithy.model.knowledge.TopDownIndex;
import software.amazon.smithy.model.node.ExpectationNotMetException;
import software.amazon.smithy.model.node.Node;
@ -242,4 +244,34 @@ public class ModelTest {
Model model = Model.builder().build();
model.getKnowledge(TopDownIndex.class);
}
@Test
public void doesNotDeadlockWhenReenteringBlackboard() {
Model model = Model.builder().build();
model.getKnowledge(FooFooFoo.class, FooFooFoo::new);
}
private static final class FooFooFoo implements KnowledgeIndex {
public FooFooFoo(Model model) {
model.getKnowledge(Baz.class, Baz::new);
}
}
private static final class Baz implements KnowledgeIndex {
public Baz(Model model) {
model.getKnowledge(Bar.class, Bar::new);
}
}
private static final class Bar implements KnowledgeIndex {
public Bar(Model model) {
model.getKnowledge(Qux.class, Qux::new);
}
}
private static final class Qux implements KnowledgeIndex {
public Qux(Model model) {
HttpBindingIndex.of(model);
}
}
}

View File

@ -56,13 +56,6 @@ public class HttpBindingIndexTest {
.assemble()
.unwrap();
@Test
public void throwsWhenShapeIsInvalid() {
Assertions.assertThrows(IllegalArgumentException.class, () -> {
HttpBindingIndex.of(model).getRequestBindings(ShapeId.from("ns.foo#Missing"));
});
}
@Test
public void providesResponseCode() {
HttpBindingIndex index = HttpBindingIndex.of(model);

View File

@ -17,18 +17,15 @@ package software.amazon.smithy.model.shapes;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasKey;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.lessThan;
import static org.hamcrest.Matchers.not;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.lang.reflect.Field;
import java.util.Collection;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@ -251,23 +248,4 @@ public class ShapeTest {
assertEquals(shapeA, shapeB);
}
@Test
public void drainsFromPriorityQueueWhenGivenBadInput() throws Exception {
for (int i = 0; i < 1024; i++) {
ShapeId.from("smithy.api#Foo" + i);
}
Field factoryField = ShapeId.class.getDeclaredField("FACTORY");
factoryField.setAccessible(true);
Object factory = factoryField.get(ShapeId.class);
Field map = factory.getClass().getDeclaredField("map");
map.setAccessible(true);
Object cache = map.get(factory);
// The size of the map is an estimate, so check if the value is +-... 25? I dunno.
int size = (int) cache.getClass().getMethod("size").invoke(cache);
assertThat(size, greaterThan(1024 - 25));
assertThat(size, lessThan(1024 + 25));
}
}