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);