summaryrefslogtreecommitdiff
path: root/src/main/java/au/id/zancanaro/javacheck/object
diff options
context:
space:
mode:
authorCarlo Zancanaro <carlo@zancanaro.id.au>2015-06-09 17:33:56 +1000
committerCarlo Zancanaro <carlo@zancanaro.id.au>2015-06-09 17:33:56 +1000
commitdd9f72b94eb7b2c37061c80457e74e8d7ac3e18f (patch)
tree17ac650c0c4a5045b1cbf0ef5c194b0ea7f7acd3 /src/main/java/au/id/zancanaro/javacheck/object
parent813e523e9e57dc38f81afc53340e216b948d87cf (diff)
Add an ObjectGenerator<>, and related machinery (also a mapOf generator)
Diffstat (limited to 'src/main/java/au/id/zancanaro/javacheck/object')
-rw-r--r--src/main/java/au/id/zancanaro/javacheck/object/CharType.java15
-rw-r--r--src/main/java/au/id/zancanaro/javacheck/object/DoubleRange.java13
-rw-r--r--src/main/java/au/id/zancanaro/javacheck/object/GeneratorProvider.java117
-rw-r--r--src/main/java/au/id/zancanaro/javacheck/object/IntRange.java13
-rw-r--r--src/main/java/au/id/zancanaro/javacheck/object/LongRange.java13
-rw-r--r--src/main/java/au/id/zancanaro/javacheck/object/ObjectGenerationException.java11
-rw-r--r--src/main/java/au/id/zancanaro/javacheck/object/ObjectGenerator.java103
-rw-r--r--src/main/java/au/id/zancanaro/javacheck/object/UseForGeneration.java11
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 {
+}