feat(config): Config tweakers

This commit is contained in:
Johannes Frohnmeyer 2023-07-18 17:23:40 +02:00
parent e4da2eadd8
commit 5fc774c53c
Signed by: Johannes
GPG Key ID: E76429612C2929F4
7 changed files with 100 additions and 21 deletions

View File

@ -5,11 +5,22 @@ import com.squareup.javapoet.TypeName;
import javax.lang.model.element.TypeElement;
public record ConfigClass(TypeElement classElement, ClassName className, TypeName typeName, ClassName generatedClassName, String[] referencedConfigs) {
public static ConfigClass of(TypeElement element, String[] referencedConfigs, boolean hasManifold) {
public record ConfigClass(
TypeElement classElement,
ClassName className,
TypeName typeName,
ClassName generatedClassName,
String[] referencedConfigs,
TypeName tweakerName
) {
public static ConfigClass of(TypeElement element, String[] referencedConfigs, TypeName tweakerName, boolean hasManifold) {
ClassName className = ClassName.get(element);
String pkg = hasManifold ? "gsoncompile.extensions." + className.packageName() + "." + className.simpleNames().get(0) : className.packageName();
ClassName generatedClassName = ClassName.get(pkg, "JFC_" + className.simpleNames().get(0), className.simpleNames().subList(1, className.simpleNames().size()).toArray(String[]::new));
return new ConfigClass(element, ClassName.get(element), TypeName.get(element.asType()), generatedClassName, referencedConfigs);
return new ConfigClass(element, ClassName.get(element), TypeName.get(element.asType()), generatedClassName, referencedConfigs, tweakerName);
}
public boolean hasTweaker() {
return ConfigProcessor.isUsable(tweakerName);
}
}

View File

@ -2,9 +2,7 @@ package io.gitlab.jfronny.libjf.config.plugin;
import com.squareup.javapoet.*;
import io.gitlab.jfronny.commons.StringFormatter;
import io.gitlab.jfronny.commons.throwable.Coerce;
import io.gitlab.jfronny.gson.compile.processor.core.*;
import io.gitlab.jfronny.gson.compile.processor.core.value.ElementException;
import io.gitlab.jfronny.gson.reflect.TypeToken;
import io.gitlab.jfronny.libjf.config.api.v1.*;
import io.gitlab.jfronny.libjf.config.api.v1.dsl.DSL;
@ -13,13 +11,13 @@ import io.gitlab.jfronny.libjf.config.api.v1.type.Type;
import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.*;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.type.*;
import javax.lang.model.util.ElementFilter;
import javax.tools.Diagnostic;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@SupportedSourceVersion(SourceVersion.RELEASE_17)
@ -44,11 +42,11 @@ public class ConfigProcessor extends AbstractProcessor2 {
annotations.stream().flatMap(s -> roundEnvironment.getElementsAnnotatedWith(s).stream()).forEach(element -> {
JfConfig jc = element.getAnnotation(JfConfig.class);
if (jc != null) {
toGenerate.add(ConfigClass.of((TypeElement) element, jc.referencedConfigs(), hasManifold));
toGenerate.add(ConfigClass.of((TypeElement) element, jc.referencedConfigs(), getTypeName(jc::tweaker), hasManifold));
}
});
// Generate adapters
toGenerate.forEach(Coerce.<ConfigClass, ElementException>consumer(toProcess -> process(toProcess, toGenerate)).addHandler(ElementException.class, e -> e.printMessage(message)));
toGenerate.forEach(toProcess -> process(toProcess, toGenerate));
for (var entry : seen.keySet().stream().collect(Collectors.groupingBy(ClassName::packageName)).entrySet()) {
Map<List<String>, TypeSpec.Builder> known = entry.getValue().stream()
.collect(Collectors.toMap(
@ -108,7 +106,7 @@ public class ConfigProcessor extends AbstractProcessor2 {
return null;
}
private void process(ConfigClass toProcess, Set<ConfigClass> other) throws ElementException {
private void process(ConfigClass toProcess, Set<ConfigClass> other) {
if (seen.containsKey(toProcess.generatedClassName())) return; // Don't process the same class more than once
TypeSpec.Builder spec = TypeSpec.classBuilder(toProcess.generatedClassName().simpleName())
@ -120,10 +118,14 @@ public class ConfigProcessor extends AbstractProcessor2 {
CodeBlock.Builder code = CodeBlock.builder();
code.addStatement("$T.getInstance().migrateFiles($S)", ConfigHolder.class, modId);
code.add("INSTANCE = $T.create($S).register(builder0 -> builder0", DSL.class, modId).indent();
code.add("INSTANCE = $T.create($S).register(builder0 -> ", DSL.class, modId);
if (toProcess.hasTweaker()) code.add("$T.tweak(", toProcess.tweakerName());
code.add("builder0").indent();
for (String s : toProcess.referencedConfigs()) code.add("\n.referenceConfig($S)", s);
process(toProcess.classElement(), code, new AtomicInteger(0));
code.unindent().add("\n);\n");
code.unindent().add("\n");
if (toProcess.hasTweaker()) code.add(")");
code.add(");\n");
spec.addField(FieldSpec.builder(double.class, "Infinity", Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL).initializer("$T.POSITIVE_INFINITY", Double.class).build());
spec.addStaticBlock(code.build());
@ -138,12 +140,17 @@ public class ConfigProcessor extends AbstractProcessor2 {
for (TypeElement klazz : ElementFilter.typesIn(source.getEnclosedElements())) {
Category v = klazz.getAnnotation(Category.class);
if (v != null) {
TypeName tweaker = getTypeName(v::tweaker);
String name = klazz.getSimpleName().toString();
name = Character.toLowerCase(name.charAt(0)) + name.substring(1);
code.add("\n.category($S, builder$L -> builder$L", name, i.incrementAndGet(), i.get()).indent();
code.add("\n.category($S, builder$L -> ", name, i.incrementAndGet());
if (isUsable(tweaker)) code.add("$T.tweak(", tweaker);
code.add("builder$L", i.get()).indent();
for (String s : v.referencedConfigs()) code.add("\n.referencedConfigs($S)", s);
process(klazz, code, i);
code.unindent().add("\n)");
code.unindent().add("\n");
if (isUsable(tweaker)) code.add(")");
code.add(")");
}
}
for (VariableElement field : ElementFilter.fieldsIn(source.getEnclosedElements())) {
@ -193,4 +200,18 @@ public class ConfigProcessor extends AbstractProcessor2 {
return type;
}
}
private TypeName getTypeName(Supplier<Class<?>> annotationSource) {
try {
TypeName name = ClassName.get(annotationSource.get());
message.printWarning("Expected access to class on mirrored annotation to throw MirroredTypeException as specified in spec, but that didn't happen. This is unsupported");
return name;
} catch (MirroredTypeException e) {
return TypeName.get(e.getTypeMirror());
}
}
public static boolean isUsable(TypeName name) {
return name != null && !Objects.equals(TypeName.VOID, name);
}
}

View File

@ -8,5 +8,12 @@ import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Category {
/**
* @return Other configs that should be shown to users by their ID
*/
String[] referencedConfigs() default {};
/**
* @return A class for modifying the config with the ConfigBuilder API. Must implement the static method {@code CategoryBuilder tweak(CategoryBuilder builder)}
*/
Class<?> tweaker() default void.class;
}

View File

@ -8,5 +8,13 @@ import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface JfConfig {
/**
* @return Other configs that should be shown to users by their ID
*/
String[] referencedConfigs() default {};
/**
* @return A class for modifying the config with the ConfigBuilder API. Must implement the static method {@code ConfigBuilder tweak(ConfigBuilder builder)}
*/
Class<?> tweaker() default void.class;
}

View File

@ -13,23 +13,28 @@ import io.gitlab.jfronny.libjf.config.impl.dsl.DslEntryInfo;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Objects;
import java.util.function.Function;
public class ReflectiveConfigBuilderImpl implements ReflectiveConfigBuilder {
private final AuxiliaryMetadata rootMeta;
private final Class<?> rootClass;
private final Class<?> rootTweaker;
private final String id;
public ReflectiveConfigBuilderImpl(String id, Class<?> klazz) {
this.id = id;
this.rootClass = Objects.requireNonNull(klazz);
this.rootMeta = AuxiliaryMetadata.of(klazz.getAnnotation(JfConfig.class))
.merge(AuxiliaryMetadata.forMod(id));
JfConfig annotation = klazz.getAnnotation(JfConfig.class);
rootTweaker = annotation.tweaker();
this.rootMeta = AuxiliaryMetadata.of(annotation).merge(AuxiliaryMetadata.forMod(id));
}
@Override
public ConfigBuilder<?> apply(ConfigBuilder<?> builder) {
return applyCategory(builder, rootClass, rootMeta);
return applyCategory(builder, rootClass, findTweaker(rootTweaker, ConfigBuilder.class), rootMeta);
}
private static <T extends CategoryBuilder<?>> T applyCategory(T builder, Class<?> configClass, AuxiliaryMetadata meta) {
private <T extends CategoryBuilder<?>> T applyCategory(T builder, Class<?> configClass, Function<T, T> tweaker, AuxiliaryMetadata meta) {
meta.applyTo(builder);
for (Field field : configClass.getFields()) {
if (field.isAnnotationPresent(Entry.class)) {
@ -50,13 +55,26 @@ public class ReflectiveConfigBuilderImpl implements ReflectiveConfigBuilder {
for (Class<?> categoryClass : configClass.getClasses()) {
if (categoryClass.isAnnotationPresent(Category.class)) {
Category annotation = categoryClass.getAnnotation(Category.class);
String name = categoryClass.getSimpleName();
name = Character.toLowerCase(name.charAt(0)) + name.substring(1); // camelCase
builder.category(name, builder1 -> applyCategory(builder1, categoryClass, AuxiliaryMetadata.of(categoryClass.getAnnotation(Category.class))));
var categoryTweaker = findTweaker(annotation.tweaker(), CategoryBuilder.class);
builder.category(name, builder1 -> applyCategory(builder1, categoryClass, categoryTweaker, AuxiliaryMetadata.of(categoryClass.getAnnotation(Category.class))));
}
}
return builder;
return tweaker.apply(builder);
}
private <T> Function<T, T> findTweaker(Class<?> targetClass, Class<T> tweakedClass) {
try {
return Objects.equals(targetClass, void.class)
? Function.identity()
: Reflect.staticFunction(targetClass, "tweak", tweakedClass, tweakedClass);
} catch (Throwable t) {
LibJf.LOGGER.error("Could not find tweaker " + targetClass + " for mod " + id, t);
return Function.identity();
}
}
public static <T> ThrowingConsumer<T, Throwable> staticToConsumer(Class<?> klazz, Method method) throws Throwable {

View File

@ -6,7 +6,7 @@ import io.gitlab.jfronny.libjf.config.api.v1.*;
import java.util.ArrayList;
import java.util.List;
@JfConfig(referencedConfigs = {"libjf-web-v0"})
@JfConfig(referencedConfigs = {"libjf-web-v0"}, tweaker = TestConfigTweaker.class)
public class TestConfig {
@Entry public static boolean disablePacks = false;
@Entry public static Boolean disablePacks2 = false;

View File

@ -0,0 +1,14 @@
package io.gitlab.jfronny.libjf.config.test.reflect;
import io.gitlab.jfronny.libjf.LibJf;
import io.gitlab.jfronny.libjf.config.api.v1.dsl.ConfigBuilder;
import java.util.Objects;
public class TestConfigTweaker {
public static ConfigBuilder<?> tweak(ConfigBuilder<?> builder) {
if (!Objects.equals("libjf-config-reflect-v1-testmod", builder.getId())) throw new IllegalStateException("No!");
LibJf.LOGGER.info("Called config tweaker");
return builder;
}
}