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.format : format;
177 	string ret = "switch(getSection(line)) { // " ~ fullyQualifiedName!T ~ "\n";
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 			ret ~= ("case \"%s\": { line = readINIFileImpl" ~
186 					"(t.%s, input, deaph+1); goto repeatL; }\n").
187 				format(fullyQualifiedName!(typeof(__traits(getMember, T, it))),
188 					it
189 				);
190 		}
191 	}
192 
193 	return ret ~ "default: return line;\n}\n";
194 }
195 
196 string buildValueParse(T)() @safe {
197 	import std.traits : hasUDA, fullyQualifiedName, isArray, isBasicType, isSomeString;
198 	import std.format : format;
199 	string ret = "switch(getKey(line)) { // " ~ fullyQualifiedName!T ~ "\n";
200 
201 	foreach(it; __traits(allMembers, T)) {
202 		if(hasUDA!(__traits(getMember, T, it), INI) && (isBasicType!(typeof(__traits(getMember, T, it))) 
203 			|| isSomeString!(typeof(__traits(getMember, T, it)))))
204 		{
205 			ret ~= ("case \"%s\": { t.%s = to!(typeof(t.%s))("
206 				~ "getValue(line)); break; }\n").format(it, it, it);
207 		} else if(hasUDA!(__traits(getMember, T, it), INI) 
208 				&& isArray!(typeof(__traits(getMember, T, it)))) 
209 		{
210 			ret ~= ("case \"%s\": { t.%s = to!(typeof(t.%s))("
211 				~ "getValueArray(line).split(',')); break; }\n").format(it, it, it);
212 		}
213 	}
214 
215 	return ret ~ "default: break;\n}\n";
216 }
217 
218 string readINIFileImpl(T,IRange)(ref T t, ref IRange input, int deaph = 0)
219 		if(isInputRange!IRange) 
220 {
221 	import std.conv : to;
222 	import std.string : split;
223 	import std.algorithm.searching : startsWith;
224 	import std.traits : fullyQualifiedName;
225 	debug {
226 		import std.stdio : writefln;
227 	}
228 	debug {
229 		writefln("%*s%d %s %x", deaph, "", __LINE__, fullyQualifiedName!(typeof(t)),
230 			cast(void*)&input);
231 	}
232 	string line;
233 	while(!input.empty()) {
234 		line = input.front().idup;
235 		input.popFront();
236 
237 		repeatL:
238 		if(line.startsWith(";")) {
239 			continue;
240 		}
241 		debug {
242 			writefln("%*s%d %s %s %b", deaph, "", __LINE__, line, fullyQualifiedName!T, 
243 				isSection(line));
244 		}
245 
246 		if(isSection(line) && getSection(line) != fullyQualifiedName!T) {
247 			debug {
248 				//pragma(msg, buildSectionParse!(T));
249 				writefln("%*s%d %s", deaph, "", __LINE__, getSection(line));
250 				writefln("%*s%d %x", deaph, "", __LINE__, 
251 					cast(void*)&input);
252 			}
253 			
254 			mixin(buildSectionParse!(T));
255 		} else if(isKeyValue(line)) {
256 			debug {
257 				//pragma(msg, buildValueParse!(T));
258 				writefln("%*s%d %s %s", deaph, "", __LINE__, getKey(line), 
259 					getValue(line));
260 			}
261 			
262 			mixin(buildValueParse!(T));
263 		}
264 	}
265 
266 	return line;
267 }
268 
269 void writeComment(ORange,IRange)(ORange orange, IRange irange) @trusted 
270 	if(isOutputRange!(ORange, ElementType!IRange) && isInputRange!IRange)
271 {
272 	size_t idx = 0;
273 	foreach(it; irange) {
274 		if(idx % 77 == 0) {
275 			orange.put("; ");
276 		}
277 		orange.put(it);
278 
279 		if((idx+1) % 77 == 0) {
280 			orange.put('\n');
281 		}
282 
283 		++idx;
284 	}
285 	orange.put('\n');
286 }
287 
288 void writeValue(ORange,T)(ORange orange, string name, T value) @trusted 
289 	if(isOutputRange!(ORange, string))
290 {
291 	import std.traits : isArray, isSomeString;
292 	import std.format : formattedWrite;
293 	static if(isArray!T && !isSomeString!T) {
294 		orange.formattedWrite("%s=\"", name);
295 		foreach(idx, it; value) {
296 			if(idx != 0) {
297 				orange.put(',');
298 			} 
299 			orange.formattedWrite("%s", it);
300 		}
301 		orange.formattedWrite("\"");
302 	} else {
303 		orange.formattedWrite("%s=\"%s\"\n", name, value);
304 	}
305 }
306 
307 string removeFromLastPoint(string input) @safe {
308 	import std.string : lastIndexOf;
309 	ptrdiff_t lDot = input.lastIndexOf('.');
310 	if(lDot != -1 && lDot+1 != input.length) {
311 		return input[lDot+1 .. $];
312 	} else {
313 		return input;
314 	}
315 }
316 
317 void writeValues(ORange,T)(ORange oRange, string name, T value) @trusted 
318 	if(isOutputRange!(ORange, string))
319 {
320 	import std.traits : isBasicType, isSomeString;
321 	import std.format : formattedWrite;
322 	static if(isSomeString!(ElementType!T) || isBasicType!(ElementType!T)) {
323 		oRange.formattedWrite("%s=\"", removeFromLastPoint(name));
324 		foreach(idx, it; value) {
325 			if(idx != 0) {
326 				oRange.put(',');
327 			} 
328 			oRange.formattedWrite("%s", it);
329 		}
330 		oRange.put('"');
331 		oRange.put('\n');
332 	} else {
333 		for(size_t i = 0; i < value.length; ++i) {
334 			oRange.formattedWrite("[%s]\n", name);
335 			writeINIFileImpl(value[i], oRange, false);
336 		}
337 	}
338 }
339 
340 void writeINIFile(T)(ref T t, string filename) @trusted {
341 	import std.stdio : File;
342 	auto oFile = File(filename, "w");
343 	auto oRange = oFile.lockingTextWriter();
344 	writeINIFileImpl(t, oRange, true);
345 }
346 
347 void writeINIFileImpl(T,ORange)(ref T t, ORange oRange, bool section) 
348 		@trusted 
349 {
350 	import std.traits : getUDAs, hasUDA, Unqual, isArray, isBasicType,
351 		   isSomeString;
352 	import std.format : formattedWrite;
353 	if(hasUDA!(T, INI) && section) {
354 		writeComment(oRange, getINI!T());
355 	}
356 
357 	if(section) {
358 		oRange.formattedWrite("[%s]\n", getTypeName!T);
359 	}
360 
361 	foreach(it; __traits(allMembers, T)) {
362 		if(hasUDA!(__traits(getMember, T, it), INI)) {
363 			static if(isBasicType!(typeof(__traits(getMember, T, it))) ||
364 				isSomeString!(typeof(__traits(getMember, T, it)))) 
365 			{
366 				writeComment(oRange, getINI!(T,it));
367 				writeValue(oRange, it, __traits(getMember, t, it));
368 			} else static if(isArray!(typeof(__traits(getMember, T, it)))) {
369 				writeComment(oRange, getINI!(T,it));
370 				writeValues(oRange, getTypeName!T ~ "." ~ it, 
371 					__traits(getMember, t, it));
372 			//} else static if(isINI!(typeof(__traits(getMember, t, it)))) {
373 			} else static if(hasUDA!(__traits(getMember, t, it),INI))
374 			{
375 				writeINIFileImpl(__traits(getMember, t, it), oRange, true);
376 			}
377 		}
378 	}
379 }
380 
381 version(unittest) {
382 @INI("A child must have a parent")
383 struct Child {
384 	@INI("The firstname of the child")
385 	string firstname;
386 
387 	@INI("The age of the child")
388 	int age;
389 
390 	bool opEquals(Child other) {
391 		return this.firstname == other.firstname
392 			&& this.age == other.age;
393 	}
394 }
395 
396 @INI("A Spose")
397 struct Spose {
398 	@INI("The firstname of the spose")
399 	string firstname;
400 
401 	@INI("The age of the spose")
402 	int age;
403 
404 	@INI("The House of the spose")
405 	House house;
406 
407 	bool opEquals(Spose other) {
408 		return this.firstname == other.firstname
409 			&& this.age == other.age
410 			&& this.house == other.house;
411 	}
412 }
413 
414 @INI("A Dog")
415 struct Dog {
416 	@INI("The name of the Dog")
417 	string name;
418 
419 	@INI("The food consumed")
420 	float kg;
421 
422 	bool opEquals(Dog other) {
423 		import std.math : approxEqual, isNaN;
424 		return this.name == other.name
425 			&& (approxEqual(this.kg, other.kg)
426 					|| (isNaN(this.kg) && isNaN(other.kg))
427 				);
428 	}
429 }
430 
431 @INI("A Person")
432 struct Person {
433 	@INI("The firstname of the Person")
434 	string firstname;
435 	
436 	@INI("The lastname of the Person")
437 	string lastname;
438 
439 	@INI("The age of the Person")
440 	int age;
441 
442 	@INI("The height of the Person")
443 	float height;
444 
445 	@INI("Some strings with a very long long INI description that is longer" ~
446 		" than eigthy lines hopefully."
447 	)
448 	string[] someStrings = [":::60180", "asd"];
449 
450 	@INI("Some ints")
451 	int[] someInts;
452 
453 	int dontShowThis;
454 
455 	@INI("A Spose")
456 	Spose spose;
457 
458 	@INI("The family dog")
459 	Dog dog;
460 
461 	bool opEquals(Person other) {
462 		import std.math : approxEqual, isNaN;
463 		import std.algorithm.comparison : equal;
464 		return this.firstname == other.firstname
465 			&& this.lastname == other.lastname
466 			&& this.age == other.age
467 			&& (approxEqual(this.height, other.height)
468 				|| (isNaN(this.height) && isNaN(other.height)))
469 			&& equal(this.someStrings, other.someStrings)
470 			&& equal(this.someInts, other.someInts)
471 			&& this.spose == other.spose
472 			&& this.dog == other.dog;
473 	}
474 }
475 
476 @INI("A House")
477 struct House {
478 	@INI("Number of Rooms")
479 	uint rooms;
480 
481 	@INI("Number of Floors")
482 	uint floors;
483 
484 	bool opEquals(House other) {
485 		return this.rooms == other.rooms
486 			&& this.floors == other.floors;
487 	}
488 }
489 }
490 
491 unittest {
492 	import std.stdio : writefln;
493 	Person p;
494 	p.firstname = "Foo";
495 	p.lastname = "Bar";
496 	p.age = 1337;
497 	p.height = 7331.0;
498 
499 	p.someStrings ~= "Hello";
500 	p.someStrings ~= "World";
501 
502 	p.someInts ~= [1,2];
503 
504 	p.spose.firstname = "World";
505 	p.spose.age = 72;
506 
507 	p.spose.house.rooms = 5;
508 	p.spose.house.floors = 2;
509 
510 	p.dog.name = "Wuff";
511 	p.dog.kg = 3.14;
512 
513 	Person p2;
514 	readINIFile(p2, "filename.ini");
515 	writefln("\n%s\n", p2);
516 	writeINIFile(p2, "filenameTmp.ini");
517 
518 	Person p3;
519 	readINIFile(p3, "filenameTmp.ini");
520 
521 	if(p2 != p3) {
522 		writefln("\n%s\n%s", p2, p3);
523 		writefln("Spose equal %b", p2.spose == p3.spose);
524 		writefln("Dog equal %b", p2.dog == p3.dog);
525 		assert(false);
526 	}	
527 	if(p != p3) {
528 		writefln("\n%s\n%s", p, p3);
529 		writefln("Spose equal %b", p.spose == p3.spose);
530 		writefln("Dog equal %b", p.dog == p3.dog);
531 		assert(false);
532 	}	
533 	if(p != p2) {
534 		writefln("\n%s\n%s", p, p2);
535 		writefln("Spose equal %b", p.spose == p2.spose);
536 		writefln("Dog equal %b", p.dog == p2.dog);
537 		assert(false);
538 	}	
539 }