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
2023-01-11 00:06:41 +01:00
* Copyright : 2022 - 2023 Mike Bierlee
2022-10-11 19:29:26 +02:00
* 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" ;
2022-10-12 23:53:50 +02:00
alias SupportExclamationComments = Flag ! "SupportExclamationComments" ;
2022-10-11 20:07:08 +02:00
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-13 20:14:25 +02:00
alias SupportMultilineValues = Flag ! "SupportMultilineValues" ;
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-12 23:53:50 +02:00
SupportExclamationComments supportExclamationComments = SupportExclamationComments . 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 ,
2022-10-13 20:14:25 +02:00
SupportKeysWithoutValues supportKeysWithoutValues = SupportKeysWithoutValues . no ,
SupportMultilineValues supportMultilineValues = SupportMultilineValues . 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-13 20:14:25 +02:00
string key = null ;
string valueBuffer = "" ;
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
2022-10-12 23:53:50 +02:00
void replaceComments ( bool isTypeSupported , char commentToken ) {
if ( isTypeSupported ) {
auto commentPosition = processedLine . indexOf ( commentToken ) ;
if ( commentPosition > = 0 ) {
processedLine = processedLine [ 0 . . commentPosition ] ;
}
2022-10-11 19:49:32 +02:00
}
2022-10-11 19:29:26 +02:00
}
2022-10-12 23:53:50 +02:00
replaceComments ( supportHashtagComments , '#' ) ;
replaceComments ( supportSemicolonComments , ';' ) ;
replaceComments ( supportExclamationComments , '!' ) ;
2022-10-11 19:49:32 +02:00
2022-10-11 20:07:08 +02:00
processedLine = processedLine . strip ;
2022-10-13 20:14:25 +02:00
if ( supportSections & &
key is null & &
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-13 20:14:25 +02:00
string value ;
2022-10-12 23:31:01 +02:00
2022-10-13 20:14:25 +02:00
if ( key is null ) {
char keyValueSplitter ;
if ( supportEqualsSeparator & & processedLine . indexOf ( '=' ) > = 0 ) {
keyValueSplitter = '=' ;
} else if ( supportColonSeparator & & processedLine . indexOf ( ':' ) > = 0 ) {
keyValueSplitter = ':' ;
}
2022-10-12 23:31:01 +02:00
2022-10-13 20:14:25 +02:00
auto parts = processedLine . split ( keyValueSplitter ) ;
2022-10-12 23:42:01 +02:00
2022-10-13 20:14:25 +02:00
enforce ! ConfigCreationException ( parts . length < = 2 , "Line has too many equals signs and cannot be parsed (L" ~ index
. to ! string ~ "): " ~ processedLine ) ;
enforce ! ConfigCreationException ( supportKeysWithoutValues | | parts . length = = 2 , "Missing value assignment (L" ~ index
. to ! string ~ "): " ~ processedLine ) ;
key = [ section , parts [ 0 ] . strip ] . join ( '.' ) ;
value = supportKeysWithoutValues & & parts . length = = 1 ? "" : parts [ 1 ] . strip ;
} else {
value = processedLine ;
}
2022-10-11 20:07:08 +02:00
2022-10-13 20:14:25 +02:00
if ( supportMultilineValues & & value . endsWith ( '\\' ) ) {
valueBuffer ~ = value [ 0 . . $ - 1 ] ;
continue ;
}
auto fullValue = valueBuffer ~ value ;
2022-10-11 20:35:36 +02:00
if ( normalizeQuotedValues & &
2022-10-13 20:14:25 +02:00
fullValue . length > 1 & &
( fullValue . startsWith ( '"' ) | | fullValue . startsWith ( '\'' ) ) & &
( fullValue . endsWith ( '"' ) | | fullValue . endsWith ( '\'' ) ) ) {
fullValue = fullValue [ 1 . . $ - 1 ] ;
2022-10-11 20:32:33 +02:00
}
2022-10-13 20:14:25 +02:00
properties . set ( key , fullValue ) ;
key = null ;
valueBuffer = "" ;
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 ,
2022-10-12 23:53:50 +02:00
SupportExclamationComments . no ,
2022-10-12 23:31:01 +02:00
SupportSections . no ,
NormalizeQuotedValues . no ,
SupportEqualsSeparator . yes ,
2022-10-12 23:42:01 +02:00
SupportColonSeparator . no ,
2022-10-13 20:14:25 +02:00
SupportKeysWithoutValues . no ,
SupportMultilineValues . 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 ,
2022-10-12 23:53:50 +02:00
SupportExclamationComments . yes ,
2022-10-12 23:31:01 +02:00
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
2022-10-12 23:53:50 +02:00
! this then is also a comment !
2022-10-11 19:49:32 +02:00
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 ,
2022-10-12 23:53:50 +02:00
SupportExclamationComments . no ,
2022-10-12 23:42:01 +02:00
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 ,
2022-10-12 23:53:50 +02:00
SupportExclamationComments . yes ,
2022-10-12 23:31:01 +02:00
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-12 23:53:50 +02:00
timeout = 36000 ! pretty long !
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-12 23:53:50 +02:00
assert ( config . get ( "timeout" ) = = "36000" ) ;
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:53:50 +02:00
SupportExclamationComments . no ,
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 ,
2022-10-12 23:53:50 +02:00
SupportExclamationComments . no ,
2022-10-11 20:32:33 +02:00
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 ,
2022-10-12 23:53:50 +02:00
SupportExclamationComments . no ,
2022-10-12 23:31:01 +02:00
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-13 20:14:25 +02:00
@ ( "Support multiline values" )
unittest {
auto config = new KeyValueConfigFactory ! (
SupportHashtagComments . yes ,
SupportSemicolonComments . no ,
SupportExclamationComments . no ,
SupportSections . yes ,
NormalizeQuotedValues . yes ,
SupportEqualsSeparator . yes ,
SupportColonSeparator . no ,
SupportKeysWithoutValues . yes ,
SupportMultilineValues . yes
) ( ) . parseConfig ( "
sentence = the quick \ \
' brown fox ' \ \ # comments
[ jump ] \ \
\ \
ed over \ \ # are not part of the
the lazy \ \
' [ dog ] ' # value
not part of the sentence
" ) ;
assert ( config . get ( "sentence" ) = = "the quick 'brown fox' [jump]ed over the lazy '[dog]'" ) ;
}
@ ( "Normalize multiline values with quotes" )
unittest {
auto config = new KeyValueConfigFactory ! (
SupportHashtagComments . no ,
SupportSemicolonComments . no ,
SupportExclamationComments . no ,
SupportSections . no ,
NormalizeQuotedValues . yes ,
SupportEqualsSeparator . yes ,
SupportColonSeparator . no ,
SupportKeysWithoutValues . no ,
SupportMultilineValues . yes
) ( ) . parseConfig ( "
doubles = \ " Well then there I was \ \
doing my thing . \ "
singles = ' When suddenly \ \
a shark bit me \ \
from the sky '
" ) ;
assert ( config . get ( "doubles" ) = = "Well then there I was doing my thing." ) ;
assert ( config . get ( "singles" ) = = "When suddenly a shark bit me from the sky" ) ;
}
2022-10-11 19:29:26 +02:00
}