1 module jaster.serialise.serialiser;
2 
3 private
4 {
5     import std.stdio;
6     import std.traits, std.format, std.range, std.typecons, std.algorithm, std.exception;
7     import jaster.serialise.archive;
8 }
9 
10 alias UseArrayBaseType = Flag!"useBaseType";
11 alias UsedObjectsT = ArchiveObject[][ArchiveObject];
12 
13 //version = SERIALISER_DEBUG_OUTPUT;
14 
15 /++
16  + This UDA should be attached to any field that is used as an attribute.
17  +
18  + For binary based archives, this likely has no real meaning.
19  + For text based archives, this will likely determine how the value is formatted.
20  +
21  + Example:
22  +  For example, with the SDLang archive (`ArchiveSDL`), the following struct.
23  +
24  +  ```
25  +  struct Foo
26  +  {
27  +      int bar;
28  +      
29  +      @Attribute
30  +      string type;
31  +  }
32  +  ```
33  +
34  +  Would produce the following SDLang file.
35  +
36  +  Foo type="SomeType" {
37  +      bar 200
38  +  }
39  + ++/
40 struct Attribute {}
41 
42 /++
43  + This UDA should be attached to a single field that represents the main value of a struct.
44  +
45  + For binary based archives, this likely has no real meaning.
46  + For text based archives, this will likely determine how the value is formatted.
47  +
48  + Example:
49  +  For example, with the SDLang archive (`ArchiveSDL`), the following struct.
50  +
51  +  ```
52  +  struct Foo
53  +  {
54  +      int bar;
55  +      
56  +      @MainValue
57  +      string type;
58  +  }
59  +  ```
60  +
61  +  Would produce the following SDLang file.
62  +
63  +  Foo "SomeType" {
64  +      bar 200
65  +  }
66  + ++/
67 struct MainValue {}
68 
69 /++
70  + This UDA is to give a custom name to a type/field.
71  +
72  + Example:
73  +  For example, with the SDLang archive (`ArchiveSDL`), the following struct.
74  +
75  +  ```
76  +  @Name("FooBar")
77  +  struct Foo
78  +  {
79  +      int bar;
80  +      
81  +      @Name("Not_a_type")
82  +      string type;
83  +  }
84  +  ```
85  +
86  +  Would produce the following SDLang file.
87  +
88  +  FooBar {
89  +      bar 200
90  +      Not_a_type "SomeType"
91  +  }
92  + ++/
93 struct Name
94 {
95     string name;
96 }
97 
98 /++
99  + This UDA is used to specify certain settings for a type/field.
100  +
101  + Notes:
102  +  If @Setting is applied to the struct (as in the type itself, instead of a field) then the setting
103  +  is applied to all of the struct's field as well.
104  +
105  + Example:
106  +  Example of applying it to the entire struct.
107  +
108  +  ```
109  +  @Setting(Serialiser.Settings.ArrayAsObject)
110  +  struct Foo
111  +  {
112  +      // Gets applied to this as well
113  +      int[] yall;
114  +  }
115  +  ```
116  +
117  +  Example of applying it to a single field.
118  +
119  +  ```
120  +  struct Foo
121  +  {
122  +      int[] bar; // Not affected
123  +      
124  +      @Setting(Serialiser.Settings.ArrayAsObject)
125  +      string[] types;
126  +  }
127  +  ```
128  + ++/
129 struct Setting
130 {
131     Serialiser.Settings settings;
132 }
133 
134 /++
135  + This UDA only functions when placed on a struct field, or an array of structs.
136  +
137  + This UDA tells the serialiser that $(B all) settingss being applied to the attached field
138  + should also be passed down onto all of the struct's fields.
139  +
140  + This is similar to placing an `@Setting` onto the struct type itself, but sometimes
141  + this may not be possible. For example, the `Vector` struct from DLSL might need some
142  + setting tweaks for it to be serialised the way you want, but you can't modify that code at all
143  + so this UDA is the best that you can do.
144  + 
145  + TODO: Example.
146  + ++/
147 struct InheritSettings {}
148 
149 /// This UDA marks a field that should be ignored by the serialiser.
150 struct Ignore {}
151 
152 /++
153  + Retrieves the name to use for the given field, for use in serialisation.
154  +
155  + Rules:
156  +  The rules for how the name is retrieved are a bit _convoluted_ so they're documented here.
157  +
158  +  For non-structs and non-array types: The return value of `F.stringof` is used.
159  +
160  +  For struct types: If the struct has an @Name UDA, then that is used.
161  +                    If not, the name of the return value of `F.stringof` is used.
162  +
163  +  For array types: If `UseBase` is set to `UseBase.yes`, then the return value of `getFieldName!(ElementType!F)` is used.
164  +                   If not, the return value of `F.stringof` is used.
165  +
166  +  For when `F` is an alias to a struct's field: The rule for the field's data type will be used, but with one change,
167  +                                                the `F.stringof` value will instead return the name of the field, instead of the type.
168  + ++/
169 static string getFieldName(alias F, UseArrayBaseType UseBase = UseArrayBaseType.yes)()
170 {
171     static if(hasUDA!(F, Name)
172            &&  (isType!F
173              || !isArray!(typeof(F))
174              || !UseBase
175                )
176              )
177         return getUDAs!(F, Name)[0].name;
178     else
179     {
180         static if(!isType!F 
181                && isArray!(typeof(F))
182                && is(ElementType!(typeof(F)) == struct)
183                && UseBase)
184             return getFieldName!(ElementType!(typeof(F)));
185         // else static if(is(typeof(F) == struct))
186         // {
187         //     alias Type = typeof(F);
188         //     return getFieldName!Type;
189         // }
190         else
191             return __traits(identifier, F);
192     }
193 }
194 
195 /++
196  + A painless way to get the `Serialiser.Settings` for a type/field.
197  +
198  + Returns:
199  +  The `Serialiser.Settings` specified by the @Settings UDA for `F`.
200  +  Or `Serialiser.Settings.None` if no settings are specified.
201  + ++/
202 static Serialiser.Settings getSettings(alias F)()
203 {
204     static if(hasUDA!(F, Setting))
205         return getUDAs!(F, Setting)[0].settings;
206     else
207         return Serialiser.Settings.None;
208 }
209 
210 /++
211  + The default serialiser provided by the engine.
212  +
213  + This serialiser is more oriented towards creating data oriented for text-based archives,
214  + rather than an optimised format that would be better for binary archives. Both can be used
215  + of course.
216  + ++/
217 final static class Serialiser
218 {
219     /++
220      + See the UDA called `Settings`.
221      + ++/
222     enum Settings : ubyte
223     {
224         /// Apply no settings.
225         None = 0,
226 
227         /++
228          + Tells the serialiser that the array field it is attached to, or all array
229          + fields in the attached struct should be serialised as a child object.
230          +
231          + Notes:
232          +  This only works on arrays of struct types for now.
233          +
234          +  If there are two arrays in the struct that contain the same type
235          +  then this settings $(B must) be used for correct serialisation.
236          +
237          + Example:
238          +  Take the given struct.
239          +
240          +  ```
241          +  struct Point{int x; int y;}
242          +  struct Foo
243          +  {
244          +      Point[] pointA = [Point(60, 60), Point(120, 120)];
245          +
246          +      @Setting(Serialiser.Settings.ArrayAsObject)
247          +      Point[] pointB = [Point(60, 60), Point(200, 200)];
248          +  }
249          +  ```
250          +
251          +  Using the SDLang archive, this would generate.
252          +
253          +  ```
254          +  Foo {
255          +      // The ones embedded inside this scope are from 'pointA'
256          +      Point {
257          +         x 60
258          +         y 60
259          +      }
260          +
261          +      Point {
262          +          x 120
263          +          y 120
264          +      }
265          +
266          +      pointB {
267          +          Point {
268          +              x 60
269          +              y 60
270          +          }
271          +
272          +          Point {
273          +              x 200
274          +              y 200
275          +          }
276          +      }
277          +  }
278          +  ```
279          + ++/
280         ArrayAsObject = 1 << 0,
281 
282         /++
283          + Tells the serialiser that the enum's value (instead of it's name) should be used as the serialised output.
284          +
285          + Notes:
286          +  This setting only affects fields that are of an enum type.
287          +
288          +  For enum types that are to be ORed together (such as an enum representing a set of flags),
289          +  this is the only way to correctly serialise the data.
290          +
291          + Example:
292          +  Take the given code:
293          +
294          +  ```
295          +  enum Flags
296          +  {
297          +      Flag1 = 1 // 0b01
298          +      Flag2 = 2 // 0b10
299          +  }
300          +
301          +  struct Foo
302          +  {
303          +      @Setting(Serialiser.Settings.EnumAsValue)
304          +      Flags firstFlag  = Flags.Flag1 | Flags.Flag2;
305          +      Flags secondFlag = Flags.Flag1;
306          +  }
307          +  ```
308          +
309          +  Using the SDLang archive, this would generate the following:
310          +
311          +  ```
312          +  Foo {
313          +      firstFlag 3
314          +      secondFlag "Flag1"
315          +  }
316          +  ```
317          + ++/
318         EnumAsValue = 1 << 1
319     }
320 
321     public static final
322     {
323         /++
324          + Serialises the given value, using the provided parent.
325          +
326          + Non-Struct & Non-Array types:
327          +  By default, these types of data are stored as child objects to the parent, holding a single
328          +  value. For example the field `string name` would become an ArchiveObject called name, holding
329          +  a single string value.
330          +
331          +  A single field can be marked as @MainValue to become the value of the `parent` object.
332          +
333          +  While binary archives may support multiple values under an object, text-based ones may only
334          +  allow a single value, which i s what @MainValue will be used for. Everything else
335          +  must be an attribute or child object.
336          +
337          +  Multiple fields can be marked as @Attribute to be used as an attribute. For example,
338          +  the field `@Attribute string name` would be added to the parent as an attribute called 'name',
339          +  holding a string value.
340          +
341          +  Multiple fields can be given the @Name UDA, and the @Setting UDA. Please see their own documentation.
342          +
343          + Struct types:
344          +  Struct types by default create a new child object inside the `parent` to store their data.
345          +  Each field in the struct follows the same rules as are being described.
346          +
347          +  @Setting and @Name can be attached to structs.
348          +
349          + Array types (for non-structs):
350          +  By default, these types of arrays will be stored as a child object in the parent, where all of the
351          +  array's values are given to the child object.
352          +
353          +  This type of array can be used as the @MainValue for a struct.
354          +
355          +  @Setting and @Name (more in the 'Array types (shared)' section) are of course supported as well.
356          +
357          + Array types (for structs):
358          +  By default, these types of arrays will write out all of the struct child objects directly into the parent.
359          +  However, the settings `Serialiser.Settings.ArrayAsObject` can be used to store these children into a seperate
360          +  child object. Please refer to it's documentation.
361          +
362          +  @Setting and @Name are supported.
363          +
364          + Array types (shared):
365          +  The names chosen when serialising arrays (and other types in general) is a bit complicated, so please refer to the documentation for
366          +  `getFieldName`.
367          +
368          + Nullable:
369          +  If the value is currently null, then nothing about it is serialised.
370          +
371          + Params:
372          +  data    = The data to serialise.
373          +  parent  = The parent to serialise the data into.
374          + ++/
375         void serialise(T)(T data, ArchiveObject parent)
376         {
377             doSerialise!(T, T, T, Settings.None)(data, parent);
378         }
379 
380         /++
381          + Deserialises the given type using `root` as the root of the data.
382          +
383          + Nullable:
384          +  If there is no data for the nullable object, then it is nullafied regardless
385          +  of what value it initialises to.
386          + ++/
387         T deserialise(T)(ArchiveObject root)
388         {
389             T toReturn = T.init;
390 
391             UsedObjectsT f;
392             doDeserialise!(T, T, T, Settings.None)(toReturn, root, f);
393 
394             return toReturn;
395         }
396 
397         /// ditto
398         T deserialise(T)(ArchiveObject root, out UsedObjectsT objectsUsed)
399         {
400             T toReturn = T.init;
401 
402             doDeserialise!(T, T, T, Settings.None)(toReturn, root, objectsUsed);
403 
404             return toReturn;
405         }
406     }
407 
408     // #################
409     // # SERIALISATION #
410     // #################
411     private static final
412     {
413         void doSerialise(T, alias MainSymbol, alias Symbol, Settings InheritedSettings)(T data, ArchiveObject parent)
414         if(ArchiveValue.allowed!T && !hasUDA!(Symbol, Attribute))
415         {
416             debug mixin(serialiseDebug("Value"));
417 
418             static if(hasUDA!(Symbol, MainValue))
419                 parent.addValueAs!T(data);
420             else
421             {
422                 auto obj = new ArchiveObject(getFieldName!Symbol);
423                 obj.addValueAs!T(data);
424                 parent.addChild(obj);
425             }
426         }
427 
428         void doSerialise(T, alias MainSymbol, alias Symbol, Settings InheritedSettings)(T data, ArchiveObject parent)
429         if(ArchiveValue.allowed!T && hasUDA!(Symbol, Attribute))
430         {
431             debug mixin(serialiseDebug("Attribute"));
432 
433             parent.setAttributeAs!T(getFieldName!Symbol, data);
434         }
435 
436         void doSerialise(T, alias MainSymbol, alias Symbol, Settings InheritedSettings)(T data, ArchiveObject parent)
437         if(isArray!T && !isSomeString!T)
438         {
439             debug mixin(serialiseDebug("Array"));
440             
441             enum settings = getSettings!MainSymbol | getSettings!Symbol | InheritedSettings;
442             
443             static if(ArchiveValue.allowed!(ElementType!T))
444             {
445                 static assert(!(settings & Settings.ArrayAsObject), "The settings 'ArrayAsObject' can only be applied to arrays of structs.");
446 
447                 static if(!hasUDA!(Symbol, MainValue))
448                 {
449                     auto obj = new ArchiveObject(getFieldName!Symbol);
450                     parent.addChild(obj);
451                 }
452                 else
453                     auto obj = parent;
454 
455                 foreach(value; data)
456                     obj.addValueAs!(ElementType!T)(value);
457             }
458             else static if(is(ElementType!T == struct))
459             {
460                 static assert(!hasUDA!(Symbol, MainValue), "Arrays of structs cannot be the main value, they can only be children.");
461 
462                 static if(settings & Settings.ArrayAsObject)
463                 {
464                     auto obj = new ArchiveObject(getFieldName!(Symbol, UseArrayBaseType.no));
465                     parent.addChild(obj);
466                 }
467                 else
468                     auto obj = parent;
469 
470                 static if(hasUDA!(Symbol, InheritSettings))
471                     enum ToInherit = InheritedSettings | getSettings!Symbol;
472                 else
473                     enum ToInherit = InheritedSettings;
474 
475                 foreach(value; data)
476                     doSerialise!(ElementType!T, MainSymbol, Symbol, cast(Settings)InheritedSettings)(value, obj);
477             }
478             else static assert(false, "Unsupported type: " ~ T.stringof);
479         }
480 
481         void doSerialise(T, alias MainSymbol, alias Symbol, Settings InheritedSettings)(T data, ArchiveObject parent)
482         if(is(T == enum))
483         {
484             import std.conv : to;
485 
486             enum settings = getSettings!Symbol;
487 
488             static if(settings & Settings.EnumAsValue)
489             {
490                 OriginalType!T value = data;
491                 alias ValueType = OriginalType!T;
492             }
493             else
494             {
495                 T value = data;
496                 alias ValueType = string;
497             }
498 
499             static if(hasUDA!(Symbol, Attribute))
500                 parent.setAttributeAs!ValueType(getFieldName!Symbol, data.to!ValueType);
501             else
502             {
503                 auto obj = new ArchiveObject(getFieldName!Symbol);
504                 obj.addValueAs!ValueType(data.to!ValueType);
505                 parent.addChild(obj);
506             }
507         }
508 
509         void doSerialise(T, alias MainSymbol, alias Symbol, Settings InheritedSettings)(T data, ArchiveObject parent)
510         if(is(T == struct))
511         {
512             debug mixin(serialiseDebug("Struct"));
513 
514             auto obj = new ArchiveObject(getFieldName!Symbol);
515             parent.addChild(obj);
516             
517             static assert(getSymbolsByUDA!(T, MainValue).length < 2, "There can only be one field marked with @MainValue");
518 
519             foreach(fieldName; FieldNameTuple!T)
520             {
521                 static if(isPublic!(T, fieldName)
522                        && !hasUDA!(mixin("T."~fieldName), Ignore))
523                 {
524                     mixin("alias FieldAlias = T.%s;".format(fieldName));
525 
526                     static if(isInstanceOf!(Nullable, typeof(FieldAlias)))
527                         alias FieldType = Unqual!(ReturnType!(FieldAlias.get));
528                     else
529                         alias FieldType = typeof(FieldAlias);
530 
531                     static if(hasUDA!(Symbol, InheritSettings))
532                         enum ToInherit = InheritedSettings | getSettings!Symbol;
533                     else
534                         enum ToInherit = InheritedSettings;
535                         
536                     auto func = () => doSerialise!(FieldType, MainSymbol, FieldAlias, cast(Settings)ToInherit)(mixin("data."~fieldName), obj);
537                     static if(isInstanceOf!(Nullable, typeof(FieldAlias)))
538                     {
539                         if(mixin("!data."~fieldName~".isNull"))
540                             func();
541                     }
542                     else
543                         func();
544                 }
545             }
546         }
547     }
548 
549     // ###################
550     // # DESERIALISATION #
551     // ###################
552     private static final
553     {
554         void doDeserialise(T, alias MainSymbol, alias Symbol, Settings InheritedSettings)(ref T data, ArchiveObject obj, ref UsedObjectsT objectsUsed)
555         if(ArchiveValue.allowed!T && !hasUDA!(Symbol, Attribute))
556         {
557             static if(hasUDA!(Symbol, MainValue))
558                 data = obj.expectValueAs!T(0);
559             else
560             {
561                 auto dataObj      = obj.expectChild(getFieldName!Symbol);
562                 data              = dataObj.expectValueAs!T(0);
563                 objectsUsed[obj] ~= dataObj;
564             }
565         }
566 
567         void doDeserialise(T, alias MainSymbol, alias Symbol, Settings InheritedSettings)(ref T data, ArchiveObject obj, ref UsedObjectsT objectsUsed)
568         if(ArchiveValue.allowed!T && hasUDA!(Symbol, Attribute))
569         {
570             data = obj.expectAttributeAs!T(getFieldName!Symbol);
571         }
572 
573         void doDeserialise(T, alias MainSymbol, alias Symbol, Settings InheritedSettings)(ref T data, ArchiveObject parent, ref UsedObjectsT objectsUsed)
574         if(isArray!T && !isSomeString!T)
575         {            
576             enum settings = getSettings!MainSymbol | getSettings!Symbol | InheritedSettings;
577             
578             static if(ArchiveValue.allowed!(ElementType!T))
579             {
580                 static if(!hasUDA!(Symbol, MainValue))
581                 {
582                     auto obj = parent.expectChild(getFieldName!Symbol);
583                     objectsUsed[parent] ~= obj;
584                 }
585                 else
586                     auto obj = parent;
587 
588                 static if(isDynamicArray!T)
589                 {
590                     foreach(value; obj.values)
591                         data ~= value.coerce!(typeof(data[0]));
592                 }
593                 else static if(isStaticArray!T)
594                 {
595                     enforce(obj.values.length == data.length, "Expected %s values, got %s.".format(data.length, obj.values.length));
596 
597                     foreach(i, value; obj.values)
598                         data[i] = value.coerce!(typeof(data[0]));
599                 }
600                 else static assert(false, "NSAUasiogoo");
601             }
602             else static if(is(ElementType!T == struct))
603             {
604                 static if(settings & Settings.ArrayAsObject)
605                 {
606                     auto obj = parent.expectChild(getFieldName!(Symbol, UseArrayBaseType.no));
607                     objectsUsed[parent] ~= obj;
608                 }
609                 else
610                     auto obj = parent;
611 
612                 static if(hasUDA!(Symbol, InheritSettings))
613                     enum ToInherit = InheritedSettings | getSettings!Symbol;
614                 else
615                     enum ToInherit = InheritedSettings;
616 
617                 foreach(child; obj.children.filter!(c => c.name == getFieldName!Symbol))
618                 {
619                     objectsUsed[obj] ~= child;
620                     data ~= typeof(data[0]).init;
621                     doDeserialise!(ElementType!T, MainSymbol, Symbol, cast(Settings)InheritedSettings)(data[$-1], child, objectsUsed);
622                 }
623             }
624             else static assert(false, "Unsupported type: " ~ T.stringof);
625         }
626 
627         void doDeserialise(T, alias MainSymbol, alias Symbol, Settings InheritedSettings)(ref T data, ArchiveObject obj, ref UsedObjectsT objectsUsed)
628         if(is(T == enum))
629         {
630             import std.conv : to;
631 
632             static if(getSettings!Symbol & Settings.EnumAsValue)
633                 alias ValueType = OriginalType!T;
634             else
635                 alias ValueType = string;
636 
637             static if(hasUDA!(Symbol, Attribute))
638                 data = obj.expectAttributeAs!ValueType(getFieldName!Symbol).to!T;
639             else
640             {
641                 auto dataObj      = obj.expectChild(getFieldName!Symbol);
642                 data              = dataObj.getValueAs!ValueType(0).to!T;
643                 objectsUsed[obj] ~= dataObj;
644             }
645         }
646 
647         void doDeserialise(T, alias MainSymbol, alias Symbol, Settings InheritedSettings)(ref T data, ArchiveObject obj, ref UsedObjectsT objectsUsed)
648         if(is(T == struct))
649         {
650             ArchiveObject structObj = (obj.name == getFieldName!Symbol) ? obj
651                                                                         : () { auto o = obj.expectChild(getFieldName!Symbol);
652                                                                                objectsUsed[obj] ~= o;
653                                                                                return o;
654                                                                              }();
655 
656             foreach(fieldName; FieldNameTuple!T)
657             {
658                 static if(isPublic!(T, fieldName)
659                        && !hasUDA!(mixin("T."~fieldName), Ignore))
660                 {
661                     mixin("alias FieldAlias = T.%s;".format(fieldName));
662 
663                     static if(isInstanceOf!(Nullable, typeof(FieldAlias)))
664                     {
665                         alias FieldType = Unqual!(ReturnType!(FieldAlias.get));
666                         enum FieldRef = "tempValue";
667                     }
668                     else
669                     {
670                         alias FieldType = typeof(FieldAlias);
671                         enum FieldRef = "data."~fieldName;
672                     }
673 
674                     static if(hasUDA!(Symbol, InheritSettings))
675                         enum ToInherit = InheritedSettings | getSettings!Symbol;
676                     else
677                         enum ToInherit = InheritedSettings;
678                     
679                     try
680                     {
681                         // We can't pass the nullable itself by ref, so we have to store it in a temp value first
682                         static if(isInstanceOf!(Nullable, typeof(FieldAlias)))
683                             FieldType tempValue;
684 
685                         doDeserialise!(FieldType, MainSymbol, FieldAlias, cast(Settings)ToInherit)(mixin(FieldRef), structObj, objectsUsed);
686                         
687                         static if(isInstanceOf!(Nullable, typeof(FieldAlias)))
688                             mixin("data."~fieldName~" = tempValue;");
689                     }
690                     catch(Exception ex)
691                     {
692                         static if(isInstanceOf!(Nullable, typeof(FieldAlias)))
693                             mixin("data."~fieldName~".nullify();");
694                         else
695                             throw ex;
696                     }
697                 }
698             }
699         }
700     }
701 }
702 
703 // Nullable test
704 version(Jasterialise_Unittests)
705 unittest
706 {
707     import fluent.asserts;
708     import std.algorithm : canFind;
709     import jaster.serialise.sdlang;
710 
711     struct A
712     {
713         Nullable!int a;
714         int b;
715         Nullable!int c;
716     }
717 
718     A a;
719     a.a.nullify;
720     a.b = 200;
721     a.c = 400;
722 
723     auto archive = new ArchiveSDL();
724     Serialiser.serialise(a, archive.root);
725 
726     archive.root["A"].getChild("a").should.beNull;
727     archive.root["A", "b"].expectValueAs!int(0).should.equal(200);
728     archive.root["A", "c"].expectValueAs!int(0).should.equal(400);
729 
730     UsedObjectsT used;
731     A b = Serialiser.deserialise!A(archive.root, used);
732     assert(a == b); // Fluent asserts doesn't like nullable
733     
734     assert(used[archive.root].canFind(archive.root["A"]));
735     assert(used[archive.root["A"]].canFind(archive.root["A", "b"]));
736     assert(used[archive.root["A"]].canFind(archive.root["A", "c"]));
737 }
738 
739 // Enum test
740 version(Jasterialise_Unittests)
741 unittest
742 {
743     import jaster.serialise.sdlang;
744     import fluent.asserts;
745 
746     enum E
747     {
748         A,
749         B = 69,
750         C
751     }
752 
753     struct A
754     {
755         @Attribute
756         E a;
757         E b;
758 
759         @Setting(Serialiser.Settings.EnumAsValue)
760         E c;
761     }
762 
763     A a;
764     a.a = E.C;
765     a.b = E.A;
766     a.c = E.B;
767 
768     auto archive = new ArchiveSDL();
769     Serialiser.serialise(a, archive.root);
770 
771     archive.root["A"].expectAttributeAs!string("a").should.equal("C");
772     archive.root["A", "b"].expectValueAs!string(0).should.equal("A");
773     archive.root["A", "c"].expectValueAs!int(0).should.equal(69);
774 
775     A b = Serialiser.deserialise!A(archive.root);
776     a.should.equal(b);
777 }
778 
779 /++
780  + Enforces that all objects used as keys have had all of their children used (based on the values in `used).
781  +
782  + Params:
783  +  used = A mapping of which children have been used for what objects.
784  + ++/
785 void enforceAllChildrenUsed(UsedObjectsT used)
786 {
787     import std.format : format;
788     import std.exception : enforce;
789 
790     foreach(parent, usedChildren; used)
791     {
792         foreach(child; parent.children)
793         {
794             bool wasUsed = false;
795             foreach(usedChild; usedChildren)
796             {
797                 if(child == usedChild)
798                 {
799                     wasUsed = true;
800                     break;
801                 }
802             }
803 
804             enforce(wasUsed, "Parent object '%s' has unused child object '%s'.".format(parent.name, child.name));
805         }
806     }
807 }
808 
809 // Best I can do at least...
810 private enum isPublic(T, string field) = is(typeof({T t = T.init; auto b = mixin("t."~field);}));
811 
812 private string serialiseDebug(string name)
813 {
814     version(SERIALISER_DEBUG_OUTPUT)
815     {
816         return format(`writefln("Func:%%-15s | T:%%-15s | MainSymbol:%%-15s | Symbol:%%-15s | data:%%-15s | parent:%%-15s",
817                                 "%s", T.stringof, MainSymbol.stringof, Symbol.stringof, data, parent.name);`, name);
818     }
819     else
820         return "";
821 }