1 /**
2  * Utilities for loading INI files.
3  *
4  * Authors:
5  *  Mike Bierlee, m.bierlee@lostmoment.com
6  * Copyright: 2022-2025 Mike Bierlee
7  * License:
8  *  This software is licensed under the terms of the MIT license.
9  *  The full terms of the license can be found in the LICENSE file.
10  */
11 
12 module mirage.ini;
13 
14 import mirage.config : ConfigDictionary;
15 import mirage.keyvalue : KeyValueConfigFactory, SupportHashtagComments, SupportSemicolonComments,
16     SupportExclamationComments, SupportSections, NormalizeQuotedValues, SupportEqualsSeparator,
17     SupportColonSeparator, SupportKeysWithoutValues, SupportMultilineValues;
18 
19 /** 
20  * Creates configuration dictionaries from INI files.
21  *
22  * Format specifications:
23  *   https://en.wikipedia.org/wiki/INI_file#Format
24  */
25 class IniConfigFactory : KeyValueConfigFactory!(
26     SupportHashtagComments.yes,
27     SupportSemicolonComments.yes,
28     SupportExclamationComments.no,
29     SupportSections.yes,
30     NormalizeQuotedValues.yes,
31     SupportEqualsSeparator.yes,
32     SupportColonSeparator.yes,
33     SupportKeysWithoutValues.no,
34     SupportMultilineValues.yes
35 ) {
36 }
37 
38 /** 
39  * Parse configuration from the given INI config string.
40 
41  * Params:
42  *   contents = Text contents of the config to be parsed.
43  * Returns: The parsed configuration.
44  */
45 ConfigDictionary parseIniConfig(const string contents) {
46     return new IniConfigFactory().parseConfig(contents);
47 }
48 
49 /** 
50  * Load a INI configuration file from disk.
51  *
52  * Params:
53  *   filePath = Path to the INI configuration file.
54  * Returns: The loaded configuration.
55  */
56 ConfigDictionary loadIniConfig(const string filePath) {
57     return new IniConfigFactory().loadFile(filePath);
58 }
59 
60 version (unittest) {
61     import std.process : environment;
62 
63     @("Parse INI config")
64     unittest {
65         auto config = parseIniConfig("
66             globalSection = yes
67             multi = 'we are \\
68                         multi!'
69 
70             [supersection]
71             thefirst = here
72 
73             [supersection.sub]
74             sandwich=maybe tasty
75 
76             [.way]
77             advertisement? = nah ; For real, not sponsored!
78 
79             # Although money would be cool
80             [back]
81             to: basics
82             much = \"very much whitespace\"
83             many = 'very many whitespace'
84         ");
85 
86         assert(config.get("globalSection") == "yes");
87         assert(config.get("multi") == "we are multi!");
88         assert(config.get("supersection.thefirst") == "here");
89         assert(config.get("supersection.sub.sandwich") == "maybe tasty");
90         assert(config.get("supersection.sub.way.advertisement?") == "nah");
91         assert(config.get("back.much") == "very much whitespace");
92         assert(config.get("back.many") == "very many whitespace");
93     }
94 
95     @("Load INI file")
96     unittest {
97         auto config = loadIniConfig("testfiles/fuzzy.ini");
98 
99         assert(config.get("globalSection") == "yes");
100         assert(config.get("supersection.thefirst") == "here");
101         assert(config.get("supersection.sub.sandwich") == "maybe tasty");
102         assert(config.get("supersection.sub.way.advertisement?") == "nah");
103         assert(config.get("back.much") == "very much whitespace");
104         assert(config.get("back.many") == "very many whitespace");
105     }
106 
107     @("Substitute env vars")
108     unittest {
109         environment["MIRAGE_TEST_INI_VAR"] = "I am ini";
110         auto config = parseIniConfig("
111             [app]
112             startInfo = ${MIRAGE_TEST_INI_VAR}
113         ");
114 
115         assert(config.get("app.startInfo") == "I am ini");
116     }
117 
118     @("Use value from other key")
119     unittest {
120         auto config = parseIniConfig("
121             [app]
122             startInfo = \"Let's get started!\"
123 
124             [logger]
125             startInfo = ${app.startInfo}
126         ");
127 
128         assert(config.get("app.startInfo") == "Let's get started!");
129         assert(config.get("logger.startInfo") == "Let's get started!");
130     }
131 }