1 /++
2     Functions related to reading from a configuration file, broken out of
3     [kameloso.config] to avoid cyclic dependencies.
4 
5     See_Also:
6         [kameloso.config]
7 
8     Copyright: [JR](https://github.com/zorael)
9     License: [Boost Software License 1.0](https://www.boost.org/users/license.html)
10 
11     Authors:
12         [JR](https://github.com/zorael)
13  +/
14 module kameloso.configreader;
15 
16 private:
17 
18 import lu.traits : isStruct;
19 import std.meta : allSatisfy;
20 
21 public:
22 
23 
24 // readConfigInto
25 /++
26     Reads a configuration file and applies the settings therein to passed objects.
27 
28     More than one object can be supplied; invalid ones for which there are no
29     settings in the configuration file will be silently ignored with no errors.
30     Orphan settings in the configuration file for which no appropriate
31     object was passed will be saved to `invalidEntries`.
32 
33     Example:
34     ---
35     IRCClient client;
36     IRCServer server;
37     string[][string] missingEntries;
38     string[][string] invalidEntries;
39 
40     "kameloso.conf".readConfigInto(missingEntries, invalidEntries, client, server);
41     ---
42 
43     Params:
44         configFile = Filename of file to read from.
45         missingEntries = Reference to an associative array of string arrays
46             of expected configuration entries that were missing.
47         invalidEntries = Reference to an associative array of string arrays
48             of unexpected configuration entries that did not belong.
49         things = Reference variadic list of things to set values of, according
50             to the text in the configuration file.
51  +/
52 void readConfigInto(T...)
53     (const string configFile,
54     ref string[][string] missingEntries,
55     ref string[][string] invalidEntries,
56     ref T things)
57 if (allSatisfy!(isStruct, T))
58 {
59     import lu.serialisation : deserialise;
60     import std.algorithm.iteration : splitter;
61 
62     return configFile
63         .configurationText
64         .splitter('\n')
65         .deserialise(missingEntries, invalidEntries, things);
66 }
67 
68 
69 // readConfigInto
70 /++
71     Reads a configuration file and applies the settings therein to passed objects.
72     Merely wraps the other [readConfigInto] overload and distinguishes itself
73     from it by not taking the two `string[][string]` out parameters it does.
74 
75     Params:
76         configFile = Filename of file to read from.
77         things = Reference variadic list of things to set values of, according
78             to the text in the configuration file.
79  +/
80 void readConfigInto(T...)(const string configFile, ref T things)
81 if (allSatisfy!(isStruct, T))
82 {
83     // Use two variables to satisfy -preview=dip1021
84     string[][string] ignore1;
85     string[][string] ignore2;
86     return configFile.readConfigInto(ignore1, ignore2, things);
87 }
88 
89 
90 // configurationText
91 /++
92     Reads a configuration file into a string.
93 
94     Example:
95     ---
96     string configText = "kameloso.conf".configurationText;
97     ---
98 
99     Params:
100         configFile = Filename of file to read from.
101 
102     Returns:
103         The contents of the supplied file.
104 
105     Throws:
106         [lu.common.FileTypeMismatchException|FileTypeMismatchException] if the
107         configuration file is a directory, a character file or any other non-file
108         type we can't write to.
109 
110         [lu.serialisation.ConfigurationFileReadFailureException|ConfigurationFileReadFailureException]
111         if the reading and decoding of the configuration file failed.
112  +/
113 auto configurationText(const string configFile)
114 {
115     import std.file : exists, getAttributes, isFile, readText;
116 
117     if (!configFile.exists)
118     {
119         return string.init;
120     }
121     else if (!configFile.isFile)
122     {
123         import lu.common : FileTypeMismatchException;
124         throw new FileTypeMismatchException(
125             "Configuration file is not a file",
126             configFile,
127             cast(ushort)getAttributes(configFile),
128             __FILE__);
129     }
130 
131     try
132     {
133         import std.array : replace;
134         import std.string : chomp;
135 
136         return configFile
137             .readText
138             .replace("[Votes]\n", "[Poll]\n")
139             .replace("[Votes]\r\n", "[Poll]\r\n")
140             .replace("[TwitchBot]\n", "[Twitch]\n")
141             .replace("[TwitchBot]\r\n", "[Twitch]\r\n")
142             .chomp;
143     }
144     catch (Exception e)
145     {
146         // catch Exception instead of UTFException, just in case there are more
147         // kinds of error than the normal "Invalid UTF-8 sequence".
148         throw new ConfigurationFileReadFailureException(
149             e.msg,
150             configFile,
151             __FILE__,
152             __LINE__);
153     }
154 }
155 
156 
157 // ConfigurationFileReadFailureException
158 /++
159     Exception, to be thrown when the specified configuration file could not be
160     read, for whatever reason.
161 
162     It is a normal [object.Exception|Exception] but with an attached filename string.
163  +/
164 final class ConfigurationFileReadFailureException : Exception
165 {
166 @safe:
167     /// The name of the configuration file the exception refers to.
168     string filename;
169 
170     /++
171         Create a new [ConfigurationFileReadFailureException], without attaching a filename.
172      +/
173     this(
174         const string message,
175         const string file = __FILE__,
176         const size_t line = __LINE__,
177         Throwable nextInChain = null) pure nothrow @nogc @safe
178     {
179         super(message, file, line, nextInChain);
180     }
181 
182     /++
183         Create a new [ConfigurationFileReadFailureException], attaching a filename.
184      +/
185     this(
186         const string message,
187         const string filename,
188         const string file = __FILE__,
189         const size_t line = __LINE__,
190         Throwable nextInChain = null) pure nothrow @nogc @safe
191     {
192         this.filename = filename;
193         super(message, file, line, nextInChain);
194     }
195 }