Skip to content

Structure of a configuration file

Ridan Vandenbergh edited this page May 2, 2020 · 6 revisions

As intermediary for configuration files, fiber uses a tree called the IR (immediate representation). The IR contains all information about your configuration: the structure, names, values, metadata, everything.

Let's first take a look at how such a tree is built. Later we will see how fiber can interact with your IR and how others can interact with it.

Building the IR

Suppose we're making a small mod: it has a feature named op_feature some server owners may want to disable, and some visual elements the user may want to tweak. All of our visual elements go in a category 'gui', making the configuration easy to navigate.

A JSON representation of this configuration may look like this:

{
  "gui": {
    "flickering_lights": false,
    "space_between_buttons": 10,
    "title": "Config"
  },
  "op_feature": false
}

Let's get around to specifying the same configuration, but using fiber's IR!

Using builders

Builders are the primary way to compose an IR. Everything starts with a ConfigTreeBuilder:

ConfigTree.builder()
...

Now, we want to add our first entry: op_feature. It is of type boolean, is named op_feature and has a default value of false. Primitive values like these are called scalar values. The other type is 'aggregate', which are values like collections and arrays.

We can declare a node like op_feature by using withValue. This method must take a name, a type, and the default value:

ConfigTree.builder()
        .withValue("op_feature", ConfigTypes.BOOLEAN, false)
...

We can add more metadata to it such as comments, listeners, etc. We'll look into that later!

Next up is the gui category: in the IR, this is called a branch. We can 'start' one by using fork(String name):

...
    .fork("gui")
...

fork will return an entirely new ConfigTreeBuilder, one which we can add everything in the gui category to - and then return to our root builder.

Just as with op_feature, let's add flickering_lights, space_between_buttons, and title:

...
    .fork("gui")
    .withValue("flickering_lights", ConfigTypes.BOOLEAN, false)
    .withValue("space_between_buttons", ConfigTypes.INTEGER, 10)
    .withValue("title", ConfigTypes.STRING, "Config")
    .finishBranch()
...

finishBranch tells the builder "I'm done with gui, take me back!". It returns the original builder you made.

All done! Just build the entire thing using build(), and save the return value in a variable:

ConfigTree.builder()
        .withValue("op_feature", ConfigTypes.BOOLEAN, false)
        .fork("gui")
            .withValue("flickering_lights", ConfigTypes.BOOLEAN, false)
            .withValue("space_between_buttons", ConfigTypes.INTEGER, 10)
            .withValue("title", ConfigTypes.STRING, "Config")
            .finishBranch()
        .build();

Using annotations

Fiber also supports defining your IR in a POJO, optionally annotated with all the necessary metadata. This does the heavy lifting (builders) for you, but has some caveats as well.

Creating the class

Start off by making a new class. It doesn't need to extend or implement anything.

class MyConfigution { }

To add op_feature to our configuration, declare a new field of type boolean and value false.

class MyConfiguration {
    boolean op_feature = false;
}

Creating the gui branch is a little trickier. We'll have to create another class for the settings belonging in gui, and declare another field for the actual branch itself.

class MyConfiguration {
    boolean op_feature = false;

    @Setting.Group
    GuiBranch branch;

    class GuiBranch {
        boolean flickering_lights = false;
        int space_between_buttons = 10;
        String title = "Config";
    }
}

The Setting.Group annotation marks that fiber should treat that field as a branch, and not as a setting.

Lovely - our class now represents our configuration file, but there's some obvious faults: we can't do anything with it yet because it's not an IR, and the naming isn't very java-esque. We'll talk about converting it to an IR later, let's fix the naming first.

Renaming fields

If you'd like to rename one field, annotate the field with Setting and provide a name:

@Setting(name = "flickering_lights")
boolean flickeringLights = false;

This way of renaming a predictable naming scheme isn't very elegant though. Instead, use fiber's built-in naming conventions!

Naming conventions

Annotate MyConfiguration with Settings, and provide a namingConvention.

@Settings(namingConvention = SnakeCaseConvention.class)
class MyConfiguration {
...

Specifying a naming convention tells fiber to run every setting's name through the specified formatter, and use those names instead.

For example, SettingNamingConvention.SNAKE_CASE produces the following names:

Field name Formatted name
fooBar foo_bar
example example
aVeryLongName a_very_long_name

Fields that received a custom name through their Setting annotation have higher authority than the naming convention: they will not be ran through the formatter.

With the new naming convention, we can use normal names for our fields:

@Settings(namingConvention = SnakeCaseConvention.class)
class MyConfiguration {
    boolean opFeature = false;

    @Setting.Group
    GuiBranch branch;

    class GuiBranch {
        boolean flickeringLights = false;
        int spaceBetweenButtons = 10;
        String title = "Config";
    }
}

The IR

Create a builder and use the applyFromPojo method:

MyConfiguration configuration = new MyConfiguration();
ConfigTree tree = ConfigTree.builder()
        .applyFromPojo(configuration)
        .build();

That's it! You'll be able to use tree like any other IR, and any changes made to your settings will be set in the POJO as well. However, you may not modify the POJO directly - fiber can not pick up changes you make to the POJO.