From 2185ceca87a4123fdf13f3e9aea8bcb4f1615f50 Mon Sep 17 00:00:00 2001 From: Patrick Spek Date: Wed, 15 Jul 2020 16:09:52 +0200 Subject: Add post on Config 3.0 --- _posts/2020-07-15-config-3.0.md | 177 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 _posts/2020-07-15-config-3.0.md diff --git a/_posts/2020-07-15-config-3.0.md b/_posts/2020-07-15-config-3.0.md new file mode 100644 index 0000000..dbaa4c8 --- /dev/null +++ b/_posts/2020-07-15-config-3.0.md @@ -0,0 +1,177 @@ +--- +title: Config 3.0 +layout: post +tags: Raku Programming +social: + email: mailto:~tyil/public-inbox@lists.sr.ht&subject=Config 3.0 +description: > + I've made a reasonably sized change to Raku's Config module, resulting in a + major version bump. This article details my reasoning behind it, and shows + some examples on how I think I solved the issues at hand. +--- + +For those who don't know, the +[`Config`](https://modules.raku.org/dist/Config:cpan:TYIL) module for the Raku +programming language is a generic class to hold... well... configuration data. +It supports +[`Config::Parser`](https://modules.raku.org/search/?q=Config%3A%3AParser) +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. + +{% highlight perl6 %} +use Config; + +my $config = Config.new.read({ + foo => "bar", + alpha => { + beta => "gamma", + }, + version => 3, +}); +{% endhighlight %} + +And after that, check for potential configuration file locations, and read any +that exist. + +{% highlight perl6 %} +$config.read($*HOME.add('config/project.toml').absolute); +{% endhighlight %} + +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. + +{% highlight perl6 %} +use Config; + +my $config = Config.new({ + foo => Str, + alpha => { + beta => "gamma", + }, + version => 3, +}, :name); +{% endhighlight %} + +{% admonition_md 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. +{% endadmonition_md %} + +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](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html). +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. + +{% highlight sh %} +$PROJECT_FOO +$PROJECT_ALPHA_BETA +$PROJECT_VERSION +{% endhighlight %} + +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`](https://modules.raku.org/dist/Log:cpan:TYIL) 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. + +{% admonition_md 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. +{% endadmonition_md %} + +## 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`. -- cgit v1.1