diff --git a/pom.xml b/pom.xml
index 453c118..0659ff9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -48,7 +48,7 @@
UTF-8
- 1.10.13
+ 1.14.12
diff --git a/projo-runtime-code-generation/pom.xml b/projo-runtime-code-generation/pom.xml
index 098564d..d376ed1 100644
--- a/projo-runtime-code-generation/pom.xml
+++ b/projo-runtime-code-generation/pom.xml
@@ -57,6 +57,7 @@
jakarta.inject
jakarta.inject-api
2.0.1
+ test
com.google.inject
diff --git a/projo-runtime-code-generation/src/main/java/pro/projo/internal/rcg/RuntimeCodeGenerationHandler.java b/projo-runtime-code-generation/src/main/java/pro/projo/internal/rcg/RuntimeCodeGenerationHandler.java
index 8d2973f..9e07635 100644
--- a/projo-runtime-code-generation/src/main/java/pro/projo/internal/rcg/RuntimeCodeGenerationHandler.java
+++ b/projo-runtime-code-generation/src/main/java/pro/projo/internal/rcg/RuntimeCodeGenerationHandler.java
@@ -23,6 +23,7 @@
import java.lang.invoke.MethodType;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
+import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.AbstractMap.SimpleEntry;
import java.util.HashMap;
@@ -74,6 +75,7 @@
import pro.projo.internal.rcg.runtime.ToStringObject;
import pro.projo.internal.rcg.runtime.ToStringValueObject;
import pro.projo.internal.rcg.runtime.ValueObject;
+import pro.projo.internal.rcg.utilities.GenericTypeResolver;
import pro.projo.internal.rcg.utilities.UncheckedMethodDescription;
import pro.projo.utilities.AnnotationList;
import pro.projo.utilities.MethodInfo;
@@ -85,8 +87,6 @@
import static java.util.stream.Stream.empty;
import static net.bytebuddy.ClassFileVersion.JAVA_V8;
import static net.bytebuddy.description.modifier.Visibility.PRIVATE;
-import static net.bytebuddy.description.type.TypeDescription.OBJECT;
-import static net.bytebuddy.description.type.TypeDescription.VOID;
import static net.bytebuddy.dynamic.loading.ClassLoadingStrategy.Default.INJECTION;
import static net.bytebuddy.implementation.bytecode.assign.Assigner.DEFAULT;
import static net.bytebuddy.implementation.bytecode.assign.Assigner.Typing.DYNAMIC;
@@ -102,7 +102,7 @@
/**
* The {@link RuntimeCodeGenerationHandler} is a {@link ProjoHandler} that generates implementation classes
-* dynamically at runtime (using the {@link ByteBuddy} library). For each, object property the generated class
+* dynamically at runtime (using the {@link ByteBuddy} library). For each object property the generated class
* will contain a field of the appropriate type, and the corresponding generated getter and setter will access
* that field directly, without using reflection. Generated implementation classes can be obtained by calling
* the {@link #getImplementationOf(Class, boolean, ClassLoader)} method.
@@ -128,6 +128,9 @@ public class RuntimeCodeGenerationHandler<_Artifact_> extends ProjoHandler<_Arti
private final static String SUFFIX = "$Projo";
+ private final static TypeDescription VOID = TypeDescription.ForLoadedType.of(void.class);
+ private final static TypeDescription OBJECT = TypeDescription.ForLoadedType.of(Object.class);
+
private static Map, Class>> baseClasses = new HashMap<>();
static
@@ -204,7 +207,7 @@ private Class extends _Artifact_> generateImplementation(Class<_Artifact_> typ
Builder<_Artifact_> builder = create(type, additionalImplements, classLoader).name(implementationName(type, defaultPackage));
TypeDescription currentType = builder.make().getTypeDescription();
return debug(getMethods(type, classLoader, additionalImplements, getter, setter, cached, overrides, returns, expects)
- .reduce(builder, (accumulator, method) -> add(accumulator, method, additionalImplements, classLoader), sequentialOnly())
+ .reduce(builder, (accumulator, method) -> add(accumulator, type, method, additionalImplements, classLoader), sequentialOnly())
.defineConstructor(PUBLIC).intercept(constructor(type, currentType, cachedMethods))
.make().load(classLoader(type, defaultPackage, classLoader), INJECTION)).getLoaded();
}
@@ -226,7 +229,7 @@ private Class extends _Artifact_> generateProxy(Class<_Artifact_> type, boolea
.name(implementationName(type, defaultPackage))
.defineField("delegate", delegateType);
builder = additionalAttributes(type, override)
- .reduce(builder, (accumulator, method) -> add(accumulator, method, emptyList(), null), sequentialOnly())
+ .reduce(builder, (accumulator, method) -> add(accumulator, type, method, emptyList(), null), sequentialOnly())
.defineConstructor(Modifier.PUBLIC)
.withParameter(delegateType)
.intercept(MethodCall.invoke(Object.class.getDeclaredConstructor())
@@ -307,11 +310,13 @@ private Builder<_Artifact_> addProxy(Builder<_Artifact_> builder, Method method,
* Adds method implementation and (if necessary) field definition for a method.
*
* @param builder the existing {@link Builder} to build upon
+ * @param declaringType the declaring type
* @param method a getter, setter, delegate or cached method
* @param additionalImplements additional interfaces implemented by means of {@link Implements} annotations
+ * @param classLoader the {@link ClassLoader}
* @return a new {@link Builder} with an additional method (and possibly an additional field)
**/
- private Builder<_Artifact_> add(Builder<_Artifact_> builder, Method method, List additionalImplements, ClassLoader classLoader)
+ private Builder<_Artifact_> add(Builder<_Artifact_> builder, Class> declaringType, Method method, List additionalImplements, ClassLoader classLoader)
{
AnnotationList annotations = new AnnotationList(method);
boolean isGetter = getter.test(method) || annotations.contains(Delegate.class) || annotations.contains(Cached.class);
@@ -319,10 +324,11 @@ private Builder<_Artifact_> add(Builder<_Artifact_> builder, Method method, List
String propertyName = matcher.propertyName(methodName);
UnaryOperator> addFieldForGetter;
Optional inject = annotations.getInject();
- Class> returnType = method.getReturnType();
+ boolean runtimeImplementation = new AnnotationList(declaringType).contains(Implements.class);
+ Type returnType = GenericTypeResolver.getReturnType(method, runtimeImplementation? method.getDeclaringClass():declaringType);
TypeDescription.Generic type = isGetter? getFieldType(annotations, returnType, classLoader):VOID.asGenericType();
addFieldForGetter = isGetter? localBuilder -> annotate(inject, localBuilder.defineField(propertyName, type, PRIVATE)):identity();
- Implementation implementation = getAccessor(method, annotations, returnType, propertyName, additionalImplements, classLoader);
+ Implementation implementation = getAccessor(method, annotations, declaringType, returnType, propertyName, additionalImplements, classLoader);
Optional returns = annotations.get(Returns.class);
Optional inherits = annotations.get(Inherits.class);
List> expects = Stream.of(method.getParameters())
@@ -335,7 +341,7 @@ private Builder<_Artifact_> add(Builder<_Artifact_> builder, Method method, List
.map(Returns::value)
.map(it -> Projo.forName(it, classLoader))
.map(Class.class::cast)
- .orElse(returnType);
+ .orElse(classOf(returnType));
@SuppressWarnings("rawtypes")
List parameterTypes = IntStream.range(0, expects.size())
.mapToObj(index -> expects.get(index)
@@ -365,7 +371,16 @@ private Builder<_Artifact_> add(Builder<_Artifact_> builder, Method method, List
return createMethod.apply(addFieldForGetter.apply(builder)).intercept(implementation);
}
- Implementation getAccessor(Method method, AnnotationList annotations, Type returnType, String property, List additionalImplements, ClassLoader classLoader)
+ Implementation getAccessor
+ (
+ Method method,
+ AnnotationList annotations,
+ Class> declaringType,
+ Type returnType,
+ String property,
+ List additionalImplements,
+ ClassLoader classLoader
+ )
{
Optional inject;
if ((inject = annotations.getInject()).isPresent())
@@ -493,22 +508,22 @@ private ByteBuddy codeGenerator()
* {@code Cache}
* for all other methods returning type {@code T}, the field type is {@code T}
*
- * @param inject an {@link Optional} {@link javax.inject.Inject} annotation
- * @param cached an {@link Optional} {@link pro.projo.annotations.Cached} annotation
+ * @param annotations a list of annotations that are present on the method
* @param originalReturnType the return type of the method
+ * @param classLoader the {@link ClassLoader}
* @return the appropriate field type for the method
**/
- Generic getFieldType(AnnotationList annotations, Class> originalReturnType, ClassLoader classLoader)
+ Generic getFieldType(AnnotationList annotations, Type originalReturnType, ClassLoader classLoader)
{
if (!annotations.containsInject() && !annotations.contains(Cached.class))
{
- return Generic.Builder.rawType(originalReturnType).build();
+ return Generic.Builder.rawType(classOf(originalReturnType)).build();
}
else
{
Optional> optionalContainer = annotations.getInject().map(inject -> Projo.forName(provider(inject), classLoader));
Class> container = optionalContainer.orElse(Cache.class);
- Type wrappedType = MethodType.methodType(originalReturnType).wrap().returnType();
+ Type wrappedType = MethodType.methodType(classOf(originalReturnType)).wrap().returnType();
Optional returns = annotations.get(Returns.class);
if (returns.isPresent())
{
@@ -605,6 +620,19 @@ private List getImplements(Class<_Artifact_> type)
return implement.collect(toList());
}
+ private Class> classOf(Type type)
+ {
+ if (type instanceof Class)
+ {
+ return (Class>)type;
+ }
+ if (type instanceof ParameterizedType)
+ {
+ return (Class>)((ParameterizedType)type).getRawType();
+ }
+ throw new UnsupportedOperationException(type + " (" + type.getClass() + ")");
+ }
+
private TypeDefinition type(Class> type)
{
return new TypeDescription.ForLoadedType(type);
diff --git a/projo-runtime-code-generation/src/main/java/pro/projo/internal/rcg/utilities/GenericTypeResolver.java b/projo-runtime-code-generation/src/main/java/pro/projo/internal/rcg/utilities/GenericTypeResolver.java
new file mode 100644
index 0000000..e744c70
--- /dev/null
+++ b/projo-runtime-code-generation/src/main/java/pro/projo/internal/rcg/utilities/GenericTypeResolver.java
@@ -0,0 +1,179 @@
+// //
+// Copyright 2013 - 2024 Peter Walser, Mirko Raner //
+// //
+// Licensed under the Apache License, Version 2.0 (the "License"); //
+// you may not use this file except in compliance with the License. //
+// You may obtain a copy of the License at //
+// //
+// http://www.apache.org/licenses/LICENSE-2.0 //
+// //
+// Unless required by applicable law or agreed to in writing, software //
+// distributed under the License is distributed on an "AS IS" BASIS, //
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. //
+// See the License for the specific language governing permissions and //
+// limitations under the License. //
+// //
+package pro.projo.internal.rcg.utilities;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
+import static java.util.stream.Collectors.toList;
+
+/**
+* The {@link GenericTypeResolver} is a utility class whose main purpose is to encapsulate
+* Peter Walser's generic type argument resolution code as found in
+* https://stackoverflow.com/questions/17297308#answer-17301917.
+* Per Stack Overflow's licensing rules,
+* section 6, subsection "Subscriber Content", source code snippets posted on the site
+* are licensed under CC BY-SA 4.0
+* and are hereby further distributed under the Apache License, version 2.0, in a manner
+* compatible with CC BY-SA 4.0, including full attribution of authorship. Author and
+* copyright holder for the {@link #resolveActualTypeArgs(Class, Class, Type...)}
+* method is Peter Walser.
+*
+* @author Peter Walser
+* @author Mirko Raner
+**/
+public class GenericTypeResolver
+{
+ /**
+ * Gets the actual return type of a method with respect to an actual type, including
+ * scenarios where the originally declared return type is a type variable.
+ *
+ * @param the base type that contains the original method declaration
+ * @param originalMethodDeclaration the original {@link Method} declaration
+ * @param actualType the actual concrete type (must be a sub-type of {@code T})
+ * @return the actual resolved return type (or the bounds type if there was no type variable binding)
+ **/
+ public static Type getReturnType(Method originalMethodDeclaration, Class extends T> actualType)
+ {
+ Type genericReturnType = originalMethodDeclaration.getGenericReturnType();
+ if (genericReturnType instanceof TypeVariable)
+ {
+ String variableName = ((TypeVariable>)genericReturnType).getName();
+ @SuppressWarnings("unchecked")
+ Class baseClass = (Class)originalMethodDeclaration.getDeclaringClass();
+ TypeVariable>[] typeVariables = baseClass.getTypeParameters();
+ List variableNames = Stream.of(typeVariables).map(TypeVariable::getName).collect(toList());
+ int variableIndex = variableNames.indexOf(variableName);
+ Class extends T> offspring = actualType;
+ Type[] resolvedTypeArguments = resolveActualTypeArgs(offspring, baseClass);
+ Type resolvedType = resolvedTypeArguments[variableIndex];
+ if (resolvedType instanceof TypeVariable)
+ {
+ return originalMethodDeclaration.getReturnType();
+ }
+ else
+ {
+ return resolvedType;
+ }
+ }
+ else
+ {
+ return genericReturnType;
+ }
+ }
+
+ /**
+ * Resolves the actual generic type arguments for a base class, as viewed from a subclass or implementation.
+ *
+ * @param base type
+ * @param offspring class or interface subclassing or extending the base type
+ * @param base base class
+ * @param actualArgs the actual type arguments passed to the offspring class
+ * @return actual generic type arguments, must match the type parameters of the offspring class. If omitted, the
+ * type parameters will be used instead.
+ **/
+ public static Type[] resolveActualTypeArgs (Class extends T> offspring, Class base, Type... actualArgs)
+ {
+ assert offspring != null;
+ assert base != null;
+ assert actualArgs.length == 0 || actualArgs.length == offspring.getTypeParameters().length;
+
+ // If actual types are omitted, the type parameters will be used instead.
+ if (actualArgs.length == 0)
+ {
+ actualArgs = offspring.getTypeParameters();
+ }
+ // map type parameters into the actual types
+ Map typeVariables = new HashMap();
+ for (int i = 0; i < actualArgs.length; i++)
+ {
+ TypeVariable> typeVariable = (TypeVariable>) offspring.getTypeParameters()[i];
+ typeVariables.put(typeVariable.getName(), actualArgs[i]);
+ }
+
+ // Find direct ancestors (superclass, interfaces)
+ List ancestors = new LinkedList();
+ if (offspring.getGenericSuperclass() != null)
+ {
+ ancestors.add(offspring.getGenericSuperclass());
+ }
+ for (Type t: offspring.getGenericInterfaces())
+ {
+ ancestors.add(t);
+ }
+
+ // Recurse into ancestors (superclass, interfaces)
+ for (Type type: ancestors)
+ {
+ if (type instanceof Class>)
+ {
+ // ancestor is non-parameterized. Recurse only if it matches the base class.
+ Class> ancestorClass = (Class>) type;
+ if (base.isAssignableFrom(ancestorClass))
+ {
+ @SuppressWarnings("unchecked")
+ Type[] result = resolveActualTypeArgs((Class extends T>) ancestorClass, base);
+ if (result != null)
+ {
+ return result;
+ }
+ }
+ }
+ if (type instanceof ParameterizedType)
+ {
+ // ancestor is parameterized. Recurse only if the raw type matches the base class.
+ ParameterizedType parameterizedType = (ParameterizedType)type;
+ Type rawType = parameterizedType.getRawType();
+ if (rawType instanceof Class>)
+ {
+ Class> rawTypeClass = (Class>) rawType;
+ if (base.isAssignableFrom(rawTypeClass))
+ {
+ // loop through all type arguments and replace type variables with the actually known types
+ List resolvedTypes = new LinkedList();
+ for (Type t: parameterizedType.getActualTypeArguments())
+ {
+ if (t instanceof TypeVariable>)
+ {
+ Type resolvedType = typeVariables.get(((TypeVariable>)t).getName());
+ resolvedTypes.add(resolvedType != null? resolvedType:t);
+ } else
+ {
+ resolvedTypes.add(t);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ Type[] result = resolveActualTypeArgs((Class extends T>) rawTypeClass, base, resolvedTypes.toArray(new Type[] {}));
+ if (result != null)
+ {
+ return result;
+ }
+ }
+ }
+ }
+ }
+
+ // we have a result if we reached the base class.
+ return offspring.equals(base) ? actualArgs : null;
+ }
+}
diff --git a/projo-runtime-code-generation/src/main/resources/META-INF/MANIFEST.MF b/projo-runtime-code-generation/src/main/resources/META-INF/MANIFEST.MF
index 655280d..bd1075a 100644
--- a/projo-runtime-code-generation/src/main/resources/META-INF/MANIFEST.MF
+++ b/projo-runtime-code-generation/src/main/resources/META-INF/MANIFEST.MF
@@ -3,11 +3,11 @@ Bundle-ManifestVersion: 2
Bundle-Name: pro.projo.projo-runtime-code-generation
Bundle-Vendor: Mirko Raner
Bundle-Version: ${parsedVersion.majorVersion}.${parsedVersion.minorVersion}.${parsedVersion.incrementalVersion}
-Bundle-SymbolicName: pro.projo.projo-runtime-code-generation; singleton:=true
Bundle-ActivationPolicy: lazy
+Bundle-SymbolicName: pro.projo.projo-runtime-code-generation; singleton:=true
Automatic-Module-Name: pro.projo.projo-runtime-code-generation
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
Fragment-Host: pro.projo.projo
Export-Package: pro.projo.internal.rcg; x-internal:=true,
pro.projo.internal.rcg.runtime; x-internal:=true
-Require-Bundle: net.bytebuddy.byte-buddy; bundle-version="1.9.0"
+Require-Bundle: net.bytebuddy.byte-buddy; bundle-version="1.14.12"
diff --git a/projo-runtime-code-generation/src/test/java/pro/projo/integration/tests/ProjoRCGManifestIT.java b/projo-runtime-code-generation/src/test/java/pro/projo/integration/tests/ProjoRCGManifestIT.java
index 9a810a0..b3745d6 100644
--- a/projo-runtime-code-generation/src/test/java/pro/projo/integration/tests/ProjoRCGManifestIT.java
+++ b/projo-runtime-code-generation/src/test/java/pro/projo/integration/tests/ProjoRCGManifestIT.java
@@ -1,5 +1,5 @@
// //
-// Copyright 2022 Mirko Raner //
+// Copyright 2022 - 2024 Mirko Raner //
// //
// Licensed under the Apache License, Version 2.0 (the "License"); //
// you may not use this file except in compliance with the License. //
@@ -79,7 +79,7 @@ public String expectedFragmentHost()
@Override
public Set expectedRequireBundle()
{
- return Collections.singleton("net.bytebuddy.byte-buddy; bundle-version=\"1.9.0\"");
+ return Collections.singleton("net.bytebuddy.byte-buddy; bundle-version=\"1.14.12\"");
}
@Test
diff --git a/projo/src/main/java/pro/projo/internal/Numbers.java b/projo/src/main/java/pro/projo/internal/Numbers.java
index 42ee283..52e7734 100644
--- a/projo/src/main/java/pro/projo/internal/Numbers.java
+++ b/projo/src/main/java/pro/projo/internal/Numbers.java
@@ -1,5 +1,5 @@
// //
-// Copyright 2019 Mirko Raner //
+// Copyright 2019 - 2024 Mirko Raner //
// //
// Licensed under the Apache License, Version 2.0 (the "License"); //
// you may not use this file except in compliance with the License. //
@@ -172,6 +172,12 @@ <_Numeric_ extends Number> _Numeric_ to(Class<_Numeric_> type)
Function extends Number, ? extends Number> function = map.get(new SimpleEntry<>(number.getClass(), type));
if (function == null)
{
+ if (Number.class.equals(type))
+ {
+ @SuppressWarnings("unchecked")
+ _Numeric_ numeric = (_Numeric_)number;
+ return numeric;
+ }
throw new IllegalArgumentException(number.getClass() + " cannot be cast to " + type);
}
@SuppressWarnings("unchecked")
diff --git a/projo/src/test/java/pro/projo/BuilderTest.java b/projo/src/test/java/pro/projo/BuilderTest.java
index cf8d935..33338b9 100644
--- a/projo/src/test/java/pro/projo/BuilderTest.java
+++ b/projo/src/test/java/pro/projo/BuilderTest.java
@@ -1,5 +1,5 @@
// //
-// Copyright 2019 - 2021 Mirko Raner //
+// Copyright 2019 - 2024 Mirko Raner //
// //
// Licensed under the Apache License, Version 2.0 (the "License"); //
// you may not use this file except in compliance with the License. //
@@ -16,7 +16,10 @@
package pro.projo;
import org.junit.Test;
+import java.util.AbstractMap.SimpleEntry;
+import java.util.Map.Entry;
import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
public class BuilderTest
@@ -46,6 +49,33 @@ static interface LongInterval
long end();
}
+ static interface Quantity
+ {
+ VALUE value();
+ }
+
+ static interface RealQuantity extends Quantity
+ {
+ // No additional methods
+ }
+
+ static interface StringBoolean extends Entry
+ {
+ // No additional methods
+ }
+
+ @SuppressWarnings("rawtypes")
+ static interface Something<$ extends Something> {}
+
+ @SuppressWarnings("rawtypes")
+ static interface SIQuantity<$ extends SIQuantity, VALUE extends Number> extends Something<$>
+ {
+ VALUE value();
+ }
+
+ @SuppressWarnings("rawtypes")
+ static interface RealSIQuantity<$ extends RealSIQuantity> extends SIQuantity<$, Double> {}
+
@Test
public void testBuilderWithTwoProperties()
{
@@ -158,4 +188,60 @@ public void testBuilderWithIncorrectPropertyTypeAndLossyCast()
IllegalArgumentException.class,
() -> Projo.builder(Interval.class).with(Interval::begin, -1).with(Interval::end, 1L).build());
}
+
+ @Test
+ public void testRealQuantityWithGenericType()
+ {
+ Number fortyTwo = Double.valueOf(42D);
+ RealQuantity quantity = Projo.builder(RealQuantity.class).with(Quantity::value, fortyTwo).build();
+ assertEquals(Double.valueOf(42D), quantity.value());
+ }
+
+ @Test
+ public void testRealQuantityWithGenericTypeButSpecificValue()
+ {
+ RealQuantity quantity = Projo.builder(RealQuantity.class).with(Quantity::value, Double.valueOf(42D)).build();
+ assertEquals(Double.valueOf(42D), quantity.value());
+ }
+
+ @Test
+ public void testRealQuantityWithSpecificType()
+ {
+ RealQuantity quantity = Projo.builder(RealQuantity.class).with(RealQuantity::value, Double.valueOf(42D)).build();
+ assertEquals(Double.valueOf(42D), quantity.value());
+ }
+
+ @Test
+ public void testRealSIQuantityWithGenericTypeButSpecificValue()
+ {
+ RealSIQuantity> quantity = Projo.builder(RealSIQuantity.class).with(SIQuantity::value, Double.valueOf(42D)).build();
+ assertEquals(Double.valueOf(42D), quantity.value());
+ }
+
+ @Test
+ public void testRealSIQuantityWithSpecificType()
+ {
+ RealSIQuantity> quantity = Projo.builder(RealSIQuantity.class).with(RealSIQuantity::value, Double.valueOf(42D)).build();
+ assertEquals(Double.valueOf(42D), quantity.value());
+ }
+
+ @Test
+ public void testMapEntry()
+ {
+ StringBoolean pair = Projo.builder(StringBoolean.class)
+ .with(StringBoolean::getKey, "answer")
+ .with(StringBoolean::getValue, true)
+ .build();
+ assertEquals(new SimpleEntry("answer", true), pair);
+ }
+
+ @Test
+ public void testMapEntryWithGenericMethods()
+ {
+ StringBoolean pair = Projo.builder(StringBoolean.class)
+ .with(Entry::getKey, "answer")
+ .with(Entry::getValue, true)
+ .build();
+ assertEquals(new SimpleEntry("answer", true), pair);
+ }
}