Browse Source

Added self-reflection API

vpenades 8 months ago
parent
commit
40c86de95f

+ 99 - 9
build/SharpGLTF.CodeGen/CodeGen/EmitCSharp.cs

@@ -311,9 +311,9 @@ namespace SharpGLTF.CodeGen
             }
         }
 
-        private Object _GetConstantRuntimeValue(SchemaType type, Object value)
+        internal Object _GetConstantRuntimeValue(SchemaType type, Object value)
         {
-            if (value == null) throw new ArgumentNullException(nameof(value));
+            ArgumentNullException.ThrowIfNull(value);
 
             switch (type)
             {
@@ -331,8 +331,8 @@ namespace SharpGLTF.CodeGen
 
                             var str = value as string;
 
-                            if (str.ToUpperInvariant() == "FALSE") return false;
-                            if (str.ToUpperInvariant() == "TRUE") return true;
+                            if (str.Equals("FALSE", StringComparison.OrdinalIgnoreCase)) return false;
+                            if (str.Equals("TRUE", StringComparison.OrdinalIgnoreCase)) return true;
                             throw new NotImplementedException();
                         }
 
@@ -389,14 +389,22 @@ namespace SharpGLTF.CodeGen
             sb.AppendLine("#pragma warning disable SA1508");
             sb.AppendLine("#pragma warning disable SA1652");
 
-            sb.AppendLine();           
+            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("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;
 
@@ -481,6 +489,7 @@ namespace SharpGLTF.CodeGen
             var xclass = new CSharpClassEmitter(this)
             {
                 ClassSummary = type.Description,
+                SchemaName = type.ShortIdentifier,
                 ClassDeclaration = _GetClassDeclaration(type),
                 HasBaseClass = type.BaseClass != null
             };
@@ -594,8 +603,19 @@ namespace SharpGLTF.CodeGen
         private readonly List<string> _SerializerBody = new List<string>();
         private readonly List<string> _DeserializerSwitchBody = new List<string>();
 
+        private readonly List<string> _FieldsNamesReflection = new List<string>();
+        private readonly List<string> _FieldsSwitchReflection = new List<string>();
+
         public string ClassSummary { get; set; }
 
+        /// <summary>
+        /// The name used in the schema $id field, minus the prefix and suffix
+        /// </summary>
+        public string SchemaName { get; set; }
+
+        /// <summary>
+        /// Represents the Runtime Class Name
+        /// </summary>
         public string ClassDeclaration { get; set; }
 
         public bool HasBaseClass { get; set; }
@@ -620,7 +640,11 @@ namespace SharpGLTF.CodeGen
 
                 _Fields.AddRange(_Emitter._GetClassField(f));
 
-                if (f.FieldType is EnumType etype)
+                AddFieldReflection(f);
+
+                // serialization
+
+                if (f.FieldType is EnumType etype) // special case for enums
                 {
                     // emit serializer
                     var smethod = etype.UseIntegers ? "SerializePropertyEnumValue" : "SerializePropertyEnumSymbol";
@@ -640,6 +664,31 @@ namespace SharpGLTF.CodeGen
             }
         }
 
+        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;
@@ -697,8 +746,8 @@ namespace SharpGLTF.CodeGen
             var readerType = "JsonReader";
             var writerType = "JsonWriter";
             #else
-            var readerType = "ref Utf8JsonReader";
-            var writerType = "Utf8JsonWriter";
+            var readerType = "ref JSONREADER";
+            var writerType = "JSONWRITER";
             #endif
 
             foreach (var l in _Comments) yield return $"// {l}";
@@ -716,8 +765,46 @@ namespace SharpGLTF.CodeGen
 
             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<string> 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 "/// <inheritdoc />".Indent(1);
@@ -743,6 +830,9 @@ namespace SharpGLTF.CodeGen
             yield return "}".Indent(1);
 
             yield return string.Empty;
+            yield return "#endregion".Indent(1);
+
+            yield return string.Empty;            
 
             yield return "}";
         }

+ 4 - 0
build/SharpGLTF.CodeGen/Ext.KHR_DiffuseTransmission.cs

@@ -8,6 +8,10 @@ namespace SharpGLTF
 {
     class DiffuseTransmissionExtension : SchemaProcessor
     {
+        // it seems KHR_materials_diffuse_transmission has
+        // file naming issues and it's subject to change anytime.
+        // https://github.com/KhronosGroup/glTF/issues/2482
+
         private static string SchemaUri => Constants.KhronosExtensionPath("KHR_materials_diffuse_transmission", "glTF.KHR_materials_diffuse_transmission.schema.json");
 
         private const string ExtensionRootClassName = "KHR_materials_diffuse_transmission glTF extension"; // not correctly named?

+ 10 - 4
build/SharpGLTF.CodeGen/MainSchemaProcessor.cs

@@ -28,10 +28,16 @@ namespace SharpGLTF
 
             // replace Node.Matrix, Node.Rotation, Node.Scale and Node.Translation with System.Numerics.Vectors types
             var node = ctx.FindClass("Node");
-            node.GetField("matrix").SetDataType(typeof(System.Numerics.Matrix4x4), true).RemoveDefaultValue().SetItemsRange(0);
-            node.GetField("rotation").SetDataType(typeof(System.Numerics.Quaternion), true).RemoveDefaultValue().SetItemsRange(0);
-            node.GetField("scale").SetDataType(typeof(System.Numerics.Vector3), true).RemoveDefaultValue().SetItemsRange(0);
-            node.GetField("translation").SetDataType(typeof(System.Numerics.Vector3), true).RemoveDefaultValue().SetItemsRange(0);
+
+            // the default values of the transform properties is both a "known value" and null, so
+            // we preffer to set here the "known value" since it's also used to check whether
+            // the value should be serialized.
+            // But then, we need to set the values to null in the Node Constructor,
+            // because Matrix and SRT are mutually exclusive.
+            node.GetField("matrix").SetDataType(typeof(System.Numerics.Matrix4x4), true).SetDefaultValue("System.Numerics.Matrix4x4.Identity").SetItemsRange(0);
+            node.GetField("scale").SetDataType(typeof(System.Numerics.Vector3), true).SetDefaultValue("Vector3.One").SetItemsRange(0);
+            node.GetField("rotation").SetDataType(typeof(System.Numerics.Quaternion), true).SetDefaultValue("Quaternion.Identity").SetItemsRange(0);            
+            node.GetField("translation").SetDataType(typeof(System.Numerics.Vector3), true).SetDefaultValue("Vector3.Zero").SetItemsRange(0);
 
             // replace Material.emissiveFactor with System.Numerics.Vectors types
             ctx.FindClass("Material")

+ 27 - 3
src/SharpGLTF.Core/Collections/ChildrenDictionary.cs

@@ -4,8 +4,8 @@ using System.Collections.Generic;
 using System.Diagnostics;
 using System.Diagnostics.CodeAnalysis;
 using System.Linq;
-using System.Reflection;
-using System.Text;
+
+using FIELDINFO = SharpGLTF.Reflection.FieldInfo;
 
 namespace SharpGLTF.Collections
 {
@@ -13,7 +13,10 @@ namespace SharpGLTF.Collections
     /// An Specialisation of <see cref="Dictionary{TKey, TValue}"/>, which interconnects the dictionary items with the parent of the collection.
     /// </summary>    
     [System.Diagnostics.DebuggerDisplay("{Count}")]
-    public sealed class ChildrenDictionary<T, TParent> : IReadOnlyDictionary<string, T> , IDictionary<string, T>
+    public sealed class ChildrenDictionary<T, TParent>
+        : IReadOnlyDictionary<string, T>
+        , IDictionary<string, T>
+        , Reflection.IReflectionObject
         where T : class, IChildOfDictionary<TParent>
         where TParent : class
     {
@@ -170,5 +173,26 @@ namespace SharpGLTF.Collections
         }
 
         #endregion
+
+        #region API . Reflection
+
+        public IEnumerable<FIELDINFO> GetFields()
+        {
+            return this.Select(kvp => FIELDINFO.From(kvp.Key, this, dict => dict[kvp.Key]));
+        }
+
+        public bool TryGetField(string name, out FIELDINFO value)
+        {
+            if (this.TryGetValue(name, out var val))
+            {
+                value = FIELDINFO.From(name, this, dict => dict[name]);
+                return true;
+            }
+
+            value = default;
+            return false;
+        }
+
+        #endregion
     }
 }

+ 34 - 2
src/SharpGLTF.Core/Collections/ChildrenList.cs

@@ -3,7 +3,8 @@ using System.Collections;
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.Linq;
-using System.Text;
+
+using FIELDINFO = SharpGLTF.Reflection.FieldInfo;
 
 namespace SharpGLTF.Collections
 {
@@ -13,7 +14,9 @@ namespace SharpGLTF.Collections
     /// <typeparam name="T"></typeparam>
     /// <typeparam name="TParent"></typeparam>
     [System.Diagnostics.DebuggerDisplay("{Count}")]
-    public sealed class ChildrenList<T, TParent> : IList<T>, IReadOnlyList<T>
+    public sealed class ChildrenList<T, TParent>
+        : IList<T>, IReadOnlyList<T>
+        , Reflection.IReflectionArray
         where T : class, IChildOfList<TParent>
         where TParent : class
     {
@@ -214,5 +217,34 @@ namespace SharpGLTF.Collections
         }
 
         #endregion
+
+        #region Reflection
+
+        IEnumerable<FIELDINFO> Reflection.IReflectionObject.GetFields()
+        {
+            for(int i=0; i < Count; ++i)
+            {
+                yield return ((Reflection.IReflectionArray)this).GetField(i);
+            }
+        }
+
+        FIELDINFO Reflection.IReflectionArray.GetField(int index)
+        {
+            return FIELDINFO.From(index.ToString(System.Globalization.CultureInfo.InvariantCulture), this, list => list[index]);
+        }
+
+        public bool TryGetField(string name, out FIELDINFO value)
+        {
+            if (int.TryParse(name, System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.InvariantCulture, out var index))
+            {
+                value = FIELDINFO.From(name, this, list => list[index]);
+                return true;
+            }
+
+            value = default;
+            return false;
+        }
+
+        #endregion
     }
 }

+ 42 - 36
src/SharpGLTF.Core/IO/JsonSerializable.cs

@@ -1,17 +1,17 @@
-using SharpGLTF.Collections;
-
-using System;
+using System;
 using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
-using System.Linq;
 using System.Numerics;
-using System.Text;
+using System.Diagnostics.CodeAnalysis;
 
-using System.Text.Json;
+using SharpGLTF.Collections;
 
 using JSONEXCEPTION = System.Text.Json.JsonException;
 using JSONTOKEN = System.Text.Json.JsonTokenType;
 
+using JSONREADER = System.Text.Json.Utf8JsonReader;
+using JSONWRITER = System.Text.Json.Utf8JsonWriter;
+
+
 namespace SharpGLTF.IO
 {
     /// <summary>
@@ -42,9 +42,15 @@ namespace SharpGLTF.IO
 
         #endregion
 
+        #region reflection
+        internal string _SchemaName => GetSchemaName();
+        protected virtual string GetSchemaName() => "JsonSerializable";
+
+        #endregion
+
         #region serialization
 
-        internal void Serialize(Utf8JsonWriter writer)
+        internal void Serialize(JSONWRITER writer)
         {
             Guard.NotNull(writer, nameof(writer));
 
@@ -53,9 +59,9 @@ namespace SharpGLTF.IO
             writer.WriteEndObject();
         }
 
-        protected abstract void SerializeProperties(Utf8JsonWriter writer);
+        protected abstract void SerializeProperties(JSONWRITER writer);
 
-        protected static void SerializeProperty(Utf8JsonWriter writer, string name, Object value)
+        protected static void SerializeProperty(JSONWRITER writer, string name, Object value)
         {
             if (value == null) return;
 
@@ -64,7 +70,7 @@ namespace SharpGLTF.IO
             _SerializeProperty(writer, name, value);
         }
 
-        protected static void SerializeProperty(Utf8JsonWriter writer, string name, Boolean? value, Boolean? defval = null)
+        protected static void SerializeProperty(JSONWRITER writer, string name, Boolean? value, Boolean? defval = null)
         {
             if (!value.HasValue) return;
             if (defval.HasValue && defval.Value.Equals(value.Value)) return;
@@ -74,7 +80,7 @@ namespace SharpGLTF.IO
             writer.WriteBoolean(name, value.Value);
         }
 
-        protected static void SerializeProperty(Utf8JsonWriter writer, string name, Int32? value, Int32? defval = null)
+        protected static void SerializeProperty(JSONWRITER writer, string name, Int32? value, Int32? defval = null)
         {
             if (!value.HasValue) return;
             if (defval.HasValue && defval.Value.Equals(value.Value)) return;
@@ -84,7 +90,7 @@ namespace SharpGLTF.IO
             writer.WriteNumber(name, value.Value);
         }
 
-        protected static void SerializeProperty(Utf8JsonWriter writer, string name, Single? value, Single? defval = null)
+        protected static void SerializeProperty(JSONWRITER writer, string name, Single? value, Single? defval = null)
         {
             if (!value.HasValue) return;
             if (defval.HasValue && defval.Value.Equals(value.Value)) return;
@@ -94,7 +100,7 @@ namespace SharpGLTF.IO
             writer.WriteNumber(name, value.Value);
         }
 
-        protected static void SerializeProperty(Utf8JsonWriter writer, string name, Double? value, Double? defval = null)
+        protected static void SerializeProperty(JSONWRITER writer, string name, Double? value, Double? defval = null)
         {
             if (!value.HasValue) return;
             if (defval.HasValue && defval.Value.Equals(value.Value)) return;
@@ -104,7 +110,7 @@ namespace SharpGLTF.IO
             writer.WriteNumber(name, value.Value);
         }
 
-        protected static void SerializeProperty(Utf8JsonWriter writer, string name, Vector2? value, Vector2? defval = null)
+        protected static void SerializeProperty(JSONWRITER writer, string name, Vector2? value, Vector2? defval = null)
         {
             if (!value.HasValue) return;
             if (defval.HasValue && defval.Value.Equals(value.Value)) return;
@@ -115,7 +121,7 @@ namespace SharpGLTF.IO
             writer.WriteVector2(value.Value);
         }
 
-        protected static void SerializeProperty(Utf8JsonWriter writer, string name, Vector3? value, Vector3? defval = null)
+        protected static void SerializeProperty(JSONWRITER writer, string name, Vector3? value, Vector3? defval = null)
         {
             if (!value.HasValue) return;
             if (defval.HasValue && defval.Value.Equals(value.Value)) return;
@@ -126,7 +132,7 @@ namespace SharpGLTF.IO
             writer.WriteVector3(value.Value);
         }
 
-        protected static void SerializeProperty(Utf8JsonWriter writer, string name, Vector4? value, Vector4? defval = null)
+        protected static void SerializeProperty(JSONWRITER writer, string name, Vector4? value, Vector4? defval = null)
         {
             if (!value.HasValue) return;
             if (defval.HasValue && defval.Value.Equals(value.Value)) return;
@@ -137,7 +143,7 @@ namespace SharpGLTF.IO
             writer.WriteVector4(value.Value);
         }
 
-        protected static void SerializeProperty(Utf8JsonWriter writer, string name, Quaternion? value, Quaternion? defval = null)
+        protected static void SerializeProperty(JSONWRITER writer, string name, Quaternion? value, Quaternion? defval = null)
         {
             if (!value.HasValue) return;
             if (defval.HasValue && defval.Value.Equals(value.Value)) return;
@@ -148,7 +154,7 @@ namespace SharpGLTF.IO
             writer.WriteQuaternion(value.Value);
         }
 
-        protected static void SerializeProperty(Utf8JsonWriter writer, string name, Matrix4x4? value, Matrix4x4? defval = null)
+        protected static void SerializeProperty(JSONWRITER writer, string name, Matrix4x4? value, Matrix4x4? defval = null)
         {
             if (!value.HasValue) return;
             if (defval.HasValue && defval.Value.Equals(value.Value)) return;
@@ -159,7 +165,7 @@ namespace SharpGLTF.IO
             writer.WriteMatrix4x4(value.Value);
         }
 
-        protected static void SerializePropertyEnumValue<T>(Utf8JsonWriter writer, string name, T? value, T? defval = null)
+        protected static void SerializePropertyEnumValue<T>(JSONWRITER writer, string name, T? value, T? defval = null)
             where T : struct
         {
             Guard.IsTrue(typeof(T).IsEnum, nameof(T));
@@ -172,7 +178,7 @@ namespace SharpGLTF.IO
             writer.WriteNumber(name, (int)(Object)value);
         }
 
-        protected static void SerializePropertyEnumSymbol<T>(Utf8JsonWriter writer, string name, T? value, T? defval = null)
+        protected static void SerializePropertyEnumSymbol<T>(JSONWRITER writer, string name, T? value, T? defval = null)
             where T : struct
         {
             Guard.IsTrue(typeof(T).IsEnum, nameof(T));
@@ -185,7 +191,7 @@ namespace SharpGLTF.IO
             writer.WriteString(name, Enum.GetName(typeof(T), value));
         }
 
-        protected static void SerializePropertyObject<T>(Utf8JsonWriter writer, string name, T value)
+        protected static void SerializePropertyObject<T>(JSONWRITER writer, string name, T value)
             where T : JsonSerializable
         {
             if (value == null) return;
@@ -195,7 +201,7 @@ namespace SharpGLTF.IO
             _SerializeProperty(writer, name, value);
         }
 
-        protected static void SerializeProperty<T>(Utf8JsonWriter writer, string name, IReadOnlyList<T> collection, int? minItems = 1)
+        protected static void SerializeProperty<T>(JSONWRITER writer, string name, IReadOnlyList<T> collection, int? minItems = 1)
         {
             if (collection == null) return;
             if (minItems.HasValue && collection.Count < minItems.Value) return;
@@ -211,7 +217,7 @@ namespace SharpGLTF.IO
             writer.WriteEndArray();
         }
 
-        protected static void SerializeProperty<T>(Utf8JsonWriter writer, string name, IReadOnlyDictionary<String, T> collection)
+        protected static void SerializeProperty<T>(JSONWRITER writer, string name, IReadOnlyDictionary<String, T> collection)
         {
             if (collection == null) return;
             if (collection.Count < 1) return;
@@ -227,7 +233,7 @@ namespace SharpGLTF.IO
             writer.WriteEndObject();
         }
 
-        private static void _SerializeProperty(Utf8JsonWriter writer, String name, Object value)
+        private static void _SerializeProperty(JSONWRITER writer, String name, Object value)
         {
             Guard.NotNull(writer, nameof(writer));
             Guard.NotNull(value, nameof(value));
@@ -247,7 +253,7 @@ namespace SharpGLTF.IO
             return false;
         }
 
-        private static void _SerializeValue(Utf8JsonWriter writer, Object value)
+        private static void _SerializeValue(JSONWRITER writer, Object value)
         {
             Guard.NotNull(writer, nameof(writer));
             Guard.NotNull(value, nameof(value));
@@ -314,7 +320,7 @@ namespace SharpGLTF.IO
 
         #region deserialization
 
-        internal void Deserialize(ref Utf8JsonReader reader)
+        internal void Deserialize(ref JSONREADER reader)
         {
             if (reader.TokenType == JSONTOKEN.PropertyName) reader.Read();
 
@@ -340,7 +346,7 @@ namespace SharpGLTF.IO
             throw new JSONEXCEPTION($"Unexpected token {reader.TokenType}");
         }
 
-        protected static Object DeserializeUnknownObject(ref Utf8JsonReader reader)
+        protected static Object DeserializeUnknownObject(ref JSONREADER reader)
         {
             if (reader.TokenType == JSONTOKEN.PropertyName) reader.Read();
 
@@ -385,13 +391,13 @@ namespace SharpGLTF.IO
             return reader.GetAnyValue();
         }
 
-        protected abstract void DeserializeProperty(string jsonPropertyName, ref Utf8JsonReader reader);
+        protected abstract void DeserializeProperty(string jsonPropertyName, ref JSONREADER reader);
 
         protected static T DeserializePropertyValue<
             #if !NETSTANDARD
             [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)]
             #endif
-            T>(ref Utf8JsonReader reader)
+            T>(ref JSONREADER reader)
         {
             _TryCastValue<T>(ref reader, out Object v);
 
@@ -407,7 +413,7 @@ namespace SharpGLTF.IO
             #if !NETSTANDARD
             [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)]
             #endif
-            T>(ref Utf8JsonReader reader, TParent owner, out T property)
+            T>(ref JSONREADER reader, TParent owner, out T property)
             where TParent: class
         {
             _TryCastValue<T>(ref reader, out Object v);
@@ -429,7 +435,7 @@ namespace SharpGLTF.IO
             #if !NETSTANDARD
             [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)]
             #endif
-            T>(ref Utf8JsonReader reader, TParent owner, IList<T> list)
+            T>(ref JSONREADER reader, TParent owner, IList<T> list)
             where TParent : class
         {
             DeserializePropertyList<T>(ref reader, list);
@@ -439,7 +445,7 @@ namespace SharpGLTF.IO
             #if !NETSTANDARD
             [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)]
             #endif
-            T>(ref Utf8JsonReader reader, IList<T> list)
+            T>(ref JSONREADER reader, IList<T> list)
         {
             // Guard.NotNull(reader, nameof(reader));
             Guard.NotNull(list, nameof(list));
@@ -471,7 +477,7 @@ namespace SharpGLTF.IO
             #if !NETSTANDARD
             [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)]
             #endif
-            T>(ref Utf8JsonReader reader, TParent owner, IDictionary<string, T> dict)
+            T>(ref JSONREADER reader, TParent owner, IDictionary<string, T> dict)
             where TParent : class
         {
             DeserializePropertyDictionary<T>(ref reader, dict);
@@ -481,7 +487,7 @@ namespace SharpGLTF.IO
             #if !NETSTANDARD
             [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)]
             #endif
-            T>(ref Utf8JsonReader reader, IDictionary<string, T> dict)            
+            T>(ref JSONREADER reader, IDictionary<string, T> dict)            
         {
             Guard.NotNull(dict, nameof(dict));
 
@@ -516,7 +522,7 @@ namespace SharpGLTF.IO
             #if !NETSTANDARD
             [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.NonPublicConstructors)]
             #endif
-            T>(ref Utf8JsonReader reader, out Object value)
+            T>(ref JSONREADER reader, out Object value)
         {
             value = null;
 

+ 106 - 0
src/SharpGLTF.Core/Reflection/FieldInfo.cs

@@ -0,0 +1,106 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace SharpGLTF.Reflection
+{
+    /// <summary>
+    /// Represents a reflected glTF property
+    /// </summary>
+    /// <remarks>
+    /// This structure and its API is subject to change in the next versions, so for now avoid using it directly.
+    /// </remarks>
+    [System.Diagnostics.DebuggerDisplay("({ValueType}) {Name} = {Value}")]
+    public readonly struct FieldInfo
+    {
+        #region diagnostics
+
+        /// <summary>
+        /// Verifies a path points to an existing property
+        /// </summary>
+        /// <param name="reflectionObject">The root object to reflect.</param>
+        /// <param name="path">The reflection path.</param>
+        /// <exception cref="ArgumentException">If the property pointed by the path does not exist.</exception>
+        public static void Verify(IReflectionObject reflectionObject, string path)
+        {
+            if (path.Contains("/extras/", StringComparison.Ordinal)) return;
+
+            var backingField = From(reflectionObject, path);
+            if (!backingField.IsEmpty) throw new ArgumentException($"{path} not found in the current model, add objects before animations, or disable verification.", nameof(path));
+        }
+
+        #endregion
+
+        #region lifecycle
+
+        /// <summary>
+        /// Finds a child object using a pointer path
+        /// </summary>
+        /// <param name="reflectionObject">the root object</param>
+        /// <param name="path">the path to the child object</param>
+        /// <returns>a <see cref="FieldInfo"/> or null</returns>
+        /// <example>
+        /// "/nodes/0/rotation"
+        /// </example>        
+        public static FieldInfo From(IReflectionObject reflectionObject, string path)
+        {
+            while (path.Length > 0 && reflectionObject != null)
+            {
+                if (path[0] != '/') throw new ArgumentException($"invalid path: {path}", nameof(path));
+                path = path.Substring(1);
+
+                #if NETSTANDARD2_0
+                var len = path.IndexOf('/');
+                #else
+                var len = path.IndexOf('/', StringComparison.Ordinal);
+                #endif                
+                if (len < 0) len = path.Length;
+                
+                var part = path.Substring(0, len);
+                if (!reflectionObject.TryGetField(part, out var field)) return default;
+
+                path = path.Substring(len);
+                if (path.Length == 0) return field;
+
+                reflectionObject = field.Value as IReflectionObject;
+            }
+
+            return default;
+        }
+
+        public static FieldInfo From<TInstance, TValue>(string name, TInstance instance, Func<TInstance, TValue> getter)
+        {
+            return new FieldInfo(name, typeof(TValue), instance, inst => getter.Invoke((TInstance)inst));
+        }
+
+        private FieldInfo(string name, Type valueType, Object instance, Func<Object, Object> getter)
+        {
+            this.Name = name;
+            this.ValueType = valueType;
+            this.Instance = instance;
+            _Getter = getter;            
+        }
+
+        #endregion
+
+        #region data
+
+        public string Name { get; }
+
+        private readonly Func<Object, Object> _Getter;
+
+        public Object Instance { get; }
+
+        #endregion
+
+        #region properties
+
+        public bool IsEmpty => _Getter == null;
+        public Type ValueType { get; }
+        public Object Value => _Getter.Invoke(Instance);
+
+        #endregion
+    }
+}

+ 20 - 0
src/SharpGLTF.Core/Reflection/Interfaces.cs

@@ -0,0 +1,20 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+using FIELDINFO = SharpGLTF.Reflection.FieldInfo;
+
+namespace SharpGLTF.Reflection
+{
+    public interface IReflectionObject
+    {
+        IEnumerable<FIELDINFO> GetFields();
+        bool TryGetField(string name, out FIELDINFO value);
+    }
+
+    public interface IReflectionArray : IReflectionObject
+    {
+        int Count { get; }
+        FIELDINFO GetField(int index);
+    }
+}

+ 22 - 10
src/SharpGLTF.Core/Schema2/gltf.Animations.cs

@@ -25,6 +25,11 @@ namespace SharpGLTF.Schema2
             _samplers = new ChildrenList<AnimationSampler, Animation>(this);
         }
 
+        protected override IEnumerable<ExtraProperties> GetLogicalChildren()
+        {
+            return base.GetLogicalChildren().Concat(_samplers).Concat(_channels);
+        }
+
         #endregion
 
         #region properties
@@ -37,16 +42,21 @@ namespace SharpGLTF.Schema2
 
         #endregion
 
-        #region API
-
-        protected override IEnumerable<ExtraProperties> GetLogicalChildren()
-        {
-            return base.GetLogicalChildren().Concat(_samplers).Concat(_channels);
-        }
+        #region API        
 
         public IEnumerable<AnimationChannel> FindChannels(string rootPath)
         {
-            return Channels.Where(item => item.TargetPointerPath.StartsWith(rootPath));
+            if (string.IsNullOrWhiteSpace(rootPath)) throw new ArgumentNullException(nameof(rootPath));
+            if (rootPath[0] != '/') throw new ArgumentException($"invalid path: {rootPath}", nameof(rootPath));
+
+            if (rootPath.EndsWith("/"))
+            {
+                return Channels.Where(item => item.TargetPointerPath.StartsWith(rootPath));
+            }
+            else
+            {
+                return Channels.Where(item => item.TargetPointerPath == rootPath);
+            }            
         }
 
         public IEnumerable<AnimationChannel> FindChannels(Node node)
@@ -117,7 +127,7 @@ namespace SharpGLTF.Schema2
             Guard.NotNullOrEmpty(propertyName, nameof(propertyName));
             Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
 
-            DangerousCreatePointerChannel<T>($"/materials/{material.LogicalIndex}/{propertyName}", keyframes, linear);
+            DangerousCreatePointerChannel($"/materials/{material.LogicalIndex}/{propertyName}", keyframes, linear);
         }
 
         /// <summary>
@@ -127,9 +137,11 @@ namespace SharpGLTF.Schema2
         /// <param name="pointerPath">The path to the porperty, ex: '/nodes/0/rotation'.</param>
         /// <param name="keyframes">The keyframes to set</param>
         /// <param name="linear">Whether the keyframes are linearly interporlated or not.</param>        
-        public void DangerousCreatePointerChannel<T>(string pointerPath, IReadOnlyDictionary<Single, T> keyframes, bool linear = true)
+        public void DangerousCreatePointerChannel<T>(string pointerPath, IReadOnlyDictionary<Single, T> keyframes, bool linear = true, bool verifyBackingFieldExists = true)
         {
-            Guard.NotNullOrEmpty(keyframes, nameof(keyframes));
+            Guard.NotNullOrEmpty(keyframes, nameof(keyframes));           
+
+            if (verifyBackingFieldExists) Reflection.FieldInfo.Verify(this.LogicalParent, pointerPath);
 
             var sampler = this._CreateSampler(linear ? AnimationInterpolationMode.LINEAR : AnimationInterpolationMode.STEP);
 

+ 78 - 4
src/SharpGLTF.Core/Schema2/gltf.ExtraProperties.cs

@@ -6,12 +6,12 @@ using System.Linq;
 using System.Text.Json;
 
 using SharpGLTF.IO;
+using SharpGLTF.Reflection;
 
 using JsonToken = System.Text.Json.JsonTokenType;
 
 using JSONEXTRAS = System.Text.Json.Nodes.JsonNode;
 
-
 namespace SharpGLTF.Schema2
 {
     public interface IExtraProperties
@@ -27,8 +27,10 @@ namespace SharpGLTF.Schema2
     /// <remarks>
     /// Defines the <see cref="Extras"/> property for every glTF object.
     /// </remarks>
-    public abstract class ExtraProperties : JsonSerializable,
-        IExtraProperties
+    public abstract class ExtraProperties
+        : JsonSerializable
+        , IExtraProperties
+        , Reflection.IReflectionObject
     {
         #region data
 
@@ -67,7 +69,42 @@ namespace SharpGLTF.Schema2
 
         #endregion
 
-        #region API
+        #region reflection
+
+        protected override string GetSchemaName() => "ExtraProperties";
+
+        IEnumerable<FieldInfo> Reflection.IReflectionObject.GetFields()
+        {
+            foreach (var name in ReflectFieldsNames())
+            {
+                if (TryReflectField(name, out var finfo)) yield return finfo;
+            }
+        }
+
+        bool Reflection.IReflectionObject.TryGetField(string name, out SharpGLTF.Reflection.FieldInfo value)
+        {
+            return TryReflectField(name ,out value);
+        }
+
+        protected virtual IEnumerable<string> ReflectFieldsNames()
+        {
+            yield return "extensions";
+            yield return "extras";
+
+        }
+        protected virtual bool TryReflectField(string name, out Reflection.FieldInfo value)
+        {
+            switch (name)
+            {
+                case "extensions": value = Reflection.FieldInfo.From("extensions", _extensions, exts => new _ExtensionsReflection(exts)); return true;
+                case "extras": value = Reflection.FieldInfo.From("extras", _extras, inst => inst); return true;
+                default: value = default; return false;
+            }
+        }
+
+        #endregion
+
+        #region API        
 
         protected static void SetProperty<TParent, TProperty, TValue>(TParent parent, ref TProperty property, TValue value)
             where TParent : ExtraProperties
@@ -280,5 +317,42 @@ namespace SharpGLTF.Schema2
         }
 
         #endregion
+
+        #region nested types
+
+        private readonly struct _ExtensionsReflection : Reflection.IReflectionObject
+        {
+            public _ExtensionsReflection(IReadOnlyList<JsonSerializable> extensions)
+            {
+                _Extensions = extensions;
+            }
+
+            private readonly IReadOnlyList<JsonSerializable> _Extensions;
+
+            public bool TryGetField(string name, out FieldInfo value)
+            {
+                var extension = _Extensions.FirstOrDefault(item => item._SchemaName == name);
+                if (extension == null)
+                {
+                    value = default;
+                    return false;
+                }
+
+                value = Reflection.FieldInfo.From(extension._SchemaName, extension, ext => ext);
+                return true;
+            }
+
+            public IEnumerable<FieldInfo> GetFields()
+            {
+                foreach(var extension in _Extensions)
+                {
+                    yield return Reflection.FieldInfo.From(extension._SchemaName, extension, ext => ext);
+                }
+            }
+
+            
+        }
+
+        #endregion
     }
 }

+ 5 - 0
src/SharpGLTF.Core/Schema2/gltf.Node.cs

@@ -73,6 +73,11 @@ namespace SharpGLTF.Schema2
         {
             _children = new List<int>();
             _weights = new List<double>();
+            
+            _scale = null;
+            _rotation = null;
+            _translation = null;
+            _matrix = null;
         }
 
         #endregion

+ 84 - 0
tests/SharpGLTF.Core.Tests/Reflection/ReflectionTests.cs

@@ -0,0 +1,84 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using NUnit.Framework;
+
+using SharpGLTF.Schema2;
+
+namespace SharpGLTF.Reflection
+{
+    internal class ReflectionTests
+    {
+        [TestCase("Avocado")]
+        [TestCase("AnimationPointerUVs")]
+        public void ReflectionDump(string modelName)
+        {
+            var mpath = TestFiles.GetSampleModelsPaths().FirstOrDefault(item => item.Contains(modelName));
+
+            var model = ModelRoot.Load(mpath, Validation.ValidationMode.TryFix);
+
+            _DumpReflectionItem(string.Empty, string.Empty, model);
+        }
+
+        private static void _DumpReflectionItem(string indent, string name, Object value)
+        {
+            if (value == null) return;
+
+            TestContext.Out.Write($"{indent}{name}:");
+
+            switch(value)
+            {
+                case IReflectionObject reflectionObject:
+                    TestContext.Out.WriteLine(string.Empty);
+                    indent += "    ";
+                    foreach (var field in reflectionObject.GetFields())
+                    {                        
+                        _DumpReflectionItem(indent, field.Name, field.Value);
+                    }
+                    break;
+
+                case IConvertible convertible:
+                    TestContext.Out.WriteLine($"{value}");
+                    break;
+
+                case IEnumerable enumerable:
+                    TestContext.Out.WriteLine(string.Empty);
+                    indent += "    ";
+                    foreach (var item in enumerable)
+                    {
+                        TestContext.Out.WriteLine($"{indent}{value}");
+                    }
+                    break;
+
+                default:
+                    TestContext.Out.WriteLine($"{value}");
+                    break;
+            }            
+        }
+
+        [TestCase("Avocado", "/materials/0/alphaCutoff", "0.5")]
+        [TestCase("Avocado", "/nodes/0/rotation", "{X:0 Y:1 Z:0 W:0}")]
+        [TestCase("AnimationPointerUVs", "/materials/61/extensions/KHR_materials_specular/specularTexture/extensions/KHR_texture_transform/offset", "<-0.2388889, 0.2388889>")]
+        [TestCase("AnimationPointerUVs", "/materials/25/extensions/KHR_materials_anisotropy/anisotropyTexture/extensions/KHR_texture_transform/offset", "<-0.2388889, 0.2388889>")]
+        [TestCase("AnimationPointerUVs", "/materials/1/extensions/KHR_materials_diffuse_transmission/diffuseTransmissionTexture/extensions/KHR_texture_transform/offset", "<-0.2388889, 0.2388889>")]
+        public void ReflectionPointerPathTest(string modelName, string pointerPath, string expectedValue)
+        {
+            var mpath = TestFiles.GetSampleModelsPaths()
+                .Where(item => !item.Contains("Quantized"))
+                .FirstOrDefault(item => item.Contains(modelName));
+
+            var model = ModelRoot.Load(mpath, Validation.ValidationMode.TryFix);
+
+            var field = Reflection.FieldInfo.From(model, pointerPath);
+
+            var result = FormattableString.Invariant($"{field.Value}");
+
+            Assert.That(result, Is.EqualTo(expectedValue));
+        }
+    }
+}

+ 5 - 2
tests/SharpGLTF.NUnit/TestFiles.cs

@@ -89,6 +89,8 @@ namespace SharpGLTF
         private static readonly System.IO.DirectoryInfo _BabylonJsMeshesDir = _UsingExternalFiles("BabylonJS-Assets");
         private static readonly System.IO.DirectoryInfo _GeneratedModelsDir = _UsingExternalFiles("GeneratedReferenceModels", "v_0_6_1");
 
+        private static KhronosSampleModel[] _KhronosSampleModels;
+
         #endregion
 
         #region properties        
@@ -148,9 +150,9 @@ namespace SharpGLTF
 
         public static IReadOnlyList<string> GetSampleModelsPaths()
         {
-            var entries = KhronosSampleModel.Load(_KhronosSampleAssetsDir._DefFile("Models", "model-index.json"));
+            _KhronosSampleModels ??= KhronosSampleModel.Load(_KhronosSampleAssetsDir._DefFile("Models", "model-index.json"));
 
-            var files = entries
+            var files = _KhronosSampleModels
                 .SelectMany(item => item.GetPaths())
                 .ToList();
 
@@ -210,6 +212,7 @@ namespace SharpGLTF
                 "\\meshes\\Tests\\AssetGenerator", // already covered separately.
                 "\\meshes\\KHR_materials_volume_testing.glb", // draco compression-
                 "\\meshes\\Yeti\\MayaExport\\", // validator reports out of bounds accesor
+                "\\meshes\\Demos\\optimized\\", // uses MeshOpt extension
                 "\\meshes\\Demos\\retargeting\\riggedMesh.glb", // validator reports errors
                 "\\meshes\\Buildings\\road gap.glb", // uses KHR_Draco compression  
                 "\\meshes\\Buildings\\Road corner.glb", // uses KHR_Draco compression