diff --git a/README.md b/README.md index 0fd0da0..94eb718 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,18 @@ # ConfigLib -**A Minecraft library for saving, loading, updating, and commenting YAML configuration files.** +**A Minecraft library for saving, loading, updating, and commenting YAML +configuration files.** -This library facilitates creating, saving, loading, updating, and commenting YAML configuration -files. It does so by automatically mapping instances of configuration classes to serializable maps -which are first transformed into YAML and then saved to some specified file. +This library facilitates creating, saving, loading, updating, and commenting +YAML configuration files. It does so by automatically mapping instances of +configuration classes to serializable maps which are first transformed into YAML +and then saved to some specified file. -Information on how to [import](#import) this library can be found at the end of this documentation. -For a step-by-step tutorial that shows most features of this library in action check out -the [Tutorial](https://github.com/Exlll/ConfigLib/wiki/Tutorial) page on the wiki! +Information on how to [import](#import) this library can be found at the end of +this documentation. For a step-by-step tutorial that shows most features of this +library in action check out +the [Tutorial](https://github.com/Exlll/ConfigLib/wiki/Tutorial) page on the +wiki! ## Features @@ -30,12 +34,13 @@ the [Tutorial](https://github.com/Exlll/ConfigLib/wiki/Tutorial) page on the wik ## Usage example -This section contains a short usage example to get you started. The whole range of features is -discussed in the following sections. Information on how to [import](#import) this library is located -at the end of this documentation. +This section contains a short usage example to get you started. The whole range +of features is discussed in the following sections. Information on how +to [import](#import) this library is located at the end of this documentation. For a step-by-step tutorial with a more advanced example check out -the [Tutorial](https://github.com/Exlll/ConfigLib/wiki/Tutorial) page on the wiki. +the [Tutorial](https://github.com/Exlll/ConfigLib/wiki/Tutorial) page on the +wiki. If you want support for Bukkit classes like `ItemStack`, check out the [Configuration properties](#configuration-properties) section. @@ -96,8 +101,8 @@ public final class Example { } ``` -By running the above code, a new YAML configuration is created at `/tmp/config.yml`. Its content -looks like this: +By running the above code, a new YAML configuration is created +at `/tmp/config.yml`. Its content looks like this: ```yaml host: 127.0.0.1 @@ -119,33 +124,38 @@ blockedUsers: Two things are noticeable here: -1. Not every user in the `blockedUsers` list has a `password` mapping. This is because null values - are not output by default. That behavior can be changed by the builder. -2. The password of the user with username `user3` has no comment. This is due to limitations of - the YAML library. Configurations in lists, sets, or maps cannot have their comments printed. +1. Not every user in the `blockedUsers` list has a `password` mapping. This is + because null values are not output by default. That behavior can be changed + by the builder. +2. The password of the user with username `user3` has no comment. This is due to + limitations of the YAML library. Configurations in lists, sets, or maps + cannot have their comments printed. ## General information -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 +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 configuration type. ### Declaring configuration types 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. +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 -class with non-null default values. +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 configuration elements of the following types: +A configuration type may only contain configuration elements of the following +types: | Type class | Types | |-----------------------------|--------------------------------------------------------------------| @@ -161,8 +171,8 @@ A configuration type may only contain configuration elements of the following ty | `ConfigurationSerializable` | All Bukkit classes that implement this interface, like `ItemStack` | | Collections | (Nested) Lists, sets, maps*, or arrays of previously listed types | -(*) Map keys can only be of simple or enum type, i.e. they cannot be in the `Collections`, -`Configurations`, or `ConfigurationSerializable` type class. +(*) 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). @@ -204,7 +214,8 @@ public final class SupportedTypes {
Examples of unsupported types -The following class contains examples of types that this library does (and will) not support: +The following class contains examples of types that this library does (and will) +not support: ```java public final class UnsupportedTypes { @@ -223,31 +234,38 @@ public final class UnsupportedTypes { } ``` -**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`](#the-serializewith-annotation) -annotation. That serializer then has to be applied to top-level type (i.e. `nesting` must be set +**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`](#the-serializewith-annotation) annotation. That +serializer then has to be applied to top-level type (i.e. `nesting` must be set to `0`, which is the default).
### Loading and saving configurations -There are two ways to load and save configurations. Which way you choose depends on your liking. -Both ways have three methods in common: - -* The `save` method saves a configuration to a file. The file is created if it does not exist and - is overwritten otherwise. -* The `load` method creates a new configuration instance and populates it with values taken from a - file. For classes, the no-args constructor is used to create a new instance. For records, the - 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 configuration elements of) the configuration type. +There are two ways to load and save configurations. Which way you choose depends +on your liking. Both ways have five methods in common: + +* The `save` method converts a configuration to a string in YAML format and + saves that string to a file. The file is created if it does not exist and is + overwritten otherwise. +* The `load` method creates a new configuration instance and populates it with + values taken from a file. For classes, the no-args constructor is used to + create a new instance. For records, the 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 + configuration elements of) the configuration type. +* The `write` method works the same way as the `save` method but writes the + string to a `java.io.OutputStream`. +* The `read` method works the same way as the `load` method but reads the values + from a `java.io.InputStream`.
Example of update behavior when configuration file exists -Let's say you have the following configuration type: +Let's say you have the following configuration type ```java @Configuration @@ -264,31 +282,34 @@ i: 20 k: 30 ``` -Now, when you use one of the methods below to call `update` for that configuration type and file, -the configuration instance that `update` returns will have its `i` variable initialized to `20` -and its `j` variable will have its default of `11`. After the operation, the configuration file will -contain: +Now, when you call the `update` method for that configuration type and file +using any of the two options listed below, the configuration instance +that `update` returns will have its `i` variable initialized to `20` and its `j` +variable will have its default of `11`. After the operation, the configuration +file will contain the following content (note that `k` has been dropped): ```yaml i: 20 j: 11 ``` +
+
-To exemplify the usage of these three methods we assume for the following sections that you have -implemented the configuration type below and have access to some regular `java.nio.file.Path` -object `configurationFile`. +To exemplify the usage of these five methods we assume for the following +sections that you have implemented the configuration type below and have access +to some regular `java.nio.file.Path` object `configurationFile`. ```java @Configuration public final class Config { /* some fields */ } ``` -#### Way 1 +#### Option 1 -The first way is to create a configuration store and use it directly to save, load, or update -configurations. +The first option is to create a `YamlConfigurationStore` instance and use it to +save, load, or update configurations. ```java YamlConfigurationProperties properties = YamlConfigurationProperties.newBuilder().build(); @@ -299,9 +320,14 @@ store.save(config1, configurationFile); Config config2 = store.update(configurationFile); ``` -#### Way 2 +Using a `YamlConfigurationStore` directly is always more efficient than the +second option show below, especially if you are calling any of its method +multiple times. + +#### Option 2 -The second way is to use the static methods from the `YamlConfigurations` class. +The second option is to use the static methods from the `YamlConfigurations` +class. ```java Config config1 = YamlConfigurations.load(configurationFile, Config.class); @@ -309,9 +335,9 @@ YamlConfigurations.save(configurationFile, Config.class, config1); Config config2 = YamlConfigurations.update(configurationFile, Config.class); ``` -Each of these methods has two additional overloads: One that takes a properties object and another -that lets you configure a properties object builder. For example, the overloads of the `load` -method are: +Each of these methods has two additional overloads: One that takes a properties +object and another that lets you configure a properties object builder. For +example, the overloads of the `load` method are: ```java // overload 1 @@ -328,10 +354,10 @@ Config config2 = YamlConfigurations.load(
-All three methods can also be passed a Java record instead of a class. To provide default values -for records when calling the `update` method, you can add a constructor with no parameters that -initializes its components. This constructor is only called if the configuration file does not -exist. +All five methods can also be passed a Java record instead of a class. To provide +default values for records when calling the `update` method, you can add a +constructor with no parameters that initializes its components. This constructor +is only called if the configuration file does not exist. ```java record User(String name, String email) { @@ -342,19 +368,21 @@ User user = YamlConfigurations.update(configurationFile, User.class); ### Configuration properties -Instances of the `ConfigurationProperties` class allow customization of how configurations are -stored and loaded. To create such an instance, instantiate a new builder using -the `YamlConfigurationProperties.newBuilder()` method, configure it, and finally call its `build()` -method. Alternatively, you can use the `toBuilder()` method of an -existing `YamlConfigurationProperties` to create a new builder that is initialized with values -takes from the properties object. +Instances of the `ConfigurationProperties` class allow customization of how +configurations are stored and loaded. To create such an instance, instantiate a +new builder using the `YamlConfigurationProperties.newBuilder()` method, +configure it, and finally call its `build()` method. Alternatively, you can use +the `toBuilder()` method of an existing `YamlConfigurationProperties` to create +a new builder that is initialized with values takes from the properties object. -Check out the methods of the builder class to see which configuration options are available. +Check out the methods of the builder class to see which configuration options +are available. #### Support for Bukkit classes like `ItemStack` -There is a special `YamlConfigurationProperties` object with name `BUKKIT_DEFAULT_PROPERTIES` -that adds support for Bukkit's `ConfigurationSerializable` types. If you want to use any of these +There is a special `YamlConfigurationProperties` object with +name `BUKKIT_DEFAULT_PROPERTIES` that adds support for +Bukkit's `ConfigurationSerializable` types. If you want to use any of these types in your configuration, you have to use that object as a starting point: ```java @@ -363,20 +391,21 @@ YamlConfigurationProperties properties = ConfigLib.BUKKIT_DEFAULT_PROPERTIES.toB .build(); ``` -To get access to this object, you have to import `configlib-paper` instead of `configlib-yaml` as -described in the [Import](#import) section. +To get access to this object, you have to import `configlib-paper` instead +of `configlib-yaml` as described in the [Import](#import) section. ### 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 +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 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. +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. Serializing the following configuration as YAML ... @@ -420,56 +449,66 @@ address: ### Subclassing -Subclassing of configurations types is supported. Subclasses of configuration types don't need to be -annotated with `@Configuration`. When a configuration is written, the fields of parent classes -are written before the fields of the child in a top to bottom manner. Parent configurations can -be `abstract`. +Subclassing of configurations types is supported. Subclasses of configuration +types don't need to be annotated with `@Configuration`. When a configuration is +written, the fields of parent classes are written before the fields of the child +in a top to bottom manner. Parent configurations can be `abstract`. #### Shadowing of fields -Shadowing of fields refers to the situation where a subclass of configuration has a field that has -the same name as a field in one of its super classes. Shadowing of fields is currently not -supported. (This restriction might easily be lifted. If you need this feature, please open an issue -and describe how to handle name clashes.) +Shadowing of fields refers to the situation where a subclass of configuration +has a field that has the same name as a field in one of its super classes. +Shadowing of fields is currently not supported. (This restriction might easily +be lifted. If you need this feature, please open an issue and describe how to +handle name clashes.) ### Ignoring and filtering fields -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. Record components cannot be filtered. +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. Record components cannot be filtered. ### Handling of missing and `null` values #### 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 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. +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 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 classes keep the default value you assigned to them and -record components are initialized with the default value of their corresponding type. +In such cases, fields of configuration classes keep the default value you +assigned to them and record components are initialized with the default value of +their corresponding type. #### 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. - -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, 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 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. +**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. + +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, 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 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. ```java YamlConfigurationProperties.newBuilder() @@ -480,13 +519,14 @@ YamlConfigurationProperties.newBuilder() ### Formatting the names of configuration elements -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. +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). +The following code formats fields using the `IDENTITY` formatter (which is the +default). ```java YamlConfigurationProperties.newBuilder() @@ -496,9 +536,10 @@ YamlConfigurationProperties.newBuilder() ### 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 -library knows how to handle). The conversion happens according to the following table: +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 library knows how to handle). The conversion +happens according to the following table: | Source type | Target type | |-----------------------------|------------------| @@ -511,64 +552,74 @@ library knows how to handle). The conversion happens according to the following | Utility types | `String` | | Enums | `String` | | Configurations | `Map` | -| `Set` | `List`* | +| `Set` | `List` | | `List` | `List` | | `S[]` | `List` | | `Map` | `Map` | | `ConfigurationSerializable` | `String` | -(*) By default, sets are serialized as lists. This can be changed through the configuration -properties. This also means that `Set`s are valid target types. - #### Serializer selection -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, by default, always selected based on the compile-time type of `E` and never on the -runtime type of its value. +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, by default, always +selected based on the compile-time type of `E` and never on the runtime type of +its value.
Why should I care about this? -This distinction makes a difference (and might lead to confusion) when you have configuration -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 -compile-time type of `a` which is `A` and not `B`. The same happens if you have a `List` 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 +configuration 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 compile-time type of `a` which is `A` and not `B`. The same happens if you +have a `List` and put instances of `B` (or some other subclass of `A`) in it. + +If you need such behavior, have a look at +the [`@Polymorphic`](#the-polymorphic-annotation) annotation.
+
+ Order of serializer selection + You can override the default selection by annotating a configuration -element with [`@SerializeWith`](#the-serializewith-annotation), by annotating a type -with `@SerializeWith`, 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. - * Serializers created by factories that were added through `addSerializerFactory` for some type - take precedence over serializers added by `addSerializer` for the same type. -3. If the type is annotated `@SerializeWith`, the serializer referenced by the annotation is +element with [`@SerializeWith`](#the-serializewith-annotation), by annotating a +type with `@SerializeWith`, 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. + * Serializers created by factories that were added + through `addSerializerFactory` for some type take precedence over + serializers added by `addSerializer` for the same type. +3. If the type is annotated `@SerializeWith`, the serializer referenced by the + annotation is selected. +4. If the type is annotated with an annotation which is annotated + with `@SerializeWith`, the serializer referenced by `@SerializeWith` is + returned. +5. If this library defines a serializer for that type, that serializer is selected. -4. If the type is annotated with an annotation which is annotated with `@SerializeWith`, the - serializer referenced by `@SerializeWith` is returned. -5. If this library defines a serializer for that type, that serializer is selected. 6. Ultimately, if no serializer can be found, an exception is thrown. -For lists, sets, and maps, the algorithm is applied to their generic type arguments recursively -first. +For lists, sets, and maps, the algorithm is applied to their generic type +arguments recursively first. + +
-##### The `SerializeWith` annotation +##### The `@SerializeWith` annotation -The `SerializeWith` annotation enforces the use of the specified serializer for a configuration -element or type. It can be applied to configuration elements (i.e. class fields and -record components), to types, and to other annotations. +The `@SerializeWith` annotation enforces the use of the specified serializer for +a configuration element or type. It can be applied to configuration elements +(i.e. class fields and record components), to types, and to other annotations. ```java @SerializeWith(serializer = MyPointSerializer.class) @@ -580,21 +631,24 @@ Point point; public final class SomeClass {/* ... */} ``` -The serializer referenced by this annotation is selected regardless of whether the annotated type or -type of configuration element matches the type the serializer expects. +The serializer referenced by this annotation is selected regardless of whether +the annotated type or type of configuration element matches the type the +serializer expects. -If the annotation is applied to a configuration element and that 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. +If the annotation is applied to a configuration element and that 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> 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. For type -annotations, the `nesting` has no effect. +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. For type annotations, the `nesting` has no +effect.
More nesting examples @@ -608,12 +662,13 @@ List> list; * a nesting of `0` would apply the serializer to `list` (which is of type `List>`), -* a nesting of `1` would apply it to the `Set` elements within `list`, and +* a nesting of `1` would apply it to the `Set` 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. +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: @@ -647,12 +702,13 @@ Map> map; #### The `@Polymorphic` annotation -The `@Polymorphic` annotation indicates that the annotated type is polymorphic. Serializers for -polymorphic types are not selected based on the compile-time types of configuration elements, but -instead are chosen at runtime based on the actual types of their values. +The `@Polymorphic` annotation indicates that the annotated type is polymorphic. +Serializers for polymorphic types are not selected based on the compile-time +types of configuration elements, but instead are chosen at runtime based on the +actual types of their values. -This enables adding instances of subclasses / implementations of a polymorphic type to collections. -The subtypes must be valid configurations. +This enables adding instances of subclasses / implementations of a polymorphic +type to collections. The subtypes must be valid configurations. ```java @Polymorphic @@ -665,10 +721,11 @@ static final class Impl2 extends A { ... } List as = List.of(new Impl1(...), new Impl2(...), ...); ``` -For correct deserialization, if an instance of polymorphic type (or one of its implementations / -subclasses) is serialized, an additional property that holds type information is added to its -serialization. By default, that type information is the Java class name of the actual type. It is -possible to provide type aliases by using the `PolymorphicTypes` annotation. +For correct deserialization, if an instance of polymorphic type (or one of its +implementations / subclasses) is serialized, an additional property that holds +type information is added to its serialization. By default, that type +information is the Java class name of the actual type. It is possible to provide +type aliases by using the `PolymorphicTypes` annotation. ```java @Polymorphic @@ -684,21 +741,25 @@ record Impl2(...) implements B { ... } ### 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. +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 you convert your source type into one of the valid target types -listed in [type conversion](#type-conversion-and-serializer-selection) section. +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 [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 or type -with [`@SerializeWith`](#the-serializewith-annotation). If you want to use the `@SerializeWith` -annotation, your serializer class must either have a constructor with no parameters or one with -exactly one parameter of type [`SerializerContext`](#the-serializercontext-interface). +The serializer then has to be registered through a `ConfigurationProperties` +object or alternatively be applied to a configuration element or type +with [`@SerializeWith`](#the-serializewith-annotation). If you want to use +the `@SerializeWith` annotation, your serializer class must either have a +constructor with no parameters or one with exactly one parameter of +type [`SerializerContext`](#the-serializercontext-interface). -The following `Serializer` serializes instances of `java.awt.Point` into strings and vice versa. +The following `Serializer` serializes instances of `java.awt.Point` into strings +and vice versa. ```java public final class PointSerializer implements Serializer { @@ -725,12 +786,13 @@ YamlConfigurationProperties properties = YamlConfigurationProperties.newBuilder( ##### The `SerializerContext` interface -Instances of the `SerializerContext` interface contain contextual information for custom -serializers. A context object gives access to the configuration properties, configuration element, -and the annotated type for which the serializer was selected. +Instances of the `SerializerContext` interface contain contextual information +for custom serializers. A context object gives access to the configuration +properties, configuration element, and the annotated type for which the +serializer was selected. -Context objects can be obtained when adding serializer factories through the `addSerializerFactory` -method: +Context objects can be obtained when adding serializer factories through +the `addSerializerFactory` method: ```java public final class PointSerializer implements Serializer { @@ -749,21 +811,22 @@ YamlConfigurationProperties properties = YamlConfigurationProperties.newBuilder( .build(); ``` -Custom serializers used with `@SerializeWith` are allowed to declare a constructor with one -parameter of type `SerializerContext`. If such a constructor exists, a context object is passed to -it when the serializer is instantiated by this library. +Custom serializers used with `@SerializeWith` are allowed to declare a +constructor with one parameter of type `SerializerContext`. If such a +constructor exists, a context object is passed to it when the serializer is +instantiated by this library. ### Changing the type of configuration elements -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. +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. ### Recursive type definitions -Recursive type definitions are currently not allowed but might be supported in a future version if -this feature is requested. +Recursive type definitions are currently not allowed but might be supported in a +future version if this feature is requested.
Examples of recursive type definitions @@ -797,54 +860,63 @@ public final class RecursiveTypDefinitions { This project contains three classes of modules: -* The `configlib-core` module contains most of the logic of this library. In it, you can find (among - other things), the object mapper that converts configuration instances to maps (and vice versa), - most serializers, and the classes responsible for the extraction of comments. It does not - contain anything Minecraft related. -* The `configlib-yaml` module contains the classes that can save configuration instances as YAML - files and instantiate new instances from such files. This module does not contain anything - Minecraft related, either. -* The `configlib-paper`, `configlib-velocity`, and `configlib-waterfall` modules contain basic - plugins that are used to conveniently load this library. These three modules shade the `-core` - module, the `-yaml` module, and the YAML parser when the `shadowJar` task is executed. The shaded - jar files are released on the [releases page](https://github.com/Exlll/ConfigLib/releases). - * The `configlib-paper` module additionally contains the `ConfigLib.BUKKIT_DEFAULT_PROPERTIES` - object which adds support for the serialization of Bukkit classes like `ItemStack` as +* The `configlib-core` module contains most of the logic of this library. In it, + you can find (among other things), the object mapper that converts + configuration instances to maps (and vice versa), most serializers, and the + classes responsible for the extraction of comments. It does not contain + anything Minecraft related. +* The `configlib-yaml` module contains the classes that can save configuration + instances as YAML files and instantiate new instances from such files. This + module does not contain anything Minecraft related, either. +* The `configlib-paper`, `configlib-velocity`, and `configlib-waterfall` modules + contain basic plugins that are used to conveniently load this library. These + three modules shade the `-core` module, the `-yaml` module, and the YAML + parser when the `shadowJar` task is executed. The shaded jar files are + released on the [releases page](https://github.com/Exlll/ConfigLib/releases). + * The `configlib-paper` module additionally contains + the `ConfigLib.BUKKIT_DEFAULT_PROPERTIES` object which adds support for + the serialization of Bukkit classes like `ItemStack` as described [here](#support-for-bukkit-classes-like-itemstack). The GitHub repository of this project uses two branches: * The `master` branch contains the functionality of the latest release version. -* The `dev` branch contains the newest, possibly unstable features and refactorings. +* The `dev` branch contains the newest, possibly unstable features and + refactorings. -If you plan to contribute to this project, please base your commits on the `dev` branch. +**If you plan to contribute to this project, please base your commits on +the `dev` branch.** ## Import -To use this library, import it into your project with Maven or Gradle. Examples of how to do that -are at the end of this section within the spoilers. Currently, there are two repositories from -which you can choose: [jitpack.io](https://jitpack.io/#Exlll/ConfigLib) and GitHub (which requires -authentication, see this [issue](https://github.com/Exlll/ConfigLib/issues/12) if you have any +To use this library, import it into your project with Maven or Gradle. Examples +of how to do that are at the end of this section within the spoilers. Currently, +there are two repositories from which you can +choose: [jitpack.io](https://jitpack.io/#Exlll/ConfigLib) and GitHub (which +requires authentication, see +this [issue](https://github.com/Exlll/ConfigLib/issues/12) if you have any problems). -This library has additional dependencies (namely, a YAML parser) which are not exposed by the -artifact you import. You can download _plugin versions_ of this library that bundle all its -dependencies. The artifacts of these versions can be found on -the [releases page](https://github.com/Exlll/ConfigLib/releases) where you can identify them by -their `-paper-`, `-waterfall-`, and `-velocity-` infix and `-all` suffix. Except for -the `-paper-` version, the other plugin versions currently do not add any additional features. -A benefit of these versions is that they make it easier for you to update this library if you have -written multiple plugins that use it. If you plan to use these versions, don't forget to add the -plugin as a dependency to the `plugin.yml` (for Paper and Waterfall) or to the dependencies array -(for Velocity) of your own plugin. - -Alternatively, if you don't want to use an extra plugin, you can shade the `-yaml` version with its -YAML parser yourself. +This library has additional dependencies (namely, a YAML parser) which are not +exposed by the artifact you import. You can download _plugin versions_ of this +library that bundle all its dependencies. The artifacts of these versions can be +found on the [releases page](https://github.com/Exlll/ConfigLib/releases) where +you can identify them by their `-paper-`, `-waterfall-`, and `-velocity-` infix +and `-all` suffix. Except for the `-paper-` version, the other plugin versions +currently do not add any additional features. A benefit of these versions is +that they make it easier for you to update this library if you have written +multiple plugins that use it. If you plan to use these versions, don't forget to +add the plugin as a dependency to the `plugin.yml` (for Paper and Waterfall) or +to the dependencies array (for Velocity) of your own plugin. + +Alternatively, if you don't want to use an extra plugin, you can shade +the `-yaml` version with its YAML parser yourself. ### Import examples -If you want serialization support for Bukkit classes like `ItemStack`, replace `configlib-yaml` -with `configlib-paper` (see [here](#support-for-bukkit-classes-like-itemstack)). +If you want serialization support for Bukkit classes like `ItemStack`, +replace `configlib-yaml` with `configlib-paper` +(see [here](#support-for-bukkit-classes-like-itemstack)).
@@ -888,7 +960,8 @@ dependencies { implementation("com.github.Exlll.ConfigLib:configlib-yaml:v4.2.0" Importing via GitHub requires authentication. Check -this [issue](https://github.com/Exlll/ConfigLib/issues/12) if you have any trouble with that. +this [issue](https://github.com/Exlll/ConfigLib/issues/12) if you have any +trouble with that. **Maven** @@ -923,13 +996,15 @@ dependencies { implementation("de.exlll:configlib-yaml:4.2.0") } ## Future work -This section contains ideas for upcoming features. If you want any of these to happen any time soon, -please [open an issue](https://github.com/Exlll/ConfigLib/issues/new) where we can discuss the -details. +This section contains ideas for upcoming features. If you want any of these to +happen any time soon, +please [open an issue](https://github.com/Exlll/ConfigLib/issues/new) where we +can discuss the details. - JSON, TOML, XML support - Post-load/Pre-save hooks - More features and control over updating/versioning -- More control over the ordering of fields, especially in parent/child class scenarios +- More control over the ordering of fields, especially in parent/child class + scenarios - Recursive definitions - Shadowing of fields diff --git a/configlib-core/src/main/java/de/exlll/configlib/FileConfigurationStore.java b/configlib-core/src/main/java/de/exlll/configlib/FileConfigurationStore.java index 7a55c74..3ec6f17 100644 --- a/configlib-core/src/main/java/de/exlll/configlib/FileConfigurationStore.java +++ b/configlib-core/src/main/java/de/exlll/configlib/FileConfigurationStore.java @@ -45,13 +45,14 @@ public interface FileConfigurationStore { * *
  • * 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 + * values taken from the configuration file, and immediately saved to reflect potential 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 + * @return a newly created configuration initialized with values taken from the configuration + * file or a default configuration * @throws ConfigurationException if the configuration cannot be deserialized * @throws NullPointerException if {@code configurationFile} is null * @throws RuntimeException if loading or saving the configuration throws an exception diff --git a/configlib-core/src/main/java/de/exlll/configlib/IOStreamConfigurationStore.java b/configlib-core/src/main/java/de/exlll/configlib/IOStreamConfigurationStore.java new file mode 100644 index 0000000..9ce7fa9 --- /dev/null +++ b/configlib-core/src/main/java/de/exlll/configlib/IOStreamConfigurationStore.java @@ -0,0 +1,38 @@ +package de.exlll.configlib; + +import java.io.InputStream; +import java.io.OutputStream; + +/** + * Instances of this class read and write configurations from input streams and to output streams, + * respectively. + *

    + * The details of how configurations are serialized and deserialized are defined by the + * implementations of this interface. + * + * @param the configuration type + */ +public interface IOStreamConfigurationStore { + /** + * Writes a configuration instance to the given output stream. + * + * @param configuration the configuration + * @param outputStream the output stream the configuration is written to + * @throws ConfigurationException if the configuration contains invalid values or + * cannot be serialized + * @throws NullPointerException if any argument is null + * @throws RuntimeException if writing the configuration throws an exception + */ + void write(T configuration, OutputStream outputStream); + + /** + * Reads a configuration from the given input stream. + * + * @param inputStream the input stream the configuration is read from + * @return a newly created configuration initialized with values read from {@code inputStream} + * @throws ConfigurationException if the configuration cannot be deserialized + * @throws NullPointerException if {@code inputStream} is null + * @throws RuntimeException if reading the input stream throws an exception + */ + T read(InputStream inputStream); +} diff --git a/configlib-core/src/testFixtures/java/de/exlll/configlib/TestUtils.java b/configlib-core/src/testFixtures/java/de/exlll/configlib/TestUtils.java index 77f7cad..8ed490f 100644 --- a/configlib-core/src/testFixtures/java/de/exlll/configlib/TestUtils.java +++ b/configlib-core/src/testFixtures/java/de/exlll/configlib/TestUtils.java @@ -15,8 +15,6 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import static java.util.stream.Collectors.joining; - public final class TestUtils { public static final PointSerializer POINT_SERIALIZER = new PointSerializer(); public static final PointIdentitySerializer POINT_IDENTITY_SERIALIZER = @@ -279,8 +277,8 @@ public static > boolean collectionOfArraysDeepEqual } public static String readFile(Path file) { - try (Stream lines = Files.lines(file)) { - return lines.collect(joining("\n")); + try { + return Files.readString(file); } catch (IOException e) { throw new RuntimeException(e); } @@ -312,4 +310,4 @@ public static String createPlatformSpecificFilePath(String path) { public static List createListOfPlatformSpecificFilePaths(String... paths) { return Stream.of(paths).map(TestUtils::createPlatformSpecificFilePath).toList(); } -} \ No newline at end of file +} diff --git a/configlib-yaml/src/main/java/de/exlll/configlib/YamlConfigurationStore.java b/configlib-yaml/src/main/java/de/exlll/configlib/YamlConfigurationStore.java index 73a31b9..9658613 100644 --- a/configlib-yaml/src/main/java/de/exlll/configlib/YamlConfigurationStore.java +++ b/configlib-yaml/src/main/java/de/exlll/configlib/YamlConfigurationStore.java @@ -10,23 +10,26 @@ import org.snakeyaml.engine.v2.nodes.Tag; import org.snakeyaml.engine.v2.representer.StandardRepresenter; -import java.io.BufferedWriter; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Arrays; -import java.util.List; import java.util.Map; -import java.util.Queue; import static de.exlll.configlib.Validator.requireNonNull; /** - * A configuration store that saves and loads configurations as YAML text files. + * A configuration store for YAML configurations. This class provides two pairs of methods: + * One pair for loading configurations from and saving them as YAML text files, and a second pair + * for reading configurations from input streams and writing them to output streams. * * @param the configuration type */ -public final class YamlConfigurationStore implements FileConfigurationStore { +public final class YamlConfigurationStore implements + FileConfigurationStore, + IOStreamConfigurationStore { + private static final Dump YAML_DUMPER = newYamlDumper(); private static final Load YAML_LOADER = newYamlLoader(); private final YamlConfigurationProperties properties; @@ -47,13 +50,24 @@ public YamlConfigurationStore(Class configurationType, YamlConfigurationPrope this.extractor = new CommentNodeExtractor(properties); } + + @Override + public void write(T configuration, OutputStream outputStream) { + requireNonNull(configuration, "configuration"); + requireNonNull(outputStream, "output stream"); + var extractedCommentNodes = extractor.extractCommentNodes(configuration); + var yamlFileWriter = new YamlWriter(outputStream, properties); + var dumpedYaml = tryDump(configuration); + yamlFileWriter.writeYaml(dumpedYaml, extractedCommentNodes); + } + @Override public void save(T configuration, Path configurationFile) { requireNonNull(configuration, "configuration"); requireNonNull(configurationFile, "configuration file"); tryCreateParentDirectories(configurationFile); var extractedCommentNodes = extractor.extractCommentNodes(configuration); - var yamlFileWriter = new YamlFileWriter(configurationFile, properties); + var yamlFileWriter = new YamlWriter(configurationFile, properties); var dumpedYaml = tryDump(configuration); yamlFileWriter.writeYaml(dumpedYaml, extractedCommentNodes); } @@ -80,12 +94,41 @@ private String tryDump(T configuration) { } } + @Override + public T read(InputStream inputStream) { + requireNonNull(inputStream, "input stream"); + try { + var yaml = YAML_LOADER.loadFromInputStream(inputStream); + var conf = requireYamlMapForRead(yaml); + return serializer.deserialize(conf); + } catch (YamlEngineException e) { + String msg = "The input stream does not contain valid YAML."; + throw new ConfigurationException(msg, e); + } + } + + private Map requireYamlMapForRead(Object yaml) { + if (yaml == null) { + String msg = "The input stream is empty or only contains null."; + throw new ConfigurationException(msg); + } + + if (!(yaml instanceof Map map)) { + String msg = "The contents of the input stream do not represent a configuration. " + + "A valid configuration contains a YAML map but instead a " + + "'" + yaml.getClass() + "' was found."; + throw new ConfigurationException(msg); + } + + return map; + } + @Override public T load(Path configurationFile) { requireNonNull(configurationFile, "configuration file"); try (var reader = Files.newBufferedReader(configurationFile)) { var yaml = YAML_LOADER.loadFromReader(reader); - var conf = requireYamlMap(yaml, configurationFile); + var conf = requireYamlMapForLoad(yaml, configurationFile); return serializer.deserialize(conf); } catch (YamlEngineException e) { String msg = "The configuration file at %s does not contain valid YAML."; @@ -95,20 +138,20 @@ public T load(Path configurationFile) { } } - private Map requireYamlMap(Object yaml, Path configurationFile) { + private Map requireYamlMapForLoad(Object yaml, Path configurationFile) { if (yaml == null) { String msg = "The configuration file at %s is empty or only contains null."; throw new ConfigurationException(msg.formatted(configurationFile)); } - if (!(yaml instanceof Map)) { + if (!(yaml instanceof Map map)) { String msg = "The contents of the YAML file at %s do not represent a configuration. " + "A valid configuration file contains a YAML map but instead a " + "'" + yaml.getClass() + "' was found."; throw new ConfigurationException(msg.formatted(configurationFile)); } - return (Map) yaml; + return map; } @Override @@ -137,134 +180,6 @@ static Load newYamlLoader() { return new Load(settings); } - /** - * A writer that writes YAML to a file. - */ - static final class YamlFileWriter { - private final Path configurationFile; - private final YamlConfigurationProperties properties; - private BufferedWriter writer; - - YamlFileWriter(Path configurationFile, YamlConfigurationProperties properties) { - this.configurationFile = requireNonNull(configurationFile, "configuration file"); - this.properties = requireNonNull(properties, "configuration properties"); - } - - public void writeYaml(String yaml, Queue nodes) { - try (BufferedWriter writer = Files.newBufferedWriter(configurationFile)) { - this.writer = writer; - writeHeader(); - writeContent(yaml, nodes); - writeFooter(); - } catch (IOException e) { - throw new RuntimeException(e); - } finally { - this.writer = null; - } - } - - private void writeHeader() throws IOException { - if (properties.getHeader() != null) { - writeAsComment(properties.getHeader()); - writer.newLine(); - } - } - - private void writeFooter() throws IOException { - if (properties.getFooter() != null) { - writer.newLine(); - writeAsComment(properties.getFooter()); - } - } - - private void writeAsComment(String comment) throws IOException { - String[] lines = comment.split("\n"); - writeComments(Arrays.asList(lines), 0); - } - - private void writeComments(List comments, int indentLevel) throws IOException { - String indent = " ".repeat(indentLevel); - for (String comment : comments) { - if (comment.isEmpty()) { - writer.newLine(); - continue; - } - String line = indent + "# " + comment; - writeLine(line); - } - } - - private void writeLine(String line) throws IOException { - writer.write(line); - writer.newLine(); - } - - private void writeContent(String yaml, Queue nodes) throws IOException { - if (nodes.isEmpty()) { - writer.write(yaml); - } else { - writeCommentedYaml(yaml, nodes); - } - } - - private void writeCommentedYaml(String yaml, Queue nodes) - throws IOException { - /* - * The following algorithm is necessary since no Java YAML library seems - * 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 elementNames lists which come from the nodes in - * the 'nodes' queue. The 'nodes' queue contains nodes in the order in - * 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; - - for (final String line : yaml.split("\n")) { - if (node == null) { - writeLine(line); - continue; - } - - final var elementNames = node.elementNames(); - final var indent = " ".repeat(currentIndentLevel); - - final var lineStart = indent + elementNames.get(currentIndentLevel) + ":"; - if (!line.startsWith(lineStart)) { - writeLine(line); - continue; - } - - final var commentIndentLevel = elementNames.size() - 1; - if (currentIndentLevel++ == commentIndentLevel) { - writeComments(node.comments(), commentIndentLevel); - if ((node = nodes.poll()) != null) { - currentIndentLevel = lengthCommonPrefix(node.elementNames(), elementNames); - } - } - - writeLine(line); - } - } - - static int lengthCommonPrefix(List l1, List l2) { - final int maxLen = Math.min(l1.size(), l2.size()); - int result = 0; - for (int i = 0; i < maxLen; i++) { - String s1 = l1.get(i); - String s2 = l2.get(i); - if (s1.equals(s2)) - result++; - else return result; - } - return result; - } - } - /** * A custom representer that prevents aliasing. */ diff --git a/configlib-yaml/src/main/java/de/exlll/configlib/YamlConfigurations.java b/configlib-yaml/src/main/java/de/exlll/configlib/YamlConfigurations.java index e84030c..916f739 100644 --- a/configlib-yaml/src/main/java/de/exlll/configlib/YamlConfigurations.java +++ b/configlib-yaml/src/main/java/de/exlll/configlib/YamlConfigurations.java @@ -1,10 +1,13 @@ package de.exlll.configlib; +import java.io.InputStream; +import java.io.OutputStream; import java.nio.file.Path; import java.util.function.Consumer; /** - * This class contains convenience methods for loading, saving, and updating configurations. + * This class contains convenience methods for reading, writing, loading, saving, + * and updating configurations. */ public final class YamlConfigurations { private YamlConfigurations() {} @@ -78,6 +81,72 @@ public static T load( return store.load(configurationFile); } + /** + * Reads a configuration of the given type from the given input stream using a + * {@code YamlConfigurationProperties} object with default values. + * + * @param inputStream the input stream the configuration is read from + * @param configurationType the type of configuration + * @param the configuration type + * @return a newly created configuration initialized with values read from {@code inputStream} + * @throws ConfigurationException if the configuration cannot be deserialized + * @throws NullPointerException if any parameter is null + * @throws RuntimeException if reading the configuration throws an exception + * @see YamlConfigurationStore#read(InputStream) + */ + public static T read(InputStream inputStream, Class configurationType) { + final var properties = YamlConfigurationProperties.newBuilder().build(); + return read(inputStream, configurationType, properties); + } + + /** + * Reads a configuration of the given type from the given input stream using a + * {@code YamlConfigurationProperties} object that is built by a builder. The builder is + * initialized with default values and can be configured by the {@code propertiesConfigurer}. + * + * @param inputStream the input stream the configuration is read from + * @param configurationType the type of configuration + * @param propertiesConfigurer the consumer used to configure the builder + * @param the configuration type + * @return a newly created configuration initialized with values read from {@code inputStream} + * @throws ConfigurationException if the configuration cannot be deserialized + * @throws NullPointerException if any parameter is null + * @throws RuntimeException if reading the configuration throws an exception + * @see YamlConfigurationStore#read(InputStream) + */ + public static T read( + InputStream inputStream, + Class configurationType, + Consumer> propertiesConfigurer + ) { + final var builder = YamlConfigurationProperties.newBuilder(); + propertiesConfigurer.accept(builder); + return read(inputStream, configurationType, builder.build()); + } + + /** + * Reads a configuration of the given type from the given input stream using the given + * {@code YamlConfigurationProperties} object. + * + * @param inputStream the input stream the configuration is read from + * @param configurationType the type of configuration + * @param properties the configuration properties + * @param the configuration type + * @return a newly created configuration initialized with values read from {@code inputStream} + * @throws ConfigurationException if the configuration cannot be deserialized + * @throws NullPointerException if any parameter is null + * @throws RuntimeException if reading the configuration throws an exception + * @see YamlConfigurationStore#read(InputStream) + */ + public static T read( + InputStream inputStream, + Class configurationType, + YamlConfigurationProperties properties + ) { + final var store = new YamlConfigurationStore<>(configurationType, properties); + return store.read(inputStream); + } + /** * Updates a YAML configuration file with a configuration of the given type using a * {@code YamlConfigurationProperties} object with default values. @@ -224,4 +293,79 @@ public static void save( final var store = new YamlConfigurationStore<>(configurationType, properties); store.save(configuration, configurationFile); } + + /** + * Writes a configuration instance to the given output stream using a + * {@code YamlConfigurationProperties} object with default values. + * + * @param configuration the configuration that is saved + * @param configurationType the type of configuration + * @param outputStream the output stream the configuration is written to + * @param the configuration type + * @throws ConfigurationException if the configuration contains invalid values or + * cannot be serialized + * @throws NullPointerException if any argument is null + * @throws RuntimeException if writing the configuration throws an exception + * @see YamlConfigurationStore#write(Object, OutputStream) + */ + public static void write( + OutputStream outputStream, + Class configurationType, + T configuration + ) { + final var properties = YamlConfigurationProperties.newBuilder().build(); + write(outputStream, configurationType, configuration, properties); + } + + /** + * Writes a configuration instance to the given output stream using a + * {@code YamlConfigurationProperties} object that is built by a builder. The builder is + * initialized with default values and can be configured by the {@code propertiesConfigurer}. + * + * @param configuration the configuration that is saved + * @param configurationType the type of configuration + * @param outputStream the output stream the configuration is written to + * @param propertiesConfigurer the consumer used to configure the builder + * @param the configuration type + * @throws ConfigurationException if the configuration contains invalid values or + * cannot be serialized + * @throws NullPointerException if any argument is null + * @throws RuntimeException if writing the configuration throws an exception + * @see YamlConfigurationStore#write(Object, OutputStream) + */ + public static void write( + OutputStream outputStream, + Class configurationType, + T configuration, + Consumer> propertiesConfigurer + ) { + final var builder = YamlConfigurationProperties.newBuilder(); + propertiesConfigurer.accept(builder); + write(outputStream, configurationType, configuration, builder.build()); + } + + /** + * Writes a configuration instance to the given output stream using the given + * {@code YamlConfigurationProperties} object. + * + * @param configuration the configuration that is saved + * @param configurationType the type of configuration + * @param outputStream the output stream the configuration is written to + * @param properties the configuration properties + * @param the configuration type + * @throws ConfigurationException if the configuration contains invalid values or + * cannot be serialized + * @throws NullPointerException if any argument is null + * @throws RuntimeException if writing the configuration throws an exception + * @see YamlConfigurationStore#write(Object, OutputStream) + */ + public static void write( + OutputStream outputStream, + Class configurationType, + T configuration, + YamlConfigurationProperties properties + ) { + final var store = new YamlConfigurationStore<>(configurationType, properties); + store.write(configuration, outputStream); + } } diff --git a/configlib-yaml/src/main/java/de/exlll/configlib/YamlWriter.java b/configlib-yaml/src/main/java/de/exlll/configlib/YamlWriter.java new file mode 100644 index 0000000..7addc7b --- /dev/null +++ b/configlib-yaml/src/main/java/de/exlll/configlib/YamlWriter.java @@ -0,0 +1,151 @@ +package de.exlll.configlib; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.Queue; + +import static de.exlll.configlib.Validator.requireNonNull; + +/** + * A writer that writes YAML to a file. + */ +final class YamlWriter { + private final OutputStream outputStream; + private final YamlConfigurationProperties properties; + private BufferedWriter writer; + + YamlWriter(OutputStream outputStream, YamlConfigurationProperties properties) { + this.outputStream = requireNonNull(outputStream, "output stream"); + this.properties = requireNonNull(properties, "configuration properties"); + } + + YamlWriter(Path configurationFile, YamlConfigurationProperties properties) { + requireNonNull(configurationFile, "configuration file"); + try { + this.outputStream = Files.newOutputStream(configurationFile); + this.properties = properties; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public void writeYaml(String yaml, Queue nodes) { + try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream))) { + this.writer = writer; + writeHeader(); + writeContent(yaml, nodes); + writeFooter(); + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + this.writer = null; + } + } + + private void writeHeader() throws IOException { + if (properties.getHeader() != null) { + writeAsComment(properties.getHeader()); + writer.newLine(); + } + } + + private void writeFooter() throws IOException { + if (properties.getFooter() != null) { + writer.newLine(); + writeAsComment(properties.getFooter()); + } + } + + private void writeAsComment(String comment) throws IOException { + String[] lines = comment.split("\n"); + writeComments(Arrays.asList(lines), 0); + } + + private void writeComments(List comments, int indentLevel) throws IOException { + String indent = " ".repeat(indentLevel); + for (String comment : comments) { + if (comment.isEmpty()) { + writer.newLine(); + continue; + } + String line = indent + "# " + comment; + writeLine(line); + } + } + + private void writeLine(String line) throws IOException { + writer.write(line); + writer.newLine(); + } + + private void writeContent(String yaml, Queue nodes) throws IOException { + if (nodes.isEmpty()) { + writer.write(yaml); + } else { + writeCommentedYaml(yaml, nodes); + } + } + + private void writeCommentedYaml(String yaml, Queue nodes) + throws IOException { + /* + * The following algorithm is necessary since no Java YAML library seems + * 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 elementNames lists which come from the nodes in + * the 'nodes' queue. The 'nodes' queue contains nodes in the order in + * 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; + + for (final String line : yaml.split("\n")) { + if (node == null) { + writeLine(line); + continue; + } + + final var elementNames = node.elementNames(); + final var indent = " ".repeat(currentIndentLevel); + + final var lineStart = indent + elementNames.get(currentIndentLevel) + ":"; + if (!line.startsWith(lineStart)) { + writeLine(line); + continue; + } + + final var commentIndentLevel = elementNames.size() - 1; + if (currentIndentLevel++ == commentIndentLevel) { + writeComments(node.comments(), commentIndentLevel); + if ((node = nodes.poll()) != null) { + currentIndentLevel = lengthCommonPrefix(node.elementNames(), elementNames); + } + } + + writeLine(line); + } + } + + static int lengthCommonPrefix(List l1, List l2) { + final int maxLen = Math.min(l1.size(), l2.size()); + int result = 0; + for (int i = 0; i < maxLen; i++) { + String s1 = l1.get(i); + String s2 = l2.get(i); + if (s1.equals(s2)) + result++; + else return result; + } + return result; + } +} diff --git a/configlib-yaml/src/test/java/de/exlll/configlib/YamlConfigurationStoreTest.java b/configlib-yaml/src/test/java/de/exlll/configlib/YamlConfigurationStoreTest.java index 1c374b3..14131af 100644 --- a/configlib-yaml/src/test/java/de/exlll/configlib/YamlConfigurationStoreTest.java +++ b/configlib-yaml/src/test/java/de/exlll/configlib/YamlConfigurationStoreTest.java @@ -6,7 +6,10 @@ import org.junit.jupiter.api.Test; import java.awt.Point; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; import java.nio.file.FileSystem; import java.nio.file.Files; import java.nio.file.Path; @@ -18,9 +21,9 @@ class YamlConfigurationStoreTest { private final FileSystem fs = Jimfs.newFileSystem(); private final String yamlFilePath = createPlatformSpecificFilePath("/tmp/config.yml"); - private final Path yamlFile = fs.getPath(yamlFilePath); - private final String abcFilePath = createPlatformSpecificFilePath("/a/b/c.yml"); + private final Path yamlFile = fs.getPath(yamlFilePath); + private final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); @BeforeEach void setUp() throws IOException { @@ -54,6 +57,21 @@ void saveRequiresNonNullArguments() { ); } + @Test + void writeRequiresNonNullArguments() { + YamlConfigurationStore store = newDefaultStore(A.class); + + assertThrowsNullPointerException( + () -> store.write(null, new ByteArrayOutputStream()), + "configuration" + ); + + assertThrowsNullPointerException( + () -> store.write(new A(), null), + "output stream" + ); + } + @Test void loadRequiresNonNullArguments() { YamlConfigurationStore store = newDefaultStore(A.class); @@ -64,6 +82,16 @@ void loadRequiresNonNullArguments() { ); } + @Test + void readRequiresNonNullArguments() { + YamlConfigurationStore store = newDefaultStore(A.class); + + assertThrowsNullPointerException( + () -> store.read(null), + "input stream" + ); + } + @Test void updateRequiresNonNullArguments() { YamlConfigurationStore store = newDefaultStore(A.class); @@ -75,15 +103,18 @@ void updateRequiresNonNullArguments() { } @Test - void save() { + void saveAndWrite() { YamlConfigurationProperties properties = YamlConfigurationProperties.newBuilder() .header("The\nHeader") .footer("The\nFooter") .outputNulls(true) .setNameFormatter(String::toUpperCase) .build(); + YamlConfigurationStore store = new YamlConfigurationStore<>(A.class, properties); + store.save(new A(), yamlFile); + store.write(new A(), outputStream); String expected = """ @@ -95,13 +126,15 @@ void save() { I: null # The - # Footer\ + # Footer """; + assertEquals(expected, readFile(yamlFile)); + assertEquals(expected, outputStream.toString()); } @Test - void saveRecord() { + void saveAndWriteRecord() { record R(String s, @Comment("A comment") Integer i) {} YamlConfigurationProperties properties = YamlConfigurationProperties.newBuilder() .header("The\nHeader") @@ -110,7 +143,9 @@ record R(String s, @Comment("A comment") Integer i) {} .setNameFormatter(String::toUpperCase) .build(); YamlConfigurationStore store = new YamlConfigurationStore<>(R.class, properties); + store.save(new R("S1", null), yamlFile); + store.write(new R("S1", null), outputStream); String expected = """ @@ -122,9 +157,11 @@ record R(String s, @Comment("A comment") Integer i) {} I: null # The - # Footer\ + # Footer """; + assertEquals(expected, readFile(yamlFile)); + assertEquals(expected, outputStream.toString()); } @Configuration @@ -135,36 +172,40 @@ static final class B { } @Test - void load() throws IOException { + void loadAndRead() throws IOException { YamlConfigurationProperties properties = YamlConfigurationProperties.newBuilder() .inputNulls(true) .setNameFormatter(String::toUpperCase) .build(); YamlConfigurationStore store = new YamlConfigurationStore<>(B.class, properties); - Files.writeString( - yamlFile, - """ - # The - # Header - - S: S2 - t: T2 - I: null - - # The - # Footer\ - """ - ); - - B config = store.load(yamlFile); - assertEquals("S2", config.s); - assertEquals("T1", config.t); - assertNull(config.i); + String actual = """ + # The + # Header + + S: S2 + t: T2 + I: null + + # The + # Footer + """; + Files.writeString(yamlFile, actual); + outputStream.writeBytes(actual.getBytes()); + + B config1 = store.load(yamlFile); + assertEquals("S2", config1.s); + assertEquals("T1", config1.t); + assertNull(config1.i); + + B config2 = store.read(inputFromOutput()); + assertEquals("S2", config2.s); + assertEquals("T1", config2.t); + assertNull(config2.i); } @Test - void loadRecord() throws IOException { + void loadAndReadRecord() throws IOException { record R(String s, String t, Integer i) {} YamlConfigurationProperties properties = YamlConfigurationProperties.newBuilder() .inputNulls(true) @@ -172,25 +213,29 @@ record R(String s, String t, Integer i) {} .build(); YamlConfigurationStore store = new YamlConfigurationStore<>(R.class, properties); - Files.writeString( - yamlFile, - """ - # The - # Header - - S: S2 - t: T2 - I: null - - # The - # Footer\ - """ - ); - - R config = store.load(yamlFile); - assertEquals("S2", config.s); - assertNull(config.t); - assertNull(config.i); + String actual = """ + # The + # Header + + S: S2 + t: T2 + I: null + + # The + # Footer + """; + Files.writeString(yamlFile, actual); + outputStream.writeBytes(actual.getBytes()); + + R config1 = store.load(yamlFile); + assertEquals("S2", config1.s); + assertNull(config1.t); + assertNull(config1.i); + + R config2 = store.read(inputFromOutput()); + assertEquals("S2", config2.s); + assertNull(config2.t); + assertNull(config2.i); } @Configuration @@ -199,46 +244,63 @@ static final class C { } @Test - void loadInvalidYaml() throws IOException { + void loadAndReadInvalidYaml() throws IOException { YamlConfigurationStore store = newDefaultStore(C.class); - Files.writeString( - yamlFile, - """ - - - - - - a - a - """ - ); + String actual = """ + - - - - - a + a + """; + + Files.writeString(yamlFile, actual); + outputStream.writeBytes(actual.getBytes()); assertThrowsConfigurationException( () -> store.load(yamlFile), String.format("The configuration file at %s does not contain valid YAML.", yamlFilePath) ); + assertThrowsConfigurationException( + () -> store.read(inputFromOutput()), + "The input stream does not contain valid YAML." + ); } @Test - void loadEmptyYaml() throws IOException { + void loadAndReadEmptyYaml() throws IOException { YamlConfigurationStore store = newDefaultStore(C.class); Files.writeString(yamlFile, "null"); + outputStream.writeBytes("null".getBytes()); assertThrowsConfigurationException( () -> store.load(yamlFile), - String.format("The configuration file at %s is empty or only contains null.", yamlFilePath) + String.format("The configuration file at %s is empty or only contains null.", yamlFilePath) + ); + assertThrowsConfigurationException( + () -> store.read(inputFromOutput()), + "The input stream is empty or only contains null." ); } @Test - void loadNonMapYaml() throws IOException { + void loadAndReadNonMapYaml() throws IOException { YamlConfigurationStore store = newDefaultStore(C.class); Files.writeString(yamlFile, "a"); + outputStream.writeBytes("a".getBytes()); assertThrowsConfigurationException( () -> store.load(yamlFile), - String.format("The contents of the YAML file at %s do not represent a " + - "configuration. A valid configuration file contains a YAML map but instead a " + - "'class java.lang.String' was found.", yamlFilePath) + String.format( + "The contents of the YAML file at %s do not represent a " + + "configuration. A valid configuration file contains a YAML map but instead a " + + "'class java.lang.String' was found.", yamlFilePath) + ); + assertThrowsConfigurationException( + () -> store.read(inputFromOutput()), + "The contents of the input stream do not represent a configuration. " + + "A valid configuration contains a YAML map but instead a " + + "'class java.lang.String' was found." ); } @@ -248,17 +310,16 @@ static final class D { } @Test - void saveConfigurationWithInvalidTargetType() { + void saveAndWriteConfigurationWithInvalidTargetType() { YamlConfigurationProperties properties = YamlConfigurationProperties.newBuilder() .addSerializer(Point.class, POINT_IDENTITY_SERIALIZER) .build(); YamlConfigurationStore store = new YamlConfigurationStore<>(D.class, properties); - assertThrowsConfigurationException( - () -> store.save(new D(), yamlFile), - "The given configuration could not be converted into YAML. \n" + - "Do all custom serializers produce valid target types?" - ); + String exceptionMessage = "The given configuration could not be converted into YAML. \n" + + "Do all custom serializers produce valid target types?"; + assertThrowsConfigurationException(() -> store.save(new D(), yamlFile), exceptionMessage); + assertThrowsConfigurationException(() -> store.write(new D(), outputStream), exceptionMessage); } @Test @@ -305,7 +366,7 @@ void updateCreatesConfigurationFileIfItDoesNotExist() { assertFalse(Files.exists(yamlFile)); E config = store.update(yamlFile); - assertEquals("i: 10\nj: 11", readFile(yamlFile)); + assertEquals("i: 10\nj: 11\n", readFile(yamlFile)); assertEquals(10, config.i); assertEquals(11, config.j); } @@ -324,7 +385,7 @@ record R(int i, char c, String s) {} """ i: 0 c: "\\0" - s: null\ + s: null """, readFile(yamlFile) ); @@ -346,7 +407,7 @@ record R(int i, char c, String s) { """ i: 10 c: c - s: s\ + s: s """, readFile(yamlFile) ); @@ -384,7 +445,7 @@ void updateUpdatesFile() throws IOException { E config = store.update(yamlFile); assertEquals(20, config.i); assertEquals(11, config.j); - assertEquals("i: 20\nj: 11", readFile(yamlFile)); + assertEquals("i: 20\nj: 11\n", readFile(yamlFile)); } @Test @@ -396,11 +457,15 @@ record R(int i, int j) {} R config = store.update(yamlFile); assertEquals(20, config.i); assertEquals(0, config.j); - assertEquals("i: 20\nj: 0", readFile(yamlFile)); + assertEquals("i: 20\nj: 0\n", readFile(yamlFile)); } private static YamlConfigurationStore newDefaultStore(Class configType) { YamlConfigurationProperties properties = YamlConfigurationProperties.newBuilder().build(); return new YamlConfigurationStore<>(configType, properties); } + + private InputStream inputFromOutput() { + return new ByteArrayInputStream(outputStream.toByteArray()); + } } diff --git a/configlib-yaml/src/test/java/de/exlll/configlib/YamlConfigurationsTest.java b/configlib-yaml/src/test/java/de/exlll/configlib/YamlConfigurationsTest.java index 9dd81ce..bd20bed 100644 --- a/configlib-yaml/src/test/java/de/exlll/configlib/YamlConfigurationsTest.java +++ b/configlib-yaml/src/test/java/de/exlll/configlib/YamlConfigurationsTest.java @@ -5,7 +5,10 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; import java.nio.file.FileSystem; import java.nio.file.Files; import java.nio.file.Path; @@ -17,6 +20,7 @@ class YamlConfigurationsTest { private static final FieldFilter includeI = field -> field.getName().equals("i"); private final FileSystem fs = Jimfs.newFileSystem(); private final Path yamlFile = fs.getPath(createPlatformSpecificFilePath("/tmp/config.yml")); + private final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); @BeforeEach void setUp() throws IOException { @@ -32,13 +36,6 @@ void tearDown() throws IOException { private static final class Config { int i = 10; int j = 11; - - public Config() {} - - public Config(int i, int j) { - this.i = i; - this.j = j; - } } @Test @@ -46,13 +43,28 @@ void saveYamlConfiguration1() { Config configuration = new Config(); YamlConfigurations.save(yamlFile, Config.class, configuration); - assertEquals("i: 10\nj: 11", TestUtils.readFile(yamlFile)); + assertEquals("i: 10\nj: 11\n", TestUtils.readFile(yamlFile)); configuration.i = 20; YamlConfigurations.save(yamlFile, Config.class, configuration); - assertEquals("i: 20\nj: 11", TestUtils.readFile(yamlFile)); + assertEquals("i: 20\nj: 11\n", TestUtils.readFile(yamlFile)); } + @Test + void writeYamlConfiguration1() { + Config configuration = new Config(); + + YamlConfigurations.write(outputStream, Config.class, configuration); + assertEquals("i: 10\nj: 11\n", outputStream.toString()); + + outputStream.reset(); + + configuration.i = 20; + YamlConfigurations.write(outputStream, Config.class, configuration); + assertEquals("i: 20\nj: 11\n", outputStream.toString()); + } + + @Test void saveYamlConfiguration2() { Config configuration = new Config(); @@ -61,7 +73,18 @@ void saveYamlConfiguration2() { yamlFile, Config.class, configuration, builder -> builder.setFieldFilter(includeI) ); - assertEquals("i: 10", TestUtils.readFile(yamlFile)); + assertEquals("i: 10\n", TestUtils.readFile(yamlFile)); + } + + @Test + void writeYamlConfiguration2() { + Config configuration = new Config(); + + YamlConfigurations.write( + outputStream, Config.class, configuration, + builder -> builder.setFieldFilter(includeI) + ); + assertEquals("i: 10\n", outputStream.toString()); } @Test @@ -72,23 +95,48 @@ void saveYamlConfiguration3() { yamlFile, Config.class, configuration, YamlConfigurationProperties.newBuilder().setFieldFilter(includeI).build() ); - assertEquals("i: 10", TestUtils.readFile(yamlFile)); + assertEquals("i: 10\n", TestUtils.readFile(yamlFile)); + } + + + @Test + void writeYamlConfiguration3() { + Config configuration = new Config(); + + YamlConfigurations.write( + outputStream, Config.class, configuration, + YamlConfigurationProperties.newBuilder().setFieldFilter(includeI).build() + ); + assertEquals("i: 10\n", outputStream.toString()); } @Test void loadYamlConfiguration1() { - writeString("i: 20\nk: 30"); + writeStringToFile("i: 20\nk: 30"); Config config = YamlConfigurations.load(yamlFile, Config.class); assertConfigEquals(config, 20, 11); - writeString("i: 20\nj: 30"); + writeStringToFile("i: 20\nj: 30"); config = YamlConfigurations.load(yamlFile, Config.class); assertConfigEquals(config, 20, 30); } + @Test + void readYamlConfiguration1() { + writeStringToStream("i: 20\nk: 30"); + Config config = YamlConfigurations.read(inputFromOutput(), Config.class); + assertConfigEquals(config, 20, 11); + + outputStream.reset(); + + writeStringToStream("i: 20\nj: 30"); + config = YamlConfigurations.read(inputFromOutput(), Config.class); + assertConfigEquals(config, 20, 30); + } + @Test void loadYamlConfiguration2() { - writeString("i: 20\nj: 30"); + writeStringToFile("i: 20\nj: 30"); Config config = YamlConfigurations.load( yamlFile, Config.class, builder -> builder.setFieldFilter(includeI) @@ -96,9 +144,19 @@ void loadYamlConfiguration2() { assertConfigEquals(config, 20, 11); } + @Test + void readYamlConfiguration2() { + writeStringToStream("i: 20\nj: 30"); + Config config = YamlConfigurations.read( + inputFromOutput(), Config.class, + builder -> builder.setFieldFilter(includeI) + ); + assertConfigEquals(config, 20, 11); + } + @Test void loadYamlConfiguration3() { - writeString("i: 20\nj: 30"); + writeStringToFile("i: 20\nj: 30"); Config config = YamlConfigurations.load( yamlFile, Config.class, @@ -108,16 +166,28 @@ void loadYamlConfiguration3() { assertConfigEquals(config, 20, 11); } + @Test + void readYamlConfiguration3() { + writeStringToStream("i: 20\nj: 30"); + + Config config = YamlConfigurations.read( + inputFromOutput(), Config.class, + YamlConfigurationProperties.newBuilder().setFieldFilter(includeI).build() + ); + + assertConfigEquals(config, 20, 11); + } + @Test void updateYamlConfiguration1() { Config config = YamlConfigurations.update(yamlFile, Config.class); assertConfigEquals(config, 10, 11); - assertEquals("i: 10\nj: 11", TestUtils.readFile(yamlFile)); + assertEquals("i: 10\nj: 11\n", TestUtils.readFile(yamlFile)); - writeString("i: 20\nk: 30"); + writeStringToFile("i: 20\nk: 30"); config = YamlConfigurations.update(yamlFile, Config.class); assertConfigEquals(config, 20, 11); - assertEquals("i: 20\nj: 11", TestUtils.readFile(yamlFile)); + assertEquals("i: 20\nj: 11\n", TestUtils.readFile(yamlFile)); } @Test @@ -127,7 +197,7 @@ void updateYamlConfiguration2() { builder -> builder.setFieldFilter(includeI) ); assertConfigEquals(config, 10, 11); - assertEquals("i: 10", TestUtils.readFile(yamlFile)); + assertEquals("i: 10\n", TestUtils.readFile(yamlFile)); } @Test @@ -137,7 +207,7 @@ void updateYamlConfiguration3() { YamlConfigurationProperties.newBuilder().setFieldFilter(includeI).build() ); assertConfigEquals(config, 10, 11); - assertEquals("i: 10", TestUtils.readFile(yamlFile)); + assertEquals("i: 10\n", TestUtils.readFile(yamlFile)); } private static void assertConfigEquals(Config config, int i, int j) { @@ -145,11 +215,19 @@ private static void assertConfigEquals(Config config, int i, int j) { assertEquals(j, config.j); } - private void writeString(String string) { + private void writeStringToFile(String string) { try { Files.writeString(yamlFile, string); } catch (IOException e) { throw new RuntimeException(e); } } -} \ No newline at end of file + + private void writeStringToStream(String string) { + outputStream.writeBytes(string.getBytes()); + } + + private InputStream inputFromOutput() { + return new ByteArrayInputStream(outputStream.toByteArray()); + } +} diff --git a/configlib-yaml/src/test/java/de/exlll/configlib/YamlFileWriterTest.java b/configlib-yaml/src/test/java/de/exlll/configlib/YamlFileWriterTest.java deleted file mode 100644 index 3883905..0000000 --- a/configlib-yaml/src/test/java/de/exlll/configlib/YamlFileWriterTest.java +++ /dev/null @@ -1,505 +0,0 @@ -package de.exlll.configlib; - -import com.google.common.jimfs.Jimfs; -import de.exlll.configlib.YamlConfigurationStore.YamlFileWriter; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.snakeyaml.engine.v2.api.Dump; - -import java.io.IOException; -import java.nio.file.FileSystem; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.List; -import java.util.Map; -import java.util.Queue; -import java.util.function.Consumer; - -import static de.exlll.configlib.TestUtils.createPlatformSpecificFilePath; -import static org.junit.jupiter.api.Assertions.assertEquals; - -@SuppressWarnings("unused") -class YamlFileWriterTest { - private final FileSystem fs = Jimfs.newFileSystem(); - private final Path yamlFile = fs.getPath(createPlatformSpecificFilePath("/tmp/config.yml")); - - @BeforeEach - void setUp() throws IOException { - Files.createDirectories(yamlFile.getParent()); - } - - @AfterEach - void tearDown() throws IOException { - fs.close(); - } - - @Configuration - static final class A { - String s = ""; - } - - @Test - void writeYamlWithNoComments() { - writeConfig(A.class); - assertFileContentEquals("s: ''"); - } - - @Test - void writeYamlWithHeaderAndFooter() { - writeConfig( - A.class, - builder -> builder - .header("This is a \n\n \nheader.") - .footer("That is a\n\n \nfooter.") - ); - assertFileContentEquals( - """ - # This is a\s - - # \s - # header. - - s: '' - - # That is a - - # \s - # footer.\ - """ - ); - } - - @Configuration - static final class B { - @Comment("Hello") - String s = "s"; - } - - @Test - void writeYamlSingleComment() { - writeConfig(B.class); - assertFileContentEquals( - """ - # Hello - s: s\ - """ - ); - } - - @Configuration - static final class C { - @Comment({"Hello", "World"}) - Map mapStringInteger = Map.of("1", 2); - @Comment({"world", "hello"}) - Map mapIntegerString = Map.of(2, "1"); - } - - @Test - void writeYamlMultipleComments() { - writeConfig(C.class); - assertFileContentEquals( - """ - # Hello - # World - mapStringInteger: - '1': 2 - # world - # hello - mapIntegerString: - 2: '1'\ - """ - ); - } - - @Configuration - static final class D { - @Comment({"Hello", "", " ", "World"}) - String s1 = "s1"; - @Comment({"", "", " ", "How are ", "you?", ""}) - String s2 = "s2"; - } - - @Test - void writeYamlEmptyComments() { - writeConfig(D.class); - assertFileContentEquals( - """ - # Hello - - # \s - # World - s1: s1 - - - # \s - # How are\s - # you? - - s2: s2\ - """ - ); - } - - @Configuration - static final class E1 { - @Comment("m") - Map> m = Map.of("c", Map.of("i", 1)); - @Comment("e2") - E2 e2 = new E2(); - } - - @Configuration - static final class E2 { - Map m = Map.of("i", 1); - @Comment("e3") - E3 e3 = new E3(); - @Comment("j") - int j = 10; - } - - @Configuration - static final class E3 { - @Comment("i") - int i = 1; - } - - @Test - void writeYamlNestedComments1() { - writeConfig(E1.class); - assertFileContentEquals( - """ - # m - m: - c: - i: 1 - # e2 - e2: - m: - i: 1 - # e3 - e3: - # i - i: 1 - # j - j: 10\ - """ - ); - } - - @Configuration - static final class F1 { - Map m1 = Map.of("i", 1); - F2 f2 = new F2(); - @Comment("f1.m2") - Map m2 = Map.of("i", 1); - } - - @Configuration - static final class F2 { - @Comment("f2.i") - int i; - } - - @Test - void writeYamlNestedComments2() { - writeConfig(F1.class); - assertFileContentEquals( - """ - m1: - i: 1 - f2: - # f2.i - i: 0 - # f1.m2 - m2: - i: 1\ - """ - ); - } - - @Configuration - static final class G1 { - @Comment("g1.g2") - G2 g2 = new G2(); - } - - @Configuration - static final class G2 { - G3 g3 = new G3(); - } - - @Configuration - static final class G3 { - G4 g4 = new G4(); - } - - @Configuration - static final class G4 { - @Comment({"g4.g3 1", "g4.g3 2"}) - int g3; - @Comment("g4.g4") - int g4; - } - - @Test - void writeYamlNestedComments3() { - writeConfig(G1.class); - assertFileContentEquals( - """ - # g1.g2 - g2: - g3: - g4: - # g4.g3 1 - # g4.g3 2 - g3: 0 - # g4.g4 - g4: 0\ - """ - ); - } - - @Configuration - static final class H1 { - @Comment("h2.1") - H2 h21 = new H2(); - @Comment("h2.2") - H2 h22 = null; - } - - @Configuration - static final class H2 { - @Comment("j") - int j = 10; - } - - @Test - void writeYamlNullFields() { - writeConfig(H1.class); - assertFileContentEquals( - """ - # h2.1 - h21: - # j - j: 10\ - """ - ); - writeConfig(H1.class, builder -> builder.outputNulls(true)); - assertFileContentEquals( - """ - # h2.1 - h21: - # j - j: 10 - # h2.2 - h22: null\ - """ - ); - } - - @Configuration - static class J1 { - @Comment("sj1") - String sJ1 = "sj1"; - } - - static final class J2 extends J1 { - @Comment("sj2") - String sJ2 = "sj2"; - } - - @Configuration - static class K1 { - @Comment("k1.j1") - J1 k1J1 = new J1(); - @Comment("k1.j2") - J2 k1J2 = new J2(); - } - - static final class K2 extends K1 { - @Comment("k2.j1") - J1 k2J1 = new J1(); - @Comment("k2.j2") - J2 k2J2 = new J2(); - } - - @Test - void writeYamlInheritance() { - writeConfig(K2.class); - assertFileContentEquals( - """ - # k1.j1 - k1J1: - # sj1 - sJ1: sj1 - # k1.j2 - k1J2: - # sj1 - sJ1: sj1 - # sj2 - sJ2: sj2 - # k2.j1 - k2J1: - # sj1 - sJ1: sj1 - # k2.j2 - k2J2: - # sj1 - sJ1: sj1 - # sj2 - sJ2: sj2\ - """ - ); - } - - record R1(@Comment("Hello") int i, int j, @Comment("World") int k) {} - - @Configuration - static class L1 { - @Comment("l1") - R1 r1 = new R1(1, 2, 3); - } - - @Test - void writeYamlConfigWithRecord() { - writeConfig(L1.class); - assertFileContentEquals( - """ - # l1 - r1: - # Hello - i: 1 - j: 2 - # World - k: 3\ - """ - ); - } - - record R2(@Comment("r2i") int i, int j, @Comment("r2k") int k) {} - - record R3(@Comment("r3r2") R2 r2) {} - - record R4(@Comment("r4m1") M1 m1, @Comment("r4r3") R3 r3) {} - - @Configuration - static class M1 { - @Comment("m1r2") - R2 r2 = new R2(1, 2, 3); - @Comment("m1r3") - R3 r3 = new R3(new R2(4, 5, 6)); - } - - @Configuration - static class M2 { - @Comment("m2r4") - R4 r4 = new R4(new M1(), new R3(new R2(7, 8, 9))); - } - - @Test - void writeYamlConfigWithRecordNested() { - writeConfig(M2.class); - assertFileContentEquals( - """ - # m2r4 - r4: - # r4m1 - m1: - # m1r2 - r2: - # r2i - i: 1 - j: 2 - # r2k - k: 3 - # m1r3 - r3: - # r3r2 - r2: - # r2i - i: 4 - j: 5 - # r2k - k: 6 - # r4r3 - r3: - # r3r2 - r2: - # r2i - i: 7 - j: 8 - # r2k - k: 9\ - """ - ); - } - - @Test - void lengthCommonPrefix() { - List ab = List.of("a", "b"); - List abc = List.of("a", "b", "c"); - List abcd = List.of("a", "b", "c", "d"); - List aef = List.of("a", "e", "f"); - List def = List.of("d", "e", "f"); - - assertEquals(2, YamlFileWriter.lengthCommonPrefix(ab, ab)); - assertEquals(2, YamlFileWriter.lengthCommonPrefix(abc, ab)); - assertEquals(2, YamlFileWriter.lengthCommonPrefix(ab, abc)); - assertEquals(2, YamlFileWriter.lengthCommonPrefix(ab, abcd)); - assertEquals(3, YamlFileWriter.lengthCommonPrefix(abc, abc)); - assertEquals(3, YamlFileWriter.lengthCommonPrefix(abc, abcd)); - - assertEquals(1, YamlFileWriter.lengthCommonPrefix(ab, aef)); - assertEquals(1, YamlFileWriter.lengthCommonPrefix(abcd, aef)); - - assertEquals(0, YamlFileWriter.lengthCommonPrefix(ab, def)); - assertEquals(0, YamlFileWriter.lengthCommonPrefix(abcd, def)); - } - - String readFile() { - return TestUtils.readFile(yamlFile); - } - - record YamlFileWriterArguments( - String yaml, - Queue nodes, - YamlConfigurationProperties properties - ) {} - - void assertFileContentEquals(String expected) { - assertEquals(expected, readFile()); - } - - void writeConfig(Class cls) { - writeConfig(cls, builder -> {}); - } - - void writeConfig(Class cls, Consumer> configurer) { - YamlFileWriterArguments args = argsFromConfig( - cls, - Reflect.callNoParamConstructor(cls), - configurer - ); - YamlFileWriter writer = new YamlFileWriter(yamlFile, args.properties); - writer.writeYaml(args.yaml, args.nodes); - } - - static YamlFileWriterArguments argsFromConfig( - Class t, - T c, - Consumer> configurer - ) { - YamlConfigurationProperties.Builder builder = YamlConfigurationProperties.newBuilder(); - configurer.accept(builder); - YamlConfigurationProperties properties = builder.build(); - - ConfigurationSerializer serializer = new ConfigurationSerializer<>(t, properties); - Map serialize = serializer.serialize(c); - Dump dump = YamlConfigurationStore.newYamlDumper(); - String yaml = dump.dumpToString(serialize); - CommentNodeExtractor extractor = new CommentNodeExtractor(properties); - Queue nodes = extractor.extractCommentNodes(c); - return new YamlFileWriterArguments(yaml, nodes, properties); - } -} \ No newline at end of file diff --git a/configlib-yaml/src/test/java/de/exlll/configlib/YamlWriterTest.java b/configlib-yaml/src/test/java/de/exlll/configlib/YamlWriterTest.java new file mode 100644 index 0000000..c485601 --- /dev/null +++ b/configlib-yaml/src/test/java/de/exlll/configlib/YamlWriterTest.java @@ -0,0 +1,569 @@ +package de.exlll.configlib; + +import com.google.common.jimfs.Jimfs; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.snakeyaml.engine.v2.api.Dump; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.function.Consumer; + +import static de.exlll.configlib.TestUtils.createPlatformSpecificFilePath; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@SuppressWarnings("unused") +class YamlWriterTest { + private final FileSystem fs = Jimfs.newFileSystem(); + private final Path yamlFile = fs.getPath(createPlatformSpecificFilePath("/tmp/config.yml")); + private final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + @BeforeEach + void setUp() throws IOException { + Files.createDirectories(yamlFile.getParent()); + } + + @AfterEach + void tearDown() throws IOException { + fs.close(); + } + + @Configuration + static final class A { + String s = ""; + } + + @Test + void writeYamlWithNoComments() { + writeConfigToFile(A.class); + writeConfigToStream(A.class); + + assertFileContentEquals("s: ''\n"); + assertStreamContentEquals("s: ''\n"); + } + + @Test + void writeYamlWithHeaderAndFooter() { + Consumer> builderConsumer = builder -> builder + .header("This is a \n\n \nheader.") + .footer("That is a\n\n \nfooter."); + + writeConfigToFile(A.class, builderConsumer); + writeConfigToStream(A.class, builderConsumer); + + String expected = """ + # This is a\s + + # \s + # header. + + s: '' + + # That is a + + # \s + # footer. + """; + + assertFileContentEquals(expected); + assertStreamContentEquals(expected); + } + + @Configuration + static final class B { + @Comment("Hello") + String s = "s"; + } + + @Test + void writeYamlSingleComment() { + writeConfigToFile(B.class); + writeConfigToStream(B.class); + + String expected = """ + # Hello + s: s + """; + + assertFileContentEquals(expected); + assertStreamContentEquals(expected); + } + + @Configuration + static final class C { + @Comment({"Hello", "World"}) + Map mapStringInteger = Map.of("1", 2); + @Comment({"world", "hello"}) + Map mapIntegerString = Map.of(2, "1"); + } + + @Test + void writeYamlMultipleComments() { + writeConfigToFile(C.class); + writeConfigToStream(C.class); + + String expected = """ + # Hello + # World + mapStringInteger: + '1': 2 + # world + # hello + mapIntegerString: + 2: '1' + """; + + assertFileContentEquals(expected); + assertStreamContentEquals(expected); + } + + @Configuration + static final class D { + @Comment({"Hello", "", " ", "World"}) + String s1 = "s1"; + @Comment({"", "", " ", "How are ", "you?", ""}) + String s2 = "s2"; + } + + @Test + void writeYamlEmptyComments() { + writeConfigToFile(D.class); + writeConfigToStream(D.class); + + String expected = """ + # Hello + + # \s + # World + s1: s1 + + + # \s + # How are\s + # you? + + s2: s2 + """; + + assertFileContentEquals(expected); + assertStreamContentEquals(expected); + } + + @Configuration + static final class E1 { + @Comment("m") + Map> m = Map.of("c", Map.of("i", 1)); + @Comment("e2") + E2 e2 = new E2(); + } + + @Configuration + static final class E2 { + Map m = Map.of("i", 1); + @Comment("e3") + E3 e3 = new E3(); + @Comment("j") + int j = 10; + } + + @Configuration + static final class E3 { + @Comment("i") + int i = 1; + } + + @Test + void writeYamlNestedComments1() { + writeConfigToFile(E1.class); + writeConfigToStream(E1.class); + + String expected = """ + # m + m: + c: + i: 1 + # e2 + e2: + m: + i: 1 + # e3 + e3: + # i + i: 1 + # j + j: 10 + """; + assertFileContentEquals(expected); + assertStreamContentEquals(expected); + } + + @Configuration + static final class F1 { + Map m1 = Map.of("i", 1); + F2 f2 = new F2(); + @Comment("f1.m2") + Map m2 = Map.of("i", 1); + } + + @Configuration + static final class F2 { + @Comment("f2.i") + int i; + } + + @Test + void writeYamlNestedComments2() { + writeConfigToFile(F1.class); + writeConfigToStream(F1.class); + + String expected = """ + m1: + i: 1 + f2: + # f2.i + i: 0 + # f1.m2 + m2: + i: 1 + """; + + assertFileContentEquals(expected); + assertStreamContentEquals(expected); + } + + @Configuration + static final class G1 { + @Comment("g1.g2") + G2 g2 = new G2(); + } + + @Configuration + static final class G2 { + G3 g3 = new G3(); + } + + @Configuration + static final class G3 { + G4 g4 = new G4(); + } + + @Configuration + static final class G4 { + @Comment({"g4.g3 1", "g4.g3 2"}) + int g3; + @Comment("g4.g4") + int g4; + } + + @Test + void writeYamlNestedComments3() { + writeConfigToFile(G1.class); + writeConfigToStream(G1.class); + + String expected = """ + # g1.g2 + g2: + g3: + g4: + # g4.g3 1 + # g4.g3 2 + g3: 0 + # g4.g4 + g4: 0 + """; + + assertFileContentEquals(expected); + assertStreamContentEquals(expected); + } + + @Configuration + static final class H1 { + @Comment("h2.1") + H2 h21 = new H2(); + @Comment("h2.2") + H2 h22 = null; + } + + @Configuration + static final class H2 { + @Comment("j") + int j = 10; + } + + @Test + void writeYamlNullFields1() { + writeConfigToFile(H1.class); + writeConfigToStream(H1.class); + + String expected = """ + # h2.1 + h21: + # j + j: 10 + """; + + assertFileContentEquals(expected); + assertStreamContentEquals(expected); + } + + @Test + void writeYamlNullFields2() { + writeConfigToFile(H1.class, builder -> builder.outputNulls(true)); + writeConfigToStream(H1.class, builder -> builder.outputNulls(true)); + + String expected = """ + # h2.1 + h21: + # j + j: 10 + # h2.2 + h22: null + """; + + assertFileContentEquals(expected); + assertStreamContentEquals(expected); + } + + @Configuration + static class J1 { + @Comment("sj1") + String sJ1 = "sj1"; + } + + static final class J2 extends J1 { + @Comment("sj2") + String sJ2 = "sj2"; + } + + @Configuration + static class K1 { + @Comment("k1.j1") + J1 k1J1 = new J1(); + @Comment("k1.j2") + J2 k1J2 = new J2(); + } + + static final class K2 extends K1 { + @Comment("k2.j1") + J1 k2J1 = new J1(); + @Comment("k2.j2") + J2 k2J2 = new J2(); + } + + @Test + void writeYamlInheritance() { + writeConfigToFile(K2.class); + writeConfigToStream(K2.class); + + String expected = """ + # k1.j1 + k1J1: + # sj1 + sJ1: sj1 + # k1.j2 + k1J2: + # sj1 + sJ1: sj1 + # sj2 + sJ2: sj2 + # k2.j1 + k2J1: + # sj1 + sJ1: sj1 + # k2.j2 + k2J2: + # sj1 + sJ1: sj1 + # sj2 + sJ2: sj2 + """; + + assertFileContentEquals(expected); + assertStreamContentEquals(expected); + } + + record R1(@Comment("Hello") int i, int j, @Comment("World") int k) {} + + @Configuration + static class L1 { + @Comment("l1") + R1 r1 = new R1(1, 2, 3); + } + + @Test + void writeYamlConfigWithRecord() { + writeConfigToFile(L1.class); + writeConfigToStream(L1.class); + + String expected = """ + # l1 + r1: + # Hello + i: 1 + j: 2 + # World + k: 3 + """; + + assertFileContentEquals(expected); + assertStreamContentEquals(expected); + } + + record R2(@Comment("r2i") int i, int j, @Comment("r2k") int k) {} + + record R3(@Comment("r3r2") R2 r2) {} + + record R4(@Comment("r4m1") M1 m1, @Comment("r4r3") R3 r3) {} + + @Configuration + static class M1 { + @Comment("m1r2") + R2 r2 = new R2(1, 2, 3); + @Comment("m1r3") + R3 r3 = new R3(new R2(4, 5, 6)); + } + + @Configuration + static class M2 { + @Comment("m2r4") + R4 r4 = new R4(new M1(), new R3(new R2(7, 8, 9))); + } + + @Test + void writeYamlConfigWithRecordNested() { + writeConfigToFile(M2.class); + writeConfigToStream(M2.class); + + String expected = """ + # m2r4 + r4: + # r4m1 + m1: + # m1r2 + r2: + # r2i + i: 1 + j: 2 + # r2k + k: 3 + # m1r3 + r3: + # r3r2 + r2: + # r2i + i: 4 + j: 5 + # r2k + k: 6 + # r4r3 + r3: + # r3r2 + r2: + # r2i + i: 7 + j: 8 + # r2k + k: 9 + """; + + assertFileContentEquals(expected); + assertStreamContentEquals(expected); + } + + @Test + void lengthCommonPrefix() { + List ab = List.of("a", "b"); + List abc = List.of("a", "b", "c"); + List abcd = List.of("a", "b", "c", "d"); + List aef = List.of("a", "e", "f"); + List def = List.of("d", "e", "f"); + + assertEquals(2, YamlWriter.lengthCommonPrefix(ab, ab)); + assertEquals(2, YamlWriter.lengthCommonPrefix(abc, ab)); + assertEquals(2, YamlWriter.lengthCommonPrefix(ab, abc)); + assertEquals(2, YamlWriter.lengthCommonPrefix(ab, abcd)); + assertEquals(3, YamlWriter.lengthCommonPrefix(abc, abc)); + assertEquals(3, YamlWriter.lengthCommonPrefix(abc, abcd)); + + assertEquals(1, YamlWriter.lengthCommonPrefix(ab, aef)); + assertEquals(1, YamlWriter.lengthCommonPrefix(abcd, aef)); + + assertEquals(0, YamlWriter.lengthCommonPrefix(ab, def)); + assertEquals(0, YamlWriter.lengthCommonPrefix(abcd, def)); + } + + String readFile() { + return TestUtils.readFile(yamlFile); + } + + String readOutputStream() { + return outputStream.toString(); + } + + void assertFileContentEquals(String expected) { + assertEquals(expected, readFile()); + } + + void assertStreamContentEquals(String expected) { + assertEquals(expected, readOutputStream()); + } + + void writeConfigToFile(Class cls) { + writeConfigToFile(cls, builder -> {}); + } + + void writeConfigToFile(Class cls, Consumer> configurer) { + YamlWriterArguments args = argsFromConfig( + cls, + Reflect.callNoParamConstructor(cls), + configurer + ); + YamlWriter writer = new YamlWriter(yamlFile, args.properties); + writer.writeYaml(args.yaml, args.nodes); + } + + void writeConfigToStream(Class cls) { + writeConfigToStream(cls, builder -> {}); + } + + void writeConfigToStream(Class cls, Consumer> configurer) { + YamlWriterArguments args = argsFromConfig( + cls, + Reflect.callNoParamConstructor(cls), + configurer + ); + YamlWriter writer = new YamlWriter(outputStream, args.properties); + writer.writeYaml(args.yaml, args.nodes); + } + + record YamlWriterArguments( + String yaml, + Queue nodes, + YamlConfigurationProperties properties + ) {} + + static YamlWriterArguments argsFromConfig( + Class t, + T c, + Consumer> configurer + ) { + YamlConfigurationProperties.Builder builder = YamlConfigurationProperties.newBuilder(); + configurer.accept(builder); + YamlConfigurationProperties properties = builder.build(); + + ConfigurationSerializer serializer = new ConfigurationSerializer<>(t, properties); + Map serialize = serializer.serialize(c); + Dump dump = YamlConfigurationStore.newYamlDumper(); + String yaml = dump.dumpToString(serialize); + CommentNodeExtractor extractor = new CommentNodeExtractor(properties); + Queue nodes = extractor.extractCommentNodes(c); + return new YamlWriterArguments(yaml, nodes, properties); + } +}