From 0e64f6b30455fe26e1f808298328f44c9a8e25dd Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Mon, 3 Mar 2025 16:36:07 -0600 Subject: [PATCH 01/28] Initial work on reflective structures --- build.gradle | 3 +- .../MethodHandleRecordCodecBuilder.java | 21 +- .../codecextras/structured/Interpreter.java | 3 +- .../codecextras/structured/Structure.java | 11 + .../BuiltInReflectiveStructureCreator.java | 724 ++++++++++++++++++ .../ReflectiveStructureCreator.java | 88 +++ .../reflective/SerializedProperty.java | 9 + .../structured/reflective/package-info.java | 6 + src/main/java/module-info.java | 2 + .../codecextras/test/CodecAssertions.java | 12 +- .../structured/reflective/TestReflective.java | 79 ++ .../structured/reflective/package-info.java | 4 + 12 files changed, 949 insertions(+), 13 deletions(-) create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/reflective/BuiltInReflectiveStructureCreator.java create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/reflective/SerializedProperty.java create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/reflective/package-info.java create mode 100644 src/test/java/dev/lukebemish/codecextras/test/structured/reflective/TestReflective.java create mode 100644 src/test/java/dev/lukebemish/codecextras/test/structured/reflective/package-info.java diff --git a/build.gradle b/build.gradle index 4acc9e0..d7bd87c 100644 --- a/build.gradle +++ b/build.gradle @@ -182,6 +182,8 @@ dependencies { api 'com.mojang:datafixerupper:8.0.16' api 'org.slf4j:slf4j-api:2.0.1' + implementation 'org.ow2.asm:asm:9.5' + jmhCompileOnly cLibs.bundles.compileonly jmhImplementation project(':') jmhImplementation 'org.openjdk.jmh:jmh-core:1.37' @@ -198,7 +200,6 @@ dependencies { compileOnly 'com.electronwill.night-config:core:3.6.4' compileOnly 'com.electronwill.night-config:toml:3.6.4' compileOnly 'blue.endless:jankson:1.2.2' - compileOnly 'org.ow2.asm:asm:9.5' testImplementation 'com.electronwill.night-config:core:3.6.4' testImplementation 'com.electronwill.night-config:toml:3.6.4' diff --git a/src/main/java/dev/lukebemish/codecextras/record/MethodHandleRecordCodecBuilder.java b/src/main/java/dev/lukebemish/codecextras/record/MethodHandleRecordCodecBuilder.java index 59b02b2..a9828b2 100644 --- a/src/main/java/dev/lukebemish/codecextras/record/MethodHandleRecordCodecBuilder.java +++ b/src/main/java/dev/lukebemish/codecextras/record/MethodHandleRecordCodecBuilder.java @@ -6,6 +6,14 @@ import com.mojang.serialization.MapCodec; import com.mojang.serialization.MapLike; import com.mojang.serialization.RecordBuilder; +import org.jetbrains.annotations.ApiStatus; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.ConstantDynamic; +import org.objectweb.asm.Handle; +import org.objectweb.asm.Label; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; + import java.lang.invoke.CallSite; import java.lang.invoke.ConstantCallSite; import java.lang.invoke.MethodHandle; @@ -16,13 +24,6 @@ import java.util.List; import java.util.function.Function; import java.util.stream.Stream; -import org.jetbrains.annotations.ApiStatus; -import org.objectweb.asm.ClassWriter; -import org.objectweb.asm.ConstantDynamic; -import org.objectweb.asm.Handle; -import org.objectweb.asm.Label; -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.Type; @ApiStatus.Experimental public final class MethodHandleRecordCodecBuilder { @@ -54,7 +55,7 @@ public MapCodec buildMapWithConstructor(MethodHandles.Lookup lookup, Class } else if (ctors.size() > 1) { throw new IllegalArgumentException("Multiple constructors with " + fields.size() + " parameters found"); } - return lookup.unreflectConstructor(ctors.get(0)); + return lookup.unreflectConstructor(ctors.getFirst()); }); } @@ -216,10 +217,10 @@ public MapCodec buildMap(HandleSupplier constructor) { } } - private static ConstantDynamic conDyn(String Descriptor, int i) { + private static ConstantDynamic conDyn(String descriptor, int i) { return new ConstantDynamic( "_", - Descriptor, + descriptor, new Handle( Opcodes.H_INVOKESTATIC, Type.getInternalName(MethodHandles.class), diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java index 928e90b..c60ff1a 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java @@ -33,7 +33,7 @@ default Stream> keyConsumers() { return Stream.of(); } - public interface KeyConsumer { + interface KeyConsumer { Key key(); App convert(App input); } @@ -59,6 +59,7 @@ default DataResult> bounded(Structure input, Supplier> v Key FLOAT = Key.create("FLOAT"); Key DOUBLE = Key.create("DOUBLE"); Key STRING = Key.create("STRING"); + Key CHAR = Key.create("CHAR"); Key> PASSTHROUGH = Key.create("PASSTHROUGH"); Key EMPTY_MAP = Key.create("EMPTY_MAP"); Key EMPTY_LIST = Key.create("EMPTY_LIST"); diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java index a4bc40f..4068f41 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java @@ -641,6 +641,17 @@ static Structure record(RecordStructure.Builder builder) { */ Structure STRING = keyed(Interpreter.STRING); + /** + * Represents a {@link Character} value. + */ + Structure CHAR = keyed(Interpreter.CHAR, STRING.validate(s -> { + if (s.length() == 1) { + return DataResult.success(s); + } else { + return DataResult.error(() -> "String must be 1 character long, found '" + s + "'"); + } + }).xmap(s -> s.charAt(0), String::valueOf)); + /** * Represents a {@link Dynamic} value. */ diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/BuiltInReflectiveStructureCreator.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/BuiltInReflectiveStructureCreator.java new file mode 100644 index 0000000..2f70195 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/BuiltInReflectiveStructureCreator.java @@ -0,0 +1,724 @@ +package dev.lukebemish.codecextras.structured.reflective; + +import com.google.auto.service.AutoService; +import com.google.common.base.Suppliers; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.mojang.datafixers.util.Either; +import com.mojang.datafixers.util.Unit; +import com.mojang.serialization.Dynamic; +import dev.lukebemish.codecextras.structured.RecordStructure; +import dev.lukebemish.codecextras.structured.Structure; +import org.jetbrains.annotations.ApiStatus; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.ConstantDynamic; +import org.objectweb.asm.Handle; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +import java.lang.invoke.CallSite; +import java.lang.invoke.ConstantCallSite; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.AccessFlag; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.function.Supplier; + +@ApiStatus.Internal +@AutoService(ReflectiveStructureCreator.class) +public class BuiltInReflectiveStructureCreator implements ReflectiveStructureCreator { + @Override + public Map, Creator> creators() { + return ImmutableMap., Creator>builder() + .put(Unit.class, () -> Structure.UNIT) + .put(Boolean.class, () -> Structure.BOOL) + .put(Byte.class, () -> Structure.BYTE) + .put(Short.class, () -> Structure.SHORT) + .put(Integer.class, () -> Structure.INT) + .put(Long.class, () -> Structure.LONG) + .put(Float.class, () -> Structure.FLOAT) + .put(Double.class, () -> Structure.DOUBLE) + .put(String.class, () -> Structure.STRING) + .put(Character.class, () -> Structure.CHAR) + .put(Dynamic.class, () -> Structure.PASSTHROUGH) + // Primitives + .put(Boolean.TYPE, () -> Structure.BOOL) + .put(Byte.TYPE, () -> Structure.BYTE) + .put(Short.TYPE, () -> Structure.SHORT) + .put(Integer.TYPE, () -> Structure.INT) + .put(Long.TYPE, () -> Structure.LONG) + .put(Float.TYPE, () -> Structure.FLOAT) + .put(Double.TYPE, () -> Structure.DOUBLE) + .put(Character.TYPE, () -> Structure.CHAR) + // Arrays + .put(boolean[].class, () -> Structure.BOOL.listOf().xmap(list -> { + var bools = new boolean[list.size()]; + for (int i = 0; i < bools.length; i++) { + bools[i] = list.get(i); + } + return bools; + }, bools -> { + var list = new ArrayList(bools.length); + for (var b : bools) { + list.add(b); + } + return list; + })) + .put(byte[].class, () -> Structure.BYTE.listOf().xmap(list -> { + var bytes = new byte[list.size()]; + for (int i = 0; i < bytes.length; i++) { + bytes[i] = list.get(i); + } + return bytes; + }, bytes -> { + var list = new ArrayList(bytes.length); + for (var b : bytes) { + list.add(b); + } + return list; + })) + .put(short[].class, () -> Structure.SHORT.listOf().xmap(list -> { + var shorts = new short[list.size()]; + for (int i = 0; i < shorts.length; i++) { + shorts[i] = list.get(i); + } + return shorts; + }, shorts -> { + var list = new ArrayList(shorts.length); + for (var s : shorts) { + list.add(s); + } + return list; + })) + .put(int[].class, () -> Structure.INT.listOf().xmap(list -> { + var ints = new int[list.size()]; + for (int i = 0; i < ints.length; i++) { + ints[i] = list.get(i); + } + return ints; + }, ints -> { + var list = new ArrayList(ints.length); + for (var i : ints) { + list.add(i); + } + return list; + })) + .put(long[].class, () -> Structure.LONG.listOf().xmap(list -> { + var longs = new long[list.size()]; + for (int i = 0; i < longs.length; i++) { + longs[i] = list.get(i); + } + return longs; + }, longs -> { + var list = new ArrayList(longs.length); + for (var l : longs) { + list.add(l); + } + return list; + })) + .put(float[].class, () -> Structure.FLOAT.listOf().xmap(list -> { + var floats = new float[list.size()]; + for (int i = 0; i < floats.length; i++) { + floats[i] = list.get(i); + } + return floats; + }, floats -> { + var list = new ArrayList(floats.length); + for (var f : floats) { + list.add(f); + } + return list; + })) + .put(double[].class, () -> Structure.DOUBLE.listOf().xmap(list -> { + var doubles = new double[list.size()]; + for (int i = 0; i < doubles.length; i++) { + doubles[i] = list.get(i); + } + return doubles; + }, doubles -> { + var list = new ArrayList(doubles.length); + for (var d : doubles) { + list.add(d); + } + return list; + })) + .put(char[].class, () -> Structure.CHAR.listOf().xmap(list -> { + var chars = new char[list.size()]; + for (int i = 0; i < chars.length; i++) { + chars[i] = list.get(i); + } + return chars; + }, chars -> { + var list = new ArrayList(chars.length); + for (var c : chars) { + list.add(c); + } + return list; + })) + .build(); + } + + @Override + public Map, ParameterizedCreator> parameterizedCreators() { + return ImmutableMap., ParameterizedCreator>builder() + .put(List.class, parameters -> parameters[0].create().listOf()) + .put(Map.class, parameters -> Structure.unboundedMap(parameters[0].create(), parameters[1].create())) + .put(Either.class, parameters -> Structure.unboundedMap(parameters[0].create(), parameters[1].create())) + .build(); + } + + private static ConstantDynamic conDyn(String descriptor, int i) { + return new ConstantDynamic( + "_", + descriptor, + new Handle( + Opcodes.H_INVOKESTATIC, + org.objectweb.asm.Type.getInternalName(MethodHandles.class), + "classDataAt", + MethodType.methodType(Object.class, MethodHandles.Lookup.class, String.class, Class.class, int.class).descriptorString(), + false + ), + i + ); + } + + public static Function getterWrapper(Class exact, MethodHandle getter) { + var writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); + var name = BuiltInReflectiveStructureCreator.class.getName().replace('.', '/') + "$GetterWrapper"; + writer.visit(Opcodes.V21, Opcodes.ACC_FINAL, name, null, "java/lang/Object", new String[]{"java/util/function/Function"}); + var mv = writer.visitMethod(Opcodes.ACC_PUBLIC, "", "()V", null, null); + mv.visitCode(); + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "", "()V", false); + mv.visitInsn(Opcodes.RETURN); + mv.visitMaxs(0, 0); + mv.visitEnd(); + mv = writer.visitMethod(Opcodes.ACC_PUBLIC, "apply", "(Ljava/lang/Object;)Ljava/lang/Object;", null, null); + mv.visitCode(); + mv.visitVarInsn(Opcodes.ALOAD, 1); + invokeAsCallSite(mv, "(Ljava/lang/Object;)Ljava/lang/Object;", 0); + mv.visitInsn(Opcodes.ARETURN); + mv.visitMaxs(0, 0); + mv.visitEnd(); + writer.visitEnd(); + var bytes = writer.toByteArray(); + try { + var lookup = MethodHandles.lookup().defineHiddenClassWithClassData(bytes, List.of(getter), true, MethodHandles.Lookup.ClassOption.NESTMATE); + @SuppressWarnings("unchecked") var instance = (Function) lookup.lookupClass().getConstructor().newInstance(); + return instance; + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + private static void invokeAsCallSite(MethodVisitor mv, String descriptor, int index) { + mv.visitInvokeDynamicInsn( + "asCallSite", + descriptor, + new Handle( + Opcodes.H_INVOKESTATIC, + org.objectweb.asm.Type.getInternalName(BuiltInReflectiveStructureCreator.class), + "asCallSite", + MethodType.methodType(CallSite.class, MethodHandles.Lookup.class, String.class, MethodType.class, MethodHandle.class).descriptorString(), + false + ), + conDyn(org.objectweb.asm.Type.getDescriptor(MethodHandle.class), index) + ); + } + + private static CallSite asCallSite(MethodHandles.Lookup ignoredLookup, String ignoredName, MethodType type, MethodHandle handle) { + return new ConstantCallSite(handle.asType(type)); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + public static RecordStructure.Key add(RecordStructure builder, String name, Type type, Function getter, Function> creator) { + // TODO: optional fields + return builder.add(name, (Structure) creator.apply(type), (Function) getter); + } + + @Override + public List flexibleCreators() { + return ImmutableList.builder() + .add(new FlexibleCreator() { + @Override + public Structure create(Class exact, Function> creator) { + Supplier values = Suppliers.memoize(() -> { + try { + return (Object[]) exact.getMethod("values").invoke(null); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new RuntimeException(e); + } + }); + return Structure.stringRepresentable(values, t -> ((Enum)t).name()); + } + + @Override + public boolean supports(Class exact) { + return Enum.class.isAssignableFrom(exact); + } + }) + .add(new FlexibleCreator() { + private Function, ?> arrayMaker(Class arrayComponentType) { + var cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); + var name = BuiltInReflectiveStructureCreator.class.getName().replace('.', '/') + "$ArrayMaker"; + cw.visit(Opcodes.V21, Opcodes.ACC_FINAL, name, null, "java/lang/Object", new String[]{"java/util/function/Function"}); + var mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "", "()V", null, null); + mv.visitCode(); + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "", "()V", false); + mv.visitInsn(Opcodes.RETURN); + mv.visitMaxs(0, 0); + mv.visitEnd(); + mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "apply", "(Ljava/lang/Object;)Ljava/lang/Object;", null, null); + mv.visitCode(); + mv.visitVarInsn(Opcodes.ALOAD, 1); + mv.visitTypeInsn(Opcodes.CHECKCAST, org.objectweb.asm.Type.getInternalName(List.class)); + mv.visitInsn(Opcodes.DUP); + mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, org.objectweb.asm.Type.getInternalName(List.class), "size", "()I", true); + mv.visitTypeInsn(Opcodes.ANEWARRAY, org.objectweb.asm.Type.getInternalName(arrayComponentType)); + mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, org.objectweb.asm.Type.getInternalName(List.class), "toArray", "([Ljava/lang/Object;)[Ljava/lang/Object;", true); + mv.visitTypeInsn(Opcodes.CHECKCAST, org.objectweb.asm.Type.getInternalName(arrayComponentType.arrayType())); + mv.visitInsn(Opcodes.ARETURN); + mv.visitMaxs(0, 0); + mv.visitEnd(); + cw.visitEnd(); + var bytes = cw.toByteArray(); + try { + var lookup = MethodHandles.lookup().defineHiddenClassWithClassData(bytes, List.of(arrayComponentType), true, MethodHandles.Lookup.ClassOption.NESTMATE); + @SuppressWarnings("unchecked") var instance = (Function, ?>) lookup.lookupClass().getConstructor().newInstance(); + return instance; + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + @Override + public Structure create(Class exact, Function> creator) { + var arrayMaker = arrayMaker(exact.getComponentType()); + return creator.apply(exact.getComponentType()).listOf().xmap(arrayMaker::apply, obj -> { + var array = (Object[]) obj; + var list = new ArrayList<>(array.length); + list.addAll(Arrays.asList(array)); + return (List) list; + }); + } + + @Override + public boolean supports(Class exact) { + return exact.isArray() && !exact.getComponentType().isPrimitive(); + } + }) + .add(new FlexibleCreator() { + @Override + public Structure create(Class exact, Function> creator) { + return Structure.record(builder -> { + Constructor validCtor = null; + if (exact.isRecord()) { + Class[] types = new Class[exact.getRecordComponents().length]; + for (int i = 0; i < types.length; i++) { + types[i] = exact.getRecordComponents()[i].getType(); + } + try { + validCtor = exact.getConstructor(types); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + } else { + for (var ctor : exact.getConstructors()) { + if (!ctor.accessFlags().contains(AccessFlag.PUBLIC)) { + continue; + } + if (ctor.getParameterCount() == 0) { + validCtor = ctor; + } else { + var hasSerializedProperties = true; + for (var param : ctor.getParameters()) { + if (!param.isAnnotationPresent(SerializedProperty.class)) { + hasSerializedProperties = false; + break; + } + var annotation = param.getAnnotation(SerializedProperty.class); + try { + var field = exact.getField(annotation.property()); + if (field.getType().equals(param.getType()) && field.accessFlags().contains(AccessFlag.PUBLIC) && !field.accessFlags().contains(AccessFlag.STATIC)) { + continue; + } + } catch (NoSuchFieldException ignored) { + } + + var hasGetter = false; + var hasGetterAlt = false; + try { + var getterMethod = exact.getMethod("get" + annotation.property().substring(0, 1).toUpperCase() + annotation.property().substring(1)); + if (getterMethod.getGenericReturnType().equals(param.getParameterizedType()) && getterMethod.accessFlags().contains(AccessFlag.PUBLIC) && !getterMethod.accessFlags().contains(AccessFlag.STATIC)) { + hasGetter = true; + } + } catch (NoSuchMethodException ignored) { + } + try { + var getterMethod = exact.getMethod(annotation.property()); + if (getterMethod.getGenericReturnType().equals(param.getParameterizedType()) && getterMethod.accessFlags().contains(AccessFlag.PUBLIC) && !getterMethod.accessFlags().contains(AccessFlag.STATIC)) { + hasGetterAlt = true; + } + } catch (NoSuchMethodException ignored) { + } + if ((!hasGetter && !hasGetterAlt) || (hasGetter && hasGetterAlt)) { + hasSerializedProperties = false; + break; + } + } + if (hasSerializedProperties) { + validCtor = ctor; + break; + } + } + } + } + + Objects.requireNonNull(validCtor); + + Map> getters = new HashMap<>(); + Map setters = new HashMap<>(); + Map ctorSetters = new HashMap<>(); + String[] ctorSettersArray = new String[validCtor.getParameterCount()]; + Map types = new HashMap<>(); + Map> context = new HashMap<>(); + if (!exact.isRecord()) { + for (int i = 0; i < validCtor.getParameterCount(); i++) { + var param = validCtor.getParameters()[i]; + var annotation = param.getAnnotation(SerializedProperty.class); + + var thisContext = context.computeIfAbsent(annotation.property(), k -> new ArrayList<>()); + thisContext.add(param); + + ctorSetters.put(annotation.property(), i); + ctorSettersArray[i] = annotation.property(); + types.put(annotation.property(), param.getParameterizedType()); + + // Prefer the bean getter method, then the field + + try { + var getterMethod = exact.getMethod("get" + annotation.property().substring(0, 1).toUpperCase() + annotation.property().substring(1)); + if (getterMethod.getGenericReturnType().equals(param.getParameterizedType()) && getterMethod.accessFlags().contains(AccessFlag.PUBLIC) && !getterMethod.accessFlags().contains(AccessFlag.STATIC)) { + var getter = getterWrapper(exact, MethodHandles.lookup().unreflect(getterMethod)); + getters.put(annotation.property(), getter); + thisContext.add(getterMethod); + continue; + } + } catch (NoSuchMethodException ignored) { + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + + try { + var field = exact.getField(annotation.property()); + if (field.getType().equals(param.getType()) && field.accessFlags().contains(AccessFlag.PUBLIC) && !field.accessFlags().contains(AccessFlag.STATIC)) { + var getter = getterWrapper(exact, MethodHandles.lookup().unreflectGetter(field)); + getters.put(annotation.property(), getter); + thisContext.add(field); + } + } catch (NoSuchFieldException ignored) { + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + for (var method : exact.getMethods()) { + if (method.accessFlags().contains(AccessFlag.PUBLIC) && !method.accessFlags().contains(AccessFlag.STATIC)) { + var isGetter = method.getParameterCount() == 0 && ( + method.getName().startsWith("get") || + (method.getName().startsWith("is") && method.getGenericReturnType().equals(Boolean.TYPE)) + ); + var isSetter = method.getParameterCount() == 1 && method.getName().startsWith("set") && method.getGenericReturnType().equals(Void.TYPE); + if (isGetter) { + var property = method.getName().substring(method.getName().startsWith("is") ? 2 : 3); + property = property.substring(0, 1).toLowerCase() + property.substring(1); + if (!types.containsKey(property) && method.getGenericReturnType().equals(types.get(property))) { + types.put(property, method.getGenericReturnType()); + if (!getters.containsKey(property)) { + try { + var getter = getterWrapper(exact, MethodHandles.lookup().unreflect(method)); + getters.put(property, getter); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + context.computeIfAbsent(property, k -> new ArrayList<>()).add(method); + } + } else if (isSetter) { + var property = method.getName().substring(3); + property = property.substring(0, 1).toLowerCase() + property.substring(1); + if (!types.containsKey(property) && method.getParameterTypes()[0].equals(types.get(property))) { + types.put(property, method.getGenericReturnType()); + if (!setters.containsKey(property)) { + try { + var setter = MethodHandles.lookup().unreflect(method); + setters.put(property, setter); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + context.computeIfAbsent(property, k -> new ArrayList<>()).add(method); + } + } + } + } + + for (var field : exact.getFields()) { + if (field.accessFlags().contains(AccessFlag.PUBLIC) && !field.accessFlags().contains(AccessFlag.STATIC) && !field.accessFlags().contains(AccessFlag.TRANSIENT)) { + try { + if (!types.containsKey(field.getName())) { + types.put(field.getName(), field.getGenericType()); + } + context.computeIfAbsent(field.getName(), k -> new ArrayList<>()).add(field); + if (!getters.containsKey(field.getName())) { + var getter = getterWrapper(exact, MethodHandles.lookup().unreflectGetter(field)); + getters.put(field.getName(), getter); + } + if (!setters.containsKey(field.getName()) && (!ctorSetters.containsKey(field.getName())) && !field.accessFlags().contains(AccessFlag.FINAL)) { + var setter = MethodHandles.lookup().unreflectSetter(field); + setters.put(field.getName(), setter); + } + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + } + } else { + for (int i = 0; i < exact.getRecordComponents().length; i++) { + var component = exact.getRecordComponents()[i]; + + var thisContext = context.computeIfAbsent(component.getName(), k -> new ArrayList<>()); + thisContext.add(component); + + ctorSetters.put(component.getName(), i); + ctorSettersArray[i] = component.getName(); + types.put(component.getName(), component.getGenericType()); + + try { + var getterMethod = exact.getMethod(component.getName()); + var getter = getterWrapper(exact, MethodHandles.lookup().unreflect(getterMethod)); + thisContext.add(getterMethod); + getters.put(component.getName(), getter); + } catch (NoSuchMethodException ignored) { + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + } + + var properties = new LinkedHashSet(); + for (var entry : types.keySet()) { + if (getters.containsKey(entry) && (setters.containsKey(entry) || ctorSetters.containsKey(entry))) { + properties.add(entry); + } + } + var propertyList = new ArrayList<>(properties); + var keyList = new ArrayList>(propertyList.size()); + for (var property : propertyList) { + var getter = getters.get(property); + var key = add(builder, property, types.get(property), getter, creator); + keyList.add(key); + } + var cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); + var name = BuiltInReflectiveStructureCreator.class.getName().replace('.', '/') + "$RecordWrapper"; + cw.visit(Opcodes.V21, Opcodes.ACC_FINAL, name, null, "java/lang/Object", new String[]{"java/util/function/Function"}); + var mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "", "()V", null, null); + mv.visitCode(); + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "", "()V", false); + mv.visitInsn(Opcodes.RETURN); + mv.visitMaxs(0, 0); + mv.visitEnd(); + mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "apply", "(Ljava/lang/Object;)Ljava/lang/Object;", null, null); + var classData = new ArrayList<>(); + Map offsetMap = new HashMap<>(); + String ctorDescriptor; + try { + var ctorHandle = MethodHandles.lookup().unreflectConstructor(validCtor); + classData.add(ctorHandle); + ctorDescriptor = ctorHandle.type().changeReturnType(Void.TYPE).descriptorString(); + var j = 1; + for (int i = 0; i < propertyList.size(); i++) { + var key = keyList.get(i); + var property = propertyList.get(i); + classData.add(key); + offsetMap.put(property, j); + j++; + if (!ctorSetters.containsKey(property)) { + var setter = setters.get(property).asType(MethodType.methodType(Void.TYPE, Object.class, Object.class)); + classData.add(setter); + j++; + } + } + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + mv.visitVarInsn(Opcodes.ALOAD, 1); + mv.visitTypeInsn(Opcodes.CHECKCAST, RecordStructure.Container.class.getName().replace('.', '/')); + mv.visitVarInsn(Opcodes.ASTORE, 2); + mv.visitTypeInsn(Opcodes.NEW, exact.getName().replace('.', '/')); + mv.visitInsn(Opcodes.DUP); + for (int i = 0; i < ctorSettersArray.length; i++) { + var property = ctorSettersArray[i]; + int keyOffset = offsetMap.get(property); + mv.visitLdcInsn(conDyn(org.objectweb.asm.Type.getDescriptor(RecordStructure.Key.class), keyOffset)); + mv.visitVarInsn(Opcodes.ALOAD, 2); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, org.objectweb.asm.Type.getInternalName(RecordStructure.Key.class), "apply", MethodType.methodType(Object.class, RecordStructure.Container.class).descriptorString(), false); + convertType(mv, validCtor.getParameters()[i].getType()); + } + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, exact.getName().replace('.', '/'), "", ctorDescriptor, false); + mv.visitVarInsn(Opcodes.ASTORE, 3); + for (var property : propertyList) { + if (!ctorSetters.containsKey(property)) { + mv.visitVarInsn(Opcodes.ALOAD, 3); + var keyOffset = offsetMap.get(property); + mv.visitLdcInsn(conDyn(org.objectweb.asm.Type.getDescriptor(RecordStructure.Key.class), keyOffset)); + mv.visitVarInsn(Opcodes.ALOAD, 2); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, org.objectweb.asm.Type.getInternalName(RecordStructure.Key.class), "apply", MethodType.methodType(Object.class, RecordStructure.Container.class).descriptorString(), false); + invokeAsCallSite(mv, "(Ljava/lang/Object;Ljava/lang/Object;)V", offsetMap.get(property)+1); + } + } + mv.visitVarInsn(Opcodes.ALOAD, 3); + mv.visitInsn(Opcodes.ARETURN); + mv.visitMaxs(0, 0); + mv.visitEnd(); + cw.visitEnd(); + var bytes = cw.toByteArray(); + try { + var lookup = MethodHandles.lookup().defineHiddenClassWithClassData(bytes, classData, true, MethodHandles.Lookup.ClassOption.NESTMATE); + @SuppressWarnings("unchecked") var instance = (Function) lookup.lookupClass().getConstructor().newInstance(); + return instance; + } catch (Throwable e) { + throw new RuntimeException(e); + } + }); + } + + @Override + public int priority() { + // This takes low priority as it is meant as a final fallback + return -10; + } + + @Override + public boolean supports(Class exact) { + // For a class to be record-able, it must meet a few conditions. It must: + // - Be a record + // - OR: Have a public no-args constructor + // - OR: Have a single public constructor with all args marked with @SerializedProperty + // Then, the structure works by: + // - constructing the object, with any properties that are present in that fashion + // - setting any properties that have setters present + // Properties are included if they have both a getter (method or public field) and setter (ctor argument, method, or public non-final field) present. + if (exact.isRecord()) { + return true; + } + + var validCtors = 0; + for (var ctor : exact.getConstructors()) { + if (!ctor.accessFlags().contains(AccessFlag.PUBLIC)) { + continue; + } + if (ctor.getParameterCount() == 0) { + validCtors++; + } else { + var hasSerializedProperties = true; + for (var param : ctor.getParameters()) { + if (!param.isAnnotationPresent(SerializedProperty.class)) { + hasSerializedProperties = false; + break; + } + var annotation = param.getAnnotation(SerializedProperty.class); + try { + var field = exact.getField(annotation.property()); + if (field.getType().equals(param.getType()) && field.accessFlags().contains(AccessFlag.PUBLIC) && !field.accessFlags().contains(AccessFlag.STATIC)) { + continue; + } + } catch (NoSuchFieldException ignored) {} + + var hasGetter = false; + var hasGetterAlt = false; + try { + var getterMethod = exact.getMethod("get" + annotation.property().substring(0, 1).toUpperCase() + annotation.property().substring(1)); + if (getterMethod.getGenericReturnType().equals(param.getParameterizedType()) && getterMethod.accessFlags().contains(AccessFlag.PUBLIC) && !getterMethod.accessFlags().contains(AccessFlag.STATIC)) { + hasGetter = true; + } + } catch (NoSuchMethodException ignored) {} + try { + var getterMethod = exact.getMethod(annotation.property()); + if (getterMethod.getGenericReturnType().equals(param.getParameterizedType()) && getterMethod.accessFlags().contains(AccessFlag.PUBLIC) && !getterMethod.accessFlags().contains(AccessFlag.STATIC)) { + hasGetterAlt = true; + } + } catch (NoSuchMethodException ignored) {} + if ((!hasGetter && !hasGetterAlt) || (hasGetter && hasGetterAlt)) { + hasSerializedProperties = false; + break; + } + } + if (hasSerializedProperties) { + validCtors++; + } + } + } + return validCtors == 1; + } + }) + .build(); + } + + private static void convertType(MethodVisitor mv, Class type) { + if (type.isPrimitive()) { + switch (type.getName()) { + case "void" -> mv.visitInsn(Opcodes.POP); + case "boolean" -> { + mv.visitTypeInsn(Opcodes.CHECKCAST, Boolean.class.getName().replace('.', '/')); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Boolean.class.getName().replace('.', '/'), "booleanValue", "()Z", false); + } + case "byte" -> { + mv.visitTypeInsn(Opcodes.CHECKCAST, Byte.class.getName().replace('.', '/')); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Byte.class.getName().replace('.', '/'), "byteValue", "()B", false); + } + case "short" -> { + mv.visitTypeInsn(Opcodes.CHECKCAST, Short.class.getName().replace('.', '/')); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Short.class.getName().replace('.', '/'), "shortValue", "()S", false); + } + case "int" -> { + mv.visitTypeInsn(Opcodes.CHECKCAST, Integer.class.getName().replace('.', '/')); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Integer.class.getName().replace('.', '/'), "intValue", "()I", false); + } + case "long" -> { + mv.visitTypeInsn(Opcodes.CHECKCAST, Long.class.getName().replace('.', '/')); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Long.class.getName().replace('.', '/'), "longValue", "()J", false); + } + case "float" -> { + mv.visitTypeInsn(Opcodes.CHECKCAST, Float.class.getName().replace('.', '/')); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Float.class.getName().replace('.', '/'), "floatValue", "()F", false); + } + case "double" -> { + mv.visitTypeInsn(Opcodes.CHECKCAST, Double.class.getName().replace('.', '/')); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Double.class.getName().replace('.', '/'), "doubleValue", "()D", false); + } + case "char" -> { + mv.visitTypeInsn(Opcodes.CHECKCAST, Character.class.getName().replace('.', '/')); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Character.class.getName().replace('.', '/'), "charValue", "()C", false); + } + } + } else { + mv.visitTypeInsn(Opcodes.CHECKCAST, org.objectweb.asm.Type.getInternalName(type)); + } + } +} diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java new file mode 100644 index 0000000..a019084 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java @@ -0,0 +1,88 @@ +package dev.lukebemish.codecextras.structured.reflective; + +import dev.lukebemish.codecextras.structured.Structure; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.function.Function; + +public interface ReflectiveStructureCreator { + Map, Creator> creators(); + Map, ParameterizedCreator> parameterizedCreators(); + List flexibleCreators(); + + interface Creator { + Structure create(); + } + + interface FlexibleCreator { + Structure create(Class exact, Function> creator); + boolean supports(Class exact); + default int priority() { + return 0; + } + default Creator creator(Class exact, Function> creator) { + return () -> create(exact, creator); + } + } + + interface ParameterizedCreator { + Structure create(Creator[] parameters); + default Creator creator(Creator[] parameters) { + return () -> create(parameters); + } + } + + @SuppressWarnings("unchecked") + static Structure create(Class clazz) { + List services = new ArrayList<>(); + ServiceLoader.load(ReflectiveStructureCreator.class).forEach(services::add); + if (clazz.getModule().getLayer() == null) { + ServiceLoader.load(ReflectiveStructureCreator.class, clazz.getClassLoader()).forEach(services::add); + } else { + ServiceLoader.load(clazz.getModule().getLayer(), ReflectiveStructureCreator.class).forEach(services::add); + } + Map, Creator> creatorsMap = new IdentityHashMap<>(); + Map, ParameterizedCreator> parameterizedCreatorsMap = new IdentityHashMap<>(); + List flexibleCreators = new ArrayList<>(); + services.forEach(creator -> { + creatorsMap.putAll(creator.creators()); + parameterizedCreatorsMap.putAll(creator.parameterizedCreators()); + flexibleCreators.addAll(creator.flexibleCreators()); + }); + flexibleCreators.sort((a, b) -> Integer.compare(b.priority(), a.priority())); + var creator = forType(clazz, creatorsMap, parameterizedCreatorsMap, flexibleCreators); + return (Structure) creator.create(); + } + + private static Creator forType(Type type, Map, Creator> creatorsMap, Map, ParameterizedCreator> parameterizedCreatorsMap, List flexibleCreators) { + if (type instanceof ParameterizedType parameterizedType) { + var rawType = parameterizedType.getRawType(); + if (rawType instanceof Class clazz && parameterizedCreatorsMap.containsKey(clazz)) { + var parameters = parameterizedType.getActualTypeArguments(); + Creator[] parameterCreators = new Creator[parameters.length]; + for (int i = 0; i < parameters.length; i++) { + parameterCreators[i] = forType(parameters[i], creatorsMap, parameterizedCreatorsMap, flexibleCreators); + } + return parameterizedCreatorsMap.get(clazz).creator(parameterCreators); + } + } + if (type instanceof Class clazz) { + var foundCreator = creatorsMap.get(clazz); + if (foundCreator != null) { + return foundCreator; + } + for (var flexibleCreator : flexibleCreators) { + if (flexibleCreator.supports(clazz)) { + return flexibleCreator.creator(clazz, type1 -> forType(type1, creatorsMap, parameterizedCreatorsMap, flexibleCreators).create()); + } + } + } + throw new IllegalArgumentException("No creator found for type: " + type); + } +} diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/SerializedProperty.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/SerializedProperty.java new file mode 100644 index 0000000..2ac8553 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/SerializedProperty.java @@ -0,0 +1,9 @@ +package dev.lukebemish.codecextras.structured.reflective; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +public @interface SerializedProperty { + String property(); +} diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/package-info.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/package-info.java new file mode 100644 index 0000000..88860e3 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/package-info.java @@ -0,0 +1,6 @@ +@NullMarked +@ApiStatus.Experimental +package dev.lukebemish.codecextras.structured.reflective; + +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index edf4411..578928a 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -1,5 +1,6 @@ module dev.lukebemish.codecextras { uses dev.lukebemish.codecextras.companion.AlternateCompanionRetriever; + uses dev.lukebemish.codecextras.structured.reflective.ReflectiveStructureCreator; requires static autoextension; requires static com.electronwill.nightconfig.core; @@ -13,6 +14,7 @@ requires static org.jspecify; requires static org.objectweb.asm; requires static org.slf4j; + requires com.google.auto.service; exports dev.lukebemish.codecextras; exports dev.lukebemish.codecextras.comments; diff --git a/src/test/java/dev/lukebemish/codecextras/test/CodecAssertions.java b/src/test/java/dev/lukebemish/codecextras/test/CodecAssertions.java index 781e12f..7bde5aa 100644 --- a/src/test/java/dev/lukebemish/codecextras/test/CodecAssertions.java +++ b/src/test/java/dev/lukebemish/codecextras/test/CodecAssertions.java @@ -20,7 +20,17 @@ public static void assertDecodes(DynamicOps jsonOps, String jso public static void assertDecodes(DynamicOps ops, T data, O expected, Codec codec) { DataResult dataResult = codec.parse(ops, data); Assertions.assertTrue(dataResult.result().isPresent(), () -> dataResult.error().orElseThrow().message()); - Assertions.assertEquals(expected, dataResult.result().get()); + switch (expected) { + case boolean[] booleans -> Assertions.assertArrayEquals(booleans, (boolean[]) dataResult.result().get()); + case byte[] bytes -> Assertions.assertArrayEquals(bytes, (byte[]) dataResult.result().get()); + case int[] ints -> Assertions.assertArrayEquals(ints, (int[]) dataResult.result().get()); + case long[] longs -> Assertions.assertArrayEquals(longs, (long[]) dataResult.result().get()); + case float[] floats -> Assertions.assertArrayEquals(floats, (float[]) dataResult.result().get(), 0.0f); + case double[] doubles -> Assertions.assertArrayEquals(doubles, (double[]) dataResult.result().get(), 0.0); + case char[] chars -> Assertions.assertArrayEquals(chars, (char[]) dataResult.result().get()); + case Object[] objects -> Assertions.assertArrayEquals(objects, (Object[]) dataResult.result().get()); + default -> Assertions.assertEquals(expected, dataResult.result().get()); + } } public static void assertDecodesOrPartial(DynamicOps jsonOps, String json, O expected, Codec codec) { diff --git a/src/test/java/dev/lukebemish/codecextras/test/structured/reflective/TestReflective.java b/src/test/java/dev/lukebemish/codecextras/test/structured/reflective/TestReflective.java new file mode 100644 index 0000000..ced41a8 --- /dev/null +++ b/src/test/java/dev/lukebemish/codecextras/test/structured/reflective/TestReflective.java @@ -0,0 +1,79 @@ +package dev.lukebemish.codecextras.test.structured.reflective; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.JsonOps; +import dev.lukebemish.codecextras.structured.CodecInterpreter; +import dev.lukebemish.codecextras.structured.Structure; +import dev.lukebemish.codecextras.structured.reflective.ReflectiveStructureCreator; +import dev.lukebemish.codecextras.test.CodecAssertions; +import org.junit.jupiter.api.Test; + +public class TestReflective { + public record TestRecord(int a, String b, TestEnum c) { + private static final Structure STRUCTURE = ReflectiveStructureCreator.create(TestRecord.class); + } + + public enum TestEnum { + A, + B, + C + } + + private static final Codec CODEC = CodecInterpreter.create().interpret(TestRecord.STRUCTURE).getOrThrow(); + + private static final Codec ARRAY_CODEC = CodecInterpreter.create().interpret(ReflectiveStructureCreator.create(TestRecord[].class)).getOrThrow(); + private static final Codec PRIMITIVE_ARRAY_CODEC = CodecInterpreter.create().interpret(ReflectiveStructureCreator.create(int[].class)).getOrThrow(); + + private final String json = """ + { + "a": 1, + "b": "test", + "c": "A" + }"""; + + private final String arrayJson = """ + [ + { + "a": 1, + "b": "test", + "c": "A" + } + ]"""; + + private final String primitiveArrayJson = """ + [1, 2, 3]"""; + + private final TestRecord object = new TestRecord(1, "test", TestEnum.A); + private final TestRecord[] array = new TestRecord[] { object }; + private final int[] primitiveArray = new int[] { 1, 2, 3 }; + + @Test + void testDecoding() { + CodecAssertions.assertDecodes(JsonOps.INSTANCE, json, object, CODEC); + } + + @Test + void testEncoding() { + CodecAssertions.assertEncodes(JsonOps.INSTANCE, object, json, CODEC); + } + + @Test + void testDecodingArray() { + CodecAssertions.assertDecodes(JsonOps.INSTANCE, arrayJson, array, ARRAY_CODEC); + } + + @Test + void testEncodingArray() { + CodecAssertions.assertEncodes(JsonOps.INSTANCE, array, arrayJson, ARRAY_CODEC); + } + + @Test + void testDecodingPrimitiveArray() { + CodecAssertions.assertDecodes(JsonOps.INSTANCE, primitiveArrayJson, primitiveArray, PRIMITIVE_ARRAY_CODEC); + } + + @Test + void testEncodingPrimitiveArray() { + CodecAssertions.assertEncodes(JsonOps.INSTANCE, primitiveArray, primitiveArrayJson, PRIMITIVE_ARRAY_CODEC); + } +} diff --git a/src/test/java/dev/lukebemish/codecextras/test/structured/reflective/package-info.java b/src/test/java/dev/lukebemish/codecextras/test/structured/reflective/package-info.java new file mode 100644 index 0000000..f56e0f4 --- /dev/null +++ b/src/test/java/dev/lukebemish/codecextras/test/structured/reflective/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package dev.lukebemish.codecextras.test.structured.reflective; + +import org.jspecify.annotations.NullMarked; From f4ff7426489428426513c44467fbc181265bec5e Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Mon, 3 Mar 2025 16:44:14 -0600 Subject: [PATCH 02/28] Use caller discovered services as well. --- .../reflective/ReflectiveStructureCreator.java | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java index a019084..fc6eef7 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java @@ -6,8 +6,10 @@ import java.lang.reflect.Type; import java.util.ArrayList; import java.util.IdentityHashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.SequencedMap; import java.util.ServiceLoader; import java.util.function.Function; @@ -40,17 +42,23 @@ default Creator creator(Creator[] parameters) { @SuppressWarnings("unchecked") static Structure create(Class clazz) { - List services = new ArrayList<>(); - ServiceLoader.load(ReflectiveStructureCreator.class).forEach(services::add); + SequencedMap, ReflectiveStructureCreator> services = new LinkedHashMap<>(); + ServiceLoader.load(ReflectiveStructureCreator.class).forEach(s -> services.putIfAbsent(s.getClass(), s)); if (clazz.getModule().getLayer() == null) { - ServiceLoader.load(ReflectiveStructureCreator.class, clazz.getClassLoader()).forEach(services::add); + ServiceLoader.load(ReflectiveStructureCreator.class, clazz.getClassLoader()).forEach(s -> services.putIfAbsent(s.getClass(), s)); } else { - ServiceLoader.load(clazz.getModule().getLayer(), ReflectiveStructureCreator.class).forEach(services::add); + ServiceLoader.load(clazz.getModule().getLayer(), ReflectiveStructureCreator.class).forEach(s -> services.putIfAbsent(s.getClass(), s)); + } + var caller = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE).getCallerClass(); + if (caller.getModule().getLayer() == null) { + ServiceLoader.load(ReflectiveStructureCreator.class, caller.getClassLoader()).forEach(s -> services.putIfAbsent(s.getClass(), s)); + } else { + ServiceLoader.load(caller.getModule().getLayer(), ReflectiveStructureCreator.class).forEach(s -> services.putIfAbsent(s.getClass(), s)); } Map, Creator> creatorsMap = new IdentityHashMap<>(); Map, ParameterizedCreator> parameterizedCreatorsMap = new IdentityHashMap<>(); List flexibleCreators = new ArrayList<>(); - services.forEach(creator -> { + services.values().forEach(creator -> { creatorsMap.putAll(creator.creators()); parameterizedCreatorsMap.putAll(creator.parameterizedCreators()); flexibleCreators.addAll(creator.flexibleCreators()); From cab51c48cd4932ba7c3da71638614c1f2cce3846 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Mon, 3 Mar 2025 16:47:40 -0600 Subject: [PATCH 03/28] Add jmh benchmark for reflective structure creator --- .../codecextras/jmh/LargeRecordsDecode.java | 16 +++++++++++++++- .../codecextras/jmh/LargeRecordsEncode.java | 16 +++++++++++++++- .../lukebemish/codecextras/jmh/TestRecord.java | 5 +++++ 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/jmh/java/dev/lukebemish/codecextras/jmh/LargeRecordsDecode.java b/src/jmh/java/dev/lukebemish/codecextras/jmh/LargeRecordsDecode.java index 56cedc6..8bef038 100644 --- a/src/jmh/java/dev/lukebemish/codecextras/jmh/LargeRecordsDecode.java +++ b/src/jmh/java/dev/lukebemish/codecextras/jmh/LargeRecordsDecode.java @@ -2,7 +2,6 @@ import com.google.gson.JsonElement; import com.mojang.serialization.JsonOps; -import java.util.concurrent.TimeUnit; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.Fork; @@ -15,6 +14,8 @@ import org.openjdk.jmh.annotations.Warmup; import org.openjdk.jmh.infra.Blackhole; +import java.util.concurrent.TimeUnit; + public class LargeRecordsDecode { @Measurement(time = 2, iterations = 5) @Warmup(time = 2, iterations = 2) @@ -51,6 +52,13 @@ public void methodHandleRecordCodecBuilder(Blackhole blackhole) { var result = TestRecord.MHRCB.decode(JsonOps.INSTANCE, json); blackhole.consume(result.result().orElseThrow()); } + + @Benchmark + public void reflectiveStructureCreator(Blackhole blackhole) { + JsonElement json = TestRecord.makeData(counter++); + var result = TestRecord.RSC.decode(JsonOps.INSTANCE, json); + blackhole.consume(result.result().orElseThrow()); + } } @OutputTimeUnit(TimeUnit.MICROSECONDS) @@ -91,5 +99,11 @@ public void methodHandleRecordCodecBuilder(Blackhole blackhole) { var result = TestRecord.MHRCB.decode(JsonOps.INSTANCE, json); blackhole.consume(result.result().orElseThrow()); } + + @Benchmark + public void reflectiveStructureCreator(Blackhole blackhole) { + var result = TestRecord.RSC.decode(JsonOps.INSTANCE, json); + blackhole.consume(result.result().orElseThrow()); + } } } diff --git a/src/jmh/java/dev/lukebemish/codecextras/jmh/LargeRecordsEncode.java b/src/jmh/java/dev/lukebemish/codecextras/jmh/LargeRecordsEncode.java index 0113c05..82d0fcd 100644 --- a/src/jmh/java/dev/lukebemish/codecextras/jmh/LargeRecordsEncode.java +++ b/src/jmh/java/dev/lukebemish/codecextras/jmh/LargeRecordsEncode.java @@ -1,7 +1,6 @@ package dev.lukebemish.codecextras.jmh; import com.mojang.serialization.JsonOps; -import java.util.concurrent.TimeUnit; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.Fork; @@ -14,6 +13,8 @@ import org.openjdk.jmh.annotations.Warmup; import org.openjdk.jmh.infra.Blackhole; +import java.util.concurrent.TimeUnit; + public class LargeRecordsEncode { @Measurement(time = 2, iterations = 5) @Warmup(time = 2, iterations = 2) @@ -50,6 +51,13 @@ public void methodHandleRecordCodecBuilder(Blackhole blackhole) { var result = TestRecord.MHRCB.encodeStart(JsonOps.INSTANCE, record); blackhole.consume(result.result().orElseThrow()); } + + @Benchmark + public void reflectiveStructureCreator(Blackhole blackhole) { + TestRecord record = TestRecord.makeRecord(counter++); + var result = TestRecord.RSC.encodeStart(JsonOps.INSTANCE, record); + blackhole.consume(result.result().orElseThrow()); + } } @OutputTimeUnit(TimeUnit.MICROSECONDS) @@ -90,5 +98,11 @@ public void methodHandleRecordCodecBuilder(Blackhole blackhole) { var result = TestRecord.MHRCB.encodeStart(JsonOps.INSTANCE, record); blackhole.consume(result.result().orElseThrow()); } + + @Benchmark + public void reflectiveStructureCreator(Blackhole blackhole) { + var result = TestRecord.RSC.encodeStart(JsonOps.INSTANCE, record); + blackhole.consume(result.result().orElseThrow()); + } } } diff --git a/src/jmh/java/dev/lukebemish/codecextras/jmh/TestRecord.java b/src/jmh/java/dev/lukebemish/codecextras/jmh/TestRecord.java index b932aba..266e5e8 100644 --- a/src/jmh/java/dev/lukebemish/codecextras/jmh/TestRecord.java +++ b/src/jmh/java/dev/lukebemish/codecextras/jmh/TestRecord.java @@ -6,6 +6,9 @@ import dev.lukebemish.codecextras.record.CurriedRecordCodecBuilder; import dev.lukebemish.codecextras.record.KeyedRecordCodecBuilder; import dev.lukebemish.codecextras.record.MethodHandleRecordCodecBuilder; +import dev.lukebemish.codecextras.structured.CodecInterpreter; +import dev.lukebemish.codecextras.structured.reflective.ReflectiveStructureCreator; + import java.lang.invoke.MethodHandles; record TestRecord( @@ -101,6 +104,8 @@ record TestRecord( ); }); + public static final Codec RSC = CodecInterpreter.create().interpret(ReflectiveStructureCreator.create(TestRecord.class)).getOrThrow(); + public static TestRecord makeRecord(int i) { return new TestRecord( i, i + 1, i + 2, i + 3, i + 4, i + 5, i + 6, i + 7, i + 8, i + 9, i + 10, i + 11, i + 12, i + 13, i + 14, i + 15 From dc02d2a553084be0d4f213cc8fe0afa2ff19e04a Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Mon, 3 Mar 2025 16:55:58 -0600 Subject: [PATCH 04/28] Make JMH benchmark test record public for reflective structure creator --- src/jmh/java/dev/lukebemish/codecextras/jmh/TestRecord.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jmh/java/dev/lukebemish/codecextras/jmh/TestRecord.java b/src/jmh/java/dev/lukebemish/codecextras/jmh/TestRecord.java index 266e5e8..8f36b24 100644 --- a/src/jmh/java/dev/lukebemish/codecextras/jmh/TestRecord.java +++ b/src/jmh/java/dev/lukebemish/codecextras/jmh/TestRecord.java @@ -11,7 +11,7 @@ import java.lang.invoke.MethodHandles; -record TestRecord( +public record TestRecord( int a, int b, int c, int d, int e, int f, int g, int h, int i, int j, int k, int l, From ae1be3f407f75f13a08ad39a5752a35025024392 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Mon, 3 Mar 2025 16:58:02 -0600 Subject: [PATCH 05/28] Apply formatting --- .../codecextras/jmh/LargeRecordsDecode.java | 3 +-- .../codecextras/jmh/LargeRecordsEncode.java | 3 +-- .../lukebemish/codecextras/jmh/TestRecord.java | 1 - .../record/MethodHandleRecordCodecBuilder.java | 15 +++++++-------- .../BuiltInReflectiveStructureCreator.java | 13 ++++++------- .../reflective/ReflectiveStructureCreator.java | 1 - 6 files changed, 15 insertions(+), 21 deletions(-) diff --git a/src/jmh/java/dev/lukebemish/codecextras/jmh/LargeRecordsDecode.java b/src/jmh/java/dev/lukebemish/codecextras/jmh/LargeRecordsDecode.java index 8bef038..1d18cc8 100644 --- a/src/jmh/java/dev/lukebemish/codecextras/jmh/LargeRecordsDecode.java +++ b/src/jmh/java/dev/lukebemish/codecextras/jmh/LargeRecordsDecode.java @@ -2,6 +2,7 @@ import com.google.gson.JsonElement; import com.mojang.serialization.JsonOps; +import java.util.concurrent.TimeUnit; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.Fork; @@ -14,8 +15,6 @@ import org.openjdk.jmh.annotations.Warmup; import org.openjdk.jmh.infra.Blackhole; -import java.util.concurrent.TimeUnit; - public class LargeRecordsDecode { @Measurement(time = 2, iterations = 5) @Warmup(time = 2, iterations = 2) diff --git a/src/jmh/java/dev/lukebemish/codecextras/jmh/LargeRecordsEncode.java b/src/jmh/java/dev/lukebemish/codecextras/jmh/LargeRecordsEncode.java index 82d0fcd..2c60abd 100644 --- a/src/jmh/java/dev/lukebemish/codecextras/jmh/LargeRecordsEncode.java +++ b/src/jmh/java/dev/lukebemish/codecextras/jmh/LargeRecordsEncode.java @@ -1,6 +1,7 @@ package dev.lukebemish.codecextras.jmh; import com.mojang.serialization.JsonOps; +import java.util.concurrent.TimeUnit; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.Fork; @@ -13,8 +14,6 @@ import org.openjdk.jmh.annotations.Warmup; import org.openjdk.jmh.infra.Blackhole; -import java.util.concurrent.TimeUnit; - public class LargeRecordsEncode { @Measurement(time = 2, iterations = 5) @Warmup(time = 2, iterations = 2) diff --git a/src/jmh/java/dev/lukebemish/codecextras/jmh/TestRecord.java b/src/jmh/java/dev/lukebemish/codecextras/jmh/TestRecord.java index 8f36b24..2ab4d95 100644 --- a/src/jmh/java/dev/lukebemish/codecextras/jmh/TestRecord.java +++ b/src/jmh/java/dev/lukebemish/codecextras/jmh/TestRecord.java @@ -8,7 +8,6 @@ import dev.lukebemish.codecextras.record.MethodHandleRecordCodecBuilder; import dev.lukebemish.codecextras.structured.CodecInterpreter; import dev.lukebemish.codecextras.structured.reflective.ReflectiveStructureCreator; - import java.lang.invoke.MethodHandles; public record TestRecord( diff --git a/src/main/java/dev/lukebemish/codecextras/record/MethodHandleRecordCodecBuilder.java b/src/main/java/dev/lukebemish/codecextras/record/MethodHandleRecordCodecBuilder.java index a9828b2..177da73 100644 --- a/src/main/java/dev/lukebemish/codecextras/record/MethodHandleRecordCodecBuilder.java +++ b/src/main/java/dev/lukebemish/codecextras/record/MethodHandleRecordCodecBuilder.java @@ -6,14 +6,6 @@ import com.mojang.serialization.MapCodec; import com.mojang.serialization.MapLike; import com.mojang.serialization.RecordBuilder; -import org.jetbrains.annotations.ApiStatus; -import org.objectweb.asm.ClassWriter; -import org.objectweb.asm.ConstantDynamic; -import org.objectweb.asm.Handle; -import org.objectweb.asm.Label; -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.Type; - import java.lang.invoke.CallSite; import java.lang.invoke.ConstantCallSite; import java.lang.invoke.MethodHandle; @@ -24,6 +16,13 @@ import java.util.List; import java.util.function.Function; import java.util.stream.Stream; +import org.jetbrains.annotations.ApiStatus; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.ConstantDynamic; +import org.objectweb.asm.Handle; +import org.objectweb.asm.Label; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; @ApiStatus.Experimental public final class MethodHandleRecordCodecBuilder { diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/BuiltInReflectiveStructureCreator.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/BuiltInReflectiveStructureCreator.java index 2f70195..6553910 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/BuiltInReflectiveStructureCreator.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/BuiltInReflectiveStructureCreator.java @@ -9,13 +9,6 @@ import com.mojang.serialization.Dynamic; import dev.lukebemish.codecextras.structured.RecordStructure; import dev.lukebemish.codecextras.structured.Structure; -import org.jetbrains.annotations.ApiStatus; -import org.objectweb.asm.ClassWriter; -import org.objectweb.asm.ConstantDynamic; -import org.objectweb.asm.Handle; -import org.objectweb.asm.MethodVisitor; -import org.objectweb.asm.Opcodes; - import java.lang.invoke.CallSite; import java.lang.invoke.ConstantCallSite; import java.lang.invoke.MethodHandle; @@ -35,6 +28,12 @@ import java.util.Objects; import java.util.function.Function; import java.util.function.Supplier; +import org.jetbrains.annotations.ApiStatus; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.ConstantDynamic; +import org.objectweb.asm.Handle; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; @ApiStatus.Internal @AutoService(ReflectiveStructureCreator.class) diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java index fc6eef7..a3726d2 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java @@ -1,7 +1,6 @@ package dev.lukebemish.codecextras.structured.reflective; import dev.lukebemish.codecextras.structured.Structure; - import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; From 2b99259c288ec296d263334b063d09ff0f7fc5a2 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Mon, 3 Mar 2025 17:20:23 -0600 Subject: [PATCH 06/28] Use ClassValue-backed system for better wacky service loading --- .../codecextras/companion/DelegatingOps.java | 41 +------- .../ReflectiveStructureCreator.java | 95 +++++++++++++------ .../utility/LayeredServiceLoader.java | 61 ++++++++++++ .../codecextras/utility/package-info.java | 4 + 4 files changed, 138 insertions(+), 63 deletions(-) create mode 100644 src/main/java/dev/lukebemish/codecextras/utility/LayeredServiceLoader.java create mode 100644 src/main/java/dev/lukebemish/codecextras/utility/package-info.java diff --git a/src/main/java/dev/lukebemish/codecextras/companion/DelegatingOps.java b/src/main/java/dev/lukebemish/codecextras/companion/DelegatingOps.java index c02594d..2709748 100644 --- a/src/main/java/dev/lukebemish/codecextras/companion/DelegatingOps.java +++ b/src/main/java/dev/lukebemish/codecextras/companion/DelegatingOps.java @@ -1,6 +1,5 @@ package dev.lukebemish.codecextras.companion; -import com.google.common.collect.MapMaker; import com.mojang.datafixers.util.Pair; import com.mojang.serialization.DataResult; import com.mojang.serialization.Decoder; @@ -9,13 +8,13 @@ import com.mojang.serialization.ListBuilder; import com.mojang.serialization.MapLike; import com.mojang.serialization.RecordBuilder; +import dev.lukebemish.codecextras.utility.LayeredServiceLoader; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.ServiceLoader; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; @@ -60,10 +59,9 @@ public static DynamicOps without(Q to } } - private static final List ALTERNATE_COMPANION_RETRIEVERS; - private static final Map> RETRIEVERS = new MapMaker().weakKeys().weakValues().makeMap(); + private static final LayeredServiceLoader SERVICE_LOADER = LayeredServiceLoader.of(AlternateCompanionRetriever.class); - static { + static List forOps(DynamicOps ops) { List retrievers = new ArrayList<>(); retrievers.add(new AlternateCompanionRetriever() { @Override @@ -79,38 +77,9 @@ public AccompaniedOps delegate(DynamicOps ops, AccompaniedOps deleg return delegate; } }); - retrievers.addAll(ServiceLoader.load(AlternateCompanionRetriever.class).stream().map(ServiceLoader.Provider::get).toList()); - ALTERNATE_COMPANION_RETRIEVERS = List.copyOf(retrievers); - } - - static List forOps(DynamicOps ops) { var clazz = ops.getClass(); - var layer = clazz.getModule().getLayer(); - if (layer == null) { - return ALTERNATE_COMPANION_RETRIEVERS; - } - return RETRIEVERS.computeIfAbsent(layer, k -> { - List retrievers = new ArrayList<>(); - retrievers.add(new AlternateCompanionRetriever() { - @Override - public Optional> locateCompanionDelegate(DynamicOps ops) { - if (ops instanceof MapDelegatingOps mapOps) { - return Optional.of(mapOps); - } - return Optional.empty(); - } - - @Override - public AccompaniedOps delegate(DynamicOps ops, AccompaniedOps delegate) { - return delegate; - } - }); - retrievers.addAll(ServiceLoader.load(layer, AlternateCompanionRetriever.class).stream().map(ServiceLoader.Provider::get).toList()); - if (layer != DelegatingOps.class.getModule().getLayer()) { - retrievers.addAll(ServiceLoader.load(AlternateCompanionRetriever.class).stream().map(ServiceLoader.Provider::get).toList()); - } - return List.copyOf(retrievers); - }); + retrievers.addAll(LayeredServiceLoader.unique(SERVICE_LOADER.at(DelegatingOps.class), SERVICE_LOADER.at(clazz))); + return List.copyOf(retrievers); } private static @Nullable Pair> retrieveMapOps(DynamicOps ops) { diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java index a3726d2..86ec90b 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java @@ -1,15 +1,13 @@ package dev.lukebemish.codecextras.structured.reflective; import dev.lukebemish.codecextras.structured.Structure; +import dev.lukebemish.codecextras.utility.LayeredServiceLoader; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.IdentityHashMap; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.SequencedMap; -import java.util.ServiceLoader; import java.util.function.Function; public interface ReflectiveStructureCreator { @@ -39,32 +37,75 @@ default Creator creator(Creator[] parameters) { } } - @SuppressWarnings("unchecked") - static Structure create(Class clazz) { - SequencedMap, ReflectiveStructureCreator> services = new LinkedHashMap<>(); - ServiceLoader.load(ReflectiveStructureCreator.class).forEach(s -> services.putIfAbsent(s.getClass(), s)); - if (clazz.getModule().getLayer() == null) { - ServiceLoader.load(ReflectiveStructureCreator.class, clazz.getClassLoader()).forEach(s -> services.putIfAbsent(s.getClass(), s)); - } else { - ServiceLoader.load(clazz.getModule().getLayer(), ReflectiveStructureCreator.class).forEach(s -> services.putIfAbsent(s.getClass(), s)); + final class Instance { + private final Map, Creator> creators; + private final Map, ParameterizedCreator> parameterizedCreators; + private final List flexibleCreators; + + private Instance(Map, Creator> creators, Map, ParameterizedCreator> parameterizedCreators, List flexibleCreators) { + this.creators = creators; + this.parameterizedCreators = parameterizedCreators; + this.flexibleCreators = flexibleCreators; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private final Map, Creator> creators = new IdentityHashMap<>(); + private final Map, ParameterizedCreator> parameterizedCreators = new IdentityHashMap<>(); + private final List flexibleCreators = new ArrayList<>(); + + private Builder() {} + + public Builder withCreator(Class clazz, Creator creator) { + creators.put(clazz, creator); + return this; + } + + public Builder withParameterizedCreator(Class clazz, ParameterizedCreator creator) { + parameterizedCreators.put(clazz, creator); + return this; + } + + public Builder withFlexibleCreator(FlexibleCreator creator) { + flexibleCreators.add(creator); + return this; + } + + public Instance build() { + return new Instance(creators, parameterizedCreators, flexibleCreators); + } } - var caller = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE).getCallerClass(); - if (caller.getModule().getLayer() == null) { - ServiceLoader.load(ReflectiveStructureCreator.class, caller.getClassLoader()).forEach(s -> services.putIfAbsent(s.getClass(), s)); - } else { - ServiceLoader.load(caller.getModule().getLayer(), ReflectiveStructureCreator.class).forEach(s -> services.putIfAbsent(s.getClass(), s)); + + private static final LayeredServiceLoader SERVICE_LOADER = LayeredServiceLoader.of(ReflectiveStructureCreator.class); + + @SuppressWarnings("unchecked") + public Structure create(Class clazz) { + var caller = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE).getCallerClass(); + List services = LayeredServiceLoader.unique(SERVICE_LOADER.at(ReflectiveStructureCreator.class), SERVICE_LOADER.at(clazz), SERVICE_LOADER.at(caller)); + Map, Creator> creatorsMap = new IdentityHashMap<>(); + Map, ParameterizedCreator> parameterizedCreatorsMap = new IdentityHashMap<>(); + List flexibleCreatorsList = new ArrayList<>(); + services.forEach(creator -> { + creatorsMap.putAll(creator.creators()); + parameterizedCreatorsMap.putAll(creator.parameterizedCreators()); + flexibleCreatorsList.addAll(creator.flexibleCreators()); + }); + + creatorsMap.putAll(this.creators); + parameterizedCreatorsMap.putAll(this.parameterizedCreators); + flexibleCreatorsList.addAll(this.flexibleCreators); + + flexibleCreatorsList.sort((a, b) -> Integer.compare(b.priority(), a.priority())); + var creator = forType(clazz, creatorsMap, parameterizedCreatorsMap, flexibleCreatorsList); + return (Structure) creator.create(); } - Map, Creator> creatorsMap = new IdentityHashMap<>(); - Map, ParameterizedCreator> parameterizedCreatorsMap = new IdentityHashMap<>(); - List flexibleCreators = new ArrayList<>(); - services.values().forEach(creator -> { - creatorsMap.putAll(creator.creators()); - parameterizedCreatorsMap.putAll(creator.parameterizedCreators()); - flexibleCreators.addAll(creator.flexibleCreators()); - }); - flexibleCreators.sort((a, b) -> Integer.compare(b.priority(), a.priority())); - var creator = forType(clazz, creatorsMap, parameterizedCreatorsMap, flexibleCreators); - return (Structure) creator.create(); + } + + static Structure create(Class clazz) { + return Instance.builder().build().create(clazz); } private static Creator forType(Type type, Map, Creator> creatorsMap, Map, ParameterizedCreator> parameterizedCreatorsMap, List flexibleCreators) { diff --git a/src/main/java/dev/lukebemish/codecextras/utility/LayeredServiceLoader.java b/src/main/java/dev/lukebemish/codecextras/utility/LayeredServiceLoader.java new file mode 100644 index 0000000..9ab4c65 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/utility/LayeredServiceLoader.java @@ -0,0 +1,61 @@ +package dev.lukebemish.codecextras.utility; + +import java.lang.ref.WeakReference; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.SequencedMap; +import java.util.ServiceLoader; +import java.util.WeakHashMap; + +public final class LayeredServiceLoader { + private final Class service; + + private final WeakHashMap>> cache = new WeakHashMap<>(); + private final ClassValue> providersValue = new ClassValue<>() { + @Override + protected SingleImplementation computeValue(Class type) { + var existingReference = cache.get(type.getClassLoader()); + if (existingReference != null) { + var existing = existingReference.get(); + if (existing != null) { + return existing; + } + } + var singleImplementation = new SingleImplementation(); + if (type.getModule().getLayer() == null) { + ServiceLoader.load(service, type.getClassLoader()).forEach(provider -> singleImplementation.implementations.put(provider.getClass(), provider)); + } else { + ServiceLoader.load(type.getModule().getLayer(), service).forEach(provider -> singleImplementation.implementations.put(provider.getClass(), provider)); + } + cache.put(type.getClassLoader(), new WeakReference<>(singleImplementation)); + return singleImplementation; + } + }; + + private LayeredServiceLoader(Class service) { + this.service = service; + } + + public SingleImplementation at(Class type) { + return providersValue.get(type); + } + + @SafeVarargs + public static List unique(SingleImplementation... implementations) { + var out = new LinkedHashMap, T>(); + for (var impl : implementations) { + for (var entry : impl.implementations.entrySet()) { + out.putIfAbsent(entry.getKey(), entry.getValue()); + } + } + return List.copyOf(out.values()); + } + + public static LayeredServiceLoader of(Class service) { + return new LayeredServiceLoader<>(service); + } + + public static final class SingleImplementation { + private final SequencedMap, T> implementations = new LinkedHashMap<>(); + } +} diff --git a/src/main/java/dev/lukebemish/codecextras/utility/package-info.java b/src/main/java/dev/lukebemish/codecextras/utility/package-info.java new file mode 100644 index 0000000..a10b517 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/utility/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package dev.lukebemish.codecextras.utility; + +import org.jspecify.annotations.NullMarked; From 532aae81d6d8c011510e0d092f09951e5d5e19d5 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Mon, 3 Mar 2025 17:35:57 -0600 Subject: [PATCH 07/28] Add built-in reflective structure creators for MC types. --- build.gradle | 7 ++++ .../MinecraftReflectiveStructureCreator.java | 37 +++++++++++++++++++ .../codecextras/test/common/TestConfig.java | 20 +++++++--- .../codecextras/test/common/package-info.java | 4 ++ .../codecextras/test/fabric/package-info.java | 4 ++ 5 files changed, 66 insertions(+), 6 deletions(-) create mode 100644 src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftReflectiveStructureCreator.java create mode 100644 src/testCommon/java/dev/lukebemish/codecextras/test/common/package-info.java create mode 100644 src/testFabric/java/dev/lukebemish/codecextras/test/fabric/package-info.java diff --git a/build.gradle b/build.gradle index d7bd87c..8959f9c 100644 --- a/build.gradle +++ b/build.gradle @@ -219,6 +219,13 @@ dependencies { minecraftFabricApi project(':') minecraftNeoforgeApi project(':') + testCommonCompileOnly cLibs.bundles.compileonly + testCommonAnnotationProcessor cLibs.bundles.annotationprocessor + testFabricCompileOnly cLibs.bundles.compileonly + testFabricAnnotationProcessor cLibs.bundles.annotationprocessor + testNeoforgeCompileOnly cLibs.bundles.compileonly + testNeoforgeAnnotationProcessor cLibs.bundles.annotationprocessor + testNeoforgeCompileOnly sourceSets.minecraftNeoforge.output testNeoforgeCompileOnly sourceSets.minecraft.output testFabricCompileOnly sourceSets.minecraftFabric.output diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftReflectiveStructureCreator.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftReflectiveStructureCreator.java new file mode 100644 index 0000000..911766a --- /dev/null +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftReflectiveStructureCreator.java @@ -0,0 +1,37 @@ +package dev.lukebemish.codecextras.minecraft.structured; + +import com.google.auto.service.AutoService; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import dev.lukebemish.codecextras.structured.reflective.ReflectiveStructureCreator; +import java.util.List; +import java.util.Map; +import net.minecraft.core.component.DataComponentMap; +import net.minecraft.core.component.DataComponentPatch; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; + +@AutoService(ReflectiveStructureCreator.class) +public class MinecraftReflectiveStructureCreator implements ReflectiveStructureCreator { + @Override + public Map, Creator> creators() { + return ImmutableMap., Creator>builder() + .put(ResourceLocation.class, () -> MinecraftStructures.RESOURCE_LOCATION) + .put(DataComponentMap.class, () -> MinecraftStructures.DATA_COMPONENT_MAP) + .put(DataComponentPatch.class, () -> MinecraftStructures.DATA_COMPONENT_PATCH) + .put(ItemStack.class, () -> MinecraftStructures.ITEM_STACK) + .build(); + } + + @Override + public Map, ParameterizedCreator> parameterizedCreators() { + return ImmutableMap., ParameterizedCreator>builder() + .build(); + } + + @Override + public List flexibleCreators() { + return ImmutableList.builder() + .build(); + } +} diff --git a/src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java b/src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java index 6019151..b1fb3b7 100644 --- a/src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java +++ b/src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java @@ -10,19 +10,22 @@ import dev.lukebemish.codecextras.structured.Annotation; import dev.lukebemish.codecextras.structured.IdentityInterpreter; import dev.lukebemish.codecextras.structured.Structure; +import dev.lukebemish.codecextras.structured.reflective.ReflectiveStructureCreator; import dev.lukebemish.codecextras.structured.schema.SchemaAnnotations; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; import net.minecraft.core.component.DataComponentPatch; import net.minecraft.core.registries.Registries; import net.minecraft.references.Items; import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Rarity; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + public record TestConfig( int a, float b, boolean c, String d, Optional e, Optional f, @@ -30,7 +33,7 @@ public record TestConfig( int intInRange, float floatInRange, int argb, int rgb, ResourceKey item, Rarity rarity, Map unbounded, Either either, Map dispatchedMap, - DataComponentPatch patch, ItemStack itemStack + DataComponentPatch patch, ItemStack itemStack, ReflectiveRecord reflectiveRecord ) { private static final Map> DISPATCHES = new HashMap<>(); @@ -72,6 +75,10 @@ public String key() { } } + public record ReflectiveRecord(String x, ResourceLocation y, int[] z) { + private static final Structure STRUCTURE = ReflectiveStructureCreator.create(ReflectiveRecord.class); + } + static { DISPATCHES.put("abc", Abc.STRUCTURE); DISPATCHES.put("xyz", Xyz.STRUCTURE); @@ -98,6 +105,7 @@ public String key() { var dispatchedMap = builder.addOptional("dispatchedMap", Structure.STRING.dispatchedMap(DISPATCHES::keySet, k -> DataResult.success(DISPATCHES.get(k))), TestConfig::dispatchedMap, Map::of); var patch = builder.addOptional("patch", MinecraftStructures.DATA_COMPONENT_PATCH, TestConfig::patch, () -> DataComponentPatch.EMPTY); var itemStack = builder.addOptional("itemStack", MinecraftStructures.OPTIONAL_ITEM_STACK, TestConfig::itemStack, () -> ItemStack.EMPTY); + var reflectiveRecord = builder.addOptional("reflectiveRecord", ReflectiveRecord.STRUCTURE, TestConfig::reflectiveRecord, () -> new ReflectiveRecord("test", ResourceLocation.fromNamespaceAndPath("test", "test"), new int[] {1, 2, 3})); return container -> new TestConfig( a.apply(container), b.apply(container), c.apply(container), d.apply(container), e.apply(container), f.apply(container), @@ -105,7 +113,7 @@ public String key() { intInRange.apply(container), floatInRange.apply(container), argb.apply(container), rgb.apply(container), item.apply(container), rarity.apply(container), unbounded.apply(container), either.apply(container), dispatchedMap.apply(container), - patch.apply(container), itemStack.apply(container) + patch.apply(container), itemStack.apply(container), reflectiveRecord.apply(container) ); }); diff --git a/src/testCommon/java/dev/lukebemish/codecextras/test/common/package-info.java b/src/testCommon/java/dev/lukebemish/codecextras/test/common/package-info.java new file mode 100644 index 0000000..aba4da9 --- /dev/null +++ b/src/testCommon/java/dev/lukebemish/codecextras/test/common/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package dev.lukebemish.codecextras.test.common; + +import org.jspecify.annotations.NullMarked; diff --git a/src/testFabric/java/dev/lukebemish/codecextras/test/fabric/package-info.java b/src/testFabric/java/dev/lukebemish/codecextras/test/fabric/package-info.java new file mode 100644 index 0000000..f9d5b7a --- /dev/null +++ b/src/testFabric/java/dev/lukebemish/codecextras/test/fabric/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package dev.lukebemish.codecextras.test.fabric; + +import org.jspecify.annotations.NullMarked; From a54e1f8ebf06fdb9fba1407c49ef1c1fb10a0693 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Mon, 3 Mar 2025 17:45:03 -0600 Subject: [PATCH 08/28] Fix formatting --- .../lukebemish/codecextras/test/common/TestConfig.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java b/src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java index b1fb3b7..1901362 100644 --- a/src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java +++ b/src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java @@ -12,6 +12,10 @@ import dev.lukebemish.codecextras.structured.Structure; import dev.lukebemish.codecextras.structured.reflective.ReflectiveStructureCreator; import dev.lukebemish.codecextras.structured.schema.SchemaAnnotations; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; import net.minecraft.core.component.DataComponentPatch; import net.minecraft.core.registries.Registries; import net.minecraft.references.Items; @@ -21,11 +25,6 @@ import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Rarity; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; - public record TestConfig( int a, float b, boolean c, String d, Optional e, Optional f, From bd9d91589d3fff05b7e4558ef1fe458d6ab9e3d6 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Mon, 3 Mar 2025 17:54:05 -0600 Subject: [PATCH 09/28] Simplify ctor locating logic --- .../BuiltInReflectiveStructureCreator.java | 141 ++++++------------ 1 file changed, 44 insertions(+), 97 deletions(-) diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/BuiltInReflectiveStructureCreator.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/BuiltInReflectiveStructureCreator.java index 6553910..a44c8c6 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/BuiltInReflectiveStructureCreator.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/BuiltInReflectiveStructureCreator.java @@ -321,10 +321,50 @@ public boolean supports(Class exact) { } }) .add(new FlexibleCreator() { + private List> validCtors(Class exact) { + List> validCtors = new ArrayList<>(); + for (var ctor : exact.getConstructors()) { + if (!ctor.accessFlags().contains(AccessFlag.PUBLIC)) { + continue; + } + if (ctor.getParameterCount() == 0) { + validCtors.add(ctor); + } else { + var hasSerializedProperties = true; + for (var param : ctor.getParameters()) { + if (!param.isAnnotationPresent(SerializedProperty.class)) { + hasSerializedProperties = false; + break; + } + var annotation = param.getAnnotation(SerializedProperty.class); + try { + var field = exact.getField(annotation.property()); + if (field.getType().equals(param.getType()) && field.accessFlags().contains(AccessFlag.PUBLIC) && !field.accessFlags().contains(AccessFlag.STATIC)) { + continue; + } + } catch (NoSuchFieldException ignored) {} + + try { + var getterMethod = exact.getMethod("get" + annotation.property().substring(0, 1).toUpperCase() + annotation.property().substring(1)); + if (getterMethod.getGenericReturnType().equals(param.getParameterizedType()) && getterMethod.accessFlags().contains(AccessFlag.PUBLIC) && !getterMethod.accessFlags().contains(AccessFlag.STATIC)) { + continue; + } + } catch (NoSuchMethodException ignored) {} + hasSerializedProperties = false; + break; + } + if (hasSerializedProperties) { + validCtors.add(ctor); + } + } + } + return validCtors; + } + @Override public Structure create(Class exact, Function> creator) { return Structure.record(builder -> { - Constructor validCtor = null; + Constructor validCtor; if (exact.isRecord()) { Class[] types = new Class[exact.getRecordComponents().length]; for (int i = 0; i < types.length; i++) { @@ -336,55 +376,8 @@ public Structure create(Class exact, Function> creator) throw new RuntimeException(e); } } else { - for (var ctor : exact.getConstructors()) { - if (!ctor.accessFlags().contains(AccessFlag.PUBLIC)) { - continue; - } - if (ctor.getParameterCount() == 0) { - validCtor = ctor; - } else { - var hasSerializedProperties = true; - for (var param : ctor.getParameters()) { - if (!param.isAnnotationPresent(SerializedProperty.class)) { - hasSerializedProperties = false; - break; - } - var annotation = param.getAnnotation(SerializedProperty.class); - try { - var field = exact.getField(annotation.property()); - if (field.getType().equals(param.getType()) && field.accessFlags().contains(AccessFlag.PUBLIC) && !field.accessFlags().contains(AccessFlag.STATIC)) { - continue; - } - } catch (NoSuchFieldException ignored) { - } - - var hasGetter = false; - var hasGetterAlt = false; - try { - var getterMethod = exact.getMethod("get" + annotation.property().substring(0, 1).toUpperCase() + annotation.property().substring(1)); - if (getterMethod.getGenericReturnType().equals(param.getParameterizedType()) && getterMethod.accessFlags().contains(AccessFlag.PUBLIC) && !getterMethod.accessFlags().contains(AccessFlag.STATIC)) { - hasGetter = true; - } - } catch (NoSuchMethodException ignored) { - } - try { - var getterMethod = exact.getMethod(annotation.property()); - if (getterMethod.getGenericReturnType().equals(param.getParameterizedType()) && getterMethod.accessFlags().contains(AccessFlag.PUBLIC) && !getterMethod.accessFlags().contains(AccessFlag.STATIC)) { - hasGetterAlt = true; - } - } catch (NoSuchMethodException ignored) { - } - if ((!hasGetter && !hasGetterAlt) || (hasGetter && hasGetterAlt)) { - hasSerializedProperties = false; - break; - } - } - if (hasSerializedProperties) { - validCtor = ctor; - break; - } - } - } + var ctors = validCtors(exact); + validCtor = ctors.getFirst(); } Objects.requireNonNull(validCtor); @@ -627,53 +620,7 @@ public boolean supports(Class exact) { return true; } - var validCtors = 0; - for (var ctor : exact.getConstructors()) { - if (!ctor.accessFlags().contains(AccessFlag.PUBLIC)) { - continue; - } - if (ctor.getParameterCount() == 0) { - validCtors++; - } else { - var hasSerializedProperties = true; - for (var param : ctor.getParameters()) { - if (!param.isAnnotationPresent(SerializedProperty.class)) { - hasSerializedProperties = false; - break; - } - var annotation = param.getAnnotation(SerializedProperty.class); - try { - var field = exact.getField(annotation.property()); - if (field.getType().equals(param.getType()) && field.accessFlags().contains(AccessFlag.PUBLIC) && !field.accessFlags().contains(AccessFlag.STATIC)) { - continue; - } - } catch (NoSuchFieldException ignored) {} - - var hasGetter = false; - var hasGetterAlt = false; - try { - var getterMethod = exact.getMethod("get" + annotation.property().substring(0, 1).toUpperCase() + annotation.property().substring(1)); - if (getterMethod.getGenericReturnType().equals(param.getParameterizedType()) && getterMethod.accessFlags().contains(AccessFlag.PUBLIC) && !getterMethod.accessFlags().contains(AccessFlag.STATIC)) { - hasGetter = true; - } - } catch (NoSuchMethodException ignored) {} - try { - var getterMethod = exact.getMethod(annotation.property()); - if (getterMethod.getGenericReturnType().equals(param.getParameterizedType()) && getterMethod.accessFlags().contains(AccessFlag.PUBLIC) && !getterMethod.accessFlags().contains(AccessFlag.STATIC)) { - hasGetterAlt = true; - } - } catch (NoSuchMethodException ignored) {} - if ((!hasGetter && !hasGetterAlt) || (hasGetter && hasGetterAlt)) { - hasSerializedProperties = false; - break; - } - } - if (hasSerializedProperties) { - validCtors++; - } - } - } - return validCtors == 1; + return validCtors(exact).size() == 1; } }) .build(); From fe3e50ad52019e9908f952d9cea9ed381efc4067 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Mon, 3 Mar 2025 17:55:59 -0600 Subject: [PATCH 10/28] Add StringRepresentable reflective structure creator --- .../MinecraftReflectiveStructureCreator.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftReflectiveStructureCreator.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftReflectiveStructureCreator.java index 911766a..71d7f32 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftReflectiveStructureCreator.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftReflectiveStructureCreator.java @@ -1,14 +1,21 @@ package dev.lukebemish.codecextras.minecraft.structured; import com.google.auto.service.AutoService; +import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import dev.lukebemish.codecextras.structured.Structure; import dev.lukebemish.codecextras.structured.reflective.ReflectiveStructureCreator; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Type; import java.util.List; import java.util.Map; +import java.util.function.Function; +import java.util.function.Supplier; import net.minecraft.core.component.DataComponentMap; import net.minecraft.core.component.DataComponentPatch; import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.StringRepresentable; import net.minecraft.world.item.ItemStack; @AutoService(ReflectiveStructureCreator.class) @@ -32,6 +39,29 @@ public Map, ParameterizedCreator> parameterizedCreators() { @Override public List flexibleCreators() { return ImmutableList.builder() + .add(new FlexibleCreator() { + @Override + public Structure create(Class exact, Function> creator) { + Supplier values = Suppliers.memoize(() -> { + try { + return (Object[]) exact.getMethod("values").invoke(null); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new RuntimeException(e); + } + }); + return Structure.stringRepresentable(values, t -> ((StringRepresentable)t).getSerializedName()); + } + + @Override + public int priority() { + return 10; + } + + @Override + public boolean supports(Class exact) { + return Enum.class.isAssignableFrom(exact) && StringRepresentable.class.isAssignableFrom(exact); + } + }) .build(); } } From d80042af99351aecffb23f9397e8997df63a7f59 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Mon, 3 Mar 2025 23:47:21 -0600 Subject: [PATCH 11/28] Support for more types in reflective structure creation --- .../codecextras/structured/Interpreter.java | 34 +++ .../structured/RecordStructure.java | 30 +++ .../codecextras/structured/Structure.java | 46 ++++ .../BuiltInReflectiveStructureCreator.java | 196 ++++++++++++++++-- .../ReflectiveStructureCreator.java | 74 +++++-- .../MinecraftReflectiveStructureCreator.java | 4 +- .../structured/reflective/TestReflective.java | 20 +- 7 files changed, 357 insertions(+), 47 deletions(-) diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java index c60ff1a..f3cee13 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java @@ -9,6 +9,22 @@ import com.mojang.serialization.Dynamic; import dev.lukebemish.codecextras.StringRepresentation; import dev.lukebemish.codecextras.types.Identity; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.MonthDay; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.Period; +import java.time.Year; +import java.time.YearMonth; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; import java.util.List; import java.util.Map; import java.util.Set; @@ -60,6 +76,24 @@ default DataResult> bounded(Structure input, Supplier> v Key DOUBLE = Key.create("DOUBLE"); Key STRING = Key.create("STRING"); Key CHAR = Key.create("CHAR"); + Key BIG_INTEGER = Key.create("BIG_INTEGER"); + Key BIG_DECIMAL = Key.create("BIG_DECIMAL"); + + Key DURATION = Key.create("DURATION"); + Key INSTANT = Key.create("INSTANT"); + Key LOCAL_DATE = Key.create("LOCAL_DATE"); + Key LOCAL_DATE_TIME = Key.create("LOCAL_DATE_TIME"); + Key LOCAL_TIME = Key.create("LOCAL_TIME"); + Key MONTH_DAY = Key.create("MONTH_DAY"); + Key OFFSET_DATE_TIME = Key.create("OFFSET_DATE_TIME"); + Key OFFSET_TIME = Key.create("OFFSET_TIME"); + Key PERIOD = Key.create("PERIOD"); + Key YEAR = Key.create("YEAR"); + Key YEAR_MONTH = Key.create("YEAR_MONTH"); + Key ZONED_DATE_TIME = Key.create("ZONED_DATE_TIME"); + Key ZONE_ID = Key.create("ZONE_ID"); + Key ZONE_OFFSET = Key.create("ZONE_OFFSET"); + Key> PASSTHROUGH = Key.create("PASSTHROUGH"); Key EMPTY_MAP = Key.create("EMPTY_MAP"); Key EMPTY_LIST = Key.create("EMPTY_LIST"); diff --git a/src/main/java/dev/lukebemish/codecextras/structured/RecordStructure.java b/src/main/java/dev/lukebemish/codecextras/structured/RecordStructure.java index f4177e2..13257b3 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/RecordStructure.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/RecordStructure.java @@ -7,6 +7,9 @@ import java.util.HashSet; import java.util.List; import java.util.Optional; +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.OptionalLong; import java.util.Set; import java.util.function.Function; import java.util.function.Predicate; @@ -187,6 +190,33 @@ public Key> addOptional(String name, Structure structure, Fun return key; } + public Function addOptionalInt(String name, Function getter) { + return addOptional(name, Structure.INT, getter.andThen(o -> { + if (o.isPresent()) { + return Optional.of(o.getAsInt()); + } + return Optional.empty(); + })).andThen(o -> o.map(OptionalInt::of).orElse(OptionalInt.empty())); + } + + public Function addOptionalDouble(String name, Function getter) { + return addOptional(name, Structure.DOUBLE, getter.andThen(o -> { + if (o.isPresent()) { + return Optional.of(o.getAsDouble()); + } + return Optional.empty(); + })).andThen(o -> o.map(OptionalDouble::of).orElse(OptionalDouble.empty())); + } + + public Function addOptionalLong(String name, Function getter) { + return addOptional(name, Structure.LONG, getter.andThen(o -> { + if (o.isPresent()) { + return Optional.of(o.getAsLong()); + } + return Optional.empty(); + })).andThen(o -> o.map(OptionalLong::of).orElse(OptionalLong.empty())); + } + /** * Add a field to the record structure with a default value. The field will not be encoded if equal to its default value. * @param name the name of the field diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java index 4068f41..8365bb6 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java @@ -12,6 +12,22 @@ import dev.lukebemish.codecextras.StringRepresentation; import dev.lukebemish.codecextras.types.Flip; import dev.lukebemish.codecextras.types.Identity; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.MonthDay; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.Period; +import java.time.Year; +import java.time.YearMonth; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; import java.util.List; import java.util.Map; import java.util.Optional; @@ -652,6 +668,26 @@ static Structure record(RecordStructure.Builder builder) { } }).xmap(s -> s.charAt(0), String::valueOf)); + Structure BIG_INTEGER = stringFallbackBacked(Interpreter.BIG_INTEGER, BigInteger::new, BigInteger::toString); + + Structure BIG_DECIMAL = stringFallbackBacked(Interpreter.BIG_DECIMAL, BigDecimal::new, BigDecimal::toPlainString); + + Structure DURATION = stringFallbackBacked(Interpreter.DURATION, Duration::parse, Duration::toString); + Structure INSTANT = stringFallbackBacked(Interpreter.INSTANT, Instant::parse, Instant::toString); + Structure LOCAL_DATE = stringFallbackBacked(Interpreter.LOCAL_DATE, LocalDate::parse, LocalDate::toString); + Structure LOCAL_DATE_TIME = stringFallbackBacked(Interpreter.LOCAL_DATE_TIME, LocalDateTime::parse, LocalDateTime::toString); + Structure LOCAL_TIME = stringFallbackBacked(Interpreter.LOCAL_TIME, LocalTime::parse, LocalTime::toString); + Structure MONTH_DAY = stringFallbackBacked(Interpreter.MONTH_DAY, MonthDay::parse, MonthDay::toString); + Structure OFFSET_DATE_TIME = stringFallbackBacked(Interpreter.OFFSET_DATE_TIME, OffsetDateTime::parse, OffsetDateTime::toString); + Structure OFFSET_TIME = stringFallbackBacked(Interpreter.OFFSET_TIME, OffsetTime::parse, OffsetTime::toString); + Structure PERIOD = stringFallbackBacked(Interpreter.PERIOD, Period::parse, Period::toString); + Structure YEAR = stringFallbackBacked(Interpreter.YEAR, Year::parse, Year::toString); + Structure YEAR_MONTH = stringFallbackBacked(Interpreter.YEAR_MONTH, YearMonth::parse, YearMonth::toString); + Structure ZONED_DATE_TIME = stringFallbackBacked(Interpreter.ZONED_DATE_TIME, ZonedDateTime::parse, ZonedDateTime::toString); + Structure ZONE_ID = stringFallbackBacked(Interpreter.ZONE_ID, ZoneId::of, ZoneId::toString); + Structure ZONE_OFFSET = stringFallbackBacked(Interpreter.ZONE_OFFSET, ZoneOffset::of, ZoneOffset::toString); + + /** * Represents a {@link Dynamic} value. */ @@ -750,4 +786,14 @@ static Structure stringRepresentable(Supplier values, Function (Identity) app) .xmap(i -> Identity.unbox(i).value(), Identity::new); } + + private static Structure stringFallbackBacked(Key key, Function parser, Function stringifier) { + return keyed(key, STRING.comapFlatMap(s -> { + try { + return DataResult.success(parser.apply(s)); + } catch (Exception e) { + return DataResult.error(() -> "Could not parse: " + s); + } + }, stringifier)); + } } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/BuiltInReflectiveStructureCreator.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/BuiltInReflectiveStructureCreator.java index a44c8c6..e112a6b 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/BuiltInReflectiveStructureCreator.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/BuiltInReflectiveStructureCreator.java @@ -18,14 +18,61 @@ import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.MonthDay; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.Period; +import java.time.Year; +import java.time.YearMonth; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; +import java.util.Deque; +import java.util.EnumMap; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.NavigableMap; +import java.util.NavigableSet; import java.util.Objects; +import java.util.Optional; +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.OptionalLong; +import java.util.Queue; +import java.util.SequencedCollection; +import java.util.SequencedMap; +import java.util.SequencedSet; +import java.util.Set; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.concurrent.BlockingDeque; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ConcurrentNavigableMap; +import java.util.concurrent.ConcurrentSkipListMap; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.LinkedTransferQueue; +import java.util.concurrent.TransferQueue; import java.util.function.Function; import java.util.function.Supplier; import org.jetbrains.annotations.ApiStatus; @@ -166,18 +213,64 @@ public Map, Creator> creators() { } return list; })) + .put(BigInteger.class, () -> Structure.BIG_INTEGER) + .put(BigDecimal.class, () -> Structure.BIG_DECIMAL) + .put(Duration.class, () -> Structure.DURATION) + .put(Instant.class, () -> Structure.INSTANT) + .put(LocalDate.class, () -> Structure.LOCAL_DATE) + .put(LocalDateTime.class, () -> Structure.LOCAL_DATE_TIME) + .put(LocalTime.class, () -> Structure.LOCAL_TIME) + .put(MonthDay.class, () -> Structure.MONTH_DAY) + .put(OffsetDateTime.class, () -> Structure.OFFSET_DATE_TIME) + .put(OffsetTime.class, () -> Structure.OFFSET_TIME) + .put(Period.class, () -> Structure.PERIOD) + .put(Year.class, () -> Structure.YEAR) + .put(YearMonth.class, () -> Structure.YEAR_MONTH) + .put(ZonedDateTime.class, () -> Structure.ZONED_DATE_TIME) + .put(ZoneId.class, () -> Structure.ZONE_ID) + .put(ZoneOffset.class, () -> Structure.ZONE_OFFSET) .build(); } @Override public Map, ParameterizedCreator> parameterizedCreators() { return ImmutableMap., ParameterizedCreator>builder() - .put(List.class, parameters -> parameters[0].create().listOf()) - .put(Map.class, parameters -> Structure.unboundedMap(parameters[0].create(), parameters[1].create())) .put(Either.class, parameters -> Structure.unboundedMap(parameters[0].create(), parameters[1].create())) + // Collections + .put(Collection.class, collectionMaker(ArrayList::new)) + .put(SequencedCollection.class, collectionMaker(ArrayList::new)) + .put(Deque.class, collectionMaker(ArrayDeque::new)) + .put(Set.class, collectionMaker(LinkedHashSet::new)) + .put(NavigableSet.class, collectionMaker(TreeSet::new)) + .put(SequencedSet.class, collectionMaker(LinkedHashSet::new)) + .put(SortedSet.class, collectionMaker(TreeSet::new)) + .put(Queue.class, collectionMaker(ArrayDeque::new)) + .put(List.class, collectionMaker(ArrayList::new)) + .put(BlockingDeque.class, collectionMaker(LinkedBlockingDeque::new)) + .put(BlockingQueue.class, collectionMaker(LinkedBlockingQueue::new)) + .put(TransferQueue.class, collectionMaker(LinkedTransferQueue::new)) + .put(ImmutableList.class, collectionMaker(ImmutableList::copyOf)) + // Map-likes + .put(Map.class, mapMaker(LinkedHashMap::new)) + .put(NavigableMap.class, mapMaker(TreeMap::new)) + .put(SortedMap.class, mapMaker(TreeMap::new)) + .put(SequencedMap.class, mapMaker(LinkedHashMap::new)) + .put(ConcurrentMap.class, mapMaker(ConcurrentHashMap::new)) + .put(ConcurrentNavigableMap.class, mapMaker(ConcurrentSkipListMap::new)) + .put(ImmutableMap.class, mapMaker(ImmutableMap::copyOf)) .build(); } + @SuppressWarnings({"rawtypes", "Convert2MethodRef", "unchecked"}) + private static ParameterizedCreator collectionMaker(Function, T> function) { + return parameters -> parameters[0].create().listOf().xmap(function::apply, c -> new ArrayList<>(c)); + } + + @SuppressWarnings({"rawtypes", "Convert2MethodRef", "unchecked"}) + private static ParameterizedCreator mapMaker(Function, T> function) { + return parameters -> Structure.unboundedMap(parameters[0].create(), parameters[1].create()).xmap(function::apply, c -> new LinkedHashMap<>(c)); + } + private static ConstantDynamic conDyn(String descriptor, int i) { return new ConstantDynamic( "_", @@ -193,7 +286,7 @@ private static ConstantDynamic conDyn(String descriptor, int i) { ); } - public static Function getterWrapper(Class exact, MethodHandle getter) { + public static Function functionWrapper(MethodHandle getter) { var writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); var name = BuiltInReflectiveStructureCreator.class.getName().replace('.', '/') + "$GetterWrapper"; writer.visit(Opcodes.V21, Opcodes.ACC_FINAL, name, null, "java/lang/Object", new String[]{"java/util/function/Function"}); @@ -215,7 +308,7 @@ public static Function getterWrapper(Class exact, MethodHandle var bytes = writer.toByteArray(); try { var lookup = MethodHandles.lookup().defineHiddenClassWithClassData(bytes, List.of(getter), true, MethodHandles.Lookup.ClassOption.NESTMATE); - @SuppressWarnings("unchecked") var instance = (Function) lookup.lookupClass().getConstructor().newInstance(); + @SuppressWarnings("unchecked") var instance = (Function) lookup.lookupClass().getConstructor().newInstance(); return instance; } catch (Throwable e) { throw new RuntimeException(e); @@ -242,17 +335,74 @@ private static CallSite asCallSite(MethodHandles.Lookup ignoredLookup, String ig } @SuppressWarnings({"rawtypes", "unchecked"}) - public static RecordStructure.Key add(RecordStructure builder, String name, Type type, Function getter, Function> creator) { - // TODO: optional fields - return builder.add(name, (Structure) creator.apply(type), (Function) getter); + public static Function add(RecordStructure builder, String name, Type type, Function getter, Function> creator) { + if (type instanceof ParameterizedType parameterizedType && parameterizedType.getRawType() instanceof Class rawType) { + if (rawType.equals(Optional.class)) { + var innerType = parameterizedType.getActualTypeArguments()[0]; + return builder.addOptional(name, (Structure) creator.apply(innerType), (Function) getter); + } + } else if (type instanceof Class clazz) { + if (clazz.equals(OptionalInt.class)) { + return builder.addOptionalInt(name, (Function) getter); + } else if (clazz.equals(OptionalDouble.class)) { + return builder.addOptionalDouble(name, (Function) getter); + } else if (clazz.equals(OptionalLong.class)) { + return builder.addOptionalLong(name, (Function) getter); + } + } + Function> key = builder.addOptional(name, (Structure) creator.apply(type), (Function) getter.andThen(Optional::ofNullable)); + return key.andThen(o -> o.orElse(null)); } @Override public List flexibleCreators() { return ImmutableList.builder() + .add(new FlexibleCreator() { + @SuppressWarnings({"unchecked", "rawtypes"}) + @Override + public Structure create(Class exact, TypedCreator[] parameters, Function> creator) { + try { + var ctor = exact.getConstructor(Collection.class); + var function = functionWrapper(MethodHandles.lookup().unreflectConstructor(ctor)); + return parameters[0].create().listOf().xmap(function::apply, c -> new ArrayList((Collection) c)); + } catch (NoSuchMethodException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + @Override + public boolean supports(Class exact, TypedCreator[] parameters) { + if (parameters.length == 1 && Collection.class.isAssignableFrom(exact)) { + try { + var ctor = exact.getConstructor(Collection.class); + return ctor.accessFlags().contains(AccessFlag.PUBLIC); + } catch (NoSuchMethodException e) { + return false; + } + } + return false; + } + }) + .add(new FlexibleCreator() { + @SuppressWarnings({"unchecked", "rawtypes"}) + @Override + public Structure create(Class exact, TypedCreator[] parameters, Function> creator) { + var keyType = parameters[0].rawType(); + return Structure.unboundedMap(parameters[0].create(), parameters[1].create()).xmap(map -> { + var enumMap = new EnumMap(keyType); + enumMap.putAll(map); + return enumMap; + }, Function.identity()); + } + + @Override + public boolean supports(Class exact, TypedCreator[] parameters) { + return exact.equals(EnumMap.class) && parameters.length == 2; + } + }) .add(new FlexibleCreator() { @Override - public Structure create(Class exact, Function> creator) { + public Structure create(Class exact, TypedCreator[] parameters, Function> creator) { Supplier values = Suppliers.memoize(() -> { try { return (Object[]) exact.getMethod("values").invoke(null); @@ -264,7 +414,7 @@ public Structure create(Class exact, Function> creator) } @Override - public boolean supports(Class exact) { + public boolean supports(Class exact, TypedCreator[] parameters) { return Enum.class.isAssignableFrom(exact); } }) @@ -305,7 +455,7 @@ public boolean supports(Class exact) { @SuppressWarnings({"rawtypes", "unchecked"}) @Override - public Structure create(Class exact, Function> creator) { + public Structure create(Class exact, TypedCreator[] parameters, Function> creator) { var arrayMaker = arrayMaker(exact.getComponentType()); return creator.apply(exact.getComponentType()).listOf().xmap(arrayMaker::apply, obj -> { var array = (Object[]) obj; @@ -316,7 +466,7 @@ public Structure create(Class exact, Function> creator) } @Override - public boolean supports(Class exact) { + public boolean supports(Class exact, TypedCreator[] parameters) { return exact.isArray() && !exact.getComponentType().isPrimitive(); } }) @@ -362,7 +512,7 @@ private List> validCtors(Class exact) { } @Override - public Structure create(Class exact, Function> creator) { + public Structure create(Class exact, TypedCreator[] parameters, Function> creator) { return Structure.record(builder -> { Constructor validCtor; if (exact.isRecord()) { @@ -405,7 +555,7 @@ public Structure create(Class exact, Function> creator) try { var getterMethod = exact.getMethod("get" + annotation.property().substring(0, 1).toUpperCase() + annotation.property().substring(1)); if (getterMethod.getGenericReturnType().equals(param.getParameterizedType()) && getterMethod.accessFlags().contains(AccessFlag.PUBLIC) && !getterMethod.accessFlags().contains(AccessFlag.STATIC)) { - var getter = getterWrapper(exact, MethodHandles.lookup().unreflect(getterMethod)); + var getter = functionWrapper(MethodHandles.lookup().unreflect(getterMethod)); getters.put(annotation.property(), getter); thisContext.add(getterMethod); continue; @@ -418,7 +568,7 @@ public Structure create(Class exact, Function> creator) try { var field = exact.getField(annotation.property()); if (field.getType().equals(param.getType()) && field.accessFlags().contains(AccessFlag.PUBLIC) && !field.accessFlags().contains(AccessFlag.STATIC)) { - var getter = getterWrapper(exact, MethodHandles.lookup().unreflectGetter(field)); + var getter = functionWrapper(MethodHandles.lookup().unreflectGetter(field)); getters.put(annotation.property(), getter); thisContext.add(field); } @@ -442,7 +592,7 @@ public Structure create(Class exact, Function> creator) types.put(property, method.getGenericReturnType()); if (!getters.containsKey(property)) { try { - var getter = getterWrapper(exact, MethodHandles.lookup().unreflect(method)); + var getter = functionWrapper(MethodHandles.lookup().unreflect(method)); getters.put(property, getter); } catch (IllegalAccessException e) { throw new RuntimeException(e); @@ -477,7 +627,7 @@ public Structure create(Class exact, Function> creator) } context.computeIfAbsent(field.getName(), k -> new ArrayList<>()).add(field); if (!getters.containsKey(field.getName())) { - var getter = getterWrapper(exact, MethodHandles.lookup().unreflectGetter(field)); + var getter = functionWrapper(MethodHandles.lookup().unreflectGetter(field)); getters.put(field.getName(), getter); } if (!setters.containsKey(field.getName()) && (!ctorSetters.containsKey(field.getName())) && !field.accessFlags().contains(AccessFlag.FINAL)) { @@ -502,7 +652,7 @@ public Structure create(Class exact, Function> creator) try { var getterMethod = exact.getMethod(component.getName()); - var getter = getterWrapper(exact, MethodHandles.lookup().unreflect(getterMethod)); + var getter = functionWrapper(MethodHandles.lookup().unreflect(getterMethod)); thisContext.add(getterMethod); getters.put(component.getName(), getter); } catch (NoSuchMethodException ignored) { @@ -519,7 +669,7 @@ public Structure create(Class exact, Function> creator) } } var propertyList = new ArrayList<>(properties); - var keyList = new ArrayList>(propertyList.size()); + var keyList = new ArrayList>(propertyList.size()); for (var property : propertyList) { var getter = getters.get(property); var key = add(builder, property, types.get(property), getter, creator); @@ -567,9 +717,9 @@ public Structure create(Class exact, Function> creator) for (int i = 0; i < ctorSettersArray.length; i++) { var property = ctorSettersArray[i]; int keyOffset = offsetMap.get(property); - mv.visitLdcInsn(conDyn(org.objectweb.asm.Type.getDescriptor(RecordStructure.Key.class), keyOffset)); + mv.visitLdcInsn(conDyn(org.objectweb.asm.Type.getDescriptor(Function.class), keyOffset)); mv.visitVarInsn(Opcodes.ALOAD, 2); - mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, org.objectweb.asm.Type.getInternalName(RecordStructure.Key.class), "apply", MethodType.methodType(Object.class, RecordStructure.Container.class).descriptorString(), false); + mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, org.objectweb.asm.Type.getInternalName(Function.class), "apply", MethodType.methodType(Object.class, Object.class).descriptorString(), true); convertType(mv, validCtor.getParameters()[i].getType()); } mv.visitMethodInsn(Opcodes.INVOKESPECIAL, exact.getName().replace('.', '/'), "", ctorDescriptor, false); @@ -578,9 +728,9 @@ public Structure create(Class exact, Function> creator) if (!ctorSetters.containsKey(property)) { mv.visitVarInsn(Opcodes.ALOAD, 3); var keyOffset = offsetMap.get(property); - mv.visitLdcInsn(conDyn(org.objectweb.asm.Type.getDescriptor(RecordStructure.Key.class), keyOffset)); + mv.visitLdcInsn(conDyn(org.objectweb.asm.Type.getDescriptor(Function.class), keyOffset)); mv.visitVarInsn(Opcodes.ALOAD, 2); - mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, org.objectweb.asm.Type.getInternalName(RecordStructure.Key.class), "apply", MethodType.methodType(Object.class, RecordStructure.Container.class).descriptorString(), false); + mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, org.objectweb.asm.Type.getInternalName(Function.class), "apply", MethodType.methodType(Object.class, Object.class).descriptorString(), true); invokeAsCallSite(mv, "(Ljava/lang/Object;Ljava/lang/Object;)V", offsetMap.get(property)+1); } } @@ -607,7 +757,7 @@ public int priority() { } @Override - public boolean supports(Class exact) { + public boolean supports(Class exact, TypedCreator[] parameters) { // For a class to be record-able, it must meet a few conditions. It must: // - Be a record // - OR: Have a public no-args constructor diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java index 86ec90b..cbcf443 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java @@ -8,6 +8,7 @@ import java.util.IdentityHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.function.Function; public interface ReflectiveStructureCreator { @@ -15,24 +16,30 @@ public interface ReflectiveStructureCreator { Map, ParameterizedCreator> parameterizedCreators(); List flexibleCreators(); + interface TypedCreator { + Structure create(); + Type type(); + Class rawType(); + } + interface Creator { Structure create(); } interface FlexibleCreator { - Structure create(Class exact, Function> creator); - boolean supports(Class exact); + Structure create(Class exact, TypedCreator[] parameters, Function> creator); + boolean supports(Class exact, TypedCreator[] parameters); default int priority() { return 0; } - default Creator creator(Class exact, Function> creator) { - return () -> create(exact, creator); + default Creator creator(Class exact, TypedCreator[] parameters, Function> creator) { + return () -> create(exact, parameters, creator); } } interface ParameterizedCreator { - Structure create(Creator[] parameters); - default Creator creator(Creator[] parameters) { + Structure create(TypedCreator[] parameters); + default Creator creator(TypedCreator[] parameters) { return () -> create(parameters); } } @@ -109,26 +116,57 @@ static Structure create(Class clazz) { } private static Creator forType(Type type, Map, Creator> creatorsMap, Map, ParameterizedCreator> parameterizedCreatorsMap, List flexibleCreators) { + Class rawType = null; + TypedCreator[] parameterCreators = null; if (type instanceof ParameterizedType parameterizedType) { - var rawType = parameterizedType.getRawType(); - if (rawType instanceof Class clazz && parameterizedCreatorsMap.containsKey(clazz)) { + if (parameterizedType.getRawType() instanceof Class clazz) { + rawType = clazz; var parameters = parameterizedType.getActualTypeArguments(); - Creator[] parameterCreators = new Creator[parameters.length]; - for (int i = 0; i < parameters.length; i++) { - parameterCreators[i] = forType(parameters[i], creatorsMap, parameterizedCreatorsMap, flexibleCreators); + parameterCreators = new TypedCreator[parameters.length]; + if (parameterizedCreatorsMap.containsKey(clazz)) { + for (int i = 0; i < parameters.length; i++) { + var creator = forType(parameters[i], creatorsMap, parameterizedCreatorsMap, flexibleCreators); + var parameterType = parameters[i]; + parameterCreators[i] = new TypedCreator() { + @Override + public Structure create() { + return creator.create(); + } + + @Override + public Type type() { + return parameterType; + } + + @Override + public Class rawType() { + if (parameterType instanceof Class clazz) { + return clazz; + } else if (parameterType instanceof ParameterizedType parameterizedType) { + return (Class) parameterizedType.getRawType(); + } else { + throw new IllegalArgumentException("Unknown type: " + type); + } + } + }; + } + return parameterizedCreatorsMap.get(clazz).creator(parameterCreators); } - return parameterizedCreatorsMap.get(clazz).creator(parameterCreators); } - } - if (type instanceof Class clazz) { + } else if (type instanceof Class clazz) { + rawType = clazz; + parameterCreators = new TypedCreator[0]; var foundCreator = creatorsMap.get(clazz); if (foundCreator != null) { return foundCreator; } - for (var flexibleCreator : flexibleCreators) { - if (flexibleCreator.supports(clazz)) { - return flexibleCreator.creator(clazz, type1 -> forType(type1, creatorsMap, parameterizedCreatorsMap, flexibleCreators).create()); - } + } else { + throw new IllegalArgumentException("Unknown type: " + type); + } + + for (var flexibleCreator : flexibleCreators) { + if (flexibleCreator.supports(Objects.requireNonNull(rawType), parameterCreators)) { + return flexibleCreator.creator(rawType, parameterCreators, type1 -> forType(type1, creatorsMap, parameterizedCreatorsMap, flexibleCreators).create()); } } throw new IllegalArgumentException("No creator found for type: " + type); diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftReflectiveStructureCreator.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftReflectiveStructureCreator.java index 71d7f32..8459e3d 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftReflectiveStructureCreator.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftReflectiveStructureCreator.java @@ -41,7 +41,7 @@ public List flexibleCreators() { return ImmutableList.builder() .add(new FlexibleCreator() { @Override - public Structure create(Class exact, Function> creator) { + public Structure create(Class exact, TypedCreator[] parameters, Function> creator) { Supplier values = Suppliers.memoize(() -> { try { return (Object[]) exact.getMethod("values").invoke(null); @@ -58,7 +58,7 @@ public int priority() { } @Override - public boolean supports(Class exact) { + public boolean supports(Class exact, TypedCreator[] parameters) { return Enum.class.isAssignableFrom(exact) && StringRepresentable.class.isAssignableFrom(exact); } }) diff --git a/src/test/java/dev/lukebemish/codecextras/test/structured/reflective/TestReflective.java b/src/test/java/dev/lukebemish/codecextras/test/structured/reflective/TestReflective.java index ced41a8..332fcb1 100644 --- a/src/test/java/dev/lukebemish/codecextras/test/structured/reflective/TestReflective.java +++ b/src/test/java/dev/lukebemish/codecextras/test/structured/reflective/TestReflective.java @@ -6,10 +6,16 @@ import dev.lukebemish.codecextras.structured.Structure; import dev.lukebemish.codecextras.structured.reflective.ReflectiveStructureCreator; import dev.lukebemish.codecextras.test.CodecAssertions; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.SequencedSet; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; public class TestReflective { - public record TestRecord(int a, String b, TestEnum c) { + public record TestRecord(int a, String b, TestEnum c, OptionalInt d, Optional e, @Nullable String f, SequencedSet g) { private static final Structure STRUCTURE = ReflectiveStructureCreator.create(TestRecord.class); } @@ -28,7 +34,10 @@ public enum TestEnum { { "a": 1, "b": "test", - "c": "A" + "c": "A", + "d": 2, + "e": "test", + "g": [1, 2, 3] }"""; private final String arrayJson = """ @@ -36,14 +45,17 @@ public enum TestEnum { { "a": 1, "b": "test", - "c": "A" + "c": "A", + "d": 2, + "e": "test", + "g": [1, 2, 3] } ]"""; private final String primitiveArrayJson = """ [1, 2, 3]"""; - private final TestRecord object = new TestRecord(1, "test", TestEnum.A); + private final TestRecord object = new TestRecord(1, "test", TestEnum.A, OptionalInt.of(2), Optional.of("test"), null, new LinkedHashSet<>(List.of(1, 2, 3))); private final TestRecord[] array = new TestRecord[] { object }; private final int[] primitiveArray = new int[] { 1, 2, 3 }; From 95709176702ab570d3ffac4cd277cf9aac42067b Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Tue, 4 Mar 2025 14:20:34 -0600 Subject: [PATCH 12/28] Support for more types in reflective structure creation, and recursive structures --- .../structured/CodecInterpreter.java | 28 ++++ .../structured/IdentityInterpreter.java | 11 ++ .../codecextras/structured/Interpreter.java | 4 + .../structured/MapCodecInterpreter.java | 31 ++++ .../codecextras/structured/Structure.java | 9 ++ .../ReflectiveStructureCreator.java | 133 +++++++++++------- .../schema/JsonSchemaInterpreter.java | 6 + .../codecextras/utility/package-info.java | 2 + src/main/java/module-info.java | 1 + .../config/ConfigScreenInterpreter.java | 6 + .../structured/StreamCodecInterpreter.java | 6 + .../structured/reflective/TestReflective.java | 30 ++++ 12 files changed, 214 insertions(+), 53 deletions(-) diff --git a/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java index a603907..5931217 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java @@ -1,12 +1,15 @@ package dev.lukebemish.codecextras.structured; +import com.google.common.base.Suppliers; import com.mojang.datafixers.kinds.App; import com.mojang.datafixers.kinds.Const; import com.mojang.datafixers.kinds.K1; import com.mojang.datafixers.util.Either; +import com.mojang.datafixers.util.Pair; import com.mojang.datafixers.util.Unit; import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; +import com.mojang.serialization.DynamicOps; import com.mojang.serialization.MapCodec; import dev.lukebemish.codecextras.PartialDispatchedMapCodec; import dev.lukebemish.codecextras.StringRepresentation; @@ -191,6 +194,31 @@ public App convert(App input ); } + @Override + public DataResult> recursive(Function, Structure> function) { + var key = Key.create("recursive"); + var keyed = Structure.keyed(key); + var complete = function.apply(keyed); + var codec = new Codec() { + private final Holder holder = new Holder<>(this); + private final CodecInterpreter interpreterWithKeys = with(Keys.builder().add(key, holder).build(), Keys2., K1, K1>builder().build()); + private final Supplier>> wrapped = Suppliers.memoize(() -> + complete.interpret(interpreterWithKeys).map(CodecInterpreter::unbox) + ); + + @Override + public DataResult encode(A input, DynamicOps ops, T prefix) { + return wrapped.get().flatMap(codec -> codec.encode(input, ops, prefix)); + } + + @Override + public DataResult> decode(DynamicOps ops, T input) { + return wrapped.get().flatMap(codec -> codec.decode(ops, input)); + } + }; + return DataResult.success(new Holder<>(codec)); + } + public static Codec unbox(App box) { return Holder.unbox(box).codec(); } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/IdentityInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/IdentityInterpreter.java index 7bdb28e..0cd3418 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/IdentityInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/IdentityInterpreter.java @@ -99,6 +99,17 @@ public DataResult> dispatch(String key, Structure return DataResult.error(() -> "No default value available for a dispatch"); } + @Override + public DataResult> recursive(Function, Structure> function) { + var recursion = new Structure() { + @Override + public DataResult> interpret(Interpreter interpreter) { + return DataResult.error(() -> "Detected infinite recursion of default value"); + } + }; + return function.apply(recursion).interpret(this); + } + @Override public DataResult>> dispatchedMap(Structure keyStructure, Supplier> keys, Function>> valueStructures) { return DataResult.error(() -> "No default value available for a dispatched map"); diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java index f3cee13..c129a7d 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java @@ -45,6 +45,8 @@ public interface Interpreter { DataResult> dispatch(String key, Structure keyStructure, Function> function, Supplier> keys, Function>> structures); + DataResult> recursive(Function, Structure> function); + default Stream> keyConsumers() { return Stream.of(); } @@ -113,4 +115,6 @@ default DataResult> bounded(Structure input, Supplier> v DataResult>> xor(App left, App right); DataResult>> dispatchedMap(Structure keyStructure, Supplier> keys, Function>> valueStructures); + + } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java index cb46dac..d7fabe5 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java @@ -1,5 +1,6 @@ package dev.lukebemish.codecextras.structured; +import com.google.common.base.Suppliers; import com.mojang.datafixers.kinds.App; import com.mojang.datafixers.kinds.K1; import com.mojang.datafixers.util.Either; @@ -120,6 +121,36 @@ public DataResult> dispatch(String key, Structure ke }); } + @Override + public DataResult> recursive(Function, Structure> function) { + var key = Key.create("recursive"); + var keyed = Structure.keyed(key); + var complete = function.apply(keyed); + var mapCodec = new MapCodec() { + private final Holder holder = new Holder<>(this); + private final MapCodecInterpreter interpreterWithKeys = with(Keys.builder().add(key, holder).build(), Keys2., K1, K1>builder().build()); + private final Supplier>> wrapped = Suppliers.memoize(() -> + complete.interpret(interpreterWithKeys).map(MapCodecInterpreter::unbox) + ); + + @Override + public RecordBuilder encode(A input, DynamicOps ops, RecordBuilder prefix) { + return wrapped.get().mapOrElse(c -> c.encode(input, ops, prefix), e -> prefix).withErrorsFrom(wrapped.get()); + } + + @Override + public DataResult decode(DynamicOps ops, MapLike input) { + return wrapped.get().flatMap(codec -> codec.decode(ops, input)); + } + + @Override + public Stream keys(DynamicOps ops) { + return wrapped.get().mapOrElse(c -> c.keys(ops), e -> Stream.empty()); + } + }; + return DataResult.success(new Holder<>(mapCodec)); + } + @Override public DataResult>> dispatchedMap(Structure keyStructure, Supplier> keys, Function>> valueStructures) { return DataResult.error(() -> "Cannot make a MapCodec for a dispatched map"); diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java index 8365bb6..90c6c28 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java @@ -357,6 +357,15 @@ public DataResult> interpret(Interpreter interpre }; } + static Structure recursive(Function, Structure> function) { + return new Structure<>() { + @Override + public DataResult> interpret(Interpreter interpreter) { + return interpreter.recursive(function); + } + }; + } + /** * Keys provide a way of representing the smallest building blocks of a structure. Interpreters are responsible for * finding a matching specific representation given a key when interpreting a structure. diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java index cbcf443..dae6234 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java @@ -1,15 +1,18 @@ package dev.lukebemish.codecextras.structured.reflective; +import com.google.common.base.Suppliers; import dev.lukebemish.codecextras.structured.Structure; import dev.lukebemish.codecextras.utility.LayeredServiceLoader; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; +import java.util.HashMap; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.function.Function; +import java.util.function.Supplier; public interface ReflectiveStructureCreator { Map, Creator> creators(); @@ -88,8 +91,10 @@ public Instance build() { private static final LayeredServiceLoader SERVICE_LOADER = LayeredServiceLoader.of(ReflectiveStructureCreator.class); + private final Map cachedCreators = new HashMap<>(); + @SuppressWarnings("unchecked") - public Structure create(Class clazz) { + public synchronized Structure create(Class clazz) { var caller = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE).getCallerClass(); List services = LayeredServiceLoader.unique(SERVICE_LOADER.at(ReflectiveStructureCreator.class), SERVICE_LOADER.at(clazz), SERVICE_LOADER.at(caller)); Map, Creator> creatorsMap = new IdentityHashMap<>(); @@ -106,7 +111,10 @@ public Structure create(Class clazz) { flexibleCreatorsList.addAll(this.flexibleCreators); flexibleCreatorsList.sort((a, b) -> Integer.compare(b.priority(), a.priority())); - var creator = forType(clazz, creatorsMap, parameterizedCreatorsMap, flexibleCreatorsList); + + var recursionCache = new HashMap>(); + + var creator = forType(cachedCreators, recursionCache, clazz, creatorsMap, parameterizedCreatorsMap, flexibleCreatorsList); return (Structure) creator.create(); } } @@ -115,60 +123,79 @@ static Structure create(Class clazz) { return Instance.builder().build().create(clazz); } - private static Creator forType(Type type, Map, Creator> creatorsMap, Map, ParameterizedCreator> parameterizedCreatorsMap, List flexibleCreators) { - Class rawType = null; - TypedCreator[] parameterCreators = null; - if (type instanceof ParameterizedType parameterizedType) { - if (parameterizedType.getRawType() instanceof Class clazz) { - rawType = clazz; - var parameters = parameterizedType.getActualTypeArguments(); - parameterCreators = new TypedCreator[parameters.length]; - if (parameterizedCreatorsMap.containsKey(clazz)) { - for (int i = 0; i < parameters.length; i++) { - var creator = forType(parameters[i], creatorsMap, parameterizedCreatorsMap, flexibleCreators); - var parameterType = parameters[i]; - parameterCreators[i] = new TypedCreator() { - @Override - public Structure create() { - return creator.create(); - } - - @Override - public Type type() { - return parameterType; - } - - @Override - public Class rawType() { - if (parameterType instanceof Class clazz) { - return clazz; - } else if (parameterType instanceof ParameterizedType parameterizedType) { - return (Class) parameterizedType.getRawType(); - } else { - throw new IllegalArgumentException("Unknown type: " + type); - } + private static Creator forType(Map cachedCreators, Map> recursionCache, Type type, Map, Creator> creatorsMap, Map, ParameterizedCreator> parameterizedCreatorsMap, List flexibleCreators) { + if (cachedCreators.containsKey(type)) { + return cachedCreators.get(type); + } + if (recursionCache.containsKey(type)) { + var value = recursionCache.get(type); + return () -> value; + } + @SuppressWarnings({"rawtypes", "unchecked"}) Supplier> full = Suppliers.memoize(() -> Structure.recursive((Function) (Function) (Structure itself) -> { + recursionCache.put(type, itself); + + Supplier creatorSupplier = () -> { + Class rawType = null; + TypedCreator[] parameterCreators = null; + if (type instanceof ParameterizedType parameterizedType) { + if (parameterizedType.getRawType() instanceof Class clazz) { + rawType = clazz; + var parameters = parameterizedType.getActualTypeArguments(); + parameterCreators = new TypedCreator[parameters.length]; + if (parameterizedCreatorsMap.containsKey(clazz)) { + for (int i = 0; i < parameters.length; i++) { + var creator = forType(cachedCreators, recursionCache, parameters[i], creatorsMap, parameterizedCreatorsMap, flexibleCreators); + var parameterType = parameters[i]; + parameterCreators[i] = new TypedCreator() { + @Override + public Structure create() { + return creator.create(); + } + + @Override + public Type type() { + return parameterType; + } + + @Override + public Class rawType() { + if (parameterType instanceof Class clazz) { + return clazz; + } else if (parameterType instanceof ParameterizedType parameterizedType) { + return (Class) parameterizedType.getRawType(); + } else { + throw new IllegalArgumentException("Unknown type: " + type); + } + } + }; } - }; + return parameterizedCreatorsMap.get(clazz).creator(parameterCreators); + } } - return parameterizedCreatorsMap.get(clazz).creator(parameterCreators); + } else if (type instanceof Class clazz) { + rawType = clazz; + parameterCreators = new TypedCreator[0]; + var foundCreator = creatorsMap.get(clazz); + if (foundCreator != null) { + return foundCreator; + } + } else { + throw new IllegalArgumentException("Unknown type: " + type); } - } - } else if (type instanceof Class clazz) { - rawType = clazz; - parameterCreators = new TypedCreator[0]; - var foundCreator = creatorsMap.get(clazz); - if (foundCreator != null) { - return foundCreator; - } - } else { - throw new IllegalArgumentException("Unknown type: " + type); - } - for (var flexibleCreator : flexibleCreators) { - if (flexibleCreator.supports(Objects.requireNonNull(rawType), parameterCreators)) { - return flexibleCreator.creator(rawType, parameterCreators, type1 -> forType(type1, creatorsMap, parameterizedCreatorsMap, flexibleCreators).create()); - } - } - throw new IllegalArgumentException("No creator found for type: " + type); + for (var flexibleCreator : flexibleCreators) { + if (flexibleCreator.supports(Objects.requireNonNull(rawType), parameterCreators)) { + return flexibleCreator.creator(rawType, parameterCreators, type1 -> forType(cachedCreators, recursionCache, type1, creatorsMap, parameterizedCreatorsMap, flexibleCreators).create()); + } + } + throw new IllegalArgumentException("No creator found for type: " + type); + }; + var creator = creatorSupplier.get(); + var structure = creator.create(); + recursionCache.remove(type); + return structure; + })); + cachedCreators.put(type, full::get); + return full::get; } } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java index 3039004..12aec8f 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java @@ -227,6 +227,12 @@ public DataResult> annotate(Structure input, Keys(schema, definitions)); } + @Override + public DataResult> recursive(Function, Structure> function) { + // TODO: implement + return DataResult.error(() -> "Not yet implemented"); + } + @Override public DataResult> dispatch(String key, Structure keyStructure, Function> function, Supplier> keys, Function>> structures) { return keyStructure.interpret(this).flatMap(keySchemaApp -> { diff --git a/src/main/java/dev/lukebemish/codecextras/utility/package-info.java b/src/main/java/dev/lukebemish/codecextras/utility/package-info.java index a10b517..390c2f6 100644 --- a/src/main/java/dev/lukebemish/codecextras/utility/package-info.java +++ b/src/main/java/dev/lukebemish/codecextras/utility/package-info.java @@ -1,4 +1,6 @@ @NullMarked +@ApiStatus.Internal package dev.lukebemish.codecextras.utility; +import org.jetbrains.annotations.ApiStatus; import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 578928a..8f3262c 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -31,6 +31,7 @@ exports dev.lukebemish.codecextras.repair; exports dev.lukebemish.codecextras.structured; + exports dev.lukebemish.codecextras.structured.reflective; exports dev.lukebemish.codecextras.structured.schema; exports dev.lukebemish.codecextras.types; diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java index 6872a69..52e99f2 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java @@ -1031,6 +1031,12 @@ public DataResult> dispatch(String key, Stru )); } + @Override + public DataResult> recursive(Function, Structure> function) { + // TODO: implement + return DataResult.error(() -> "Not yet implemented"); + } + @Override public DataResult>> dispatchedMap(Structure keyStructure, Supplier> keys, Function>> valueStructures) { var keyResult = interpret(keyStructure); diff --git a/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java b/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java index 79adda3..e494b68 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java @@ -431,6 +431,12 @@ public DataResult, Either>> xor(App, return either(left, right); } + @Override + public DataResult, A>> recursive(Function, Structure> function) { + // TODO: implement + return DataResult.error(() -> "Not yet implemented"); + } + public record Holder(StreamCodec streamCodec) implements App, T> { public static final class Mu implements K1 {} diff --git a/src/test/java/dev/lukebemish/codecextras/test/structured/reflective/TestReflective.java b/src/test/java/dev/lukebemish/codecextras/test/structured/reflective/TestReflective.java index 332fcb1..25b4bd6 100644 --- a/src/test/java/dev/lukebemish/codecextras/test/structured/reflective/TestReflective.java +++ b/src/test/java/dev/lukebemish/codecextras/test/structured/reflective/TestReflective.java @@ -25,11 +25,15 @@ public enum TestEnum { C } + public record TestRecursive(String name, List list) {} + private static final Codec CODEC = CodecInterpreter.create().interpret(TestRecord.STRUCTURE).getOrThrow(); private static final Codec ARRAY_CODEC = CodecInterpreter.create().interpret(ReflectiveStructureCreator.create(TestRecord[].class)).getOrThrow(); private static final Codec PRIMITIVE_ARRAY_CODEC = CodecInterpreter.create().interpret(ReflectiveStructureCreator.create(int[].class)).getOrThrow(); + private static final Codec RECURSIVE_CODEC = CodecInterpreter.create().interpret(ReflectiveStructureCreator.create(TestRecursive.class)).getOrThrow(); + private final String json = """ { "a": 1, @@ -55,9 +59,25 @@ public enum TestEnum { private final String primitiveArrayJson = """ [1, 2, 3]"""; + private final String recursiveJson = """ + { + "name": "test1", + "list": [ + { + "name": "test2", + "list": [] + }, + { + "name": "test3", + "list": [] + } + ] + }"""; + private final TestRecord object = new TestRecord(1, "test", TestEnum.A, OptionalInt.of(2), Optional.of("test"), null, new LinkedHashSet<>(List.of(1, 2, 3))); private final TestRecord[] array = new TestRecord[] { object }; private final int[] primitiveArray = new int[] { 1, 2, 3 }; + private final TestRecursive recursive = new TestRecursive("test1", List.of(new TestRecursive("test2", List.of()), new TestRecursive("test3", List.of()))); @Test void testDecoding() { @@ -88,4 +108,14 @@ void testDecodingPrimitiveArray() { void testEncodingPrimitiveArray() { CodecAssertions.assertEncodes(JsonOps.INSTANCE, primitiveArray, primitiveArrayJson, PRIMITIVE_ARRAY_CODEC); } + + @Test + void testDecodingRecursive() { + CodecAssertions.assertDecodes(JsonOps.INSTANCE, recursiveJson, recursive, RECURSIVE_CODEC); + } + + @Test + void testEncodingRecursive() { + CodecAssertions.assertEncodes(JsonOps.INSTANCE, recursive, recursiveJson, RECURSIVE_CODEC); + } } From b11b2af0cc72448a6430b7f71dcfb7031d7393b9 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Tue, 4 Mar 2025 15:16:42 -0600 Subject: [PATCH 13/28] Allow config screen interpreter to work with recursive structures --- .../lukebemish/codecextras/utility/Lazy.java | 26 ++++ src/main/java/module-info.java | 2 + .../config/ConfigScreenBuilder.java | 4 +- .../structured/config/ConfigScreenEntry.java | 2 +- .../config/ConfigScreenInterpreter.java | 123 +++++++++++------- .../config/DispatchScreenEntryProvider.java | 4 +- .../structured/config/EntryCreationInfo.java | 5 +- .../config/RecordScreenEntryProvider.java | 4 +- .../minecraft/structured/config/Widgets.java | 26 ++-- .../structured/StreamCodecInterpreter.java | 38 ++++-- .../codecextras/test/common/TestConfig.java | 10 +- 11 files changed, 164 insertions(+), 80 deletions(-) create mode 100644 src/main/java/dev/lukebemish/codecextras/utility/Lazy.java diff --git a/src/main/java/dev/lukebemish/codecextras/utility/Lazy.java b/src/main/java/dev/lukebemish/codecextras/utility/Lazy.java new file mode 100644 index 0000000..1765412 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/utility/Lazy.java @@ -0,0 +1,26 @@ +package dev.lukebemish.codecextras.utility; + +import com.google.common.base.Suppliers; +import java.util.function.Function; +import java.util.function.Supplier; + +public final class Lazy implements Supplier { + private final Supplier memoized; + + private Lazy(Supplier memoized) { + this.memoized = Suppliers.memoize(memoized::get); + } + + @Override + public T get() { + return memoized.get(); + } + + public Lazy andThen(Function function) { + return new Lazy<>(() -> function.apply(get())); + } + + public static Lazy of(Supplier supplier) { + return new Lazy<>(supplier); + } +} diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 8f3262c..e7bc75c 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -35,4 +35,6 @@ exports dev.lukebemish.codecextras.structured.schema; exports dev.lukebemish.codecextras.types; + + exports dev.lukebemish.codecextras.utility to codecextras_minecraft, dev.lukebemish.codecextras.minecraft; } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenBuilder.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenBuilder.java index c07a098..e3f0568 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenBuilder.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenBuilder.java @@ -55,12 +55,12 @@ public void onExit(EntryCreationContext context) {} @Override public void addEntries(ScreenEntryList list, Runnable rebuild, Screen parent) { for (var screen : screens) { - var label = new StringWidget(Button.DEFAULT_WIDTH, Button.DEFAULT_HEIGHT, screen.screenEntry().entryCreationInfo().componentInfo().title(), Minecraft.getInstance().font).alignLeft(); + var label = new StringWidget(Button.DEFAULT_WIDTH, Button.DEFAULT_HEIGHT, screen.screenEntry().entryCreationInfo().componentInfo().get().title(), Minecraft.getInstance().font).alignLeft(); var button = Button.builder(Component.translatable("codecextras.config.configurerecord"), b -> { var newScreen = openSingleScreen(parent, screen); Minecraft.getInstance().setScreen(newScreen); }).width(Button.DEFAULT_WIDTH).build(); - screen.screenEntry().entryCreationInfo().componentInfo().maybeDescription().ifPresent(description -> { + screen.screenEntry().entryCreationInfo().componentInfo().get().maybeDescription().ifPresent(description -> { var tooltip = Tooltip.create(description); label.setTooltip(tooltip); button.setTooltip(tooltip); diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenEntry.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenEntry.java index c5236c1..e6ef102 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenEntry.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenEntry.java @@ -65,6 +65,6 @@ Screen rootScreen(Screen parent, Consumer onClose, EntryCreationContext conte onClose.accept(decoded.getOrThrow()); } }, this.entryCreationInfo()); - return ScreenEntryProvider.create(provider, parent, context, entryCreationInfo.componentInfo()); + return ScreenEntryProvider.create(provider, parent, context, entryCreationInfo.componentInfo().get()); } } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java index 52e99f2..279cb3c 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java @@ -36,6 +36,7 @@ import dev.lukebemish.codecextras.structured.RecordStructure; import dev.lukebemish.codecextras.structured.Structure; import dev.lukebemish.codecextras.types.Identity; +import dev.lukebemish.codecextras.utility.Lazy; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Comparator; @@ -97,7 +98,7 @@ public ConfigScreenInterpreter( return DataResult.error(() -> "Not an integer: "+string); } }, integer -> DataResult.success(integer+""), string -> string.matches("^-?[0-9]*$"), true), - new EntryCreationInfo<>(Codec.INT, ComponentInfo.empty()) + new EntryCreationInfo<>(Codec.INT, Lazy.of(ComponentInfo::empty)) )) .add(Interpreter.BYTE, ConfigScreenEntry.single( Widgets.text(string -> { @@ -107,7 +108,7 @@ public ConfigScreenInterpreter( return DataResult.error(() -> "Not a byte: "+string); } }, byteValue -> DataResult.success(byteValue+""), string -> string.matches("^-?[0-9]*$"), true), - new EntryCreationInfo<>(Codec.BYTE, ComponentInfo.empty()) + new EntryCreationInfo<>(Codec.BYTE, Lazy.of(ComponentInfo::empty)) )) .add(Interpreter.SHORT, ConfigScreenEntry.single( Widgets.text(string -> { @@ -117,7 +118,7 @@ public ConfigScreenInterpreter( return DataResult.error(() -> "Not a short: "+string); } }, shortValue -> DataResult.success(shortValue+""), string -> string.matches("^-?[0-9]*$"), true), - new EntryCreationInfo<>(Codec.SHORT, ComponentInfo.empty()) + new EntryCreationInfo<>(Codec.SHORT, Lazy.of(ComponentInfo::empty)) )) .add(Interpreter.LONG, ConfigScreenEntry.single( Widgets.text(string -> { @@ -127,7 +128,7 @@ public ConfigScreenInterpreter( return DataResult.error(() -> "Not a long: "+string); } }, longValue -> DataResult.success(longValue+""), string -> string.matches("^-?[0-9]*$"), true), - new EntryCreationInfo<>(Codec.LONG, ComponentInfo.empty()) + new EntryCreationInfo<>(Codec.LONG, Lazy.of(ComponentInfo::empty)) )) .add(Interpreter.DOUBLE, ConfigScreenEntry.single( Widgets.text(string -> { @@ -137,7 +138,7 @@ public ConfigScreenInterpreter( return DataResult.error(() -> "Not a double: "+string); } }, doubleValue -> DataResult.success(doubleValue+""), string -> string.matches("^-?[0-9]*(\\.[0-9]*)?$"), true), - new EntryCreationInfo<>(Codec.DOUBLE, ComponentInfo.empty()) + new EntryCreationInfo<>(Codec.DOUBLE, Lazy.of(ComponentInfo::empty)) )) .add(Interpreter.FLOAT, ConfigScreenEntry.single( Widgets.text(string -> { @@ -147,31 +148,31 @@ public ConfigScreenInterpreter( return DataResult.error(() -> "Not a float: "+string); } }, floatValue -> DataResult.success(floatValue+""), string -> string.matches("^-?[0-9]*(\\.[0-9]*)?$"), true), - new EntryCreationInfo<>(Codec.FLOAT, ComponentInfo.empty()) + new EntryCreationInfo<>(Codec.FLOAT, Lazy.of(ComponentInfo::empty)) )) .add(Interpreter.BOOL, ConfigScreenEntry.single( Widgets.bool(), - new EntryCreationInfo<>(Codec.BOOL, ComponentInfo.empty()) + new EntryCreationInfo<>(Codec.BOOL, Lazy.of(ComponentInfo::empty)) )) .add(Interpreter.UNIT, ConfigScreenEntry.single( Widgets.unit(), - new EntryCreationInfo<>(Codec.unit(Unit.INSTANCE), ComponentInfo.empty()) + new EntryCreationInfo<>(Codec.unit(Unit.INSTANCE), Lazy.of(ComponentInfo::empty)) )) .add(Interpreter.EMPTY_LIST, ConfigScreenEntry.single( Widgets.unit(Component.translatable("codecextras.config.unit.empty")), - new EntryCreationInfo<>(MinecraftInterpreters.CODEC_INTERPRETER.interpret(Structure.EMPTY_LIST).getOrThrow(), ComponentInfo.empty()) + new EntryCreationInfo<>(MinecraftInterpreters.CODEC_INTERPRETER.interpret(Structure.EMPTY_LIST).getOrThrow(), Lazy.of(ComponentInfo::empty)) )) .add(Interpreter.EMPTY_MAP, ConfigScreenEntry.single( Widgets.unit(Component.translatable("codecextras.config.unit.empty")), - new EntryCreationInfo<>(MinecraftInterpreters.CODEC_INTERPRETER.interpret(Structure.EMPTY_MAP).getOrThrow(), ComponentInfo.empty()) + new EntryCreationInfo<>(MinecraftInterpreters.CODEC_INTERPRETER.interpret(Structure.EMPTY_MAP).getOrThrow(), Lazy.of(ComponentInfo::empty)) )) .add(Interpreter.STRING, ConfigScreenEntry.single( Widgets.wrapWithOptionalHandling(Widgets.text(DataResult::success, DataResult::success, false)), - new EntryCreationInfo<>(Codec.STRING, ComponentInfo.empty()) + new EntryCreationInfo<>(Codec.STRING, Lazy.of(ComponentInfo::empty)) )) .add(Interpreter.PASSTHROUGH, ConfigScreenEntry.single( Widgets.wrapWithOptionalHandling(ConfigScreenInterpreter::byJson), - new EntryCreationInfo<>(Codec.PASSTHROUGH, ComponentInfo.empty()) + new EntryCreationInfo<>(Codec.PASSTHROUGH, Lazy.of(ComponentInfo::empty)) )) .add(MinecraftKeys.ITEM, ConfigScreenEntry.single( Widgets.pickWidget(new StringRepresentation<>( @@ -194,19 +195,19 @@ public ConfigScreenInterpreter( }, false )), - new EntryCreationInfo<>(Item.CODEC, ComponentInfo.empty()) + new EntryCreationInfo<>(Item.CODEC, Lazy.of(ComponentInfo::empty)) )) .add(MinecraftKeys.RESOURCE_LOCATION, ConfigScreenEntry.single( Widgets.wrapWithOptionalHandling(Widgets.text(ResourceLocation::read, rl -> DataResult.success(rl.toString()), string -> string.matches("^([a-z0-9._-]+:)?[a-z0-9/._-]*$"), false)), - new EntryCreationInfo<>(ResourceLocation.CODEC, ComponentInfo.empty()) + new EntryCreationInfo<>(ResourceLocation.CODEC, Lazy.of(ComponentInfo::empty)) )) .add(MinecraftKeys.ARGB_COLOR, ConfigScreenEntry.single( Widgets.color(true), - new EntryCreationInfo<>(Codec.INT, ComponentInfo.empty()) + new EntryCreationInfo<>(Codec.INT, Lazy.of(ComponentInfo::empty)) )) .add(MinecraftKeys.RGB_COLOR, ConfigScreenEntry.single( Widgets.color(false), - new EntryCreationInfo<>(Codec.INT, ComponentInfo.empty()) + new EntryCreationInfo<>(Codec.INT, Lazy.of(ComponentInfo::empty)) )) .add(MinecraftKeys.DATA_COMPONENT_PATCH_KEY, ConfigScreenEntry.single( (parent, width, context, original, update, creationInfo, handleOptional) -> { @@ -273,7 +274,7 @@ public ConfigScreenInterpreter( } }, creationInfo.withCodec(ResourceKey.codec(Registries.DATA_COMPONENT_TYPE)), false); var tooltipToggle = Tooltip.create(Component.translatable("codecextras.config.datacomponent.keytoggle")); - var tooltipType = Tooltip.create(creationInfo.componentInfo().description()); + var tooltipType = Tooltip.create(creationInfo.componentInfo().get().description()); cycle.setTooltip(tooltipToggle); actual.visitWidgets(w -> w.setTooltip(tooltipType)); var layout = new EqualSpacingLayout(width, 0, EqualSpacingLayout.Orientation.HORIZONTAL); @@ -281,7 +282,7 @@ public ConfigScreenInterpreter( layout.addChild(actual, LayoutSettings.defaults().alignVerticallyMiddle()); return layout; }, - new EntryCreationInfo<>(MinecraftInterpreters.CODEC_INTERPRETER.interpret(MinecraftStructures.DATA_COMPONENT_PATCH_KEY).getOrThrow(), ComponentInfo.empty()) + new EntryCreationInfo<>(MinecraftInterpreters.CODEC_INTERPRETER.interpret(MinecraftStructures.DATA_COMPONENT_PATCH_KEY).getOrThrow(), Lazy.of(ComponentInfo::empty)) )).build()), parametricKeys.join(Keys2., K1, K1>builder() .add(Interpreter.INT_IN_RANGE, new ParametricKeyedValue<>() { @@ -299,7 +300,7 @@ public App, T>> convert(App "Not an integer: " + json); - }, false), new EntryCreationInfo<>(codec, ComponentInfo.empty()) + }, false), new EntryCreationInfo<>(codec, Lazy.of(ComponentInfo::empty)) ).withEntryCreationInfo( info -> info.withCodec(codec.xmap(Const::create, Const::unbox)), info -> info.withCodec(codec) @@ -321,7 +322,7 @@ public App, T>> convert(App "Not a byte: " + json); - }, false), new EntryCreationInfo<>(codec, ComponentInfo.empty()) + }, false), new EntryCreationInfo<>(codec, Lazy.of(ComponentInfo::empty)) ).withEntryCreationInfo( info -> info.withCodec(codec.xmap(Const::create, Const::unbox)), info -> info.withCodec(codec) @@ -343,7 +344,7 @@ public App, T>> convert(App "Not a short: " + json); - }, false), new EntryCreationInfo<>(codec, ComponentInfo.empty()) + }, false), new EntryCreationInfo<>(codec, Lazy.of(ComponentInfo::empty)) ).withEntryCreationInfo( info -> info.withCodec(codec.xmap(Const::create, Const::unbox)), info -> info.withCodec(codec) @@ -365,7 +366,7 @@ public App, T>> convert(App "Not a long: " + json); - }, false), new EntryCreationInfo<>(codec, ComponentInfo.empty()) + }, false), new EntryCreationInfo<>(codec, Lazy.of(ComponentInfo::empty)) ).withEntryCreationInfo( info -> info.withCodec(codec.xmap(Const::create, Const::unbox)), info -> info.withCodec(codec) @@ -387,7 +388,7 @@ public App, T>> convert(App "Not a float: " + json); - }, true), new EntryCreationInfo<>(codec, ComponentInfo.empty()) + }, true), new EntryCreationInfo<>(codec, Lazy.of(ComponentInfo::empty)) ).withEntryCreationInfo( info -> info.withCodec(codec.xmap(Const::create, Const::unbox)), info -> info.withCodec(codec) @@ -409,7 +410,7 @@ public App, T>> convert(App "Not a double: " + json); - }, true), new EntryCreationInfo<>(codec, ComponentInfo.empty()) + }, true), new EntryCreationInfo<>(codec, Lazy.of(ComponentInfo::empty)) ).withEntryCreationInfo( info -> info.withCodec(codec.xmap(Const::create, Const::unbox)), info -> info.withCodec(codec) @@ -424,7 +425,7 @@ public App> convert(App>xmap(Identity::new, app -> Identity.unbox(app).value()); return ConfigScreenEntry.single( Widgets.pickWidget(representation), - new EntryCreationInfo<>(codec, ComponentInfo.empty()) + new EntryCreationInfo<>(codec, Lazy.of(ComponentInfo::empty)) ).withEntryCreationInfo(i -> i.withCodec(identityCodec), i -> i.withCodec(codec)); } }) @@ -452,7 +453,7 @@ public App> } return wrapped.create(parent, width, context, original, update, creationInfo, handleOptional); }, - new EntryCreationInfo<>(codec, ComponentInfo.empty()) + new EntryCreationInfo<>(codec, Lazy.of(ComponentInfo::empty)) ).withEntryCreationInfo(i -> i.withCodec(holderCodec), i -> i.withCodec(codec)); } }) @@ -476,13 +477,13 @@ public App> convert(App(Codec.EMPTY.codec().flatXmap( ignored -> DataResult.error(() -> type + " is not a persistent component"), ignored -> DataResult.error(() -> type + " is not a persistent component") - ), ComponentInfo.empty()) + ), Lazy.of(ComponentInfo::empty)) ); } var identityCodec = codec.>xmap(Identity::new, app -> Identity.unbox(app).value()); return ConfigScreenEntry.single( Widgets.wrapWithOptionalHandling(ConfigScreenInterpreter::byJson), - new EntryCreationInfo<>(identityCodec, ComponentInfo.empty()) + new EntryCreationInfo<>(identityCodec, Lazy.of(ComponentInfo::empty)) ); } }) @@ -514,19 +515,19 @@ private static LayoutElement byJson(Screen parentOuter, int widthOuter, Entr var entryHolder = new Object() { final EntryCreationInfo jsonInfo = new EntryCreationInfo<>( Codec.PASSTHROUGH.xmap(d -> d.convert(contextOuter.ops()).getValue(), v -> new Dynamic<>(contextOuter.ops(), v)), - ComponentInfo.empty() + Lazy.of(ComponentInfo::empty) ); final EntryCreationInfo stringInfo = new EntryCreationInfo<>( Codec.STRING, - ComponentInfo.empty() + Lazy.of(ComponentInfo::empty) ); final EntryCreationInfo numberInfo = new EntryCreationInfo<>( BIG_DECIMAL_CODEC.xmap(Function.identity(), number -> number instanceof BigDecimal bigDecimal ? bigDecimal : new BigDecimal(number.toString())), - ComponentInfo.empty() + Lazy.of(ComponentInfo::empty) ); final EntryCreationInfo booleanInfo = new EntryCreationInfo<>( Codec.BOOL, - ComponentInfo.empty() + Lazy.of(ComponentInfo::empty) ); final ConfigScreenEntry stringEntry = ConfigScreenEntry.single( Widgets.text(DataResult::success, DataResult::success, false), @@ -666,7 +667,7 @@ enum JsonType { new UnboundedMapScreenEntryProvider<>(stringEntry, jsonEntry, context, elements.get(JsonType.OBJECT), newJsonValue -> { elements.put(JsonType.OBJECT, newJsonValue); checkedUpdate.accept(newJsonValue); - }), parent, context, creationInfo.componentInfo() + }), parent, context, creationInfo.componentInfo().get() )); }).width(remainingWidth).build())); layouts.put(JsonType.ARRAY, Button.builder(Component.translatable("codecextras.config.configurelist"), b -> { @@ -674,7 +675,7 @@ enum JsonType { new ListScreenEntryProvider<>(jsonEntry, context, elements.get(JsonType.ARRAY), newJsonValue -> { elements.put(JsonType.ARRAY, newJsonValue); checkedUpdate.accept(newJsonValue); - }), parent, context, creationInfo.componentInfo() + }), parent, context, creationInfo.componentInfo().get() )); }).width(remainingWidth).build()); layouts.put(JsonType.STRING, stringEntry.layout().create(parent, remainingWidth, context, elements.get(JsonType.STRING), newJsonValue -> { @@ -757,7 +758,7 @@ public DataResult>> either(App(codecResult.getOrThrow(), ComponentInfo.empty()) + new EntryCreationInfo<>(codecResult.getOrThrow(), Lazy.of(ComponentInfo::empty)) )); } @@ -774,7 +775,7 @@ public DataResult>> xor(App(codecResult.getOrThrow(), ComponentInfo.empty()) + new EntryCreationInfo<>(codecResult.getOrThrow(), Lazy.of(ComponentInfo::empty)) )); } @@ -846,11 +847,11 @@ public DataResult>> list(App(codecResult.getOrThrow(), ComponentInfo.empty()) + new EntryCreationInfo<>(codecResult.getOrThrow(), Lazy.of(ComponentInfo::empty)) )); } @@ -881,11 +882,11 @@ public DataResult>> unboundedMap(App< finalOriginal[0] = jsonValue; update.accept(jsonValue); }, creationInfo - ), parent, subContext, creationInfo.componentInfo())) + ), parent, subContext, creationInfo.componentInfo().get())) ).width(width).build(); }), factory, - new EntryCreationInfo<>(codecResult.getOrThrow(), ComponentInfo.empty()) + new EntryCreationInfo<>(codecResult.getOrThrow(), Lazy.of(ComponentInfo::empty)) )); } @@ -922,11 +923,11 @@ public DataResult> record(List(CodecInterpreter.unbox(codecResult.getOrThrow()), ComponentInfo.empty()) + new EntryCreationInfo<>(CodecInterpreter.unbox(codecResult.getOrThrow()), Lazy.of(ComponentInfo::empty)) )); } @@ -1023,18 +1024,44 @@ public DataResult> dispatch(String key, Stru finalOriginal[0] = value; update.accept(value); }, creationInfo - ), parent, subContext, creationInfo.componentInfo())) + ), parent, subContext, creationInfo.componentInfo().get())) ).width(width).build(); }), factory, - new EntryCreationInfo<>(CodecInterpreter.unbox(codecResult.getOrThrow()), ComponentInfo.empty()) + new EntryCreationInfo<>(CodecInterpreter.unbox(codecResult.getOrThrow()), Lazy.of(ComponentInfo::empty)) )); } @Override public DataResult> recursive(Function, Structure> function) { - // TODO: implement - return DataResult.error(() -> "Not yet implemented"); + var codecResult = codecInterpreter.recursive(function).map(CodecInterpreter::unbox); + if (codecResult.isError()) { + return DataResult.error(() -> "Error creating recursive codec: "+codecResult.error().orElseThrow().messageSupplier()); + } + var key = Key.create("recursive"); + var withKeyCodecInterpreter = this.codecInterpreter.with(Keys.builder().add(key, new CodecInterpreter.Holder<>(codecResult.getOrThrow())).build(), Keys2., K1, K1>builder().build()); + var keyed = Structure.keyed(key); + var complete = function.apply(keyed); + var configScreenEntryWrapper = new Object() { + private final LayoutFactory layoutFactory = (parent, width, context, original, update, creationInfo, handleOptional) -> + this.wrapped.get().result().orElseThrow().layout().create(parent, width, context, original, update, creationInfo, handleOptional); + private final ScreenEntryFactory screenEntryFactory = (context, original, onClose, entry) -> + this.wrapped.get().result().orElseThrow().screenEntryProvider().open(context, original, onClose, entry); + private final EntryCreationInfo entryCreationInfo = new EntryCreationInfo<>( + codecResult.getOrThrow(), + Lazy.of(() -> this.wrapped.get().result().orElseThrow().entryCreationInfo().componentInfo().get()) + ); + private final ConfigScreenEntry holder = new ConfigScreenEntry<>(layoutFactory, screenEntryFactory, entryCreationInfo); + private final ConfigScreenInterpreter interpreterWithKeys = new ConfigScreenInterpreter( + keys().with(key, holder), + parametricKeys(), + withKeyCodecInterpreter + ); + private final Supplier>> wrapped = Suppliers.memoize(() -> + complete.interpret(interpreterWithKeys).map(ConfigScreenEntry::unbox) + ); + }; + return DataResult.success(configScreenEntryWrapper.holder); } @Override @@ -1073,11 +1100,11 @@ public DataResult>> dispatchedMap(Str finalOriginal[0] = value; update.accept(value); }, creationInfo - ), parent, subContext, creationInfo.componentInfo())) + ), parent, subContext, creationInfo.componentInfo().get())) ).width(width).build(); }), factory, - new EntryCreationInfo<>(CodecInterpreter.unbox(codecResult.getOrThrow()), ComponentInfo.empty()) + new EntryCreationInfo<>(CodecInterpreter.unbox(codecResult.getOrThrow()), Lazy.of(ComponentInfo::empty)) )); } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/DispatchScreenEntryProvider.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/DispatchScreenEntryProvider.java index c04d35a..95238dc 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/DispatchScreenEntryProvider.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/DispatchScreenEntryProvider.java @@ -76,7 +76,7 @@ public void onExit(EntryCreationContext context) { @Override public void addEntries(ScreenEntryList list, Runnable rebuild, Screen parent) { - var label = new StringWidget(Button.DEFAULT_WIDTH, Button.DEFAULT_HEIGHT, keyEntry.entryCreationInfo().componentInfo().title(), Minecraft.getInstance().font).alignLeft(); + var label = new StringWidget(Button.DEFAULT_WIDTH, Button.DEFAULT_HEIGHT, keyEntry.entryCreationInfo().componentInfo().get().title(), Minecraft.getInstance().font).alignLeft(); var contents = keyEntry.layout().create(parent, Button.DEFAULT_WIDTH, context, keyValue, newKeyValue -> { if (!Objects.equals(newKeyValue, oldKeyValue)) { keyValue = newKeyValue; @@ -95,7 +95,7 @@ public void addEntries(ScreenEntryList list, Runnable rebuild, Screen parent) { } } }, keyEntry.entryCreationInfo(), false); - keyEntry.entryCreationInfo().componentInfo().maybeDescription().ifPresent(description -> { + keyEntry.entryCreationInfo().componentInfo().get().maybeDescription().ifPresent(description -> { var tooltip = Tooltip.create(description); label.setTooltip(tooltip); }); diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/EntryCreationInfo.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/EntryCreationInfo.java index b68c47f..08e89ef 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/EntryCreationInfo.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/EntryCreationInfo.java @@ -1,11 +1,12 @@ package dev.lukebemish.codecextras.minecraft.structured.config; import com.mojang.serialization.Codec; +import dev.lukebemish.codecextras.utility.Lazy; import java.util.function.UnaryOperator; -public record EntryCreationInfo(Codec codec, ComponentInfo componentInfo) { +public record EntryCreationInfo(Codec codec, Lazy componentInfo) { public EntryCreationInfo withComponentInfo(UnaryOperator function) { - return new EntryCreationInfo<>(this.codec, function.apply(this.componentInfo)); + return new EntryCreationInfo<>(this.codec, componentInfo.andThen(function)); } public EntryCreationInfo withCodec(Codec codec) { diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/RecordScreenEntryProvider.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/RecordScreenEntryProvider.java index 3c58783..7d77ad7 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/RecordScreenEntryProvider.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/RecordScreenEntryProvider.java @@ -46,9 +46,9 @@ public void onExit(EntryCreationContext context) { public void addEntries(ScreenEntryList list, Runnable rebuild, Screen parent) { for (var entry: this.entries) { JsonElement specificValue = this.jsonValue.has(entry.key()) ? this.jsonValue.get(entry.key()) : JsonNull.INSTANCE; - var label = new StringWidget(Button.DEFAULT_WIDTH, Button.DEFAULT_HEIGHT, entry.entry().entryCreationInfo().componentInfo().title(), Minecraft.getInstance().font).alignLeft(); + var label = new StringWidget(Button.DEFAULT_WIDTH, Button.DEFAULT_HEIGHT, entry.entry().entryCreationInfo().componentInfo().get().title(), Minecraft.getInstance().font).alignLeft(); var contents = createEntryWidget(entry, specificValue, parent); - entry.entry().entryCreationInfo().componentInfo().maybeDescription().ifPresent(description -> { + entry.entry().entryCreationInfo().componentInfo().get().maybeDescription().ifPresent(description -> { var tooltip = Tooltip.create(description); label.setTooltip(tooltip); }); diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/Widgets.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/Widgets.java index 82718a5..834238f 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/Widgets.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/Widgets.java @@ -42,8 +42,8 @@ private Widgets() {} public static LayoutFactory text(Function> toData, Function> fromData, Predicate filter, boolean emptyIsMissing) { return (parent, width, context, original, update, creationInfo, handleOptional) -> { - var widget = new EditBox(Minecraft.getInstance().font, width, Button.DEFAULT_HEIGHT, creationInfo.componentInfo().title()); - creationInfo.componentInfo().maybeDescription().ifPresent(description -> { + var widget = new EditBox(Minecraft.getInstance().font, width, Button.DEFAULT_HEIGHT, creationInfo.componentInfo().get().title()); + creationInfo.componentInfo().get().maybeDescription().ifPresent(description -> { var tooltip = Tooltip.create(description); widget.setTooltip(tooltip); }); @@ -116,7 +116,7 @@ public static LayoutFactory pickWidget(StringRepresentation representa Supplier calculateMessage = () -> Component.literal(stringValue[0] == null ? "" : stringValue[0]); var holder = new Object() { private final Button button = Button.builder(calculateMessage.get(), b -> { - Minecraft.getInstance().setScreen(new ChoiceScreen(parent, creationInfo.componentInfo().title(), values, stringValue[0], newKeyValue -> { + Minecraft.getInstance().setScreen(new ChoiceScreen(parent, creationInfo.componentInfo().get().title(), values, stringValue[0], newKeyValue -> { if (!Objects.equals(newKeyValue, stringValue[0])) { stringValue[0] = newKeyValue; if (newKeyValue == null) { @@ -127,7 +127,7 @@ public static LayoutFactory pickWidget(StringRepresentation representa this.button.setMessage(calculateMessage.get()); } })); - }).width(width).tooltip(Tooltip.create(creationInfo.componentInfo().description())).build(); + }).width(width).tooltip(Tooltip.create(creationInfo.componentInfo().get().description())).build(); }; return holder.button; }); @@ -181,7 +181,7 @@ public static LayoutFactory wrapWithOptionalHandling(LayoutFactory ass wrapped.setVisible(!missing); wrapped.setActive(!missing); - creationInfo.componentInfo().maybeDescription().ifPresent(description -> { + creationInfo.componentInfo().get().maybeDescription().ifPresent(description -> { var tooltip = Tooltip.create(description); lock.setTooltip(tooltip); disabled.setTooltip(tooltip); @@ -224,12 +224,12 @@ public static LayoutFactory color(boolean includeAlpha) { return new AbstractButton(0, 0, width, Button.DEFAULT_HEIGHT, Component.empty()) { { - setTooltip(Tooltip.create(creationInfo.componentInfo().description())); + setTooltip(Tooltip.create(creationInfo.componentInfo().get().description())); } @Override public void onPress() { - var screen = new ColorPickScreen(parent, creationInfo.componentInfo().title(), color -> { + var screen = new ColorPickScreen(parent, creationInfo.componentInfo().get().title(), color -> { update.accept(new JsonPrimitive(color)); value[0] = color; }, includeAlpha); @@ -319,7 +319,7 @@ protected void applyValue() { this.value = valueInRange(range, value); } }; - widget.setTooltip(Tooltip.create(creationInfo.componentInfo().description())); + widget.setTooltip(Tooltip.create(creationInfo.componentInfo().get().description())); return widget; }); } @@ -428,18 +428,18 @@ public static LayoutFactory unit(Component text) { }) .selected(original.isJsonPrimitive() && original.getAsJsonPrimitive().getAsBoolean()) .build(); - creationInfo.componentInfo().maybeDescription().ifPresent(description -> { + creationInfo.componentInfo().get().maybeDescription().ifPresent(description -> { var tooltip = Tooltip.create(description); w.setTooltip(tooltip); }); - w.setMessage(creationInfo.componentInfo().title()); + w.setMessage(creationInfo.componentInfo().get().title()); return w; } else { var button = Button.builder(text, b -> { }) .width(width) .build(); - var tooltip = Tooltip.create(creationInfo.componentInfo().description()); + var tooltip = Tooltip.create(creationInfo.componentInfo().get().description()); button.setTooltip(tooltip); button.active = false; return VisibilityWrapperElement.ofInactive(button); @@ -462,11 +462,11 @@ public static LayoutFactory bool() { }) .selected(original.isJsonPrimitive() && original.getAsJsonPrimitive().getAsBoolean()) .build(); - creationInfo.componentInfo().maybeDescription().ifPresent(description -> { + creationInfo.componentInfo().get().maybeDescription().ifPresent(description -> { var tooltip = Tooltip.create(description); w.setTooltip(tooltip); }); - w.setMessage(creationInfo.componentInfo().title()); + w.setMessage(creationInfo.componentInfo().get().title()); return w; }; return wrapWithOptionalHandling(widget); diff --git a/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java b/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java index e494b68..b182715 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java @@ -22,6 +22,13 @@ import io.netty.buffer.ByteBuf; import io.netty.handler.codec.DecoderException; import io.netty.handler.codec.EncoderException; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.VarInt; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import org.jspecify.annotations.Nullable; + import java.util.ArrayList; import java.util.HashMap; import java.util.IdentityHashMap; @@ -33,12 +40,6 @@ import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Stream; -import net.minecraft.network.FriendlyByteBuf; -import net.minecraft.network.RegistryFriendlyByteBuf; -import net.minecraft.network.VarInt; -import net.minecraft.network.codec.ByteBufCodecs; -import net.minecraft.network.codec.StreamCodec; -import org.jspecify.annotations.Nullable; /** * Interprets a {@link Structure} into a {@link StreamCodec} for the same type. @@ -433,8 +434,29 @@ public DataResult, Either>> xor(App, @Override public DataResult, A>> recursive(Function, Structure> function) { - // TODO: implement - return DataResult.error(() -> "Not yet implemented"); + var key = Key.create("recursive"); + var keyed = Structure.keyed(key); + var complete = function.apply(keyed); + var codec = new StreamCodec() { + private final Holder holder = new Holder<>(this); + private final StreamCodecInterpreter interpreterWithKeys = with(Keys., Object>builder().add(key, holder).build(), Keys2.>, K1, K1>builder().build()); + private final Supplier>> wrapped = Suppliers.memoize(() -> + complete.interpret(interpreterWithKeys).map(StreamCodecInterpreter::unbox) + ); + + @Override + public void encode(B object, A object2) { + var wrappedStreamCodec = wrapped.get().result().orElseThrow(() -> new EncoderException("Issue creating recursive codec: "+wrapped.get().error().orElseThrow().message())); + wrappedStreamCodec.encode(object, object2); + } + + @Override + public A decode(B object) { + var wrappedStreamCodec = wrapped.get().result().orElseThrow(() -> new DecoderException("Issue creating recursive codec: "+wrapped.get().error().orElseThrow().message())); + return wrappedStreamCodec.decode(object); + } + }; + return DataResult.success(new Holder<>(codec)); } public record Holder(StreamCodec streamCodec) implements App, T> { diff --git a/src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java b/src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java index 1901362..a608558 100644 --- a/src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java +++ b/src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java @@ -32,10 +32,14 @@ public record TestConfig( int intInRange, float floatInRange, int argb, int rgb, ResourceKey item, Rarity rarity, Map unbounded, Either either, Map dispatchedMap, - DataComponentPatch patch, ItemStack itemStack, ReflectiveRecord reflectiveRecord + DataComponentPatch patch, ItemStack itemStack, ReflectiveRecord reflectiveRecord, RecursiveRecord recursive ) { private static final Map> DISPATCHES = new HashMap<>(); + public record RecursiveRecord(String name, List list) { + private static final Structure STRUCTURE = ReflectiveStructureCreator.create(RecursiveRecord.class); + } + public interface Dispatches { Structure STRUCTURE = Structure.STRING.dispatch( "type", @@ -105,6 +109,7 @@ public record ReflectiveRecord(String x, ResourceLocation y, int[] z) { var patch = builder.addOptional("patch", MinecraftStructures.DATA_COMPONENT_PATCH, TestConfig::patch, () -> DataComponentPatch.EMPTY); var itemStack = builder.addOptional("itemStack", MinecraftStructures.OPTIONAL_ITEM_STACK, TestConfig::itemStack, () -> ItemStack.EMPTY); var reflectiveRecord = builder.addOptional("reflectiveRecord", ReflectiveRecord.STRUCTURE, TestConfig::reflectiveRecord, () -> new ReflectiveRecord("test", ResourceLocation.fromNamespaceAndPath("test", "test"), new int[] {1, 2, 3})); + var testRecursive = builder.addOptional("recursive", RecursiveRecord.STRUCTURE, TestConfig::recursive, () -> new RecursiveRecord("test1", List.of(new RecursiveRecord("test2", List.of()), new RecursiveRecord("test3", List.of())))); return container -> new TestConfig( a.apply(container), b.apply(container), c.apply(container), d.apply(container), e.apply(container), f.apply(container), @@ -112,7 +117,8 @@ public record ReflectiveRecord(String x, ResourceLocation y, int[] z) { intInRange.apply(container), floatInRange.apply(container), argb.apply(container), rgb.apply(container), item.apply(container), rarity.apply(container), unbounded.apply(container), either.apply(container), dispatchedMap.apply(container), - patch.apply(container), itemStack.apply(container), reflectiveRecord.apply(container) + patch.apply(container), itemStack.apply(container), reflectiveRecord.apply(container), + testRecursive.apply(container) ); }); From 22bc9d9a53d2501327b9b7b1a88d3f2e349d4e28 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Tue, 4 Mar 2025 15:45:58 -0600 Subject: [PATCH 14/28] Reflective structure creation options and avoid null-checking for primitives --- .../BuiltInReflectiveStructureCreator.java | 28 +++++++----- .../structured/reflective/CreationOption.java | 3 ++ .../reflective/CreationOptions.java | 16 +++++++ .../ReflectiveStructureCreator.java | 45 +++++++++++-------- .../reflective/SerializedProperty.java | 1 + .../reflective/SimpleCreatorOption.java | 5 +++ .../MinecraftReflectiveStructureCreator.java | 7 +-- 7 files changed, 73 insertions(+), 32 deletions(-) create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/reflective/CreationOption.java create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/reflective/CreationOptions.java create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/reflective/SimpleCreatorOption.java diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/BuiltInReflectiveStructureCreator.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/BuiltInReflectiveStructureCreator.java index e112a6b..94b3432 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/BuiltInReflectiveStructureCreator.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/BuiltInReflectiveStructureCreator.java @@ -86,7 +86,7 @@ @AutoService(ReflectiveStructureCreator.class) public class BuiltInReflectiveStructureCreator implements ReflectiveStructureCreator { @Override - public Map, Creator> creators() { + public Map, Creator> creators(CreationOptions options) { return ImmutableMap., Creator>builder() .put(Unit.class, () -> Structure.UNIT) .put(Boolean.class, () -> Structure.BOOL) @@ -233,9 +233,9 @@ public Map, Creator> creators() { } @Override - public Map, ParameterizedCreator> parameterizedCreators() { + public Map, ParameterizedCreator> parameterizedCreators(CreationOptions options) { return ImmutableMap., ParameterizedCreator>builder() - .put(Either.class, parameters -> Structure.unboundedMap(parameters[0].create(), parameters[1].create())) + .put(Either.class, (parameters) -> Structure.unboundedMap(parameters[0].create(), parameters[1].create())) // Collections .put(Collection.class, collectionMaker(ArrayList::new)) .put(SequencedCollection.class, collectionMaker(ArrayList::new)) @@ -263,12 +263,12 @@ public Map, ParameterizedCreator> parameterizedCreators() { @SuppressWarnings({"rawtypes", "Convert2MethodRef", "unchecked"}) private static ParameterizedCreator collectionMaker(Function, T> function) { - return parameters -> parameters[0].create().listOf().xmap(function::apply, c -> new ArrayList<>(c)); + return (parameters) -> parameters[0].create().listOf().xmap(function::apply, c -> new ArrayList<>(c)); } @SuppressWarnings({"rawtypes", "Convert2MethodRef", "unchecked"}) private static ParameterizedCreator mapMaker(Function, T> function) { - return parameters -> Structure.unboundedMap(parameters[0].create(), parameters[1].create()).xmap(function::apply, c -> new LinkedHashMap<>(c)); + return (parameters) -> Structure.unboundedMap(parameters[0].create(), parameters[1].create()).xmap(function::apply, c -> new LinkedHashMap<>(c)); } private static ConstantDynamic conDyn(String descriptor, int i) { @@ -335,7 +335,8 @@ private static CallSite asCallSite(MethodHandles.Lookup ignoredLookup, String ig } @SuppressWarnings({"rawtypes", "unchecked"}) - public static Function add(RecordStructure builder, String name, Type type, Function getter, Function> creator) { + public static Function add(CreationOptions options, RecordStructure builder, String name, Type type, Function getter, Function> creator) { + // TODO: check annotations if (type instanceof ParameterizedType parameterizedType && parameterizedType.getRawType() instanceof Class rawType) { if (rawType.equals(Optional.class)) { var innerType = parameterizedType.getActualTypeArguments()[0]; @@ -350,12 +351,19 @@ private static CallSite asCallSite(MethodHandles.Lookup ignoredLookup, String ig return builder.addOptionalLong(name, (Function) getter); } } - Function> key = builder.addOptional(name, (Structure) creator.apply(type), (Function) getter.andThen(Optional::ofNullable)); - return key.andThen(o -> o.orElse(null)); + boolean isNotNull = options.hasOption(SimpleCreatorOption.NOT_NULL_BY_DEFAULT); + Function key; + if (isNotNull || (type instanceof Class clazz && clazz.isPrimitive())) { + key = builder.add(name, (Structure) creator.apply(type), (Function) getter); + } else { + key = ((Function>) builder.addOptional(name, (Structure) creator.apply(type), (Function) getter.andThen(Optional::ofNullable))) + .andThen(o -> o.orElse(null)); + } + return key; } @Override - public List flexibleCreators() { + public List flexibleCreators(CreationOptions options) { return ImmutableList.builder() .add(new FlexibleCreator() { @SuppressWarnings({"unchecked", "rawtypes"}) @@ -672,7 +680,7 @@ public Structure create(Class exact, TypedCreator[] parameters, Function>(propertyList.size()); for (var property : propertyList) { var getter = getters.get(property); - var key = add(builder, property, types.get(property), getter, creator); + var key = add(options, builder, property, types.get(property), getter, creator); keyList.add(key); } var cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/CreationOption.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/CreationOption.java new file mode 100644 index 0000000..d3016d4 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/CreationOption.java @@ -0,0 +1,3 @@ +package dev.lukebemish.codecextras.structured.reflective; + +public interface CreationOption {} diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/CreationOptions.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/CreationOptions.java new file mode 100644 index 0000000..8aacec4 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/CreationOptions.java @@ -0,0 +1,16 @@ +package dev.lukebemish.codecextras.structured.reflective; + +import java.util.Collection; +import java.util.Set; + +public final class CreationOptions { + private final Set options; + + CreationOptions(Collection options) { + this.options = Set.copyOf(options); + } + + public boolean hasOption(CreationOption option) { + return options.contains(option); + } +} diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java index dae6234..8648015 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java @@ -15,9 +15,9 @@ import java.util.function.Supplier; public interface ReflectiveStructureCreator { - Map, Creator> creators(); - Map, ParameterizedCreator> parameterizedCreators(); - List flexibleCreators(); + Map, Creator> creators(CreationOptions options); + Map, ParameterizedCreator> parameterizedCreators(CreationOptions options); + List flexibleCreators(CreationOptions options); interface TypedCreator { Structure create(); @@ -52,10 +52,11 @@ final class Instance { private final Map, ParameterizedCreator> parameterizedCreators; private final List flexibleCreators; - private Instance(Map, Creator> creators, Map, ParameterizedCreator> parameterizedCreators, List flexibleCreators) { + private Instance(Map, Creator> creators, Map, ParameterizedCreator> parameterizedCreators, List flexibleCreators, CreationOptions options) { this.creators = creators; this.parameterizedCreators = parameterizedCreators; this.flexibleCreators = flexibleCreators; + this.options = options; } public static Builder builder() { @@ -66,6 +67,7 @@ public static final class Builder { private final Map, Creator> creators = new IdentityHashMap<>(); private final Map, ParameterizedCreator> parameterizedCreators = new IdentityHashMap<>(); private final List flexibleCreators = new ArrayList<>(); + private final List options = new ArrayList<>(); private Builder() {} @@ -84,14 +86,21 @@ public Builder withFlexibleCreator(FlexibleCreator creator) { return this; } + public Builder withOption(CreationOption option) { + options.add(option); + return this; + } + public Instance build() { - return new Instance(creators, parameterizedCreators, flexibleCreators); + return new Instance(creators, parameterizedCreators, flexibleCreators, new CreationOptions(options)); } } private static final LayeredServiceLoader SERVICE_LOADER = LayeredServiceLoader.of(ReflectiveStructureCreator.class); - private final Map cachedCreators = new HashMap<>(); + private final Map> cachedCreators = new HashMap<>(); + + private final CreationOptions options; @SuppressWarnings("unchecked") public synchronized Structure create(Class clazz) { @@ -101,9 +110,9 @@ public synchronized Structure create(Class clazz) { Map, ParameterizedCreator> parameterizedCreatorsMap = new IdentityHashMap<>(); List flexibleCreatorsList = new ArrayList<>(); services.forEach(creator -> { - creatorsMap.putAll(creator.creators()); - parameterizedCreatorsMap.putAll(creator.parameterizedCreators()); - flexibleCreatorsList.addAll(creator.flexibleCreators()); + creatorsMap.putAll(creator.creators(options)); + parameterizedCreatorsMap.putAll(creator.parameterizedCreators(options)); + flexibleCreatorsList.addAll(creator.flexibleCreators(options)); }); creatorsMap.putAll(this.creators); @@ -114,8 +123,7 @@ public synchronized Structure create(Class clazz) { var recursionCache = new HashMap>(); - var creator = forType(cachedCreators, recursionCache, clazz, creatorsMap, parameterizedCreatorsMap, flexibleCreatorsList); - return (Structure) creator.create(); + return (Structure) forType(cachedCreators, recursionCache, clazz, creatorsMap, parameterizedCreatorsMap, flexibleCreatorsList); } } @@ -123,13 +131,12 @@ static Structure create(Class clazz) { return Instance.builder().build().create(clazz); } - private static Creator forType(Map cachedCreators, Map> recursionCache, Type type, Map, Creator> creatorsMap, Map, ParameterizedCreator> parameterizedCreatorsMap, List flexibleCreators) { + private static Structure forType(Map> cachedCreators, Map> recursionCache, Type type, Map, Creator> creatorsMap, Map, ParameterizedCreator> parameterizedCreatorsMap, List flexibleCreators) { if (cachedCreators.containsKey(type)) { return cachedCreators.get(type); } if (recursionCache.containsKey(type)) { - var value = recursionCache.get(type); - return () -> value; + return recursionCache.get(type); } @SuppressWarnings({"rawtypes", "unchecked"}) Supplier> full = Suppliers.memoize(() -> Structure.recursive((Function) (Function) (Structure itself) -> { recursionCache.put(type, itself); @@ -144,12 +151,12 @@ private static Creator forType(Map cachedCreators, Map create() { - return creator.create(); + return structure; } @Override @@ -185,7 +192,7 @@ public Class rawType() { for (var flexibleCreator : flexibleCreators) { if (flexibleCreator.supports(Objects.requireNonNull(rawType), parameterCreators)) { - return flexibleCreator.creator(rawType, parameterCreators, type1 -> forType(cachedCreators, recursionCache, type1, creatorsMap, parameterizedCreatorsMap, flexibleCreators).create()); + return flexibleCreator.creator(rawType, parameterCreators, type1 -> forType(cachedCreators, recursionCache, type1, creatorsMap, parameterizedCreatorsMap, flexibleCreators)); } } throw new IllegalArgumentException("No creator found for type: " + type); @@ -195,7 +202,7 @@ public Class rawType() { recursionCache.remove(type); return structure; })); - cachedCreators.put(type, full::get); - return full::get; + cachedCreators.put(type, full.get()); + return full.get(); } } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/SerializedProperty.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/SerializedProperty.java index 2ac8553..851da77 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/SerializedProperty.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/SerializedProperty.java @@ -3,6 +3,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +// TODO: targets @Retention(RetentionPolicy.RUNTIME) public @interface SerializedProperty { String property(); diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/SimpleCreatorOption.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/SimpleCreatorOption.java new file mode 100644 index 0000000..b0acd39 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/SimpleCreatorOption.java @@ -0,0 +1,5 @@ +package dev.lukebemish.codecextras.structured.reflective; + +public enum SimpleCreatorOption implements CreationOption { + NOT_NULL_BY_DEFAULT +} diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftReflectiveStructureCreator.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftReflectiveStructureCreator.java index 8459e3d..4acb373 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftReflectiveStructureCreator.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftReflectiveStructureCreator.java @@ -5,6 +5,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import dev.lukebemish.codecextras.structured.Structure; +import dev.lukebemish.codecextras.structured.reflective.CreationOptions; import dev.lukebemish.codecextras.structured.reflective.ReflectiveStructureCreator; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Type; @@ -21,7 +22,7 @@ @AutoService(ReflectiveStructureCreator.class) public class MinecraftReflectiveStructureCreator implements ReflectiveStructureCreator { @Override - public Map, Creator> creators() { + public Map, Creator> creators(CreationOptions options) { return ImmutableMap., Creator>builder() .put(ResourceLocation.class, () -> MinecraftStructures.RESOURCE_LOCATION) .put(DataComponentMap.class, () -> MinecraftStructures.DATA_COMPONENT_MAP) @@ -31,13 +32,13 @@ public Map, Creator> creators() { } @Override - public Map, ParameterizedCreator> parameterizedCreators() { + public Map, ParameterizedCreator> parameterizedCreators(CreationOptions options) { return ImmutableMap., ParameterizedCreator>builder() .build(); } @Override - public List flexibleCreators() { + public List flexibleCreators(CreationOptions options) { return ImmutableList.builder() .add(new FlexibleCreator() { @Override From 8ac5f07ec4aa74d8eb47020c1dc86bb633a3e63a Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Tue, 4 Mar 2025 15:49:46 -0600 Subject: [PATCH 15/28] Fix formatting --- .../stream/structured/StreamCodecInterpreter.java | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java b/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java index b182715..1a20b72 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java @@ -22,13 +22,6 @@ import io.netty.buffer.ByteBuf; import io.netty.handler.codec.DecoderException; import io.netty.handler.codec.EncoderException; -import net.minecraft.network.FriendlyByteBuf; -import net.minecraft.network.RegistryFriendlyByteBuf; -import net.minecraft.network.VarInt; -import net.minecraft.network.codec.ByteBufCodecs; -import net.minecraft.network.codec.StreamCodec; -import org.jspecify.annotations.Nullable; - import java.util.ArrayList; import java.util.HashMap; import java.util.IdentityHashMap; @@ -40,6 +33,12 @@ import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Stream; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.VarInt; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import org.jspecify.annotations.Nullable; /** * Interprets a {@link Structure} into a {@link StreamCodec} for the same type. From 4f7b02cf770ebeb6d33224d2644408c132dbe910 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Tue, 4 Mar 2025 19:39:43 -0600 Subject: [PATCH 16/28] Update benchmarks --- .../codecextras/jmh/LargeRecordsDecode.java | 16 +++++++++--- .../codecextras/jmh/LargeRecordsEncode.java | 16 +++++++++--- .../codecextras/jmh/TestRecord.java | 26 +++++++++++++++++++ .../BuiltInReflectiveStructureCreator.java | 15 +++++------ 4 files changed, 59 insertions(+), 14 deletions(-) diff --git a/src/jmh/java/dev/lukebemish/codecextras/jmh/LargeRecordsDecode.java b/src/jmh/java/dev/lukebemish/codecextras/jmh/LargeRecordsDecode.java index 1d18cc8..3427703 100644 --- a/src/jmh/java/dev/lukebemish/codecextras/jmh/LargeRecordsDecode.java +++ b/src/jmh/java/dev/lukebemish/codecextras/jmh/LargeRecordsDecode.java @@ -58,6 +58,13 @@ public void reflectiveStructureCreator(Blackhole blackhole) { var result = TestRecord.RSC.decode(JsonOps.INSTANCE, json); blackhole.consume(result.result().orElseThrow()); } + + @Benchmark + public void interpretedRecordStructure(Blackhole blackhole) { + JsonElement json = TestRecord.makeData(counter++); + var result = TestRecord.STRUCT.decode(JsonOps.INSTANCE, json); + blackhole.consume(result.result().orElseThrow()); + } } @OutputTimeUnit(TimeUnit.MICROSECONDS) @@ -70,9 +77,6 @@ public static class SingleShot { @Setup public void setup() { json = TestRecord.makeData(0); - TestRecord.RCB.decode(JsonOps.INSTANCE, json); - TestRecord.KRCB.decode(JsonOps.INSTANCE, json); - TestRecord.CRCB.decode(JsonOps.INSTANCE, json); } @Benchmark @@ -104,5 +108,11 @@ public void reflectiveStructureCreator(Blackhole blackhole) { var result = TestRecord.RSC.decode(JsonOps.INSTANCE, json); blackhole.consume(result.result().orElseThrow()); } + + @Benchmark + public void interpretedRecordStructure(Blackhole blackhole) { + var result = TestRecord.STRUCT.decode(JsonOps.INSTANCE, json); + blackhole.consume(result.result().orElseThrow()); + } } } diff --git a/src/jmh/java/dev/lukebemish/codecextras/jmh/LargeRecordsEncode.java b/src/jmh/java/dev/lukebemish/codecextras/jmh/LargeRecordsEncode.java index 2c60abd..76aa0d9 100644 --- a/src/jmh/java/dev/lukebemish/codecextras/jmh/LargeRecordsEncode.java +++ b/src/jmh/java/dev/lukebemish/codecextras/jmh/LargeRecordsEncode.java @@ -57,6 +57,13 @@ public void reflectiveStructureCreator(Blackhole blackhole) { var result = TestRecord.RSC.encodeStart(JsonOps.INSTANCE, record); blackhole.consume(result.result().orElseThrow()); } + + @Benchmark + public void interpretedRecordStructure(Blackhole blackhole) { + TestRecord record = TestRecord.makeRecord(counter++); + var result = TestRecord.STRUCT.encodeStart(JsonOps.INSTANCE, record); + blackhole.consume(result.result().orElseThrow()); + } } @OutputTimeUnit(TimeUnit.MICROSECONDS) @@ -69,9 +76,6 @@ public static class SingleShot { @Setup public void setup() { record = TestRecord.makeRecord(0); - TestRecord.RCB.encodeStart(JsonOps.INSTANCE, record); - TestRecord.KRCB.encodeStart(JsonOps.INSTANCE, record); - TestRecord.CRCB.encodeStart(JsonOps.INSTANCE, record); } @Benchmark @@ -103,5 +107,11 @@ public void reflectiveStructureCreator(Blackhole blackhole) { var result = TestRecord.RSC.encodeStart(JsonOps.INSTANCE, record); blackhole.consume(result.result().orElseThrow()); } + + @Benchmark + public void interpretedRecordStructure(Blackhole blackhole) { + var result = TestRecord.STRUCT.encodeStart(JsonOps.INSTANCE, record); + blackhole.consume(result.result().orElseThrow()); + } } } diff --git a/src/jmh/java/dev/lukebemish/codecextras/jmh/TestRecord.java b/src/jmh/java/dev/lukebemish/codecextras/jmh/TestRecord.java index 2ab4d95..409fa56 100644 --- a/src/jmh/java/dev/lukebemish/codecextras/jmh/TestRecord.java +++ b/src/jmh/java/dev/lukebemish/codecextras/jmh/TestRecord.java @@ -7,6 +7,7 @@ import dev.lukebemish.codecextras.record.KeyedRecordCodecBuilder; import dev.lukebemish.codecextras.record.MethodHandleRecordCodecBuilder; import dev.lukebemish.codecextras.structured.CodecInterpreter; +import dev.lukebemish.codecextras.structured.Structure; import dev.lukebemish.codecextras.structured.reflective.ReflectiveStructureCreator; import java.lang.invoke.MethodHandles; @@ -105,6 +106,31 @@ public record TestRecord( public static final Codec RSC = CodecInterpreter.create().interpret(ReflectiveStructureCreator.create(TestRecord.class)).getOrThrow(); + public static final Codec STRUCT = CodecInterpreter.create().interpret(Structure.record(builder -> { + var a = builder.add("a", Structure.INT, TestRecord::a); + var b = builder.add("b", Structure.INT, TestRecord::b); + var c = builder.add("c", Structure.INT, TestRecord::c); + var d = builder.add("d", Structure.INT, TestRecord::d); + var e = builder.add("e", Structure.INT, TestRecord::e); + var f = builder.add("f", Structure.INT, TestRecord::f); + var g = builder.add("g", Structure.INT, TestRecord::g); + var h = builder.add("h", Structure.INT, TestRecord::h); + var i = builder.add("i", Structure.INT, TestRecord::i); + var j = builder.add("j", Structure.INT, TestRecord::j); + var k = builder.add("k", Structure.INT, TestRecord::k); + var l = builder.add("l", Structure.INT, TestRecord::l); + var m = builder.add("m", Structure.INT, TestRecord::m); + var n = builder.add("n", Structure.INT, TestRecord::n); + var o = builder.add("o", Structure.INT, TestRecord::o); + var p = builder.add("p", Structure.INT, TestRecord::p); + return container -> new TestRecord( + a.apply(container), b.apply(container), c.apply(container), d.apply(container), + e.apply(container), f.apply(container), g.apply(container), h.apply(container), + i.apply(container), j.apply(container), k.apply(container), l.apply(container), + m.apply(container), n.apply(container), o.apply(container), p.apply(container) + ); + })).getOrThrow(); + public static TestRecord makeRecord(int i) { return new TestRecord( i, i + 1, i + 2, i + 3, i + 4, i + 5, i + 6, i + 7, i + 8, i + 9, i + 10, i + 11, i + 12, i + 13, i + 14, i + 15 diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/BuiltInReflectiveStructureCreator.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/BuiltInReflectiveStructureCreator.java index 94b3432..f222cc6 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/BuiltInReflectiveStructureCreator.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/BuiltInReflectiveStructureCreator.java @@ -717,32 +717,31 @@ public Structure create(Class exact, TypedCreator[] parameters, Function", ctorDescriptor, false); - mv.visitVarInsn(Opcodes.ASTORE, 3); + mv.visitVarInsn(Opcodes.ASTORE, 2); for (var property : propertyList) { if (!ctorSetters.containsKey(property)) { - mv.visitVarInsn(Opcodes.ALOAD, 3); + // Load the object, then load the value, then call the setter + mv.visitVarInsn(Opcodes.ALOAD, 2); var keyOffset = offsetMap.get(property); mv.visitLdcInsn(conDyn(org.objectweb.asm.Type.getDescriptor(Function.class), keyOffset)); - mv.visitVarInsn(Opcodes.ALOAD, 2); + mv.visitVarInsn(Opcodes.ALOAD, 1); mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, org.objectweb.asm.Type.getInternalName(Function.class), "apply", MethodType.methodType(Object.class, Object.class).descriptorString(), true); invokeAsCallSite(mv, "(Ljava/lang/Object;Ljava/lang/Object;)V", offsetMap.get(property)+1); } } - mv.visitVarInsn(Opcodes.ALOAD, 3); + mv.visitVarInsn(Opcodes.ALOAD, 2); mv.visitInsn(Opcodes.ARETURN); mv.visitMaxs(0, 0); mv.visitEnd(); From 4571b65b7906fffe57021f6f74c33ae1d62b1639 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Tue, 4 Mar 2025 21:38:02 -0600 Subject: [PATCH 17/28] Transient properties and annotation serialization --- .../BuiltInReflectiveStructureCreator.java | 333 ++++++++++++++---- .../reflective/SerializedProperty.java | 10 - .../annotations/SerializedProperty.java | 12 + .../reflective/annotations/Transient.java | 11 + src/main/java/module-info.java | 1 + .../structured/reflective/TestReflective.java | 230 +++++++++++- 6 files changed, 525 insertions(+), 72 deletions(-) delete mode 100644 src/main/java/dev/lukebemish/codecextras/structured/reflective/SerializedProperty.java create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/SerializedProperty.java create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Transient.java diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/BuiltInReflectiveStructureCreator.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/BuiltInReflectiveStructureCreator.java index f222cc6..31bc644 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/BuiltInReflectiveStructureCreator.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/BuiltInReflectiveStructureCreator.java @@ -9,6 +9,8 @@ import com.mojang.serialization.Dynamic; import dev.lukebemish.codecextras.structured.RecordStructure; import dev.lukebemish.codecextras.structured.Structure; +import dev.lukebemish.codecextras.structured.reflective.annotations.SerializedProperty; +import dev.lukebemish.codecextras.structured.reflective.annotations.Transient; import java.lang.invoke.CallSite; import java.lang.invoke.ConstantCallSite; import java.lang.invoke.MethodHandle; @@ -362,6 +364,185 @@ private static CallSite asCallSite(MethodHandles.Lookup ignoredLookup, String ig return key; } + private Class implementAnnotation(Class annotationType) { + var cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); + var name = BuiltInReflectiveStructureCreator.class.getName().replace('.', '/') + "$AnnotationProxy"; + cw.visit(Opcodes.V21, Opcodes.ACC_FINAL, name, null, "java/lang/Object", new String[]{annotationType.getName().replace('.', '/')}); + var entryTypes = new org.objectweb.asm.Type[annotationType.getDeclaredMethods().length]; + var entryClasses = new Class[annotationType.getDeclaredMethods().length]; + var entryNames = new String[annotationType.getDeclaredMethods().length]; + int index = 0; + for (var method : annotationType.getDeclaredMethods()) { + var descriptor = org.objectweb.asm.Type.getMethodDescriptor(method); + entryTypes[index] = org.objectweb.asm.Type.getReturnType(descriptor); + entryClasses[index] = method.getReturnType(); + entryNames[index] = method.getName(); + index++; + } + + for (int i = 0; i < entryTypes.length; i++) { + cw.visitField(Opcodes.ACC_PRIVATE | Opcodes.ACC_FINAL, entryNames[i], entryTypes[i].getDescriptor(), null, null); + } + + var ctorDescriptor = org.objectweb.asm.Type.getMethodDescriptor(org.objectweb.asm.Type.VOID_TYPE, entryTypes); + var mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "", ctorDescriptor, null, null); + mv.visitAnnotableParameterCount(entryTypes.length, true); + for (int i = 0; i < entryTypes.length; i++) { + var av = mv.visitParameterAnnotation(i, SerializedProperty.class.descriptorString(), true); + av.visit("value", entryNames[i]); + av.visitEnd(); + } + mv.visitCode(); + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "", "()V", false); + int j = 1; + for (int i = 0; i < entryTypes.length; i++) { + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitVarInsn(entryTypes[i].getOpcode(Opcodes.ILOAD), j); + mv.visitFieldInsn(Opcodes.PUTFIELD, name, entryNames[i], entryTypes[i].getDescriptor()); + j += entryTypes[i].getSize(); + } + mv.visitInsn(Opcodes.RETURN); + mv.visitMaxs(0, 0); + mv.visitEnd(); + + // annotationType + mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "annotationType", "()Ljava/lang/Class;", null, null); + mv.visitCode(); + mv.visitLdcInsn(org.objectweb.asm.Type.getType(annotationType)); + mv.visitInsn(Opcodes.ARETURN); + mv.visitMaxs(0, 0); + mv.visitEnd(); + + for (int i = 0; i < entryTypes.length; i++) { + mv = cw.visitMethod(Opcodes.ACC_PUBLIC, entryNames[i], org.objectweb.asm.Type.getMethodDescriptor(entryTypes[i]), null, null); + mv.visitCode(); + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitFieldInsn(Opcodes.GETFIELD, name, entryNames[i], entryTypes[i].getDescriptor()); + mv.visitInsn(entryTypes[i].getOpcode(Opcodes.IRETURN)); + mv.visitMaxs(0, 0); + mv.visitEnd(); + } + + // equals + mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "equals", "(Ljava/lang/Object;)Z", null, null); + mv.visitCode(); + mv.visitVarInsn(Opcodes.ALOAD, 1); + mv.visitTypeInsn(Opcodes.INSTANCEOF, annotationType.getName().replace('.', '/')); + var label = new org.objectweb.asm.Label(); + mv.visitJumpInsn(Opcodes.IFEQ, label); + + for (int i = 0; i < entryTypes.length; i++) { + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitFieldInsn(Opcodes.GETFIELD, name, entryNames[i], entryTypes[i].getDescriptor()); + mv.visitVarInsn(Opcodes.ALOAD, 1); + mv.visitTypeInsn(Opcodes.CHECKCAST, annotationType.getName().replace('.', '/')); + mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, annotationType.getName().replace('.', '/'), entryNames[i], org.objectweb.asm.Type.getMethodDescriptor(entryTypes[i]), true); + var clazz = entryClasses[i]; + if (clazz.isPrimitive()) { + switch (clazz.getName()) { + case "double" -> { + mv.visitMethodInsn(Opcodes.INVOKESTATIC, BuiltInReflectiveStructureCreator.class.getName().replace('.', '/'), "doubleEquals", "(DD)Z", false); + mv.visitJumpInsn(Opcodes.IFEQ, label); + } + case "float" -> { + mv.visitMethodInsn(Opcodes.INVOKESTATIC, BuiltInReflectiveStructureCreator.class.getName().replace('.', '/'), "floatEquals", "(FF)Z", false); + mv.visitJumpInsn(Opcodes.IFEQ, label); + } + case "long" -> { + mv.visitInsn(Opcodes.LCMP); + mv.visitJumpInsn(Opcodes.IFNE, label); + } + default -> mv.visitJumpInsn(Opcodes.IF_ICMPNE, label); + } + } else if (clazz.isArray()) { + String arrayDescString; + if (clazz.componentType().isPrimitive()) { + arrayDescString = clazz.descriptorString(); + } else { + arrayDescString = Object[].class.descriptorString(); + } + mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/util/Arrays", "equals", "("+arrayDescString+arrayDescString+")Z", false); + mv.visitJumpInsn(Opcodes.IFEQ, label); + } else { + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Object", "equals", "(Ljava/lang/Object;)Z", false); + mv.visitJumpInsn(Opcodes.IFEQ, label); + } + } + + mv.visitInsn(Opcodes.ICONST_1); + mv.visitInsn(Opcodes.IRETURN); + + mv.visitLabel(label); + mv.visitInsn(Opcodes.ICONST_0); + mv.visitInsn(Opcodes.IRETURN); + mv.visitMaxs(0, 0); + mv.visitEnd(); + + mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "hashCode", "()I", null, null); + mv.visitCode(); + mv.visitInsn(Opcodes.ICONST_0); + for (int i = 0; i < entryTypes.length; i++) { + mv.visitLdcInsn(entryNames[i]); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Object", "hashCode", "()I", false); + mv.visitLdcInsn(127); + mv.visitInsn(Opcodes.IMUL); + + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitFieldInsn(Opcodes.GETFIELD, name, entryNames[i], entryTypes[i].getDescriptor()); + var clazz = entryClasses[i]; + if (clazz.isPrimitive()) { + var wrapper = switch (clazz.getName()) { + case "int" -> Integer.class; + case "long" -> Long.class; + case "short" -> Short.class; + case "byte" -> Byte.class; + case "char" -> Character.class; + case "float" -> Float.class; + case "double" -> Double.class; + case "boolean" -> Boolean.class; + default -> throw new IllegalStateException("Unexpected value: " + clazz.getName()); + }; + mv.visitMethodInsn(Opcodes.INVOKESTATIC, wrapper.getName().replace('.', '/'), "valueOf", "("+clazz.descriptorString()+")L"+wrapper.getName().replace('.', '/')+";", false); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Object", "hashCode", "()I", false); + } else if (clazz.isArray()) { + String arrayDescString; + if (clazz.componentType().isPrimitive()) { + arrayDescString = clazz.descriptorString(); + } else { + arrayDescString = Object[].class.descriptorString(); + } + mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/util/Arrays", "hashCode", "("+arrayDescString+")I", false); + } else { + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Object", "hashCode", "()I", false); + } + mv.visitInsn(Opcodes.IXOR); + + mv.visitInsn(Opcodes.IADD); + } + mv.visitInsn(Opcodes.IRETURN); + mv.visitMaxs(0, 0); + mv.visitEnd(); + + cw.visitEnd(); + + var bytes = cw.toByteArray(); + try { + var lookup = MethodHandles.lookup().defineHiddenClassWithClassData(bytes, List.of(annotationType), true, MethodHandles.Lookup.ClassOption.NESTMATE); + return lookup.lookupClass(); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + private static boolean doubleEquals(double a, double b) { + return Double.valueOf(a).equals(b); + } + + private static boolean floatEquals(float a, float b) { + return Float.valueOf(a).equals(b); + } + @Override public List flexibleCreators(CreationOptions options) { return ImmutableList.builder() @@ -496,14 +677,14 @@ private List> validCtors(Class exact) { } var annotation = param.getAnnotation(SerializedProperty.class); try { - var field = exact.getField(annotation.property()); + var field = exact.getField(annotation.value()); if (field.getType().equals(param.getType()) && field.accessFlags().contains(AccessFlag.PUBLIC) && !field.accessFlags().contains(AccessFlag.STATIC)) { continue; } } catch (NoSuchFieldException ignored) {} try { - var getterMethod = exact.getMethod("get" + annotation.property().substring(0, 1).toUpperCase() + annotation.property().substring(1)); + var getterMethod = exact.getMethod("get" + annotation.value().substring(0, 1).toUpperCase() + annotation.value().substring(1)); if (getterMethod.getGenericReturnType().equals(param.getParameterizedType()) && getterMethod.accessFlags().contains(AccessFlag.PUBLIC) && !getterMethod.accessFlags().contains(AccessFlag.STATIC)) { continue; } @@ -533,12 +714,22 @@ public Structure create(Class exact, TypedCreator[] parameters, Function> getters = new HashMap<>(); Map setters = new HashMap<>(); @@ -546,25 +737,67 @@ public Structure create(Class exact, TypedCreator[] parameters, Function types = new HashMap<>(); Map> context = new HashMap<>(); - if (!exact.isRecord()) { + if (exact.isRecord()) { + for (int i = 0; i < exact.getRecordComponents().length; i++) { + var component = exact.getRecordComponents()[i]; + + var thisContext = context.computeIfAbsent(component.getName(), k -> new ArrayList<>()); + thisContext.add(component); + + ctorSetters.put(component.getName(), i); + ctorSettersArray[i] = component.getName(); + types.put(component.getName(), component.getGenericType()); + + try { + var getterMethod = exact.getMethod(component.getName()); + var getter = functionWrapper(MethodHandles.lookup().unreflect(getterMethod)); + thisContext.add(getterMethod); + getters.put(component.getName(), getter); + } catch (NoSuchMethodException ignored) { + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + } else if (exact.isAnnotation()) { + for (int i = 0; i < validCtor.getParameterCount(); i++) { + var parameter = validCtor.getParameters()[i]; + var annotation = parameter.getAnnotation(SerializedProperty.class); + + var thisContext = context.computeIfAbsent(annotation.value(), k -> new ArrayList<>()); + thisContext.add(parameter); + + ctorSetters.put(annotation.value(), i); + ctorSettersArray[i] = annotation.value(); + types.put(annotation.value(), parameter.getParameterizedType()); + + try { + var getterMethod = exact.getMethod(annotation.value()); + var getter = functionWrapper(MethodHandles.lookup().unreflect(getterMethod)); + getters.put(annotation.value(), getter); + thisContext.add(getterMethod); + } catch (NoSuchMethodException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + } else { for (int i = 0; i < validCtor.getParameterCount(); i++) { var param = validCtor.getParameters()[i]; var annotation = param.getAnnotation(SerializedProperty.class); - var thisContext = context.computeIfAbsent(annotation.property(), k -> new ArrayList<>()); + var thisContext = context.computeIfAbsent(annotation.value(), k -> new ArrayList<>()); thisContext.add(param); - ctorSetters.put(annotation.property(), i); - ctorSettersArray[i] = annotation.property(); - types.put(annotation.property(), param.getParameterizedType()); + ctorSetters.put(annotation.value(), i); + ctorSettersArray[i] = annotation.value(); + types.put(annotation.value(), param.getParameterizedType()); // Prefer the bean getter method, then the field try { - var getterMethod = exact.getMethod("get" + annotation.property().substring(0, 1).toUpperCase() + annotation.property().substring(1)); + var getterMethod = exact.getMethod("get" + annotation.value().substring(0, 1).toUpperCase() + annotation.value().substring(1)); if (getterMethod.getGenericReturnType().equals(param.getParameterizedType()) && getterMethod.accessFlags().contains(AccessFlag.PUBLIC) && !getterMethod.accessFlags().contains(AccessFlag.STATIC)) { var getter = functionWrapper(MethodHandles.lookup().unreflect(getterMethod)); - getters.put(annotation.property(), getter); + getters.put(annotation.value(), getter); thisContext.add(getterMethod); continue; } @@ -574,10 +807,10 @@ public Structure create(Class exact, TypedCreator[] parameters, Function create(Class exact, TypedCreator[] parameters, Function 3) || + (method.getName().startsWith("is") && method.getName().length() > 2 && method.getGenericReturnType().equals(Boolean.TYPE)) ); - var isSetter = method.getParameterCount() == 1 && method.getName().startsWith("set") && method.getGenericReturnType().equals(Void.TYPE); + var isSetter = method.getParameterCount() == 1 && method.getName().startsWith("set") && method.getName().length() > 3 && method.getGenericReturnType().equals(Void.TYPE); if (isGetter) { var property = method.getName().substring(method.getName().startsWith("is") ? 2 : 3); property = property.substring(0, 1).toLowerCase() + property.substring(1); - if (!types.containsKey(property) && method.getGenericReturnType().equals(types.get(property))) { + if (!types.containsKey(property) || method.getGenericReturnType().equals(types.get(property))) { types.put(property, method.getGenericReturnType()); if (!getters.containsKey(property)) { try { @@ -608,11 +844,11 @@ public Structure create(Class exact, TypedCreator[] parameters, Function new ArrayList<>()).add(method); } - } else if (isSetter) { + } if (isSetter) { var property = method.getName().substring(3); property = property.substring(0, 1).toLowerCase() + property.substring(1); - if (!types.containsKey(property) && method.getParameterTypes()[0].equals(types.get(property))) { - types.put(property, method.getGenericReturnType()); + if (!types.containsKey(property) || method.getParameterTypes()[0].equals(types.get(property))) { + types.put(property, method.getGenericParameterTypes()[0]); if (!setters.containsKey(property)) { try { var setter = MethodHandles.lookup().unreflect(method); @@ -647,36 +883,18 @@ public Structure create(Class exact, TypedCreator[] parameters, Function new ArrayList<>()); - thisContext.add(component); - - ctorSetters.put(component.getName(), i); - ctorSettersArray[i] = component.getName(); - types.put(component.getName(), component.getGenericType()); - - try { - var getterMethod = exact.getMethod(component.getName()); - var getter = functionWrapper(MethodHandles.lookup().unreflect(getterMethod)); - thisContext.add(getterMethod); - getters.put(component.getName(), getter); - } catch (NoSuchMethodException ignored) { - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } - } } var properties = new LinkedHashSet(); for (var entry : types.keySet()) { if (getters.containsKey(entry) && (setters.containsKey(entry) || ctorSetters.containsKey(entry))) { properties.add(entry); + } else if (ctorSetters.containsKey(entry) && !getters.containsKey(entry)) { + throw new IllegalStateException("Property " + entry + " of class " + exact + " is a constructor argument but has no getter"); } } var propertyList = new ArrayList<>(properties); + var keyList = new ArrayList>(propertyList.size()); for (var property : propertyList) { var getter = getters.get(property); @@ -696,29 +914,21 @@ public Structure create(Class exact, TypedCreator[] parameters, Function(); Map offsetMap = new HashMap<>(); - String ctorDescriptor; - try { - var ctorHandle = MethodHandles.lookup().unreflectConstructor(validCtor); - classData.add(ctorHandle); - ctorDescriptor = ctorHandle.type().changeReturnType(Void.TYPE).descriptorString(); - var j = 1; - for (int i = 0; i < propertyList.size(); i++) { - var key = keyList.get(i); - var property = propertyList.get(i); - classData.add(key); - offsetMap.put(property, j); + classData.add(validCtorHandle); + var j = 1; + for (int i = 0; i < propertyList.size(); i++) { + var key = keyList.get(i); + var property = propertyList.get(i); + classData.add(key); + offsetMap.put(property, j); + j++; + if (!ctorSetters.containsKey(property)) { + var setter = setters.get(property).asType(MethodType.methodType(Void.TYPE, Object.class, Object.class)); + classData.add(setter); j++; - if (!ctorSetters.containsKey(property)) { - var setter = setters.get(property).asType(MethodType.methodType(Void.TYPE, Object.class, Object.class)); - classData.add(setter); - j++; - } } - } catch (IllegalAccessException e) { - throw new RuntimeException(e); } - mv.visitTypeInsn(Opcodes.NEW, exact.getName().replace('.', '/')); - mv.visitInsn(Opcodes.DUP); + for (int i = 0; i < ctorSettersArray.length; i++) { // Load ctor args var property = ctorSettersArray[i]; @@ -728,7 +938,7 @@ public Structure create(Class exact, TypedCreator[] parameters, Function", ctorDescriptor, false); + invokeAsCallSite(mv, validCtorHandle.type().descriptorString(), 0); mv.visitVarInsn(Opcodes.ASTORE, 2); for (var property : propertyList) { if (!ctorSetters.containsKey(property)) { @@ -747,6 +957,7 @@ public Structure create(Class exact, TypedCreator[] parameters, Function) lookup.lookupClass().getConstructor().newInstance(); @@ -773,7 +984,7 @@ public boolean supports(Class exact, TypedCreator[] parameters) { // - constructing the object, with any properties that are present in that fashion // - setting any properties that have setters present // Properties are included if they have both a getter (method or public field) and setter (ctor argument, method, or public non-final field) present. - if (exact.isRecord()) { + if (exact.isRecord() || exact.isAnnotation()) { return true; } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/SerializedProperty.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/SerializedProperty.java deleted file mode 100644 index 851da77..0000000 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/SerializedProperty.java +++ /dev/null @@ -1,10 +0,0 @@ -package dev.lukebemish.codecextras.structured.reflective; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -// TODO: targets -@Retention(RetentionPolicy.RUNTIME) -public @interface SerializedProperty { - String property(); -} diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/SerializedProperty.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/SerializedProperty.java new file mode 100644 index 0000000..233e974 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/SerializedProperty.java @@ -0,0 +1,12 @@ +package dev.lukebemish.codecextras.structured.reflective.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface SerializedProperty { + String value(); +} diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Transient.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Transient.java new file mode 100644 index 0000000..f03d3ad --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Transient.java @@ -0,0 +1,11 @@ +package dev.lukebemish.codecextras.structured.reflective.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Transient { +} diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index e7bc75c..6106bd4 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -32,6 +32,7 @@ exports dev.lukebemish.codecextras.structured; exports dev.lukebemish.codecextras.structured.reflective; + exports dev.lukebemish.codecextras.structured.reflective.annotations; exports dev.lukebemish.codecextras.structured.schema; exports dev.lukebemish.codecextras.types; diff --git a/src/test/java/dev/lukebemish/codecextras/test/structured/reflective/TestReflective.java b/src/test/java/dev/lukebemish/codecextras/test/structured/reflective/TestReflective.java index 25b4bd6..46282b0 100644 --- a/src/test/java/dev/lukebemish/codecextras/test/structured/reflective/TestReflective.java +++ b/src/test/java/dev/lukebemish/codecextras/test/structured/reflective/TestReflective.java @@ -5,9 +5,14 @@ import dev.lukebemish.codecextras.structured.CodecInterpreter; import dev.lukebemish.codecextras.structured.Structure; import dev.lukebemish.codecextras.structured.reflective.ReflectiveStructureCreator; +import dev.lukebemish.codecextras.structured.reflective.annotations.SerializedProperty; +import dev.lukebemish.codecextras.structured.reflective.annotations.Transient; import dev.lukebemish.codecextras.test.CodecAssertions; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.LinkedHashSet; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.OptionalInt; import java.util.SequencedSet; @@ -15,7 +20,7 @@ import org.junit.jupiter.api.Test; public class TestReflective { - public record TestRecord(int a, String b, TestEnum c, OptionalInt d, Optional e, @Nullable String f, SequencedSet g) { + public record TestRecord(long a, String b, TestEnum c, OptionalInt d, Optional e, @Nullable String f, SequencedSet g) { private static final Structure STRUCTURE = ReflectiveStructureCreator.create(TestRecord.class); } @@ -25,6 +30,120 @@ public enum TestEnum { C } + public static class TestNoArgCtor { + public int a; + private @Nullable String b; + + public @Nullable String getB() { + return this.b; + } + + public void setB(String b) { + this.b = b; + } + + @Override + public boolean equals(Object object) { + if (this == object) return true; + if (!(object instanceof TestNoArgCtor that)) return false; + return a == that.a && Objects.equals(b, that.b); + } + + @Override + public int hashCode() { + return Objects.hash(a, b); + } + + @Override + public String toString() { + return "TestNoArgCtor{" + + "a=" + a + + ", b='" + b + '\'' + + '}'; + } + } + + public static class TestCtor { + public final int a; + private final String b; + + public String getB() { + return this.b; + } + + public TestCtor( + @SerializedProperty("a") int a, + @SerializedProperty("b") String b + ) { + this.a = a; + this.b = b; + } + + @Override + public boolean equals(Object object) { + if (this == object) return true; + if (!(object instanceof TestCtor testCtor)) return false; + return a == testCtor.a && Objects.equals(b, testCtor.b); + } + + @Override + public int hashCode() { + return Objects.hash(a, b); + } + + @Override + public String toString() { + return "TestCtor{" + + "a=" + a + + ", b='" + b + '\'' + + '}'; + } + } + + public static class TestAnnotations { + public long a; + public transient boolean b; + + private boolean c; + @Transient + public boolean getC() { + return this.c; + } + public void setC(boolean c) { + this.c = c; + } + + private boolean d; + public boolean isD() { + return this.d; + } + public void setD(boolean d) { + this.d = d; + } + + @Override + public boolean equals(Object object) { + if (this == object) return true; + if (!(object instanceof TestAnnotations that)) return false; + return a == that.a && b == that.b && c == that.c && d == that.d; + } + + @Override + public int hashCode() { + return Objects.hash(a, b, c, d); + } + + @Override + public String toString() { + return "TestAnnotations{" + + "a=" + a + + ", b=" + b + + ", c=" + c + + ", d=" + d + + '}'; + } + } + public record TestRecursive(String name, List list) {} private static final Codec CODEC = CodecInterpreter.create().interpret(TestRecord.STRUCTURE).getOrThrow(); @@ -34,6 +153,11 @@ public record TestRecursive(String name, List list) {} private static final Codec RECURSIVE_CODEC = CodecInterpreter.create().interpret(ReflectiveStructureCreator.create(TestRecursive.class)).getOrThrow(); + private static final Codec CTOR_CODEC = CodecInterpreter.create().interpret(ReflectiveStructureCreator.create(TestCtor.class)).getOrThrow(); + private static final Codec NO_ARG_CTOR_CODEC = CodecInterpreter.create().interpret(ReflectiveStructureCreator.create(TestNoArgCtor.class)).getOrThrow(); + + private static final Codec ANNOTATIONS_CODEC = CodecInterpreter.create().interpret(ReflectiveStructureCreator.create(TestAnnotations.class)).getOrThrow(); + private final String json = """ { "a": 1, @@ -74,11 +198,36 @@ public record TestRecursive(String name, List list) {} ] }"""; + private final String ctorJson = """ + { + "a": 1, + "b": "test" + }"""; + + private final String annotationsJson = """ + { + "a": 1, + "d": true + }"""; + private final TestRecord object = new TestRecord(1, "test", TestEnum.A, OptionalInt.of(2), Optional.of("test"), null, new LinkedHashSet<>(List.of(1, 2, 3))); private final TestRecord[] array = new TestRecord[] { object }; private final int[] primitiveArray = new int[] { 1, 2, 3 }; private final TestRecursive recursive = new TestRecursive("test1", List.of(new TestRecursive("test2", List.of()), new TestRecursive("test3", List.of()))); + private final TestNoArgCtor noArgCtor = new TestNoArgCtor(); + { + noArgCtor.a = 1; + noArgCtor.setB("test"); + } + private final TestCtor ctor = new TestCtor(1, "test"); + + private final TestAnnotations annotations = new TestAnnotations(); + { + annotations.a = 1; + annotations.setD(true); + } + @Test void testDecoding() { CodecAssertions.assertDecodes(JsonOps.INSTANCE, json, object, CODEC); @@ -118,4 +267,83 @@ void testDecodingRecursive() { void testEncodingRecursive() { CodecAssertions.assertEncodes(JsonOps.INSTANCE, recursive, recursiveJson, RECURSIVE_CODEC); } + + @Test + void testDecodingCtor() { + CodecAssertions.assertDecodes(JsonOps.INSTANCE, ctorJson, ctor, CTOR_CODEC); + } + + @Test + void testEncodingCtor() { + CodecAssertions.assertEncodes(JsonOps.INSTANCE, ctor, ctorJson, CTOR_CODEC); + } + + @Test + void testDecodingNoArgCtor() { + CodecAssertions.assertDecodes(JsonOps.INSTANCE, ctorJson, noArgCtor, NO_ARG_CTOR_CODEC); + } + + @Test + void testEncodingNoArgCtor() { + CodecAssertions.assertEncodes(JsonOps.INSTANCE, noArgCtor, ctorJson, NO_ARG_CTOR_CODEC); + } + + @Test + void testDecodingAnnotations() { + CodecAssertions.assertDecodes(JsonOps.INSTANCE, annotationsJson, annotations, ANNOTATIONS_CODEC); + } + + @Test + void testEncodingAnnotations() { + CodecAssertions.assertEncodes(JsonOps.INSTANCE, annotations, annotationsJson, ANNOTATIONS_CODEC); + } + + @Retention(RetentionPolicy.RUNTIME) + public @interface TestAnnotation { + String a(); + long b(); + String[] c(); + long[] d(); + Nested e(); + Nested[] f(); + + @interface Nested { + long a(); + } + } + private final String testAnnotationJson = """ + { + "a": "test", + "b": 1, + "c": ["test1", "test2"], + "d": [1, 2], + "e": {"a": 1}, + "f": [{"a": 1}, {"a": 2}] + }"""; + private final TestAnnotation testAnnotation = new Object() { + @TestAnnotation( + a = "test", + b = 1, + c = {"test1", "test2"}, + d = {1, 2}, + e = @TestAnnotation.Nested(a = 1), + f = {@TestAnnotation.Nested(a = 1), @TestAnnotation.Nested(a = 2)} + ) + static class Source { + + } + + final TestAnnotation annotation = Objects.requireNonNull(Source.class.getAnnotation(TestAnnotation.class)); + }.annotation; + private static final Codec TEST_ANNOTATION_CODEC = CodecInterpreter.create().interpret(ReflectiveStructureCreator.create(TestAnnotation.class)).getOrThrow(); + + @Test + void testDecodingTestAnnotation() { + CodecAssertions.assertDecodes(JsonOps.INSTANCE, testAnnotationJson, testAnnotation, TEST_ANNOTATION_CODEC); + } + + @Test + void testEncodingTestAnnotation() { + CodecAssertions.assertEncodes(JsonOps.INSTANCE, testAnnotation, testAnnotationJson, TEST_ANNOTATION_CODEC); + } } From 4eada0d22a99293b4e05db86e2c89e07921d6058 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Wed, 5 Mar 2025 22:20:10 -0600 Subject: [PATCH 18/28] Fix comment ops and recover annotation values in reflective structures --- .../codecextras/comments/CommentMapCodec.java | 129 +++++++------- .../comments/CommentRecordBuilder.java | 55 ++++++ .../codecextras/companion/AccompaniedOps.java | 3 + .../compat/jankson/JanksonOps.java | 13 +- .../nightconfig/CommentedNightConfigOps.java | 7 + .../compat/nightconfig/NightConfigOps.java | 35 ++-- .../codecextras/structured/Annotation.java | 3 +- .../structured/RecordStructure.java | 24 ++- .../BuiltInReflectiveStructureCreator.java | 160 +++++++++++++++++- .../reflective/CreationOptions.java | 17 +- .../ReflectiveStructureCreator.java | 68 ++++++-- .../reflective/annotations/Annotated.java | 22 +++ .../reflective/annotations/Value.java | 15 ++ .../codecextras/test/CodecAssertions.java | 7 + .../test/comments/TestComments.java | 48 ++++++ .../test/comments/package-info.java | 4 + .../structured/reflective/TestReflective.java | 107 ------------ .../TestReflectiveConstructors.java | 119 +++++++++++++ .../TestReflectiveStructureAnnotations.java | 88 ++++++++++ 19 files changed, 713 insertions(+), 211 deletions(-) create mode 100644 src/main/java/dev/lukebemish/codecextras/comments/CommentRecordBuilder.java create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Annotated.java create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Value.java create mode 100644 src/test/java/dev/lukebemish/codecextras/test/comments/TestComments.java create mode 100644 src/test/java/dev/lukebemish/codecextras/test/comments/package-info.java create mode 100644 src/test/java/dev/lukebemish/codecextras/test/structured/reflective/TestReflectiveConstructors.java create mode 100644 src/test/java/dev/lukebemish/codecextras/test/structured/reflective/TestReflectiveStructureAnnotations.java diff --git a/src/main/java/dev/lukebemish/codecextras/comments/CommentMapCodec.java b/src/main/java/dev/lukebemish/codecextras/comments/CommentMapCodec.java index cc24d03..69df78c 100644 --- a/src/main/java/dev/lukebemish/codecextras/comments/CommentMapCodec.java +++ b/src/main/java/dev/lukebemish/codecextras/comments/CommentMapCodec.java @@ -47,67 +47,76 @@ public DataResult decode(DynamicOps ops, MapLike input) { @Override public RecordBuilder encode(A input, DynamicOps ops, RecordBuilder prefix) { - final RecordBuilder builder = delegate.encode(input, ops, prefix); - - return new RecordBuilder<>() { - RecordBuilder mutableBuilder = builder; - - @Override - public DynamicOps ops() { - return builder.ops(); + prefix = delegate.encode(input, ops, prefix); + if (prefix instanceof CommentRecordBuilder commentRecordBuilder) { + for (Map.Entry entry : comments.entrySet()) { + prefix = commentRecordBuilder.comment(ops.createString(entry.getKey()), ops.createString(entry.getValue())); } - - @Override - public RecordBuilder add(T key, T value) { - mutableBuilder = mutableBuilder.add(key, value); - return this; - } - - @Override - public RecordBuilder add(T key, DataResult value) { - mutableBuilder = mutableBuilder.add(key, value); - return this; - } - - @Override - public RecordBuilder add(DataResult key, DataResult value) { - mutableBuilder = mutableBuilder.add(key, value); - return this; - } - - @Override - public RecordBuilder withErrorsFrom(DataResult result) { - mutableBuilder = mutableBuilder.withErrorsFrom(result); - return this; - } - - @Override - public RecordBuilder setLifecycle(Lifecycle lifecycle) { - mutableBuilder = mutableBuilder.setLifecycle(lifecycle); - return this; - } - - @Override - public RecordBuilder mapError(UnaryOperator onError) { - mutableBuilder = mutableBuilder.mapError(onError); - return this; - } - - @Override - public DataResult build(T prefix) { - DataResult built = builder.build(prefix); - return AccompaniedOps.find(this.ops()).map(accompaniedOps -> { - Optional> commentOps = accompaniedOps.getCompanion(CommentOps.TOKEN); - if (commentOps.isPresent()) { - return built.flatMap(t -> - commentOps.get().commentToMap(t, comments.entrySet().stream().collect(Collectors.toMap(e -> - ops.createString(e.getKey()), e -> ops.createString(e.getValue())))) - ); - } - return built; - }).orElse(built); - } - }; + return prefix; + } else { + // Best-attempt -- anything that fails to pass the builder downstream will cause this to fail + var builder = prefix; + return new RecordBuilder<>() { + RecordBuilder mutableBuilder = builder; + + @Override + public DynamicOps ops() { + return builder.ops(); + } + + @Override + public RecordBuilder add(T key, T value) { + mutableBuilder = mutableBuilder.add(key, value); + return this; + } + + @Override + public RecordBuilder add(T key, DataResult value) { + mutableBuilder = mutableBuilder.add(key, value); + return this; + } + + @Override + public RecordBuilder add(DataResult key, DataResult value) { + mutableBuilder = mutableBuilder.add(key, value); + return this; + } + + @Override + public RecordBuilder withErrorsFrom(DataResult result) { + mutableBuilder = mutableBuilder.withErrorsFrom(result); + return this; + } + + @Override + public RecordBuilder setLifecycle(Lifecycle lifecycle) { + mutableBuilder = mutableBuilder.setLifecycle(lifecycle); + return this; + } + + @Override + public RecordBuilder mapError(UnaryOperator onError) { + mutableBuilder = mutableBuilder.mapError(onError); + return this; + } + + @Override + public DataResult build(T prefix) { + // TODO: the RecordBuilder is now tossed out; fix this + DataResult built = builder.build(prefix); + return AccompaniedOps.find(this.ops()).map(accompaniedOps -> { + Optional> commentOps = accompaniedOps.getCompanion(CommentOps.TOKEN); + if (commentOps.isPresent()) { + return built.flatMap(t -> + commentOps.get().commentToMap(t, comments.entrySet().stream().collect(Collectors.toMap(e -> + ops.createString(e.getKey()), e -> ops.createString(e.getValue())))) + ); + } + return built; + }).orElse(built); + } + }; + } } @Override diff --git a/src/main/java/dev/lukebemish/codecextras/comments/CommentRecordBuilder.java b/src/main/java/dev/lukebemish/codecextras/comments/CommentRecordBuilder.java new file mode 100644 index 0000000..608acea --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/comments/CommentRecordBuilder.java @@ -0,0 +1,55 @@ +package dev.lukebemish.codecextras.comments; + +import com.google.common.collect.ImmutableMap; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.DynamicOps; +import com.mojang.serialization.RecordBuilder; +import dev.lukebemish.codecextras.companion.AccompaniedOps; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +public interface CommentRecordBuilder extends RecordBuilder { + CommentRecordBuilder comment(T key, T value); + + final class MapBuilder extends AbstractUniversalBuilder> implements CommentRecordBuilder { + private final ImmutableMap.Builder commentsBuilder = ImmutableMap.builder(); + + public MapBuilder(final DynamicOps ops) { + super(ops); + } + + @Override + public CommentRecordBuilder comment(T key, T value) { + commentsBuilder.put(key, value); + return this; + } + + @Override + protected ImmutableMap.Builder initBuilder() { + return ImmutableMap.builder(); + } + + @Override + protected ImmutableMap.Builder append(final T key, final T value, final ImmutableMap.Builder builder) { + return builder.put(key, value); + } + + @Override + protected DataResult build(final ImmutableMap.Builder builder, final T prefix) { + var built = ops().mergeToMap(prefix, builder.buildKeepingLast()); + var comments = commentsBuilder.build(); + return AccompaniedOps.find(this.ops()).map(accompaniedOps -> { + Optional> commentOps = accompaniedOps.getCompanion(CommentOps.TOKEN); + if (commentOps.isPresent()) { + return built.flatMap(t -> + commentOps.get().commentToMap(t, comments.entrySet().stream().collect( + Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue) + )) + ); + } + return built; + }).orElse(built); + } + } +} diff --git a/src/main/java/dev/lukebemish/codecextras/companion/AccompaniedOps.java b/src/main/java/dev/lukebemish/codecextras/companion/AccompaniedOps.java index fccffed..53e6f2e 100644 --- a/src/main/java/dev/lukebemish/codecextras/companion/AccompaniedOps.java +++ b/src/main/java/dev/lukebemish/codecextras/companion/AccompaniedOps.java @@ -15,6 +15,9 @@ static Optional> find(DynamicOps ops) { return companion; } } + if (ops instanceof AccompaniedOps accompaniedOps) { + return Optional.of(accompaniedOps); + } return Optional.empty(); } } diff --git a/src/main/java/dev/lukebemish/codecextras/compat/jankson/JanksonOps.java b/src/main/java/dev/lukebemish/codecextras/compat/jankson/JanksonOps.java index 2df68f4..36bfb90 100644 --- a/src/main/java/dev/lukebemish/codecextras/compat/jankson/JanksonOps.java +++ b/src/main/java/dev/lukebemish/codecextras/compat/jankson/JanksonOps.java @@ -1,12 +1,18 @@ package dev.lukebemish.codecextras.compat.jankson; -import blue.endless.jankson.*; +import blue.endless.jankson.JsonArray; +import blue.endless.jankson.JsonElement; +import blue.endless.jankson.JsonNull; +import blue.endless.jankson.JsonObject; +import blue.endless.jankson.JsonPrimitive; import com.google.common.collect.Lists; import com.mojang.datafixers.util.Pair; import com.mojang.serialization.DataResult; import com.mojang.serialization.DynamicOps; import com.mojang.serialization.MapLike; +import com.mojang.serialization.RecordBuilder; import dev.lukebemish.codecextras.comments.CommentOps; +import dev.lukebemish.codecextras.comments.CommentRecordBuilder; import dev.lukebemish.codecextras.companion.AccompaniedOps; import dev.lukebemish.codecextras.companion.Companion; import java.util.List; @@ -28,6 +34,11 @@ public > } return super.getCompanion(token); } + + @Override + public RecordBuilder mapBuilder() { + return new CommentRecordBuilder.MapBuilder<>(this); + } }; @Override diff --git a/src/main/java/dev/lukebemish/codecextras/compat/nightconfig/CommentedNightConfigOps.java b/src/main/java/dev/lukebemish/codecextras/compat/nightconfig/CommentedNightConfigOps.java index 6f02aad..1b892bc 100644 --- a/src/main/java/dev/lukebemish/codecextras/compat/nightconfig/CommentedNightConfigOps.java +++ b/src/main/java/dev/lukebemish/codecextras/compat/nightconfig/CommentedNightConfigOps.java @@ -2,6 +2,8 @@ import com.electronwill.nightconfig.core.CommentedConfig; import com.electronwill.nightconfig.core.Config; +import com.mojang.serialization.RecordBuilder; +import dev.lukebemish.codecextras.comments.CommentRecordBuilder; import dev.lukebemish.codecextras.companion.AccompaniedOps; public abstract class CommentedNightConfigOps extends NightConfigOps implements AccompaniedOps { @@ -13,4 +15,9 @@ public T copyConfig(Config config) { } return out; } + + @Override + public RecordBuilder mapBuilder() { + return new CommentRecordBuilder.MapBuilder<>(this); + } } diff --git a/src/main/java/dev/lukebemish/codecextras/compat/nightconfig/NightConfigOps.java b/src/main/java/dev/lukebemish/codecextras/compat/nightconfig/NightConfigOps.java index 4c42a84..f5d98f6 100644 --- a/src/main/java/dev/lukebemish/codecextras/compat/nightconfig/NightConfigOps.java +++ b/src/main/java/dev/lukebemish/codecextras/compat/nightconfig/NightConfigOps.java @@ -121,20 +121,30 @@ public Object emptyMap() { @Override public DataResult mergeToMap(Object map, Object key, Object value) { - if (map instanceof Config config) { - Config newConfig = copyConfig(config); - if (key instanceof String string) { - newConfig.set(string, value); - return DataResult.success(newConfig); - } - return DataResult.error(() -> "Not a string: " + key); + Config config; + if (map instanceof Config isConfig) { + config = isConfig; + } else if (map == empty()) { + config = newConfig(); + } else { + return DataResult.error(() -> "Not a map: " + map); + } + Config newConfig = copyConfig(config); + if (key instanceof String string) { + newConfig.set(string, value); + return DataResult.success(newConfig); } - return DataResult.error(() -> "Not a map: " + map); + return DataResult.error(() -> "Not a string: " + key); } @Override public DataResult mergeToMap(Object map, MapLike values) { - if (!(map instanceof Config config)) { + Config config; + if (map instanceof Config isConfig) { + config = isConfig; + } else if (map == empty()) { + config = newConfig(); + } else { return DataResult.error(() -> "Not a map: " + map); } Config newConfig = copyConfig(config); @@ -154,7 +164,12 @@ public DataResult mergeToMap(Object map, MapLike values) { @Override public DataResult mergeToMap(Object map, Map values) { - if (!(map instanceof Config config)) { + Config config; + if (map instanceof Config isConfig) { + config = isConfig; + } else if (map == empty()) { + config = newConfig(); + } else { return DataResult.error(() -> "Not a map: " + map); } Config newConfig = copyConfig(config); diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Annotation.java b/src/main/java/dev/lukebemish/codecextras/structured/Annotation.java index b82b9d4..d5b2623 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Annotation.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Annotation.java @@ -37,7 +37,8 @@ public class Annotation { * @return the value of the annotation, if present * @param the type of the annotation value */ - public static Optional get(Keys keys, Key key) { + public static Optional get( + Keys keys, Key key) { return keys.get(key).map(app -> Identity.unbox(app).value()); } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/RecordStructure.java b/src/main/java/dev/lukebemish/codecextras/structured/RecordStructure.java index 13257b3..11d919e 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/RecordStructure.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/RecordStructure.java @@ -190,8 +190,8 @@ public Key> addOptional(String name, Structure structure, Fun return key; } - public Function addOptionalInt(String name, Function getter) { - return addOptional(name, Structure.INT, getter.andThen(o -> { + public Function addOptionalInt(String name, Structure structure, Function getter) { + return addOptional(name, structure, getter.andThen(o -> { if (o.isPresent()) { return Optional.of(o.getAsInt()); } @@ -199,8 +199,12 @@ public Function addOptionalInt(String name, Function o.map(OptionalInt::of).orElse(OptionalInt.empty())); } - public Function addOptionalDouble(String name, Function getter) { - return addOptional(name, Structure.DOUBLE, getter.andThen(o -> { + public Function addOptionalInt(String name, Function getter) { + return addOptionalInt(name, Structure.INT, getter); + } + + public Function addOptionalDouble(String name, Structure structure, Function getter) { + return addOptional(name, structure, getter.andThen(o -> { if (o.isPresent()) { return Optional.of(o.getAsDouble()); } @@ -208,8 +212,12 @@ public Function addOptionalDouble(String name, Functi })).andThen(o -> o.map(OptionalDouble::of).orElse(OptionalDouble.empty())); } - public Function addOptionalLong(String name, Function getter) { - return addOptional(name, Structure.LONG, getter.andThen(o -> { + public Function addOptionalDouble(String name, Function getter) { + return addOptionalDouble(name, Structure.DOUBLE, getter); + } + + public Function addOptionalLong(String name, Structure structure, Function getter) { + return addOptional(name, structure, getter.andThen(o -> { if (o.isPresent()) { return Optional.of(o.getAsLong()); } @@ -217,6 +225,10 @@ public Function addOptionalLong(String name, Function o.map(OptionalLong::of).orElse(OptionalLong.empty())); } + public Function addOptionalLong(String name, Function getter) { + return addOptionalLong(name, Structure.LONG, getter); + } + /** * Add a field to the record structure with a default value. The field will not be encoded if equal to its default value. * @param name the name of the field diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/BuiltInReflectiveStructureCreator.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/BuiltInReflectiveStructureCreator.java index 31bc644..cfca554 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/BuiltInReflectiveStructureCreator.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/BuiltInReflectiveStructureCreator.java @@ -7,10 +7,16 @@ import com.mojang.datafixers.util.Either; import com.mojang.datafixers.util.Unit; import com.mojang.serialization.Dynamic; +import dev.lukebemish.codecextras.structured.Key; +import dev.lukebemish.codecextras.structured.Keys; import dev.lukebemish.codecextras.structured.RecordStructure; import dev.lukebemish.codecextras.structured.Structure; +import dev.lukebemish.codecextras.structured.reflective.annotations.Annotated; import dev.lukebemish.codecextras.structured.reflective.annotations.SerializedProperty; import dev.lukebemish.codecextras.structured.reflective.annotations.Transient; +import dev.lukebemish.codecextras.structured.reflective.annotations.Value; +import dev.lukebemish.codecextras.types.Identity; +import java.lang.annotation.Annotation; import java.lang.invoke.CallSite; import java.lang.invoke.ConstantCallSite; import java.lang.invoke.MethodHandle; @@ -234,6 +240,129 @@ public Map, Creator> creators(CreationOptions options) { .build(); } + private static Object parseValue(Value value) { + var holdingClass = value.location(); + var isField = !value.field().isEmpty(); + var isMethod = !value.method().isEmpty(); + if (isField && isMethod) { + throw new IllegalArgumentException("@Value cannot have both a field and a method"); + } + if (isField) { + try { + var field = holdingClass.getField(value.field()); + return field.get(null); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException("@Value must refer to a public static field or method with no arguments", e); + } + } else if (isMethod) { + try { + var method = holdingClass.getMethod(value.method()); + return method.invoke(null); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException("@Value must refer to a public static field or method with no arguments", e); + } + } else { + throw new IllegalArgumentException("@Value must have either a field or a method"); + } + } + + @SuppressWarnings("rawtypes") + @Override + public Map, Function>> annotationParsers(Set options) { + return ImmutableMap., Function>>builder() + .put(Annotated.class, (Annotated annotation) -> { + Key key = (Key) parseValue(annotation.key()); + Object value = null; + int referred = 0; + if (annotation.value().length > 0) { + if (annotation.value().length > 1) { + throw new IllegalArgumentException("@Annotated must have exactly one value"); + } + value = parseValue(annotation.value()[0]); + referred++; + } + if (annotation.stringValue().length > 0) { + if (annotation.stringValue().length > 1) { + throw new IllegalArgumentException("@Annotated must have exactly one stringValue"); + } + value = annotation.stringValue()[0]; + referred++; + } + if (annotation.intValue().length > 0) { + if (annotation.intValue().length > 1) { + throw new IllegalArgumentException("@Annotated must have exactly one intValue"); + } + value = annotation.intValue()[0]; + referred++; + } + if (annotation.longValue().length > 0) { + if (annotation.longValue().length > 1) { + throw new IllegalArgumentException("@Annotated must have exactly one longValue"); + } + value = annotation.longValue()[0]; + referred++; + } + if (annotation.doubleValue().length > 0) { + if (annotation.doubleValue().length > 1) { + throw new IllegalArgumentException("@Annotated must have exactly one doubleValue"); + } + value = annotation.doubleValue()[0]; + referred++; + } + if (annotation.floatValue().length > 0) { + if (annotation.floatValue().length > 1) { + throw new IllegalArgumentException("@Annotated must have exactly one floatValue"); + } + value = annotation.floatValue()[0]; + referred++; + } + if (annotation.booleanValue().length > 0) { + if (annotation.booleanValue().length > 1) { + throw new IllegalArgumentException("@Annotated must have exactly one booleanValue"); + } + value = annotation.booleanValue()[0]; + referred++; + } + if (annotation.byteValue().length > 0) { + if (annotation.byteValue().length > 1) { + throw new IllegalArgumentException("@Annotated must have exactly one byteValue"); + } + value = annotation.byteValue()[0]; + referred++; + } + if (annotation.shortValue().length > 0) { + if (annotation.shortValue().length > 1) { + throw new IllegalArgumentException("@Annotated must have exactly one shortValue"); + } + value = annotation.shortValue()[0]; + referred++; + } + if (annotation.charValue().length > 0) { + if (annotation.charValue().length > 1) { + throw new IllegalArgumentException("@Annotated must have exactly one charValue"); + } + value = annotation.charValue()[0]; + referred++; + } + if (value == null || referred != 1) { + throw new IllegalArgumentException("@Annotated must have exactly one value"); + } + Object finalValue = value; + return new AnnotationInfo() { + @Override + public Key key() { + return key; + } + + @Override + public Object value() { + return finalValue; + } + }; + }) + .build(); + } + @Override public Map, ParameterizedCreator> parameterizedCreators(CreationOptions options) { return ImmutableMap., ParameterizedCreator>builder() @@ -337,28 +466,41 @@ private static CallSite asCallSite(MethodHandles.Lookup ignoredLookup, String ig } @SuppressWarnings({"rawtypes", "unchecked"}) - public static Function add(CreationOptions options, RecordStructure builder, String name, Type type, Function getter, Function> creator) { - // TODO: check annotations + public static Function add(CreationOptions options, RecordStructure builder, String name, Type type, Function getter, Function> creator, List annotated) { + var annotations = annotated.stream().flatMap(a -> Arrays.stream(a.getAnnotations())).toList(); + var annotationInfo = annotations.stream() + .>map(options::parseAnnotation) + .filter(Objects::nonNull) + .toList(); + + Function structureUpdater = structure -> { + Keys.Builder keys = Keys.builder(); + for (var info : annotationInfo) { + keys.add((Key) info.key(), new Identity<>(info.value())); + } + return structure.annotate(keys.build()); + }; + if (type instanceof ParameterizedType parameterizedType && parameterizedType.getRawType() instanceof Class rawType) { if (rawType.equals(Optional.class)) { var innerType = parameterizedType.getActualTypeArguments()[0]; - return builder.addOptional(name, (Structure) creator.apply(innerType), (Function) getter); + return builder.addOptional(name, structureUpdater.apply(creator.apply(innerType)), (Function) getter); } } else if (type instanceof Class clazz) { if (clazz.equals(OptionalInt.class)) { - return builder.addOptionalInt(name, (Function) getter); + return builder.addOptionalInt(name, structureUpdater.apply(Structure.INT), (Function) getter); } else if (clazz.equals(OptionalDouble.class)) { - return builder.addOptionalDouble(name, (Function) getter); + return builder.addOptionalDouble(name, structureUpdater.apply(Structure.DOUBLE), (Function) getter); } else if (clazz.equals(OptionalLong.class)) { - return builder.addOptionalLong(name, (Function) getter); + return builder.addOptionalLong(name, structureUpdater.apply(Structure.LONG), (Function) getter); } } boolean isNotNull = options.hasOption(SimpleCreatorOption.NOT_NULL_BY_DEFAULT); Function key; if (isNotNull || (type instanceof Class clazz && clazz.isPrimitive())) { - key = builder.add(name, (Structure) creator.apply(type), (Function) getter); + key = builder.add(name, structureUpdater.apply(creator.apply(type)), (Function) getter); } else { - key = ((Function>) builder.addOptional(name, (Structure) creator.apply(type), (Function) getter.andThen(Optional::ofNullable))) + key = ((Function>) builder.addOptional(name, structureUpdater.apply(creator.apply(type)), (Function) getter.andThen(Optional::ofNullable))) .andThen(o -> o.orElse(null)); } return key; @@ -898,7 +1040,7 @@ public Structure create(Class exact, TypedCreator[] parameters, Function>(propertyList.size()); for (var property : propertyList) { var getter = getters.get(property); - var key = add(options, builder, property, types.get(property), getter, creator); + var key = add(options, builder, property, types.get(property), getter, creator, context.get(property)); keyList.add(key); } var cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/CreationOptions.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/CreationOptions.java index 8aacec4..22cd428 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/CreationOptions.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/CreationOptions.java @@ -1,16 +1,31 @@ package dev.lukebemish.codecextras.structured.reflective; +import java.lang.annotation.Annotation; import java.util.Collection; +import java.util.Map; import java.util.Set; +import java.util.function.Function; +import org.jspecify.annotations.Nullable; public final class CreationOptions { private final Set options; + private final Map, Function>> annotationParsers; - CreationOptions(Collection options) { + CreationOptions(Collection options, Map, Function>> annotationParsers) { this.options = Set.copyOf(options); + this.annotationParsers = annotationParsers; } public boolean hasOption(CreationOption option) { return options.contains(option); } + + @SuppressWarnings({"unchecked", "rawtypes"}) + public ReflectiveStructureCreator.@Nullable AnnotationInfo parseAnnotation(Annotation annotation) { + var function = (Function) annotationParsers.get(annotation.annotationType()); + if (function != null) { + return (ReflectiveStructureCreator.AnnotationInfo) function.apply(annotation); + } + return null; + } } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java index 8648015..aad990c 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java @@ -1,8 +1,11 @@ package dev.lukebemish.codecextras.structured.reflective; import com.google.common.base.Suppliers; +import com.google.common.collect.ImmutableSet; +import dev.lukebemish.codecextras.structured.Key; import dev.lukebemish.codecextras.structured.Structure; import dev.lukebemish.codecextras.utility.LayeredServiceLoader; +import java.lang.annotation.Annotation; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; @@ -11,6 +14,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.function.Function; import java.util.function.Supplier; @@ -18,6 +22,14 @@ public interface ReflectiveStructureCreator { Map, Creator> creators(CreationOptions options); Map, ParameterizedCreator> parameterizedCreators(CreationOptions options); List flexibleCreators(CreationOptions options); + default Map, Function>> annotationParsers(Set options) { + return Map.of(); + } + + interface AnnotationInfo { + Key key(); + T value(); + } interface TypedCreator { Structure create(); @@ -48,14 +60,17 @@ default Creator creator(TypedCreator[] parameters) { } final class Instance { - private final Map, Creator> creators; - private final Map, ParameterizedCreator> parameterizedCreators; - private final List flexibleCreators; + private final Map, Function> creators; + private final Map, Function> parameterizedCreators; + private final List> flexibleCreators; + private final Map, Function, Function>>> annotationParsers; + private final List options; - private Instance(Map, Creator> creators, Map, ParameterizedCreator> parameterizedCreators, List flexibleCreators, CreationOptions options) { + private Instance(Map, Function> creators, Map, Function> parameterizedCreators, List> flexibleCreators, Map, Function, Function>>> annotationParsers, List options) { this.creators = creators; this.parameterizedCreators = parameterizedCreators; this.flexibleCreators = flexibleCreators; + this.annotationParsers = annotationParsers; this.options = options; } @@ -64,25 +79,26 @@ public static Builder builder() { } public static final class Builder { - private final Map, Creator> creators = new IdentityHashMap<>(); - private final Map, ParameterizedCreator> parameterizedCreators = new IdentityHashMap<>(); - private final List flexibleCreators = new ArrayList<>(); + private final Map, Function> creators = new IdentityHashMap<>(); + private final Map, Function> parameterizedCreators = new IdentityHashMap<>(); + private final List> flexibleCreators = new ArrayList<>(); private final List options = new ArrayList<>(); + private final Map, Function, Function>>> annotationParsers = new IdentityHashMap<>(); private Builder() {} public Builder withCreator(Class clazz, Creator creator) { - creators.put(clazz, creator); + creators.put(clazz, ignored -> creator); return this; } public Builder withParameterizedCreator(Class clazz, ParameterizedCreator creator) { - parameterizedCreators.put(clazz, creator); + parameterizedCreators.put(clazz, ignored -> creator); return this; } public Builder withFlexibleCreator(FlexibleCreator creator) { - flexibleCreators.add(creator); + flexibleCreators.add(ignored -> creator); return this; } @@ -91,8 +107,13 @@ public Builder withOption(CreationOption option) { return this; } + public Builder withAnnotationParser(Class annotation, Function> discoverer) { + annotationParsers.put(annotation, ignored -> discoverer); + return this; + } + public Instance build() { - return new Instance(creators, parameterizedCreators, flexibleCreators, new CreationOptions(options)); + return new Instance(creators, parameterizedCreators, flexibleCreators, annotationParsers, options); } } @@ -100,12 +121,21 @@ public Instance build() { private final Map> cachedCreators = new HashMap<>(); - private final CreationOptions options; - @SuppressWarnings("unchecked") public synchronized Structure create(Class clazz) { var caller = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE).getCallerClass(); List services = LayeredServiceLoader.unique(SERVICE_LOADER.at(ReflectiveStructureCreator.class), SERVICE_LOADER.at(clazz), SERVICE_LOADER.at(caller)); + + var creationOptions = ImmutableSet.copyOf(this.options); + + Map, Function>> annotationParsersMap = new IdentityHashMap<>(); + services.forEach(creator -> annotationParsersMap.putAll(creator.annotationParsers(creationOptions))); + this.annotationParsers.forEach((key, function) -> { + annotationParsersMap.put(key, function.apply(creationOptions)); + }); + + var options = new CreationOptions(this.options, annotationParsersMap); + Map, Creator> creatorsMap = new IdentityHashMap<>(); Map, ParameterizedCreator> parameterizedCreatorsMap = new IdentityHashMap<>(); List flexibleCreatorsList = new ArrayList<>(); @@ -115,9 +145,15 @@ public synchronized Structure create(Class clazz) { flexibleCreatorsList.addAll(creator.flexibleCreators(options)); }); - creatorsMap.putAll(this.creators); - parameterizedCreatorsMap.putAll(this.parameterizedCreators); - flexibleCreatorsList.addAll(this.flexibleCreators); + this.creators.forEach((key, function) -> { + creatorsMap.put(key, function.apply(options)); + }); + this.parameterizedCreators.forEach((key, function) -> { + parameterizedCreatorsMap.put(key, function.apply(options)); + }); + this.flexibleCreators.forEach(function -> { + flexibleCreatorsList.add(function.apply(options)); + }); flexibleCreatorsList.sort((a, b) -> Integer.compare(b.priority(), a.priority())); diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Annotated.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Annotated.java new file mode 100644 index 0000000..1d29739 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Annotated.java @@ -0,0 +1,22 @@ +package dev.lukebemish.codecextras.structured.reflective.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.METHOD, ElementType.RECORD_COMPONENT, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Annotated { + Value key(); + Value[] value() default {}; + String[] stringValue() default {}; + int[] intValue() default {}; + long[] longValue() default {}; + double[] doubleValue() default {}; + float[] floatValue() default {}; + boolean[] booleanValue() default {}; + byte[] byteValue() default {}; + short[] shortValue() default {}; + char[] charValue() default {}; +} diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Value.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Value.java new file mode 100644 index 0000000..0d44c52 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Value.java @@ -0,0 +1,15 @@ +package dev.lukebemish.codecextras.structured.reflective.annotations; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Value { + Class location(); + + String field() default ""; + + String method() default ""; +} diff --git a/src/test/java/dev/lukebemish/codecextras/test/CodecAssertions.java b/src/test/java/dev/lukebemish/codecextras/test/CodecAssertions.java index 7bde5aa..86ed5cd 100644 --- a/src/test/java/dev/lukebemish/codecextras/test/CodecAssertions.java +++ b/src/test/java/dev/lukebemish/codecextras/test/CodecAssertions.java @@ -6,6 +6,7 @@ import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; import com.mojang.serialization.DynamicOps; +import java.util.function.Function; import org.junit.jupiter.api.Assertions; public final class CodecAssertions { @@ -57,6 +58,12 @@ public static void assertEncodes(DynamicOps ops, O value, T expected, Assertions.assertEquals(expected, dataResult.result().get()); } + public static void assertEncodesString(DynamicOps ops, O value, String expected, Function converter, Codec codec) { + DataResult dataResult = codec.encodeStart(ops, value); + Assertions.assertTrue(dataResult.result().isPresent(), () -> dataResult.error().orElseThrow().message()); + Assertions.assertEquals(expected, converter.apply(dataResult.result().get())); + } + public static void assertJsonEquals(String expected, String actual) { Gson gson = new GsonBuilder().create(); JsonElement expectedElement = gson.fromJson(expected, JsonElement.class); diff --git a/src/test/java/dev/lukebemish/codecextras/test/comments/TestComments.java b/src/test/java/dev/lukebemish/codecextras/test/comments/TestComments.java new file mode 100644 index 0000000..54852ed --- /dev/null +++ b/src/test/java/dev/lukebemish/codecextras/test/comments/TestComments.java @@ -0,0 +1,48 @@ +package dev.lukebemish.codecextras.test.comments; + +import com.electronwill.nightconfig.core.Config; +import com.electronwill.nightconfig.toml.TomlParser; +import com.electronwill.nightconfig.toml.TomlWriter; +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import dev.lukebemish.codecextras.comments.CommentMapCodec; +import dev.lukebemish.codecextras.compat.nightconfig.TomlConfigOps; +import dev.lukebemish.codecextras.test.CodecAssertions; +import java.util.function.Function; +import org.junit.jupiter.api.Test; + +public class TestComments { + private record TestRecord(int a) { + static final Codec CODEC = RecordCodecBuilder.create(i -> i.group( + CommentMapCodec.of(Codec.INT.fieldOf("a"), "Commented field").forGetter(TestRecord::a) + ).apply(i, TestRecord::new)); + } + + private final String toml = """ + #Commented field + a = 1 + + """; + + private final Config tomlConfig = new TomlParser().parse(toml); + + private static final Function TOML_TO_STRING = toml -> { + if (toml instanceof Config config) { + return new TomlWriter().writeToString(config); + } else { + return toml.toString(); + } + }; + + private final TestRecord testRecord = new TestRecord(1); + + @Test + void testEncoding() { + CodecAssertions.assertEncodesString(TomlConfigOps.COMMENTED, testRecord, toml, TOML_TO_STRING, TestRecord.CODEC); + } + + @Test + void testDecoding() { + CodecAssertions.assertDecodes(TomlConfigOps.COMMENTED, tomlConfig, testRecord, TestRecord.CODEC); + } +} diff --git a/src/test/java/dev/lukebemish/codecextras/test/comments/package-info.java b/src/test/java/dev/lukebemish/codecextras/test/comments/package-info.java new file mode 100644 index 0000000..561cd82 --- /dev/null +++ b/src/test/java/dev/lukebemish/codecextras/test/comments/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package dev.lukebemish.codecextras.test.comments; + +import org.jspecify.annotations.NullMarked; diff --git a/src/test/java/dev/lukebemish/codecextras/test/structured/reflective/TestReflective.java b/src/test/java/dev/lukebemish/codecextras/test/structured/reflective/TestReflective.java index 46282b0..53e0a28 100644 --- a/src/test/java/dev/lukebemish/codecextras/test/structured/reflective/TestReflective.java +++ b/src/test/java/dev/lukebemish/codecextras/test/structured/reflective/TestReflective.java @@ -5,7 +5,6 @@ import dev.lukebemish.codecextras.structured.CodecInterpreter; import dev.lukebemish.codecextras.structured.Structure; import dev.lukebemish.codecextras.structured.reflective.ReflectiveStructureCreator; -import dev.lukebemish.codecextras.structured.reflective.annotations.SerializedProperty; import dev.lukebemish.codecextras.structured.reflective.annotations.Transient; import dev.lukebemish.codecextras.test.CodecAssertions; import java.lang.annotation.Retention; @@ -30,76 +29,6 @@ public enum TestEnum { C } - public static class TestNoArgCtor { - public int a; - private @Nullable String b; - - public @Nullable String getB() { - return this.b; - } - - public void setB(String b) { - this.b = b; - } - - @Override - public boolean equals(Object object) { - if (this == object) return true; - if (!(object instanceof TestNoArgCtor that)) return false; - return a == that.a && Objects.equals(b, that.b); - } - - @Override - public int hashCode() { - return Objects.hash(a, b); - } - - @Override - public String toString() { - return "TestNoArgCtor{" + - "a=" + a + - ", b='" + b + '\'' + - '}'; - } - } - - public static class TestCtor { - public final int a; - private final String b; - - public String getB() { - return this.b; - } - - public TestCtor( - @SerializedProperty("a") int a, - @SerializedProperty("b") String b - ) { - this.a = a; - this.b = b; - } - - @Override - public boolean equals(Object object) { - if (this == object) return true; - if (!(object instanceof TestCtor testCtor)) return false; - return a == testCtor.a && Objects.equals(b, testCtor.b); - } - - @Override - public int hashCode() { - return Objects.hash(a, b); - } - - @Override - public String toString() { - return "TestCtor{" + - "a=" + a + - ", b='" + b + '\'' + - '}'; - } - } - public static class TestAnnotations { public long a; public transient boolean b; @@ -153,9 +82,6 @@ public record TestRecursive(String name, List list) {} private static final Codec RECURSIVE_CODEC = CodecInterpreter.create().interpret(ReflectiveStructureCreator.create(TestRecursive.class)).getOrThrow(); - private static final Codec CTOR_CODEC = CodecInterpreter.create().interpret(ReflectiveStructureCreator.create(TestCtor.class)).getOrThrow(); - private static final Codec NO_ARG_CTOR_CODEC = CodecInterpreter.create().interpret(ReflectiveStructureCreator.create(TestNoArgCtor.class)).getOrThrow(); - private static final Codec ANNOTATIONS_CODEC = CodecInterpreter.create().interpret(ReflectiveStructureCreator.create(TestAnnotations.class)).getOrThrow(); private final String json = """ @@ -198,12 +124,6 @@ public record TestRecursive(String name, List list) {} ] }"""; - private final String ctorJson = """ - { - "a": 1, - "b": "test" - }"""; - private final String annotationsJson = """ { "a": 1, @@ -215,13 +135,6 @@ public record TestRecursive(String name, List list) {} private final int[] primitiveArray = new int[] { 1, 2, 3 }; private final TestRecursive recursive = new TestRecursive("test1", List.of(new TestRecursive("test2", List.of()), new TestRecursive("test3", List.of()))); - private final TestNoArgCtor noArgCtor = new TestNoArgCtor(); - { - noArgCtor.a = 1; - noArgCtor.setB("test"); - } - private final TestCtor ctor = new TestCtor(1, "test"); - private final TestAnnotations annotations = new TestAnnotations(); { annotations.a = 1; @@ -268,26 +181,6 @@ void testEncodingRecursive() { CodecAssertions.assertEncodes(JsonOps.INSTANCE, recursive, recursiveJson, RECURSIVE_CODEC); } - @Test - void testDecodingCtor() { - CodecAssertions.assertDecodes(JsonOps.INSTANCE, ctorJson, ctor, CTOR_CODEC); - } - - @Test - void testEncodingCtor() { - CodecAssertions.assertEncodes(JsonOps.INSTANCE, ctor, ctorJson, CTOR_CODEC); - } - - @Test - void testDecodingNoArgCtor() { - CodecAssertions.assertDecodes(JsonOps.INSTANCE, ctorJson, noArgCtor, NO_ARG_CTOR_CODEC); - } - - @Test - void testEncodingNoArgCtor() { - CodecAssertions.assertEncodes(JsonOps.INSTANCE, noArgCtor, ctorJson, NO_ARG_CTOR_CODEC); - } - @Test void testDecodingAnnotations() { CodecAssertions.assertDecodes(JsonOps.INSTANCE, annotationsJson, annotations, ANNOTATIONS_CODEC); diff --git a/src/test/java/dev/lukebemish/codecextras/test/structured/reflective/TestReflectiveConstructors.java b/src/test/java/dev/lukebemish/codecextras/test/structured/reflective/TestReflectiveConstructors.java new file mode 100644 index 0000000..497007b --- /dev/null +++ b/src/test/java/dev/lukebemish/codecextras/test/structured/reflective/TestReflectiveConstructors.java @@ -0,0 +1,119 @@ +package dev.lukebemish.codecextras.test.structured.reflective; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.JsonOps; +import dev.lukebemish.codecextras.structured.CodecInterpreter; +import dev.lukebemish.codecextras.structured.reflective.ReflectiveStructureCreator; +import dev.lukebemish.codecextras.structured.reflective.annotations.SerializedProperty; +import dev.lukebemish.codecextras.test.CodecAssertions; +import java.util.Objects; +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.Test; + +public class TestReflectiveConstructors { + public static class TestNoArgCtor { + public int a; + private @Nullable String b; + + public @Nullable String getB() { + return this.b; + } + + public void setB(String b) { + this.b = b; + } + + @Override + public boolean equals(Object object) { + if (this == object) return true; + if (!(object instanceof TestNoArgCtor that)) return false; + return a == that.a && Objects.equals(b, that.b); + } + + @Override + public int hashCode() { + return Objects.hash(a, b); + } + + @Override + public String toString() { + return "TestNoArgCtor{" + + "a=" + a + + ", b='" + b + '\'' + + '}'; + } + } + + public static class TestCtor { + public final int a; + private final String b; + + public String getB() { + return this.b; + } + + public TestCtor( + @SerializedProperty("a") int a, + @SerializedProperty("b") String b + ) { + this.a = a; + this.b = b; + } + + @Override + public boolean equals(Object object) { + if (this == object) return true; + if (!(object instanceof TestCtor testCtor)) return false; + return a == testCtor.a && Objects.equals(b, testCtor.b); + } + + @Override + public int hashCode() { + return Objects.hash(a, b); + } + + @Override + public String toString() { + return "TestCtor{" + + "a=" + a + + ", b='" + b + '\'' + + '}'; + } + } + + private static final Codec CTOR_CODEC = CodecInterpreter.create().interpret(ReflectiveStructureCreator.create(TestCtor.class)).getOrThrow(); + private static final Codec NO_ARG_CTOR_CODEC = CodecInterpreter.create().interpret(ReflectiveStructureCreator.create(TestNoArgCtor.class)).getOrThrow(); + + private final String ctorJson = """ + { + "a": 1, + "b": "test" + }"""; + + private final TestNoArgCtor noArgCtor = new TestNoArgCtor(); + { + noArgCtor.a = 1; + noArgCtor.setB("test"); + } + private final TestCtor ctor = new TestCtor(1, "test"); + + @Test + void testDecodingCtor() { + CodecAssertions.assertDecodes(JsonOps.INSTANCE, ctorJson, ctor, CTOR_CODEC); + } + + @Test + void testEncodingCtor() { + CodecAssertions.assertEncodes(JsonOps.INSTANCE, ctor, ctorJson, CTOR_CODEC); + } + + @Test + void testDecodingNoArgCtor() { + CodecAssertions.assertDecodes(JsonOps.INSTANCE, ctorJson, noArgCtor, NO_ARG_CTOR_CODEC); + } + + @Test + void testEncodingNoArgCtor() { + CodecAssertions.assertEncodes(JsonOps.INSTANCE, noArgCtor, ctorJson, NO_ARG_CTOR_CODEC); + } +} diff --git a/src/test/java/dev/lukebemish/codecextras/test/structured/reflective/TestReflectiveStructureAnnotations.java b/src/test/java/dev/lukebemish/codecextras/test/structured/reflective/TestReflectiveStructureAnnotations.java new file mode 100644 index 0000000..4c53170 --- /dev/null +++ b/src/test/java/dev/lukebemish/codecextras/test/structured/reflective/TestReflectiveStructureAnnotations.java @@ -0,0 +1,88 @@ +package dev.lukebemish.codecextras.test.structured.reflective; + +import com.electronwill.nightconfig.core.Config; +import com.electronwill.nightconfig.toml.TomlWriter; +import com.mojang.serialization.Codec; +import dev.lukebemish.codecextras.compat.nightconfig.TomlConfigOps; +import dev.lukebemish.codecextras.structured.Annotation; +import dev.lukebemish.codecextras.structured.CodecInterpreter; +import dev.lukebemish.codecextras.structured.reflective.ReflectiveStructureCreator; +import dev.lukebemish.codecextras.structured.reflective.annotations.Annotated; +import dev.lukebemish.codecextras.structured.reflective.annotations.Value; +import dev.lukebemish.codecextras.test.CodecAssertions; +import java.util.function.Function; +import org.junit.jupiter.api.Test; + +public class TestReflectiveStructureAnnotations { + public record TestRecordAnnotated( + @Annotated( + key = @Value(location = Annotation.class, field = "COMMENT"), + stringValue = "Commented field" + ) int a + ) {} + + public static class TestFieldAnnotated { + @Annotated( + key = @Value(location = Annotation.class, field = "COMMENT"), + stringValue = "Commented field" + ) + public int a; + } + + public static class TestMethodAnnotated { + private int a; + + public void setA(int a) { + this.a = a; + } + + @Annotated( + key = @Value(location = Annotation.class, field = "COMMENT"), + stringValue = "Commented field" + ) + public int getA() { + return this.a; + } + } + + private static final Codec RECORD_ANNOTATED_CODEC = CodecInterpreter.create().interpret(ReflectiveStructureCreator.create(TestRecordAnnotated.class)).getOrThrow(); + private static final Codec FIELD_ANNOTATED_CODEC = CodecInterpreter.create().interpret(ReflectiveStructureCreator.create(TestFieldAnnotated.class)).getOrThrow(); + private static final Codec METHOD_ANNOTATED_CODEC = CodecInterpreter.create().interpret(ReflectiveStructureCreator.create(TestMethodAnnotated.class)).getOrThrow(); + + private final TestRecordAnnotated testRecordAnnotated = new TestRecordAnnotated(1); + private final TestFieldAnnotated testFieldAnnotated = new TestFieldAnnotated(); + private final TestMethodAnnotated testMethodAnnotated = new TestMethodAnnotated(); + { + testFieldAnnotated.a = 1; + testMethodAnnotated.setA(1); + } + + private final String toml = """ + #Commented field + a = 1 + + """; + + private static final Function TOML_TO_STRING = toml -> { + if (toml instanceof Config config) { + return new TomlWriter().writeToString(config); + } else { + return toml.toString(); + } + }; + + @Test + void testEncodingRecordAnnotated() { + CodecAssertions.assertEncodesString(TomlConfigOps.COMMENTED, testRecordAnnotated, toml, TOML_TO_STRING, RECORD_ANNOTATED_CODEC); + } + + @Test + void testEncodingFieldAnnotated() { + CodecAssertions.assertEncodesString(TomlConfigOps.COMMENTED, testFieldAnnotated, toml, TOML_TO_STRING, FIELD_ANNOTATED_CODEC); + } + + @Test + void testEncodingMethodAnnotated() { + CodecAssertions.assertEncodesString(TomlConfigOps.COMMENTED, testMethodAnnotated, toml, TOML_TO_STRING, METHOD_ANNOTATED_CODEC); + } +} From d1827a3271b0edf0cf05ce036c5eef22cfb48ef1 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Thu, 6 Mar 2025 21:39:17 -0600 Subject: [PATCH 19/28] Comment and Structured annotations --- .../compat/nightconfig/NightConfigOps.java | 9 ++ .../BuiltInReflectiveStructureCreator.java | 61 ++++++++--- .../reflective/CreationOptions.java | 12 +-- .../ReflectiveStructureCreator.java | 24 +++-- .../reflective/annotations/Comment.java | 12 +++ .../reflective/annotations/Structured.java | 13 +++ .../TestReflectiveStructureAnnotations.java | 101 ++++++++++++++++-- 7 files changed, 198 insertions(+), 34 deletions(-) create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Comment.java create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Structured.java diff --git a/src/main/java/dev/lukebemish/codecextras/compat/nightconfig/NightConfigOps.java b/src/main/java/dev/lukebemish/codecextras/compat/nightconfig/NightConfigOps.java index f5d98f6..34fe63f 100644 --- a/src/main/java/dev/lukebemish/codecextras/compat/nightconfig/NightConfigOps.java +++ b/src/main/java/dev/lukebemish/codecextras/compat/nightconfig/NightConfigOps.java @@ -91,6 +91,11 @@ public Object createBoolean(boolean value) { @Override public DataResult mergeToList(Object list, Object value) { + if (list == empty()) { + List out = new ArrayList<>(); + out.add(value); + return DataResult.success(out); + } if (list instanceof List list1) { List out = new ArrayList<>(list1); out.add(value); @@ -101,6 +106,10 @@ public DataResult mergeToList(Object list, Object value) { @Override public DataResult mergeToList(Object list, List values) { + if (list == empty()) { + List out = new ArrayList<>(values); + return DataResult.success(out); + } if (list instanceof List list1) { List out = new ArrayList<>(list1); out.addAll(values); diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/BuiltInReflectiveStructureCreator.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/BuiltInReflectiveStructureCreator.java index cfca554..071a1e1 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/BuiltInReflectiveStructureCreator.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/BuiltInReflectiveStructureCreator.java @@ -12,7 +12,9 @@ import dev.lukebemish.codecextras.structured.RecordStructure; import dev.lukebemish.codecextras.structured.Structure; import dev.lukebemish.codecextras.structured.reflective.annotations.Annotated; +import dev.lukebemish.codecextras.structured.reflective.annotations.Comment; import dev.lukebemish.codecextras.structured.reflective.annotations.SerializedProperty; +import dev.lukebemish.codecextras.structured.reflective.annotations.Structured; import dev.lukebemish.codecextras.structured.reflective.annotations.Transient; import dev.lukebemish.codecextras.structured.reflective.annotations.Value; import dev.lukebemish.codecextras.types.Identity; @@ -268,8 +270,9 @@ private static Object parseValue(Value value) { @SuppressWarnings("rawtypes") @Override - public Map, Function>> annotationParsers(Set options) { - return ImmutableMap., Function>>builder() + public Map, Function>>> annotationParsers(Set options) { + var builder = ImmutableMap., Function>>>builder(); + return builder .put(Annotated.class, (Annotated annotation) -> { Key key = (Key) parseValue(annotation.key()); Object value = null; @@ -348,7 +351,7 @@ public Map, Function>> annotati throw new IllegalArgumentException("@Annotated must have exactly one value"); } Object finalValue = value; - return new AnnotationInfo() { + List> list = List.of(new AnnotationInfo() { @Override public Key key() { return key; @@ -358,8 +361,20 @@ public Key key() { public Object value() { return finalValue; } - }; + }); + return list; }) + .put(Comment.class, (Comment annotation) -> List.>of(new AnnotationInfo() { + @Override + public Key key() { + return dev.lukebemish.codecextras.structured.Annotation.COMMENT; + } + + @Override + public Object value() { + return annotation.value(); + } + })) .build(); } @@ -467,10 +482,9 @@ private static CallSite asCallSite(MethodHandles.Lookup ignoredLookup, String ig @SuppressWarnings({"rawtypes", "unchecked"}) public static Function add(CreationOptions options, RecordStructure builder, String name, Type type, Function getter, Function> creator, List annotated) { - var annotations = annotated.stream().flatMap(a -> Arrays.stream(a.getAnnotations())).toList(); + var annotations = annotated.stream().distinct().flatMap(a -> Arrays.stream(a.getAnnotations())).toList(); var annotationInfo = annotations.stream() - .>map(options::parseAnnotation) - .filter(Objects::nonNull) + .flatMap(a -> options.parseAnnotation(a).stream()) .toList(); Function structureUpdater = structure -> { @@ -481,26 +495,47 @@ private static CallSite asCallSite(MethodHandles.Lookup ignoredLookup, String ig return structure.annotate(keys.build()); }; + var structureInfos = annotations.stream() + .map(info -> { + if (info instanceof Structured structured) { + return structured; + } + return null; + }).filter(Objects::nonNull).distinct().toList(); + + if (structureInfos.size() > 1) { + throw new IllegalArgumentException("Multiple @Structured annotations found"); + } + + Structure explicitStructure = null; + if (!structureInfos.isEmpty()) { + explicitStructure = (Structure) parseValue(structureInfos.getFirst().value()); + if (structureInfos.getFirst().directOptional()) { + return ((Function>) builder.addOptional(name, structureUpdater.apply(explicitStructure), (Function) getter.andThen(Optional::ofNullable))) + .andThen(o -> o.orElse(null)); + } + } + if (type instanceof ParameterizedType parameterizedType && parameterizedType.getRawType() instanceof Class rawType) { if (rawType.equals(Optional.class)) { var innerType = parameterizedType.getActualTypeArguments()[0]; - return builder.addOptional(name, structureUpdater.apply(creator.apply(innerType)), (Function) getter); + return builder.addOptional(name, structureUpdater.apply(explicitStructure != null ? explicitStructure : creator.apply(innerType)), (Function) getter); } } else if (type instanceof Class clazz) { if (clazz.equals(OptionalInt.class)) { - return builder.addOptionalInt(name, structureUpdater.apply(Structure.INT), (Function) getter); + return builder.addOptionalInt(name, structureUpdater.apply(explicitStructure != null ? explicitStructure : Structure.INT), (Function) getter); } else if (clazz.equals(OptionalDouble.class)) { - return builder.addOptionalDouble(name, structureUpdater.apply(Structure.DOUBLE), (Function) getter); + return builder.addOptionalDouble(name, structureUpdater.apply(explicitStructure != null ? explicitStructure : Structure.DOUBLE), (Function) getter); } else if (clazz.equals(OptionalLong.class)) { - return builder.addOptionalLong(name, structureUpdater.apply(Structure.LONG), (Function) getter); + return builder.addOptionalLong(name, structureUpdater.apply(explicitStructure != null ? explicitStructure : Structure.LONG), (Function) getter); } } boolean isNotNull = options.hasOption(SimpleCreatorOption.NOT_NULL_BY_DEFAULT); Function key; if (isNotNull || (type instanceof Class clazz && clazz.isPrimitive())) { - key = builder.add(name, structureUpdater.apply(creator.apply(type)), (Function) getter); + key = builder.add(name, structureUpdater.apply(explicitStructure != null ? explicitStructure : creator.apply(type)), (Function) getter); } else { - key = ((Function>) builder.addOptional(name, structureUpdater.apply(creator.apply(type)), (Function) getter.andThen(Optional::ofNullable))) + key = ((Function>) builder.addOptional(name, structureUpdater.apply(explicitStructure != null ? explicitStructure : creator.apply(type)), (Function) getter.andThen(Optional::ofNullable))) .andThen(o -> o.orElse(null)); } return key; diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/CreationOptions.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/CreationOptions.java index 22cd428..45dfcab 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/CreationOptions.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/CreationOptions.java @@ -2,16 +2,16 @@ import java.lang.annotation.Annotation; import java.util.Collection; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Function; -import org.jspecify.annotations.Nullable; public final class CreationOptions { private final Set options; - private final Map, Function>> annotationParsers; + private final Map, Function>>> annotationParsers; - CreationOptions(Collection options, Map, Function>> annotationParsers) { + CreationOptions(Collection options, Map, Function>>> annotationParsers) { this.options = Set.copyOf(options); this.annotationParsers = annotationParsers; } @@ -21,11 +21,11 @@ public boolean hasOption(CreationOption option) { } @SuppressWarnings({"unchecked", "rawtypes"}) - public ReflectiveStructureCreator.@Nullable AnnotationInfo parseAnnotation(Annotation annotation) { + public List> parseAnnotation(Annotation annotation) { var function = (Function) annotationParsers.get(annotation.annotationType()); if (function != null) { - return (ReflectiveStructureCreator.AnnotationInfo) function.apply(annotation); + return (List>) function.apply(annotation); } - return null; + return List.of(); } } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java index aad990c..0baf862 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java @@ -19,10 +19,16 @@ import java.util.function.Supplier; public interface ReflectiveStructureCreator { - Map, Creator> creators(CreationOptions options); - Map, ParameterizedCreator> parameterizedCreators(CreationOptions options); - List flexibleCreators(CreationOptions options); - default Map, Function>> annotationParsers(Set options) { + default Map, Creator> creators(CreationOptions options) { + return Map.of(); + } + default Map, ParameterizedCreator> parameterizedCreators(CreationOptions options) { + return Map.of(); + } + default List flexibleCreators(CreationOptions options) { + return List.of(); + } + default Map, Function>>> annotationParsers(Set options) { return Map.of(); } @@ -63,10 +69,10 @@ final class Instance { private final Map, Function> creators; private final Map, Function> parameterizedCreators; private final List> flexibleCreators; - private final Map, Function, Function>>> annotationParsers; + private final Map, Function, Function>>>> annotationParsers; private final List options; - private Instance(Map, Function> creators, Map, Function> parameterizedCreators, List> flexibleCreators, Map, Function, Function>>> annotationParsers, List options) { + private Instance(Map, Function> creators, Map, Function> parameterizedCreators, List> flexibleCreators, Map, Function, Function>>>> annotationParsers, List options) { this.creators = creators; this.parameterizedCreators = parameterizedCreators; this.flexibleCreators = flexibleCreators; @@ -83,7 +89,7 @@ public static final class Builder { private final Map, Function> parameterizedCreators = new IdentityHashMap<>(); private final List> flexibleCreators = new ArrayList<>(); private final List options = new ArrayList<>(); - private final Map, Function, Function>>> annotationParsers = new IdentityHashMap<>(); + private final Map, Function, Function>>>> annotationParsers = new IdentityHashMap<>(); private Builder() {} @@ -107,7 +113,7 @@ public Builder withOption(CreationOption option) { return this; } - public Builder withAnnotationParser(Class annotation, Function> discoverer) { + public Builder withAnnotationParser(Class annotation, Function>> discoverer) { annotationParsers.put(annotation, ignored -> discoverer); return this; } @@ -128,7 +134,7 @@ public synchronized Structure create(Class clazz) { var creationOptions = ImmutableSet.copyOf(this.options); - Map, Function>> annotationParsersMap = new IdentityHashMap<>(); + Map, Function>>> annotationParsersMap = new IdentityHashMap<>(); services.forEach(creator -> annotationParsersMap.putAll(creator.annotationParsers(creationOptions))); this.annotationParsers.forEach((key, function) -> { annotationParsersMap.put(key, function.apply(creationOptions)); diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Comment.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Comment.java new file mode 100644 index 0000000..3043070 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Comment.java @@ -0,0 +1,12 @@ +package dev.lukebemish.codecextras.structured.reflective.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.METHOD, ElementType.RECORD_COMPONENT, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Comment { + String value(); +} diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Structured.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Structured.java new file mode 100644 index 0000000..3e19fda --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Structured.java @@ -0,0 +1,13 @@ +package dev.lukebemish.codecextras.structured.reflective.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.METHOD, ElementType.RECORD_COMPONENT, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Structured { + Value value(); + boolean directOptional() default false; +} diff --git a/src/test/java/dev/lukebemish/codecextras/test/structured/reflective/TestReflectiveStructureAnnotations.java b/src/test/java/dev/lukebemish/codecextras/test/structured/reflective/TestReflectiveStructureAnnotations.java index 4c53170..f60706c 100644 --- a/src/test/java/dev/lukebemish/codecextras/test/structured/reflective/TestReflectiveStructureAnnotations.java +++ b/src/test/java/dev/lukebemish/codecextras/test/structured/reflective/TestReflectiveStructureAnnotations.java @@ -1,68 +1,142 @@ package dev.lukebemish.codecextras.test.structured.reflective; import com.electronwill.nightconfig.core.Config; +import com.electronwill.nightconfig.toml.TomlParser; import com.electronwill.nightconfig.toml.TomlWriter; import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; import dev.lukebemish.codecextras.compat.nightconfig.TomlConfigOps; import dev.lukebemish.codecextras.structured.Annotation; import dev.lukebemish.codecextras.structured.CodecInterpreter; +import dev.lukebemish.codecextras.structured.Structure; import dev.lukebemish.codecextras.structured.reflective.ReflectiveStructureCreator; import dev.lukebemish.codecextras.structured.reflective.annotations.Annotated; +import dev.lukebemish.codecextras.structured.reflective.annotations.Comment; +import dev.lukebemish.codecextras.structured.reflective.annotations.Structured; import dev.lukebemish.codecextras.structured.reflective.annotations.Value; import dev.lukebemish.codecextras.test.CodecAssertions; +import java.util.List; +import java.util.Objects; import java.util.function.Function; import org.junit.jupiter.api.Test; public class TestReflectiveStructureAnnotations { + public static final Structure INT_AS_LIST = Structure.INT.listOf().comapFlatMap(l -> { + if (l.size() == 1) { + return DataResult.success(l.getFirst()); + } else { + return DataResult.error(() -> "Expected exactly one element in list"); + } + }, List::of); + public record TestRecordAnnotated( @Annotated( key = @Value(location = Annotation.class, field = "COMMENT"), - stringValue = "Commented field" - ) int a + stringValue = "Commented field with @Annotated" + ) int a, + @Comment("Commented field with @Comment") int b, + @Structured(@Value(location = TestReflectiveStructureAnnotations.class, field = "INT_AS_LIST")) int c ) {} public static class TestFieldAnnotated { @Annotated( key = @Value(location = Annotation.class, field = "COMMENT"), - stringValue = "Commented field" + stringValue = "Commented field with @Annotated" ) public int a; + + @Comment("Commented field with @Comment") + public int b; + + @Structured(@Value(location = TestReflectiveStructureAnnotations.class, field = "INT_AS_LIST")) + public int c; + + @Override + public boolean equals(Object object) { + if (this == object) return true; + if (!(object instanceof TestFieldAnnotated that)) return false; + return a == that.a && b == that.b && c == that.c; + } + + @Override + public int hashCode() { + return Objects.hash(a, b, c); + } } public static class TestMethodAnnotated { private int a; + private int b; + private int c; public void setA(int a) { this.a = a; } + public void setB(int b) { + this.b = b; + } + public void setC(int c) { + this.c = c; + } @Annotated( key = @Value(location = Annotation.class, field = "COMMENT"), - stringValue = "Commented field" + stringValue = "Commented field with @Annotated" ) public int getA() { return this.a; } + + @Comment("Commented field with @Comment") + public int getB() { + return this.b; + } + + @Structured(@Value(location = TestReflectiveStructureAnnotations.class, field = "INT_AS_LIST")) + public int getC() { + return this.c; + } + + @Override + public boolean equals(Object object) { + if (this == object) return true; + if (!(object instanceof TestMethodAnnotated that)) return false; + return a == that.a && b == that.b && c == that.c; + } + + @Override + public int hashCode() { + return Objects.hash(a, b, c); + } } private static final Codec RECORD_ANNOTATED_CODEC = CodecInterpreter.create().interpret(ReflectiveStructureCreator.create(TestRecordAnnotated.class)).getOrThrow(); private static final Codec FIELD_ANNOTATED_CODEC = CodecInterpreter.create().interpret(ReflectiveStructureCreator.create(TestFieldAnnotated.class)).getOrThrow(); private static final Codec METHOD_ANNOTATED_CODEC = CodecInterpreter.create().interpret(ReflectiveStructureCreator.create(TestMethodAnnotated.class)).getOrThrow(); - private final TestRecordAnnotated testRecordAnnotated = new TestRecordAnnotated(1); + private final TestRecordAnnotated testRecordAnnotated = new TestRecordAnnotated(1, 2, 3); private final TestFieldAnnotated testFieldAnnotated = new TestFieldAnnotated(); private final TestMethodAnnotated testMethodAnnotated = new TestMethodAnnotated(); { testFieldAnnotated.a = 1; + testFieldAnnotated.b = 2; + testFieldAnnotated.c = 3; testMethodAnnotated.setA(1); + testMethodAnnotated.setB(2); + testMethodAnnotated.setC(3); } private final String toml = """ - #Commented field + #Commented field with @Annotated a = 1 + #Commented field with @Comment + b = 2 + c = [3] """; + private final Config tomlParsed = new TomlParser().parse(toml); + private static final Function TOML_TO_STRING = toml -> { if (toml instanceof Config config) { return new TomlWriter().writeToString(config); @@ -76,13 +150,28 @@ void testEncodingRecordAnnotated() { CodecAssertions.assertEncodesString(TomlConfigOps.COMMENTED, testRecordAnnotated, toml, TOML_TO_STRING, RECORD_ANNOTATED_CODEC); } + @Test + void testDecodingRecordAnnotated() { + CodecAssertions.assertDecodes(TomlConfigOps.COMMENTED, tomlParsed, testRecordAnnotated, RECORD_ANNOTATED_CODEC); + } + @Test void testEncodingFieldAnnotated() { CodecAssertions.assertEncodesString(TomlConfigOps.COMMENTED, testFieldAnnotated, toml, TOML_TO_STRING, FIELD_ANNOTATED_CODEC); } + @Test + void testDecodingFieldAnnotated() { + CodecAssertions.assertDecodes(TomlConfigOps.COMMENTED, tomlParsed, testFieldAnnotated, FIELD_ANNOTATED_CODEC); + } + @Test void testEncodingMethodAnnotated() { CodecAssertions.assertEncodesString(TomlConfigOps.COMMENTED, testMethodAnnotated, toml, TOML_TO_STRING, METHOD_ANNOTATED_CODEC); } + + @Test + void testDecodingMethodAnnotated() { + CodecAssertions.assertDecodes(TomlConfigOps.COMMENTED, tomlParsed, testMethodAnnotated, METHOD_ANNOTATED_CODEC); + } } From fe95906766920475abd0b70b7a3386a0802ee9c8 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Sun, 9 Mar 2025 01:55:09 -0600 Subject: [PATCH 20/28] More annotation work -- support for gson's SerializedName, and support for groovy groovydoc --- build.gradle | 2 + .../codecextras/companion/DelegatingOps.java | 2 +- .../LayeredServiceLoader.java | 2 +- .../{utility => internal}/Lazy.java | 2 +- .../{utility => internal}/package-info.java | 2 +- .../BuiltInReflectiveStructureCreator.java | 224 +++++++++++++++--- ...ationOptions.java => CreationContext.java} | 16 +- .../structured/reflective/GroovyIsolator.java | 41 ++++ .../ReflectiveStructureCreator.java | 51 +++- .../reflective/annotations/Lenient.java | 11 + src/main/java/module-info.java | 5 +- .../MinecraftReflectiveStructureCreator.java | 8 +- .../config/ConfigScreenInterpreter.java | 2 +- .../structured/config/EntryCreationInfo.java | 2 +- 14 files changed, 312 insertions(+), 58 deletions(-) rename src/main/java/dev/lukebemish/codecextras/{utility => internal}/LayeredServiceLoader.java (98%) rename src/main/java/dev/lukebemish/codecextras/{utility => internal}/Lazy.java (93%) rename src/main/java/dev/lukebemish/codecextras/{utility => internal}/package-info.java (72%) rename src/main/java/dev/lukebemish/codecextras/structured/reflective/{CreationOptions.java => CreationContext.java} (57%) create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/reflective/GroovyIsolator.java create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Lenient.java diff --git a/build.gradle b/build.gradle index 8959f9c..986aae2 100644 --- a/build.gradle +++ b/build.gradle @@ -201,6 +201,8 @@ dependencies { compileOnly 'com.electronwill.night-config:toml:3.6.4' compileOnly 'blue.endless:jankson:1.2.2' + compileOnly 'org.apache.groovy:groovy:4.0.26' + testImplementation 'com.electronwill.night-config:core:3.6.4' testImplementation 'com.electronwill.night-config:toml:3.6.4' testImplementation 'blue.endless:jankson:1.2.2' diff --git a/src/main/java/dev/lukebemish/codecextras/companion/DelegatingOps.java b/src/main/java/dev/lukebemish/codecextras/companion/DelegatingOps.java index 2709748..26afbf5 100644 --- a/src/main/java/dev/lukebemish/codecextras/companion/DelegatingOps.java +++ b/src/main/java/dev/lukebemish/codecextras/companion/DelegatingOps.java @@ -8,7 +8,7 @@ import com.mojang.serialization.ListBuilder; import com.mojang.serialization.MapLike; import com.mojang.serialization.RecordBuilder; -import dev.lukebemish.codecextras.utility.LayeredServiceLoader; +import dev.lukebemish.codecextras.internal.LayeredServiceLoader; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.HashMap; diff --git a/src/main/java/dev/lukebemish/codecextras/utility/LayeredServiceLoader.java b/src/main/java/dev/lukebemish/codecextras/internal/LayeredServiceLoader.java similarity index 98% rename from src/main/java/dev/lukebemish/codecextras/utility/LayeredServiceLoader.java rename to src/main/java/dev/lukebemish/codecextras/internal/LayeredServiceLoader.java index 9ab4c65..8bc7aed 100644 --- a/src/main/java/dev/lukebemish/codecextras/utility/LayeredServiceLoader.java +++ b/src/main/java/dev/lukebemish/codecextras/internal/LayeredServiceLoader.java @@ -1,4 +1,4 @@ -package dev.lukebemish.codecextras.utility; +package dev.lukebemish.codecextras.internal; import java.lang.ref.WeakReference; import java.util.LinkedHashMap; diff --git a/src/main/java/dev/lukebemish/codecextras/utility/Lazy.java b/src/main/java/dev/lukebemish/codecextras/internal/Lazy.java similarity index 93% rename from src/main/java/dev/lukebemish/codecextras/utility/Lazy.java rename to src/main/java/dev/lukebemish/codecextras/internal/Lazy.java index 1765412..c46cec9 100644 --- a/src/main/java/dev/lukebemish/codecextras/utility/Lazy.java +++ b/src/main/java/dev/lukebemish/codecextras/internal/Lazy.java @@ -1,4 +1,4 @@ -package dev.lukebemish.codecextras.utility; +package dev.lukebemish.codecextras.internal; import com.google.common.base.Suppliers; import java.util.function.Function; diff --git a/src/main/java/dev/lukebemish/codecextras/utility/package-info.java b/src/main/java/dev/lukebemish/codecextras/internal/package-info.java similarity index 72% rename from src/main/java/dev/lukebemish/codecextras/utility/package-info.java rename to src/main/java/dev/lukebemish/codecextras/internal/package-info.java index 390c2f6..5280b87 100644 --- a/src/main/java/dev/lukebemish/codecextras/utility/package-info.java +++ b/src/main/java/dev/lukebemish/codecextras/internal/package-info.java @@ -1,6 +1,6 @@ @NullMarked @ApiStatus.Internal -package dev.lukebemish.codecextras.utility; +package dev.lukebemish.codecextras.internal; import org.jetbrains.annotations.ApiStatus; import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/BuiltInReflectiveStructureCreator.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/BuiltInReflectiveStructureCreator.java index 071a1e1..f420134 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/BuiltInReflectiveStructureCreator.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/BuiltInReflectiveStructureCreator.java @@ -4,6 +4,7 @@ import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.gson.annotations.SerializedName; import com.mojang.datafixers.util.Either; import com.mojang.datafixers.util.Unit; import com.mojang.serialization.Dynamic; @@ -13,6 +14,7 @@ import dev.lukebemish.codecextras.structured.Structure; import dev.lukebemish.codecextras.structured.reflective.annotations.Annotated; import dev.lukebemish.codecextras.structured.reflective.annotations.Comment; +import dev.lukebemish.codecextras.structured.reflective.annotations.Lenient; import dev.lukebemish.codecextras.structured.reflective.annotations.SerializedProperty; import dev.lukebemish.codecextras.structured.reflective.annotations.Structured; import dev.lukebemish.codecextras.structured.reflective.annotations.Transient; @@ -83,6 +85,7 @@ import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedTransferQueue; import java.util.concurrent.TransferQueue; +import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Supplier; import org.jetbrains.annotations.ApiStatus; @@ -96,7 +99,7 @@ @AutoService(ReflectiveStructureCreator.class) public class BuiltInReflectiveStructureCreator implements ReflectiveStructureCreator { @Override - public Map, Creator> creators(CreationOptions options) { + public Map, Creator> creators(CreationContext options) { return ImmutableMap., Creator>builder() .put(Unit.class, () -> Structure.UNIT) .put(Boolean.class, () -> Structure.BOOL) @@ -272,6 +275,10 @@ private static Object parseValue(Value value) { @Override public Map, Function>>> annotationParsers(Set options) { var builder = ImmutableMap., Function>>>builder(); + var groovyIsolator = GroovyIsolator.getInstance(); + if (groovyIsolator != null) { + groovyIsolator.collectAnnotationParsers(builder); + } return builder .put(Annotated.class, (Annotated annotation) -> { Key key = (Key) parseValue(annotation.key()); @@ -351,7 +358,7 @@ public Map, Function>>> an throw new IllegalArgumentException("@Annotated must have exactly one value"); } Object finalValue = value; - List> list = List.of(new AnnotationInfo() { + List> list = List.>of(new AnnotationInfo() { @Override public Key key() { return key; @@ -364,22 +371,33 @@ public Object value() { }); return list; }) - .put(Comment.class, (Comment annotation) -> List.>of(new AnnotationInfo() { + .put(Comment.class, (Comment annotation) -> List.>of(new AnnotationInfo() { @Override - public Key key() { + public Key key() { return dev.lukebemish.codecextras.structured.Annotation.COMMENT; } @Override - public Object value() { + public String value() { return annotation.value(); } })) + .put(Lenient.class, (Lenient annotation) -> List.>of(new AnnotationInfo() { + @Override + public Key key() { + return dev.lukebemish.codecextras.structured.Annotation.LENIENT; + } + + @Override + public Unit value() { + return Unit.INSTANCE; + } + })) .build(); } @Override - public Map, ParameterizedCreator> parameterizedCreators(CreationOptions options) { + public Map, ParameterizedCreator> parameterizedCreators(CreationContext options) { return ImmutableMap., ParameterizedCreator>builder() .put(Either.class, (parameters) -> Structure.unboundedMap(parameters[0].create(), parameters[1].create())) // Collections @@ -480,20 +498,30 @@ private static CallSite asCallSite(MethodHandles.Lookup ignoredLookup, String ig return new ConstantCallSite(handle.asType(type)); } + @SuppressWarnings({"unchecked", "rawtypes"}) + @Override + public List structureContextualTransforms(Set options) { + return List.of( + (annotated, context) -> { + var annotations = annotated.stream().distinct().flatMap(a -> Arrays.stream(a.getAnnotations())).toList(); + var annotationInfo = annotations.stream() + .flatMap(a -> context.parseAnnotation(a).stream()) + .toList(); + + return structure -> { + Keys.Builder keys = Keys.builder(); + for (var info : annotationInfo) { + keys.add((Key) info.key(), new Identity<>(info.value())); + } + return structure.annotate(keys.build()); + }; + } + ); + } + @SuppressWarnings({"rawtypes", "unchecked"}) - public static Function add(CreationOptions options, RecordStructure builder, String name, Type type, Function getter, Function> creator, List annotated) { + public static Function add(CreationContext options, RecordStructure builder, String name, Type type, Function getter, Function> creator, List annotated) { var annotations = annotated.stream().distinct().flatMap(a -> Arrays.stream(a.getAnnotations())).toList(); - var annotationInfo = annotations.stream() - .flatMap(a -> options.parseAnnotation(a).stream()) - .toList(); - - Function structureUpdater = structure -> { - Keys.Builder keys = Keys.builder(); - for (var info : annotationInfo) { - keys.add((Key) info.key(), new Identity<>(info.value())); - } - return structure.annotate(keys.build()); - }; var structureInfos = annotations.stream() .map(info -> { @@ -507,36 +535,170 @@ private static CallSite asCallSite(MethodHandles.Lookup ignoredLookup, String ig throw new IllegalArgumentException("Multiple @Structured annotations found"); } - Structure explicitStructure = null; + Function structureUpdater = (Function) options.contextualTransform(annotated); + + var serializedName = name; + var serializedNameAnnotations = annotations.stream() + .map(info -> { + if (info instanceof SerializedName serializedNameAnnotation) { + return serializedNameAnnotation; + } + return null; + }).filter(Objects::nonNull).distinct().toList(); + + if (serializedNameAnnotations.size() > 1) { + throw new IllegalArgumentException("Multiple @SerializedName annotations found"); + } + + String[] otherNames = new String[0]; + + if (!serializedNameAnnotations.isEmpty()) { + serializedName = serializedNameAnnotations.getFirst().value(); + otherNames = serializedNameAnnotations.getFirst().alternate(); + } + + var namedCreator = structureByNameCreator(options, builder, type, getter, creator, structureInfos, structureUpdater); + + if (otherNames.length > 0) { + var list = new ArrayList(); + list.add(serializedName); + list.addAll(Arrays.asList(otherNames)); + return namedCreator.forNames(list); + } else { + return namedCreator.forName(serializedName); + } + } + + private interface StructureNamedCreator { + Function forNames(List names); + Function forName(String name); + + interface StructureMaker { + Function make(String name, boolean first); + } + + record StructureData(StructureMaker creator, BiFunction combiner) implements StructureNamedCreator { + @Override + public Function forNames(List names) { + if (names.isEmpty()) { + throw new IllegalArgumentException("No names provided"); + } + var first = creator.make(names.getFirst(), true); + var rest = new ArrayList>(); + for (int i = 1; i < names.size(); i++) { + rest.add(creator.make(names.get(i), false)); + } + if (rest.isEmpty()) { + return first; + } + return container -> { + var result = first.apply(container); + for (var f : rest) { + result = combiner.apply(result, f.apply(container)); + } + return result; + }; + } + + @Override + public Function forName(String name) { + return creator.make(name, true); + } + } + + + static StructureData> optional(StructureMaker> creator) { + return new StructureData<>(creator, (a, b) -> a.or(() -> b)); + } + + static StructureNamedCreator optionalInt(StructureMaker creator) { + return new StructureData<>(creator, (a, b) -> a.isPresent() ? a : b); + } + + static StructureNamedCreator optionalDouble(StructureMaker creator) { + return new StructureData<>(creator, (a, b) -> a.isPresent() ? a : b); + } + + static StructureNamedCreator optionalLong(StructureMaker creator) { + return new StructureData<>(creator, (a, b) -> a.isPresent() ? a : b); + } + + static StructureNamedCreator notOptional(StructureMaker creator) { + return new StructureNamedCreator<>() { + @Override + public Function forName(String name) { + return creator.make(name, true); + } + + @Override + public Function forNames(List names) { + if (names.isEmpty()) { + throw new IllegalArgumentException("No names provided"); + } + + if (names.size() == 1) { + return creator.make(names.getFirst(), true); + } + + var creators = new ArrayList>(); + creators.add(creator.make(names.getFirst(), true)); + for (int i = 1; i < names.size(); i++) { + creators.add(creator.make(names.get(i), false)); + } + + return container -> { + T result = null; + for (var f : creators) { + result = f.apply(container); + if (result != null) { + return result; + } + } + return result; + }; + } + }; + } + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private static StructureNamedCreator structureByNameCreator(CreationContext options, RecordStructure builder, Type type, Function getter, Function> creator, List structureInfos, Function structureUpdater) { + Structure mutableExplicitStructure = null; if (!structureInfos.isEmpty()) { - explicitStructure = (Structure) parseValue(structureInfos.getFirst().value()); + mutableExplicitStructure = (Structure) parseValue(structureInfos.getFirst().value()); if (structureInfos.getFirst().directOptional()) { - return ((Function>) builder.addOptional(name, structureUpdater.apply(explicitStructure), (Function) getter.andThen(Optional::ofNullable))) - .andThen(o -> o.orElse(null)); + final var explicitStructure = mutableExplicitStructure; + return StructureNamedCreator.notOptional((serializedName, first) -> ((Function>) builder.addOptional(serializedName, structureUpdater.apply(explicitStructure), first ? (Function) getter.andThen(Optional::ofNullable) : o -> Optional.empty())) + .andThen(o -> o.orElse(null))); } } + final var explicitStructure = mutableExplicitStructure; + if (type instanceof ParameterizedType parameterizedType && parameterizedType.getRawType() instanceof Class rawType) { if (rawType.equals(Optional.class)) { var innerType = parameterizedType.getActualTypeArguments()[0]; - return builder.addOptional(name, structureUpdater.apply(explicitStructure != null ? explicitStructure : creator.apply(innerType)), (Function) getter); + return StructureNamedCreator.optional((serializedName, first) -> builder.addOptional(serializedName, structureUpdater.apply(explicitStructure != null ? explicitStructure : creator.apply(innerType)), first ? (Function) getter : o -> Optional.empty())); } } else if (type instanceof Class clazz) { if (clazz.equals(OptionalInt.class)) { - return builder.addOptionalInt(name, structureUpdater.apply(explicitStructure != null ? explicitStructure : Structure.INT), (Function) getter); + return StructureNamedCreator.optionalInt((serializedName, first) -> builder.addOptionalInt(serializedName, structureUpdater.apply(explicitStructure != null ? explicitStructure : Structure.INT), first ? (Function) getter : o -> OptionalInt.empty())); } else if (clazz.equals(OptionalDouble.class)) { - return builder.addOptionalDouble(name, structureUpdater.apply(explicitStructure != null ? explicitStructure : Structure.DOUBLE), (Function) getter); + return StructureNamedCreator.optionalDouble((serializedName, first) -> builder.addOptionalDouble(serializedName, structureUpdater.apply(explicitStructure != null ? explicitStructure : Structure.DOUBLE), first ? (Function) getter : o -> OptionalDouble.empty())); } else if (clazz.equals(OptionalLong.class)) { - return builder.addOptionalLong(name, structureUpdater.apply(explicitStructure != null ? explicitStructure : Structure.LONG), (Function) getter); + return StructureNamedCreator.optionalLong((serializedName, first) -> builder.addOptionalLong(serializedName, structureUpdater.apply(explicitStructure != null ? explicitStructure : Structure.LONG), first ? (Function) getter : o -> OptionalLong.empty())); } } boolean isNotNull = options.hasOption(SimpleCreatorOption.NOT_NULL_BY_DEFAULT); - Function key; + StructureNamedCreator key; if (isNotNull || (type instanceof Class clazz && clazz.isPrimitive())) { - key = builder.add(name, structureUpdater.apply(explicitStructure != null ? explicitStructure : creator.apply(type)), (Function) getter); + key = StructureNamedCreator.notOptional((serializedName, first) -> first ? + builder.add(serializedName, structureUpdater.apply(explicitStructure != null ? explicitStructure : creator.apply(type)), (Function) getter) : + ((Function>) builder.addOptional(serializedName, structureUpdater.apply(explicitStructure != null ? explicitStructure : creator.apply(type)), o -> Optional.empty())) + .andThen(o -> o.orElse(null))); } else { - key = ((Function>) builder.addOptional(name, structureUpdater.apply(explicitStructure != null ? explicitStructure : creator.apply(type)), (Function) getter.andThen(Optional::ofNullable))) - .andThen(o -> o.orElse(null)); + key = StructureNamedCreator.notOptional((serializedName, first) -> ((Function>) builder.addOptional(serializedName, structureUpdater.apply(explicitStructure != null ? explicitStructure : creator.apply(type)), first ? (Function) getter.andThen(Optional::ofNullable) : o -> Optional.empty())) + .andThen(o -> o.orElse(null))); } return key; } @@ -721,7 +883,7 @@ private static boolean floatEquals(float a, float b) { } @Override - public List flexibleCreators(CreationOptions options) { + public List flexibleCreators(CreationContext options) { return ImmutableList.builder() .add(new FlexibleCreator() { @SuppressWarnings({"unchecked", "rawtypes"}) diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/CreationOptions.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/CreationContext.java similarity index 57% rename from src/main/java/dev/lukebemish/codecextras/structured/reflective/CreationOptions.java rename to src/main/java/dev/lukebemish/codecextras/structured/reflective/CreationContext.java index 45dfcab..d24f050 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/CreationOptions.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/CreationContext.java @@ -1,19 +1,23 @@ package dev.lukebemish.codecextras.structured.reflective; +import dev.lukebemish.codecextras.structured.Structure; import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Function; -public final class CreationOptions { +public final class CreationContext { private final Set options; private final Map, Function>>> annotationParsers; + private final List contextualTransforms; - CreationOptions(Collection options, Map, Function>>> annotationParsers) { + CreationContext(Collection options, Map, Function>>> annotationParsers, List contextualTransforms) { this.options = Set.copyOf(options); this.annotationParsers = annotationParsers; + this.contextualTransforms = contextualTransforms; } public boolean hasOption(CreationOption option) { @@ -28,4 +32,12 @@ public List> parseAnnotation(Annota } return List.of(); } + + public Function, Structure> contextualTransform(List elements) { + Function, Structure> function = Function.identity(); + for (var transform : contextualTransforms) { + function = function.andThen(transform.transform(elements, this)); + } + return function; + } } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/GroovyIsolator.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/GroovyIsolator.java new file mode 100644 index 0000000..271def0 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/GroovyIsolator.java @@ -0,0 +1,41 @@ +package dev.lukebemish.codecextras.structured.reflective; + +import com.google.common.collect.ImmutableMap; +import dev.lukebemish.codecextras.structured.Key; +import groovy.lang.Groovydoc; +import java.lang.annotation.Annotation; +import java.util.List; +import java.util.function.Function; +import org.jspecify.annotations.Nullable; + +interface GroovyIsolator { + void collectAnnotationParsers(ImmutableMap.Builder, Function>>> builder); + + static @Nullable GroovyIsolator getInstance() { + try { + var groovyObject = Class.forName("groovy.lang.GroovyObject"); + } catch (ClassNotFoundException e) { + return null; + } + + return new Impl(); + } + + final class Impl implements GroovyIsolator { + @Override + public void collectAnnotationParsers(ImmutableMap.Builder, Function>>> builder) { + builder + .put(Groovydoc.class, (Groovydoc annotation) -> List.>of(new ReflectiveStructureCreator.AnnotationInfo() { + @Override + public Key key() { + return dev.lukebemish.codecextras.structured.Annotation.COMMENT; + } + + @Override + public String value() { + return annotation.value(); + } + })); + } + } +} diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java index 0baf862..0f51a33 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java @@ -2,10 +2,11 @@ import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableSet; +import dev.lukebemish.codecextras.internal.LayeredServiceLoader; import dev.lukebemish.codecextras.structured.Key; import dev.lukebemish.codecextras.structured.Structure; -import dev.lukebemish.codecextras.utility.LayeredServiceLoader; import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; @@ -19,18 +20,21 @@ import java.util.function.Supplier; public interface ReflectiveStructureCreator { - default Map, Creator> creators(CreationOptions options) { + default Map, Creator> creators(CreationContext options) { return Map.of(); } - default Map, ParameterizedCreator> parameterizedCreators(CreationOptions options) { + default Map, ParameterizedCreator> parameterizedCreators(CreationContext options) { return Map.of(); } - default List flexibleCreators(CreationOptions options) { + default List flexibleCreators(CreationContext options) { return List.of(); } default Map, Function>>> annotationParsers(Set options) { return Map.of(); } + default List structureContextualTransforms(Set options) { + return List.of(); + } interface AnnotationInfo { Key key(); @@ -45,6 +49,13 @@ interface TypedCreator { interface Creator { Structure create(); + default Creator andThen(Function, Structure> function) { + return () -> function.apply(create()); + } + } + + interface ContextualTransform { + Function, Structure> transform(List elements, CreationContext context); } interface FlexibleCreator { @@ -66,17 +77,19 @@ default Creator creator(TypedCreator[] parameters) { } final class Instance { - private final Map, Function> creators; - private final Map, Function> parameterizedCreators; - private final List> flexibleCreators; + private final Map, Function> creators; + private final Map, Function> parameterizedCreators; + private final List> flexibleCreators; private final Map, Function, Function>>>> annotationParsers; + private final List, ContextualTransform>> contextualTransforms; private final List options; - private Instance(Map, Function> creators, Map, Function> parameterizedCreators, List> flexibleCreators, Map, Function, Function>>>> annotationParsers, List options) { + private Instance(Map, Function> creators, Map, Function> parameterizedCreators, List> flexibleCreators, Map, Function, Function>>>> annotationParsers, List, ContextualTransform>> contextualTransforms, List options) { this.creators = creators; this.parameterizedCreators = parameterizedCreators; this.flexibleCreators = flexibleCreators; this.annotationParsers = annotationParsers; + this.contextualTransforms = contextualTransforms; this.options = options; } @@ -85,11 +98,12 @@ public static Builder builder() { } public static final class Builder { - private final Map, Function> creators = new IdentityHashMap<>(); - private final Map, Function> parameterizedCreators = new IdentityHashMap<>(); - private final List> flexibleCreators = new ArrayList<>(); + private final Map, Function> creators = new IdentityHashMap<>(); + private final Map, Function> parameterizedCreators = new IdentityHashMap<>(); + private final List> flexibleCreators = new ArrayList<>(); private final List options = new ArrayList<>(); private final Map, Function, Function>>>> annotationParsers = new IdentityHashMap<>(); + private final List, ContextualTransform>> contextualTransforms = new ArrayList<>(); private Builder() {} @@ -118,8 +132,13 @@ public Builder withAnnotationParser(Class annotation, return this; } + public Builder withContextualTransform(Function, ContextualTransform> transform) { + contextualTransforms.add(transform); + return this; + } + public Instance build() { - return new Instance(creators, parameterizedCreators, flexibleCreators, annotationParsers, options); + return new Instance(creators, parameterizedCreators, flexibleCreators, annotationParsers, contextualTransforms, options); } } @@ -140,7 +159,13 @@ public synchronized Structure create(Class clazz) { annotationParsersMap.put(key, function.apply(creationOptions)); }); - var options = new CreationOptions(this.options, annotationParsersMap); + List contextualTransformList = new ArrayList<>(); + services.forEach(creator -> contextualTransformList.addAll(creator.structureContextualTransforms(creationOptions))); + this.contextualTransforms.forEach(function -> { + contextualTransformList.add(function.apply(creationOptions)); + }); + + var options = new CreationContext(this.options, annotationParsersMap, contextualTransformList); Map, Creator> creatorsMap = new IdentityHashMap<>(); Map, ParameterizedCreator> parameterizedCreatorsMap = new IdentityHashMap<>(); diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Lenient.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Lenient.java new file mode 100644 index 0000000..c1a541f --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Lenient.java @@ -0,0 +1,11 @@ +package dev.lukebemish.codecextras.structured.reflective.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.METHOD, ElementType.RECORD_COMPONENT, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Lenient { +} diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 6106bd4..0017972 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -14,7 +14,8 @@ requires static org.jspecify; requires static org.objectweb.asm; requires static org.slf4j; - requires com.google.auto.service; + requires static com.google.auto.service; + requires static org.apache.groovy; exports dev.lukebemish.codecextras; exports dev.lukebemish.codecextras.comments; @@ -37,5 +38,5 @@ exports dev.lukebemish.codecextras.types; - exports dev.lukebemish.codecextras.utility to codecextras_minecraft, dev.lukebemish.codecextras.minecraft; + exports dev.lukebemish.codecextras.internal to codecextras_minecraft, dev.lukebemish.codecextras.minecraft; } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftReflectiveStructureCreator.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftReflectiveStructureCreator.java index 4acb373..62161b0 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftReflectiveStructureCreator.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftReflectiveStructureCreator.java @@ -5,7 +5,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import dev.lukebemish.codecextras.structured.Structure; -import dev.lukebemish.codecextras.structured.reflective.CreationOptions; +import dev.lukebemish.codecextras.structured.reflective.CreationContext; import dev.lukebemish.codecextras.structured.reflective.ReflectiveStructureCreator; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Type; @@ -22,7 +22,7 @@ @AutoService(ReflectiveStructureCreator.class) public class MinecraftReflectiveStructureCreator implements ReflectiveStructureCreator { @Override - public Map, Creator> creators(CreationOptions options) { + public Map, Creator> creators(CreationContext options) { return ImmutableMap., Creator>builder() .put(ResourceLocation.class, () -> MinecraftStructures.RESOURCE_LOCATION) .put(DataComponentMap.class, () -> MinecraftStructures.DATA_COMPONENT_MAP) @@ -32,13 +32,13 @@ public Map, Creator> creators(CreationOptions options) { } @Override - public Map, ParameterizedCreator> parameterizedCreators(CreationOptions options) { + public Map, ParameterizedCreator> parameterizedCreators(CreationContext options) { return ImmutableMap., ParameterizedCreator>builder() .build(); } @Override - public List flexibleCreators(CreationOptions options) { + public List flexibleCreators(CreationContext options) { return ImmutableList.builder() .add(new FlexibleCreator() { @Override diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java index 279cb3c..011d5a2 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java @@ -21,6 +21,7 @@ import com.mojang.serialization.DynamicOps; import com.mojang.serialization.codecs.PrimitiveCodec; import dev.lukebemish.codecextras.StringRepresentation; +import dev.lukebemish.codecextras.internal.Lazy; import dev.lukebemish.codecextras.minecraft.structured.MinecraftInterpreters; import dev.lukebemish.codecextras.minecraft.structured.MinecraftKeys; import dev.lukebemish.codecextras.minecraft.structured.MinecraftStructures; @@ -36,7 +37,6 @@ import dev.lukebemish.codecextras.structured.RecordStructure; import dev.lukebemish.codecextras.structured.Structure; import dev.lukebemish.codecextras.types.Identity; -import dev.lukebemish.codecextras.utility.Lazy; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Comparator; diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/EntryCreationInfo.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/EntryCreationInfo.java index 08e89ef..ab52241 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/EntryCreationInfo.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/EntryCreationInfo.java @@ -1,7 +1,7 @@ package dev.lukebemish.codecextras.minecraft.structured.config; import com.mojang.serialization.Codec; -import dev.lukebemish.codecextras.utility.Lazy; +import dev.lukebemish.codecextras.internal.Lazy; import java.util.function.UnaryOperator; public record EntryCreationInfo(Codec codec, Lazy componentInfo) { From 5e034440a2d2571b3d9ba812198200f85126d599 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Sun, 9 Mar 2025 13:06:49 -0500 Subject: [PATCH 21/28] Move groovy stuff to its own feature variant --- build.gradle | 21 +++++++++- .../GroovyReflectiveStructureCreator.java | 36 ++++++++++++++++ .../implementation/package-info.java | 6 +++ src/groovy/java/module-info.java | 19 +++++++++ .../structured/reflective/GroovyIsolator.java | 41 ------------------- .../BuiltInReflectiveStructureCreator.java | 10 ++--- .../implementation/package-info.java | 6 +++ src/main/java/module-info.java | 5 ++- .../RegistryOpsCompanionRetriever.java | 2 + 9 files changed, 97 insertions(+), 49 deletions(-) create mode 100644 src/groovy/java/dev/lukebemish/codecextras/groovy/structured/reflective/implementation/GroovyReflectiveStructureCreator.java create mode 100644 src/groovy/java/dev/lukebemish/codecextras/groovy/structured/reflective/implementation/package-info.java create mode 100644 src/groovy/java/module-info.java delete mode 100644 src/main/java/dev/lukebemish/codecextras/structured/reflective/GroovyIsolator.java rename src/main/java/dev/lukebemish/codecextras/structured/reflective/{ => implementation}/BuiltInReflectiveStructureCreator.java (99%) create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/reflective/implementation/package-info.java diff --git a/build.gradle b/build.gradle index 986aae2..d5a20e4 100644 --- a/build.gradle +++ b/build.gradle @@ -103,6 +103,7 @@ sourceSets { minecraft {} minecraftFabric {} jmh {} + groovy {} } configurations { @@ -135,6 +136,11 @@ java { toolchain.languageVersion.set(JavaLanguageVersion.of(21)) withSourcesJar() withJavadocJar() + registerFeature("groovy") { + usingSourceSet sourceSets.groovy + withSourcesJar() + withJavadocJar() + } registerFeature("minecraft") { usingSourceSet sourceSets.minecraft withSourcesJar() @@ -184,6 +190,11 @@ dependencies { implementation 'org.ow2.asm:asm:9.5' + groovyApi project(':') + groovyApi 'org.apache.groovy:groovy:4.0.26' + groovyCompileOnly cLibs.bundles.compileonly + groovyAnnotationProcessor cLibs.bundles.annotationprocessor + jmhCompileOnly cLibs.bundles.compileonly jmhImplementation project(':') jmhImplementation 'org.openjdk.jmh:jmh-core:1.37' @@ -201,8 +212,6 @@ dependencies { compileOnly 'com.electronwill.night-config:toml:3.6.4' compileOnly 'blue.endless:jankson:1.2.2' - compileOnly 'org.apache.groovy:groovy:4.0.26' - testImplementation 'com.electronwill.night-config:core:3.6.4' testImplementation 'com.electronwill.night-config:toml:3.6.4' testImplementation 'blue.endless:jankson:1.2.2' @@ -269,6 +278,14 @@ tasks.named('jar', Jar) { } } +tasks.named('groovyJar', Jar) { + manifest { + attributes( + 'Automatic-Module-Name': project.group + '.' + project.name + '.groovy' + ) + } +} + ['minecraftJar', 'minecraftFabricJar', 'minecraftNeoforgeJar'].each { tasks.named(it, Jar) { manifest { diff --git a/src/groovy/java/dev/lukebemish/codecextras/groovy/structured/reflective/implementation/GroovyReflectiveStructureCreator.java b/src/groovy/java/dev/lukebemish/codecextras/groovy/structured/reflective/implementation/GroovyReflectiveStructureCreator.java new file mode 100644 index 0000000..9ad3541 --- /dev/null +++ b/src/groovy/java/dev/lukebemish/codecextras/groovy/structured/reflective/implementation/GroovyReflectiveStructureCreator.java @@ -0,0 +1,36 @@ +package dev.lukebemish.codecextras.groovy.structured.reflective.implementation; + +import com.google.auto.service.AutoService; +import com.google.common.collect.ImmutableMap; +import dev.lukebemish.codecextras.structured.Key; +import dev.lukebemish.codecextras.structured.reflective.CreationOption; +import dev.lukebemish.codecextras.structured.reflective.ReflectiveStructureCreator; +import groovy.lang.Groovydoc; +import java.lang.annotation.Annotation; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import org.jetbrains.annotations.ApiStatus; + +@AutoService(ReflectiveStructureCreator.class) +@ApiStatus.Internal +public class GroovyReflectiveStructureCreator implements ReflectiveStructureCreator { + @Override + public Map, Function>>> annotationParsers(Set options) { + var builder = ImmutableMap., Function>>>builder(); + return builder + .put(Groovydoc.class, (Groovydoc annotation) -> List.>of(new ReflectiveStructureCreator.AnnotationInfo() { + @Override + public Key key() { + return dev.lukebemish.codecextras.structured.Annotation.COMMENT; + } + + @Override + public String value() { + return annotation.value(); + } + })) + .build(); + } +} diff --git a/src/groovy/java/dev/lukebemish/codecextras/groovy/structured/reflective/implementation/package-info.java b/src/groovy/java/dev/lukebemish/codecextras/groovy/structured/reflective/implementation/package-info.java new file mode 100644 index 0000000..92f11d2 --- /dev/null +++ b/src/groovy/java/dev/lukebemish/codecextras/groovy/structured/reflective/implementation/package-info.java @@ -0,0 +1,6 @@ +@NullMarked +@ApiStatus.Internal +package dev.lukebemish.codecextras.groovy.structured.reflective.implementation; + +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.NullMarked; diff --git a/src/groovy/java/module-info.java b/src/groovy/java/module-info.java new file mode 100644 index 0000000..e0b73f7 --- /dev/null +++ b/src/groovy/java/module-info.java @@ -0,0 +1,19 @@ +import dev.lukebemish.codecextras.groovy.structured.reflective.implementation.GroovyReflectiveStructureCreator; +import dev.lukebemish.codecextras.structured.reflective.ReflectiveStructureCreator; + +module dev.lukebemish.codeceextras.groovy { + requires static org.jetbrains.annotations; + requires static org.jspecify; + requires static org.slf4j; + requires static com.google.auto.service; + + requires dev.lukebemish.codecextras; + requires org.apache.groovy; + + requires com.google.common; + requires com.google.gson; + requires datafixerupper; + requires it.unimi.dsi.fastutil; + + provides ReflectiveStructureCreator with GroovyReflectiveStructureCreator; +} diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/GroovyIsolator.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/GroovyIsolator.java deleted file mode 100644 index 271def0..0000000 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/GroovyIsolator.java +++ /dev/null @@ -1,41 +0,0 @@ -package dev.lukebemish.codecextras.structured.reflective; - -import com.google.common.collect.ImmutableMap; -import dev.lukebemish.codecextras.structured.Key; -import groovy.lang.Groovydoc; -import java.lang.annotation.Annotation; -import java.util.List; -import java.util.function.Function; -import org.jspecify.annotations.Nullable; - -interface GroovyIsolator { - void collectAnnotationParsers(ImmutableMap.Builder, Function>>> builder); - - static @Nullable GroovyIsolator getInstance() { - try { - var groovyObject = Class.forName("groovy.lang.GroovyObject"); - } catch (ClassNotFoundException e) { - return null; - } - - return new Impl(); - } - - final class Impl implements GroovyIsolator { - @Override - public void collectAnnotationParsers(ImmutableMap.Builder, Function>>> builder) { - builder - .put(Groovydoc.class, (Groovydoc annotation) -> List.>of(new ReflectiveStructureCreator.AnnotationInfo() { - @Override - public Key key() { - return dev.lukebemish.codecextras.structured.Annotation.COMMENT; - } - - @Override - public String value() { - return annotation.value(); - } - })); - } - } -} diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/BuiltInReflectiveStructureCreator.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/implementation/BuiltInReflectiveStructureCreator.java similarity index 99% rename from src/main/java/dev/lukebemish/codecextras/structured/reflective/BuiltInReflectiveStructureCreator.java rename to src/main/java/dev/lukebemish/codecextras/structured/reflective/implementation/BuiltInReflectiveStructureCreator.java index f420134..717426a 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/BuiltInReflectiveStructureCreator.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/implementation/BuiltInReflectiveStructureCreator.java @@ -1,4 +1,4 @@ -package dev.lukebemish.codecextras.structured.reflective; +package dev.lukebemish.codecextras.structured.reflective.implementation; import com.google.auto.service.AutoService; import com.google.common.base.Suppliers; @@ -12,6 +12,10 @@ import dev.lukebemish.codecextras.structured.Keys; import dev.lukebemish.codecextras.structured.RecordStructure; import dev.lukebemish.codecextras.structured.Structure; +import dev.lukebemish.codecextras.structured.reflective.CreationContext; +import dev.lukebemish.codecextras.structured.reflective.CreationOption; +import dev.lukebemish.codecextras.structured.reflective.ReflectiveStructureCreator; +import dev.lukebemish.codecextras.structured.reflective.SimpleCreatorOption; import dev.lukebemish.codecextras.structured.reflective.annotations.Annotated; import dev.lukebemish.codecextras.structured.reflective.annotations.Comment; import dev.lukebemish.codecextras.structured.reflective.annotations.Lenient; @@ -275,10 +279,6 @@ private static Object parseValue(Value value) { @Override public Map, Function>>> annotationParsers(Set options) { var builder = ImmutableMap., Function>>>builder(); - var groovyIsolator = GroovyIsolator.getInstance(); - if (groovyIsolator != null) { - groovyIsolator.collectAnnotationParsers(builder); - } return builder .put(Annotated.class, (Annotated annotation) -> { Key key = (Key) parseValue(annotation.key()); diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/implementation/package-info.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/implementation/package-info.java new file mode 100644 index 0000000..1063f1d --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/implementation/package-info.java @@ -0,0 +1,6 @@ +@NullMarked +@ApiStatus.Internal +package dev.lukebemish.codecextras.structured.reflective.implementation; + +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 0017972..1287e26 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -1,3 +1,5 @@ +import dev.lukebemish.codecextras.structured.reflective.implementation.BuiltInReflectiveStructureCreator; + module dev.lukebemish.codecextras { uses dev.lukebemish.codecextras.companion.AlternateCompanionRetriever; uses dev.lukebemish.codecextras.structured.reflective.ReflectiveStructureCreator; @@ -15,7 +17,6 @@ requires static org.objectweb.asm; requires static org.slf4j; requires static com.google.auto.service; - requires static org.apache.groovy; exports dev.lukebemish.codecextras; exports dev.lukebemish.codecextras.comments; @@ -39,4 +40,6 @@ exports dev.lukebemish.codecextras.types; exports dev.lukebemish.codecextras.internal to codecextras_minecraft, dev.lukebemish.codecextras.minecraft; + + provides dev.lukebemish.codecextras.structured.reflective.ReflectiveStructureCreator with BuiltInReflectiveStructureCreator; } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/companion/RegistryOpsCompanionRetriever.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/companion/RegistryOpsCompanionRetriever.java index c06ec13..cf35bcb 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/companion/RegistryOpsCompanionRetriever.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/companion/RegistryOpsCompanionRetriever.java @@ -9,8 +9,10 @@ import java.util.Optional; import net.minecraft.resources.DelegatingOps; import net.minecraft.resources.RegistryOps; +import org.jetbrains.annotations.ApiStatus; @AutoService(AlternateCompanionRetriever.class) +@ApiStatus.Internal public class RegistryOpsCompanionRetriever implements AlternateCompanionRetriever { static final MethodHandle DELEGATE_FIELD; From a18fb9b538edaee8cab5c92fadbbd0ad924b2ace Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Sun, 9 Mar 2025 16:50:56 -0500 Subject: [PATCH 22/28] Switch to new CreatorSystem setup for greater flexibility in reflective system discovery --- .../GroovyReflectiveStructureCreator.java | 42 ++-- .../codecextras/structured/Keys.java | 5 + .../reflective/CreationContext.java | 35 ++- .../ReflectiveStructureCreator.java | 228 ++++++++++-------- .../BuiltInReflectiveStructureCreator.java | 30 ++- .../reflective/systems/AnnotationParsers.java | 28 +++ .../systems/ContextualTransforms.java | 49 ++++ .../reflective/systems/CreationOptions.java | 44 ++++ .../reflective/systems/Creators.java | 26 ++ .../reflective/systems/FlexibleCreators.java | 35 +++ .../systems/ParameterizedCreators.java | 26 ++ .../reflective/systems/package-info.java | 6 + src/main/java/module-info.java | 2 + .../MinecraftReflectiveStructureCreator.java | 84 ++++--- 14 files changed, 459 insertions(+), 181 deletions(-) create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/AnnotationParsers.java create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/ContextualTransforms.java create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/CreationOptions.java create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/Creators.java create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/FlexibleCreators.java create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/ParameterizedCreators.java create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/package-info.java diff --git a/src/groovy/java/dev/lukebemish/codecextras/groovy/structured/reflective/implementation/GroovyReflectiveStructureCreator.java b/src/groovy/java/dev/lukebemish/codecextras/groovy/structured/reflective/implementation/GroovyReflectiveStructureCreator.java index 9ad3541..60480ac 100644 --- a/src/groovy/java/dev/lukebemish/codecextras/groovy/structured/reflective/implementation/GroovyReflectiveStructureCreator.java +++ b/src/groovy/java/dev/lukebemish/codecextras/groovy/structured/reflective/implementation/GroovyReflectiveStructureCreator.java @@ -3,13 +3,14 @@ import com.google.auto.service.AutoService; import com.google.common.collect.ImmutableMap; import dev.lukebemish.codecextras.structured.Key; -import dev.lukebemish.codecextras.structured.reflective.CreationOption; +import dev.lukebemish.codecextras.structured.Keys; +import dev.lukebemish.codecextras.structured.reflective.CreationContext; import dev.lukebemish.codecextras.structured.reflective.ReflectiveStructureCreator; +import dev.lukebemish.codecextras.structured.reflective.systems.AnnotationParsers; import groovy.lang.Groovydoc; import java.lang.annotation.Annotation; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.function.Function; import org.jetbrains.annotations.ApiStatus; @@ -17,20 +18,29 @@ @ApiStatus.Internal public class GroovyReflectiveStructureCreator implements ReflectiveStructureCreator { @Override - public Map, Function>>> annotationParsers(Set options) { - var builder = ImmutableMap., Function>>>builder(); - return builder - .put(Groovydoc.class, (Groovydoc annotation) -> List.>of(new ReflectiveStructureCreator.AnnotationInfo() { - @Override - public Key key() { - return dev.lukebemish.codecextras.structured.Annotation.COMMENT; - } + public Keys systems() { + var builder = Keys.builder(); + builder.add(AnnotationParsers.TYPE.key(), new AnnotationParsers() { + @Override + public Function, Function>>>> make() { + return context -> { + var builder = ImmutableMap., Function>>>builder(); + return builder + .put(Groovydoc.class, (Groovydoc annotation) -> List.>of(new ReflectiveStructureCreator.AnnotationInfo() { + @Override + public Key key() { + return dev.lukebemish.codecextras.structured.Annotation.COMMENT; + } - @Override - public String value() { - return annotation.value(); - } - })) - .build(); + @Override + public String value() { + return annotation.value(); + } + })) + .build(); + }; + } + }); + return builder.build(); } } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Keys.java b/src/main/java/dev/lukebemish/codecextras/structured/Keys.java index 23555e2..f664efd 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Keys.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Keys.java @@ -5,6 +5,7 @@ import java.util.IdentityHashMap; import java.util.Map; import java.util.Optional; +import java.util.Set; /** * A collection of keys and their associated values. Each key is parameterized by a type extending {@code L}, and a @@ -40,6 +41,10 @@ public Keys map(Converter converter) { return new Keys<>(map); } + public Set> keys() { + return keys.keySet(); + } + /** * Effectively "lifts" values from {@code Mu} to {@code N}. Type parameters are bounded by {@code L}. * @param diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/CreationContext.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/CreationContext.java index d24f050..55941a7 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/CreationContext.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/CreationContext.java @@ -1,31 +1,47 @@ package dev.lukebemish.codecextras.structured.reflective; import dev.lukebemish.codecextras.structured.Structure; +import dev.lukebemish.codecextras.structured.reflective.systems.AnnotationParsers; +import dev.lukebemish.codecextras.structured.reflective.systems.ContextualTransforms; +import dev.lukebemish.codecextras.structured.reflective.systems.CreationOptions; import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; -import java.util.Collection; +import java.util.IdentityHashMap; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.function.Function; public final class CreationContext { - private final Set options; - private final Map, Function>>> annotationParsers; - private final List contextualTransforms; + private final Map, Object> systems; + private final Map, Object> bakedSystems = new IdentityHashMap<>(); - CreationContext(Collection options, Map, Function>>> annotationParsers, List contextualTransforms) { - this.options = Set.copyOf(options); - this.annotationParsers = annotationParsers; - this.contextualTransforms = contextualTransforms; + CreationContext(Map, Object> systems) { + this.systems = systems; } public boolean hasOption(CreationOption option) { + var options = retrieve(CreationOptions.TYPE); return options.contains(option); } + @SuppressWarnings("unchecked") + public synchronized T retrieve(ReflectiveStructureCreator.CreatorSystem.Type type) { + var existingBaked = bakedSystems.get(type); + if (existingBaked != null) { + return (T) existingBaked; + } + var existing = systems.get(type); + if (existing != null) { + var baked = type.bake((R) existing, this); + bakedSystems.put(type, baked); + return baked; + } + return (T) bakedSystems.computeIfAbsent(type, t -> type.bake(type.empty(), this)); + } + @SuppressWarnings({"unchecked", "rawtypes"}) public List> parseAnnotation(Annotation annotation) { + var annotationParsers = retrieve(AnnotationParsers.TYPE); var function = (Function) annotationParsers.get(annotation.annotationType()); if (function != null) { return (List>) function.apply(annotation); @@ -34,6 +50,7 @@ public List> parseAnnotation(Annota } public Function, Structure> contextualTransform(List elements) { + var contextualTransforms = retrieve(ContextualTransforms.TYPE); Function, Structure> function = Function.identity(); for (var transform : contextualTransforms) { function = function.andThen(transform.transform(elements, this)); diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java index 0f51a33..4f3d265 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java @@ -1,11 +1,17 @@ package dev.lukebemish.codecextras.structured.reflective; import com.google.common.base.Suppliers; -import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.mojang.datafixers.kinds.App; +import com.mojang.datafixers.kinds.K1; import dev.lukebemish.codecextras.internal.LayeredServiceLoader; import dev.lukebemish.codecextras.structured.Key; +import dev.lukebemish.codecextras.structured.Keys; import dev.lukebemish.codecextras.structured.Structure; -import java.lang.annotation.Annotation; +import dev.lukebemish.codecextras.structured.reflective.systems.Creators; +import dev.lukebemish.codecextras.structured.reflective.systems.FlexibleCreators; +import dev.lukebemish.codecextras.structured.reflective.systems.ParameterizedCreators; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; @@ -15,25 +21,88 @@ import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Set; +import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; public interface ReflectiveStructureCreator { - default Map, Creator> creators(CreationContext options) { - return Map.of(); - } - default Map, ParameterizedCreator> parameterizedCreators(CreationContext options) { - return Map.of(); - } - default List flexibleCreators(CreationContext options) { - return List.of(); + interface CreatorSystem> extends App { + final class Mu implements K1 { private Mu() {} } + + O type(); + + interface Type> { + R merge(R a, R b); + R empty(); + Key key(); + T bake(R value, CreationContext context); + } + + interface ListType> extends Type, Function>, O> { + @Override + default Function> merge(Function> a, Function> b) { + return context -> { + var out = a.apply(context); + out.addAll(b.apply(context)); + return out; + }; + } + + @Override + default Function> empty() { + return c -> new ArrayList<>(); + } + + @Override + default List bake(Function> value, CreationContext context) { + var out = ImmutableList.builder(); + out.addAll(value.apply(context)); + return out.build(); + } + } + + interface MapType> extends Type, Function>, O> { + @Override + default Function> merge(Function> a, Function> b) { + return context -> { + var out = a.apply(context); + out.putAll(b.apply(context)); + return out; + }; + } + + @Override + default Function> empty() { + return c -> new HashMap<>(); + } + + @Override + default Map bake(Function> value, CreationContext context) { + var out = ImmutableMap.builder(); + out.putAll(value.apply(context)); + return out.build(); + } + } + + interface IdentityMapType> extends MapType { + @Override + default Function> empty() { + return c -> new IdentityHashMap<>(); + } + } + + R make(); } - default Map, Function>>> annotationParsers(Set options) { - return Map.of(); + + @SuppressWarnings("unchecked") + private static R mergeUnchecked(Object existing, Object specific, CreatorSystem.Type type) { + R existingCast = (R) existing; + R specificCast = (R) specific; + return type.merge(existingCast, specificCast); } - default List structureContextualTransforms(Set options) { - return List.of(); + + default Keys systems() { + return Keys.builder().build(); } interface AnnotationInfo { @@ -49,13 +118,13 @@ interface TypedCreator { interface Creator { Structure create(); - default Creator andThen(Function, Structure> function) { - return () -> function.apply(create()); - } } interface ContextualTransform { Function, Structure> transform(List elements, CreationContext context); + default int priority() { + return 0; + } } interface FlexibleCreator { @@ -77,20 +146,10 @@ default Creator creator(TypedCreator[] parameters) { } final class Instance { - private final Map, Function> creators; - private final Map, Function> parameterizedCreators; - private final List> flexibleCreators; - private final Map, Function, Function>>>> annotationParsers; - private final List, ContextualTransform>> contextualTransforms; - private final List options; - - private Instance(Map, Function> creators, Map, Function> parameterizedCreators, List> flexibleCreators, Map, Function, Function>>>> annotationParsers, List, ContextualTransform>> contextualTransforms, List options) { - this.creators = creators; - this.parameterizedCreators = parameterizedCreators; - this.flexibleCreators = flexibleCreators; - this.annotationParsers = annotationParsers; - this.contextualTransforms = contextualTransforms; - this.options = options; + private final Keys systems; + + private Instance(Keys systems) { + this.systems = systems; } public static Builder builder() { @@ -98,47 +157,17 @@ public static Builder builder() { } public static final class Builder { - private final Map, Function> creators = new IdentityHashMap<>(); - private final Map, Function> parameterizedCreators = new IdentityHashMap<>(); - private final List> flexibleCreators = new ArrayList<>(); - private final List options = new ArrayList<>(); - private final Map, Function, Function>>>> annotationParsers = new IdentityHashMap<>(); - private final List, ContextualTransform>> contextualTransforms = new ArrayList<>(); + private final Keys.Builder systems = Keys.builder(); private Builder() {} - public Builder withCreator(Class clazz, Creator creator) { - creators.put(clazz, ignored -> creator); - return this; - } - - public Builder withParameterizedCreator(Class clazz, ParameterizedCreator creator) { - parameterizedCreators.put(clazz, ignored -> creator); - return this; - } - - public Builder withFlexibleCreator(FlexibleCreator creator) { - flexibleCreators.add(ignored -> creator); - return this; - } - - public Builder withOption(CreationOption option) { - options.add(option); - return this; - } - - public Builder withAnnotationParser(Class annotation, Function>> discoverer) { - annotationParsers.put(annotation, ignored -> discoverer); - return this; - } - - public Builder withContextualTransform(Function, ContextualTransform> transform) { - contextualTransforms.add(transform); + public > Builder add(CreatorSystem system) { + systems.add(system.type().key(), system); return this; } public Instance build() { - return new Instance(creators, parameterizedCreators, flexibleCreators, annotationParsers, contextualTransforms, options); + return new Instance(systems.build()); } } @@ -151,46 +180,28 @@ public synchronized Structure create(Class clazz) { var caller = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE).getCallerClass(); List services = LayeredServiceLoader.unique(SERVICE_LOADER.at(ReflectiveStructureCreator.class), SERVICE_LOADER.at(clazz), SERVICE_LOADER.at(caller)); - var creationOptions = ImmutableSet.copyOf(this.options); - - Map, Function>>> annotationParsersMap = new IdentityHashMap<>(); - services.forEach(creator -> annotationParsersMap.putAll(creator.annotationParsers(creationOptions))); - this.annotationParsers.forEach((key, function) -> { - annotationParsersMap.put(key, function.apply(creationOptions)); - }); - - List contextualTransformList = new ArrayList<>(); - services.forEach(creator -> contextualTransformList.addAll(creator.structureContextualTransforms(creationOptions))); - this.contextualTransforms.forEach(function -> { - contextualTransformList.add(function.apply(creationOptions)); - }); - - var options = new CreationContext(this.options, annotationParsersMap, contextualTransformList); - - Map, Creator> creatorsMap = new IdentityHashMap<>(); - Map, ParameterizedCreator> parameterizedCreatorsMap = new IdentityHashMap<>(); - List flexibleCreatorsList = new ArrayList<>(); - services.forEach(creator -> { - creatorsMap.putAll(creator.creators(options)); - parameterizedCreatorsMap.putAll(creator.parameterizedCreators(options)); - flexibleCreatorsList.addAll(creator.flexibleCreators(options)); - }); - - this.creators.forEach((key, function) -> { - creatorsMap.put(key, function.apply(options)); - }); - this.parameterizedCreators.forEach((key, function) -> { - parameterizedCreatorsMap.put(key, function.apply(options)); - }); - this.flexibleCreators.forEach(function -> { - flexibleCreatorsList.add(function.apply(options)); - }); - - flexibleCreatorsList.sort((a, b) -> Integer.compare(b.priority(), a.priority())); + Map, Object> systemsMap = new IdentityHashMap<>(); + Consumer> addSystems = systems -> { + systems.keys().forEach(key -> { + var value = (CreatorSystem) systems.get(key).orElseThrow(); + var type = value.type(); + systemsMap.compute(type, (k, existing) -> + mergeUnchecked( + Objects.requireNonNullElseGet(existing, type::empty), + value.make(), + type + ) + ); + }); + }; + services.forEach(creator -> addSystems.accept(creator.systems())); + addSystems.accept(this.systems); + + var context = new CreationContext(systemsMap); var recursionCache = new HashMap>(); - return (Structure) forType(cachedCreators, recursionCache, clazz, creatorsMap, parameterizedCreatorsMap, flexibleCreatorsList); + return (Structure) forType(cachedCreators, recursionCache, clazz, context); } } @@ -198,13 +209,18 @@ static Structure create(Class clazz) { return Instance.builder().build().create(clazz); } - private static Structure forType(Map> cachedCreators, Map> recursionCache, Type type, Map, Creator> creatorsMap, Map, ParameterizedCreator> parameterizedCreatorsMap, List flexibleCreators) { + private static Structure forType(Map> cachedCreators, Map> recursionCache, Type type, CreationContext context) { if (cachedCreators.containsKey(type)) { return cachedCreators.get(type); } if (recursionCache.containsKey(type)) { return recursionCache.get(type); } + + var creatorsMap = context.retrieve(Creators.TYPE); + var parameterizedCreatorsMap = context.retrieve(ParameterizedCreators.TYPE); + var flexibleCreators = context.retrieve(FlexibleCreators.TYPE); + @SuppressWarnings({"rawtypes", "unchecked"}) Supplier> full = Suppliers.memoize(() -> Structure.recursive((Function) (Function) (Structure itself) -> { recursionCache.put(type, itself); @@ -218,7 +234,7 @@ private static Structure forType(Map> cachedCreators, Map< parameterCreators = new TypedCreator[parameters.length]; if (parameterizedCreatorsMap.containsKey(clazz)) { for (int i = 0; i < parameters.length; i++) { - var structure = forType(cachedCreators, recursionCache, parameters[i], creatorsMap, parameterizedCreatorsMap, flexibleCreators); + var structure = forType(cachedCreators, recursionCache, parameters[i], context); var parameterType = parameters[i]; parameterCreators[i] = new TypedCreator() { @Override @@ -259,7 +275,7 @@ public Class rawType() { for (var flexibleCreator : flexibleCreators) { if (flexibleCreator.supports(Objects.requireNonNull(rawType), parameterCreators)) { - return flexibleCreator.creator(rawType, parameterCreators, type1 -> forType(cachedCreators, recursionCache, type1, creatorsMap, parameterizedCreatorsMap, flexibleCreators)); + return flexibleCreator.creator(rawType, parameterCreators, type1 -> forType(cachedCreators, recursionCache, type1, context)); } } throw new IllegalArgumentException("No creator found for type: " + type); diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/implementation/BuiltInReflectiveStructureCreator.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/implementation/BuiltInReflectiveStructureCreator.java index 717426a..13eedd9 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/implementation/BuiltInReflectiveStructureCreator.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/implementation/BuiltInReflectiveStructureCreator.java @@ -13,7 +13,6 @@ import dev.lukebemish.codecextras.structured.RecordStructure; import dev.lukebemish.codecextras.structured.Structure; import dev.lukebemish.codecextras.structured.reflective.CreationContext; -import dev.lukebemish.codecextras.structured.reflective.CreationOption; import dev.lukebemish.codecextras.structured.reflective.ReflectiveStructureCreator; import dev.lukebemish.codecextras.structured.reflective.SimpleCreatorOption; import dev.lukebemish.codecextras.structured.reflective.annotations.Annotated; @@ -23,6 +22,11 @@ import dev.lukebemish.codecextras.structured.reflective.annotations.Structured; import dev.lukebemish.codecextras.structured.reflective.annotations.Transient; import dev.lukebemish.codecextras.structured.reflective.annotations.Value; +import dev.lukebemish.codecextras.structured.reflective.systems.AnnotationParsers; +import dev.lukebemish.codecextras.structured.reflective.systems.ContextualTransforms; +import dev.lukebemish.codecextras.structured.reflective.systems.Creators; +import dev.lukebemish.codecextras.structured.reflective.systems.FlexibleCreators; +import dev.lukebemish.codecextras.structured.reflective.systems.ParameterizedCreators; import dev.lukebemish.codecextras.types.Identity; import java.lang.annotation.Annotation; import java.lang.invoke.CallSite; @@ -103,7 +107,17 @@ @AutoService(ReflectiveStructureCreator.class) public class BuiltInReflectiveStructureCreator implements ReflectiveStructureCreator { @Override - public Map, Creator> creators(CreationContext options) { + public Keys systems() { + var builder = Keys.builder(); + builder.add(Creators.TYPE.key(), (Creators) () -> this::creators); + builder.add(ParameterizedCreators.TYPE.key(), (ParameterizedCreators) () -> this::parameterizedCreators); + builder.add(FlexibleCreators.TYPE.key(), (FlexibleCreators) () -> this::flexibleCreators); + builder.add(AnnotationParsers.TYPE.key(), (AnnotationParsers) () -> this::annotationParsers); + builder.add(ContextualTransforms.TYPE.key(), (ContextualTransforms) () -> this::structureContextualTransforms); + return builder.build(); + } + + private Map, Creator> creators(CreationContext context) { return ImmutableMap., Creator>builder() .put(Unit.class, () -> Structure.UNIT) .put(Boolean.class, () -> Structure.BOOL) @@ -276,8 +290,7 @@ private static Object parseValue(Value value) { } @SuppressWarnings("rawtypes") - @Override - public Map, Function>>> annotationParsers(Set options) { + private Map, Function>>> annotationParsers(CreationContext context) { var builder = ImmutableMap., Function>>>builder(); return builder .put(Annotated.class, (Annotated annotation) -> { @@ -396,8 +409,7 @@ public Unit value() { .build(); } - @Override - public Map, ParameterizedCreator> parameterizedCreators(CreationContext options) { + private Map, ParameterizedCreator> parameterizedCreators(CreationContext options) { return ImmutableMap., ParameterizedCreator>builder() .put(Either.class, (parameters) -> Structure.unboundedMap(parameters[0].create(), parameters[1].create())) // Collections @@ -499,8 +511,7 @@ private static CallSite asCallSite(MethodHandles.Lookup ignoredLookup, String ig } @SuppressWarnings({"unchecked", "rawtypes"}) - @Override - public List structureContextualTransforms(Set options) { + private List structureContextualTransforms() { return List.of( (annotated, context) -> { var annotations = annotated.stream().distinct().flatMap(a -> Arrays.stream(a.getAnnotations())).toList(); @@ -882,8 +893,7 @@ private static boolean floatEquals(float a, float b) { return Float.valueOf(a).equals(b); } - @Override - public List flexibleCreators(CreationContext options) { + private List flexibleCreators(CreationContext options) { return ImmutableList.builder() .add(new FlexibleCreator() { @SuppressWarnings({"unchecked", "rawtypes"}) diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/AnnotationParsers.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/AnnotationParsers.java new file mode 100644 index 0000000..e79097b --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/AnnotationParsers.java @@ -0,0 +1,28 @@ +package dev.lukebemish.codecextras.structured.reflective.systems; + +import dev.lukebemish.codecextras.structured.Key; +import dev.lukebemish.codecextras.structured.reflective.CreationContext; +import dev.lukebemish.codecextras.structured.reflective.ReflectiveStructureCreator; +import java.lang.annotation.Annotation; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +public interface AnnotationParsers extends ReflectiveStructureCreator.CreatorSystem, Function>>>, Function, Function>>>>, AnnotationParsers.Type> { + Type TYPE = new Type(); + + @Override + default Type type() { + return TYPE; + } + + final class Type implements ReflectiveStructureCreator.CreatorSystem.IdentityMapType, Function>>, AnnotationParsers.Type> { + private Type() {} + private static final Key KEY = Key.create("annotation_parsers"); + + @Override + public Key key() { + return KEY; + } + } +} diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/ContextualTransforms.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/ContextualTransforms.java new file mode 100644 index 0000000..94bc10c --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/ContextualTransforms.java @@ -0,0 +1,49 @@ +package dev.lukebemish.codecextras.structured.reflective.systems; + +import com.google.common.collect.ImmutableList; +import dev.lukebemish.codecextras.structured.Key; +import dev.lukebemish.codecextras.structured.reflective.CreationContext; +import dev.lukebemish.codecextras.structured.reflective.ReflectiveStructureCreator; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +public interface ContextualTransforms extends ReflectiveStructureCreator.CreatorSystem, Supplier>, ContextualTransforms.Type> { + Type TYPE = new Type(); + + @Override + default Type type() { + return TYPE; + } + + final class Type implements ReflectiveStructureCreator.CreatorSystem.Type, Supplier>, Type> { + private Type() {} + private static final Key KEY = Key.create("contextual_transforms"); + + @Override + public Supplier> merge(Supplier> a, Supplier> b) { + return () -> { + var out = a.get(); + out.addAll(b.get()); + return out; + }; + } + + @Override + public Supplier> empty() { + return ArrayList::new; + } + + @Override + public Key key() { + return KEY; + } + + @Override + public List bake(Supplier> value, CreationContext context) { + var temporary = new ArrayList<>(value.get()); + temporary.sort((a, b) -> Integer.compare(b.priority(), a.priority())); + return ImmutableList.copyOf(temporary); + } + } +} diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/CreationOptions.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/CreationOptions.java new file mode 100644 index 0000000..8b8c869 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/CreationOptions.java @@ -0,0 +1,44 @@ +package dev.lukebemish.codecextras.structured.reflective.systems; + +import dev.lukebemish.codecextras.structured.Key; +import dev.lukebemish.codecextras.structured.reflective.CreationContext; +import dev.lukebemish.codecextras.structured.reflective.CreationOption; +import dev.lukebemish.codecextras.structured.reflective.ReflectiveStructureCreator; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +public interface CreationOptions extends ReflectiveStructureCreator.CreatorSystem, List, CreationOptions.Type> { + Type TYPE = new Type(); + + @Override + default Type type() { + return TYPE; + } + + final class Type implements ReflectiveStructureCreator.CreatorSystem.Type, List, Type> { + private Type() {} + private static final Key KEY = Key.create("creation_options"); + + @Override + public List merge(List a, List b) { + a.addAll(b); + return a; + } + + @Override + public List empty() { + return new ArrayList<>(); + } + + @Override + public Key key() { + return KEY; + } + + @Override + public Set bake(List value, CreationContext context) { + return Set.copyOf(value); + } + } +} diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/Creators.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/Creators.java new file mode 100644 index 0000000..fe114c0 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/Creators.java @@ -0,0 +1,26 @@ +package dev.lukebemish.codecextras.structured.reflective.systems; + +import dev.lukebemish.codecextras.structured.Key; +import dev.lukebemish.codecextras.structured.reflective.CreationContext; +import dev.lukebemish.codecextras.structured.reflective.ReflectiveStructureCreator; +import java.util.Map; +import java.util.function.Function; + +public interface Creators extends ReflectiveStructureCreator.CreatorSystem, ReflectiveStructureCreator.Creator>, Function, ReflectiveStructureCreator.Creator>>, Creators.Type> { + Type TYPE = new Type(); + + @Override + default Type type() { + return TYPE; + } + + final class Type implements ReflectiveStructureCreator.CreatorSystem.IdentityMapType, ReflectiveStructureCreator.Creator, Creators.Type> { + private Type() {} + private static final Key KEY = Key.create("creators"); + + @Override + public Key key() { + return KEY; + } + } +} diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/FlexibleCreators.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/FlexibleCreators.java new file mode 100644 index 0000000..5204930 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/FlexibleCreators.java @@ -0,0 +1,35 @@ +package dev.lukebemish.codecextras.structured.reflective.systems; + +import com.google.common.collect.ImmutableList; +import dev.lukebemish.codecextras.structured.Key; +import dev.lukebemish.codecextras.structured.reflective.CreationContext; +import dev.lukebemish.codecextras.structured.reflective.ReflectiveStructureCreator; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +public interface FlexibleCreators extends ReflectiveStructureCreator.CreatorSystem, Function>, FlexibleCreators.Type> { + Type TYPE = new Type(); + + @Override + default Type type() { + return TYPE; + } + + final class Type implements ReflectiveStructureCreator.CreatorSystem.ListType { + private Type() {} + private static final Key KEY = Key.create("flexible_creators"); + + @Override + public List bake(Function> value, CreationContext context) { + var temporary = new ArrayList<>(value.apply(context)); + temporary.sort((a, b) -> Integer.compare(b.priority(), a.priority())); + return ImmutableList.copyOf(temporary); + } + + @Override + public Key key() { + return KEY; + } + } +} diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/ParameterizedCreators.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/ParameterizedCreators.java new file mode 100644 index 0000000..052c788 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/ParameterizedCreators.java @@ -0,0 +1,26 @@ +package dev.lukebemish.codecextras.structured.reflective.systems; + +import dev.lukebemish.codecextras.structured.Key; +import dev.lukebemish.codecextras.structured.reflective.CreationContext; +import dev.lukebemish.codecextras.structured.reflective.ReflectiveStructureCreator; +import java.util.Map; +import java.util.function.Function; + +public interface ParameterizedCreators extends ReflectiveStructureCreator.CreatorSystem, ReflectiveStructureCreator.ParameterizedCreator>, Function, ReflectiveStructureCreator.ParameterizedCreator>>, ParameterizedCreators.Type> { + Type TYPE = new Type(); + + @Override + default Type type() { + return TYPE; + } + + final class Type implements ReflectiveStructureCreator.CreatorSystem.MapType, ReflectiveStructureCreator.ParameterizedCreator, ParameterizedCreators.Type> { + private Type() {} + private static final Key KEY = Key.create("parameterized_creators"); + + @Override + public Key key() { + return KEY; + } + } +} diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/package-info.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/package-info.java new file mode 100644 index 0000000..1242888 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/package-info.java @@ -0,0 +1,6 @@ +@NullMarked +@ApiStatus.Experimental +package dev.lukebemish.codecextras.structured.reflective.systems; + +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 1287e26..f29c11b 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -17,6 +17,7 @@ requires static org.objectweb.asm; requires static org.slf4j; requires static com.google.auto.service; + requires org.checkerframework.checker.qual; exports dev.lukebemish.codecextras; exports dev.lukebemish.codecextras.comments; @@ -40,6 +41,7 @@ exports dev.lukebemish.codecextras.types; exports dev.lukebemish.codecextras.internal to codecextras_minecraft, dev.lukebemish.codecextras.minecraft; + exports dev.lukebemish.codecextras.structured.reflective.systems; provides dev.lukebemish.codecextras.structured.reflective.ReflectiveStructureCreator with BuiltInReflectiveStructureCreator; } diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftReflectiveStructureCreator.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftReflectiveStructureCreator.java index 62161b0..cf577a7 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftReflectiveStructureCreator.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftReflectiveStructureCreator.java @@ -4,11 +4,13 @@ import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import dev.lukebemish.codecextras.structured.Keys; import dev.lukebemish.codecextras.structured.Structure; import dev.lukebemish.codecextras.structured.reflective.CreationContext; import dev.lukebemish.codecextras.structured.reflective.ReflectiveStructureCreator; +import dev.lukebemish.codecextras.structured.reflective.systems.Creators; +import dev.lukebemish.codecextras.structured.reflective.systems.FlexibleCreators; import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Type; import java.util.List; import java.util.Map; import java.util.function.Function; @@ -22,47 +24,49 @@ @AutoService(ReflectiveStructureCreator.class) public class MinecraftReflectiveStructureCreator implements ReflectiveStructureCreator { @Override - public Map, Creator> creators(CreationContext options) { - return ImmutableMap., Creator>builder() - .put(ResourceLocation.class, () -> MinecraftStructures.RESOURCE_LOCATION) - .put(DataComponentMap.class, () -> MinecraftStructures.DATA_COMPONENT_MAP) - .put(DataComponentPatch.class, () -> MinecraftStructures.DATA_COMPONENT_PATCH) - .put(ItemStack.class, () -> MinecraftStructures.ITEM_STACK) - .build(); - } - - @Override - public Map, ParameterizedCreator> parameterizedCreators(CreationContext options) { - return ImmutableMap., ParameterizedCreator>builder() - .build(); - } - - @Override - public List flexibleCreators(CreationContext options) { - return ImmutableList.builder() - .add(new FlexibleCreator() { - @Override - public Structure create(Class exact, TypedCreator[] parameters, Function> creator) { - Supplier values = Suppliers.memoize(() -> { - try { - return (Object[]) exact.getMethod("values").invoke(null); - } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { - throw new RuntimeException(e); + public Keys systems() { + var builder = Keys.builder(); + builder.add(Creators.TYPE.key(), new Creators() { + @Override + public Function, Creator>> make() { + return context -> ImmutableMap., Creator>builder() + .put(ResourceLocation.class, () -> MinecraftStructures.RESOURCE_LOCATION) + .put(DataComponentMap.class, () -> MinecraftStructures.DATA_COMPONENT_MAP) + .put(DataComponentPatch.class, () -> MinecraftStructures.DATA_COMPONENT_PATCH) + .put(ItemStack.class, () -> MinecraftStructures.ITEM_STACK) + .build(); + } + }); + builder.add(FlexibleCreators.TYPE.key(), new FlexibleCreators() { + @Override + public Function> make() { + return context -> ImmutableList.builder() + .add(new FlexibleCreator() { + @Override + public Structure create(Class exact, TypedCreator[] parameters, Function> creator) { + Supplier values = Suppliers.memoize(() -> { + try { + return (Object[]) exact.getMethod("values").invoke(null); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new RuntimeException(e); + } + }); + return Structure.stringRepresentable(values, t -> ((StringRepresentable)t).getSerializedName()); } - }); - return Structure.stringRepresentable(values, t -> ((StringRepresentable)t).getSerializedName()); - } - @Override - public int priority() { - return 10; - } + @Override + public int priority() { + return 10; + } - @Override - public boolean supports(Class exact, TypedCreator[] parameters) { - return Enum.class.isAssignableFrom(exact) && StringRepresentable.class.isAssignableFrom(exact); - } - }) - .build(); + @Override + public boolean supports(Class exact, TypedCreator[] parameters) { + return Enum.class.isAssignableFrom(exact) && StringRepresentable.class.isAssignableFrom(exact); + } + }) + .build(); + } + }); + return builder.build(); } } From fd77afb9bd25386df13eeba839d9b496940770af Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Mon, 10 Mar 2025 14:18:29 -0500 Subject: [PATCH 23/28] Working groovy support --- build.gradle | 12 ++ .../GroovyReflectiveStructureCreator.java | 136 +++++++++++++++++- .../reflective/CreationContext.java | 4 +- .../reflective/PropertyNamingOption.java | 128 +++++++++++++++++ .../ReflectiveStructureCreator.java | 51 ++----- .../BuiltInReflectiveStructureCreator.java | 100 +++++++++---- .../reflective/systems/AnnotationParsers.java | 9 +- .../systems/ContextualTransforms.java | 20 ++- .../reflective/systems/CreationOptions.java | 5 + .../reflective/systems/Creators.java | 9 +- .../systems/FallbackPropertyDiscoverers.java | 47 ++++++ .../reflective/systems/FlexibleCreators.java | 18 ++- .../systems/ParameterizedCreators.java | 12 +- .../reflective/TestCaptureGroovydoc.groovy | 55 +++++++ 14 files changed, 520 insertions(+), 86 deletions(-) create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/reflective/PropertyNamingOption.java create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/FallbackPropertyDiscoverers.java create mode 100644 src/test/groovy/dev/lukebemish/codecextras/test/groovy/structured/reflective/TestCaptureGroovydoc.groovy diff --git a/build.gradle b/build.gradle index d5a20e4..2244166 100644 --- a/build.gradle +++ b/build.gradle @@ -3,6 +3,7 @@ import dev.lukebemish.codecextras.gradle.FormatJmhOutput plugins { alias cLibs.plugins.conventions.java id 'signing' + id 'groovy' id 'dev.lukebemish.managedversioning' } @@ -203,6 +204,11 @@ dependencies { jmhRuntimeOnly 'org.ow2.asm:asm:9.5' testCompileOnly cLibs.bundles.compileonly + testImplementation(project(':')) { + capabilities { + requireCapability("$project.group:$project.name-groovy") + } + } testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.2' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.2' @@ -325,6 +331,12 @@ tasks.compileJava { ] } +tasks.compileTestGroovy { + groovyOptions.optimizationOptions += [ + 'runtimeGroovydoc': true + ] +} + ['processResources', 'processMinecraftResources', 'processMinecraftFabricResources', 'processMinecraftNeoforgeResources'].each { tasks.named(it, ProcessResources) { var version = project.version.toString() diff --git a/src/groovy/java/dev/lukebemish/codecextras/groovy/structured/reflective/implementation/GroovyReflectiveStructureCreator.java b/src/groovy/java/dev/lukebemish/codecextras/groovy/structured/reflective/implementation/GroovyReflectiveStructureCreator.java index 60480ac..5e74151 100644 --- a/src/groovy/java/dev/lukebemish/codecextras/groovy/structured/reflective/implementation/GroovyReflectiveStructureCreator.java +++ b/src/groovy/java/dev/lukebemish/codecextras/groovy/structured/reflective/implementation/GroovyReflectiveStructureCreator.java @@ -7,16 +7,43 @@ import dev.lukebemish.codecextras.structured.reflective.CreationContext; import dev.lukebemish.codecextras.structured.reflective.ReflectiveStructureCreator; import dev.lukebemish.codecextras.structured.reflective.systems.AnnotationParsers; +import dev.lukebemish.codecextras.structured.reflective.systems.FallbackPropertyDiscoverers; import groovy.lang.Groovydoc; +import groovy.lang.MetaBeanProperty; +import groovy.lang.MetaClass; +import groovy.lang.MetaProperty; import java.lang.annotation.Annotation; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Modifier; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.function.Function; +import java.util.stream.Collectors; +import org.codehaus.groovy.reflection.CachedField; +import org.codehaus.groovy.runtime.DefaultGroovyMethods; import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.Nullable; @AutoService(ReflectiveStructureCreator.class) @ApiStatus.Internal public class GroovyReflectiveStructureCreator implements ReflectiveStructureCreator { + private static final MethodHandle META_PROPERTY_GET; + private static final MethodHandle META_PROPERTY_SET; + + static { + var lookup = MethodHandles.lookup(); + try { + META_PROPERTY_GET = lookup.findVirtual(MetaProperty.class, "getProperty", MethodType.methodType(Object.class, Object.class)); + META_PROPERTY_SET = lookup.findVirtual(MetaProperty.class, "setProperty", MethodType.methodType(void.class, Object.class, Object.class)); + } catch (NoSuchMethodException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + @Override public Keys systems() { var builder = Keys.builder(); @@ -26,7 +53,7 @@ public Function, Function { var builder = ImmutableMap., Function>>>builder(); return builder - .put(Groovydoc.class, (Groovydoc annotation) -> List.>of(new ReflectiveStructureCreator.AnnotationInfo() { + .put(Groovydoc.class, (Groovydoc annotation) -> List.>of(new AnnotationInfo() { @Override public Key key() { return dev.lukebemish.codecextras.structured.Annotation.COMMENT; @@ -34,13 +61,118 @@ public Key key() { @Override public String value() { - return annotation.value(); + String groovydoc = annotation.value(); + if (groovydoc.startsWith("/**@")) { + groovydoc = groovydoc.substring(4, groovydoc.length() - 2); + } else if (groovydoc.startsWith("/**")) { + groovydoc = groovydoc.substring(3, groovydoc.length() - 2); + } + groovydoc = groovydoc.stripTrailing(); + List lines = new ArrayList<>(groovydoc.lines().toList()); + while (lines.getFirst().isBlank()) { + lines.removeFirst(); + } + while (lines.getLast().isBlank()) { + lines.removeLast(); + } + lines = lines.stream().map(line -> { + line = line.trim(); + if (line.startsWith("*")) { + line = line.substring(1); + } + return line; + }).toList(); + var minSpaceCount = lines.stream().mapToInt(line -> { + int count = 0; + while (count < line.length() && line.charAt(count) == ' ') { + count++; + } + return count; + }).min().orElse(0); + return lines.stream().map(line -> + line.substring(minSpaceCount) + ).collect(Collectors.joining("\n")); } })) .build(); }; } }); + builder.add(FallbackPropertyDiscoverers.TYPE.key(), new FallbackPropertyDiscoverers() { + @Override + public Function> make() { + return context -> List.of(new Discoverer() { + @Override + public void modifyProperties(Class clazz, Map known) { + var metaClass = DefaultGroovyMethods.getMetaClass(clazz); + if (known.get("metaClass").equals(MetaClass.class)) { + known.remove("metaClass"); + } + metaClass.getProperties().forEach(metaProperty -> { + var name = metaProperty.getName(); + var type = metaProperty.getType(); + var modifiers = metaProperty.getModifiers(); + // We can only handle bean properties, due to needing to introspect them + if ((modifiers & Modifier.PUBLIC) != 0 && metaProperty instanceof MetaBeanProperty) { + if (!known.containsKey(name)) { + known.put(name, type); + } + } + }); + } + + @Override + public int priority() { + // Low priority -- only discover this if nothing else is found + return -10; + } + + @Override + public @Nullable MethodHandle getter(Class clazz, String property, boolean exists) { + if (exists) { + return null; + } + var metaClass = DefaultGroovyMethods.getMetaClass(clazz); + var metaProperty = metaClass.getMetaProperty(property); + if (metaProperty instanceof MetaBeanProperty beanProperty && (beanProperty.getModifiers() & Modifier.PUBLIC) != 0) { + var getter = beanProperty.getGetter(); + if (getter != null) { + return META_PROPERTY_GET.bindTo(metaProperty); + } + } + return null; + } + + @Override + public @Nullable MethodHandle setter(Class clazz, String property, boolean exists) { + if (exists) { + return null; + } + var metaClass = DefaultGroovyMethods.getMetaClass(clazz); + var metaProperty = metaClass.getMetaProperty(property); + if (metaProperty instanceof MetaBeanProperty beanProperty && (beanProperty.getModifiers() & Modifier.PUBLIC) != 0) { + var setter = beanProperty.getSetter(); + if (setter != null) { + return META_PROPERTY_SET.bindTo(metaProperty); + } + } + return null; + } + + @Override + public List context(Class clazz, String property) { + var metaProperty = DefaultGroovyMethods.getMetaClass(clazz).getMetaProperty(property); + if (metaProperty instanceof MetaBeanProperty beanProperty) { + if (beanProperty.getField() instanceof CachedField field) { + return List.of(field.getCachedField()); + } + } + // Unfortunately can't do too much with this + return List.of(); + } + }); + } + }); return builder.build(); } } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/CreationContext.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/CreationContext.java index 55941a7..138ddbe 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/CreationContext.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/CreationContext.java @@ -40,11 +40,11 @@ public synchronized T retrieve(ReflectiveStructureCreator.CreatorSystem.T } @SuppressWarnings({"unchecked", "rawtypes"}) - public List> parseAnnotation(Annotation annotation) { + public List> parseAnnotation(Annotation annotation) { var annotationParsers = retrieve(AnnotationParsers.TYPE); var function = (Function) annotationParsers.get(annotation.annotationType()); if (function != null) { - return (List>) function.apply(annotation); + return (List>) function.apply(annotation); } return List.of(); } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/PropertyNamingOption.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/PropertyNamingOption.java new file mode 100644 index 0000000..e3f26ea --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/PropertyNamingOption.java @@ -0,0 +1,128 @@ +package dev.lukebemish.codecextras.structured.reflective; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +public enum PropertyNamingOption implements CreationOption { + IDENTITY { + @Override + protected String formatPart(String part) { + return part; + } + + @Override + protected String joinParts(List parts) { + return String.join("", parts); + } + }, + PASCAL_CASE { + @Override + protected String formatPart(String part) { + return capitalizeFirst(part); + } + + @Override + protected String joinParts(List parts) { + return String.join("", parts); + } + }, + CAMEL_CASE { + @Override + protected String formatPart(String part) { + return capitalizeFirst(part); + } + + @Override + protected String joinParts(List parts) { + if (parts.isEmpty()) { + return ""; + } + var result = new StringBuilder(); + result.append(parts.getFirst().toLowerCase(Locale.ROOT)); + for (int i = 1; i < parts.size(); i++) { + result.append(parts.get(i)); + } + return result.toString(); + } + }, + SNAKE_CASE { + @Override + protected String formatPart(String part) { + return part.toLowerCase(Locale.ROOT); + } + + @Override + protected String joinParts(List parts) { + return String.join("_", parts); + } + }, + SCREAMING_SNAKE_CASE { + @Override + protected String formatPart(String part) { + return part.toUpperCase(Locale.ROOT); + } + + @Override + protected String joinParts(List parts) { + return String.join("_", parts); + } + }, + KEBAB_CASE { + @Override + protected String formatPart(String part) { + return part.toLowerCase(Locale.ROOT); + } + + @Override + protected String joinParts(List parts) { + return String.join("-", parts); + } + }, + SCREAMING_KEBAB_CASE { + @Override + protected String formatPart(String part) { + return part.toUpperCase(Locale.ROOT); + } + + @Override + protected String joinParts(List parts) { + return String.join("-", parts); + } + }; + + public String format(String name) { + int firstAlphaChar = -1; + for (int i = 0; i < name.length(); i++) { + if (Character.isAlphabetic(name.charAt(i))) { + firstAlphaChar = i; + break; + } + } + if (firstAlphaChar == -1) { + return name; + } + var prologue = name.substring(0, firstAlphaChar); + var parts = new ArrayList(); + var part = new StringBuilder(); + for (int i = firstAlphaChar; i < name.length(); i++) { + var c = name.charAt(i); + if (Character.isUpperCase(c)) { + if (!part.isEmpty()) { + parts.add(formatPart(part.toString())); + part = new StringBuilder(); + } + } + part.append(c); + } + parts.add(formatPart(part.toString())); + return prologue + joinParts(parts); + } + + protected abstract String formatPart(String part); + protected abstract String joinParts(List parts); + + private static String capitalizeFirst(String s) { + return s.substring(0, 1).toUpperCase(Locale.ROOT) + s.substring(1); + } +} diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java index 4f3d265..850464b 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java @@ -12,7 +12,6 @@ import dev.lukebemish.codecextras.structured.reflective.systems.Creators; import dev.lukebemish.codecextras.structured.reflective.systems.FlexibleCreators; import dev.lukebemish.codecextras.structured.reflective.systems.ParameterizedCreators; -import java.lang.reflect.AnnotatedElement; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; @@ -21,7 +20,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.function.Consumer; +import java.util.function.BiConsumer; import java.util.function.Function; import java.util.function.Supplier; @@ -36,6 +35,9 @@ interface Type> { R empty(); Key key(); T bake(R value, CreationContext context); + default boolean allowedFromServices() { + return true; + } } interface ListType> extends Type, Function>, O> { @@ -105,46 +107,12 @@ default Keys systems() { return Keys.builder().build(); } - interface AnnotationInfo { - Key key(); - T value(); - } - interface TypedCreator { Structure create(); Type type(); Class rawType(); } - interface Creator { - Structure create(); - } - - interface ContextualTransform { - Function, Structure> transform(List elements, CreationContext context); - default int priority() { - return 0; - } - } - - interface FlexibleCreator { - Structure create(Class exact, TypedCreator[] parameters, Function> creator); - boolean supports(Class exact, TypedCreator[] parameters); - default int priority() { - return 0; - } - default Creator creator(Class exact, TypedCreator[] parameters, Function> creator) { - return () -> create(exact, parameters, creator); - } - } - - interface ParameterizedCreator { - Structure create(TypedCreator[] parameters); - default Creator creator(TypedCreator[] parameters) { - return () -> create(parameters); - } - } - final class Instance { private final Keys systems; @@ -181,10 +149,13 @@ public synchronized Structure create(Class clazz) { List services = LayeredServiceLoader.unique(SERVICE_LOADER.at(ReflectiveStructureCreator.class), SERVICE_LOADER.at(clazz), SERVICE_LOADER.at(caller)); Map, Object> systemsMap = new IdentityHashMap<>(); - Consumer> addSystems = systems -> { + BiConsumer> addSystems = (isService, systems) -> { systems.keys().forEach(key -> { var value = (CreatorSystem) systems.get(key).orElseThrow(); var type = value.type(); + if (isService && !type.allowedFromServices()) { + throw new IllegalStateException("CreatorSystem " + value.type().key() + " is not allowed to be implemented by services; it may only be used by building a ReflectiveStructureCreator.Instance"); + } systemsMap.compute(type, (k, existing) -> mergeUnchecked( Objects.requireNonNullElseGet(existing, type::empty), @@ -194,8 +165,8 @@ public synchronized Structure create(Class clazz) { ); }); }; - services.forEach(creator -> addSystems.accept(creator.systems())); - addSystems.accept(this.systems); + services.forEach(creator -> addSystems.accept(true, creator.systems())); + addSystems.accept(false, this.systems); var context = new CreationContext(systemsMap); @@ -224,7 +195,7 @@ private static Structure forType(Map> cachedCreators, Map< @SuppressWarnings({"rawtypes", "unchecked"}) Supplier> full = Suppliers.memoize(() -> Structure.recursive((Function) (Function) (Structure itself) -> { recursionCache.put(type, itself); - Supplier creatorSupplier = () -> { + Supplier creatorSupplier = () -> { Class rawType = null; TypedCreator[] parameterCreators = null; if (type instanceof ParameterizedType parameterizedType) { diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/implementation/BuiltInReflectiveStructureCreator.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/implementation/BuiltInReflectiveStructureCreator.java index 13eedd9..a645592 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/implementation/BuiltInReflectiveStructureCreator.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/implementation/BuiltInReflectiveStructureCreator.java @@ -13,6 +13,7 @@ import dev.lukebemish.codecextras.structured.RecordStructure; import dev.lukebemish.codecextras.structured.Structure; import dev.lukebemish.codecextras.structured.reflective.CreationContext; +import dev.lukebemish.codecextras.structured.reflective.PropertyNamingOption; import dev.lukebemish.codecextras.structured.reflective.ReflectiveStructureCreator; import dev.lukebemish.codecextras.structured.reflective.SimpleCreatorOption; import dev.lukebemish.codecextras.structured.reflective.annotations.Annotated; @@ -25,6 +26,7 @@ import dev.lukebemish.codecextras.structured.reflective.systems.AnnotationParsers; import dev.lukebemish.codecextras.structured.reflective.systems.ContextualTransforms; import dev.lukebemish.codecextras.structured.reflective.systems.Creators; +import dev.lukebemish.codecextras.structured.reflective.systems.FallbackPropertyDiscoverers; import dev.lukebemish.codecextras.structured.reflective.systems.FlexibleCreators; import dev.lukebemish.codecextras.structured.reflective.systems.ParameterizedCreators; import dev.lukebemish.codecextras.types.Identity; @@ -117,8 +119,8 @@ public Keys systems() { return builder.build(); } - private Map, Creator> creators(CreationContext context) { - return ImmutableMap., Creator>builder() + private Map, Creators.Creator> creators(CreationContext context) { + return ImmutableMap., Creators.Creator>builder() .put(Unit.class, () -> Structure.UNIT) .put(Boolean.class, () -> Structure.BOOL) .put(Byte.class, () -> Structure.BYTE) @@ -290,8 +292,8 @@ private static Object parseValue(Value value) { } @SuppressWarnings("rawtypes") - private Map, Function>>> annotationParsers(CreationContext context) { - var builder = ImmutableMap., Function>>>builder(); + private Map, Function>>> annotationParsers(CreationContext context) { + var builder = ImmutableMap., Function>>>builder(); return builder .put(Annotated.class, (Annotated annotation) -> { Key key = (Key) parseValue(annotation.key()); @@ -371,7 +373,7 @@ private Map, Function>>> a throw new IllegalArgumentException("@Annotated must have exactly one value"); } Object finalValue = value; - List> list = List.>of(new AnnotationInfo() { + List> list = List.>of(new AnnotationParsers.AnnotationInfo() { @Override public Key key() { return key; @@ -384,7 +386,7 @@ public Object value() { }); return list; }) - .put(Comment.class, (Comment annotation) -> List.>of(new AnnotationInfo() { + .put(Comment.class, (Comment annotation) -> List.>of(new AnnotationParsers.AnnotationInfo() { @Override public Key key() { return dev.lukebemish.codecextras.structured.Annotation.COMMENT; @@ -395,7 +397,7 @@ public String value() { return annotation.value(); } })) - .put(Lenient.class, (Lenient annotation) -> List.>of(new AnnotationInfo() { + .put(Lenient.class, (Lenient annotation) -> List.>of(new AnnotationParsers.AnnotationInfo() { @Override public Key key() { return dev.lukebemish.codecextras.structured.Annotation.LENIENT; @@ -409,8 +411,8 @@ public Unit value() { .build(); } - private Map, ParameterizedCreator> parameterizedCreators(CreationContext options) { - return ImmutableMap., ParameterizedCreator>builder() + private Map, ParameterizedCreators.ParameterizedCreator> parameterizedCreators(CreationContext options) { + return ImmutableMap., ParameterizedCreators.ParameterizedCreator>builder() .put(Either.class, (parameters) -> Structure.unboundedMap(parameters[0].create(), parameters[1].create())) // Collections .put(Collection.class, collectionMaker(ArrayList::new)) @@ -438,12 +440,12 @@ private Map, ParameterizedCreator> parameterizedCreators(CreationContex } @SuppressWarnings({"rawtypes", "Convert2MethodRef", "unchecked"}) - private static ParameterizedCreator collectionMaker(Function, T> function) { + private static ParameterizedCreators.ParameterizedCreator collectionMaker(Function, T> function) { return (parameters) -> parameters[0].create().listOf().xmap(function::apply, c -> new ArrayList<>(c)); } @SuppressWarnings({"rawtypes", "Convert2MethodRef", "unchecked"}) - private static ParameterizedCreator mapMaker(Function, T> function) { + private static ParameterizedCreators.ParameterizedCreator mapMaker(Function, T> function) { return (parameters) -> Structure.unboundedMap(parameters[0].create(), parameters[1].create()).xmap(function::apply, c -> new LinkedHashMap<>(c)); } @@ -511,7 +513,7 @@ private static CallSite asCallSite(MethodHandles.Lookup ignoredLookup, String ig } @SuppressWarnings({"unchecked", "rawtypes"}) - private List structureContextualTransforms() { + private List structureContextualTransforms() { return List.of( (annotated, context) -> { var annotations = annotated.stream().distinct().flatMap(a -> Arrays.stream(a.getAnnotations())).toList(); @@ -531,7 +533,7 @@ private List structureContextualTransforms() { } @SuppressWarnings({"rawtypes", "unchecked"}) - public static Function add(CreationContext options, RecordStructure builder, String name, Type type, Function getter, Function> creator, List annotated) { + public static Function add(CreationContext options, RecordStructure builder, String name, Type type, Function getter, Function> creator, SequencedSet annotated) { var annotations = annotated.stream().distinct().flatMap(a -> Arrays.stream(a.getAnnotations())).toList(); var structureInfos = annotations.stream() @@ -546,9 +548,22 @@ private List structureContextualTransforms() { throw new IllegalArgumentException("Multiple @Structured annotations found"); } - Function structureUpdater = (Function) options.contextualTransform(annotated); + Function structureUpdater = (Function) options.contextualTransform(annotated.stream().toList()); var serializedName = name; + PropertyNamingOption namingOption = null; + for (var option : PropertyNamingOption.values()) { + if (options.hasOption(option)) { + if (namingOption != null) { + throw new IllegalArgumentException("Multiple naming options found: "+namingOption+" and "+option); + } + namingOption = option; + } + } + if (namingOption != null) { + serializedName = namingOption.format(name); + } + var serializedNameAnnotations = annotations.stream() .map(info -> { if (info instanceof SerializedName serializedNameAnnotation) { @@ -893,9 +908,9 @@ private static boolean floatEquals(float a, float b) { return Float.valueOf(a).equals(b); } - private List flexibleCreators(CreationContext options) { - return ImmutableList.builder() - .add(new FlexibleCreator() { + private List flexibleCreators(CreationContext options) { + return ImmutableList.builder() + .add(new FlexibleCreators.FlexibleCreator() { @SuppressWarnings({"unchecked", "rawtypes"}) @Override public Structure create(Class exact, TypedCreator[] parameters, Function> creator) { @@ -921,7 +936,7 @@ public boolean supports(Class exact, TypedCreator[] parameters) { return false; } }) - .add(new FlexibleCreator() { + .add(new FlexibleCreators.FlexibleCreator() { @SuppressWarnings({"unchecked", "rawtypes"}) @Override public Structure create(Class exact, TypedCreator[] parameters, Function> creator) { @@ -938,7 +953,7 @@ public boolean supports(Class exact, TypedCreator[] parameters) { return exact.equals(EnumMap.class) && parameters.length == 2; } }) - .add(new FlexibleCreator() { + .add(new FlexibleCreators.FlexibleCreator() { @Override public Structure create(Class exact, TypedCreator[] parameters, Function> creator) { Supplier values = Suppliers.memoize(() -> { @@ -956,7 +971,7 @@ public boolean supports(Class exact, TypedCreator[] parameters) { return Enum.class.isAssignableFrom(exact); } }) - .add(new FlexibleCreator() { + .add(new FlexibleCreators.FlexibleCreator() { private Function, ?> arrayMaker(Class arrayComponentType) { var cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); var name = BuiltInReflectiveStructureCreator.class.getName().replace('.', '/') + "$ArrayMaker"; @@ -1008,7 +1023,7 @@ public boolean supports(Class exact, TypedCreator[] parameters) { return exact.isArray() && !exact.getComponentType().isPrimitive(); } }) - .add(new FlexibleCreator() { + .add(new FlexibleCreators.FlexibleCreator() { private List> validCtors(Class exact) { List> validCtors = new ArrayList<>(); for (var ctor : exact.getConstructors()) { @@ -1085,12 +1100,12 @@ public Structure create(Class exact, TypedCreator[] parameters, Function ctorSetters = new HashMap<>(); String[] ctorSettersArray = new String[validCtor.getParameterCount()]; Map types = new HashMap<>(); - Map> context = new HashMap<>(); + Map> context = new HashMap<>(); if (exact.isRecord()) { for (int i = 0; i < exact.getRecordComponents().length; i++) { var component = exact.getRecordComponents()[i]; - var thisContext = context.computeIfAbsent(component.getName(), k -> new ArrayList<>()); + var thisContext = context.computeIfAbsent(component.getName(), k -> new LinkedHashSet<>()); thisContext.add(component); ctorSetters.put(component.getName(), i); @@ -1112,7 +1127,7 @@ public Structure create(Class exact, TypedCreator[] parameters, Function new ArrayList<>()); + var thisContext = context.computeIfAbsent(annotation.value(), k -> new LinkedHashSet<>()); thisContext.add(parameter); ctorSetters.put(annotation.value(), i); @@ -1133,7 +1148,7 @@ public Structure create(Class exact, TypedCreator[] parameters, Function new ArrayList<>()); + var thisContext = context.computeIfAbsent(annotation.value(), k -> new LinkedHashSet<>()); thisContext.add(param); ctorSetters.put(annotation.value(), i); @@ -1191,7 +1206,7 @@ public Structure create(Class exact, TypedCreator[] parameters, Function new ArrayList<>()).add(method); + context.computeIfAbsent(property, k -> new LinkedHashSet<>()).add(method); } } if (isSetter) { var property = method.getName().substring(3); @@ -1206,7 +1221,7 @@ public Structure create(Class exact, TypedCreator[] parameters, Function new ArrayList<>()).add(method); + context.computeIfAbsent(property, k -> new LinkedHashSet<>()).add(method); } } } @@ -1218,7 +1233,7 @@ public Structure create(Class exact, TypedCreator[] parameters, Function new ArrayList<>()).add(field); + context.computeIfAbsent(field.getName(), k -> new LinkedHashSet<>()).add(field); if (!getters.containsKey(field.getName())) { var getter = functionWrapper(MethodHandles.lookup().unreflectGetter(field)); getters.put(field.getName(), getter); @@ -1234,6 +1249,35 @@ public Structure create(Class exact, TypedCreator[] parameters, Function new LinkedHashSet<>()).addAll(discoverer.context(exact, entry)); + } + } + var properties = new LinkedHashSet(); for (var entry : types.keySet()) { if (getters.containsKey(entry) && (setters.containsKey(entry) || ctorSetters.containsKey(entry))) { diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/AnnotationParsers.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/AnnotationParsers.java index e79097b..9e3f9f5 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/AnnotationParsers.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/AnnotationParsers.java @@ -8,7 +8,7 @@ import java.util.Map; import java.util.function.Function; -public interface AnnotationParsers extends ReflectiveStructureCreator.CreatorSystem, Function>>>, Function, Function>>>>, AnnotationParsers.Type> { +public interface AnnotationParsers extends ReflectiveStructureCreator.CreatorSystem, Function>>>, Function, Function>>>>, AnnotationParsers.Type> { Type TYPE = new Type(); @Override @@ -16,7 +16,12 @@ default Type type() { return TYPE; } - final class Type implements ReflectiveStructureCreator.CreatorSystem.IdentityMapType, Function>>, AnnotationParsers.Type> { + interface AnnotationInfo { + Key key(); + T value(); + } + + final class Type implements ReflectiveStructureCreator.CreatorSystem.IdentityMapType, Function>>, AnnotationParsers.Type> { private Type() {} private static final Key KEY = Key.create("annotation_parsers"); diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/ContextualTransforms.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/ContextualTransforms.java index 94bc10c..0f148eb 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/ContextualTransforms.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/ContextualTransforms.java @@ -2,13 +2,16 @@ import com.google.common.collect.ImmutableList; import dev.lukebemish.codecextras.structured.Key; +import dev.lukebemish.codecextras.structured.Structure; import dev.lukebemish.codecextras.structured.reflective.CreationContext; import dev.lukebemish.codecextras.structured.reflective.ReflectiveStructureCreator; +import java.lang.reflect.AnnotatedElement; import java.util.ArrayList; import java.util.List; +import java.util.function.Function; import java.util.function.Supplier; -public interface ContextualTransforms extends ReflectiveStructureCreator.CreatorSystem, Supplier>, ContextualTransforms.Type> { +public interface ContextualTransforms extends ReflectiveStructureCreator.CreatorSystem, Supplier>, ContextualTransforms.Type> { Type TYPE = new Type(); @Override @@ -16,12 +19,19 @@ default Type type() { return TYPE; } - final class Type implements ReflectiveStructureCreator.CreatorSystem.Type, Supplier>, Type> { + interface ContextualTransform { + Function, Structure> transform(List elements, CreationContext context); + default int priority() { + return 0; + } + } + + final class Type implements ReflectiveStructureCreator.CreatorSystem.Type, Supplier>, Type> { private Type() {} private static final Key KEY = Key.create("contextual_transforms"); @Override - public Supplier> merge(Supplier> a, Supplier> b) { + public Supplier> merge(Supplier> a, Supplier> b) { return () -> { var out = a.get(); out.addAll(b.get()); @@ -30,7 +40,7 @@ public Supplier> merge(Supp } @Override - public Supplier> empty() { + public Supplier> empty() { return ArrayList::new; } @@ -40,7 +50,7 @@ public Key key() { } @Override - public List bake(Supplier> value, CreationContext context) { + public List bake(Supplier> value, CreationContext context) { var temporary = new ArrayList<>(value.get()); temporary.sort((a, b) -> Integer.compare(b.priority(), a.priority())); return ImmutableList.copyOf(temporary); diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/CreationOptions.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/CreationOptions.java index 8b8c869..583b1be 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/CreationOptions.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/CreationOptions.java @@ -40,5 +40,10 @@ public Key key() { public Set bake(List value, CreationContext context) { return Set.copyOf(value); } + + @Override + public boolean allowedFromServices() { + return false; + } } } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/Creators.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/Creators.java index fe114c0..86b6900 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/Creators.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/Creators.java @@ -1,12 +1,13 @@ package dev.lukebemish.codecextras.structured.reflective.systems; import dev.lukebemish.codecextras.structured.Key; +import dev.lukebemish.codecextras.structured.Structure; import dev.lukebemish.codecextras.structured.reflective.CreationContext; import dev.lukebemish.codecextras.structured.reflective.ReflectiveStructureCreator; import java.util.Map; import java.util.function.Function; -public interface Creators extends ReflectiveStructureCreator.CreatorSystem, ReflectiveStructureCreator.Creator>, Function, ReflectiveStructureCreator.Creator>>, Creators.Type> { +public interface Creators extends ReflectiveStructureCreator.CreatorSystem, Creators.Creator>, Function, Creators.Creator>>, Creators.Type> { Type TYPE = new Type(); @Override @@ -14,7 +15,11 @@ default Type type() { return TYPE; } - final class Type implements ReflectiveStructureCreator.CreatorSystem.IdentityMapType, ReflectiveStructureCreator.Creator, Creators.Type> { + interface Creator { + Structure create(); + } + + final class Type implements ReflectiveStructureCreator.CreatorSystem.IdentityMapType, Creator, Creators.Type> { private Type() {} private static final Key KEY = Key.create("creators"); diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/FallbackPropertyDiscoverers.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/FallbackPropertyDiscoverers.java new file mode 100644 index 0000000..c892722 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/FallbackPropertyDiscoverers.java @@ -0,0 +1,47 @@ +package dev.lukebemish.codecextras.structured.reflective.systems; + +import dev.lukebemish.codecextras.structured.Key; +import dev.lukebemish.codecextras.structured.reflective.CreationContext; +import dev.lukebemish.codecextras.structured.reflective.ReflectiveStructureCreator; +import java.lang.invoke.MethodHandle; +import java.lang.reflect.AnnotatedElement; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import org.jspecify.annotations.Nullable; + +public interface FallbackPropertyDiscoverers extends ReflectiveStructureCreator.CreatorSystem, Function>, FallbackPropertyDiscoverers.Type> { + interface Discoverer { + void modifyProperties(Class clazz, Map known); + @Nullable MethodHandle getter(Class clazz, String property, boolean exists); + @Nullable MethodHandle setter(Class clazz, String property, boolean exists); + List context(Class clazz, String property); + default int priority() { + return 0; + } + } + + Type TYPE = new Type(); + + @Override + default Type type() { + return TYPE; + } + + final class Type implements ReflectiveStructureCreator.CreatorSystem.ListType { + private Type() {} + private static final Key KEY = Key.create("fallback_property_discoverers"); + + @Override + public List bake(Function> value, CreationContext context) { + var out = value.apply(context); + out.sort((a, b) -> Integer.compare(b.priority(), a.priority())); + return out; + } + + @Override + public Key key() { + return KEY; + } + } +} diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/FlexibleCreators.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/FlexibleCreators.java index 5204930..069f6f8 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/FlexibleCreators.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/FlexibleCreators.java @@ -2,13 +2,14 @@ import com.google.common.collect.ImmutableList; import dev.lukebemish.codecextras.structured.Key; +import dev.lukebemish.codecextras.structured.Structure; import dev.lukebemish.codecextras.structured.reflective.CreationContext; import dev.lukebemish.codecextras.structured.reflective.ReflectiveStructureCreator; import java.util.ArrayList; import java.util.List; import java.util.function.Function; -public interface FlexibleCreators extends ReflectiveStructureCreator.CreatorSystem, Function>, FlexibleCreators.Type> { +public interface FlexibleCreators extends ReflectiveStructureCreator.CreatorSystem, Function>, FlexibleCreators.Type> { Type TYPE = new Type(); @Override @@ -16,12 +17,23 @@ default Type type() { return TYPE; } - final class Type implements ReflectiveStructureCreator.CreatorSystem.ListType { + interface FlexibleCreator { + Structure create(Class exact, ReflectiveStructureCreator.TypedCreator[] parameters, Function> creator); + boolean supports(Class exact, ReflectiveStructureCreator.TypedCreator[] parameters); + default int priority() { + return 0; + } + default Creators.Creator creator(Class exact, ReflectiveStructureCreator.TypedCreator[] parameters, Function> creator) { + return () -> create(exact, parameters, creator); + } + } + + final class Type implements ReflectiveStructureCreator.CreatorSystem.ListType { private Type() {} private static final Key KEY = Key.create("flexible_creators"); @Override - public List bake(Function> value, CreationContext context) { + public List bake(Function> value, CreationContext context) { var temporary = new ArrayList<>(value.apply(context)); temporary.sort((a, b) -> Integer.compare(b.priority(), a.priority())); return ImmutableList.copyOf(temporary); diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/ParameterizedCreators.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/ParameterizedCreators.java index 052c788..ce88ff0 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/ParameterizedCreators.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/ParameterizedCreators.java @@ -1,12 +1,13 @@ package dev.lukebemish.codecextras.structured.reflective.systems; import dev.lukebemish.codecextras.structured.Key; +import dev.lukebemish.codecextras.structured.Structure; import dev.lukebemish.codecextras.structured.reflective.CreationContext; import dev.lukebemish.codecextras.structured.reflective.ReflectiveStructureCreator; import java.util.Map; import java.util.function.Function; -public interface ParameterizedCreators extends ReflectiveStructureCreator.CreatorSystem, ReflectiveStructureCreator.ParameterizedCreator>, Function, ReflectiveStructureCreator.ParameterizedCreator>>, ParameterizedCreators.Type> { +public interface ParameterizedCreators extends ReflectiveStructureCreator.CreatorSystem, ParameterizedCreators.ParameterizedCreator>, Function, ParameterizedCreators.ParameterizedCreator>>, ParameterizedCreators.Type> { Type TYPE = new Type(); @Override @@ -14,7 +15,14 @@ default Type type() { return TYPE; } - final class Type implements ReflectiveStructureCreator.CreatorSystem.MapType, ReflectiveStructureCreator.ParameterizedCreator, ParameterizedCreators.Type> { + interface ParameterizedCreator { + Structure create(ReflectiveStructureCreator.TypedCreator[] parameters); + default Creators.Creator creator(ReflectiveStructureCreator.TypedCreator[] parameters) { + return () -> create(parameters); + } + } + + final class Type implements ReflectiveStructureCreator.CreatorSystem.MapType, ParameterizedCreator, ParameterizedCreators.Type> { private Type() {} private static final Key KEY = Key.create("parameterized_creators"); diff --git a/src/test/groovy/dev/lukebemish/codecextras/test/groovy/structured/reflective/TestCaptureGroovydoc.groovy b/src/test/groovy/dev/lukebemish/codecextras/test/groovy/structured/reflective/TestCaptureGroovydoc.groovy new file mode 100644 index 0000000..c6f0152 --- /dev/null +++ b/src/test/groovy/dev/lukebemish/codecextras/test/groovy/structured/reflective/TestCaptureGroovydoc.groovy @@ -0,0 +1,55 @@ +package dev.lukebemish.codecextras.test.groovy.structured.reflective + +import com.electronwill.nightconfig.core.Config +import com.electronwill.nightconfig.toml.TomlParser +import com.electronwill.nightconfig.toml.TomlWriter +import com.mojang.serialization.Codec +import dev.lukebemish.codecextras.compat.nightconfig.TomlConfigOps +import dev.lukebemish.codecextras.structured.CodecInterpreter +import dev.lukebemish.codecextras.structured.reflective.ReflectiveStructureCreator +import dev.lukebemish.codecextras.test.CodecAssertions +import groovy.transform.EqualsAndHashCode +import org.junit.jupiter.api.Test + +import java.util.function.Function + +class TestCaptureGroovydoc { + @EqualsAndHashCode + static class TestClass { + /**@ + * This is a test field + */ + long a + } + + static final Codec CODEC = CodecInterpreter.create().interpret(ReflectiveStructureCreator.create(TestClass)).getOrThrow() + + private final TestClass test = new TestClass().tap { + a = 10 + } + + private final String toml = """#This is a test field +a = 10 + +""" + + private final Config tomlParsed = new TomlParser().parse(toml) + + private static final Function TOML_TO_STRING = { toml -> + if (toml instanceof Config) { + return new TomlWriter().writeToString(toml) + } else { + return toml.toString() + } + } + + @Test + void testEncoding() { + CodecAssertions.assertEncodesString(TomlConfigOps.COMMENTED, test, toml, TOML_TO_STRING, CODEC) + } + + @Test + void testDecoding() { + CodecAssertions.assertDecodes(TomlConfigOps.COMMENTED, tomlParsed, test, CODEC) + } +} From b0a0a83e14f1bd74eced99b4e1dfcfaa134f2583 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Wed, 12 Mar 2025 13:32:46 -0500 Subject: [PATCH 24/28] Better optional/defaulted behavior and fix transitive-annotated beans --- .../GroovyReflectiveStructureCreator.java | 15 +- .../structured/CodecInterpreter.java | 2 +- .../structured/IdentityInterpreter.java | 4 +- .../codecextras/structured/Interpreter.java | 2 +- .../structured/MapCodecInterpreter.java | 2 +- .../structured/RecordStructure.java | 18 +- .../codecextras/structured/Structure.java | 11 + .../structured/StructuredMapCodec.java | 14 +- .../reflective/annotations/Annotated.java | 11 +- .../reflective/annotations/Default.java | 12 + .../reflective/annotations/Value.java | 12 +- .../BuiltInReflectiveStructureCreator.java | 267 ++++++++++-------- .../schema/JsonSchemaInterpreter.java | 2 +- .../config/ConfigScreenInterpreter.java | 2 +- .../structured/StreamCodecInterpreter.java | 6 +- .../reflective/TestOptionalBehavior.java | 56 ++++ .../TestReflectiveStructureAnnotations.java | 6 +- 17 files changed, 295 insertions(+), 147 deletions(-) create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Default.java create mode 100644 src/test/java/dev/lukebemish/codecextras/test/structured/reflective/TestOptionalBehavior.java diff --git a/src/groovy/java/dev/lukebemish/codecextras/groovy/structured/reflective/implementation/GroovyReflectiveStructureCreator.java b/src/groovy/java/dev/lukebemish/codecextras/groovy/structured/reflective/implementation/GroovyReflectiveStructureCreator.java index 5e74151..d41ab87 100644 --- a/src/groovy/java/dev/lukebemish/codecextras/groovy/structured/reflective/implementation/GroovyReflectiveStructureCreator.java +++ b/src/groovy/java/dev/lukebemish/codecextras/groovy/structured/reflective/implementation/GroovyReflectiveStructureCreator.java @@ -21,6 +21,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.function.Function; import java.util.stream.Collectors; import org.codehaus.groovy.reflection.CachedField; @@ -105,15 +106,18 @@ public Function> make() { @Override public void modifyProperties(Class clazz, Map known) { var metaClass = DefaultGroovyMethods.getMetaClass(clazz); - if (known.get("metaClass").equals(MetaClass.class)) { + if (Objects.equals(known.get("metaClass"), MetaClass.class)) { known.remove("metaClass"); } metaClass.getProperties().forEach(metaProperty -> { + if ((metaProperty.getModifiers() & Modifier.TRANSIENT) != 0) { + return; + } var name = metaProperty.getName(); var type = metaProperty.getType(); var modifiers = metaProperty.getModifiers(); // We can only handle bean properties, due to needing to introspect them - if ((modifiers & Modifier.PUBLIC) != 0 && metaProperty instanceof MetaBeanProperty) { + if ((modifiers & Modifier.PUBLIC) != 0 && metaProperty instanceof MetaBeanProperty metaBeanProperty) { if (!known.containsKey(name)) { known.put(name, type); } @@ -162,13 +166,14 @@ public int priority() { @Override public List context(Class clazz, String property) { var metaProperty = DefaultGroovyMethods.getMetaClass(clazz).getMetaProperty(property); + var list = new ArrayList(); if (metaProperty instanceof MetaBeanProperty beanProperty) { if (beanProperty.getField() instanceof CachedField field) { - return List.of(field.getCachedField()); + list.add(field.getCachedField()); } } - // Unfortunately can't do too much with this - return List.of(); + // And that's about all we can do... + return list; } }); } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java index 5931217..b52f9f3 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java @@ -88,7 +88,7 @@ public DataResult>> list(App single) { } @Override - public DataResult> record(List> fields, Function creator) { + public DataResult> record(List> fields, Function> creator) { return StructuredMapCodec.of(fields, creator, this, CodecInterpreter::unbox) .map(mapCodec -> new Holder<>(mapCodec.codec())); } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/IdentityInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/IdentityInterpreter.java index 0cd3418..bb31728 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/IdentityInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/IdentityInterpreter.java @@ -60,13 +60,13 @@ public DataResult> keyed(Key key) { } @Override - public DataResult> record(List> fields, Function creator) { + public DataResult> record(List> fields, Function> creator) { var builder = RecordStructure.Container.builder(); for (var field : fields) { DataResult> result = forField(field, builder); if (result != null) return result; } - return DataResult.success(new Identity<>(creator.apply(builder.build()))); + return creator.apply(builder.build()).map(Identity::new); } private @Nullable DataResult> forField(RecordStructure.Field field, RecordStructure.Container.Builder builder) { diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java index c129a7d..a9e9ce7 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Interpreter.java @@ -37,7 +37,7 @@ public interface Interpreter { DataResult> keyed(Key key); - DataResult> record(List> fields, Function creator); + DataResult> record(List> fields, Function> creator); DataResult> flatXmap(App input, Function> to, Function> from); diff --git a/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java index d7fabe5..4462c5d 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java @@ -55,7 +55,7 @@ public DataResult>> list(App single) { } @Override - public DataResult> record(List> fields, Function creator) { + public DataResult> record(List> fields, Function> creator) { return StructuredMapCodec.of(fields, creator, codecInterpreter(), CodecInterpreter::unbox) .map(Holder::new); } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/RecordStructure.java b/src/main/java/dev/lukebemish/codecextras/structured/RecordStructure.java index 11d919e..7451186 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/RecordStructure.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/RecordStructure.java @@ -279,7 +279,7 @@ private void partialField(Function getter, Field field) { * @return a new structure * @param the type of the data represented */ - static Structure create(RecordStructure.Builder builder) { + static Structure create(RecordStructure.FlatBuilder builder) { RecordStructure instance = new RecordStructure<>(); var creator = builder.build(instance); return new Structure<>() { @@ -305,5 +305,21 @@ public interface Builder { * @return a function to assemble the final type from a {@link Container} */ Function build(RecordStructure builder); + + default FlatBuilder asFlatBuilder() { + return builder -> build(builder).andThen(DataResult::success); + } + } + + @FunctionalInterface + public interface FlatBuilder { + /** + * Assemble a record structure for the given type. Should collect {@link Key}s for every field needed and return + * a function that uses those keys to assemble the final type from a {@link Container}. Unlike a {@link Builder}, + * allows for failures. + * @param builder a blank record structure to add fields to + * @return a function to assemble the final type from a {@link Container} + */ + Function> build(RecordStructure builder); } } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java index 90c6c28..50ee273 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/Structure.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/Structure.java @@ -626,6 +626,17 @@ default Structure validate(Function> verifier) { * @see RecordStructure */ static Structure record(RecordStructure.Builder builder) { + return RecordStructure.create(builder.asFlatBuilder()); + } + + /** + * {@return a structure representing a collection of key-value pairs with defined structures, which may be optionally present, and which can handle failures} + * @param builder the builder to use to create the record structure + * @param the type of data the structure represents + * @see RecordStructure + * @see #record(RecordStructure.Builder) + */ + static Structure flatRecord(RecordStructure.FlatBuilder builder) { return RecordStructure.create(builder); } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/StructuredMapCodec.java b/src/main/java/dev/lukebemish/codecextras/structured/StructuredMapCodec.java index 666ab0a..bda7f34 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/StructuredMapCodec.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/StructuredMapCodec.java @@ -22,9 +22,9 @@ class StructuredMapCodec extends MapCodec { private record Field(String name, MapCodec codec, RecordStructure.Key key, Function getter) {} private final List> fields; - private final Function creator; + private final Function> creator; - private StructuredMapCodec(List> fields, Function creator) { + private StructuredMapCodec(List> fields, Function> creator) { this.fields = fields; this.creator = creator; } @@ -33,7 +33,7 @@ public interface Unboxer { Codec unbox(App box); } - public static DataResult> of(List> fields, Function creator, Interpreter interpreter, Unboxer unboxer) { + public static DataResult> of(List> fields, Function> creator, Interpreter interpreter, Unboxer unboxer) { var mapCodecFields = new ArrayList>(); for (var field : fields) { DataResult> result = recordSingleField(field, mapCodecFields, interpreter, unboxer); @@ -93,12 +93,16 @@ public DataResult decode(DynamicOps ops, MapLike input) { } if (isError) { if (isPartial) { - return DataResult.error(errorMessage, creator.apply(builder.build()), errorLifecycle); + var result = creator.apply(builder.build()); + if (result.isError()) { + return DataResult.error(errorMessage, errorLifecycle); + } + return DataResult.error(errorMessage, result.result().orElseThrow(), errorLifecycle); } else { return DataResult.error(errorMessage, errorLifecycle); } } else { - return DataResult.success(creator.apply(builder.build())); + return creator.apply(builder.build()); } } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Annotated.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Annotated.java index 1d29739..1863126 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Annotated.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Annotated.java @@ -9,14 +9,5 @@ @Retention(RetentionPolicy.RUNTIME) public @interface Annotated { Value key(); - Value[] value() default {}; - String[] stringValue() default {}; - int[] intValue() default {}; - long[] longValue() default {}; - double[] doubleValue() default {}; - float[] floatValue() default {}; - boolean[] booleanValue() default {}; - byte[] byteValue() default {}; - short[] shortValue() default {}; - char[] charValue() default {}; + Value value(); } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Default.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Default.java new file mode 100644 index 0000000..86da3e1 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Default.java @@ -0,0 +1,12 @@ +package dev.lukebemish.codecextras.structured.reflective.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.METHOD, ElementType.RECORD_COMPONENT, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Default { + Value value(); +} diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Value.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Value.java index 0d44c52..8c6b09e 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Value.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Value.java @@ -7,9 +7,19 @@ @Target({}) @Retention(RetentionPolicy.RUNTIME) public @interface Value { - Class location(); + Class location() default Value.class; String field() default ""; String method() default ""; + + String[] stringValue() default {}; + int[] intValue() default {}; + long[] longValue() default {}; + double[] doubleValue() default {}; + float[] floatValue() default {}; + boolean[] booleanValue() default {}; + byte[] byteValue() default {}; + short[] shortValue() default {}; + char[] charValue() default {}; } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/implementation/BuiltInReflectiveStructureCreator.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/implementation/BuiltInReflectiveStructureCreator.java index a645592..4d9979f 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/implementation/BuiltInReflectiveStructureCreator.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/implementation/BuiltInReflectiveStructureCreator.java @@ -7,6 +7,7 @@ import com.google.gson.annotations.SerializedName; import com.mojang.datafixers.util.Either; import com.mojang.datafixers.util.Unit; +import com.mojang.serialization.DataResult; import com.mojang.serialization.Dynamic; import dev.lukebemish.codecextras.structured.Key; import dev.lukebemish.codecextras.structured.Keys; @@ -18,6 +19,7 @@ import dev.lukebemish.codecextras.structured.reflective.SimpleCreatorOption; import dev.lukebemish.codecextras.structured.reflective.annotations.Annotated; import dev.lukebemish.codecextras.structured.reflective.annotations.Comment; +import dev.lukebemish.codecextras.structured.reflective.annotations.Default; import dev.lukebemish.codecextras.structured.reflective.annotations.Lenient; import dev.lukebemish.codecextras.structured.reflective.annotations.SerializedProperty; import dev.lukebemish.codecextras.structured.reflective.annotations.Structured; @@ -265,30 +267,99 @@ private Map, Creators.Creator> creators(CreationContext context) { .build(); } - private static Object parseValue(Value value) { - var holdingClass = value.location(); - var isField = !value.field().isEmpty(); - var isMethod = !value.method().isEmpty(); - if (isField && isMethod) { - throw new IllegalArgumentException("@Value cannot have both a field and a method"); + private static Object parseValue(Value annotation) { + Object value = null; + int referred = 0; + if (annotation.stringValue().length > 0) { + if (annotation.stringValue().length > 1) { + throw new IllegalArgumentException("@Value must have exactly one stringValue"); + } + value = annotation.stringValue()[0]; + referred++; + } + if (annotation.intValue().length > 0) { + if (annotation.intValue().length > 1) { + throw new IllegalArgumentException("@Value must have exactly one intValue"); + } + value = annotation.intValue()[0]; + referred++; + } + if (annotation.longValue().length > 0) { + if (annotation.longValue().length > 1) { + throw new IllegalArgumentException("@Value must have exactly one longValue"); + } + value = annotation.longValue()[0]; + referred++; + } + if (annotation.doubleValue().length > 0) { + if (annotation.doubleValue().length > 1) { + throw new IllegalArgumentException("@Value must have exactly one doubleValue"); + } + value = annotation.doubleValue()[0]; + referred++; + } + if (annotation.floatValue().length > 0) { + if (annotation.floatValue().length > 1) { + throw new IllegalArgumentException("@Value must have exactly one floatValue"); + } + value = annotation.floatValue()[0]; + referred++; + } + if (annotation.booleanValue().length > 0) { + if (annotation.booleanValue().length > 1) { + throw new IllegalArgumentException("@Value must have exactly one booleanValue"); + } + value = annotation.booleanValue()[0]; + referred++; + } + if (annotation.byteValue().length > 0) { + if (annotation.byteValue().length > 1) { + throw new IllegalArgumentException("@Value must have exactly one byteValue"); + } + value = annotation.byteValue()[0]; + referred++; + } + if (annotation.shortValue().length > 0) { + if (annotation.shortValue().length > 1) { + throw new IllegalArgumentException("@Value must have exactly one shortValue"); + } + value = annotation.shortValue()[0]; + referred++; + } + if (annotation.charValue().length > 0) { + if (annotation.charValue().length > 1) { + throw new IllegalArgumentException("@Value must have exactly one charValue"); + } + value = annotation.charValue()[0]; + referred++; } + + var holdingClass = annotation.location(); + var isField = !annotation.field().isEmpty(); + var isMethod = !annotation.method().isEmpty(); if (isField) { try { - var field = holdingClass.getField(value.field()); - return field.get(null); + var field = holdingClass.getField(annotation.field()); + value = field.get(null); } catch (NoSuchFieldException | IllegalAccessException e) { throw new RuntimeException("@Value must refer to a public static field or method with no arguments", e); } - } else if (isMethod) { + referred++; + } + if (isMethod) { try { - var method = holdingClass.getMethod(value.method()); - return method.invoke(null); + var method = holdingClass.getMethod(annotation.method()); + value = method.invoke(null); } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { throw new RuntimeException("@Value must refer to a public static field or method with no arguments", e); } - } else { - throw new IllegalArgumentException("@Value must have either a field or a method"); + referred++; + } + + if (value == null || referred != 1) { + throw new IllegalArgumentException("@Value must have exactly one value"); } + return value; } @SuppressWarnings("rawtypes") @@ -297,83 +368,8 @@ private Map, Function { Key key = (Key) parseValue(annotation.key()); - Object value = null; - int referred = 0; - if (annotation.value().length > 0) { - if (annotation.value().length > 1) { - throw new IllegalArgumentException("@Annotated must have exactly one value"); - } - value = parseValue(annotation.value()[0]); - referred++; - } - if (annotation.stringValue().length > 0) { - if (annotation.stringValue().length > 1) { - throw new IllegalArgumentException("@Annotated must have exactly one stringValue"); - } - value = annotation.stringValue()[0]; - referred++; - } - if (annotation.intValue().length > 0) { - if (annotation.intValue().length > 1) { - throw new IllegalArgumentException("@Annotated must have exactly one intValue"); - } - value = annotation.intValue()[0]; - referred++; - } - if (annotation.longValue().length > 0) { - if (annotation.longValue().length > 1) { - throw new IllegalArgumentException("@Annotated must have exactly one longValue"); - } - value = annotation.longValue()[0]; - referred++; - } - if (annotation.doubleValue().length > 0) { - if (annotation.doubleValue().length > 1) { - throw new IllegalArgumentException("@Annotated must have exactly one doubleValue"); - } - value = annotation.doubleValue()[0]; - referred++; - } - if (annotation.floatValue().length > 0) { - if (annotation.floatValue().length > 1) { - throw new IllegalArgumentException("@Annotated must have exactly one floatValue"); - } - value = annotation.floatValue()[0]; - referred++; - } - if (annotation.booleanValue().length > 0) { - if (annotation.booleanValue().length > 1) { - throw new IllegalArgumentException("@Annotated must have exactly one booleanValue"); - } - value = annotation.booleanValue()[0]; - referred++; - } - if (annotation.byteValue().length > 0) { - if (annotation.byteValue().length > 1) { - throw new IllegalArgumentException("@Annotated must have exactly one byteValue"); - } - value = annotation.byteValue()[0]; - referred++; - } - if (annotation.shortValue().length > 0) { - if (annotation.shortValue().length > 1) { - throw new IllegalArgumentException("@Annotated must have exactly one shortValue"); - } - value = annotation.shortValue()[0]; - referred++; - } - if (annotation.charValue().length > 0) { - if (annotation.charValue().length > 1) { - throw new IllegalArgumentException("@Annotated must have exactly one charValue"); - } - value = annotation.charValue()[0]; - referred++; - } - if (value == null || referred != 1) { - throw new IllegalArgumentException("@Annotated must have exactly one value"); - } - Object finalValue = value; - List> list = List.>of(new AnnotationParsers.AnnotationInfo() { + Object finalValue = parseValue(annotation.value()); + return List.>of(new AnnotationParsers.AnnotationInfo() { @Override public Key key() { return key; @@ -384,7 +380,6 @@ public Object value() { return finalValue; } }); - return list; }) .put(Comment.class, (Comment annotation) -> List.>of(new AnnotationParsers.AnnotationInfo() { @Override @@ -548,6 +543,23 @@ private List structureContextualTransf throw new IllegalArgumentException("Multiple @Structured annotations found"); } + var defaultInfos = annotations.stream() + .map(info -> { + if (info instanceof Default defaultAnnotation) { + return defaultAnnotation; + } + return null; + }).filter(Objects::nonNull).distinct().toList(); + + if (defaultInfos.size() > 1) { + throw new IllegalArgumentException("Multiple @Default annotations found"); + } + + Object defaultValue = null; + if (!defaultInfos.isEmpty()) { + defaultValue = parseValue(defaultInfos.getFirst().value()); + } + Function structureUpdater = (Function) options.contextualTransform(annotated.stream().toList()); var serializedName = name; @@ -583,7 +595,7 @@ private List structureContextualTransf otherNames = serializedNameAnnotations.getFirst().alternate(); } - var namedCreator = structureByNameCreator(options, builder, type, getter, creator, structureInfos, structureUpdater); + var namedCreator = structureByNameCreator(options, builder, type, getter, creator, structureInfos, structureUpdater, defaultValue); if (otherNames.length > 0) { var list = new ArrayList(); @@ -603,6 +615,20 @@ interface StructureMaker { Function make(String name, boolean first); } + default StructureNamedCreator andThen(Function function) { + return new StructureNamedCreator<>() { + @Override + public Function forNames(List names) { + return StructureNamedCreator.this.forNames(names).andThen(function); + } + + @Override + public Function forName(String name) { + return StructureNamedCreator.this.forName(name).andThen(function); + } + }; + } + record StructureData(StructureMaker creator, BiFunction combiner) implements StructureNamedCreator { @Override public Function forNames(List names) { @@ -633,7 +659,7 @@ public Function forName(String name) { } - static StructureData> optional(StructureMaker> creator) { + static StructureNamedCreator> optional(StructureMaker> creator) { return new StructureData<>(creator, (a, b) -> a.or(() -> b)); } @@ -649,13 +675,15 @@ static StructureNamedCreator optionalLong(StructureMaker(creator, (a, b) -> a.isPresent() ? a : b); } - static StructureNamedCreator notOptional(StructureMaker creator) { + static StructureNamedCreator notOptional(StructureMaker creator, Object defaultValue) { return new StructureNamedCreator<>() { + @SuppressWarnings("unchecked") @Override public Function forName(String name) { - return creator.make(name, true); + return creator.make(name, true).andThen(o -> o == null ? (T) defaultValue : o); } + @SuppressWarnings("unchecked") @Override public Function forNames(List names) { if (names.isEmpty()) { @@ -663,7 +691,7 @@ public Function forNames(List names) { } if (names.size() == 1) { - return creator.make(names.getFirst(), true); + return forName(names.getFirst()); } var creators = new ArrayList>(); @@ -673,14 +701,14 @@ public Function forNames(List names) { } return container -> { - T result = null; + T result; for (var f : creators) { result = f.apply(container); if (result != null) { return result; } } - return result; + return (T) defaultValue; }; } }; @@ -688,14 +716,14 @@ public Function forNames(List names) { } @SuppressWarnings({"unchecked", "rawtypes"}) - private static StructureNamedCreator structureByNameCreator(CreationContext options, RecordStructure builder, Type type, Function getter, Function> creator, List structureInfos, Function structureUpdater) { + private static StructureNamedCreator structureByNameCreator(CreationContext options, RecordStructure builder, Type type, Function getter, Function> creator, List structureInfos, Function structureUpdater, Object defaultValue) { Structure mutableExplicitStructure = null; if (!structureInfos.isEmpty()) { mutableExplicitStructure = (Structure) parseValue(structureInfos.getFirst().value()); if (structureInfos.getFirst().directOptional()) { final var explicitStructure = mutableExplicitStructure; return StructureNamedCreator.notOptional((serializedName, first) -> ((Function>) builder.addOptional(serializedName, structureUpdater.apply(explicitStructure), first ? (Function) getter.andThen(Optional::ofNullable) : o -> Optional.empty())) - .andThen(o -> o.orElse(null))); + .andThen(o -> o.orElse(null)), defaultValue); } } @@ -704,27 +732,31 @@ private static StructureNamedCreator structureByNameCreator(CreationContext o if (type instanceof ParameterizedType parameterizedType && parameterizedType.getRawType() instanceof Class rawType) { if (rawType.equals(Optional.class)) { var innerType = parameterizedType.getActualTypeArguments()[0]; - return StructureNamedCreator.optional((serializedName, first) -> builder.addOptional(serializedName, structureUpdater.apply(explicitStructure != null ? explicitStructure : creator.apply(innerType)), first ? (Function) getter : o -> Optional.empty())); + return StructureNamedCreator.optional((serializedName, first) -> builder.addOptional(serializedName, structureUpdater.apply(explicitStructure != null ? explicitStructure : creator.apply(innerType)), first ? (Function) getter : o -> Optional.empty())) + .andThen(o -> ((Optional) o).or(() -> Optional.ofNullable(defaultValue))); } } else if (type instanceof Class clazz) { if (clazz.equals(OptionalInt.class)) { - return StructureNamedCreator.optionalInt((serializedName, first) -> builder.addOptionalInt(serializedName, structureUpdater.apply(explicitStructure != null ? explicitStructure : Structure.INT), first ? (Function) getter : o -> OptionalInt.empty())); + return StructureNamedCreator.optionalInt((serializedName, first) -> builder.addOptionalInt(serializedName, structureUpdater.apply(explicitStructure != null ? explicitStructure : Structure.INT), first ? (Function) getter : o -> OptionalInt.empty())) + .andThen(o -> ((OptionalInt) o).isPresent() ? o : defaultValue == null ? o : OptionalInt.of((int) defaultValue)); } else if (clazz.equals(OptionalDouble.class)) { - return StructureNamedCreator.optionalDouble((serializedName, first) -> builder.addOptionalDouble(serializedName, structureUpdater.apply(explicitStructure != null ? explicitStructure : Structure.DOUBLE), first ? (Function) getter : o -> OptionalDouble.empty())); + return StructureNamedCreator.optionalDouble((serializedName, first) -> builder.addOptionalDouble(serializedName, structureUpdater.apply(explicitStructure != null ? explicitStructure : Structure.DOUBLE), first ? (Function) getter : o -> OptionalDouble.empty())) + .andThen(o -> ((OptionalDouble) o).isPresent() ? o : defaultValue == null ? o : OptionalDouble.of((double) defaultValue)); } else if (clazz.equals(OptionalLong.class)) { - return StructureNamedCreator.optionalLong((serializedName, first) -> builder.addOptionalLong(serializedName, structureUpdater.apply(explicitStructure != null ? explicitStructure : Structure.LONG), first ? (Function) getter : o -> OptionalLong.empty())); + return StructureNamedCreator.optionalLong((serializedName, first) -> builder.addOptionalLong(serializedName, structureUpdater.apply(explicitStructure != null ? explicitStructure : Structure.LONG), first ? (Function) getter : o -> OptionalLong.empty())) + .andThen(o -> ((OptionalLong) o).isPresent() ? o : defaultValue == null ? o : OptionalLong.of((long) defaultValue)); } } boolean isNotNull = options.hasOption(SimpleCreatorOption.NOT_NULL_BY_DEFAULT); StructureNamedCreator key; - if (isNotNull || (type instanceof Class clazz && clazz.isPrimitive())) { + if (defaultValue == null && (isNotNull || (type instanceof Class clazz && clazz.isPrimitive()))) { key = StructureNamedCreator.notOptional((serializedName, first) -> first ? builder.add(serializedName, structureUpdater.apply(explicitStructure != null ? explicitStructure : creator.apply(type)), (Function) getter) : ((Function>) builder.addOptional(serializedName, structureUpdater.apply(explicitStructure != null ? explicitStructure : creator.apply(type)), o -> Optional.empty())) - .andThen(o -> o.orElse(null))); + .andThen(o -> o.orElse(null)), defaultValue); } else { key = StructureNamedCreator.notOptional((serializedName, first) -> ((Function>) builder.addOptional(serializedName, structureUpdater.apply(explicitStructure != null ? explicitStructure : creator.apply(type)), first ? (Function) getter.andThen(Optional::ofNullable) : o -> Optional.empty())) - .andThen(o -> o.orElse(null))); + .andThen(o -> o.orElse(null)), defaultValue); } return key; } @@ -1066,7 +1098,7 @@ private List> validCtors(Class exact) { @Override public Structure create(Class exact, TypedCreator[] parameters, Function> creator) { - return Structure.record(builder -> { + return Structure.flatRecord(builder -> { Constructor validCtor; if (exact.isRecord()) { Class[] types = new Class[exact.getRecordComponents().length]; @@ -1184,9 +1216,6 @@ public Structure create(Class exact, TypedCreator[] parameters, Function 3) || @@ -1278,6 +1307,12 @@ public Structure create(Class exact, TypedCreator[] parameters, Function { + if (elements.stream().anyMatch(e -> e.getAnnotation(Transient.class) != null)) { + types.remove(property); + } + }); + var properties = new LinkedHashSet(); for (var entry : types.keySet()) { if (getters.containsKey(entry) && (setters.containsKey(entry) || ctorSetters.containsKey(entry))) { @@ -1316,7 +1351,7 @@ public Structure create(Class exact, TypedCreator[] parameters, Function create(Class exact, TypedCreator[] parameters, Function) lookup.lookupClass().getConstructor().newInstance(); - return instance; + return container -> { + try { + return DataResult.success(instance.apply(container)); + } catch (Exception t) { + return DataResult.error(() -> "Failed to construct " + exact + ": " + t.getMessage()); + } + }; } catch (Throwable e) { throw new RuntimeException(e); } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java index 12aec8f..d65d39c 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java @@ -139,7 +139,7 @@ public DataResult>> list(App single) { } @Override - public DataResult> record(List> fields, Function creator) { + public DataResult> record(List> fields, Function> creator) { var object = OBJECT.get(); var properties = new JsonObject(); var required = new JsonArray(); diff --git a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java index 011d5a2..e6e0915 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/config/ConfigScreenInterpreter.java @@ -891,7 +891,7 @@ public DataResult>> unboundedMap(App< } @Override - public DataResult> record(List> fields, Function creator) { + public DataResult> record(List> fields, Function> creator) { List> entries = new ArrayList<>(); List> errors = new ArrayList<>(); for (var field : fields) { diff --git a/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java b/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java index 1a20b72..1422f14 100644 --- a/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java +++ b/src/minecraft/java/dev/lukebemish/codecextras/stream/structured/StreamCodecInterpreter.java @@ -241,7 +241,7 @@ private static StreamCodec> list(StreamCodec DataResult, A>> record(List> fields, Function creator) { + public DataResult, A>> record(List> fields, Function> creator) { var streamFields = new ArrayList>(); for (var field : fields) { DataResult, A>> result = recordSingleField(field, streamFields); @@ -258,7 +258,9 @@ public DataResult, A>> record(List { + throw new DecoderException("Failed to decode record: " + s); + }).getOrThrow(); } ))); } diff --git a/src/test/java/dev/lukebemish/codecextras/test/structured/reflective/TestOptionalBehavior.java b/src/test/java/dev/lukebemish/codecextras/test/structured/reflective/TestOptionalBehavior.java new file mode 100644 index 0000000..edb4255 --- /dev/null +++ b/src/test/java/dev/lukebemish/codecextras/test/structured/reflective/TestOptionalBehavior.java @@ -0,0 +1,56 @@ +package dev.lukebemish.codecextras.test.structured.reflective; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.JsonOps; +import dev.lukebemish.codecextras.structured.CodecInterpreter; +import dev.lukebemish.codecextras.structured.IdentityInterpreter; +import dev.lukebemish.codecextras.structured.Structure; +import dev.lukebemish.codecextras.structured.reflective.ReflectiveStructureCreator; +import dev.lukebemish.codecextras.structured.reflective.annotations.Default; +import dev.lukebemish.codecextras.structured.reflective.annotations.Structured; +import dev.lukebemish.codecextras.structured.reflective.annotations.Value; +import dev.lukebemish.codecextras.test.CodecAssertions; +import java.util.Optional; +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.OptionalLong; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class TestOptionalBehavior { + public record TestRecord( + @Default(value = @Value(intValue = 1)) int a, + @Default(value = @Value(intValue = 1)) OptionalInt b, + @Default(value = @Value(longValue = 1)) OptionalLong c, + @Default(value = @Value(doubleValue = 1)) OptionalDouble d, + @Default(value = @Value(stringValue = "string")) String e, + @Default(value = @Value(stringValue = "string")) Optional f, + @Default(value = @Value(location = TestOptionalBehavior.class, field = "OPTIONAL_VALUE")) @Structured(value = @Value(location = TestOptionalBehavior.class, field = "OPTIONAL_STRUCTURE"), directOptional = true) Optional g + ) {} + + public static Structure> OPTIONAL_STRUCTURE = Structure.STRING.flatComapMap(Optional::of, o -> o.map(DataResult::success).orElseGet(() -> DataResult.error(() -> "No value present"))); + public static Optional OPTIONAL_VALUE = Optional.of("string"); + + private static final Structure STRUCTURE = ReflectiveStructureCreator.create(TestRecord.class); + private static final Codec CODEC = CodecInterpreter.create().interpret(STRUCTURE).getOrThrow(); + + private final TestRecord defaultValue = new TestRecord( + 1, OptionalInt.of(1), OptionalLong.of(1), + OptionalDouble.of(1), "string", Optional.of("string"), + Optional.of("string") + ); + + private final String json = "{}"; + + @Test + void testDecode() { + CodecAssertions.assertDecodes(JsonOps.INSTANCE, json, defaultValue, CODEC); + } + + @Test + void testIdentity() { + var instance = IdentityInterpreter.INSTANCE.interpret(STRUCTURE).getOrThrow(); + Assertions.assertEquals(defaultValue, instance); + } +} diff --git a/src/test/java/dev/lukebemish/codecextras/test/structured/reflective/TestReflectiveStructureAnnotations.java b/src/test/java/dev/lukebemish/codecextras/test/structured/reflective/TestReflectiveStructureAnnotations.java index f60706c..4870191 100644 --- a/src/test/java/dev/lukebemish/codecextras/test/structured/reflective/TestReflectiveStructureAnnotations.java +++ b/src/test/java/dev/lukebemish/codecextras/test/structured/reflective/TestReflectiveStructureAnnotations.java @@ -32,7 +32,7 @@ public class TestReflectiveStructureAnnotations { public record TestRecordAnnotated( @Annotated( key = @Value(location = Annotation.class, field = "COMMENT"), - stringValue = "Commented field with @Annotated" + value = @Value(stringValue = "Commented field with @Annotated") ) int a, @Comment("Commented field with @Comment") int b, @Structured(@Value(location = TestReflectiveStructureAnnotations.class, field = "INT_AS_LIST")) int c @@ -41,7 +41,7 @@ public record TestRecordAnnotated( public static class TestFieldAnnotated { @Annotated( key = @Value(location = Annotation.class, field = "COMMENT"), - stringValue = "Commented field with @Annotated" + value = @Value(stringValue = "Commented field with @Annotated") ) public int a; @@ -81,7 +81,7 @@ public void setC(int c) { @Annotated( key = @Value(location = Annotation.class, field = "COMMENT"), - stringValue = "Commented field with @Annotated" + value = @Value(stringValue = "Commented field with @Annotated") ) public int getA() { return this.a; From 0dfcec53f418f3634f282149171a05b8a4ff44b9 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Wed, 2 Apr 2025 20:29:30 -0500 Subject: [PATCH 25/28] Working generic records and arrays and structured data elements --- .../codecextras/mutable/DataElement.java | 2 +- .../codecextras/mutable/DataElementType.java | 50 ++-- .../mutable/GenericDataElementType.java | 41 +++ .../structured/StructuredDataElementType.java | 128 +++++++++ .../reflective/ParameterizedTypeResults.java | 3 + .../ReflectiveStructureCreator.java | 104 ++++++-- .../BuiltInReflectiveStructureCreator.java | 252 ++++++++++-------- .../implementation/GenericArrayTypeImpl.java | 35 +++ .../implementation/ParameterizedTypeImpl.java | 64 +++++ .../implementation/TypeResolver.java | 53 ++++ .../implementation/WildcardTypeImpl.java | 59 ++++ .../test/mutable/TestDataElements.java | 3 +- .../reflective/TestGenericRecord.java | 91 +++++++ 13 files changed, 720 insertions(+), 165 deletions(-) create mode 100644 src/main/java/dev/lukebemish/codecextras/mutable/GenericDataElementType.java create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/StructuredDataElementType.java create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/reflective/ParameterizedTypeResults.java create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/reflective/implementation/GenericArrayTypeImpl.java create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/reflective/implementation/ParameterizedTypeImpl.java create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/reflective/implementation/TypeResolver.java create mode 100644 src/main/java/dev/lukebemish/codecextras/structured/reflective/implementation/WildcardTypeImpl.java create mode 100644 src/test/java/dev/lukebemish/codecextras/test/structured/reflective/TestGenericRecord.java diff --git a/src/main/java/dev/lukebemish/codecextras/mutable/DataElement.java b/src/main/java/dev/lukebemish/codecextras/mutable/DataElement.java index 1751dcd..73877d4 100644 --- a/src/main/java/dev/lukebemish/codecextras/mutable/DataElement.java +++ b/src/main/java/dev/lukebemish/codecextras/mutable/DataElement.java @@ -6,7 +6,7 @@ import java.util.function.Supplier; /** - * A holdr for a mutable value. + * A holder for a mutable value. * @param the type of the value */ public interface DataElement { diff --git a/src/main/java/dev/lukebemish/codecextras/mutable/DataElementType.java b/src/main/java/dev/lukebemish/codecextras/mutable/DataElementType.java index 93833b7..a42381d 100644 --- a/src/main/java/dev/lukebemish/codecextras/mutable/DataElementType.java +++ b/src/main/java/dev/lukebemish/codecextras/mutable/DataElementType.java @@ -18,12 +18,8 @@ * @param the type of the holder object * @param the type of data being retrieved */ -public interface DataElementType { - /** - * {@return a matching {@link DataElement} retrieved from the provided object} - * @param data the object to retrieve the element from - */ - DataElement from(D data); +// TODO: rename to CodecDataElementType in next BC window +public interface DataElementType extends GenericDataElementType { /** * {@return the codec to (de)serialize the element} @@ -31,9 +27,23 @@ public interface DataElementType { Codec codec(); /** - * {@return the name of the data type} Used when encoding; should be unique within a given set of data types. + * @deprecated use the method on {@link GenericDataElementType} instead + * @see GenericDataElementType#cleaner(GenericDataElementType[]) */ - String name(); + @Deprecated(forRemoval = true) + static Consumer cleaner(GenericDataElementType... types) { + return GenericDataElementType.cleaner(types); + } + + /** + * @deprecated use the method on {@link GenericDataElementType} instead + * @see GenericDataElementType#cleaner(List) + */ + // Being moved to GenericDataElementType + @Deprecated(forRemoval = true) + static Consumer cleaner(List> types) { + return GenericDataElementType.cleaner(types); + } /** * {@return a new {@link DataElementType} with the provided name, codec, and getter} @@ -62,30 +72,6 @@ public String name() { }; } - /** - * {@return a {@link Consumer} that marks all the provided data elements as clean} - * @param types the data elements to mark as clean - * @param the type of object containing the data elements - */ - @SafeVarargs - static Consumer cleaner(DataElementType... types) { - List> list = List.of(types); - return cleaner(list); - } - - /** - * {@return a {@link Consumer} that marks all the provided data elements as clean} - * @param types the data elements to mark as clean - * @param the type of object containing the data elements - */ - static Consumer cleaner(List> types) { - return data -> { - for (var type : types) { - type.from(data).setDirty(false); - } - }; - } - /** * Creates a {@link Codec} for a series of data elements. This codec will encode from an instance of the type that * holds the data elements, and will decode to a {@link Consumer} that can be applied to an instance of that type to diff --git a/src/main/java/dev/lukebemish/codecextras/mutable/GenericDataElementType.java b/src/main/java/dev/lukebemish/codecextras/mutable/GenericDataElementType.java new file mode 100644 index 0000000..226bf03 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/mutable/GenericDataElementType.java @@ -0,0 +1,41 @@ +package dev.lukebemish.codecextras.mutable; + +import java.util.List; +import java.util.function.Consumer; + +public interface GenericDataElementType { + /** + * {@return a matching {@link DataElement} retrieved from the provided object} + * @param data the object to retrieve the element from + */ + DataElement from(D data); + + /** + * {@return the name of the data type} Used when encoding; should be unique within a given set of data types. + */ + String name(); + + /** + * {@return a {@link Consumer } that marks all the provided data elements as clean} + * @param types the data elements to mark as clean + * @param the type of object containing the data elements + */ + @SafeVarargs + static Consumer cleaner(GenericDataElementType... types) { + List> list = List.of(types); + return cleaner(list); + } + + /** + * {@return a {@link Consumer} that marks all the provided data elements as clean} + * @param types the data elements to mark as clean + * @param the type of object containing the data elements + */ + static Consumer cleaner(List> types) { + return data -> { + for (var type : types) { + type.from(data).setDirty(false); + } + }; + } +} diff --git a/src/main/java/dev/lukebemish/codecextras/structured/StructuredDataElementType.java b/src/main/java/dev/lukebemish/codecextras/structured/StructuredDataElementType.java new file mode 100644 index 0000000..4812c63 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/StructuredDataElementType.java @@ -0,0 +1,128 @@ +package dev.lukebemish.codecextras.structured; + +import com.mojang.serialization.DataResult; +import dev.lukebemish.codecextras.Asymmetry; +import dev.lukebemish.codecextras.mutable.DataElement; +import dev.lukebemish.codecextras.mutable.GenericDataElementType; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +public interface StructuredDataElementType extends GenericDataElementType { + Structure structure(); + + static StructuredDataElementType create(String name, Structure structure, Function> getter) { + return new StructuredDataElementType<>() { + @Override + public Structure structure() { + return structure; + } + + @Override + public DataElement from(D data) { + return getter.apply(data); + } + + @Override + public String name() { + return name; + } + }; + } + + @SafeVarargs + static Structure, D>> structure(boolean encodeFull, StructuredDataElementType... elements) { + List> list = List.of(elements); + return structure(encodeFull, list); + } + + static Structure, D>> structure(boolean encodeFull, List> elements) { + Map> elementTypeMap = new HashMap<>(); + + for (var element : elements) { + if (elementTypeMap.containsKey(element.name())) { + throw new IllegalArgumentException("Duplicate name for DataElementType: " + element.name()); + } + elementTypeMap.put(element.name(), element); + } + + return Structure.flatRecord(builder -> { + record Mutation(StructuredDataElementType elementType, T value) { + public void set(D data) { + elementType.from(data).set(value); + } + + static RecordStructure.Key>>> of(boolean encodeFull, StructuredDataElementType elementType, RecordStructure, D>> asymmetryBuilder) { + return asymmetryBuilder.addOptional( + elementType.name(), + elementType.structure().flatComapMap( + t -> DataResult.success(new Mutation<>(elementType, t)), + r -> r.map(Mutation::value) + ), + asymmetry -> { + DataResult>> nested = asymmetry.encoding().map(d -> + elementType.from(d).ifEncodingOrElse(encodeFull, t -> + Optional.of(new Mutation<>(elementType, t)), + Optional::empty + ) + ); + return nested.mapOrElse( + optional -> optional.map(DataResult::success), + error -> Optional.of(DataResult.error(error.messageSupplier())) + ); + } + ); + } + } + + Map>>>> containerKeys = new IdentityHashMap<>(); + List keysInOrder = new ArrayList<>(); + + for (var element : elements) { + RecordStructure.Key>>> containerKey = Mutation.of(encodeFull, element, builder); + + var key = new Object(); + containerKeys.put(key, containerKey); + keysInOrder.add(key); + } + + return container -> { + Map> mutations = new IdentityHashMap<>(); + List foundKeys = new ArrayList<>(); + List> errors = new ArrayList<>(); + for (var key : keysInOrder) { + var containerKey = containerKeys.get(key); + var value = containerKey.apply(container); + value.ifPresent(result -> { + result.ifError(e -> errors.add(e.messageSupplier())); + result.ifSuccess(mutation -> { + mutations.put(key, mutation); + foundKeys.add(key); + }); + }); + } + Consumer consumer = d -> { + for (var key : foundKeys) { + mutations.get(key).set(d); + } + }; + if (errors.isEmpty()) { + return DataResult.success(Asymmetry.ofDecoding(consumer)); + } else { + var error = errors.getFirst(); + var result = DataResult., D>>error(error, Asymmetry.ofDecoding(consumer)); + for (var e : errors.subList(1, errors.size())) { + result = result.mapError(s -> s + "; " + e.get()); + } + return result; + } + }; + }); + } +} diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/ParameterizedTypeResults.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/ParameterizedTypeResults.java new file mode 100644 index 0000000..132c0a8 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/ParameterizedTypeResults.java @@ -0,0 +1,3 @@ +package dev.lukebemish.codecextras.structured.reflective; + +record ParameterizedTypeResults(Class rawType, ReflectiveStructureCreator.TypedCreator[] parameterCreators) {} diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java index 850464b..8da5572 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java @@ -12,6 +12,7 @@ import dev.lukebemish.codecextras.structured.reflective.systems.Creators; import dev.lukebemish.codecextras.structured.reflective.systems.FlexibleCreators; import dev.lukebemish.codecextras.structured.reflective.systems.ParameterizedCreators; +import java.lang.reflect.GenericArrayType; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; @@ -198,38 +199,39 @@ private static Structure forType(Map> cachedCreators, Map< Supplier creatorSupplier = () -> { Class rawType = null; TypedCreator[] parameterCreators = null; + if (type instanceof ParameterizedType parameterizedType) { if (parameterizedType.getRawType() instanceof Class clazz) { rawType = clazz; var parameters = parameterizedType.getActualTypeArguments(); parameterCreators = new TypedCreator[parameters.length]; - if (parameterizedCreatorsMap.containsKey(clazz)) { - for (int i = 0; i < parameters.length; i++) { - var structure = forType(cachedCreators, recursionCache, parameters[i], context); - var parameterType = parameters[i]; - parameterCreators[i] = new TypedCreator() { - @Override - public Structure create() { - return structure; - } - - @Override - public Type type() { - return parameterType; - } - - @Override - public Class rawType() { - if (parameterType instanceof Class clazz) { - return clazz; - } else if (parameterType instanceof ParameterizedType parameterizedType) { - return (Class) parameterizedType.getRawType(); - } else { - throw new IllegalArgumentException("Unknown type: " + type); - } + for (int i = 0; i < parameters.length; i++) { + var structure = forType(cachedCreators, recursionCache, parameters[i], context); + var parameterType = parameters[i]; + parameterCreators[i] = new TypedCreator() { + @Override + public Structure create() { + return structure; + } + + @Override + public Type type() { + return parameterType; + } + + @Override + public Class rawType() { + if (parameterType instanceof Class clazz) { + return clazz; + } else if (parameterType instanceof ParameterizedType parameterizedType) { + return (Class) parameterizedType.getRawType(); + } else { + throw new IllegalArgumentException("Unknown type: " + type); } - }; - } + } + }; + } + if (parameterizedCreatorsMap.containsKey(clazz)) { return parameterizedCreatorsMap.get(clazz).creator(parameterCreators); } } @@ -240,6 +242,10 @@ public Class rawType() { if (foundCreator != null) { return foundCreator; } + } else if (type instanceof GenericArrayType genericArrayType) { + var results = handleGenericArrayType(cachedCreators, recursionCache, type, context, genericArrayType); + rawType = results.rawType().arrayType(); + parameterCreators = results.parameterCreators(); } else { throw new IllegalArgumentException("Unknown type: " + type); } @@ -259,4 +265,50 @@ public Class rawType() { cachedCreators.put(type, full.get()); return full.get(); } + + private static ParameterizedTypeResults handleGenericArrayType(Map> cachedCreators, Map> recursionCache, Type type, CreationContext context, GenericArrayType genericArrayType) { + TypedCreator[] parameterCreators; + Class rawType; + var componentType = genericArrayType.getGenericComponentType(); + if (componentType instanceof Class clazz) { + rawType = clazz; + parameterCreators = new TypedCreator[0]; + } else if (componentType instanceof ParameterizedType parameterizedType && parameterizedType.getRawType() instanceof Class clazz) { + rawType = clazz; + parameterCreators = new TypedCreator[parameterizedType.getActualTypeArguments().length]; + for (int i = 0; i < parameterizedType.getActualTypeArguments().length; i++) { + var structure = forType(cachedCreators, recursionCache, parameterizedType.getActualTypeArguments()[i], context); + var parameterType = parameterizedType.getActualTypeArguments()[i]; + parameterCreators[i] = new TypedCreator() { + @Override + public Structure create() { + return structure; + } + + @Override + public Type type() { + return parameterType; + } + + @Override + public Class rawType() { + if (parameterType instanceof Class clazz) { + return clazz; + } else if (parameterType instanceof ParameterizedType parameterizedType) { + return (Class) parameterizedType.getRawType(); + } else { + throw new IllegalArgumentException("Unknown type: " + type); + } + } + }; + } + } else if (componentType instanceof GenericArrayType genericArrayComponentType) { + var results = handleGenericArrayType(cachedCreators, recursionCache, type, context, genericArrayComponentType); + rawType = results.rawType().arrayType(); + parameterCreators = results.parameterCreators(); + } else { + throw new IllegalArgumentException("Unknown type: " + type); + } + return new ParameterizedTypeResults(rawType, parameterCreators); + } } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/implementation/BuiltInReflectiveStructureCreator.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/implementation/BuiltInReflectiveStructureCreator.java index 4d9979f..9b26978 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/implementation/BuiltInReflectiveStructureCreator.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/implementation/BuiltInReflectiveStructureCreator.java @@ -100,6 +100,7 @@ import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Supplier; +import java.util.function.UnaryOperator; import org.jetbrains.annotations.ApiStatus; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.ConstantDynamic; @@ -1042,7 +1043,28 @@ public boolean supports(Class exact, TypedCreator[] parameters) { @Override public Structure create(Class exact, TypedCreator[] parameters, Function> creator) { var arrayMaker = arrayMaker(exact.getComponentType()); - return creator.apply(exact.getComponentType()).listOf().xmap(arrayMaker::apply, obj -> { + Type genericComponentType = exact.getComponentType(); + Class baseArrayType = exact.getComponentType(); + int depth = 0; + while (baseArrayType.isArray()) { + depth++; + baseArrayType = baseArrayType.getComponentType(); + } + if (parameters.length != 0) { + Type[] parameterTypes = new Type[parameters.length]; + for (int i = 0; i < parameters.length; i++) { + parameterTypes[i] = parameters[i].type(); + } + genericComponentType = new ParameterizedTypeImpl( + parameterTypes, + baseArrayType, + null + ); + for (int i = 0; i < depth; i++) { + genericComponentType = new GenericArrayTypeImpl(genericComponentType); + } + } + return creator.apply(genericComponentType).listOf().xmap(arrayMaker::apply, obj -> { var array = (Object[]) obj; var list = new ArrayList<>(array.length); list.addAll(Arrays.asList(array)); @@ -1056,49 +1078,21 @@ public boolean supports(Class exact, TypedCreator[] parameters) { } }) .add(new FlexibleCreators.FlexibleCreator() { - private List> validCtors(Class exact) { - List> validCtors = new ArrayList<>(); - for (var ctor : exact.getConstructors()) { - if (!ctor.accessFlags().contains(AccessFlag.PUBLIC)) { - continue; - } - if (ctor.getParameterCount() == 0) { - validCtors.add(ctor); - } else { - var hasSerializedProperties = true; - for (var param : ctor.getParameters()) { - if (!param.isAnnotationPresent(SerializedProperty.class)) { - hasSerializedProperties = false; - break; - } - var annotation = param.getAnnotation(SerializedProperty.class); - try { - var field = exact.getField(annotation.value()); - if (field.getType().equals(param.getType()) && field.accessFlags().contains(AccessFlag.PUBLIC) && !field.accessFlags().contains(AccessFlag.STATIC)) { - continue; - } - } catch (NoSuchFieldException ignored) {} - - try { - var getterMethod = exact.getMethod("get" + annotation.value().substring(0, 1).toUpperCase() + annotation.value().substring(1)); - if (getterMethod.getGenericReturnType().equals(param.getParameterizedType()) && getterMethod.accessFlags().contains(AccessFlag.PUBLIC) && !getterMethod.accessFlags().contains(AccessFlag.STATIC)) { - continue; - } - } catch (NoSuchMethodException ignored) {} - hasSerializedProperties = false; - break; - } - if (hasSerializedProperties) { - validCtors.add(ctor); - } - } - } - return validCtors; - } - @Override public Structure create(Class exact, TypedCreator[] parameters, Function> creator) { return Structure.flatRecord(builder -> { + final UnaryOperator resolver; + if (parameters.length != 0) { + var typeVars = exact.getTypeParameters(); + var values = new Type[typeVars.length]; + for (int i = 0; i < typeVars.length; i++) { + values[i] = parameters[i].type(); + } + resolver = type -> TypeResolver.resolve(type, typeVars, values); + } else { + resolver = UnaryOperator.identity(); + } + Constructor validCtor; if (exact.isRecord()) { Class[] types = new Class[exact.getRecordComponents().length]; @@ -1142,7 +1136,7 @@ public Structure create(Class exact, TypedCreator[] parameters, Function create(Class exact, TypedCreator[] parameters, Function create(Class exact, TypedCreator[] parameters, Function create(Class exact, TypedCreator[] parameters, Function 3) || - (method.getName().startsWith("is") && method.getName().length() > 2 && method.getGenericReturnType().equals(Boolean.TYPE)) - ); - var isSetter = method.getParameterCount() == 1 && method.getName().startsWith("set") && method.getName().length() > 3 && method.getGenericReturnType().equals(Void.TYPE); - if (isGetter) { - var property = method.getName().substring(method.getName().startsWith("is") ? 2 : 3); - property = property.substring(0, 1).toLowerCase() + property.substring(1); - if (!types.containsKey(property) || method.getGenericReturnType().equals(types.get(property))) { - types.put(property, method.getGenericReturnType()); - if (!getters.containsKey(property)) { - try { - var getter = functionWrapper(MethodHandles.lookup().unreflect(method)); - getters.put(property, getter); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } - } - context.computeIfAbsent(property, k -> new LinkedHashSet<>()).add(method); - } - } if (isSetter) { - var property = method.getName().substring(3); - property = property.substring(0, 1).toLowerCase() + property.substring(1); - if (!types.containsKey(property) || method.getParameterTypes()[0].equals(types.get(property))) { - types.put(property, method.getGenericParameterTypes()[0]); - if (!setters.containsKey(property)) { - try { - var setter = MethodHandles.lookup().unreflect(method); - setters.put(property, setter); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } - } - context.computeIfAbsent(property, k -> new LinkedHashSet<>()).add(method); - } - } - } - } - - for (var field : exact.getFields()) { - if (field.accessFlags().contains(AccessFlag.PUBLIC) && !field.accessFlags().contains(AccessFlag.STATIC) && !field.accessFlags().contains(AccessFlag.TRANSIENT)) { - try { - if (!types.containsKey(field.getName())) { - types.put(field.getName(), field.getGenericType()); - } - context.computeIfAbsent(field.getName(), k -> new LinkedHashSet<>()).add(field); - if (!getters.containsKey(field.getName())) { - var getter = functionWrapper(MethodHandles.lookup().unreflectGetter(field)); - getters.put(field.getName(), getter); - } - if (!setters.containsKey(field.getName()) && (!ctorSetters.containsKey(field.getName())) && !field.accessFlags().contains(AccessFlag.FINAL)) { - var setter = MethodHandles.lookup().unreflectSetter(field); - setters.put(field.getName(), setter); - } - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } - } - } + discoverBeanProperties(exact, types, getters, context, setters, ctorSetters); } // Go from high priority to low priority @@ -1422,12 +1356,120 @@ public boolean supports(Class exact, TypedCreator[] parameters) { return true; } + if (parameters.length != 0 && exact.getTypeParameters().length != parameters.length) { + return false; + } + return validCtors(exact).size() == 1; } }) .build(); } + private static void discoverBeanProperties(Class exact, Map types, Map> getters, Map> context, Map setters, Map existingSetters) { + for (var method : exact.getMethods()) { + if (method.accessFlags().contains(AccessFlag.PUBLIC) && !method.accessFlags().contains(AccessFlag.STATIC)) { + var isGetter = method.getParameterCount() == 0 && ( + (method.getName().startsWith("get") && method.getName().length() > 3) || + (method.getName().startsWith("is") && method.getName().length() > 2 && method.getGenericReturnType().equals(Boolean.TYPE)) + ); + var isSetter = method.getParameterCount() == 1 && method.getName().startsWith("set") && method.getName().length() > 3 && method.getGenericReturnType().equals(Void.TYPE); + if (isGetter) { + var property = method.getName().substring(method.getName().startsWith("is") ? 2 : 3); + property = property.substring(0, 1).toLowerCase() + property.substring(1); + if (!types.containsKey(property) || method.getGenericReturnType().equals(types.get(property))) { + types.put(property, method.getGenericReturnType()); + if (!getters.containsKey(property)) { + try { + var getter = functionWrapper(MethodHandles.lookup().unreflect(method)); + getters.put(property, getter); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + context.computeIfAbsent(property, k -> new LinkedHashSet<>()).add(method); + } + } if (isSetter) { + var property = method.getName().substring(3); + property = property.substring(0, 1).toLowerCase() + property.substring(1); + if (!types.containsKey(property) || method.getParameterTypes()[0].equals(types.get(property))) { + types.put(property, method.getGenericParameterTypes()[0]); + if (!setters.containsKey(property)) { + try { + var setter = MethodHandles.lookup().unreflect(method); + setters.put(property, setter); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + context.computeIfAbsent(property, k -> new LinkedHashSet<>()).add(method); + } + } + } + } + + for (var field : exact.getFields()) { + if (field.accessFlags().contains(AccessFlag.PUBLIC) && !field.accessFlags().contains(AccessFlag.STATIC) && !field.accessFlags().contains(AccessFlag.TRANSIENT)) { + try { + if (!types.containsKey(field.getName())) { + types.put(field.getName(), field.getGenericType()); + } + context.computeIfAbsent(field.getName(), k -> new LinkedHashSet<>()).add(field); + if (!getters.containsKey(field.getName())) { + var getter = functionWrapper(MethodHandles.lookup().unreflectGetter(field)); + getters.put(field.getName(), getter); + } + if (!setters.containsKey(field.getName()) && (!existingSetters.containsKey(field.getName())) && !field.accessFlags().contains(AccessFlag.FINAL)) { + var setter = MethodHandles.lookup().unreflectSetter(field); + setters.put(field.getName(), setter); + } + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + } + } + + private List> validCtors(Class exact) { + List> validCtors = new ArrayList<>(); + for (var ctor : exact.getConstructors()) { + if (!ctor.accessFlags().contains(AccessFlag.PUBLIC)) { + continue; + } + if (ctor.getParameterCount() == 0) { + validCtors.add(ctor); + } else { + var hasSerializedProperties = true; + for (var param : ctor.getParameters()) { + if (!param.isAnnotationPresent(SerializedProperty.class)) { + hasSerializedProperties = false; + break; + } + var annotation = param.getAnnotation(SerializedProperty.class); + try { + var field = exact.getField(annotation.value()); + if (field.getType().equals(param.getType()) && field.accessFlags().contains(AccessFlag.PUBLIC) && !field.accessFlags().contains(AccessFlag.STATIC)) { + continue; + } + } catch (NoSuchFieldException ignored) {} + + try { + var getterMethod = exact.getMethod("get" + annotation.value().substring(0, 1).toUpperCase() + annotation.value().substring(1)); + if (getterMethod.getGenericReturnType().equals(param.getParameterizedType()) && getterMethod.accessFlags().contains(AccessFlag.PUBLIC) && !getterMethod.accessFlags().contains(AccessFlag.STATIC)) { + continue; + } + } catch (NoSuchMethodException ignored) {} + hasSerializedProperties = false; + break; + } + if (hasSerializedProperties) { + validCtors.add(ctor); + } + } + } + return validCtors; + } + private static void convertType(MethodVisitor mv, Class type) { if (type.isPrimitive()) { switch (type.getName()) { diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/implementation/GenericArrayTypeImpl.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/implementation/GenericArrayTypeImpl.java new file mode 100644 index 0000000..6f56419 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/implementation/GenericArrayTypeImpl.java @@ -0,0 +1,35 @@ +package dev.lukebemish.codecextras.structured.reflective.implementation; + +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.Type; +import java.util.Objects; + +class GenericArrayTypeImpl implements GenericArrayType { + private final Type genericComponentType; + + GenericArrayTypeImpl(Type genericComponentType) { + this.genericComponentType = genericComponentType; + } + + @Override + public Type getGenericComponentType() { + return genericComponentType; + } + + @Override + public boolean equals(Object object) { + if (this == object) return true; + if (!(object instanceof GenericArrayType that)) return false; + return Objects.equals(genericComponentType, that.getGenericComponentType()); + } + + @Override + public int hashCode() { + return Objects.hashCode(genericComponentType); + } + + @Override + public String toString() { + return genericComponentType.getTypeName() + "[]"; + } +} diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/implementation/ParameterizedTypeImpl.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/implementation/ParameterizedTypeImpl.java new file mode 100644 index 0000000..39031ba --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/implementation/ParameterizedTypeImpl.java @@ -0,0 +1,64 @@ +package dev.lukebemish.codecextras.structured.reflective.implementation; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.Objects; +import org.jspecify.annotations.Nullable; + +class ParameterizedTypeImpl implements ParameterizedType { + private final Type[] actualTypeArguments; + private final Type rawType; + private final @Nullable Type ownerType; + + ParameterizedTypeImpl(Type[] actualTypeArguments, Type rawType, @Nullable Type ownerType) { + this.actualTypeArguments = actualTypeArguments; + this.rawType = rawType; + this.ownerType = ownerType; + } + + @Override + public Type[] getActualTypeArguments() { + return Arrays.copyOf(actualTypeArguments, actualTypeArguments.length); + } + + @Override + public Type getRawType() { + return rawType; + } + + @Override + public @Nullable Type getOwnerType() { + return ownerType; + } + + @Override + public boolean equals(Object object) { + if (this == object) return true; + if (!(object instanceof ParameterizedType that)) return false; + return Objects.deepEquals(actualTypeArguments, that.getActualTypeArguments()) && Objects.equals(rawType, that.getRawType()) && Objects.equals(ownerType, that.getOwnerType()); + } + + @Override + public int hashCode() { + return Arrays.hashCode(actualTypeArguments) ^ Objects.hashCode(ownerType) ^ Objects.hashCode(rawType); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + if (ownerType != null) { + builder.append(ownerType.getTypeName()); + builder.append("$"); + } + builder.append(rawType.getTypeName()); + builder.append("<"); + builder.append(actualTypeArguments[0].getTypeName()); + for (int i = 1; i < actualTypeArguments.length; i++) { + builder.append(", "); + builder.append(actualTypeArguments[i].getTypeName()); + } + builder.append(">"); + return builder.toString(); + } +} diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/implementation/TypeResolver.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/implementation/TypeResolver.java new file mode 100644 index 0000000..5c8fbfb --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/implementation/TypeResolver.java @@ -0,0 +1,53 @@ +package dev.lukebemish.codecextras.structured.reflective.implementation; + +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; + +final class TypeResolver { + private TypeResolver() {} + + static Type resolve(Type type, TypeVariable>[] variables, Type[] values) { + switch (type) { + case Class ignored -> { + return type; + } + case ParameterizedType parameterizedType -> { + var args = parameterizedType.getActualTypeArguments(); + for (var i = 0; i < args.length; i++) { + args[i] = resolve(args[i], variables, values); + } + return new ParameterizedTypeImpl( + args, + parameterizedType.getRawType(), + parameterizedType.getOwnerType() == null ? null : resolve(parameterizedType.getOwnerType(), variables, values) + ); + } + case GenericArrayType genericArrayType -> { + return new GenericArrayTypeImpl(resolve(genericArrayType.getGenericComponentType(), variables, values)); + } + case WildcardType wildcardType -> { + var lowerBounds = wildcardType.getLowerBounds(); + var upperBounds = wildcardType.getUpperBounds(); + for (var i = 0; i < lowerBounds.length; i++) { + lowerBounds[i] = resolve(lowerBounds[i], variables, values); + } + for (var i = 0; i < upperBounds.length; i++) { + upperBounds[i] = resolve(upperBounds[i], variables, values); + } + return new WildcardTypeImpl(lowerBounds, upperBounds); + } + case TypeVariable typeVariable -> { + for (var i = 0; i < variables.length; i++) { + if (variables[i].equals(typeVariable)) { + return resolve(values[i], variables, values); + } + } + } + default -> {} + } + return type; + } +} diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/implementation/WildcardTypeImpl.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/implementation/WildcardTypeImpl.java new file mode 100644 index 0000000..9ceff75 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/implementation/WildcardTypeImpl.java @@ -0,0 +1,59 @@ +package dev.lukebemish.codecextras.structured.reflective.implementation; + +import java.lang.reflect.Type; +import java.lang.reflect.WildcardType; +import java.util.Arrays; +import java.util.Objects; + +class WildcardTypeImpl implements WildcardType { + private final Type[] lowerBounds; + private final Type[] upperBounds; + + WildcardTypeImpl(Type[] lowerBounds, Type[] upperBounds) { + this.lowerBounds = lowerBounds; + this.upperBounds = upperBounds; + } + + @Override + public Type[] getUpperBounds() { + return Arrays.copyOf(upperBounds, upperBounds.length); + } + + @Override + public Type[] getLowerBounds() { + return Arrays.copyOf(lowerBounds, lowerBounds.length); + } + + @Override + public boolean equals(Object object) { + if (this == object) return true; + if (!(object instanceof WildcardType that)) return false; + return Objects.deepEquals(lowerBounds, that.getLowerBounds()) && Objects.deepEquals(upperBounds, that.getUpperBounds()); + } + + @Override + public int hashCode() { + return Arrays.hashCode(upperBounds) ^ Arrays.hashCode(lowerBounds); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("?"); + if (lowerBounds.length > 0) { + builder.append(" super "); + builder.append(lowerBounds[0].getTypeName()); + for (int i = 1; i < lowerBounds.length; i++) { + builder.append(" & "); + builder.append(lowerBounds[i].getTypeName()); + } + } else if (upperBounds.length > 0 && !upperBounds[0].equals(Object.class)) { + builder.append(" extends "); + builder.append(upperBounds[0].getTypeName()); + for (int i = 1; i < upperBounds.length; i++) { + builder.append(" & "); + builder.append(upperBounds[i].getTypeName()); + } + } + return builder.toString(); + } +} diff --git a/src/test/java/dev/lukebemish/codecextras/test/mutable/TestDataElements.java b/src/test/java/dev/lukebemish/codecextras/test/mutable/TestDataElements.java index 7f3834e..34ddf3e 100644 --- a/src/test/java/dev/lukebemish/codecextras/test/mutable/TestDataElements.java +++ b/src/test/java/dev/lukebemish/codecextras/test/mutable/TestDataElements.java @@ -8,6 +8,7 @@ import dev.lukebemish.codecextras.Asymmetry; import dev.lukebemish.codecextras.mutable.DataElement; import dev.lukebemish.codecextras.mutable.DataElementType; +import dev.lukebemish.codecextras.mutable.GenericDataElementType; import dev.lukebemish.codecextras.test.CodecAssertions; import java.util.Objects; import java.util.function.Consumer; @@ -20,7 +21,7 @@ private static class WithDataElements { private static final DataElementType INTEGER = DataElementType.create("integer", Codec.INT, d -> d.integer); private static final Codec, WithDataElements>> CODEC = DataElementType.codec(true, STRING, INTEGER); private static final Codec, WithDataElements>> CHANGED_CODEC = DataElementType.codec(false, STRING, INTEGER); - private static final Consumer CLEANER = DataElementType.cleaner(STRING, INTEGER); + private static final Consumer CLEANER = GenericDataElementType.cleaner(STRING, INTEGER); private final DataElement string = new DataElement.Simple<>(""); private final DataElement integer = new DataElement.Simple<>(0); diff --git a/src/test/java/dev/lukebemish/codecextras/test/structured/reflective/TestGenericRecord.java b/src/test/java/dev/lukebemish/codecextras/test/structured/reflective/TestGenericRecord.java new file mode 100644 index 0000000..89ba979 --- /dev/null +++ b/src/test/java/dev/lukebemish/codecextras/test/structured/reflective/TestGenericRecord.java @@ -0,0 +1,91 @@ +package dev.lukebemish.codecextras.test.structured.reflective; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.JsonOps; +import dev.lukebemish.codecextras.structured.CodecInterpreter; +import dev.lukebemish.codecextras.structured.Structure; +import dev.lukebemish.codecextras.structured.reflective.ReflectiveStructureCreator; +import dev.lukebemish.codecextras.structured.reflective.annotations.SerializedProperty; +import dev.lukebemish.codecextras.test.CodecAssertions; +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.Test; + +public class TestGenericRecord { + public record GenericRecord(T value) {} + public record TestRecord(GenericRecord> generic) { + private static final Structure STRUCTURE = ReflectiveStructureCreator.create(TestRecord.class); + } + public static class TestGenericArray { + private final GenericRecord[][][] array; + + private static final Structure STRUCTURE = ReflectiveStructureCreator.create(TestGenericArray.class); + + public TestGenericArray(@SerializedProperty("array") GenericRecord[][][] array) { + this.array = array; + } + + public GenericRecord[][][] getArray() { + return array; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof TestGenericArray that)) return false; + return Arrays.deepEquals(array, that.array); + } + + @Override + public int hashCode() { + return Arrays.deepHashCode(array); + } + } + + private static final Codec CODEC = CodecInterpreter.create().interpret(TestRecord.STRUCTURE).getOrThrow(); + + private final String json = """ + { + "generic": { + "value": ["a", "b", "c"] + } + }"""; + + private final TestRecord object = new TestRecord(new GenericRecord<>(List.of("a", "b", "c"))); + + private final String genericArrayJson = """ + { + "array": [[[ + { + "value": "string" + } + ]]] + }"""; + + @SuppressWarnings("unchecked") + private final TestGenericArray genericArrayObject = new TestGenericArray(new GenericRecord[][][]{new GenericRecord[][]{new GenericRecord[]{ + new GenericRecord<>("string") + }}}); + + private static final Codec GENERIC_ARRAY_CODEC = CodecInterpreter.create().interpret(TestGenericArray.STRUCTURE).getOrThrow(); + + @Test + void testDecoding() { + CodecAssertions.assertDecodes(JsonOps.INSTANCE, json, object, CODEC); + } + + @Test + void testEncoding() { + CodecAssertions.assertEncodes(JsonOps.INSTANCE, object, json, CODEC); + } + + @Test + void testGenericArrayDecoding() { + CodecAssertions.assertDecodes(JsonOps.INSTANCE, genericArrayJson, genericArrayObject, GENERIC_ARRAY_CODEC); + } + + @Test + void testGenericArrayEncoding() { + CodecAssertions.assertEncodes(JsonOps.INSTANCE, genericArrayObject, genericArrayJson, GENERIC_ARRAY_CODEC); + } +} From a0768e6820adbe680ec0ceda1e70af99c7132e95 Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Wed, 2 Apr 2025 21:28:52 -0500 Subject: [PATCH 26/28] Add javadoc --- .../GroovyReflectiveStructureCreator.java | 2 +- .../reflective/CreationContext.java | 23 ++ .../structured/reflective/CreationOption.java | 3 + .../reflective/PropertyNamingOption.java | 30 ++ .../ReflectiveStructureCreator.java | 266 +++++++++++++----- .../reflective/SimpleCreatorOption.java | 6 + .../reflective/annotations/Annotated.java | 10 + .../reflective/annotations/Comment.java | 4 + .../reflective/annotations/Default.java | 3 + .../reflective/annotations/Lenient.java | 4 + .../annotations/SerializedProperty.java | 5 + .../reflective/annotations/Structured.java | 13 + .../reflective/annotations/Transient.java | 3 + .../reflective/annotations/Value.java | 49 ++++ .../BuiltInReflectiveStructureCreator.java | 6 +- .../reflective/systems/AnnotationParsers.java | 16 ++ .../systems/ContextualTransforms.java | 11 + .../reflective/systems/CreationOptions.java | 6 + .../reflective/systems/Creators.java | 12 + .../systems/FallbackPropertyDiscoverers.java | 40 ++- .../reflective/systems/FlexibleCreators.java | 27 ++ .../systems/ParameterizedCreators.java | 13 + 22 files changed, 472 insertions(+), 80 deletions(-) diff --git a/src/groovy/java/dev/lukebemish/codecextras/groovy/structured/reflective/implementation/GroovyReflectiveStructureCreator.java b/src/groovy/java/dev/lukebemish/codecextras/groovy/structured/reflective/implementation/GroovyReflectiveStructureCreator.java index d41ab87..615618a 100644 --- a/src/groovy/java/dev/lukebemish/codecextras/groovy/structured/reflective/implementation/GroovyReflectiveStructureCreator.java +++ b/src/groovy/java/dev/lukebemish/codecextras/groovy/structured/reflective/implementation/GroovyReflectiveStructureCreator.java @@ -104,7 +104,7 @@ public String value() { public Function> make() { return context -> List.of(new Discoverer() { @Override - public void modifyProperties(Class clazz, Map known) { + public void modifyProperties(Class clazz, Map known, java.lang.reflect.Type[] parameters) { var metaClass = DefaultGroovyMethods.getMetaClass(clazz); if (Objects.equals(known.get("metaClass"), MetaClass.class)) { known.remove("metaClass"); diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/CreationContext.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/CreationContext.java index 138ddbe..18ea4d1 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/CreationContext.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/CreationContext.java @@ -11,6 +11,9 @@ import java.util.Map; import java.util.function.Function; +/** + * Context provided to systems in reflective structure creation + */ public final class CreationContext { private final Map, Object> systems; private final Map, Object> bakedSystems = new IdentityHashMap<>(); @@ -19,11 +22,21 @@ public final class CreationContext { this.systems = systems; } + /** + * {@return whether the given creation option is present} + */ public boolean hasOption(CreationOption option) { var options = retrieve(CreationOptions.TYPE); return options.contains(option); } + /** + * Retrieves a system of the given type, baking it as necessary. + * @param type the type of the system to retrieve + * @return the system's results + * @param the intermediary type of the system + * @param the result type of the system + */ @SuppressWarnings("unchecked") public synchronized T retrieve(ReflectiveStructureCreator.CreatorSystem.Type type) { var existingBaked = bakedSystems.get(type); @@ -39,6 +52,11 @@ public synchronized T retrieve(ReflectiveStructureCreator.CreatorSystem.T return (T) bakedSystems.computeIfAbsent(type, t -> type.bake(type.empty(), this)); } + /** + * Parses the given annotation using the {@link AnnotationParsers} system. + * @param annotation the annotation to parse + * @return the parsed annotation information + */ @SuppressWarnings({"unchecked", "rawtypes"}) public List> parseAnnotation(Annotation annotation) { var annotationParsers = retrieve(AnnotationParsers.TYPE); @@ -49,6 +67,11 @@ public List> parseAnnotation(Annotation anno return List.of(); } + /** + * Find a contextual transform using the {@link ContextualTransforms} system. + * @param elements the elements associated with the target property + * @return a function to transform the property's structure + */ public Function, Structure> contextualTransform(List elements) { var contextualTransforms = retrieve(ContextualTransforms.TYPE); Function, Structure> function = Function.identity(); diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/CreationOption.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/CreationOption.java index d3016d4..47b5e46 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/CreationOption.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/CreationOption.java @@ -1,3 +1,6 @@ package dev.lukebemish.codecextras.structured.reflective; +/** + * An option to modify reflective creation of structures. + */ public interface CreationOption {} diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/PropertyNamingOption.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/PropertyNamingOption.java index e3f26ea..c6f87bb 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/PropertyNamingOption.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/PropertyNamingOption.java @@ -4,7 +4,13 @@ import java.util.List; import java.util.Locale; +/** + * {@link CreationOption}s for modifying the naming of properties within a structure. + */ public enum PropertyNamingOption implements CreationOption { + /** + * Structure fields are given the same name as the property + */ IDENTITY { @Override protected String formatPart(String part) { @@ -16,6 +22,10 @@ protected String joinParts(List parts) { return String.join("", parts); } }, + + /** + * Property names are converted to {@code PascalCase} + */ PASCAL_CASE { @Override protected String formatPart(String part) { @@ -27,6 +37,10 @@ protected String joinParts(List parts) { return String.join("", parts); } }, + + /** + * Property names are converted to {@code camelCase} + */ CAMEL_CASE { @Override protected String formatPart(String part) { @@ -46,6 +60,10 @@ protected String joinParts(List parts) { return result.toString(); } }, + + /** + * Property names are converted to {@code snake_case} + */ SNAKE_CASE { @Override protected String formatPart(String part) { @@ -57,6 +75,10 @@ protected String joinParts(List parts) { return String.join("_", parts); } }, + + /** + * Property names are converted to {@code SCREAMING_SNAKE_CASE} + */ SCREAMING_SNAKE_CASE { @Override protected String formatPart(String part) { @@ -68,6 +90,10 @@ protected String joinParts(List parts) { return String.join("_", parts); } }, + + /** + * Property names are converted to {@code kebab-case} + */ KEBAB_CASE { @Override protected String formatPart(String part) { @@ -79,6 +105,10 @@ protected String joinParts(List parts) { return String.join("-", parts); } }, + + /** + * Property names are converted to {@code SCREAMING-KEBAB-CASE} + */ SCREAMING_KEBAB_CASE { @Override protected String formatPart(String part) { diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java index 8da5572..3a0e952 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java @@ -25,22 +25,74 @@ import java.util.function.Function; import java.util.function.Supplier; +/** + * A tool for creating a {@link Structure} from a type reflectively. Implementations of this type provide specific + * implementations of various {@link CreatorSystem}. Instances of this type are discovered via the service locator. To + * create a structure, obtain an {@link Instance}. + */ public interface ReflectiveStructureCreator { + /** + * A system involved in structure creation. Systems provide an intermediary type, that can be baked to a + * given result type given proper context. + * @param the final result type of the system + * @param the intermediary type of the system + * @param the type of the system + */ interface CreatorSystem> extends App { final class Mu implements K1 { private Mu() {} } + /** + * {@return the type of the system} + */ O type(); + /** + * A type of {@link CreatorSystem}. + * @param the final result type of the system + * @param the intermediary type of the system + * @param the type of the system; in an implementation, should be the self type + */ interface Type> { + /** + * Merge two intermediary values. + * @param a the first value + * @param b the second value + * @return the merged value + */ R merge(R a, R b); + + /** + * {@return an empty intermediary value} + */ R empty(); + + /** + * {@return the key for this type} + */ Key key(); + + /** + * Bake an intermediary value given context. + * @param value the intermediary value + * @param context the context to bake with + * @return the final baked value + */ T bake(R value, CreationContext context); + + /** + * {@return whether this type is allowed to be implemented by services} If false, this system may only be + * provided on instance creation. + */ default boolean allowedFromServices() { return true; } } + /** + * A type of {@link CreatorSystem} that produces a list of values, where baking involves applying the context to a function. + * @param the type of the values in the list + * @param the type of the system; in an implementation, should be the self type + */ interface ListType> extends Type, Function>, O> { @Override default Function> merge(Function> a, Function> b) { @@ -64,6 +116,12 @@ default List bake(Function> value, CreationContext c } } + /** + * A type of {@link CreatorSystem} that produces a map of values, where baking involves applying the context to a function. + * @param the type of the keys in the map + * @param the type of the values in the map + * @param the type of the system; in an implementation, should be the self type + */ interface MapType> extends Type, Function>, O> { @Override default Function> merge(Function> a, Function> b) { @@ -87,6 +145,12 @@ default Map bake(Function> value, CreationConte } } + /** + * A specialized version of {@link MapType} for when the keys may be compared by identity. + * @param the type of the keys in the map + * @param the type of the values in the map + * @param the type of the system; in an implementation, should be the self type + */ interface IdentityMapType> extends MapType { @Override default Function> empty() { @@ -94,6 +158,9 @@ default Function> empty() { } } + /** + * {@return the created intermediary value} + */ R make(); } @@ -104,16 +171,25 @@ private static R mergeUnchecked(Object existing, Object specific, CreatorSys return type.merge(existingCast, specificCast); } + /** + * {@return system implementations for this creator implementation} + */ default Keys systems() { return Keys.builder().build(); } + /** + * {@return a structure creator for a specific reified type} + */ interface TypedCreator { Structure create(); Type type(); Class rawType(); } + /** + * Allows creation of structures reflectively. + */ final class Instance { private final Keys systems; @@ -121,20 +197,38 @@ private Instance(Keys systems) { this.systems = systems; } + /** + * {@return a new builder} + */ public static Builder builder() { return new Builder(); } + /** + * A builder for {@link ReflectiveStructureCreator.Instance}. + */ public static final class Builder { private final Keys.Builder systems = Keys.builder(); private Builder() {} + /** + * Add a specific system implementation to the instance being built. This will override any implementations + * of the same system added so far in the builder. + * @param system the system to add + * @return this builder + * @param the final value type of the system + * @param the intermediary type of the system + * @param the type of the system + */ public > Builder add(CreatorSystem system) { systems.add(system.type().key(), system); return this; } + /** + * {@return a new instance with the systems added in this builder} + */ public Instance build() { return new Instance(systems.build()); } @@ -144,6 +238,12 @@ public Instance build() { private final Map> cachedCreators = new HashMap<>(); + /** + * Create a structure reflectively for the given class. + * @param clazz the class to create a structure for + * @return the structure created + * @param the type of the class + */ @SuppressWarnings("unchecked") public synchronized Structure create(Class clazz) { var caller = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE).getCallerClass(); @@ -177,6 +277,12 @@ public synchronized Structure create(Class clazz) { } } + /** + * Create a structure reflectively for the given class, using an empty {@link Instance}. + * @param clazz the class to create a structure for + * @return the structure created + * @param the type of the class + */ static Structure create(Class clazz) { return Instance.builder().build().create(clazz); } @@ -200,54 +306,57 @@ private static Structure forType(Map> cachedCreators, Map< Class rawType = null; TypedCreator[] parameterCreators = null; - if (type instanceof ParameterizedType parameterizedType) { - if (parameterizedType.getRawType() instanceof Class clazz) { - rawType = clazz; - var parameters = parameterizedType.getActualTypeArguments(); - parameterCreators = new TypedCreator[parameters.length]; - for (int i = 0; i < parameters.length; i++) { - var structure = forType(cachedCreators, recursionCache, parameters[i], context); - var parameterType = parameters[i]; - parameterCreators[i] = new TypedCreator() { - @Override - public Structure create() { - return structure; - } - - @Override - public Type type() { - return parameterType; - } - - @Override - public Class rawType() { - if (parameterType instanceof Class clazz) { - return clazz; - } else if (parameterType instanceof ParameterizedType parameterizedType) { - return (Class) parameterizedType.getRawType(); - } else { - throw new IllegalArgumentException("Unknown type: " + type); + switch (type) { + case ParameterizedType parameterizedType -> { + if (parameterizedType.getRawType() instanceof Class clazz) { + rawType = clazz; + var parameters = parameterizedType.getActualTypeArguments(); + parameterCreators = new TypedCreator[parameters.length]; + for (int i = 0; i < parameters.length; i++) { + var structure = forType(cachedCreators, recursionCache, parameters[i], context); + var parameterType = parameters[i]; + parameterCreators[i] = new TypedCreator() { + @Override + public Structure create() { + return structure; } - } - }; + + @Override + public Type type() { + return parameterType; + } + + @Override + public Class rawType() { + if (parameterType instanceof Class clazz) { + return clazz; + } else if (parameterType instanceof ParameterizedType parameterizedType) { + return (Class) parameterizedType.getRawType(); + } else { + throw new IllegalArgumentException("Unknown type: " + type); + } + } + }; + } + if (parameterizedCreatorsMap.containsKey(clazz)) { + return parameterizedCreatorsMap.get(clazz).creator(parameterCreators); + } } - if (parameterizedCreatorsMap.containsKey(clazz)) { - return parameterizedCreatorsMap.get(clazz).creator(parameterCreators); + } + case Class clazz -> { + rawType = clazz; + parameterCreators = new TypedCreator[0]; + var foundCreator = creatorsMap.get(clazz); + if (foundCreator != null) { + return foundCreator; } } - } else if (type instanceof Class clazz) { - rawType = clazz; - parameterCreators = new TypedCreator[0]; - var foundCreator = creatorsMap.get(clazz); - if (foundCreator != null) { - return foundCreator; + case GenericArrayType genericArrayType -> { + var results = handleGenericArrayType(cachedCreators, recursionCache, type, context, genericArrayType); + rawType = results.rawType().arrayType(); + parameterCreators = results.parameterCreators(); } - } else if (type instanceof GenericArrayType genericArrayType) { - var results = handleGenericArrayType(cachedCreators, recursionCache, type, context, genericArrayType); - rawType = results.rawType().arrayType(); - parameterCreators = results.parameterCreators(); - } else { - throw new IllegalArgumentException("Unknown type: " + type); + default -> throw new IllegalArgumentException("Unknown type: " + type); } for (var flexibleCreator : flexibleCreators) { @@ -270,44 +379,47 @@ private static ParameterizedTypeResults handleGenericArrayType(Map rawType; var componentType = genericArrayType.getGenericComponentType(); - if (componentType instanceof Class clazz) { - rawType = clazz; - parameterCreators = new TypedCreator[0]; - } else if (componentType instanceof ParameterizedType parameterizedType && parameterizedType.getRawType() instanceof Class clazz) { - rawType = clazz; - parameterCreators = new TypedCreator[parameterizedType.getActualTypeArguments().length]; - for (int i = 0; i < parameterizedType.getActualTypeArguments().length; i++) { - var structure = forType(cachedCreators, recursionCache, parameterizedType.getActualTypeArguments()[i], context); - var parameterType = parameterizedType.getActualTypeArguments()[i]; - parameterCreators[i] = new TypedCreator() { - @Override - public Structure create() { - return structure; - } + switch (componentType) { + case Class clazz -> { + rawType = clazz; + parameterCreators = new TypedCreator[0]; + } + case ParameterizedType parameterizedType when parameterizedType.getRawType() instanceof Class clazz -> { + rawType = clazz; + parameterCreators = new TypedCreator[parameterizedType.getActualTypeArguments().length]; + for (int i = 0; i < parameterizedType.getActualTypeArguments().length; i++) { + var structure = forType(cachedCreators, recursionCache, parameterizedType.getActualTypeArguments()[i], context); + var parameterType = parameterizedType.getActualTypeArguments()[i]; + parameterCreators[i] = new TypedCreator() { + @Override + public Structure create() { + return structure; + } - @Override - public Type type() { - return parameterType; - } + @Override + public Type type() { + return parameterType; + } - @Override - public Class rawType() { - if (parameterType instanceof Class clazz) { - return clazz; - } else if (parameterType instanceof ParameterizedType parameterizedType) { - return (Class) parameterizedType.getRawType(); - } else { - throw new IllegalArgumentException("Unknown type: " + type); + @Override + public Class rawType() { + if (parameterType instanceof Class clazz) { + return clazz; + } else if (parameterType instanceof ParameterizedType parameterizedType) { + return (Class) parameterizedType.getRawType(); + } else { + throw new IllegalArgumentException("Unknown type: " + type); + } } - } - }; + }; + } + } + case GenericArrayType genericArrayComponentType -> { + var results = handleGenericArrayType(cachedCreators, recursionCache, type, context, genericArrayComponentType); + rawType = results.rawType().arrayType(); + parameterCreators = results.parameterCreators(); } - } else if (componentType instanceof GenericArrayType genericArrayComponentType) { - var results = handleGenericArrayType(cachedCreators, recursionCache, type, context, genericArrayComponentType); - rawType = results.rawType().arrayType(); - parameterCreators = results.parameterCreators(); - } else { - throw new IllegalArgumentException("Unknown type: " + type); + default -> throw new IllegalArgumentException("Unknown type: " + type); } return new ParameterizedTypeResults(rawType, parameterCreators); } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/SimpleCreatorOption.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/SimpleCreatorOption.java index b0acd39..b126afa 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/SimpleCreatorOption.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/SimpleCreatorOption.java @@ -1,5 +1,11 @@ package dev.lukebemish.codecextras.structured.reflective; +/** + * Built-in options for modifying the behavior of {@link ReflectiveStructureCreator}. + */ public enum SimpleCreatorOption implements CreationOption { + /** + * Fields are assumed to be not-null, instead of nullable, by default + */ NOT_NULL_BY_DEFAULT } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Annotated.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Annotated.java index 1863126..5b141bf 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Annotated.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Annotated.java @@ -5,9 +5,19 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +/** + * Annotates a property to notate that an annotation should be added to the structure for that field in a final record structure. + */ @Target({ElementType.METHOD, ElementType.RECORD_COMPONENT, ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) public @interface Annotated { + /** + * {@return a {@link Value} pointing to an {@link dev.lukebemish.codecextras.structured.Key} for the annotation + */ Value key(); + + /** + * {@return a {@link Value} pointing to the value assigned to the annotation} + */ Value value(); } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Comment.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Comment.java index 3043070..26477ab 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Comment.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Comment.java @@ -5,6 +5,10 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +/** + * Add a comment to the field representing a property in the final record structure. Equivalent to {@link Annotated} + * with {@link dev.lukebemish.codecextras.structured.Annotation#COMMENT}. + */ @Target({ElementType.METHOD, ElementType.RECORD_COMPONENT, ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) public @interface Comment { diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Default.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Default.java index 86da3e1..9e8bb41 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Default.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Default.java @@ -5,6 +5,9 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +/** + * Mark a property as having a default value to be used if no value is present. + */ @Target({ElementType.METHOD, ElementType.RECORD_COMPONENT, ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) public @interface Default { diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Lenient.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Lenient.java index c1a541f..fc7136e 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Lenient.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Lenient.java @@ -5,6 +5,10 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +/** + * Mark a property as lenient. Equivalent to {@link Annotated} with + * {@link dev.lukebemish.codecextras.structured.Annotation#LENIENT}. + */ @Target({ElementType.METHOD, ElementType.RECORD_COMPONENT, ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) public @interface Lenient { diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/SerializedProperty.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/SerializedProperty.java index 233e974..7d10bbf 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/SerializedProperty.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/SerializedProperty.java @@ -5,6 +5,11 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +/** + * Mark a constructor parameter as being a serialized property, and notate what its name is. As parameter names are not + * as consistently kept as field or method names in some cases, this annotation is necessary to use constructor-injected + * properties in reflective structure creation. + */ @Target({ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) public @interface SerializedProperty { diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Structured.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Structured.java index 3e19fda..c1451b5 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Structured.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Structured.java @@ -5,9 +5,22 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +/** + * Make a property use a specific structure, instead of a reflectively-generated one. + */ @Target({ElementType.METHOD, ElementType.RECORD_COMPONENT, ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) public @interface Structured { + /** + * {@return a {@link Value} pointing to an {@link dev.lukebemish.codecextras.structured.Structure}} + */ Value value(); + + /** + * {@return Whether to allow direct use of structures representing optional-typed properties} Normally the structure provided + * is used as the structure of the field, and an encircling {@link java.util.Optional} type, or its various + * friends such as {@link java.util.OptionalInt}, is interpreted as making the field optional; if this is set to + * true, the structure provided will instead be used directly for the optional type. + */ boolean directOptional() default false; } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Transient.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Transient.java index f03d3ad..dd75d02 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Transient.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Transient.java @@ -5,6 +5,9 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +/** + * Mark a property as transient, meaning it will be ignored in (de)serialization. + */ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface Transient { diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Value.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Value.java index 8c6b09e..3ce125c 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Value.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Value.java @@ -4,22 +4,71 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +/** + * Allows reference to a value in code, from within another annotation. Values represented may be either direct, with + * the value directly embedded in the annotation, or indirect, with the value being retrieved from a static field or + * static getter. + */ @Target({}) @Retention(RetentionPolicy.RUNTIME) public @interface Value { + /** + * {@return the location where the getter or field for an indirect value may be found} + */ Class location() default Value.class; + /** + * {@return the name of the field to retrieve an indirect value from} + */ String field() default ""; + /** + * {@return the name of the method to retrieve an indirect value from} + */ String method() default ""; + /** + * {@return a direct string value} Must contain at most 1 value. + */ String[] stringValue() default {}; + + /** + * {@return a direct int value} Must contain at most 1 value. + */ int[] intValue() default {}; + + /** + * {@return a direct long value} Must contain at most 1 value. + */ long[] longValue() default {}; + + /** + * {@return a direct double value} Must contain at most 1 value. + */ double[] doubleValue() default {}; + + /** + * {@return a direct float value} Must contain at most 1 value. + */ float[] floatValue() default {}; + + /** + * {@return a direct boolean value} Must contain at most 1 value. + */ boolean[] booleanValue() default {}; + + /** + * {@return a direct byte value} Must contain at most 1 value. + */ byte[] byteValue() default {}; + + /** + * {@return a direct short value} Must contain at most 1 value. + */ short[] shortValue() default {}; + + /** + * {@return a direct char value} Must contain at most 1 value. + */ char[] charValue() default {}; } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/implementation/BuiltInReflectiveStructureCreator.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/implementation/BuiltInReflectiveStructureCreator.java index 9b26978..24bb744 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/implementation/BuiltInReflectiveStructureCreator.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/implementation/BuiltInReflectiveStructureCreator.java @@ -1215,7 +1215,11 @@ public Structure create(Class exact, TypedCreator[] parameters, Function, Function>>>, Function, Function>>>>, AnnotationParsers.Type> { + /** + * The {@link dev.lukebemish.codecextras.structured.reflective.ReflectiveStructureCreator.CreatorSystem.Type} for this system. + */ Type TYPE = new Type(); @Override @@ -16,8 +22,18 @@ default Type type() { return TYPE; } + /** + * A result of interpretation + * @param the value represented + */ interface AnnotationInfo { + /** + * {@return the key of the extracted structure annotation} + */ Key key(); + /** + * {@return the value of the extracted structure annotation} + */ T value(); } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/ContextualTransforms.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/ContextualTransforms.java index 0f148eb..942b570 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/ContextualTransforms.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/ContextualTransforms.java @@ -11,7 +11,13 @@ import java.util.function.Function; import java.util.function.Supplier; +/** + * A system that allows transformation of a property's structure given annotation context. + */ public interface ContextualTransforms extends ReflectiveStructureCreator.CreatorSystem, Supplier>, ContextualTransforms.Type> { + /** + * The {@link dev.lukebemish.codecextras.structured.reflective.ReflectiveStructureCreator.CreatorSystem.Type} for this system. + */ Type TYPE = new Type(); @Override @@ -20,6 +26,11 @@ default Type type() { } interface ContextualTransform { + /** + * {@return a transformer to be applied to the structure of the targeted property} + * @param elements the elements associated with the property + * @param context the context of reflective structure creation + */ Function, Structure> transform(List elements, CreationContext context); default int priority() { return 0; diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/CreationOptions.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/CreationOptions.java index 583b1be..059b1af 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/CreationOptions.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/CreationOptions.java @@ -8,7 +8,13 @@ import java.util.List; import java.util.Set; +/** + * A system that allows specifying specific keyed options for the reflective creation of structures. + */ public interface CreationOptions extends ReflectiveStructureCreator.CreatorSystem, List, CreationOptions.Type> { + /** + * The {@link dev.lukebemish.codecextras.structured.reflective.ReflectiveStructureCreator.CreatorSystem.Type} for this system. + */ Type TYPE = new Type(); @Override diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/Creators.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/Creators.java index 86b6900..742c09b 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/Creators.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/Creators.java @@ -7,7 +7,13 @@ import java.util.Map; import java.util.function.Function; +/** + * A system that allows linking of simple structure creators to classes. + */ public interface Creators extends ReflectiveStructureCreator.CreatorSystem, Creators.Creator>, Function, Creators.Creator>>, Creators.Type> { + /** + * The {@link dev.lukebemish.codecextras.structured.reflective.ReflectiveStructureCreator.CreatorSystem.Type} for this system. + */ Type TYPE = new Type(); @Override @@ -15,7 +21,13 @@ default Type type() { return TYPE; } + /** + * Creates a specific structure on-demand + */ interface Creator { + /** + * {@return the created structure} + */ Structure create(); } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/FallbackPropertyDiscoverers.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/FallbackPropertyDiscoverers.java index c892722..f888f28 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/FallbackPropertyDiscoverers.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/FallbackPropertyDiscoverers.java @@ -10,17 +10,55 @@ import java.util.function.Function; import org.jspecify.annotations.Nullable; +/** + * A system that allows discovery of properties in non-standard ways, as a fallback from normal property discovery. + */ public interface FallbackPropertyDiscoverers extends ReflectiveStructureCreator.CreatorSystem, Function>, FallbackPropertyDiscoverers.Type> { + /** + * Discovers fallback properties given context. + */ interface Discoverer { - void modifyProperties(Class clazz, Map known); + /** + * Modifies properties discovered for a class. + * + * @param clazz the class to modify properties for + * @param known the known properties for the class + * @param parameters type parameters for the reified version of the class + */ + void modifyProperties(Class clazz, Map known, java.lang.reflect.Type[] parameters); + + /** + * {@return a method handle to the getter for the given property, or {@code null} if this discoverer cannot find it} + * @param clazz the class to get the property from + * @param property the property to get + * @param exists whether a getter for the property has already been found + */ @Nullable MethodHandle getter(Class clazz, String property, boolean exists); + /** + * {@return a method handle to the setter for the given property, or {@code null} if this discoverer cannot find it} + * @param clazz the class to get the property from + * @param property the property to get + * @param exists whether a setter for the property has already been found + */ @Nullable MethodHandle setter(Class clazz, String property, boolean exists); + /** + * {@return a list of elements that are associated with the property} + * @param clazz the class to get the property from + * @param property the property to get + */ List context(Class clazz, String property); + + /** + * {@return the priority of this discoverer, used to determine the order in which they are applied} High priority is applied first. + */ default int priority() { return 0; } } + /** + * The {@link dev.lukebemish.codecextras.structured.reflective.ReflectiveStructureCreator.CreatorSystem.Type} for this system. + */ Type TYPE = new Type(); @Override diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/FlexibleCreators.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/FlexibleCreators.java index 069f6f8..d61692c 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/FlexibleCreators.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/FlexibleCreators.java @@ -8,8 +8,15 @@ import java.util.ArrayList; import java.util.List; import java.util.function.Function; +import org.jetbrains.annotations.ApiStatus; +/** + * A system that provides flexible structure creators, which can create structures for broad ranges of types. + */ public interface FlexibleCreators extends ReflectiveStructureCreator.CreatorSystem, Function>, FlexibleCreators.Type> { + /** + * The {@link dev.lukebemish.codecextras.structured.reflective.ReflectiveStructureCreator.CreatorSystem.Type} for this system. + */ Type TYPE = new Type(); @Override @@ -17,12 +24,32 @@ default Type type() { return TYPE; } + /** + * A flexible structure creator that can create structures for a wide range of types. + */ interface FlexibleCreator { + /** + * {@return a structure for the given class and parameters} + * @param exact the ra w class to create + * @param parameters the parameters of the reified type to create + * @param creator a function to create nested structures with + */ Structure create(Class exact, ReflectiveStructureCreator.TypedCreator[] parameters, Function> creator); + + /** + * {@return whether this creator supports the given class and parameters} + * @param exact the raw class to create + * @param parameters the parameters of the reified type to create + */ boolean supports(Class exact, ReflectiveStructureCreator.TypedCreator[] parameters); + /** + * {@return the priority of this creator, used to determine the order in which they are checked} High priority is applied first. + */ default int priority() { return 0; } + + @ApiStatus.NonExtendable default Creators.Creator creator(Class exact, ReflectiveStructureCreator.TypedCreator[] parameters, Function> creator) { return () -> create(exact, parameters, creator); } diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/ParameterizedCreators.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/ParameterizedCreators.java index ce88ff0..7e76d18 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/ParameterizedCreators.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/ParameterizedCreators.java @@ -7,7 +7,13 @@ import java.util.Map; import java.util.function.Function; +/** + * A system that provides parameterized structure creators linked to raw types. + */ public interface ParameterizedCreators extends ReflectiveStructureCreator.CreatorSystem, ParameterizedCreators.ParameterizedCreator>, Function, ParameterizedCreators.ParameterizedCreator>>, ParameterizedCreators.Type> { + /** + * The {@link dev.lukebemish.codecextras.structured.reflective.ReflectiveStructureCreator.CreatorSystem.Type} for this system. + */ Type TYPE = new Type(); @Override @@ -15,7 +21,14 @@ default Type type() { return TYPE; } + /** + * Creates structures for reified types of a single raw type. + */ interface ParameterizedCreator { + /** + * {@return a structure for the given parameters} + * @param parameters the parameters of the reified type to create + */ Structure create(ReflectiveStructureCreator.TypedCreator[] parameters); default Creators.Creator creator(ReflectiveStructureCreator.TypedCreator[] parameters) { return () -> create(parameters); From 2e1ea134e782f8f3051ebe12959a00de9003f7ef Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Wed, 2 Apr 2025 21:40:54 -0500 Subject: [PATCH 27/28] Fix javadoc issues --- .../structured/reflective/ReflectiveStructureCreator.java | 2 +- .../structured/reflective/annotations/Annotated.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java index 3a0e952..73a5969 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java @@ -179,7 +179,7 @@ default Keys systems() { } /** - * {@return a structure creator for a specific reified type} + * A structure creator for a specific reified type. */ interface TypedCreator { Structure create(); diff --git a/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Annotated.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Annotated.java index 5b141bf..29ab7d0 100644 --- a/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Annotated.java +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Annotated.java @@ -12,7 +12,7 @@ @Retention(RetentionPolicy.RUNTIME) public @interface Annotated { /** - * {@return a {@link Value} pointing to an {@link dev.lukebemish.codecextras.structured.Key} for the annotation + * {@return a {@link Value} pointing to an {@link dev.lukebemish.codecextras.structured.Key} for the annotation} */ Value key(); From f1930e4be2d9d4635743b2f907c315d60b89fc7a Mon Sep 17 00:00:00 2001 From: Luke Bemish Date: Wed, 2 Apr 2025 21:41:25 -0500 Subject: [PATCH 28/28] Update version.properties --- version.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.properties b/version.properties index 4950f0d..5b2a680 100644 --- a/version.properties +++ b/version.properties @@ -1 +1 @@ -version=3.0.0 +version=3.1.0