Skip to content

Commit

Permalink
Merge pull request #66 from michal309/sealed-interface-support
Browse files Browse the repository at this point in the history
Basic sealed interface support
  • Loading branch information
mjureczko authored Jul 25, 2024
2 parents 676ca7c + d05346d commit 92951b8
Show file tree
Hide file tree
Showing 6 changed files with 296 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,10 @@ EnhancedRandom simplifiedRandom() {
return randomWithArrangers(simplifiedArrangers, new EnhancedRandom.Builder(ArrangersConfigurer::getEasyRandomSimplifiedParameters));
}

EnhancedRandom randomForGivenConfiguration(Class<?> type, Map<Class<?>, CustomArranger<?>> arrangers, Supplier<EasyRandomParameters> parametersSupplier) {
EnhancedRandom randomForGivenConfiguration(Class<?> type, boolean withoutGivenType, Map<Class<?>, CustomArranger<?>> arrangers, Supplier<EasyRandomParameters> parametersSupplier) {
EnhancedRandom.Builder randomBuilder = new EnhancedRandom.Builder(parametersSupplier);
long seed = SeedHelper.calculateSeed();
CustomArranger<?> arrangerToUpdate = arrangers.get(type);
if (arrangerToUpdate != null) {
if (withoutGivenType && arrangers.get(type) != null) {
seed = SeedHelper.customArrangerTypeSpecificSeedRespectingRandomSeedSetting(type);
arrangers = withoutGivenType(arrangers, type);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,19 @@
public abstract class CustomArranger<T> {

protected EnhancedRandom enhancedRandom = null;
protected final Class<T> type = (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
protected final Class<T> type;

protected CustomArranger() {
this.type = (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
initEnhancedRandom();
}

protected CustomArranger(Class<T> type) {
this.type = type;
initEnhancedRandom();
}

private void initEnhancedRandom() {
if (ArrangersConfigurer.defaultInitialized.get()) {
enhancedRandom = ArrangersConfigurer.instance().defaultRandom();
} else {
Expand Down
93 changes: 84 additions & 9 deletions src/main/java/com/ocadotechnology/gembus/test/EnhancedRandom.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,22 @@
import org.jeasy.random.EasyRandomParameters;
import org.jeasy.random.api.Randomizer;

import java.util.*;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static java.util.function.Function.identity;

/**
* Class for random object generator.
*/
Expand Down Expand Up @@ -84,26 +95,90 @@ public <T> Stream<T> objects(final Class<T> type, final int amount, final String
}

private <T> EasyRandom selectEasyRandomWithRespectToExclusion(Class<T> type, String[] excludedFields) {
if (newEasyRandomWithFieldExclusionConfigIsRequired(type, excludedFields)) {
return createEasyRandomWithExclusions(type, excludedFields);
if (newEasyRandomWithCustomRandomizersOrFieldExclusionConfigIsRequired(type, excludedFields)) {
return createEasyRandomWithCustomRandomizersAndExclusions(type, excludedFields);
}
return easyRandom;
}

private <T> boolean newEasyRandomWithFieldExclusionConfigIsRequired(Class<T> type, String[] excludedFields) {
private <T> boolean newEasyRandomWithCustomRandomizersOrFieldExclusionConfigIsRequired(Class<T> type, String[] excludedFields) {
/* There is a logical inconsistency in using a custom arranger and field exclusion for the same type - the
* exclusion can be configured in the custom arranger. Technically, creating an arranger with exclusion disables
* the custom arranger for the type that is being instantiated. */
return !arrangers.containsKey(type) && excludedFields.length != 0;
return !arrangers.containsKey(type) && (excludedFields.length != 0 || isSealedInterface(type) || !nestedSealedInterfaceFields(type).isEmpty());
}

private <T> EasyRandom createEasyRandomWithExclusions(Class<T> type, String[] excludedFields) {
private <T> EasyRandom createEasyRandomWithCustomRandomizersAndExclusions(Class<T> type, String[] excludedFields) {
Set<String> fields = new HashSet<>(Arrays.asList(excludedFields));
cache.computeIfAbsent(fields, key -> {
EnhancedRandom er = ArrangersConfigurer.instance().randomForGivenConfiguration(type, arrangers, () -> addExclusionToParameters(fields));
var forSealedInterfaces = createCustomArrangersForSealedInterfaces(type, fields);
Set<String> cacheKey = getCacheKey(fields, forSealedInterfaces.keySet());
cache.computeIfAbsent(cacheKey, key -> {
HashMap<Class<?>, CustomArranger<?>> enhancedArrangers = new HashMap<>(arrangers);
enhancedArrangers.putAll(forSealedInterfaces);
EnhancedRandom er = ArrangersConfigurer.instance()
.randomForGivenConfiguration(type, !forSealedInterfaces.containsKey(type), enhancedArrangers, () -> addExclusionToParameters(fields));
return er.easyRandom;
});
return cache.get(fields);
return cache.get(cacheKey);
}

private Set<String> getCacheKey(Set<String> fields, Set<Class<?>> sealedInterfaces) {
Set<String> cacheKey = new HashSet<>(fields);
cacheKey.addAll(sealedInterfaces.stream().map(Class::getName).toList());
return cacheKey;
}

private <T> Map<Class<?>, CustomArranger<?>> createCustomArrangersForSealedInterfaces(Class<T> type, Set<String> excludedFields) {
Map<Class<?>, CustomArranger<?>> sealedInterfaceArrangers = new HashMap<>();
if (isSealedInterface(type)) {
sealedInterfaceArrangers.put(type, new SealedInterfaceArranger<T>(type));
}
sealedInterfaceArrangers.putAll(nestedSealedInterfaceFields(type)
.entrySet()
.stream()
.filter(entry -> !excludedFields.contains(entry.getKey()))
.map(Map.Entry::getValue)
.collect(Collectors.toMap(identity(), SealedInterfaceArranger::new)));
return sealedInterfaceArrangers;
}

private <T> Map<String, Class<?>> nestedSealedInterfaceFields(Class<T> type) {
return allNestedFields(new HashMap<>(), type)
.entrySet()
.stream()
.filter(entry -> isSealedInterface(entry.getValue()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}

private Map<String, Class<?>> allNestedFields(Map<String, Class<?>> acc, Class<?> clazz) {
if (clazz == null || clazz.isPrimitive()) {
return Map.of();
}
if (isSealedInterface(clazz)) {
for (Class<?> permittedSubclasses : clazz.getPermittedSubclasses()) {
acc.putAll(allNestedFields(acc, permittedSubclasses));
}
}
for (Field field : clazz.getDeclaredFields()) {
if (!acc.containsKey(field.getName())) {
if (field.getGenericType() instanceof ParameterizedType parameterizedType) {
for (var genericType : parameterizedType.getActualTypeArguments()) {
if(genericType instanceof Class<?> genericTypeClass) {
acc.put(field.getName(), genericTypeClass);
acc.putAll(allNestedFields(acc, genericTypeClass));
}
}
} else {
acc.put(field.getName(), field.getType());
acc.putAll(allNestedFields(acc, field.getType()));
}
}
}
return acc;
}

private static boolean isSealedInterface(Class<?> type) {
return type.isSealed() && type.isInterface();
}

private EasyRandomParameters addExclusionToParameters(Set<String> fields) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class ReflectionHelper {
.loadClasses(CustomArranger.class, true)
.stream()
.filter(clazz -> isNotAbstract(clazz))
.filter(clazz -> !SealedInterfaceArranger.class.equals(clazz))
.map(clazz -> extractConstructor(clazz))
.filter(constructor -> constructor.isPresent())
.map(constructor -> constructor.get())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright © 2020 Ocado ([email protected])
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.ocadotechnology.gembus.test;

public class SealedInterfaceArranger<T> extends CustomArranger<T> {

public SealedInterfaceArranger() {
}

protected SealedInterfaceArranger(Class<T> clazz) {
super(clazz);
}

@Override
protected T instance() {
Class<?>[] subclasses = type.getPermittedSubclasses();
return (T) enhancedRandom.nextObject(subclasses[enhancedRandom.nextInt(subclasses.length)]);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/*
* Copyright © 2020 Ocado ([email protected])
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.ocadotechnology.gembus.test;

import org.jeasy.random.ObjectCreationException;
import org.junit.jupiter.api.Test;

import java.util.List;
import java.util.Map;

import static com.ocadotechnology.gembus.test.Arranger.some;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

public class ArrangerSealedInterfacesTest {

@Test
void should_arrangeSealedInterface() {
//when
SealedInterfaceData actual = some(SealedInterfaceData.class);

//then
assertThat(actual).isInstanceOfAny(ConcreteData1.class, ConcreteData2.class);
}

@Test
void should_arrangeSealedInterfaceWithCustomArranger() {
//when
SealedInterfaceWithCustomArranger actual = some(SealedInterfaceWithCustomArranger.class);

//then
assertThat(actual).isEqualTo(new ConcreteDataWithCustomArranger("expected-test-string"));
}

@Test
void should_arrangeDataWithSealedInterfaceField() {
//when
DataWithAbstract actual = some(DataWithAbstract.class, "excludedSealedInterface", "nonSealedInterface");

//then
assertThat(actual.sealedInterfaceData()).isInstanceOfAny(ConcreteData1.class, ConcreteData2.class);
assertThat(actual.excludedSealedInterface()).isNull();
}

@Test
void should_arrangeDataWithSealedInterfaceFieldOverride() {
//when
ConcreteData1 expected = new ConcreteData1("expected-data");
DataWithAbstract actual = some(DataWithAbstract.class, Map.of(
"sealedInterfaceData", () -> expected,
"excludedSealedInterface", () -> null,
"nonSealedInterface", () -> null));

//then
assertThat(actual.sealedInterfaceData()).isEqualTo(expected);
assertThat(actual.excludedSealedInterface()).isNull();
}

@Test
void should_arrangeDataWithNestedSealedInterfaceField() {
//when
NestedSealedInterfaceData actual = some(NestedSealedInterfaceData.class);

//then
assertThat(actual).isInstanceOf(ConcreteNested1.class);
assertThat((ConcreteNested1) actual)
.extracting(ConcreteNested1::sealedInterfaceData)
.isInstanceOfAny(ConcreteData1.class, ConcreteData2.class);

}

@Test
void should_arrangeRecordWithListWithSealedInterfaceField() {
//when
RootRecordWithNestedList actual = some(RootRecordWithNestedList.class);

//then
assertThat(actual.nestedRecordWithSealedInterfaces().get(0))
.extracting(NestedRecordWithSealedInterface::sealedInterfaceData)
.isInstanceOfAny(ConcreteData1.class, ConcreteData2.class);
}

@Test
void should_failForNonSealedInterface() {
//when
assertThatThrownBy(() -> some(DataWithAbstract.class, "excludedSealedInterface"))

//then
.isInstanceOf(ObjectCreationException.class);

}

}

record DataWithAbstract(Integer value,
SealedInterfaceData sealedInterfaceData,
ExcludedSealedInterface excludedSealedInterface,
NonSealedInterface nonSealedInterface) {
DataWithAbstract {
}

DataWithAbstract(String name) {
this(0, new ConcreteData1(""), new ExcludedData1(), new NonSealedInterface() {
});
}
}

sealed interface SealedInterfaceData permits ConcreteData1, ConcreteData2 {
}

record ConcreteData1(String value) implements SealedInterfaceData {
}

final class ConcreteData2 implements SealedInterfaceData {

Integer value;

}

sealed interface ExcludedSealedInterface permits ExcludedData1 {
}

record ExcludedData1() implements ExcludedSealedInterface {
}

interface NonSealedInterface {
}

sealed interface NestedSealedInterfaceData permits ConcreteNested1 {
}

record ConcreteNested1(SealedInterfaceData sealedInterfaceData) implements NestedSealedInterfaceData {
}

sealed interface SealedInterfaceWithCustomArranger permits ConcreteDataWithCustomArranger {
}

record ConcreteDataWithCustomArranger(String test) implements SealedInterfaceWithCustomArranger {
}

class CustomSealedInterfaceArranger extends SealedInterfaceArranger<SealedInterfaceWithCustomArranger> {

@Override
protected SealedInterfaceWithCustomArranger instance() {
return new ConcreteDataWithCustomArranger("expected-test-string");
}
}

record RootRecordWithNestedList(List<NestedRecordWithSealedInterface> nestedRecordWithSealedInterfaces) {
}

record NestedRecordWithSealedInterface(SealedInterfaceData sealedInterfaceData) {
}

0 comments on commit 92951b8

Please sign in to comment.