Skip to main content

2. Adding Features to the Config

In the previous step, we defined a basic configuration. However, real-world configurations are rarely that simple. They require default values, validation constraints, collections, and nested structures.

Let's expand our simple config into a more robust AdvancedConfig.

Building the Config

Here is our expanded interface:

package me.bristermitten.mittenlib.docs.tutorial;

import me.bristermitten.mittenlib.config.Config;
import me.bristermitten.mittenlib.config.Source;
import me.bristermitten.mittenlib.config.validation.Min;
import me.bristermitten.mittenlib.config.validation.NotBlank;

import java.util.List;

@Config(requireDynamicInitialization = false)
@Source("config.yml")
public interface AdvancedConfig {
@NotBlank
String host();

@Min(1)
default int port() {
return 3306;
}

default List<String> motd() {
return List.of("Welcome to the server!");
}

DatabaseConfig database();

@Config
interface DatabaseConfig {
String username();
@Nullable String password();
}
}

Let's break down the new features we added.

1. The @Source Annotation

This annotation actually binds the config to a real file on the filesystem. When this annotation is present, MittenLib will automatically:

  1. Perform some extra validation on the config to ensure that a default config can be generated.
  2. Add a constant me.bristermitten.mittenlib.config.Configuration field to the implementation, storing the target file name.
  3. Set up Guice bindings for the config

Automated Guice Features

The generated Guice bindings handle the following automatically:

  1. Create a default config file if it doesn't exist OR load a handwritten default from the JAR.
  2. Automatically read the config file and deserialize it into the config object.
  3. Live-reload the config file when it changes!
warning

When a config has a @Source, MittenLib requires it to be dynamically initializable (meaning every field has a default or is @Nullable). This allows the library to create a default config file if it's missing. If it's not, MittenLib will throw a compile-time error by default.

If you don't want this, and instead want to hand-write the default, you can set requireDynamicInitialization = false, which disables this check. You MUST have a default config file present in your plugin's JAR if you take this approach, or an error will be thrown on startup.

For the sake of this example, we set requireDynamicInitialization = false because host and database do not have defaults.

2. Defaults and Nullable Fields

By default, all fields in a MittenLib configuration are required. If a user omits them from the config.yml, the application will fail to start.

There are two ways to disable this:

Nullable Fields

If you want to allow a field to be omitted from the config, you can mark it as @Nullable. We do this on the password() property in DatabaseConfig.

Defaults

We can also provide a default value for a field, which will be used if it's not present in the config.

Default values can be defined with a default method, as demonstrated with the port() method. If the user doesn't specify a port, the loader will automatically use the value returned by return 3306;.

This means we can use standard Java code to provide backup default values!

2. Validation (@NotBlank, @Min)

MittenLib has built-in support for automatically validating your config.

  • @NotBlank ensures the host string isn't empty or whitespace.
  • @Min(1) ensures the port is a positive number.

If any of these constraints fail during loading, MittenLib will collect all the errors and present them in a user-friendly way.

3. Collections (List<String>)

MittenLib supports a subset of standard Java collections out of the box (List, and Map), which work in the way you'd expect.

4. Nested Configurations

As your configuration grows, it's helpful to organize it into subsections. You can do this by defining another @Config type (like DatabaseConfig above) and using it like normal. This also allows reusing the same type for multiple subsections.

Using other configurations as properties translates to a nested structure in the YAML/JSON file.

Example

The config class we've written handles files with this structure:

host:
port: 3306
motd:
- Welcome to the server!
database:
username:
password:

What's Next?

We now have a robust, validated, and feature-rich configuration definition. The final step is learning how to actually load this configuration into your application and use it.