com.indeed.proctor.common.SpecificationGenerator Maven / Gradle / Ivy
package com.indeed.proctor.common;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
import com.google.common.collect.Ordering;
import com.google.common.primitives.Ints;
import com.indeed.proctor.common.model.Payload;
import com.indeed.proctor.common.model.TestBucket;
import com.indeed.proctor.common.model.TestDefinition;
import javax.annotation.Nonnull;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
/**
* infers a specification from an actual testDefinition. Buckets need to have compatible payloads
* because code generation generates typesafe payload accessors
*/
public class SpecificationGenerator {
/**
* Generates a usable test specification for a given test definition Uses the bucket with
* smallest value as the fallback value
*
* @param testDefinition a {@link TestDefinition}
* @return a {@link TestSpecification} which corresponding to given test definition.
*/
// non-static for easy mocking in tests
@Nonnull
public TestSpecification generateSpecification(@Nonnull final TestDefinition testDefinition) {
final TestSpecification testSpecification = new TestSpecification();
// Sort buckets by value ascending
final List testDefinitionBuckets =
Ordering.from(
new Comparator() {
@Override
public int compare(final TestBucket lhs, final TestBucket rhs) {
return Ints.compare(lhs.getValue(), rhs.getValue());
}
})
.immutableSortedCopy(testDefinition.getBuckets());
int fallbackValue = -1;
if (!testDefinitionBuckets.isEmpty()) {
// buckets are sorted, choose smallest value as the fallback value
fallbackValue = testDefinitionBuckets.get(0).getValue();
final Optional specOpt =
generatePayloadSpecification(
testDefinitionBuckets.stream()
.map(TestBucket::getPayload)
.filter(Objects::nonNull)
.collect(Collectors.toList()));
specOpt.ifPresent(testSpecification::setPayload);
}
final Map buckets = Maps.newLinkedHashMap();
for (final TestBucket bucket : testDefinitionBuckets) {
buckets.put(bucket.getName(), bucket.getValue());
}
testSpecification.setBuckets(buckets);
testSpecification.setDescription(testDefinition.getDescription());
testSpecification.setFallbackValue(fallbackValue);
return testSpecification;
}
/** If list of payloads contains payloads, unifies all to infer a payload specification */
@VisibleForTesting
@Nonnull
static Optional generatePayloadSpecification(
final List payloads) {
return determinePayloadTypeFromPayloads(payloads)
.map(
payloadType -> {
final PayloadSpecification payloadSpecification =
new PayloadSpecification();
payloadSpecification.setType(payloadType.payloadTypeName);
if (payloadType.equals(PayloadType.MAP)) {
generateMapPayloadSchema(payloads)
.ifPresent(
schema ->
payloadSpecification.setSchema(
schema.entrySet().stream()
.collect(
Collectors.toMap(
Map.Entry
::getKey,
e ->
e
.getValue()
.payloadTypeName))));
}
return payloadSpecification;
});
}
/**
* @return the unified payload type of all buckets, empty if no bucket has any payload
* @throws IllegalArgumentException if any 2 buckets have incompatible payload types
*/
@Nonnull
private static Optional determinePayloadTypeFromPayloads(
@Nonnull final List testDefinitionBuckets) {
final List types =
testDefinitionBuckets.stream()
.map(Payload::fetchPayloadType)
.filter(Optional::isPresent)
.map(Optional::get)
.distinct()
.collect(Collectors.toList());
if (types.size() > 1) {
throw new IllegalArgumentException("Payloads not compatible: " + types);
}
if (types.size() == 1) {
return Optional.of(types.get(0));
}
return Optional.empty();
}
/**
* creates a specification based on existing payloads. For map type payloads, unifies all
* information from all payloads
*/
@Nonnull
private static Optional