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