1 module inifiled;
2 
3 import std.conv : to;
4 import std.format : format, formattedWrite;
5 import std.math : isClose, isNaN;
6 import std.range : isInputRange, isOutputRange, ElementType;
7 import std.string : indexOf, lastIndexOf, split, strip, stripRight;
8 import std.traits : getUDAs, hasUDA, fullyQualifiedName, isArray, isBasicType
9 	, isSomeString, isArray;
10 
11 string genINIparser(T)() {
12 	return "";
13 }
14 
15 struct INI {
16 @safe:
17 	string msg;
18 	string name;
19 
20 	static INI opCall(string s, string name = null) pure {
21 		INI ret;
22 		ret.msg = s;
23 		ret.name = name;
24 
25 		return ret;
26 	}
27 }
28 
29 private INI getINI(T)() pure @trusted {
30 	foreach(it; __traits(getAttributes, T)) {
31 		static if(is(it == INI)) {
32 			return INI(null, null);
33 		}
34 		static if(is(typeof(it) == INI)) {
35 			return it;
36 		}
37 	}
38 	assert(false);
39 }
40 
41 private INI getINI(T, string mem)() @trusted {
42 	foreach(it; __traits(getAttributes, __traits(getMember, T, mem))) {
43 		static if(is(it == INI)) {
44 			return INI(null, null);
45 		}
46 		static if(is(typeof(it) == INI)) {
47 			return it;
48 		}
49 	}
50 	assert(false, mem);
51 }
52 
53 private pure string getTypeName(T)() @trusted {
54 	return fullyQualifiedName!T;
55 }
56 
57 void readINIFile(T)(ref T t, string filename) {
58 	import std.stdio : File;
59 	auto iFile = File(filename, "r");
60 	auto iRange = iFile.byLine();
61 	readINIFileImpl(t, iRange);
62 }
63 
64 private pure bool isSection(T)(T line) @safe nothrow {
65 	static assert(isInputRange!T, T.stringof ~ " is not an InputRange");
66 
67 	bool f;
68 	bool b;
69 
70 	foreach(it; line) {
71 		if(it == ' ' || it == '\t') {
72 			continue;
73 		} else if(it == '[') {
74 			f = true;
75 			break;
76 		} else {
77 			break;
78 		}
79 	}
80 
81 	foreach_reverse(it; line) {
82 		if(it == ' ' || it == '\t') {
83 			continue;
84 		} else if(it == ']') {
85 			b = true;
86 			break;
87 		} else {
88 			break;
89 		}
90 	}
91 
92 	return f && b;
93 }
94 
95 @safe pure unittest {
96 	assert(isSection("[initest.Person]"));
97 	assert(isSection(" [initest.Person]"));
98 	assert(isSection(" [initest.Person] "));
99 	assert(!isSection(";[initest.Person] "));
100 }
101 
102 private pure string getSection(T)(T line) @safe {
103 	static assert(isInputRange!T, T.stringof ~ " is not an InputRange");
104 	return getTimpl!('[',']')(line);
105 }
106 
107 private pure string getValue(T)(T line) @safe {
108 	static assert(isInputRange!T, T.stringof ~ " is not an InputRange");
109 	return getTimpl!('"','"')(line);
110 }
111 
112 private pure string getValueArray(T)(T line) @safe {
113 	static assert(isInputRange!T, T.stringof ~ " is not an InputRange");
114 	return getTimpl!('"','"')(line);
115 }
116 
117 @safe pure unittest {
118 	assert(getValue("firstname=\"Foo\"") == "Foo");
119 	assert(getValue("firstname=\"Foo\",\"Bar\"") == "Foo\",\"Bar");
120 }
121 
122 private pure string getKey(T)(T line) @safe {
123 	import std.exception : enforce;
124 
125 	static assert(isInputRange!T, T.stringof ~ " is not an InputRange");
126 
127 	ptrdiff_t eq = line.indexOf('=');
128 	enforce(eq != -1, "key value pair needs equal sign");
129 
130 	return line[0 .. eq].strip();
131 }
132 
133 @safe pure unittest {
134 	assert(getKey("firstname=\"Foo\"") == "firstname");
135 	assert(getKey("lastname =\"Foo\",\"Bar\"") == "lastname");
136 }
137 
138 private pure string getTimpl(char l, char r, T)(T line) @safe {
139 	static assert(isInputRange!T, T.stringof ~ " is not an InputRange");
140 
141 	ptrdiff_t l = line.indexOf(l);
142 	ptrdiff_t r = line.lastIndexOf(r);
143 
144 	assert(l+1 < line.length, format("l+1 %u line %u", l+1, line.length));
145 	return line[l+1 .. r].idup;
146 }
147 
148 private pure bool isKeyValue(T)(T line) @safe {
149 	static assert(isInputRange!T, T.stringof ~ " is not an InputRange");
150 
151 	ptrdiff_t idx = line.indexOf('=');
152 	return idx != -1;
153 }
154 
155 @safe pure unittest {
156 	assert(getSection("[initest.Person]") == "initest.Person",
157 		getSection("[initest.Person]"));
158 	assert(getSection(" [initest.Person]") == "initest.Person",
159 		getSection("[initest.Person]"));
160 	assert(getSection(" [initest.Person] ") == "initest.Person",
161 		getSection("[initest.Person]"));
162 	assert(getSection("[initest.Person] ") == "initest.Person",
163 		getSection("[initest.Person]"));
164 
165 	assert(getValue("\"initest.Person\"") == "initest.Person",
166 		getValue("\"initest.Person\""));
167 	assert(getValue(" \"initest.Person\"") == "initest.Person",
168 		getValue("\"initest.Person\""));
169 	assert(getValue(" \"initest.Person\" ") == "initest.Person",
170 		getValue("\"initest.Person\""));
171 	assert(getValue("\"initest.Person\" ") == "initest.Person",
172 		getValue("\"initest.Person\""));
173 }
174 
175 private string buildSectionParse(T)() @safe {
176 	import std.array : join;
177 	string[] ret;
178 
179 	foreach(it; __traits(allMembers, T)) {
180 		if(hasUDA!(__traits(getMember, T, it), INI)
181 			&& !isBasicType!(typeof(__traits(getMember, T, it)))
182 			&& !isSomeString!(typeof(__traits(getMember, T, it)))
183 			&& !isArray!(typeof(__traits(getMember, T, it))))
184 		{
185 			alias MemberType = typeof(__traits(getMember, T, it));
186 			static if(__traits(compiles, getINI!(MemberType))) {
187 				const name = getINI!(MemberType).name is null
188 					? fullyQualifiedName!(typeof(__traits(getMember, T, it)))
189 					: getINI!(MemberType).name;
190 			} else {
191 				const name = fullyQualifiedName!(typeof(__traits(getMember, T, it)));
192 			}
193 			ret ~= ("case \"%s\": { line = readINIFileImpl" ~
194 					"(t.%s, input, depth+1); } ").format(name,it);
195 		}
196 	}
197 
198 	// Avoid DMD switch fallthrough warnings
199 	if(ret.length) {
200 		return "switch(getSection(line)) { // " ~ fullyQualifiedName!T ~ "\n" ~
201 			ret.join("goto case; \n") ~ "goto default;\n default: return line;\n}\n";
202 	} else {
203 		return "return line;";
204 	}
205 }
206 
207 private string buildValueParse(T)() @safe {
208 	string ret = "switch(getKey(line)) { // " ~ fullyQualifiedName!T ~ "\n";
209 
210 	foreach(it; __traits(allMembers, T)) {
211 		if(hasUDA!(__traits(getMember, T, it), INI) && (isBasicType!(typeof(__traits(getMember, T, it)))
212 			|| isSomeString!(typeof(__traits(getMember, T, it)))))
213 		{
214 			const string name = getINI!(T, it).name is null ? it : getINI!(T, it).name;
215 			ret ~= ("case \"%s\": { t.%s = to!(typeof(t.%s))("
216 				~ "getValue(line)); break; }\n").format(name, it, it);
217 		} else if(hasUDA!(__traits(getMember, T, it), INI)
218 				&& isArray!(typeof(__traits(getMember, T, it))))
219 		{
220 			const string name = getINI!(T, it).name is null ? it : getINI!(T, it).name;
221 			ret ~= ("case \"%s\": { t.%s = to!(typeof(t.%s))("
222 				~ "getValueArray(line).split(',')); break; }\n").format(name, it, it);
223 		}
224 	}
225 
226 	return ret ~ "default: break;\n}\n";
227 }
228 
229 private string readINIFileImpl(T,IRange)(ref T t, ref IRange input, int depth = 0)
230 {
231 	static assert(isInputRange!IRange, IRange.stringof ~ " is not an InputRange");
232 
233 	import std.algorithm.searching : endsWith, startsWith;
234 
235 	debug version(debugLogs) {
236 		import std.stdio : writefln;
237 	}
238 	debug version(debugLogs) {
239 		writefln("%*s%d %s %x", depth, "", __LINE__, fullyQualifiedName!(typeof(t)),
240 			cast(void*)&input);
241 	}
242 	string line;
243 	bool isMultiLine;
244 	while(!input.empty()) {
245 		immutable bool wasMultiLine = isMultiLine;
246 		auto currentLine = input.front.stripRight;
247 		isMultiLine = currentLine.endsWith(`\`);
248 		// remove backslash if existent
249 		if(isMultiLine) {
250 			currentLine = currentLine[0 .. $ - 1];
251 		}
252 
253 		if(wasMultiLine) {
254 			line ~= currentLine;
255 		} else {
256 			line = currentLine.idup;
257 		}
258 
259 		input.popFront();
260 
261 		if(line.startsWith(";") || isMultiLine) {
262 			continue;
263 		}
264 		debug version(debugLogs) {
265 			writefln("%*s%d %s %s %b", depth, "", __LINE__, line, fullyQualifiedName!T,
266 				isSection(line));
267 		}
268 
269 		static if(hasUDA!(T, INI)) {
270 			const name = getINI!T().name is null ? fullyQualifiedName!T : getINI!T().name;
271 		} else {
272 			const name = fullyQualifiedName!T;
273 		}
274 		if(isSection(line) && getSection(line) != name) {
275 			debug version(debugLogs) {
276 				pragma(msg, buildSectionParse!(T));
277 				writefln("%*s%d %s", depth, "", __LINE__, getSection(line));
278 				writefln("%*s%d %x", depth, "", __LINE__,
279 					cast(void*)&input);
280 			}
281 
282 			mixin(buildSectionParse!(T));
283 		} else if(isKeyValue(line)) {
284 			debug version(debugLogs) {
285 				pragma(msg, buildValueParse!(T));
286 				writefln("%*s%d %s %s", depth, "", __LINE__, getKey(line),
287 					getValue(line));
288 			}
289 
290 			mixin(buildValueParse!(T));
291 		}
292 	}
293 
294 	return line;
295 }
296 
297 private void writeComment(ORange,IRange)(ORange orange, IRange irange) @trusted {
298 	static assert(isOutputRange!(ORange, ElementType!IRange)
299 		, ORange.stringf ~ " is not and OutputRange for " 
300 		~ ElementType!(IRange).stringof);
301 	static assert(isInputRange!IRange, IRange.stringof 
302 		~ " is not an InputRange");
303 	size_t idx = 0;
304 	foreach(it; irange) {
305 		if(idx % 77 == 0) {
306 			orange.put("; ");
307 		}
308 		orange.put(it);
309 
310 		if((idx+1) % 77 == 0) {
311 			orange.put('\n');
312 		}
313 
314 		++idx;
315 	}
316 	orange.put('\n');
317 }
318 
319 private void writeValue(ORange,T)(ORange orange, string name, T value) @trusted {
320 	static assert(isOutputRange!(ORange, string)
321 		, ORange.stringf ~ " is not and OutputRange for strings");
322 	static if(isArray!T && !isSomeString!T) {
323 		orange.formattedWrite("%s=\"", name);
324 		foreach(idx, it; value) {
325 			if(idx != 0) {
326 				orange.put(',');
327 			}
328 			orange.formattedWrite("%s", it);
329 		}
330 		orange.formattedWrite("\"");
331 	} else {
332 		orange.formattedWrite("%s=\"%s\"\n", name, value);
333 	}
334 }
335 
336 private string removeFromLastPoint(string input) pure @safe {
337 	ptrdiff_t lDot = input.lastIndexOf('.');
338 	return lDot != -1 && lDot+1 != input.length
339 		? input[lDot+1 .. $]
340 		: input;
341 }
342 
343 private void writeValues(ORange,T)(ORange oRange, string name, T value) @trusted {
344 	static assert(isOutputRange!(ORange, string)
345 		, ORange.stringf ~ " is not and OutputRange for strings");
346 	static if(isSomeString!(ElementType!T) || isBasicType!(ElementType!T)) {
347 		oRange.formattedWrite("%s=\"", removeFromLastPoint(name));
348 		foreach(idx, it; value) {
349 			if(idx != 0) {
350 				oRange.put(',');
351 			}
352 			oRange.formattedWrite("%s", it);
353 		}
354 		oRange.put('"');
355 		oRange.put('\n');
356 	} else {
357 		for(size_t i = 0; i < value.length; ++i) {
358 			oRange.formattedWrite("[%s]\n", name);
359 			writeINIFileImpl(value[i], oRange, false);
360 		}
361 	}
362 }
363 
364 void writeINIFile(T)(ref T t, string filename) @trusted {
365 	import std.stdio : File;
366 	auto oFile = File(filename, "w");
367 	auto oRange = oFile.lockingTextWriter();
368 	writeINIFileImpl(t, oRange, true);
369 }
370 
371 void writeINIFileImpl(T,ORange)(ref T t, ORange oRange, bool section)
372 		@trusted
373 {
374 	if(hasUDA!(T, INI) && section) {
375 		writeComment(oRange, getINI!T().msg);
376 	}
377 
378 	if(section) {
379 		if(hasUDA!(T, INI)) {
380 			auto ini = getINI!(T);
381 			if(ini.name !is null) {
382 				oRange.formattedWrite("[%s]\n", ini.name);
383 			} else {
384 				oRange.formattedWrite("[%s]\n", getTypeName!T);
385 			}
386 		} else {
387 			oRange.formattedWrite("[%s]\n", getTypeName!T);
388 		}
389 	}
390 
391 	foreach(it; __traits(allMembers, T)) {
392 		if(hasUDA!(__traits(getMember, T, it), INI)) {
393 			static if(isBasicType!(typeof(__traits(getMember, T, it))) ||
394 				isSomeString!(typeof(__traits(getMember, T, it))))
395 			{
396 				const ini = getINI!(T,it);
397 				const name = ini.name is null ? it : ini.name;
398 				writeComment(oRange, ini.msg);
399 				writeValue(oRange, name, __traits(getMember, t, it));
400 			} else static if(isArray!(typeof(__traits(getMember, T, it)))) {
401 				const ini = getINI!(T,it);
402 				const name = getTypeName!T ~ "." ~ (ini.name is null ? it : ini.name);
403 				writeComment(oRange, ini.msg);
404 				writeValues(oRange, name, __traits(getMember, t, it));
405 			} else static if(hasUDA!(__traits(getMember, t, it),INI)) {
406 				writeINIFileImpl(__traits(getMember, t, it), oRange, true);
407 			}
408 		}
409 	}
410 }
411 
412 version(unittest) {
413 @INI("A child must have a parent")
414 struct Child {
415 	@INI("The firstname of the child")
416 	string firstname;
417 
418 	@INI("The age of the child")
419 	int age;
420 
421 	bool opEquals(Child other) {
422 		return this.firstname == other.firstname
423 			&& this.age == other.age;
424 	}
425 }
426 
427 @INI("A Spouse")
428 struct Spouse {
429 	@INI("The firstname of the spouse")
430 	string firstname;
431 
432 	@INI("The age of the spouse")
433 	int age;
434 
435 	@INI("The House of the spouse")
436 	House house;
437 
438 	bool opEquals(Spouse other) {
439 		return this.firstname == other.firstname
440 			&& this.age == other.age
441 			&& this.house == other.house;
442 	}
443 }
444 
445 @INI("A Dog")
446 struct Dog {
447 	@INI("The name of the Dog")
448 	string name;
449 
450 	@INI("The food consumed")
451 	float kg;
452 
453 	bool opEquals(Dog other) {
454 		return this.name == other.name
455 			&& (isClose(this.kg, other.kg)
456 					|| (isNaN(this.kg) && isNaN(other.kg))
457 				);
458 	}
459 }
460 
461 @INI("A Person")
462 struct Person {
463 	@INI("The firstname of the Person")
464 	string firstname;
465 
466 	@INI("The lastname of the Person")
467 	string lastname;
468 
469 	@INI("The age of the Person")
470 	int age;
471 
472 	@INI("The height of the Person")
473 	float height;
474 
475 	@INI("Some strings with a very long long INI description that is longer" ~
476 		" than eigthy lines hopefully."
477 	)
478 	string[] someStrings = [":::60180", "asd"];
479 
480 	@INI("Some ints")
481 	int[] someInts;
482 
483 	int dontShowThis;
484 
485 	@INI("A Spouse")
486 	Spouse spouse;
487 
488 	@INI("The family dog")
489 	Dog dog;
490 
491 	bool opEquals(Person other) {
492 		import std.algorithm.comparison : equal;
493 		return this.firstname == other.firstname
494 			&& this.lastname == other.lastname
495 			&& this.age == other.age
496 			&& (isClose(this.height, other.height)
497 				|| (isNaN(this.height) && isNaN(other.height)))
498 			&& equal(this.someStrings, other.someStrings)
499 			&& equal(this.someInts, other.someInts)
500 			&& this.spouse == other.spouse
501 			&& this.dog == other.dog;
502 	}
503 }
504 
505 @INI("A House")
506 struct House {
507 	@INI("Number of Rooms")
508 	uint rooms;
509 
510 	@INI("Number of Floors")
511 	uint floors;
512 
513 	bool opEquals(House other) {
514 		return this.rooms == other.rooms
515 			&& this.floors == other.floors;
516 	}
517 }
518 }
519 
520 unittest {
521 	import std.stdio : writefln;
522 	Person p;
523 	p.firstname = "Foo";
524 	p.lastname = "Bar";
525 	p.age = 1337;
526 	p.height = 7331.0;
527 
528 	p.someStrings ~= "Hello";
529 	p.someStrings ~= "World";
530 
531 	p.someInts ~= [1,2];
532 
533 	p.spouse.firstname = "World";
534 	p.spouse.age = 72;
535 
536 	p.spouse.house.rooms = 5;
537 	p.spouse.house.floors = 2;
538 
539 	p.dog.name = "Wuff";
540 	p.dog.kg = 3.14;
541 
542 	Person p2;
543 	readINIFile(p2, "test/filename.ini");
544 	writefln("\n%s\n", p2);
545 	writeINIFile(p2, "test/filenameTmp.ini");
546 
547 	Person p3;
548 	readINIFile(p3, "test/filenameTmp.ini");
549 
550 	if(p2 != p3) {
551 		writefln("\n%s\n%s", p2, p3);
552 		writefln("Spouse equal %b", p2.spouse == p3.spouse);
553 		writefln("Dog equal %b", p2.dog == p3.dog);
554 		assert(false);
555 	}
556 	if(p != p3) {
557 		writefln("\n%s\n%s", p, p3);
558 		writefln("Spouse equal %b", p.spouse == p3.spouse);
559 		writefln("Dog equal %b", p.dog == p3.dog);
560 		assert(false);
561 	}
562 	if(p != p2) {
563 		writefln("\n%s\n%s", p, p2);
564 		writefln("Spouse equal %b", p.spouse == p2.spouse);
565 		writefln("Dog equal %b", p.dog == p2.dog);
566 		assert(false);
567 	}
568 }
569 
570 version(unittest) {
571 	enum Check : string { disabled = "disabled"}
572 
573 	@INI
574 	struct StaticAnalysisConfig {
575 		@INI
576 		string style_check = Check.disabled;
577 
578 		@INI
579 		ModuleFilters filters;
580 
581 		@INI @ConfuseTheParser
582 		string multi_line;
583 	}
584 
585 	private template ModuleFiltersMixin(A) {
586 		const string ModuleFiltersMixin = () {
587 			string s;
588 			foreach (mem; __traits(allMembers, StaticAnalysisConfig))
589 				static if(
590 					is(
591 						typeof(__traits(getMember, StaticAnalysisConfig, mem)) 
592 						== string
593 					)
594 				) {
595 					s ~= `@INI string[] ` ~ mem ~ ";\n";
596 				}
597 
598 			return s;
599 		}();
600 	}
601 
602 	@INI
603 	struct ModuleFilters { mixin(ModuleFiltersMixin!int); }
604 }
605 
606 unittest {
607 	StaticAnalysisConfig config;
608 	readINIFile(config, "test/dscanner.ini");
609 	assert(config.style_check == "disabled");
610 	assert(config.multi_line == `+std.algorithm -std.foo `);
611 	assert(config.filters.style_check == ["+std.algorithm"]);
612 }
613 
614 version(unittest) {
615 	struct ConfuseTheParser;
616 }
617 
618 unittest {
619 	@INI("Reactor Configuration", "reactorConfig")
620 	struct NukeConfig
621 	{
622 		@INI double a;
623 		@INI double b;
624 		@INI double c;
625 	}
626 
627 	@INI("General Configuration", "general")
628 	struct Configuration
629 	{
630 		@INI("Color of the bikeshed", "shedColor") string shedC;
631 		@INI @ConfuseTheParser NukeConfig nConfig;
632 	}
633 
634 	Configuration c = Configuration("blue");
635 	c.nConfig.a = 1;
636 	c.nConfig.b = 2;
637 	c.nConfig.c = 3;
638 	c.writeINIFile("test/bikeShed.ini");
639 	Configuration c2;
640 	readINIFile(c2, "test/bikeShed.ini");
641 	assert(c2.shedC == "blue");
642 	assert(c2.nConfig.a == 1);
643 	assert(c2.nConfig.b == 2);
644 	assert(c2.nConfig.c == 3);
645 }