summaryrefslogtreecommitdiff
path: root/content/posts/2020/2020-07-15-config-3.0.md
blob: 67a64c41d41a57cc23b9ef440a11d8ea33d027c2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
---
title: Config 3.0
date: 2020-07-15
tags:
- Raku
- Programming
---

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.

```raku
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.

```raku
$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.

```raku
use Config;

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

{{< admonition title="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.
{{< / admonition_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`](https://modules.raku.org/dist/IO::Path::XDG:cpan:TYIL), for
this, together with
[`IO::Glob`](https://modules.raku.org/dist/IO::Glob:cpan:HANENKAMP).
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.

```sh
$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`](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 title="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.
{{< / admonition >}}

## 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`.