diff --git a/build.gradle b/build.gradle index 4acc9e0..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' } @@ -103,6 +104,7 @@ sourceSets { minecraft {} minecraftFabric {} jmh {} + groovy {} } configurations { @@ -135,6 +137,11 @@ java { toolchain.languageVersion.set(JavaLanguageVersion.of(21)) withSourcesJar() withJavadocJar() + registerFeature("groovy") { + usingSourceSet sourceSets.groovy + withSourcesJar() + withJavadocJar() + } registerFeature("minecraft") { usingSourceSet sourceSets.minecraft withSourcesJar() @@ -182,6 +189,13 @@ dependencies { api 'com.mojang:datafixerupper:8.0.16' api 'org.slf4j:slf4j-api:2.0.1' + 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' @@ -190,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' @@ -198,7 +217,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' @@ -218,6 +236,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 @@ -259,6 +284,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 { @@ -298,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 new file mode 100644 index 0000000..615618a --- /dev/null +++ b/src/groovy/java/dev/lukebemish/codecextras/groovy/structured/reflective/implementation/GroovyReflectiveStructureCreator.java @@ -0,0 +1,183 @@ +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.Keys; +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.Objects; +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(); + 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 AnnotationInfo() { + @Override + public Key key() { + return dev.lukebemish.codecextras.structured.Annotation.COMMENT; + } + + @Override + public String 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, java.lang.reflect.Type[] parameters) { + var metaClass = DefaultGroovyMethods.getMetaClass(clazz); + 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 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); + var list = new ArrayList(); + if (metaProperty instanceof MetaBeanProperty beanProperty) { + if (beanProperty.getField() instanceof CachedField field) { + list.add(field.getCachedField()); + } + } + // And that's about all we can do... + return list; + } + }); + } + }); + return builder.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/jmh/java/dev/lukebemish/codecextras/jmh/LargeRecordsDecode.java b/src/jmh/java/dev/lukebemish/codecextras/jmh/LargeRecordsDecode.java index 56cedc6..3427703 100644 --- a/src/jmh/java/dev/lukebemish/codecextras/jmh/LargeRecordsDecode.java +++ b/src/jmh/java/dev/lukebemish/codecextras/jmh/LargeRecordsDecode.java @@ -51,6 +51,20 @@ 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()); + } + + @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) @@ -63,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 @@ -91,5 +102,17 @@ 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()); + } + + @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 0113c05..76aa0d9 100644 --- a/src/jmh/java/dev/lukebemish/codecextras/jmh/LargeRecordsEncode.java +++ b/src/jmh/java/dev/lukebemish/codecextras/jmh/LargeRecordsEncode.java @@ -50,6 +50,20 @@ 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()); + } + + @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) @@ -62,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 @@ -90,5 +101,17 @@ 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()); + } + + @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 b932aba..409fa56 100644 --- a/src/jmh/java/dev/lukebemish/codecextras/jmh/TestRecord.java +++ b/src/jmh/java/dev/lukebemish/codecextras/jmh/TestRecord.java @@ -6,9 +6,12 @@ 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.Structure; +import dev.lukebemish.codecextras.structured.reflective.ReflectiveStructureCreator; 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, @@ -101,6 +104,33 @@ 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/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/companion/DelegatingOps.java b/src/main/java/dev/lukebemish/codecextras/companion/DelegatingOps.java index c02594d..26afbf5 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.internal.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/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..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); @@ -121,20 +130,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 +173,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/internal/LayeredServiceLoader.java b/src/main/java/dev/lukebemish/codecextras/internal/LayeredServiceLoader.java new file mode 100644 index 0000000..8bc7aed --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/internal/LayeredServiceLoader.java @@ -0,0 +1,61 @@ +package dev.lukebemish.codecextras.internal; + +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/internal/Lazy.java b/src/main/java/dev/lukebemish/codecextras/internal/Lazy.java new file mode 100644 index 0000000..c46cec9 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/internal/Lazy.java @@ -0,0 +1,26 @@ +package dev.lukebemish.codecextras.internal; + +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/dev/lukebemish/codecextras/internal/package-info.java b/src/main/java/dev/lukebemish/codecextras/internal/package-info.java new file mode 100644 index 0000000..5280b87 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/internal/package-info.java @@ -0,0 +1,6 @@ +@NullMarked +@ApiStatus.Internal +package dev.lukebemish.codecextras.internal; + +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.NullMarked; 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/record/MethodHandleRecordCodecBuilder.java b/src/main/java/dev/lukebemish/codecextras/record/MethodHandleRecordCodecBuilder.java index 59b02b2..177da73 100644 --- a/src/main/java/dev/lukebemish/codecextras/record/MethodHandleRecordCodecBuilder.java +++ b/src/main/java/dev/lukebemish/codecextras/record/MethodHandleRecordCodecBuilder.java @@ -54,7 +54,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 +216,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/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/CodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/CodecInterpreter.java index a603907..b52f9f3 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; @@ -85,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())); } @@ -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..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) { @@ -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 928e90b..a9e9ce7 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; @@ -21,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); @@ -29,11 +45,13 @@ 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(); } - public interface KeyConsumer { + interface KeyConsumer { Key key(); App convert(App input); } @@ -59,6 +77,25 @@ 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 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"); @@ -78,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/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/MapCodecInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/MapCodecInterpreter.java index cb46dac..4462c5d 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; @@ -54,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); } @@ -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/RecordStructure.java b/src/main/java/dev/lukebemish/codecextras/structured/RecordStructure.java index f4177e2..7451186 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,45 @@ public Key> addOptional(String name, Structure structure, Fun return key; } + public Function addOptionalInt(String name, Structure structure, Function getter) { + return addOptional(name, structure, 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 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()); + } + return Optional.empty(); + })).andThen(o -> o.map(OptionalDouble::of).orElse(OptionalDouble.empty())); + } + + 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()); + } + return Optional.empty(); + })).andThen(o -> 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 @@ -237,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<>() { @@ -263,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 a4bc40f..50ee273 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; @@ -341,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. @@ -601,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); } @@ -641,6 +677,37 @@ 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)); + + 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. */ @@ -739,4 +806,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/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/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/CreationContext.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/CreationContext.java new file mode 100644 index 0000000..18ea4d1 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/CreationContext.java @@ -0,0 +1,83 @@ +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.IdentityHashMap; +import java.util.List; +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<>(); + + CreationContext(Map, Object> systems) { + 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); + 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)); + } + + /** + * 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); + var function = (Function) annotationParsers.get(annotation.annotationType()); + if (function != null) { + return (List>) function.apply(annotation); + } + 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(); + for (var transform : contextualTransforms) { + function = function.andThen(transform.transform(elements, this)); + } + return function; + } +} 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..47b5e46 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/CreationOption.java @@ -0,0 +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/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/PropertyNamingOption.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/PropertyNamingOption.java new file mode 100644 index 0000000..c6f87bb --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/PropertyNamingOption.java @@ -0,0 +1,158 @@ +package dev.lukebemish.codecextras.structured.reflective; + +import java.util.ArrayList; +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) { + return part; + } + + @Override + protected String joinParts(List parts) { + return String.join("", parts); + } + }, + + /** + * Property names are converted to {@code PascalCase} + */ + PASCAL_CASE { + @Override + protected String formatPart(String part) { + return capitalizeFirst(part); + } + + @Override + protected String joinParts(List parts) { + return String.join("", parts); + } + }, + + /** + * Property names are converted to {@code camelCase} + */ + 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(); + } + }, + + /** + * Property names are converted to {@code snake_case} + */ + SNAKE_CASE { + @Override + protected String formatPart(String part) { + return part.toLowerCase(Locale.ROOT); + } + + @Override + 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) { + return part.toUpperCase(Locale.ROOT); + } + + @Override + 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) { + return part.toLowerCase(Locale.ROOT); + } + + @Override + 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) { + 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 new file mode 100644 index 0000000..73a5969 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/ReflectiveStructureCreator.java @@ -0,0 +1,426 @@ +package dev.lukebemish.codecextras.structured.reflective; + +import com.google.common.base.Suppliers; +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 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; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.BiConsumer; +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) { + 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(); + } + } + + /** + * 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) { + 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(); + } + } + + /** + * 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() { + return c -> new IdentityHashMap<>(); + } + } + + /** + * {@return the created intermediary value} + */ + R make(); + } + + @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); + } + + /** + * {@return system implementations for this creator implementation} + */ + default Keys systems() { + return Keys.builder().build(); + } + + /** + * 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; + + 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()); + } + } + + private static final LayeredServiceLoader SERVICE_LOADER = LayeredServiceLoader.of(ReflectiveStructureCreator.class); + + 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(); + List services = LayeredServiceLoader.unique(SERVICE_LOADER.at(ReflectiveStructureCreator.class), SERVICE_LOADER.at(clazz), SERVICE_LOADER.at(caller)); + + Map, Object> systemsMap = new IdentityHashMap<>(); + 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), + value.make(), + type + ) + ); + }); + }; + services.forEach(creator -> addSystems.accept(true, creator.systems())); + addSystems.accept(false, this.systems); + + var context = new CreationContext(systemsMap); + + var recursionCache = new HashMap>(); + + return (Structure) forType(cachedCreators, recursionCache, clazz, context); + } + } + + /** + * 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); + } + + 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); + + Supplier creatorSupplier = () -> { + Class rawType = null; + TypedCreator[] parameterCreators = null; + + 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); + } + } + } + case 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(); + } + default -> throw new IllegalArgumentException("Unknown type: " + type); + } + + for (var flexibleCreator : flexibleCreators) { + if (flexibleCreator.supports(Objects.requireNonNull(rawType), parameterCreators)) { + return flexibleCreator.creator(rawType, parameterCreators, type1 -> forType(cachedCreators, recursionCache, type1, context)); + } + } + 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(); + } + + private static ParameterizedTypeResults handleGenericArrayType(Map> cachedCreators, Map> recursionCache, Type type, CreationContext context, GenericArrayType genericArrayType) { + TypedCreator[] parameterCreators; + Class rawType; + var componentType = genericArrayType.getGenericComponentType(); + 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 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(); + } + 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 new file mode 100644 index 0000000..b126afa --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/SimpleCreatorOption.java @@ -0,0 +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 new file mode 100644 index 0000000..29ab7d0 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Annotated.java @@ -0,0 +1,23 @@ +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; + +/** + * 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 new file mode 100644 index 0000000..26477ab --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Comment.java @@ -0,0 +1,16 @@ +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; + +/** + * 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 { + String 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..9e8bb41 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Default.java @@ -0,0 +1,15 @@ +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; + +/** + * 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 { + Value value(); +} 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..fc7136e --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Lenient.java @@ -0,0 +1,15 @@ +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; + +/** + * 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 new file mode 100644 index 0000000..7d10bbf --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/SerializedProperty.java @@ -0,0 +1,17 @@ +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; + +/** + * 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 { + 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..c1451b5 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Structured.java @@ -0,0 +1,26 @@ +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; + +/** + * 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 new file mode 100644 index 0000000..dd75d02 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Transient.java @@ -0,0 +1,14 @@ +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; + +/** + * 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 new file mode 100644 index 0000000..3ce125c --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/annotations/Value.java @@ -0,0 +1,74 @@ +package dev.lukebemish.codecextras.structured.reflective.annotations; + +import java.lang.annotation.Retention; +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 new file mode 100644 index 0000000..24bb744 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/implementation/BuiltInReflectiveStructureCreator.java @@ -0,0 +1,1518 @@ +package dev.lukebemish.codecextras.structured.reflective.implementation; + +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.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; +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; +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; +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.FallbackPropertyDiscoverers; +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; +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.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.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; +import org.objectweb.asm.Handle; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +@ApiStatus.Internal +@AutoService(ReflectiveStructureCreator.class) +public class BuiltInReflectiveStructureCreator implements ReflectiveStructureCreator { + @Override + 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, 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) + .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; + })) + .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(); + } + + 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(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); + } + referred++; + } + if (isMethod) { + try { + 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); + } + referred++; + } + + if (value == null || referred != 1) { + throw new IllegalArgumentException("@Value must have exactly one value"); + } + return value; + } + + @SuppressWarnings("rawtypes") + private Map, Function>>> annotationParsers(CreationContext context) { + var builder = ImmutableMap., Function>>>builder(); + return builder + .put(Annotated.class, (Annotated annotation) -> { + Key key = (Key) parseValue(annotation.key()); + Object finalValue = parseValue(annotation.value()); + return List.>of(new AnnotationParsers.AnnotationInfo() { + @Override + public Key key() { + return key; + } + + @Override + public Object value() { + return finalValue; + } + }); + }) + .put(Comment.class, (Comment annotation) -> List.>of(new AnnotationParsers.AnnotationInfo() { + @Override + public Key key() { + return dev.lukebemish.codecextras.structured.Annotation.COMMENT; + } + + @Override + public String value() { + return annotation.value(); + } + })) + .put(Lenient.class, (Lenient annotation) -> List.>of(new AnnotationParsers.AnnotationInfo() { + @Override + public Key key() { + return dev.lukebemish.codecextras.structured.Annotation.LENIENT; + } + + @Override + public Unit value() { + return Unit.INSTANCE; + } + })) + .build(); + } + + 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)) + .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 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 ParameterizedCreators.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( + "_", + 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 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"}); + 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({"unchecked", "rawtypes"}) + private List structureContextualTransforms() { + 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(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() + .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"); + } + + 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; + 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) { + 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, defaultValue); + + 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); + } + + 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) { + 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 StructureNamedCreator> 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, Object defaultValue) { + return new StructureNamedCreator<>() { + @SuppressWarnings("unchecked") + @Override + public Function forName(String name) { + return creator.make(name, true).andThen(o -> o == null ? (T) defaultValue : o); + } + + @SuppressWarnings("unchecked") + @Override + public Function forNames(List names) { + if (names.isEmpty()) { + throw new IllegalArgumentException("No names provided"); + } + + if (names.size() == 1) { + return forName(names.getFirst()); + } + + 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; + for (var f : creators) { + result = f.apply(container); + if (result != null) { + return result; + } + } + return (T) defaultValue; + }; + } + }; + } + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + 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)), defaultValue); + } + } + + 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 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())) + .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())) + .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())) + .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 (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)), 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)), defaultValue); + } + 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); + } + + 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) { + 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 FlexibleCreators.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 FlexibleCreators.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 -> ((Enum)t).name()); + } + + @Override + public boolean supports(Class exact, TypedCreator[] parameters) { + return Enum.class.isAssignableFrom(exact); + } + }) + .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"; + 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, TypedCreator[] parameters, Function> creator) { + var arrayMaker = arrayMaker(exact.getComponentType()); + 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)); + return (List) list; + }); + } + + @Override + public boolean supports(Class exact, TypedCreator[] parameters) { + return exact.isArray() && !exact.getComponentType().isPrimitive(); + } + }) + .add(new FlexibleCreators.FlexibleCreator() { + @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]; + 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 if (exact.isAnnotation()) { + var actualClass = implementAnnotation(exact); + validCtor = actualClass.getConstructors()[0]; + } else { + var ctors = validCtors(exact); + validCtor = ctors.getFirst(); + } + + Objects.requireNonNull(validCtor); + MethodHandle validCtorHandle; + try { + validCtorHandle = MethodHandles.lookup().unreflectConstructor(validCtor); + validCtorHandle = validCtorHandle.asType(MethodType.methodType(exact, validCtor.getParameterTypes())); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + + 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 < exact.getRecordComponents().length; i++) { + var component = exact.getRecordComponents()[i]; + + var thisContext = context.computeIfAbsent(component.getName(), k -> new LinkedHashSet<>()); + thisContext.add(component); + + ctorSetters.put(component.getName(), i); + ctorSettersArray[i] = component.getName(); + types.put(component.getName(), resolver.apply(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 LinkedHashSet<>()); + thisContext.add(parameter); + + ctorSetters.put(annotation.value(), i); + ctorSettersArray[i] = annotation.value(); + types.put(annotation.value(), resolver.apply(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.value(), k -> new LinkedHashSet<>()); + thisContext.add(param); + + ctorSetters.put(annotation.value(), i); + ctorSettersArray[i] = annotation.value(); + types.put(annotation.value(), resolver.apply(param.getParameterizedType())); + + // Prefer the bean getter method, then the field + + 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)) { + var getter = functionWrapper(MethodHandles.lookup().unreflect(getterMethod)); + getters.put(annotation.value(), getter); + thisContext.add(getterMethod); + continue; + } + } catch (NoSuchMethodException ignored) { + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + + try { + var field = exact.getField(annotation.value()); + if (field.getType().equals(param.getType()) && field.accessFlags().contains(AccessFlag.PUBLIC) && !field.accessFlags().contains(AccessFlag.STATIC)) { + var getter = functionWrapper(MethodHandles.lookup().unreflectGetter(field)); + getters.put(annotation.value(), getter); + thisContext.add(field); + } + } catch (NoSuchFieldException ignored) { + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + discoverBeanProperties(exact, types, getters, context, setters, ctorSetters); + } + + // Go from high priority to low priority + var propertyDiscoverers = options.retrieve(FallbackPropertyDiscoverers.TYPE).reversed(); + for (var discoverer : propertyDiscoverers) { + var typeParameters = new Type[parameters.length]; + for (int i = 0; i < parameters.length; i++) { + typeParameters[i] = parameters[i].type(); + } + discoverer.modifyProperties(exact, types, typeParameters); + } + for (var entry : types.keySet()) { + var hasGetter = getters.containsKey(entry); + for (var discoverer : propertyDiscoverers) { + var newEntry = discoverer.getter(exact, entry, hasGetter); + if (newEntry != null) { + hasGetter = true; + getters.put(entry, functionWrapper(newEntry)); + } + } + if (!ctorSetters.containsKey(entry)) { + var hasSetter = setters.containsKey(entry); + for (var discoverer : propertyDiscoverers) { + var newEntry = discoverer.setter(exact, entry, hasSetter); + if (newEntry != null) { + hasSetter = true; + setters.put(entry, newEntry); + } + } + } + for (var discoverer : propertyDiscoverers) { + context.computeIfAbsent(entry, k -> new LinkedHashSet<>()).addAll(discoverer.context(exact, entry)); + } + } + + context.forEach((property, elements) -> { + 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))) { + 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); + 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); + 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<>(); + 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.class, Object.class, Object.class)); + classData.add(setter); + j++; + } + } + + for (int i = 0; i < ctorSettersArray.length; i++) { + // Load ctor args + var property = ctorSettersArray[i]; + int keyOffset = offsetMap.get(property); + mv.visitLdcInsn(conDyn(org.objectweb.asm.Type.getDescriptor(Function.class), keyOffset)); + 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); + convertType(mv, validCtor.getParameters()[i].getType()); + } + invokeAsCallSite(mv, validCtorHandle.type().descriptorString(), 0); + mv.visitVarInsn(Opcodes.ASTORE, 2); + for (var property : propertyList) { + if (!ctorSetters.containsKey(property)) { + // 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, 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, 2); + 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 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); + } + }); + } + + @Override + public int priority() { + // This takes low priority as it is meant as a final fallback + return -10; + } + + @Override + 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 + // - 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() || exact.isAnnotation()) { + 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()) { + 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/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/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/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/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..91aa09d --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/AnnotationParsers.java @@ -0,0 +1,49 @@ +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; + +/** + * A system that allows interpreting annotations representing structure annotations on properties within a reflective structure creation. + */ +public interface AnnotationParsers extends ReflectiveStructureCreator.CreatorSystem, Function>>>, Function, Function>>>>, AnnotationParsers.Type> { + /** + * The {@link dev.lukebemish.codecextras.structured.reflective.ReflectiveStructureCreator.CreatorSystem.Type} for this system. + */ + Type TYPE = new Type(); + + @Override + 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(); + } + + 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..942b570 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/ContextualTransforms.java @@ -0,0 +1,70 @@ +package dev.lukebemish.codecextras.structured.reflective.systems; + +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; + +/** + * 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 + default Type type() { + return 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; + } + } + + 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..059b1af --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/CreationOptions.java @@ -0,0 +1,55 @@ +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; + +/** + * 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 + 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); + } + + @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 new file mode 100644 index 0000000..742c09b --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/Creators.java @@ -0,0 +1,43 @@ +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; + +/** + * 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 + default Type type() { + return TYPE; + } + + /** + * Creates a specific structure on-demand + */ + interface Creator { + /** + * {@return the created structure} + */ + Structure create(); + } + + final class Type implements ReflectiveStructureCreator.CreatorSystem.IdentityMapType, 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/FallbackPropertyDiscoverers.java b/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/FallbackPropertyDiscoverers.java new file mode 100644 index 0000000..f888f28 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/FallbackPropertyDiscoverers.java @@ -0,0 +1,85 @@ +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; + +/** + * 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 { + /** + * 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 + 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 new file mode 100644 index 0000000..d61692c --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/FlexibleCreators.java @@ -0,0 +1,74 @@ +package dev.lukebemish.codecextras.structured.reflective.systems; + +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; +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 + 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); + } + } + + 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..7e76d18 --- /dev/null +++ b/src/main/java/dev/lukebemish/codecextras/structured/reflective/systems/ParameterizedCreators.java @@ -0,0 +1,47 @@ +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; + +/** + * 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 + 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); + } + } + + final class Type implements ReflectiveStructureCreator.CreatorSystem.MapType, 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/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java b/src/main/java/dev/lukebemish/codecextras/structured/schema/JsonSchemaInterpreter.java index 3039004..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(); @@ -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/module-info.java b/src/main/java/module-info.java index edf4411..f29c11b 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -1,5 +1,8 @@ +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; requires static autoextension; requires static com.electronwill.nightconfig.core; @@ -13,6 +16,8 @@ requires static org.jspecify; 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; @@ -29,7 +34,14 @@ exports dev.lukebemish.codecextras.repair; 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; + + 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/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; 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..cf577a7 --- /dev/null +++ b/src/minecraft/java/dev/lukebemish/codecextras/minecraft/structured/MinecraftReflectiveStructureCreator.java @@ -0,0 +1,72 @@ +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.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.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) +public class MinecraftReflectiveStructureCreator implements ReflectiveStructureCreator { + @Override + 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()); + } + + @Override + public int priority() { + return 10; + } + + @Override + public boolean supports(Class exact, TypedCreator[] parameters) { + return Enum.class.isAssignableFrom(exact) && StringRepresentable.class.isAssignableFrom(exact); + } + }) + .build(); + } + }); + return builder.build(); + } +} 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 6872a69..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 @@ -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; @@ -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,16 +882,16 @@ 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)) )); } @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) { @@ -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,14 +1024,46 @@ 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) { + 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 public DataResult>> dispatchedMap(Structure keyStructure, Supplier> keys, Function>> valueStructures) { var keyResult = interpret(keyStructure); @@ -1067,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..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,11 +1,12 @@ package dev.lukebemish.codecextras.minecraft.structured.config; import com.mojang.serialization.Codec; +import dev.lukebemish.codecextras.internal.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 79adda3..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(); } ))); } @@ -431,6 +433,33 @@ public DataResult, Either>> xor(App, return either(left, right); } + @Override + public DataResult, A>> recursive(Function, Structure> function) { + 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> { public static final class Mu implements K1 {} 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) + } +} diff --git a/src/test/java/dev/lukebemish/codecextras/test/CodecAssertions.java b/src/test/java/dev/lukebemish/codecextras/test/CodecAssertions.java index 781e12f..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 { @@ -20,7 +21,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) { @@ -47,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/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); + } +} 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/TestReflective.java b/src/test/java/dev/lukebemish/codecextras/test/structured/reflective/TestReflective.java new file mode 100644 index 0000000..53e0a28 --- /dev/null +++ b/src/test/java/dev/lukebemish/codecextras/test/structured/reflective/TestReflective.java @@ -0,0 +1,242 @@ +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.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; +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.Test; + +public class TestReflective { + 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); + } + + public enum TestEnum { + A, + B, + C + } + + 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(); + + 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 static final Codec ANNOTATIONS_CODEC = CodecInterpreter.create().interpret(ReflectiveStructureCreator.create(TestAnnotations.class)).getOrThrow(); + + private final String json = """ + { + "a": 1, + "b": "test", + "c": "A", + "d": 2, + "e": "test", + "g": [1, 2, 3] + }"""; + + private final String arrayJson = """ + [ + { + "a": 1, + "b": "test", + "c": "A", + "d": 2, + "e": "test", + "g": [1, 2, 3] + } + ]"""; + + private final String primitiveArrayJson = """ + [1, 2, 3]"""; + + private final String recursiveJson = """ + { + "name": "test1", + "list": [ + { + "name": "test2", + "list": [] + }, + { + "name": "test3", + "list": [] + } + ] + }"""; + + 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 TestAnnotations annotations = new TestAnnotations(); + { + annotations.a = 1; + annotations.setD(true); + } + + @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); + } + + @Test + void testDecodingRecursive() { + CodecAssertions.assertDecodes(JsonOps.INSTANCE, recursiveJson, recursive, RECURSIVE_CODEC); + } + + @Test + void testEncodingRecursive() { + CodecAssertions.assertEncodes(JsonOps.INSTANCE, recursive, recursiveJson, RECURSIVE_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); + } +} 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..4870191 --- /dev/null +++ b/src/test/java/dev/lukebemish/codecextras/test/structured/reflective/TestReflectiveStructureAnnotations.java @@ -0,0 +1,177 @@ +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"), + 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 + ) {} + + public static class TestFieldAnnotated { + @Annotated( + key = @Value(location = Annotation.class, field = "COMMENT"), + value = @Value(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"), + value = @Value(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, 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 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); + } else { + return toml.toString(); + } + }; + + @Test + 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); + } +} 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; 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..a608558 100644 --- a/src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java +++ b/src/testCommon/java/dev/lukebemish/codecextras/test/common/TestConfig.java @@ -10,6 +10,7 @@ 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; @@ -19,6 +20,7 @@ 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; @@ -30,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 + 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", @@ -72,6 +78,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 +108,8 @@ 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})); + 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), @@ -105,7 +117,8 @@ 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), + testRecursive.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; 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