[construct, core, weaver-pojo] Replace simple class injections with more sophisticated construction

This commit is contained in:
2025-04-23 11:16:15 +02:00
parent de92d6843f
commit 6e79957207
22 changed files with 1163 additions and 223 deletions

View File

@@ -0,0 +1,19 @@
package de.siphalor.tweed5.construct.api;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Provides more information about a parameter of a {@link TweedConstruct}.
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface ConstructParameter {
/**
* Allows defining a named parameter with the given name.
* Parameters with this set are not considered as typed parameters.
*/
String name();
}

View File

@@ -0,0 +1,21 @@
package de.siphalor.tweed5.construct.api;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Indicates a method or constructor that should be used for construction using {@link TweedConstructFactory}.
* <p>
* There must only be a single annotation for a certain target class on any constructor or static method of a class.
*/
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface TweedConstruct {
/**
* Defines the target base class that this constructor may be used to create.
* This is the base class defined in {@link TweedConstructFactory#builder(Class)}.
*/
Class<?> value();
}

View File

@@ -0,0 +1,99 @@
package de.siphalor.tweed5.construct.api;
import de.siphalor.tweed5.construct.impl.TweedConstructFactoryImpl;
import org.jetbrains.annotations.CheckReturnValue;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* A factory that allows to construct instances of subclasses of a specific type.
* <p>
* This factory basically extends the interfaces' contract to include
* a public constructor or public static method with any of the defined arguments.
* <p>
* The factory should usually be defined as a public static final member of the base class.
*
* @param <T> the base class/interface
*/
public interface TweedConstructFactory<T> {
/**
* Starts building a new factory for the given base class.
*/
static <T> TweedConstructFactory.@NotNull FactoryBuilder<T> builder(Class<T> baseClass) {
return TweedConstructFactoryImpl.builder(baseClass);
}
/**
* Starts the instantiation process for a subclass.
* All defined arguments must be bound to values.
*/
@CheckReturnValue
@Contract(pure = true)
<C extends T> @NotNull Construct<C> construct(@NotNull Class<C> subClass);
/**
* Builder for the factory.
*/
interface FactoryBuilder<T> {
/**
* Defines a new typed argument of the given type.
*/
@Contract(mutates = "this", value = "_ -> this")
<A> @NotNull FactoryBuilder<T> typedArg(@NotNull Class<A> argType);
/**
* Defines a new named argument with the given name and value type.
*/
@Contract(mutates = "this", value = "_, _ -> this")
<A> @NotNull FactoryBuilder<T> namedArg(@NotNull String name, @NotNull Class<A> argType);
/**
* Builds the factory.
*/
@Contract(pure = true)
@NotNull TweedConstructFactory<T> build();
}
/**
* Builder-style helper for the instantiation process.
* <p>
* Allows to successively bind all previously defined arguments to actual values.
* <p>
* Any method call in this class may perform checks against the defined arguments and throw according exceptions.
*/
interface Construct<C> {
/**
* Binds a value to a typed argument of the exact same class.
* <p>
* This will not work if the given value merely inherits from the defined class.
* Use {@link #typedArg(Class, Object)} for these cases instead.
* @see #namedArg(String, Object)
*/
@Contract(mutates = "this", value = "_ -> this")
<A> @NotNull Construct<C> typedArg(@NotNull A value);
/**
* Binds a value to a typed argument of the given type.
* <p>
* This allows binding the value to super classes of the value.
* @see #typedArg(Object)
* @see #namedArg(String, Object)
*/
@Contract(mutates = "this", value = "_, _ -> this")
<A> @NotNull Construct<C> typedArg(@NotNull Class<? super A> argType, @Nullable A value);
/**
* Binds a value to a named argument.
* @see #typedArg(Object)
*/
@Contract(mutates = "this", value = "_, _ -> this")
<A> @NotNull Construct<C> namedArg(@NotNull String name, @Nullable A value);
/**
* Finishes the binding and actually constructs the class.
*/
@Contract(pure = true)
@NotNull C finish();
}
}

View File

@@ -0,0 +1,9 @@
package de.siphalor.tweed5.construct.impl;
import lombok.Value;
@Value
class Entry<K, V> {
K key;
V value;
}

View File

@@ -0,0 +1,464 @@
package de.siphalor.tweed5.construct.impl;
import de.siphalor.tweed5.construct.api.ConstructParameter;
import de.siphalor.tweed5.construct.api.TweedConstruct;
import de.siphalor.tweed5.construct.api.TweedConstructFactory;
import lombok.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.WrongMethodTypeException;
import java.lang.reflect.*;
import java.util.*;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@RequiredArgsConstructor
@Getter(AccessLevel.PACKAGE)
public class TweedConstructFactoryImpl<T> implements TweedConstructFactory<T> {
private static final int CONSTRUCTOR_MODIFIERS = Modifier.PUBLIC;
private static final int STATIC_METHOD_MODIFIERS = Modifier.PUBLIC | Modifier.STATIC;
private final Class<T> constructBaseClass;
private final Set<Class<?>> typedArgs;
private final Map<String, Class<?>> namedArgs;
private final MethodHandles.Lookup lookup = MethodHandles.publicLookup();
private final Map<Class<?>, Optional<ConstructTarget<?>>> cachedConstructTargets = new HashMap<>();
@SuppressWarnings("unused")
private final ReadWriteLock cachedConstructTargetsLock = new ReentrantReadWriteLock();
public static <T> TweedConstructFactoryImpl.FactoryBuilder<T> builder(Class<T> baseClass) {
return new FactoryBuilder<>(baseClass);
}
@Override
public <C extends T> TweedConstructFactory.@NotNull Construct<C> construct(@NotNull Class<C> subClass) {
return new Construct<>(getConstructTarget(subClass));
}
private <C extends T> @NotNull ConstructTarget<C> getConstructTarget(Class<C> type) {
ConstructTarget<C> cachedConstructTarget = readConstructTargetFromCache(type);
if (cachedConstructTarget != null) {
return cachedConstructTarget;
}
ConstructTarget<C> constructTarget = locateConstructTarget(type);
cacheConstructTarget(type, constructTarget);
return constructTarget;
}
@Locked.Read("cachedConstructTargetsLock")
private <C extends T> @Nullable ConstructTarget<C> readConstructTargetFromCache(Class<C> type) {
Optional<ConstructTarget<?>> cachedConstructTarget = cachedConstructTargets.get(type);
if (cachedConstructTarget != null) {
if (!cachedConstructTarget.isPresent()) {
throw new IllegalStateException("Could not locate construct for " + type.getName());
} else {
//noinspection unchecked
return (ConstructTarget<C>) cachedConstructTarget.get();
}
}
return null;
}
@Locked.Write("cachedConstructTargetsLock")
private <C extends T> void cacheConstructTarget(Class<C> type, ConstructTarget<C> constructTarget) {
cachedConstructTargets.put(type, Optional.of(constructTarget));
}
private <C extends T> ConstructTarget<C> locateConstructTarget(Class<C> type) {
if (!constructBaseClass.isAssignableFrom(type)) {
throw new IllegalArgumentException(
"Type " + type.getName() + " is not a subclass of " + constructBaseClass.getName()
);
}
Collection<Constructor<?>> constructorCandidates = findConstructorCandidates(type);
Collection<Method> staticConstructorCandidates = findStaticConstructorCandidates(type);
List<Executable> annotated = Stream.concat(constructorCandidates.stream(), staticConstructorCandidates.stream())
.filter(candidate -> {
TweedConstruct annotation = candidate.getAnnotation(TweedConstruct.class);
return annotation != null && annotation.value().equals(constructBaseClass);
})
.collect(Collectors.toList());
if (annotated.size() > 1) {
throw new IllegalStateException(
"Found multiple matching constructors for " + type.getName()
+ " annotated with a matching TweedConstruct for " + constructBaseClass.getName() + ": "
+ annotated
);
} else if (annotated.size() == 1) {
return resolveConstructTarget(type, annotated.get(0));
} else if (constructorCandidates.size() == 1) {
return resolveConstructTarget(type, constructorCandidates.iterator().next());
} else {
throw new IllegalStateException(
"Failed to determine actual constructor on " + type.getName()
+ " for " + constructBaseClass.getName() + ". "
+ "Constructor candidates: " + constructorCandidates + "; "
+ "Static method candidates: " + staticConstructorCandidates + ". "
+ "The desired constructor should be marked with @" + TweedConstruct.class.getName()
);
}
}
private Collection<Constructor<?>> findConstructorCandidates(Class<?> type) {
return Arrays.stream(type.getConstructors())
.filter(constructor -> (constructor.getModifiers() & CONSTRUCTOR_MODIFIERS) == CONSTRUCTOR_MODIFIERS)
.collect(Collectors.toList());
}
private Collection<Method> findStaticConstructorCandidates(Class<?> type) {
return Arrays.stream(type.getDeclaredMethods())
.filter(method -> (method.getModifiers() & STATIC_METHOD_MODIFIERS) == STATIC_METHOD_MODIFIERS)
.filter(method -> type.isAssignableFrom(method.getReturnType()))
.collect(Collectors.toList());
}
private <C extends T> ConstructTarget<C> resolveConstructTarget(Class<C> type, Executable executable) {
Object[] argOrder = new Object[executable.getParameterCount()];
Map<Class<?>, List<Parameter>> typedParameters = new HashMap<>();
Map<String, List<Parameter>> namedParameters = new HashMap<>();
Parameter[] parameters = executable.getParameters();
boolean issue = false;
for (int i = 0; i < parameters.length; i++) {
Parameter parameter = parameters[i];
ConstructParameter annotation = parameter.getAnnotation(ConstructParameter.class);
if (annotation != null) {
String name = annotation.name();
List<Parameter> named = namedParameters.computeIfAbsent(name, n -> new ArrayList<>());
named.add(parameter);
Class<?> argType = namedArgs.get(name);
argOrder[i] = name;
if (!issue && (
named.size() > 1
|| argType == null
|| !boxClass(parameter.getType()).isAssignableFrom(argType)
)) {
issue = true;
}
} else {
Class<?> paramType = boxClass(parameter.getType());
List<Parameter> typed = typedParameters.computeIfAbsent(paramType, n -> new ArrayList<>());
typed.add(parameter);
argOrder[i] = paramType;
if (!issue && (typed.size() > 1 || !typedArgs.contains(paramType))) {
issue = true;
}
}
}
if (issue) {
throw new IllegalStateException(
createConstructorTargetArgCheckFailMessage(executable, typedParameters, namedParameters)
);
}
return new ConstructTarget<>(type, argOrder, createInvokerFromCandidate(type, executable));
}
private <C> Function<Object[], C> createInvokerFromCandidate(Class<C> type, Executable executable) {
MethodHandle handle;
try {
if (executable instanceof Method) {
handle = lookup.unreflect((Method) executable);
} else if (executable instanceof Constructor) {
handle = lookup.unreflectConstructor((Constructor<?>) executable);
} else {
throw new IllegalStateException("Unsupported executable type: " + executable);
}
} catch (IllegalAccessException e) {
throw new IllegalStateException("Constructor for type " + type.getName() + " is not accessible", e);
}
return args -> {
try {
//noinspection unchecked
return (C) handle.invokeWithArguments(args);
} catch (ClassCastException | WrongMethodTypeException e) {
throw new IllegalStateException(
"Failed to construct type " + type.getName() + " as " + constructBaseClass.getName(), e
);
} catch (Throwable e) {
throw new RuntimeException(
"Uncaught exception during construct of type " + type.getName()
+ " as " + constructBaseClass.getName(),
e
);
}
};
}
private String createConstructorTargetArgCheckFailMessage(
Executable executable,
Map<Class<?>, List<Parameter>> typedParameters,
Map<String, List<Parameter>> namedParameters
) {
StringBuilder sb = new StringBuilder();
sb.append("Failed to resolve parameters for ");
sb.append(executable);
sb.append(" (for ");
sb.append(constructBaseClass.getName());
sb.append("). The following issues have been detected:");
Set<Class<?>> unexpectedTypes = new HashSet<>(typedParameters.keySet());
unexpectedTypes.removeAll(typedArgs);
if (!unexpectedTypes.isEmpty()) {
for (Class<?> unexpectedType : unexpectedTypes) {
sb.append("\n - Typed parameter of type ");
sb.append(unexpectedType.getName());
sb.append(" is not known: ");
sb.append(typedParameters.get(unexpectedType));
}
}
Set<String> unexpectedNames = new HashSet<>(namedParameters.keySet());
unexpectedNames.removeAll(namedArgs.keySet());
if (!unexpectedNames.isEmpty()) {
for (String unexpectedName : unexpectedNames) {
sb.append("\n - Named parameter ");
sb.append(unexpectedName);
sb.append(" is not known: ");
sb.append(namedParameters.get(unexpectedName));
}
}
typedParameters.entrySet().stream()
.filter(entry -> entry.getValue().size() > 1)
.forEach(entry -> sb.append("\n - Duplicate typed parameter ")
.append(entry.getKey())
.append(": ")
.append(entry.getValue()));
namedParameters.entrySet().stream()
.filter(entry -> entry.getValue().size() > 1)
.forEach(entry -> sb.append("\n - Duplicate named parameter ")
.append(entry.getKey())
.append(": ")
.append(entry.getValue()));
namedParameters.entrySet().stream()
.filter(entry -> !unexpectedNames.contains(entry.getKey()))
.flatMap(entry -> entry.getValue().stream().map(parameter -> new Entry<>(entry.getKey(), parameter)))
.forEach(entry -> {
Class<?> argType = namedArgs.get(entry.key());
if (!boxClass(entry.value().getType()).isAssignableFrom(argType)) {
sb.append("\n - Named parameter ").append(entry.key());
sb.append(" expects values of type ").append(argType.getName());
sb.append(": ").append(entry.value());
}
});
return sb.toString();
}
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public static class FactoryBuilder<T> implements TweedConstructFactory.FactoryBuilder<T> {
private final Class<T> constructBaseClass;
private final Set<Class<?>> typedArgs = new HashSet<>();
private final Map<String, Class<?>> namedArgs = new HashMap<>();
@Override
public <A> TweedConstructFactory.@NotNull FactoryBuilder<T> typedArg(@NotNull Class<A> argType) {
argType = boxClass(argType);
if (typedArgs.contains(argType)) {
throw new IllegalArgumentException("Argument for type " + argType + " has already been registered");
}
typedArgs.add(argType);
return this;
}
@Override
public <A> TweedConstructFactory.@NotNull FactoryBuilder<T> namedArg(
@NotNull String name,
@NotNull Class<A> argType
) {
Class<?> existingArgType = namedArgs.get(name);
if (existingArgType != null) {
throw new IllegalArgumentException(
"Argument for name " + name + " has already been registered; "
+ "existing type " + existingArgType.getName() + "; "
+ "new type " + argType.getName()
);
}
namedArgs.put(name, boxClass(argType));
return this;
}
@Override
public @NotNull TweedConstructFactory<T> build() {
return new TweedConstructFactoryImpl<>(
constructBaseClass,
typedArgs,
namedArgs
);
}
}
@RequiredArgsConstructor
private class Construct<C> implements TweedConstructFactory.Construct<C> {
private final ConstructTarget<C> target;
private final Map<Class<?>, Object> typedArgValues = new HashMap<>();
private final Map<String, Object> namedArgValues = new HashMap<>();
@Override
public <A> TweedConstructFactory.@NotNull Construct<C> typedArg(@NotNull A value) {
requireTypedArgExists(value.getClass(), value);
typedArgValues.put(value.getClass(), value);
return this;
}
@Override
public <A> TweedConstructFactory.@NotNull Construct<C> typedArg(@NotNull Class<? super A> argType, @Nullable A value) {
argType = boxClass(argType);
if (value != null && !argType.isAssignableFrom(value.getClass())) {
throw new IllegalArgumentException(
"Typed argument for type " + argType.getName()
+ " is of incorrect type " + value.getClass().getName()
+ ", value: " + value
);
}
requireTypedArgExists(argType, value);
typedArgValues.put(argType, value);
return this;
}
private <A> void requireTypedArgExists(@NotNull Class<?> type, @Nullable A value) {
if (!typedArgs.contains(type)) {
throw new IllegalArgumentException(
"Typed argument for type " + type.getName() + " does not exist, value: " + value
);
}
}
@Override
public <A> TweedConstructFactory.@NotNull Construct<C> namedArg(@NotNull String name, @Nullable A value) {
Class<?> argType = namedArgs.get(name);
if (argType == null) {
throw new IllegalArgumentException(
"Named argument for name " + name + " does not exist, value: " + value
);
} else if (value != null && !argType.isAssignableFrom(value.getClass())) {
throw new IllegalArgumentException(
"Named argument for name " + name + " is defined with type " + argType.getName() +
" but got type " + value.getClass().getName() + " with value " + value
);
}
namedArgValues.put(name, value);
return this;
}
@Override
public @NotNull C finish() {
checkAllArgsFilled();
Object[] argValues = new Object[target.argOrder.length];
for (int i = 0; i < target.argOrder.length; i++) {
Object arg = target.argOrder[i];
if (arg instanceof Class<?>) {
argValues[i] = typedArgValues.get((Class<?>) arg);
} else if (arg instanceof String) {
argValues[i] = namedArgValues.get((String) arg);
} else {
throw new IllegalStateException("Encountered illegal argument indicator " + arg + " at " + i);
}
}
return target.invoker.apply(argValues);
}
private void checkAllArgsFilled() {
Set<Class<?>> missingTypedArgs = Collections.emptySet();
if (typedArgValues.size() != typedArgs.size()) {
missingTypedArgs = new HashSet<>(typedArgs);
missingTypedArgs.removeAll(typedArgValues.keySet());
}
Set<String> missingNamedArgs = Collections.emptySet();
if (namedArgValues.size() != namedArgs.size()) {
missingNamedArgs = new HashSet<>(namedArgs.keySet());
missingNamedArgs.removeAll(namedArgValues.keySet());
}
if (!missingTypedArgs.isEmpty() || !missingNamedArgs.isEmpty()) {
throw new IllegalArgumentException(createMissingArgsMessage(missingTypedArgs, missingNamedArgs));
}
}
private String createMissingArgsMessage(Set<Class<?>> missingTypedArgs, Set<String> missingNamedArgs) {
StringBuilder message = new StringBuilder()
.append("Missing arguments for construction of ")
.append(target.type().getName())
.append(" as ")
.append(constructBaseClass.getName())
.append(", missing: ");
if (!missingTypedArgs.isEmpty()) {
message.append("typed args (");
boolean requiresDelimiter = false;
for (Class<?> missingTypedArg : missingTypedArgs) {
if (requiresDelimiter) {
message.append(", ");
}
message.append(missingTypedArg.getName());
requiresDelimiter = true;
}
message.append(") ");
}
if (!missingNamedArgs.isEmpty()) {
message.append("named args (");
boolean requiresDelimiter = false;
for (String missingNamedArg : missingNamedArgs) {
if (requiresDelimiter) {
message.append(", ");
}
message.append(missingNamedArg);
requiresDelimiter = true;
}
message.append(") ");
}
return message.toString();
}
}
/**
* Boxes primitive classes into their reference variants.
* Allows for easier class comparison down the line.
*/
@SuppressWarnings("unchecked")
static <V> Class<V> boxClass(Class<V> type) {
if (!type.isPrimitive()) {
return type;
}
if (type == boolean.class) {
return (Class<V>) Boolean.class;
} else if (type == byte.class) {
return (Class<V>) Byte.class;
} else if (type == char.class) {
return (Class<V>) Character.class;
} else if (type == short.class) {
return (Class<V>) Short.class;
} else if (type == int.class) {
return (Class<V>) Integer.class;
} else if (type == long.class) {
return (Class<V>) Long.class;
} else if (type == float.class) {
return (Class<V>) Float.class;
} else if (type == double.class) {
return (Class<V>) Double.class;
} else if (type == void.class) {
return (Class<V>) Void.class;
} else {
throw new IllegalArgumentException("Unsupported primitive type " + type);
}
}
@Value
private static class ConstructTarget<C> {
Class<?> type;
Object[] argOrder;
Function<Object[], C> invoker;
}
}

View File

@@ -0,0 +1,4 @@
@ApiStatus.Internal
package de.siphalor.tweed5.construct.impl;
import org.jetbrains.annotations.ApiStatus;

View File

@@ -0,0 +1,483 @@
package de.siphalor.tweed5.construct.impl;
import de.siphalor.tweed5.construct.api.ConstructParameter;
import de.siphalor.tweed5.construct.api.TweedConstruct;
import de.siphalor.tweed5.construct.api.TweedConstructFactory;
import lombok.*;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.assertj.core.api.Assertions.*;
import static org.assertj.core.api.InstanceOfAssertFactories.type;
@SuppressWarnings("unused")
class TweedConstructFactoryImplTest {
@SuppressWarnings("unchecked")
@Test
void factoryBuilder() {
val builder = TweedConstructFactoryImpl.builder(DummyBase.class);
builder.typedArg(Integer.class).typedArg(String.class);
builder.namedArg("hey", String.class).namedArg("ho", String.class);
TweedConstructFactory<DummyBase> factory = builder.build();
assertThat(factory)
.asInstanceOf(type(TweedConstructFactoryImpl.class))
.satisfies(
f -> assertThat(f.constructBaseClass()).isEqualTo(DummyBase.class),
f -> assertThat(f.typedArgs())
.containsExactlyInAnyOrder(Integer.class, String.class),
f -> assertThat(f.namedArgs())
.containsEntry("hey", String.class)
.containsEntry("ho", String.class)
.hasSize(2)
);
}
@SuppressWarnings("unchecked")
@Test
void factoryBuilderPrimitives() {
val builder = TweedConstructFactoryImpl.builder(DummyBase.class);
builder.typedArg(int.class).typedArg(long.class);
builder.namedArg("bool", boolean.class).namedArg("byte", byte.class);
TweedConstructFactory<DummyBase> factory = builder.build();
assertThat(factory)
.asInstanceOf(type(TweedConstructFactoryImpl.class))
.satisfies(
f -> assertThat(f.typedArgs())
.containsExactlyInAnyOrder(Integer.class, Long.class),
f -> assertThat(f.namedArgs())
.containsEntry("bool", Boolean.class)
.containsEntry("byte", Byte.class)
.hasSize(2)
);
}
@Test
void factoryBuilderDuplicateTypedArgs() {
val builder = TweedConstructFactoryImpl.builder(DummyBase.class);
assertThatThrownBy(() -> {
builder.typedArg(Integer.class).typedArg(String.class).typedArg(Integer.class);
builder.build();
}).isInstanceOf(IllegalArgumentException.class).hasMessageContaining("java.lang.Integer");
}
@Test
void factoryBuilderDuplicateNamedArgs() {
val builder = TweedConstructFactoryImpl.builder(DummyBase.class);
assertThatThrownBy(() -> {
builder.namedArg("hey", String.class).namedArg("ho", String.class).namedArg("hey", Integer.class);
builder.build();
}).isInstanceOf(IllegalArgumentException.class).hasMessageContaining("hey");
}
@Test
void constructMissingInheritance() {
val factory = TweedConstructFactoryImpl.builder(DummyBase.class).typedArg(Integer.class).build();
//noinspection unchecked,RedundantCast
assertThatThrownBy(() ->
factory.construct((Class<? extends DummyBase>) (Class<?>) MissingInheritance.class)
).isInstanceOf(IllegalArgumentException.class).hasMessageContaining(DummyBase.class.getName());
}
@Test
void constructPrivateConstructor() {
val factory = TweedConstructFactoryImpl.builder(DummyBase.class).typedArg(Integer.class).build();
assertThatThrownBy(() -> factory.construct(PrivateConstructor.class)).isInstanceOf(IllegalStateException.class);
}
@Test
void constructConflictingConstructors() {
val factory = TweedConstructFactoryImpl.builder(DummyBase.class).typedArg(Integer.class).build();
assertThatThrownBy(() -> factory.construct(ConstructorConflict.class))
.isInstanceOf(IllegalStateException.class);
}
@Test
void constructConflictingStatics() {
val factory = TweedConstructFactoryImpl.builder(DummyBase.class).typedArg(Integer.class).build();
assertThatThrownBy(() -> factory.construct(StaticConflict.class))
.isInstanceOf(IllegalStateException.class);
}
@Test
void constructConflictingMixed() {
val factory = TweedConstructFactoryImpl.builder(DummyBase.class).typedArg(Integer.class).build();
assertThatThrownBy(() -> factory.construct(MixedConflict.class))
.isInstanceOf(IllegalStateException.class);
}
@Test
void constructMissingTypedValue() {
val factory = TweedConstructFactoryImpl.builder(DummyBase.class)
.typedArg(Integer.class)
.namedArg("user", String.class)
.namedArg("context", String.class)
.build();
assertThatThrownBy(() ->
factory.construct(SingleConstructor.class)
.namedArg("user", "Siphalor")
.namedArg("context", "world")
.finish()
).isInstanceOf(IllegalArgumentException.class).hasMessageContaining("java.lang.Integer");
}
@Test
void constructMissingNamedValue() {
val factory = TweedConstructFactoryImpl.builder(DummyBase.class)
.typedArg(Integer.class)
.namedArg("user", String.class)
.namedArg("context", String.class)
.build();
assertThatThrownBy(() ->
factory.construct(SingleConstructor.class)
.typedArg(123)
.namedArg("context", "world")
.finish()
).isInstanceOf(IllegalArgumentException.class).hasMessageContaining("user");
}
@Test
void constructForSingleConstructor() {
val factory = TweedConstructFactoryImpl.builder(DummyBase.class)
.typedArg(Integer.class)
.namedArg("user", String.class)
.namedArg("context", String.class)
.namedArg("other", String.class)
.build();
val result = factory.construct(SingleConstructor.class)
.typedArg(123)
.namedArg("user", "Siphalor")
.namedArg("context", "world")
.namedArg("other", "something")
.finish();
assertThat(result).isEqualTo(new SingleConstructor(123, "Siphalor", "world"));
}
@Test
void constructForStatic() {
val factory = TweedConstructFactoryImpl.builder(DummyBase.class)
.typedArg(Integer.class)
.namedArg("user", String.class)
.namedArg("context", String.class)
.namedArg("other", String.class)
.build();
val result = factory.construct(Static.class)
.typedArg(123)
.namedArg("user", "Siphalor")
.namedArg("context", "world")
.namedArg("other", "something")
.finish();
assertThat(result).isEqualTo(new Static(1230, "Siphalor", "static"));
}
@ParameterizedTest
@CsvSource(
{
"de.siphalor.tweed5.construct.impl.TweedConstructFactoryImplTest$DummyBase, base, 4560",
"de.siphalor.tweed5.construct.impl.TweedConstructFactoryImplTest$DummyOtherBase, other, -456",
"de.siphalor.tweed5.construct.impl.TweedConstructFactoryImplTest$DummyAltBase, alt, -4560",
}
)
void constructFindBase(Class<? super FindBase> base, String origin, int value) {
val factory = TweedConstructFactoryImpl.builder(base).typedArg(int.class).build();
val result = factory.construct(FindBase.class).typedArg(456).finish();
assertThat(result.origin()).isEqualTo(origin);
assertThat(result.value()).isEqualTo(value);
}
@Test
void constructPrimitives() {
val factory = TweedConstructFactoryImpl.builder(DummyBase.class)
.typedArg(Integer.class)
.typedArg(long.class)
.build();
val result = factory.construct(Primitives.class)
.typedArg(1)
.typedArg(2L)
.finish();
assertThat(result).isEqualTo(new Primitives(1, 2L));
}
@Test
void constructNamedCasting() {
val factory = TweedConstructFactoryImpl.builder(DummyBase.class).namedArg("test", Integer.class).build();
val result = factory.construct(NamedCasting.class).namedArg("test", 1234).finish();
assertThat(result.value()).isEqualTo(1234);
}
@Test
void constructDuplicateParams() {
val factory = TweedConstructFactoryImpl.builder(DummyBase.class)
.typedArg(String.class)
.typedArg(Long.class)
.namedArg("number", int.class)
.build();
assertThatThrownBy(() -> factory.construct(DuplicateParams.class))
.isInstanceOf(IllegalStateException.class)
.message()
.contains("java.lang.String", "number")
.containsIgnoringCase("typed")
.containsIgnoringCase("named")
.hasLineCount(3);
}
@Test
void constructUnexpectedTypedParameter() {
val factory = TweedConstructFactoryImpl.builder(DummyBase.class)
.typedArg(Long.class)
.namedArg("user", String.class)
.build();
assertThatThrownBy(() -> factory.construct(Static.class))
.isInstanceOf(IllegalStateException.class)
.message()
.contains("java.lang.Integer")
.containsIgnoringCase("typed")
.hasLineCount(2);
}
@Test
void constructUnexpectedNamedParameter() {
val factory = TweedConstructFactoryImpl.builder(DummyBase.class)
.typedArg(Integer.class)
.namedArg("other", String.class)
.build();
assertThatThrownBy(() -> factory.construct(Static.class))
.isInstanceOf(IllegalStateException.class)
.message()
.contains("user", "java.lang.String")
.containsIgnoringCase("named")
.hasLineCount(2);
}
@Test
void constructIllegalNamedType() {
val factory = TweedConstructFactoryImpl.builder(DummyBase.class)
.typedArg(Integer.class)
.namedArg("user", Long.class)
.build();
assertThatThrownBy(() -> factory.construct(Static.class))
.isInstanceOf(IllegalStateException.class)
.message()
.contains("user", "java.lang.String", "java.lang.Long")
.containsIgnoringCase("named")
.hasLineCount(2);
}
@Test
void constructFinishUnknownTypedArgument() {
val factory = TweedConstructFactoryImpl.builder(DummyBase.class)
.typedArg(Integer.class)
.namedArg("user", String.class)
.build();
assertThatThrownBy(() ->
factory.construct(Static.class)
.typedArg(12)
.namedArg("user", "Someone")
.typedArg(567L)
.finish()
).isInstanceOf(IllegalArgumentException.class).hasMessageContaining("java.lang.Long");
}
@Test
void constructFinishUnknownNamedArgument() {
val factory = TweedConstructFactoryImpl.builder(DummyBase.class)
.typedArg(Integer.class)
.namedArg("user", String.class)
.build();
assertThatThrownBy(() ->
factory.construct(Static.class)
.typedArg(12)
.namedArg("user", "Someone")
.namedArg("other", "test")
.finish()
).isInstanceOf(IllegalArgumentException.class).hasMessageContaining("other");
}
@Test
void constructFinishNamedArgumentWrongType() {
val factory = TweedConstructFactoryImpl.builder(DummyBase.class)
.typedArg(Integer.class)
.namedArg("user", String.class)
.build();
assertThatThrownBy(() ->
factory.construct(Static.class)
.typedArg(12)
.namedArg("user", 456L)
.finish())
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("user", "java.lang.String", "java.lang.Long");
}
@Test
void constructFinishInconsistentNamedArgument() {
val factory = TweedConstructFactoryImpl.builder(DummyBase.class)
.typedArg(Integer.class)
.namedArg("user", String.class)
.build();
//noinspection unchecked
assertThatThrownBy(() ->
factory.construct(Static.class)
.typedArg((Class<Object>)(Class<?>) String.class, 123)
.finish()
).isInstanceOf(IllegalArgumentException.class);
}
@ParameterizedTest
@CsvSource(
{
"boolean, java.lang.Boolean",
"java.lang.Boolean, java.lang.Boolean",
"byte, java.lang.Byte",
"java.lang.Byte, java.lang.Byte",
"char, java.lang.Character",
"java.lang.Character, java.lang.Character",
"short, java.lang.Short",
"java.lang.Short, java.lang.Short",
"int, java.lang.Integer",
"java.lang.Integer, java.lang.Integer",
"long, java.lang.Long",
"java.lang.Long, java.lang.Long",
"float, java.lang.Float",
"java.lang.Float, java.lang.Float",
"double, java.lang.Double",
"java.lang.Double, java.lang.Double",
"void, java.lang.Void",
"java.lang.Void, java.lang.Void",
"java.lang.String, java.lang.String",
}
)
void boxClass(Class<?> type, Class<?> expected) {
assertThat(TweedConstructFactoryImpl.boxClass(type)).isEqualTo(expected);
}
interface DummyBase {
}
interface DummyOtherBase {
}
interface DummyAltBase {
}
public static class MissingInheritance {
@TweedConstruct(DummyBase.class)
public MissingInheritance() {
}
}
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static class PrivateConstructor implements DummyBase {
}
public static class ConstructorConflict implements DummyBase {
@TweedConstruct(DummyBase.class)
public ConstructorConflict() {
}
@TweedConstruct(DummyBase.class)
public ConstructorConflict(String context) {
}
}
public static class StaticConflict implements DummyBase {
@TweedConstruct(DummyBase.class)
public static StaticConflict ofA() {
return null;
}
@TweedConstruct(DummyBase.class)
public static StaticConflict ofB() {
return null;
}
}
public static class MixedConflict implements DummyBase {
@TweedConstruct(DummyBase.class)
public MixedConflict() {
}
@TweedConstruct(DummyBase.class)
public static MixedConflict of() {
return null;
}
}
@Getter
@EqualsAndHashCode
public static class SingleConstructor implements DummyBase {
private final Integer times;
private final String user;
private final String context;
public SingleConstructor(
Integer times,
@ConstructParameter(name = "user") String user,
@ConstructParameter(name = "context") String context
) {
this.times = times;
this.user = user;
this.context = context;
}
}
@Value
public static class Static implements DummyBase {
Integer times;
String user;
String context;
@TweedConstruct(DummyBase.class)
public static Static of(Integer times, @ConstructParameter(name = "user") String user) {
return new Static(times * 10, user, "static");
}
}
@Value
@AllArgsConstructor
public static class FindBase implements DummyBase, DummyOtherBase, DummyAltBase {
String origin;
int value;
@TweedConstruct(DummyBase.class)
public FindBase(int value) {
this("base", value * 10);
}
@TweedConstruct(DummyOtherBase.class)
public static FindBase ofOther(int value) {
return new FindBase("other", value * -1);
}
@TweedConstruct(DummyAltBase.class)
public static FindBase ofAlt(int value) {
return new FindBase("alt", value * -10);
}
}
@Value
public static class Primitives implements DummyBase {
int a;
Long b;
}
@Value
public static class NamedCasting implements DummyBase {
Number value;
public NamedCasting(@ConstructParameter(name = "test") Number value) {
this.value = value;
}
}
@Value
public static class DuplicateParams implements DummyBase {
public DuplicateParams(
String one,
String two,
Long test,
@ConstructParameter(name = "number") int three,
@ConstructParameter(name = "number") int four
) {
}
}
}