Skip to content

Reflective structures #4

New issue

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

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

Already on GitHub? Sign in to your account

Open
wants to merge 28 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
0e64f6b
Initial work on reflective structures
lukebemish Mar 3, 2025
f4ff742
Use caller discovered services as well.
lukebemish Mar 3, 2025
cab51c4
Add jmh benchmark for reflective structure creator
lukebemish Mar 3, 2025
dc02d2a
Make JMH benchmark test record public for reflective structure creator
lukebemish Mar 3, 2025
ae1be3f
Apply formatting
lukebemish Mar 3, 2025
2b99259
Use ClassValue-backed system for better wacky service loading
lukebemish Mar 3, 2025
532aae8
Add built-in reflective structure creators for MC types.
lukebemish Mar 3, 2025
a54e1f8
Fix formatting
lukebemish Mar 3, 2025
bd9d915
Simplify ctor locating logic
lukebemish Mar 3, 2025
fe3e50a
Add StringRepresentable reflective structure creator
lukebemish Mar 3, 2025
d80042a
Support for more types in reflective structure creation
lukebemish Mar 4, 2025
9570917
Support for more types in reflective structure creation, and recursiv…
lukebemish Mar 4, 2025
b11b2af
Allow config screen interpreter to work with recursive structures
lukebemish Mar 4, 2025
22bc9d9
Reflective structure creation options and avoid null-checking for pri…
lukebemish Mar 4, 2025
8ac5f07
Fix formatting
lukebemish Mar 4, 2025
4f7b02c
Update benchmarks
lukebemish Mar 5, 2025
4571b65
Transient properties and annotation serialization
lukebemish Mar 5, 2025
4eada0d
Fix comment ops and recover annotation values in reflective structures
lukebemish Mar 6, 2025
d1827a3
Comment and Structured annotations
lukebemish Mar 7, 2025
fe95906
More annotation work -- support for gson's SerializedName, and suppor…
lukebemish Mar 9, 2025
5e03444
Move groovy stuff to its own feature variant
lukebemish Mar 9, 2025
a18fb9b
Switch to new CreatorSystem setup for greater flexibility in reflecti…
lukebemish Mar 9, 2025
fd77afb
Working groovy support
lukebemish Mar 10, 2025
b0a0a83
Better optional/defaulted behavior and fix transitive-annotated beans
lukebemish Mar 12, 2025
0dfcec5
Working generic records and arrays and structured data elements
lukebemish Apr 3, 2025
a0768e6
Add javadoc
lukebemish Apr 3, 2025
2e1ea13
Fix javadoc issues
lukebemish Apr 3, 2025
f1930e4
Update version.properties
lukebemish Apr 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import dev.lukebemish.codecextras.gradle.FormatJmhOutput
plugins {
alias cLibs.plugins.conventions.java
id 'signing'
id 'groovy'
id 'dev.lukebemish.managedversioning'
}

Expand Down Expand Up @@ -103,6 +104,7 @@ sourceSets {
minecraft {}
minecraftFabric {}
jmh {}
groovy {}
}

configurations {
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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'
Expand All @@ -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'
Expand All @@ -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'
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -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<CreatorSystem.Mu, Object> systems() {
var builder = Keys.<CreatorSystem.Mu, Object>builder();
builder.add(AnnotationParsers.TYPE.key(), new AnnotationParsers() {
@Override
public Function<CreationContext, Map<Class<? extends Annotation>, Function<?, List<AnnotationInfo<?>>>>> make() {
return context -> {
var builder = ImmutableMap.<Class<? extends Annotation>, Function<?, List<AnnotationInfo<?>>>>builder();
return builder
.put(Groovydoc.class, (Groovydoc annotation) -> List.<AnnotationInfo<?>>of(new AnnotationInfo<String>() {
@Override
public Key<String> 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<String> 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<CreationContext, List<Discoverer>> make() {
return context -> List.of(new Discoverer() {
@Override
public void modifyProperties(Class<?> clazz, Map<String, java.lang.reflect.Type> 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<AnnotatedElement> context(Class<?> clazz, String property) {
var metaProperty = DefaultGroovyMethods.getMetaClass(clazz).getMetaProperty(property);
var list = new ArrayList<AnnotatedElement>();
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();
}
}
Original file line number Diff line number Diff line change
@@ -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;
19 changes: 19 additions & 0 deletions src/groovy/java/module-info.java
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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());
}
}
}
Loading