Skip to content

Commit 02ece4c

Browse files
committed
feat(experimental): automatic type conversions while loading XML props
this is an experiment to see if automatic type conversion for xml props makes sense. currently things like list conversions are still missing.
1 parent 3041667 commit 02ece4c

15 files changed

Lines changed: 506 additions & 33 deletions

File tree

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import org.gradle.api.DefaultTask
2+
import org.gradle.api.file.DirectoryProperty
3+
import org.gradle.api.tasks.InputDirectory
4+
import org.gradle.api.tasks.OutputDirectory
5+
import org.gradle.api.tasks.TaskAction
6+
import java.nio.file.Files
7+
8+
abstract class SymlinkTask : DefaultTask() {
9+
@get:InputDirectory
10+
abstract val from: DirectoryProperty
11+
12+
@get:OutputDirectory
13+
abstract val into: DirectoryProperty
14+
15+
@TaskAction
16+
fun doSymlink() {
17+
val dest = into.asFile.get()
18+
val src = from.asFile.get()
19+
if (Files.isSymbolicLink(dest.toPath())) {
20+
return
21+
} else {
22+
dest.deleteRecursively()
23+
}
24+
Files.createSymbolicLink(dest.toPath(), src.toPath())
25+
}
26+
}

build-src/src/main/kotlin/moulconfig.fabric.gradle.kts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,11 +96,23 @@ tasks.processResources {
9696
}
9797
}
9898
}
99+
val fSourceDest = layout.buildDirectory.dir("sharedModernSource")
100+
val generateFilteredSource =
101+
if (project.hasProperty("moulconfig.symlinkSharedSources"))
102+
tasks.register("generateFilteredSourc", SymlinkTask::class) {
103+
from = project(":modern").file("templates/java")
104+
into = fSourceDest
105+
}
106+
else
107+
tasks.register("generateFilteredSource", Copy::class) {
108+
doFirst {
109+
if (fSourceDest.get().asFile.isFile)
110+
fSourceDest.get().asFile.delete()
111+
}
112+
from(project(":modern").file("templates/java"))
113+
rootSpec.into(fSourceDest)
114+
}
99115

100-
val generateFilteredSource = tasks.register("generateFilteredSource", Copy::class) {
101-
from(project(":modern").file("templates/java"))
102-
rootSpec.into(layout.buildDirectory.dir("sharedModernSource"))
103-
}
104116
sourceSets.main {
105117
java {
106118
srcDir(files(generateFilteredSource))

common/src/main/java/io/github/notenoughupdates/moulconfig/common/IMinecraft.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import io.github.notenoughupdates.moulconfig.internal.MCLogger;
1010
import io.github.notenoughupdates.moulconfig.internal.Warnings;
1111
import io.github.notenoughupdates.moulconfig.processor.MoulConfigProcessor;
12+
import io.github.notenoughupdates.moulconfig.xml.XMLUniverse;
1213
import kotlin.Pair;
1314
import lombok.var;
1415
import org.jetbrains.annotations.ApiStatus;
@@ -99,6 +100,9 @@ default void sendChatMessage(StructuredText message) {
99100
@Nullable
100101
StructuredText createStructuredTextInternal(Object object);
101102

103+
@ApiStatus.Experimental
104+
void registerPlatformTypeMorphisms(XMLUniverse universe);
105+
102106
/**
103107
* This is a method to provide a render context. Note that constructing this context directly will potentially give
104108
* you an incorrect render state, leading to visual glitches. Depending on your platform, this might also require

common/src/main/java/io/github/notenoughupdates/moulconfig/internal/TypeUtils.java

Lines changed: 111 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
package io.github.notenoughupdates.moulconfig.internal;
22

3-
import java.lang.reflect.Array;
4-
import java.lang.reflect.GenericArrayType;
5-
import java.lang.reflect.ParameterizedType;
6-
import java.lang.reflect.Type;
7-
import java.lang.reflect.WildcardType;
8-
import java.util.Arrays;
3+
import lombok.val;
4+
import org.jetbrains.annotations.NotNull;
5+
6+
import java.lang.reflect.*;
7+
import java.util.*;
98

109
public class TypeUtils {
1110
public static boolean areTypesEquals(Class<?> a, Class<?> b) {
@@ -31,6 +30,17 @@ public static Class<?> normalizeNative(Class<?> clazz) {
3130
return clazz;
3231
}
3332

33+
public static Class<?> normalizeToNative(Class<?> clazz) {
34+
if (clazz == Integer.class) return int.class;
35+
if (clazz == Float.class) return float.class;
36+
if (clazz == Double.class) return double.class;
37+
if (clazz == Boolean.class) return boolean.class;
38+
if (clazz == Long.class) return long.class;
39+
if (clazz == Short.class) return short.class;
40+
if (clazz == Character.class) return char.class;
41+
return clazz;
42+
}
43+
3444
public static Class<?> resolveRawType(Type t) {
3545
if (t instanceof Class<?>) return (Class<?>) t;
3646
if (t instanceof WildcardType) return resolveRawType(((WildcardType) t).getUpperBounds()[0]);
@@ -41,4 +51,99 @@ public static Class<?> resolveRawType(Type t) {
4151
}
4252
throw new IllegalArgumentException("Could not resolve type " + t + " to a raw type");
4353
}
54+
55+
/**
56+
* obtain a list of type variable assignments from a generic type instantiation.
57+
*
58+
* @see #fillTypeUniverse for the concrete meaning of this
59+
*/
60+
public static Map<TypeVariable<?>, Type> createTypeUniverse(Type instantiation) {
61+
val universe = new HashMap<TypeVariable<?>, Type>();
62+
fillTypeUniverse(instantiation, resolveRawType(instantiation), universe);
63+
return universe;
64+
}
65+
66+
/**
67+
* Construct a universe of type variable assignments from a given type instantiation. Nota bene: this only returns individual assignments, which need to be resolved to concrete types using {@link #resolveSimpleTypeVariableInUniverse}.
68+
*
69+
* @param instantiation a generic type instantiation that is ideally fully specified
70+
* @param context the raw type of the {@code instantiation}, is used to know which generic type is being instantiated by the parameterization
71+
* @param universe an out param (i know -- bad form -- however this makes it easier to collect all the type assignments across multiple inheritance paths)
72+
*/
73+
public static void fillTypeUniverse(
74+
Type instantiation,
75+
Class<?> context,
76+
Map<TypeVariable<?>, Type> universe) {
77+
if (instantiation == null) return;
78+
if (instantiation instanceof WildcardType) {
79+
// This is technically incorrect.
80+
fillTypeUniverse(((WildcardType) instantiation).getUpperBounds()[0], context, universe);
81+
return;
82+
}
83+
if (instantiation instanceof ParameterizedType) {
84+
val par = ((ParameterizedType) instantiation);
85+
val con = par.getActualTypeArguments();
86+
final TypeVariable<?>[] abs = context.getTypeParameters();
87+
for (int i = 0; i < abs.length; i++) {
88+
universe.put(abs[i], con[i]);
89+
}
90+
fillTypeUniverse(par.getRawType(), context, universe);
91+
return;
92+
}
93+
if (instantiation instanceof GenericArrayType) {
94+
throw new IllegalArgumentException("Encountered array type while walking the type hierarchy " + instantiation);
95+
}
96+
if (instantiation instanceof Class<?>) {
97+
val cls = ((Class<?>) instantiation);
98+
val gInters = cls.getGenericInterfaces();
99+
val inters = cls.getInterfaces();
100+
for (int i = gInters.length; i-- > 0; ) {
101+
fillTypeUniverse(gInters[i], inters[i], universe);
102+
}
103+
fillTypeUniverse(cls.getGenericSuperclass(), cls.getSuperclass(), universe);
104+
return;
105+
}
106+
throw new IllegalArgumentException("Encountered unknown type kind " + instantiation + " while walking a generic hierarchy");
107+
}
108+
109+
public static ParameterizedType instantiateParameterizedType(
110+
Class<?> rawType,
111+
Type[] typeArguments
112+
) {
113+
if (rawType.getTypeParameters().length != typeArguments.length)
114+
Warnings.warn("Invalid instantiation of parameterized type " + rawType);
115+
return new ParameterizedType() {
116+
@Override
117+
public String getTypeName() {
118+
return getRawType().getTypeName();
119+
}
120+
121+
@Override
122+
public @NotNull Type @NotNull [] getActualTypeArguments() {
123+
return typeArguments;
124+
}
125+
126+
@Override
127+
public @NotNull Type getRawType() {
128+
return rawType;
129+
}
130+
131+
@Override
132+
public Type getOwnerType() {
133+
return null;
134+
}
135+
};
136+
}
137+
138+
/**
139+
* Given a universe of assignments for type-variables, resolve type-variables to a concrete non-generic type (at the top level). This will not implicitly resolve constructs like {@code U = List<T>, T = String, U?}, instead keeping resolving {@code U? = List<T>}.
140+
*/
141+
public static Optional<Type> resolveSimpleTypeVariableInUniverse(Map<TypeVariable<?>, Type> universe, TypeVariable<?> query) {
142+
Type p = query;
143+
while (p instanceof TypeVariable<?>) {
144+
p = universe.get(query);
145+
}
146+
return Optional.ofNullable(p); // .get will return null if one type variable along the chain is left unassigned
147+
}
148+
44149
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package io.github.notenoughupdates.moulconfig.xml;
2+
3+
import io.github.notenoughupdates.moulconfig.observer.GetSetter;
4+
import org.jetbrains.annotations.ApiStatus;
5+
6+
import java.lang.reflect.Type;
7+
import java.util.Optional;
8+
9+
/**
10+
* Generic interface for transforming {@link GetSetter GetSetters} into GetSetters containing another type.
11+
*/
12+
@ApiStatus.Experimental
13+
public interface ParametricTypeMorphism {
14+
/**
15+
* For a given type, return what type this morphism transform each object into.
16+
*/
17+
Optional<Type> codomain(Type domain);
18+
19+
/**
20+
* Transform an object from one type to another, wrapped into a {@link GetSetter}
21+
* @param domain the type contained in the {@link GetSetter}
22+
*/
23+
GetSetter<?> apply(Type domain, GetSetter<?> value);
24+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package io.github.notenoughupdates.moulconfig.xml;
2+
3+
import io.github.notenoughupdates.moulconfig.observer.GetSetter;
4+
import lombok.Value;
5+
import lombok.var;
6+
7+
import java.lang.reflect.Type;
8+
import java.util.ArrayList;
9+
import java.util.Collections;
10+
import java.util.List;
11+
import java.util.Optional;
12+
13+
@Value
14+
public class TypeMorphismChain {
15+
List<ParametricTypeMorphism> transformations;
16+
List<Type> domains;
17+
18+
TypeMorphismChain(List<ParametricTypeMorphism> transformations, List<Type> domains) {
19+
this.transformations = transformations;
20+
this.domains = domains;
21+
assert domains.size() == transformations.size() + 1;
22+
}
23+
public static TypeMorphismChain id(Type type) {
24+
return new TypeMorphismChain(Collections.emptyList(), Collections.singletonList(type));
25+
}
26+
27+
public Type getDomain() {
28+
return domains.get(0);
29+
}
30+
31+
public Type getCoDomain() {
32+
return domains.get(domains.size() - 1);
33+
}
34+
35+
public Optional<TypeMorphismChain> tryExtendWith(ParametricTypeMorphism morphism) {
36+
var nextCoDomain = morphism.codomain(getCoDomain());
37+
if (!nextCoDomain.isPresent()) return Optional.empty();
38+
var newTransformations = new ArrayList<ParametricTypeMorphism>(transformations.size() + 1);
39+
newTransformations.addAll(transformations);
40+
newTransformations.add(morphism);
41+
var newDomains = new ArrayList<Type>(domains.size() + 1);
42+
newDomains.addAll(domains);
43+
newDomains.add(nextCoDomain.get());
44+
return Optional.of(new TypeMorphismChain(newTransformations, newDomains));
45+
}
46+
47+
48+
public GetSetter<?> map(GetSetter<?> element) {
49+
var p = element;
50+
for (int i = 0; i < transformations.size(); i++) {
51+
var trans = transformations.get(i);
52+
var domain = domains.get(i);
53+
p = trans.apply(domain, p);
54+
}
55+
return p;
56+
}
57+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package io.github.notenoughupdates.moulconfig.xml;
2+
3+
import lombok.NonNull;
4+
import lombok.Value;
5+
6+
import java.lang.reflect.Type;
7+
8+
@Value(staticConstructor = "of")
9+
class TypePath {
10+
@NonNull
11+
Type source;
12+
@NonNull
13+
Type destination;
14+
}

common/src/main/java/io/github/notenoughupdates/moulconfig/xml/XMLBoundProperties.java

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
@Data
1919
public class XMLBoundProperties {
20+
final XMLUniverse universe;
2021
final Map<String, Field> namedProperties = new HashMap<>();
2122
final Map<String, Method> namedFunctions = new HashMap<>();
2223
private static MethodHandles.Lookup lookup = MethodHandles.lookup();
@@ -85,43 +86,41 @@ public void set(T newValue) {
8586
Method method = namedFunctions.get(name);
8687
if (method == null)
8788
throw new NullPointerException("Could not find bind target for " + name + " in " + object);
88-
if (!TypeUtils.doesAExtendB(method.getReturnType(), clazz))
89-
throw new IllegalArgumentException("Bind target " + method + " is of the wrong type " + method.getReturnType() + " instead of " + clazz);
9089
if (method.getParameterCount() != 0)
9190
throw new RuntimeException("Bind target " + method + " is not a pure getter");
9291
var unreflect = bindSometimes(lookup.unreflect(method), method.getModifiers(), object);
93-
return new GetSetter<T>() {
92+
93+
//noinspection unchecked
94+
return (GetSetter<T>) universe.mapObject(new GetSetter<Object>() {
9495
@SneakyThrows
9596
@Override
96-
public T get() {
97-
return (T) unreflect.invoke();
97+
public Object get() {
98+
return unreflect.invoke();
9899
}
99100

100101
@Override
101-
public void set(T newValue) {
102+
public void set(Object newValue) {
102103
throw new UnsupportedOperationException();
103104
}
104-
};
105+
}, method.getGenericReturnType(), clazz);
105106
}
106-
if (!TypeUtils.doesAExtendB(field.getType(), clazz))
107-
throw new IllegalArgumentException("Bind target " + name + " is of the wrong type " + field.getType() + " instead of " + clazz);
108107
field.setAccessible(true);
109108
var getter = bindSometimes(lookup.unreflectGetter(field), field.getModifiers(), object);
110109
var setter = bindSometimes(lookup.unreflectSetter(field), field.getModifiers(), object);
111-
return new GetSetter<T>() {
112-
110+
//noinspection unchecked
111+
return (GetSetter<T>) universe.mapObject(new GetSetter<Object>() {
113112
@SneakyThrows
114113
@Override
115-
public T get() {
116-
return (T) getter.invoke();
114+
public Object get() {
115+
return getter.invoke();
117116
}
118117

119118
@SneakyThrows
120119
@Override
121-
public void set(T newValue) {
120+
public void set(Object newValue) {
122121
setter.invoke(newValue);
123122
}
124-
};
123+
}, field.getGenericType(), clazz);
125124
}
126125

127126
private static MethodHandle bindSometimes(MethodHandle methodHandle, int modifiers, Object object) {

0 commit comments

Comments
 (0)