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.