1 module jaster.serialise.generator.csharp;
2 
3 private
4 {
5     import std.file, std.path, std.format, std.meta, std.traits, std.algorithm;
6     import jaster.serialise;
7 
8     struct FileInfo
9     {
10         string fileName;
11         string contents;
12     }
13 
14     FileInfo[] PREMADE_FILES;
15     const FRAMEWORK_PATH = "languages/c#/framework/Framework/";
16     static this()
17     {
18         import std.path : baseName;
19 
20         static foreach(file;
21             [
22                 "Archive.cs",
23                 "ArchiveBinary.cs",
24                 "ArchiveObject.cs",
25                 "ArchiveValue.cs",
26                 "BinaryStream.cs",
27                 "CRC32.cs",
28                 "Serialiser.cs"
29             ]
30         )
31         {{
32             const Data = import(FRAMEWORK_PATH ~ file);
33             PREMADE_FILES ~= FileInfo(file.baseName, Data);
34         }}
35     }
36 }
37 
38 class CSharpGenerator
39 {
40     private static
41     {
42         enum LockType
43         {
44             Basic,
45             Struct,
46             Array,
47             Enum
48         }
49 
50         struct LockInfo
51         {
52             LockType type;
53             Object   info;
54         }
55 
56         bool[LockInfo] _genLocks;
57     }
58 
59     public static
60     {
61         void genFilesForTypes(T...)(string outputDir, string namespace)
62         if(allSatisfy!(isType, T))
63         {
64             CSharpGenerator._genLocks.clear();
65 
66             TypeField[] info;
67             foreach(type; T)
68                 info ~= TypeIntrospect.getInfoFor!type;
69 
70             outputDir = buildNormalizedPath(outputDir);
71             if(!exists(outputDir))
72                 mkdirRecurse(outputDir);
73 
74             TypeField[] allInfo; // Some TypeFields are only created *during* the generation process.
75             CSharpGenerator.copyPremadeFiles(outputDir, namespace);
76             foreach(inf; info)
77                 CSharpGenerator.genFileFromInfo(outputDir, namespace, inf, allInfo);
78 
79             CSharpGenerator.genRegisterFile(outputDir, namespace, allInfo);
80         }
81     }
82 
83     private static
84     {
85         string getBasicTypeName(BasicSerialiseInfo info)
86         {
87             return getBasicTypeName(info.type);
88         }
89 
90         string getBasicTypeName(TypeInfo info)
91         {
92             assert(info != typeid(null)
93                 && info != typeid(ubyte[])
94                 && info != typeid(ArchiveValue[]),
95                 "Not supported (yet, if ever)"
96             );
97 
98             return (info == typeid(string)) 
99                     ? "string"
100                     : (info == typeid(byte))
101                         ? "sbyte"
102                         : (info == typeid(ubyte))
103                             ? "byte"
104                             : info.toString();
105         }
106 
107         string getStructClassName(StructSerialiseInfo info)
108         {
109             auto split = info.structInfo.toString().splitter(".");
110             
111             string last;
112             foreach(thing; split)
113                 last = thing;
114 
115             return last;
116         }
117 
118         string getArrayTypeName(ArraySerialiseInfo info)
119         {
120             return info.type.castSwitch!(
121                 (BasicSerialiseInfo i)  => "List<"~CSharpGenerator.getBasicTypeName(i)~">",
122                 (StructSerialiseInfo i) => "List<"~CSharpGenerator.getStructClassName(i)~">",
123                 (ArraySerialiseInfo i)  => "List<"~CSharpGenerator.getArrayTypeName(i)~">",
124                 (EnumSerialiseInfo i)   => "List<"~CSharpGenerator.getEnumClassName(i)~">",
125             );
126         }
127 
128         string getArrayClassName(ArraySerialiseInfo info)
129         {
130             import std.array : replace;
131 
132             return CSharpGenerator.getArrayTypeName(info).replace("<", "_").replace(">", "_");
133         }
134 
135         string getEnumClassName(EnumSerialiseInfo info)
136         {
137             auto split = info.enumType.toString().splitter(".");
138             
139             string last;
140             foreach(thing; split)
141                 last = thing;
142 
143             return last;
144         }
145 
146         string getClassName(TypeSerialiseInfo info)
147         {
148             return info.castSwitch!(
149                 (BasicSerialiseInfo i)  => CSharpGenerator.getBasicTypeName(i),
150                 (StructSerialiseInfo i) => CSharpGenerator.getStructClassName(i),
151                 (ArraySerialiseInfo i)  => CSharpGenerator.getArrayTypeName(i),
152                 (EnumSerialiseInfo i)   => CSharpGenerator.getEnumClassName(i),
153                 (Object _)                 {assert(false, info.toPrettyString());}
154             );
155         }
156 
157         void ensureGen(string outputDir, string namespace, TypeField info, ref TypeField[] allInfo)
158         {
159             if(!CSharpGenerator.isLocked(info.info))
160                 CSharpGenerator.genFileFromInfo(outputDir, namespace, info, allInfo);
161         }
162 
163         LockInfo getLockInfo(TypeSerialiseInfo info)
164         {
165             return info.castSwitch!(
166                 (BasicSerialiseInfo i)  => LockInfo(LockType.Basic, i.type),
167                 (StructSerialiseInfo i) => LockInfo(LockType.Struct, i.structInfo),
168                 (ArraySerialiseInfo i)  => LockInfo(LockType.Array, i.type),
169                 (EnumSerialiseInfo i)   => LockInfo(LockType.Enum, i.type),
170                 (Object _)                 {assert(false, info.toPrettyString());}
171             );
172         }
173 
174         void lock(TypeSerialiseInfo info)
175         {
176             CSharpGenerator._genLocks[CSharpGenerator.getLockInfo(info)] = true;
177         }
178 
179         bool isLocked(TypeSerialiseInfo info)
180         {
181             return (CSharpGenerator.getLockInfo(info) in CSharpGenerator._genLocks) !is null;
182         }
183 
184         void copyPremadeFiles(string outputDir, string namespace)
185         {
186             import std.array : replace;
187 
188             foreach(file; PREMADE_FILES)
189             {
190                 auto path = buildPath(outputDir, file.fileName);
191                 write(path, file.contents.replace("namespace Framework", "namespace "~namespace));
192             }
193         }
194 
195         string genChildInfoString(TypeField type)
196         {
197             import std.algorithm : joiner;
198             import std.array     : array;
199             import std.format    : format;
200 
201             string[] flags;
202             if(type.info.flags & TypeFlags.IsAttribute)
203                 flags ~= "TypeFlags.IsAttribute";
204             if(type.info.flags & TypeFlags.IsMainValue)
205                 flags ~= "TypeFlags.IsMainValue";
206             if(type.info.allSettings & Serialiser.Settings.ArrayAsObject)
207                 flags ~= "TypeFlags.ArrayAsObject";
208             if(type.info.allSettings & Serialiser.Settings.EnumAsValue)
209                 flags ~= "TypeFlags.EnumAsValue";
210             
211             return format("new TypeChildInfo(){ Name = \"%s\", Flags = %s }", type.name, (flags is null) ? "TypeFlags.None" : flags.joiner(" | ").array);
212         }
213 
214         void genRegisterFile(string outputDir, string namespace, TypeField[] info)
215         {
216             auto code = new CodeBuilder();
217 
218             code.put("using System;");
219             code.put("using System.Collections.Generic;");
220             code.putf("namespace %s", namespace);
221             code.putScope((_)
222             {
223                 code.put("public static class Jasterialise");
224                 code.putScope((_)
225                 {
226                     code.put("public static void RegisterSerialisers()");
227                     code.putScope((_)
228                     {
229                         foreach(inf; info)
230                         {
231                             auto className = CSharpGenerator.getClassName(inf.info);
232                             auto serialName = (inf.as!ArraySerialiseInfo) ? CSharpGenerator.getArrayClassName(inf.as!ArraySerialiseInfo)
233                                                                           : CSharpGenerator.getClassName(inf.info);
234                             code.putf("Serialiser.Register<%s>(new %sSerialiser());",
235                                 className,
236                                 serialName
237                             );
238                         }
239                     });
240                 });
241             });
242 
243             import std.conv : to;
244             write(buildPath(outputDir, "Jasterialise.cs"), code.data.to!string);
245         }
246 
247         void genFileFromInfo(string outputDir, string namespace, TypeField info, ref TypeField[] allInfo)
248         {
249             auto code = new CodeBuilder();
250             string name = (info.as!ArraySerialiseInfo) ? CSharpGenerator.getArrayClassName(info.as!ArraySerialiseInfo)
251                                                        : CSharpGenerator.getClassName(info.info);
252 
253             allInfo ~= info;
254             CSharpGenerator.lock(info.info);
255 
256             code.put("using System;");
257             code.put("using System.Collections.Generic;");
258             code.put("using System.Linq;");
259             code.putf("namespace %s", namespace);
260             code.putScope((_)
261             {
262                 // "info.info" is a "TypeSerialiseInfo", which is a base class for all the switches below.
263                 info.info.castSwitch!(
264                     (BasicSerialiseInfo _)  => CSharpGenerator.genBasicCode(code, info),
265                     (ArraySerialiseInfo _)  => CSharpGenerator.genArrayCode(outputDir, namespace, code, info, allInfo),
266                     (StructSerialiseInfo _) => CSharpGenerator.genStructCode(outputDir, namespace, code, info, allInfo),
267                     (EnumSerialiseInfo _)   => CSharpGenerator.genEnumCode(outputDir, namespace, code, info, allInfo),
268                     (Object _)                 {assert(false, info.info.toPrettyString());}
269                 );
270             });
271 
272             import std.conv;
273             write(buildPath(outputDir, name~".cs"), code.data.to!string);
274         }
275 
276         void genEnumCode(string outputDir, string namespace, CodeBuilder code, TypeField typeField, ref TypeField[] allInfo)
277         {
278             auto info = typeField.as!EnumSerialiseInfo;
279             assert(info !is null);
280 
281             auto className = CSharpGenerator.getEnumClassName(info);
282             auto typeName  = CSharpGenerator.getBasicTypeName(info.type);
283 
284             auto basicInfo = new BasicSerialiseInfo();
285             basicInfo.type = info.type;
286             CSharpGenerator.ensureGen(outputDir, namespace, TypeField(typeName, basicInfo), allInfo);
287 
288             basicInfo.type = typeid(string);
289             CSharpGenerator.ensureGen(outputDir, namespace, TypeField("string", basicInfo), allInfo);
290 
291             code.putf("public enum %s : %s", className, typeName);
292             code.putScope((_)
293             {
294                 import std.format;
295                 code.putf("%s", info.values.map!(v => format("%s = %s", v.name, v.value)).joiner(",\n\t\t"));
296             });
297 
298             code.putf("public class %sSerialiser : ITypeSerialiser", className);
299             code.putScope((_)
300             {
301                 code.put("public void Serialise(ArchiveObject parent, Object obj, TypeChildInfo info)");
302                 code.putScope((_)
303                 {
304                     code.putf("var value = (%s)obj;", className);
305                     code.putf("var typeInfo = %s;", CSharpGenerator.genChildInfoString(typeField));
306                     code.put("if(info.Flags.HasFlag(TypeFlags.EnumAsValue))");
307                         code.putf("\tSerialiser.Serialise(parent, (%s)value, typeInfo);", typeName);
308                     code.put("else");
309                         code.put("\tSerialiser.Serialise(parent, value.ToString(), typeInfo);");
310                 });
311 
312                 code.put("public Object Deserialise(ArchiveObject obj, TypeChildInfo info)");
313                 code.putScope((_)
314                 {
315                     code.putf("var typeInfo = %s;", CSharpGenerator.genChildInfoString(typeField));
316                     code.put("if(info.Flags.HasFlag(TypeFlags.EnumAsValue))");
317                         code.putf("\treturn (%s)Serialiser.Deserialise<%s>(obj, typeInfo);", className, typeName);
318                     code.put("else");
319                         code.putf("\treturn (%s)Enum.Parse(typeof(%s), Serialiser.Deserialise<string>(obj, typeInfo));", className, className);
320                 });
321             });
322         }
323 
324         void genBasicCode(CodeBuilder code, TypeField typeField)
325         {
326             auto info = typeField.as!BasicSerialiseInfo;
327             assert(info !is null);
328 
329             auto typeName = CSharpGenerator.getBasicTypeName(info);
330             code.putf("public class %sSerialiser : ITypeSerialiser", typeName);
331             code.putScope((_)
332             {
333                 code.put("public void Serialise(ArchiveObject parent, Object obj, TypeChildInfo info)");
334                 code.putScope((_)
335                 {
336                     code.put("if(info.Flags.HasFlag(TypeFlags.IsMainValue))");
337                         code.putf("\tparent.AddValueAs<%s>((%s)obj);", typeName, typeName);
338                     code.put("else if(info.Flags.HasFlag(TypeFlags.IsAttribute))");
339                         code.putf("\tparent.SetAttributeAs<%s>(info.Name ?? \"NAME ME\", (%s)obj);", typeName, typeName);
340                     code.put("else");
341                     code.putScope((_)
342                     {
343                         code.putf("var arc = new ArchiveObject(info.Name ?? \"NAME ME\");");
344                         code.putf("arc.AddValueAs<%s>((%s)obj);", typeName, typeName);
345                         code.put("parent.AddChild(arc);");
346                     });
347                 });
348 
349                 code.put("public Object Deserialise(ArchiveObject obj, TypeChildInfo info)");
350                 code.putScope((_)
351                 {
352                     code.put("if(info.Flags.HasFlag(TypeFlags.IsMainValue))");
353                         code.putf("\treturn obj.ExpectValueAs<%s>(0);", typeName);
354                     code.put("else if(info.Flags.HasFlag(TypeFlags.IsAttribute))");
355                         code.putf("\treturn obj.ExpectAttributeAs<%s>(info.Name ?? \"NAME ME\");", typeName);
356                     code.put("else");
357                         code.putf("\treturn obj.ExpectChild(info.Name ?? \"NAME ME\").ExpectValueAs<%s>(0);", typeName);
358                 });
359             });
360         }
361 
362         void genArrayCode(string outputDir, string namespace, CodeBuilder code, TypeField typeField, ref TypeField[] allInfo)
363         {
364             ArraySerialiseInfo info = typeField.as!ArraySerialiseInfo;
365             code.putf("public class %sSerialiser : ITypeSerialiser", CSharpGenerator.getArrayClassName(info));
366             code.putScope((_)
367             {
368                 code.put("public void Serialise(ArchiveObject parent, Object obj, TypeChildInfo info)");
369                 code.putScope((_)
370                 {
371                     CSharpGenerator.ensureGen(outputDir, namespace, TypeField(CSharpGenerator.getClassName(info.type), info.type), allInfo);
372                     code.put("ArchiveObject arc;");
373                     if(cast(BasicSerialiseInfo)info.type)
374                     {
375                         code.put("if(!info.Flags.HasFlag(TypeFlags.IsMainValue))");
376                         code.putScope((_)
377                         {
378                             code.put("arc = new ArchiveObject(info.Name ?? \"NAME ME\");");
379                             code.put("parent.AddChild(arc);");
380                         });
381                         code.put("else");
382                             code.put("\tarc = parent;");
383 
384                         code.putf("foreach(var val in (%s)obj)", CSharpGenerator.getArrayTypeName(info));
385                         code.putScope((_)
386                         {
387                             code.put("arc.AddValueAs(val);");
388                         });
389                     }
390                     else if(cast(StructSerialiseInfo)info.type)
391                     {
392                         code.put("if(info.Flags.HasFlag(TypeFlags.ArrayAsObject))");
393                         code.putScope((_)
394                         {
395                             code.put("arc = new ArchiveObject(info.Name ?? \"NAME ME\");");
396                             code.put("parent.AddChild(arc);");
397                         });
398                         code.put("else");
399                             code.put("\tarc = parent;");
400 
401                         code.putf("foreach(var val in (%s)obj)", CSharpGenerator.getArrayTypeName(info));
402                         code.putScope((_)
403                         {
404                             code.put("Serialiser.Serialise(arc, val, new TypeChildInfo());");
405                         });
406                     }
407                     else if(cast(ArraySerialiseInfo)info.type)
408                         assert(false, "Arrays of arrays are not supported");
409                     else
410                         assert(false, info.toPrettyString());
411                 });
412 
413                 code.put("public Object Deserialise(ArchiveObject obj, TypeChildInfo info)");
414                 code.putScope((_)
415                 {
416                     code.put("ArchiveObject arc;");
417                     code.putf("var value = new %s();", CSharpGenerator.getArrayTypeName(info));
418                     if(cast(BasicSerialiseInfo)info.type)
419                     {
420                         auto valueType = cast(BasicSerialiseInfo)info.type;
421                         code.put("if(!info.Flags.HasFlag(TypeFlags.IsMainValue))");
422                             code.put("\tarc = obj.ExpectChild(info.Name ?? \"NAME ME\");");
423                         code.put("else");
424                             code.put("\tarc = obj;");
425 
426                         code.put("foreach(var val in arc.Values)");
427                         code.putScope((_)
428                         {
429                             code.putf("value.Add(val.Get<%s>());", CSharpGenerator.getBasicTypeName(valueType));
430                         });
431                     }
432                     else if(cast(StructSerialiseInfo)info.type)
433                     {
434                         auto valueType = cast(StructSerialiseInfo)info.type;
435                         code.put("if(info.Flags.HasFlag(TypeFlags.ArrayAsObject))");
436                             code.put("\tarc = obj.ExpectChild(info.Name ?? \"NAME ME\");");
437                         code.put("else");
438                             code.put("\tarc = obj;");
439 
440                         code.putf("foreach(var val in arc.Children.Where(c => c.Name == \"%s\"))", CSharpGenerator.getStructClassName(valueType));
441                         code.putScope((_)
442                         {
443                             code.putf("value.Add(Serialiser.Deserialise<%s>(val, new TypeChildInfo()));", CSharpGenerator.getStructClassName(valueType));
444                         });
445                     }
446                     else if(cast(ArraySerialiseInfo)info.type)
447                         assert(false, "Arrays of arrays are not supported");
448                     else
449                         assert(false);
450                     code.put("return value;");
451                 });
452             });
453         }
454 
455         void genStructCode(string outputDir, string namespace, CodeBuilder code, TypeField typeField, ref TypeField[] allInfo)
456         {
457             CSharpGenerator.genStructClass(outputDir, namespace, code, typeField, allInfo);
458             CSharpGenerator.genStructSerialiser(code, typeField);
459         }
460 
461         void genStructClass(string outputDir, string namespace, CodeBuilder code, TypeField typeField, ref TypeField[] allInfo)
462         {
463             auto info = typeField.as!StructSerialiseInfo;
464 
465             code.putf("public class %s", CSharpGenerator.getStructClassName(info));
466             code.putScope((_)
467             {
468                 code.putf("public %s()", CSharpGenerator.getStructClassName(info));
469                 code.putScope((_)
470                 {
471                     foreach(name, field; info.fields)
472                     {
473                         CSharpGenerator.ensureGen(outputDir, namespace, field, allInfo);
474                         if(field.as!ArraySerialiseInfo)
475                             code.putf("this.%s = new %s();", name, CSharpGenerator.getArrayTypeName(field.as!ArraySerialiseInfo));
476                     }
477                 });
478 
479                 foreach(name, field; info.fields)
480                 {
481                     CSharpGenerator.ensureGen(outputDir, namespace, field, allInfo);
482                     code.putf("public %s %s;", CSharpGenerator.getClassName(field.info), name);
483                 }
484             });
485         }
486 
487         void genStructSerialiser(CodeBuilder code, TypeField typeField)
488         {
489             auto info = typeField.as!StructSerialiseInfo;
490 
491             code.putf("public class %sSerialiser : ITypeSerialiser", CSharpGenerator.getStructClassName(info));
492             code.putScope((_)
493             {
494                 CSharpGenerator.genStructSerialiseFunc(code, info, typeField.name);
495                 CSharpGenerator.genStructDeserialiseFunc(code, info, typeField.name);
496             });
497         }
498 
499         void genStructSerialiseFunc(CodeBuilder code, StructSerialiseInfo info, string name)
500         {
501             code.put("public void Serialise(ArchiveObject parent, Object obj, TypeChildInfo info)");
502             code.putScope((_)
503             {
504                 code.putf("var retObj = new ArchiveObject(info.Name ?? \"%s\");", name);
505                 code.putf("var value = (%s)obj;", CSharpGenerator.getStructClassName(info));
506                 foreach(fieldName, fieldType; info.fields)
507                 {
508                     // Static arrays aren't so 'static' in C#
509                     if(fieldType.as!ArraySerialiseInfo)
510                     {
511                         auto array = fieldType.as!ArraySerialiseInfo;
512                         if(!array.staticArraySize.isNull)
513                         {
514                             code.putf("if(value.%s.Count != %s)", fieldName, array.staticArraySize);
515                             code.putf("\tthrow new Exception($\"The field '%s' must have exactly %s values, not {value.%s.Count}\");",
516                                 fieldName,
517                                 array.staticArraySize,
518                                 fieldName
519                             );
520                         }
521                     }
522 
523                     code.putf("Serialiser.Serialise(retObj, value.%s, %s);", fieldName, CSharpGenerator.genChildInfoString(fieldType));
524                 }
525                 code.put("parent.AddChild(retObj);");
526             });
527         }
528 
529         void genStructDeserialiseFunc(CodeBuilder code, StructSerialiseInfo info, string name)
530         {
531             code.put("public Object Deserialise(ArchiveObject obj, TypeChildInfo info)");
532             code.putScope((_)
533             {
534                 code.putf("if(obj.Name != (info.Name ?? \"%s\"))", name);
535                 code.putf("\tobj = obj.ExpectChild(info.Name ?? \"%s\");", name);
536                 code.putf("var value = new %s();", CSharpGenerator.getStructClassName(info));
537                 foreach(fieldName, fieldType; info.fields)
538                 {
539                     string typeName = CSharpGenerator.getClassName(fieldType.info);
540                     code.putf("value.%s = Serialiser.Deserialise<%s>(obj, %s);", fieldName, typeName, CSharpGenerator.genChildInfoString(fieldType));
541 
542                     // Static arrays aren't so 'static' in C#
543                     if(fieldType.as!ArraySerialiseInfo)
544                     {
545                         auto array = fieldType.as!ArraySerialiseInfo;
546                         if(!array.staticArraySize.isNull)
547                         {
548                             code.putf("if(value.%s.Count != %s)", fieldName, array.staticArraySize);
549                             code.putf("\tthrow new Exception($\"The field '%s' expects to have exactly %s values, not {value.%s.Count}\");",
550                                 fieldName,
551                                 array.staticArraySize,
552                                 fieldName
553                             );
554                         }
555                     }
556                 }
557                 code.put("return value;");
558             });
559         }
560     }
561 }
562 unittest
563 {
564     enum E
565     {
566         A = 1,
567         B = 1,
568         C = 69
569     }
570 
571     @Name("sprite")
572     struct SpriteData
573     {
574         @MainValue
575         string name;
576 
577         int[2] position;
578         int[2] size;
579     }
580 
581     @Name("spriteSheet")
582     struct SpriteSheetData
583     {
584         @MainValue
585         string name;
586 
587         int[2] position;
588         int[2] size;
589         int[2] frameSize;
590     }
591 
592     @Name("Sprite:atlas")
593     struct AtlasData
594     {
595         E someE;
596         string name;
597         string textureRef;
598         SpriteData[] sprites;
599         SpriteSheetData[] spriteSheets;
600     }
601 
602     // import std.stdio;
603     // writeln(TypeIntrospect.getInfoFor!AtlasData.info.toPrettyString);
604 
605     CSharpGenerator.genFilesForTypes!AtlasData(buildPath(getcwd(), "test/"), "Test");
606 }