From dd9f72b94eb7b2c37061c80457e74e8d7ac3e18f Mon Sep 17 00:00:00 2001 From: Carlo Zancanaro Date: Tue, 9 Jun 2015 17:33:56 +1000 Subject: Add an ObjectGenerator<>, and related machinery (also a mapOf generator) --- .../au/id/zancanaro/javacheck/object/CharType.java | 15 +++ .../id/zancanaro/javacheck/object/DoubleRange.java | 13 +++ .../javacheck/object/GeneratorProvider.java | 117 +++++++++++++++++++++ .../au/id/zancanaro/javacheck/object/IntRange.java | 13 +++ .../id/zancanaro/javacheck/object/LongRange.java | 13 +++ .../object/ObjectGenerationException.java | 11 ++ .../javacheck/object/ObjectGenerator.java | 103 ++++++++++++++++++ .../javacheck/object/UseForGeneration.java | 11 ++ 8 files changed, 296 insertions(+) create mode 100644 src/main/java/au/id/zancanaro/javacheck/object/CharType.java create mode 100644 src/main/java/au/id/zancanaro/javacheck/object/DoubleRange.java create mode 100644 src/main/java/au/id/zancanaro/javacheck/object/GeneratorProvider.java create mode 100644 src/main/java/au/id/zancanaro/javacheck/object/IntRange.java create mode 100644 src/main/java/au/id/zancanaro/javacheck/object/LongRange.java create mode 100644 src/main/java/au/id/zancanaro/javacheck/object/ObjectGenerationException.java create mode 100644 src/main/java/au/id/zancanaro/javacheck/object/ObjectGenerator.java create mode 100644 src/main/java/au/id/zancanaro/javacheck/object/UseForGeneration.java (limited to 'src/main/java/au/id/zancanaro/javacheck/object') 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 getAnnotation(Annotation[] annotations, Class 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 implements Generator { + private final Class objectClass; + private final GeneratorProvider provider; + private final List> constructorGenerators; + private final SortedMap> fieldGenerators; + private transient Constructor constructor = null; + + public ObjectGenerator(Class objectClass) { + this(objectClass, GeneratorProvider.DEFAULT_PROVIDER); + } + + public ObjectGenerator(Class objectClass, GeneratorProvider provider) { + this.objectClass = objectClass; + this.provider = provider; + this.constructorGenerators = calculateConstructorGenerators(); + this.fieldGenerators = calculateFieldGenerators(); + } + + @SuppressWarnings("unchecked") + private Constructor getConstructor() { + if (this.constructor == null) { + Constructor[] constructors = objectClass.getConstructors(); + if (constructors.length == 1) { + return (Constructor) constructors[0]; + } else { + for (Constructor constructor : constructors) { + if (constructor.isAnnotationPresent(UseForGeneration.class)) { + if (this.constructor == null) { + this.constructor = (Constructor) 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> calculateConstructorGenerators() { + List> result = new ArrayList<>(); + Constructor 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> calculateFieldGenerators() { + Comparator fieldComparator = Comparator.comparing(Field::getName); + SortedMap> 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 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 { +} -- cgit v1.2.3