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 .
* /
2022-09-24 15:58:49 +02:00
module mirage.config ;
2022-09-23 22:34:46 +02:00
2022-09-24 00:13:08 +02:00
import std.exception : enforce ;
2022-09-25 18:38:37 +02:00
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 ;
2022-09-25 18:38:37 +02:00
import std.path : extension ;
2022-09-28 22:31:52 +02:00
import std.process : environment ;
2022-09-28 22:49:08 +02:00
import std.typecons : Flag ;
2022-09-25 18:38:37 +02:00
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-28 23:38:54 +02:00
/ * *
* Used by ConfigDictionary when the supplied path does not exist .
* /
class ConfigPathNotFoundException : 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 {
2022-09-24 01:55:44 +02:00
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 ;
}
2022-09-24 01:55:44 +02:00
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 ;
}
2022-09-24 01:07:31 +02:00
this ( string [ string ] values ) {
foreach ( key , value ; values ) {
children [ key ] = new ValueNode ( value ) ;
}
}
2022-09-24 01:55:44 +02:00
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
}
}
2022-09-24 01:55:44 +02:00
string nodeType ( ) {
return "array" ;
}
2022-09-24 00:13:08 +02:00
}
2022-09-24 16:05:47 +02:00
private interface PathSegment {
2022-09-24 00:13:08 +02:00
}
2022-09-24 16:05:47 +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 ;
}
}
2022-09-24 16:05:47 +02:00
private class PropertyPathSegment : PathSegment {
2022-09-24 01:07:31 +02:00
const string propertyName ;
this ( const string propertyName ) {
this . propertyName = propertyName ;
}
}
2022-09-24 16:05:47 +02:00
private class ConfigPath {
2022-09-24 00:13:08 +02:00
private const string path ;
2022-09-24 01:55:44 +02:00
private string [ ] previousSegments ;
2022-09-24 00:13:08 +02:00
private string [ ] segments ;
this ( const string path ) {
this . path = path ;
2022-09-24 02:46:19 +02:00
segmentAndNormalize ( path ) ;
}
2022-09-24 02:32:30 +02:00
2022-09-24 02:46:19 +02:00
private void segmentAndNormalize ( string path ) {
2022-09-24 02:32:30 +02:00
foreach ( segment ; path . split ( "." ) ) {
2022-09-25 18:20:52 +02:00
auto trimmedSegment = segment . strip ;
if ( trimmedSegment . length < = 0 ) {
2022-09-24 02:46:19 +02:00
continue ;
}
2022-09-25 18:20:52 +02:00
if ( trimmedSegment . endsWith ( "]" ) & & ! trimmedSegment . startsWith ( "[" ) ) {
auto openBracketPos = trimmedSegment . lastIndexOf ( "[" ) ;
2022-09-24 02:46:19 +02:00
if ( openBracketPos ! = - 1 ) {
2022-09-25 18:20:52 +02:00
segments ~ = trimmedSegment [ 0 . . openBracketPos ] ;
segments ~ = trimmedSegment [ openBracketPos . . $ ] ;
2022-09-24 02:46:19 +02:00
continue ;
}
2022-09-24 02:32:30 +02:00
}
2022-09-24 02:46:19 +02:00
2022-09-25 18:20:52 +02:00
segments ~ = trimmedSegment ;
2022-09-24 02:32:30 +02:00
}
2022-09-24 00:13:08 +02:00
}
PathSegment getNextSegment ( ) {
if ( segments . length = = 0 ) {
return null ;
}
PathSegment ret ( PathSegment segment ) {
2022-09-24 01:55:44 +02:00
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
}
}
2022-09-24 01:07:31 +02:00
return ret ( new PropertyPathSegment ( segment ) ) ;
2022-09-24 00:13:08 +02:00
}
2022-09-24 01:55:44 +02:00
string getCurrentPath ( ) {
return previousSegments . join ( "." ) ;
}
2022-09-23 22:34:46 +02:00
}
2022-09-28 22:49:08 +02:00
/ * *
* Used in a ConfigDictionary to enable to disable environment variable substitution .
* /
2022-09-29 00:31:10 +02:00
alias SubstituteEnvironmentVariables = Flag ! "SubstituteEnvironmentVariables" ;
/ * *
* Used in a ConfigDictionary to enable to disable config path substitution .
* /
alias SubstituteConfigVariables = Flag ! "SubstituteConfigVariables" ;
2022-09-28 22:49:08 +02:00
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 ;
2022-09-28 22:49:08 +02:00
SubstituteEnvironmentVariables substituteEnvironmentVariables = SubstituteEnvironmentVariables
. yes ;
2022-09-29 00:31:10 +02:00
SubstituteConfigVariables substituteConfigVariables = SubstituteConfigVariables . yes ;
2022-09-24 00:13:08 +02:00
2022-09-28 22:49:08 +02:00
this ( SubstituteEnvironmentVariables substituteEnvironmentVariables = SubstituteEnvironmentVariables
2022-09-29 00:31:10 +02:00
. yes , SubstituteConfigVariables substituteConfigVariables = SubstituteConfigVariables
2022-09-28 22:49:08 +02:00
. yes ) {
this . substituteEnvironmentVariables = substituteEnvironmentVariables ;
2022-09-29 00:31:10 +02:00
this . substituteConfigVariables = substituteConfigVariables ;
2022-09-24 17:05:33 +02:00
}
2022-09-28 22:49:08 +02:00
this ( ConfigNode rootNode , SubstituteEnvironmentVariables substituteEnvironmentVariables = SubstituteEnvironmentVariables
2022-09-29 00:31:10 +02:00
. yes , SubstituteConfigVariables substituteConfigVariables = SubstituteConfigVariables
2022-09-28 22:49:08 +02:00
. yes ) {
2022-09-29 00:31:10 +02:00
this ( substituteEnvironmentVariables , substituteConfigVariables ) ;
2022-09-24 17:05:33 +02:00
this . rootNode = rootNode ;
}
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 .
2022-09-28 23:38:54 +02:00
* defaultValue = ( Optional ) Value to return when the given configPath is invalid . When not supplied a ConfigPathNotFoundException exception is thrown .
*
* Throws : ConfigReadException when something goes wrong reading the config .
* ConfigPathNotFoundException when the given path does not exist in the config .
2022-09-24 18:31:21 +02:00
*
2022-09-24 18:42:29 +02:00
* 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-28 23:38:54 +02:00
string get ( string configPath , string defaultValue = null ) {
try {
auto path = new ConfigPath ( configPath ) ;
auto node = getNodeAt ( path ) ;
auto value = cast ( ValueNode ) node ;
if ( value ) {
2022-09-29 00:31:10 +02:00
if ( substituteEnvironmentVariables | | substituteConfigVariables ) {
return substituteVariables ( value ) ;
} else {
return value . value ;
}
2022-09-28 23:38:54 +02:00
} else {
throw new ConfigReadException (
"Value expected but " ~ node . nodeType ~ " found at path: " ~ createExceptionPath (
path ) ) ;
}
} catch ( ConfigPathNotFoundException e ) {
if ( defaultValue ! is null ) {
return defaultValue ;
}
throw e ;
2022-09-24 19:05:15 +02:00
}
}
/ * *
* Get values from the configuration and attempts to convert them to the specified type .
*
* Params :
2022-09-28 23:38:54 +02:00
* configPath = Path to the wanted config value . See get ( ) .
*
* Throws : ConfigReadException when something goes wrong reading the config .
* ConfigPathNotFoundException when the given path does not exist in the config .
*
2022-09-24 19:05:15 +02:00
* Returns : The value at the path in the configuration .
2022-09-28 23:38:54 +02:00
* See_Also : get
2022-09-24 19:05:15 +02:00
* /
ConvertToType get ( ConvertToType ) ( string configPath ) {
return get ( configPath ) . to ! ConvertToType ;
}
2022-09-24 00:13:08 +02:00
2022-09-28 23:38:54 +02:00
/ * *
* Get values from the configuration and attempts to convert them to the specified type .
*
* Params :
* configPath = Path to the wanted config value . See get ( ) .
* defaultValue = ( Optional ) Value to return when the given configPath is invalid . When not supplied a ConfigPathNotFoundException exception is thrown .
*
* Throws : ConfigReadException when something goes wrong reading the config .
* ConfigPathNotFoundException when the given path does not exist in the config .
*
* Returns : The value at the path in the configuration .
* See_Also : get
* /
ConvertToType get ( ConvertToType ) ( string configPath , ConvertToType defaultValue ) {
try {
return get ( configPath ) . to ! ConvertToType ;
} catch ( ConfigPathNotFoundException e ) {
return defaultValue ;
}
}
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 ) {
2022-09-28 23:38:54 +02:00
void throwPathNotFound ( ) {
throw new ConfigPathNotFoundException (
"Path does not exist: " ~ createExceptionPath ( path ) ) ;
}
if ( rootNode is null ) {
throwPathNotFound ( ) ;
}
2022-09-24 19:05:15 +02:00
2022-09-24 00:13:08 +02:00
auto currentNode = rootNode ;
PathSegment currentPathSegment = path . getNextSegment ( ) ;
2022-09-24 02:18:49 +02:00
void ifNotNullPointer ( void * obj , void delegate ( ) fn ) {
if ( obj ) {
fn ( ) ;
} else {
2022-09-28 23:38:54 +02:00
throwPathNotFound ( ) ;
2022-09-24 02:18:49 +02:00
}
}
void ifNotNull ( Object obj , void delegate ( ) fn ) {
if ( obj ) {
fn ( ) ;
} else {
2022-09-28 23:38:54 +02:00
throwPathNotFound ( ) ;
2022-09-24 02:18:49 +02:00
}
}
2022-09-24 00:13:08 +02:00
while ( currentPathSegment ! is null ) {
2022-09-24 01:55:44 +02:00
if ( currentNode is null ) {
2022-09-28 23:38:54 +02:00
throwPathNotFound ( ) ;
2022-09-24 01:55:44 +02:00
}
auto valueNode = cast ( ValueNode ) currentNode ;
if ( valueNode ) {
2022-09-28 23:38:54 +02:00
throwPathNotFound ( ) ;
2022-09-24 01:55:44 +02:00
}
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 , {
2022-09-24 01:07:31 +02:00
if ( arrayNode . children . length < arrayPath . index ) {
2022-09-24 01:55:44 +02:00
throw new ConfigReadException (
2022-09-24 19:05:15 +02:00
"Array index out of bounds: " ~ createExceptionPath ( path ) ) ;
2022-09-24 01:07:31 +02:00
}
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
}
2022-09-24 01:07:31 +02:00
auto propertyPath = cast ( PropertyPathSegment ) currentPathSegment ;
if ( propertyPath ) {
auto objectNode = cast ( ObjectNode ) currentNode ;
2022-09-24 02:18:49 +02:00
ifNotNull ( objectNode , {
2022-09-24 01:07:31 +02:00
auto propertyNode = propertyPath . propertyName in objectNode . children ;
2022-09-24 02:18:49 +02:00
ifNotNullPointer ( propertyNode , {
2022-09-24 01:07:31 +02:00
currentNode = * propertyNode ;
2022-09-24 02:18:49 +02:00
} ) ;
} ) ;
2022-09-24 01:07:31 +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-24 18:42:29 +02:00
}
2022-09-28 22:31:52 +02:00
2022-09-29 00:31:10 +02:00
private string substituteVariables ( ValueNode valueNode ) {
2022-09-28 22:31:52 +02:00
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 ;
2022-09-29 00:31:10 +02:00
auto varName = "" ;
auto defaultVarValue = "" ;
void addVarValueToResult ( ) {
string [ ] exceptionMessageParts ;
if ( substituteEnvironmentVariables ) {
exceptionMessageParts ~ = "environment variable" ;
auto envVarValue = environment . get ( varName ) ;
if ( envVarValue ! is null ) {
result ~ = envVarValue ;
return ;
}
}
2022-09-28 22:31:52 +02:00
2022-09-29 00:31:10 +02:00
if ( substituteConfigVariables ) {
exceptionMessageParts ~ = "config path" ;
auto configValue = get ( varName , defaultVarValue ) ;
if ( configValue . length > 0 ) {
result ~ = configValue ;
return ;
2022-09-28 22:31:52 +02:00
}
2022-09-29 00:31:10 +02:00
}
2022-09-28 22:31:52 +02:00
2022-09-29 00:31:10 +02:00
if ( defaultVarValue . length > 0 ) {
result ~ = defaultVarValue ;
return ;
2022-09-28 22:31:52 +02:00
}
2022-09-29 00:31:10 +02:00
auto exceptionMessageComponents = exceptionMessageParts . join ( " or " ) ;
throw new ConfigReadException (
"No substitution found for " ~ exceptionMessageComponents ~ ": " ~ varName ) ;
2022-09-28 22:31:52 +02:00
}
foreach ( size_t i , char c ; value ) {
if ( c = = '$' ) {
isParsingEnvVar = true ;
continue ;
}
if ( isParsingEnvVar ) {
if ( c = = '{' ) {
continue ;
}
if ( c = = '}' ) {
isParsingEnvVar = false ;
isParsingDefault = false ;
2022-09-29 00:31:10 +02:00
addVarValueToResult ( ) ;
varName = "" ;
defaultVarValue = "" ;
2022-09-28 22:31:52 +02:00
continue ;
}
if ( isParsingDefault ) {
2022-09-29 00:31:10 +02:00
defaultVarValue ~ = c ;
2022-09-28 22:31:52 +02:00
continue ;
}
if ( c = = ':' ) {
isParsingDefault = true ;
continue ;
}
2022-09-29 00:31:10 +02:00
varName ~ = c ;
2022-09-28 22:31:52 +02:00
continue ;
}
result ~ = c ;
}
2022-09-29 00:31:10 +02:00
if ( varName . length > 0 ) {
addVarValueToResult ( ) ;
2022-09-28 22:31:52 +02:00
}
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 ) ;
2022-09-24 16:05:47 +02:00
}
2022-09-25 18:38:37 +02:00
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 ;
2022-09-24 18:42:29 +02:00
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
] ) ;
2022-09-25 18:23:27 +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
2022-09-25 18:23:27 +02:00
@ ( "Get value in config with empty root fails" )
2022-09-24 00:13:08 +02:00
unittest {
2022-09-25 18:23:27 +02:00
auto config = new ConfigDictionary ( ) ;
2022-09-24 00:13:08 +02:00
2022-09-28 23:38:54 +02:00
assertThrown ! ConfigPathNotFoundException ( config . get ( "." ) ) ;
2022-09-24 00:13:08 +02:00
}
2022-09-24 16:09:20 +02:00
@ ( "Get value in root with empty path" )
2022-09-24 00:13:08 +02:00
unittest {
2022-09-25 18:23:27 +02:00
auto config = new ConfigDictionary ( new ValueNode ( "hehehe" ) ) ;
2022-09-24 00:13:08 +02:00
2022-09-25 18:23:27 +02:00
assert ( config . get ( "" ) = = "hehehe" ) ;
2022-09-24 00:13:08 +02:00
}
2022-09-24 16:09:20 +02:00
@ ( "Get value in root with just a dot" )
2022-09-24 00:13:08 +02:00
unittest {
2022-09-25 18:23:27 +02:00
auto config = new ConfigDictionary ( new ValueNode ( "yup" ) ) ;
2022-09-24 00:13:08 +02:00
2022-09-25 18:23:27 +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 {
2022-09-25 18:23:27 +02:00
auto config = new ConfigDictionary ( new ArrayNode ( ) ) ;
2022-09-24 00:13:08 +02:00
2022-09-25 18:23:27 +02:00
assertThrown ! ConfigReadException ( config . get ( "." ) ) ;
2022-09-24 00:13:08 +02:00
}
@ ( "Get array value from root" )
unittest {
2022-09-25 18:23:27 +02:00
auto config = new ConfigDictionary ( new ArrayNode ( "aap" , "noot" , "mies" ) ) ;
2022-09-24 00:13:08 +02:00
2022-09-25 18:23:27 +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
}
2022-09-24 01:07:31 +02:00
@ ( "Get value from object at root" )
unittest {
2022-09-25 18:23:27 +02:00
auto config = new ConfigDictionary ( new ObjectNode ( [
2022-09-24 18:49:38 +02:00
"aap" : "monkey" ,
"noot" : "nut" ,
"mies" : "mies" // It's a name!
] )
) ;
2022-09-24 01:07:31 +02:00
2022-09-25 18:23:27 +02:00
assert ( config . get ( "aap" ) = = "monkey" ) ;
assert ( config . get ( "noot" ) = = "nut" ) ;
assert ( config . get ( "mies" ) = = "mies" ) ;
2022-09-24 01:07:31 +02:00
}
2022-09-24 01:55:44 +02:00
@ ( "Get value from object in object" )
unittest {
2022-09-25 18:23:27 +02:00
auto config = new ConfigDictionary (
2022-09-24 18:49:38 +02:00
new ObjectNode ( [
"server" : new ObjectNode ( [
"port" : "8080"
] )
2022-09-24 01:55:44 +02:00
] )
2022-09-24 18:49:38 +02:00
) ;
2022-09-24 01:55:44 +02:00
2022-09-25 18:23:27 +02:00
assert ( config . get ( "server.port" ) = = "8080" ) ;
2022-09-24 01:55:44 +02:00
}
@ ( "Get value from array in object" )
unittest {
2022-09-25 18:23:27 +02:00
auto config = new ConfigDictionary (
2022-09-24 18:49:38 +02:00
new ObjectNode ( [
"hostname" : new ArrayNode ( [ "google.com" , "dlang.org" ] )
] )
) ;
2022-09-24 01:55:44 +02:00
2022-09-25 18:23:27 +02:00
assert ( config . get ( "hostname.[1]" ) = = "dlang.org" ) ;
2022-09-24 01:55:44 +02:00
}
@ ( "Exception is thrown when array out of bounds when fetching from root" )
unittest {
2022-09-25 18:23:27 +02:00
auto config = new ConfigDictionary (
2022-09-24 18:49:38 +02:00
new ArrayNode ( [
"google.com" , "dlang.org"
] )
) ;
2022-09-24 01:55:44 +02:00
2022-09-25 18:23:27 +02:00
assertThrown ! ConfigReadException ( config . get ( "[5]" ) ) ;
2022-09-24 01:55:44 +02:00
}
@ ( "Exception is thrown when array out of bounds when fetching from object" )
unittest {
2022-09-25 18:23:27 +02:00
auto config = new ConfigDictionary (
2022-09-24 18:49:38 +02:00
new ObjectNode ( [
"hostname" : new ArrayNode ( [ "google.com" , "dlang.org" ] )
] )
) ;
2022-09-24 01:55:44 +02:00
2022-09-25 18:23:27 +02:00
assertThrown ! ConfigReadException ( config . get ( "hostname.[5]" ) ) ;
2022-09-24 01:55:44 +02:00
}
@ ( "Exception is thrown when path does not exist" )
unittest {
2022-09-25 18:23:27 +02:00
auto config = new ConfigDictionary ( new ObjectNode (
2022-09-24 18:49:38 +02:00
[
"hostname" : new ObjectNode ( [ "cluster" : new ValueNode ( "" ) ] )
] )
) ;
2022-09-24 01:55:44 +02:00
2022-09-28 23:38:54 +02:00
assertThrown ! ConfigPathNotFoundException ( config . get ( "hostname.cluster.spacey" ) ) ;
2022-09-24 01:55:44 +02:00
}
@ ( "Exception is thrown when given path terminates too early" )
unittest {
2022-09-25 18:23:27 +02:00
auto config = new ConfigDictionary ( new ObjectNode (
2022-09-24 18:49:38 +02:00
[
"hostname" : new ObjectNode ( [ "cluster" : new ValueNode ( null ) ] )
] )
) ;
2022-09-24 01:55:44 +02:00
2022-09-25 18:23:27 +02:00
assertThrown ! ConfigReadException ( config . get ( "hostname" ) ) ;
2022-09-24 01:55:44 +02:00
}
2022-09-24 02:18:49 +02:00
@ ( "Exception is thrown when given path does not exist because config is an array" )
unittest {
2022-09-25 18:23:27 +02:00
auto config = new ConfigDictionary ( new ArrayNode ( ) ) ;
2022-09-24 02:18:49 +02:00
2022-09-28 23:38:54 +02:00
assertThrown ! ConfigPathNotFoundException ( 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 {
2022-09-25 18:23:27 +02:00
auto config = new ConfigDictionary ( new ArrayNode (
2022-09-24 18:49:38 +02:00
new ObjectNode ( [ "wrong" : "yes" ] ) ,
new ObjectNode ( [ "wrong" : "no" ] ) ,
new ObjectNode ( [ "wrong" : "very" ] ) ,
) ) ;
2022-09-24 02:27:41 +02:00
2022-09-25 18:23:27 +02:00
assert ( config . get ( "[1].wrong" ) = = "no" ) ;
2022-09-24 02:27:41 +02:00
}
@ ( "Get value from config with mixed types" )
unittest {
2022-09-25 18:23:27 +02:00
auto config = new ConfigDictionary (
2022-09-24 18:49:38 +02:00
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
2022-09-25 18:23:27 +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
}
2022-09-24 02:32:30 +02:00
@ ( "Ignore empty segments" )
unittest {
2022-09-25 18:23:27 +02:00
auto config = new ConfigDictionary (
2022-09-24 18:49:38 +02:00
new ObjectNode (
[
2022-09-24 02:32:30 +02:00
"one" : new ObjectNode ( [ "two" : new ObjectNode ( [ "three" : "four" ] ) ] )
2022-09-24 18:49:38 +02:00
] )
) ;
2022-09-24 02:32:30 +02:00
2022-09-25 18:23:27 +02:00
assert ( config . get ( ".one..two...three...." ) = = "four" ) ;
2022-09-24 02:32:30 +02:00
}
2022-09-24 02:46:19 +02:00
@ ( "Support conventional array indexing notation" )
unittest {
2022-09-25 18:23:27 +02:00
auto config = new ConfigDictionary (
2022-09-24 18:49:38 +02:00
new ObjectNode (
[
"one" : new ObjectNode ( [
"two" : new ArrayNode ( [ "dino" , "mino" ] )
] )
] )
) ;
2022-09-24 02:46:19 +02:00
2022-09-25 18:23:27 +02:00
assert ( config . get ( "one.two[1]" ) = = "mino" ) ;
2022-09-24 02:46:19 +02:00
}
2022-09-24 18:42:29 +02:00
@ ( "Get and convert values" )
unittest {
2022-09-25 18:23:27 +02:00
auto config = new ConfigDictionary (
2022-09-24 18:49:38 +02:00
new ObjectNode ( [
"uno" : new ValueNode ( "1223" ) ,
"dos" : new ValueNode ( "true" ) ,
"tres" : new ValueNode ( "Hi you" ) ,
"quatro" : new ValueNode ( "1.3" )
] )
) ;
2022-09-24 18:42:29 +02:00
2022-09-25 18:23:27 +02:00
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 18:42:29 +02:00
}
2022-09-24 19:05:15 +02:00
@ ( "Get config from array" )
unittest {
2022-09-25 18:23:27 +02:00
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" ] )
] )
] )
) ;
2022-09-25 18:23:27 +02:00
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 {
2022-09-25 18:23:27 +02:00
auto config = new ConfigDictionary (
2022-09-25 18:20:52 +02:00
new ObjectNode ( [ "que" : new ObjectNode ( [ "pasa hombre" : "not much" ] ) ] )
) ;
2022-09-25 18:23:27 +02:00
assert ( config . get ( " que. pasa hombre " ) = = "not much" ) ;
2022-09-25 18:20:52 +02:00
}
2022-09-25 18:38:37 +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
2022-09-28 22:33:23 +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}" ) ,
2022-09-29 00:31:10 +02:00
] ) ,
SubstituteEnvironmentVariables . yes ,
SubstituteConfigVariables . no
2022-09-28 22:31:52 +02:00
) ;
assert ( config . get ( "withBrackets" ) = = "is set!" ) ;
assert ( config . get ( "withoutBrackets" ) = = "is set!" ) ;
assert ( config . get ( "withWhiteSpace" ) = = " is set! " ) ;
assert ( config . get ( "alsoWithWhiteSpace" ) = = " is set!" ) ;
2022-09-29 00:31:10 +02:00
assertThrown ! ConfigReadException ( config . get ( "tooMuchWhiteSpace" ) ) ; // Environment variable not found (whitespace is included in env name)
assertThrown ! ConfigReadException ( config . get ( "notSet" ) ) ; // Environment variable not found
2022-09-28 22:31:52 +02:00
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" ) ;
}
2022-09-28 22:49:08 +02:00
@ ( "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}" ) ,
] ) ,
2022-09-29 00:31:10 +02:00
SubstituteEnvironmentVariables . no ,
SubstituteConfigVariables . no
2022-09-28 22:49:08 +02:00
) ;
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!" ) ;
2022-09-28 23:38:54 +02:00
assert ( config . get (
"withDefaultAndBrackets" ) = = "${MIRAGE_CONFIG_NOT_SET_TEST_ENV_VAR:use default!}" ) ;
2022-09-28 22:49:08 +02:00
assert ( config . get ( "megaMix" ) = = "${MIRAGE_CONFIG_TEST_ENV_VAR_TWO} ${MIRAGE_CONFIG_TEST_ENV_VAR} ${MIRAGE_CONFIG_NOT_SET_TEST_ENV_VAR:go}!" ) ;
2022-09-28 23:38:54 +02:00
assert ( config . get (
"typical" ) = = "${MIRAGE_CONFIG_TEST_HOSTNAME:localhost}:${MIRAGE_CONFIG_TEST_PORT:8080}" ) ;
2022-09-28 22:49:08 +02:00
}
2022-09-28 23:38:54 +02:00
@ ( "Get with default should return default" )
unittest {
auto config = new ConfigDictionary ( ) ;
assert ( config . get ( "la.la.la" , "not there" ) = = "not there" ) ;
assert ( config . get ! int ( "do.re.mi.fa.so" , 42 ) = = 42 ) ;
}
2022-09-29 00:31:10 +02:00
@ ( "Substitute values from other config paths" )
unittest {
auto config = new ConfigDictionary (
new ObjectNode ( [
"greet" : cast ( ConfigNode ) new ObjectNode ( [
"env" : "Hi"
] ) ,
"hi" : cast ( ConfigNode ) new ValueNode ( "${greet.env} there!" ) ,
"oi" : cast ( ConfigNode ) new ValueNode ( "${path.does.not.exist}" ) ,
] ) ,
SubstituteEnvironmentVariables . no ,
SubstituteConfigVariables . yes
) ;
assert ( config . get ( "hi" ) = = "Hi there!" ) ;
assertThrown ! ConfigReadException ( config . get ( "oi" ) ) ;
}
@ ( "Do not substitute values from other config paths when disabled" )
unittest {
auto config = new ConfigDictionary (
new ObjectNode ( [
"greet" : cast ( ConfigNode ) new ObjectNode ( [
"env" : "Hi"
] ) ,
"hi" : cast ( ConfigNode ) new ValueNode ( "${greet.env} there!" ) ,
"oi" : cast ( ConfigNode ) new ValueNode ( "${path.does.not.exist}" ) ,
] ) ,
SubstituteEnvironmentVariables . no ,
SubstituteConfigVariables . no
) ;
assert ( config . get ( "greet.env" ) = = "Hi" ) ;
assert ( config . get ( "hi" ) = = "${greet.env} there!" ) ;
assert ( config . get ( "oi" ) = = "${path.does.not.exist}" ) ;
}
@ ( "substitute values from both environment variables and config paths" )
unittest {
environment [ "MIRAGE_CONFIG_TEST_ENV_VAR_THREE" ] = "punch" ;
auto config = new ConfigDictionary (
new ObjectNode ( [
"one" : new ValueNode ( "${MIRAGE_CONFIG_TEST_ENV_VAR_THREE}" ) ,
"two" : new ValueNode ( "${one}" ) ,
] ) ,
SubstituteEnvironmentVariables . yes ,
SubstituteConfigVariables . yes
) ;
assert ( config . get ( "two" ) = = "punch" ) ;
}
2022-09-28 23:38:54 +02:00
//TODO: Test null nodes should gracefully fail
2022-09-23 22:34:46 +02:00
}