using System; using System.Collections.Generic; using System.IO; using System.Linq; using UniJSON; namespace UniGLTF.JsonSchema { public class JsonSchemaParser { DirectoryInfo[] m_dir; Dictionary m_cache = new Dictionary(); public JsonSchemaParser(params DirectoryInfo[] dir) { m_dir = dir; } public static JsonSchemaSource Parse(string root, string jsonPath = "") { // setup var path = new FileInfo(root); var parser = new JsonSchemaParser(path.Directory); // traverse return parser.Load(path.Name, jsonPath); } public JsonSchemaSource Load(string fileName, string jsonPath) { JsonSchemaSource loaded = null; foreach (var dir in m_dir) { var path = Path.Combine(dir.FullName, fileName); if (File.Exists(path)) { loaded = Load(new FileInfo(path), jsonPath); break; } } if (loaded is null) { throw new FileNotFoundException(fileName); } return loaded; } public JsonSchemaSource Load(FileInfo path, string jsonPath) { if (!m_cache.TryGetValue(path, out byte[] bytes)) { // Console.WriteLine($"load {path}"); bytes = File.ReadAllBytes(path.FullName); m_cache.Add(path, bytes); } { var jsonSchema = Parse(bytes.ParseAsJson(), jsonPath); jsonSchema.FilePath = path; return jsonSchema; } } void MergeTo(JsonSchemaSource src, JsonSchemaSource dst) { if (string.IsNullOrEmpty(dst.title)) { dst.title = src.title; } if (string.IsNullOrEmpty(dst.description)) { dst.description = src.description; } if (src.type != JsonSchemaType.Unknown) { dst.type = src.type; } foreach (var kv in src.EnumerateProperties()) { dst.AddProperty(kv.Key, kv.Value); } if (src.enumStringValues != null) { dst.enumStringValues = src.enumStringValues.ToArray(); } } JsonSchemaSource Parse(JsonNode json, string jsonPath) { var source = new JsonSchemaSource { JsonPath = jsonPath, }; foreach (var kv in json.ObjectItems()) { switch (kv.Key.GetString()) { case "$ref": { var reference = Load(kv.Value.GetString(), jsonPath); MergeTo(reference, source); break; } case "allOf": // glTF では継承として使われる { var reference = AllOf(kv.Value, jsonPath); MergeTo(reference, source); break; } case "$schema": break; case "type": source.type = (JsonSchemaType)Enum.Parse(typeof(JsonSchemaType), kv.Value.GetString(), true); break; case "title": source.title = kv.Value.GetString(); break; case "description": source.description = kv.Value.GetString(); break; case "gltf_detailedDescription": source.gltfDetail = kv.Value.GetString(); break; case "default": break; case "gltf_webgl": break; case "anyOf": // glTF ではenumとして使われる ParseAnyOfAsEnum(ref source, kv.Value); break; case "oneOf": // TODO: union 的な break; case "not": // TODO: プロパティの両立を禁止する、排他的な break; case "pattern": source.pattern = kv.Value.GetString(); break; case "format": // TODO break; case "gltf_uriType": // TODO break; case "minimum": if (source.type != JsonSchemaType.Number && source.type != JsonSchemaType.Integer) throw new Exception(); source.minimum = kv.Value.GetDouble(); break; case "exclusiveMinimum": if (source.type != JsonSchemaType.Number && source.type != JsonSchemaType.Integer) throw new Exception(); source.exclusiveMinimum = kv.Value.GetBoolean(); // ? break; case "maximum": if (source.type != JsonSchemaType.Number && source.type != JsonSchemaType.Integer) throw new Exception(); source.maximum = kv.Value.GetDouble(); break; case "multipleOf": if (source.type != JsonSchemaType.Number && source.type != JsonSchemaType.Integer) throw new Exception(); source.multipleOf = kv.Value.GetDouble(); break; case "properties": if (source.type != JsonSchemaType.Object) throw new Exception(); // source.properties = new Dictionary(); foreach (var prop in kv.Value.ObjectItems()) { var key = prop.Key.GetString(); var propJsonPath = $"{jsonPath}.{key}"; var propSchema = Parse(prop.Value, propJsonPath); if (string.IsNullOrEmpty(propSchema.title)) { propSchema.title = key.ToUpperCamel(); } if (propSchema is null) { if (source.baseSchema is null) { // add empty object. extras source.AddProperty(prop.Key.GetString(), new JsonSchemaSource { JsonPath = propJsonPath, type = JsonSchemaType.Object, }); } // else if (source.baseSchema.GetPropertyFromPath(propJsonPath)) // { // // ok // } else { throw new Exception("unknown"); } } else { if (source.GetProperty(prop.Key.GetString()) == null) { source.AddProperty(prop.Key.GetString(), propSchema); } } } break; case "required": source.required = kv.Value.ArrayItems().Select(x => x.GetString()).ToArray(); break; case "dependencies": // Property間の依存関係? // TODO: break; case "additionalProperties": if (source.type != JsonSchemaType.Object) throw new Exception(); if (kv.Value.Value.ValueType == ValueNodeType.Object) { source.additionalProperties = Parse(kv.Value, $"{jsonPath}{{}}"); } else if (kv.Value.Value.ValueType == ValueNodeType.Boolean && kv.Value.GetBoolean() == false) { // skip. do nothing } else { throw new NotImplementedException(); } break; case "minProperties": if (source.type != JsonSchemaType.Object) throw new Exception(); source.minProperties = kv.Value.GetInt32(); break; case "items": if (source.type != JsonSchemaType.Array) throw new Exception(); source.items = Parse(kv.Value, $"{jsonPath}[]"); break; case "uniqueItems": if (source.type != JsonSchemaType.Array) throw new Exception(); source.uniqueItems = kv.Value.GetBoolean(); break; case "maxItems": if (source.type != JsonSchemaType.Array) throw new Exception(); source.maxItems = kv.Value.GetInt32(); break; case "minItems": if (source.type != JsonSchemaType.Array) throw new Exception(); source.minItems = kv.Value.GetInt32(); break; case "enum": if (source.type == JsonSchemaType.String) { ParseStringEnum(ref source, kv.Value); } else if (source.type == JsonSchemaType.Integer || source.type == JsonSchemaType.Number) { throw new NotImplementedException(); } else { throw new NotImplementedException(); } break; default: Console.WriteLine($"unknown property: {kv.Key.GetString()} => {kv.Value}"); break; } } return source; } void ParseStringEnum(ref JsonSchemaSource source, JsonNode json) { source.enumStringValues = json.ArrayItems().Select(x => x.GetString()).ToArray(); source.type = JsonSchemaType.EnumString; } void ParseAnyOfAsEnum(ref JsonSchemaSource source, JsonNode json) { List values = new List(); List stringValues = new List(); List descriptions = new List(); foreach (var v in json.ArrayItems()) { foreach (var kv in v.ObjectItems()) { switch (kv.Key.GetString()) { case "enum": { int i = 0; foreach (var a in kv.Value.ArrayItems()) { switch (a.Value.ValueType) { case ValueNodeType.Number: case ValueNodeType.Integer: values.Add(a.GetInt32()); break; case ValueNodeType.String: stringValues.Add(a.GetString()); break; default: throw new NotImplementedException(); } ++i; } } break; case "description": { descriptions.Add(kv.Value.GetString()); } break; case "type": break; default: throw new NotImplementedException(); } } } if (stringValues.Count > 0) { if (values.Count == 0) { source.enumStringValues = stringValues.ToArray(); source.type = JsonSchemaType.EnumString; return; } } if (descriptions.Count == values.Count) { source.enumValues = new KeyValuePair[values.Count]; for (int i = 0; i < values.Count; ++i) { source.enumValues[i] = new KeyValuePair ( descriptions[i], values[i] ); } source.type = JsonSchemaType.Enum; return; } throw new NotImplementedException(); } JsonSchemaSource AllOf(JsonNode json, string jsonPath) { string refValue = null; int count = 0; foreach (var a in json.ArrayItems()) { foreach (var kv in a.ObjectItems()) { if (kv.Key.GetString() != "$ref") { throw new NotImplementedException(); } refValue = kv.Value.GetString(); ++count; } } if (count != 1) { throw new NotImplementedException(); } var reference = Load(refValue, jsonPath); return reference; } } }