Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhanced Reflective Building for Endecs #2

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@
* should be treated as nullable in serialization. Importantly, <b>this changes the serialized type of this
* component to an optional</b>
*/
@Target(ElementType.RECORD_COMPONENT)
@Target({ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
public @interface NullableComponent {}
public @interface IsNullable {}
19 changes: 19 additions & 0 deletions src/main/java/io/wispforest/endec/annotations/IsVarInt.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package io.wispforest.endec.annotations;

import io.wispforest.endec.Endec;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Indicates to the {@link io.wispforest.endec.impl.RecordEndec} that this record component
* should be treated as variable variant of the {@link Integer} or {@link Long} type in serialization
* meaning such will use either the {@link Endec#VAR_INT} or {@link Endec#VAR_LONG}.
*/
@Target({ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
public @interface IsVarInt {
boolean ignoreHumanReadable() default false;
}
8 changes: 3 additions & 5 deletions src/main/java/io/wispforest/endec/impl/RecordEndec.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import io.wispforest.endec.Endec;
import io.wispforest.endec.Serializer;
import io.wispforest.endec.StructEndec;
import io.wispforest.endec.annotations.NullableComponent;
import io.wispforest.endec.SerializationContext;

import java.lang.invoke.MethodHandle;
Expand Down Expand Up @@ -48,10 +47,9 @@ public static <R extends Record> RecordEndec<R> create(ReflectiveEndecBuilder bu
var component = recordClass.getRecordComponents()[i];
var handle = lookup.unreflect(component.getAccessor());

var endec = (Endec<Object>) builder.get(component.getGenericType());
if(component.isAnnotationPresent(NullableComponent.class)) endec = endec.nullableOf();

fields.add(new StructField<>(component.getName(), endec, instance -> getRecordEntry(instance, handle)));
fields.add(new StructField<>(component.getName(),
(Endec<Object>) builder.getAnnotated(component),
instance -> getRecordEntry(instance, handle)));

canonicalConstructorArgs[i] = component.getType();
} catch (IllegalAccessException e) {
Expand Down
182 changes: 167 additions & 15 deletions src/main/java/io/wispforest/endec/impl/ReflectiveEndecBuilder.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
package io.wispforest.endec.impl;

import io.wispforest.endec.Endec;
import io.wispforest.endec.SerializationAttributes;
import io.wispforest.endec.annotations.InnerLookup;
import io.wispforest.endec.annotations.IsNullable;
import io.wispforest.endec.annotations.SealedPolymorphic;
import io.wispforest.endec.annotations.IsVarInt;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
import org.jetbrains.annotations.Nullable;

import java.lang.reflect.Array;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import java.util.*;
import java.util.function.Consumer;
import java.util.stream.Collectors;
Expand All @@ -20,9 +22,12 @@ public class ReflectiveEndecBuilder {

private final Map<Class<?>, Endec<?>> classToEndec = new HashMap<>();

private final Map<Class<? extends Annotation>, TypedAdjuster<? extends Annotation>> classToTypeAdjuster = new HashMap<>();

public ReflectiveEndecBuilder(Consumer<ReflectiveEndecBuilder> defaultsSetup) {
defaultsSetup.accept(this);
registerDefaults(this);
registerDefaultAdjusters(this);
}

public ReflectiveEndecBuilder() {
Expand Down Expand Up @@ -50,6 +55,113 @@ public final <T> ReflectiveEndecBuilder register(Endec<T> endec, Class<T>... cla
return this;
}

public <A extends Annotation> ReflectiveEndecBuilder registerTypeAdjuster(TypedAdjuster<A> adjuster, Class<A> clazz) {
if (this.classToTypeAdjuster.containsKey(clazz)) {
throw new IllegalStateException("Class '" + clazz.getName() + "' already has an associated adjuster");
}

this.classToTypeAdjuster.put(clazz, adjuster);
return this;
}

@SafeVarargs
public final <A extends Annotation> ReflectiveEndecBuilder registerTypeAdjuster(TypedAdjuster<A> adjuster, Class<A>... classes) {
for (var clazz : classes) this.registerTypeAdjuster(adjuster, clazz);
return this;
}

@Nullable
private <T> Endec<T> adjustEndecWithType(AnnotatedType annotatedType, @Nullable Endec<T> endec) {
for (var clazz : this.classToTypeAdjuster.keySet()) {
var adjustedEndec = applyTypeAdjusterIfPresent(clazz, annotatedType, endec);

if(adjustedEndec != null) return adjustedEndec;
}

return endec;
}

@Nullable
private <A extends Annotation, T> Endec<T> applyTypeAdjusterIfPresent(Class<A> annotationClazz, AnnotatedType annotatedType, @Nullable Endec<T> endec) {
if(!annotatedType.isAnnotationPresent(annotationClazz)) return null;

return ((TypedAdjuster<A>) this.classToTypeAdjuster.get(annotationClazz))
.adjustEndec(annotatedType, annotatedType.getAnnotation(annotationClazz), endec);
}

//--

private AnnotatedType getAnnotatedType(AnnotatedElement annotatedElement) {
return switch (annotatedElement) {
case Parameter parameter -> parameter.getAnnotatedType();
case Field field -> field.getAnnotatedType();
case RecordComponent recordComponent -> recordComponent.getAnnotatedType();
default -> throw new IllegalStateException("Unable to find the annotated type for the given Annotated Element: [Element: " + annotatedElement + "]");
};
}

private Class<?> getBaseType(AnnotatedElement annotatedElement) {
return switch (annotatedElement) {
case Parameter parameter -> parameter.getType();
case Field field -> field.getType();
case RecordComponent recordComponent -> recordComponent.getType();
default -> throw new IllegalStateException("Unable to find the annotated type for the given Annotated Element: [Element: " + annotatedElement + "]");
};
}

public Endec<?> getAnnotated(AnnotatedElement annotatedElement) {
return getAnnotated(getAnnotatedType(annotatedElement), getBaseType(annotatedElement), annotatedElement);
}

public Endec<?> getAnnotated(AnnotatedType annotatedType) {
return getAnnotated(annotatedType, null,null);
}

private Endec<?> getAnnotated(AnnotatedType annotatedType, @Nullable Class<?> baseType, @Nullable AnnotatedElement annotatedElement) {
var type = annotatedType.getType();

Endec<?> endec;

if(annotatedType instanceof AnnotatedArrayType annotatedArrayType) {
Class<?> arrayClazz = (type instanceof GenericArrayType) ? baseType : (Class<?>) type;

if(arrayClazz == null) throw new IllegalStateException("Unable to get the required base Array class to get the component type!");

endec = createArrayEndec(arrayClazz.componentType(), annotatedArrayType.getAnnotatedGenericComponentType());
} else if (type instanceof Class<?> clazz) {
endec = getOrNull(clazz);
} else {
var annotatedTypeArgs = ((AnnotatedParameterizedType) annotatedType).getAnnotatedActualTypeArguments();

var annotatedType0 = annotatedTypeArgs[0];

var raw = ((ParameterizedType) type).getRawType();

endec = switch (raw) {
case Class<?> clazz when clazz.equals(Map.class) -> {
var annotatedType1 = annotatedTypeArgs[1];

yield annotatedType0.getType() == String.class
? this.getAnnotated(annotatedType1).mapOf()
: Endec.map(this.getAnnotated(annotatedType0), this.getAnnotated(annotatedType1));
}
case Class<?> clazz when clazz.equals(List.class) -> this.getAnnotated(annotatedType0).listOf();
case Class<?> clazz when clazz.equals(Set.class) -> this.getAnnotated(annotatedType0).setOf();
case Class<?> clazz when clazz.equals(Optional.class) -> this.getAnnotated(annotatedType0).optionalOf();
case Class<?> clazz -> this.getOrNull(clazz);
default -> throw new IllegalStateException("Unexpected value: " + raw);
};
}

endec = adjustEndecWithType(annotatedType, endec);

if (endec == null) {
throw new IllegalStateException("No Endec available for the given type '" + type.toString() + "'");
}

return endec;
}

/**
* Get (or potentially create) the endec associated with {@code type}. In addition
* to {@link #get(Class)}, this method uses type parameter information to automatically
Expand Down Expand Up @@ -123,7 +235,7 @@ public <T> Optional<Endec<T>> maybeGet(Class<T> clazz) {
} else if (clazz.isEnum()) {
serializer = (Endec<T>) Endec.forEnum((Class<? extends Enum>) clazz);
} else if (clazz.isArray()) {
serializer = (Endec<T>) this.createArrayEndec(clazz.getComponentType());
serializer = (Endec<T>) this.createArrayEndec(clazz.getComponentType(), null);
} else if (clazz.isAnnotationPresent(SealedPolymorphic.class)) {
serializer = (Endec<T>) this.createSealedEndec(clazz);
} else {
Expand All @@ -138,12 +250,14 @@ public <T> Optional<Endec<T>> maybeGet(Class<T> clazz) {
}

@SuppressWarnings("unchecked")
private Endec<?> createArrayEndec(Class<?> elementClass) {
var elementEndec = (Endec<Object>) this.get(elementClass);
private Endec<?> createArrayEndec(Class<?> componentType, @Nullable AnnotatedType genericComponentType) {
if(componentType.equals(byte.class) || componentType.equals(Byte.class)) return Endec.BYTES;

var elementEndec = (Endec<Object>) ((genericComponentType == null) ? this.get(componentType) : this.getAnnotated(genericComponentType));

return elementEndec.listOf().xmap(list -> {
int length = list.size();
var array = Array.newInstance(elementClass, length);
var array = Array.newInstance(componentType, length);
for (int i = 0; i < length; i++) {
Array.set(array, i, list.get(i));
}
Expand All @@ -168,10 +282,10 @@ private Endec<?> createSealedEndec(Class<?> commonClass) {
for (int i = 0; i < permittedSubclasses.size(); i++) {
var clazz = permittedSubclasses.get(i);

if (clazz.isSealed()) {
for (var subclass : clazz.getPermittedSubclasses()) {
if (!permittedSubclasses.contains(subclass)) permittedSubclasses.add(subclass);
}
if (!clazz.isSealed()) continue;

for (var subclass : clazz.getPermittedSubclasses()) {
if (!permittedSubclasses.contains(subclass)) permittedSubclasses.add(subclass);
}
}

Expand Down Expand Up @@ -200,9 +314,7 @@ private Endec<?> createSealedEndec(Class<?> commonClass) {

@SafeVarargs
private <T> ReflectiveEndecBuilder registerIfMissing(Endec<T> endec, Class<T>... classes) {
for (var clazz : classes) {
this.classToEndec.putIfAbsent(clazz, endec);
}
for (var clazz : classes) this.classToEndec.putIfAbsent(clazz, endec);

return this;
}
Expand Down Expand Up @@ -234,4 +346,44 @@ private static void registerDefaults(ReflectiveEndecBuilder builder) {
.registerIfMissing(BuiltInEndecs.DATE, Date.class)
.registerIfMissing(BuiltInEndecs.BITSET, BitSet.class);
}

private static void registerDefaultAdjusters(ReflectiveEndecBuilder builder) {
builder.registerTypeAdjuster(
new TypedAdjuster<>() {
@Override
@Nullable
public <T> Endec<T> adjustEndec(AnnotatedType annotatedType, IsNullable annotationInst, Endec<T> base) {
Type type = annotatedType.getType();

if(type instanceof Class<?> clazz && clazz.isPrimitive()) throw new IllegalStateException("Unable to create nullable Endec variant of a primitive type! [Element: " + annotatedType.toString() + "]");

return base.nullableOf();
}
}, IsNullable.class);

builder.registerTypeAdjuster(
new TypedAdjuster<>() {
@Override
@Nullable
public <T> Endec<T> adjustEndec(AnnotatedType annotatedType, IsVarInt annotationInst, Endec<T> base) {
Type type = annotatedType.getType();

if(type.equals(int.class) || type.equals(Integer.class)) {
if(annotationInst.ignoreHumanReadable()) return (Endec<T>) Endec.VAR_INT;

return Endec.ifAttr(SerializationAttributes.HUMAN_READABLE, base)
.orElse((Endec<T>) Endec.VAR_INT);
} else if (type.equals(long.class) || type.equals(Long.class)) {
if(annotationInst.ignoreHumanReadable()) return (Endec<T>) Endec.VAR_LONG;

return Endec.ifAttr(SerializationAttributes.HUMAN_READABLE, base)
.orElse((Endec<T>) Endec.VAR_LONG);
}

throw new IllegalStateException("Unable to handle the given type passed!");
}
}, IsVarInt.class);


}
}
15 changes: 15 additions & 0 deletions src/main/java/io/wispforest/endec/impl/TypedAdjuster.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.wispforest.endec.impl;

import io.wispforest.endec.Endec;
import org.jetbrains.annotations.Nullable;

import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedType;

/**
* Functional interface allowing for the adjustments of to be returned for
* @param <A>
*/
public interface TypedAdjuster<A extends Annotation> {
@Nullable <T> Endec<T> adjustEndec(AnnotatedType annotatedType, A annotationInstance, @Nullable Endec<T> base);
}