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 import ae.utils.exception;
24 
25 alias std..string.indexOf indexOf;
26 
27 /// Represents the user-defined behavior for handling a node in a
28 /// structured INI file's hierarchy.
29 struct IniHandler(S)
30 {
31 	/// User callback for parsing a value at this node.
32 	void delegate(S name, S value) leafHandler;
33 
34 	/// User callback for obtaining a child node from this node.
35 	IniHandler delegate(S name) nodeHandler;
36 }
37 
38 /// Parse a structured INI from a range of lines, through the given handler.
39 void parseIni(R, H)(R r, H rootHandler)
40 	if (isInputRange!R && isSomeString!(ElementType!R))
41 {
42 	auto currentHandler = rootHandler;
43 
44 	size_t lineNumber;
45 	while (!r.empty)
46 	{
47 		lineNumber++;
48 		mixin(exceptionContext(q{"Error while parsing INI line %s:".format(lineNumber)}));
49 
50 		auto line = r.front.chomp().stripLeft();
51 		scope(success) r.popFront();
52 		if (line.empty)
53 			continue;
54 		if (line[0] == '#' || line[0] == ';')
55 			continue;
56 
57 		if (line[0] == '[')
58 		{
59 			line = line.stripRight();
60 			enforce(line[$-1] == ']', "Malformed section line (no ']')");
61 			auto section = line[1..$-1];
62 
63 			currentHandler = rootHandler;
64 			foreach (segment; section.split("."))
65 				currentHandler = currentHandler.nodeHandler
66 					.enforce("This group may not have any nodes.")
67 					(segment);
68 		}
69 		else
70 		{
71 			auto pos = line.indexOf('=');
72 			enforce(pos > 0, "Malformed value line (no '=')");
73 			auto name = line[0..pos].strip;
74 			auto handler = currentHandler;
75 			auto segments = name.split(".");
76 			enforce(segments.length, "Malformed value line (empty name)");
77 			foreach (segment; segments[0..$-1])
78 				handler = handler.nodeHandler
79 					.enforce("This group may not have any nodes.")
80 					(segment);
81 			handler.leafHandler
82 				.enforce("This group may not have any values.")
83 				(segments[$-1], line[pos+1..$].strip);
84 		}
85 	}
86 }
87 
88 /// Helper which creates an INI handler out of delegates.
89 IniHandler!S iniHandler(S)(void delegate(S, S) leafHandler, IniHandler!S delegate(S) nodeHandler = null)
90 {
91 	return IniHandler!S(leafHandler, nodeHandler);
92 }
93 
94 unittest
95 {
96 	int count;
97 
98 	parseIni
99 	(
100 		q"<
101 			s.n1=v1
102 			[s]
103 			n2=v2
104 		>".splitLines(),
105 		iniHandler
106 		(
107 			null,
108 			(in char[] name)
109 			{
110 				assert(name == "s");
111 				return iniHandler
112 				(
113 					(in char[] name, in char[] value)
114 					{
115 						assert(name .length==2 && name [0] == 'n'
116 						    && value.length==2 && value[0] == 'v'
117 						    && name[1] == value[1]);
118 						count++;
119 					}
120 				);
121 			}
122 		)
123 	);
124 
125 	assert(count==2);
126 }
127 
128 /// Alternative API for IniHandler, where each leaf is a node
129 struct IniTraversingHandler(S)
130 {
131 	/// User callback for parsing a value at this node.
132 	void delegate(S value) leafHandler;
133 
134 	/// User callback for obtaining a child node from this node.
135 	IniTraversingHandler delegate(S name) nodeHandler;
136 
137 	private IniHandler!S conv()
138 	{
139 		// Don't reference "this" from a lambda,
140 		// as it can be a temporary on the stack
141 		IniTraversingHandler thisCopy = this;
142 		return IniHandler!S
143 		(
144 			(S name, S value)
145 			{
146 				thisCopy
147 					.nodeHandler
148 					.enforce("This group may not have any nodes.")
149 					(name)
150 					.leafHandler
151 					.enforce("This group may not have a value.")
152 					(value);
153 			},
154 			(S name)
155 			{
156 				return thisCopy
157 					.nodeHandler
158 					.enforce("This group may not have any nodes.")
159 					(name)
160 					.conv();
161 			}
162 		);
163 	}
164 }
165 
166 enum isNestingType(T) = isAssociativeArray!T || is(T == struct);
167 
168 IniTraversingHandler!S makeIniHandler(S = string, U)(ref U v)
169 {
170 	static if (!is(U == Unqual!U))
171 		return makeIniHandler!S(*cast(Unqual!U*)&v);
172 	else
173 	static if (is(typeof((ref U v){alias V=typeof(v[S.init]); v[S.init] = V.init; *(S.init in v) = V.init;})))
174 		return IniTraversingHandler!S
175 		(
176 			null,
177 			(S name)
178 			{
179 				auto pField = name in v;
180 				if (!pField)
181 				{
182 					v[name] = typeof(v[name]).init;
183 					pField = name in v;
184 				}
185 				else
186 				static if (!isNestingType!U)
187 					throw new Exception("Duplicate value: " ~ to!string(name));
188 				return makeIniHandler!S(*pField);
189 			}
190 		);
191 	else
192 	static if (is(U == struct))
193 		return IniTraversingHandler!S
194 		(
195 			null,
196 			delegate IniTraversingHandler!S (S name)
197 			{
198 				bool found;
199 				foreach (i, ref field; v.tupleof)
200 				{
201 					enum fieldName = to!S(v.tupleof[i].stringof[2..$]);
202 					if (name == fieldName)
203 					{
204 						static if (is(typeof(makeIniHandler!S(v.tupleof[i]))))
205 							return makeIniHandler!S(v.tupleof[i]);
206 						else
207 							throw new Exception("Can't parse " ~ U.stringof ~ "." ~ cast(string)name ~ " of type " ~ typeof(v.tupleof[i]).stringof);
208 					}
209 				}
210 				static if (is(ReturnType!(v.parseSection)))
211 					return v.parseSection(name);
212 				else
213 					throw new Exception("Unknown field " ~ to!string(name));
214 			}
215 		);
216 	else
217 	static if (isAssociativeArray!U)
218 		return IniTraversingHandler!S
219 		(
220 			null,
221 			(S name)
222 			{
223 				alias K = typeof(v.keys[0]);
224 				auto key = to!K(name);
225 				auto pField = key in v;
226 				if (!pField)
227 				{
228 					v[key] = typeof(v[key]).init;
229 					pField = key in v;
230 				}
231 				else
232 				static if (!isNestingType!U)
233 					throw new Exception("Duplicate value: " ~ to!string(name));
234 				return makeIniHandler!S(*pField);
235 			}
236 		);
237 	else
238 	static if (is(typeof(to!U(string.init))))
239 		return IniTraversingHandler!S
240 		(
241 			(S value)
242 			{
243 				v = to!U(value);
244 			}
245 		);
246 	else
247 	static if (is(U V : V*))
248 	{
249 		static if (is(typeof(v = new V)))
250 			if (!v)
251 				v = new V;
252 		return makeIniHandler!S(*v);
253 	}
254 	else
255 		static assert(false, "Can't parse " ~ U.stringof);
256 }
257 
258 /// Parse structured INI lines from a range of strings, into a user-defined struct.
259 T parseIni(T, R)(R r)
260 	if (isInputRange!R && isSomeString!(ElementType!R))
261 {
262 	T result;
263 	r.parseIniInto(result);
264 	return result;
265 }
266 
267 /// ditto
268 void parseIniInto(R, T)(R r, ref T result)
269 	if (isInputRange!R && isSomeString!(ElementType!R))
270 {
271 	parseIni(r, makeIniHandler!(ElementType!R)(result).conv());
272 }
273 
274 unittest
275 {
276 	static struct File
277 	{
278 		struct S
279 		{
280 			string n1, n2;
281 			int[string] a;
282 		}
283 		S s;
284 	}
285 
286 	auto f = parseIni!File
287 	(
288 		q"<
289 			s.n1=v1
290 			s.a.foo=1
291 			[s]
292 			n2=v2
293 			a.bar=2
294 		>".dup.splitLines()
295 	);
296 
297 	assert(f.s.n1=="v1");
298 	assert(f.s.n2=="v2");
299 	assert(f.s.a==["foo":1, "bar":2]);
300 }
301 
302 unittest
303 {
304 	static struct Custom
305 	{
306 		struct Section
307 		{
308 			string name;
309 			string[string] values;
310 		}
311 		Section[] sections;
312 
313 		auto parseSection(wstring name)
314 		{
315 			sections.length++;
316 			auto p = &sections[$-1];
317 			p.name = to!string(name);
318 			return makeIniHandler!wstring(p.values);
319 		}
320 	}
321 
322 	auto c = parseIni!Custom
323 	(
324 		q"<
325 			[one]
326 			a=a
327 			[two]
328 			b=b
329 		>"w.splitLines()
330 	);
331 
332 	assert(c == Custom([Custom.Section("one", ["a" : "a"]), Custom.Section("two", ["b" : "b"])]));
333 }
334 
335 version(unittest) static import ae.utils.aa;
336 
337 unittest
338 {
339 	import ae.utils.aa;
340 
341 	auto o = parseIni!(OrderedMap!(string, string))
342 	(
343 		q"<
344 			b=b
345 			a=a
346 		>".splitLines()
347 	);
348 
349 	assert(o["a"]=="a" && o["b"] == "b");
350 }
351 
352 unittest
353 {
354 	import ae.utils.aa;
355 
356 	static struct S { string x; }
357 
358 	auto o = parseIni!(OrderedMap!(string, S))
359 	(
360 		q"<
361 			b.x=b
362 			[a]
363 			x=a
364 		>".splitLines()
365 	);
366 
367 	assert(o["a"].x == "a" && o["b"].x == "b");
368 }
369 
370 unittest
371 {
372 	static struct S { string x, y; }
373 
374 	auto r = parseIni!(S[string])
375 	(
376 		q"<
377 			a.x=x
378 			[a]
379 			y=y
380 		>".splitLines()
381 	);
382 
383 	assert(r["a"].x == "x" && r["a"].y == "y");
384 }
385 
386 unittest
387 {
388 	static struct S { string x, y; }
389 	static struct T { S* s; }
390 
391 	{
392 		T t;
393 		parseIniInto(["s.x=v"], t);
394 		assert(t.s.x == "v");
395 	}
396 
397 	{
398 		S s = {"x"}; T t = {&s};
399 		parseIniInto(["s.y=v"], t);
400 		assert(s.x == "x");
401 		assert(s.y == "v");
402 	}
403 }
404 
405 // ***************************************************************************
406 
407 deprecated alias StructuredIniHandler = IniHandler;
408 deprecated alias parseStructuredIni = parseIni;
409 deprecated alias StructuredIniTraversingHandler = IniTraversingHandler;
410 deprecated alias makeStructuredIniHandler = makeIniHandler;
411 
412 // ***************************************************************************
413 
414 /// Convenience function to load a struct from an INI file.
415 /// Returns .init if the file does not exist.
416 S loadIni(S)(string fileName)
417 {
418 	S s;
419 
420 	import std.file;
421 	if (fileName.exists)
422 		s = fileName
423 			.readText()
424 			.splitLines()
425 			.parseIni!S();
426 
427 	return s;
428 }
429 
430 /// As above, though loads several INI files
431 /// (duplicate values appearing in later INI files
432 /// override any values from earlier files).
433 S loadInis(S)(in char[][] fileNames)
434 {
435 	S s;
436 
437 	import std.file;
438 	s = fileNames
439 		.map!(fileName =>
440 			fileName.exists ?
441 				fileName
442 				.readText()
443 				.splitLines()
444 			:
445 				null
446 		)
447 		.joiner(["[]"])
448 		.parseIni!S();
449 
450 	return s;
451 }
452 
453 // ***************************************************************************
454 
455 /// Simple convenience formatter for writing INI files.
456 struct IniWriter(O)
457 {
458 	O writer;
459 
460 	void startSection(string name)
461 	{
462 		writer.put('[', name, "]\n");
463 	}
464 
465 	void writeValue(string name, string value)
466 	{
467 		writer.put(name, '=', value, '\n');
468 	}
469 }
470 
471 /// Insert a blank line before each section
472 string prettifyIni(string ini) { return ini.replace("\n[", "\n\n["); }