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:
parent
7ec2b66b63
commit
31b0ada4d8
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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]
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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 -> {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 -> {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 -> {
|
||||
|
|
|
@ -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 -> {
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue