From e12f3b284dd6987b57b076414015ba6d48c5d67f Mon Sep 17 00:00:00 2001 From: Sander Hautvast Date: Tue, 14 Jan 2020 22:02:38 +0100 Subject: [PATCH] first commit --- .gitignore | 10 ++ README.md | 8 + pom.xml | 47 ++++++ src/main/java/shautvast/edie/Adapter.java | 9 ++ src/main/java/shautvast/edie/Definition.java | 127 ++++++++++++++++ src/main/java/shautvast/edie/Factory.java | 100 ++++++++++++ .../java/shautvast/edie/FactoryHelper.java | 106 +++++++++++++ .../java/shautvast/edie/FactoryTests.java | 143 ++++++++++++++++++ .../shautvast/edie/testdomain/Company.java | 23 +++ .../shautvast/edie/testdomain/Employee.java | 39 +++++ .../shautvast/edie/testdomain/Person.java | 51 +++++++ 11 files changed, 663 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 pom.xml create mode 100644 src/main/java/shautvast/edie/Adapter.java create mode 100644 src/main/java/shautvast/edie/Definition.java create mode 100644 src/main/java/shautvast/edie/Factory.java create mode 100644 src/main/java/shautvast/edie/FactoryHelper.java create mode 100644 src/test/java/shautvast/edie/FactoryTests.java create mode 100644 src/test/java/shautvast/edie/testdomain/Company.java create mode 100644 src/test/java/shautvast/edie/testdomain/Employee.java create mode 100644 src/test/java/shautvast/edie/testdomain/Person.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..affad0d --- /dev/null +++ b/.gitignore @@ -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/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6ccec82 --- /dev/null +++ b/README.md @@ -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 \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..18659eb --- /dev/null +++ b/pom.xml @@ -0,0 +1,47 @@ + + + 4.0.0 + + nl.sander + edie + 0.1-SNAPSHOT + jar + + + + info.cukes + cucumber-core + 1.2.4 + + + org.junit.jupiter + junit-jupiter + 5.5.2 + test + + + + + 1.8 + 1.8 + UTF-8 + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M4 + + + **/*Tests.java + + + + + + + diff --git a/src/main/java/shautvast/edie/Adapter.java b/src/main/java/shautvast/edie/Adapter.java new file mode 100644 index 0000000..0a2d8d5 --- /dev/null +++ b/src/main/java/shautvast/edie/Adapter.java @@ -0,0 +1,9 @@ +package shautvast.edie; + +/** + * An Adapter lambda is used to alter instances after creation according to the template. + * @param + */ +public interface Adapter { + public void adapt(T input); +} diff --git a/src/main/java/shautvast/edie/Definition.java b/src/main/java/shautvast/edie/Definition.java new file mode 100644 index 0000000..1297284 --- /dev/null +++ b/src/main/java/shautvast/edie/Definition.java @@ -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 { + + /* contains the class instances for creating elements in a List */ + private final List> typesInList = new ArrayList<>(); + + /* type of the object that will be created*/ + private Class type; + + /* constructor class for creating instances*/ + private Constructor constructor; + + /* lambda that can be used to create instances*/ + private Supplier 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 type) { + this.type = type; + } + + /* + * Constructor that takes a Supplier lambda to create instances. + * + * @param supplier + */ + Definition(Supplier 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 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 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 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 withConstructorArgs(Class... parameterTypes) { + try { + this.constructor = type.getConstructor(parameterTypes); + return this; + } catch (NoSuchMethodException e) { + throw new IllegalArgumentException(e); + } + } + + public Definition withConstructorArgs(List> typesToConstruct) { + try { + this.constructor = type.getConstructor(List.class); + this.typesInList.addAll(typesToConstruct); + return this; + } catch (NoSuchMethodException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/src/main/java/shautvast/edie/Factory.java b/src/main/java/shautvast/edie/Factory.java new file mode 100644 index 0000000..7fe5a32 --- /dev/null +++ b/src/main/java/shautvast/edie/Factory.java @@ -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, 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 Generic type of the created Definition + * @return A Definition that can be called directly to create Type instances + */ + public static Definition define(Class type, Supplier supplier) { + Definition 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 Generic type of the created Definition + * @return A Definition that can be called directly to create Type instances + */ + public static Definition define(Class type) { + Definition 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. + *

+ * Example: + * Definition personDef = define(Person.class, () -> new Person("Sander", 48)); + *

+ * Person person = personDef.build(p -> { + * p.setName("Harry"); + * return 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 build(Class type, Adapter adapter) { + return getDefinition(type).build(adapter); + } + + + /** + * Builds the instance for the specified type. + * It's logic is delegated to the Definition for the type. + *

+ * Example: + * Definition personDef = define(Person.class, () -> new Person("Sander", 48)); + *

+ * 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 build(Class type) { + return getDefinition(type).build(); + } + + private static Definition getDefinition(Class type) { + Definition 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"); + } + } +} \ No newline at end of file diff --git a/src/main/java/shautvast/edie/FactoryHelper.java b/src/main/java/shautvast/edie/FactoryHelper.java new file mode 100644 index 0000000..3cce932 --- /dev/null +++ b/src/main/java/shautvast/edie/FactoryHelper.java @@ -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 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. + *

+ * 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. + *

+ * 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. + *

+ * 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. + *

+ * 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. + *

+ * 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> listOf(Class... typesToConstruct) { + Factory.checkTypes(typesToConstruct); + return Arrays.asList(typesToConstruct); + } + + private static long incrementAndGet(LongAdder longAdder) { + long value = longAdder.longValue(); + longAdder.increment(); + return value; + } +} diff --git a/src/test/java/shautvast/edie/FactoryTests.java b/src/test/java/shautvast/edie/FactoryTests.java new file mode 100644 index 0000000..ba28e67 --- /dev/null +++ b/src/test/java/shautvast/edie/FactoryTests.java @@ -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 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 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 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 personDef = Factory.define(Person.class, () -> new Person("Sander", FactoryHelper.randomInt(100))); + Definition 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 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)); + } +} \ No newline at end of file diff --git a/src/test/java/shautvast/edie/testdomain/Company.java b/src/test/java/shautvast/edie/testdomain/Company.java new file mode 100644 index 0000000..819d146 --- /dev/null +++ b/src/test/java/shautvast/edie/testdomain/Company.java @@ -0,0 +1,23 @@ +package shautvast.edie.testdomain; + +import java.util.ArrayList; +import java.util.List; + +public class Company { + private final List employees=new ArrayList<>(); + + public Company(List employees) { + this.employees.addAll(employees); + } + + public List getEmployees() { + return employees; + } + + @Override + public String toString() { + return "Company{" + + "employees=" + employees + + '}'; + } +} diff --git a/src/test/java/shautvast/edie/testdomain/Employee.java b/src/test/java/shautvast/edie/testdomain/Employee.java new file mode 100644 index 0000000..dcbb25f --- /dev/null +++ b/src/test/java/shautvast/edie/testdomain/Employee.java @@ -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 + + '}'; + } +} diff --git a/src/test/java/shautvast/edie/testdomain/Person.java b/src/test/java/shautvast/edie/testdomain/Person.java new file mode 100644 index 0000000..99bceca --- /dev/null +++ b/src/test/java/shautvast/edie/testdomain/Person.java @@ -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 + + '}'; + } +}