Skip to content

Commit

Permalink
Fix FunctionalInterface supports equals, hashCode, toString
Browse files Browse the repository at this point in the history
  • Loading branch information
seongahjo committed Feb 8, 2025
1 parent fdbecc3 commit 68fc772
Show file tree
Hide file tree
Showing 4 changed files with 63 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,12 @@
package com.navercorp.fixturemonkey.api.introspector;

import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.Function;
import java.util.function.Supplier;

import org.apiguardian.api.API;

Expand All @@ -31,6 +35,14 @@

@API(since = "1.0.21", status = API.Status.EXPERIMENTAL)
public final class FunctionalInterfaceArbitraryIntrospector implements ArbitraryIntrospector {
private static final Map<Class<?>, String> FUNCTIONAL_INVOKE_METHOD_NAMES_BY_ASSIGNABLE_TYPE = new HashMap<>();
private static final String INVOKE_METHOD_NAME = "invoke";

static {
FUNCTIONAL_INVOKE_METHOD_NAMES_BY_ASSIGNABLE_TYPE.put(Function.class, "apply");
FUNCTIONAL_INVOKE_METHOD_NAMES_BY_ASSIGNABLE_TYPE.put(Supplier.class, "get");
}

@Override
public ArbitraryIntrospectorResult introspect(ArbitraryGeneratorContext context) {
ArbitraryProperty property = context.getArbitraryProperty();
Expand All @@ -47,18 +59,26 @@ public ArbitraryIntrospectorResult introspect(ArbitraryGeneratorContext context)
return new ArbitraryIntrospectorResult(result);
}

@SuppressWarnings("SuspiciousInvocationHandlerImplementation")
private <T> Object toFunctionalInterface(Class<?> type, T value) {
InvocationHandlerBuilder invocationHandlerBuilder = new InvocationHandlerBuilder(
type,
new HashMap<>()
);

for (Entry<Class<?>, String> entry : FUNCTIONAL_INVOKE_METHOD_NAMES_BY_ASSIGNABLE_TYPE.entrySet()) {
if (entry.getKey().isAssignableFrom(type)) {
invocationHandlerBuilder.put(entry.getValue(), value);
}
}

if (invocationHandlerBuilder.isEmpty()) {
invocationHandlerBuilder.put(INVOKE_METHOD_NAME, value);
}

return Proxy.newProxyInstance(
type.getClassLoader(),
new Class[] {type},
(proxy, method, args) -> {
if (method != null && "equals".equals(method.getName())) {
return Objects.equals(args[0], value);
}

return value;
}
invocationHandlerBuilder.build()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ final class InvocationHandlerBuilder {
private static final String HASH_CODE_METHOD = "hashCode";
private static final String EQUALS_METHOD = "equals";
private static final String TO_STRING_METHOD = "toString";
private static final String INVOKE_METHOD = "invoke";

private final Class<?> type;
private final Map<String, Object> generatedValuesByMethodName;
Expand All @@ -52,6 +53,11 @@ void put(String methodName, Object value) {

InvocationHandler build() {
return (proxy, method, args) -> {
if (method == null) {
// invoked by DecomposedContainerValueFactory to decompose the functional interface
return generatedValuesByMethodName.get(INVOKE_METHOD);
}

if (HASH_CODE_METHOD.equals(method.getName()) && args == null) {
return generatedValuesByMethodName.values().hashCode();
}
Expand All @@ -65,11 +71,6 @@ InvocationHandler build() {
return toString(proxy);
}

// check no-args
if (args != null) {
return null;
}

return generatedValuesByMethodName.get(method.getName());
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ final class InvocationHandlerBuilder {
private static final String HASH_CODE_METHOD = "hashCode";
private static final String EQUALS_METHOD = "equals";
private static final String TO_STRING_METHOD = "toString";
private static final String INVOKE_METHOD = "invoke";

private final Class<?> type;
private final Map<String, Object> generatedValuesByMethodName;
Expand All @@ -53,6 +54,11 @@ void put(String methodName, Object value) {

InvocationHandler build() {
return (proxy, method, args) -> {
if (method == null) {
// invoked by DecomposedContainerValueFactory to decompose the functional interface
return generatedValuesByMethodName.get(INVOKE_METHOD);
}

if (method.isDefault()) {
return InvocationHandler.invokeDefault(proxy, method, args);
}
Expand All @@ -70,11 +76,6 @@ InvocationHandler build() {
return toString(proxy);
}

// check no-args
if (args != null) {
return null;
}

return generatedValuesByMethodName.get(method.getName());
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ import com.navercorp.fixturemonkey.tests.kotlin.ImmutableJavaTestSpecs.ArrayObje
import com.navercorp.fixturemonkey.tests.kotlin.ImmutableJavaTestSpecs.NestedArrayObject
import com.navercorp.fixturemonkey.tests.kotlin.JavaConstructorTestSpecs.JavaTypeObject
import org.assertj.core.api.BDDAssertions.then
import org.assertj.core.api.BDDAssertions.thenNoException
import org.assertj.core.api.BDDAssertions.thenThrownBy
import org.junit.jupiter.api.RepeatedTest
import org.junit.jupiter.api.Test
Expand Down Expand Up @@ -1352,6 +1353,27 @@ class KotlinTest {
then(actual).allMatch { it.value == "1" }
}

@Test
fun sampleFunctionalObject() {
data class FunctionObject(
val value: () -> Int
)

val actual = SUT.giveMeOne<FunctionObject>().value()

then(actual).isNotNull()
}

@Test
fun toStringFunctionalObjectNotThrows() {
data class FunctionObject(
val value: () -> Int
)

thenNoException()
.isThrownBy { SUT.giveMeOne<FunctionObject>().toString() }
}

companion object {
private val SUT: FixtureMonkey = FixtureMonkey.builder()
.plugin(KotlinPlugin())
Expand Down

0 comments on commit 68fc772

Please sign in to comment.