mirage-config/source/mirage/config.d

797 lines
25 KiB
D
Raw Normal View History

2022-09-24 00:13:08 +02:00
/**
2022-09-24 18:31:21 +02:00
* Base utilities for working with configurations.
*
2022-09-24 00:13:08 +02:00
* 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.config;
2022-09-23 22:34:46 +02:00
2022-09-24 00:13:08 +02:00
import std.exception : enforce;
import std.string : split, startsWith, endsWith, join, lastIndexOf, strip, toLower;
2022-09-24 00:13:08 +02:00
import std.conv : to, ConvException;
2022-09-24 17:51:32 +02:00
import std.file : readText;
import std.path : extension;
2022-09-28 22:31:52 +02:00
import std.process : environment;
import std.typecons : Flag;
import mirage.json : loadJsonConfig;
2022-09-24 00:13:08 +02:00
2022-09-24 18:31:21 +02:00
/**
* Used by the ConfigDictionary when something goes wrong when reading configuration.
*/
2022-09-24 00:13:08 +02:00
class ConfigReadException : Exception {
this(string msg, string file = __FILE__, size_t line = __LINE__) {
super(msg, file, line);
}
}
2022-09-24 18:31:21 +02:00
/**
* Used by ConfigFactory instances when loading or parsing configuration fails.
*/
2022-09-24 17:05:33 +02:00
class ConfigCreationException : Exception {
this(string msg, string file = __FILE__, size_t line = __LINE__) {
super(msg, file, line);
}
}
2022-09-24 18:31:21 +02:00
/**
* Used by ConfigDictionary when there is something wrong with the path when calling ConfigDictionary.get()
*/
2022-09-24 00:13:08 +02:00
class PathParseException : Exception {
this(string msg, string path, string file = __FILE__, size_t line = __LINE__) {
string fullMsg = msg ~ " (Path: " ~ path ~ ")";
super(fullMsg, file, line);
}
}
2022-09-24 18:31:21 +02:00
/**
* The configuration tree is made up of specific types of ConfigNodes.
* Used as generic type for ConfigFactory and ConfigDictionary.
*/
2022-09-24 17:05:33 +02:00
interface ConfigNode {
string nodeType();
2022-09-23 22:34:46 +02:00
}
2022-09-24 18:31:21 +02:00
/**
* A configuration item that is any sort of primitive value (strings, numbers or null).
*/
2022-09-24 17:05:33 +02:00
class ValueNode : ConfigNode {
2022-09-23 22:34:46 +02:00
string value;
this() {
}
this(string value) {
this.value = value;
}
string nodeType() {
return "value";
}
2022-09-23 22:34:46 +02:00
}
2022-09-24 18:31:21 +02:00
/**
* A configuration item that is an object.
*
* ObjectNodes contain a node dictionary that points to other ConfigNodes.
*/
2022-09-24 17:05:33 +02:00
class ObjectNode : ConfigNode {
2022-09-23 22:34:46 +02:00
ConfigNode[string] children;
this() {
}
this(ConfigNode[string] children) {
this.children = children;
}
this(string[string] values) {
foreach (key, value; values) {
children[key] = new ValueNode(value);
}
}
string nodeType() {
return "object";
}
2022-09-23 22:34:46 +02:00
}
2022-09-24 18:31:21 +02:00
/**
* A configuration item that is an array.
*
* Contains other ConfigNodes as children.
*/
2022-09-24 17:05:33 +02:00
class ArrayNode : ConfigNode {
2022-09-23 22:34:46 +02:00
ConfigNode[] children;
this() {
}
this(ConfigNode[] children...) {
this.children = children;
}
2022-09-24 00:13:08 +02:00
this(string[] values...) {
foreach (string value; values) {
2022-09-24 00:15:16 +02:00
children ~= new ValueNode(value);
2022-09-24 00:13:08 +02:00
}
}
string nodeType() {
return "array";
}
2022-09-24 00:13:08 +02:00
}
private interface PathSegment {
2022-09-24 00:13:08 +02:00
}
private class ArrayPathSegment : PathSegment {
2022-09-24 00:13:08 +02:00
const size_t index;
this(const size_t index) {
this.index = index;
}
}
private class PropertyPathSegment : PathSegment {
const string propertyName;
this(const string propertyName) {
this.propertyName = propertyName;
}
}
private class ConfigPath {
2022-09-24 00:13:08 +02:00
private const string path;
private string[] previousSegments;
2022-09-24 00:13:08 +02:00
private string[] segments;
this(const string path) {
this.path = path;
segmentAndNormalize(path);
}
private void segmentAndNormalize(string path) {
foreach (segment; path.split(".")) {
2022-09-25 18:20:52 +02:00
auto trimmedSegment = segment.strip;
if (trimmedSegment.length <= 0) {
continue;
}
2022-09-25 18:20:52 +02:00
if (trimmedSegment.endsWith("]") && !trimmedSegment.startsWith("[")) {
auto openBracketPos = trimmedSegment.lastIndexOf("[");
if (openBracketPos != -1) {
2022-09-25 18:20:52 +02:00
segments ~= trimmedSegment[0 .. openBracketPos];
segments ~= trimmedSegment[openBracketPos .. $];
continue;
}
}
2022-09-25 18:20:52 +02:00
segments ~= trimmedSegment;
}
2022-09-24 00:13:08 +02:00
}
PathSegment getNextSegment() {
if (segments.length == 0) {
return null;
}
PathSegment ret(PathSegment segment) {
previousSegments ~= segments[0];
segments = segments[1 .. $];
2022-09-24 00:13:08 +02:00
return segment;
}
string segment = segments[0];
if (segment.startsWith("[") && segment.endsWith("]")) {
if (segment.length <= 2) {
throw new PathParseException("Path has array accessor but no index specified", path);
}
auto indexString = segment[1 .. $ - 1];
try {
auto index = indexString.to!size_t;
return ret(new ArrayPathSegment(index));
} catch (ConvException e) {
2022-09-24 17:05:33 +02:00
throw new PathParseException("Value '" ~ indexString ~ "' is not acceptable as an array index", path);
2022-09-24 00:13:08 +02:00
}
}
return ret(new PropertyPathSegment(segment));
2022-09-24 00:13:08 +02:00
}
string getCurrentPath() {
return previousSegments.join(".");
}
2022-09-23 22:34:46 +02:00
}
/**
* Used in a ConfigDictionary to enable to disable environment variable substitution.
*/
alias SubstituteEnvironmentVariables = Flag!"substituteEnvironmentVariables";
2022-09-24 18:31:21 +02:00
/**
* A ConfigDictionary contains the configuration tree and facilities to get values from that tree.
*/
2022-09-23 22:34:46 +02:00
class ConfigDictionary {
ConfigNode rootNode;
SubstituteEnvironmentVariables substituteEnvironmentVariables = SubstituteEnvironmentVariables
.yes;
2022-09-24 00:13:08 +02:00
this(SubstituteEnvironmentVariables substituteEnvironmentVariables = SubstituteEnvironmentVariables
.yes) {
this.substituteEnvironmentVariables = substituteEnvironmentVariables;
2022-09-24 17:05:33 +02:00
}
this(ConfigNode rootNode, SubstituteEnvironmentVariables substituteEnvironmentVariables = SubstituteEnvironmentVariables
.yes) {
2022-09-24 17:05:33 +02:00
this.rootNode = rootNode;
this.substituteEnvironmentVariables = substituteEnvironmentVariables;
2022-09-24 17:05:33 +02:00
}
2022-09-24 18:31:21 +02:00
/**
* Get values from the configuration using config path notation.
*
* Params:
* configPath = Path to the wanted config value. The path is separated by dots, e.g. "server.public.hostname".
* Values from arrays can be selected by brackets, for example: "server[3].hostname.ports[0]".
* When the config is just a value, for example just a string, it can be fetched by just specifying "." as path.
* Although the path should be universally the same over all types of config files, some might not lend to this structure,
* and have a more specific way of retrieving data from the config. See the examples and specific config factories for
* more details.
*
* Returns: The value at the path in the configuration. To convert it use get!T().
2022-09-24 18:31:21 +02:00
*/
2022-09-24 00:13:08 +02:00
string get(string configPath) {
2022-09-24 19:05:15 +02:00
auto path = new ConfigPath(configPath);
auto node = getNodeAt(path);
auto value = cast(ValueNode) node;
if (value) {
return substituteEnvironmentVariables ? substituteEnvVars(value) : value.value;
2022-09-24 19:05:15 +02:00
} else {
throw new ConfigReadException(
"Value expected but " ~ node.nodeType ~ " found at path: " ~ createExceptionPath(
path));
}
}
/**
* Get values from the configuration and attempts to convert them to the specified type.
*
* Params:
* configPath = Path to the wanted config value. See get().
* Returns: The value at the path in the configuration.
*/
ConvertToType get(ConvertToType)(string configPath) {
return get(configPath).to!ConvertToType;
}
2022-09-24 00:13:08 +02:00
2022-09-24 19:05:15 +02:00
/**
* Fetch a sub-section of the config as another config.
*
* Commonly used for example to fetch further configuration from arrays, e.g.: `getConfig("http.servers[3]")`
* which then returns the rest of the config at that path.
*
* Params:
* configPath = Path to the wanted config. See get().
* Returns: A sub-section of the configuration.
*/
ConfigDictionary getConfig(string configPath) {
2022-09-24 00:13:08 +02:00
auto path = new ConfigPath(configPath);
2022-09-24 19:05:15 +02:00
auto node = getNodeAt(path);
return new ConfigDictionary(node);
}
string createExceptionPath(ConfigPath path) {
return "'" ~ path.path ~ "' (at '" ~ path.getCurrentPath() ~ "')";
}
private ConfigNode getNodeAt(ConfigPath path) {
enforce!ConfigReadException(rootNode !is null, "The config is empty");
2022-09-24 00:13:08 +02:00
auto currentNode = rootNode;
PathSegment currentPathSegment = path.getNextSegment();
2022-09-24 02:18:49 +02:00
void throwPathNotExists() {
2022-09-24 19:05:15 +02:00
throw new ConfigReadException("Path does not exist: " ~ createExceptionPath(path));
2022-09-24 02:18:49 +02:00
}
void ifNotNullPointer(void* obj, void delegate() fn) {
if (obj) {
fn();
} else {
throwPathNotExists();
}
}
void ifNotNull(Object obj, void delegate() fn) {
if (obj) {
fn();
} else {
throwPathNotExists();
}
}
2022-09-24 00:13:08 +02:00
while (currentPathSegment !is null) {
if (currentNode is null) {
2022-09-24 02:18:49 +02:00
throwPathNotExists();
}
auto valueNode = cast(ValueNode) currentNode;
if (valueNode) {
2022-09-24 02:18:49 +02:00
throwPathNotExists();
}
2022-09-24 00:13:08 +02:00
auto arrayPath = cast(ArrayPathSegment) currentPathSegment;
if (arrayPath) {
2022-09-24 00:15:16 +02:00
auto arrayNode = cast(ArrayNode) currentNode;
2022-09-24 02:18:49 +02:00
ifNotNull(arrayNode, {
if (arrayNode.children.length < arrayPath.index) {
throw new ConfigReadException(
2022-09-24 19:05:15 +02:00
"Array index out of bounds: " ~ createExceptionPath(path));
}
2022-09-24 00:13:08 +02:00
currentNode = arrayNode.children[arrayPath.index];
2022-09-24 02:18:49 +02:00
});
2022-09-24 00:13:08 +02:00
}
auto propertyPath = cast(PropertyPathSegment) currentPathSegment;
if (propertyPath) {
auto objectNode = cast(ObjectNode) currentNode;
2022-09-24 02:18:49 +02:00
ifNotNull(objectNode, {
auto propertyNode = propertyPath.propertyName in objectNode.children;
2022-09-24 02:18:49 +02:00
ifNotNullPointer(propertyNode, {
currentNode = *propertyNode;
2022-09-24 02:18:49 +02:00
});
});
}
2022-09-24 00:13:08 +02:00
currentPathSegment = path.getNextSegment();
}
2022-09-24 19:05:15 +02:00
return currentNode;
}
2022-09-28 22:31:52 +02:00
private string substituteEnvVars(ValueNode valueNode) {
auto value = valueNode.value;
if (value == null) {
2022-09-28 22:35:10 +02:00
return value;
2022-09-28 22:31:52 +02:00
}
auto result = "";
auto isParsingEnvVar = false;
auto isParsingDefault = false;
auto envVarName = "";
auto defaultEnvVarValue = "";
void addEnvVarToResult() {
auto envVarValue = environment.get(envVarName);
if (envVarValue !is null) {
result ~= envVarValue;
} else {
if (defaultEnvVarValue.length == 0) {
throw new ConfigReadException(
"Environment variable not found: " ~ envVarName);
}
result ~= defaultEnvVarValue;
}
}
foreach (size_t i, char c; value) {
if (c == '$') {
isParsingEnvVar = true;
continue;
}
if (isParsingEnvVar) {
if (c == '{') {
continue;
}
if (c == '}') {
isParsingEnvVar = false;
isParsingDefault = false;
addEnvVarToResult();
envVarName = "";
defaultEnvVarValue = "";
continue;
}
if (isParsingDefault) {
defaultEnvVarValue ~= c;
continue;
}
if (c == ':') {
isParsingDefault = true;
continue;
}
envVarName ~= c;
continue;
}
result ~= c;
}
if (envVarName.length > 0) {
addEnvVarToResult();
}
return result;
}
2022-09-23 22:34:46 +02:00
}
2022-09-24 18:31:21 +02:00
/**
* The base class used by configuration factories for specific file types.
*/
2022-09-24 17:51:32 +02:00
abstract class ConfigFactory {
2022-09-24 18:31:21 +02:00
/**
* Loads a configuration from the specified path from disk.
*
* Params:
* path = Path to file. OS dependent, but UNIX paths are generally working.
* Returns: The parsed configuration.
*/
2022-09-24 17:51:32 +02:00
ConfigDictionary loadFile(string path) {
auto json = readText(path);
return parseConfig(json);
}
2022-09-24 18:31:21 +02:00
/**
* Parse configuration from the given string.
*
* Params:
* contents = Text contents of the config to be parsed.
* Returns: The parsed configuration.
*/
2022-09-24 17:05:33 +02:00
ConfigDictionary parseConfig(string contents);
}
ConfigDictionary loadConfig(const string configPath) {
auto extension = configPath.extension.toLower;
if (extension == ".json") {
return loadJsonConfig(configPath);
}
throw new ConfigCreationException(
"File extension '" ~ extension ~ "' is not recognized as a supported config file format. Please use a specific function to load it, such as 'loadJsonConfig()'");
}
2022-09-23 22:34:46 +02:00
version (unittest) {
2022-09-24 00:13:08 +02:00
import std.exception : assertThrown;
import std.math.operations : isClose;
2022-09-24 00:13:08 +02:00
2022-09-23 22:34:46 +02:00
@("Dictionary creation")
unittest {
2022-09-24 00:15:16 +02:00
auto root = new ObjectNode([
"english": new ArrayNode([new ValueNode("one"), new ValueNode("two")]),
"spanish": new ArrayNode(new ValueNode("uno"), new ValueNode("dos"))
2022-09-23 22:34:46 +02:00
]);
auto config = new ConfigDictionary();
config.rootNode = root;
2022-09-23 22:34:46 +02:00
}
2022-09-24 00:13:08 +02:00
@("Get value in config with empty root fails")
2022-09-24 00:13:08 +02:00
unittest {
auto config = new ConfigDictionary();
2022-09-24 00:13:08 +02:00
assertThrown!ConfigReadException(config.get("."));
2022-09-24 00:13:08 +02:00
}
@("Get value in root with empty path")
2022-09-24 00:13:08 +02:00
unittest {
auto config = new ConfigDictionary(new ValueNode("hehehe"));
2022-09-24 00:13:08 +02:00
assert(config.get("") == "hehehe");
2022-09-24 00:13:08 +02:00
}
@("Get value in root with just a dot")
2022-09-24 00:13:08 +02:00
unittest {
auto config = new ConfigDictionary(new ValueNode("yup"));
2022-09-24 00:13:08 +02:00
assert(config.get(".") == "yup");
2022-09-24 00:13:08 +02:00
}
@("Get value in root fails when root is not a value")
unittest {
auto config = new ConfigDictionary(new ArrayNode());
2022-09-24 00:13:08 +02:00
assertThrown!ConfigReadException(config.get("."));
2022-09-24 00:13:08 +02:00
}
@("Get array value from root")
unittest {
auto config = new ConfigDictionary(new ArrayNode("aap", "noot", "mies"));
2022-09-24 00:13:08 +02:00
assert(config.get("[0]") == "aap");
assert(config.get("[1]") == "noot");
assert(config.get("[2]") == "mies");
2022-09-24 00:13:08 +02:00
}
@("Get value from object at root")
unittest {
auto config = new ConfigDictionary(new ObjectNode([
"aap": "monkey",
"noot": "nut",
"mies": "mies" // It's a name!
])
);
assert(config.get("aap") == "monkey");
assert(config.get("noot") == "nut");
assert(config.get("mies") == "mies");
}
@("Get value from object in object")
unittest {
auto config = new ConfigDictionary(
new ObjectNode([
"server": new ObjectNode([
"port": "8080"
])
])
);
assert(config.get("server.port") == "8080");
}
@("Get value from array in object")
unittest {
auto config = new ConfigDictionary(
new ObjectNode([
"hostname": new ArrayNode(["google.com", "dlang.org"])
])
);
assert(config.get("hostname.[1]") == "dlang.org");
}
@("Exception is thrown when array out of bounds when fetching from root")
unittest {
auto config = new ConfigDictionary(
new ArrayNode([
"google.com", "dlang.org"
])
);
assertThrown!ConfigReadException(config.get("[5]"));
}
@("Exception is thrown when array out of bounds when fetching from object")
unittest {
auto config = new ConfigDictionary(
new ObjectNode([
"hostname": new ArrayNode(["google.com", "dlang.org"])
])
);
assertThrown!ConfigReadException(config.get("hostname.[5]"));
}
@("Exception is thrown when path does not exist")
unittest {
auto config = new ConfigDictionary(new ObjectNode(
[
"hostname": new ObjectNode(["cluster": new ValueNode("")])
])
);
assertThrown!ConfigReadException(config.get("hostname.cluster.spacey"));
}
@("Exception is thrown when given path terminates too early")
unittest {
auto config = new ConfigDictionary(new ObjectNode(
[
"hostname": new ObjectNode(["cluster": new ValueNode(null)])
])
);
assertThrown!ConfigReadException(config.get("hostname"));
}
2022-09-24 02:18:49 +02:00
@("Exception is thrown when given path does not exist because config is an array")
unittest {
auto config = new ConfigDictionary(new ArrayNode());
2022-09-24 02:18:49 +02:00
assertThrown!ConfigReadException(config.get("hostname"));
2022-09-24 02:18:49 +02:00
}
2022-09-24 02:27:41 +02:00
@("Get value from objects in array")
unittest {
auto config = new ConfigDictionary(new ArrayNode(
new ObjectNode(["wrong": "yes"]),
new ObjectNode(["wrong": "no"]),
new ObjectNode(["wrong": "very"]),
));
2022-09-24 02:27:41 +02:00
assert(config.get("[1].wrong") == "no");
2022-09-24 02:27:41 +02:00
}
@("Get value from config with mixed types")
unittest {
auto config = new ConfigDictionary(
new ObjectNode([
"uno": cast(ConfigNode) new ValueNode("one"),
"dos": cast(ConfigNode) new ArrayNode(["nope", "two"]),
"tres": cast(ConfigNode) new ObjectNode(["thisone": "three"])
])
);
2022-09-24 02:27:41 +02:00
assert(config.get("uno") == "one");
assert(config.get("dos.[1]") == "two");
assert(config.get("tres.thisone") == "three");
2022-09-24 02:27:41 +02:00
}
@("Ignore empty segments")
unittest {
auto config = new ConfigDictionary(
new ObjectNode(
[
"one": new ObjectNode(["two": new ObjectNode(["three": "four"])])
])
);
assert(config.get(".one..two...three....") == "four");
}
@("Support conventional array indexing notation")
unittest {
auto config = new ConfigDictionary(
new ObjectNode(
[
"one": new ObjectNode([
"two": new ArrayNode(["dino", "mino"])
])
])
);
assert(config.get("one.two[1]") == "mino");
}
@("Get and convert values")
unittest {
auto config = new ConfigDictionary(
new ObjectNode([
"uno": new ValueNode("1223"),
"dos": new ValueNode("true"),
"tres": new ValueNode("Hi you"),
"quatro": new ValueNode("1.3")
])
);
assert(config.get!int("uno") == 1223);
assert(config.get!bool("dos") == true);
assert(config.get!string("tres") == "Hi you");
assert(isClose(config.get!float("quatro"), 1.3));
}
2022-09-24 19:05:15 +02:00
@("Get config from array")
unittest {
auto configOne = new ConfigDictionary(new ObjectNode(
2022-09-24 19:05:15 +02:00
[
"servers": new ArrayNode([
new ObjectNode(["hostname": "lala.com"]),
new ObjectNode(["hostname": "lele.com"])
])
])
);
auto config = configOne.getConfig("servers[0]");
2022-09-24 19:05:15 +02:00
assert(config.get("hostname") == "lala.com");
}
2022-09-25 18:20:52 +02:00
@("Trim spaces in path segments")
unittest {
auto config = new ConfigDictionary(
2022-09-25 18:20:52 +02:00
new ObjectNode(["que": new ObjectNode(["pasa hombre": "not much"])])
);
assert(config.get(" que. pasa hombre ") == "not much");
2022-09-25 18:20:52 +02:00
}
@("Load configurations using the loadConfig convenience function")
unittest {
auto jsonConfig = loadConfig("testfiles/groot.json");
assert(jsonConfig.get("name") == "Groot");
assert(jsonConfig.get("traits[1]") == "tree");
assert(jsonConfig.get("age") == "8728");
assert(jsonConfig.get("taxNumber") == null);
}
2022-09-28 22:31:52 +02:00
@("Whitespace is preserved in values")
unittest {
auto config = new ConfigDictionary(new ObjectNode([
"bla": " blergh "
]));
assert(config.get("bla") == " blergh ");
}
2022-09-28 22:31:52 +02:00
2022-09-28 22:35:10 +02:00
@("Null value stays null, not string")
unittest {
auto config = new ConfigDictionary(new ValueNode(null));
assert(config.get(".") == null);
}
2022-09-28 22:31:52 +02:00
@("Read value from environment variable")
unittest {
environment["MIRAGE_CONFIG_TEST_ENV_VAR"] = "is set!";
environment["MIRAGE_CONFIG_TEST_ENV_VAR_TWO"] = "is ready!";
auto config = new ConfigDictionary(
new ObjectNode(
[
"withBrackets": new ValueNode("${MIRAGE_CONFIG_TEST_ENV_VAR}"),
"withoutBrackets": new ValueNode("$MIRAGE_CONFIG_TEST_ENV_VAR"),
"withWhiteSpace": new ValueNode(" ${MIRAGE_CONFIG_TEST_ENV_VAR} "),
"alsoWithWhiteSpace": new ValueNode(" $MIRAGE_CONFIG_TEST_ENV_VAR"),
"tooMuchWhiteSpace": new ValueNode("$MIRAGE_CONFIG_TEST_ENV_VAR "),
"notSet": new ValueNode("${MIRAGE_CONFIG_NOT_SET_TEST_ENV_VAR}"),
"withDefault": new ValueNode("$MIRAGE_CONFIG_NOT_SET_TEST_ENV_VAR:use default!"),
"withDefaultAndBrackets": new ValueNode(
"${MIRAGE_CONFIG_NOT_SET_TEST_ENV_VAR:use default!}"),
"megaMix": new ValueNode("${MIRAGE_CONFIG_TEST_ENV_VAR_TWO} ${MIRAGE_CONFIG_TEST_ENV_VAR} ${MIRAGE_CONFIG_NOT_SET_TEST_ENV_VAR:go}!"),
"typical": new ValueNode("${MIRAGE_CONFIG_TEST_HOSTNAME:localhost}:${MIRAGE_CONFIG_TEST_PORT:8080}"),
])
);
assert(config.get("withBrackets") == "is set!");
assert(config.get("withoutBrackets") == "is set!");
assert(config.get("withWhiteSpace") == " is set! ");
assert(config.get("alsoWithWhiteSpace") == " is set!");
assertThrown!Exception(config.get("tooMuchWhiteSpace")); // Environment variable not found (whitespace is included in env name)
assertThrown!Exception(config.get("notSet")); // Environment variable not found
assert(config.get("withDefault") == "use default!");
assert(config.get("withDefaultAndBrackets") == "use default!");
assert(config.get("megaMix") == "is ready! is set! go!");
assert(config.get("typical") == "localhost:8080");
}
@("Don't read value from environment variables when disabled")
unittest {
environment.remove("MIRAGE_CONFIG_TEST_ENV_VAR");
environment.remove("MIRAGE_CONFIG_TEST_ENV_VAR_TWO");
auto config = new ConfigDictionary(
new ObjectNode(
[
"withBrackets": new ValueNode("${MIRAGE_CONFIG_TEST_ENV_VAR}"),
"withoutBrackets": new ValueNode("$MIRAGE_CONFIG_TEST_ENV_VAR"),
"withWhiteSpace": new ValueNode(" ${MIRAGE_CONFIG_TEST_ENV_VAR} "),
"alsoWithWhiteSpace": new ValueNode(" $MIRAGE_CONFIG_TEST_ENV_VAR"),
"tooMuchWhiteSpace": new ValueNode("$MIRAGE_CONFIG_TEST_ENV_VAR "),
"notSet": new ValueNode("${MIRAGE_CONFIG_NOT_SET_TEST_ENV_VAR}"),
"withDefault": new ValueNode("$MIRAGE_CONFIG_NOT_SET_TEST_ENV_VAR:use default!"),
"withDefaultAndBrackets": new ValueNode(
"${MIRAGE_CONFIG_NOT_SET_TEST_ENV_VAR:use default!}"),
"megaMix": new ValueNode("${MIRAGE_CONFIG_TEST_ENV_VAR_TWO} ${MIRAGE_CONFIG_TEST_ENV_VAR} ${MIRAGE_CONFIG_NOT_SET_TEST_ENV_VAR:go}!"),
"typical": new ValueNode("${MIRAGE_CONFIG_TEST_HOSTNAME:localhost}:${MIRAGE_CONFIG_TEST_PORT:8080}"),
]),
SubstituteEnvironmentVariables.no
);
assert(config.get("withBrackets") == "${MIRAGE_CONFIG_TEST_ENV_VAR}");
assert(config.get("withoutBrackets") == "$MIRAGE_CONFIG_TEST_ENV_VAR");
assert(config.get("withWhiteSpace") == " ${MIRAGE_CONFIG_TEST_ENV_VAR} ");
assert(config.get("alsoWithWhiteSpace") == " $MIRAGE_CONFIG_TEST_ENV_VAR");
assert(config.get("tooMuchWhiteSpace") == "$MIRAGE_CONFIG_TEST_ENV_VAR ");
assert(config.get("notSet") == "${MIRAGE_CONFIG_NOT_SET_TEST_ENV_VAR}");
assert(config.get("withDefault") == "$MIRAGE_CONFIG_NOT_SET_TEST_ENV_VAR:use default!");
assert(config.get("withDefaultAndBrackets") == "${MIRAGE_CONFIG_NOT_SET_TEST_ENV_VAR:use default!}");
assert(config.get("megaMix") == "${MIRAGE_CONFIG_TEST_ENV_VAR_TWO} ${MIRAGE_CONFIG_TEST_ENV_VAR} ${MIRAGE_CONFIG_NOT_SET_TEST_ENV_VAR:go}!");
assert(config.get("typical") == "${MIRAGE_CONFIG_TEST_HOSTNAME:localhost}:${MIRAGE_CONFIG_TEST_PORT:8080}");
}
2022-09-23 22:34:46 +02:00
}