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