1 /**
2  * Structured INI
3  *
4  * License:
5  *   This Source Code Form is subject to the terms of
6  *   the Mozilla Public License, v. 2.0. If a copy of
7  *   the MPL was not distributed with this file, You
8  *   can obtain one at http://mozilla.org/MPL/2.0/.
9  *
10  * Authors:
11  *   Vladimir Panteleev <vladimir@thecybershadow.net>
12  */
13 
14 module ae.utils.sini;
15 
16 import std.algorithm;
17 import std.conv;
18 import std.exception;
19 import std.range;
20 import std.string;
21 import std.traits;
22 
23 alias std..string.indexOf indexOf;
24 
25 /// Represents the user-defined behavior for handling a node in a
26 /// structured INI file's hierarchy.
27 struct IniHandler(S)
28 {
29 	/// User callback for parsing a value at this node.
30 	void delegate(S name, S value) leafHandler;
31 
32 	/// User callback for obtaining a child node from this node.
33 	IniHandler delegate(S name) nodeHandler;
34 }
35 
36 /// Parse a structured INI from a range of lines, through the given handler.
37 void parseIni(R, H)(R r, H rootHandler)
38 	if (isInputRange!R && isSomeString!(ElementType!R))
39 {
40 	auto currentHandler = rootHandler;
41 
42 	size_t lineNumber;
43 	while (!r.empty)
44 	{
45 		lineNumber++;
46 
47 		auto line = r.front.chomp().stripLeft();
48 		scope(success) r.popFront();
49 		if (line.empty)
50 			continue;
51 		if (line[0] == '#' || line[0] == ';')
52 			continue;
53 
54 		if (line[0] == '[')
55 		{
56 			line = line.stripRight();
57 			enforce(line[$-1] == ']', "Malformed section line (no ']')");
58 			auto section = line[1..$-1];
59 
60 			currentHandler = rootHandler;
61 			foreach (segment; section.split("."))
62 				currentHandler = currentHandler.nodeHandler
63 					.enforce("This group may not have any nodes.")
64 					(segment);
65 		}
66 		else
67 		{
68 			auto pos = line.indexOf('=');
69 			enforce(pos > 0, "Malformed value line (no '=')");
70 			auto name = line[0..pos].strip;
71 			auto handler = currentHandler;
72 			auto segments = name.split(".");
73 			enforce(segments.length, "Malformed value line (empty name)");
74 			foreach (segment; segments[0..$-1])
75 				handler = handler.nodeHandler
76 					.enforce("This group may not have any nodes.")
77 					(segment);
78 			handler.leafHandler
79 				.enforce("This group may not have any values.")
80 				(segments[$-1], line[pos+1..$].strip);
81 		}
82 	}
83 }
84 
85 /// Helper which creates an INI handler out of delegates.
86 IniHandler!S iniHandler(S)(void delegate(S, S) leafHandler, IniHandler!S delegate(S) nodeHandler = null)
87 {
88 	return IniHandler!S(leafHandler, nodeHandler);
89 }
90 
91 unittest
92 {
93 	int count;
94 
95 	parseIni
96 	(
97 		q"<
98 			s.n1=v1
99 			[s]
100 			n2=v2
101 		>".splitLines(),
102 		iniHandler
103 		(
104 			null,
105 			(in char[] name)
106 			{
107 				assert(name == "s");
108 				return iniHandler
109 				(
110 					(in char[] name, in char[] value)
111 					{
112 						assert(name .length==2 && name [0] == 'n'
113 						    && value.length==2 && value[0] == 'v'
114 						    && name[1] == value[1]);
115 						count++;
116 					}
117 				);
118 			}
119 		)
120 	);
121 
122 	assert(count==2);
123 }
124 
125 /// Alternative API for IniHandler, where each leaf is a node
126 struct IniTraversingHandler(S)
127 {
128 	/// User callback for parsing a value at this node.
129 	void delegate(S value) leafHandler;
130 
131 	/// User callback for obtaining a child node from this node.
132 	IniTraversingHandler delegate(S name) nodeHandler;
133 
134 	private IniHandler!S conv()
135 	{
136 		// Don't reference "this" from a lambda,
137 		// as it can be a temporary on the stack
138 		IniTraversingHandler thisCopy = this;
139 		return IniHandler!S
140 		(
141 			(S name, S value)
142 			{
143 				thisCopy
144 					.nodeHandler
145 					.enforce("This group may not have any nodes.")
146 					(name)
147 					.leafHandler
148 					.enforce("This group may not have a value.")
149 					(value);
150 			},
151 			(S name)
152 			{
153 				return thisCopy
154 					.nodeHandler
155 					.enforce("This group may not have any nodes.")
156 					(name)
157 					.conv();
158 			}
159 		);
160 	}
161 }
162 
163 IniTraversingHandler!S makeIniHandler(S = string, U)(ref U v)
164 {
165 	static if (!is(U == Unqual!U))
166 		return makeIniHandler!S(*cast(Unqual!U*)&v);
167 	else
168 	static if (is(U == struct))
169 		return IniTraversingHandler!S
170 		(
171 			null,
172 			delegate IniTraversingHandler!S (S name)
173 			{
174 				bool found;
175 				foreach (i, field; v.tupleof)
176 				{
177 					enum fieldName = to!S(v.tupleof[i].stringof[2..$]);
178 					if (name == fieldName)
179 					{
180 						static if (is(typeof(makeIniHandler!S(v.tupleof[i]))))
181 							return makeIniHandler!S(v.tupleof[i]);
182 						else
183 							throw new Exception("Can't parse " ~ U.stringof ~ "." ~ cast(string)name ~ " of type " ~ typeof(v.tupleof[i]).stringof);
184 					}
185 				}
186 				static if (is(ReturnType!(v.parseSection)))
187 					return v.parseSection(name);
188 				else
189 					throw new Exception("Unknown field " ~ to!string(name));
190 			}
191 		);
192 	else
193 	static if (isAssociativeArray!U)
194 		return IniTraversingHandler!S
195 		(
196 			null,
197 			(S name)
198 			{
199 				alias K = typeof(v.keys[0]);
200 				auto key = to!K(name);
201 				auto pField = key in v;
202 				if (!pField)
203 				{
204 					v[key] = typeof(v[key]).init;
205 					pField = key in v;
206 				}
207 				else
208 					throw new Exception("Duplicate value: " ~ to!string(name));
209 				return makeIniHandler!S(*pField);
210 			}
211 		);
212 	else
213 	static if (is(typeof(to!U(string.init))))
214 		return IniTraversingHandler!S
215 		(
216 			(S value)
217 			{
218 				v = to!U(value);
219 			}
220 		);
221 	else
222 		static assert(false, "Can't parse " ~ U.stringof);
223 }
224 
225 /// Parse structured INI lines from a range of strings, into a user-defined struct.
226 T parseIni(T, R)(R r)
227 	if (isInputRange!R && isSomeString!(ElementType!R))
228 {
229 	T result;
230 	r.parseIniInto(result);
231 	return result;
232 }
233 
234 /// ditto
235 void parseIniInto(R, T)(R r, ref T result)
236 	if (isInputRange!R && isSomeString!(ElementType!R))
237 {
238 	parseIni(r, makeIniHandler!(ElementType!R)(result).conv());
239 }
240 
241 unittest
242 {
243 	static struct File
244 	{
245 		struct S
246 		{
247 			string n1, n2;
248 			int[string] a;
249 		}
250 		S s;
251 	}
252 
253 	auto f = parseIni!File
254 	(
255 		q"<
256 			s.n1=v1
257 			s.a.foo=1
258 			[s]
259 			n2=v2
260 			a.bar=2
261 		>".dup.splitLines()
262 	);
263 
264 	assert(f.s.n1=="v1");
265 	assert(f.s.n2=="v2");
266 	assert(f.s.a==["foo":1, "bar":2]);
267 }
268 
269 unittest
270 {
271 	static struct Custom
272 	{
273 		struct Section
274 		{
275 			string name;
276 			string[string] values;
277 		}
278 		Section[] sections;
279 
280 		auto parseSection(wstring name)
281 		{
282 			sections.length++;
283 			auto p = &sections[$-1];
284 			p.name = to!string(name);
285 			return makeIniHandler!wstring(p.values);
286 		}
287 	}
288 
289 	auto c = parseIni!Custom
290 	(
291 		q"<
292 			[one]
293 			a=a
294 			[two]
295 			b=b
296 		>"w.splitLines()
297 	);
298 
299 	assert(c == Custom([Custom.Section("one", ["a" : "a"]), Custom.Section("two", ["b" : "b"])]));
300 }
301 
302 // ***************************************************************************
303 
304 deprecated alias StructuredIniHandler = IniHandler;
305 deprecated alias parseStructuredIni = parseIni;
306 deprecated alias StructuredIniTraversingHandler = IniTraversingHandler;
307 deprecated alias makeStructuredIniHandler = makeIniHandler;
308 
309 // ***************************************************************************
310 
311 /// Convenience function to load a struct from an INI file.
312 /// Returns .init if the file does not exist.
313 S loadIni(S)(string fileName)
314 {
315 	S s;
316 
317 	import std.file;
318 	if (fileName.exists)
319 		s = fileName
320 			.readText()
321 			.splitLines()
322 			.parseIni!S();
323 
324 	return s;
325 }
326 
327 /// As above, though loads several INI files
328 /// (duplicate values appearing in later INI files
329 /// override any values from earlier files).
330 S loadInis(S)(in char[][] fileNames)
331 {
332 	S s;
333 
334 	import std.file;
335 	s = fileNames
336 		.map!(fileName =>
337 			fileName.exists ?
338 				fileName
339 				.readText()
340 				.splitLines()
341 			:
342 				null
343 		)
344 		.joiner(["[]"])
345 		.parseIni!S();
346 
347 	return s;
348 }
349 
350 // ***************************************************************************
351 
352 /// Simple convenience formatter for writing INI files.
353 struct IniWriter(O)
354 {
355 	O writer;
356 
357 	void startSection(string name)
358 	{
359 		writer.put('[', name, "]\n");
360 	}
361 
362 	void writeValue(string name, string value)
363 	{
364 		writer.put(name, '=', value, '\n');
365 	}
366 }
367 
368 /// Insert a blank line before each section
369 string prettifyIni(string ini) { return ini.replace("\n[", "\n\n["); }