2022-10-11 19:29:26 +02:00
|
|
|
/**
|
|
|
|
* Utilities for loading generic configuration files consisting of key/value pairs.
|
|
|
|
*
|
|
|
|
* Authors:
|
|
|
|
* Mike Bierlee, m.bierlee@lostmoment.com
|
|
|
|
* Copyright: 2022 Mike Bierlee
|
|
|
|
* License:
|
|
|
|
* This software is licensed under the terms of the MIT license.
|
|
|
|
* The full terms of the license can be found in the LICENSE file.
|
|
|
|
*/
|
|
|
|
|
|
|
|
module mirage.keyvalue;
|
|
|
|
|
|
|
|
import mirage.config : ConfigFactory, ConfigDictionary, ConfigNode, ValueNode, ObjectNode, ConfigCreationException;
|
|
|
|
|
2022-10-11 20:07:08 +02:00
|
|
|
import std.string : lineSplitter, strip, startsWith, endsWith, split, indexOf;
|
2022-10-11 19:29:26 +02:00
|
|
|
import std.array : array;
|
|
|
|
import std.exception : enforce;
|
|
|
|
import std.conv : to;
|
2022-10-11 19:49:32 +02:00
|
|
|
import std.typecons : Flag;
|
|
|
|
|
2022-10-11 20:07:08 +02:00
|
|
|
alias SupportHashtagComments = Flag!"SupportHashtagComments";
|
|
|
|
alias SupportSemicolonComments = Flag!"SupportSemicolonComments";
|
|
|
|
alias SupportSections = Flag!"SupportSections";
|
2022-10-11 19:29:26 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* A generic reusable key/value config factory that can be configured to parse
|
|
|
|
* the specifics of certain key/value formats.
|
|
|
|
*/
|
2022-10-11 19:49:32 +02:00
|
|
|
class KeyValueConfigFactory(
|
|
|
|
SupportHashtagComments supportHashtagComments = SupportHashtagComments.no,
|
2022-10-11 20:07:08 +02:00
|
|
|
SupportSemicolonComments supportSemicolonComments = SupportSemicolonComments.no,
|
|
|
|
SupportSections supportSections = SupportSections.no
|
2022-10-11 19:49:32 +02:00
|
|
|
) : ConfigFactory {
|
2022-10-11 19:29:26 +02:00
|
|
|
/**
|
|
|
|
* Parse a configuration file following the configured key/value conventions.
|
|
|
|
*
|
|
|
|
* Params:
|
|
|
|
* contents = Text contents of the config to be parsed.
|
|
|
|
* Returns: The parsed configuration.
|
|
|
|
*/
|
|
|
|
override ConfigDictionary parseConfig(string contents) {
|
|
|
|
enforce!ConfigCreationException(contents !is null, "Contents cannot be null.");
|
|
|
|
auto lines = contents.lineSplitter().array;
|
|
|
|
auto properties = new ConfigDictionary();
|
2022-10-11 20:07:08 +02:00
|
|
|
auto section = "";
|
2022-10-11 19:29:26 +02:00
|
|
|
foreach (size_t index, string line; lines) {
|
2022-10-11 20:07:08 +02:00
|
|
|
auto processedLine = line;
|
2022-10-11 19:49:32 +02:00
|
|
|
|
|
|
|
if (supportHashtagComments) {
|
2022-10-11 20:07:08 +02:00
|
|
|
auto commentPosition = processedLine.indexOf('#');
|
2022-10-11 19:49:32 +02:00
|
|
|
if (commentPosition >= 0) {
|
2022-10-11 20:07:08 +02:00
|
|
|
processedLine = processedLine[0 .. commentPosition];
|
2022-10-11 19:49:32 +02:00
|
|
|
}
|
2022-10-11 19:29:26 +02:00
|
|
|
}
|
|
|
|
|
2022-10-11 19:49:32 +02:00
|
|
|
if (supportSemicolonComments) {
|
2022-10-11 20:07:08 +02:00
|
|
|
auto commentPosition = processedLine.indexOf(';');
|
2022-10-11 19:49:32 +02:00
|
|
|
if (commentPosition >= 0) {
|
2022-10-11 20:07:08 +02:00
|
|
|
processedLine = processedLine[0 .. commentPosition];
|
2022-10-11 19:49:32 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-11 20:07:08 +02:00
|
|
|
processedLine = processedLine.strip;
|
|
|
|
|
|
|
|
if (supportSections && processedLine.startsWith('[') && processedLine.endsWith(']')) {
|
|
|
|
section = processedLine[1 .. $ - 1] ~ '.';
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (processedLine.length == 0) {
|
2022-10-11 19:49:32 +02:00
|
|
|
continue;
|
2022-10-11 19:29:26 +02:00
|
|
|
}
|
|
|
|
|
2022-10-11 20:07:08 +02:00
|
|
|
auto parts = processedLine.split('=');
|
2022-10-11 19:29:26 +02:00
|
|
|
enforce!ConfigCreationException(parts.length <= 2, "Line has too many equals signs and cannot be parsed (L" ~ index
|
2022-10-11 20:07:08 +02:00
|
|
|
.to!string ~ "): " ~ processedLine);
|
|
|
|
enforce!ConfigCreationException(parts.length == 2, "Missing value assignment (L" ~ index.to!string ~ "): " ~ processedLine);
|
|
|
|
|
|
|
|
properties.set(section ~ parts[0].strip, parts[1].strip);
|
2022-10-11 19:29:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return properties;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
version (unittest) {
|
|
|
|
import std.exception : assertThrown;
|
|
|
|
import std.process : environment;
|
|
|
|
|
2022-10-11 19:49:32 +02:00
|
|
|
class TestKeyValueConfigFactory : KeyValueConfigFactory!() {
|
|
|
|
}
|
|
|
|
|
2022-10-11 19:29:26 +02:00
|
|
|
@("Parse standard key/value config")
|
|
|
|
unittest {
|
2022-10-11 19:49:32 +02:00
|
|
|
auto config = new TestKeyValueConfigFactory().parseConfig("
|
2022-10-11 19:29:26 +02:00
|
|
|
bla=one
|
|
|
|
di.bla=two
|
|
|
|
");
|
|
|
|
|
|
|
|
assert(config.get("bla") == "one");
|
|
|
|
assert(config.get("di.bla") == "two");
|
|
|
|
}
|
|
|
|
|
2022-10-11 19:49:32 +02:00
|
|
|
@("Parse and ignore comments")
|
|
|
|
unittest {
|
|
|
|
auto config = new KeyValueConfigFactory!(SupportHashtagComments.yes,
|
|
|
|
SupportSemicolonComments.yes
|
|
|
|
)().parseConfig("
|
|
|
|
# this is a comment
|
|
|
|
; this is another comment
|
|
|
|
iamset=true
|
|
|
|
");
|
|
|
|
|
|
|
|
assert(config.get!bool("iamset"));
|
|
|
|
}
|
|
|
|
|
2022-10-11 19:29:26 +02:00
|
|
|
@("Fail to parse when there are too many equals signs")
|
|
|
|
unittest {
|
2022-10-11 19:49:32 +02:00
|
|
|
assertThrown!ConfigCreationException(new TestKeyValueConfigFactory()
|
2022-10-11 19:29:26 +02:00
|
|
|
.parseConfig("one=two=three"));
|
|
|
|
}
|
|
|
|
|
|
|
|
@("Fail to parse when value assignment is missing")
|
|
|
|
unittest {
|
2022-10-11 19:49:32 +02:00
|
|
|
assertThrown!ConfigCreationException(new TestKeyValueConfigFactory()
|
|
|
|
.parseConfig(
|
|
|
|
"answertolife"));
|
2022-10-11 19:29:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@("Substitute env vars")
|
|
|
|
unittest {
|
|
|
|
environment["MIRAGE_TEST_ENVY"] = "Much";
|
2022-10-11 19:49:32 +02:00
|
|
|
auto config = new TestKeyValueConfigFactory().parseConfig("envy=$MIRAGE_TEST_ENVY");
|
2022-10-11 19:29:26 +02:00
|
|
|
|
|
|
|
assert(config.get("envy") == "Much");
|
|
|
|
}
|
|
|
|
|
|
|
|
@("Use value from other key")
|
|
|
|
unittest {
|
2022-10-11 19:49:32 +02:00
|
|
|
auto config = new TestKeyValueConfigFactory().parseConfig("
|
2022-10-11 19:29:26 +02:00
|
|
|
one=money
|
|
|
|
two=${one}
|
|
|
|
");
|
|
|
|
|
|
|
|
assert(config.get("two") == "money");
|
|
|
|
}
|
|
|
|
|
|
|
|
@("Values and keys are trimmed")
|
|
|
|
unittest {
|
2022-10-11 19:49:32 +02:00
|
|
|
auto config = new TestKeyValueConfigFactory().parseConfig("
|
2022-10-11 19:29:26 +02:00
|
|
|
one = money
|
|
|
|
");
|
|
|
|
|
|
|
|
assert(config.get("one") == "money");
|
|
|
|
}
|
|
|
|
|
|
|
|
@("Remove end-of-line comments")
|
|
|
|
unittest {
|
2022-10-11 19:49:32 +02:00
|
|
|
auto config = new KeyValueConfigFactory!(SupportHashtagComments.yes,
|
|
|
|
SupportSemicolonComments.yes)().parseConfig("
|
2022-10-11 19:29:26 +02:00
|
|
|
server=localhost #todo: change me. default=localhost when not set.
|
2022-10-11 19:49:32 +02:00
|
|
|
port=9876; I think this port = right?
|
2022-10-11 19:29:26 +02:00
|
|
|
");
|
|
|
|
|
|
|
|
assert(config.get("server") == "localhost");
|
2022-10-11 19:49:32 +02:00
|
|
|
assert(config.get("port") == "9876");
|
2022-10-11 19:29:26 +02:00
|
|
|
}
|
2022-10-11 20:07:08 +02:00
|
|
|
|
|
|
|
@("Support sections when enabled")
|
|
|
|
unittest {
|
|
|
|
auto config = new KeyValueConfigFactory!(SupportHashtagComments.no,
|
|
|
|
SupportSemicolonComments.yes,
|
|
|
|
SupportSections.yes)().parseConfig("
|
|
|
|
applicationName = test me!
|
|
|
|
|
|
|
|
[server]
|
|
|
|
host=localhost
|
|
|
|
port=2873
|
|
|
|
|
|
|
|
[server.middleware] ; Stuff that handles the http protocol
|
|
|
|
protocolServer = netty
|
|
|
|
|
|
|
|
[database.driver]
|
|
|
|
id=PostgresDriver
|
|
|
|
");
|
|
|
|
|
|
|
|
assert(config.get("applicationName") == "test me!");
|
|
|
|
assert(config.get("server.host") == "localhost");
|
|
|
|
assert(config.get("server.port") == "2873");
|
|
|
|
assert(config.get("server.middleware.protocolServer") == "netty");
|
|
|
|
assert(config.get("database.driver.id") == "PostgresDriver");
|
|
|
|
}
|
2022-10-11 19:29:26 +02:00
|
|
|
}
|