Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Interface-based config support #412

Draft
wants to merge 36 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
f24998d
Initial work to support interface-based configs
Tim203 Jul 2, 2023
efcb213
Allow all tests to run properly
Tim203 Jul 3, 2023
b29d241
Split some classes and added Range annotations
Tim203 Jan 3, 2024
e4b7f7f
Added default value annotations and restructured tests
Tim203 Jan 3, 2024
948cebe
Allow setter return type to be non-void
Tim203 Jan 3, 2024
ba11369
Auto-generate simple mappings
Tim203 Jan 3, 2024
7f6059f
Added Hidden annotation and added Processor.AdvancedFactory to aid it
Tim203 Jan 5, 2024
78bcaf5
Added support for some build-in annotations and added another addProc…
Tim203 Jan 6, 2024
c8524c8
Started working on adding tests for interfaces runtime
Tim203 Jan 6, 2024
336de1c
Merge remote-tracking branch 'origin/master' into feature/interfaces
Tim203 Jan 6, 2024
6f23846
Use correct impl name for mappings
Tim203 Jan 6, 2024
f07dc2f
chore(build): Only set test flags on newer JDK versions
zml2008 Jan 7, 2024
6c7f27c
Add all annotations that support fields. Use messager for errors
Tim203 Jan 13, 2024
c009449
Made AnnotationDefaults easier to follow
Tim203 Jan 13, 2024
a9c0e2f
Added support for default getters and default setters
Tim203 Jan 14, 2024
e0d9d42
Notify users about Hidden limitation. Optimized Hidden constraint
Tim203 Jan 14, 2024
224e87c
Exit gracefully on failure
Tim203 Jan 14, 2024
b62cab5
Add support for Gradle incremental annotation processing
Tim203 Feb 1, 2024
2d89b9d
Added Field annotation
Tim203 Feb 11, 2024
40d1c07
Apply spotless
Tim203 Feb 11, 2024
ab13924
Applied forbiddenApi fixes
Tim203 Feb 11, 2024
c5533d5
Renamed error to printError to trick PMD
Tim203 Feb 11, 2024
e9c0dfc
spotlessApply
Tim203 Feb 11, 2024
ebed0c5
Fix pmdTest
Tim203 Feb 11, 2024
27786f0
Set core as api dependency
Tim203 Feb 12, 2024
2add4c9
Use superinterface instead of enclosed element
Tim203 Feb 12, 2024
c2dfc96
Set a default value for config sections
Tim203 Feb 12, 2024
1af436d
Update test
Tim203 Feb 12, 2024
7e45e31
Added serialization to InterfaceTypeSerializer
Tim203 Feb 13, 2024
6945a5d
Respect superclasses' declaration of Exclude
Camotoy May 26, 2024
edd0685
Friendly error if implementation name cannot be found
Camotoy May 26, 2024
4347700
Made it easier to use the interface's default options
Tim203 Jun 12, 2024
e79e4d8
Oops, it's the other way around woo
Tim203 Jun 12, 2024
31c63fd
Superclasses with ConfigSerializable define order
Camotoy Aug 21, 2024
7543cd4
Don't try to initialize ConfigSerializable if @Field is marked
Camotoy Aug 24, 2024
7b4769c
InterfaceDefaultOptions#addTo with ObjectMapper modification
Camotoy Sep 10, 2024
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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ build-logic/*/bin/
/format/*/.apt_generated_tests/
/format/*/.checkstyle
/format/*/.settings/
/extra/*/build/
/extra/**/build/
/extra/*/out/
/extra/*/bin/
/extra/*/.factorypath
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,44 @@ default <A extends Annotation> Builder addProcessor(final Class<A> definition, f
*/
<A extends Annotation, T> Builder addProcessor(Class<A> definition, Class<T> valueType, Processor.Factory<A, T> factory);

/**
* Register a {@link Processor} that will process fields after write.
* The difference between an AdvancedFactory and a Factory is that
* an AdvancedFactory has access to all the annotations on the
* field, which makes more advanced processors possible.
*
* <p>Processors registered without a specific data type should be
* able to operate on any value type.</p>
*
* @param definition annotation providing data
* @param factory factory for callback function
* @param <A> annotation type
* @return this builder
* @since 4.0.0
*/
default <A extends Annotation> Builder addProcessor(final Class<A> definition, final Processor.AdvancedFactory<A, Object> factory) {
return addProcessor(definition, Object.class, factory);
}

/**
* Register a {@link Processor} that will process fields after write.
* The difference between an AdvancedFactory and a Factory is that
* an AdvancedFactory has access to all the annotations on the
* field, which makes more advanced processors possible.
*
* <p>All value types will be tested against types normalized to
* their boxed variants.</p>
*
* @param definition annotation providing data
* @param valueType value types the processor will handle
* @param factory factory for callback function
* @param <A> annotation type
* @param <T> data type
* @return this builder
* @since 4.0.0
*/
<A extends Annotation, T> Builder addProcessor(Class<A> definition, Class<T> valueType, Processor.AdvancedFactory<A, T> factory);

/**
* Register a {@link Constraint} that will be used to validate fields.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ protected boolean removeEldestEntry(final Map.Entry<Type, ObjectMapper<?>> eldes
private final List<NodeResolver.Factory> resolverFactories;
private final List<FieldDiscoverer<?>> fieldDiscoverers;
private final Map<Class<? extends Annotation>, List<Definition<?, ?, ? extends Constraint.Factory<?, ?>>>> constraints;
private final Map<Class<? extends Annotation>, List<Definition<?, ?, ? extends Processor.Factory<?, ?>>>> processors;
private final Map<Class<? extends Annotation>, List<Definition<?, ?, ? extends Processor.AdvancedFactory<?, ?>>>> processors;
private final List<PostProcessor.Factory> postProcessors;

ObjectMapperFactoryImpl(final Builder builder) {
Expand All @@ -97,7 +97,7 @@ protected boolean removeEldestEntry(final Map.Entry<Type, ObjectMapper<?>> eldes
this.constraints.values().forEach(Collections::reverse);

this.processors = new HashMap<>();
for (final Definition<?, ?, ? extends Processor.Factory<?, ?>> def : builder.processors) {
for (final Definition<?, ?, ? extends Processor.AdvancedFactory<?, ?>> def : builder.processors) {
this.processors.computeIfAbsent(def.annotation(), k -> new ArrayList<>()).add(def);
}
this.processors.values().forEach(Collections::reverse);
Expand Down Expand Up @@ -206,11 +206,11 @@ private <I, O> void makeData(final List<FieldData<I, O>> fields, final String na
}
}

final List<Definition<?, ?, ? extends Processor.Factory<?, ?>>> processorDefs = this.processors.get(annotation.annotationType());
final List<Definition<?, ?, ? extends Processor.AdvancedFactory<?, ?>>> processorDefs = this.processors.get(annotation.annotationType());
if (processorDefs != null) {
for (final Definition<?, ?, ? extends Processor.Factory<?, ?>> processorDef : processorDefs) {
for (final Definition<?, ?, ? extends Processor.AdvancedFactory<?, ?>> processorDef : processorDefs) {
if (isSuperType(processorDef.type(), normalizedType)) {
processors.add(((Processor.Factory) processorDef.factory()).make(annotation, type.getType()));
processors.add(((Processor.AdvancedFactory) processorDef.factory()).make(annotation, type.getType(), container));
}
}
}
Expand Down Expand Up @@ -356,7 +356,7 @@ static class Builder implements ObjectMapper.Factory.Builder {
private final List<NodeResolver.Factory> resolvers = new ArrayList<>();
private final List<FieldDiscoverer<?>> discoverer = new ArrayList<>();
private final List<Definition<?, ?, ? extends Constraint.Factory<?, ?>>> constraints = new ArrayList<>();
private final List<Definition<?, ?, ? extends Processor.Factory<?, ?>>> processors = new ArrayList<>();
private final List<Definition<?, ?, ? extends Processor.AdvancedFactory<?, ?>>> processors = new ArrayList<>();
private final List<PostProcessor.Factory> postProcessors = new ArrayList<>();

@Override
Expand Down Expand Up @@ -384,6 +384,13 @@ public <A extends Annotation, T> Builder addProcessor(final Class<A> definition,
return this;
}

@Override
public <A extends Annotation, T> Builder addProcessor(final Class<A> definition, final Class<T> valueType,
final Processor.AdvancedFactory<A, T> factory) {
this.processors.add(Definition.of(definition, valueType, factory));
return this;
}

@Override
public <A extends Annotation, T> Builder addConstraint(final Class<A> definition, final Class<T> valueType,
final Constraint.Factory<A, T> factory) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
*
* @since 4.0.0
*/
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface Comment {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Target({ElementType.FIELD, ElementType.METHOD})
public @interface Matches {

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.spongepowered.configurate.ConfigurationNode;

import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Type;
import java.util.ResourceBundle;

Expand All @@ -40,6 +41,31 @@ public interface Processor<V> {
*/
void process(V value, ConfigurationNode destination);

/**
* Provider to, given an annotation instance and the type it's on,
* create a {@link Processor}. If you don't need access to the other
* annotations on the field, you can also choose the simpler {@link Factory}.
*
* @param <A> annotation type
* @param <T> handled value type
* @since 4.0.0
*/
@FunctionalInterface
interface AdvancedFactory<A extends Annotation, T> {

/**
* Create a new processor given the annotation and data type.
*
* @param data annotation type on record field
* @param value declared field type
* @param container container holding the field, with its annotations
* @return new processor
* @since 4.0.0
*/
Processor<T> make(A data, Type value, AnnotatedElement container);

}

/**
* Provider to, given an annotation instance and the type it's on,
* create a {@link Processor}.
Expand All @@ -49,7 +75,7 @@ public interface Processor<V> {
* @since 4.0.0
*/
@FunctionalInterface
interface Factory<A extends Annotation, T> {
interface Factory<A extends Annotation, T> extends AdvancedFactory<A, T> {

/**
* Create a new processor given the annotation and data type.
Expand All @@ -61,6 +87,10 @@ interface Factory<A extends Annotation, T> {
*/
Processor<T> make(A data, Type value);

@Override
default Processor<T> make(A data, Type value, AnnotatedElement element) {
return make(data, value);
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Target({ElementType.FIELD, ElementType.METHOD})
@SubtypeOf(NonNull.class)
public @interface Required {
}
29 changes: 29 additions & 0 deletions extra/interface/ap/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
plugins {
id "org.spongepowered.configurate.build.component"
}

description = "Annotation processor for Configurate to generate an implementation for config interfaces"

dependencies {
implementation projects.core
implementation projects.extra.extraInterface
implementation libs.javapoet
implementation libs.auto.service
annotationProcessor libs.auto.service

testImplementation libs.compile.testing
}

// there is no javadoc
tasks.withType(Javadoc).configureEach { enabled = false }

tasks.withType(Test).configureEach {
doFirst {
// See: https://github.com/google/compile-testing/issues/222
if (javaLauncher.get().metadata.languageVersion >= JavaLanguageVersion.of(9)) {
jvmArgs '--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED'
jvmArgs '--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED'
jvmArgs '--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED'
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*
* Configurate
* Copyright (C) zml and Configurate contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.spongepowered.configurate.interfaces.processor;

import static org.spongepowered.configurate.interfaces.processor.Utils.annotation;

import com.google.auto.common.MoreTypes;
import com.squareup.javapoet.AnnotationSpec;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.spongepowered.configurate.interfaces.meta.defaults.DefaultBoolean;
import org.spongepowered.configurate.interfaces.meta.defaults.DefaultDecimal;
import org.spongepowered.configurate.interfaces.meta.defaults.DefaultNumeric;
import org.spongepowered.configurate.interfaces.meta.defaults.DefaultString;

import java.lang.annotation.Annotation;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

import javax.lang.model.AnnotatedConstruct;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;

final class AnnotationDefaults implements AnnotationProcessor {

static final AnnotationDefaults INSTANCE = new AnnotationDefaults();

private AnnotationDefaults() {}

@Override
public Set<Class<? extends Annotation>> processes() {
return new HashSet<>(Arrays.asList(DefaultBoolean.class, DefaultDecimal.class, DefaultNumeric.class, DefaultString.class));
}

@Override
public void process(
final TypeElement targetInterface,
final ExecutableElement element,
final TypeMirror nodeType,
final FieldSpecBuilderTracker fieldSpec
) throws IllegalStateException {
// there are two types of default values, one using annotations and one using the default value of a default method

// first, handle default value of a default method getter
if (element.isDefault() && element.getParameters().isEmpty() && hasNoAnnotationDefaults(element)) {
fieldSpec.initializer("$T.super.$L()", targetInterface, element.getSimpleName());
return;
}

// if it's not using the default value of a default method, use the annotations
final @Nullable DefaultBoolean defaultBoolean = annotation(element, DefaultBoolean.class);
final @Nullable DefaultDecimal defaultDecimal = annotation(element, DefaultDecimal.class);
final @Nullable DefaultNumeric defaultNumeric = annotation(element, DefaultNumeric.class);
final @Nullable DefaultString defaultString = annotation(element, DefaultString.class);
final boolean hasDefault = defaultBoolean != null || defaultDecimal != null || defaultNumeric != null || defaultString != null;

@Nullable Class<? extends Annotation> annnotationType = null;
@Nullable Object value = null;
if (hasDefault) {
if (MoreTypes.isTypeOf(Boolean.TYPE, nodeType)) {
if (defaultBoolean == null) {
throw new IllegalStateException("A default value of the incorrect type was provided for " + element);
}
annnotationType = DefaultBoolean.class;
value = defaultBoolean.value();

} else if (Utils.isDecimal(nodeType)) {
if (defaultDecimal == null) {
throw new IllegalStateException("A default value of the incorrect type was provided for " + element);
}
annnotationType = DefaultDecimal.class;
value = defaultDecimal.value();

} else if (Utils.isNumeric(nodeType)) {
if (defaultNumeric == null) {
throw new IllegalStateException("A default value of the incorrect type was provided for " + element);
}
annnotationType = DefaultNumeric.class;
value = defaultNumeric.value();

} else if (MoreTypes.isTypeOf(String.class, nodeType)) {
if (defaultString == null) {
throw new IllegalStateException("A default value of the incorrect type was provided for " + element);
}
annnotationType = DefaultString.class;
value = defaultString.value();
}
}

if (annnotationType == null) {
return;
}

final boolean isString = value instanceof String;

// special cases are floats and longs, because the default for decimals
// is double and for numerics it's int.
if (MoreTypes.isTypeOf(Float.TYPE, nodeType)) {
value = value + "F";
} else if (MoreTypes.isTypeOf(Long.TYPE, nodeType)) {
value = value + "L";
}

fieldSpec.addAnnotation(
AnnotationSpec.builder(annnotationType)
.addMember("value", isString ? "$S" : "$L", value)
);
fieldSpec.initializer(isString ? "$S" : "$L", value);
}

static boolean hasNoAnnotationDefaults(final AnnotatedConstruct construct) {
for (Class<? extends Annotation> defaultAnnotation : INSTANCE.processes()) {
if (annotation(construct, defaultAnnotation) != null) {
return false;
}
}
return true;
}

}
Loading