diff options
Diffstat (limited to 'src/main/java/au/id/zancanaro/javacheck/object')
8 files changed, 296 insertions, 0 deletions
diff --git a/src/main/java/au/id/zancanaro/javacheck/object/CharType.java b/src/main/java/au/id/zancanaro/javacheck/object/CharType.java new file mode 100644 index 0000000..befbd04 --- /dev/null +++ b/src/main/java/au/id/zancanaro/javacheck/object/CharType.java @@ -0,0 +1,15 @@ +package au.id.zancanaro.javacheck.object; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface CharType { + public static enum TYPE { + ALL, ASCII, ALPHA, ALPHA_NUMERIC + }; + TYPE value(); +} diff --git a/src/main/java/au/id/zancanaro/javacheck/object/DoubleRange.java b/src/main/java/au/id/zancanaro/javacheck/object/DoubleRange.java new file mode 100644 index 0000000..ca7c9a9 --- /dev/null +++ b/src/main/java/au/id/zancanaro/javacheck/object/DoubleRange.java @@ -0,0 +1,13 @@ +package au.id.zancanaro.javacheck.object; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface DoubleRange { + double min(); + double max(); +} diff --git a/src/main/java/au/id/zancanaro/javacheck/object/GeneratorProvider.java b/src/main/java/au/id/zancanaro/javacheck/object/GeneratorProvider.java new file mode 100644 index 0000000..7e5964f --- /dev/null +++ b/src/main/java/au/id/zancanaro/javacheck/object/GeneratorProvider.java @@ -0,0 +1,117 @@ +package au.id.zancanaro.javacheck.object; + +import au.id.zancanaro.javacheck.Generator; + +import java.lang.annotation.Annotation; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static au.id.zancanaro.javacheck.Generators.*; + +public interface GeneratorProvider { + Generator<?> getGenerator(Type type, Annotation[] annotations, GeneratorProvider topLevel); + + final static GeneratorProvider DEFAULT_PROVIDER = new GeneratorProvider() { + @SuppressWarnings("unchecked") + private <T> T getAnnotation(Annotation[] annotations, Class<T> type) { + for (Annotation ann : annotations) { + if (type.isAssignableFrom(ann.getClass())) { + return (T) ann; + } + } + return null; + } + + @Override + public Generator<?> getGenerator(Type type, Annotation[] annotations, GeneratorProvider provider) { + if (type == Integer.TYPE || type == Integer.class) { + IntRange range = getAnnotation(annotations, IntRange.class); + if (range == null) { + return integer(); + } else { + return integer(range.min(), range.max()); + } + } else if (type == Long.TYPE || type == Long.class) { + LongRange range = getAnnotation(annotations, LongRange.class); + if (range == null) { + return longInteger(); + } else { + return longInteger(range.min(), range.max()); + } + } else if (type == Double.TYPE || type == Double.class) { + DoubleRange range = getAnnotation(annotations, DoubleRange.class); + if (range == null) { + return doublePrecision(); + } else { + return doublePrecision(range.min(), range.max()); + } + } else if (type == String.class) { + CharType range = getAnnotation(annotations, CharType.class); + if (range == null) { + return string(); + } else { + switch (range.value()) { + case ALPHA: + return stringOf(alphaCharacter()); + case ALPHA_NUMERIC: + return stringOf(alphaNumericCharacter()); + case ASCII: + return stringOf(asciiCharacter()); + case ALL: + return stringOf(character()); + default: + return string(); + } + } + } else if (type == List.class) { + return listOf(provider.getGenerator( + List.class.getTypeParameters()[0], + new Annotation[0], + provider)); + } else if (type == Map.class) { + TypeVariable<?>[] params = Map.class.getTypeParameters(); + return mapOf( + provider.getGenerator(params[0], new Annotation[0], provider), + provider.getGenerator(params[1], new Annotation[0], provider)); + } else if (type instanceof Class) { + return ofType((Class<?>) type, provider); + } else if (type instanceof ParameterizedType) { + ParameterizedType param = (ParameterizedType) type; + if (param.getRawType() instanceof Class) { + Class<?> container = (Class) param.getRawType(); + TypeVariable<?>[] variables = container.getTypeParameters(); + Type[] types = param.getActualTypeArguments(); + return provider.getGenerator( + container, + new Annotation[0], + provider.withSubstitutedTypeVariables(variables, types)); + } + throw new ObjectGenerationException("Cannot generate object of type " + type); + } else { + throw new ObjectGenerationException("Cannot generate object of type " + type); + } + } + }; + + default GeneratorProvider withSubstitutedTypeVariables(TypeVariable<?>[] variables, Type[] types) { + GeneratorProvider fallback = this; + return new GeneratorProvider() { + @Override + public Generator<?> getGenerator(Type type, Annotation[] annotations, GeneratorProvider provider) { + for (int i = 0; i < variables.length; ++i) { + if (Objects.equals(variables[i], type)) { + return provider.getGenerator( + types[i], + new Annotation[0], + this); + } + } + return fallback.getGenerator(type, annotations, provider); + } + }; + } +} diff --git a/src/main/java/au/id/zancanaro/javacheck/object/IntRange.java b/src/main/java/au/id/zancanaro/javacheck/object/IntRange.java new file mode 100644 index 0000000..8dc3f28 --- /dev/null +++ b/src/main/java/au/id/zancanaro/javacheck/object/IntRange.java @@ -0,0 +1,13 @@ +package au.id.zancanaro.javacheck.object; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface IntRange { + int max(); + int min(); +} diff --git a/src/main/java/au/id/zancanaro/javacheck/object/LongRange.java b/src/main/java/au/id/zancanaro/javacheck/object/LongRange.java new file mode 100644 index 0000000..dd5a61f --- /dev/null +++ b/src/main/java/au/id/zancanaro/javacheck/object/LongRange.java @@ -0,0 +1,13 @@ +package au.id.zancanaro.javacheck.object; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface LongRange { + long max(); + long min(); +} diff --git a/src/main/java/au/id/zancanaro/javacheck/object/ObjectGenerationException.java b/src/main/java/au/id/zancanaro/javacheck/object/ObjectGenerationException.java new file mode 100644 index 0000000..d2147eb --- /dev/null +++ b/src/main/java/au/id/zancanaro/javacheck/object/ObjectGenerationException.java @@ -0,0 +1,11 @@ +package au.id.zancanaro.javacheck.object; + +public class ObjectGenerationException extends RuntimeException { + public ObjectGenerationException(String message) { + super(message); + } + + public ObjectGenerationException(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/au/id/zancanaro/javacheck/object/ObjectGenerator.java b/src/main/java/au/id/zancanaro/javacheck/object/ObjectGenerator.java new file mode 100644 index 0000000..a31d990 --- /dev/null +++ b/src/main/java/au/id/zancanaro/javacheck/object/ObjectGenerator.java @@ -0,0 +1,103 @@ +package au.id.zancanaro.javacheck.object; + +import au.id.zancanaro.javacheck.Generator; +import au.id.zancanaro.javacheck.ShrinkTree; + +import java.lang.annotation.Annotation; +import java.lang.reflect.*; +import java.util.*; + +public class ObjectGenerator<T> implements Generator<T> { + private final Class<T> objectClass; + private final GeneratorProvider provider; + private final List<Generator<?>> constructorGenerators; + private final SortedMap<Field, Generator<?>> fieldGenerators; + private transient Constructor<T> constructor = null; + + public ObjectGenerator(Class<T> objectClass) { + this(objectClass, GeneratorProvider.DEFAULT_PROVIDER); + } + + public ObjectGenerator(Class<T> objectClass, GeneratorProvider provider) { + this.objectClass = objectClass; + this.provider = provider; + this.constructorGenerators = calculateConstructorGenerators(); + this.fieldGenerators = calculateFieldGenerators(); + } + + @SuppressWarnings("unchecked") + private Constructor<T> getConstructor() { + if (this.constructor == null) { + Constructor<?>[] constructors = objectClass.getConstructors(); + if (constructors.length == 1) { + return (Constructor<T>) constructors[0]; + } else { + for (Constructor<?> constructor : constructors) { + if (constructor.isAnnotationPresent(UseForGeneration.class)) { + if (this.constructor == null) { + this.constructor = (Constructor<T>) constructor; + } else { + throw new ObjectGenerationException("Multiple constructors are annotated with @UseForGeneration in class" + objectClass); + } + } + } + if (this.constructor == null) { + throw new ObjectGenerationException("Multiple constructors, but none are annotated with @UseForGeneration, in class" + objectClass); + } + } + } + return this.constructor; + } + + private List<Generator<?>> calculateConstructorGenerators() { + List<Generator<?>> result = new ArrayList<>(); + Constructor<T> constructor = getConstructor(); + Type[] parameterTypes = constructor.getGenericParameterTypes(); + Annotation[][] annotations = constructor.getParameterAnnotations(); + for (int i = 0; i < parameterTypes.length; ++i) { + result.add(provider.getGenerator(parameterTypes[i], annotations[i], provider)); + } + return result; + } + + private SortedMap<Field, Generator<?>> calculateFieldGenerators() { + Comparator<Field> fieldComparator = Comparator.comparing(Field::getName); + SortedMap<Field, Generator<?>> result = new TreeMap<>(fieldComparator); + for (Field field : objectClass.getFields()) { + if (!Modifier.isFinal(field.getModifiers()) + && !Modifier.isStatic(field.getModifiers())) { + result.put(field, provider.getGenerator( + field.getGenericType(), + field.getAnnotations(), + provider)); + } + } + return result; + } + + @Override + public ShrinkTree<T> generate(Random random, int size) { + Generator<?>[] parameters = new Generator[constructorGenerators.size()]; + parameters = constructorGenerators.toArray(parameters); + Generator<?>[] fields = new Generator[fieldGenerators.size()]; + fields = fieldGenerators.values().toArray(fields); + return Generator.tuple(Generator.tuple(parameters), Generator.tuple(fields)) + .generate(random, size) + .map(objs -> makeObject( + (List<?>) objs.get(0), + (List<?>) objs.get(1))); + } + + private T makeObject(List<?> parameters, List<?> fields) { + try { + T obj = getConstructor().newInstance(parameters.toArray()); + int i = 0; + for (Field key : fieldGenerators.keySet()) { + key.set(obj, fields.get(i++)); + } + return obj; + } catch (IllegalAccessException | InstantiationException | InvocationTargetException ex) { + throw new ObjectGenerationException(ex); + } + } +} diff --git a/src/main/java/au/id/zancanaro/javacheck/object/UseForGeneration.java b/src/main/java/au/id/zancanaro/javacheck/object/UseForGeneration.java new file mode 100644 index 0000000..68990bf --- /dev/null +++ b/src/main/java/au/id/zancanaro/javacheck/object/UseForGeneration.java @@ -0,0 +1,11 @@ +package au.id.zancanaro.javacheck.object; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.CONSTRUCTOR) +@Retention(RetentionPolicy.RUNTIME) +public @interface UseForGeneration { +} |