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:13:51 +02:00
import std.string : lineSplitter , strip , startsWith , endsWith , split , indexOf , join ;
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 20:32:33 +02:00
alias NormalizeQuotedValues = Flag ! "NormalizeQuotedValues" ;
2022-10-12 23:31:01 +02:00
alias SupportEqualsSeparator = Flag ! "SupportEqualsSeparator" ;
alias SupportColonSeparator = Flag ! "SupportColonSeparator" ;
2022-10-12 23:42:01 +02:00
alias SupportKeysWithoutValues = Flag ! "SupportKeysWithoutValues" ;
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 ,
2022-10-11 20:32:33 +02:00
SupportSections supportSections = SupportSections . no ,
2022-10-12 23:31:01 +02:00
NormalizeQuotedValues normalizeQuotedValues = NormalizeQuotedValues . no ,
SupportEqualsSeparator supportEqualsSeparator = SupportEqualsSeparator . no ,
2022-10-12 23:42:01 +02:00
SupportColonSeparator supportColonSeparator = SupportColonSeparator . no ,
SupportKeysWithoutValues supportKeysWithoutValues = SupportKeysWithoutValues . no
2022-10-11 19:49:32 +02:00
) : ConfigFactory {
2022-10-12 23:31:01 +02:00
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." ) ;
2022-10-12 23:31:01 +02:00
enforce ! ConfigCreationException ( supportEqualsSeparator | | supportColonSeparator , "No key/value separator is supported. Factory must set one either SupportEqualsSeparator or SupportColonSeparator" ) ;
2022-10-11 19:29:26 +02:00
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 ( ']' ) ) {
2022-10-11 20:13:51 +02:00
auto parsedSection = processedLine [ 1 . . $ - 1 ] ;
if ( parsedSection . startsWith ( '.' ) ) {
section ~ = parsedSection ;
} else {
section = parsedSection ;
}
2022-10-11 20:07:08 +02:00
continue ;
}
if ( processedLine . length = = 0 ) {
2022-10-11 19:49:32 +02:00
continue ;
2022-10-11 19:29:26 +02:00
}
2022-10-12 23:31:01 +02:00
char keyValueSplitter ;
if ( supportEqualsSeparator & & processedLine . indexOf ( '=' ) > = 0 ) {
keyValueSplitter = '=' ;
} else if ( supportColonSeparator & & processedLine . indexOf ( ':' ) > = 0 ) {
keyValueSplitter = ':' ;
}
auto parts = processedLine . split ( keyValueSplitter ) ;
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 ) ;
2022-10-12 23:42:01 +02:00
enforce ! ConfigCreationException ( supportKeysWithoutValues | | parts . length = = 2 , "Missing value assignment (L" ~ index
. to ! string ~ "): " ~ processedLine ) ;
auto value = supportKeysWithoutValues & & parts . length = = 1 ? "" : parts [ 1 ] . strip ;
2022-10-11 20:07:08 +02:00
2022-10-11 20:35:36 +02:00
if ( normalizeQuotedValues & &
value . length > 1 & &
( value . startsWith ( '"' ) | | value . startsWith ( '\'' ) ) & &
( value . endsWith ( '"' ) | | value . endsWith ( '\'' ) ) ) {
2022-10-11 20:32:33 +02:00
value = value [ 1 . . $ - 1 ] ;
}
2022-10-11 20:13:51 +02:00
auto key = [ section , parts [ 0 ] . strip ] . join ( '.' ) ;
2022-10-11 20:32:33 +02:00
properties . set ( key , value ) ;
2022-10-11 19:29:26 +02:00
}
return properties ;
}
}
version ( unittest ) {
import std.exception : assertThrown ;
import std.process : environment ;
2022-10-12 23:31:01 +02:00
class TestKeyValueConfigFactory : KeyValueConfigFactory ! (
SupportHashtagComments . no ,
SupportSemicolonComments . no ,
SupportSections . no ,
NormalizeQuotedValues . no ,
SupportEqualsSeparator . yes ,
2022-10-12 23:42:01 +02:00
SupportColonSeparator . no ,
SupportKeysWithoutValues . no
2022-10-12 23:31:01 +02:00
) {
2022-10-11 19:49:32 +02:00
}
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 {
2022-10-11 20:32:33 +02:00
auto config = new KeyValueConfigFactory ! (
SupportHashtagComments . yes ,
2022-10-12 23:31:01 +02:00
SupportSemicolonComments . yes ,
SupportSections . no ,
NormalizeQuotedValues . no ,
SupportEqualsSeparator . yes ,
SupportColonSeparator . no
2022-10-11 19:49:32 +02:00
) ( ) . 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 ( )
2022-10-11 20:32:33 +02:00
. parseConfig ( "answertolife" ) ) ;
2022-10-11 19:29:26 +02:00
}
2022-10-12 23:42:01 +02:00
@ ( "Succeed to parse when value assignment is missing and SupportKeysWithoutValues = yes" )
unittest {
auto config = new KeyValueConfigFactory ! (
SupportHashtagComments . no ,
SupportSemicolonComments . no ,
SupportSections . no ,
NormalizeQuotedValues . no ,
SupportEqualsSeparator . yes ,
SupportColonSeparator . no ,
SupportKeysWithoutValues . yes
) ( ) . parseConfig ( "answertolife" ) ;
assert ( config . get ( "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 20:32:33 +02:00
auto config = new KeyValueConfigFactory ! (
SupportHashtagComments . yes ,
2022-10-12 23:31:01 +02:00
SupportSemicolonComments . yes ,
SupportSections . no ,
NormalizeQuotedValues . no ,
SupportEqualsSeparator . yes ,
SupportColonSeparator . no
2022-10-11 20:32:33 +02:00
) ( ) . 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 {
2022-10-11 20:32:33 +02:00
auto config = new KeyValueConfigFactory ! (
SupportHashtagComments . no ,
2022-10-11 20:07:08 +02:00
SupportSemicolonComments . yes ,
2022-10-12 23:31:01 +02:00
SupportSections . yes ,
NormalizeQuotedValues . no ,
SupportEqualsSeparator . yes ,
SupportColonSeparator . no
2022-10-11 20:32:33 +02:00
) ( ) . parseConfig ( "
2022-10-11 20:07:08 +02:00
applicationName = test me !
[ server ]
host = localhost
port = 2873
2022-10-11 20:13:51 +02:00
[ . toaster ]
color = chrome
2022-10-11 20:07:08 +02:00
[ 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" ) ;
2022-10-11 20:13:51 +02:00
assert ( config . get ( "server.toaster.color" ) = = "chrome" ) ;
2022-10-11 20:07:08 +02:00
assert ( config . get ( "server.middleware.protocolServer" ) = = "netty" ) ;
assert ( config . get ( "database.driver.id" ) = = "PostgresDriver" ) ;
}
2022-10-11 20:32:33 +02:00
@ ( "Values with quotes are normalized and return the value within" )
unittest {
auto config = new KeyValueConfigFactory ! (
SupportHashtagComments . yes ,
SupportSemicolonComments . no ,
SupportSections . no ,
2022-10-12 23:31:01 +02:00
NormalizeQuotedValues . yes ,
SupportEqualsSeparator . yes ,
SupportColonSeparator . no
2022-10-11 20:32:33 +02:00
) ( ) . parseConfig ( "
baboon = \ "ape\"
monkey = ' ape '
human = ape
excessiveWhitespace = ' '
2022-10-11 20:35:36 +02:00
breaksWithComments = ' # Don ' t do this '
2022-10-11 20:32:33 +02:00
" ) ;
assert ( config . get ( "baboon" ) = = "ape" ) ;
assert ( config . get ( "monkey" ) = = "ape" ) ;
assert ( config . get ( "human" ) = = "ape" ) ;
assert ( config . get ( "excessiveWhitespace" ) = = " " ) ;
2022-10-11 20:35:36 +02:00
assert ( config . get ( "breaksWithComments" ) = = "'" ) ;
2022-10-11 20:32:33 +02:00
}
2022-10-12 23:31:01 +02:00
@ ( "Support colon as key/value separator" )
unittest {
auto config = new KeyValueConfigFactory ! (
SupportHashtagComments . no ,
SupportSemicolonComments . no ,
SupportSections . no ,
NormalizeQuotedValues . no ,
SupportEqualsSeparator . yes ,
SupportColonSeparator . yes
) ( ) . parseConfig ( "
one = here
two : also here
" ) ;
assert ( config . get ( "one" ) = = "here" ) ;
assert ( config . get ( "two" ) = = "also here" ) ;
assertThrown ! ConfigCreationException ( new KeyValueConfigFactory ! ( ) ( ) . parseConfig ( "a=b" ) ) ; // No separator is configured
}
2022-10-11 19:29:26 +02:00
}