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