// #define USENEWTONSOFT using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; using static System.FormattableString; namespace SharpGLTF.CodeGen { using SchemaReflection; /// /// Takes a and emits /// all its enums and classes as c# source code /// public class CSharpEmitter { #region runtime types class _RuntimeType { internal _RuntimeType(SchemaType t) { _PersistentType = t; } private readonly SchemaType _PersistentType; public string RuntimeNamespace { get; set; } public string RuntimeName { get; set; } public List Comments { get; } = new List(); private readonly Dictionary _Fields = new Dictionary(); private readonly Dictionary _Enums = new Dictionary(); public _RuntimeField UseField(FieldInfo finfo) { var key = $"{finfo.PersistentName}"; if (_Fields.TryGetValue(key, out _RuntimeField rfield)) return rfield; rfield = new _RuntimeField(finfo); _Fields[key] = rfield; return rfield; } public _RuntimeEnum UseEnum(string name) { var key = name; if (_Enums.TryGetValue(key, out _RuntimeEnum renum)) return renum; renum = new _RuntimeEnum(name); _Enums[key] = renum; return renum; } } class _RuntimeEnum { internal _RuntimeEnum(string name) { _Name = name; } private readonly string _Name; } class _RuntimeField { internal _RuntimeField(FieldInfo f) { _PersistentField = f; } private readonly FieldInfo _PersistentField; public string PrivateField { get; set; } public string PublicProperty { get; set; } public string CollectionContainer { get; set; } public string DictionaryContainer { get; set; } // MinVal, MaxVal, readonly, static // serialization sections // deserialization sections // validation sections // clone sections } private readonly Dictionary _Types = new Dictionary(); private string _DefaultCollectionContainer = "TItem[]"; #endregion #region setup & declaration private static string _SanitizeName(string name) { return name.Replace(" ", string.Empty, StringComparison.OrdinalIgnoreCase); } private _RuntimeType _UseType(SchemaType stype) { var key = $"{stype.PersistentName}"; if (_Types.TryGetValue(key, out _RuntimeType rtype)) return rtype; rtype = new _RuntimeType(stype) { RuntimeName = _SanitizeName(stype.PersistentName) }; _Types[key] = rtype; return rtype; } private bool _TryGetType(SchemaType stype, out _RuntimeType rtype) { var key = $"{stype.PersistentName}"; return _Types.TryGetValue(key, out rtype); } private _RuntimeField _UseField(FieldInfo finfo) { return _UseType(finfo.DeclaringClass).UseField(finfo); } public void SetRuntimeName(SchemaType stype, string newName, string runtimeNamespace = null) { var t = _UseType(stype); t.RuntimeNamespace = runtimeNamespace; t.RuntimeName = newName; } public void AddRuntimeComment(string persistentName, string comment) { if (!_Types.TryGetValue(persistentName, out _RuntimeType t)) return; t.Comments.Add(comment); } public IReadOnlyList GetRuntimeComments(SchemaType cls) { return !_TryGetType(cls, out var rtype) ? Array.Empty() : (IReadOnlyList)rtype.Comments; } public void SetRuntimeName(string persistentName, string runtimeName, string runtimeNamespace = null) { if (!_Types.TryGetValue(persistentName, out _RuntimeType t)) return; t.RuntimeNamespace = runtimeNamespace; t.RuntimeName = runtimeName; } public string GetRuntimeName(string persistentName) { return _Types[persistentName].RuntimeName; } public string GetRuntimeNamespace(string persistentName) { return _Types[persistentName].RuntimeNamespace ?? Constants.OutputNamespace; } public void SetFieldName(FieldInfo finfo, string name) { _UseField(finfo).PrivateField = name; } public string GetFieldRuntimeName(FieldInfo finfo) { return _UseField(finfo).PrivateField; } public void SetPropertyName(FieldInfo finfo, string name) { _UseField(finfo).PublicProperty = name; } public string GetPropertyName(FieldInfo finfo) { return _UseField(finfo).PublicProperty; } public void SetCollectionContainer(string container) { _DefaultCollectionContainer = container; } public void SetCollectionContainer(FieldInfo finfo, string container) { _UseField(finfo).CollectionContainer = container; } public void SetFieldToChildrenList(SchemaType.Context ctx, string persistentName, string fieldName) { var classType = ctx.FindClass(persistentName); if (classType == null) return; var field = classType.UseField(fieldName); var runtimeName = this.GetRuntimeName(persistentName); this.SetCollectionContainer(field, $"ChildrenList"); } public void SetFieldToChildrenDictionary(SchemaType.Context ctx, string persistentName, string fieldName) { var classType = ctx.FindClass(persistentName); if (classType == null) return; var field = classType.UseField(fieldName); var runtimeName = this.GetRuntimeName(persistentName); this.SetCollectionContainer(field, $"ChildrenDictionary"); } public void DeclareClass(ClassType type) { _UseType(type); foreach(var f in type.Fields) { var runtimeName = _SanitizeName(f.PersistentName).Replace("@","at", StringComparison.Ordinal); SetFieldName(f, $"_{runtimeName}"); SetPropertyName(f, runtimeName); } } public void DeclareEnum(EnumType type) { _UseType(type); foreach (var f in type.Values) { // SetFieldName(f, $"_{runtimeName}"); // SetPropertyName(f, runtimeName); } } public void DeclareContext(SchemaType.Context context) { foreach(var ctype in context.Classes) { DeclareClass(ctype); } foreach (var etype in context.Enumerations) { DeclareEnum(etype); } } internal string _GetRuntimeName(SchemaType type) { return _GetRuntimeName(type, null); } private string _GetRuntimeName(SchemaType type, _RuntimeField extra) { switch (type) { case null: throw new ArgumentNullException(nameof(type)); case ObjectType anyType: return anyType.PersistentName; case StringType strType: return strType.PersistentName; case BlittableType blitType: { var tname = blitType.DataType.Name; return blitType.IsNullable ? $"{tname}?" : tname; } case ArrayType arrayType: { var container = extra?.CollectionContainer; if (string.IsNullOrWhiteSpace(container)) container = _DefaultCollectionContainer; return container.Replace("TItem", _GetRuntimeName(arrayType.ItemType), StringComparison.Ordinal); } case DictionaryType dictType: { var key = _GetRuntimeName(dictType.KeyType); var val = _GetRuntimeName(dictType.ValueType); var container = extra?.CollectionContainer ?? string.Empty; if (container.StartsWith("ChildrenDictionary<")) { if (key == "String") return container.Replace("TItem", val, StringComparison.Ordinal); } return $"Dictionary<{key},{val}>"; } case EnumType enumType: return _UseType(enumType).RuntimeName; case ClassType classType: return _UseType(classType).RuntimeName; default: throw new NotImplementedException(type.PersistentName); } } private string _GetConstantRuntimeName(SchemaType type) { switch (type) { case StringType strType: return $"const {typeof(string).Name}"; case BlittableType blitType: { var tname = blitType.DataType.Name; if (blitType.DataType == typeof(int)) return $"const {tname}"; if (blitType.DataType == typeof(float)) return $"const {tname}"; if (blitType.DataType == typeof(double)) return $"const {tname}"; return $"static readonly {tname}"; } case EnumType enumType: return $"const {_UseType(enumType).RuntimeName}"; case ArrayType aType: return $"static readonly {_UseType(aType).RuntimeName}"; default: throw new NotImplementedException(); } } internal Object _GetConstantRuntimeValue(SchemaType type, Object value) { ArgumentNullException.ThrowIfNull(value); switch (type) { case StringType _: return value is string ? value : Convert.ChangeType(value, typeof(string), System.Globalization.CultureInfo.InvariantCulture); case BlittableType btype: { if (btype.DataType == typeof(bool).GetTypeInfo()) { if (value is bool) return value; var str = value as string; if (str.Equals("FALSE", StringComparison.OrdinalIgnoreCase)) return false; if (str.Equals("TRUE", StringComparison.OrdinalIgnoreCase)) return true; throw new NotImplementedException(); } return value is string ? value : Convert.ChangeType(value, btype.DataType.AsType(), System.Globalization.CultureInfo.InvariantCulture); } case EnumType etype: { var etypeName = _GetRuntimeName(type); if (value is string) return $"{etypeName}.{value}"; else return $"({etypeName}){value}"; } case ArrayType aType: { var atypeName = _GetRuntimeName(type); return value.ToString(); } default: throw new NotImplementedException(); } } #endregion #region emit public string EmitContext(SchemaType.Context context) { var sb = new StringBuilder(); sb.AppendLine("// "); sb.AppendLine(); sb.AppendLine("//------------------------------------------------------------------------------------------------"); sb.AppendLine("// This file has been programatically generated; DON´T EDIT!"); sb.AppendLine("//------------------------------------------------------------------------------------------------"); sb.AppendLine(); sb.AppendLine("#pragma warning disable SA1001"); sb.AppendLine("#pragma warning disable SA1027"); sb.AppendLine("#pragma warning disable SA1028"); sb.AppendLine("#pragma warning disable SA1121"); sb.AppendLine("#pragma warning disable SA1205"); sb.AppendLine("#pragma warning disable SA1309"); sb.AppendLine("#pragma warning disable SA1402"); sb.AppendLine("#pragma warning disable SA1505"); sb.AppendLine("#pragma warning disable SA1507"); sb.AppendLine("#pragma warning disable SA1508"); sb.AppendLine("#pragma warning disable SA1652"); sb.AppendLine(); sb.AppendLine("using System;"); sb.AppendLine("using System.Collections.Generic;"); sb.AppendLine("using System.Linq;"); sb.AppendLine("using System.Text;"); sb.AppendLine("using System.Numerics;"); sb.AppendLine("using System.Text.Json;"); sb.AppendLine(); sb.AppendLine("using JSONREADER = System.Text.Json.Utf8JsonReader;"); sb.AppendLine("using JSONWRITER = System.Text.Json.Utf8JsonWriter;"); sb.AppendLine("using FIELDINFO = SharpGLTF.Reflection.FieldInfo;"); sb.AppendLine(); string currentNamespace = null; void _setCurrentNamespace(string ns) { if (currentNamespace == ns) return; if (currentNamespace != null) sb.AppendLine("}"); currentNamespace = ns; if (currentNamespace != null) { sb.AppendLine(); sb.AppendLine($"namespace {currentNamespace}"); sb.AppendLine("{"); sb.AppendLine("using Collections;".Indent(1)); sb.AppendLine(); } } foreach (var etype in context.Enumerations) { _setCurrentNamespace(GetRuntimeNamespace(etype.PersistentName)); var cout = EmitEnum(etype); sb.AppendLine(cout); sb.AppendLine(); } foreach (var ctype in context.Classes) { if (ctype.IgnoredByEmitter) continue; _setCurrentNamespace(GetRuntimeNamespace(ctype.PersistentName)); var cout = EmitClass(ctype); sb.AppendLine(cout); sb.AppendLine(); } _setCurrentNamespace(null); return sb.ToString(); } public string EmitEnum(EnumType type) { var sb = new StringBuilder(); foreach (var l in type.Description.EmitSummary(0)) sb.EmitLine(1, l); sb.EmitLine(1, $"public enum {_GetRuntimeName(type)}"); sb.EmitLine(1, "{"); if (type.UseIntegers) { foreach (var kvp in type.Values) { var k = kvp.Key; sb.EmitLine(2, $"{k} = {kvp.Value},"); } } else { foreach (var kvp in type.Values) { var k = kvp.Key; sb.EmitLine(2, $"{k},"); } } sb.EmitLine(1, "}"); return sb.ToString(); } public string EmitClass(ClassType type) { var xclass = new CSharpClassEmitter(this) { ClassSummary = type.Description, SchemaName = type.ShortIdentifier, ClassDeclaration = _GetClassDeclaration(type), HasBaseClass = type.BaseClass != null }; xclass.AddComments(type); xclass.AddFields(type); return String.Join("\r\n",xclass.EmitCode().Indent(1)); } private string _GetClassDeclaration(ClassType type) { var classDecl = string.Empty; classDecl += "partial "; classDecl += "class "; classDecl += _GetRuntimeName(type); if (type.BaseClass != null) classDecl += $" : {_GetRuntimeName(type.BaseClass)}"; return classDecl; } internal IEnumerable _GetClassField(FieldInfo f) { var tdecl = _GetRuntimeName(f.FieldType, _UseField(f)); var fname = GetFieldRuntimeName(f); string defval = string.Empty; if (f.DefaultValue != null) { var tconst = _GetConstantRuntimeName(f.FieldType); var vconst = _GetConstantRuntimeValue(f.FieldType, f.DefaultValue); // fix boolean value if (vconst is Boolean bconst) vconst = bconst ? "true" : "false"; defval = $"{fname}Default"; yield return Invariant($"private {tconst} {defval} = {vconst};"); } if (f.ExclusiveMinimumValue != null) { var tconst = _GetConstantRuntimeName(f.FieldType); var vconst = _GetConstantRuntimeValue(f.FieldType, f.ExclusiveMinimumValue); yield return Invariant($"private {tconst} {fname}ExclusiveMinimum = {vconst};"); } if (f.InclusiveMinimumValue != null) { var tconst = _GetConstantRuntimeName(f.FieldType); var vconst = _GetConstantRuntimeValue(f.FieldType, f.InclusiveMinimumValue); yield return Invariant($"private {tconst} {fname}Minimum = {vconst};"); } if (f.InclusiveMaximumValue != null) { var tconst = _GetConstantRuntimeName(f.FieldType); var vconst = _GetConstantRuntimeValue(f.FieldType, f.InclusiveMaximumValue); yield return Invariant($"private {tconst} {fname}Maximum = {vconst};"); } if (f.ExclusiveMaximumValue != null) { var tconst = _GetConstantRuntimeName(f.FieldType); var vconst = _GetConstantRuntimeValue(f.FieldType, f.ExclusiveMaximumValue); yield return Invariant($"private {tconst} {fname}ExclusiveMaximum = {vconst};"); } if (f.MinItems > 0) { yield return $"private const int {fname}MinItems = {f.MinItems};"; } if (f.MaxItems > 0 && f.MaxItems < int.MaxValue) { yield return $"private const int {fname}MaxItems = {f.MaxItems};"; } if (f.FieldType is EnumType etype && etype.IsNullable) tdecl = tdecl + "?"; yield return string.IsNullOrEmpty(defval) ? $"private {tdecl} {fname};" : $"private {tdecl} {fname} = {defval};"; yield return string.Empty; } #endregion } /// /// Utility class to emit a /// as c# source code /// class CSharpClassEmitter { #region constructor public CSharpClassEmitter(CSharpEmitter emitter) { _Emitter = emitter; } #endregion #region data private readonly CSharpEmitter _Emitter; private readonly List _Comments = new List(); private readonly List _Fields = new List(); private readonly List _SerializerBody = new List(); private readonly List _DeserializerSwitchBody = new List(); private readonly List _FieldsNamesReflection = new List(); private readonly List _FieldsSwitchReflection = new List(); public string ClassSummary { get; set; } /// /// The name used in the schema $id field, minus the prefix and suffix /// public string SchemaName { get; set; } /// /// Represents the Runtime Class Name /// public string ClassDeclaration { get; set; } public bool HasBaseClass { get; set; } private const string _READERMODIFIER = "ref "; #endregion #region API public void AddComments(ClassType type) { _Comments.AddRange(_Emitter.GetRuntimeComments(type)); } public void AddFields(ClassType type) { foreach (var f in type.Fields) { var trname = _Emitter._GetRuntimeName(f.FieldType); var frname = _Emitter.GetFieldRuntimeName(f); _Fields.AddRange(_Emitter._GetClassField(f)); AddFieldReflection(f); // serialization if (f.FieldType is EnumType etype) // special case for enums { // emit serializer var smethod = etype.UseIntegers ? "SerializePropertyEnumValue" : "SerializePropertyEnumSymbol"; smethod = $"{smethod}<{trname}>(writer, \"{f.PersistentName}\", {frname}"; if (f.DefaultValue != null) smethod += $", {frname}Default"; smethod += ");"; this.AddFieldSerializerCase(smethod); // emit deserializer this.AddFieldDeserializerCase(f.PersistentName, $"{frname} = DeserializePropertyValue<{_Emitter._GetRuntimeName(etype)}>({_READERMODIFIER}reader);"); continue; } this.AddFieldSerializerCase(_GetJSonSerializerMethod(f)); this.AddFieldDeserializerCase(f.PersistentName, _GetJSonDeserializerMethod(f)); } } private void AddFieldReflection(FieldInfo finfo) { var trname = _Emitter._GetRuntimeName(finfo.FieldType); var frname = _Emitter.GetFieldRuntimeName(finfo); trname = trname.Replace("?", ""); // since we're adding the default value, there's no need for nullable values. var vtype = $"typeof({trname})"; var getter = $"instance => instance.{frname}"; if (finfo.DefaultValue != null) { var vconst = _Emitter._GetConstantRuntimeValue(finfo.FieldType, finfo.DefaultValue); // fix boolean value if (vconst is Boolean bconst) vconst = bconst ? "true" : "false"; getter += FormattableString.Invariant($" ?? {vconst}"); } // _FieldsReflection.Add($"yield return FIELDINFO.From(\"{finfo.PersistentName}\",this, {getter});"); _FieldsNamesReflection.Add(finfo.PersistentName); _FieldsSwitchReflection.Add($"case \"{finfo.PersistentName}\": value = FIELDINFO.From(\"{finfo.PersistentName}\",this, {getter}); return true;"); } private string _GetJSonSerializerMethod(FieldInfo f) { var pname = f.PersistentName; var fname = _Emitter.GetFieldRuntimeName(f); if (f.FieldType is ClassType ctype) { return $"SerializePropertyObject(writer, \"{pname}\", {fname});"; } if (f.FieldType is ArrayType atype) { if (f.MinItems > 0) return $"SerializeProperty(writer, \"{pname}\", {fname}, {fname}MinItems);"; return $"SerializeProperty(writer,\"{pname}\",{fname});"; } if (f.DefaultValue != null) return $"SerializeProperty(writer, \"{pname}\", {fname}, {fname}Default);"; return $"SerializeProperty(writer, \"{pname}\", {fname});"; } private string _GetJSonDeserializerMethod(FieldInfo f) { var fname = _Emitter.GetFieldRuntimeName(f); var ownerTypeName = _Emitter._GetRuntimeName(f.DeclaringClass); if (f.FieldType is ArrayType atype) { var titem = _Emitter._GetRuntimeName(atype.ItemType); return $"DeserializePropertyList<{ownerTypeName}, {titem}>({_READERMODIFIER}reader, this, {fname});"; } else if (f.FieldType is DictionaryType dtype) { var titem = _Emitter._GetRuntimeName(dtype.ValueType); return $"DeserializePropertyDictionary<{ownerTypeName}, {titem}>({_READERMODIFIER}reader, this, {fname});"; } var fieldTypeName = _Emitter._GetRuntimeName(f.FieldType); return $"DeserializePropertyValue<{ownerTypeName}, {fieldTypeName}>({_READERMODIFIER}reader, this, out {fname});"; } public void AddFieldSerializerCase(string line) { _SerializerBody.Add(line); } public void AddFieldDeserializerCase(string persistentName, string line) { _DeserializerSwitchBody.Add($"case \"{persistentName}\": {line} break;"); } public IEnumerable EmitCode() { #if USENEWTONSOFT var readerType = "JsonReader"; var writerType = "JsonWriter"; #else var readerType = "ref JSONREADER"; var writerType = "JSONWRITER"; #endif foreach (var l in _Comments) yield return $"// {l}"; foreach (var l in ClassSummary.EmitSummary(0)) yield return l; yield return "#if NET6_0_OR_GREATER"; yield return "[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]"; yield return "#endif"; yield return "[global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"SharpGLTF.CodeGen\", \"1.0.0.0\")]"; yield return ClassDeclaration; yield return "{"; yield return string.Empty; yield return "#region reflection".Indent(1); yield return string.Empty; yield return $"public const string SCHEMANAME = \"{SchemaName}\";".Indent(1); var pointerPathModifier = HasBaseClass ? "override" : "virtual"; yield return $"protected {pointerPathModifier} string GetSchemaName() => SCHEMANAME;".Indent(1); yield return string.Empty; yield return $"protected override IEnumerable ReflectFieldsNames()".Indent(1); yield return "{".Indent(1); foreach (var l in _FieldsNamesReflection) yield return $"yield return \"{l}\";".Indent(2); if (HasBaseClass) yield return "foreach(var f in base.ReflectFieldsNames()) yield return f;".Indent(2); yield return "}".Indent(1); yield return $"protected override bool TryReflectField(string name, out FIELDINFO value)".Indent(1); yield return "{".Indent(1); yield return "switch(name)".Indent(2); yield return "{".Indent(2); foreach (var l in _FieldsSwitchReflection.Indent(3)) yield return l; if (HasBaseClass) yield return "default: return base.TryReflectField(name, out value);".Indent(3); yield return "}".Indent(2); yield return "}".Indent(1); yield return string.Empty; yield return "#endregion".Indent(1); yield return string.Empty; yield return "#region data".Indent(1); yield return string.Empty; foreach (var l in _Fields.Indent(1)) yield return l; yield return "#endregion".Indent(1); yield return string.Empty; yield return "#region serialization".Indent(1); yield return string.Empty; // yield return "/// ".Indent(1); yield return $"protected override void SerializeProperties({writerType} writer)".Indent(1); yield return "{".Indent(1); if (HasBaseClass) yield return "base.SerializeProperties(writer);".Indent(2); foreach (var l in _SerializerBody.Indent(2)) yield return l; yield return "}".Indent(1); yield return string.Empty; // yield return "/// ".Indent(1); yield return $"protected override void DeserializeProperty(string jsonPropertyName, {readerType} reader)".Indent(1); yield return "{".Indent(1); yield return "switch (jsonPropertyName)".Indent(2); yield return "{".Indent(2); foreach (var l in _DeserializerSwitchBody.Indent(3)) yield return l; if (HasBaseClass) yield return $"default: base.DeserializeProperty(jsonPropertyName,{_READERMODIFIER}reader); break;".Indent(3); else yield return "default: throw new NotImplementedException();".Indent(3); yield return "}".Indent(2); yield return "}".Indent(1); yield return string.Empty; yield return "#endregion".Indent(1); yield return string.Empty; yield return "}"; } #endregion } }