Properties Files for D
Written by Andrew Lalis at 10 January, 2022
Using properties files in D with the d-properties library.
As part of Gyrobian's ongoing efforts to further adopt the D programming language as part of our daily tech stack, this article will shed some light on the d-properties library developed by Andrew recently for reading and writing Java-style Properties files for config and simple data storage.
To preface, properties files are formatted text files used mainly by Java applications (but they're simple and human-readable, so it makes sense to support them elsewhere). A properties file consists of a series of key-value pairs, each beginning on a separate line. The following snippet shows a basic example of what such a file might look like.
# My application's configuration properties
app.port=33567
app.enable-ssl=true
db.user=root
db.pass=123456
These simple key-value pair files are most often used for specifying external configuration settings for applications, like passwords or secrets that shouldn't be baked into the source code, or for internationalization files (for translating website contents, for example).
Using d-properties
It's very easy to begin using our library to read and write properties files in your D application. If you're using the dub project management tool, it's as easy as calling dub add d-properties
from your command-line, or adding "d-properties": "~>1.0.2"
to your dub.json
file.
In other cases, the MIT license permits you to simply copy and paste the source D files directly from the GitHub repository. You'll need properties.d
, reader.d
, and writer.d
.
Let's start by creating a simple D application which can read the properties we've defined in that snippet above, assuming it's stored in a my-config.properties
file.
import std.stdio;
import std.conv;
import d_properties;
void main() {
auto props = Properties("my-config.properties");
writefln!"Starting app on port %d"(props["app.port"].to!ushort);
if (props["app.enable-ssl"].to!bool) {
writeln("SSL is enabled.");
}
}
my-config.properties
, then print some debug information like we might in a real application. We can access properties using the index syntax (props["key"]
), which will return a string
value.
The above snippet should actually be enough for around 90% of use-cases, where you simply want to get config values at runtime. However, the library provides a number of utilities for more advanced use cases, including the following:
- Reading from many different properties files at once.
- Merging properties from other files into another.
- Modifying properties at runtime.
- Manually defining properties using an associative array of strings.
- Updating properties at runtime.
- Checking if certain properties are defined.
- Writing properties back to a file.
Additionally, the library also provides support for a number of different flavors, including the classic key=value
format, the colon format key:value
, and the space format key value
. For a full overview of everything you can do with d-properties, check out the documentation.
How It Works
Internally, the Properties
structure is nothing more than a fancy wrapper around an associative array of string[string] values;
, with a bunch of methods added to offer the functionality described in the previous section, and some extra checks to ensure that the programmer isn't likely to run into unexpected situations at runtime.
Writing properties to a file is quite simple as well; in fact, the majority of the serialization algorithm can be condensed into this short snippet:
auto f = File(filename, "w"); // File comment writing omitted for brevity
auto keys = props.values.keys;
keys.sort(); // Sort keys to make file writing deterministic.
foreach (key; keys) {
f.writefln("%s%s%s", sanitizeKey(key), separator, sanitizeValue(props[key]));
}
Reading properties from a file is where things get a bit more difficult, however. We won't go into the gorey details which you can find by reading the source code directly, but in short, we read the properties in a linear fashion, treating the input as a stack that we pull characters from. First we start by consuming characters from the key's string, until we determine that we've found a separator, at which point we then begin parsing the value string. We continue doing this until all key-value pairs have been read.
For both input and output, time complexity is linear with the size of the properties file, and space complexity is also linear. While small performance improvements could possibly be made to the reading algorithm, in its current state, it's more than capable of managing the vast majority of files.