using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; namespace SharpGLTF.SchemaReflection { /// /// Utility class that reads a json schema and creates a /// Which is a collection of Schema Types resembling /// static class SchemaTypesReader { // issues related to gltf2 schema parsing: // https://github.com/RSuter/NJsonSchema // https://github.com/RSuter/NJsonSchema/issues/378 // https://github.com/RSuter/NJsonSchema/issues/377 #region API public static SchemaType.Context Generate(NJsonSchema.CodeGeneration.CSharp.CSharpTypeResolver types) { var context = new SchemaType.Context(); foreach(var t in types.Types.Keys) { context._UseType(t); } return context; } #endregion #region core private static SchemaType _UseType(this SchemaType.Context ctx, NJsonSchema.JsonSchema4 schema, bool isRequired = true) { if (ctx == null) throw new ArgumentNullException(nameof(ctx)); if (schema == null) throw new ArgumentNullException(nameof(schema)); if (schema is NJsonSchema.JsonProperty prop) { // System.Diagnostics.Debug.Assert(prop.Name != "scene"); isRequired &= prop.IsRequired; } if (_IsStringType(schema)) { return ctx.UseString(); } if (_IsBlittableType(schema)) { bool isNullable = !isRequired; if (schema.Type == NJsonSchema.JsonObjectType.Integer) return ctx.UseBlittable(typeof(Int32).GetTypeInfo(), isNullable); if (schema.Type == NJsonSchema.JsonObjectType.Number) return ctx.UseBlittable(typeof(Double).GetTypeInfo(), isNullable); if (schema.Type == NJsonSchema.JsonObjectType.Boolean) return ctx.UseBlittable(typeof(Boolean).GetTypeInfo(), isNullable); throw new NotImplementedException(); } if (schema.HasReference) return ctx._UseType(schema.ActualTypeSchema, isRequired); // we could have our own ref if (schema.IsArray) { return ctx.UseArray(ctx._UseType(schema.Item.ActualSchema)); } if (_IsEnumeration(schema)) { if (schema is NJsonSchema.JsonProperty property) { bool isNullable = !isRequired; var dict = new Dictionary(); foreach (var v in property.AnyOf) { var val = v.Enumeration.FirstOrDefault(); var key = v.Description; if (val is String) { key = (string)val; val = (Int64)0; } if (string.IsNullOrWhiteSpace(key)) continue; dict[key] = (Int64)val; } // JSon Schema AnyOf enumerations are basically anonymous, so we create // a "shared name" with a concatenation of all the values: var name = string.Join("-", dict.Keys.OrderBy(item => item)); var etype = ctx.UseEnum(name, isNullable); foreach (var kvp in dict) etype.SetValue(kvp.Key, (int)kvp.Value); if (dict.Values.Distinct().Count() > 1) etype.UseIntegers = true; return etype; } throw new NotImplementedException(); } if (_IsDictionary(schema)) { var key = ctx.UseString(); var val = ctx._UseType(_GetDictionaryValue(schema)); return ctx.UseDictionary(key, val); } if (_IsClass(schema)) { var classDecl = ctx.UseClass(schema.Title); // process base class if (schema.InheritedSchema != null) classDecl.BaseClass = ctx._UseType(schema.InheritedSchema) as ClassType; // filter declared properties var keys = _GetProperyNames(schema); if (schema.InheritedSchema != null) // filter our parent properties { var baseKeys = _GetInheritedPropertyNames(schema).ToArray(); keys = keys.Except(baseKeys).ToArray(); } // process declared properties var props = keys.Select(key => schema.Properties.Values.FirstOrDefault(item => item.Name == key)); var required = schema.RequiredProperties; // System.Diagnostics.Debug.Assert(schema.Title != "Buffer View"); foreach(var p in props) { var field = classDecl.UseField(p.Name); field.FieldType = ctx._UseType(p, required.Contains(p.Name)); field.MinimumValue = p.Minimum; field.MaximumValue = p.Maximum; field.DefaultValue = p.Default; field.MinItems = p.MinItems; field.MaxItems = p.MaxItems; } return classDecl; } if (schema.Type == NJsonSchema.JsonObjectType.Object) return ctx.UseAnyType(); if (schema.Type == NJsonSchema.JsonObjectType.None) return ctx.UseAnyType(); throw new NotImplementedException(); } private static bool _IsBlittableType(NJsonSchema.JsonSchema4 schema) { if (schema == null) return false; if (schema.Type == NJsonSchema.JsonObjectType.Boolean) return true; if (schema.Type == NJsonSchema.JsonObjectType.Number) return true; if (schema.Type == NJsonSchema.JsonObjectType.Integer) return true; return false; } private static bool _IsStringType(NJsonSchema.JsonSchema4 schema) { return schema.Type == NJsonSchema.JsonObjectType.String; } private static bool _IsEnumeration(NJsonSchema.JsonSchema4 schema) { if (schema.Type != NJsonSchema.JsonObjectType.None) return false; if (schema.IsArray || schema.IsDictionary) return false; if (schema.AnyOf.Count == 0) return false; // if (!schema.IsEnumeration) return false; // useless return true; } private static bool _IsDictionary(NJsonSchema.JsonSchema4 schema) { // if (schema.Type != NJsonSchema.JsonObjectType.Object) return false; if (schema.AdditionalPropertiesSchema != null) return true; if (schema.AllowAdditionalProperties == false && schema.PatternProperties.Any()) return true; return false; } private static NJsonSchema.JsonSchema4 _GetDictionaryValue(NJsonSchema.JsonSchema4 schema) { if (schema.AdditionalPropertiesSchema != null) { return schema.AdditionalPropertiesSchema; } if (schema.AllowAdditionalProperties == false && schema.PatternProperties.Any()) { var valueTypes = schema.PatternProperties.Values.ToArray(); if (valueTypes.Length == 1) return valueTypes.First(); } throw new NotImplementedException(); } private static bool _IsClass(NJsonSchema.JsonSchema4 schema) { if (schema.Type != NJsonSchema.JsonObjectType.Object) return false; return !string.IsNullOrWhiteSpace(schema.Title); } private static string[] _GetProperyNames(NJsonSchema.JsonSchema4 schema) { return schema .Properties .Values .Select(item => item.Name) .ToArray(); } private static string[] _GetInheritedPropertyNames(NJsonSchema.JsonSchema4 schema) { if (schema?.InheritedSchema == null) return Enumerable.Empty().ToArray(); return _GetInheritedPropertyNames(schema.InheritedSchema) .Concat(_GetProperyNames(schema.InheritedSchema)) .ToArray(); } #endregion } }