Rename TypeComponent to ConfigurationElement and refactor it

This commit also introduces the term "configuration element" to refer to
either a class field or record component of a configuration type.
dev
Exlll 2 years ago
parent bb58025239
commit eaccf30d5e

@ -128,7 +128,8 @@ Two things are noticeable here:
In the following sections the term _configuration type_ refers to any record type or to any
non-generic class that is directly or indirectly (i.e. through subclassing) annotated with
`@de.exlll.configlib.Configuration`. Accordingly, the term _configuration_ refers to an instance of
such a type.
such a type. A _configuration element_ is either a class field or a record component of a
configuration type.
### Declaring configuration types
@ -143,7 +144,7 @@ class with non-null default values.
### Supported types
A configuration type may only contain fields or components of the following types:
A configuration type may only contain configuration elements of the following types:
| Type class | Types |
|-----------------------------|--------------------------------------------------------------------|
@ -232,7 +233,7 @@ Both ways have three methods in common:
canonical constructor is called.
* The `update` method is a combination of `load` and `save` and the method you'd usually want to
use: it takes care of creating the configuration file if it does not exist and otherwise updates
it to reflect changes to (the fields or components of) the configuration type.
it to reflect changes to (the configuration elements of) the configuration type.
<details>
<summary>Example of <code>update</code> behavior when configuration file exists</summary>
@ -358,12 +359,13 @@ described in the [Import](#import) section.
### Comments
The fields or components of a configuration can be annotated with the `@Comment` annotation. This
annotation takes an array of strings. Each of these strings is written onto a new line as a comment.
The strings can contain `\n` characters. Empty strings are written as newlines (not as comments).
The configuration elements of a configuration type can be annotated with the `@Comment` annotation.
This annotation takes an array of strings. Each of these strings is written onto a new line as a
comment. The strings can contain `\n` characters. Empty strings are written as newlines (not as
comments).
If a configuration type _C_ that defines comments is used (as a field or component) within another
configuration type, the comments of _C_ are written with the proper indentation. However, if
If a configuration type _C_ that defines comments is used (as a configuration element) within
another configuration type, the comments of _C_ are written with the proper indentation. However, if
instances of _C_ are stored inside a collection, their comments are not printed when the collection
is written.
@ -431,11 +433,10 @@ nor updated during deserialization. You can filter out additional fields by prov
#### Missing values
When a configuration file is read, values that correspond to a field of a configuration type or to a
component of a record type might be missing.
That can happen, for example, when somebody deleted that field from the configuration file, when the
definition of a configuration or record type is changed, or when the `NameFormatter` that was used
to create that file is replaced.
When a configuration file is read, values that correspond to a configuration element might be
missing. That can happen, for example, when somebody deleted that field from the configuration file,
when the definition of a configuration or record type is changed, or when the `NameFormatter` that
was used to create that file is replaced.
In such cases, fields of configuration types keep the default value you assigned to them and record
components are initialized with the default value of their corresponding type.
@ -450,13 +451,13 @@ configuration if the values the users set are of the wrong type.
Although strongly discouraged, null values are supported and `ConfigurationProperties` let you
configure how they are handled when serializing and deserializing a configuration:
* By setting `outputNulls` to false, class fields, record components, and collection elements that
* By setting `outputNulls` to false, configuration elements, and collection elements that
are null are not output. Any comments that belong to such fields are also not written.
* By setting `inputNulls` to false, null values read from the configuration file are treated as
missing and are, therefore, handled as described in the section above.
* By setting `inputNulls` to true, null values read from the configuration file override the
corresponding default values of a configuration type with null or set the component value of a
record type to null. If the field or component type is primitive, an exception is thrown.
corresponding default values of a configuration class with null or set the component value of a
record type to null. If the configuration element type is primitive, an exception is thrown.
The following code forbids null values to be output but allows null values to be input. By default,
both are forbidden which makes the call to `outputNulls` in this case redundant.
@ -468,12 +469,13 @@ YamlConfigurationProperties.newBuilder()
.build();
```
### Field and component name formatting
### Formatting the names of configuration elements
You can define how fields and component names are formatted by configuring the configuration
properties with a custom formatter. Formatters are implementations of the `NameFormatter`
interface. You can implement this interface yourself or use one of the several formatters this
library provides. These pre-defined formatters can be found in the `NameFormatters` class.
You can define how the names of configuration elements are formatted by configuring the
configuration properties with a custom formatter. Formatters are implementations of
the `NameFormatter` interface. You can implement this interface yourself or use one of the several
formatters this library provides. These pre-defined formatters can be found in the `NameFormatters`
class.
The following code formats fields using the `IDENTITY` formatter (which is the default).
@ -511,17 +513,17 @@ properties. This also means that `Set`s are valid target types.
#### Serializer selection
To convert the value of a field or record component `F` with (source) type `S` into a serializable
To convert the value of a configuration element `E` with (source) type `S` into a serializable
value of some target type, a serializer has to be selected. Serializers are instances of
the `de.exlll.configlib.Serializer` interface and are selected based on `S`. Put differently,
serializers are always selected based on the compile-time type of `F` and never on the runtime type
serializers are always selected based on the compile-time type of `E` and never on the runtime type
of its value.
<details>
<summary>Why should I care about this?</summary>
This distinction makes a difference (and might lead to confusion) when you have fields or record
components whose type is a configuration type, and you extend that configuration type. Concretely,
This distinction makes a difference (and might lead to confusion) when you have configuration
elements whose type is a configuration type, and you extend that configuration type. Concretely,
assume you have written two configuration classes `A` and `B` where `B extends A`. Then, if you
use `A a = new B()` in your main configuration, only the fields of a `A` will be stored when you
save your main configuration. That is because the serializer of field `a` was selected based on the
@ -559,9 +561,9 @@ public final class PointSerializer implements Serializer<Point, String> {
Custom serializers takes precedence over the serializers provided by this library.
### Changing the type of fields or record components
### Changing the type of configuration elements
Changing the type of fields or record components is not supported. If you change the type of one of
Changing the type of configuration elements is not supported. If you change the type of one of
these but your configuration file still contains a value of the old type, a type mismatch will
occur when loading a configuration from that file. Instead, remove the old element and add a new one
with a different name.

@ -13,9 +13,9 @@ import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
public @interface Comment {
/**
* Returns the comments of the annotated field or record component.
* Returns the comments of the annotated configuration element.
*
* @return field or record component comments
* @return configuration element comments
*/
String[] value();
}

@ -3,8 +3,8 @@ package de.exlll.configlib;
import java.util.List;
/**
* Holds the comments of a field or record component as well as a list of element names.
* The list of element names contains the names of all fields or record components which led
* Holds the comments of a configuration element as well as a list of element names.
* The list of element names contains the names of all configuration elements which led
* to the current element starting from the root of the configuration object.
* <p>
* For example, for the following situation, if an instance of {@code A} is our root, the

@ -1,7 +1,7 @@
package de.exlll.configlib;
import de.exlll.configlib.TypeComponent.ConfigurationField;
import de.exlll.configlib.TypeComponent.ConfigurationRecordComponent;
import de.exlll.configlib.ConfigurationElements.FieldElement;
import de.exlll.configlib.ConfigurationElements.RecordComponentElement;
import java.lang.reflect.AnnotatedElement;
import java.util.*;
@ -20,22 +20,25 @@ final class CommentNodeExtractor {
this.outputNull = properties.outputNulls();
}
private record State(Iterator<? extends TypeComponent<?>> iterator, Object componentHolder) {}
private record State(
Iterator<? extends ConfigurationElement<?>> iterator,
Object elementHolder
) {}
/**
* Extracts {@code CommentNode}s of the given configuration or record in a DFS manner.
* Extracts {@code CommentNode}s of the given configuration type in a DFS manner.
* The nodes are returned in the order in which they were found.
*
* @param componentHolder the componentHolder from which the nodes are extracted
* @param elementHolder the elementHolder from which the nodes are extracted
* @return the nodes in the order in which they are found
* @throws IllegalArgumentException if {@code componentHolder} is not a configuration or record
* @throws NullPointerException if {@code componentHolder} is null
* @throws IllegalArgumentException if {@code elementHolder} is not a configuration type
* @throws NullPointerException if {@code elementHolder} is null
*/
public Queue<CommentNode> extractCommentNodes(final Object componentHolder) {
requireConfigurationOrRecord(componentHolder.getClass());
public Queue<CommentNode> extractCommentNodes(final Object elementHolder) {
requireConfigurationOrRecord(elementHolder.getClass());
final Queue<CommentNode> result = new ArrayDeque<>();
final var elementNameStack = new ArrayDeque<>(List.of(""));
final var stateStack = new ArrayDeque<>(List.of(stateFromObject(componentHolder)));
final var stateStack = new ArrayDeque<>(List.of(stateFromObject(elementHolder)));
State state;
while (!stateStack.isEmpty()) {
@ -43,27 +46,27 @@ final class CommentNodeExtractor {
elementNameStack.removeLast();
while (state.iterator.hasNext()) {
final var component = state.iterator.next();
final var componentValue = component.value(state.componentHolder);
final var element = state.iterator.next();
final var elementValue = element.value(state.elementHolder);
if ((componentValue == null) && !outputNull)
if ((elementValue == null) && !outputNull)
continue;
final var componentName = component.name();
final var elementName = element.name();
final var commentNode = createNodeIfCommentPresent(
component.component(),
componentName,
element.element(),
elementName,
elementNameStack
);
commentNode.ifPresent(result::add);
final var componentType = component.type();
if ((componentValue != null) &&
(Reflect.isConfiguration(componentType) ||
componentType.isRecord())) {
final var elementType = element.type();
if ((elementValue != null) &&
(Reflect.isConfiguration(elementType) ||
elementType.isRecord())) {
stateStack.addLast(state);
elementNameStack.addLast(nameFormatter.format(componentName));
state = stateFromObject(componentValue);
elementNameStack.addLast(nameFormatter.format(elementName));
state = stateFromObject(elementValue);
}
}
}
@ -71,12 +74,12 @@ final class CommentNodeExtractor {
return result;
}
private State stateFromObject(final Object componentHolder) {
final var type = componentHolder.getClass();
private State stateFromObject(final Object elementHolder) {
final var type = elementHolder.getClass();
final var iter = type.isRecord()
? configurationRecordComponents(componentHolder)
: configurationFields(componentHolder);
return new State(iter, componentHolder);
? recordComponentElements(elementHolder)
: fieldElements(elementHolder);
return new State(iter, elementHolder);
}
private Optional<CommentNode> createNodeIfCommentPresent(
@ -97,16 +100,16 @@ final class CommentNodeExtractor {
return Optional.empty();
}
private Iterator<ConfigurationField> configurationFields(Object configuration) {
private Iterator<FieldElement> fieldElements(Object configuration) {
return FieldExtractors.CONFIGURATION.extract(configuration.getClass())
.filter(fieldFilter)
.map(ConfigurationField::new)
.map(FieldElement::new)
.iterator();
}
private Iterator<ConfigurationRecordComponent> configurationRecordComponents(Object record) {
private Iterator<RecordComponentElement> recordComponentElements(Object record) {
return Arrays.stream(record.getClass().getRecordComponents())
.map(ConfigurationRecordComponent::new)
.map(RecordComponentElement::new)
.iterator();
}
}

@ -0,0 +1,77 @@
package de.exlll.configlib;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.AnnotatedType;
import java.lang.reflect.Field;
import java.lang.reflect.RecordComponent;
/**
* Represents an element of a serializable configuration type. The element can either be a
* {@link Field} for configuration classes or a {@link RecordComponent} for records.
*
* @param <T> the type of the element
*/
public sealed interface ConfigurationElement<T extends AnnotatedElement>
permits
ConfigurationElements.FieldElement,
ConfigurationElements.RecordComponentElement {
/**
* Returns the element itself.
*
* @return the element
*/
T element();
/**
* Returns the name of the element.
*
* @return name of the element
*/
String name();
/**
* Returns the type of the element.
*
* @return type of the element
*/
Class<?> type();
/**
* Returns the annotated type of the element.
*
* @return annotated type of element
*/
AnnotatedType annotatedType();
/**
* Given an instance of the configuration type which defines this element, returns the value
* the element is holding.
*
* @param elementHolder an instance of the configuration type that defines this element
* @return value the element is holding
* @throws IllegalArgumentException if {@code elementHolder} is not an instance of the
* configuration type which defines this element
*/
Object value(Object elementHolder);
/**
* Returns the configuration type that defines this element.
*
* @return the configuration type that defines this element
*/
Class<?> declaringType();
/**
* Returns the annotation of the given type or null if the element is not annotated
* with such an annotation.
*
* @param annotationType the type of annotation
* @param <A> the type of annotation
* @return the annotation or null
* @throws NullPointerException if {@code annotationType} is null
*/
default <A extends Annotation> A annotation(Class<A> annotationType) {
return element().getAnnotation(annotationType);
}
}

@ -0,0 +1,64 @@
package de.exlll.configlib;
import java.lang.reflect.AnnotatedType;
import java.lang.reflect.Field;
import java.lang.reflect.RecordComponent;
final class ConfigurationElements {
private ConfigurationElements() {}
record FieldElement(Field element) implements ConfigurationElement<Field> {
@Override
public String name() {
return element.getName();
}
@Override
public Class<?> type() {
return element.getType();
}
@Override
public AnnotatedType annotatedType() {
return element.getAnnotatedType();
}
@Override
public Object value(Object elementHolder) {
return Reflect.getValue(element, elementHolder);
}
@Override
public Class<?> declaringType() {
return element.getDeclaringClass();
}
}
record RecordComponentElement(RecordComponent element)
implements ConfigurationElement<RecordComponent> {
@Override
public String name() {
return element.getName();
}
@Override
public Class<?> type() {
return element.getType();
}
@Override
public AnnotatedType annotatedType() {
return element.getAnnotatedType();
}
@Override
public Object value(Object elementHolder) {
return Reflect.getValue(element, elementHolder);
}
@Override
public Class<?> declaringType() {
return element.getDeclaringRecord();
}
}
}

@ -144,10 +144,8 @@ class ConfigurationProperties {
/**
* Adds a serializer for the condition. The serializer is selected when the condition
* evaluates to true. The {@code test} method of the condition object is invoked with
* the generic type of a field or record component. Serializers added by this method
* take precedence over all other serializers expect the ones that were added for a
* specific type by the {@link #addSerializer(Class, Serializer)} method.
* The conditions are checked in the order in which they were added.
* the generic element type. The conditions are checked in the order in which they were
* added.
*
* @param condition the condition
* @param serializer the serializer
@ -165,7 +163,7 @@ class ConfigurationProperties {
}
/**
* Sets whether fields, record components, or collection elements whose value
* Sets whether configuration elements, or collection elements whose value
* is null should be output while serializing the configuration.
* <p>
* The default value is {@code false}.
@ -179,7 +177,7 @@ class ConfigurationProperties {
}
/**
* Sets whether fields, record components, or collection elements should
* Sets whether configuration elements, or collection elements should
* allow null values to bet set while deserializing the configuration.
* <p>
* If this option is set to false, null values read from a configuration
@ -224,7 +222,7 @@ class ConfigurationProperties {
}
/**
* Returns the field filter used to filter the fields of a configuration.
* Returns the field filter used to filter the fields of a configuration class.
*
* @return the field filter
*/
@ -233,8 +231,7 @@ class ConfigurationProperties {
}
/**
* Returns the name formatter used to format the names of configuration fields and
* record components.
* Returns the name formatter used to format the names of configuration elements.
*
* @return the formatter
*/
@ -253,9 +250,7 @@ class ConfigurationProperties {
}
/**
* Returns an unmodifiable map of serializers by condition. The serializers returned by this
* method take precedence over any default serializers provided by this library expect the ones
* that were added for a specific type.
* Returns an unmodifiable map of serializers by condition.
*
* @return serializers by condition
*/

@ -1,34 +1,34 @@
package de.exlll.configlib;
import de.exlll.configlib.TypeComponent.ConfigurationField;
import de.exlll.configlib.ConfigurationElements.FieldElement;
import java.lang.reflect.Field;
import java.util.List;
import java.util.Map;
final class ConfigurationSerializer<T> extends TypeSerializer<T, ConfigurationField> {
final class ConfigurationSerializer<T> extends TypeSerializer<T, FieldElement> {
ConfigurationSerializer(Class<T> configurationType, ConfigurationProperties properties) {
super(Validator.requireConfiguration(configurationType), properties);
}
@Override
public T deserialize(Map<?, ?> element) {
public T deserialize(Map<?, ?> serializedConfiguration) {
final T result = Reflect.callNoParamConstructor(type);
for (final var component : components()) {
final var formattedName = formatter.format(component.name());
for (final var element : elements()) {
final var formattedName = formatter.format(element.name());
if (!element.containsKey(formattedName))
if (!serializedConfiguration.containsKey(formattedName))
continue;
final var serializedValue = element.get(formattedName);
final var field = component.component();
final var serializedValue = serializedConfiguration.get(formattedName);
final var field = element.element();
if ((serializedValue == null) && properties.inputNulls()) {
requireNonPrimitiveFieldType(field);
Reflect.setValue(field, result, null);
} else if (serializedValue != null) {
Object deserializeValue = deserialize(component, serializedValue);
Object deserializeValue = deserialize(element, serializedValue);
Reflect.setValue(field, result, deserializeValue);
}
}
@ -37,7 +37,7 @@ final class ConfigurationSerializer<T> extends TypeSerializer<T, ConfigurationFi
}
@Override
protected void requireSerializableComponents() {
protected void requireSerializableElements() {
if (serializers.isEmpty()) {
String msg = "Configuration class '" + type.getSimpleName() + "' " +
"does not contain any (de-)serializable fields.";
@ -46,16 +46,16 @@ final class ConfigurationSerializer<T> extends TypeSerializer<T, ConfigurationFi
}
@Override
protected String baseDeserializeExceptionMessage(ConfigurationField component, Object value) {
protected String baseDeserializeExceptionMessage(FieldElement element, Object value) {
return "Deserialization of value '%s' with type '%s' for field '%s' failed."
.formatted(value, value.getClass(), component.component());
.formatted(value, value.getClass(), element.element());
}
@Override
protected List<ConfigurationField> components() {
protected List<FieldElement> elements() {
return FieldExtractors.CONFIGURATION.extract(type)
.filter(properties.getFieldFilter())
.map(ConfigurationField::new)
.map(FieldElement::new)
.toList();
}

@ -3,12 +3,12 @@ package de.exlll.configlib;
import java.util.function.Function;
/**
* Implementations of this interface format the names of class fields or record components.
* Implementations of this interface format the names of configuration elements.
*/
@FunctionalInterface
public interface NameFormatter extends Function<String, String> {
/**
* Formats the name of a class field or record component.
* Formats the name of a configuration element.
*
* @param name the name that is formatted
* @return formatted name
@ -17,7 +17,7 @@ public interface NameFormatter extends Function<String, String> {
String format(String name);
/**
* Formats the name of a class field or record component.
* Formats the name of a configuration element.
*
* @param name the name that is formatted
* @return formatted name

@ -1,42 +1,41 @@
package de.exlll.configlib;
import de.exlll.configlib.TypeComponent.ConfigurationRecordComponent;
import de.exlll.configlib.ConfigurationElements.RecordComponentElement;
import java.lang.reflect.RecordComponent;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
final class RecordSerializer<R> extends
TypeSerializer<R, ConfigurationRecordComponent> {
final class RecordSerializer<R> extends TypeSerializer<R, RecordComponentElement> {
RecordSerializer(Class<R> recordType, ConfigurationProperties properties) {
super(Validator.requireRecord(recordType), properties);
}
@Override
public R deserialize(Map<?, ?> element) {
final var components = components();
final var constructorArguments = new Object[components.size()];
public R deserialize(Map<?, ?> serializedConfiguration) {
final var elements = elements();
final var constructorArguments = new Object[elements.size()];
for (int i = 0, size = components.size(); i < size; i++) {
final var component = components.get(i);
final var formattedName = formatter.format(component.name());
for (int i = 0, size = elements.size(); i < size; i++) {
final var element = elements.get(i);
final var formattedName = formatter.format(element.name());
if (!element.containsKey(formattedName)) {
constructorArguments[i] = Reflect.getDefaultValue(component.type());
if (!serializedConfiguration.containsKey(formattedName)) {
constructorArguments[i] = Reflect.getDefaultValue(element.type());
continue;
}
final var serializedValue = element.get(formattedName);
final var recordComponent = component.component();
final var serializedValue = serializedConfiguration.get(formattedName);
final var recordComponent = element.element();
if ((serializedValue == null) && properties.inputNulls()) {
requireNonPrimitiveComponentType(recordComponent);
constructorArguments[i] = null;
} else if (serializedValue == null) {
constructorArguments[i] = Reflect.getDefaultValue(component.type());
constructorArguments[i] = Reflect.getDefaultValue(element.type());
} else {
constructorArguments[i] = deserialize(component, serializedValue);
constructorArguments[i] = deserialize(element, serializedValue);
}
}
@ -44,7 +43,7 @@ final class RecordSerializer<R> extends
}
@Override
protected void requireSerializableComponents() {
protected void requireSerializableElements() {
if (serializers.isEmpty()) {
String msg = "Record type '%s' does not define any components."
.formatted(type.getSimpleName());
@ -53,15 +52,15 @@ final class RecordSerializer<R> extends
}
@Override
protected String baseDeserializeExceptionMessage(ConfigurationRecordComponent component, Object value) {
protected String baseDeserializeExceptionMessage(RecordComponentElement element, Object value) {
return "Deserialization of value '%s' with type '%s' for component '%s' of record '%s' failed."
.formatted(value, value.getClass(), component.component(), component.declaringType());
.formatted(value, value.getClass(), element.element(), element.declaringType());
}
@Override
protected List<ConfigurationRecordComponent> components() {
protected List<RecordComponentElement> elements() {
return Arrays.stream(type.getRecordComponents())
.map(ConfigurationRecordComponent::new)
.map(RecordComponentElement::new)
.toList();
}

@ -18,17 +18,16 @@ public interface SerializerContext {
ConfigurationProperties properties();
/**
* Returns the {@code TypeComponent} (i.e. the field or record component) which led to the
* selection of the serializer.
* Returns the {@code ConfigurationElement} for which this serializer was selected.
*
* @return component which led to the selection of the serializer
* @return element for which this serializer was selected
*/
TypeComponent<?> component();
ConfigurationElement<?> element();
/**
* Returns the {@code AnnotatedType} which led to the selection of the serializer. The annotated
* type returned by this method might be different from the one returned by
* {@link TypeComponent#annotatedType()}. Specifically, the type is different when the
* {@link ConfigurationElement#annotatedType()}. Specifically, the type is different when the
* serializer is applied to a nested type via {@link SerializeWith} in which case the annotated
* type represents the type at that nesting level.
*

@ -6,12 +6,12 @@ import static de.exlll.configlib.Validator.requireNonNull;
record SerializerContextImpl(
ConfigurationProperties properties,
TypeComponent<?> component,
ConfigurationElement<?> element,
AnnotatedType annotatedType
) implements SerializerContext {
SerializerContextImpl {
properties = requireNonNull(properties, "configuration properties");
component = requireNonNull(component, "type component");
element = requireNonNull(element, "configuration element");
annotatedType = requireNonNull(annotatedType, "annotated type");
}
}

@ -51,12 +51,12 @@ final class SerializerSelector {
);
private final ConfigurationProperties properties;
/**
* Holds the last {@link #select}ed component.
* Holds the last {@link #select}ed configuration element.
*/
private TypeComponent<?> component;
private ConfigurationElement<?> element;
/**
* Holds the {@code SerializeWith} value of the last {@link #select}ed component. If the
* component is not annotated with {@code SerializeWith}, the value of this field is null.
* Holds the {@code SerializeWith} value of the last {@link #select}ed configuration element.
* If the element is not annotated with {@code SerializeWith}, the value of this field is null.
*/
private SerializeWith serializeWith;
/**
@ -72,11 +72,11 @@ final class SerializerSelector {
this.properties = requireNonNull(properties, "configuration properties");
}
public Serializer<?, ?> select(TypeComponent<?> component) {
this.component = component;
this.serializeWith = component.annotation(SerializeWith.class);
public Serializer<?, ?> select(ConfigurationElement<?> element) {
this.element = element;
this.serializeWith = element.annotation(SerializeWith.class);
this.currentNesting = -1;
return selectForType(component.annotatedType());
return selectForType(element.annotatedType());
}
private Serializer<?, ?> selectForType(AnnotatedType annotatedType) {
@ -108,7 +108,7 @@ final class SerializerSelector {
private Serializer<?, ?> selectCustomSerializer(AnnotatedType annotatedType) {
// SerializeWith annotation
if ((serializeWith != null) && (currentNesting == serializeWith.nesting())) {
final var context = new SerializerContextImpl(properties, component, annotatedType);
final var context = new SerializerContextImpl(properties, element, annotatedType);
return Serializers.newCustomSerializer(serializeWith.serializer(), context);
}

@ -1,135 +0,0 @@
package de.exlll.configlib;
import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import static de.exlll.configlib.Validator.requireNonNull;
/**
* Represents a component of a serializable type which can either be a {@link Field} for
* configurations or a {@link RecordComponent} for records.
*
* @param <T> the type of the component
*/
sealed interface TypeComponent<T extends AnnotatedElement> {
/**
* Returns the component itself.
*
* @return the component
*/
T component();
/**
* Returns the name of the component.
*
* @return name of the component
*/
String name();
/**
* Returns the type of the component.
*
* @return type of the component
*/
Class<?> type();
/**
* Returns the annotated type of the component.
*
* @return annotated type of component
*/
AnnotatedType annotatedType();
/**
* Returns the value the component is holding.
*
* @param componentHolder the holder to which this component belongs
* @return value the component is holding
* @throws IllegalArgumentException if {@code componentHolder} is not an instance of the type to
* which this component belongs
*/
Object value(Object componentHolder);
/**
* Returns the type that declares this component.
*
* @return the declaring type
*/
Class<?> declaringType();
/**
* Returns the annotation of the given type or null if the component is not annotated
* with such an annotation.
*
* @param annotationType the type of annotation
* @param <A> the type of annotation
* @return the annotation or null
* @throws NullPointerException if {@code annotationType} is null
*/
default <A extends Annotation> A annotation(Class<A> annotationType) {
return component().getAnnotation(annotationType);
}
record ConfigurationField(Field component) implements TypeComponent<Field> {
public ConfigurationField(Field component) {
this.component = requireNonNull(component, "component");
}
@Override
public String name() {
return component.getName();
}
@Override
public Class<?> type() {
return component.getType();
}
@Override
public AnnotatedType annotatedType() {
return component.getAnnotatedType();
}
@Override
public Object value(Object componentHolder) {
return Reflect.getValue(component, componentHolder);
}
@Override
public Class<?> declaringType() {
return component.getDeclaringClass();
}
}
record ConfigurationRecordComponent(RecordComponent component)
implements TypeComponent<RecordComponent> {
public ConfigurationRecordComponent(RecordComponent component) {
this.component = requireNonNull(component, "component");
}
@Override
public String name() {
return component.getName();
}
@Override
public AnnotatedType annotatedType() {
return component.getAnnotatedType();
}
@Override
public Class<?> type() {
return component.getType();
}
@Override
public Object value(Object componentHolder) {
return Reflect.getValue(component, componentHolder);
}
@Override
public Class<?> declaringType() {
return component.getDeclaringRecord();
}
}
}

@ -7,7 +7,7 @@ import java.util.stream.Collectors;
import static de.exlll.configlib.Validator.requireNonNull;
sealed abstract class TypeSerializer<T, TC extends TypeComponent<?>>
sealed abstract class TypeSerializer<T, E extends ConfigurationElement<?>>
implements Serializer<T, Map<?, ?>>
permits ConfigurationSerializer, RecordSerializer {
protected final Class<T> type;
@ -20,7 +20,7 @@ sealed abstract class TypeSerializer<T, TC extends TypeComponent<?>>
this.properties = requireNonNull(properties, "configuration properties");
this.formatter = properties.getNameFormatter();
this.serializers = buildSerializerMap();
requireSerializableComponents();
requireSerializableElements();
}
static <T> TypeSerializer<T, ?> newSerializerFor(
@ -35,8 +35,8 @@ sealed abstract class TypeSerializer<T, TC extends TypeComponent<?>>
Map<String, Serializer<?, ?>> buildSerializerMap() {
final var selector = new SerializerSelector(properties);
try {
return components().stream().collect(Collectors.toMap(
TypeComponent::name,
return elements().stream().collect(Collectors.toMap(
ConfigurationElement::name,
selector::select
));
} catch (StackOverflowError error) {
@ -46,59 +46,59 @@ sealed abstract class TypeSerializer<T, TC extends TypeComponent<?>>
}
@Override
public final Map<?, ?> serialize(T element) {
public final Map<?, ?> serialize(T configuration) {
final Map<String, Object> result = new LinkedHashMap<>();
for (final TC component : components()) {
final Object componentValue = component.value(element);
for (final E element : elements()) {
final Object elementValue = element.value(configuration);
if ((componentValue == null) && !properties.outputNulls())
if ((elementValue == null) && !properties.outputNulls())
continue;
final Object serializedValue = serialize(component, componentValue);
final String formattedName = formatter.format(component.name());
final Object serializedValue = serialize(element, elementValue);
final String formattedName = formatter.format(element.name());
result.put(formattedName, serializedValue);
}
return result;
}
protected final Object serialize(TC component, Object value) {
protected final Object serialize(E element, Object value) {
// The following cast won't cause a ClassCastException because the serializers
// are selected based on the component type.
// are selected based on the element type.
@SuppressWarnings("unchecked")
final var serializer = (Serializer<Object, Object>)
serializers.get(component.name());
serializers.get(element.name());
return (value != null) ? serializer.serialize(value) : null;
}
protected final Object deserialize(TC component, Object value) {
protected final Object deserialize(E element, Object value) {
// This unchecked cast leads to an exception if the type of the object which
// is deserialized is not a subtype of the type the deserializer expects.
@SuppressWarnings("unchecked")
final var serializer = (Serializer<Object, Object>)
serializers.get(component.name());
serializers.get(element.name());
final Object deserialized;
try {
deserialized = serializer.deserialize(value);
} catch (ClassCastException e) {
String msg = baseDeserializeExceptionMessage(component, value) + "\n" +
String msg = baseDeserializeExceptionMessage(element, value) + "\n" +
"The type of the object to be deserialized does not " +
"match the type the deserializer expects.";
throw new ConfigurationException(msg, e);
} catch (RuntimeException e) {
String msg = baseDeserializeExceptionMessage(component, value);
String msg = baseDeserializeExceptionMessage(element, value);
throw new ConfigurationException(msg, e);
}
return deserialized;
}
protected abstract void requireSerializableComponents();
protected abstract void requireSerializableElements();
protected abstract String baseDeserializeExceptionMessage(TC component, Object value);
protected abstract String baseDeserializeExceptionMessage(E element, Object value);
protected abstract List<TC> components();
protected abstract List<E> elements();
abstract T newDefaultInstance();
}

@ -390,7 +390,7 @@ class CommentNodeExtractorTest {
}
@Test
void extractIgnoresCommentIfComponentNullAndOutputNull1() {
void extractIgnoresCommentIfElementNullAndOutputNull1() {
record R1(@Comment("Hello") int i) {}
record R2(R1 r1) {}
record R3(@Comment("World") R1 r1) {}
@ -425,7 +425,7 @@ class CommentNodeExtractorTest {
}
@Test
void extractIgnoresCommentIfComponentNullAndOutputNull2() {
void extractIgnoresCommentIfElementtNullAndOutputNull2() {
record R(@Comment("Hello") String s1, @Comment("World") String s2) {}
ConfigurationProperties properties = ConfigurationProperties.newBuilder()
.outputNulls(false)

@ -0,0 +1,95 @@
package de.exlll.configlib;
import de.exlll.configlib.ConfigurationElements.FieldElement;
import de.exlll.configlib.ConfigurationElements.RecordComponentElement;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Field;
import java.lang.reflect.RecordComponent;
import java.util.List;
import java.util.Set;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
class ConfigurationElementsTest {
static final class FieldElementTest {
private static final Field FIELD = C.class.getDeclaredFields()[0];
private static final FieldElement ELEMENT = new FieldElement(FIELD);
static final class C {
@Comment("")
List<String> field = List.of("20");
}
@Test
void elementName() {
assertThat(ELEMENT.name(), is("field"));
}
@Test
void elementType() {
assertThat(ELEMENT.type(), equalTo(List.class));
}
@Test
void elementAnnotatedType() {
assertThat(ELEMENT.annotatedType(), is(FIELD.getAnnotatedType()));
}
@Test
void elementValue() {
assertThat(ELEMENT.value(new C()), is(List.of("20")));
}
@Test
void declaringType() {
assertThat(ELEMENT.declaringType(), equalTo(C.class));
}
@Test
void annotation() {
assertThat(ELEMENT.annotation(Comment.class), notNullValue());
}
}
static final class RecordComponentElementTest {
private static final RecordComponent RECORD_COMPONENT = R.class.getRecordComponents()[0];
private static final RecordComponentElement ELEMENT =
new RecordComponentElement(RECORD_COMPONENT);
record R(@Comment("") Set<Integer> comp) {}
@Test
void elementName() {
assertThat(ELEMENT.name(), is("comp"));
}
@Test
void elementType() {
assertThat(ELEMENT.type(), equalTo(Set.class));
}
@Test
void elementAnnotatedType() {
assertThat(ELEMENT.annotatedType(), is(RECORD_COMPONENT.getAnnotatedType()));
}
@Test
void elementValue() {
assertThat(ELEMENT.value(new R(Set.of(1))), is(Set.of(1)));
}
@Test
void declaringType() {
assertThat(ELEMENT.declaringType(), equalTo(R.class));
}
@Test
void annotation() {
assertThat(ELEMENT.annotation(Comment.class), notNullValue());
}
}
}

@ -38,25 +38,25 @@ class SerializerSelectorTest {
ConfigurationProperties.newBuilder().addSerializer(Point.class, POINT_SERIALIZER).build()
);
private static TypeComponent<?> findByCondition(Predicate<Field> condition) {
private static ConfigurationElement<?> findByCondition(Predicate<Field> condition) {
for (Field field : ExampleConfigurationA2.class.getDeclaredFields()) {
if (condition.test(field))
return new TypeComponent.ConfigurationField(field);
return new ConfigurationElements.FieldElement(field);
}
throw new RuntimeException("missing field");
}
private static TypeComponent<?> findByType(Class<?> type) {
private static ConfigurationElement<?> findByType(Class<?> type) {
return findByCondition(field -> field.getType() == type);
}
private static TypeComponent<?> findByName(String name) {
private static ConfigurationElement<?> findByName(String name) {
return findByCondition(field -> field.getName().equals(name));
}
private static TypeComponent<?> forField(Class<?> type, String fieldName) {
private static ConfigurationElement<?> forField(Class<?> type, String fieldName) {
Field field = getField(type, fieldName);
return new TypeComponent.ConfigurationField(field);
return new ConfigurationElements.FieldElement(field);
}
@ParameterizedTest
@ -366,12 +366,12 @@ class SerializerSelectorTest {
class A {
Map<List<String>, String> mlss;
}
TypeComponent<?> component = forField(A.class, "mlss");
ConfigurationElement<?> element = forField(A.class, "mlss");
assertThrowsConfigurationException(
() -> SELECTOR.select(component),
() -> SELECTOR.select(element),
("Cannot select serializer for type '%s'.\n" +
"Map keys can only be of simple or enum type.")
.formatted(component.annotatedType().getType())
.formatted(element.annotatedType().getType())
);
}
@ -380,12 +380,12 @@ class SerializerSelectorTest {
class A {
Map<Point, String> mps;
}
TypeComponent<?> component = forField(A.class, "mps");
ConfigurationElement<?> element = forField(A.class, "mps");
assertThrowsConfigurationException(
() -> SELECTOR.select(component),
() -> SELECTOR.select(element),
("Cannot select serializer for type '%s'.\n" +
"Map keys can only be of simple or enum type.")
.formatted(component.annotatedType().getType())
.formatted(element.annotatedType().getType())
);
}
@ -395,12 +395,12 @@ class SerializerSelectorTest {
class A {
Box<String> box;
}
TypeComponent<?> component = forField(A.class, "box");
ConfigurationElement<?> element = forField(A.class, "box");
assertThrowsConfigurationException(
() -> SELECTOR.select(component),
() -> SELECTOR.select(element),
("Cannot select serializer for type '%s'.\n" +
"Parameterized types other than lists, sets, and maps cannot be serialized.")
.formatted(component.annotatedType().getType())
.formatted(element.annotatedType().getType())
);
}
@ -409,9 +409,9 @@ class SerializerSelectorTest {
class A {
List<?>[] ga;
}
TypeComponent<?> component = forField(A.class, "ga");
ConfigurationElement<?> element = forField(A.class, "ga");
assertThrowsConfigurationException(
() -> SELECTOR.select(component),
() -> SELECTOR.select(element),
"Cannot select serializer for type 'java.util.List<?>[]'.\n" +
"Generic array types cannot be serialized."
);
@ -422,9 +422,9 @@ class SerializerSelectorTest {
class A {
List<? extends String> les;
}
TypeComponent<?> component = forField(A.class, "les");
ConfigurationElement<?> element = forField(A.class, "les");
assertThrowsConfigurationException(
() -> SELECTOR.select(component),
() -> SELECTOR.select(element),
"Cannot select serializer for type '? extends java.lang.String'.\n" +
"Wildcard types cannot be serialized."
);
@ -435,9 +435,9 @@ class SerializerSelectorTest {
class A {
List<?> lw;
}
TypeComponent<?> component = forField(A.class, "lw");
ConfigurationElement<?> element = forField(A.class, "lw");
assertThrowsConfigurationException(
() -> SELECTOR.select(component),
() -> SELECTOR.select(element),
"Cannot select serializer for type '?'.\n" +
"Wildcard types cannot be serialized."
);
@ -448,9 +448,9 @@ class SerializerSelectorTest {
class A<T> {
T t;
}
TypeComponent<?> component = forField(A.class, "t");
ConfigurationElement<?> element = forField(A.class, "t");
assertThrowsConfigurationException(
() -> SELECTOR.select(component),
() -> SELECTOR.select(element),
"Cannot select serializer for type 'T'.\n" +
"Type variables cannot be serialized."
);
@ -596,13 +596,13 @@ class SerializerSelectorTest {
String s;
}
var component = forField(A.class, "s");
var element = forField(A.class, "s");
var field = getField(A.class, "s");
var serializer = (SerializerWithContext) SELECTOR.select(component);
var serializer = (SerializerWithContext) SELECTOR.select(element);
var context = serializer.ctx;
assertThat(context.properties(), sameInstance(DEFAULT_PROPS));
assertThat(context.component(), is(component));
assertThat(context.element(), is(element));
assertThat(context.annotatedType(), is(field.getAnnotatedType()));
}
@ -613,14 +613,14 @@ class SerializerSelectorTest {
List<String> l;
}
var component = forField(A.class, "l");
var element = forField(A.class, "l");
var field = getField(A.class, "l");
var outerSerializer = (ListSerializer<?, ?>) SELECTOR.select(component);
var outerSerializer = (ListSerializer<?, ?>) SELECTOR.select(element);
var innerSerializer = (SerializerWithContext) outerSerializer.getElementSerializer();
var context = innerSerializer.ctx;
assertThat(context.properties(), sameInstance(DEFAULT_PROPS));
assertThat(context.component(), is(component));
assertThat(context.element(), is(element));
var annotatedType = (AnnotatedParameterizedType) field.getAnnotatedType();
var argument = annotatedType.getAnnotatedActualTypeArguments()[0];

@ -1,94 +0,0 @@
package de.exlll.configlib;
import de.exlll.configlib.TypeComponent.ConfigurationField;
import de.exlll.configlib.TypeComponent.ConfigurationRecordComponent;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Field;
import java.lang.reflect.RecordComponent;
import java.util.List;
import java.util.Set;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
class TypeComponentTest {
static final class ConfigurationFieldTest {
private static final Field FIELD = TestUtils.getField(C.class, "field");
private static final ConfigurationField COMPONENT = new ConfigurationField(FIELD);
static final class C {
@Comment("")
List<String> field = List.of("20");
}
@Test
void componentName() {
assertThat(COMPONENT.name(), is("field"));
}
@Test
void componentType() {
assertThat(COMPONENT.type(), equalTo(List.class));
}
@Test
void componentAnnotatedType() {
assertThat(COMPONENT.annotatedType(), is(FIELD.getAnnotatedType()));
}
@Test
void componentValue() {
assertThat(COMPONENT.value(new C()), is(List.of("20")));
}
@Test
void declaringType() {
assertThat(COMPONENT.declaringType(), equalTo(C.class));
}
@Test
void annotation() {
assertThat(COMPONENT.annotation(Comment.class), notNullValue());
}
}
static final class ConfigurationRecordComponentTest {
private static final RecordComponent RECORD_COMPONENT = R.class.getRecordComponents()[0];
private static final ConfigurationRecordComponent COMPONENT =
new ConfigurationRecordComponent(RECORD_COMPONENT);
record R(@Comment("") Set<Integer> comp) {}
@Test
void componentName() {
assertThat(COMPONENT.name(), is("comp"));
}
@Test
void componentType() {
assertThat(COMPONENT.type(), equalTo(Set.class));
}
@Test
void componentAnnotatedType() {
assertThat(COMPONENT.annotatedType(), is(RECORD_COMPONENT.getAnnotatedType()));
}
@Test
void componentValue() {
assertThat(COMPONENT.value(new R(Set.of(1))), is(Set.of(1)));
}
@Test
void declaringType() {
assertThat(COMPONENT.declaringType(), equalTo(R.class));
}
@Test
void annotation() {
assertThat(COMPONENT.annotation(Comment.class), notNullValue());
}
}
}
Loading…
Cancel
Save