first commit

This commit is contained in:
Sander Hautvast 2020-01-14 22:02:38 +01:00
commit e12f3b284d
11 changed files with 663 additions and 0 deletions

10
.gitignore vendored Normal file
View file

@ -0,0 +1,10 @@
# Created by .ignore support plugin (hsz.mobi)
### Example user template template
### Example user template
# IntelliJ project files
.idea
*.iml
out
gen
target/

8
README.md Normal file
View file

@ -0,0 +1,8 @@
This project is called Edie after Edie Sedgwick who was one of the artists in Andy Warhols Factory.
The film about her life is called Factory Girl (2006).
This project was inspired by the FactoryGirl project that was later renamed FactoryBot.
* https://en.wikipedia.org/wiki/Edie_Sedgwick
* https://www.imdb.com/title/tt0432402
* https://hn.algolia.com/?q=factorybot

47
pom.xml Normal file
View file

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>nl.sander</groupId>
<artifactId>edie</artifactId>
<version>0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>info.cukes</groupId>
<artifactId>cucumber-core</artifactId>
<version>1.2.4</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.5.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M4</version>
<configuration>
<includes>
<include>**/*Tests.java</include>
</includes>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>

View file

@ -0,0 +1,9 @@
package shautvast.edie;
/**
* An Adapter lambda is used to alter instances after creation according to the template.
* @param <T>
*/
public interface Adapter<T> {
public void adapt(T input);
}

View file

@ -0,0 +1,127 @@
package shautvast.edie;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Collectors;
/**
* Contains the specification for creating instances. They are either Supplier lambda or a Constructor instance.
*/
public class Definition<T> {
/* contains the class instances for creating elements in a List */
private final List<Class<?>> typesInList = new ArrayList<>();
/* type of the object that will be created*/
private Class<T> type;
/* constructor class for creating instances*/
private Constructor<T> constructor;
/* lambda that can be used to create instances*/
private Supplier<T> template;
/*
* Constructor that takes a type. The constructor member variable must be added later on.
*
* @param type Type of the instances this Definition will create.
*/
Definition(Class<T> type) {
this.type = type;
}
/*
* Constructor that takes a Supplier lambda to create instances.
*
* @param supplier
*/
Definition(Supplier<T> supplier) {
this.template = supplier;
}
/**
* Used to create the instance, once the definition is constructed.
* Can be called directly, or via the Factory.build() method
*
* @return a new instance by calling the supplier or a constructor.
*/
@SuppressWarnings("unchecked")
public T build() {
if (template != null) {
return template.get();
} else if (constructor != null) {
if (!typesInList.isEmpty()) {
return (T) newInstance(new Object[]{
typesInList.stream()
.map(Factory::build)
.collect(Collectors.toList())});
} else {
Object[] args = Arrays.stream(constructor.getParameterTypes())
.map(Factory::build)
.collect(Collectors.toList()).toArray(new Object[]{});
return (T) newInstance(args);
}
} else {
throw new IllegalStateException("Template and constructor cannot both be empty");
}
}
/**
* Build method that takes an adapter lambda for customizing the final object
* Example:
* Definition<Person> personDef = define(Person.class, () -> new Person("Sander", 48));
* Person person = personDef.build(p -> {
* p.setName("Harry");
* return p;
* });
* @param adapter
* @return
*/
public T build(Adapter<T> adapter) {
T instance = template.get();
adapter.adapt(instance);
return instance;
}
private Object newInstance(Object[] args) {
try {
return constructor.newInstance(args);
} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
throw new IllegalStateException(e);
}
}
/**
* Example:
* Definition<Employee> employeeDef = Factory.define(Employee.class).withConstructorArgs(Person.class);
* Can be used given that Employee has a constructor with one parameterof type Person
* This will use this constructor to create instances.
*
* @param parameterTypes varargs argument for the parameters of the constructor to look up.
*
* @return a Definition with the correct constructor
*/
@SuppressWarnings("rawtypes")
public Definition<T> withConstructorArgs(Class... parameterTypes) {
try {
this.constructor = type.getConstructor(parameterTypes);
return this;
} catch (NoSuchMethodException e) {
throw new IllegalArgumentException(e);
}
}
public Definition<T> withConstructorArgs(List<Class<?>> typesToConstruct) {
try {
this.constructor = type.getConstructor(List.class);
this.typesInList.addAll(typesToConstruct);
return this;
} catch (NoSuchMethodException e) {
throw new IllegalStateException(e);
}
}
}

View file

@ -0,0 +1,100 @@
package shautvast.edie;
import java.util.Arrays;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Supplier;
/**
*
*/
@SuppressWarnings({"rawtypes","unchecked"})
public class Factory {
private static final ConcurrentMap<Class<?>, Definition> definitions = new ConcurrentHashMap<>();
/**
* Creates a definition for the given type, using a Supplier that will create that type every time build() is called.
*
* @param type Class that specifies what type must be created
* @param supplier Lambda that creates the given type
* @param <T> Generic type of the created Definition
* @return A Definition that can be called directly to create Type instances
*/
public static <T> Definition<T> define(Class<T> type, Supplier<T> supplier) {
Definition<T> definition = new Definition<>(supplier);
definitions.put(type, definition);
return definition;
}
/**
* Creates a definition for the given type.
* when using this method, the resulting definition is not complete yet. See Definition.withConstructor.
*
* @param type Class that specifies what type must be created
* @param <T> Generic type of the created Definition
* @return A Definition that can be called directly to create Type instances
*/
public static <T> Definition<T> define(Class<T> type) {
Definition<T> definition = new Definition<>(type);
definitions.put(type, definition);
return definition;
}
/**
* Builds the instance for the specified type, using an Adapter lambda that can alter specific attributes if needed.
* It's logic is delegated to the Definition for the type.
* <p>
* Example:
* Definition<Person> personDef = define(Person.class, () -> new Person("Sander", 48));
* <p>
* Person person = personDef.build(p -> {
* p.setName("Harry");
* return p;
* });
* <p>
* The template name is 'Sander' but the person instance has a different name.
*
* @param type Class to denote the type to create.
* @param adapter Can be used to change the type after creation
* @return an instance of the given type using template and adapter
*/
public static <T> T build(Class<T> type, Adapter<T> adapter) {
return getDefinition(type).build(adapter);
}
/**
* Builds the instance for the specified type.
* It's logic is delegated to the Definition for the type.
* <p>
* Example:
* Definition<Person> personDef = define(Person.class, () -> new Person("Sander", 48));
* <p>
* Person person = personDef.build()
* will always yields a new instance
*
* @param type Class to denote the type to create.
* @return an instance of the given type using template and adapter
*/
public static <T> T build(Class<T> type) {
return getDefinition(type).build();
}
private static <T> Definition<T> getDefinition(Class<T> type) {
Definition<T> definition = definitions.get(type);
if (definition == null) {
throw new IllegalStateException("definition not found for " + type);
} else {
return definition;
}
}
static void checkTypes(Class<?>[] typesToConstruct) {
if (Arrays.stream(typesToConstruct)
.anyMatch(type -> !definitions.containsKey(type))) {
throw new IllegalStateException("not all parameters bound to definition");
}
}
}

View file

@ -0,0 +1,106 @@
package shautvast.edie;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.LongAdder;
public class FactoryHelper {
/* unnamed counter */
private static LongAdder globalCounter = new LongAdder();
/* contains named counters */
private static ConcurrentMap<String, LongAdder> namedCounters = new ConcurrentHashMap<>();
/* util for random numbers*/
private static Random random = new Random();
/**
* Values that need an index can be created with this helper method.
* <p>
* Example:
* Factory.define(Person.class, () -> new Person("person[" + increment()+"]", 33));
*
* @return a number that increments on every invocation of the type supplier.
*/
public static long increment() {
return incrementAndGet(globalCounter);
}
/**
* Creates an named incrementer that is called every time the Factory creates the object.
* <p>
* Example:
* Factory.define(Person.class, () -> new Person("person[" + increment("personIndex")+"]", 33));
*
* @param name the name of the incrementer
* @return a number that increments every time this method is called with the same name.
*/
public static long increment(String name) {
return incrementAndGet(namedCounters.computeIfAbsent(name, k -> new LongAdder()));
}
/**
* Generate a random 64 bit integer number.
* <p>
* Example:
* define(Person.class, () -> new Person("Sander", randomLong()));
*
* @return a random number on every invocation
*/
public static long randomLong() {
return random.nextLong();
}
/**
* Generate a random 32 bit integer number.
* <p>
* Example:
* define(Person.class, () -> new Person("Sander", randomInt()));
*
* @return a random number on every invocation
*/
public static int randomInt() {
return random.nextInt();
}
/**
* Generate a random 32 bit integer number below an upper bound.
* <p>
* Example:
* define(Person.class, () -> new Person("Sander", randomInt(100)));
*
* @param bound exlusive upper bound
* @return a random number on every invocation
*/
public static int randomInt(int bound) {
return random.nextInt(bound);
}
/**
* Generate a random 64 bit floating point number below an upper bound.
*
* @return a random number on every invocation
*/
public static double randomDouble() {
return random.nextDouble();
}
/**
* Helper for the creation of factory types within a list.
*
* @param typesToConstruct Any types (should be known in Factory) that are to be elements in a List
* @return A List of the same types. See
*/
public static List<Class<?>> listOf(Class<?>... typesToConstruct) {
Factory.checkTypes(typesToConstruct);
return Arrays.asList(typesToConstruct);
}
private static long incrementAndGet(LongAdder longAdder) {
long value = longAdder.longValue();
longAdder.increment();
return value;
}
}

View file

@ -0,0 +1,143 @@
package shautvast.edie;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import shautvast.edie.testdomain.Company;
import shautvast.edie.testdomain.Employee;
import shautvast.edie.testdomain.Person;
import java.util.Arrays;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@DisplayName("Assertions for the factory and definitions")
public class FactoryTests {
@Test
@DisplayName("Should build instance with definition.build()")
public void simpleTemplate() {
Definition<Person> personDef = Factory.define(Person.class, () -> new Person("Sander", 48));
Person person = personDef.build();
Assertions.assertEquals("Sander", person.getName());
Assertions.assertEquals(48, person.getAge());
}
@Test
@DisplayName("Should build instance with Factory.build()")
public void simpleTemplateStatic() {
Factory.define(Person.class, () -> new Person("Sander", 48));
Person person = Factory.build(Person.class);
Assertions.assertEquals("Sander", person.getName());
Assertions.assertEquals(48, person.getAge());
}
@Test
@DisplayName("Should build a customized instance while the other attributes are according to the template")
public void simpleTemplateAlteration() {
Factory.define(Person.class, () -> new Person("Sander", 48));
Person person = Factory.build(Person.class, p -> p.setName("Harry"));
Assertions.assertEquals("Harry", person.getName());
Assertions.assertEquals(48, person.getAge());
}
@Test
@DisplayName("Should create different values for an attribute for every instance")
public void counters() {
Definition<Person> personDef = Factory.define(Person.class, () -> new Person("Sander", 48));
Person person = personDef.build(p ->
p.setName("Harry" + FactoryHelper.increment())
);
Assertions.assertEquals("Harry0", person.getName());
Assertions.assertEquals(48, person.getAge());
}
@Test
@DisplayName("Should lookup a constructor with a nested type definition and create nested instances")
public void nestedDefinitionsWithConstructor() {
Factory.define(Person.class, () -> new Person("Sander", FactoryHelper.randomInt(100)));
Definition<Employee> employeeDef = Factory.define(Employee.class).withConstructorArgs(Person.class);
Employee employee = employeeDef.build();
Assertions.assertEquals("Sander", employee.getPerson().getName());
}
@Test
@DisplayName("Should create nested instances using only suppliers")
public void nestedDefinitionsWithoutReflection() {
Definition<Person> personDef = Factory.define(Person.class, () -> new Person("Sander", FactoryHelper.randomInt(100)));
Definition<Employee> employeeDef = Factory.define(Employee.class, () -> new Employee(personDef.build()));
Employee employee = employeeDef.build();
Assertions.assertEquals("Sander", employee.getPerson().getName());
}
@Test
@DisplayName("Should create random attributes for every instance")
public void randomInts() {
Definition<Person> personDef = Factory.define(Person.class, () -> new Person("Sander", FactoryHelper.randomInt(100)));
Person person = personDef.build();
Assertions.assertEquals("Sander", person.getName());
assertTrue(person.getAge() < 100);
}
@Test
@DisplayName("Should create an object with a list of factory produced elements")
public void definitionsList() {
Factory.define(Person.class, () -> new Person("Sander" + FactoryHelper.increment(), FactoryHelper.randomInt(100)));
Factory.define(Employee.class).withConstructorArgs(Person.class);
Factory.define(Company.class).withConstructorArgs(FactoryHelper.listOf(Employee.class, Employee.class));
Company company = Factory.build(Company.class);
Assertions.assertEquals(2, company.getEmployees().size());
}
@Test
@DisplayName("Should create an object with a list of factory produced elements without reflection")
public void definitionsListWithoutReflection() {
Factory.define(Person.class, () -> new Person("Sander", 10));
Factory.define(Employee.class).withConstructorArgs(Person.class);
Factory.define(Company.class, () -> new Company(Arrays.asList(Factory.build(Employee.class), Factory.build(Employee.class))));
Company company = Factory.build(Company.class);
Assertions.assertEquals(2, company.getEmployees().size());
Assertions.assertTrue(company.getEmployees().contains(new Employee(new Person("Sander", 10))));
Assertions.assertTrue(company.getEmployees().contains(new Employee(new Person("Sander", 10))));
}
@Test
@DisplayName("Should create a list with two elements")
public void definitionsListWithIncrementInNestedDefinition() {
Factory.define(Person.class, () -> new Person("Sander" + FactoryHelper.increment("sander"), 10));
Factory.define(Employee.class).withConstructorArgs(Person.class);
Factory.define(Company.class, () -> new Company(Arrays.asList(Factory.build(Employee.class), Factory.build(Employee.class))));
Company company = Factory.build(Company.class);
Assertions.assertEquals(2, company.getEmployees().size());
Assertions.assertTrue(company.getEmployees().contains(new Employee(new Person("Sander0", 10))));
Assertions.assertTrue(company.getEmployees().contains(new Employee(new Person("Sander1", 10))));
}
@Test
@DisplayName("Method withConstructor for a constructor with the wrong types should raise an exception")
public void constructorNotFound() {
assertThrows(
IllegalArgumentException.class, () ->
Factory.define(Person.class).withConstructorArgs(String.class));
}
}

View file

@ -0,0 +1,23 @@
package shautvast.edie.testdomain;
import java.util.ArrayList;
import java.util.List;
public class Company {
private final List<Employee> employees=new ArrayList<>();
public Company(List<Employee> employees) {
this.employees.addAll(employees);
}
public List<Employee> getEmployees() {
return employees;
}
@Override
public String toString() {
return "Company{" +
"employees=" + employees +
'}';
}
}

View file

@ -0,0 +1,39 @@
package shautvast.edie.testdomain;
import java.util.Objects;
public class Employee {
private Person person;
public Employee(Person person) {
this.person = person;
}
public Person getPerson() {
return person;
}
public void setPerson(Person person) {
this.person = person;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Employee employee = (Employee) o;
return Objects.equals(person, employee.person);
}
@Override
public int hashCode() {
return Objects.hash(person);
}
@Override
public String toString() {
return "Employee{" +
"person=" + person +
'}';
}
}

View file

@ -0,0 +1,51 @@
package shautvast.edie.testdomain;
import java.util.Objects;
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age &&
Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}