diff --git a/README.md b/README.md index 8a690c8..8933c11 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ The following file formats are currently supported: | _any below_ | _any below_ | `mirage` | `loadConfig`** | _(N/A)_ | | | JSON | .json | `mirage.json` | `loadJsonConfig` | `parseJsonConfig`*** | `JsonConfigFactory` | | Java | .properties | `mirage.java` | `loadJavaProperties` | `parseJavaProperties` | `JavaPropertiesFactory` | +| INI | .ini | `mirage.ini` | `loadIniConfig` | `parseIniConfig` | `IniConfigFactory` | \* _Any loader or parser can be imported from the `mirage` package since they are all publicly imported._ \*\* _Loads files based on their extension. If the file does not use one of the extensions in the table, you must use a specific loader._ diff --git a/TODO.md b/TODO.md index a48d98c..94e1a64 100644 --- a/TODO.md +++ b/TODO.md @@ -1,12 +1,16 @@ # TODO -* Add tutorial - * Config loading - * Config parsing - * Config manip - * Env and config var substitution - * Escaping -* Java properties - * Add unicode escaping - * Support multi-line values with backslash - * Add escaping of key/value separator = and : \ No newline at end of file +- Add tutorial + - Config loading + - Config parsing + - Config manip + - Env and config var substitution + - Escaping +- Java properties + - Add unicode escaping + - Support multi-line values with backslash + - Add escaping of key/value separator = and : +- INI config + - Case insensitive properties and sections + - Escape characters + - Support multi-line values with backslash diff --git a/source/mirage/config.d b/source/mirage/config.d index 0cea812..2634f27 100644 --- a/source/mirage/config.d +++ b/source/mirage/config.d @@ -21,6 +21,7 @@ import std.typecons : Flag; import mirage.json : loadJsonConfig; import mirage.java : loadJavaProperties; +import mirage.ini : loadIniConfig; /** * Used by the ConfigDictionary when something goes wrong when reading configuration. @@ -603,6 +604,10 @@ ConfigDictionary loadConfig(const string configPath) { return loadJavaProperties(configPath); } + if (extension == ".ini") { + return loadIniConfig(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()'"); } @@ -853,6 +858,11 @@ version (unittest) { assert(javaProperties.get("name") == "Groot"); assert(javaProperties.get("age") == "8728"); assert(javaProperties.get("taxNumber") == "null"); + + auto iniConfig = loadConfig("testfiles/groot.ini"); + assert(iniConfig.get("groot.name") == "Groot"); + assert(iniConfig.get("groot.age") == "8728"); + assert(iniConfig.get("groot.taxNumber") == "null"); } @("Whitespace is preserved in values") diff --git a/source/mirage/ini.d b/source/mirage/ini.d new file mode 100644 index 0000000..6ab21ef --- /dev/null +++ b/source/mirage/ini.d @@ -0,0 +1,127 @@ +/** + * Utilities for loading INI files. + * + * 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.ini; + +import mirage.config : ConfigDictionary; +import mirage.keyvalue : KeyValueConfigFactory, SupportHashtagComments, SupportSemicolonComments, + SupportExclamationComments, SupportSections, NormalizeQuotedValues, SupportEqualsSeparator, + SupportColonSeparator, SupportKeysWithoutValues; + +/** + * Creates configuration dictionaries from INI files. + * + * Format specifications: + * https://en.wikipedia.org/wiki/INI_file#Format + */ +class IniConfigFactory : KeyValueConfigFactory!( + SupportHashtagComments.yes, + SupportSemicolonComments.yes, + SupportExclamationComments.no, + SupportSections.yes, + NormalizeQuotedValues.yes, + SupportEqualsSeparator.yes, + SupportColonSeparator.yes, + SupportKeysWithoutValues.no +) { +} + +/** + * Parse configuration from the given INI config string. + + * Params: + * contents = Text contents of the config to be parsed. + * Returns: The parsed configuration. + */ +ConfigDictionary parseIniConfig(const string contents) { + return new IniConfigFactory().parseConfig(contents); +} + +/** + * Load a INI configuration file from disk. + * + * Params: + * filePath = Path to the INI configuration file. + * Returns: The loaded configuration. + */ +ConfigDictionary loadIniConfig(const string filePath) { + return new IniConfigFactory().loadFile(filePath); +} + +version (unittest) { + import std.process : environment; + + @("Parse INI config") + unittest { + auto config = parseIniConfig(" + globalSection = yes + + [supersection] + thefirst = here + + [supersection.sub] + sandwich=maybe tasty + + [.way] + advertisement? = nah ; For real, not sponsored! + + # Although money would be cool + [back] + to: basics + much = \"very much whitespace\" + many = 'very many whitespace' + "); + + assert(config.get("globalSection") == "yes"); + assert(config.get("supersection.thefirst") == "here"); + assert(config.get("supersection.sub.sandwich") == "maybe tasty"); + assert(config.get("supersection.sub.way.advertisement?") == "nah"); + assert(config.get("back.much") == "very much whitespace"); + assert(config.get("back.many") == "very many whitespace"); + } + + @("Load INI file") + unittest { + auto config = loadIniConfig("testfiles/fuzzy.ini"); + + assert(config.get("globalSection") == "yes"); + assert(config.get("supersection.thefirst") == "here"); + assert(config.get("supersection.sub.sandwich") == "maybe tasty"); + assert(config.get("supersection.sub.way.advertisement?") == "nah"); + assert(config.get("back.much") == "very much whitespace"); + assert(config.get("back.many") == "very many whitespace"); + } + + @("Substitute env vars") + unittest { + environment["MIRAGE_TEST_INI_VAR"] = "I am ini"; + auto config = parseIniConfig(" + [app] + startInfo = ${MIRAGE_TEST_INI_VAR} + "); + + assert(config.get("app.startInfo") == "I am ini"); + } + + @("Use value from other key") + unittest { + auto config = parseIniConfig(" + [app] + startInfo = \"Let's get started!\" + + [logger] + startInfo = ${app.startInfo} + "); + + assert(config.get("app.startInfo") == "Let's get started!"); + assert(config.get("logger.startInfo") == "Let's get started!"); + } +} diff --git a/source/mirage/package.d b/source/mirage/package.d index 49f840c..bab4f25 100644 --- a/source/mirage/package.d +++ b/source/mirage/package.d @@ -10,6 +10,7 @@ module mirage; public import mirage.config; +public import mirage.ini; public import mirage.java; public import mirage.json; public import mirage.keyvalue; \ No newline at end of file diff --git a/testfiles/fuzzy.ini b/testfiles/fuzzy.ini new file mode 100644 index 0000000..9b34550 --- /dev/null +++ b/testfiles/fuzzy.ini @@ -0,0 +1,16 @@ +globalSection = yes + +[supersection] +thefirst = here + +[supersection.sub] +sandwich=maybe tasty + +[.way] +advertisement? = nah ; For real, not sponsored! + +# Although money would be cool +[back] +to: basics +much = "very much whitespace" +many = 'very many whitespace' \ No newline at end of file diff --git a/testfiles/groot.ini b/testfiles/groot.ini new file mode 100644 index 0000000..55b91eb --- /dev/null +++ b/testfiles/groot.ini @@ -0,0 +1,4 @@ +[groot] +name=Groot +age=8728 +taxNumber=null \ No newline at end of file