Config 3.0

#raku #programming

For those who don’t know, the Config module for the Raku programming language is a generic class to hold… well… configuration data. It supports Config::Parser modules to handle different configuration file formats, such as JSON, YAML and TOML.

Up until now, the module didn’t do much for you other than provide an interface that’s generally the same, so you won’t need to learn differing methods to handle differing configuration file formats. It was my first Raku module, and as such, the code wasn’t the cleanest. I’ve written many new modules since then, and learned about a good number of (hopefully better) practices.

For version 3.0, I specifically wanted to remove effort from using the Config module on the developer’s end. It should check default locations for configuration files, so I don’t have to rewrite that code in every other module all the time. Additionally, configuration using environment variables is quite popular in the current day and age, especially for Dockerized applications. So, I set out to make an automated way to read those too.

The Old Way

First, let’s take a look at how it used to work. Generally, I’d create the default configuration structure and values first.

use Config;

my $config = Config.new.read({
    foo => "bar",
    alpha => {
        beta => "gamma",
    },
    version => 3,
});

And after that, check for potential configuration file locations, and read any that exist.

$config.read($*HOME.add('config/project.toml').absolute);

The .absolute call was necessary because I wrote the initial Config version with the .read method not supporting IO::Path objects. A fix for this has existed for a while, but wasn’t released, so couldn’t be relied on outside of my general development machines.

If you wanted to add additional environment variable lookups, you’d have to check for those as well, and perhaps also cast them as well, since environment variables are all strings by default.

Version 3.0

So, how does the new version improve this? For starters, the .new method of Config now takes a Hash as positional argument, in order to create the structure, and optionally types or default values of your configuration object.

use Config;

my $config = Config.new({
    foo => Str,
    alpha => {
        beta => "gamma",
    },
    version => 3,
}, :name<project>);
note

foo has been made into the Str type object, rather than a Str value. This was technically allowed in previous Config versions, but it comes with actual significance in 3.0.

Using .new instead of .read is a minor syntactic change, which saves 1 word per program. This isn’t quite that big of a deal. However, the optional name argument will enable the new automagic features. The name you give to .new is arbitrary, but will be used to deduce which directories to check, and which environment variables to read.

Automatic Configuration File Handling

By setting name to the value project, Config will consult the configuration directories from the XDG Base Directory Specification. It uses one of my other modules, IO::Path::XDG, for this, together with IO::Glob. Specifically, it will check my $XDG_CONFIG_DIRS and $XDG_CONFIG_HOME (in that order) for any files that match the globs project.* or project/config.*.

If any files are found to match, they will be read as well, and the configuration values contained therein, merged into $config. It will load the appropriate Config::Parser implementation based on the file’s extension. I intend to add a number of these to future Rakudo Star releases, to ensure most default configuration file formats are supported out of the box.

Automatic Environment Variable Handling

After this step, it will try out some environment variables for configuration values. Which variables are checked depends on the structure (and name) of the Config object. The entire structure is squashed into a 1-dimensional list of fields. Each level is replaced by an _. Additionally, each variable name is prefixed with the name. Lastly, all the variable names are uppercased.

For the example Config given above, this would result in the following environment variables being checked.

$PROJECT_FOO
$PROJECT_ALPHA_BETA
$PROJECT_VERSION

If any are found, they’re also cast to the appropriate type. Thus, $PROJECT_FOO would be cast to a Str, and so would $PROJECT_ALPHA_BETA. In this case that doesn’t do much, since they’re already strings. But $PROJECT_VERSION would be cast to an Int, since it’s default value is also of the Int type. This should ensure that your variables are always in the type you expected them to be originally, no matter the user’s configuration choices.

Debugging

In addition to these new features, Config now also makes use of my Log module. This module is made around the idea that logging should be simple if module developers are to use it, and the way logs are represented is up to the end-user. When running an application in your local terminal, you may want more human-friendly logs, whereas in production you may want JSON formatted logs to make it fit better into other tools.

You can tune the amount of logging performed using the $RAKU_LOG_LEVEL environment variable, as per the Log module’s interface. When set to 7 (for “debug”), it will print the configuration files that are being merged into your Config and which environment veriables are being used as well.

note

A downside is that the application using Config for its configuration must also support Log to actually make the new logging work. Luckily, this is quite easy to set up, and there's example code for this in Log's README.

Too Fancy For Me

It could very well be that you don’t want these features, and you want to stick to the old ways as much as possible. No tricks, just plain and simple configuration handling. This can be done by simply ommitting the name argument to .new. The new features depend on this name to be set, and won’t do anything without it.

Alternatively, both the automatic configuration file handling and the environment variable handling can be turned off individually using :!from-xdg and :!from-env arguments respectively.

In Conclusion

The new Config module should result in cleaner code in modules using it, and more convenience for the developer. If you find any bugs or have other ideas for improving the module, feel free to send an email to https://lists.sr.ht/~tyil/raku-devel.