1 /++
2  + Macros:
3  + 	SUPPORTEDFORMATS = YAML, JSON, AutoDetect
4  + 	SUPPORTEDAUTOFORMATS = .yml, .yaml for YAML and .json for JSON
5  + 	SUPPORTEDSTRUCTURES = structs, built-in data types, Nullable, and std.datetime structs
6  +/
7 module siryul.siryul;
8 import siryul;
9 import std.datetime;
10 import std.meta;
11 import std.range;
12 import std.traits;
13 import std.typecons;
14 alias siryulizers = AliasSeq!(JSON, YAML);
15 
16 /++
17  + Deserializes data from a file.
18  +
19  + Files are assumed to be UTF-8 encoded. If no format (or AutoDetect) is
20  + specified, an attempt at autodetection is made.
21  +
22  + Supports $(SUPPORTEDSTRUCTURES).
23  +
24  + Params:
25  + T = Type stored in the file
26  + Format = Serialization format ($(SUPPORTEDFORMATS))
27  + path = Absolute or relative path to the file
28  +
29  + Returns: Data from the file in the format specified
30  +/
31 T fromFile(T, Format = AutoDetect, DeSiryulize flags = DeSiryulize.none)(string path) if (isSiryulizer!Format || is(Format == AutoDetect)) {
32 	static if (is(Format == AutoDetect)) {
33 		import std.path : extension;
34 		switch(path.extension) {
35 			foreach (siryulizer; siryulizers) {
36 				foreach (type; siryulizer.types) {
37 					case type:
38 						return fromFile!(T, siryulizer, flags)(path);
39 				}
40 			}
41 			default:
42 				throw new SerializeException("Unknown extension");
43 		}
44 	} else { //Not autodetecting
45 		import std.algorithm : joiner;
46 		import std.stdio : File, KeepTerminator;
47 		return fromString!(T, Format, flags)(File(path, "r").byLine(KeepTerminator.yes).joiner());
48 	}
49 }
50 ///
51 unittest {
52 	import std.exception : assertThrown;
53 	import std.file : exists, remove;
54 	import std.stdio : File;
55 	struct TestStruct {
56 		string a;
57 	}
58 	//Write some example files...
59 	File("int.yml", "w").write("---\n9");
60 	File("string.json", "w").write(`"test"`);
61 	File("struct.yml", "w").write("---\na: b");
62 	scope(exit) { //Clean up when done
63 		if ("int.yml".exists) {
64 			remove("int.yml");
65 		}
66 		if ("string.json".exists) {
67 			remove("string.json");
68 		}
69 		if ("struct.yml".exists) {
70 			remove("struct.yml");
71 		}
72 	}
73 	//Read examples from respective files
74 	assert(fromFile!(uint, YAML)("int.yml") == 9);
75 	assert(fromFile!(string, JSON)("string.json") == "test");
76 	assert(fromFile!(TestStruct, YAML)("struct.yml") == TestStruct("b"));
77 	//Read examples from respective files using automatic format detection
78 	assert(fromFile!uint("int.yml") == 9);
79 	assert(fromFile!string("string.json") == "test");
80 	assert(fromFile!TestStruct("struct.yml") == TestStruct("b"));
81 
82 	assertThrown("file.obviouslybadextension".fromFile!uint);
83 }
84 /++
85  + Deserializes data from a string.
86  +
87  + String is assumed to be UTF-8 encoded.
88  +
89  + Params:
90  + T = Type of the data to be deserialized
91  + Format = Serialization format ($(SUPPORTEDFORMATS))
92  + str = A string containing serialized data in the specified format
93  +
94  + Supports $(SUPPORTEDSTRUCTURES).
95  +
96  + Returns: Data contained in the string
97  +/
98 T fromString(T, Format, DeSiryulize flags = DeSiryulize.none,U)(U str) if (isSiryulizer!Format && isInputRange!U) {
99 	return Format.parseInput!(T, flags)(str);
100 }
101 ///
102 unittest {
103 	struct TestStruct {
104 		string a;
105 	}
106 	//Compare a struct serialized into two different formats
107 	const aStruct = fromString!(TestStruct, JSON)(`{"a": "b"}`);
108 	const anotherStruct = fromString!(TestStruct, YAML)("---\na: b");
109 	assert(aStruct == anotherStruct);
110 }
111 /++
112  + Serializes data to a string.
113  +
114  + UTF-8 encoded by default.
115  +
116  + Supports $(SUPPORTEDSTRUCTURES).
117  +
118  + Params:
119  + Format = Serialization format ($(SUPPORTEDFORMATS))
120  + data = The data to be serialized
121  +
122  + Returns: A string in the specified format representing the user's data, UTF-8 encoded
123  +/
124 @property auto toString(Format, Siryulize flags = Siryulize.none, T)(T data) if (isSiryulizer!Format) {
125 	return Format.asString!flags(data);
126 }
127 ///
128 unittest {
129 	//3 as a JSON object
130 	assert(3.toString!JSON == `3`);
131 	//"str" as a JSON object
132 	assert("str".toString!JSON == `"str"`);
133 }
134 ///For cases where toString is already defined
135 alias toFormattedString = toString;
136 /++
137  + Serializes data to a file.
138  +
139  + Any format supported by this library may be specified. If no format is
140  + specified, it will be chosen from the file extension if possible.
141  +
142  + Supports $(SUPPORTEDSTRUCTURES).
143  +
144  + This function will NOT create directories as necessary.
145  +
146  + Params:
147  + Format = Serialization format ($(SUPPORTEDFORMATS))
148  + data = The data to be serialized
149  + path = The path for the file to be written
150  +/
151 @property void toFile(Format = AutoDetect, Siryulize flags = Siryulize.none, T)(T data, string path) if (isSiryulizer!Format || is(Format == AutoDetect)) {
152 	static if (is(Format == AutoDetect)) {
153 		import std.path : extension;
154 		switch(path.extension) {
155 			foreach (siryulizer; siryulizers) {
156 				foreach (type; siryulizer.types) {
157 					case type:
158 						return data.toFile!(siryulizer, flags)(path);
159 				}
160 			}
161 			default:
162 				throw new DeserializeException("Unknown extension");
163 		}
164 	} else { //Not autodetecting
165 		import std.algorithm : copy;
166 		import std.stdio : File;
167 		data.toFormattedString!(Format, flags).copy(File(path, "w").lockingTextWriter());
168 	}
169 }
170 ///
171 unittest {
172 	import std.exception : assertThrown;
173 	import std.file : exists, remove;
174 	struct TestStruct {
175 		string a;
176 	}
177 	scope(exit) { //Clean up when done
178 		if ("int.yml".exists) {
179 			remove("int.yml");
180 		}
181 		if ("string.json".exists) {
182 			remove("string.json");
183 		}
184 		if ("struct.yml".exists) {
185 			remove("struct.yml");
186 		}
187 		if ("int-auto.yml".exists) {
188 			remove("int-auto.yml");
189 		}
190 		if ("string-auto.json".exists) {
191 			remove("string-auto.json");
192 		}
193 		if ("struct-auto.yml".exists) {
194 			remove("struct-auto.yml");
195 		}
196 	}
197 	//Write the integer "3" to "int.yml"
198 	3.toFile!YAML("int.yml");
199 	//Write the string "str" to "string.json"
200 	"str".toFile!JSON("string.json");
201 	//Write a structure to "struct.yml"
202 	TestStruct("b").toFile!YAML("struct.yml");
203 
204 	//Check that contents are correct
205 	assert("int.yml".fromFile!uint == 3);
206 	assert("string.json".fromFile!string == "str");
207 	assert("struct.yml".fromFile!TestStruct == TestStruct("b"));
208 
209 	//Write the integer "3" to "int-auto.yml", but detect format automatically
210 	3.toFile("int-auto.yml");
211 	//Write the string "str" to "string-auto.json", but detect format automatically
212 	"str".toFile("string-auto.json");
213 	//Write a structure to "struct-auto.yml", but detect format automatically
214 	TestStruct("b").toFile("struct-auto.yml");
215 
216 	//Check that contents are correct
217 	assert("int-auto.yml".fromFile!uint == 3);
218 	assert("string-auto.json".fromFile!string == "str");
219 	assert("struct-auto.yml".fromFile!TestStruct == TestStruct("b"));
220 
221 	//Bad extension for auto-detection mechanism
222 	assertThrown(3.toFile("file.obviouslybadextension"));
223 }
224 version(unittest) {
225 	struct Test2 {
226 		string inner;
227 	}
228 	enum TestEnum : uint { test = 0, something = 1, wont = 3, ya = 2 }
229 	struct TestNull {
230 		import std.typecons : Nullable;
231 		uint notNull;
232 		string aString;
233 		uint[] emptyArray;
234 		Nullable!uint aNullable;
235 		Nullable!(uint,0) anotherNullable;
236 		Nullable!SysTime noDate;
237 		Nullable!TestEnum noEnum;
238 		void toString(scope void delegate(const(char)[]) @safe sink) @safe const {
239 			import std.format : formattedWrite;
240 			sink("TestNull(");
241 			formattedWrite(sink, "%s, ", notNull);
242 			formattedWrite(sink, "%s, ", aString);
243 			formattedWrite(sink, "%s, ", emptyArray);
244 			if (aNullable.isNull) {
245 				sink("null, ");
246 			} else {
247 				formattedWrite(sink, "%s, ", aNullable.get);
248 			}
249 			if (anotherNullable.isNull) {
250 				sink("null, ");
251 			} else {
252 				formattedWrite(sink, "%s, ", anotherNullable.get);
253 			}
254 			if (noDate.isNull) {
255 				sink("null, ");
256 			} else {
257 				formattedWrite(sink, "%s, ", noDate.get);
258 			}
259 			if (noEnum.isNull) {
260 				sink("null, ");
261 			} else {
262 				formattedWrite(sink, "%s, ", noEnum.get);
263 			}
264 			sink(")");
265 		}
266 	}
267 }
268 @system unittest {
269 	import std.algorithm : canFind, filter;
270 	import std.conv : text, to;
271 	import std.datetime : Date, DateTime, SysTime, TimeOfDay;
272 	import std.exception : assertThrown;
273 	import std.format : format;
274 	import std.meta : AliasSeq, Filter;
275 	import std.stdio : writeln;
276 	import std.traits : Fields;
277 	static assert(siryulizers.length > 0);
278 	struct Test {
279 		string a;
280 		uint b;
281 		ubyte c;
282 		string[] d;
283 		short[string] e;
284 		@Optional bool f = false;
285 		Test2[string] g;
286 		double h;
287 		char i;
288 	}
289 	alias SkipImmutable = Flag!"SkipImmutable";
290 	void runTest2(SkipImmutable flag = SkipImmutable.no, T, U)(auto ref T input, auto ref U expected) {
291 		import std.traits : isPointer;
292 		foreach (siryulizer; siryulizers) {
293 			assert(isSiryulizer!siryulizer);
294 			auto gotYAMLValue = input.toFormattedString!siryulizer.fromString!(U, siryulizer);
295 			auto gotYAMLValueOmit = input.toFormattedString!(siryulizer, Siryulize.omitInits).fromString!(U, siryulizer, DeSiryulize.optionalByDefault);
296 			static if (flag == SkipImmutable.no) {
297 				immutable immutableTest = cast(immutable)(cast(immutable)input).toFormattedString!siryulizer.fromString!(U, siryulizer);
298 				immutable immutableExpected = cast(immutable)expected;
299 				const constTest = (cast(const(T))input).toFormattedString!siryulizer.fromString!(U, siryulizer);
300 				const constExpected = cast(const)expected;
301 			}
302 			debug(verbosetesting) {
303 				static if (isPointer!T) {
304 					writeln("Input:\n ", *input);
305 				} else {
306 					writeln("Input:\n ", input);
307 				}
308 				writeln("Serialized:\n", input.toFormattedString!siryulizer);
309 				static if (isPointer!T) {
310 					writeln("Output:\n ", *gotYAMLValue);
311 				} else {
312 					writeln("Output:\n ", gotYAMLValue);
313 				}
314 			}
315 			static if (isPointer!T && isPointer!U) {
316 				auto vals = format("expected %s, got %s", *expected, *gotYAMLValue);
317 				auto valsOmit = format("expected %s, got %s", *expected, *gotYAMLValueOmit);
318 				assert(*gotYAMLValue == *expected, format("%s->%s->%s failed, %s", T.stringof, siryulizer.stringof, U.stringof, vals));
319 				static if (flag == SkipImmutable.no) {
320 					assert(*constTest == *constExpected, format("%s->%s->%s failed, %s", T.stringof, siryulizer.stringof, U.stringof, vals));
321 					assert(*immutableTest == *immutableExpected, format("%s->%s->%s failed, %s", T.stringof, siryulizer.stringof, U.stringof, vals));
322 				}
323 				assert(*gotYAMLValueOmit == *expected, format("%s->%s->%s failed, %s", T.stringof, siryulizer.stringof, U.stringof, valsOmit));
324 			} else {
325 				auto vals = format("expected %s, got %s", expected, gotYAMLValue);
326 				auto valsOmit = format("expected %s, got %s", expected, gotYAMLValueOmit);
327 				assert(gotYAMLValue == expected, format("%s->%s->%s failed, %s", T.stringof, siryulizer.stringof, U.stringof, vals));
328 				static if (flag == SkipImmutable.no) {
329 					assert(constTest == constExpected, format("%s->%s->%s failed, %s", T.stringof, siryulizer.stringof, U.stringof, vals));
330 					assert(immutableTest == immutableExpected, format("%s->%s->%s failed, %s", T.stringof, siryulizer.stringof, U.stringof, vals));
331 				}
332 				assert(gotYAMLValueOmit == expected, format("%s->%s->%s failed, %s", T.stringof, siryulizer.stringof, U.stringof, valsOmit));
333 			}
334 		}
335 	}
336 	void runTest2Fail(T, U)(auto ref U value, string file = __FILE__, size_t line = __LINE__) {
337 		foreach (siryulizer; siryulizers) {
338 			assertThrown(value.toString!siryulizer.fromString!(T, siryulizer), "Expected "~siryulizer.stringof~" to throw for "~value.text~" to "~T.stringof, file, line);
339 		}
340 	}
341 	void runTest(T)(auto ref T expected) {
342 		runTest2(expected, expected);
343 	}
344 	void runTestFail(T)(auto ref T expected) {
345 		runTest2Fail!T(expected);
346 	}
347 	auto testInstance = Test("beep", 2, 4, ["derp", "blorp"], ["one":1, "two":3], false, ["Test2":Test2("test")], 4.5, 'g');
348 
349 	runTest(testInstance);
350 	runTest(testInstance.d);
351 	runTest(testInstance.g);
352 	struct StringCharTest {
353 		char a;
354 		wchar b;
355 		dchar c;
356 		string d;
357 		wstring e;
358 		dstring f;
359 	}
360 	runTest(StringCharTest('a', '‽', '\U00010300', "↑↑↓↓←→←→ⒷⒶ", "↑↑↓↓←→←→ⒷⒶ", "↑↑↓↓←→←→ⒷⒶ"));
361 
362 	assert(`{"a": null, "b": null, "c": null, "d": null, "e": null, "f": null}`.fromString!(StringCharTest,JSON) == StringCharTest.init);
363 
364 	int[4] staticArray = [0, 1, 2, 3];
365 	runTest(staticArray);
366 
367 
368 	runTest(TimeOfDay(1, 1, 1));
369 	runTest(Date(2000, 1, 1));
370 	runTest(DateTime(2000, 1, 1, 1, 1, 1));
371 	runTest(SysTime(DateTime(2000, 1, 1), UTC()));
372 
373 	runTest2!(SkipImmutable.yes)([0,1,2,3,4].filter!((a) => a%2 != 1), [0, 2, 4]);
374 
375 
376 	runTest2(3, TestEnum.wont);
377 
378 	runTest2(TestEnum.something, TestEnum.something);
379 	runTest2(TestEnum.something, "something");
380 
381 	foreach (siryulizer; siryulizers) {
382 		auto result = TestNull().toFormattedString!siryulizer.fromString!(TestNull, siryulizer);
383 
384 		assert(result.notNull == 0);
385 		assert(result.aString == "");
386 		assert(result.emptyArray == []);
387 		assert(result.aNullable.isNull());
388 		assert(result.anotherNullable.isNull());
389 		assert(result.noDate.isNull());
390 		assert(result.noEnum.isNull());
391 	}
392 	auto nullableTest2 = TestNull(1, "a");
393 	nullableTest2.aNullable = 3;
394 	nullableTest2.anotherNullable = 4;
395 	nullableTest2.noDate = SysTime(DateTime(2000, 1, 1), UTC());
396 	nullableTest2.noEnum = TestEnum.ya;
397 	runTest(nullableTest2);
398 
399 	struct SiryulizeAsTest {
400 		@SiryulizeAs("word") string something;
401 	}
402 	struct SiryulizeAsTest2 {
403 		string word;
404 	}
405 	runTest(SiryulizeAsTest("a"));
406 	runTest2(SiryulizeAsTest("a"), SiryulizeAsTest2("a"));
407 
408 	struct TestNull2 {
409 		@Optional @SiryulizeAs("v") Nullable!bool value;
410 	}
411 	auto testval = TestNull2();
412 	testval.value = true;
413 	runTest(testval);
414 	testval.value = false;
415 	runTest(testval);
416 	foreach (siryulizer; siryulizers) {
417 		assert(TestNull2().toString!siryulizer.fromString!(TestNull2, siryulizer).value.isNull);
418 		assert(siryulizer.emptyObject.fromString!(TestNull2, siryulizer).value.isNull);
419 	}
420 
421 	runTest2Fail!bool("b");
422 	runTest2!(SkipImmutable.yes)(Nullable!string.init, wstring.init);
423 	runTest2!(SkipImmutable.yes)(Nullable!char.init, wchar.init);
424 
425 	//Autoconversion tests
426 	//string <-> int
427 	runTest2("3", 3);
428 	runTest2(3, "3");
429 	//string <-> float
430 	runTest2("3.0", 3.0);
431 	runTest2(3.0, "3");
432 
433 	//Custom parser
434 	struct TimeTest {
435 		@CustomParser("fromJunk", "toJunk") SysTime time;
436 		static SysTime fromJunk(string) @safe {
437 			return SysTime(DateTime(2015,10,7,15,4,46),UTC());
438 		}
439 		static string toJunk(SysTime) @safe {
440 			return "this has nothing to do with time.";
441 		}
442 	}
443 	struct TimeTestString {
444 		string time;
445 	}
446 	runTest2(TimeTest(SysTime(DateTime(2015,10,7,15,4,46),UTC())), TimeTestString("this has nothing to do with time."));
447 	runTest2(TimeTestString("this has nothing to do with time."), TimeTest(SysTime(DateTime(2015,10,7,15,4,46),UTC())));
448 
449 	union Unhandleable { //Unions are too dangerous to handle automatically
450 		int a;
451 		char[4] b;
452 	}
453 	assert(!__traits(compiles, runTest(Unhandleable())));
454 
455 	import std.typecons : Flag;
456 	runTest2(true, Flag!"Yep".yes);
457 
458 	import std.utf : toUTF16, toUTF32;
459 	enum testStr = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
460 	enum testStrD = testStr.toUTF16;
461 	enum testStrW = testStr.toUTF32;
462 	char[32] testChr = testStr;
463 	runTest2(testChr, testStr);
464 	runTest2(testStr, testChr);
465 	dchar[32] testChr2 = testStr;
466 	runTest2(testChr2, testStrD);
467 	runTest2(testStrD, testChr2);
468 	wchar[32] testChr3 = testStr;
469 	runTest2(testChr3, testStrW);
470 	runTest2(testStrW, testChr3);
471 
472 	//int -> float[] array doesn't even make sense, should be rejected
473 	runTest2Fail!(float[])(3);
474 	//Precision loss should be rejected by default
475 	runTest2Fail!int(3.5);
476 	//bool -> string???
477 	runTest2Fail!string(true);
478 	//string -> bool???
479 	runTest2Fail!bool("nah");
480 
481 	struct PrivateTest {
482 		private uint x;
483 		bool y;
484 	}
485 
486 	runTest(PrivateTest(0,true));
487 
488 	struct IgnoreErrBad {
489 		float n = 1.2;
490 	}
491 	struct IgnoreErrGood {
492 		@IgnoreErrors int n = 5;
493 	}
494 	foreach (siryulizer; siryulizers) {
495 		assert(IgnoreErrBad().toString!siryulizer.fromString!(IgnoreErrGood, siryulizer).n == IgnoreErrGood.init.n, "IgnoreErrors test failed for "~siryulizer.stringof);
496 	}
497 
498 	struct StructPtr {
499 		ubyte[100] bytes;
500 	}
501 	StructPtr* structPtr = new StructPtr;
502 	runTest(structPtr);
503 	structPtr.bytes[0] = 1;
504 	runTest(structPtr);
505 
506 	static struct CustomSerializer {
507 		bool x;
508 		@SerializationMethod
509 		bool serialize() const @safe {
510 			return !x;
511 		}
512 		@DeserializationMethod
513 		static auto deserialize(bool input) @safe {
514 			return CustomSerializer(!input);
515 		}
516 	}
517 	runTest2(CustomSerializer(true), false);
518 }
519 ///Use standard ISO8601 format for dates and times - YYYYMMDDTHHMMSS.FFFFFFFTZ
520 enum ISO8601;
521 ///Use extended ISO8601 format for dates and times - YYYY-MM-DDTHH:MM:SS.FFFFFFFTZ
522 ///Generally more readable than standard format.
523 enum ISO8601Extended;
524 ///Autodetect the serialization format where possible.
525 enum AutoDetect;
526 package template isTimeType(T) {
527 	enum isTimeType = is(T == DateTime) || is(T == SysTime) || is(T == TimeOfDay) || is(T == Date);
528 }
529 static assert(isTimeType!DateTime);
530 static assert(isTimeType!SysTime);
531 static assert(isTimeType!Date);
532 static assert(isTimeType!TimeOfDay);
533 static assert(!isTimeType!string);
534 static assert(!isTimeType!uint);
535 static assert(!isTimeType!(DateTime[]));
536 /++
537  + Gets the value contained within an UDA (only first attribute)
538  +/
539 /++
540  + Determines whether or not the given type is a valid (de)serializer
541  +/
542 template isSiryulizer(T) {
543 	debug enum isSiryulizer = true;
544 	else enum isSiryulizer = __traits(compiles, () {
545 		uint val = T.parseInput!(uint, DeSiryulize.none)("");
546 		string str = T.asString!(Siryulize.none)(3);
547 	});
548 }
549 static assert(allSatisfy!(isSiryulizer, siryulizers));
550 debug {} else static assert(!isSiryulizer!uint);