Update documentation for Records

dev
Exlll 2 years ago
parent e5028e6199
commit 6ee99ff619

@ -13,12 +13,12 @@ the [Tutorial](https://github.com/Exlll/ConfigLib/wiki/Tutorial) page on the wik
* Automatic creation, saving, loading, and updating of configuration files
* Support for comments through annotations
* Support for all primitive types, their wrapper types, and Strings
* Support for all primitive types, their wrapper types, and strings
* Support for enums, records, and POJOs (+ inheritance!)
* Support for (nested) lists, sets, arrays, and maps
* Support for `BigInteger` and `BigDecimal`
* Support for `LocalDate`, `LocalTime`, `LocalDateTime`, and `Instant`
* Support for `UUID`, `File`, `Path`, `URL`, and `URI`
* Support for (nested) lists, sets, arrays, and maps
* Support for enums and POJOs (+ inheritance!)
* Support for Bukkit's `ConfigurationSerializable` types (e.g. `ItemStack`)
* Option to exclude fields from being converted
* Option to format field and component names before conversion
@ -68,18 +68,12 @@ public final class Example {
);
}
@Configuration
public static final class User {
private String username;
@Comment("Please choose a strong password.")
private String password;
// Configuration classes require a no-args constructor.
// The constructor can be private though.
private User() {}
public User(String username, String password) {/* initialize */}
}
// This library supports records; no @Configuration annotation required
public record User(
String username,
@Comment("Please choose a strong password.")
String password
) {}
public static void main(String[] args) {
var configFile = Paths.get("/tmp/config.yml");
@ -129,25 +123,25 @@ Two things are noticeable here:
## General information
In the following sections the term _configuration type_ refers to any non-generic class that is
directly or indirectly (i.e. through subclassing) annotated with
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.
### Declaring configuration types
To declare a configuration type, annotate a class with `@Configuration` and make sure that it has a
no-args constructor. The no-args constructor can be set `private`. Inner classes (i.e. the ones that
are nested but not `static`) have an implicit synthetic constructor with at least one argument and
are therefore not supported.
To declare a configuration type, either define a record or annotate a class with `@Configuration`
and make sure that it has a no-args constructor. The no-args constructor can be set `private`. Inner
classes (i.e. the ones that are nested but not `static`) have an implicit synthetic constructor with
at least one argument and are therefore not supported.
Add fields to your class whose type is any of the supported types listed in the next section. You
should initialize all fields of reference types with non-null default values, though you can leave
them null. Handling of null values is discussed in one of the sections further below.
Now simply add components to your record or fields to your class whose type is any of the supported
types listed in the next section. You can (and should) initialize all fields of a configuration
class with non-null default values.
### Supported types
A configuration type may only contain fields of the following types:
A configuration type may only contain fields or components of the following types:
| Type class | Types |
|-----------------------------|--------------------------------------------------------------------|
@ -159,7 +153,7 @@ A configuration type may only contain fields of the following types:
| Time related types | `LocalTime`, `LocalDate`, `LocalDateTime`, `Instant` |
| Utility types | `UUID`, `File`, `Path`, `URL`, `URI` |
| Enums | Any Java enum |
| Configurations | Any configuration type |
| Configurations | Any Java record or any class annotated with `@Configuration` |
| `ConfigurationSerializable` | All Bukkit classes that implement this interface, like `ItemStack` |
| Collections | (Nested) Lists, sets, maps*, or arrays of previously listed types |
@ -178,18 +172,19 @@ public final class SupportedTypes {
LocalTime supported;
UUID supported;
ExampleEnum supported; // where 'ExampleEnum' is some Java enum type
ExampleConf supported; // where 'ExampleConf' is another configuration type
ExampleConfig supported; // where 'ExampleConfig' is a class annotated with @Configuration
ExampleRecord supported; // where 'ExampleRecord' is a Java record
/* collection types */
List<BigInteger> supported;
Set<Double> supported;
LocalDate[] supported;
Map<ExampleEnum, ExampleConf> supported;
Map<ExampleEnum, ExampleConfig> supported;
/* nested collection types */
List<Map<ExampleEnum, LocalDate>> supported;
int[][] supported;
Map<Integer, List<Map<Short, Set<ExampleConf>>>> supported;
Map<Integer, List<Map<Short, Set<ExampleRecord>>>> supported;
// supported if a custom serializer is registered
java.awt.Point supported;
@ -231,8 +226,8 @@ Both ways have three methods in common:
* `save` saves a configuration to a file
* `load` creates a new configuration instance and populates it with values taken from a file
* `update` 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 updates it otherwise to reflect
changes to (the fields of) the configuration type.
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.
<details>
<summary>Example of <code>update</code> behavior when configuration file exists</summary>
@ -347,9 +342,9 @@ described in the [Import](#import) section.
### Comments
The fields 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. Empty strings
are written as newlines.
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.
Empty strings are written as newlines.
Serializing the following configuration as YAML ...
@ -371,9 +366,10 @@ public final class ExampleConfiguration {
commentedField: commented field
```
If a configuration type _C_ that defines comments is used (as a field) 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.
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
instances of _C_ are stored inside a collection, their comments are not printed when the collection
is written.
### Subclassing
@ -393,7 +389,7 @@ and describe how to handle name clashes.)
Fields that are `final`, `static`, `transient` or annotated with `@Ignore` are neither serialized
nor updated during deserialization. You can filter out additional fields by providing an instance of
`FieldFilter` to the configuration properties.
`FieldFilter` to the configuration properties. Record components cannot be filtered.
### Handling of missing and `null` values
@ -403,15 +399,20 @@ When a configuration file is read, values that correspond to a field of a config
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 changed.
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.
#### `null` values
#### Null values
**NOTE:** Null values written to a configuration file generally don't give any indication about
which kinds of values the configuration expects. Therefore, they not only make it harder for the
users of that configuration file to properly configure it, but they might also prevent loading a
configuration if the values the users set are of the wrong type.
Configuration properties let you configure how `null` values are handled when serializing and
deserializing a configuration or record 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
are null are not output. Any comments that belong to such fields are also not written.
@ -474,8 +475,8 @@ properties. This also means that `Set`s are valid target types.
#### Serializer selection
To convert the value of a field `F` with (source) type `S` into a serializable value of some
target type, a serializer has to be selected. Serializers are instances of
To convert the value of a field or record component `F` 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
of its value.
@ -483,23 +484,23 @@ 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 whose type is
a configuration type or a collection of some configuration type, and you extend that configuration
type. Concretely, assume you have and written two configuration types `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 compile-time type of `a` which is `A` and not `B`. The same
happens if you have a `List<A>` and put instances of `B` (or some other subclass of `A`) in it.
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,
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
compile-time type of `a` which is `A` and not `B`. The same happens if you have a `List<A>` and put
instances of `B` (or some other subclass of `A`) in it.
</details>
#### Custom serializers
If you want to add support for a type whose class is not annotated with `@Configuration`, you can
register a custom serializer. Serializers are instances of the `de.exlll.configlib.Serializer`
interface. When implementing that interface you have to make sure that you convert your source type
into one of the valid target types listed in the table above. The serializer then has to be
registered through a `ConfigurationProperties` object.
If you want to add support for a type is not a record or whose class is not annotated
with `@Configuration`, you can register a custom serializer. Serializers are instances of
the `de.exlll.configlib.Serializer` interface. When implementing that interface you have to make
sure that you convert your source type into one of the valid target types listed in the table above.
The serializer then has to be registered through a `ConfigurationProperties` object.
The following `Serializer` serializes instances of `java.awt.Point` into strings.
@ -522,11 +523,12 @@ public final class PointSerializer implements Serializer<Point, String> {
Custom serializers takes precedence over the serializers provided by this library.
### Changing the type of fields
### Changing the type of fields or record components
Changing the type of fields is not supported. If you change the type of one of your fields but your
configuration file still contains a value of the old type, a type mismatch will occur when reading
that file. Instead, remove the old field and add a new one with a different name.
Changing the type of fields or record components 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.
### Recursive type definitions

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

@ -3,28 +3,30 @@ package de.exlll.configlib;
import java.util.List;
/**
* Holds the comments of a field as well as a list of field names. The list of
* field names contains the names of all fields which led to this field from the
* root of the configuration object. For example, for the following situation the
* 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
* 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
* {@code CommentNode} of the field {@code fn2} would hold {@code comments} and
* {@code fieldNames} lists that contain the values {@code ["Hello World"]} and
* {@code ["fn0", "fn1", "fn2"]}, respectively.
* {@code elementNames} lists that contain the values {@code ["Hello","World"]} and
* {@code ["fn0","fn1","fn2"]}, respectively.
*
* <pre>
* class A {
* B fn0 = new B();
* }
* class B {
* C fn1 = new C();
* }
*
* record B(C fn1) {}
*
* class C {
* {@code @Comment("Hello world")}
* {@code @Comment({"Hello", "World"})}
* int fn2;
* }
* </pre>
*
* @param comments
* @param fieldNames
* @param elementNames
*/
// TODO change javadoc
record CommentNode(List<String> comments, List<String> fieldNames) {}
record CommentNode(List<String> comments, List<String> elementNames) {}

@ -33,13 +33,13 @@ final class CommentNodeExtractor {
public Queue<CommentNode> extractCommentNodes(final Object componentHolder) {
requireConfigurationOrRecord(componentHolder.getClass());
final Queue<CommentNode> result = new ArrayDeque<>();
final var fnameStack = new ArrayDeque<>(List.of(""));
final var elementNameStack = new ArrayDeque<>(List.of(""));
final var stateStack = new ArrayDeque<>(List.of(stateFromObject(componentHolder)));
State state;
while (!stateStack.isEmpty()) {
state = stateStack.removeLast();
fnameStack.removeLast();
elementNameStack.removeLast();
while (state.iterator.hasNext()) {
final var component = state.iterator.next();
@ -60,7 +60,11 @@ final class CommentNodeExtractor {
assert element != null;
final var componentName = component.componentName();
final var commentNode = createNodeIfCommentPresent(element, componentName, fnameStack);
final var commentNode = createNodeIfCommentPresent(
element,
componentName,
elementNameStack
);
commentNode.ifPresent(result::add);
if ((componentValue == null) ||
@ -69,7 +73,7 @@ final class CommentNodeExtractor {
continue;
stateStack.addLast(new State(state.iterator, state.componentHolder));
fnameStack.addLast(nameFormatter.format(componentName));
elementNameStack.addLast(nameFormatter.format(componentName));
state = stateFromObject(componentValue);
}
}
@ -88,14 +92,14 @@ final class CommentNodeExtractor {
private Optional<CommentNode> createNodeIfCommentPresent(
final AnnotatedElement element,
final String elementName,
final Deque<String> fileNameStack
final Deque<String> elementNameStack
) {
if (element.isAnnotationPresent(Comment.class)) {
final var comments = element.getAnnotation(Comment.class).value();
final var fieldName = nameFormatter.format(elementName);
final var fieldNames = new ArrayList<>(fileNameStack);
fieldNames.add(fieldName);
final var result = new CommentNode(Arrays.asList(comments), fieldNames);
final var formattedName = nameFormatter.format(elementName);
final var elementNames = new ArrayList<>(elementNameStack);
elementNames.add(formattedName);
final var result = new CommentNode(Arrays.asList(comments), elementNames);
return Optional.of(result);
}
return Optional.empty();

@ -144,10 +144,10 @@ 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 type of a field. 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 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.
*
* @param condition the condition
* @param serializer the serializer
@ -165,8 +165,8 @@ class ConfigurationProperties {
}
/**
* Sets whether fields or collection elements whose value is null should be output
* while serializing the configuration.
* Sets whether fields, record components, or collection elements whose value
* is null should be output while serializing the configuration.
* <p>
* The default value is {@code false}.
*
@ -179,8 +179,11 @@ class ConfigurationProperties {
}
/**
* Sets whether fields or collection elements should allow null values to bet set
* while deserializing the configuration.
* Sets whether fields, record components, 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
* are treated as missing.
* <p>
* The default value is {@code false}.
*

@ -35,9 +35,10 @@ public interface FileConfigurationStore<T> {
/**
* Updates the configuration file. If the file does not exist, it is created and populated
* with the default values with which the fields of the configuration have been initialized.
* Otherwise, a new configuration instance is created, initialized with the values taken from
* the configuration file, and immediately saved to reflect possible changes of the
* configuration type.
* If the configuration is of record type, the default values are chosen to be the default
* values of its component types. Otherwise, if the file exists, a new configuration instance
* is created, initialized with the values taken from the configuration file, and immediately
* saved to reflect possible changes of the configuration type.
*
* @param configurationFile the configuration file that is updated
* @return a newly created configuration initialized with values taken from the configuration file

@ -211,11 +211,12 @@ public final class YamlConfigurationStore<T> implements FileConfigurationStore<T
* to properly support comments, at least not the way I want them.
*
* The algorithm writes YAML line by line and keeps track of the current
* context with the help of fieldNames lists which come from the nodes in
* context with the help of elementNames lists which come from the nodes in
* the 'nodes' queue. The 'nodes' queue contains nodes in the order in
* which the fields were read, which happened in DFS manner and with fields
* of a parent class being read before the fields of the child. That order
* ultimately represents the order in which the YAML file is structured.
* which fields and records components were extracted, which happened in
* DFS manner and with fields of a parent class being read before the fields
* of a child. That order ultimately represents the order in which the
* YAML file is structured.
*/
var node = nodes.poll();
var currentIndentLevel = 0;
@ -226,20 +227,20 @@ public final class YamlConfigurationStore<T> implements FileConfigurationStore<T
continue;
}
final var fieldNames = node.fieldNames();
final var elementNames = node.elementNames();
final var indent = " ".repeat(currentIndentLevel);
final var lineStart = indent + fieldNames.get(currentIndentLevel) + ":";
final var lineStart = indent + elementNames.get(currentIndentLevel) + ":";
if (!line.startsWith(lineStart)) {
writeLine(line);
continue;
}
final var commentIndentLevel = fieldNames.size() - 1;
final var commentIndentLevel = elementNames.size() - 1;
if (currentIndentLevel++ == commentIndentLevel) {
writeComments(node.comments(), commentIndentLevel);
if ((node = nodes.poll()) != null) {
currentIndentLevel = lengthCommonPrefix(node.fieldNames(), fieldNames);
currentIndentLevel = lengthCommonPrefix(node.elementNames(), elementNames);
}
}

@ -519,7 +519,7 @@ class CommentNodeExtractorTest {
assertTrue(nodes.isEmpty());
}
private static CommentNode cn(List<String> comments, String... fieldNames) {
return new CommentNode(comments, List.of(fieldNames));
private static CommentNode cn(List<String> comments, String... elementNames) {
return new CommentNode(comments, List.of(elementNames));
}
}
Loading…
Cancel
Save