414 lines
15 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UniJSON;
namespace UniGLTF.JsonSchema
{
public class JsonSchemaParser
{
DirectoryInfo[] m_dir;
Dictionary<FileInfo, byte[]> m_cache = new Dictionary<FileInfo, byte[]>();
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<string, JsonSchemaSource>();
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<int> values = new List<int>();
List<string> stringValues = new List<string>();
List<string> descriptions = new List<string>();
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<string, int>[values.Count];
for (int i = 0; i < values.Count; ++i)
{
source.enumValues[i] = new KeyValuePair<string, int>
(
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;
}
}
}