Add documentation about SerializeWith and SerializerContext

dev
Exlll 2 years ago
parent eaccf30d5e
commit f0c76d5c5a

@ -14,14 +14,14 @@ 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 enums, records, and POJOs (+ inheritance!)
* Support for all Java 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 Bukkit's `ConfigurationSerializable` types (e.g. `ItemStack`)
* Option to format the names of configuration elements
* Option to exclude fields from being converted
* Option to format field and component names before conversion
* Option to customize null handling
* Option to customize serialization by providing your own serializers
* Option to add headers and footers to configuration files
@ -125,7 +125,7 @@ Two things are noticeable here:
## General information
In the following sections the term _configuration type_ refers to any record type or to any
In the following sections the term _configuration type_ refers to any Java 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. A _configuration element_ is either a class field or a record component of a
@ -133,10 +133,10 @@ configuration type.
### Declaring configuration types
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.
To declare a configuration type, either define a Java record or annotate a class
with `@Configuration` and make sure that it has a no-args constructor. The no-args constructor can
be `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.
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
@ -163,6 +163,9 @@ A configuration type may only contain configuration elements of the following ty
(*) Map keys can only be of simple or enum type, i.e. they cannot be in the `Collections`,
`Configurations`, or `ConfigurationSerializable` type class.
For all types that are not listed in the table above, you can provide your
own [custom serializer](#custom-serializers).
#### Examples of supported types
The following class contains examples of types that this library supports:
@ -174,7 +177,7 @@ public final class SupportedTypes {
String supported;
LocalTime supported;
UUID supported;
ExampleEnum supported; // where 'ExampleEnum' is some Java enum type
ExampleEnum supported; // where 'ExampleEnum' is some Java enum type
ExampleConfig supported; // where 'ExampleConfig' is a class annotated with @Configuration
ExampleRecord supported; // where 'ExampleRecord' is a Java record
@ -219,6 +222,11 @@ public final class UnsupportedTypes<T> {
}
```
**NOTE:** Even though this library does not support these types, it is still possible to serialize
them by providing a custom serializer via
[the `@SerializeWith` annotation](#the-serializewith-annotation). That serializer then has to
be applied to top-level type (i.e. `nesting` must be set to `0`, which is the default).
</details>
### Loading and saving configurations
@ -307,10 +315,10 @@ method are:
```java
// overload 1
YamlConfigurationProperties properties = YamlConfigurationProperties.newBuilder().build();
Config c1 = YamlConfigurations.load(configurationFile, Config.class, properties);
Config config1 = YamlConfigurations.load(configurationFile, Config.class, properties);
// overload 2
Config c2 = YamlConfigurations.load(
Config config2 = YamlConfigurations.load(
configurationFile,
Config.class,
builder -> builder.inputNulls(true).outputNulls(false)
@ -392,8 +400,8 @@ public final class ExampleConfiguration {
Similarly, if you define the following record configuration and save it ...
```java
record User(@Comment("The name") String name, @Comment("The address") Address address) {}
record Address(@Comment("The street") String street) {}
record User(@Comment("The name") String name, @Comment("The address") Address address) {}
User user = new User("John Doe", new Address("10 Downing St"));
```
@ -434,8 +442,8 @@ 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 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
missing. That can happen, for example, when somebody deleted that value from the configuration file,
when you add configuration elements to your configuration type, 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
@ -485,7 +493,7 @@ YamlConfigurationProperties.newBuilder()
.build();
```
### Type conversion and custom serializers
### Type conversion and serializer selection
Before instances of the types listed in the [supported types](#supported-types) section can be
stored, they need to be converted into serializable types (i.e. into types the underlying YAML
@ -516,14 +524,14 @@ properties. This also means that `Set`s are valid target types.
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 `E` and never on the runtime type
of its value.
serializers are, by default, 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 configuration
elements whose type is a configuration type, and you extend that configuration type. Concretely,
elements that are configuration classes, and you extend those classes. 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
@ -532,13 +540,113 @@ instances of `B` (or some other subclass of `A`) in it.
</details>
#### Custom serializers
You can override the default selection by annotating a configuration
element with [`@SerializeWith`](#the-serializewith-annotation) or by adding your own serializer
for `S` to the configuration properties. When you do so, it can happen that there multiple
serializers available for a particular configuration element and its type. In that case, one of them
chosen according to the following precedence rules:
1. If the element is annotated with `@SerializeWith` and the `nesting` matches, the serializer
referenced by the annotation is selected.
2. Otherwise, if the `ConfigurationProperties` contain a serializer for the type in question, that
serializer is returned.
3. Otherwise, if this library defines a serializer for that type, that serializer is selected.
4. Ultimately, if no serializer can be found, an exception is thrown.
For lists, sets, and maps, the algorithm is recursively applied to their generic type arguments
recursively first.
##### The `SerializeWith` annotation
The `SerializeWith` annotation can be applied to configuration elements (i.e. class fields and
record components) and enforces the use of the specified serializer for that element.
```java
@SerializeWith(serializer = MyPointSerializer.class)
Point point;
```
The serializer referenced by this annotation is selected regardless of whether the type of the
configuration element matches the type the serializer expects.
If the configuration element is an array, list, set, or map a nesting level can be set to apply the
serializer not to the top-level type but to its elements. For maps, the serializer is applied to
the values and not the keys.
```java
@SerializeWith(serializer = MySetSerializer.class, nesting = 1)
List<Set<String>> list;
```
Setting `nesting` to an invalid value, i.e. a negative one or one that is greater than the number
of levels the element actually has, results in the serializer not being selected.
<details>
<summary>More <code>nesting</code> examples</summary>
In this example...
```java
@SerializeWith(serializer = MySetSerializer.class, nesting = 1)
List<Set<String>> list;
```
* a nesting of `0` would apply the serializer to `list` (which is of
type `List<Set<String>>`),
* a nesting of `1` would apply it to the `Set<String>` elements within `list`, and
* a nesting of `2` would apply it to the strings within the sets of `list`.
However, since the referenced serializer `MySetSerializer` most likely expects `Set`s as input,
setting `nesting` to `0` or `2` would result in an exception being thrown when the configuration
is serialized.
Some more examples:
```java
// MyListSerializer is applied to 'list'
@SerializeWith(serializer = MyListSerializer.class)
List<Set<String>> list;
// MySetSerializer is applied to the Set<String> elements of 'list'
@SerializeWith(serializer = MySetSerializer.class, nesting = 1)
List<Set<String>> list;
// MyStringSerializer is applied to the strings within the set elements of 'list'
@SerializeWith(serializer = MyStringSerializer.class, nesting = 2)
List<Set<String>> list;
// MyMap0Serializer is applied to 'map'
@SerializeWith(serializer = MyMap0Serializer.class)
Map<Integer, Map<String, Double>> map;
// MyMap1Serializer is applied to the Map<String, Double> values of 'map'
@SerializeWith(serializer = MyMap1Serializer.class, nesting = 1)
Map<Integer, Map<String, Double>> map;
// MyDoubleSerializer is applied to the doubles within the nested values of 'map'
@SerializeWith(serializer = MyDoubleSerializer.class, nesting = 2)
Map<Integer, Map<String, Double>> map;
```
</details>
### Custom serializers
If you want to add support for a type that is not a Java record or whose class is not annotated
with `@Configuration`, or if you don't like how one of the supported types is serialized by default,
you can write your own custom serializer.
Serializers are instances of the `de.exlll.configlib.Serializer` interface. When implementing that
interface you have to make sure that...
If you want to add support for a type that 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.
* your class either has a constructor with no parameters or one with exactly one parameter of
type [`SerializerContext`](#the-serializercontext-interface), and
* you convert your source type into one of the valid target types listed
in [type conversion](#type-conversion-and-serializer-selection) section.
The serializer then has to be registered through a `ConfigurationProperties` object or alternatively
be applied to a configuration element
with [the `@SerializeWith` annotation](#the-serializewith-annotation).
The following `Serializer` serializes instances of `java.awt.Point` into strings and vice versa.
@ -559,7 +667,19 @@ public final class PointSerializer implements Serializer<Point, String> {
}
```
Custom serializers takes precedence over the serializers provided by this library.
```java
YamlConfigurationProperties properties = YamlConfigurationProperties.newBuilder()
.addSerializer(Point.class, new PointSerializer())
.build();
```
##### The `SerializerContext` interface
Instead of a no-args constructor custom serializers are allowed to declare a constructor with one
parameter of type `SerializerContext`. If such a constructor exists, an instance of that class is
passed to it when the serializer is instantiated by this library. The context object gives access
to the configuration properties, configuration element, and the annotated type for which the
serializer was selected.
### Changing the type of configuration elements
@ -649,7 +769,9 @@ If you want serialization support for Bukkit classes like `ItemStack`, replace `
with `configlib-paper` (see [here](#support-for-bukkit-classes-like-itemstack)).
<details>
<summary>Import via <code>jitpack.io</code></summary>
<summary>
Import via <a href="https://jitpack.io/#Exlll/ConfigLib">jitpack.io</a>
</summary>
**Maven**

Loading…
Cancel
Save